Skip to content

Commit

Permalink
Merge pull request #10 from binarapps/feat/expo-notifications
Browse files Browse the repository at this point in the history
feat(expo-notifications): handle expo-notifications
  • Loading branch information
MateuszRostkowski authored Nov 24, 2022
2 parents 73d8de3 + 22e5433 commit aec56a9
Show file tree
Hide file tree
Showing 20 changed files with 461 additions and 28 deletions.
11 changes: 8 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// duplicated import of gesture handler is required for bottom sheet modal to work on android
/* eslint-disable import/no-duplicates */
// FIXME: see how why did you render works
// import './wdyr'
import 'react-native-gesture-handler'
import 'react-native-reanimated'
import '~i18n'
import * as Device from 'expo-device'

import { Navigation } from '~navigation'
import { Providers } from '~providers'
import { startMockedServer } from '~services'
import { enableAndroidBackgroundNotificationListener, startMockedServer } from '~services'

// FIXME: there is some issue with miragejs that causes console.log to not work
const DISABLE_CONSOLE_ENABLE_MOCKED_SERVER = false
Expand All @@ -23,6 +22,12 @@ if (DISABLE_CONSOLE_ENABLE_MOCKED_SERVER) {
// require('./ReactotronConfig')
// }

// Workaround for the notifications received in background on android
// src: https://github.com/expo/expo/issues/14078#issuecomment-1041294084
if (Device.isDevice) {
enableAndroidBackgroundNotificationListener()
}

const App = (): JSX.Element => {
return (
<Providers>
Expand Down
58 changes: 58 additions & 0 deletions NOTIFICATIONS_SETUP.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Expo notifications configuration guide

Expo notifications are already preconfigured in this template. However, you still have to provide some secrets and keys in order to use them across your applications that uses this template.

<b>Expo Go</b> doesn't require any additional configuration so you can check notifications by copying push token (from `Settings` screen) and test notifications (on RL device) on [expo.dev/notifications](http://expo.dev/notifications) tool.

## 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).
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.:

```json
{
"expo": {
...,
"owner": "@binarapps",
"slug": "expo-typescript-template",
...,
"extra": {
"experienceid": "@binarapps/expo-typescript-template"
}
}
}
```

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

### 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)

## 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).
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ expo init --template=@binarapps/expo-ts-template name_of_your_app
- animations with `reanimated` and `moti`
- `@gorhom/bottom-sheet`
- Reactotron
- expo-notifications (You can read how to configure them [here](/NOTIFICATIONS_SETUP.md))

See all the details in the documentation.

Expand Down
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ CI / CD
- theme - DONE
- ThemeProvider - DONE
- Dark / Light theme switch - DONE
- expo-notifications - DONE

## Nice articles:

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"expo-local-authentication": "~12.3.0",
"expo-localization": "~13.1.0",
"expo-network": "~4.3.0",
"expo-notifications": "~0.16.1",
"expo-screen-orientation": "~4.3.0",
"expo-secure-store": "~11.3.0",
"expo-splash-screen": "~0.16.2",
Expand Down
1 change: 1 addition & 0 deletions src/constants/asyncStorageKeys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const ASYNC_STORAGE_KEYS = {
PUSH_TOKEN: '@notification/push-token',
NAVIGATION_STATE: '@navigation/navigation-state',
USER_LANGUAGE: '@language/user-language',
COLOR_SCHEME: '@theme/colorScheme',
Expand Down
15 changes: 15 additions & 0 deletions src/contexts/NotificationContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PermissionStatus } from 'expo-modules-core'
import * as Notifications from 'expo-notifications'
import { Dispatch, SetStateAction } from 'react'

import createGenericContext from '~utils/createGenericContext'

export type NotificationContextType = {
permissionStatus?: PermissionStatus
setPermissionStatus: Dispatch<SetStateAction<PermissionStatus | undefined>>
notification?: Notifications.Notification
setNotification: Dispatch<SetStateAction<Notifications.Notification | undefined>>
}

export const [useNotificationContext, NotificationContextProvider] =
createGenericContext<NotificationContextType>('NotificationContext')
1 change: 1 addition & 0 deletions src/contexts/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './AuthContext'
export * from './NotificationContext'
export * from './ColorSchemeContext'
2 changes: 1 addition & 1 deletion src/hooks/forms/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './useSignInForm'
export * from './useSignUpForm'
export * from './useSignInForm'
4 changes: 2 additions & 2 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ export {
} from 'react-query'
export { useDebounce, useDebouncedCallback, useThrottledCallback } from 'use-debounce'

// Custom hooks implemented in app

export * from './forms'
export * from './navigation'

// Custom hooks implemented in app
export * from './useAppStateActive'
export * from './useAuth'
export * from './useBoolean'
export * from './useCachedResources'
export * from './useNotificationSetup'
export * from './useSecurePassword'
export * from './useTimestamp'
export * from './useToggle'
26 changes: 26 additions & 0 deletions src/hooks/useNotificationSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect } from 'react'
import { Alert } from 'react-native'

import { useNotificationContext } from '~contexts'
import { registerForPushNotificationsAsync } from '~services'

export const useNotificationSetup = () => {
const { notification, setNotification } = useNotificationContext()

useEffect(() => {
const initNotifications = async () => {
await registerForPushNotificationsAsync()
}
initNotifications()
}, [])

// CONFIG: Handle in app notification
useEffect(() => {
if (notification) {
Alert.alert('Notification', JSON.stringify(notification), [
{ text: 'Cancel', onPress: () => setNotification(undefined), style: 'cancel' },
{ text: 'Ok', onPress: () => setNotification(undefined) },
])
}
}, [notification, setNotification])
}
1 change: 1 addition & 0 deletions src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
}
},
"settings_screen": {
"copy_push_token": "Copy push token",
"current_theme": "Current theme: {{theme}}",
"sign_out": "Sign out!",
"selected": " - selected"
Expand Down
5 changes: 4 additions & 1 deletion src/navigation/RootNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FC } from 'react'

import { BottomTabNavigator } from './BottomTabNavigator'

import { useAuth, useTranslation } from '~hooks'
import { useAuth, useNotificationSetup, useTranslation } from '~hooks'
import {
ApplicationInfoScreen,
NotFoundScreen,
Expand All @@ -18,6 +18,9 @@ export const RootNavigator: FC = () => {
const { t } = useTranslation()
const { isSignedIn } = useAuth()

// CONFIG: Handle in app notification
useNotificationSetup()

return (
<Navigator>
{!isSignedIn ? (
Expand Down
55 changes: 55 additions & 0 deletions src/providers/NotificationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as Notifications from 'expo-notifications'
import { PropsWithChildren, FC, useEffect } from 'react'

import { NotificationContextProvider, NotificationContextType } from '~contexts'
import { useState, useMemo } from '~hooks'
import {
disableAndroidBackgroundNotificationListener,
getNotificationFromStack,
getNotificationStackLength,
} from '~services'

export const NotificationProvider: FC<PropsWithChildren> = ({ children }) => {
const [permissionStatus, setPermissionStatus] =
useState<NotificationContextType['permissionStatus']>()
const [notification, setNotification] = useState<NotificationContextType['notification']>()

useEffect(() => {
const getPermissionStatus = async () => {
const { status } = await Notifications.getPermissionsAsync()
setPermissionStatus(status)
}
getPermissionStatus()
}, [])

useEffect(() => {
while (getNotificationStackLength() > 0) {
const androidBackgroundNotification = getNotificationFromStack()
if (androidBackgroundNotification) {
setNotification(androidBackgroundNotification)
}
}
disableAndroidBackgroundNotificationListener()

const notificationResponseReceived = Notifications.addNotificationResponseReceivedListener(
({ notification }) => {
setNotification(notification)
}
)

const notificationReceived = Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification)
})

return () => {
Notifications.removeNotificationSubscription(notificationResponseReceived)
Notifications.removeNotificationSubscription(notificationReceived)
}
}, [])

const value = useMemo(
() => ({ permissionStatus, setPermissionStatus, notification, setNotification }),
[notification, permissionStatus]
)
return <NotificationContextProvider value={value}>{children}</NotificationContextProvider>
}
35 changes: 19 additions & 16 deletions src/providers/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { QueryClientProvider, QueryClient } from 'react-query'
import { AuthProvider } from './AuthProvider'
import { ColorSchemeProvider } from './ColorSchemeProvider'
import { NotificationsProvider } from './NotificatedProvider'
import { NotificationProvider as ExpoNotificationsProvider } from './NotificationProvider'

import { AppLoading, WebWrapper } from '~components'
import { theme, nativeBaseConfig } from '~constants'
Expand All @@ -22,22 +23,24 @@ export const Providers = ({ children }: { children: ReactNode }): JSX.Element =>
return (
// NativeBaseProvider includes SafeAreaProvider so that we don't have to include it in a root render tree
<NativeBaseProvider theme={theme} config={nativeBaseConfig}>
{/* @ts-expect-error: error comes from a react-native-notificated library which doesn't have declared children in types required in react 18 */}
<NotificationsProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<AppLoading>
<ColorSchemeProvider>
<GestureHandlerRootView style={styles.gestureHandlerRootView}>
<BottomSheetModalProvider>
<WebWrapper>{children}</WebWrapper>
</BottomSheetModalProvider>
</GestureHandlerRootView>
</ColorSchemeProvider>
</AppLoading>
</AuthProvider>
</QueryClientProvider>
</NotificationsProvider>
<ExpoNotificationsProvider>
{/* @ts-expect-error: error comes from a react-native-notificated library which doesn't have declared children in types required in react 18 */}
<NotificationsProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<AppLoading>
<ColorSchemeProvider>
<GestureHandlerRootView style={styles.gestureHandlerRootView}>
<BottomSheetModalProvider>
<WebWrapper>{children}</WebWrapper>
</BottomSheetModalProvider>
</GestureHandlerRootView>
</ColorSchemeProvider>
</AppLoading>
</AuthProvider>
</QueryClientProvider>
</NotificationsProvider>
</ExpoNotificationsProvider>
</NativeBaseProvider>
)
}
Expand Down
1 change: 1 addition & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { NativeBaseProvider } from 'native-base'
export { SafeAreaProvider } from 'react-native-safe-area-context'
export * from './AuthProvider'
export * from './NotificatedProvider'
export * from './NotificationProvider'
export * from './Providers'
45 changes: 42 additions & 3 deletions src/screens/SettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
import * as Clipboard from 'expo-clipboard'
import Constants from 'expo-constants'
import * as Notifications from 'expo-notifications'
import { ScrollView, Text, Button, Center } from 'native-base'

import { Version, Spacer } from '~components'
import { colorSchemesList } from '~constants'
import { colorSchemesList, isExpoGo } from '~constants'
import { useColorScheme } from '~contexts'
import { useAuth, useTranslation } from '~hooks'
import { useAuth, useCallback, useTranslation } from '~hooks'
import { noop } from '~utils'

const experienceId = Constants.expoConfig?.extra?.experienceId

export const SettingsScreen = (): JSX.Element => {
const { t } = useTranslation()
const { setColorSchemeSetting, colorSchemeSetting } = useColorScheme()
const { signOut } = useAuth()

const handleCopyPushToken = useCallback(async () => {
try {
if (!isExpoGo && !experienceId) {
throw new Error(
'You must provide `experienceId` in app.json `extra` section in order to use notifications without Expo Go.'
)
}
const token = (
await Notifications.getExpoPushTokenAsync(
!isExpoGo
? {
experienceId,
}
: {}
)
).data

await Clipboard.setStringAsync(token)
alert('Copied push token to clipboard.')
} catch (error) {
console.log('error', error)
}
}, [])

const handleColorSchemeSettingChange = useCallback(
(scheme: typeof colorSchemeSetting) => () => {
setColorSchemeSetting(scheme)
},
[setColorSchemeSetting]
)

return (
<ScrollView>
<Center flex={1}>
<Button size="lg" width="64" my={2} onPress={handleCopyPushToken}>
{t('settings_screen.copy_push_token')}
</Button>
<Text fontSize="2xl" bold mb={2}>
{t('settings_screen.current_theme', { theme: colorSchemeSetting })}
</Text>
Expand All @@ -26,7 +65,7 @@ export const SettingsScreen = (): JSX.Element => {
width="64"
key={scheme}
mb={2}
onPress={() => setColorSchemeSetting(scheme)}
onPress={handleColorSchemeSettingChange(scheme)}
>
{scheme}
{isSelected ? ' - selected' : ''}
Expand Down
Loading

0 comments on commit aec56a9

Please sign in to comment.