diff --git a/.gitignore b/.gitignore
index 46ec6948..a5704bb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,4 @@ scripts/doppler_variables.sh
env.json
.vercel
+.env*.local
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/patches/expo-router+3.4.8.patch b/patches/expo-router+3.4.8.patch
new file mode 100644
index 00000000..ad70234b
--- /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,10 @@ 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,
diff --git a/src/components/HelpersScreenComponents.tsx b/src/components/HelpersScreenComponents.tsx
new file mode 100644
index 00000000..9365141e
--- /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 (typeof children === 'undefined') {
+ 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/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/hooks/usePasswordValidation.tsx b/src/hooks/usePasswordValidation.tsx
index 08abe42f..c89a1c96 100644
--- a/src/hooks/usePasswordValidation.tsx
+++ b/src/hooks/usePasswordValidation.tsx
@@ -23,7 +23,7 @@ export const usePasswordValidation = () => {
!showValidationState && setShowValidationState(true)
setIsPasswordError(!!min8Chars || !!min1SpecialChar)
setPasswordErrors([min8Chars, min1SpecialChar])
- return !!min8Chars || !!min1SpecialChar ? 'Error' : false
+ return !!min8Chars || !!min1SpecialChar ? 'Error' : undefined
},
[showValidationState]
)
diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json
index 22faacba..a4ef8a1e 100644
--- a/src/i18n/translations/en.json
+++ b/src/i18n/translations/en.json
@@ -226,7 +226,7 @@
"do_not_have_an_account": "Don't have an account?",
"forgot_password": "Forgot password",
"sign_in_by_google": "Log in with Google",
- "sign_in": "Sign in",
+ "sign_in": "Log in",
"sign_up": "Sign up",
"welcome_back_enter_details": "Please enter your details",
"welcome_back": "Welcome back"
diff --git a/src/i18n/translations/pl.json b/src/i18n/translations/pl.json
index f77a8cc1..69ba91f3 100644
--- a/src/i18n/translations/pl.json
+++ b/src/i18n/translations/pl.json
@@ -231,7 +231,7 @@
},
"sign_up_screen": {
"already_have_an_account": "Masz już konto?",
- "get_started": "Rozpoczynamy",
+ "get_started": "Rozpocznij",
"log_in": "Zaloguj się",
"sign_up": "Zarejestruj się",
"start_free_trail": "Rozpocznij darmowy 30 dniowy okres próbny."
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}
}
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}
diff --git a/src/screens/ExamplesScreen.tsx b/src/screens/ExamplesScreen.tsx
index 8b493f66..caba6d66 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 goToUserSession = useCallback(() => push('/example/user-session'), [push])
const goToHomeStackDetails = useCallback(() => push('/home/details'), [push])
@@ -43,6 +47,10 @@ export const ExamplesScreen = () => {
+ {/* TODO: Add translations */}
+
diff --git a/src/screens/PushNotificationsHelpersScreen.tsx b/src/screens/PushNotificationsHelpersScreen.tsx
new file mode 100644
index 00000000..67af91b9
--- /dev/null
+++ b/src/screens/PushNotificationsHelpersScreen.tsx
@@ -0,0 +1,143 @@
+import { HelperRenderJson, HelperSection } from '@baca/components'
+import { ENV, isExpoGo } from '@baca/constants'
+import { useNotificationContext } from '@baca/contexts'
+import { Text, Button, ScrollView } from '@baca/design-system'
+import { useCallback, useEffect, useState, useTranslation } from '@baca/hooks'
+import { wait } from '@baca/utils'
+import * as Clipboard from 'expo-clipboard'
+import * as Notifications from 'expo-notifications'
+
+export const PushNotificationsHelpersScreen = (): JSX.Element => {
+ const { t } = useTranslation()
+ const { notification } = useNotificationContext()
+
+ 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 getListOfScheduledNotifications = useCallback(async () => {
+ const listOfScheduledNotifications = await Notifications.getAllScheduledNotificationsAsync()
+
+ setListOfScheduledNotifications(listOfScheduledNotifications)
+ }, [])
+
+ useEffect(() => {
+ getListOfScheduledNotifications()
+ }, [getListOfScheduledNotifications])
+
+ 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 getListOfScheduledNotifications()
+ }, [getListOfScheduledNotifications])
+
+ return (
+
+ {/* TODO: Add translations */}
+
+
+
+
+ {/* 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}
+ ) : (
+ // TODO: Add translations
+ 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)}
-
-
-
- Refresh token:
- Is refreshing token:{JSON.stringify(isRefreshing, null, 2)}
-
- {JSON.stringify(
- { ...token, expDate: new Date(token?.tokenExpires || 0).toLocaleString() },
- null,
- 2
- )}
-
-
-
+
+
+ Is fetching user data:
+ {isInitialLoading || isRefetching}
+ User data:
+ {data}
+
+
+
+
+ Is refreshing token:
+ {isRefreshing}
+ Refresh token:
+
+ {{ ...token, expDate: new Date(token?.tokenExpires || 0).toLocaleString() }}
+
+
+
)
}
diff --git a/src/screens/index.ts b/src/screens/index.ts
index dadb8984..8eff38bd 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'