v0.6.2: show and manage on-phone sessions offline and signed out
All checks were successful
build-apk / build (push) Successful in 9m58s
All checks were successful
build-apk / build (push) Successful in 9m58s
A city-map timer started without signing in did not appear under Sessions. The tab returned "Sign in to see your sessions" before rendering anything, so a local session could never show; and even signed in it only ever listed ParkSmarter's sessions. A local timer is the one kind that has no server copy, which made it the one kind the screen could not display. - Sessions now leads with "Tracking on this phone": the live countdown with +1 hour and End, working with no account and no network, because that is the only place the session exists. - Recent local sessions are kept in a small on-device history (50 max). Without it a local session vanished the instant it ended — there is no server to ask. Paid ParkSmarter sessions are excluded so they don't appear twice. - The ParkSmarter half is layered on top when signed in and can fail independently: offline it reports that and keeps the local half visible, rather than the whole tab going blank. It also no longer leaves an unhandled rejection when the fetch throws (it had try/finally but no catch). - The card's second button follows the notification's rule: +1 hour for a local timer, Extend -> purchase screen for a bought session, since only one of those can honestly add time. History is written in endActiveParking() before the record is dropped, which also covers expiry — syncActiveParking() routes a lapsed session through the same call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7fc92d24e4
commit
6af3dad762
5 changed files with 309 additions and 56 deletions
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '@/features/notifications/reminderPrefs';
|
||||
import { logLine } from '@/features/diagnostics/fileLogger';
|
||||
import type { ParkingArea } from '@/api/parkingAreas';
|
||||
import { recordLocalSession } from './localHistory';
|
||||
import {
|
||||
clearActiveParking,
|
||||
getActiveParking,
|
||||
|
|
@ -255,6 +256,10 @@ export async function extendAreaParking(minutes = EXTEND_MINUTES): Promise<void>
|
|||
* keeps running at the meter whether or not the app is showing it.
|
||||
*/
|
||||
export async function endActiveParking(): Promise<void> {
|
||||
// Write it to history before dropping it. A local session has no server copy, so
|
||||
// if it isn't recorded here it is simply gone.
|
||||
const current = await getActiveParking();
|
||||
if (current) await recordLocalSession(current);
|
||||
await clearActiveParking();
|
||||
await clearNotification();
|
||||
await Notifications.cancelScheduledNotificationAsync(EXPIRY_REMINDER_ID).catch(() => {});
|
||||
|
|
|
|||
81
app/src/features/session/localHistory.ts
Normal file
81
app/src/features/session/localHistory.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import type { AreaKind } from '@/api/parkingAreas';
|
||||
import type { ActiveParking, ParkedSpot, ParkingKind } from './activeParkingStore';
|
||||
|
||||
/**
|
||||
* History for the sessions ParkSmarter never sees.
|
||||
*
|
||||
* A city-map timer or a free check-in exists only on this phone, so if it isn't
|
||||
* recorded here it vanishes the moment it ends — there is no server to ask. Paid
|
||||
* ParkSmarter sessions are deliberately excluded: those already come back from the
|
||||
* account, and storing them too would show every one of them twice.
|
||||
*/
|
||||
|
||||
const KEY = 'ps_local_session_history';
|
||||
/** Enough to cover months of parking without letting the record grow forever. */
|
||||
const MAX = 50;
|
||||
|
||||
export interface LocalSessionRecord {
|
||||
/** Start time doubles as the id — there is only ever one session at a time. */
|
||||
id: string;
|
||||
kind: ParkingKind;
|
||||
zoneName: string;
|
||||
areaId?: string;
|
||||
areaKind?: AreaKind;
|
||||
color?: string;
|
||||
legend?: string;
|
||||
startMs: number;
|
||||
/** When it was due to end. */
|
||||
plannedEndMs: number;
|
||||
/** When it actually ended. */
|
||||
endedAtMs: number;
|
||||
/** True when the user ended it before the clock ran out. */
|
||||
endedEarly: boolean;
|
||||
spot?: ParkedSpot;
|
||||
}
|
||||
|
||||
/** True when ParkSmarter has no record of this session, so we must keep our own. */
|
||||
export function isLocalOnly(p: ActiveParking): boolean {
|
||||
return !p.transactionId;
|
||||
}
|
||||
|
||||
export async function getLocalHistory(): Promise<LocalSessionRecord[]> {
|
||||
const raw = await AsyncStorage.getItem(KEY);
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const list = JSON.parse(raw) as LocalSessionRecord[];
|
||||
return Array.isArray(list) ? list : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Record a finished local session. No-op for anything ParkSmarter already has. */
|
||||
export async function recordLocalSession(p: ActiveParking): Promise<void> {
|
||||
if (!isLocalOnly(p)) return;
|
||||
const endedAtMs = Date.now();
|
||||
const record: LocalSessionRecord = {
|
||||
id: String(p.startMs),
|
||||
kind: p.kind,
|
||||
zoneName: p.zoneName,
|
||||
areaId: p.area?.id,
|
||||
areaKind: p.area?.kind,
|
||||
color: p.area?.color,
|
||||
legend: p.area?.legend,
|
||||
startMs: p.startMs,
|
||||
plannedEndMs: p.endMs,
|
||||
endedAtMs,
|
||||
endedEarly: endedAtMs < p.endMs - 60_000, // a minute's slack for timer wake-up
|
||||
spot: p.spot,
|
||||
};
|
||||
const list = await getLocalHistory();
|
||||
// Guard against double-recording: ending can be driven from the notification and
|
||||
// the screen at nearly the same moment.
|
||||
const deduped = list.filter((r) => r.id !== record.id);
|
||||
deduped.unshift(record);
|
||||
await AsyncStorage.setItem(KEY, JSON.stringify(deduped.slice(0, MAX)));
|
||||
}
|
||||
|
||||
export async function clearLocalHistory(): Promise<void> {
|
||||
await AsyncStorage.removeItem(KEY);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue