diff --git a/app/app.json b/app/app.json index 0732d31..81a63de 100644 --- a/app/app.json +++ b/app/app.json @@ -3,14 +3,14 @@ "name": "BigBrainParking", "slug": "bigbrainparking", "scheme": "bigbrainparking", - "version": "0.2.3", + "version": "0.2.4", "orientation": "portrait", "userInterfaceStyle": "automatic", "newArchEnabled": true, "icon": "./assets/icon.png", "android": { "package": "top.mowden.bigbrainparking", - "versionCode": 13, + "versionCode": 14, "edgeToEdgeEnabled": true, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", diff --git a/app/modules/bbp-notify/android/src/main/java/expo/modules/bbpnotify/BbpNotifyModule.kt b/app/modules/bbp-notify/android/src/main/java/expo/modules/bbpnotify/BbpNotifyModule.kt index 8d4173b..d13a962 100644 --- a/app/modules/bbp-notify/android/src/main/java/expo/modules/bbpnotify/BbpNotifyModule.kt +++ b/app/modules/bbp-notify/android/src/main/java/expo/modules/bbpnotify/BbpNotifyModule.kt @@ -21,14 +21,21 @@ class BbpNotifyModule : Module() { override fun definition() = ModuleDefinition { Name("BbpNotify") + // Returns a short diagnostic string so the JS layer can log exactly what + // happened (posted / notifications-disabled / exception). "Nothing appears" + // bugs are otherwise invisible because notify() fails silently. AsyncFunction("showCountdown") { title: String, body: String, endTimeMillis: Double -> - val ctx = appContext.reactContext ?: return@AsyncFunction + val ctx = appContext.reactContext + ?: return@AsyncFunction "no-context" ensureChannel(ctx) + val mgr = NotificationManagerCompat.from(ctx) + val enabled = mgr.areNotificationsEnabled() + val builder = NotificationCompat.Builder(ctx, CHANNEL_ID) .setContentTitle(title) .setContentText(body) - .setSmallIcon(ctx.applicationInfo.icon) + .setSmallIcon(R.drawable.bbp_stat_parking) .setOngoing(true) .setOnlyAlertOnce(true) .setShowWhen(true) @@ -52,10 +59,14 @@ class BbpNotifyModule : Module() { ) } - try { - NotificationManagerCompat.from(ctx).notify(NOTIF_ID, builder.build()) - } catch (_: SecurityException) { - // POST_NOTIFICATIONS not granted yet; caller requests it separately. + return@AsyncFunction try { + mgr.notify(NOTIF_ID, builder.build()) + "posted enabled=$enabled sdk=${Build.VERSION.SDK_INT}" + } catch (e: SecurityException) { + // POST_NOTIFICATIONS not granted. + "security-exception enabled=$enabled msg=${e.message}" + } catch (e: Exception) { + "exception enabled=$enabled ${e.javaClass.simpleName}=${e.message}" } } diff --git a/app/modules/bbp-notify/android/src/main/res/drawable/bbp_stat_parking.xml b/app/modules/bbp-notify/android/src/main/res/drawable/bbp_stat_parking.xml new file mode 100644 index 0000000..f19321c --- /dev/null +++ b/app/modules/bbp-notify/android/src/main/res/drawable/bbp_stat_parking.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/modules/bbp-notify/index.ts b/app/modules/bbp-notify/index.ts index 319e5ea..ee513a8 100644 --- a/app/modules/bbp-notify/index.ts +++ b/app/modules/bbp-notify/index.ts @@ -2,8 +2,11 @@ import { Platform } from 'react-native'; import { requireOptionalNativeModule } from 'expo-modules-core'; interface BbpNotifyNative { - /** Post/replace an ongoing notification with a native chronometer counting down to endTimeMillis. */ - showCountdown(title: string, body: string, endTimeMillis: number): Promise; + /** + * Post/replace an ongoing notification with a native chronometer counting down + * to endTimeMillis. Returns a short diagnostic string (e.g. "posted enabled=true"). + */ + showCountdown(title: string, body: string, endTimeMillis: number): Promise; /** Remove the countdown notification. */ clear(): Promise; } @@ -22,8 +25,9 @@ export async function showCountdown( title: string, body: string, endTimeMillis: number, -): Promise { - await native?.showCountdown(title, body, endTimeMillis); +): Promise { + if (!native) return 'no-native-module'; + return native.showCountdown(title, body, endTimeMillis); } export async function clearCountdown(): Promise { diff --git a/app/src/notifications/sessionStatus.ts b/app/src/notifications/sessionStatus.ts index 340b96c..17f5e22 100644 --- a/app/src/notifications/sessionStatus.ts +++ b/app/src/notifications/sessionStatus.ts @@ -53,13 +53,28 @@ function fmtLeft(min: number): string { /** Post (or replace) the ongoing status notification for a session end time. */ export async function showSessionStatus(args: { zoneName: string; endTime: Date }): Promise { - if (!(await getCountdownEnabled())) return; - await ensureNotificationPermission(); + if (!(await getCountdownEnabled())) { + logLine('[STATUS] skip: countdown disabled'); + return; + } + const granted = await ensureNotificationPermission(); const ends = args.endTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + logLine( + `[STATUS] show native=${hasNativeCountdown} perm=${granted} end=${args.endTime.toISOString()}`, + ); if (hasNativeCountdown) { // Native chronometer: a live ticking countdown, updated by the OS. - await showCountdown(`Parking · ${args.zoneName}`, `Expires ${ends}`, args.endTime.getTime()); + try { + const diag = await showCountdown( + `Parking · ${args.zoneName}`, + `Expires ${ends}`, + args.endTime.getTime(), + ); + logLine(`[STATUS] native result: ${diag}`); + } catch (e: any) { + logLine(`[STATUS] native threw: ${e?.message ?? e}`); + } return; } @@ -103,6 +118,7 @@ export async function refreshSessionStatus(): Promise { } try { const res = await ps.getActiveParkingSessions(); + logLine(`[STATUS] active sessions raw: ${JSON.stringify(res.ParkingSession ?? [])}`); const withEnd = (res.ParkingSession ?? []) .map((s: any) => ({ s, end: parseApiTime(s.EndTime ?? s.EndTimeDisplay) })) .filter((x) => x.end != null && x.end.getTime() > Date.now()) diff --git a/app/src/screens/SessionDetailScreen.tsx b/app/src/screens/SessionDetailScreen.tsx index eb1e770..2744681 100644 --- a/app/src/screens/SessionDetailScreen.tsx +++ b/app/src/screens/SessionDetailScreen.tsx @@ -21,6 +21,16 @@ type DetailRoute = RouteProp; const show = (v: unknown) => (v == null || v === '' ? '—' : String(v)); const dollars = (v: unknown) => (v == null || v === '' ? undefined : `$${v}`); +/** API's TimeRemaining is in SECONDS — render it as a human "Xh Ym". */ +const fmtRemaining = (secs: unknown): string | undefined => { + const n = Number(secs); + if (!Number.isFinite(n) || n <= 0) return undefined; + const total = Math.round(n / 60); + const h = Math.floor(total / 60); + const m = total % 60; + return h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`; +}; + export function SessionDetailScreen() { const { colors } = useTheme(); const { session, kind } = useRoute().params; @@ -90,7 +100,7 @@ export function SessionDetailScreen() { ['Space', s.SpaceName ?? s.Space], ['Started', s.StartTimeDisplay ?? s.StartTime], ['Ends', s.EndTimeDisplay ?? s.EndTime], - ['Time remaining', s.TimeRemaining != null ? `${s.TimeRemaining} min` : undefined], + ['Time remaining', fmtRemaining(s.TimeRemaining)], ['Amount', dollars(s.Amount)], ]; diff --git a/parksmarter-client/src/types.ts b/parksmarter-client/src/types.ts index 62c922b..db7a37e 100644 --- a/parksmarter-client/src/types.ts +++ b/parksmarter-client/src/types.ts @@ -488,6 +488,7 @@ export interface ActiveSession { EndTime?: string; StartTimeDisplay?: string; EndTimeDisplay?: string; + /** Seconds left on the session (CONFIRMED via a live session — NOT minutes). */ TimeRemaining?: number | string; VehiclePlate?: string; VehicleNumber?: string;