From 0ce90f42bc8e59c92181fed272035ee2e55a9bf6 Mon Sep 17 00:00:00 2001 From: Michal S Date: Mon, 6 Jan 2025 16:12:23 +0100 Subject: [PATCH] feat(wallet-mobile): Add in-app notifications (#3775) --- apps/wallet-mobile/src/AppNavigator.tsx | 3 + .../src/components/Icon/Bell.tsx | 15 ++ .../src/components/Icon/Icon.stories.tsx | 2 + .../src/components/Icon/index.ts | 2 + .../useCases/NotificationUIHandler.tsx | 70 +++++++ .../useCases/NotificationsDevScreen.tsx | 4 + .../useCases/common/NotificationPopup.tsx | 134 ++++++++++++ .../useCases/common/NotificationStack.tsx | 39 ++++ .../useCases/common/SwipeOutWrapper.tsx | 54 +++++ .../Notifications/useCases/common/hooks.ts | 2 +- .../useCases/common/notification-manager.ts | 2 - ...rimary-token-price-changed-notification.ts | 36 ---- .../common/rewards-updated-notification.ts | 55 +---- .../transaction-received-notification.ts | 49 +---- .../useCases/common/useStrings.tsx | 26 +++ .../NotificationsDisplaySettings.tsx | 48 +++++ .../WalletSettingsScreen.tsx | 41 ++++ .../src/kernel/i18n/locales/en-US.json | 7 +- .../messages/src/AppNavigator.json | 32 +-- .../useCases/common/useStrings.json | 47 +++++ .../WalletSettingsScreen.json | 198 ++++++++++-------- .../src/notification-manager.test.ts | 3 +- .../notifications/src/notification-manager.ts | 19 +- .../src/translators/reactjs/mocks.ts | 1 - .../useReceivedNotificationEvents.test.tsx | 1 + packages/types/src/notifications/manager.ts | 7 +- 26 files changed, 644 insertions(+), 253 deletions(-) create mode 100644 apps/wallet-mobile/src/components/Icon/Bell.tsx create mode 100644 apps/wallet-mobile/src/features/Notifications/useCases/NotificationUIHandler.tsx create mode 100644 apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationPopup.tsx create mode 100644 apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationStack.tsx create mode 100644 apps/wallet-mobile/src/features/Notifications/useCases/common/SwipeOutWrapper.tsx create mode 100644 apps/wallet-mobile/src/features/Notifications/useCases/common/useStrings.tsx create mode 100644 apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/Notifications/NotificationsDisplaySettings.tsx create mode 100644 apps/wallet-mobile/translations/messages/src/features/Notifications/useCases/common/useStrings.json diff --git a/apps/wallet-mobile/src/AppNavigator.tsx b/apps/wallet-mobile/src/AppNavigator.tsx index b8f09c03f2..3c0e6bbd9a 100644 --- a/apps/wallet-mobile/src/AppNavigator.tsx +++ b/apps/wallet-mobile/src/AppNavigator.tsx @@ -34,6 +34,7 @@ import { import {useDeepLinkWatcher} from './features/Links/common/useDeepLinkWatcher' import {useInitNotifications} from './features/Notifications/useCases/common/hooks' import {NotificationsDevScreen} from './features/Notifications/useCases/NotificationsDevScreen' +import {NotificationUIHandler} from './features/Notifications/useCases/NotificationUIHandler' import {SearchProvider} from './features/Search/SearchContext' import {SetupWalletNavigator} from './features/SetupWallet/SetupWalletNavigator' import {useHasWallets} from './features/WalletManager/common/hooks/useHasWallets' @@ -118,6 +119,8 @@ export const AppNavigator = () => { onReady={onReady} ref={navRef} > + + { + return ( + + + + ) +} diff --git a/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx b/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx index a81b0cb2c7..4df22aff26 100644 --- a/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx +++ b/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx @@ -304,6 +304,8 @@ storiesOf('Icon', module).add('Gallery', () => { } title="Bluetooth" /> } title="Usb" /> + + } title="Bell" /> ) diff --git a/apps/wallet-mobile/src/components/Icon/index.ts b/apps/wallet-mobile/src/components/Icon/index.ts index 37bb92c860..3469dca48b 100644 --- a/apps/wallet-mobile/src/components/Icon/index.ts +++ b/apps/wallet-mobile/src/components/Icon/index.ts @@ -7,6 +7,7 @@ import {ArrowRight} from './ArrowRight' import {Assets} from './Assets' import {Backspace} from './Backspace' import {Backward} from './Backward' +import {Bell} from './Bell' import {Bio} from './Bio' import {Bluetooth} from './Bluetooth' import {Bug} from './Bug' @@ -286,4 +287,5 @@ export const Icon = { InfoCircle, AngleUp, AngleDown, + Bell, } diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/NotificationUIHandler.tsx b/apps/wallet-mobile/src/features/Notifications/useCases/NotificationUIHandler.tsx new file mode 100644 index 0000000000..6fb4491e31 --- /dev/null +++ b/apps/wallet-mobile/src/features/Notifications/useCases/NotificationUIHandler.tsx @@ -0,0 +1,70 @@ +import {useNotificationManager} from '@yoroi/notifications' +import {Notifications} from '@yoroi/types' +import * as React from 'react' +import {useMemo} from 'react' + +import {useNotificationDisplaySettings} from '../../Settings/useCases/changeWalletSettings/Notifications/NotificationsDisplaySettings' +import {useWalletManager} from '../../WalletManager/context/WalletManagerProvider' +import {NotificationPopup} from './common/NotificationPopup' +import {NotificationStack} from './common/NotificationStack' + +const displayLimit = 3 +const displayTime = 20 * 1000 + +export const NotificationUIHandler = () => { + const enabled = useNotificationDisplaySettings() + const {events, removeEvent} = useCollectNewNotifications({enabled}) + const reversed = useMemo(() => [...events].reverse(), [events]) + const displayed = reversed.slice(0, displayLimit) + + if (displayed.length === 0) { + return null + } + + return ( + + {displayed.map((event) => ( + removeEvent(event.id)} + onPress={() => removeEvent(event.id)} + /> + ))} + + ) +} + +const useCollectNewNotifications = ({enabled}: {enabled: boolean}) => { + const manager = useNotificationManager() + const walletManager = useWalletManager() + const selectedWalletId = walletManager.selected.wallet?.id + const [events, setEvents] = React.useState([]) + + React.useEffect(() => { + if (!enabled) return + const pushEvent = (event: Notifications.Event) => { + setEvents((e) => [...e, event]) + setTimeout(() => setEvents((e) => e.filter((ev) => ev.id !== event.id)), displayTime) + } + + const subscription = manager.newEvents$.subscribe((event) => { + if (event.trigger === Notifications.Trigger.RewardsUpdated && event.metadata.walletId === selectedWalletId) { + pushEvent(event) + } + + if (event.trigger === Notifications.Trigger.TransactionReceived && event.metadata.walletId === selectedWalletId) { + pushEvent(event) + } + }) + return () => { + subscription.unsubscribe() + } + }, [manager, setEvents, selectedWalletId, enabled]) + + const removeEvent = (id: number) => { + setEvents((e) => e.filter((ev) => ev.id !== id)) + } + + return {events, removeEvent} +} diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx b/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx index 440ad15ecd..43efc4f63a 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx +++ b/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx @@ -15,6 +15,7 @@ import {SafeAreaView} from 'react-native-safe-area-context' import {Button} from '../../../components/Button/Button' import {ScrollView} from '../../../components/ScrollView/ScrollView' import {Text} from '../../../components/Text' +import {useWalletManager} from '../../WalletManager/context/WalletManagerProvider' import {notificationManager} from './common/notification-manager' import {createTransactionReceivedNotification} from './common/transaction-received-notification' @@ -28,6 +29,8 @@ export const NotificationsDevScreen = () => { const Screen = () => { const manager = useNotificationManager() + const walletManager = useWalletManager() + const selectedWalletId = walletManager.selected.wallet?.id ?? 'walletId' const handleOnTriggerTransactionReceived = () => { manager.events.push( @@ -36,6 +39,7 @@ const Screen = () => { nextTxsCounter: 1, txId: '123', isSentByUser: false, + walletId: selectedWalletId, }), ) } diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationPopup.tsx b/apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationPopup.tsx new file mode 100644 index 0000000000..f5d67362ac --- /dev/null +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationPopup.tsx @@ -0,0 +1,134 @@ +import {useTheme} from '@yoroi/theme' +import {Notifications} from '@yoroi/types' +import * as React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../../components/Icon' +import {Text} from '../../../../components/Text' +import {useWalletNavigation} from '../../../../kernel/navigation' +import {SwipeOutWrapper} from './SwipeOutWrapper' +import {useStrings} from './useStrings' + +type Props = { + event: Notifications.Event + onPress: () => void + onCancel: () => void +} + +export const NotificationPopup = ({event, onPress, onCancel}: Props) => { + const navigation = useWalletNavigation() + const strings = useStrings() + + if (event.trigger === Notifications.Trigger.TransactionReceived) { + return ( + + { + onPress() + navigation.navigateToTxHistory() + }} + icon={} + title={strings.assetsReceived} + description={strings.tapToView} + /> + + ) + } + + if (event.trigger === Notifications.Trigger.RewardsUpdated) { + return ( + + { + onPress() + navigation.navigateToStakingDashboard() + }} + icon={} + title={strings.stakingRewardsReceived} + description={strings.tapToView} + /> + + ) + } + + return null +} + +const NotificationItem = ({ + onPress, + icon, + title, + description, +}: { + onPress: () => void + icon: React.ReactNode + title: string + description: string +}) => { + const {styles} = useStyles() + return ( + + {icon} + + + {title} + + {description} + + + ) +} + +const TransactionReceivedIcon = () => { + const {styles, colors} = useStyles() + return ( + + + + ) +} + +const RewardsUpdatedIcon = () => { + const {styles, colors} = useStyles() + return ( + + + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + container: { + flex: 1, + height: 76, + borderRadius: 6, + ...atoms.p_lg, + ...atoms.gap_lg, + ...atoms.flex_row, + backgroundColor: color.bg_color_max, + }, + icon: { + width: 40, + height: 40, + borderRadius: 20, + ...atoms.align_center, + ...atoms.justify_center, + }, + content: { + ...atoms.flex_col, + ...atoms.gap_xs, + }, + title: { + ...atoms.body_2_md_regular, + ...atoms.font_semibold, + }, + description: { + ...atoms.link_2_md, + color: color.gray_600, + }, + }) + + return {styles, colors: {iconColor: color.secondary_600, iconBackground: color.secondary_100}} +} diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationStack.tsx b/apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationStack.tsx new file mode 100644 index 0000000000..1c0d5e1946 --- /dev/null +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationStack.tsx @@ -0,0 +1,39 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, View} from 'react-native' +import {SafeAreaView} from 'react-native-safe-area-context' + +type Props = { + children: React.ReactNode +} + +export const NotificationStack = ({children}: Props) => { + const {styles} = useStyles() + return ( + + + {children} + + + ) +} + +const useStyles = () => { + const {atoms} = useTheme() + const styles = StyleSheet.create({ + absolute: { + ...atoms.absolute, + top: 0, + left: 0, + right: 0, + ...atoms.z_50, + ...atoms.p_lg, + }, + flex: { + ...atoms.gap_sm, + ...atoms.flex_col, + }, + }) + + return {styles} +} diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/SwipeOutWrapper.tsx b/apps/wallet-mobile/src/features/Notifications/useCases/common/SwipeOutWrapper.tsx new file mode 100644 index 0000000000..9018752b71 --- /dev/null +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/SwipeOutWrapper.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import {Animated, Dimensions, PanResponder} from 'react-native' + +type Props = { + children: React.ReactNode + onSwipeOut: () => void +} + +export const SwipeOutWrapper = ({children, onSwipeOut}: Props) => { + const {pan, panResponder} = usePanAnimation({onRelease: onSwipeOut}) + + return ( + + {children} + + ) +} + +const usePanAnimation = ({onRelease}: {onRelease: () => void}) => { + const pan = React.useRef(new Animated.ValueXY()).current + const screenWidth = Dimensions.get('window').width + const screenLimitInPercentAfterWhichShouldRelease = 0.3 + + const panResponder = React.useRef( + PanResponder.create({ + onMoveShouldSetPanResponder: () => true, + onPanResponderMove: (e, gestureState) => { + if (gestureState.dx > 0) { + Animated.event([null, {dx: pan.x, dy: pan.y}], {useNativeDriver: false})(e, gestureState) + } + }, + onPanResponderRelease: (e, gestureState) => { + if (gestureState.dx > screenWidth * screenLimitInPercentAfterWhichShouldRelease) { + Animated.spring(pan, { + toValue: {x: screenWidth, y: 0}, + useNativeDriver: false, + }).start(() => onRelease()) + } else { + Animated.spring(pan, { + toValue: {x: 0, y: 0}, + useNativeDriver: false, + }).start() + } + }, + }), + ).current + + return {pan, panResponder} +} diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/hooks.ts b/apps/wallet-mobile/src/features/Notifications/useCases/common/hooks.ts index 293e5b082e..fe6538f7ce 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/hooks.ts +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/hooks.ts @@ -51,7 +51,7 @@ const init = () => { export const useInitNotifications = ({enabled}: {enabled: boolean}) => { React.useEffect(() => (enabled ? init() : undefined), [enabled]) useTransactionReceivedNotifications({enabled}) - usePrimaryTokenPriceChangedNotification({enabled}) + usePrimaryTokenPriceChangedNotification({enabled: false}) // Temporarily disabled until requested by product team useRewardsUpdatedNotifications({enabled}) usePushNotifications({enabled}) } diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/notification-manager.ts b/apps/wallet-mobile/src/features/Notifications/useCases/common/notification-manager.ts index 2bcc1937d8..7c340e01f4 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/notification-manager.ts +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/notification-manager.ts @@ -2,7 +2,6 @@ import {mountAsyncStorage} from '@yoroi/common' import {notificationManagerMaker} from '@yoroi/notifications' import {Notifications} from '@yoroi/types' -import {displayNotificationEvent} from './notifications' import {primaryTokenPriceChangedSubject} from './primary-token-price-changed-notification' import {rewardsUpdatedSubject} from './rewards-updated-notification' import {transactionReceivedSubject} from './transaction-received-notification' @@ -13,7 +12,6 @@ const notificationStorage = appStorage.join('notifications/') export const notificationManager = notificationManagerMaker({ eventsStorage: notificationStorage.join('events/'), configStorage: notificationStorage.join('settings/'), - display: displayNotificationEvent, subscriptions: { [Notifications.Trigger.TransactionReceived]: transactionReceivedSubject, [Notifications.Trigger.PrimaryTokenPriceChanged]: primaryTokenPriceChangedSubject, diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/primary-token-price-changed-notification.ts b/apps/wallet-mobile/src/features/Notifications/useCases/common/primary-token-price-changed-notification.ts index 438c8a77ce..fddfec8c34 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/primary-token-price-changed-notification.ts +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/primary-token-price-changed-notification.ts @@ -1,8 +1,5 @@ import {isRight, useAsyncStorage} from '@yoroi/common' -import {mountAsyncStorage} from '@yoroi/common/src' import {App, Notifications as NotificationTypes} from '@yoroi/types' -import * as BackgroundFetch from 'expo-background-fetch' -import * as TaskManager from 'expo-task-manager' import * as React from 'react' import {Subject} from 'rxjs' @@ -14,23 +11,10 @@ import {notificationManager} from './notification-manager' import {generateNotificationId} from './notifications' import {buildProcessedNotificationsStorage} from './storage' -const backgroundTaskId = 'yoroi-primary-token-price-changed-background-fetch' const refetchIntervalInSeconds = 60 * 10 const refetchIntervalInMilliseconds = refetchIntervalInSeconds * 1000 const storageKey = 'notifications/primary-token-price-changed/' -// Check is needed for hot reloading, as task can not be defined twice -if (!TaskManager.isTaskDefined(backgroundTaskId)) { - const appStorage = mountAsyncStorage({path: '/'}) - TaskManager.defineTask(backgroundTaskId, async () => { - const notifications = await buildNotifications(appStorage) - notifications.forEach((notification) => notificationManager.events.push(notification)) - - const hasNewData = notifications.length > 0 - return hasNewData ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData - }) -} - const buildNotifications = async ( appStorage: App.Storage, ): Promise => { @@ -70,14 +54,6 @@ export const usePrimaryTokenPriceChangedNotification = ({enabled}: {enabled: boo const {walletManager} = useWalletManager() const asyncStorage = useAsyncStorage() - React.useEffect(() => { - if (!enabled) return - registerBackgroundFetchAsync() - return () => { - unregisterBackgroundFetchAsync() - } - }, [enabled]) - React.useEffect(() => { if (!enabled) return @@ -90,18 +66,6 @@ export const usePrimaryTokenPriceChangedNotification = ({enabled}: {enabled: boo }, [walletManager, asyncStorage, enabled]) } -const registerBackgroundFetchAsync = () => { - return BackgroundFetch.registerTaskAsync(backgroundTaskId, { - minimumInterval: refetchIntervalInSeconds, - stopOnTerminate: false, - startOnBoot: true, - }) -} - -const unregisterBackgroundFetchAsync = () => { - return BackgroundFetch.unregisterTaskAsync(backgroundTaskId) -} - const createPrimaryTokenPriceChangedNotification = ( metadata: NotificationTypes.PrimaryTokenPriceChangedEvent['metadata'], ): NotificationTypes.PrimaryTokenPriceChangedEvent => { diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/rewards-updated-notification.ts b/apps/wallet-mobile/src/features/Notifications/useCases/common/rewards-updated-notification.ts index b4d58eaa34..a136ad6a09 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/rewards-updated-notification.ts +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/rewards-updated-notification.ts @@ -1,42 +1,17 @@ -import {mountAsyncStorage, useAsyncStorage} from '@yoroi/common' +import {useAsyncStorage} from '@yoroi/common' import {App, Notifications as NotificationTypes} from '@yoroi/types' -import * as BackgroundFetch from 'expo-background-fetch' -import * as TaskManager from 'expo-task-manager' import * as React from 'react' import {Subject} from 'rxjs' import {useWalletManager} from '../../../WalletManager/context/WalletManagerProvider' import {walletManager} from '../../../WalletManager/wallet-manager' -import {notificationManager} from './notification-manager' import {generateNotificationId} from './notifications' import {buildProcessedNotificationsStorage} from './storage' -const backgroundTaskId = 'yoroi-rewards-updated-notifications-background-fetch' const storageKey = 'rewards-updated-notification-history' -const backgroundSyncInMinutes = 60 * 10 - -// Check is needed for hot reloading, as task can not be defined twice -if (!TaskManager.isTaskDefined(backgroundTaskId)) { - const appStorage = mountAsyncStorage({path: '/'}) - TaskManager.defineTask(backgroundTaskId, async () => { - await syncAllWallets() - const notifications = await buildNotifications(appStorage) - notifications.forEach((notification) => notificationManager.events.push(notification)) - const hasNewData = notifications.length > 0 - return hasNewData ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData - }) -} export const rewardsUpdatedSubject = new Subject() -const registerBackgroundFetchAsync = () => { - return BackgroundFetch.registerTaskAsync(backgroundTaskId, { - minimumInterval: backgroundSyncInMinutes, - stopOnTerminate: false, - startOnBoot: true, - }) -} - const buildNotifications = async (appStorage: App.Storage) => { const walletIds = [...walletManager.walletMetas.keys()] const notifications: NotificationTypes.RewardsUpdatedEvent[] = [] @@ -61,31 +36,21 @@ const buildNotifications = async (appStorage: App.Storage) => { if (latestReward === rewards) continue await storage.setValues([rewards]) - notifications.push(createRewardsUpdatedNotification()) + notifications.push(createRewardsUpdatedNotification(walletId)) } return notifications } -const unregisterBackgroundFetchAsync = () => { - return BackgroundFetch.unregisterTaskAsync(backgroundTaskId) -} - -const syncAllWallets = async () => { - const ids = [...walletManager.walletMetas.keys()] - for (const id of ids) { - const wallet = walletManager.getWalletById(id) - if (!wallet) continue - await wallet.sync({}) - } -} - -const createRewardsUpdatedNotification = () => { +const createRewardsUpdatedNotification = (walletId: string): NotificationTypes.RewardsUpdatedEvent => { return { id: generateNotificationId(), date: new Date().toISOString(), isRead: false, trigger: NotificationTypes.Trigger.RewardsUpdated, + metadata: { + walletId, + }, } as const } @@ -93,14 +58,6 @@ export const useRewardsUpdatedNotifications = ({enabled}: {enabled: boolean}) => const {walletManager} = useWalletManager() const asyncStorage = useAsyncStorage() - React.useEffect(() => { - if (!enabled) return - registerBackgroundFetchAsync() - return () => { - unregisterBackgroundFetchAsync() - } - }, [enabled]) - React.useEffect(() => { if (!enabled) return const subscription = walletManager.syncWalletInfos$.subscribe(async (status) => { diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/transaction-received-notification.ts b/apps/wallet-mobile/src/features/Notifications/useCases/common/transaction-received-notification.ts index 08bc87a206..b6e5bc479d 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/transaction-received-notification.ts +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/transaction-received-notification.ts @@ -1,8 +1,5 @@ import {useAsyncStorage} from '@yoroi/common' -import {mountAsyncStorage} from '@yoroi/common/src' import {App, Notifications as NotificationTypes} from '@yoroi/types' -import * as BackgroundFetch from 'expo-background-fetch' -import * as TaskManager from 'expo-task-manager' import * as React from 'react' import {Subject} from 'rxjs' @@ -10,46 +7,11 @@ import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' import {TRANSACTION_DIRECTION} from '../../../../yoroi-wallets/types/other' import {useWalletManager} from '../../../WalletManager/context/WalletManagerProvider' import {walletManager} from '../../../WalletManager/wallet-manager' -import {notificationManager} from './notification-manager' import {generateNotificationId} from './notifications' import {buildProcessedNotificationsStorage} from './storage' -const backgroundTaskId = 'yoroi-transaction-received-notifications-background-fetch' const storageKey = 'transaction-received-notification-history' -// Check is needed for hot reloading, as task can not be defined twice -if (!TaskManager.isTaskDefined(backgroundTaskId)) { - const appStorage = mountAsyncStorage({path: '/'}) - TaskManager.defineTask(backgroundTaskId, async () => { - await syncAllWallets() - const notifications = await buildNotifications(appStorage) - notifications.forEach((notification) => notificationManager.events.push(notification)) - const hasNewData = notifications.length > 0 - return hasNewData ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData - }) -} - -const registerBackgroundFetchAsync = () => { - return BackgroundFetch.registerTaskAsync(backgroundTaskId, { - minimumInterval: 60 * 10, - stopOnTerminate: false, - startOnBoot: true, - }) -} - -const unregisterBackgroundFetchAsync = () => { - return BackgroundFetch.unregisterTaskAsync(backgroundTaskId) -} - -const syncAllWallets = async () => { - const ids = [...walletManager.walletMetas.keys()] - for (const id of ids) { - const wallet = walletManager.getWalletById(id) - if (!wallet) continue - await wallet.sync({}) - } -} - const buildNotifications = async (appStorage: App.Storage) => { const walletIds = [...walletManager.walletMetas.keys()] const notifications: NotificationTypes.TransactionReceivedEvent[] = [] @@ -82,6 +44,7 @@ const buildNotifications = async (appStorage: App.Storage) => { isSentByUser: wallet.transactions[id]?.direction === TRANSACTION_DIRECTION.SENT, nextTxsCounter: newTxIds.length + processed.length, previousTxsCounter: processed.length, + walletId, } notifications.push(createTransactionReceivedNotification(metadata)) }) @@ -97,7 +60,7 @@ const getTxIds = (wallet: YoroiWallet) => { export const createTransactionReceivedNotification = ( metadata: NotificationTypes.TransactionReceivedEvent['metadata'], -) => { +): NotificationTypes.TransactionReceivedEvent => { return { id: generateNotificationId(), date: new Date().toISOString(), @@ -113,14 +76,6 @@ export const useTransactionReceivedNotifications = ({enabled}: {enabled: boolean const {walletManager} = useWalletManager() const asyncStorage = useAsyncStorage() - React.useEffect(() => { - if (!enabled) return - registerBackgroundFetchAsync() - return () => { - unregisterBackgroundFetchAsync() - } - }, [enabled]) - React.useEffect(() => { if (!enabled) return const subscription = walletManager.syncWalletInfos$.subscribe(async (status) => { diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/useStrings.tsx b/apps/wallet-mobile/src/features/Notifications/useCases/common/useStrings.tsx new file mode 100644 index 0000000000..0af81981fa --- /dev/null +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/useStrings.tsx @@ -0,0 +1,26 @@ +import {defineMessages, useIntl} from 'react-intl' + +export const useStrings = () => { + const intl = useIntl() + + return { + tapToView: intl.formatMessage(messages.tapToView), + stakingRewardsReceived: intl.formatMessage(messages.stakingRewardsReceived), + assetsReceived: intl.formatMessage(messages.assetsReceived), + } +} + +const messages = defineMessages({ + tapToView: { + id: 'notifications.tapToView', + defaultMessage: '!!!Tap to view', + }, + stakingRewardsReceived: { + id: 'notifications.stakingRewardsReceived', + defaultMessage: '!!!Staking rewards received', + }, + assetsReceived: { + id: 'notifications.assetsReceived', + defaultMessage: '!!!Assets received', + }, +}) diff --git a/apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/Notifications/NotificationsDisplaySettings.tsx b/apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/Notifications/NotificationsDisplaySettings.tsx new file mode 100644 index 0000000000..954cce0424 --- /dev/null +++ b/apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/Notifications/NotificationsDisplaySettings.tsx @@ -0,0 +1,48 @@ +import {isString, useAsyncStorage, useMutationWithInvalidations} from '@yoroi/common' +import {App} from '@yoroi/types' +import {useQuery} from 'react-query' + +import {useWalletManager} from '../../../../WalletManager/context/WalletManagerProvider' + +const defaultNotificationsEnabled = true + +export const useNotificationDisplaySettings = () => { + const storage = useAsyncStorage() + const walletManager = useWalletManager() + const selectedWalletId = walletManager.selected.wallet?.id + const query = useQuery({ + queryKey: ['settings', selectedWalletId, 'notifications'], + queryFn: () => { + if (!isString(selectedWalletId)) return defaultNotificationsEnabled + return getNotificationDisplaySettings(storage, selectedWalletId) + }, + enabled: isString(selectedWalletId), + }) + + return query.data ?? defaultNotificationsEnabled +} + +export const useChangeNotificationDisplaySettings = () => { + const storage = useAsyncStorage() + const walletManager = useWalletManager() + const selectedWalletId = walletManager.selected.wallet?.id + const mutationFn = async (value: boolean) => { + if (!isString(selectedWalletId)) throw new Error('useChangeNotificationDisplaySettings: No wallet selected') + await changeNotificationDisplaySettings(storage, selectedWalletId, value) + } + return useMutationWithInvalidations({ + mutationFn, + invalidateQueries: [['settings', selectedWalletId, 'notifications']], + }) +} + +const getNotificationDisplaySettings = async (storage: App.Storage, walletId: string): Promise => { + const setting = await storage + .join(`wallet/${walletId}/`) + .getItem('displayNotifications', (value) => (isString(value) ? JSON.parse(value) : null)) + return setting ?? defaultNotificationsEnabled +} + +const changeNotificationDisplaySettings = async (storage: App.Storage, walletId: string, value: boolean) => { + await storage.join(`wallet/${walletId}/`).setItem('displayNotifications', value) +} diff --git a/apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx b/apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx index ac252a3229..6de7b07b5a 100644 --- a/apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx +++ b/apps/wallet-mobile/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx @@ -10,6 +10,7 @@ import {SafeAreaView} from 'react-native-safe-area-context' import {Icon} from '../../../../components/Icon' import {Spacer} from '../../../../components/Spacer/Spacer' import {DIALOG_BUTTONS, showConfirmationDialog} from '../../../../kernel/dialogs' +import {features} from '../../../../kernel/features' import {confirmationMessages} from '../../../../kernel/i18n/global-messages' import {SettingsRouteNavigation, useWalletNavigation} from '../../../../kernel/navigation' import {useResync} from '../../../../yoroi-wallets/hooks' @@ -26,6 +27,10 @@ import { SettingsItem, SettingsSection, } from '../../SettingsItems' +import { + useChangeNotificationDisplaySettings, + useNotificationDisplaySettings, +} from './Notifications/NotificationsDisplaySettings' export const WalletSettingsScreen = () => { const intl = useIntl() @@ -130,6 +135,18 @@ export const WalletSettingsScreen = () => { + {features.notifications && ( + <> + + } label={strings.allowNotifications}> + + + + + + + )} + @@ -198,6 +215,20 @@ const AddressModeSwitcher = (props: {isSingle: boolean}) => { return } +const NotificationDisplaySwitcher = () => { + const displayNotifications = useNotificationDisplaySettings() + const {mutate} = useChangeNotificationDisplaySettings() + const [localValue, setLocalValue] = React.useState(displayNotifications) + + const handleOnToggle = () => { + const newValue = !localValue + setLocalValue(newValue) + mutate(newValue) + } + + return +} + const useLogout = () => { const {logout} = useAuth() const intl = useIntl() @@ -296,6 +327,14 @@ const messages = defineMessages({ id: 'components.settings.walletsettingscreen.resyncWallet', defaultMessage: '!!!Resync', }, + inAppNotifications: { + id: 'components.settings.walletsettingscreen.inAppNotifications', + defaultMessage: '!!!In-app notifications', + }, + allowNotifications: { + id: 'components.settings.walletsettingscreen.allowNotifications', + defaultMessage: '!!!Allow notifications', + }, }) const useStrings = () => { @@ -323,6 +362,8 @@ const useStrings = () => { multipleAddresses: intl.formatMessage(messages.multipleAddresses), singleAddress: intl.formatMessage(messages.singleAddress), multipleAddressesInfo: intl.formatMessage(messages.multipleAddressesInfo), + inAppNotifications: intl.formatMessage(messages.inAppNotifications), + allowNotifications: intl.formatMessage(messages.allowNotifications), } } diff --git a/apps/wallet-mobile/src/kernel/i18n/locales/en-US.json b/apps/wallet-mobile/src/kernel/i18n/locales/en-US.json index d62a34ae9d..014647f227 100644 --- a/apps/wallet-mobile/src/kernel/i18n/locales/en-US.json +++ b/apps/wallet-mobile/src/kernel/i18n/locales/en-US.json @@ -486,6 +486,8 @@ "components.settings.walletsettingscreen.unknownWalletType": "Unknown Wallet Type", "components.settings.walletsettingscreen.walletName": "Wallet name", "components.settings.walletsettingscreen.walletType": "Wallet type:", + "components.settings.walletsettingscreen.inAppNotifications": "In-app notifications", + "components.settings.walletsettingscreen.allowNotifications": "Allow notifications", "components.somethingWentWrong.title": "Oops!", "components.somethingWentWrong.description": "Something went wrong.\nTry to reload this page or restart the app.", "components.stakingcenter.confirmDelegation.delegateButtonLabel": "Delegate", @@ -1317,5 +1319,8 @@ "txReview.createdBy": "Created by", "txReview.overview.operationsNoticeText": "You are about to interact with operations, which are key components used in governance and various blockchain activities. These include Cardano Governance Certificates, as outlined in CIP-0095, which facilitate governance transactions.", "txReview.overview.operationsNoticeButton": "Ok", - "txReview.overview.operationsNoticeTitle": "What are operations?" + "txReview.overview.operationsNoticeTitle": "What are operations?", + "notifications.tapToView": "Tap to view", + "notifications.stakingRewardsReceived": "Staking rewards received", + "notifications.assetsReceived": "Assets received" } diff --git a/apps/wallet-mobile/translations/messages/src/AppNavigator.json b/apps/wallet-mobile/translations/messages/src/AppNavigator.json index 446e819fb5..41c40edef2 100644 --- a/apps/wallet-mobile/translations/messages/src/AppNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/AppNavigator.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Enter PIN", "file": "src/AppNavigator.tsx", "start": { - "line": 251, + "line": 254, "column": 17, - "index": 9472 + "index": 9599 }, "end": { - "line": 254, + "line": 257, "column": 3, - "index": 9562 + "index": 9689 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Set PIN", "file": "src/AppNavigator.tsx", "start": { - "line": 255, + "line": 258, "column": 18, - "index": 9582 + "index": 9709 }, "end": { - "line": 258, + "line": 261, "column": 3, - "index": 9680 + "index": 9807 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Auth with OS changes", "file": "src/AppNavigator.tsx", "start": { - "line": 259, + "line": 262, "column": 25, - "index": 9707 + "index": 9834 }, "end": { - "line": 262, + "line": 265, "column": 3, - "index": 9821 + "index": 9948 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Auth with OS changed detected", "file": "src/AppNavigator.tsx", "start": { - "line": 263, + "line": 266, "column": 27, - "index": 9850 + "index": 9977 }, "end": { - "line": 266, + "line": 269, "column": 3, - "index": 9971 + "index": 10098 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/features/Notifications/useCases/common/useStrings.json b/apps/wallet-mobile/translations/messages/src/features/Notifications/useCases/common/useStrings.json new file mode 100644 index 0000000000..d443081677 --- /dev/null +++ b/apps/wallet-mobile/translations/messages/src/features/Notifications/useCases/common/useStrings.json @@ -0,0 +1,47 @@ +[ + { + "id": "notifications.tapToView", + "defaultMessage": "!!!Tap to view", + "file": "src/features/Notifications/useCases/common/useStrings.tsx", + "start": { + "line": 14, + "column": 13, + "index": 378 + }, + "end": { + "line": 17, + "column": 3, + "index": 456 + } + }, + { + "id": "notifications.stakingRewardsReceived", + "defaultMessage": "!!!Staking rewards received", + "file": "src/features/Notifications/useCases/common/useStrings.tsx", + "start": { + "line": 18, + "column": 26, + "index": 484 + }, + "end": { + "line": 21, + "column": 3, + "index": 588 + } + }, + { + "id": "notifications.assetsReceived", + "defaultMessage": "!!!Assets received", + "file": "src/features/Notifications/useCases/common/useStrings.tsx", + "start": { + "line": 22, + "column": 18, + "index": 608 + }, + "end": { + "line": 25, + "column": 3, + "index": 695 + } + } +] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.json b/apps/wallet-mobile/translations/messages/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.json index 30115fdaed..1017d4b7f6 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.json +++ b/apps/wallet-mobile/translations/messages/src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!General", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 214, + "line": 245, "column": 11, - "index": 6780 + "index": 7799 }, "end": { - "line": 217, + "line": 248, "column": 3, - "index": 6878 + "index": 7897 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Actions", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 218, + "line": 249, "column": 11, - "index": 6891 + "index": 7910 }, "end": { - "line": 221, + "line": 252, "column": 3, - "index": 6989 + "index": 8008 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Switch wallet", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 222, + "line": 253, "column": 16, - "index": 7007 + "index": 8026 }, "end": { - "line": 225, + "line": 256, "column": 3, - "index": 7116 + "index": 8135 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Logout", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 226, + "line": 257, "column": 10, - "index": 7128 + "index": 8147 }, "end": { - "line": 229, + "line": 260, "column": 3, - "index": 7224 + "index": 8243 } }, { @@ -64,14 +64,14 @@ "defaultMessage": "!!!Wallet name", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 230, + "line": 261, "column": 14, - "index": 7240 + "index": 8259 }, "end": { - "line": 233, + "line": 264, "column": 3, - "index": 7345 + "index": 8364 } }, { @@ -79,14 +79,14 @@ "defaultMessage": "!!!Security", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 234, + "line": 265, "column": 12, - "index": 7359 + "index": 8378 }, "end": { - "line": 237, + "line": 268, "column": 3, - "index": 7459 + "index": 8478 } }, { @@ -94,14 +94,14 @@ "defaultMessage": "!!!Change spending password", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 238, + "line": 269, "column": 18, - "index": 7479 + "index": 8498 }, "end": { - "line": 241, + "line": 272, "column": 3, - "index": 7601 + "index": 8620 } }, { @@ -109,14 +109,14 @@ "defaultMessage": "!!!Easy transaction confirmation", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 242, + "line": 273, "column": 20, - "index": 7623 + "index": 8642 }, "end": { - "line": 245, + "line": 276, "column": 3, - "index": 7752 + "index": 8771 } }, { @@ -124,14 +124,14 @@ "defaultMessage": "!!!Skip the password and approve transactions with biometrics", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 246, + "line": 277, "column": 24, - "index": 7778 + "index": 8797 }, "end": { - "line": 249, + "line": 280, "column": 3, - "index": 7940 + "index": 8959 } }, { @@ -139,14 +139,14 @@ "defaultMessage": "!!!Remove wallet", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 250, + "line": 281, "column": 16, - "index": 7958 + "index": 8977 }, "end": { - "line": 253, + "line": 284, "column": 3, - "index": 8067 + "index": 9086 } }, { @@ -154,14 +154,14 @@ "defaultMessage": "!!!Collateral", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 254, + "line": 285, "column": 14, - "index": 8083 + "index": 9102 }, "end": { - "line": 257, + "line": 288, "column": 3, - "index": 8154 + "index": 9173 } }, { @@ -169,14 +169,14 @@ "defaultMessage": "!!!Multiple addresses", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 258, + "line": 289, "column": 21, - "index": 8177 + "index": 9196 }, "end": { - "line": 261, + "line": 292, "column": 3, - "index": 8263 + "index": 9282 } }, { @@ -184,14 +184,14 @@ "defaultMessage": "!!!Single address", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 262, + "line": 293, "column": 17, - "index": 8282 + "index": 9301 }, "end": { - "line": 265, + "line": 296, "column": 3, - "index": 8360 + "index": 9379 } }, { @@ -199,14 +199,14 @@ "defaultMessage": "!!!By enabling this you can operate with more wallet addresses", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 266, + "line": 297, "column": 25, - "index": 8387 + "index": 9406 }, "end": { - "line": 269, + "line": 300, "column": 3, - "index": 8518 + "index": 9537 } }, { @@ -214,14 +214,14 @@ "defaultMessage": "!!!Network:", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 271, + "line": 302, "column": 11, - "index": 8579 + "index": 9598 }, "end": { - "line": 274, + "line": 305, "column": 3, - "index": 8645 + "index": 9664 } }, { @@ -229,14 +229,14 @@ "defaultMessage": "!!!Wallet type:", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 275, + "line": 306, "column": 14, - "index": 8661 + "index": 9680 }, "end": { - "line": 278, + "line": 309, "column": 3, - "index": 8767 + "index": 9786 } }, { @@ -244,14 +244,14 @@ "defaultMessage": "!!!Byron-era wallet", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 279, + "line": 310, "column": 15, - "index": 8784 + "index": 9803 }, "end": { - "line": 282, + "line": 313, "column": 3, - "index": 8895 + "index": 9914 } }, { @@ -259,14 +259,14 @@ "defaultMessage": "!!!Shelley-era wallet", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 283, + "line": 314, "column": 17, - "index": 8914 + "index": 9933 }, "end": { - "line": 286, + "line": 317, "column": 3, - "index": 9029 + "index": 10048 } }, { @@ -274,14 +274,14 @@ "defaultMessage": "!!!Unknown Wallet Type", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 287, + "line": 318, "column": 21, - "index": 9052 + "index": 10071 }, "end": { - "line": 290, + "line": 321, "column": 3, - "index": 9172 + "index": 10191 } }, { @@ -289,14 +289,14 @@ "defaultMessage": "!!!About", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 291, + "line": 322, "column": 9, - "index": 9183 + "index": 10202 }, "end": { - "line": 294, + "line": 325, "column": 3, - "index": 9277 + "index": 10296 } }, { @@ -304,14 +304,44 @@ "defaultMessage": "!!!Resync", "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", "start": { - "line": 295, + "line": 326, "column": 10, - "index": 9289 + "index": 10308 + }, + "end": { + "line": 329, + "column": 3, + "index": 10410 + } + }, + { + "id": "components.settings.walletsettingscreen.inAppNotifications", + "defaultMessage": "!!!In-app notifications", + "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", + "start": { + "line": 330, + "column": 22, + "index": 10434 + }, + "end": { + "line": 333, + "column": 3, + "index": 10556 + } + }, + { + "id": "components.settings.walletsettingscreen.allowNotifications", + "defaultMessage": "!!!Allow notifications", + "file": "src/features/Settings/useCases/changeWalletSettings/WalletSettingsScreen.tsx", + "start": { + "line": 334, + "column": 22, + "index": 10580 }, "end": { - "line": 298, + "line": 337, "column": 3, - "index": 9391 + "index": 10701 } } ] \ No newline at end of file diff --git a/packages/notifications/src/notification-manager.test.ts b/packages/notifications/src/notification-manager.test.ts index eaef7cbdfa..52ebd4aa29 100644 --- a/packages/notifications/src/notification-manager.test.ts +++ b/packages/notifications/src/notification-manager.test.ts @@ -10,7 +10,6 @@ const createManager = () => { return notificationManagerMaker({ eventsStorage, configStorage, - display: jest.fn(), }) } @@ -256,7 +255,6 @@ describe('NotificationManager', () => { [Notifications.Trigger.RewardsUpdated]: new Subject(), [Notifications.Trigger.PrimaryTokenPriceChanged]: new Subject(), }, - display: jest.fn(), }) manager.hydrate() @@ -311,6 +309,7 @@ const createTransactionReceivedEvent = ( nextTxsCounter: 1, txId: '1', isSentByUser: true, + walletId: 'walletId', }, ...overrides, }) diff --git a/packages/notifications/src/notification-manager.ts b/packages/notifications/src/notification-manager.ts index 4c938d904b..cde487b161 100644 --- a/packages/notifications/src/notification-manager.ts +++ b/packages/notifications/src/notification-manager.ts @@ -11,7 +11,6 @@ export const notificationManagerMaker = ({ eventsStorage, configStorage, subscriptions, - display, eventsLimit = 100, }: Notifications.ManagerMakerProps): Notifications.Manager => { const localSubscriptions: Subscription[] = [] @@ -32,10 +31,9 @@ export const notificationManagerMaker = ({ } const config = configManagerMaker({storage: configStorage}) - const {events, unreadCounterByGroup$} = eventsManagerMaker({ + const {events, unreadCounterByGroup$, newEvents$} = eventsManagerMaker({ storage: eventsStorage, config, - display, eventsLimit, }) @@ -56,6 +54,7 @@ export const notificationManagerMaker = ({ events, config, unreadCounterByGroup$, + newEvents$, } } @@ -77,19 +76,13 @@ const notificationTriggerGroups: Record< const eventsManagerMaker = ({ storage, config, - display, eventsLimit, }: { - display: (event: Notifications.Event) => void storage: App.Storage config: Notifications.Manager['config'] eventsLimit?: number -}): { - events: Notifications.Manager['events'] - unreadCounterByGroup$: BehaviorSubject< - Readonly> - > -} => { +}) => { + const newEvents$ = new Subject() const unreadCounterByGroup$ = new BehaviorSubject< Map >(buildUnreadCounterDefaultValue()) @@ -136,7 +129,7 @@ const eventsManagerMaker = ({ await storage.setItem('events', newEvents) if (!event.isRead) { await updateUnreadCounter() - display(event) + newEvents$.next(event) } }, clear: async (): Promise => { @@ -144,7 +137,7 @@ const eventsManagerMaker = ({ unreadCounterByGroup$.next(buildUnreadCounterDefaultValue()) }, } - return {events, unreadCounterByGroup$} + return {events, unreadCounterByGroup$, newEvents$} } const configManagerMaker = ({ diff --git a/packages/notifications/src/translators/reactjs/mocks.ts b/packages/notifications/src/translators/reactjs/mocks.ts index e65d825c88..2b80336674 100644 --- a/packages/notifications/src/translators/reactjs/mocks.ts +++ b/packages/notifications/src/translators/reactjs/mocks.ts @@ -7,6 +7,5 @@ export const createManagerMock = () => { return notificationManagerMaker({ eventsStorage, configStorage, - display: jest.fn(), }) } diff --git a/packages/notifications/src/translators/reactjs/useReceivedNotificationEvents.test.tsx b/packages/notifications/src/translators/reactjs/useReceivedNotificationEvents.test.tsx index eedd80d0fe..7576b24a35 100644 --- a/packages/notifications/src/translators/reactjs/useReceivedNotificationEvents.test.tsx +++ b/packages/notifications/src/translators/reactjs/useReceivedNotificationEvents.test.tsx @@ -55,6 +55,7 @@ describe('useReceivedNotificationEvents', () => { isSentByUser: false, nextTxsCounter: 1, previousTxsCounter: 0, + walletId: 'walletId', }, date: new Date().toISOString(), trigger: Notifications.Trigger.TransactionReceived, diff --git a/packages/types/src/notifications/manager.ts b/packages/types/src/notifications/manager.ts index 2f5669a51b..059db6cd07 100644 --- a/packages/types/src/notifications/manager.ts +++ b/packages/types/src/notifications/manager.ts @@ -15,7 +15,6 @@ export type NotificationManagerMakerProps = { [NotificationTrigger.RewardsUpdated]: Subject [NotificationTrigger.PrimaryTokenPriceChanged]: Subject }> - display: (event: NotificationEvent) => void eventsLimit?: number } @@ -23,6 +22,7 @@ export interface NotificationTransactionReceivedEvent extends NotificationEventBase { trigger: NotificationTrigger.TransactionReceived metadata: { + walletId: string previousTxsCounter: number nextTxsCounter: number txId: string @@ -32,6 +32,9 @@ export interface NotificationTransactionReceivedEvent export interface NotificationRewardsUpdatedEvent extends NotificationEventBase { trigger: NotificationTrigger.RewardsUpdated + metadata: { + walletId: string + } } export interface NotificationPrimaryTokenPriceChangedEvent @@ -79,6 +82,8 @@ export type NotificationManager = { Readonly> > + newEvents$: Subject + // Events represent a notification event that was trigger by a config rule events: { markAllAsRead: () => Promise