v0.6.1: stop Anonymous Mode bouncing to sign-in on every foreground
All checks were successful
build-apk / build (push) Successful in 9m55s
All checks were successful
build-apk / build (push) Successful in 9m55s
Parking without an account showed "Your session expired — please sign in again." repeatedly and kicked the user back to the login screen. The 401 hook on the API client is global: it fires on every unauthorized response whether or not the caller caught the error. Anonymous Mode has no token, so any ParkSmarter call 401s, and two of them run automatically — discoverPaidSession() on open and every foreground, and the map's lastSessionLot() on every mount. Each one flipped the status to signedOut; re-entering anonymous mode ran them again, hence "over and over". Both call sites already caught their errors, which is why this hid: the bounce came from the client hook, not from the catch. - A 401 in Anonymous Mode is no longer treated as a session expiry. There is no session to expire, and bouncing to sign-in makes the mode pointless. This also covers the Sessions/Favorites/Scan tabs, which 401 the same way. - authBus carries the mode to the non-React modules that need it. - discoverPaidSession() and lastSessionLot() are skipped entirely when anonymous rather than fired and discarded. - "Last lot" now says it needs an account instead of silently doing nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
148e1635d3
commit
7fc92d24e4
5 changed files with 35 additions and 6 deletions
|
|
@ -3,14 +3,14 @@
|
|||
"name": "BigBrainParking",
|
||||
"slug": "bigbrainparking",
|
||||
"scheme": "bigbrainparking",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"orientation": "portrait",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"icon": "./assets/icon.png",
|
||||
"android": {
|
||||
"package": "top.mowden.bigbrainparking",
|
||||
"versionCode": 21,
|
||||
"versionCode": 22,
|
||||
"edgeToEdgeEnabled": true,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
|
|
|
|||
|
|
@ -31,9 +31,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
const [validity, setValidity] = useState<ApplicationValidityResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Let the non-React modules see the mode. Kept in sync here rather than read
|
||||
// from storage: anonymous mode is deliberately not persisted across restarts.
|
||||
useEffect(() => {
|
||||
authBus.isAnonymous = status === 'anonymous';
|
||||
}, [status]);
|
||||
|
||||
// Any 401 from the API (expired/rotated token) bounces us back to sign-in.
|
||||
useEffect(() => {
|
||||
authBus.onUnauthorized = () => {
|
||||
// ...except in Anonymous Mode, where there is no session to expire. A 401
|
||||
// there just means something asked ParkSmarter a question it had no
|
||||
// business asking, and bouncing to sign-in would make the app unusable
|
||||
// without an account — which is the whole point of the mode. Read at call
|
||||
// time, so this stays correct as the status changes.
|
||||
if (authBus.isAnonymous) return;
|
||||
setError('Your session expired — please sign in again.');
|
||||
setStatus('signedOut');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,5 +2,13 @@
|
|||
* 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.
|
||||
*
|
||||
* `isAnonymous` mirrors the auth status for the non-React modules that need it.
|
||||
* It matters because the 401 hook is global: it fires on every unauthorized
|
||||
* response whether or not the caller caught the error, so without this a single
|
||||
* stray ParkSmarter call in Anonymous Mode throws the user to the sign-in screen.
|
||||
*/
|
||||
export const authBus: { onUnauthorized?: () => void } = {};
|
||||
export const authBus: {
|
||||
onUnauthorized?: () => void;
|
||||
isAnonymous: boolean;
|
||||
} = { isAnonymous: false };
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|||
import * as Notifications from 'expo-notifications';
|
||||
import type { Zone } from 'parksmarter-client';
|
||||
import { ps } from '@/api/client';
|
||||
import { authBus } from '@/auth/authBus';
|
||||
import { parseApiTime } from '@/api/parseTime';
|
||||
import type { LabelKind } from '@/api/zoneLabels';
|
||||
import type { RootStackParamList } from '@/navigation/RootNavigator';
|
||||
|
|
@ -326,8 +327,9 @@ export async function syncActiveParking(onExtend: (zone?: Zone) => void): Promis
|
|||
}
|
||||
|
||||
// A city-map session is never on the ParkSmarter server, so don't let a stale
|
||||
// server session overwrite it.
|
||||
if (!current) current = await discoverPaidSession();
|
||||
// server session overwrite it. In Anonymous Mode there is no account to ask at
|
||||
// all — asking anyway would 401 on every single foreground.
|
||||
if (!current && !authBus.isAnonymous) current = await discoverPaidSession();
|
||||
|
||||
if (current) {
|
||||
await postNotification(current);
|
||||
|
|
|
|||
|
|
@ -113,6 +113,9 @@ export function MapScreen() {
|
|||
// Look up the LAST session's parking-lot coordinate (the meter's own location
|
||||
// from history — never the user's GPS). Used to open the map and by "Last lot".
|
||||
const lastSessionLot = useCallback(async (): Promise<Coords | null> => {
|
||||
// No account, no session history — and asking anyway 401s, which the global
|
||||
// handler would turn into a bogus "session expired" bounce.
|
||||
if (isAnonymous) return null;
|
||||
try {
|
||||
const past = await ps.getPastParkingSessions({ currentPage: 1, pageSize: 1 });
|
||||
const s = past.Session?.[0] as Record<string, any> | undefined;
|
||||
|
|
@ -125,7 +128,7 @@ export function MapScreen() {
|
|||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
}, [isAnonymous]);
|
||||
|
||||
// Open on your last parking lot — NOT your GPS. Your location is only ever sent
|
||||
// to the API when you explicitly tap "My location", so we never auto-center on it.
|
||||
|
|
@ -257,6 +260,10 @@ export function MapScreen() {
|
|||
// Center on the LAST SESSION's parking lot (the meter's own coordinate from
|
||||
// history — never your GPS) and search around it with a ~couple-mile view.
|
||||
const searchLastSessionLot = async () => {
|
||||
if (isAnonymous) {
|
||||
setStatus('Sign in to use your last parking lot — it comes from your account history.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setStatus('Finding your last parking lot…');
|
||||
const lot = await lastSessionLot();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue