1- import { DarkTheme , DefaultTheme , ThemeProvider } from '@react-navigation/native' ;
2- import { useFonts } from 'expo-font' ;
3- import { Stack } from 'expo-router' ;
4- import { StatusBar } from 'expo-status-bar' ;
5- import { AuthProvider } from '@/contexts/AuthContext' ;
6- import { ProtectedRoute } from '@/components/ProtectedRoute' ;
7- import 'react-native-reanimated' ;
8-
9- import { useColorScheme } from '@/hooks/useColorScheme' ;
10- import { UserProvider } from '@/contexts/UserContext' ;
11- import { WorkoutProvider } from '@/contexts/WorkoutContext' ;
1+ // app/_layout.tsx
2+
3+ import React , { useEffect , useRef , useState } from "react" ;
4+ import { Platform , Alert } from "react-native" ;
5+
6+ import { DarkTheme , DefaultTheme , ThemeProvider } from "@react-navigation/native" ;
7+ import { useFonts } from "expo-font" ;
8+ import { Stack } from "expo-router" ;
9+ import { StatusBar } from "expo-status-bar" ;
10+ import { AuthProvider } from "@/contexts/AuthContext" ;
11+ import { ProtectedRoute } from "@/components/ProtectedRoute" ;
12+ import "react-native-reanimated" ;
13+
14+ import { useColorScheme } from "@/hooks/useColorScheme" ;
15+ import { UserProvider } from "@/contexts/UserContext" ;
16+ import { WorkoutProvider } from "@/contexts/WorkoutContext" ;
17+
18+ // ─── IMPORTS FOR PUSH NOTIFICATIONS ─────────────────────────────────────────────
19+ import * as Device from "expo-device" ;
20+ import * as Notifications from "expo-notifications" ;
21+ // ────────────────────────────────────────────────────────────────────────────────
1222
1323export default function RootLayout ( ) {
1424 const colorScheme = useColorScheme ( ) ;
1525 const [ loaded ] = useFonts ( {
16- SpaceMono : require ( ' ../assets/fonts/SpaceMono-Regular.ttf' ) ,
26+ SpaceMono : require ( " ../assets/fonts/SpaceMono-Regular.ttf" ) ,
1727 } ) ;
1828
29+ // ─── STATE / REFS FOR PUSH REGISTRATION ────────────────────────────────────────
30+ const [ expoPushToken , setExpoPushToken ] = useState < string > ( "" ) ;
31+ const notificationListener = useRef < any > ( ) ;
32+ const responseListener = useRef < any > ( ) ;
33+
34+ // Replace this with your actual user‐ID retrieval logic (e.g. from AuthContext)
35+ const fakeUserId = "user123" ;
36+ // ───────────────────────────────────────────────────────────────────────────────
37+
38+ useEffect ( ( ) => {
39+ // 1) Configure how notifications are handled when the app is foregrounded:
40+ Notifications . setNotificationHandler ( {
41+ handleNotification : async ( ) : Promise < Notifications . NotificationBehavior > => ( {
42+ shouldShowAlert : true , // (still allowed, but deprecated)
43+ shouldShowBanner : true , // required on iOS 14+ to actually display in‐app banners
44+ shouldShowList : true , // required on iOS 14+ to show in Notification Center
45+ shouldPlaySound : true ,
46+ shouldSetBadge : false ,
47+ } ) ,
48+ } ) ;
49+
50+ // 2) Register for push notifications & get Expo Push Token
51+ ( async ( ) => {
52+ if ( ! Device . isDevice ) {
53+ Alert . alert ( "Push notifications require a physical device." ) ;
54+ return ;
55+ }
56+
57+ // 2a) Check existing permissions
58+ const { status : existingStatus } = await Notifications . getPermissionsAsync ( ) ;
59+ let finalStatus = existingStatus ;
60+ if ( existingStatus !== "granted" ) {
61+ const { status } = await Notifications . requestPermissionsAsync ( ) ;
62+ finalStatus = status ;
63+ }
64+ if ( finalStatus !== "granted" ) {
65+ Alert . alert ( "Failed to get push token for push notifications!" ) ;
66+ return ;
67+ }
68+
69+ // 2b) Get the Expo Push Token
70+ const tokenData = await Notifications . getExpoPushTokenAsync ( ) ;
71+ const token = tokenData . data ;
72+ console . log ( "Obtained Expo Push Token:" , token ) ;
73+ setExpoPushToken ( token ) ;
74+
75+ // 2c) Send that token to your FastAPI backend
76+ try {
77+ await fetch ( "http://localhost:8000/api/v1/notifications/register_push_token" , {
78+ method : "POST" ,
79+ headers : { "Content-Type" : "application/json" } ,
80+ body : JSON . stringify ( {
81+ user_id : fakeUserId ,
82+ expo_token : token ,
83+ } ) ,
84+ } ) ;
85+ } catch ( err ) {
86+ console . error ( "Error sending push token to backend:" , err ) ;
87+ }
88+
89+ // 2d) (Android only) Create a notification channel
90+ if ( Platform . OS === "android" ) {
91+ await Notifications . setNotificationChannelAsync ( "default" , {
92+ name : "default" ,
93+ importance : Notifications . AndroidImportance . MAX ,
94+ vibrationPattern : [ 0 , 250 , 250 , 250 ] ,
95+ lightColor : "#FF231F7C" ,
96+ } ) ;
97+ }
98+ } ) ( ) ;
99+
100+ // 3) Foreground‐notification listener
101+ notificationListener . current = Notifications . addNotificationReceivedListener (
102+ ( notification ) => {
103+ console . log ( "Notification Received (foreground):" , notification ) ;
104+ }
105+ ) ;
106+
107+ // 4) User‐tap listener (background or foreground)
108+ responseListener . current = Notifications . addNotificationResponseReceivedListener (
109+ ( response ) => {
110+ console . log ( "User tapped on notification:" , response ) ;
111+ // You can navigate or handle notification.data here
112+ }
113+ ) ;
114+
115+ return ( ) => {
116+ if ( notificationListener . current ) {
117+ Notifications . removeNotificationSubscription ( notificationListener . current ) ;
118+ }
119+ if ( responseListener . current ) {
120+ Notifications . removeNotificationSubscription ( responseListener . current ) ;
121+ }
122+ } ;
123+ } , [ ] ) ;
124+
19125 if ( ! loaded ) {
20126 return null ;
21127 }
@@ -24,7 +130,7 @@ export default function RootLayout() {
24130 < AuthProvider >
25131 < UserProvider >
26132 < WorkoutProvider >
27- < ThemeProvider value = { colorScheme === ' dark' ? DarkTheme : DefaultTheme } >
133+ < ThemeProvider value = { colorScheme === " dark" ? DarkTheme : DefaultTheme } >
28134 < ProtectedRoute >
29135 < Stack >
30136 < Stack . Screen name = "index" options = { { headerShown : false } } />
@@ -45,4 +151,3 @@ export default function RootLayout() {
45151 </ AuthProvider >
46152 ) ;
47153}
48-
0 commit comments