diff --git a/app/app.json b/app/app.json index 4ba1384..83b4399 100644 --- a/app/app.json +++ b/app/app.json @@ -7,8 +7,14 @@ "orientation": "portrait", "userInterfaceStyle": "automatic", "newArchEnabled": true, + "icon": "./assets/icon.png", "android": { "package": "top.mowden.bigbrainparking", + "edgeToEdgeEnabled": true, + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#0F2A33" + }, "permissions": [ "ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION", diff --git a/app/assets/adaptive-icon.png b/app/assets/adaptive-icon.png new file mode 100644 index 0000000..f7e90a2 Binary files /dev/null and b/app/assets/adaptive-icon.png differ diff --git a/app/assets/icon.png b/app/assets/icon.png new file mode 100644 index 0000000..c5fabb3 Binary files /dev/null and b/app/assets/icon.png differ diff --git a/app/src/api/client.ts b/app/src/api/client.ts index 9b18f61..9ff8659 100644 --- a/app/src/api/client.ts +++ b/app/src/api/client.ts @@ -2,16 +2,19 @@ import Constants from 'expo-constants'; import * as Localization from 'expo-localization'; import { ParkSmarterClient, type EnvironmentName } from 'parksmarter-client'; import { secureTokenStore } from './secureTokenStore'; +import { authBus } from '@/auth/authBus'; const env = (Constants.expoConfig?.extra?.psEnvironment as EnvironmentName) ?? 'prodv2'; /** * The single app-wide API client. React Native ships a global `fetch`, so no - * fetchImpl override is needed. Tokens persist in the OS keystore. + * fetchImpl override is needed. Tokens persist in the OS keystore. On a 401 the + * client clears the token and we bounce the user to sign-in via authBus. */ export const ps = new ParkSmarterClient({ environment: env, tokens: secureTokenStore, localeCode: (Localization.getLocales()[0]?.languageCode ?? 'en').toLowerCase(), + onUnauthorized: () => authBus.onUnauthorized?.(), }); diff --git a/app/src/auth/AuthContext.tsx b/app/src/auth/AuthContext.tsx index 270d824..3b5e098 100644 --- a/app/src/auth/AuthContext.tsx +++ b/app/src/auth/AuthContext.tsx @@ -6,6 +6,7 @@ import React, { useState, } from 'react'; import { ps } from '@/api/client'; +import { authBus } from '@/auth/authBus'; import type { ApplicationValidityResponse } from 'parksmarter-client'; type AuthStatus = 'loading' | 'signedOut' | 'signedIn'; @@ -25,6 +26,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [validity, setValidity] = useState(null); const [error, setError] = useState(null); + // Any 401 from the API (expired/rotated token) bounces us back to sign-in. + useEffect(() => { + authBus.onUnauthorized = () => { + setError('Your session expired — please sign in again.'); + setStatus('signedOut'); + }; + return () => { + authBus.onUnauthorized = undefined; + }; + }, []); + // On launch: bootstrap (seeds SessionId + feature flags) and probe for an // existing token by attempting an authenticated read. useEffect(() => { diff --git a/app/src/auth/authBus.ts b/app/src/auth/authBus.ts new file mode 100644 index 0000000..075d9fd --- /dev/null +++ b/app/src/auth/authBus.ts @@ -0,0 +1,6 @@ +/** + * Tiny bridge so the API client (created at module load) can notify the React + * auth layer when a 401 happens, without a circular import. AuthProvider + * registers a handler; the client calls it via app/src/api/client.ts. + */ +export const authBus: { onUnauthorized?: () => void } = {}; diff --git a/app/src/screens/MapScreen.tsx b/app/src/screens/MapScreen.tsx index 2eecf3d..0603f95 100644 --- a/app/src/screens/MapScreen.tsx +++ b/app/src/screens/MapScreen.tsx @@ -9,6 +9,7 @@ import { } from '@maplibre/maplibre-react-native'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ps } from '@/api/client'; import { useLocation, getLastKnownSavedLocation, type Coords } from '@/features/location/useLocation'; import type { RootStackParamList } from '@/navigation/RootNavigator'; @@ -26,6 +27,7 @@ type Nav = NativeStackNavigationProp; export function MapScreen() { const navigation = useNavigation