From 0b9bbd15ec72facd955ec1343aed2d8795f76ede Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Sat, 30 Mar 2024 12:42:45 +0100 Subject: [PATCH 1/9] chore: remove push helpers from application info screen --- src/screens/ApplicationInfoScreen.tsx | 54 +-------------------------- 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/src/screens/ApplicationInfoScreen.tsx b/src/screens/ApplicationInfoScreen.tsx index ffb81e77..6499e7ae 100644 --- a/src/screens/ApplicationInfoScreen.tsx +++ b/src/screens/ApplicationInfoScreen.tsx @@ -1,16 +1,7 @@ -import { ENV, isExpoGo } from '@baca/constants' import { Box, Button, Text } from '@baca/design-system' -import { - useCallback, - usePreventGoBack, - useSafeAreaInsets, - useScreenOptions, - useTranslation, -} from '@baca/hooks' +import { usePreventGoBack, useSafeAreaInsets, useScreenOptions, useTranslation } from '@baca/hooks' // TODO: there are tons of more interesting methods there! import * as Application from 'expo-application' -import * as Clipboard from 'expo-clipboard' -import * as Notifications from 'expo-notifications' import { useRouter } from 'expo-router' import { ScrollView, StyleSheet } from 'react-native' @@ -25,51 +16,8 @@ export const ApplicationInfoScreen = (): JSX.Element => { usePreventGoBack() - const checkNotificationPermissionStatus = useCallback(async () => { - const permissions = await Notifications.getPermissionsAsync() - - alert('Permission status' + JSON.stringify(permissions, null, 2)) - }, []) - - const handleCopyPushToken = useCallback(async () => { - try { - if (!isExpoGo && !ENV.EAS_PROJECT_ID) { - throw new Error( - 'You must set `projectId` in eas build then value will be available from Constants?.expoConfig?.extra?.eas?.projectId' - ) - } - const token = ( - await Notifications.getExpoPushTokenAsync( - !isExpoGo - ? { - projectId: ENV.EAS_PROJECT_ID, - } - : {} - ) - ).data - - console.log(token) - await Clipboard.setStringAsync(token) - alert('Copied push token to clipboard.') - } catch (error) { - console.log('error', error) - alert( - JSON.stringify({ - message: 'There was an error when copying push token', - error, - }) - ) - } - }, []) - return ( - - {t('application_info_screen.navigation_info')} {Application.applicationId} {Application.applicationName} From 25ed6940e77edc2f3b0b78ec84e9c6de32a55852 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Sat, 30 Mar 2024 12:44:11 +0100 Subject: [PATCH 2/9] chore: add push notifications helpers screen --- .../example/push-notifications-helpers.tsx | 3 + .../PushNotificationsHelpersScreen.tsx | 149 ++++++++++++++++++ src/screens/index.ts | 1 + 3 files changed, 153 insertions(+) create mode 100644 app/(app)/(authorized)/(tabs)/example/push-notifications-helpers.tsx create mode 100644 src/screens/PushNotificationsHelpersScreen.tsx diff --git a/app/(app)/(authorized)/(tabs)/example/push-notifications-helpers.tsx b/app/(app)/(authorized)/(tabs)/example/push-notifications-helpers.tsx new file mode 100644 index 00000000..8673a537 --- /dev/null +++ b/app/(app)/(authorized)/(tabs)/example/push-notifications-helpers.tsx @@ -0,0 +1,3 @@ +import { PushNotificationsHelpersScreen } from '@baca/screens' + +export default PushNotificationsHelpersScreen diff --git a/src/screens/PushNotificationsHelpersScreen.tsx b/src/screens/PushNotificationsHelpersScreen.tsx new file mode 100644 index 00000000..09950ad7 --- /dev/null +++ b/src/screens/PushNotificationsHelpersScreen.tsx @@ -0,0 +1,149 @@ +import { ENV, isExpoGo } from '@baca/constants' +import { useNotificationContext } from '@baca/contexts' +import { Box, Text, Button, ScrollView, Spacer } from '@baca/design-system' +import { useCallback, useEffect, useScreenOptions, useState, useTranslation } from '@baca/hooks' +import { wait } from '@baca/utils' +import * as Clipboard from 'expo-clipboard' +import * as Notifications from 'expo-notifications' + +const Section = ({ header = '', children }: { header: string; children: React.ReactNode }) => { + return ( + + {header} + + {children} + + ) +} + +export const PushNotificationsHelpersScreen = (): JSX.Element => { + const { t } = useTranslation() + const { notification } = useNotificationContext() + useScreenOptions({ + title: 'push notifications', + }) + + const [notificationPermissionStatus, setNotificationPermissionStatus] = + useState() + + const [listOfscheduledNotifications, setListOfScheduledNotifications] = useState< + Notifications.NotificationRequest[] + >([]) + + const checkNotificationPermissionStatus = useCallback(async () => { + const permissions = await Notifications.getPermissionsAsync() + + setNotificationPermissionStatus(permissions) + }, []) + + useEffect(() => { + checkNotificationPermissionStatus() + }, [checkNotificationPermissionStatus]) + + const getListOfScheduledNotificaitons = useCallback(async () => { + const listOfScheduledNotifications = await Notifications.getAllScheduledNotificationsAsync() + + setListOfScheduledNotifications(listOfScheduledNotifications) + }, []) + + useEffect(() => { + getListOfScheduledNotificaitons() + }, [getListOfScheduledNotificaitons]) + + const handleCopyPushToken = useCallback(async () => { + try { + if (!isExpoGo && !ENV.EAS_PROJECT_ID) { + throw new Error( + 'You must set `projectId` in eas build then value will be available from Constants?.expoConfig?.extra?.eas?.projectId' + ) + } + const token = ( + await Notifications.getExpoPushTokenAsync( + !isExpoGo + ? { + projectId: ENV.EAS_PROJECT_ID, + } + : {} + ) + ).data + + console.log(token) + await Clipboard.setStringAsync(token) + alert('Copied push token to clipboard.') + } catch (error) { + console.log('error', error) + alert( + JSON.stringify({ + message: 'There was an error when copying push token', + error, + }) + ) + } + }, []) + + const scheduleNotification = useCallback(async () => { + const content = { + body: 'PUSH BODY', + title: 'PUSH TITLE', + data: { deeplink: '/example/push-notifications-helpers' }, + } + const trigger10Seconds = new Date(Date.now() + 1000 * 10) + + await Notifications.scheduleNotificationAsync({ + content, + trigger: trigger10Seconds, + }) + + await wait(200) + await getListOfScheduledNotificaitons() + }, [getListOfScheduledNotificaitons]) + + return ( + +
+ +
+ +
+ + {notificationPermissionStatus && ( + <> + + Notification permission status + {JSON.stringify(notificationPermissionStatus, null, 2)} + + )} +
+ +
+ + {notification ? JSON.stringify(notification, null, 2) : "There wasn't any notification"} + + {/* When there is no notification we would like to also display if notification is null or undefined */} + {!notification ? typeof notification : undefined} +
+ +
+ + + + + + List of scheduled notifications + {listOfscheduledNotifications.length ? ( + {JSON.stringify(listOfscheduledNotifications, null, 2)} + ) : ( + No scheduled notifications + )} +
+
+ ) +} diff --git a/src/screens/index.ts b/src/screens/index.ts index c15d255e..b73f5f96 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -10,6 +10,7 @@ export * from './ExamplesScreen' export * from './HomeScreen' export * from './NotFoundScreen' export * from './ProfileScreen' +export * from './PushNotificationsHelpersScreen' export * from './SettingsScreen' export * from './TestFormScreen' export * from './TypographyScreen' From 45184b03277dde36b8494a224ae80d81058eaa3c Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Sun, 31 Mar 2024 09:21:04 +0200 Subject: [PATCH 3/9] chore: add possibility to navigate to push notifications helpers screen --- src/screens/ExamplesScreen.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/screens/ExamplesScreen.tsx b/src/screens/ExamplesScreen.tsx index 1434b48c..8b591b8f 100644 --- a/src/screens/ExamplesScreen.tsx +++ b/src/screens/ExamplesScreen.tsx @@ -16,6 +16,10 @@ export const ExamplesScreen = () => { const goToTypography = useCallback(() => push('/example/typography'), [push]) const goToCityListScreen_EXAMPLE = useCallback(() => push('/example/data-from-be'), [push]) const goToTestForm = useCallback(() => push('/example/test-form'), [push]) + const goToPushNotificationsHelpers = useCallback( + () => push('/example/push-notifications-helpers'), + [push] + ) const goToHomeStackDetails = useCallback(() => push('/home/details'), [push]) @@ -42,6 +46,10 @@ export const ExamplesScreen = () => { + {/* TODO: Add translations */} +
) } From 97937444aaff25ae47cf55fccb1af67ee3d3ab33 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Mon, 1 Apr 2024 18:13:22 +0200 Subject: [PATCH 4/9] chore: add expo router patch that fixes initial route --- patches/expo-router+3.4.8.patch | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 patches/expo-router+3.4.8.patch diff --git a/patches/expo-router+3.4.8.patch b/patches/expo-router+3.4.8.patch new file mode 100644 index 00000000..cdf590a4 --- /dev/null +++ b/patches/expo-router+3.4.8.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/expo-router/build/global-state/routing.js b/node_modules/expo-router/build/global-state/routing.js +index 2fba9d1..9cce0c9 100644 +--- a/node_modules/expo-router/build/global-state/routing.js ++++ b/node_modules/expo-router/build/global-state/routing.js +@@ -173,6 +173,9 @@ function getNavigateAction(state, parentState, type = 'NAVIGATE') { + else if (type === 'REPLACE' && parentState.type === 'tab') { + type = 'JUMP_TO'; + } ++ ++ // https://github.com/expo/expo/issues/26211 ++ params.initial = false ++ + return { + type, + target: parentState.key, From 46b3d4e935dab8ce1a5e386db17147c329bdbd2b Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Tue, 2 Apr 2024 09:43:07 +0200 Subject: [PATCH 5/9] fix: properly setup notifications listeners --- src/contexts/NotificationContext.ts | 11 ++-- src/providers/NotificationProvider.tsx | 91 +++++++++++++++----------- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/src/contexts/NotificationContext.ts b/src/contexts/NotificationContext.ts index aca04d89..1a11ae6b 100644 --- a/src/contexts/NotificationContext.ts +++ b/src/contexts/NotificationContext.ts @@ -3,13 +3,16 @@ import { PermissionStatus } from 'expo-modules-core' import * as Notifications from 'expo-notifications' import { Dispatch, SetStateAction } from 'react' +export type ReceivedNotification = + | (Notifications.Notification & { context: { [key: string]: string } }) + | null + | undefined + export type NotificationContextType = { permissionStatus?: PermissionStatus setPermissionStatus: Dispatch> - notification?: Notifications.Notification - setNotification: Dispatch> - inAppNotification?: Notifications.Notification - setInAppNotification: Dispatch> + notification: ReceivedNotification + setNotification: Dispatch> } export const [useNotificationContext, NotificationContextProvider] = diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx index 7c7c8e89..95c152ff 100644 --- a/src/providers/NotificationProvider.tsx +++ b/src/providers/NotificationProvider.tsx @@ -1,17 +1,16 @@ import { ASYNC_STORAGE_KEYS } from '@baca/constants' -import { NotificationContextProvider, NotificationContextType } from '@baca/contexts' -import { useState, useMemo, useEffect, useAppStateActive } from '@baca/hooks' import { - assignPushToken, - disableAndroidBackgroundNotificationListener, - getNotificationFromStack, - getNotificationStackLength, -} from '@baca/services' + NotificationContextProvider, + NotificationContextType, + ReceivedNotification, +} from '@baca/contexts' +import { useState, useMemo, useEffect, useAppStateActive } from '@baca/hooks' +import { assignPushToken } from '@baca/services' import { store } from '@baca/store' import { isSignedInAtom } from '@baca/store/auth' import AsyncStorage from '@react-native-async-storage/async-storage' import * as Notifications from 'expo-notifications' -import { router } from 'expo-router' +import { useRootNavigationState, router } from 'expo-router' import { PropsWithChildren, FC, useCallback } from 'react' import { Alert, AlertButton } from 'react-native' @@ -28,17 +27,37 @@ const deeplinkWhenNotificationReceived = async ( // Alternatively we can prevent navigating to this routes when user is not logged in if (deeplinkPath) { - router.push(deeplinkPath) + router.navigate(deeplinkPath) } } export const NotificationProvider: FC = ({ children }) => { + // ------------------------------------------------------------- + // ----------------------- HOOKS ------------------------------- + // ------------------------------------------------------------- const [permissionStatus, setPermissionStatus] = useState() - const [notification, setNotification] = useState() - const [inAppNotification, setInAppNotification] = - useState() + const [notification, setNotification] = useState(undefined) + const backgroundNotification = Notifications.useLastNotificationResponse() + const rootNavigationState = useRootNavigationState() + + // ------------------------------------------------------------- + // ------ Navigating to screen after opening notification ------ + // ------------------------------------------------------------- + + // When initializing push notifications logic navigation is not ready yet + // We need to wait for navigation to set up and that's why there is `rootNavigationState.key` listener + // Ideally this should be added as hook to layout file as described in this tutorial: + // - https://docs.expo.dev/versions/latest/sdk/notifications/#handle-push-notifications-with-navigation + useEffect(() => { + if (notification && rootNavigationState.key) { + deeplinkWhenNotificationReceived(notification) + } + }, [rootNavigationState.key, notification]) + // ------------------------------------------------------------- + // --------------- Sending push token to backend --------------- + // ------------------------------------------------------------- const tryToRegisterPushToken = useCallback(async () => { const wasPushTokenSendStringified = await AsyncStorage.getItem( ASYNC_STORAGE_KEYS.WAS_PUSH_TOKEN_SEND @@ -69,29 +88,23 @@ export const NotificationProvider: FC = ({ children }) => { // To update immediately permission status useAppStateActive(tryToRegisterPushToken, true) - // ---------------------------------------------- - // fix notifications on android when app is killed - // ---------------------------------------------- + // ------------------------------------------------------------- + // Listener for notifications when app is killed and in background + // ------------------------------------------------------------- useEffect(() => { - while (getNotificationStackLength() > 0) { - const androidBackgroundNotification = getNotificationFromStack() - if (androidBackgroundNotification) { - setNotification(androidBackgroundNotification) - deeplinkWhenNotificationReceived(androidBackgroundNotification) - } + if (backgroundNotification) { + setNotification({ + ...backgroundNotification?.notification, + context: { + source: 'useLastNotificationResponse', + }, + }) + } else { + setNotification(undefined) } - disableAndroidBackgroundNotificationListener() - - // ------------------------------------------------------------- - // Listener for notifications when app is killed and in background - // ------------------------------------------------------------- - const notificationResponseReceived = Notifications.addNotificationResponseReceivedListener( - ({ notification }) => { - setNotification(notification) - deeplinkWhenNotificationReceived(notification) - } - ) + }, [backgroundNotification]) + useEffect(() => { // -------------------------------------------------- // listener for notifications when app is in background // -------------------------------------------------- @@ -105,7 +118,14 @@ export const NotificationProvider: FC = ({ children }) => { { text: 'Ok', style: 'default', - onPress: () => deeplinkWhenNotificationReceived(notification), + onPress: () => { + setNotification({ + ...notification, + context: { + source: 'addNotificationReceivedListener', + }, + }) + }, }, ] @@ -119,7 +139,6 @@ export const NotificationProvider: FC = ({ children }) => { }) return () => { - Notifications.removeNotificationSubscription(notificationResponseReceived) Notifications.removeNotificationSubscription(notificationReceived) } }, []) @@ -130,10 +149,8 @@ export const NotificationProvider: FC = ({ children }) => { setPermissionStatus, notification, setNotification, - inAppNotification, - setInAppNotification, }), - [inAppNotification, notification, permissionStatus] + [notification, permissionStatus] ) return {children} } From 024f9a189a9db864436679c6ce51864fba993434 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Tue, 2 Apr 2024 10:24:24 +0200 Subject: [PATCH 6/9] chore: update expo router patch --- patches/expo-router+3.4.8.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/expo-router+3.4.8.patch b/patches/expo-router+3.4.8.patch index cdf590a4..ad70234b 100644 --- a/patches/expo-router+3.4.8.patch +++ b/patches/expo-router+3.4.8.patch @@ -2,7 +2,7 @@ diff --git a/node_modules/expo-router/build/global-state/routing.js b/node_modul index 2fba9d1..9cce0c9 100644 --- a/node_modules/expo-router/build/global-state/routing.js +++ b/node_modules/expo-router/build/global-state/routing.js -@@ -173,6 +173,9 @@ function getNavigateAction(state, parentState, type = 'NAVIGATE') { +@@ -173,6 +173,10 @@ function getNavigateAction(state, parentState, type = 'NAVIGATE') { else if (type === 'REPLACE' && parentState.type === 'tab') { type = 'JUMP_TO'; } From 1cebb1f21391909a54e7042d1c338fb657004233 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Thu, 4 Apr 2024 10:29:23 +0200 Subject: [PATCH 7/9] chore: improve testing screens --- src/components/HelpersScreenComponents.tsx | 29 +++++++++++ src/components/index.ts | 1 + .../PushNotificationsHelpersScreen.tsx | 51 +++++++------------ src/screens/UserSessionScreen.tsx | 43 ++++++++-------- 4 files changed, 67 insertions(+), 57 deletions(-) create mode 100644 src/components/HelpersScreenComponents.tsx diff --git a/src/components/HelpersScreenComponents.tsx b/src/components/HelpersScreenComponents.tsx new file mode 100644 index 00000000..f1ae4483 --- /dev/null +++ b/src/components/HelpersScreenComponents.tsx @@ -0,0 +1,29 @@ +import { Box, Spacer, Text } from '@baca/design-system' + +export const HelperSection = ({ + header = '', + children, +}: { + header: string + children: React.ReactNode +}) => { + return ( + + {header} + + {children} + + ) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const HelperRenderJson = ({ children }: { children: any }) => { + if (!children) { + return null + } + return ( + + {JSON.stringify(children, null, 4)} + + ) +} diff --git a/src/components/index.ts b/src/components/index.ts index 129263d8..d211f686 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,7 @@ export * from './wrappers' export * from './AppLoading' export * from './CompanyLogo' export * from './FeaturedIcon' +export * from './HelpersScreenComponents' export * from './KeyboardAwareScrollView' export * from './LandingHeader' export * from './LanguagePicker' diff --git a/src/screens/PushNotificationsHelpersScreen.tsx b/src/screens/PushNotificationsHelpersScreen.tsx index 09950ad7..ab908996 100644 --- a/src/screens/PushNotificationsHelpersScreen.tsx +++ b/src/screens/PushNotificationsHelpersScreen.tsx @@ -1,21 +1,12 @@ +import { HelperRenderJson, HelperSection } from '@baca/components' import { ENV, isExpoGo } from '@baca/constants' import { useNotificationContext } from '@baca/contexts' -import { Box, Text, Button, ScrollView, Spacer } from '@baca/design-system' +import { Text, Button, ScrollView } from '@baca/design-system' import { useCallback, useEffect, useScreenOptions, useState, useTranslation } from '@baca/hooks' import { wait } from '@baca/utils' import * as Clipboard from 'expo-clipboard' import * as Notifications from 'expo-notifications' -const Section = ({ header = '', children }: { header: string; children: React.ReactNode }) => { - return ( - - {header} - - {children} - - ) -} - export const PushNotificationsHelpersScreen = (): JSX.Element => { const { t } = useTranslation() const { notification } = useNotificationContext() @@ -100,50 +91,42 @@ export const PushNotificationsHelpersScreen = (): JSX.Element => { return ( -
- -
+ + + -
- {notificationPermissionStatus && ( <> - Notification permission status - {JSON.stringify(notificationPermissionStatus, null, 2)} + {notificationPermissionStatus} )} -
+ -
- - {notification ? JSON.stringify(notification, null, 2) : "There wasn't any notification"} - + + {notification} {/* When there is no notification we would like to also display if notification is null or undefined */} {!notification ? typeof notification : undefined} -
+ -
- + + - - List of scheduled notifications {listOfscheduledNotifications.length ? ( - {JSON.stringify(listOfscheduledNotifications, null, 2)} + {listOfscheduledNotifications} ) : ( No scheduled notifications )} -
+
) } diff --git a/src/screens/UserSessionScreen.tsx b/src/screens/UserSessionScreen.tsx index 72aec0a8..2dd8157c 100644 --- a/src/screens/UserSessionScreen.tsx +++ b/src/screens/UserSessionScreen.tsx @@ -1,5 +1,6 @@ import { useAuthControllerMe } from '@baca/api/query/auth/auth' -import { Box, Button, ScrollView, Text } from '@baca/design-system' +import { HelperRenderJson, HelperSection } from '@baca/components' +import { Button, ScrollView, Text } from '@baca/design-system' import { Token, getToken } from '@baca/services' import { isRefreshingTokenAtom } from '@baca/store' import { wait } from '@baca/utils' @@ -34,28 +35,24 @@ export const UserSessionScreen = () => { }, [fetchToken]) return ( - - - User data: - - Is fetching user data: - {JSON.stringify(isInitialLoading || isRefetching, null, 10)} - - {JSON.stringify(data, null, 10)} - - + {/* TODO: Add translations */} + {/* TODO: Add translations */} {notificationPermissionStatus && ( <> + {/* TODO: Add translations */} Notification permission status {notificationPermissionStatus} )} + {/* TODO: Add translations */} {notification} {/* When there is no notification we would like to also display if notification is null or undefined */} {!notification ? typeof notification : undefined} + {/* TODO: Add translations */} + {/* TODO: Add translations */} - + + {/* TODO: Add translations */} + Count of scheduled notifications: {listOfScheduledNotifications.length} + + {/* TODO: Add translations */} List of scheduled notifications - {listOfscheduledNotifications.length ? ( - {listOfscheduledNotifications} + {listOfScheduledNotifications.length ? ( + {listOfScheduledNotifications} ) : ( + // TODO: Add translations No scheduled notifications )}