From aef978c625d50748511e884b7ac150137f063dc5 Mon Sep 17 00:00:00 2001 From: digitalnomad91 Date: Thu, 11 Sep 2025 00:40:34 -0500 Subject: [PATCH 1/2] feat(session): add SessionProvider, unify FCM+auth flow; migrate background fetch to expo-background-task with guarded registration; add debug screen and logging improvements --- app.config.js | 7 +- app/(tabs)/index.tsx | 4 +- app/(tabs)/settings.tsx | 2 +- app/_layout.tsx | 55 ++- app/debug.tsx | 129 +++++++ app/login.tsx | 62 +++- hooks/useLocation.ts | 28 +- hooks/usePushNotifications.ts | 59 +--- package-lock.json | 549 ++++------------------------- package.json | 6 +- providers/SessionProvider.tsx | 126 +++++++ services/errorReporting.service.ts | 2 +- utils/location.utils.ts | 28 +- utils/notifications.utils.ts | 2 +- utils/tasks.utils.ts | 157 ++++++--- 15 files changed, 599 insertions(+), 617 deletions(-) create mode 100644 app/debug.tsx create mode 100644 providers/SessionProvider.tsx diff --git a/app.config.js b/app.config.js index bfd7661..1d8f97f 100644 --- a/app.config.js +++ b/app.config.js @@ -59,7 +59,12 @@ module.exports = { splash: { image: './assets/images/splash-icon.png', resizeMode: 'contain', - backgroundColor: '#ffffff', + backgroundColor: '#000000', + }, + androidStatusBar: { + barStyle: 'light-content', + backgroundColor: '#000000', + translucent: false, }, ios: { buildNumber: versionData.iosBuildNumber, // Using iOS build number from version.json diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index ffe7219..1e683b9 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -15,9 +15,9 @@ export default function LocationComponent() { const textColor = '#ffffff'; return ( - + - + diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index 0d22deb..721b953 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -4,7 +4,7 @@ import CustomHeader from "@/components/CustomHeader"; export default function SimplePage() { return ( - + My Header diff --git a/app/_layout.tsx b/app/_layout.tsx index 04c6fe8..19ff458 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -8,6 +8,8 @@ import 'react-native-reanimated'; import { useFonts } from 'expo-font'; import FontAwesome from '@expo/vector-icons/FontAwesome'; import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; +import { View } from 'react-native'; +import { StatusBar } from 'expo-status-bar'; import { usePushNotifications, useNotificationObserver } from '@/hooks/usePushNotifications'; import { useColorScheme } from '@/hooks/useColorScheme'; @@ -17,6 +19,7 @@ import { NotificationProvider } from '@/providers/NotificationProvider'; // ✅ import from providers + hooks (don’t import AuthProvider from hooks) import { AuthProvider } from '@/providers/AuthProvider'; +import { SessionProvider } from '@/providers/SessionProvider'; import { useAuth } from '@/hooks/useAuth'; import { setJSExceptionHandler, setNativeExceptionHandler } from 'react-native-exception-handler'; @@ -63,10 +66,6 @@ setNativeExceptionHandler(nativeExceptionHandler); export default function RootLayout() { const navigationRef = useNavigationContainerRef(); - // Enable push notifications and observer - usePushNotifications(); - useNotificationObserver(); - // Register background fetch task useEffect(() => { registerBackgroundFetch(); @@ -91,14 +90,30 @@ export default function RootLayout() { if (!loaded) return null; return ( - - - + + + + + + ); } +// Forced dark theme (can be extended later for dynamic theming) +const ForcedDarkTheme = { + ...DarkTheme, + colors: { + ...DarkTheme.colors, + background: '#000000', + card: '#000000', + text: '#ffffff', + border: '#222222', + primary: '#ffffff', + }, +}; + function RootLayoutNav() { - const colorScheme = useColorScheme(); + // const colorScheme = useColorScheme(); // Not used since we force dark const { user } = useAuth(); // Avoid logging during render to prevent side-effects in LogViewer useEffect(() => { @@ -112,13 +127,27 @@ function RootLayoutNav() { return ( - - - {user ? : } - - + + + + + {user ? : } + + + + ); } + +// Separate component so hooks mount within providers +function NotificationBootstrap() { + usePushNotifications(); + useNotificationObserver(); + return null; +} diff --git a/app/debug.tsx b/app/debug.tsx new file mode 100644 index 0000000..076ca7f --- /dev/null +++ b/app/debug.tsx @@ -0,0 +1,129 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { View, Text, StyleSheet, ScrollView, Pressable } from 'react-native'; +import { useSessionUser, useSession } from '@/providers/SessionProvider'; +import { BACKGROUND_TASK_IDENTIFIER, getBackgroundTaskRegistrationState, registerBackgroundFetch, unregisterBackgroundTask, triggerBackgroundTaskForTesting } from '@/utils/tasks.utils'; + +interface BgState { + status: number | null; + isRegistered: boolean; + loading: boolean; + lastAction?: string; +} + +export default function DebugScreen() { + const { fcmToken, authReady, fcmReady, user, accessToken } = useSessionUser(); + const { session } = useSession(); + const [bg, setBg] = useState({ status: null, isRegistered: false, loading: false }); + + const refresh = useCallback(async () => { + setBg((s) => ({ ...s, loading: true })); + try { + const r = await getBackgroundTaskRegistrationState(); + setBg((s) => ({ ...s, ...r, loading: false })); + } catch (e) { + setBg((s) => ({ ...s, loading: false, lastAction: 'refresh failed' })); + } + }, []); + + useEffect(() => { + refresh(); + }, [refresh]); + + const register = async () => { + setBg((s) => ({ ...s, loading: true })); + await registerBackgroundFetch(); + await refresh(); + setBg((s) => ({ ...s, lastAction: 'registered' })); + }; + const unregister = async () => { + setBg((s) => ({ ...s, loading: true })); + await unregisterBackgroundTask(); + await refresh(); + setBg((s) => ({ ...s, lastAction: 'unregistered' })); + }; + const trigger = async () => { + setBg((s) => ({ ...s, lastAction: 'triggering' })); + await triggerBackgroundTaskForTesting(); + }; + + const tokenTail = fcmToken ? fcmToken.slice(-10) : 'none'; + + return ( + + Debug +
+ + + + + +
+
+ {JSON.stringify(session, null, 2)} +
+
+ + + + + + + + + + +
+ Build Debug Utilities v1 +
+ ); +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( + + {title} + {children} + + ); +} + +function Mono({ label, value }: { label: string; value: string }) { + return ( + + {label} + {value} + + ); +} + +function Btn({ label, onPress, disabled }: { label: string; onPress: () => void; disabled?: boolean }) { + return ( + + {label} + + ); +} + +const styles = StyleSheet.create({ + root: { flex: 1, backgroundColor: '#000' }, + content: { padding: 16, paddingBottom: 48 }, + h1: { fontSize: 26, fontWeight: '600', color: '#fff', marginBottom: 12 }, + section: { marginBottom: 24, backgroundColor: '#111', borderRadius: 8, padding: 12 }, + sectionTitle: { color: '#9acd32', fontWeight: '600', fontSize: 16, marginBottom: 8 }, + kvRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 }, + kvLabel: { color: '#bbb', fontSize: 13 }, + kvValue: { color: '#fff', fontFamily: 'monospace', fontSize: 13, flexShrink: 1, textAlign: 'right' }, + code: { color: '#8f8', fontFamily: 'monospace', fontSize: 12 }, + rowWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 8 }, + btn: { backgroundColor: '#222', paddingVertical: 8, paddingHorizontal: 12, borderRadius: 6, marginRight: 8, marginBottom: 8 }, + btnDisabled: { opacity: 0.4 }, + btnText: { color: '#fff', fontSize: 12, fontWeight: '600' }, + footer: { textAlign: 'center', color: '#444', fontSize: 12 }, + row: { flexDirection: 'row', alignItems: 'center', gap: 12 }, + debugButton: { marginLeft: 12, backgroundColor: '#333', paddingHorizontal: 14, paddingVertical: 10, borderRadius: 6, justifyContent: 'center' }, + debugButtonText: { color: '#fff', fontWeight: '600' }, +}); diff --git a/app/login.tsx b/app/login.tsx index 02a7f48..50ace34 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -1,9 +1,11 @@ import { useEffect, useRef, useState } from 'react'; -import { View, StyleSheet, Text, SafeAreaView, Image, ScrollView } from 'react-native'; +import { View, StyleSheet, Text, SafeAreaView, Image, ScrollView, Pressable } from 'react-native'; import { GoogleSignin, GoogleSigninButton } from '@react-native-google-signin/google-signin'; import Constants from 'expo-constants'; import { useAuth } from '@/hooks/useAuth'; +import { useSession } from '@/providers/SessionProvider'; import { notify } from '@/services/notifier.service'; +import { router } from 'expo-router'; export default function LoginScreen() { // Add state for tracking rendering and errors @@ -23,7 +25,7 @@ export default function LoginScreen() { // If we can't even render this basic UI, there's a serious problem elsewhere if (isRendering) { return ( - + Loading login screen... ); @@ -31,7 +33,7 @@ export default function LoginScreen() { if (renderError) { return ( - + Error rendering login: {renderError} ); @@ -43,7 +45,7 @@ export default function LoginScreen() { } catch (err: any) { console.error('❌ Error rendering LoginScreenContent:', err?.message || String(err)); return ( - + Failed to render login screen {err?.message || String(err)} @@ -54,6 +56,7 @@ export default function LoginScreen() { // Separate the complex logic into its own component function LoginScreenContent() { const { user, setUser } = useAuth(); + const { updateAfterLogin } = useSession(); // Flag to track if we've already configured GoogleSignin const hasConfiguredRef = useRef(false); @@ -217,6 +220,17 @@ function LoginScreenContent() { givenName: userData.givenName, }, }); + updateAfterLogin({ + idToken, + profile: { + id: userData.id, + name: userData.name, + email: userData.email, + photo: userData.photo, + familyName: userData.familyName, + givenName: userData.givenName, + }, + }); // Make the API call console.log('🟢 Making network request to auth API...'); @@ -261,6 +275,19 @@ function LoginScreenContent() { accessToken: responseData.accessToken, refreshToken: responseData.refreshToken, })); + updateAfterLogin({ + idToken, + profile: { + id: userData.id, + name: userData.name, + email: userData.email, + photo: userData.photo, + familyName: userData.familyName, + givenName: userData.givenName, + }, + accessToken: responseData.accessToken, + refreshToken: responseData.refreshToken, + }); // Show success notification notify({ @@ -392,7 +419,12 @@ function LoginScreenContent() { return ( Login - + + + router.push('/debug' as any)} hitSlop={8}> + Debug + + ); } @@ -454,17 +486,20 @@ function LoginScreenContent() { } const styles = StyleSheet.create({ - container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16 }, - title: { fontSize: 24, fontWeight: '600', marginBottom: 16 }, + container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16, backgroundColor: '#000' }, + title: { fontSize: 24, fontWeight: '600', marginBottom: 16, color: '#fff' }, + row: { flexDirection: 'row', alignItems: 'center' }, profileImage: { width: 100, height: 100, borderRadius: 50, marginBottom: 20, }, + debugButton: { marginLeft: 12, backgroundColor: '#333', paddingHorizontal: 14, paddingVertical: 10, borderRadius: 6, justifyContent: 'center' }, + debugButtonText: { color: '#fff', fontWeight: '600' }, infoContainer: { width: '100%', - backgroundColor: '#f5f5f5', + backgroundColor: '#111', padding: 16, borderRadius: 8, marginBottom: 20, @@ -472,19 +507,20 @@ const styles = StyleSheet.create({ label: { fontWeight: '600', marginBottom: 4, - color: '#555', + color: '#ccc', }, value: { marginBottom: 16, fontSize: 16, + color: '#fff', }, statusBadge: { - color: '#2e7d32', + color: '#4caf50', fontWeight: '600', }, tokenInfo: { width: '100%', - backgroundColor: '#e8f5e9', + backgroundColor: '#1a2d1a', padding: 16, borderRadius: 8, marginBottom: 20, @@ -492,13 +528,13 @@ const styles = StyleSheet.create({ tokenLabel: { fontWeight: '600', marginBottom: 4, - color: '#2e7d32', + color: '#66bb6a', }, tokenValue: { marginBottom: 16, fontSize: 14, fontFamily: 'monospace', - color: '#1b5e20', + color: '#c8e6c9', }, buttonContainer: { flexDirection: 'row', diff --git a/hooks/useLocation.ts b/hooks/useLocation.ts index 284799e..61b4d3a 100644 --- a/hooks/useLocation.ts +++ b/hooks/useLocation.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useSession } from '@/providers/SessionProvider'; import * as LocationLibrary from "expo-location"; import { requestLocationPermission, @@ -20,6 +21,12 @@ export const useLocation = (fcmToken: string | null) => { try { setLoading(true); + if (!fcmToken) { + console.log( + "fetchLocation invoked without an FCM token; aborting location save step." + ); + } + const hasPermission = await requestLocationPermission(); if (!hasPermission) { setError("Permission to access location was denied."); @@ -36,7 +43,9 @@ export const useLocation = (fcmToken: string | null) => { setAddress(geoAddress); if (geoAddress) { - await saveLocation(currentLocation, geoAddress, fcmToken); // Pass fcmToken + await saveLocation(currentLocation, geoAddress, fcmToken); // Pass fcmToken (may be null; saveLocation will re-check) + } else { + console.log("Geo address unavailable; skipping saveLocation."); } } } catch (e) { @@ -47,11 +56,20 @@ export const useLocation = (fcmToken: string | null) => { } }; + const { waitForFcmToken } = useSession(); + useEffect(() => { - if (fcmToken) { - fetchLocation(); - } - }, [fcmToken]); // Trigger fetch when token changes + let cancelled = false; + (async () => { + const token = fcmToken || (await waitForFcmToken()); + if (!cancelled && token) { + fetchLocation(); + } + })(); + return () => { + cancelled = true; + }; + }, [fcmToken, waitForFcmToken]); return { location, address, error, loading, fetchLocation }; }; diff --git a/hooks/usePushNotifications.ts b/hooks/usePushNotifications.ts index 0a91524..404caa1 100644 --- a/hooks/usePushNotifications.ts +++ b/hooks/usePushNotifications.ts @@ -1,52 +1,14 @@ -import { useEffect, useState } from 'react'; -import { handleForegroundNotification, handleBackgroundNotifications, registerForPushNotificationsAsync } from '../utils/notifications.utils'; -//import messaging from "@react-native-firebase/messaging"; +import { useEffect } from 'react'; +import { handleForegroundNotification, handleBackgroundNotifications } from '../utils/notifications.utils'; import * as Notifications from 'expo-notifications'; import { router } from 'expo-router'; -import { getFirebaseApp } from '@/utils/firebase.utils'; +import { useSession } from '@/providers/SessionProvider'; -let globalFcmToken: string | null = null; export const usePushNotifications = () => { - const [fcmToken, setFcmToken] = useState(globalFcmToken); - + const { session } = useSession(); useEffect(() => { - const initializeNotifications = async () => { - getFirebaseApp(); - - // Register for push notifications and get the push token - registerForPushNotificationsAsync().then(async (token) => { - if (token) { - console.log('Finished registering for Notifications - FCM Token:', token); - setFcmToken(token); // Store the push token - globalFcmToken = token; // Update the global token - } - }); - - // const hasPermission = await requestPermission(); - // if (hasPermission) { - // const token = await messaging().getToken(); - // console.log("FCM Token:", token); - - // // Check if the token is already saved globally - // if (token && token !== globalFcmToken) { - // globalFcmToken = token; // Store in a global variable - // setFcmToken(token); - - // // Save the token to the server - // try { - // await savePushToken(token); - // } catch (error) { - // console.error("Error saving push token:", error); - // } - // } - // } - }; - - initializeNotifications(); - handleBackgroundNotifications(); const unsubscribe = handleForegroundNotification(); - - // Set a global notification handler + handleBackgroundNotifications(); Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldPlaySound: true, @@ -55,16 +17,13 @@ export const usePushNotifications = () => { shouldShowList: true, }), }); - - return () => { - unsubscribe(); - }; + return () => unsubscribe(); }, []); - - return { fcmToken }; + return { fcmToken: session.fcmToken }; }; -export const getFcmToken = () => globalFcmToken; +// Deprecated legacy getter (maintained for compatibility, returns null now) +// Removed legacy getter entirely to avoid accidental misuse export const useNotificationObserver = () => { useEffect(() => { diff --git a/package-lock.json b/package-lock.json index 54fb9a6..4054db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,16 +14,18 @@ "dependencies": { "@dev-plugins/react-navigation": "^0.3.1", "@expo/vector-icons": "^14.1.0", + "@react-native-async-storage/async-storage": "^1.23.0", "@react-native-firebase/app": "^22.2.1", "@react-native-firebase/auth": "^22.2.1", "@react-native-firebase/messaging": "^22.2.1", "@react-native-google-signin/google-signin": "^15.0.0", + "@react-native-masked-view/masked-view": "^0.3.1", "@react-navigation/native": "^7.1.6", "@react-navigation/native-stack": "^7.3.10", "babel-preset-expo": "~13.0.0", "dotenv": "^16.5.0", "expo": "53.0.12", - "expo-background-fetch": "^13.1.5", + "expo-background-task": "~1.0.6", "expo-battery": "^9.1.4", "expo-build-properties": "^0.14.6", "expo-camera": "^16.1.8", @@ -1631,40 +1633,6 @@ "node": ">=0.8.0" } }, - "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@expo/cli": { "version": "0.24.15", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.15.tgz", @@ -4514,19 +4482,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", - "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4685,6 +4640,18 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.60 <1.0" + } + }, "node_modules/@react-native-community/cli": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-18.0.0.tgz", @@ -5494,6 +5461,16 @@ } } }, + "node_modules/@react-native-masked-view/masked-view": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz", + "integrity": "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16", + "react-native": ">=0.57" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.4", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.4.tgz", @@ -5992,17 +5969,6 @@ "node": ">= 10" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -6199,188 +6165,6 @@ "dev": true, "license": "ISC" }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz", - "integrity": "sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz", - "integrity": "sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz", - "integrity": "sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz", - "integrity": "sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz", - "integrity": "sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz", - "integrity": "sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz", - "integrity": "sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz", - "integrity": "sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz", - "integrity": "sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz", - "integrity": "sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz", - "integrity": "sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz", - "integrity": "sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz", - "integrity": "sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz", @@ -6409,65 +6193,6 @@ "linux" ] }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz", - "integrity": "sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz", - "integrity": "sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz", - "integrity": "sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz", - "integrity": "sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@urql/core": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", @@ -9921,18 +9646,37 @@ "react-native": "*" } }, - "node_modules/expo-background-fetch": { - "version": "13.1.5", - "resolved": "https://registry.npmjs.org/expo-background-fetch/-/expo-background-fetch-13.1.5.tgz", - "integrity": "sha512-tQiQEH68o6iduDDOKdlTx7oa1cWf8d+RsT4vm2dFzoSEIr1gJtdNojN4U2qBxwTGYttJI1ug5/PYKpQUIS5VmA==", + "node_modules/expo-background-task": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/expo-background-task/-/expo-background-task-1.0.6.tgz", + "integrity": "sha512-/lGXfpetLtFkAzfLyoVdagB9XMaMkXUgjRy/rLnZy5bXlTK3d7AxITzYs4ePQWRaJ89MPeOWokXg1cy3JgwOeA==", "license": "MIT", "dependencies": { - "expo-task-manager": "~13.1.5" + "expo-task-manager": "~14.0.6" }, "peerDependencies": { "expo": "*" } }, + "node_modules/expo-background-task/node_modules/expo-task-manager": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-14.0.6.tgz", + "integrity": "sha512-3JVLgnhD7P23wpZxBGmK+GHD2Wy57snAENAOymoVHURgk2EkdgEfbVl2lcsFtr6UyACF/6i/aIAKW3mWpUGx+A==", + "license": "MIT", + "dependencies": { + "unimodules-app-loader": "~6.0.6" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-background-task/node_modules/unimodules-app-loader": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-6.0.6.tgz", + "integrity": "sha512-VpXX3H9QXP5qp1Xe0JcR2S/+i2VOuiOmyVi6q6S0hFFXcHBXoFYOcBnQDkqU/JXogZt3Wf/HSM0NGuiL3MhZIQ==", + "license": "MIT" + }, "node_modules/expo-battery": { "version": "9.1.4", "resolved": "https://registry.npmjs.org/expo-battery/-/expo-battery-9.1.4.tgz", @@ -10902,20 +10646,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -11654,6 +11384,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -14630,126 +14369,6 @@ "lightningcss-win32-x64-msvc": "1.27.0" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz", - "integrity": "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz", - "integrity": "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz", - "integrity": "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz", - "integrity": "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz", - "integrity": "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz", - "integrity": "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz", @@ -14790,46 +14409,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz", - "integrity": "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz", - "integrity": "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -15147,6 +14726,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 8d05bd2..ec97528 100644 --- a/package.json +++ b/package.json @@ -22,16 +22,18 @@ "dependencies": { "@dev-plugins/react-navigation": "^0.3.1", "@expo/vector-icons": "^14.1.0", + "@react-native-async-storage/async-storage": "^1.23.0", "@react-native-firebase/app": "^22.2.1", "@react-native-firebase/auth": "^22.2.1", "@react-native-firebase/messaging": "^22.2.1", "@react-native-google-signin/google-signin": "^15.0.0", + "@react-native-masked-view/masked-view": "^0.3.1", "@react-navigation/native": "^7.1.6", "@react-navigation/native-stack": "^7.3.10", "babel-preset-expo": "~13.0.0", "dotenv": "^16.5.0", "expo": "53.0.12", - "expo-background-fetch": "^13.1.5", + "expo-background-task": "~1.0.6", "expo-battery": "^9.1.4", "expo-build-properties": "^0.14.6", "expo-camera": "^16.1.8", @@ -96,4 +98,4 @@ } } } -} \ No newline at end of file +} diff --git a/providers/SessionProvider.tsx b/providers/SessionProvider.tsx new file mode 100644 index 0000000..aa1252d --- /dev/null +++ b/providers/SessionProvider.tsx @@ -0,0 +1,126 @@ +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { registerForPushNotificationsAsync } from '@/utils/notifications.utils'; +import { getFirebaseApp } from '@/utils/firebase.utils'; + +export interface SessionUserProfile { + id?: string; + name?: string | null; + email?: string | null; + photo?: string | null; + familyName?: string | null; + givenName?: string | null; +} + +export interface SessionState { + fcmToken: string | null; + fcmReady: boolean; + authReady: boolean; + idToken?: string; + accessToken?: string; + refreshToken?: string; + user?: SessionUserProfile; +} + +interface SessionContextValue { + session: SessionState; + setSession: React.Dispatch>; + waitForFcmToken: (timeoutMs?: number) => Promise; + updateAfterLogin: (data: { idToken: string; profile: SessionUserProfile; accessToken?: string; refreshToken?: string }) => void; + clearSession: () => Promise; +} + +const SessionContext = createContext(undefined); +const FCM_STORAGE_KEY = 'session.fcmToken'; + +export const SessionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [session, setSession] = useState({ fcmToken: null, fcmReady: false, authReady: false }); + const waitersRef = useRef<((token: string | null) => void)[]>([]); + const initializingRef = useRef(false); + + useEffect(() => { + const init = async () => { + if (initializingRef.current) return; + initializingRef.current = true; + try { + const cached = await AsyncStorage.getItem(FCM_STORAGE_KEY); + if (cached) { + setSession((s) => ({ ...s, fcmToken: cached, fcmReady: true })); + resolveWaiters(cached); + } + getFirebaseApp(); + const token = await registerForPushNotificationsAsync(); + if (token) { + await AsyncStorage.setItem(FCM_STORAGE_KEY, token); + setSession((s) => ({ ...s, fcmToken: token, fcmReady: true })); + resolveWaiters(token); + } else { + setSession((s) => ({ ...s, fcmReady: true })); + resolveWaiters(null); + } + } catch (e) { + console.log('[SessionProvider] FCM init error:', (e as any)?.message || e); + setSession((s) => ({ ...s, fcmReady: true })); + resolveWaiters(null); + } + }; + init(); + }, []); + + const resolveWaiters = (token: string | null) => { + waitersRef.current.forEach((res) => res(token)); + waitersRef.current = []; + }; + + const waitForFcmToken = (timeoutMs = 7000): Promise => { + if (session.fcmReady) return Promise.resolve(session.fcmToken); + return new Promise((resolve) => { + waitersRef.current.push(resolve); + setTimeout(() => { + resolve(session.fcmToken); + }, timeoutMs); + }); + }; + + const updateAfterLogin = (data: { idToken: string; profile: SessionUserProfile; accessToken?: string; refreshToken?: string }) => { + setSession((s) => ({ + ...s, + idToken: data.idToken, + user: { ...s.user, ...data.profile }, + accessToken: data.accessToken || s.accessToken, + refreshToken: data.refreshToken || s.refreshToken, + authReady: true, + })); + }; + + const clearSession = async () => { + await AsyncStorage.removeItem(FCM_STORAGE_KEY); + setSession({ fcmToken: null, fcmReady: true, authReady: false }); + }; + + return ( + + {children} + + ); +}; + +export const useSession = () => { + const ctx = useContext(SessionContext); + if (!ctx) throw new Error('useSession must be used within '); + return ctx; +}; + +// Convenience selector-style hook to reduce re-renders elsewhere +export const useSessionUser = () => { + const { session } = useSession(); + return { + fcmToken: session.fcmToken, + user: session.user, + idToken: session.idToken, + accessToken: session.accessToken, + refreshToken: session.refreshToken, + authReady: session.authReady, + fcmReady: session.fcmReady, + }; +}; diff --git a/services/errorReporting.service.ts b/services/errorReporting.service.ts index d41e593..9a26209 100644 --- a/services/errorReporting.service.ts +++ b/services/errorReporting.service.ts @@ -16,7 +16,7 @@ export interface ErrorReport { options?: ReportOptions; } -const ERROR_REPORTING_ENDPOINT = 'https://new.codebuilder.org/api/errors'; +const ERROR_REPORTING_ENDPOINT = 'https://api.codebuilder.org/errors'; // Circuit breaker implementation const circuitBreaker = { diff --git a/utils/location.utils.ts b/utils/location.utils.ts index b705b88..41e78c5 100644 --- a/utils/location.utils.ts +++ b/utils/location.utils.ts @@ -1,5 +1,10 @@ import * as LocationLibrary from "expo-location"; -import { getFcmToken } from "../hooks/usePushNotifications"; + +const tsLog = (...args: any[]) => { + const ts = new Date().toISOString(); + // eslint-disable-next-line no-console + console.log('[location][' + ts + ']', ...args); +}; /** * Request location permissions. @@ -57,21 +62,26 @@ export const saveLocation = async ( geoAddress: LocationLibrary.LocationGeocodedAddress, token: string | null ): Promise => { - if (!token) { - console.log("No FCM token available; skipping save location."); + // Token now solely managed by SessionProvider; do not attempt legacy global fallback + let effectiveToken = token; + + if (!effectiveToken) { + tsLog( + "No FCM token available; skipping save location.", + { passedTokenNull: !token } + ); return; } - - console.log("Saving location to server...", token); + tsLog("Saving location to server", { tokenTail: effectiveToken.slice(-8) }); try { const payload = { - subscriptionId: token, + subscriptionId: effectiveToken, latitude: location.coords.latitude, longitude: location.coords.longitude, }; - const response = await fetch("https://new.codebuilder.org/api/location", { + const response = await fetch("https://api.codebuilder.org/location", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), @@ -87,8 +97,8 @@ export const saveLocation = async ( ); } - console.log("Location and address saved to server successfully."); + tsLog("Location saved successfully"); } catch (error: any) { - console.error("Error saving location to server:", error?.message || error); + tsLog("Error saving location to server", error?.message || error); } }; diff --git a/utils/notifications.utils.ts b/utils/notifications.utils.ts index a4404bb..899eccf 100644 --- a/utils/notifications.utils.ts +++ b/utils/notifications.utils.ts @@ -131,7 +131,7 @@ export const handleBackgroundNotifications = () => { export const savePushToken = async (token: string): Promise => { try { const response = await fetch( - "https://new.codebuilder.org/api/notifications/subscribe", + "https://api.codebuilder.org/notifications/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/utils/tasks.utils.ts b/utils/tasks.utils.ts index 0553346..0c40fb7 100644 --- a/utils/tasks.utils.ts +++ b/utils/tasks.utils.ts @@ -1,52 +1,129 @@ import * as TaskManager from 'expo-task-manager'; -import * as BackgroundFetch from 'expo-background-fetch'; +let BackgroundTask: any; +try { + // Dynamically require to avoid crashing if native module not yet installed / prebuilt + BackgroundTask = require('expo-background-task'); +} catch (e) { + console.warn('[BackgroundTask] Module not available (dev or web?):', (e as any)?.message); + BackgroundTask = { + BackgroundTaskResult: { Success: 1, Failed: 2 }, + BackgroundTaskStatus: { Restricted: 1, Available: 2 }, + getStatusAsync: async () => 1, + registerTaskAsync: async () => {}, + unregisterTaskAsync: async () => {}, + }; +} -const BACKGROUND_FETCH_TASK = 'background-fetch-task'; +export const BACKGROUND_TASK_IDENTIFIER = 'background-fetch-task'; // legacy id retained -// Define the task -TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { +let taskDefined = false; +function defineBackgroundTask() { + if (taskDefined) return; try { - console.log('----------------------- Background fetch task started ---------------------'); - // Perform your API request - const response = await fetch('https://api.codebuilder.org/jobs/fetch'); - const contentType = response.headers.get('content-type') || ''; - - if (!response.ok) { - const text = await response.text(); - console.error('Background fetch HTTP error', response.status, text.slice(0, 200)); - return BackgroundFetch.BackgroundFetchResult.Failed; - } + TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, async () => { + try { + const startedAt = new Date().toISOString(); + console.log('[BackgroundTask] Started', startedAt); + const response = await fetch('https://api.codebuilder.org/jobs/fetch'); + const contentType = response.headers.get('content-type') || ''; + + if (!response.ok) { + const text = await response.text(); + console.error('[BackgroundTask] HTTP error', response.status, text.slice(0, 200)); + return BackgroundTask.BackgroundTaskResult.Failed; + } - let data: unknown; - if (contentType.includes('application/json')) { - data = await response.json(); + let data: unknown; + if (contentType.includes('application/json')) { + data = await response.json(); + } else { + const text = await response.text(); + console.warn('[BackgroundTask] Non-JSON response', text.slice(0, 200)); + data = text; + } + + console.log('[BackgroundTask] Success fetched data snapshot type:', typeof data); + return BackgroundTask.BackgroundTaskResult.Success; + } catch (error) { + console.error('[BackgroundTask] Failed', error); + return BackgroundTask.BackgroundTaskResult.Failed; + } + }); + taskDefined = true; + console.log('[BackgroundTask] Task definition ensured'); + } catch (e) { + // If already defined, ignore; otherwise log + const msg = (e as any)?.message || ''; + if (!/already defined/i.test(msg)) { + console.warn('[BackgroundTask] defineTask error', msg); } else { - const text = await response.text(); - console.warn('Background fetch received non-JSON response:', text.slice(0, 200)); - data = text; + taskDefined = true; // treat as defined } + } +} - // Handle the fetched data - console.log('Fetched data:', data); +// Ensure definition at module load +defineBackgroundTask(); - return BackgroundFetch.BackgroundFetchResult.NewData; // Task succeeded - } catch (error) { - console.error('Background fetch failed:', error); - return BackgroundFetch.BackgroundFetchResult.Failed; // Task failed - } -}); - -// Register the task -export async function registerBackgroundFetch() { - const status = await BackgroundFetch.getStatusAsync(); - if (status === BackgroundFetch.BackgroundFetchStatus.Available) { - await BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { - minimumInterval: 60, // Fetch interval in seconds (not guaranteed to be exact) - stopOnTerminate: false, // Continue task when app is closed - startOnBoot: true, // Start task when device is rebooted +/** + * Register background task (replaces deprecated expo-background-fetch). + * minimumInterval is in MINUTES (platform minimum ~15 on Android; iOS may defer more). + */ +export async function registerBackgroundFetch(minimumIntervalMinutes: number = 15) { + try { + // Double-ensure task is defined before registering + defineBackgroundTask(); + const status = await BackgroundTask.getStatusAsync(); + if (status !== BackgroundTask.BackgroundTaskStatus.Available) { + console.warn('[BackgroundTask] Not available, status=', status); + return false; + } + await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, { + minimumInterval: Math.max(15, minimumIntervalMinutes), }); - console.log('Background fetch task registered'); - } else { - console.error('Background fetch is not available'); + console.log('[BackgroundTask] Registered (min interval minutes):', Math.max(15, minimumIntervalMinutes)); + return true; + } catch (e) { + const msg = (e as any)?.message || String(e); + console.error('[BackgroundTask] Register error', msg); + if (/not defined/i.test(msg)) { + console.log('[BackgroundTask] Retrying after ensure definition...'); + try { + defineBackgroundTask(); + await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, { + minimumInterval: Math.max(15, minimumIntervalMinutes), + }); + console.log('[BackgroundTask] Registered on retry'); + return true; + } catch (e2) { + console.error('[BackgroundTask] Retry failed', (e2 as any)?.message || e2); + } + } + return false; } } + +export async function unregisterBackgroundTask() { + try { + await BackgroundTask.unregisterTaskAsync(BACKGROUND_TASK_IDENTIFIER); + console.log('[BackgroundTask] Unregistered'); + } catch (e) { + console.error('[BackgroundTask] Unregister error', (e as any)?.message || e); + } +} + +// Dev helper to force run tasks (no-op in prod) +export async function triggerBackgroundTaskForTesting() { + try { + const triggered = await (BackgroundTask as any).triggerTaskWorkerForTestingAsync?.(); + console.log('[BackgroundTask] Trigger test invoked', triggered); + } catch (e) { + console.error('[BackgroundTask] Trigger test error', (e as any)?.message || e); + } +} + +export async function getBackgroundTaskRegistrationState() { + const status = await BackgroundTask.getStatusAsync(); + const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_TASK_IDENTIFIER); + return { status, isRegistered }; +} From 4d932f03ccfb18ed01d43d3c74d85aa14e07b439 Mon Sep 17 00:00:00 2001 From: digitalnomad91 Date: Thu, 11 Sep 2025 00:43:03 -0500 Subject: [PATCH 2/2] chore(background-task): strengthen task definition & retry guards --- utils/tasks.utils.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/utils/tasks.utils.ts b/utils/tasks.utils.ts index 0c40fb7..44e7516 100644 --- a/utils/tasks.utils.ts +++ b/utils/tasks.utils.ts @@ -16,11 +16,12 @@ try { export const BACKGROUND_TASK_IDENTIFIER = 'background-fetch-task'; // legacy id retained -let taskDefined = false; +// Track definition across Fast Refresh cycles +let taskDefined = (globalThis as any).__CODEBUILDER_BACKGROUND_TASK_DEFINED__ === true; function defineBackgroundTask() { if (taskDefined) return; try { - TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, async () => { + const handler = async () => { try { const startedAt = new Date().toISOString(); console.log('[BackgroundTask] Started', startedAt); @@ -48,8 +49,21 @@ function defineBackgroundTask() { console.error('[BackgroundTask] Failed', error); return BackgroundTask.BackgroundTaskResult.Failed; } - }); + }; + // Prefer TaskManager; some libs might also expose a define API + if (typeof (BackgroundTask as any).defineTask === 'function') { + try { + (BackgroundTask as any).defineTask(BACKGROUND_TASK_IDENTIFIER, handler); + } catch (btErr: any) { + const m = btErr?.message || ''; + if (!/already defined/i.test(m)) { + console.warn('[BackgroundTask] BackgroundTask.defineTask failed, falling back to TaskManager.defineTask', m); + } + } + } + TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, handler); taskDefined = true; + (globalThis as any).__CODEBUILDER_BACKGROUND_TASK_DEFINED__ = true; console.log('[BackgroundTask] Task definition ensured'); } catch (e) { // If already defined, ignore; otherwise log @@ -58,6 +72,7 @@ function defineBackgroundTask() { console.warn('[BackgroundTask] defineTask error', msg); } else { taskDefined = true; // treat as defined + (globalThis as any).__CODEBUILDER_BACKGROUND_TASK_DEFINED__ = true; } } } @@ -73,6 +88,8 @@ export async function registerBackgroundFetch(minimumIntervalMinutes: number = 1 try { // Double-ensure task is defined before registering defineBackgroundTask(); + // Small delay to let task registry settle (helps with Fast Refresh race conditions) + await new Promise(res => setTimeout(res, 25)); const status = await BackgroundTask.getStatusAsync(); if (status !== BackgroundTask.BackgroundTaskStatus.Available) { console.warn('[BackgroundTask] Not available, status=', status); @@ -90,6 +107,7 @@ export async function registerBackgroundFetch(minimumIntervalMinutes: number = 1 console.log('[BackgroundTask] Retrying after ensure definition...'); try { defineBackgroundTask(); + await new Promise(res => setTimeout(res, 100)); await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, { minimumInterval: Math.max(15, minimumIntervalMinutes), });