diff --git a/apps/mobile/.easignore b/apps/mobile/.easignore new file mode 100644 index 0000000000..91a9f56902 --- /dev/null +++ b/apps/mobile/.easignore @@ -0,0 +1,63 @@ +# Auto generated storybook file +.storybook/storybook.requires.ts + +# From jest +html +coverage + +# macOS +.DS_Store + +/.idea +# Tamagui UI generates a lot of cache files +.tamagui + +*storybook.log +/storybook-static + +# Android and iOS build files +/android/* +/ios/* + +# @generated expo-cli sync-8d4afeec25ea8a192358fae2f8e2fc766bdce4ec +# The following patterns were generated by expo-cli + +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*. +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# @end expo-cli \ No newline at end of file diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index 91a9f56902..5b3c4fb198 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -18,6 +18,8 @@ coverage # Android and iOS build files /android/* /ios/* +google-services.json +GoogleService-Info.plist # @generated expo-cli sync-8d4afeec25ea8a192358fae2f8e2fc766bdce4ec # The following patterns were generated by expo-cli diff --git a/apps/mobile/app.config.js b/apps/mobile/app.config.js index 43af1a1361..b1441353cf 100644 --- a/apps/mobile/app.config.js +++ b/apps/mobile/app.config.js @@ -24,10 +24,15 @@ export default { }, infoPlist: { NSFaceIDUsageDescription: 'Enabling Face ID allows you to create/access secure keys.', + UIBackgroundModes: ['remote-notification'], }, supportsTablet: true, appleTeamId: 'MXRS32BBL4', bundleIdentifier: IS_DEV ? 'global.safe.mobileapp.dev' : 'global.safe.mobileapp', + entitlements: { + 'aps-environment': 'production', + }, + googleServicesFile: process.env.GOOGLE_SERVICES_PLIST ?? './GoogleService-Info.plist', }, android: { adaptiveIcon: { @@ -36,6 +41,7 @@ export default { monochromeImage: './assets/images/monochrome-icon.png', }, package: IS_DEV ? 'global.safe.mobileapp.dev' : 'global.safe.mobileapp', + googleServicesFile: process.env.GOOGLE_SERVICES_JSON ?? './google-services.json', }, web: { bundler: 'metro', @@ -63,6 +69,16 @@ export default { }, ], ['./expo-plugins/withDrawableAssets.js', './assets/android/drawable'], + [ + 'expo-build-properties', + { + ios: { + useFrameworks: 'static', + }, + }, + ], + '@react-native-firebase/app', + '@react-native-firebase/messaging', ], experiments: { typedRoutes: true, diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index d430524923..a481a5ab47 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -10,6 +10,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler' import { HeaderBackButton } from '@react-navigation/elements' import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import { PortalProvider } from '@tamagui/portal' +import { NotificationsProvider } from '@/src/context/NotificationsContext' import { SafeToastProvider } from '@/src/theme/provider/toastProvider' import { configureReanimatedLogger, ReanimatedLogLevel } from 'react-native-reanimated' import { OnboardingHeader } from '@/src/features/Onboarding/components/OnboardingHeader' @@ -28,43 +29,45 @@ function RootLayout() { return ( - - - - - - ({ - headerBackButtonDisplayMode: 'minimal', - headerShadowVisible: false, - headerLeft: (props) => ( - - ), - })} - > - - - - - - - - - - - - - + + + + + + + ({ + headerBackButtonDisplayMode: 'minimal', + headerShadowVisible: false, + headerLeft: (props) => ( + + ), + })} + > + + + + + + + + + + + + + + ) diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json index a4ad6d5441..10f852016f 100644 --- a/apps/mobile/eas.json +++ b/apps/mobile/eas.json @@ -19,6 +19,9 @@ "developmentClient": true, "env": { "APP_VARIANT": "development" + }, + "android": { + "image": "ubuntu-18.04-jdk-11-ndk-r19c" } }, "preview-ios-simulator": { @@ -42,6 +45,16 @@ "extends": "base", "environment": "production", "autoIncrement": true, + "ios": { + "env": { + "GOOGLE_SERVICES_FILE": "./GoogleService-Info.plist" + } + }, + "android": { + "env": { + "GOOGLE_SERVICES_FILE": "./google-services.json" + } + }, "env": { "APP_VARIANT": "production" } diff --git a/apps/mobile/firebase.json b/apps/mobile/firebase.json new file mode 100644 index 0000000000..97ec0198ef --- /dev/null +++ b/apps/mobile/firebase.json @@ -0,0 +1,9 @@ +{ + "react-native": { + "analytics_auto_collection_enabled": false, + "messaging_auto_init_enabled": true, + "messaging_ios_auto_register_for_remote_messages": false, + "android_task_executor_maximum_pool_size": 10, + "android_task_executor_keep_alive_seconds": 3 + } +} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 10c3ffde1f..48e7c9c1c1 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -41,8 +41,11 @@ "@ethersproject/shims": "^5.7.0", "@expo/config-plugins": "^9.0.10", "@expo/vector-icons": "^14.0.2", + "@notifee/react-native": "^9.1.8", "@react-native-clipboard/clipboard": "^1.15.0", "@react-native-community/blur": "^4.4.1", + "@react-native-firebase/app": "^21.7.1", + "@react-native-firebase/messaging": "^21.7.1", "@react-native-menu/menu": "^1.1.6", "@react-native/babel-preset": "^0.76.2", "@react-navigation/material-top-tabs": "^7.1.0", @@ -64,6 +67,7 @@ "ethers": "^6.13.4", "expo": "~52.0.14", "expo-blur": "~14.0.1", + "expo-build-properties": "^0.13.2", "expo-constants": "~17.0.4", "expo-dev-client": "~5.0.5", "expo-font": "~13.0.3", diff --git a/apps/mobile/src/context/NotificationsContext.tsx b/apps/mobile/src/context/NotificationsContext.tsx new file mode 100644 index 0000000000..e002d79ba8 --- /dev/null +++ b/apps/mobile/src/context/NotificationsContext.tsx @@ -0,0 +1,37 @@ +import React, { createContext, useContext, ReactNode } from 'react' + +import useNotifications from '@/src/hooks/useNotifications' +import { FirebaseMessagingTypes } from '@react-native-firebase/messaging' + +interface NotificationContextType { + isAppNotificationEnabled: boolean + fcmToken: string | null + remoteMessages: FirebaseMessagingTypes.RemoteMessage[] | [] +} + +const NotificationContext = createContext(undefined) + +export const useNotification = () => { + const context = useContext(NotificationContext) + if (!context) { + throw new Error('useNotification must be used within a NotificationProvider') + } + return context +} + +interface NotificationProviderProps { + children: ReactNode +} + +export const NotificationsProvider: React.FC = ({ children }) => { + /** + * Enables notifications for the app if the user has enabled them + */ + const { isAppNotificationEnabled, fcmToken, remoteMessages } = useNotifications() + + return ( + + {children} + + ) +} diff --git a/apps/mobile/src/hooks/useNotifications.ts b/apps/mobile/src/hooks/useNotifications.ts new file mode 100644 index 0000000000..2dd494d011 --- /dev/null +++ b/apps/mobile/src/hooks/useNotifications.ts @@ -0,0 +1,89 @@ +import { useEffect } from 'react' +import FCMService from '@/src/services/notifications/FCMService' +import { useAppSelector, useAppDispatch } from '@/src/store/hooks' +import { + selectAppNotificationStatus, + selectFCMToken, + selectPromptAttempts, + selectLastTimePromptAttempted, + selectRemoteMessages, + toggleAppNotifications, +} from '@/src/store/notificationsSlice' +import NotificationsService from '@/src/services/notifications/NotificationService' +import { FirebaseMessagingTypes } from '@react-native-firebase/messaging' +import Logger from '@/src/utils/logger' + +interface NotificationsProps { + isAppNotificationEnabled: boolean + fcmToken: string | null + remoteMessages: FirebaseMessagingTypes.RemoteMessage[] +} + +const useNotifications = (): NotificationsProps => { + const dispatch = useAppDispatch() + /** + * We need to check if the user has enabled notifications for the device in order to keep listening for messages + * since the user can disable notifications at any time on their device, we need to handle app behavior accordingly + * if device notifications are disabled, the user has been prompt more than 3 times within a month to enable the app notifications + * we should only ask the user to enable notifications again after a month has passed + * + * If the user has disabled notifications for the app, we should disable app notifications + */ + const isAppNotificationEnabled = useAppSelector(selectAppNotificationStatus) + const fcmToken = useAppSelector(selectFCMToken) + const remoteMessages = useAppSelector(selectRemoteMessages) + + const promptAttempts = useAppSelector(selectPromptAttempts) + const lastTimePromptAttempted = useAppSelector(selectLastTimePromptAttempted) + + useEffect(() => { + const checkNotifications = async () => { + const isDeviceNotificationEnabled = await NotificationsService.isDeviceNotificationEnabled() + if (!isDeviceNotificationEnabled) { + /** + * If the user has been prompt more than 3 times within a month to enable the device notifications + * we should only ask the user to enable it again after a month has passed + * + * This also disables app notifications if the user has disabled device notifications and denied to re-enabled it after 3 attempts + */ + if ( + promptAttempts && + promptAttempts >= 3 && + lastTimePromptAttempted && + new Date().getTime() - new Date(lastTimePromptAttempted).getTime() < 2592000000 + ) { + if (isAppNotificationEnabled) { + dispatch(toggleAppNotifications(false)) + } + return + } + + const { permission } = await NotificationsService.getAllPermissions() + + if (permission !== 'authorized') { + return + } + } + + try { + // Firebase Cloud Messaging + await FCMService.registerAppWithFCM() + await FCMService.saveFCMToken() + FCMService.listenForMessagesBackground() + } catch (error) { + Logger.error('FCM Registration or Token Save failed', error) + return + } + + return () => { + FCMService.listenForMessagesForeground()() + } + } + + checkNotifications() + }, [isAppNotificationEnabled]) + + return { isAppNotificationEnabled, fcmToken, remoteMessages } +} + +export default useNotifications diff --git a/apps/mobile/src/services/notifications/FCMService.ts b/apps/mobile/src/services/notifications/FCMService.ts new file mode 100644 index 0000000000..00bcf4ddc6 --- /dev/null +++ b/apps/mobile/src/services/notifications/FCMService.ts @@ -0,0 +1,67 @@ +import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging' +import Logger from '@/src/utils/logger' +import NotificationsService from './NotificationService' +import { ChannelId } from '@/src/utils/notifications' +import { store } from '@/src/store' +import { savePushToken } from '@/src/store/notificationsSlice' + +type UnsubscribeFunc = () => void + +class FCMService { + async getFCMToken(): Promise { + const { fcmToken } = store.getState().notifications + const token = fcmToken || undefined + if (!token) { + Logger.info('getFCMToken: No FCM token found') + } + return token + } + + async saveFCMToken(): Promise { + try { + const fcmToken = await messaging().getToken() + if (fcmToken) { + store.dispatch(savePushToken(fcmToken)) + } + } catch (error) { + Logger.info('FCMService :: error saving', error) + } + } + + listenForMessagesForeground = (): UnsubscribeFunc => + messaging().onMessage(async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => { + NotificationsService.displayNotification({ + channelId: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID, + title: remoteMessage.notification?.title || '', + body: remoteMessage.notification?.body || '', + data: remoteMessage.data, + }) + Logger.trace('listenForMessagesForeground: listening for messages in Foreground', remoteMessage) + }) + + listenForMessagesBackground = (): void => { + messaging().setBackgroundMessageHandler(async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => { + NotificationsService.displayNotification({ + channelId: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID, + title: remoteMessage.notification?.title || '', + body: remoteMessage.notification?.body || '', + data: remoteMessage.data, + }) + Logger.trace('listenForMessagesBackground :: listening for messages in background', remoteMessage) + }) + } + + async registerAppWithFCM(): Promise { + if (!messaging().registerDeviceForRemoteMessages) { + await messaging() + .registerDeviceForRemoteMessages() + .then((status: unknown) => { + Logger.info('registerDeviceForRemoteMessages status', status) + }) + .catch((error) => { + Logger.error('registerAppWithFCM: Something went wrong', error) + }) + } + } +} +export default new FCMService() diff --git a/apps/mobile/src/services/notifications/NotificationService.ts b/apps/mobile/src/services/notifications/NotificationService.ts new file mode 100644 index 0000000000..2c4f4e8707 --- /dev/null +++ b/apps/mobile/src/services/notifications/NotificationService.ts @@ -0,0 +1,283 @@ +import notifee, { + AuthorizationStatus, + Event as NotifeeEvent, + EventType, + EventDetail, + AndroidChannel, +} from '@notifee/react-native' +import { Linking, Platform, Alert as NativeAlert } from 'react-native' +import { store } from '@/src/store' +import { updatePromptAttempts, updateLastTimePromptAttempted } from '@/src/store/notificationsSlice' +import { toggleAppNotifications, toggleDeviceNotifications } from '@/src/store/notificationsSlice' + +import { HandleNotificationCallback, LAUNCH_ACTIVITY, PressActionId } from '@/src/store/constants' + +import { ChannelId, notificationChannels, withTimeout } from '@/src/utils/notifications' +import Logger from '@/src/utils/logger' + +import { FirebaseMessagingTypes } from '@react-native-firebase/messaging' + +interface AlertButton { + text: string + onPress: () => void | Promise +} + +class NotificationsService { + async getBlockedNotifications(): Promise> { + try { + const settings = await notifee.getNotificationSettings() + const channels = await notifee.getChannels() + + switch (settings.authorizationStatus) { + case AuthorizationStatus.NOT_DETERMINED: + case AuthorizationStatus.DENIED: + return notificationChannels.reduce((map, next) => { + map.set(next.id as ChannelId, true) + return map + }, new Map()) + } + + return channels.reduce((map, next) => { + if (next.blocked) { + map.set(next.id as ChannelId, true) + } + return map + }, new Map()) + } catch (error) { + Logger.error('Error checking if a user has push notifications permission', error) + return new Map() + } + } + + async getAllPermissions(shouldOpenSettings = true) { + try { + const promises: Promise[] = notificationChannels.map((channel: AndroidChannel) => + withTimeout(this.createChannel(channel), 5000), + ) + // 1 - Creates android's notifications channel + await Promise.allSettled(promises) + // 2 - Verifies granted permission from device + let permission = await withTimeout(this.checkCurrentPermissions(), 5000) + // 3 - Verifies blocked notifications + const blockedNotifications = await withTimeout(this.getBlockedNotifications(), 5000) + /** + * 4 - If permission has not being granted already or blocked notifications are found, open device's settings + * so that user can enable DEVICE notifications + **/ + if ((permission !== 'authorized' || blockedNotifications.size !== 0) && shouldOpenSettings) { + await this.requestPushNotificationsPermission() + permission = await withTimeout(this.checkCurrentPermissions(), 5000) + } + return { permission, blockedNotifications } + } catch (error) { + Logger.error('Error occurred while fetching permissions:', error) + + return { permission: 'denied', blockedNotifications: new Set() } + } + } + + async isDeviceNotificationEnabled() { + const permission = await notifee.getNotificationSettings() + + const isAuthorized = + permission.authorizationStatus === AuthorizationStatus.AUTHORIZED || + permission.authorizationStatus === AuthorizationStatus.PROVISIONAL + + return isAuthorized + } + + defaultButtons = (resolve: (value: boolean) => void): AlertButton[] => [ + { + text: 'Maybe later', + onPress: () => { + /** + * When user decides to NOT enable notifications, we should register the number of attempts and its dates + * so we avoid to prompt the user again within a month given a maximum of 3 attempts + */ + store.dispatch(updatePromptAttempts(1)) + store.dispatch(updateLastTimePromptAttempted(Date.now())) + + resolve(false) + }, + }, + { + text: 'Turn on', + onPress: async () => { + store.dispatch(toggleDeviceNotifications(true)) + store.dispatch(toggleAppNotifications(true)) + store.dispatch(updatePromptAttempts(0)) + store.dispatch(updateLastTimePromptAttempted(0)) + + await notifee.requestPermission() + this.openSystemSettings() + resolve(true) + }, + }, + ] + + asyncAlert = ( + title: string, + msg: string, + getButtons: (resolve: (value: boolean) => void) => AlertButton[] = this.defaultButtons, + ): Promise => + new Promise((resolve) => { + NativeAlert.alert(title, msg, getButtons(resolve), { + cancelable: false, + }) + }) + + async requestPushNotificationsPermission(): Promise { + try { + await this.asyncAlert( + 'Enable Push Notifications', + 'Turn on notifications from Settings to get important alerts on wallet activity and more.', + ) + } catch (error) { + Logger.error('Error checking if a user has push notifications permission', error) + } + } + + openSystemSettings() { + if (Platform.OS === 'ios') { + Linking.openSettings() + } else { + notifee.openNotificationSettings() + } + } + + async checkCurrentPermissions() { + const settings = await notifee.getNotificationSettings() + return settings.authorizationStatus === AuthorizationStatus.AUTHORIZED || + settings.authorizationStatus === AuthorizationStatus.PROVISIONAL + ? 'authorized' + : 'denied' + } + + onForegroundEvent(observer: (event: NotifeeEvent) => Promise): () => void { + return notifee.onForegroundEvent(observer) + } + + onBackgroundEvent(observer: (event: NotifeeEvent) => Promise) { + return notifee.onBackgroundEvent(observer) + } + + async incrementBadgeCount(incrementBy?: number) { + return await notifee.incrementBadgeCount(incrementBy) + } + + async decrementBadgeCount(decrementBy?: number) { + return await notifee.decrementBadgeCount(decrementBy) + } + + async setBadgeCount(count: number) { + return await notifee.setBadgeCount(count) + } + + async getBadgeCount() { + return await notifee.getBadgeCount() + } + + async handleNotificationPress({ + detail, + callback, + }: { + detail: EventDetail + callback?: (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => void + }) { + this.decrementBadgeCount(1) + if (detail?.notification?.id) { + await this.cancelTriggerNotification(detail.notification.id) + } + + if (detail?.notification?.data) { + callback?.(detail.notification as FirebaseMessagingTypes.RemoteMessage) + } + } + + async handleNotificationEvent({ + type, + detail, + callback, + }: NotifeeEvent & { + callback?: (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => void + }) { + switch (type as unknown as EventType) { + case EventType.DELIVERED: + this.incrementBadgeCount(1) + break + case EventType.PRESS: + this.handleNotificationPress({ + detail, + callback, + }) + break + } + } + + async cancelTriggerNotification(id?: string) { + if (!id) { + return + } + await notifee.cancelTriggerNotification(id) + } + + async getInitialNotification(callback: HandleNotificationCallback): Promise { + const event = await notifee.getInitialNotification() + if (event) { + callback(event.notification.data as Notification['data']) + } + } + + async cancelAllNotifications() { + await notifee.cancelAllNotifications() + } + + async createChannel(channel: AndroidChannel): Promise { + return await notifee.createChannel(channel) + } + + async displayNotification({ + channelId, + title, + body, + data, + }: { + channelId: ChannelId + title: string + body?: string + data?: FirebaseMessagingTypes.RemoteMessage['data'] + }): Promise { + try { + await notifee.displayNotification({ + title, + body, + data, + android: { + smallIcon: 'ic_notification_small', + largeIcon: 'ic_notification', + channelId: channelId ?? ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID, + pressAction: { + id: PressActionId.OPEN_NOTIFICATIONS_VIEW, + launchActivity: LAUNCH_ACTIVITY, + }, + }, + ios: { + launchImageName: 'Default', + sound: 'default', + interruptionLevel: 'critical', + foregroundPresentationOptions: { + alert: true, + sound: true, + badge: true, + banner: true, + list: true, + }, + }, + }) + } catch (error) { + Logger.error('NotificationService.displayNotification :: error', error) + } + } +} + +export default new NotificationsService() diff --git a/apps/mobile/src/store/constants.ts b/apps/mobile/src/store/constants.ts index f189c9eabd..10b6e8bef7 100644 --- a/apps/mobile/src/store/constants.ts +++ b/apps/mobile/src/store/constants.ts @@ -1,5 +1,6 @@ import { SafeOverview } from '@safe-global/store/gateway/AUTO_GENERATED/safes' import { SafeInfo } from '../types/address' +import { FirebaseMessagingTypes } from '@react-native-firebase/messaging' export const mockedActiveAccount: SafeInfo = { address: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', @@ -322,3 +323,12 @@ export const mapStorageTypeToIds = (id: STORAGE_IDS): STORAGE_TYPES => { return STORAGE_TYPES.STRING } } + +export type HandleNotificationCallback = (data: FirebaseMessagingTypes.RemoteMessage['data'] | undefined) => void + +export enum PressActionId { + OPEN_NOTIFICATIONS_VIEW = 'open-notifications-view-press-action-id', + OPEN_TRANSACTION_VIEW = 'open-transactions-view-press-action-id', +} + +export const LAUNCH_ACTIVITY = 'global.safe.mobileapp.ui.MainActivity' diff --git a/apps/mobile/src/store/index.ts b/apps/mobile/src/store/index.ts index bc60c33de1..076b076ab1 100644 --- a/apps/mobile/src/store/index.ts +++ b/apps/mobile/src/store/index.ts @@ -4,6 +4,7 @@ import { reduxStorage } from './storage' import txHistory from './txHistorySlice' import activeSafe from './activeSafeSlice' import myAccounts from './myAccountsSlice' +import notifications from './notificationsSlice' import safes from './safesSlice' import { cgwClient, setBaseUrl } from '@safe-global/store/gateway/cgwClient' import devToolsEnhancer from 'redux-devtools-expo-dev-plugin' @@ -20,6 +21,7 @@ export const rootReducer = combineReducers({ txHistory, safes, activeSafe, + notifications, myAccounts, [cgwClient.reducerPath]: cgwClient.reducer, }) diff --git a/apps/mobile/src/store/notificationsSlice.ts b/apps/mobile/src/store/notificationsSlice.ts new file mode 100644 index 0000000000..b9564ea0b4 --- /dev/null +++ b/apps/mobile/src/store/notificationsSlice.ts @@ -0,0 +1,57 @@ +import { createSlice } from '@reduxjs/toolkit' +import { RootState } from '.' + +const initialState = { + isDeviceNotificationsEnabled: false, + isAppNotificationsEnabled: false, + fcmToken: null, + remoteMessages: [], + promptAttempts: 0, + lastTimePromptAttempted: null, +} + +const notificationsSlice = createSlice({ + name: 'notifications', + initialState, + reducers: { + toggleAppNotifications: (state, action) => { + state.isAppNotificationsEnabled = action.payload + }, + toggleDeviceNotifications: (state, action) => { + state.isDeviceNotificationsEnabled = action.payload + }, + savePushToken: (state, action) => { + state.fcmToken = action.payload + }, + updateRemoteMessages: (state, action) => { + state.remoteMessages = action.payload + }, + updatePromptAttempts: (state, action) => { + if (action.payload === 0) { + state.promptAttempts = 0 + } + state.promptAttempts += 1 + }, + updateLastTimePromptAttempted: (state, action) => { + state.lastTimePromptAttempted = action.payload + }, + }, +}) + +export const { + toggleAppNotifications, + toggleDeviceNotifications, + savePushToken, + updateRemoteMessages, + updatePromptAttempts, + updateLastTimePromptAttempted, +} = notificationsSlice.actions + +export const selectAppNotificationStatus = (state: RootState) => state.notifications.isAppNotificationsEnabled +export const selectDeviceNotificationStatus = (state: RootState) => state.notifications.isDeviceNotificationsEnabled +export const selectFCMToken = (state: RootState) => state.notifications.fcmToken +export const selectRemoteMessages = (state: RootState) => state.notifications.remoteMessages +export const selectPromptAttempts = (state: RootState) => state.notifications.promptAttempts +export const selectLastTimePromptAttempted = (state: RootState) => state.notifications.lastTimePromptAttempted + +export default notificationsSlice.reducer diff --git a/apps/mobile/src/utils/notifications/index.ts b/apps/mobile/src/utils/notifications/index.ts new file mode 100644 index 0000000000..49bcd2f68b --- /dev/null +++ b/apps/mobile/src/utils/notifications/index.ts @@ -0,0 +1,38 @@ +import { AndroidChannel, AndroidImportance } from '@notifee/react-native' + +export enum ChannelId { + DEFAULT_NOTIFICATION_CHANNEL_ID = 'DEFAULT_NOTIFICATION_CHANNEL_ID', + ANNOUNCEMENT_NOTIFICATION_CHANNEL_ID = 'ANNOUNCEMENT_NOTIFICATION_CHANNEL_ID', +} + +export interface SafeAndroidChannel extends AndroidChannel { + id: ChannelId + title: string + subtitle: string +} + +export const notificationChannels = [ + { + id: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID, + name: 'Transaction Complete', + lights: true, + vibration: true, + importance: AndroidImportance.HIGH, + title: 'Transaction', + subtitle: 'Transaction Complete', + } as SafeAndroidChannel, + { + id: ChannelId.ANNOUNCEMENT_NOTIFICATION_CHANNEL_ID, + name: 'Safe Announcement', + lights: true, + vibration: true, + importance: AndroidImportance.HIGH, + title: 'Announcement', + subtitle: 'Safe Announcement', + } as SafeAndroidChannel, +] + +export function withTimeout(promise: Promise, ms: number): Promise { + const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms)) + return Promise.race([promise, timeout]) +} diff --git a/yarn.lock b/yarn.lock index 781dfacbbf..7fbe0ebcb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4106,6 +4106,21 @@ __metadata: languageName: node linkType: hard +"@firebase/analytics-compat@npm:0.2.14": + version: 0.2.14 + resolution: "@firebase/analytics-compat@npm:0.2.14" + dependencies: + "@firebase/analytics": "npm:0.10.8" + "@firebase/analytics-types": "npm:0.8.2" + "@firebase/component": "npm:0.6.9" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/0e368159d24223076b488b27308c11e5ef50456aff49fc58e1f66616228021c61e60c3299f63ce52ddc2f7099d803e9048bc28cd952cf5c302917002c03c85ee + languageName: node + linkType: hard + "@firebase/analytics-compat@npm:0.2.16": version: 0.2.16 resolution: "@firebase/analytics-compat@npm:0.2.16" @@ -4121,6 +4136,13 @@ __metadata: languageName: node linkType: hard +"@firebase/analytics-types@npm:0.8.2": + version: 0.8.2 + resolution: "@firebase/analytics-types@npm:0.8.2" + checksum: 10/297fb7becbc51950c7de5809fed896c391d1e87b4d8bb4bf88f4e8760b2e32f903a7dd8e92de4424b49c4e2ecb60a44d49e2f9c68ac3f7ffe3a0194f78910392 + languageName: node + linkType: hard + "@firebase/analytics-types@npm:0.8.3": version: 0.8.3 resolution: "@firebase/analytics-types@npm:0.8.3" @@ -4143,6 +4165,37 @@ __metadata: languageName: node linkType: hard +"@firebase/analytics@npm:0.10.8": + version: 0.10.8 + resolution: "@firebase/analytics@npm:0.10.8" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/installations": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/152ddaf68146f02baa7060d34426c25ec13890a53942ffa2db09faa148bef35f59ee9810e6fb8f561fb3d115b71d1fb9fb111d2a0f0199aa510220782557c765 + languageName: node + linkType: hard + +"@firebase/app-check-compat@npm:0.3.15": + version: 0.3.15 + resolution: "@firebase/app-check-compat@npm:0.3.15" + dependencies: + "@firebase/app-check": "npm:0.8.8" + "@firebase/app-check-types": "npm:0.5.2" + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/ae541d324d5f91dbb7b479855d3380c4fe73e365013b80973a54620405093e6fd2f8e418549155b3a527530472a19b6edf6df1481a708f823eba42e376105b28 + languageName: node + linkType: hard + "@firebase/app-check-compat@npm:0.3.17": version: 0.3.17 resolution: "@firebase/app-check-compat@npm:0.3.17" @@ -4159,6 +4212,13 @@ __metadata: languageName: node linkType: hard +"@firebase/app-check-interop-types@npm:0.3.2": + version: 0.3.2 + resolution: "@firebase/app-check-interop-types@npm:0.3.2" + checksum: 10/3effe656a4762c541838f4bde91b4498e51d48389046b930dc3dbb012e54b6ab0727f7c68a3e94198f633d57833346fc337a0847b6b03d2407030e1489d466fe + languageName: node + linkType: hard + "@firebase/app-check-interop-types@npm:0.3.3": version: 0.3.3 resolution: "@firebase/app-check-interop-types@npm:0.3.3" @@ -4166,6 +4226,13 @@ __metadata: languageName: node linkType: hard +"@firebase/app-check-types@npm:0.5.2": + version: 0.5.2 + resolution: "@firebase/app-check-types@npm:0.5.2" + checksum: 10/2b33a7adfb7b6ebf5423940bf0af5909df69bf2d6184e12e989f6c76062077be16c31193795349862b4f8aab6b3059806b732a92995cae30fd77419f19a86c6e + languageName: node + linkType: hard + "@firebase/app-check-types@npm:0.5.3": version: 0.5.3 resolution: "@firebase/app-check-types@npm:0.5.3" @@ -4187,6 +4254,33 @@ __metadata: languageName: node linkType: hard +"@firebase/app-check@npm:0.8.8": + version: 0.8.8 + resolution: "@firebase/app-check@npm:0.8.8" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/a3676f2143c8e438d7e8ac11bb163af30880f6ce6acc5cc54cfcc214b8efd5dabce14c040626f8a64a3967db144b99834f1108c2076a0eae8a6baf864b5a3d77 + languageName: node + linkType: hard + +"@firebase/app-compat@npm:0.2.41": + version: 0.2.41 + resolution: "@firebase/app-compat@npm:0.2.41" + dependencies: + "@firebase/app": "npm:0.10.11" + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + checksum: 10/67e4b0572a3c24c4acc13e2c3b55a4fc778d286bae10f1df684a142c9790b4f131519fe84087341884bd67b04b822c3f7092b9748dfa3b52086b6f82ca8a1001 + languageName: node + linkType: hard + "@firebase/app-compat@npm:0.2.47": version: 0.2.47 resolution: "@firebase/app-compat@npm:0.2.47" @@ -4200,6 +4294,13 @@ __metadata: languageName: node linkType: hard +"@firebase/app-types@npm:0.9.2": + version: 0.9.2 + resolution: "@firebase/app-types@npm:0.9.2" + checksum: 10/566b3714a4d7e8180514258e4b1549bf5b28ae0383b4ff53d3532a45e114048afdd27c1fef8688d871dd9e5ad5307e749776e23f094122655ac6b0fb550eb11a + languageName: node + linkType: hard + "@firebase/app-types@npm:0.9.3": version: 0.9.3 resolution: "@firebase/app-types@npm:0.9.3" @@ -4207,6 +4308,19 @@ __metadata: languageName: node linkType: hard +"@firebase/app@npm:0.10.11": + version: 0.10.11 + resolution: "@firebase/app@npm:0.10.11" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + idb: "npm:7.1.1" + tslib: "npm:^2.1.0" + checksum: 10/529d9e59b39e96cd97a8402e1cee508dbbb962aa1805345dc902ecbfe61709bb46ab3b821cd3b50b3d2e3e9f898515eb91cded030492e376550a97518cbcdb70 + languageName: node + linkType: hard + "@firebase/app@npm:0.10.17": version: 0.10.17 resolution: "@firebase/app@npm:0.10.17" @@ -4220,6 +4334,22 @@ __metadata: languageName: node linkType: hard +"@firebase/auth-compat@npm:0.5.14": + version: 0.5.14 + resolution: "@firebase/auth-compat@npm:0.5.14" + dependencies: + "@firebase/auth": "npm:1.7.9" + "@firebase/auth-types": "npm:0.12.2" + "@firebase/component": "npm:0.6.9" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + undici: "npm:6.19.7" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/85d5259e7b04b14b5d02dc1fb19b015d742c594c14138f33f13146ed9f6caa7ed9d19d65bb99aaca57e70ffd2a491e520d8638eadefbd00f839d37ef972cbbda + languageName: node + linkType: hard + "@firebase/auth-compat@npm:0.5.16": version: 0.5.16 resolution: "@firebase/auth-compat@npm:0.5.16" @@ -4235,6 +4365,13 @@ __metadata: languageName: node linkType: hard +"@firebase/auth-interop-types@npm:0.2.3": + version: 0.2.3 + resolution: "@firebase/auth-interop-types@npm:0.2.3" + checksum: 10/e55b8ded6bd1a5e6a2845c9c7ed520bb9a8a76e4ddf90249bf685986ac7b1fb079be2fa4edcb6a3aa81d1d56870a470eadcd5a8f20b797dccd803d72ed4c80aa + languageName: node + linkType: hard + "@firebase/auth-interop-types@npm:0.2.4": version: 0.2.4 resolution: "@firebase/auth-interop-types@npm:0.2.4" @@ -4242,6 +4379,16 @@ __metadata: languageName: node linkType: hard +"@firebase/auth-types@npm:0.12.2": + version: 0.12.2 + resolution: "@firebase/auth-types@npm:0.12.2" + peerDependencies: + "@firebase/app-types": 0.x + "@firebase/util": 1.x + checksum: 10/f55449381de8e2a24ffaf19f12b5c4a093c8323034253ea7a5f7afc946327d20b09f32a483c12960862a1c4814645ea80bc4343f0a9f22db5dc048ca82773132 + languageName: node + linkType: hard + "@firebase/auth-types@npm:0.12.3": version: 0.12.3 resolution: "@firebase/auth-types@npm:0.12.3" @@ -4252,6 +4399,25 @@ __metadata: languageName: node linkType: hard +"@firebase/auth@npm:1.7.9": + version: 1.7.9 + resolution: "@firebase/auth@npm:1.7.9" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + undici: "npm:6.19.7" + peerDependencies: + "@firebase/app": 0.x + "@react-native-async-storage/async-storage": ^1.18.1 + peerDependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true + checksum: 10/010013ec339c9ef7b4d9278c6cacfd8e2eb3282f27a3e4e89c42a5968955976a26277421f34fda3e9400409a22a61f632bcc03e713b3f39d71e4777bc003165d + languageName: node + linkType: hard + "@firebase/auth@npm:1.8.1": version: 1.8.1 resolution: "@firebase/auth@npm:1.8.1" @@ -4280,6 +4446,16 @@ __metadata: languageName: node linkType: hard +"@firebase/component@npm:0.6.9": + version: 0.6.9 + resolution: "@firebase/component@npm:0.6.9" + dependencies: + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + checksum: 10/76c865d640e4b24a0e50876ecdc0e1199df38af562131a937b5a4bac924d61b6933339afb7906881dca509f38f3b0c511cd6b5008e061424c61b20876de9531e + languageName: node + linkType: hard + "@firebase/data-connect@npm:0.1.3": version: 0.1.3 resolution: "@firebase/data-connect@npm:0.1.3" @@ -4295,6 +4471,20 @@ __metadata: languageName: node linkType: hard +"@firebase/database-compat@npm:1.0.8": + version: 1.0.8 + resolution: "@firebase/database-compat@npm:1.0.8" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/database": "npm:1.0.8" + "@firebase/database-types": "npm:1.0.5" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + checksum: 10/28389efcc87da77b822cb27c31707824fe98e7b0a3bf9cbf2b0c0fccd9edd72e2681a9467b76b120281464dbfc814852ebca63d99a385a9cb68fb55c7b334105 + languageName: node + linkType: hard + "@firebase/database-compat@npm:2.0.1": version: 2.0.1 resolution: "@firebase/database-compat@npm:2.0.1" @@ -4309,6 +4499,16 @@ __metadata: languageName: node linkType: hard +"@firebase/database-types@npm:1.0.5": + version: 1.0.5 + resolution: "@firebase/database-types@npm:1.0.5" + dependencies: + "@firebase/app-types": "npm:0.9.2" + "@firebase/util": "npm:1.10.0" + checksum: 10/bdf667da0369dce8623987fc01cad8db09cfe1895130f69ab581d34a0ee043ca6113c32457629147ae1441a934d985ede9d7cbe104ac346de6d0c21629903a8b + languageName: node + linkType: hard + "@firebase/database-types@npm:1.0.7": version: 1.0.7 resolution: "@firebase/database-types@npm:1.0.7" @@ -4334,6 +4534,36 @@ __metadata: languageName: node linkType: hard +"@firebase/database@npm:1.0.8": + version: 1.0.8 + resolution: "@firebase/database@npm:1.0.8" + dependencies: + "@firebase/app-check-interop-types": "npm:0.3.2" + "@firebase/auth-interop-types": "npm:0.2.3" + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + faye-websocket: "npm:0.11.4" + tslib: "npm:^2.1.0" + checksum: 10/adb199a6ad7866b418e8b319cc505e108bfc8200b5406f21857706df0849d4e5982a1b0e44e07001821edebef73c4dfffc7f96fb77a2cff10bb9ac26f17d40c3 + languageName: node + linkType: hard + +"@firebase/firestore-compat@npm:0.3.37": + version: 0.3.37 + resolution: "@firebase/firestore-compat@npm:0.3.37" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/firestore": "npm:4.7.2" + "@firebase/firestore-types": "npm:3.0.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/c152ba401f0d786699b25e1101d77351b7a6503f1a1f774efa7fecacc66aec58aca58a7b54e3f8587fcb45ffa3772d5e123ae79ddd90d0a87f2042ac34880d8a + languageName: node + linkType: hard + "@firebase/firestore-compat@npm:0.3.40": version: 0.3.40 resolution: "@firebase/firestore-compat@npm:0.3.40" @@ -4349,6 +4579,16 @@ __metadata: languageName: node linkType: hard +"@firebase/firestore-types@npm:3.0.2": + version: 3.0.2 + resolution: "@firebase/firestore-types@npm:3.0.2" + peerDependencies: + "@firebase/app-types": 0.x + "@firebase/util": 1.x + checksum: 10/81e91f836a026ecb70937407ca8699add7abb5b050d8815620cde97c3eec3f78f7dfbb366225758909f0df31d9f21e98a84ba62701bd27ee38b2609898c11acd + languageName: node + linkType: hard + "@firebase/firestore-types@npm:3.0.3": version: 3.0.3 resolution: "@firebase/firestore-types@npm:3.0.3" @@ -4359,6 +4599,24 @@ __metadata: languageName: node linkType: hard +"@firebase/firestore@npm:4.7.2": + version: 4.7.2 + resolution: "@firebase/firestore@npm:4.7.2" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + "@firebase/webchannel-wrapper": "npm:1.0.1" + "@grpc/grpc-js": "npm:~1.9.0" + "@grpc/proto-loader": "npm:^0.7.8" + tslib: "npm:^2.1.0" + undici: "npm:6.19.7" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/066a125760bc2163bbc9c6fcde8b3f67da97791f8ce6f5ffa8ff3c40567aff97b2fe02020c3403857f104f051e4d6452aee60fe75ed5e408e467c611c397b4bb + languageName: node + linkType: hard + "@firebase/firestore@npm:4.7.5": version: 4.7.5 resolution: "@firebase/firestore@npm:4.7.5" @@ -4376,6 +4634,21 @@ __metadata: languageName: node linkType: hard +"@firebase/functions-compat@npm:0.3.14": + version: 0.3.14 + resolution: "@firebase/functions-compat@npm:0.3.14" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/functions": "npm:0.11.8" + "@firebase/functions-types": "npm:0.6.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/a8d6cbcdc646d78adecfcdc1f8fa14a5d9af2394dd69cac00c6826106b923e01d246c67fb7e09025ca7cfb876f8d5df97240cc056c64ccee8899ca5f17178a6c + languageName: node + linkType: hard + "@firebase/functions-compat@npm:0.3.17": version: 0.3.17 resolution: "@firebase/functions-compat@npm:0.3.17" @@ -4391,6 +4664,13 @@ __metadata: languageName: node linkType: hard +"@firebase/functions-types@npm:0.6.2": + version: 0.6.2 + resolution: "@firebase/functions-types@npm:0.6.2" + checksum: 10/5b8733f9d4bd85a617d35dd10ce296d9ec0490494e584697c4eda8098ff1e865607d7880b84194e86c35d438bbcd714977c111180502d0d1b6b2da1cde1b37ca + languageName: node + linkType: hard + "@firebase/functions-types@npm:0.6.3": version: 0.6.3 resolution: "@firebase/functions-types@npm:0.6.3" @@ -4398,6 +4678,23 @@ __metadata: languageName: node linkType: hard +"@firebase/functions@npm:0.11.8": + version: 0.11.8 + resolution: "@firebase/functions@npm:0.11.8" + dependencies: + "@firebase/app-check-interop-types": "npm:0.3.2" + "@firebase/auth-interop-types": "npm:0.2.3" + "@firebase/component": "npm:0.6.9" + "@firebase/messaging-interop-types": "npm:0.2.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + undici: "npm:6.19.7" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/44f3e42df189f3f3cb3c366b38e93a0ffdfaa1a7b3f6dba624bcd9a7cda3d3271df66f2769b7cbe7e1e5ff01bf6ab3bef6c1e1e15c6646e34514d1e2ebb60555 + languageName: node + linkType: hard + "@firebase/functions@npm:0.12.0": version: 0.12.0 resolution: "@firebase/functions@npm:0.12.0" @@ -4429,6 +4726,30 @@ __metadata: languageName: node linkType: hard +"@firebase/installations-compat@npm:0.2.9": + version: 0.2.9 + resolution: "@firebase/installations-compat@npm:0.2.9" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/installations": "npm:0.6.9" + "@firebase/installations-types": "npm:0.5.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/919e1a4f4b63f5fe757a3c9cefb4a36cbab92deb4a6e15f249c94d6e80d1c6d37e5e384a460af8c17fc88e3091594bf43d036c88b704516c279b5ab8401977e1 + languageName: node + linkType: hard + +"@firebase/installations-types@npm:0.5.2": + version: 0.5.2 + resolution: "@firebase/installations-types@npm:0.5.2" + peerDependencies: + "@firebase/app-types": 0.x + checksum: 10/2e795280c299d644b8c8e3fdfa5c6f20cb367dd3b7df32317211f84393fa169b33dee0cbed28de407f3b22dc8f1fb2f7a11ae5a373f8082cc570ef61ef6b91ba + languageName: node + linkType: hard + "@firebase/installations-types@npm:0.5.3": version: 0.5.3 resolution: "@firebase/installations-types@npm:0.5.3" @@ -4452,6 +4773,29 @@ __metadata: languageName: node linkType: hard +"@firebase/installations@npm:0.6.9": + version: 0.6.9 + resolution: "@firebase/installations@npm:0.6.9" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/util": "npm:1.10.0" + idb: "npm:7.1.1" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/349c8b7e877b002fb29f274f4d239fbca4c2c266ccb66ecfb5f1762f973a7fe1be99cc3346184d1230e6e35feb2b6f9e8b7169479fa0018b53e4a83837848619 + languageName: node + linkType: hard + +"@firebase/logger@npm:0.4.2": + version: 0.4.2 + resolution: "@firebase/logger@npm:0.4.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/961b4605220c0a56c5f3ccf4e6049e44c27303c1ca998c6fa1d19de785c76d93e3b1a3da455e9f40655711345d8d779912366e4f369d93eda8d08c407cc5b140 + languageName: node + linkType: hard + "@firebase/logger@npm:0.4.4": version: 0.4.4 resolution: "@firebase/logger@npm:0.4.4" @@ -4461,6 +4805,20 @@ __metadata: languageName: node linkType: hard +"@firebase/messaging-compat@npm:0.2.11": + version: 0.2.11 + resolution: "@firebase/messaging-compat@npm:0.2.11" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/messaging": "npm:0.12.11" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/8ace6d65adcf891b272875b7b3f43978a15644b23f7ee796346f02eb50007c20c99719f4991772911005697613bf122167ca150d8245d0fccb2b959472b4a625 + languageName: node + linkType: hard + "@firebase/messaging-compat@npm:0.2.15": version: 0.2.15 resolution: "@firebase/messaging-compat@npm:0.2.15" @@ -4475,6 +4833,13 @@ __metadata: languageName: node linkType: hard +"@firebase/messaging-interop-types@npm:0.2.2": + version: 0.2.2 + resolution: "@firebase/messaging-interop-types@npm:0.2.2" + checksum: 10/547f8ebf2c5a8dcbc484991b97d76bd3dc3eb4bd9d4e6ea2ffc652097c7065d92dc68d389ddb19fba41e0ce3b5f4cd757ed22f96b4744801149b0f8dbf323af7 + languageName: node + linkType: hard + "@firebase/messaging-interop-types@npm:0.2.3": version: 0.2.3 resolution: "@firebase/messaging-interop-types@npm:0.2.3" @@ -4482,6 +4847,22 @@ __metadata: languageName: node linkType: hard +"@firebase/messaging@npm:0.12.11": + version: 0.12.11 + resolution: "@firebase/messaging@npm:0.12.11" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/installations": "npm:0.6.9" + "@firebase/messaging-interop-types": "npm:0.2.2" + "@firebase/util": "npm:1.10.0" + idb: "npm:7.1.1" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/1de21d56c74996e99151a902e0f1ff0825d47ebff044104483a838ff5cb4883433b2f541616b033255e4fd2780b29f71982d8832edf4987c101df97ed508828a + languageName: node + linkType: hard + "@firebase/messaging@npm:0.12.15": version: 0.12.15 resolution: "@firebase/messaging@npm:0.12.15" @@ -4514,6 +4895,29 @@ __metadata: languageName: node linkType: hard +"@firebase/performance-compat@npm:0.2.9": + version: 0.2.9 + resolution: "@firebase/performance-compat@npm:0.2.9" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/performance": "npm:0.6.9" + "@firebase/performance-types": "npm:0.2.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/bc4e8b0208c9bc603518e1388713ec80658ee109c6af80d429479447ccb85e8e831269383233c379ed66bf37469d13f5c234074d0c0c9e7e69e909be5fdeca4f + languageName: node + linkType: hard + +"@firebase/performance-types@npm:0.2.2": + version: 0.2.2 + resolution: "@firebase/performance-types@npm:0.2.2" + checksum: 10/d25ae06cb75ab6b44ffacf7affadc1f651881f283e58381c444eb63b62dfb74c33c544ab89843518ec1d15367ba7c4343b4d6b22de1f1df35126a1283baa578d + languageName: node + linkType: hard + "@firebase/performance-types@npm:0.2.3": version: 0.2.3 resolution: "@firebase/performance-types@npm:0.2.3" @@ -4536,6 +4940,21 @@ __metadata: languageName: node linkType: hard +"@firebase/performance@npm:0.6.9": + version: 0.6.9 + resolution: "@firebase/performance@npm:0.6.9" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/installations": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/d682d0b1e342ed3eda1a5ddab39c8ddac33afc9edb2c7335a2f9a28eb8c268b975bbf450a3bad5443138edebaf2aa731dca0b774bcf3211a6dc215b35d86d849 + languageName: node + linkType: hard + "@firebase/remote-config-compat@npm:0.2.11": version: 0.2.11 resolution: "@firebase/remote-config-compat@npm:0.2.11" @@ -4552,6 +4971,29 @@ __metadata: languageName: node linkType: hard +"@firebase/remote-config-compat@npm:0.2.9": + version: 0.2.9 + resolution: "@firebase/remote-config-compat@npm:0.2.9" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/remote-config": "npm:0.4.9" + "@firebase/remote-config-types": "npm:0.3.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/a6db7509512d8d22b7ddf1127c741715e379e04e5b3246372bb0302d7c84afb421a94550adebecddcce5def115d61729a9580940dce6e65f8d77f9af30f69fe1 + languageName: node + linkType: hard + +"@firebase/remote-config-types@npm:0.3.2": + version: 0.3.2 + resolution: "@firebase/remote-config-types@npm:0.3.2" + checksum: 10/6c91599c653825708aba9fe9e4562997f108c3e4f3eaf5d188f31c859a6ad013414aa7a213b6b021b68049dd0dd57158546dbc9fb64384652274ef7f57ce7d7d + languageName: node + linkType: hard + "@firebase/remote-config-types@npm:0.3.3": version: 0.3.3 resolution: "@firebase/remote-config-types@npm:0.3.3" @@ -4574,6 +5016,36 @@ __metadata: languageName: node linkType: hard +"@firebase/remote-config@npm:0.4.9": + version: 0.4.9 + resolution: "@firebase/remote-config@npm:0.4.9" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/installations": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/f14189f38c8cf75db16bf8b85dd004486b1dd8242f62d697c716fa85cd32928aed549ccea8c632a528870a424fc7f04f1132a14b3b099276cd7696c78e644b28 + languageName: node + linkType: hard + +"@firebase/storage-compat@npm:0.3.12": + version: 0.3.12 + resolution: "@firebase/storage-compat@npm:0.3.12" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/storage": "npm:0.13.2" + "@firebase/storage-types": "npm:0.8.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 10/4eea49a57f1d7537da697e5ff8b4e035ff1af69e416e7eab14485753c39c25eaa5a71bd2bafba0985ac6a7ce803f98f2f2f83c613c78c8f74bce286e3259b8ec + languageName: node + linkType: hard + "@firebase/storage-compat@npm:0.3.14": version: 0.3.14 resolution: "@firebase/storage-compat@npm:0.3.14" @@ -4589,6 +5061,16 @@ __metadata: languageName: node linkType: hard +"@firebase/storage-types@npm:0.8.2": + version: 0.8.2 + resolution: "@firebase/storage-types@npm:0.8.2" + peerDependencies: + "@firebase/app-types": 0.x + "@firebase/util": 1.x + checksum: 10/e00716932370d2004dc9f7ef6d7e3aff72305b91569fa6ec15e8bc2ec784b03a150391e8be2c063234edbbfda7796da915d48e26ce2f1f7c5d3343acd39afd99 + languageName: node + linkType: hard + "@firebase/storage-types@npm:0.8.3": version: 0.8.3 resolution: "@firebase/storage-types@npm:0.8.3" @@ -4599,6 +5081,20 @@ __metadata: languageName: node linkType: hard +"@firebase/storage@npm:0.13.2": + version: 0.13.2 + resolution: "@firebase/storage@npm:0.13.2" + dependencies: + "@firebase/component": "npm:0.6.9" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + undici: "npm:6.19.7" + peerDependencies: + "@firebase/app": 0.x + checksum: 10/d887f80cf95ef5daa80ffb2e6d564d25abb8a3e84099bee9730c95082597a12028bbf73bfe66fca2df3cdf04eaadea8e9d74ec0a826f946bc8f002293a9983ea + languageName: node + linkType: hard + "@firebase/storage@npm:0.13.4": version: 0.13.4 resolution: "@firebase/storage@npm:0.13.4" @@ -4612,6 +5108,15 @@ __metadata: languageName: node linkType: hard +"@firebase/util@npm:1.10.0": + version: 1.10.0 + resolution: "@firebase/util@npm:1.10.0" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/eb161f1c6294ff097f3c40236820e9e6e29cd6582e5e1254148157143272493580535ee2cb9e7c6055d3909b3ef39d8b64086895b071c665827acb66742b63eb + languageName: node + linkType: hard + "@firebase/util@npm:1.10.2": version: 1.10.2 resolution: "@firebase/util@npm:1.10.2" @@ -4621,6 +5126,22 @@ __metadata: languageName: node linkType: hard +"@firebase/vertexai-preview@npm:0.0.4": + version: 0.0.4 + resolution: "@firebase/vertexai-preview@npm:0.0.4" + dependencies: + "@firebase/app-check-interop-types": "npm:0.3.2" + "@firebase/component": "npm:0.6.9" + "@firebase/logger": "npm:0.4.2" + "@firebase/util": "npm:1.10.0" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + "@firebase/app-types": 0.x + checksum: 10/8ec48d81f48aebdcc63b65d802c67bf36880f256e5c2f5f3b152dc91c8c0e924053fba2fac5218716612f8ee720b25d0822337a214f16f5b7e51ce0247dfc4e5 + languageName: node + linkType: hard + "@firebase/vertexai@npm:1.0.2": version: 1.0.2 resolution: "@firebase/vertexai@npm:1.0.2" @@ -4637,6 +5158,13 @@ __metadata: languageName: node linkType: hard +"@firebase/webchannel-wrapper@npm:1.0.1": + version: 1.0.1 + resolution: "@firebase/webchannel-wrapper@npm:1.0.1" + checksum: 10/22fc7e1e6dd36ab7c13f3a6c1ff51f4d405304424dc323cb146109e7a3ab3b592e2ddb29f53197ee5719a8448cdedb98d9e86a080f9365e389f8429b1c6555c2 + languageName: node + linkType: hard + "@firebase/webchannel-wrapper@npm:1.0.3": version: 1.0.3 resolution: "@firebase/webchannel-wrapper@npm:1.0.3" @@ -6441,6 +6969,15 @@ __metadata: languageName: node linkType: hard +"@notifee/react-native@npm:^9.1.8": + version: 9.1.8 + resolution: "@notifee/react-native@npm:9.1.8" + peerDependencies: + react-native: "*" + checksum: 10/7c5237fba99906d8da02146afbfe8ff6de9f4047eecd806dea889bfd76b1cfe7ad3c181881bf2729aa348b83e035f4d02cfc3ac8eb8c4ba3a4415b445b439c38 + languageName: node + linkType: hard + "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -6910,6 +7447,35 @@ __metadata: languageName: node linkType: hard +"@react-native-firebase/app@npm:^21.7.1": + version: 21.7.1 + resolution: "@react-native-firebase/app@npm:21.7.1" + dependencies: + firebase: "npm:10.13.2" + peerDependencies: + expo: ">=47.0.0" + react: "*" + react-native: "*" + peerDependenciesMeta: + expo: + optional: true + checksum: 10/33e35907cb564f6e4537eba963b202070c596c34a84027cccc343567d96f714f444cfa3e56a3f2c276c0aa9eef274a5b223f4308e70b8f0fc628db4362c24e3e + languageName: node + linkType: hard + +"@react-native-firebase/messaging@npm:^21.7.1": + version: 21.7.1 + resolution: "@react-native-firebase/messaging@npm:21.7.1" + peerDependencies: + "@react-native-firebase/app": 21.7.1 + expo: ">=47.0.0" + peerDependenciesMeta: + expo: + optional: true + checksum: 10/035a7e07fb2ff4d7f5b60eb7ba17c55b72f4c0438649085059e724e14bbf3d7a433701e7e22ea92450ca72c14b5b1d71ecdee2d49b6a5eae7cca4e3411b6138f + languageName: node + linkType: hard + "@react-native-menu/menu@npm:^1.1.6": version: 1.1.7 resolution: "@react-native-menu/menu@npm:1.1.7" @@ -7604,11 +8170,14 @@ __metadata: "@expo/config-plugins": "npm:^9.0.10" "@expo/vector-icons": "npm:^14.0.2" "@gorhom/bottom-sheet": "npm:^5.0.6" + "@notifee/react-native": "npm:^9.1.8" "@react-native-async-storage/async-storage": "npm:1.23.1" "@react-native-clipboard/clipboard": "npm:^1.15.0" "@react-native-community/blur": "npm:^4.4.1" "@react-native-community/datetimepicker": "npm:8.2.0" "@react-native-community/slider": "npm:4.5.5" + "@react-native-firebase/app": "npm:^21.7.1" + "@react-native-firebase/messaging": "npm:^21.7.1" "@react-native-menu/menu": "npm:^1.1.6" "@react-native/babel-preset": "npm:^0.76.2" "@react-navigation/material-top-tabs": "npm:^7.1.0" @@ -7654,6 +8223,7 @@ __metadata: ethers: "npm:^6.13.4" expo: "npm:~52.0.14" expo-blur: "npm:~14.0.1" + expo-build-properties: "npm:^0.13.2" expo-constants: "npm:~17.0.4" expo-dev-client: "npm:~5.0.5" expo-font: "npm:~13.0.3" @@ -19388,6 +19958,18 @@ __metadata: languageName: node linkType: hard +"expo-build-properties@npm:^0.13.2": + version: 0.13.2 + resolution: "expo-build-properties@npm:0.13.2" + dependencies: + ajv: "npm:^8.11.0" + semver: "npm:^7.6.0" + peerDependencies: + expo: "*" + checksum: 10/88a2b0f491112398314502cc51172d4695d62ed9998fa96d8630450a5451f5f9779e0737c997175502ab16a3eb0194efb1cac44be44d5bfa2025054e4e4ce1c7 + languageName: node + linkType: hard + "expo-constants@npm:~17.0.0, expo-constants@npm:~17.0.3": version: 17.0.3 resolution: "expo-constants@npm:17.0.3" @@ -20161,6 +20743,41 @@ __metadata: languageName: node linkType: hard +"firebase@npm:10.13.2": + version: 10.13.2 + resolution: "firebase@npm:10.13.2" + dependencies: + "@firebase/analytics": "npm:0.10.8" + "@firebase/analytics-compat": "npm:0.2.14" + "@firebase/app": "npm:0.10.11" + "@firebase/app-check": "npm:0.8.8" + "@firebase/app-check-compat": "npm:0.3.15" + "@firebase/app-compat": "npm:0.2.41" + "@firebase/app-types": "npm:0.9.2" + "@firebase/auth": "npm:1.7.9" + "@firebase/auth-compat": "npm:0.5.14" + "@firebase/database": "npm:1.0.8" + "@firebase/database-compat": "npm:1.0.8" + "@firebase/firestore": "npm:4.7.2" + "@firebase/firestore-compat": "npm:0.3.37" + "@firebase/functions": "npm:0.11.8" + "@firebase/functions-compat": "npm:0.3.14" + "@firebase/installations": "npm:0.6.9" + "@firebase/installations-compat": "npm:0.2.9" + "@firebase/messaging": "npm:0.12.11" + "@firebase/messaging-compat": "npm:0.2.11" + "@firebase/performance": "npm:0.6.9" + "@firebase/performance-compat": "npm:0.2.9" + "@firebase/remote-config": "npm:0.4.9" + "@firebase/remote-config-compat": "npm:0.2.9" + "@firebase/storage": "npm:0.13.2" + "@firebase/storage-compat": "npm:0.3.12" + "@firebase/util": "npm:1.10.0" + "@firebase/vertexai-preview": "npm:0.0.4" + checksum: 10/c91a047b34f3e2a0b0f563a4b9b4aca4887c0052f82819384acc482c1523c83c108d47eb8a96aa2adce94e07d0f9eeabbd7fd4d2b4fde1e2706fb90a6aea2db1 + languageName: node + linkType: hard + "firebase@npm:^11.1.0": version: 11.1.0 resolution: "firebase@npm:11.1.0" @@ -32684,6 +33301,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:6.19.7": + version: 6.19.7 + resolution: "undici@npm:6.19.7" + checksum: 10/77fb8b0377388f6dba8244b015841318d621031211b4f3c2273d809304b77ec44adeba4b89dfd6708bdc044190e18f068e5b231882ef15d057d4624e46f544e3 + languageName: node + linkType: hard + "undici@npm:^6.11.1, undici@npm:^6.18.2": version: 6.21.1 resolution: "undici@npm:6.21.1"