Skip to content

Commit

Permalink
feat: added expo-notifications and react-query
Browse files Browse the repository at this point in the history
  • Loading branch information
usotsukki committed Oct 20, 2024
1 parent ae51e2f commit 142d7e0
Show file tree
Hide file tree
Showing 23 changed files with 300 additions and 31 deletions.
3 changes: 2 additions & 1 deletion .env.public
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ EXPO_PUBLIC_LOG_DEBUG=false
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_SENTRY_DSN=https://6bf30b84d315b016b9529662b5f11182@o4505033974284288.ingest.us.sentry.io/4508070604374016
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=762459437378-v3tsf9qvlfmvftirgt3sllj5p1rpi1lp.apps.googleusercontent.com
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=333850807930-ij59g3k6p4pslnus2elrm5egkgju61e3.apps.googleusercontent.com
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=333850807930-ij59g3k6p4pslnus2elrm5egkgju61e3.apps.googleusercontent.com
EXPO_PUBLIC_EAS_PROJECT_ID=6afd57d1-5bac-48fc-b3f3-40a510289ae2
8 changes: 6 additions & 2 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConfigContext, ExpoConfig } from 'expo/config'
export default ({ config: initConfig }: ConfigContext): ExpoConfig => {
const appName = initConfig?.name || 'Skeleton'
const environment = process.env.EXPO_PUBLIC_NODE_ENV as 'production' | 'development' | 'testing'
const projectId = process.env.EXPO_PUBLIC_EAS_PROJECT_ID

const name = {
production: appName,
Expand All @@ -26,6 +27,9 @@ export default ({ config: initConfig }: ConfigContext): ExpoConfig => {
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY_IOS,
usesNonExemptEncryption: false,
},
infoPlist: {
UIBackgroundModes: ['remote-notification', 'processing'],
},
},
android: {
...initConfig.android,
Expand All @@ -36,14 +40,14 @@ export default ({ config: initConfig }: ConfigContext): ExpoConfig => {
},
},
updates: {
url: 'https://u.expo.dev/6afd57d1-5bac-48fc-b3f3-40a510289ae2',
url: `https://u.expo.dev/${projectId}`,
},
runtimeVersion: {
policy: 'appVersion',
},
extra: {
eas: {
projectId: '6afd57d1-5bac-48fc-b3f3-40a510289ae2',
projectId,
},
},
}
Expand Down
8 changes: 8 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
}
}
],
[
"expo-notifications",
{
"icon": "@assets/png/logo.png",
"color": "#ffffff",
"defaultChannel": "default"
}
],
[
"expo-dev-launcher",
{
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"@react-native-masked-view/masked-view": "^0.3.1",
"@sentry/react-native": "~5.24.3",
"@shopify/flash-list": "1.6.4",
"@tanstack/react-query": "^5.59.15",
"axios": "^1.7.7",
"expo": "~51.0.38",
"expo-apple-authentication": "~6.4.2",
"expo-application": "~5.9.1",
Expand All @@ -66,9 +68,11 @@
"expo-linking": "~6.3.1",
"expo-localization": "~15.0.3",
"expo-location": "~17.0.1",
"expo-notifications": "~0.28.18",
"expo-router": "~3.5.23",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
"expo-task-manager": "~11.8.2",
"expo-updates": "~0.25.27",
"i18next": "^23.15.1",
"immer": "^10.1.1",
Expand Down
18 changes: 18 additions & 0 deletions src/api/jokes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from 'axios'

const BASE_URL = 'https://icanhazdadjoke.com/'

interface JokeResponse {
id: string
joke: string
status: number
}

export const fetchRandomJoke = async (): Promise<JokeResponse> => {
const res = await axios.get(BASE_URL, {
headers: {
Accept: 'application/json',
},
})
return res.data
}
3 changes: 3 additions & 0 deletions src/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { Colors } from 'react-native-ui-lib'
import { ErrorFallback } from '@app/components'
import { ios } from '@app/env'
import { useNotifications } from '@app/hooks'
import { iconSizes } from '@app/theme/designSystem'

const TabLayout = () => {
Expand All @@ -20,6 +21,8 @@ const TabLayout = () => {
<ErrorFallback error={error} resetErrorBoundary={resetErrorBoundary} />
)

useNotifications()

return (
<ErrorBoundary fallbackRender={renderFallback} onReset={onReset}>
<Tabs
Expand Down
14 changes: 9 additions & 5 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import auth from '@react-native-firebase/auth'
import { GoogleSignin } from '@react-native-google-signin/google-signin'
import * as Sentry from '@sentry/react-native'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { isRunningInExpoGo } from 'expo'
import { Slot, useNavigationContainerRef, useRouter } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'
Expand Down Expand Up @@ -36,6 +37,7 @@ const RootLayout = () => {
const ref = useNavigationContainerRef()
const { fontsFinishedLoading } = useLoadFonts()
const router = useRouter()
const queryClient = new QueryClient()

useEffect(() => {
if (fontsFinishedLoading) {
Expand All @@ -60,11 +62,13 @@ const RootLayout = () => {
useStorageDevTools()

return (
<SafeAreaProvider>
<NetInfoToast />
<Toast />
<Slot />
</SafeAreaProvider>
<QueryClientProvider client={queryClient}>
<SafeAreaProvider>
<NetInfoToast />
<Toast />
<Slot />
</SafeAreaProvider>
</QueryClientProvider>
)
}

Expand Down
2 changes: 2 additions & 0 deletions src/env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const TRANSLATIONS_DEBUG = !IS_PROD && false
export const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN

export const GOOGLE_WEB_CLIENT_ID = process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID

export const EAS_PROJECT_ID = process.env.EXPO_PUBLIC_EAS_PROJECT_ID
2 changes: 2 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { default as useLoadFonts } from './useLoadFonts'
export { default as useConnectionStatus } from './useConnectionStatus'
export { default as useAuth } from './useAuth'
export { default as useAvoidKeyboard } from './useAvoidKeyboard'
export { default as useJokes } from './useJokes'
export { default as useNotifications } from './useNotifications'
11 changes: 8 additions & 3 deletions src/hooks/useConnectionStatus.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as NetInfo from '@react-native-community/netinfo'
import { useEffect, useState } from 'react'
import { onlineManager } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useStore } from '@app/store'
import { logger } from '@app/utils'

const useConnectionStatus = () => {
const [hasInternetConnection, setHasInternetConnection] = useState(true)
const hasInternetConnection = useStore(state => state.connection.hasInternetConnection)
const setConnectionStatus = useStore(state => state.setConnectionStatus)

const onChange: NetInfo.NetInfoChangeHandler = ({ isInternetReachable }) => {
if (isInternetReachable != null) {
Expand All @@ -15,7 +18,9 @@ const useConnectionStatus = () => {
}
return isInternetReachable
}
setHasInternetConnection(updateStateAction)
setConnectionStatus(updateStateAction(hasInternetConnection))

onlineManager.setOnline(isInternetReachable)
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/hooks/useJokes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query'
import { fetchRandomJoke } from '@app/api/jokes'

const useJokes = () => {
return useQuery({
queryKey: ['joke'],
queryFn: fetchRandomJoke,
select: data => data.joke,
refetchOnReconnect: 'always',
experimental_prefetchInRender: true,
})
}

export default useJokes
79 changes: 79 additions & 0 deletions src/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as Notifications from 'expo-notifications'
import { useEffect, useRef, useState } from 'react'
import { Alert } from 'react-native'
import { EAS_PROJECT_ID } from '@app/env'

Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
})

const registerForPushNotificationsAsync = async (): Promise<string | null> => {
try {
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus

if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
}

if (finalStatus !== 'granted') {
Alert.alert('Failed to get push token for push notification!')
return null
}

const projectId = EAS_PROJECT_ID
if (!projectId) {
throw new Error('Project ID not found')
}

const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data
return token
} catch (e) {
console.error('Error getting push token:', e)
return null
}
}

const useNotifications = () => {
const [expoPushToken, setExpoPushToken] = useState<string | null>(null)
const [notification, setNotification] = useState<Notifications.Notification | null>(null)
const notificationListener = useRef<Notifications.Subscription | null>(null)
const responseListener = useRef<Notifications.Subscription | null>(null)

const registerPushToken = async () => {
const token = await registerForPushNotificationsAsync()
if (token) {
setExpoPushToken(token)
}
}

useEffect(() => {
registerPushToken()

notificationListener.current = Notifications.addNotificationReceivedListener(n => setNotification(n))
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
console.log('User interacted with notification:', response)
})

return () => {
if (notificationListener.current) {
Notifications.removeNotificationSubscription(notificationListener.current)
}
if (responseListener.current) {
Notifications.removeNotificationSubscription(responseListener.current)
}
}
}, [])

return {
notification,
token: expoPushToken,
}
}

export default useNotifications
18 changes: 16 additions & 2 deletions src/screens/Demo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import { withProfiler } from '@sentry/react-native'
import { scheduleNotificationAsync } from 'expo-notifications'
import { Link } from 'expo-router'
import { useTranslation } from 'react-i18next'
import { Button, Colors, Text, View } from 'react-native-ui-lib'
import { useAuth } from '@app/hooks'
import { useAuth, useJokes } from '@app/hooks'

const Demo = () => {
const { t, i18n } = useTranslation('translation', { keyPrefix: 'modules.demoScreen' })
const { signOut } = useAuth()
const jokeQuery = useJokes()

const scheduleNotification = async () => {
await scheduleNotificationAsync({
content: {
title: t('jokeOfTheDay'),
body: jokeQuery.data,
},
trigger: null,
})
jokeQuery.refetch()
}

return (
<View flex center backgroundColor={Colors.grayBlack} testID="Demo">
<View flex center backgroundColor={Colors.grayBlack} testID="Demo" gap-s4>
<Link href="/(demo)/Map">
<Text h1 white>
{t('navigateToMap')}
</Text>
</Link>
<Button label={t('getRandomJoke')} onPress={scheduleNotification} />
<Button label={i18n.t('modules.auth.signOut')} onPress={signOut} testID="Demo.SignOutButton" />
</View>
)
Expand Down
5 changes: 2 additions & 3 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { create } from 'zustand'
import { withSlices } from 'zustand-slices'
import { persist } from 'zustand/middleware'
import authSlice from './authSlice'
import persistConfig from './persistConfig'
import toastSlice from './toastSlice'
import { authSlice, connectionSlice, toastSlice } from './slices'

export const useStore = create(persist(withSlices(authSlice, toastSlice), persistConfig))
export const useStore = create(persist(withSlices(authSlice, toastSlice, connectionSlice), persistConfig))
6 changes: 3 additions & 3 deletions src/store/persistConfig.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { createJSONStorage, PersistOptions, StateStorage } from 'zustand/middleware'
import { zustandStorage } from '@app/storage'
import { Store } from './types'
import { RootStore } from './slices'

const persistStorage: StateStorage = {
setItem: (name, value) => zustandStorage.set(name, value),
getItem: name => zustandStorage.getString(name) || null,
removeItem: name => zustandStorage.delete(name),
}

const persistConfig: PersistOptions<Store> = {
const persistConfig: PersistOptions<RootStore> = {
name: 'rootStore',
storage: createJSONStorage(() => persistStorage),
partialize: state =>
({
auth: {
user: state.auth.user,
},
}) as Store,
}) as RootStore,
}

export default persistConfig
File renamed without changes.
23 changes: 23 additions & 0 deletions src/store/slices/connectionSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createSliceWithImmer } from '@app/utils/store/createSliceWithImmer'

export interface ConnectionSliceState {
hasInternetConnection: boolean
}

export interface ConnectionSliceActions {
setConnectionStatus: (status: boolean) => void
}

const connectionSlice = createSliceWithImmer({
name: 'connection',
value: {
hasInternetConnection: true,
} as ConnectionSliceState,
actions: {
setConnectionStatus: (status: boolean) => state => {
state.hasInternetConnection = status
},
},
})

export default connectionSlice
11 changes: 11 additions & 0 deletions src/store/slices/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { default as authSlice, AuthSliceActions, AuthSliceState } from './authSlice'
import { default as connectionSlice, ConnectionSliceActions, ConnectionSliceState } from './connectionSlice'
import { default as toastSlice, ToastSliceActions, ToastSliceState } from './toastSlice'

interface RootStore extends AuthSliceActions, ToastSliceActions, ConnectionSliceActions {
auth: AuthSliceState
toast: ToastSliceState
connection: ConnectionSliceState
}

export { authSlice, connectionSlice, toastSlice, RootStore }
File renamed without changes.
Loading

0 comments on commit 142d7e0

Please sign in to comment.