diff --git a/packages/common/src/store/pages/settings/actions.ts b/packages/common/src/store/pages/settings/actions.ts index 4fdb30516a8..b2a517742b4 100644 --- a/packages/common/src/store/pages/settings/actions.ts +++ b/packages/common/src/store/pages/settings/actions.ts @@ -14,6 +14,9 @@ export const TOGGLE_PUSH_NOTIFICATION_SETTING = export const TOGGLE_PUSH_NOTIFICATION_SETTING_FAILED = 'SETTINGS_PAGE/TOGGLE_PUSH_NOTIFICATION_SETTING_FAILED' +export const REQUEST_PUSH_NOTIFICATION_PERMISSIONS = + 'SETTINGS_PAGE/REQUEST_PUSH_NOTIFICATION_PERMISSIONS' + export const UPDATE_EMAIL_FREQUENCY = 'SETTINGS_PAGE/UPDATE_EMAIL_FREQUENCY' export const GET_NOTIFICATION_SETTINGS = @@ -36,6 +39,8 @@ export const SET_BROWSER_NOTIFICATION_ENABLED = 'SETTINGS/SET_BROWSER_NOTIFICATION_ENABLED' export const SET_BROWSER_NOTIFICATION_SETTINGS_ON = 'SETTINGS/SET_BROWSER_NOTIFICATION_SETTINGS_ON' +export const SET_BROWSER_NOTIFICATION_SETTINGS_OFF = + 'SETTINGS/SET_BROWSER_NOTIFICATION_SETTINGS_OFF' export const BROWSER_PUSH_NOTIFICATION_FAILED = 'SETTINGS/BROWSER_PUSH_NOTIFICATION_FAILED' @@ -91,6 +96,12 @@ export function togglePushNotificationSettingFailed( } } +export function requestPushNotificationPermissions() { + return { + type: REQUEST_PUSH_NOTIFICATION_PERMISSIONS + } +} + export function updateEmailFrequency( frequency: EmailFrequency, updateServer = true @@ -113,6 +124,10 @@ export function setBrowserNotificationSettingsOn() { return { type: SET_BROWSER_NOTIFICATION_SETTINGS_ON } } +export function setBrowserNotificationSettingsOff() { + return { type: SET_BROWSER_NOTIFICATION_SETTINGS_OFF } +} + export function browserPushNotificationFailed(error: string) { return { type: BROWSER_PUSH_NOTIFICATION_FAILED, error } } @@ -131,6 +146,10 @@ export type TogglePushNotificationSettingFailed = ReturnType< typeof togglePushNotificationSettingFailed > +export type RequestPushNotificationPermissions = ReturnType< + typeof requestPushNotificationPermissions +> + export type UpdateEmailFrequency = ReturnType export type GetNotificationSettings = ReturnType @@ -157,6 +176,9 @@ export type SetBrowserNotificationEnabled = ReturnType< export type SetBrowserNotificationSettingsOn = ReturnType< typeof setBrowserNotificationSettingsOn > +export type SetBrowserNotificationSettingsOff = ReturnType< + typeof setBrowserNotificationSettingsOff +> export type BrowserPushNotificationFailed = ReturnType< typeof browserPushNotificationFailed > @@ -164,6 +186,7 @@ export type BrowserPushNotificationFailed = ReturnType< export type SetAiAttribution = ReturnType export type SettingActions = + | RequestPushNotificationPermissions | ToggleNotificationSetting | TogglePushNotificationSetting | TogglePushNotificationSettingFailed @@ -177,4 +200,5 @@ export type SettingActions = | SetBrowserNotificationPermission | SetBrowserNotificationEnabled | SetBrowserNotificationSettingsOn + | SetBrowserNotificationSettingsOff | BrowserPushNotificationFailed diff --git a/packages/mobile/src/components/enable-push-notifications-drawer/EnablePushNotificationsDrawer.tsx b/packages/mobile/src/components/enable-push-notifications-drawer/EnablePushNotificationsDrawer.tsx index 3f90c7648b8..bd721081070 100644 --- a/packages/mobile/src/components/enable-push-notifications-drawer/EnablePushNotificationsDrawer.tsx +++ b/packages/mobile/src/components/enable-push-notifications-drawer/EnablePushNotificationsDrawer.tsx @@ -8,6 +8,7 @@ import IconCoSign from 'app/assets/images/iconCoSign.svg' import IconFollow from 'app/assets/images/iconFollow.svg' import IconNotification from 'app/assets/images/iconGradientNotification.svg' import IconHeart from 'app/assets/images/iconHeart.svg' +import IconMessage from 'app/assets/images/iconMessage.svg' import IconNewReleases from 'app/assets/images/iconNewReleases.svg' import IconRemix from 'app/assets/images/iconRemix.svg' import IconRepost from 'app/assets/images/iconRepost.svg' @@ -28,6 +29,7 @@ const messages = { coSigns: 'Co-Signs', remixes: 'Remixes', newReleases: 'New Releases', + messages: 'Messages', enable: 'Enable Notifications' } @@ -55,6 +57,10 @@ const actions = [ { label: messages.newReleases, icon: IconNewReleases + }, + { + label: messages.messages, + icon: IconMessage } ] diff --git a/packages/mobile/src/notifications.ts b/packages/mobile/src/notifications.ts index b8a2736a691..9c4a4a1c6bf 100644 --- a/packages/mobile/src/notifications.ts +++ b/packages/mobile/src/notifications.ts @@ -1,5 +1,4 @@ import AsyncStorage from '@react-native-async-storage/async-storage' -import type { PushNotificationPermissions } from 'react-native' import { Platform } from 'react-native' import { Notifications } from 'react-native-notifications' import type { Registered, Notification } from 'react-native-notifications' @@ -89,17 +88,15 @@ class PushNotifications { } } + async hasPermission(): Promise { + return await Notifications.isRegisteredForRemoteNotifications() + } + requestPermission() { isRegistering = true Notifications.registerRemoteNotifications() } - async checkPermission( - callback: (permissions: PushNotificationPermissions) => void - ) { - Notifications.ios.checkPermissions().then(callback) - } - cancelNotif() { Notifications.cancelLocalNotification(this.lastId) } diff --git a/packages/mobile/src/store/settings/sagas.ts b/packages/mobile/src/store/settings/sagas.ts index c54751a0fee..adfcfb02c89 100644 --- a/packages/mobile/src/store/settings/sagas.ts +++ b/packages/mobile/src/store/settings/sagas.ts @@ -11,6 +11,7 @@ import { } from '@audius/common' import { waitForRead } from 'audius-client/src/utils/sagaHelpers' import commonSettingsSagas from 'common/store/pages/settings/sagas' +import { mapValues } from 'lodash' import { select, call, put, takeEvery } from 'typed-redux-saga' import PushNotifications from 'app/notifications' @@ -18,13 +19,53 @@ import PushNotifications from 'app/notifications' const { getPushNotificationSettings } = settingsPageSelectors const { getAccountUser } = accountSelectors -export function* disablePushNotifications() { +export function* deregisterPushNotifications() { const audiusBackendInstance = yield* getContext('audiusBackendInstance') const { token } = yield* call([PushNotifications, 'getToken']) PushNotifications.deregister() yield* call(audiusBackendInstance.deregisterDeviceToken, token) } +function* enablePushNotifications() { + yield* call([PushNotifications, 'requestPermission']) + const { token, os } = yield* call([PushNotifications, 'getToken']) + + const audiusBackendInstance = yield* getContext('audiusBackendInstance') + + // Enabling push notifications should enable all of the notification types + const newSettings = { ...initialState.pushNotifications } + yield* put(actions.setPushNotificationSettings(newSettings)) + + // We need a user for this to work (and in the case of sign up, we might not + // have one right away when this function is called) + yield* call(waitForValue, getAccountUser) + yield* call(audiusBackendInstance.updatePushNotificationSettings, newSettings) + + yield* call(audiusBackendInstance.registerDeviceToken, token, os) +} + +function* disablePushNotifications() { + const audiusBackendInstance = yield* getContext('audiusBackendInstance') + const newSettings = mapValues( + initialState.pushNotifications, + function (_val: boolean) { + return false + } + ) + yield* put(actions.setPushNotificationSettings(newSettings)) + yield* call(waitForValue, getAccountUser) + yield* call(audiusBackendInstance.updatePushNotificationSettings, newSettings) + yield* call(deregisterPushNotifications) +} + +function pushNotificationsEnabled(settings: TPushNotifications): boolean { + for (const key in initialState.pushNotifications) { + if (key === PushNotificationSetting.MobilePush) continue + if (settings[key]) return true + } + return false +} + function* watchGetPushNotificationSettings() { const audiusBackendInstance = yield* getContext('audiusBackendInstance') yield* takeEvery(actions.GET_PUSH_NOTIFICATION_SETTINGS, function* () { @@ -33,9 +74,21 @@ function* watchGetPushNotificationSettings() { const settings = (yield* call( audiusBackendInstance.getPushNotificationSettings )) as TPushNotifications - const pushNotificationSettings = { - ...settings, - [PushNotificationSetting.MobilePush]: !!settings + let pushNotificationSettings = mapValues( + initialState.pushNotifications, + function (_val: boolean) { + return false + } + ) + + if (settings) { + pushNotificationSettings = { + ...settings, + [PushNotificationSetting.MobilePush]: yield* call( + pushNotificationsEnabled, + settings + ) + } } yield* put(actions.setPushNotificationSettings(pushNotificationSettings)) } catch (error) { @@ -56,20 +109,7 @@ function* watchUpdatePushNotificationSettings() { try { if (action.notificationType === PushNotificationSetting.MobilePush) { if (isOn) { - yield* call([PushNotifications, 'requestPermission']) - const { token, os } = yield* call([PushNotifications, 'getToken']) - // Enabling push notifications should enable all of the notification types - const newSettings = { ...initialState.pushNotifications } - yield* put(actions.setPushNotificationSettings(newSettings)) - - // We need a user for this to work (and in the case of sign up, we might not - // have one right away when this function is called) - yield* call(waitForValue, getAccountUser) - yield* call( - audiusBackendInstance.updatePushNotificationSettings, - newSettings - ) - yield* call(audiusBackendInstance.registerDeviceToken, token, os) + yield* call(enablePushNotifications) } else { yield* call(disablePushNotifications) } @@ -96,10 +136,29 @@ function* watchUpdatePushNotificationSettings() { ) } +function* watchRequestPushNotificationPermissions() { + yield* takeEvery( + actions.REQUEST_PUSH_NOTIFICATION_PERMISSIONS, + function* (_action: actions.RequestPushNotificationPermissions) { + const hasPermissions = yield* call([PushNotifications, 'hasPermission']) + if (!hasPermissions) { + // Request permission to send push notifications and enable all if accepted + yield* put( + actions.togglePushNotificationSetting( + PushNotificationSetting.MobilePush, + true + ) + ) + } + } + ) +} + export default function sagas() { return [ ...commonSettingsSagas(), watchGetPushNotificationSettings, - watchUpdatePushNotificationSettings + watchUpdatePushNotificationSettings, + watchRequestPushNotificationPermissions ] } diff --git a/packages/mobile/src/store/sign-out/sagas.ts b/packages/mobile/src/store/sign-out/sagas.ts index 41333038d8d..c22bc860c75 100644 --- a/packages/mobile/src/store/sign-out/sagas.ts +++ b/packages/mobile/src/store/sign-out/sagas.ts @@ -24,7 +24,7 @@ import { localStorage } from 'app/services/local-storage' import { resetOAuthState } from '../oauth/actions' import { clearOfflineDownloads } from '../offline-downloads/slice' import { clearHistory } from '../search/searchSlice' -import { disablePushNotifications } from '../settings/sagas' +import { deregisterPushNotifications } from '../settings/sagas' const { resetAccount } = accountActions const { resetState: resetWalletState } = tokenDashboardPageActions @@ -47,7 +47,7 @@ function* signOut() { yield* put(clearOfflineDownloads()) yield* put(resetWalletState()) - yield* call(disablePushNotifications) + yield* call(deregisterPushNotifications) yield* call([localStorage, 'clearAudiusAccount']) yield* call([localStorage, 'clearAudiusAccountUser']) yield* call([audiusBackendInstance, 'signOut']) diff --git a/packages/web/src/common/store/pages/signon/sagas.js b/packages/web/src/common/store/pages/signon/sagas.js index 8d9f6681a8e..fd65ec46c0e 100644 --- a/packages/web/src/common/store/pages/signon/sagas.js +++ b/packages/web/src/common/store/pages/signon/sagas.js @@ -52,7 +52,9 @@ import { watchSignOnError } from './errorSagas' import { getRouteOnCompletion, getSignOn } from './selectors' import { FollowArtistsCategory, Pages } from './types' import { checkHandle } from './verifiedChecker' -const { togglePushNotificationSetting } = settingsPageActions + +const { requestPushNotificationPermissions, togglePushNotificationSetting } = + settingsPageActions const { getFeePayer } = solanaSelectors const { saveCollection } = collectionsSocialActions const { getUsers } = cacheUsersSelectors @@ -426,6 +428,7 @@ function* signUp() { const isNativeMobile = yield getContext('isNativeMobile') if (isNativeMobile) { + // Request permission to send push notifications and enable all if accepted yield put( togglePushNotificationSetting( PushNotificationSetting.MobilePush, @@ -531,12 +534,9 @@ function* signIn(action) { yield put(signOnActions.resetSignOn()) const isNativeMobile = yield getContext('isNativeMobile') if (isNativeMobile) { - yield put( - togglePushNotificationSetting( - PushNotificationSetting.MobilePush, - true - ) - ) + // If permissions not already enabled, request permission to send push notifications + // and enable all if accepted + yield put(requestPushNotificationPermissions()) } else { setHasRequestedBrowserPermission() yield put(accountActions.showPushNotificationConfirmation()) diff --git a/packages/web/src/pages/settings-page/SettingsPageProvider.tsx b/packages/web/src/pages/settings-page/SettingsPageProvider.tsx index 665457d50de..af7df92c8a7 100644 --- a/packages/web/src/pages/settings-page/SettingsPageProvider.tsx +++ b/packages/web/src/pages/settings-page/SettingsPageProvider.tsx @@ -113,6 +113,7 @@ class SettingsPage extends PureComponent< if (!isOn) { // if turning off, set all settings values to false this.props.setBrowserNotificationEnabled(false) + this.props.setBrowserNotificationSettingsOff() } else if ( this.props.notificationSettings.permission === Permission.GRANTED ) { @@ -264,6 +265,8 @@ function mapDispatchToProps(dispatch: Dispatch) { dispatch(accountActions.subscribeBrowserPushNotifications()), setBrowserNotificationSettingsOn: () => dispatch(settingPageActions.setBrowserNotificationSettingsOn()), + setBrowserNotificationSettingsOff: () => + dispatch(settingPageActions.setBrowserNotificationSettingsOff()), setNotificationSettings: (settings: object) => dispatch(settingPageActions.setNotificationSettings(settings)), setBrowserNotificationEnabled: (enabled: boolean) => diff --git a/packages/web/src/pages/settings-page/store/sagas.ts b/packages/web/src/pages/settings-page/store/sagas.ts index fd3f1164f53..4b861388de9 100644 --- a/packages/web/src/pages/settings-page/store/sagas.ts +++ b/packages/web/src/pages/settings-page/store/sagas.ts @@ -164,6 +164,34 @@ function* watchSetBrowserNotificationSettingsOn() { ) } +function* watchSetBrowserNotificationSettingsOff() { + const audiusBackendInstance = yield* getContext('audiusBackendInstance') + yield* takeEvery( + actions.SET_BROWSER_NOTIFICATION_SETTINGS_OFF, + function* (action: actions.SetBrowserNotificationSettingsOff) { + try { + const updatedSettings = { + [BrowserNotificationSetting.MilestonesAndAchievements]: false, + [BrowserNotificationSetting.Followers]: false, + [BrowserNotificationSetting.Reposts]: false, + [BrowserNotificationSetting.Favorites]: false, + [BrowserNotificationSetting.Remixes]: false, + [BrowserNotificationSetting.Messages]: false + } + yield* put(actions.setNotificationSettings(updatedSettings)) + yield* call( + audiusBackendInstance.updateNotificationSettings, + updatedSettings + ) + } catch (error) { + yield* put( + actions.browserPushNotificationFailed(getErrorMessage(error)) + ) + } + } + ) +} + function* watchUpdateNotificationSettings() { const audiusBackendInstance = yield* getContext('audiusBackendInstance') yield* takeEvery( @@ -201,6 +229,7 @@ export default function sagas() { ...commonSettingsSagas(), watchGetSettings, watchSetBrowserNotificationSettingsOn, + watchSetBrowserNotificationSettingsOff, watchToogleBrowserPushNotification, watchUpdateNotificationSettings ]