Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
60768a4
chore: update async storage keys
MateuszRostkowski Mar 18, 2024
9a98172
feat: create simple functions to handle push tokens
MateuszRostkowski Mar 18, 2024
1f7ecc2
feat: handle getting and setting push tokens
MateuszRostkowski Mar 18, 2024
1c19598
feat: remove push token when siging out
MateuszRostkowski Mar 18, 2024
f9b1aff
chore: remove not needed code
MateuszRostkowski Mar 18, 2024
208a899
chore: update notifications setup docs
MateuszRostkowski Mar 18, 2024
f7632db
chore: change TODO to FIXME
MateuszRostkowski Mar 20, 2024
223b67e
fix: remove not needed useRouterNotifications file
MateuszRostkowski Mar 20, 2024
01b58aa
chore: change FIXME to CONFIG
MateuszRostkowski Mar 20, 2024
4674368
fix: properly handle push notifications
MateuszRostkowski Mar 20, 2024
bee4b09
chore: add option to check push notifications status to application i…
MateuszRostkowski Mar 20, 2024
240aa93
chore: move EAS_PROJECT_ID to envs
MateuszRostkowski Mar 20, 2024
386e4f4
chore: update notifications settings docs
MateuszRostkowski Mar 20, 2024
21a08c8
chore: revert wrong commited code
MateuszRostkowski Mar 20, 2024
61a942a
fix typos
micbaumr Mar 20, 2024
367d804
bump expo packages
micbaumr Mar 20, 2024
02136e1
remove react-native types installed directly
micbaumr Mar 20, 2024
ebc20ca
remove deprecated expo-app-loading package
micbaumr Mar 20, 2024
1da994f
Merge pull request #33 from binarapps/chore/update-expo
MateuszRostkowski Mar 20, 2024
f4d6db8
chore: move remove push token to external function
MateuszRostkowski Mar 20, 2024
d3db126
chore: remove not needed code
MateuszRostkowski Mar 20, 2024
856e47d
chore: remove notification provider logic on web
MateuszRostkowski Mar 20, 2024
35c84eb
chore: change check notification permission status alert text
MateuszRostkowski Mar 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { StatusBar } from '@baca/components'
import { AbsoluteFullFill, Loader } from '@baca/design-system'
import { useNavigationTheme, useRouterNotifications } from '@baca/hooks'
import { useNavigationTheme } from '@baca/hooks'
import { Providers } from '@baca/providers'
import { registerForPushNotificationsAsync } from '@baca/services'
import { isSignedInAtom } from '@baca/store/auth'
import { ThemeProvider } from '@react-navigation/native'
import { Slot } from 'expo-router'
Expand All @@ -11,12 +12,12 @@ export const unstable_settings = {
initialRouteName: 'index',
}

registerForPushNotificationsAsync()

const Layout = () => {
const isSignedIn = useAtomValue(isSignedInAtom)
const { navigationTheme } = useNavigationTheme()

useRouterNotifications() // TODO: check if handling notification deeplinks works correctly

if (isSignedIn === null) {
return (
<AbsoluteFullFill w="full" h="full" justifyContent="center" alignItems="center">
Expand Down
55 changes: 36 additions & 19 deletions docs/docs/tutorials/NOTIFICATIONS_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,17 @@ Expo notifications are already preconfigured in this template. However, you stil
## Usage in expo dev client (expo run:\[android:ios\])

1. Make sure you have created your account in [expo.dev](http://expo.dev).
2. Sign in to your account using `yarn run login` (or `expo login` inside project directory).
2. Follow [bootstrap](/docs/bootstrap/intro) docs
3. Follow platform specific configuration.

### Android

1. Configure firebase to get `google-services.json` file - [follow this guide](https://docs.expo.dev/push-notifications/using-fcm/).
2. Make sure that you have changed your `owner` name in `app.json`.
3. Put your `google-services.json` in a project directory and provide path to it in `app.json` in `android` section ex.:

```json
{
"expo": {
...,
"owner": "@binarapps",
...,
"android": {
"googleServicesFile": "./path/to/google-services.json"
}
}
}
```

4. Provide your `experienceId` in `extra` section in `app.json` typically it follows this scheme - `@owner/slug` ex.:
3. Download `google-services.json` file
4. Encode this file to base64
5. Place base64 string in environment variable in this value: `ANDROID_FIREBASE_CONFIG`
6. Provide your `experienceId` in `extra` section in `app.json` typically it follows this scheme - `@owner/slug` ex.:

```json
{
Expand All @@ -61,12 +49,41 @@ Expo notifications are already preconfigured in this template. However, you stil

<b>Make sure that you have provided your own secrets for those fields.</b>

7. Get credentials

For Android, you need to configure Firebase Cloud Messaging (FCM) V1 to get credentials and set up your Expo project.

Follow the steps in [Add Android FCM V1 credentials](https://docs.expo.dev/push-notifications/fcm-credentials/) to set up your credentials.

### iOS

`iOS` notification credentials are automatically generated (paid apple developer account is required to make them working).

[You can check this guide how to setup push notifications on iOS.](https://docs.expo.dev/push-notifications/push-notifications-setup/#credentials)
### Test using the push notifications tool

[Check expo docs](https://docs.expo.dev/push-notifications/push-notifications-setup/#test-using-the-push-notifications-tool)

## Extending `expo-notifications` config

If u need additional `expo-notifications` config [follow this guide](https://github.com/expo/expo/tree/sdk-47/packages/expo-notifications).
If u need additional `expo-notifications` config [follow this guide](https://github.com/expo/expo/tree/main/packages/expo-notifications#config-plugin-setup-optional).

## Push notifications logic

When working with expo push notifications, probably you will not need to do any additional logic, but here are described key files that you can follow when some changes will be needed:

- `NotificationsProvider`
- assign push token when opening the app (thanks to that push token will be send every time user will change their push permissions outside the app)
- setup push listeners
- navigate to screens when push is pressed
- `NotificationService`
- Register push - ask for permissions
- Assign push token - send push token to backend

## Sending push token to backend

This starter comes with support to send expo push token to backend, you will just need to add your api calls in this two files (replace console.logs with api calls):

- `NotificationService`
- `console.log('SEND ME TO BACKEND', pushExpoToken)`
- `authActions`
- `console.log('REMOVE ME from BACKEND', pushTokenStorage)`
12 changes: 5 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,13 @@
"@react-navigation/stack": "^6.3.20",
"@tanstack/react-query": "^4.29.19",
"axios": "^1.6.7",
"expo": "~50.0.11",
"expo-app-loading": "^2.1.1",
"expo": "~50.0.13",
"expo-application": "~5.8.3",
"expo-asset": "~9.0.2",
"expo-clipboard": "~5.0.1",
"expo-constants": "~15.4.5",
"expo-crypto": "~12.8.1",
"expo-dev-client": "~3.3.9",
"expo-dev-client": "~3.3.11",
"expo-device": "~5.9.3",
"expo-font": "~11.10.3",
"expo-haptics": "~12.8.1",
Expand All @@ -127,7 +126,7 @@
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3",
"expo-updates": "~0.24.11",
"expo-updates": "~0.24.12",
"expo-web-browser": "~12.8.2",
"i18next": "^23.7.20",
"jotai": "^2.4.3",
Expand All @@ -136,7 +135,7 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.49.3",
"react-i18next": "^14.0.1",
"react-native": "0.73.4",
"react-native": "0.73.5",
"react-native-gesture-handler": "~2.14.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-notificated": "^0.0.1-beta.2",
Expand Down Expand Up @@ -165,7 +164,6 @@
"@testing-library/react-native": "^12.4.3",
"@types/jest": "^29.5.3",
"@types/react": "~18.2.45",
"@types/react-native": "^0.72.2",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
Expand All @@ -186,7 +184,7 @@
"fast-text-encoding": "^1.0.6",
"husky": "^8.0.3",
"jest": "^29.6.1",
"jest-expo": "~50.0.3",
"jest-expo": "~50.0.4",
"lint-staged": "^13.2.3",
"msw": "^2.2.2",
"orval": "^6.25.0",
Expand Down
10 changes: 5 additions & 5 deletions src/constants/asyncStorageKeys.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export const ASYNC_STORAGE_KEYS = {
PUSH_TOKEN: '@notification/push-token',
NEXT_DEEP_LINK: '@navigation/next_deeplink',
COLOR_SCHEME: '@theme/colorScheme',
NAVIGATION_STATE: '@navigation/navigation-state',
NEXT_DEEP_LINK: '@navigation/next_deeplink',
PUSH_TOKEN: '@notification/push-token',
USER_LANGUAGE: '@language/user-language',
COLOR_SCHEME: '@theme/colorScheme',
// This value is used in `expo-secure-store` package and it can't include '@' and '/'
USER_TOKEN: 'user_token',
USER_TOKEN: 'user_token', // This value is used in `expo-secure-store` package and it can't include '@' and '/'
WAS_PUSH_TOKEN_SEND: '@notification/was-push-token-send',
} as const
1 change: 1 addition & 0 deletions src/constants/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import Constants from 'expo-constants'
export const ENV = {
API_URL: Constants?.expoConfig?.extra?.API_URL,
ENVIRONMENT: Constants?.expoConfig?.extra?.ENVIRONMENT,
EAS_PROJECT_ID: 'ac562c27-4a4e-4532-869f-fe6f9447bee6', // FIXME: Move it to .env
}
5 changes: 4 additions & 1 deletion src/hooks/forms/useSignInForm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useAuthControllerLogin } from '@baca/api/query/auth/auth'
import { AuthEmailLoginDto } from '@baca/api/types'
import { setToken } from '@baca/services'
import { assignPushToken, setToken } from '@baca/services'
import { isSignedInAtom } from '@baca/store/auth'
import { hapticImpact } from '@baca/utils'
import { handleFormError } from '@baca/utils/handleFormErrors'
Expand Down Expand Up @@ -57,6 +57,9 @@ export const useSignInForm = () => {
onSuccess: async (response) => {
await setToken(response.accessToken)
setIsSignedIn(true)

// Send push token to backend
await assignPushToken()
},
}
)
Expand Down
1 change: 0 additions & 1 deletion src/hooks/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ export * from './usePreventGoBack'
export * from './useScreenOptions'
export * from './useScreenTracker'
export * from './useWeb'
export * from './useRouterNotifications'
43 changes: 0 additions & 43 deletions src/hooks/navigation/useRouterNotifications.ts

This file was deleted.

93 changes: 84 additions & 9 deletions src/providers/NotificationProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
import { ASYNC_STORAGE_KEYS } from '@baca/constants'
import { NotificationContextProvider, NotificationContextType } from '@baca/contexts'
import { useState, useMemo, useEffect } from '@baca/hooks'
import { useState, useMemo, useEffect, useAppStateActive } from '@baca/hooks'
import {
assignPushToken,
disableAndroidBackgroundNotificationListener,
getNotificationFromStack,
getNotificationStackLength,
} 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 { PropsWithChildren, FC } from 'react'
import { router } from 'expo-router'
import { PropsWithChildren, FC, useCallback } from 'react'
import { Alert, AlertButton } from 'react-native'

const deeplinkWhenNotificationReceived = async (
notification: Notifications.Notification,
deeplink?: string
) => {
const { data: payload } = notification?.request?.content || {}
const deeplinkPath: string | undefined = deeplink || (payload?.deeplink as string)

// FIXME: Authenticated routes not working when user is logged out
// It will not work properly when we will try to navigate to routes where user needs authentication
// We need to find some way to look for this routes, and later delay navigating to this routes when user will log in
// Alternatively we can prevent navigating to this routes when user is not logged in

if (deeplinkPath) {
router.push(deeplinkPath)
}
}

export const NotificationProvider: FC<PropsWithChildren> = ({ children }) => {
const [permissionStatus, setPermissionStatus] =
Expand All @@ -15,32 +39,83 @@ export const NotificationProvider: FC<PropsWithChildren> = ({ children }) => {
const [inAppNotification, setInAppNotification] =
useState<NotificationContextType['inAppNotification']>()

useEffect(() => {
const getPermissionStatus = async () => {
const { status } = await Notifications.getPermissionsAsync()
setPermissionStatus(status)
const tryToRegisterPushToken = useCallback(async () => {
const wasPushTokenSendStringified = await AsyncStorage.getItem(
ASYNC_STORAGE_KEYS.WAS_PUSH_TOKEN_SEND
)
const wasPushTokenSend: boolean = JSON.parse(wasPushTokenSendStringified ?? 'false')

if (wasPushTokenSend) {
return
}

const isSignedIn = store.get(isSignedInAtom)

if (!isSignedIn) {
return
}
getPermissionStatus()

// This function will also be executed after first installation of app
// It's used like that because we want to ask user for permissions
const status = await assignPushToken()

if (!status) {
return
}

setPermissionStatus(status)
}, [])

// To update immediately permission status
useAppStateActive(tryToRegisterPushToken, true)

// ----------------------------------------------
// fix notifications on android when app is killed
// ----------------------------------------------
useEffect(() => {
while (getNotificationStackLength() > 0) {
const androidBackgroundNotification = getNotificationFromStack()
if (androidBackgroundNotification) {
setNotification(androidBackgroundNotification)
deeplinkWhenNotificationReceived(androidBackgroundNotification)
}
}
disableAndroidBackgroundNotificationListener()

// -------------------------------------------------------------
// Listener for notifications when app is killed and in background
// -------------------------------------------------------------
const notificationResponseReceived = Notifications.addNotificationResponseReceivedListener(
({ notification }) => {
setNotification(notification)
deeplinkWhenNotificationReceived(notification)
}
)

// --------------------------------------------------
// listener for notifications when app is in background
// --------------------------------------------------
const notificationReceived = Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification)
setInAppNotification(notification)
// This notification will be received when user have opened app in current moment
// We need to display some UI component and this component should handle presses
const { title, body, data } = notification?.request?.content || {}

// TODO: Add translations here
const buttons: AlertButton[] = [
{
text: 'Ok',
style: 'default',
onPress: () => deeplinkWhenNotificationReceived(notification),
},
]

// TODO: Add translations here
if (data?.deeplink) {
buttons.unshift({ text: 'Anuluj', style: 'cancel', onPress: () => undefined })
}

// TODO: Add translations here
Alert.alert(title || 'Otrzymałeś powiadomienie', body || 'Przejdź dalej', buttons)
})

return () => {
Expand Down
5 changes: 5 additions & 0 deletions src/providers/NotificationProvider.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PropsWithChildren, FC } from 'react'

export const NotificationProvider: FC<PropsWithChildren> = ({ children }) => {
return children
}
Loading