diff --git a/apps/wallet-mobile/.storybook/storybook.requires.js b/apps/wallet-mobile/.storybook/storybook.requires.js index f593d32e88..7046d09d9b 100644 --- a/apps/wallet-mobile/.storybook/storybook.requires.js +++ b/apps/wallet-mobile/.storybook/storybook.requires.js @@ -75,6 +75,7 @@ const getStories = () => { "./src/components/ExpandableInfoCard/ExpandableInfoCard.stories.tsx": require("../src/components/ExpandableInfoCard/ExpandableInfoCard.stories.tsx"), "./src/components/HideableText/HideableText.stories.tsx": require("../src/components/HideableText/HideableText.stories.tsx"), "./src/components/Icon/Icon.stories.tsx": require("../src/components/Icon/Icon.stories.tsx"), + "./src/components/InfoBanner/InfoBanner.stories.tsx": require("../src/components/InfoBanner/InfoBanner.stories.tsx"), "./src/components/LanguagePicker/LanguagePicker.stories.tsx": require("../src/components/LanguagePicker/LanguagePicker.stories.tsx"), "./src/components/LanguagePicker/LanguagePickerWarning.stories.tsx": require("../src/components/LanguagePicker/LanguagePickerWarning.stories.tsx"), "./src/components/legacy/Modal/Modal.stories.tsx": require("../src/components/legacy/Modal/Modal.stories.tsx"), diff --git a/apps/wallet-mobile/src/AppNavigator.tsx b/apps/wallet-mobile/src/AppNavigator.tsx index 89f7e2c376..44e6691b4d 100644 --- a/apps/wallet-mobile/src/AppNavigator.tsx +++ b/apps/wallet-mobile/src/AppNavigator.tsx @@ -33,7 +33,6 @@ import { import {useDeepLinkWatcher} from './features/Links/common/useDeepLinkWatcher' import {useNotifications} from './features/Notifications/useCases/common/hooks' import {NotificationsDevScreen} from './features/Notifications/useCases/NotificationsDevScreen' -import {PortfolioScreen} from './features/Portfolio/useCases/PortfolioScreen' import {SearchProvider} from './features/Search/SearchContext' import {SetupWalletNavigator} from './features/SetupWallet/SetupWalletNavigator' import {useHasWallets} from './features/WalletManager/common/hooks/useHasWallets' @@ -206,8 +205,6 @@ export const AppNavigator = () => { - - )} diff --git a/apps/wallet-mobile/src/YoroiApp.tsx b/apps/wallet-mobile/src/YoroiApp.tsx index 97c06b05aa..8404aa272d 100644 --- a/apps/wallet-mobile/src/YoroiApp.tsx +++ b/apps/wallet-mobile/src/YoroiApp.tsx @@ -28,6 +28,8 @@ import {useMigrations} from './kernel/storage/migrations/useMigrations' import {rootStorage} from './kernel/storage/rootStorage' import {PoolTransitionProvider} from './legacy/Staking/PoolTransition/PoolTransitionProvider' import {useThemeStorageMaker} from './yoroi-wallets/hooks' +import {NotificationProvider} from '@yoroi/notifications' +import {notificationManager} from './features/Notifications/useCases/common/notification-manager' enableScreens(true) enableFreeze(true) @@ -63,7 +65,9 @@ const Yoroi = () => { - + + + diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx b/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx index 004b94f495..265ccf81e4 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx +++ b/apps/wallet-mobile/src/features/Notifications/useCases/NotificationsDevScreen.tsx @@ -14,11 +14,11 @@ import {SafeAreaView} from 'react-native-safe-area-context' import {Button} from '../../../components/Button/Button' import {ScrollView} from '../../../components/ScrollView/ScrollView' -import {useMockedNotifications} from './common/mocks' import {createTransactionReceivedNotification} from './common/transaction-received-notification' +import {notificationManager} from './common/notification-manager' export const NotificationsDevScreen = () => { - const {manager} = useMockedNotifications() + const manager = notificationManager return ( @@ -30,7 +30,7 @@ const Screen = () => { const manager = useNotificationManager() const handleOnTriggerTransactionReceived = () => { - manager.notification$.next( + manager.events.push( createTransactionReceivedNotification({ previousTxsCounter: 0, nextTxsCounter: 1, 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 31f8d522e1..621b386082 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/hooks.ts +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/hooks.ts @@ -1,12 +1,7 @@ import {Notification, Notifications} from '@jamsinclair/react-native-notifications' -import {useAsyncStorage} from '@yoroi/common' -import {notificationManagerMaker} from '@yoroi/notifications' -import {Notifications as NotificationTypes} from '@yoroi/types' -import * as React from 'react' import {useEffect} from 'react' - -import {displayNotificationEvent} from './notifications' -import {useTransactionReceivedNotificationSubject} from './transaction-received-notification' +import {notificationManager} from './notification-manager' +import {useTransactionReceivedNotifications} from './transaction-received-notification' let initialized = false @@ -21,51 +16,14 @@ const init = () => { Notifications.events().registerNotificationReceivedBackground((notification: Notification) => { console.log(`Notification received in background: ${notification.title} : ${notification.body}`) }) -} - -export const useNotificationsManager = (options?: { - subscriptions?: NotificationTypes.ManagerMakerProps['subscriptions'] -}) => { - const storage = useAsyncStorage() - const subscriptions = options?.subscriptions - - const manager = React.useMemo(() => { - const eventsStorage = storage.join('events/') - const configStorage = storage.join('settings/') - - return notificationManagerMaker({ - eventsStorage, - configStorage, - subscriptions, - }) - }, [storage, subscriptions]) + notificationManager.hydrate() - React.useEffect(() => { - manager.hydrate() - return () => { - manager.destroy() - } - }, [manager]) - - return manager + return () => { + notificationManager.destroy() + } } export const useNotifications = () => { - useEffect(() => { - init() - }, []) - - const transactionReceivedSubject = useTransactionReceivedNotificationSubject() - - const manager = useNotificationsManager({ - subscriptions: { - [NotificationTypes.Trigger.TransactionReceived]: transactionReceivedSubject, - }, - }) - React.useEffect(() => { - const subscription = manager.notification$.subscribe(displayNotificationEvent) - return () => { - subscription.unsubscribe() - } - }, [manager]) + useEffect(() => init(), []) + useTransactionReceivedNotifications() } diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/mocks.ts b/apps/wallet-mobile/src/features/Notifications/useCases/common/mocks.ts deleted file mode 100644 index 6c94b7f026..0000000000 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/mocks.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {Notifications as NotificationTypes} from '@yoroi/types' -import * as React from 'react' -import {Subject} from 'rxjs' - -import {useNotificationsManager} from './hooks' -import {displayNotificationEvent} from './notifications' -import {createTransactionReceivedNotification} from './transaction-received-notification' - -export const useMockedNotifications = () => { - const [transactionReceivedSubject] = React.useState(new Subject()) - const manager = useNotificationsManager({ - subscriptions: { - [NotificationTypes.Trigger.TransactionReceived]: transactionReceivedSubject, - }, - }) - React.useEffect(() => { - const subscription = manager.notification$.subscribe(displayNotificationEvent) - return () => { - subscription.unsubscribe() - } - }, [manager]) - - const triggerTransactionReceived = (metadata: NotificationTypes.TransactionReceivedEvent['metadata']) => { - transactionReceivedSubject.next(createTransactionReceivedNotification(metadata)) - } - - return {triggerTransactionReceived, manager} -} 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 eeb7b1286d..b8a1db6349 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 @@ -1,9 +1,16 @@ -import {mountAsyncStorage} from '@yoroi/common/src' +import {mountAsyncStorage} from '@yoroi/common' import {notificationManagerMaker} from '@yoroi/notifications' +import {displayNotificationEvent} from './notifications' +import {Notifications} from '@yoroi/types' +import {transactionReceivedSubject} from './transaction-received-notification' const appStorage = mountAsyncStorage({path: '/'}) export const notificationManager = notificationManagerMaker({ eventsStorage: appStorage.join('events/'), configStorage: appStorage.join('settings/'), + display: displayNotificationEvent, + subscriptions: { + [Notifications.Trigger.TransactionReceived]: transactionReceivedSubject, + }, }) diff --git a/apps/wallet-mobile/src/features/Notifications/useCases/common/notifications.ts b/apps/wallet-mobile/src/features/Notifications/useCases/common/notifications.ts index 4677d3581b..6cda39ffb9 100644 --- a/apps/wallet-mobile/src/features/Notifications/useCases/common/notifications.ts +++ b/apps/wallet-mobile/src/features/Notifications/useCases/common/notifications.ts @@ -8,7 +8,7 @@ export const displayNotificationEvent = (notificationEvent: NotificationTypes.Ev } } -export const sendNotification = (title: string, body: string) => { +const sendNotification = (title: string, body: string) => { const notification = new Notification({ title, body, 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 0289b028d8..d18e9f1585 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 @@ -11,27 +11,19 @@ import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' import {useWalletManager} from '../../../WalletManager/context/WalletManagerProvider' import {WalletManager, walletManager} from '../../../WalletManager/wallet-manager' import {notificationManager} from './notification-manager' -import {displayNotificationEvent} from './notifications' const BACKGROUND_FETCH_TASK = 'yoroi-notifications-background-fetch' if (!TaskManager.isTaskDefined(BACKGROUND_FETCH_TASK)) { const appStorage = mountAsyncStorage({path: '/'}) - TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { - const now = Date.now() - console.log(`Got background fetch call at date: ${new Date(now).toISOString()}`) - await syncAllWallets(walletManager) - await checkForNewTransactions(walletManager, appStorage) - - // TODO: Be sure to return the correct result type! - return BackgroundFetch.BackgroundFetchResult.NewData + const notifications = await checkForNewTransactions(walletManager, appStorage) + notifications.forEach((notification) => notificationManager.events.push(notification)) + const hasNewData = notifications.length > 0 + return hasNewData ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData }) } -// 2. Register the task at some point in your app by providing the same name, -// and some configuration options for how the background fetch should behave -// Note: This does NOT need to be in the global scope and CAN be used in your React components! export async function registerBackgroundFetchAsync() { return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { minimumInterval: 60 * 10, // 10 minutes @@ -61,26 +53,29 @@ const syncAllWallets = async (walletManager: WalletManager) => { export const checkForNewTransactions = async (walletManager: WalletManager, appStorage: App.Storage) => { const walletIds = [...walletManager.walletMetas.keys()] const notificationsAsyncStorage = appStorage.join('notifications-transaction-received-subject/') + const notifications: NotificationTypes.TransactionReceivedEvent[] = [] for (const id of walletIds) { const wallet = walletManager.getWalletById(id) - if (!wallet) return + if (!wallet) continue const processed = (await notificationsAsyncStorage.getItem(id)) || [] const txIds = getTxIds(wallet) - // TODO: Improve this if (processed.length === 0) { console.log(`Wallet ${id} has no processed tx ids`) await notificationsAsyncStorage.setItem(id, txIds) - return + continue } + const newTxIds = txIds.filter((txId) => !processed.includes(txId)) + if (newTxIds.length === 0) { console.log(`Wallet ${id} has no new tx ids on network ${wallet.networkManager.network}`) - return + continue } console.log('new tx ids', newTxIds) await notificationsAsyncStorage.setItem(id, [...processed, ...newTxIds]) + newTxIds.forEach((id) => { const metadata: NotificationTypes.TransactionReceivedEvent['metadata'] = { txId: id, @@ -88,11 +83,11 @@ export const checkForNewTransactions = async (walletManager: WalletManager, appS nextTxsCounter: 1, previousTxsCounter: 0, } - const notification = createTransactionReceivedNotification(metadata) - notificationManager.events.save(notification) - displayNotificationEvent(notification) + notifications.push(createTransactionReceivedNotification(metadata)) }) } + + return notifications } const getTxIds = (wallet: YoroiWallet) => { @@ -112,10 +107,11 @@ export const createTransactionReceivedNotification = ( } as const } -export const useTransactionReceivedNotificationSubject = () => { +export const transactionReceivedSubject = new Subject() + +export const useTransactionReceivedNotifications = () => { const {walletManager} = useWalletManager() const asyncStorage = useAsyncStorage() - const [transactionReceivedSubject] = React.useState(new Subject()) React.useEffect(() => { registerBackgroundFetchAsync() @@ -131,7 +127,8 @@ export const useTransactionReceivedNotificationSubject = () => { const areAllDone = walletsDoneSyncing.length === walletInfos.length if (!areAllDone) return - await checkForNewTransactions(walletManager, asyncStorage) + const notifications = await checkForNewTransactions(walletManager, asyncStorage) + notifications.forEach((notification) => transactionReceivedSubject.next(notification)) }) return () => { diff --git a/apps/wallet-mobile/translations/messages/src/AppNavigator.json b/apps/wallet-mobile/translations/messages/src/AppNavigator.json index 8902a072e3..6e8d9c302b 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": 230, + "line": 235, "column": 17, - "index": 8390 + "index": 8675 }, "end": { - "line": 233, + "line": 238, "column": 3, - "index": 8480 + "index": 8765 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Set PIN", "file": "src/AppNavigator.tsx", "start": { - "line": 234, + "line": 239, "column": 18, - "index": 8500 + "index": 8785 }, "end": { - "line": 237, + "line": 242, "column": 3, - "index": 8598 + "index": 8883 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Auth with OS changes", "file": "src/AppNavigator.tsx", "start": { - "line": 238, + "line": 243, "column": 25, - "index": 8625 + "index": 8910 }, "end": { - "line": 241, + "line": 246, "column": 3, - "index": 8739 + "index": 9024 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Auth with OS changed detected", "file": "src/AppNavigator.tsx", "start": { - "line": 242, + "line": 247, "column": 27, - "index": 8768 + "index": 9053 }, "end": { - "line": 245, + "line": 250, "column": 3, - "index": 8889 + "index": 9174 } } ] \ No newline at end of file diff --git a/packages/notifications/src/notification-manager.ts b/packages/notifications/src/notification-manager.ts index e01a131e9c..efd3699832 100644 --- a/packages/notifications/src/notification-manager.ts +++ b/packages/notifications/src/notification-manager.ts @@ -7,11 +7,11 @@ type ConfigStorageData = Notifications.Config const getAllTriggers = (): Array => Object.values(Notifications.Trigger) -// TODO: Add handler to show notification export const notificationManagerMaker = ({ eventsStorage, configStorage, subscriptions, + display, }: Notifications.ManagerMakerProps): Notifications.Manager => { const localSubscriptions: Subscription[] = [] @@ -31,6 +31,7 @@ export const notificationManagerMaker = ({ const {events, unreadCounterByGroup$, notification$} = eventsManagerMaker({ storage: eventsStorage, config, + display, }) const clear = async () => { @@ -72,7 +73,9 @@ const notificationTriggerGroups: Record< const eventsManagerMaker = ({ storage, config, + display, }: { + display: (event: Notifications.Event) => void storage: App.Storage config: Notifications.Manager['config'] }): { @@ -120,8 +123,7 @@ const eventsManagerMaker = ({ read: async (): Promise => { return (await storage.getItem('events')) ?? [] }, - save: async (event: Readonly) => { - // TODO: Maybe rename to notify + push: async (event: Readonly) => { if (!shouldNotify(event, await config.read())) { return } @@ -130,6 +132,7 @@ const eventsManagerMaker = ({ if (!event.isRead) { await updateUnreadCounter() notification$.next(event) + display(event) } }, clear: async (): Promise => { diff --git a/packages/types/src/notifications/manager.ts b/packages/types/src/notifications/manager.ts index de74784da8..4214207535 100644 --- a/packages/types/src/notifications/manager.ts +++ b/packages/types/src/notifications/manager.ts @@ -13,6 +13,7 @@ export type NotificationManagerMakerProps = { subscriptions?: Partial< Record> > + display: (event: NotificationEvent) => void } export interface NotificationTransactionReceivedEvent @@ -78,7 +79,7 @@ export type NotificationManager = { markAllAsRead: () => Promise markAsRead(id: NotificationEventId): Promise read: () => Promise> - save: (event: Readonly) => Promise + push: (event: Readonly) => Promise clear: () => Promise } // Config sets the ground to what, when, and if should notify user