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;