Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c84cce54af | ||
|
|
4f339eba32 | ||
|
|
eb2240b901 | ||
|
|
724131d9a8 |
@@ -121,7 +121,7 @@ See [Logging Configuration Guide](doc/logging-configuration.md) for complete det
|
||||
|
||||
## Notification Debug Panel (dev builds)
|
||||
|
||||
In non-production bundles (for example `vite dev` or a Vite build whose mode is not `production`), the **Notification Debug Panel** at `/dev/notifications` helps you test notification registration, backend refresh, WAKEUP_PING handling, and local schedule inspection on native builds.
|
||||
In non-production bundles (for example `vite dev` or a Vite build whose mode is not `production`), the **Notification Debug Panel** at `/dev/notifications` helps you test FCM token registration, AlertSearch authorization upload, FCM wakeup delivery (`/debug/send-wakeup`), and local schedule inspection on native builds. The app no longer consumes `WAKEUP_PING` to call `/notifications/refresh` or schedule `api_*` notifications.
|
||||
|
||||
**Access:** **Account** → enable **Show All General Advanced Functions** → **Notification Debug Panel**.
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
**Created:** 2026-06-02
|
||||
**Source document:** [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md)
|
||||
**Purpose:** Plan a future **Android** counterpart guide by mapping what can be reused from the iOS ngrok workflow and what must be written for Android-specific push, permissions, and OS behavior.
|
||||
**Status:** Planning snapshot from 2026-06-02. Several iOS-guide assumptions (app **Refresh Notifications**, Simulate WAKEUP, `applyNotificationRefreshPayload`, WAKEUP_PING → `/notifications/refresh` → `api_*`) are **retired**. For current Android procedure see [local-android-testing-ngrok.md](./local-android-testing-ngrok.md). Do not treat the reuse tables below as the live app path.
|
||||
|
||||
**Status:** Planning only — does not replace or modify the iOS guide.
|
||||
**Purpose:** Plan a future **Android** counterpart guide by mapping what can be reused from the iOS ngrok workflow and what must be written for Android-specific push, permissions, and OS behavior.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Local Android Testing with ngrok (notification-wakeup-service)
|
||||
|
||||
**Last updated:** 2026-06-02 (verification checklist, end-to-end test)
|
||||
**Last updated:** 2026-09-24 (retired WAKEUP_PING → refresh → `api_*` consumption)
|
||||
**Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service**
|
||||
**Goal:** Exercise FCM wakeup (`WAKEUP_PING`), FCM token registration, and notification refresh against a Mac-hosted backend reachable from a physical Android device.
|
||||
**Goal:** Exercise FCM token registration and FCM wakeup **delivery** (`WAKEUP_PING` via `/debug/send-wakeup`) against a Mac-hosted backend reachable from a physical Android device.
|
||||
|
||||
> **Retired (do not expect this in the app):** `WAKEUP_PING` → `POST /notifications/refresh` → `applyNotificationRefreshPayload()` → `api_*` local schedules. The app logs ignored push types and does not refresh or schedule from wakeup. `/debug/send-wakeup` remains an FCM diagnostic. Mac `curl` of `/notifications/refresh` only tests the backend. Local schedules come from Static Daily Reminder, New Activity / dual, and the native fetcher.
|
||||
|
||||
**See also:** [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) for the iOS workflow (APNs, Xcode). [android-physical-device-guide.md](./android-physical-device-guide.md) for USB debugging and build/run commands.
|
||||
|
||||
@@ -10,7 +12,7 @@
|
||||
|
||||
## Architecture overview
|
||||
|
||||
End-to-end flow when testing New Activity / silent wake on a physical Android phone:
|
||||
End-to-end flow when testing FCM registration and wakeup **delivery** on a physical Android phone:
|
||||
|
||||
```text
|
||||
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
||||
@@ -19,7 +21,7 @@ End-to-end flow when testing New Activity / silent wake on a physical Android ph
|
||||
│ wakeup-service │ └──────────┬───────────┘
|
||||
└──────────┬──────────┘ │
|
||||
│ │ fetch
|
||||
│ POST /notifications/refresh │ POST /notifications/register
|
||||
│ │ POST /notifications/register
|
||||
│ ▼
|
||||
│ ┌──────────────────────┐
|
||||
│ │ crowd-funder-for- │
|
||||
@@ -28,7 +30,7 @@ End-to-end flow when testing New Activity / silent wake on a physical Android ph
|
||||
│ └──────────┬───────────┘
|
||||
│ │
|
||||
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
||||
▼ ▼ (local schedule replace)
|
||||
▼ ▼ (Daily Reminder / dual / fetcher)
|
||||
┌─────────────────────┐ ┌──────────────────────┐
|
||||
│ Firebase Cloud │ ──FCM────────► │ Android device │
|
||||
│ Messaging │ direct │ app.timesafari.app │
|
||||
@@ -41,18 +43,16 @@ Unlike iOS, Android does **not** use APNs. FCM delivers directly to the app via
|
||||
|
||||
| Repo | Role |
|
||||
|------|------|
|
||||
| **notification-wakeup-service** | HTTP API: device registration, refresh payload (`nextNotifications`), health, debug wakeup send |
|
||||
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register` & `/refresh`, handles `WAKEUP_PING` push |
|
||||
| **daily-notification-plugin** | Native Android: clear + reschedule local notifications from refresh timestamps |
|
||||
| **notification-wakeup-service** | HTTP API: device registration, health, debug wakeup send; may still expose `/notifications/refresh` for the backend |
|
||||
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register`; logs `WAKEUP_PING` without refresh/`api_*` scheduling |
|
||||
| **daily-notification-plugin** | Native Android: Daily Reminder, New Activity / dual, native fetcher; Phase 4 `clearApiNotifications()` |
|
||||
|
||||
### Android wakeup flow (production path)
|
||||
### Android wakeup flow (current)
|
||||
|
||||
1. **notification-wakeup-service** (or `/debug/send-wakeup`) sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
||||
1. **notification-wakeup-service** `/debug/send-wakeup` sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
||||
2. FCM delivers to the device (best-effort; see [Android Platform Notes](#8-android-platform-notes) and [Battery Optimization Caveats](#9-battery-optimization-caveats)).
|
||||
3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()` in `NativeNotificationService.ts`.
|
||||
4. Handler calls `refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })`, which `POST`s `{backend}/notifications/refresh` with `testMode` from the debug config.
|
||||
5. Backend returns `nextNotifications: [{ timestamp }, ...]`.
|
||||
6. App calls `applyNotificationRefreshPayload()` → **daily-notification-plugin** clears existing local alarms and schedules new ones.
|
||||
4. Handler logs `[Notifications] push handler ignored type=WAKEUP_PING`. It does **not** POST `/notifications/refresh` or schedule `api_*` notifications.
|
||||
|
||||
Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`).
|
||||
|
||||
@@ -290,12 +290,11 @@ For a full panel reference (configuration, URL resolution order, authentication,
|
||||
| Control | Purpose |
|
||||
|---------|---------|
|
||||
| **Notification Backend URL** | Paste ngrok HTTPS URL → **Save Backend URL** (changes target server only) |
|
||||
| **Test Mode** | Sends `testMode: true/false` in register/refresh JSON bodies (default on when unset in storage) |
|
||||
| **Test Mode** | Sends `testMode: true/false` in register / send-wakeup JSON bodies (default on when unset in storage) |
|
||||
| **Skip JWT Authentication (Local Development Only)** | When on, omits `Authorization` headers for local servers that accept unauthenticated requests (default **off**) |
|
||||
| **Register Token Now** | `POST /notifications/register` with current FCM token and `platform: "android"` |
|
||||
| **Refresh Notifications** | `POST /notifications/refresh` (same as post-wakeup flow) |
|
||||
| **Simulate WAKEUP_PING (Local)** | Calls refresh API directly (no FCM) — quick backend + ngrok test |
|
||||
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup` on the backend override URL; server sends a real FCM `WAKEUP_PING` to the panel’s current FCM token (see below) |
|
||||
| **Upload AlertSearch Authorization** | Uploads AlertSearch delegated JWTs (`/notifications/alert-authorization`) |
|
||||
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup`; FCM delivery diagnostic only (no app-side refresh/`api_*` scheduling) |
|
||||
| **Event Log** | Shared `[Notifications]` panel log (100 entries) |
|
||||
|
||||
Persistence: `localStorage` keys `notificationDebug.backendBaseUrl`, `notificationDebug.testMode`, and `notificationDebug.bypassAuth` (`NotificationDebugConfig.ts`).
|
||||
@@ -314,19 +313,13 @@ For **local ngrok / localhost**: set the backend URL to your tunnel or `http://1
|
||||
|
||||
### testMode
|
||||
|
||||
When **Test Mode** is on (default if never saved), register and refresh requests include `"testMode": true`. The backend can route dev traffic separately from production. Turn it off in the panel only if you intentionally want production-mode API behavior against your tunnel.
|
||||
When **Test Mode** is on (default if never saved), register and send-wakeup requests include `"testMode": true`. The backend can route dev traffic separately from production. Turn it off in the panel only if you intentionally want production-mode API behavior against your tunnel.
|
||||
|
||||
### WAKEUP_PING debug controls
|
||||
|
||||
Three panel actions exercise different segments of the wakeup pipeline. Use them to bisect failures (see [Troubleshooting §14](#fcm-message-not-received)).
|
||||
**Send Real WAKEUP_PING** is the remaining panel control for wakeup. Mock refresh, Simulate WAKEUP_PING, Wakeup Ping Simulator, and Flood Test were removed with the retired refresh pipeline.
|
||||
|
||||
| Button | Behavior |
|
||||
|--------|----------|
|
||||
| **Backend Testing → Simulate WAKEUP_PING (Local)** | Skips FCM; calls refresh API only (ngrok path test) |
|
||||
| **Backend Testing → Send Real WAKEUP_PING** | Full pipeline: backend `/debug/send-wakeup` → FCM → Capacitor listener → refresh (see below) |
|
||||
| **Wakeup Ping Simulator** (lower on panel) | Runs production handler with synthetic `WAKEUP_PING` payload (no FCM, no backend wakeup call) |
|
||||
|
||||
Use **Simulate WAKEUP_PING (Local)** to verify ngrok + refresh; use **Wakeup Ping Simulator** to verify handler + refresh chaining without FCM; use **Send Real WAKEUP_PING** for end-to-end FCM delivery on device.
|
||||
Use **Send Real WAKEUP_PING** to confirm backend → FCM → Capacitor listener delivery. Do not expect `/notifications/refresh` or `api_*` rescheduling.
|
||||
|
||||
### Send Real WAKEUP_PING
|
||||
|
||||
@@ -337,9 +330,9 @@ Use **Simulate WAKEUP_PING (Local)** to verify ngrok + refresh; use **Wakeup Pin
|
||||
1. Reads the **Current FCM Token** shown in the panel (same token used by **Register Token Now**). If no token is available, the action fails with a panel status message.
|
||||
2. `POST`s to `{backend override}/debug/send-wakeup` with `deviceId`, `fcmToken`, `platform: "android"`, and `testMode` from the panel config (`NotificationDebugService.sendRealWakeupPing()`).
|
||||
3. On HTTP success, the **notification-wakeup-service** enqueues a real FCM **data** message with `data.type = "WAKEUP_PING"` to that device.
|
||||
4. When FCM delivers the message to the app, Capacitor fires `pushNotificationReceived` → `handleCapacitorPushNotificationReceived()` → `refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })` → `POST /notifications/refresh` → `applyNotificationRefreshPayload()` (clear + reschedule via **daily-notification-plugin**).
|
||||
4. When FCM delivers the message to the app, Capacitor fires `pushNotificationReceived` → `handleCapacitorPushNotificationReceived()`, which logs `push handler ignored type=WAKEUP_PING`. There is no `refreshNotificationsWithDiagnostics` and no `applyNotificationRefreshPayload`.
|
||||
|
||||
That exercises the full **backend → FCM → Capacitor push listener → refresh request → notification rescheduling** path on Android without manual `curl` on the Mac.
|
||||
That exercises **backend → FCM → Capacitor push listener** on Android without manual `curl` on the Mac. It does **not** reschedule local notifications.
|
||||
|
||||
**Prerequisites:** ngrok backend override saved, **Test Mode** as needed, notification permission granted, **Register Token Now** succeeded, app **backgrounded** (Home — not force-stop) before expecting FCM delivery ([§8](#8-android-platform-notes)).
|
||||
|
||||
@@ -348,27 +341,24 @@ That exercises the full **backend → FCM → Capacitor push listener → refres
|
||||
Filter logcat (prefix is always `[Notifications]`):
|
||||
|
||||
```bash
|
||||
adb logcat | grep -E '\[Notifications\].*(Real WAKEUP_PING|pushNotificationReceived|WAKEUP_PING|Refresh started|Refresh completed)'
|
||||
adb logcat | grep -E '\[Notifications\].*(Real WAKEUP_PING|push handler ignored)'
|
||||
```
|
||||
|
||||
On a **successful end-to-end** run (HTTP success from the panel, then FCM delivery within ~30–120s), expect these key lines in order:
|
||||
On a **successful FCM delivery** run (HTTP success from the panel, then FCM delivery within ~30–120s), expect these key lines in order:
|
||||
|
||||
```
|
||||
[Notifications] Real WAKEUP_PING requested
|
||||
[Notifications] Real WAKEUP_PING success
|
||||
[Notifications] pushNotificationReceived type=WAKEUP_PING
|
||||
[Notifications] WAKEUP_PING handler — invoking refresh
|
||||
[Notifications] Refresh started (WAKEUP_PING)
|
||||
[Notifications] Refresh completed (WAKEUP_PING) in …ms (scheduled N)
|
||||
[Notifications] push handler ignored type=WAKEUP_PING
|
||||
```
|
||||
|
||||
Intermediate lines (e.g. `WAKEUP_PING received — will trigger refresh`, `Schedule replacement: …`, `Using authenticated notification request` or auth bypass messages when **Skip JWT Authentication** is on) are normal. ngrok should also show a new `POST /notifications/refresh` after the push is handled.
|
||||
Auth bypass messages when **Skip JWT Authentication** is on are normal. ngrok should **not** show a new `POST /notifications/refresh` from the app after the push.
|
||||
|
||||
#### Send Real WAKEUP_PING vs end-to-end success
|
||||
#### Send Real WAKEUP_PING vs delivery success
|
||||
|
||||
The panel status **“Real WAKEUP_PING sent via backend.”** and the Event Log line **`Real WAKEUP_PING success`** only confirm that the backend **accepted the request and attempted FCM delivery**. They do **not** prove the device received the push or ran refresh.
|
||||
The panel status **“Real WAKEUP_PING sent via backend.”** and the Event Log line **`Real WAKEUP_PING success`** only confirm that the backend **accepted the request and attempted FCM delivery**. They do **not** prove the device received the push.
|
||||
|
||||
**Successful end-to-end delivery** is confirmed only when the subsequent **`pushNotificationReceived`**, **`WAKEUP_PING handler`**, and **`Refresh started (WAKEUP_PING)`** / **`Refresh completed (WAKEUP_PING)`** lines appear in logcat (or Event Log) within the delivery window. If you see `Real WAKEUP_PING success` but not those lines, treat it as an FCM delivery problem ([FCM message not received](#fcm-message-not-received)), not a backend enqueue failure.
|
||||
**Successful delivery** is confirmed when **`push handler ignored type=WAKEUP_PING`** appears in logcat (or Event Log) within the delivery window. If you see `Real WAKEUP_PING success` but not that line, treat it as an FCM delivery problem ([FCM message not received](#fcm-message-not-received)), not a backend enqueue failure.
|
||||
|
||||
### Programmatic override (optional)
|
||||
|
||||
@@ -455,15 +445,11 @@ On **API 33+**, `POST_NOTIFICATIONS` is a runtime permission ([section 7](#7-and
|
||||
|
||||
### Network connectivity
|
||||
|
||||
FCM and your ngrok-backed refresh both need network:
|
||||
|
||||
- Device must reach **Google’s FCM endpoints** (mobile data or Wi‑Fi).
|
||||
- After wake, the app must reach the **ngrok HTTPS URL** for `POST /notifications/refresh`.
|
||||
- Airplane mode, captive portals, VPNs, or flaky Wi‑Fi cause “server sent wakeup but app never refreshed” symptoms even when FCM eventually arrives.
|
||||
FCM needs network to Google’s endpoints. Registration needs the ngrok HTTPS URL for `POST /notifications/register`. Airplane mode, captive portals, VPNs, or flaky Wi‑Fi cause “server sent wakeup but app never logged ignored type” symptoms even when FCM eventually arrives.
|
||||
|
||||
### Physical device vs emulator
|
||||
|
||||
Doze, App Standby, and OEM battery menus are weak or absent on many emulators. Use a **physical device** for wakeup SLA testing; emulators are fine for panel-only register/refresh smoke tests.
|
||||
Doze, App Standby, and OEM battery menus are weak or absent on many emulators. Use a **physical device** for wakeup SLA testing; emulators are fine for panel-only register smoke tests.
|
||||
|
||||
### Logcat
|
||||
|
||||
@@ -471,7 +457,7 @@ Doze, App Standby, and OEM battery menus are weak or absent on many emulators. U
|
||||
adb logcat | grep -E '\[Notifications\]|\[FirebaseMessaging\]|\[NativeNotificationService\]'
|
||||
```
|
||||
|
||||
Expect `pushNotificationReceived type=WAKEUP_PING` and `WAKEUP_PING handler — invoking refresh` after a successful wake. For a panel-driven test, use **Send Real WAKEUP_PING** ([§6](#send-real-wakeup_ping)) instead of manual `curl`.
|
||||
Expect `push handler ignored type=WAKEUP_PING` after a successful wake. For a panel-driven test, use **Send Real WAKEUP_PING** ([§6](#send-real-wakeup_ping)) instead of manual `curl`.
|
||||
|
||||
---
|
||||
|
||||
@@ -499,7 +485,7 @@ Apps used infrequently move to **standby** buckets with reduced background netwo
|
||||
|
||||
### Manufacturer-specific restrictions
|
||||
|
||||
OEMs add layers on top of AOSP. Aggressive battery management can delay **WAKEUP_PING** delivery or prevent the refresh `fetch` from running promptly.
|
||||
OEMs add layers on top of AOSP. Aggressive battery management can delay **WAKEUP_PING** delivery.
|
||||
|
||||
| Vendor | Where to look (names vary by OS version) |
|
||||
|--------|------------------------------------------|
|
||||
@@ -515,8 +501,8 @@ If wakeup works on a **Pixel** but fails on an OEM phone, assume battery policy
|
||||
|
||||
1. Server accepts `/debug/send-wakeup` → FCM enqueue succeeds.
|
||||
2. Device may **hold** the message until Doze maintenance or OEM policy allows delivery.
|
||||
3. `pushNotificationReceived` runs → `refreshNotificationsWithDiagnostics()` → ngrok `POST /notifications/refresh`.
|
||||
4. Any step can lag under battery savers; use **Simulate WAKEUP_PING (Local)** in the debug panel to separate FCM delay from refresh/API issues; use **Send Real WAKEUP_PING** for the full FCM path ([§6](#send-real-wakeup_ping)).
|
||||
3. `pushNotificationReceived` runs → `push handler ignored type=WAKEUP_PING`.
|
||||
4. Any step can lag under battery savers; use **Send Real WAKEUP_PING** for the FCM path ([§6](#send-real-wakeup_ping)).
|
||||
|
||||
---
|
||||
|
||||
@@ -528,12 +514,10 @@ If wakeup works on a **Pixel** but fails on an OEM phone, assume battery policy
|
||||
4. Grant notification permission when prompted (or enable in Settings).
|
||||
5. Open **Notification Debug Panel** → paste ngrok URL → **Save Backend URL**; confirm **Test Mode** is on; enable **Skip JWT Authentication** only if your local backend accepts unauthenticated requests.
|
||||
6. Tap **Register Token Now** → confirm ngrok `POST /notifications/register` and `[Notifications] Token registration success` in Event Log / logcat.
|
||||
7. Tap **Refresh Notifications** → confirm `POST /notifications/refresh` and `Refresh completed in Nms (scheduled X)` in Event Log.
|
||||
8. Optional: tap **Simulate WAKEUP_PING (Local)** to verify ngrok + refresh without FCM.
|
||||
9. Background the app (Home), then tap **Send Real WAKEUP_PING** (or from the Mac, call **`/debug/send-wakeup`** — see [curl examples](#13-sample-curl-commands)) with the registered `deviceId` / token as required by **notification-wakeup-service**.
|
||||
10. Watch logcat for `WAKEUP_PING` and `Refresh completed (WAKEUP_PING)` lines ([expected output](#expected-logcat-output)).
|
||||
11. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate HTTP traffic.
|
||||
12. Use **Pending Notification Inspector** on the panel to confirm locally scheduled fires after refresh.
|
||||
7. Background the app (Home), then tap **Send Real WAKEUP_PING** (or from the Mac, call **`/debug/send-wakeup`** — see [curl examples](#13-sample-curl-commands)) with the registered `deviceId` / token as required by **notification-wakeup-service**.
|
||||
8. Watch logcat for `push handler ignored type=WAKEUP_PING` ([expected output](#expected-logcat-output)).
|
||||
9. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate HTTP traffic (register and send-wakeup; not app-initiated refresh).
|
||||
10. Use **Pending Notification Inspector** for Daily Reminder / New Activity / dual schedules — not for retired `api_*` refresh.
|
||||
|
||||
For a formal pass/fail sequence, use the [Verification Checklist](#11-verification-checklist) below.
|
||||
|
||||
@@ -549,12 +533,12 @@ Use this checklist during development or QA sign-off. Each step lists **actions*
|
||||
| 2 | Device → backend override | §2 |
|
||||
| 3 | FCM token registered | §3 |
|
||||
| 4 | Device record in wakeup service | §4 |
|
||||
| 5 | Refresh returns schedule data | §5 |
|
||||
| 6 | Locals scheduled | §6 |
|
||||
| 5 | Optional backend `/notifications/refresh` curl | §5 (backend only; app does not call this) |
|
||||
| 6 | Locals scheduled | §6 (Daily Reminder / dual / fetcher — not refresh) |
|
||||
| 7 | Manual wakeup sends FCM | §7 |
|
||||
| 8 | WAKEUP_PING → refresh | §8 |
|
||||
| 9 | Replace after refresh | §9 |
|
||||
| 10 | Test mode frequent refreshes | §10 |
|
||||
| 8 | WAKEUP_PING delivered | §8 |
|
||||
| 9 | _(retired)_ Replace after refresh | skipped |
|
||||
| 10 | _(retired)_ Test mode frequent refreshes | skipped |
|
||||
|
||||
### 1. Backend reachable through ngrok
|
||||
|
||||
@@ -578,7 +562,7 @@ curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
|
||||
3. Confirm **Backend Status → URL** matches the saved ngrok host.
|
||||
4. Enable **Test Mode** if using dev backend behavior.
|
||||
|
||||
**Expected outcome:** **Active** URL in the panel equals your ngrok `https://…` host. Subsequent app requests use that base (not the default `DEFAULT_NOTIFY_API_SERVER`) for `/notifications/register` and `/notifications/refresh`.
|
||||
**Expected outcome:** **Active** URL in the panel equals your ngrok `https://…` host. Subsequent app requests use that base (not the default `DEFAULT_NOTIFY_API_SERVER`) for `/notifications/register` and `/debug/send-wakeup`.
|
||||
|
||||
---
|
||||
|
||||
@@ -611,12 +595,11 @@ curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
|
||||
|
||||
---
|
||||
|
||||
### 5. Refresh endpoint returns schedule data
|
||||
### 5. Refresh endpoint (backend only; app does not call this)
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Tap **Refresh Notifications** in the panel (or curl below).
|
||||
2. Inspect ngrok response body.
|
||||
Mac-side curl against **notification-wakeup-service** if you need to confirm the backend still answers. The app has no **Refresh Notifications** button and does not schedule from this payload.
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$BASE/notifications/refresh" \
|
||||
@@ -624,18 +607,18 @@ curl -sS -X POST "$BASE/notifications/refresh" \
|
||||
-d '{"platform":"android","testMode":true}'
|
||||
```
|
||||
|
||||
**Expected outcome:** HTTP **200**; JSON includes `nextNotifications` array with at least one `{ "timestamp": <number> }` in the **future** (epoch ms). Event Log: `Refresh completed in …ms (scheduled N)` with **N ≥ 1**. If `scheduled 0` or empty array, fix backend auth, DID/native fetcher, or `testMode` handling before scheduling tests.
|
||||
**Expected outcome:** HTTP **200** from the **service** if that route is still deployed. Do **not** expect Event Log `Refresh completed` or `api_*` schedules in the app.
|
||||
|
||||
---
|
||||
|
||||
### 6. Local notifications are scheduled
|
||||
### 6. Local notifications are scheduled (not from refresh)
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. After a successful refresh (step 5), open **Pending Notification Inspector** → **Refresh** (list button).
|
||||
2. Optionally cross-check logcat for `Schedule replacement applied (N timestamp(s))`.
|
||||
1. Configure Static Daily Reminder and/or New Activity (dual) in the app, or rely on the native fetcher.
|
||||
2. Open **Pending Notification Inspector** → **Refresh** (list button).
|
||||
|
||||
**Expected outcome:** Inspector lists **N** pending item(s) matching refresh count; each row has a **future** `nextTriggerDate` / wall-clock time. No `Schedule replacement aborted` or `skipped (no valid timestamps)` in Event Log.
|
||||
**Expected outcome:** Inspector lists pending Daily Reminder / dual / fetcher items. Do not expect `api_*` identifiers after Phase 4 startup cleanup. There is no `Schedule replacement applied` from WAKEUP_PING.
|
||||
|
||||
---
|
||||
|
||||
@@ -656,11 +639,11 @@ curl -sS -X POST "$BASE/debug/send-wakeup" \
|
||||
|
||||
3. Check **notification-wakeup-service** logs for FCM send success (no Admin SDK / token errors).
|
||||
|
||||
**Expected outcome:** HTTP **200** (or documented success code) from `/debug/send-wakeup`; Event Log / logcat: `Real WAKEUP_PING success` when using the panel button; server logs indicate FCM message enqueued/sent with `data.type = "WAKEUP_PING"`. This step alone does not prove device delivery (see step 8 and [Send Real WAKEUP_PING vs end-to-end success](#send-real-wakeup_ping-vs-end-to-end-success)).
|
||||
**Expected outcome:** HTTP **200** (or documented success code) from `/debug/send-wakeup`; Event Log / logcat: `Real WAKEUP_PING success` when using the panel button; server logs indicate FCM message enqueued/sent with `data.type = "WAKEUP_PING"`. This step alone does not prove device delivery (see step 8 and [Send Real WAKEUP_PING vs delivery success](#send-real-wakeup_ping-vs-delivery-success)).
|
||||
|
||||
---
|
||||
|
||||
### 8. WAKEUP_PING triggers refreshNotifications()
|
||||
### 8. WAKEUP_PING is delivered (no refresh)
|
||||
|
||||
**Actions:**
|
||||
|
||||
@@ -669,42 +652,28 @@ curl -sS -X POST "$BASE/debug/send-wakeup" \
|
||||
3. Filter logcat:
|
||||
|
||||
```bash
|
||||
adb logcat | grep -E 'WAKEUP_PING|pushNotificationReceived|Refresh completed'
|
||||
adb logcat | grep -E 'WAKEUP_PING|push handler ignored'
|
||||
```
|
||||
|
||||
**Expected outcome (within ~30–120s, longer under Doze/OEM):**
|
||||
|
||||
1. `pushNotificationReceived type=WAKEUP_PING` / `WAKEUP_PING received`
|
||||
2. `WAKEUP_PING handler — invoking refresh`
|
||||
3. `Refresh started (WAKEUP_PING)`
|
||||
4. ngrok: new `POST /notifications/refresh`
|
||||
5. `Refresh completed (WAKEUP_PING) in …ms (scheduled N)`
|
||||
1. `Real WAKEUP_PING success` (panel path)
|
||||
2. `push handler ignored type=WAKEUP_PING`
|
||||
3. ngrok: **no** app-initiated `POST /notifications/refresh`
|
||||
|
||||
**Isolation:** **Wakeup Ping Simulator** on the panel should produce lines 2–5 without FCM. **Simulate WAKEUP_PING (Local)** proves refresh only (no handler, source `WAKEUP_PING simulation`). Full expected logcat for **Send Real WAKEUP_PING**: [§6](#expected-logcat-output).
|
||||
Full expected logcat for **Send Real WAKEUP_PING**: [§6](#expected-logcat-output).
|
||||
|
||||
---
|
||||
|
||||
### 9. Existing notifications are replaced after refresh
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Note pending count and identifiers in **Pending Notification Inspector**.
|
||||
2. Tap **Refresh Notifications** again (or trigger step 8).
|
||||
3. Refresh inspector; compare IDs/times to step 1.
|
||||
|
||||
**Expected outcome:** Event Log shows `clearAllNotifications` or `cancelAllNotifications` then `Schedule replacement applied`; pending list reflects **new** timestamps (old alarms not accumulated). Total pending count should match latest refresh `N`, not double from duplicate refreshes unless backend returned more slots.
|
||||
**Retired.** The app no longer replaces local schedules from `/notifications/refresh`. Pending list changes come from Daily Reminder / dual / native fetcher, or from Phase 4 `clearApiNotifications()`.
|
||||
|
||||
---
|
||||
|
||||
### 10. Test mode produces frequent notification refreshes
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Confirm **Test Mode** checked; **Backend Status → testMode: true**.
|
||||
2. Call refresh twice (panel or curl) with `testMode: true`.
|
||||
3. Compare `nextNotifications` timestamps in ngrok responses.
|
||||
|
||||
**Expected outcome:** With **testMode: true**, **notification-wakeup-service** returns **dev-friendly** schedule data—typically **sooner** fire times than production mode (shorter horizons). Pending Inspector updates to nearer triggers after each refresh. With Test Mode **off**, timestamps should be farther out (production-like); use that contrast to confirm the flag is wired end-to-end.
|
||||
**Retired.** `testMode` still goes on register and send-wakeup bodies. It does not drive app-side `api_*` refresh cadences.
|
||||
|
||||
---
|
||||
|
||||
@@ -739,31 +708,23 @@ npm run dev
|
||||
|
||||
8. Paste `BASE` → **Save Backend URL**; enable **Test Mode**.
|
||||
|
||||
### Phase C — Register and schedule
|
||||
### Phase C — Register
|
||||
|
||||
9. **Register Token Now** → Event Log success; ngrok `POST /notifications/register` **200**; copy `deviceId` from ngrok request body.
|
||||
|
||||
10. Confirm device in **notification-wakeup-service** (logs/DB per that repo).
|
||||
|
||||
11. **Refresh Notifications** → Event Log `scheduled N`, N ≥ 1; ngrok refresh **200** with `nextNotifications`.
|
||||
### Phase D — FCM wakeup delivery
|
||||
|
||||
12. **Pending Notification Inspector** → **Refresh** → future alarm(s) listed.
|
||||
11. Background app (Home).
|
||||
|
||||
### Phase D — FCM wakeup path
|
||||
12. Tap **Send Real WAKEUP_PING** in the panel (or Mac: `curl -X POST "$BASE/debug/send-wakeup" -H "Content-Type: application/json" -d '{"deviceId":"…","testMode":true}'`) → panel `Real WAKEUP_PING success` and server FCM enqueue success.
|
||||
|
||||
13. Background app (Home).
|
||||
13. Within 30–120s (longer if unplugged/Doze): logcat shows `push handler ignored type=WAKEUP_PING`; ngrok does **not** show an app `POST /notifications/refresh`.
|
||||
|
||||
14. Tap **Send Real WAKEUP_PING** in the panel (or Mac: `curl -X POST "$BASE/debug/send-wakeup" -H "Content-Type: application/json" -d '{"deviceId":"…","testMode":true}'`) → panel `Real WAKEUP_PING success` and server FCM enqueue success.
|
||||
### Phase E — Optional local notification proof
|
||||
|
||||
15. Within 30–120s (longer if unplugged/Doze): logcat shows `pushNotificationReceived` → `Refresh completed (WAKEUP_PING)`; ngrok shows second `POST /notifications/refresh`.
|
||||
|
||||
16. Pending Inspector **Refresh** → timestamps updated (replacement, not duplicate stack).
|
||||
|
||||
### Phase E — Optional delivery proof
|
||||
|
||||
17. If `testMode` returned a trigger within a few minutes, wait for wall-clock fire with app backgrounded; confirm notification appears (permission + channel + exact alarm rules).
|
||||
|
||||
18. If FCM step 15 failed but step 11 passed: run **Simulate WAKEUP_PING (Local)** then **Wakeup Ping Simulator** to bisect FCM vs handler; see [Troubleshooting](#14-troubleshooting) and [Send Real WAKEUP_PING vs end-to-end success](#send-real-wakeup_ping-vs-end-to-end-success).
|
||||
14. Enable Daily Reminder or New Activity; wait for wall-clock fire with app backgrounded; confirm the OS notification is **not** the retired generic `api_*` “Reminder” copy.
|
||||
|
||||
### End-to-end pass criteria
|
||||
|
||||
@@ -771,9 +732,9 @@ npm run dev
|
||||
|-------|------|
|
||||
| A | Health OK local + ngrok |
|
||||
| B | Override URL + testMode active |
|
||||
| C | Register + refresh + pending list populated |
|
||||
| D | Wakeup curl OK → logcat refresh chain → pending updated |
|
||||
| E | Optional visible notification at scheduled time |
|
||||
| C | Register succeeded |
|
||||
| D | Wakeup curl/panel OK → logcat ignored-type line |
|
||||
| E | Optional visible Daily Reminder / New Activity notification |
|
||||
|
||||
---
|
||||
|
||||
@@ -804,7 +765,7 @@ curl -sS -X POST "$BASE/notifications/register" \
|
||||
}'
|
||||
```
|
||||
|
||||
### Refresh (mirror app payload)
|
||||
### Refresh (backend only; app does not consume)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$BASE/notifications/refresh" \
|
||||
@@ -815,19 +776,7 @@ curl -sS -X POST "$BASE/notifications/refresh" \
|
||||
}'
|
||||
```
|
||||
|
||||
Example success body shape (actual fields may vary by service version):
|
||||
|
||||
```json
|
||||
{
|
||||
"shouldNotify": true,
|
||||
"nextNotifications": [
|
||||
{ "timestamp": 1710000000000 },
|
||||
{ "timestamp": 1710003600000 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The app schedules those timestamps via **daily-notification-plugin** (`applyNotificationRefreshPayload` in `NativeNotificationService.ts`).
|
||||
This exercises **notification-wakeup-service** only. The app does **not** call `applyNotificationRefreshPayload` or schedule `api_*` from the response.
|
||||
|
||||
### Send wakeup push (debug)
|
||||
|
||||
@@ -842,7 +791,7 @@ curl -sS -X POST "$BASE/debug/send-wakeup" \
|
||||
}'
|
||||
```
|
||||
|
||||
Confirm parameters (token vs `deviceId`, auth headers) in that repo’s README or OpenAPI spec. The server must send FCM data including `type: "WAKEUP_PING"` to match `handleCapacitorPushNotificationReceived`.
|
||||
Confirm parameters (token vs `deviceId`, auth headers) in that repo’s README or OpenAPI spec. The server may still send FCM data including `type: "WAKEUP_PING"`; the app logs it as ignored and does not refresh.
|
||||
|
||||
---
|
||||
|
||||
@@ -899,35 +848,28 @@ Compare with panel **Backend Status** and Event Log error text.
|
||||
|
||||
### Refresh endpoint failures
|
||||
|
||||
**Symptoms:** **Refresh Notifications** fails; Event Log HTTP error; no `scheduled X` line; ngrok missing `POST /notifications/refresh`.
|
||||
**Symptoms:** Mac `curl` of `/notifications/refresh` fails; ngrok missing that path. This is a **backend** issue. The app does not POST refresh.
|
||||
|
||||
**Likely causes:** Stale ngrok URL; backend down; 404/wrong path; JWT/native fetcher not configured; refresh auth failure.
|
||||
**Verification:** `curl` health and refresh from the Mac; confirm the service still ships that route.
|
||||
|
||||
**Verification:**
|
||||
|
||||
1. **Simulate WAKEUP_PING (Local)** — if this fails, problem is ngrok/refresh API, not FCM.
|
||||
2. ngrok inspect for refresh status code and response body.
|
||||
3. Logcat: `refreshNotifications failed` or JWT errors from `configureNativeFetcherIfReady()`.
|
||||
|
||||
**Fixes:** Fix backend URL and health; ensure active DID and endorser settings ([notification-from-api-call.md](./notification-from-api-call.md)); confirm `testMode` if backend requires it.
|
||||
**Fixes:** Fix backend URL and health. App scheduling uses Daily Reminder / dual / native fetcher, not this payload.
|
||||
|
||||
---
|
||||
|
||||
### FCM message not received
|
||||
|
||||
**Symptoms:** `/debug/send-wakeup` or panel **Send Real WAKEUP_PING** shows success (`Real WAKEUP_PING success` in Event Log); no `pushNotificationReceived` / `WAKEUP_PING` in logcat within 2 minutes; refresh never triggered.
|
||||
**Symptoms:** `/debug/send-wakeup` or panel **Send Real WAKEUP_PING** shows success (`Real WAKEUP_PING success` in Event Log); no `push handler ignored type=WAKEUP_PING` in logcat within 2 minutes.
|
||||
|
||||
**Likely causes:** Force-stopped app; wrong FCM token on server; Doze/OEM delay; no Google Play services; payload missing `data.type = "WAKEUP_PING"`; device offline.
|
||||
|
||||
**Note:** `Real WAKEUP_PING success` only means the backend accepted and sent the FCM request. Missing downstream refresh logs indicates a **delivery** failure, not a failed wakeup API call ([§6](#send-real-wakeup_ping-vs-end-to-end-success)).
|
||||
**Note:** `Real WAKEUP_PING success` only means the backend accepted and sent the FCM request. Missing the ignored-type log indicates a **delivery** failure, not a failed wakeup API call ([§6](#send-real-wakeup_ping-vs-delivery-success)).
|
||||
|
||||
**Verification:**
|
||||
|
||||
1. App **backgrounded** (Home), not force-stopped.
|
||||
2. Panel FCM token matches token used by server/register.
|
||||
3. **Simulate WAKEUP_PING (Local)** works → isolates FCM path from refresh/API.
|
||||
4. Wait 30–120s (longer on Doze/OEM).
|
||||
5. Battery **Unrestricted** and OEM autostart enabled for test device.
|
||||
3. Wait 30–120s (longer on Doze/OEM).
|
||||
4. Battery **Unrestricted** and OEM autostart enabled for test device.
|
||||
|
||||
**Fixes:** Re-register token; relaunch app; relax battery settings ([section 9](#9-battery-optimization-caveats)); confirm **notification-wakeup-service** message format; test on Pixel vs suspect OEM policy.
|
||||
|
||||
@@ -947,30 +889,28 @@ Compare with panel **Backend Status** and Event Log error text.
|
||||
|
||||
### Notifications not appearing
|
||||
|
||||
**Symptoms:** Refresh succeeds (`scheduled X` in log) but no visible notification at fire time; Pending Inspector empty or stale.
|
||||
**Symptoms:** Daily Reminder or New Activity configured but no visible notification at fire time; Pending Inspector empty or stale.
|
||||
|
||||
**Likely causes:** Permission denied (display blocked); exact alarm permission on Android 12+; timestamps in past; plugin schedule error; DND/channel settings.
|
||||
|
||||
**Verification:**
|
||||
|
||||
1. Permission granted ([section 7](#7-android-notification-permissions)).
|
||||
2. Pending Notification Inspector after refresh.
|
||||
3. Logcat: `Schedule replacement applied` vs aborted messages.
|
||||
4. Confirm `nextNotifications` timestamps are in the future (curl refresh response).
|
||||
2. Pending Notification Inspector for Daily Reminder / dual identifiers (not `api_*` after Phase 4 cleanup).
|
||||
|
||||
**Fixes:** Grant `POST_NOTIFICATIONS`; check `SCHEDULE_EXACT_ALARM` / alarm permission per plugin docs; fix refresh payload; test with nearer timestamps via backend `testMode`.
|
||||
**Fixes:** Grant `POST_NOTIFICATIONS`; check `SCHEDULE_EXACT_ALARM` / alarm permission per plugin docs. Do not expect wakeup refresh payloads to populate the inspector.
|
||||
|
||||
---
|
||||
|
||||
### Duplicate notifications
|
||||
|
||||
**Symptoms:** Multiple identical local notifications; Event Log shows repeated refresh lines.
|
||||
**Symptoms:** Multiple identical local notifications.
|
||||
|
||||
**Likely causes:** Multiple `WAKEUP_PING` deliveries; repeated manual **Refresh**; flood test; separate Daily Reminder vs New Activity schedules.
|
||||
**Likely causes:** Separate Daily Reminder vs New Activity schedules; duplicate dual config; leftover `api_*` before Phase 4 cleanup.
|
||||
|
||||
**Verification:** Event Log count of refresh completions; ngrok inspect for duplicate `POST /notifications/refresh`.
|
||||
**Verification:** Pending Inspector identifiers; see [notification-new-activity-lay-of-the-land.md](./notification-new-activity-lay-of-the-land.md) for product-level double-schedule issues.
|
||||
|
||||
**Fixes:** Each refresh should **replace** schedule (clear + schedule)—if duplicates persist, check plugin logs; see [notification-new-activity-lay-of-the-land.md](./notification-new-activity-lay-of-the-land.md) for product-level double-schedule issues.
|
||||
**Fixes:** Confirm Daily Reminder vs dual settings. WAKEUP_PING no longer stacks `api_*` refreshes.
|
||||
|
||||
---
|
||||
|
||||
@@ -1006,7 +946,7 @@ Compare with panel **Backend Status** and Event Log error text.
|
||||
| `src/services/notifications/NotificationDebugEvents.ts` | Panel event log + `logNotification()` |
|
||||
| `src/services/notifications/notificationLog.ts` | Structured log helpers |
|
||||
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
||||
| `src/services/notifications/NativeNotificationService.ts` | `refreshNotifications`, `WAKEUP_PING`, `applyNotificationRefreshPayload` |
|
||||
| `src/services/notifications/NativeNotificationService.ts` | Push delivery hook (logs ignored types; no refresh) |
|
||||
| `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners, permission, token registration |
|
||||
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
||||
| `src/main.capacitor.ts` | Native push init at startup |
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# Local iOS Testing with ngrok (notification-wakeup-service)
|
||||
|
||||
**Last updated:** 2026-05-18
|
||||
**Last updated:** 2026-09-24 (retired WAKEUP_PING → refresh → `api_*` consumption)
|
||||
**Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service**
|
||||
**Goal:** Exercise silent push wake (`WAKEUP_PING`), FCM token registration, and notification refresh against a Mac-hosted backend reachable from a physical iPhone.
|
||||
**Goal:** Exercise FCM token registration and silent-push **delivery** (`WAKEUP_PING` via `/debug/send-wakeup`) against a Mac-hosted backend reachable from a physical iPhone.
|
||||
|
||||
> **Retired (do not expect this in the app):** `WAKEUP_PING` → `POST /notifications/refresh` → `applyNotificationRefreshPayload()` → `api_*` local schedules. The app logs ignored push types and does not refresh or schedule from wakeup. `/debug/send-wakeup` remains an FCM/APNs diagnostic. Mac `curl` of `/notifications/refresh` only tests the backend.
|
||||
|
||||
---
|
||||
|
||||
## Architecture overview
|
||||
|
||||
End-to-end flow when testing New Activity / silent wake on a physical iPhone:
|
||||
End-to-end flow when testing FCM registration and wakeup **delivery** on a physical iPhone:
|
||||
|
||||
```text
|
||||
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
||||
@@ -17,7 +19,7 @@ End-to-end flow when testing New Activity / silent wake on a physical iPhone:
|
||||
│ wakeup-service │ └──────────┬───────────┘
|
||||
└──────────┬──────────┘ │
|
||||
│ │ fetch
|
||||
│ POST /notifications/refresh │ POST /notifications/register
|
||||
│ │ POST /notifications/register
|
||||
│ ▼
|
||||
│ ┌──────────────────────┐
|
||||
│ │ crowd-funder-for- │
|
||||
@@ -26,7 +28,7 @@ End-to-end flow when testing New Activity / silent wake on a physical iPhone:
|
||||
│ └──────────┬───────────┘
|
||||
│ │
|
||||
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
||||
▼ ▼ (local schedule replace)
|
||||
▼ ▼ (Daily Reminder / dual / fetcher)
|
||||
┌─────────────────────┐ ┌──────────────────────┐
|
||||
│ Firebase Cloud │ ──APNs──────► │ iPhone (physical) │
|
||||
│ Messaging │ silent push │ app.timesafari │
|
||||
@@ -37,18 +39,16 @@ End-to-end flow when testing New Activity / silent wake on a physical iPhone:
|
||||
|
||||
| Repo | Role |
|
||||
|------|------|
|
||||
| **notification-wakeup-service** | HTTP API: device registration, refresh payload (`nextNotifications`), health, debug wakeup send |
|
||||
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register` & `/refresh`, handles `WAKEUP_PING` push |
|
||||
| **daily-notification-plugin** | Native iOS/Android: clear + reschedule local notifications from refresh timestamps |
|
||||
| **notification-wakeup-service** | HTTP API: device registration, health, debug wakeup send; may still expose `/notifications/refresh` |
|
||||
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register`; logs `WAKEUP_PING` without refresh/`api_*` scheduling |
|
||||
| **daily-notification-plugin** | Native iOS/Android: Daily Reminder, New Activity / dual, native fetcher; Phase 4 `clearApiNotifications()` |
|
||||
|
||||
### Silent wake sequence (production path)
|
||||
### Silent wake sequence (current)
|
||||
|
||||
1. Backend (or `/debug/send-wakeup`) sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
||||
1. Backend `/debug/send-wakeup` sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
||||
2. APNs delivers to the device (best-effort; see iOS caveats below).
|
||||
3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()`.
|
||||
4. App calls `POST {backend}/notifications/refresh` with `testMode` (from debug config).
|
||||
5. Backend returns `nextNotifications: [{ timestamp }, ...]`.
|
||||
6. App calls `applyNotificationRefreshPayload()` → plugin clears and schedules new local alarms.
|
||||
4. Handler logs `[Notifications] push handler ignored type=WAKEUP_PING`. It does **not** POST `/notifications/refresh` or schedule `api_*` notifications.
|
||||
|
||||
Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`).
|
||||
|
||||
@@ -297,12 +297,11 @@ For a full panel reference (configuration, URL resolution order, authentication,
|
||||
| Control | Purpose |
|
||||
|---------|---------|
|
||||
| **Notification Backend URL** | Paste ngrok HTTPS URL → **Save Backend URL** (changes target server only) |
|
||||
| **Test Mode** | Sends `testMode: true/false` in register/refresh JSON bodies (default on when unset in storage) |
|
||||
| **Test Mode** | Sends `testMode: true/false` in register / send-wakeup JSON bodies (default on when unset in storage) |
|
||||
| **Skip JWT Authentication (Local Development Only)** | When on, omits `Authorization` headers for local servers that accept unauthenticated requests (default **off**) |
|
||||
| **Register Token Now** | `POST /notifications/register` with current FCM token |
|
||||
| **Refresh Notifications** | `POST /notifications/refresh` (same as post-wakeup flow) |
|
||||
| **Simulate WAKEUP_PING (Local)** | Calls refresh API directly (no FCM) — quick backend test |
|
||||
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup`; server sends real FCM `WAKEUP_PING` (Android doc has full flow) |
|
||||
| **Upload AlertSearch Authorization** | Uploads AlertSearch delegated JWTs |
|
||||
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup`; FCM/APNs delivery diagnostic only |
|
||||
| **Event Log** | Shared `[Notifications]` panel log (100 entries) |
|
||||
|
||||
Persistence: `localStorage` keys `notificationDebug.backendBaseUrl`, `notificationDebug.testMode`, and `notificationDebug.bypassAuth` (`NotificationDebugConfig.ts`).
|
||||
@@ -352,7 +351,7 @@ This section is a quick verification checklist for the detailed Firebase/APNs se
|
||||
| **GoogleService-Info.plist** | Present in the iOS target if using Firebase iOS SDK paths in your build |
|
||||
| **FCM token** | Confirm **Register Token Now** succeeds in the debug panel and ngrok shows `POST /notifications/register` |
|
||||
|
||||
Silent/data pushes used for wake typically use a **content-available** style payload; confirm **notification-wakeup-service** and Firebase message format match what `handleCapacitorPushNotificationReceived` expects (`data.type === "WAKEUP_PING"`).
|
||||
Silent/data pushes used for wake typically use a **content-available** style payload; confirm **notification-wakeup-service** and Firebase message format. The app logs `WAKEUP_PING` as ignored and does not refresh.
|
||||
|
||||
---
|
||||
|
||||
@@ -378,14 +377,9 @@ Silent/data pushes used for wake typically use a **content-available** style pay
|
||||
- **Low Power Mode** can reduce background execution.
|
||||
- **Focus / Do Not Disturb** may affect notification presentation (separate from silent data wake, but confusing during tests).
|
||||
|
||||
### Two “Simulate WAKEUP_PING” buttons
|
||||
### Send Real WAKEUP_PING (FCM/APNs diagnostic)
|
||||
|
||||
| Button | Behavior |
|
||||
|--------|----------|
|
||||
| **Backend Testing → Simulate WAKEUP_PING** | Skips FCM; calls refresh API only (ngrok path test) |
|
||||
| **Wakeup Ping Simulator** (lower on panel) | Runs production handler with synthetic `WAKEUP_PING` payload |
|
||||
|
||||
Use the backend button to verify ngrok + refresh; use the simulator to verify handler + refresh chaining.
|
||||
**Send Real WAKEUP_PING** posts `/debug/send-wakeup`. Delivery is confirmed by `push handler ignored type=WAKEUP_PING`. Mock refresh, Simulate WAKEUP_PING, and Wakeup Ping Simulator were removed.
|
||||
|
||||
---
|
||||
|
||||
@@ -395,11 +389,10 @@ Use the backend button to verify ngrok + refresh; use the simulator to verify ha
|
||||
2. Start **ngrok** and copy the HTTPS URL.
|
||||
3. Set URL + **Test Mode** in the Notification Debug Panel; confirm **Backend Status**.
|
||||
4. Tap **Register Token Now** → confirm ngrok request and `[Notifications] Token registration success`.
|
||||
5. Tap **Refresh Notifications** → confirm `Refresh completed in Nms (scheduled X)` in Event Log and ngrok `POST /notifications/refresh`.
|
||||
6. From the backend, call **`/debug/send-wakeup`** (see curl below) with the registered `deviceId` / FCM token as required by that service.
|
||||
7. Watch **Xcode console** for `[Notifications] pushNotificationReceived type=WAKEUP_PING` and refresh timing lines.
|
||||
8. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate requests.
|
||||
9. Use **Pending Notification Inspector** on the panel to see locally scheduled fires after refresh.
|
||||
5. From the backend, call **`/debug/send-wakeup`** (see curl below) with the registered `deviceId` / FCM token as required by that service, or tap **Send Real WAKEUP_PING**.
|
||||
6. Watch **Xcode console** for `[Notifications] push handler ignored type=WAKEUP_PING`.
|
||||
7. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate requests (register and send-wakeup; not app-initiated refresh).
|
||||
8. Use **Pending Notification Inspector** for Daily Reminder / dual / fetcher schedules.
|
||||
|
||||
---
|
||||
|
||||
@@ -430,7 +423,7 @@ curl -sS -X POST "$BASE/notifications/register" \
|
||||
}'
|
||||
```
|
||||
|
||||
### Refresh (mirror app payload)
|
||||
### Refresh (backend only; app does not consume)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$BASE/notifications/refresh" \
|
||||
@@ -441,19 +434,7 @@ curl -sS -X POST "$BASE/notifications/refresh" \
|
||||
}'
|
||||
```
|
||||
|
||||
Example success body shape (actual fields may vary by service version):
|
||||
|
||||
```json
|
||||
{
|
||||
"shouldNotify": true,
|
||||
"nextNotifications": [
|
||||
{ "timestamp": 1710000000000 },
|
||||
{ "timestamp": 1710003600000 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The app schedules those timestamps via **daily-notification-plugin** (`applyNotificationRefreshPayload` in `NativeNotificationService.ts`).
|
||||
This exercises **notification-wakeup-service** only. The app does **not** call `applyNotificationRefreshPayload` or schedule `api_*` from the response.
|
||||
|
||||
### Send wakeup push (debug)
|
||||
|
||||
@@ -478,8 +459,8 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
||||
|
||||
| Symptom | Checks |
|
||||
|---------|--------|
|
||||
| Network error in Event Log | ngrok running? URL saved without typo/trailing slash? |
|
||||
| HTTP 404 | Tunnel port matches backend `PORT`; path is `/notifications/refresh` |
|
||||
| Mac `curl` of `/notifications/refresh` fails | ngrok running? URL saved without typo/trailing slash? |
|
||||
| HTTP 404 | Tunnel port matches backend `PORT`; path is `/notifications/refresh` (backend route; the app does not call it) |
|
||||
| CORS (web only) | Native Capacitor fetch usually avoids browser CORS; if testing in Safari PWA, configure CORS on the service |
|
||||
| ngrok browser warning | Free tier may show an interstitial for browser clients; native `fetch` from the app is usually unaffected |
|
||||
|
||||
@@ -495,15 +476,15 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
||||
- App **backgrounded**, not force-quit
|
||||
- Physical device, correct provisioning profile
|
||||
- APNs key uploaded to Firebase; bundle ID matches
|
||||
- FCM message includes `data.type = "WAKEUP_PING"` (see `NativeNotificationService.ts`)
|
||||
- FCM message includes `data.type = "WAKEUP_PING"` (logged as ignored in `NativeNotificationService.ts`)
|
||||
- Server actually sent to the **same** FCM token shown in the debug panel
|
||||
- Wait 30–120s — delivery is not instant
|
||||
- Try **Simulate WAKEUP_PING** (refresh API) to isolate app/plugin from FCM/APNs
|
||||
- Confirm Xcode shows `push handler ignored type=WAKEUP_PING` (there is no Simulate WAKEUP_PING / refresh API in the app)
|
||||
|
||||
### Notifications duplicating
|
||||
|
||||
- Multiple refresh calls (flood test, repeated wakeups) each **replace** schedule via clear + schedule — check Event Log for repeated refreshes
|
||||
- Separate issue: Daily Reminder vs New Activity both scheduling — see `doc/notification-new-activity-lay-of-the-land.md`
|
||||
- Daily Reminder vs New Activity both scheduling — see `doc/notification-new-activity-lay-of-the-land.md`
|
||||
- Leftover `api_*` before Phase 4 cleanup (startup `clearApiNotifications()`)
|
||||
|
||||
### Stale ngrok URL
|
||||
|
||||
@@ -525,7 +506,7 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
||||
| `src/services/notifications/NotificationDebugEvents.ts` | Panel event log + `logNotification()` |
|
||||
| `src/services/notifications/notificationLog.ts` | Structured log helpers |
|
||||
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
||||
| `src/services/notifications/NativeNotificationService.ts` | Refresh, `WAKEUP_PING`, schedule replace |
|
||||
| `src/services/notifications/NativeNotificationService.ts` | Push delivery hook (logs ignored types; no refresh) |
|
||||
| `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners |
|
||||
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
||||
| `src/main.capacitor.ts` | Native push init at startup |
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Notification Debug Panel
|
||||
|
||||
**Created:** 2026-07-07
|
||||
**Updated:** 2026-07-22
|
||||
**Audience:** Developers testing notification registration, refresh, and WAKEUP_PING flows on native (iOS/Android) dev builds.
|
||||
**Updated:** 2026-09-24
|
||||
**Audience:** Developers testing notification registration, AlertSearch authorization upload, and FCM delivery diagnostics on native (iOS/Android) dev builds.
|
||||
|
||||
The **Notification Debug Panel** is a dev-only UI for exercising the same notification orchestration paths the production app uses: FCM token registration, backend refresh, wakeup handling, and local schedule inspection. It does not duplicate scheduling logic.
|
||||
The **Notification Debug Panel** is a dev-only UI for FCM token registration, AlertSearch authorization upload, FCM wakeup delivery diagnostics (`/debug/send-wakeup`), and local schedule inspection. It does not schedule notifications from `WAKEUP_PING`. The production `WAKEUP_PING` → `/notifications/refresh` → `api_*` path was retired; leftover `api_*` schedules are cleared once at startup (Phase 4).
|
||||
|
||||
---
|
||||
|
||||
## Notification API base URL
|
||||
|
||||
Notification HTTP calls (`/notifications/register`, `/notifications/refresh`, `/debug/send-wakeup`, etc.) do **not** use `APP_SERVER`. They use a dedicated Notification API host, resolved at runtime by `getNotificationApiBaseUrl()` in `NotificationDebugConfig.ts`.
|
||||
Notification HTTP calls (`/notifications/register`, `/notifications/alert-authorization`, `/debug/send-wakeup`, etc.) do **not** use `APP_SERVER`. They use a dedicated Notification API host, resolved at runtime by `getNotificationApiBaseUrl()` in `NotificationDebugConfig.ts`. The app does not call `/notifications/refresh`.
|
||||
|
||||
### Configuration constants
|
||||
|
||||
@@ -65,7 +65,7 @@ Settings persist in `localStorage` via `NotificationDebugConfig.ts`:
|
||||
| `notificationDebug.testMode` | `true` | Sent in JSON request bodies (`testMode: true/false`) |
|
||||
| `notificationDebug.bypassAuth` | `false` | When `true`, omit JWT `Authorization` headers on notification API calls |
|
||||
|
||||
All notification API requests (`/notifications/register`, `/notifications/refresh`, `/debug/send-wakeup`, etc.) obtain headers through `getNotificationApiHeaders()` in `notificationApiAuth.ts`.
|
||||
All notification API requests (`/notifications/register`, `/notifications/alert-authorization`, `/debug/send-wakeup`, etc.) obtain headers through `getNotificationApiHeaders()` in `notificationApiAuth.ts`.
|
||||
|
||||
### Notification Backend URL
|
||||
|
||||
@@ -75,7 +75,7 @@ Leave empty to use the configured build default (`DEFAULT_NOTIFY_API_SERVER`, fr
|
||||
|
||||
### Test Mode
|
||||
|
||||
When enabled (default if never saved), register and refresh requests include `"testMode": true` in the JSON body. The backend can use this to return dev-friendly schedules or route test traffic separately from production.
|
||||
When enabled (default if never saved), register and send-wakeup requests include `"testMode": true` in the JSON body. The backend can use this to route test traffic separately from production.
|
||||
|
||||
Test Mode is **independent of authentication**. It does not control whether `Authorization` headers are sent.
|
||||
|
||||
@@ -124,9 +124,8 @@ Example: `https://abc123.ngrok-free.app` or `http://127.0.0.1:3000`
|
||||
| Action | What it does |
|
||||
|--------|----------------|
|
||||
| **Register Token Now** | `POST {backend}/notifications/register` with current FCM token, `deviceId`, `platform`, and `testMode`. Forces re-registration (bypasses duplicate-token skip). |
|
||||
| **Refresh Notifications** | `POST {backend}/notifications/refresh` — same path used after a real WAKEUP_PING. Applies returned schedule to the native plugin. |
|
||||
| **Simulate WAKEUP_PING (Local)** | Calls the refresh API directly (no FCM). Quick test of backend URL + auth + refresh parsing without push delivery. |
|
||||
| **Send Real WAKEUP_PING** | `POST {backend}/debug/send-wakeup`; server sends a real FCM data message with `data.type = "WAKEUP_PING"`. Exercises backend → FCM → Capacitor listener → refresh → reschedule. Background the app before expecting delivery. |
|
||||
| **Upload AlertSearch Authorization** | Mints and uploads delegated AlertSearch JWTs (`POST /notifications/alert-authorization`). Requires an active `did:ethr` identity; Test Mode is not used. |
|
||||
| **Send Real WAKEUP_PING** | `POST {backend}/debug/send-wakeup`; server sends a real FCM data message with `data.type = "WAKEUP_PING"`. FCM delivery diagnostic only — the app logs `push handler ignored type=…` and does **not** call `/notifications/refresh` or schedule `api_*` notifications. Background the app before expecting delivery. |
|
||||
|
||||
**Current FCM Token** displays the last token from Capacitor/Firebase registration. **Event Log** shows the last 100 `[Notifications]` messages (also visible in logcat / Xcode console on native).
|
||||
|
||||
@@ -136,12 +135,8 @@ Example: `https://abc123.ngrok-free.app` or `http://127.0.0.1:3000`
|
||||
|
||||
| Section | Purpose |
|
||||
|---------|---------|
|
||||
| **Mock Timing Presets** | Interval for mock refresh timestamps (30 sec – 10 min). |
|
||||
| **Trigger Mock Refresh** | Applies synthetic future timestamps locally — no backend call. |
|
||||
| **Wakeup Ping Simulator** | Runs the production push handler with a synthetic `WAKEUP_PING` payload (no FCM, no backend). |
|
||||
| **Flood Test** | Runs 20 sequential mock refreshes (stress test). |
|
||||
| **Pending Notification Inspector** | Lists locally scheduled notifications (iOS; Android may show unavailable). |
|
||||
| **Clear Notifications** | Clears/cancels all plugin-scheduled notifications on native. |
|
||||
| **Pending Notification Inspector** | Lists locally scheduled notifications (Daily Reminder, New Activity / dual, and any leftover `api_*` until Phase 4 cleanup). |
|
||||
| **Clear Notifications** | Clears/cancels plugin-scheduled notifications on native. Does not replace the one-time `api_*` startup cleanup. |
|
||||
|
||||
---
|
||||
|
||||
@@ -198,11 +193,11 @@ The app deferred registration because JWT could not be built (no active DID or e
|
||||
|
||||
Same as above. Confirm the **Active** URL in the panel matches your running tunnel or local server port.
|
||||
|
||||
### Register succeeds but Send Real WAKEUP_PING does not trigger refresh
|
||||
### Register succeeds but Send Real WAKEUP_PING does not show delivery
|
||||
|
||||
**Real WAKEUP_PING success** only means the backend accepted the wakeup request and attempted FCM delivery. Missing `pushNotificationReceived` / `Refresh completed (WAKEUP_PING)` indicates an FCM delivery or background execution issue — not necessarily a bad wakeup API call.
|
||||
**Real WAKEUP_PING success** only means the backend accepted the wakeup request and attempted FCM delivery. It does **not** schedule local `api_*` notifications. Delivery is confirmed when logcat / Event Log shows `push handler ignored type=WAKEUP_PING` (the retired refresh chain is gone).
|
||||
|
||||
**Checks:** App backgrounded (not force-stopped); FCM token matches registration; **Simulate WAKEUP_PING (Local)** works (isolates FCM from refresh API).
|
||||
**Checks:** App backgrounded (not force-stopped); FCM token matches registration; Firebase / Play services available.
|
||||
|
||||
See platform-specific guides for extended ngrok and FCM workflows:
|
||||
|
||||
@@ -222,7 +217,7 @@ See platform-specific guides for extended ngrok and FCM workflows:
|
||||
| `src/services/notifications/notificationApiDebugMode.ts` | Auth bypass gate |
|
||||
| `src/services/notifications/NotificationDebugService.ts` | Panel action handlers |
|
||||
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
||||
| `src/services/notifications/NativeNotificationService.ts` | `POST /notifications/refresh`, WAKEUP_PING handler |
|
||||
| `src/services/notifications/NativeNotificationService.ts` | Push delivery hook (logs ignored types; no refresh) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||
C86585E02ED456DE00824752 /* Embed Foundation Extensions */,
|
||||
3FE25897CF40A571D4AC2ACE /* [CP] Copy Pods Resources */,
|
||||
2B3F98670AF3508A35AC3248 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -294,19 +294,19 @@
|
||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3FE25897CF40A571D4AC2ACE /* [CP] Copy Pods Resources */ = {
|
||||
2B3F98670AF3508A35AC3248 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-resources.sh\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */ = {
|
||||
@@ -455,7 +455,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -512,7 +512,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@@ -596,7 +596,7 @@
|
||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -634,7 +634,7 @@
|
||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
||||
@@ -82,6 +82,10 @@ post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f
|
||||
if deployment_target > 0.0 && deployment_target < 15.5
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5'
|
||||
end
|
||||
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
|
||||
merge_sqlite_omit_load_extension_definition(config)
|
||||
strip_system_sqlite_from_pod_config(config)
|
||||
|
||||
@@ -177,6 +177,6 @@ SPEC CHECKSUMS:
|
||||
TimesafariDailyNotificationPlugin: 69277c884380a9a620f671b68e0327eaa4b3d27d
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: abe640043e6b8adea745693d980ead03251912f9
|
||||
PODFILE CHECKSUM: 5736811d271d5309d3e2de8f3eefdbb6632086a3
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
Generated
+108
-793
File diff suppressed because it is too large
Load Diff
@@ -88,25 +88,6 @@
|
||||
Manually mints and uploads 100 delegated day JWTs. Requires an active
|
||||
did:ethr identity and JWT authentication; Test Mode is not used.
|
||||
</p>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||
@click="onBackendRefresh"
|
||||
>
|
||||
Refresh Notifications
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-violet-400 to-violet-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||
@click="onSimulateWakeupRefresh"
|
||||
>
|
||||
Simulate WAKEUP_PING (Local)
|
||||
</button>
|
||||
<p class="text-xs text-slate-500">
|
||||
Local simulation only — calls the refresh API directly (no FCM push).
|
||||
</p>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
@@ -128,8 +109,8 @@
|
||||
{{ realWakeupStatus.message }}
|
||||
</p>
|
||||
<p v-else class="text-xs text-slate-500">
|
||||
Full pipeline — backend `/debug/send-wakeup` → FCM → WAKEUP_PING
|
||||
handler.
|
||||
FCM delivery diagnostic only — backend `/debug/send-wakeup`. The app
|
||||
no longer schedules api_* notifications from WAKEUP_PING.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -177,70 +158,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION F: Mock Timing Presets -->
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 font-bold">Mock Timing Presets</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset.ms"
|
||||
class="px-3 py-2 rounded border border-slate-300 bg-white text-sm"
|
||||
:class="{
|
||||
'border-blue-500 ring-1 ring-blue-300': intervalMs === preset.ms,
|
||||
}"
|
||||
@click="intervalMs = preset.ms"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 mt-2">
|
||||
Selected interval: <b>{{ intervalLabel }}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION A: Mock Refresh Controls -->
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 font-bold">Mock Refresh Controls</h2>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||
@click="onMockRefresh"
|
||||
>
|
||||
Trigger Mock Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- SECTION B: Wakeup Ping Simulator -->
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 font-bold">Wakeup Ping Simulator</h2>
|
||||
<p class="text-xs text-slate-500 mb-2">
|
||||
Exercises the production push handler (not the refresh API shortcut
|
||||
above).
|
||||
</p>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||
@click="onWakeupPing"
|
||||
>
|
||||
Simulate WAKEUP_PING
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- SECTION C: Flood Test -->
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 font-bold">Flood Test</h2>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-rose-400 to-rose-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||
@click="onFloodTest"
|
||||
>
|
||||
Run 20 Refreshes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- SECTION D: Pending Notification Inspector -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
@@ -358,14 +275,6 @@ type PendingInfo = {
|
||||
wallClockSource?: string | null;
|
||||
};
|
||||
|
||||
const presets = [
|
||||
{ label: "30 sec", ms: 30_000 },
|
||||
{ label: "1 min", ms: 60_000 },
|
||||
{ label: "5 min", ms: 5 * 60_000 },
|
||||
{ label: "10 min", ms: 10 * 60_000 },
|
||||
];
|
||||
|
||||
const intervalMs = ref<number>(60_000);
|
||||
const busy = ref(false);
|
||||
const pending = ref<PendingInfo[]>([]);
|
||||
const pendingInspectorMessage = ref<string | null>(null);
|
||||
@@ -395,11 +304,6 @@ const truncatedFcmToken = computed(() => {
|
||||
const eventLog = ref<string[]>([]);
|
||||
let unsubscribeEventLog: (() => void) | undefined;
|
||||
|
||||
const intervalLabel = computed(() => {
|
||||
const preset = presets.find((p) => p.ms === intervalMs.value);
|
||||
return preset?.label ?? `${intervalMs.value}ms`;
|
||||
});
|
||||
|
||||
function formatIsoMs(ms: number | null | undefined): string {
|
||||
if (ms == null || !Number.isFinite(ms)) {
|
||||
return "";
|
||||
@@ -423,27 +327,6 @@ async function refreshPending(): Promise<void> {
|
||||
pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null;
|
||||
}
|
||||
|
||||
async function onMockRefresh(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await NotificationDebugService.triggerMockRefresh(intervalMs.value);
|
||||
await refreshPending();
|
||||
});
|
||||
}
|
||||
|
||||
async function onWakeupPing(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await NotificationDebugService.simulateWakeupPing();
|
||||
await refreshPending();
|
||||
});
|
||||
}
|
||||
|
||||
async function onFloodTest(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await NotificationDebugService.runFloodTest(intervalMs.value);
|
||||
await refreshPending();
|
||||
});
|
||||
}
|
||||
|
||||
async function onClearNotifications(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await NotificationDebugService.clearNotifications();
|
||||
@@ -514,20 +397,6 @@ async function onUploadAlertAuthorization(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function onBackendRefresh(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await NotificationDebugService.triggerBackendRefresh();
|
||||
await refreshPending();
|
||||
});
|
||||
}
|
||||
|
||||
async function onSimulateWakeupRefresh(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await NotificationDebugService.simulateWakeupViaRefresh();
|
||||
await refreshPending();
|
||||
});
|
||||
}
|
||||
|
||||
function formatRealWakeupStatusMessage(
|
||||
result: Awaited<
|
||||
ReturnType<typeof NotificationDebugService.sendRealWakeupPing>
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { inject, onBeforeUnmount, onMounted } from "vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { registerToken } from "@/services/notifications/NotificationService";
|
||||
import { refreshNotifications } from "@/services/notifications/NativeNotificationService";
|
||||
|
||||
/**
|
||||
* Vue 3 composable for notifications
|
||||
* Provides a concise API for common notification patterns
|
||||
*/
|
||||
|
||||
export const NOTIFICATION_TIMEOUTS = {
|
||||
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
|
||||
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
|
||||
STANDARD: 3000, // Standard notifications (success messages, general info)
|
||||
LONG: 5000, // Longer notifications (errors, warnings, important info)
|
||||
VERY_LONG: 7000, // Very long notifications (complex operations)
|
||||
MODAL: -1, // Modal confirmations (no auto-dismiss)
|
||||
} as const;
|
||||
|
||||
export function useNotifications() {
|
||||
// Inject the notify function from the app
|
||||
const notify =
|
||||
inject<(notification: NotificationIface, timeout?: number) => void>(
|
||||
"notify",
|
||||
);
|
||||
|
||||
if (!notify) {
|
||||
throw new Error(
|
||||
"useNotifications must be used within a component that has $notify available",
|
||||
);
|
||||
}
|
||||
|
||||
let refreshTimer: number | undefined = undefined;
|
||||
let refreshInFlight: Promise<void> | null = null;
|
||||
|
||||
async function refreshNotificationsDebounced(): Promise<void> {
|
||||
if (refreshTimer != null) {
|
||||
window.clearTimeout(refreshTimer);
|
||||
}
|
||||
refreshTimer = window.setTimeout(() => {
|
||||
if (!refreshInFlight) {
|
||||
refreshInFlight = refreshNotifications().finally(() => {
|
||||
refreshInFlight = null;
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
const onResume = () => {
|
||||
void refreshNotificationsDebounced();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
void refreshNotificationsDebounced();
|
||||
document.addEventListener("resume", onResume);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("resume", onResume);
|
||||
if (refreshTimer != null) {
|
||||
window.clearTimeout(refreshTimer);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function success(_notification: NotificationIface, _timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function error(_notification: NotificationIface, _timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function warning(_notification: NotificationIface, _timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function info(_notification: NotificationIface, _timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function toast(_title: string, _text?: string, _timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function copied(_item: string, _timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function sent(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function confirm(
|
||||
_text: string,
|
||||
_onYes: () => Promise<void>,
|
||||
_timeout?: number,
|
||||
) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function confirmationSubmitted(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function genericError(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function genericSuccess(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function alreadyConfirmed(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function cannotConfirmIssuer(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function cannotConfirmHidden(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function notRegistered(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function notAGive(_timeout?: number) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function notificationOff(
|
||||
_title: string,
|
||||
_callback: (success: boolean) => Promise<void>,
|
||||
_timeout?: number,
|
||||
) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function downloadStarted(_format: string = "Dexie", _timeout?: number) {}
|
||||
|
||||
return {
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
toast,
|
||||
copied,
|
||||
sent,
|
||||
confirm,
|
||||
confirmationSubmitted,
|
||||
genericError,
|
||||
genericSuccess,
|
||||
alreadyConfirmed,
|
||||
cannotConfirmIssuer,
|
||||
cannotConfirmHidden,
|
||||
notRegistered,
|
||||
notAGive,
|
||||
notificationOff,
|
||||
downloadStarted,
|
||||
/** POST FCM token to `/notifications/register` (same as startup native hook). */
|
||||
registerFcmToken: registerToken,
|
||||
refreshNotifications: refreshNotificationsDebounced,
|
||||
};
|
||||
}
|
||||
@@ -78,7 +78,10 @@ export interface NotificationRegisterRequest extends NotifyApiTestModeFlag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Body of `POST /notifications/refresh`. The service finds the caller's device
|
||||
* Body of `POST /notifications/refresh`. The Capacitor app no longer calls this
|
||||
* route (WAKEUP_PING → refresh → `api_*` scheduling was retired). These types
|
||||
* remain because notification-wakeup-service still exposes the endpoint.
|
||||
* The service finds the caller's device
|
||||
* by `deviceId`, or by `fcmToken` when no `deviceId` is sent, and answers 400
|
||||
* when neither is present and non-empty. When both are sent they must name the
|
||||
* same device, or the answer is 404.
|
||||
|
||||
@@ -23,7 +23,7 @@ import { LRUCache } from "lru-cache";
|
||||
import * as R from "ramda";
|
||||
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { NOTIFICATION_TIMEOUTS } from "../composables/useNotifications";
|
||||
import { NOTIFICATION_TIMEOUTS } from "../utils/notificationUtils";
|
||||
import { createNotifyHelpers } from "../utils/notify";
|
||||
import { NOTIFY_PERSONAL_DATA_ERROR } from "../constants/notifications";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
configureNativeFetcherIfReady,
|
||||
initializeNativePushAndFirebaseMessaging,
|
||||
onNotificationAuthMayBeReady,
|
||||
runLegacyApiNotificationsCleanupOnce,
|
||||
} from "@/services/notifications";
|
||||
|
||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||
@@ -361,6 +362,8 @@ setTimeout(async () => {
|
||||
);
|
||||
await registerDeepLinkListener();
|
||||
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
|
||||
// One-time: remove persisted legacy api_* schedules from pre-retirement installs
|
||||
await runLegacyApiNotificationsCleanupOnce();
|
||||
// Firebase Messaging (JS) + Capacitor PushNotifications (FCM/APNs token, delivery listeners)
|
||||
await initializeNativePushAndFirebaseMessaging();
|
||||
// Configure native fetcher for API-driven daily notifications (activeDid + JWT)
|
||||
|
||||
@@ -13,30 +13,8 @@
|
||||
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
||||
import type {
|
||||
NotificationRefreshRequest,
|
||||
NotificationRefreshResponse,
|
||||
} from "@/interfaces/notifyApi";
|
||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||
import { getOrCreateDeviceId } from "./deviceId";
|
||||
import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds";
|
||||
import { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||
import {
|
||||
getNotificationApiBaseUrl,
|
||||
getTestMode,
|
||||
} from "./NotificationDebugConfig";
|
||||
import {
|
||||
logRefreshFailure,
|
||||
logRefreshStarted,
|
||||
logRefreshSuccess,
|
||||
logScheduleReplacement,
|
||||
} from "./notificationLog";
|
||||
import {
|
||||
getNotificationApiHeaders,
|
||||
logSkippingRefreshDueToMissingAuth,
|
||||
notificationApiFailureMessage,
|
||||
readNotificationApiBody,
|
||||
} from "./notificationApiAuth";
|
||||
import { logNotification } from "./NotificationDebugEvents";
|
||||
|
||||
/**
|
||||
@@ -563,200 +541,14 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
export type RefreshNotificationsResult = {
|
||||
ok: boolean;
|
||||
scheduledCount: number;
|
||||
status?: number;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-applies native API fetcher credentials (JWT pool, active DID) so background
|
||||
* notification workers can run. No UI; safe from push handlers while backgrounded.
|
||||
*/
|
||||
export async function refreshNotificationsWithDiagnostics(options?: {
|
||||
source?: string;
|
||||
}): Promise<RefreshNotificationsResult> {
|
||||
const startedAt = performance.now();
|
||||
const source = options?.source;
|
||||
logRefreshStarted(source);
|
||||
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
const errorMessage = "not a native platform";
|
||||
logRefreshFailure(startedAt, errorMessage, undefined, source);
|
||||
return {
|
||||
ok: false,
|
||||
scheduledCount: 0,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const auth = await getNotificationApiHeaders("refresh");
|
||||
if (!auth.ok) {
|
||||
logSkippingRefreshDueToMissingAuth();
|
||||
logRefreshFailure(startedAt, auth.message, undefined, source);
|
||||
return {
|
||||
ok: false,
|
||||
scheduledCount: 0,
|
||||
errorMessage: auth.message,
|
||||
};
|
||||
}
|
||||
|
||||
let deviceId: string | undefined;
|
||||
try {
|
||||
deviceId = await getOrCreateDeviceId();
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[NativeNotificationService] Could not obtain deviceId; skipping refresh",
|
||||
err,
|
||||
);
|
||||
}
|
||||
if (!deviceId) {
|
||||
// The service finds the device by deviceId or fcmToken and answers 400
|
||||
// without either, so there is no request worth sending.
|
||||
const errorMessage = "no deviceId (cannot identify this device)";
|
||||
logRefreshFailure(startedAt, errorMessage, undefined, source);
|
||||
return { ok: false, scheduledCount: 0, errorMessage };
|
||||
}
|
||||
|
||||
const body: NotificationRefreshRequest = {
|
||||
deviceId,
|
||||
platform: Capacitor.getPlatform(),
|
||||
testMode: getTestMode(),
|
||||
};
|
||||
const baseUrl = getNotificationApiBaseUrl();
|
||||
const res = await fetch(`${baseUrl}/notifications/refresh`, {
|
||||
method: "POST",
|
||||
headers: auth.headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorMessage = notificationApiFailureMessage(
|
||||
res.status,
|
||||
await readNotificationApiBody(res),
|
||||
);
|
||||
logger.warn("[NativeNotificationService] refreshNotifications failed", {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
errorMessage,
|
||||
});
|
||||
logRefreshFailure(startedAt, errorMessage, res.status, source);
|
||||
return {
|
||||
ok: false,
|
||||
scheduledCount: 0,
|
||||
status: res.status,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
const payload = (await res.json()) as NotificationRefreshResponse;
|
||||
const scheduledCount = Array.isArray(payload?.nextNotifications)
|
||||
? payload.nextNotifications.length
|
||||
: 0;
|
||||
await applyNotificationRefreshPayload(payload);
|
||||
logRefreshSuccess(startedAt, scheduledCount, source);
|
||||
return { ok: true, scheduledCount };
|
||||
} catch (err) {
|
||||
logger.error("[NativeNotificationService] Refresh failed", err);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
logRefreshFailure(startedAt, message, undefined, source);
|
||||
return { ok: false, scheduledCount: 0, errorMessage: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshNotifications(): Promise<void> {
|
||||
await refreshNotificationsWithDiagnostics();
|
||||
}
|
||||
|
||||
export type NotificationRefreshPayload = {
|
||||
shouldNotify?: boolean;
|
||||
nextNotifications?: Array<{ timestamp?: number }>;
|
||||
};
|
||||
|
||||
// `handleCapacitorPushNotificationReceived` and `applyNotificationRefreshPayload` are used by
|
||||
// DEV notification simulation tooling; they must stay production-safe because that tooling
|
||||
// exercises real flows. (`applyNotificationRefreshPayload` is also used by production refresh.)
|
||||
|
||||
/**
|
||||
* Apply a "refresh notifications" payload by clearing and scheduling timestamps via the native plugin.
|
||||
*
|
||||
* This is the shared implementation used by:
|
||||
* - production refresh flow (`refreshNotifications` fetching from backend)
|
||||
* - dev-only debug flows (mock refresh with local payloads)
|
||||
*
|
||||
* Important: This function intentionally mirrors production behavior and does not introduce
|
||||
* any scheduling logic in UI layers.
|
||||
*/
|
||||
export async function applyNotificationRefreshPayload(
|
||||
payload: unknown,
|
||||
): Promise<void> {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = payload as NotificationRefreshPayload;
|
||||
const nextNotifications = data?.nextNotifications;
|
||||
if (!Array.isArray(nextNotifications)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamps = nextNotifications
|
||||
.map((n) => (n as { timestamp?: unknown })?.timestamp)
|
||||
.filter((t): t is number => typeof t === "number" && Number.isFinite(t));
|
||||
|
||||
if (timestamps.length === 0) {
|
||||
logNotification("Schedule replacement skipped (no valid timestamps)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep existing behavior: ensure background worker credentials are current.
|
||||
await configureNativeFetcherIfReady();
|
||||
|
||||
logScheduleReplacement(timestamps.length);
|
||||
|
||||
if (typeof DailyNotification.clearApiNotifications !== "function") {
|
||||
logger.warn(
|
||||
"[NativeNotificationService] API notification clear unavailable (plugin clearApiNotifications missing); cannot replace schedule",
|
||||
);
|
||||
logNotification(
|
||||
"Schedule replacement aborted (API notification clear unavailable on plugin)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logNotification("Clearing API notifications before refresh");
|
||||
await DailyNotification.clearApiNotifications();
|
||||
logNotification("Cleared API notifications");
|
||||
|
||||
if (typeof DailyNotification.scheduleApiNotifications !== "function") {
|
||||
logger.warn(
|
||||
"[NativeNotificationService] scheduleApiNotifications not available on plugin; cannot apply timestamps",
|
||||
);
|
||||
logNotification(
|
||||
"Schedule replacement aborted (scheduleApiNotifications unavailable)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await DailyNotification.scheduleApiNotifications({ timestamps });
|
||||
logNotification(
|
||||
`Schedule replacement applied (${timestamps.length} timestamp(s))`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Silent FCM/APNs data push: refresh native notification pipeline when requested by backend.
|
||||
* Capacitor push delivery hook. Legacy WAKEUP_PING → /notifications/refresh
|
||||
* consumption was retired; AlertSearch uses visible FCM notifications and is
|
||||
* not handled here. Keep logging for diagnostics without scheduling api_* work.
|
||||
*/
|
||||
export async function handleCapacitorPushNotificationReceived(
|
||||
notification: PushNotificationSchema,
|
||||
): Promise<void> {
|
||||
if (notification.data?.type === "WAKEUP_PING") {
|
||||
logNotification("WAKEUP_PING handler — invoking refresh");
|
||||
await refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" });
|
||||
return;
|
||||
}
|
||||
const type =
|
||||
typeof notification.data?.type === "string"
|
||||
? notification.data.type
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
/**
|
||||
* DEV-only notification testing utilities.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* This service intentionally routes through the same production notification
|
||||
* orchestration paths used by refresh flows, wakeup pushes, and replacement.
|
||||
* Avoid adding duplicate scheduling logic here.
|
||||
* Legacy WAKEUP_PING → /notifications/refresh → api_* tooling was removed in
|
||||
* the Phase 2 retirement. Remaining helpers cover FCM registration, AlertSearch
|
||||
* authorization upload, backend URL overrides, and pending-notification inspection.
|
||||
*/
|
||||
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
||||
import type {
|
||||
DebugSendWakeupRequest,
|
||||
DebugSendWakeupResponse,
|
||||
@@ -38,12 +36,6 @@ import {
|
||||
notificationApiFailureMessage,
|
||||
readNotificationApiBody,
|
||||
} from "./notificationApiAuth";
|
||||
import {
|
||||
applyNotificationRefreshPayload,
|
||||
handleCapacitorPushNotificationReceived,
|
||||
refreshNotificationsWithDiagnostics,
|
||||
type NotificationRefreshPayload,
|
||||
} from "./NativeNotificationService";
|
||||
import { truncateFcmTokenForLog } from "./notificationLog";
|
||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
|
||||
@@ -179,19 +171,10 @@ export const NotificationDebugService = {
|
||||
return result;
|
||||
},
|
||||
|
||||
async triggerBackendRefresh(): Promise<void> {
|
||||
await refreshNotificationsWithDiagnostics({ source: "debug panel" });
|
||||
},
|
||||
|
||||
/** Local simulation: same API call as a WAKEUP_PING handler (no push payload). */
|
||||
async simulateWakeupViaRefresh(): Promise<void> {
|
||||
logNotification("WAKEUP_PING simulation (local refresh API only)");
|
||||
await refreshNotificationsWithDiagnostics({
|
||||
source: "WAKEUP_PING simulation",
|
||||
});
|
||||
},
|
||||
|
||||
/** Full pipeline: backend `/debug/send-wakeup` → FCM → native WAKEUP_PING handler. */
|
||||
/**
|
||||
* Backend `/debug/send-wakeup` → FCM only. App no longer consumes WAKEUP_PING
|
||||
* for api_* scheduling; kept for FCM delivery diagnostics until backend Phase 3.
|
||||
*/
|
||||
async sendRealWakeupPing(): Promise<SendRealWakeupPingResult> {
|
||||
logNotification("Real WAKEUP_PING requested");
|
||||
|
||||
@@ -264,62 +247,6 @@ export const NotificationDebugService = {
|
||||
}
|
||||
},
|
||||
|
||||
generateMockNotifications(
|
||||
intervalMs: number = 60_000,
|
||||
): NotificationRefreshPayload {
|
||||
const now = Date.now();
|
||||
const future1 = now + intervalMs;
|
||||
const future2 = now + intervalMs * 2;
|
||||
|
||||
return {
|
||||
shouldNotify: true,
|
||||
nextNotifications: [{ timestamp: future1 }, { timestamp: future2 }],
|
||||
};
|
||||
},
|
||||
|
||||
async triggerMockRefresh(intervalMs?: number): Promise<void> {
|
||||
logNotification("Mock refresh requested");
|
||||
|
||||
const payload = this.generateMockNotifications(intervalMs);
|
||||
const timestamps = payload.nextNotifications?.map((n) => n.timestamp) ?? [];
|
||||
logNotification(`Mock payload generated (${timestamps.length} timestamps)`);
|
||||
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
logNotification("Mock refresh skipped: not running on native platform");
|
||||
return;
|
||||
}
|
||||
|
||||
await applyNotificationRefreshPayload(payload);
|
||||
logNotification("Mock refresh applied");
|
||||
},
|
||||
|
||||
async simulateWakeupPing(): Promise<void> {
|
||||
logNotification("Simulating WAKEUP_PING (production push handler)");
|
||||
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
logNotification("WAKEUP_PING simulation skipped: not native platform");
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = {
|
||||
title: "WAKEUP_PING",
|
||||
body: "",
|
||||
id: "dev_wakeup_ping",
|
||||
data: { type: "WAKEUP_PING" },
|
||||
} as unknown as PushNotificationSchema;
|
||||
|
||||
await handleCapacitorPushNotificationReceived(notification);
|
||||
},
|
||||
|
||||
async runFloodTest(intervalMs?: number): Promise<void> {
|
||||
logNotification("Flood test started (20 sequential refreshes)");
|
||||
for (let i = 0; i < 20; i++) {
|
||||
logNotification(`Flood iteration ${i + 1}/20`);
|
||||
await this.triggerMockRefresh(intervalMs);
|
||||
}
|
||||
logNotification("Flood test completed");
|
||||
},
|
||||
|
||||
async clearNotifications(): Promise<void> {
|
||||
logNotification("Clear notifications (debug panel)");
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export { uploadAlertSearchAuthorization } from "./alertAuthorization";
|
||||
export type { AlertAuthorizationUploadResult } from "./alertAuthorization";
|
||||
|
||||
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||
export { runLegacyApiNotificationsCleanupOnce } from "./legacyApiNotificationsCleanup";
|
||||
export {
|
||||
deferFcmRegistration,
|
||||
flushDeferredFcmRegistration,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { Preferences } from "@capacitor/preferences";
|
||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||
import {
|
||||
LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
||||
runLegacyApiNotificationsCleanupOnce,
|
||||
} from "./legacyApiNotificationsCleanup";
|
||||
|
||||
jest.mock("@capacitor/core", () => ({
|
||||
Capacitor: {
|
||||
isNativePlatform: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("@capacitor/preferences", () => ({
|
||||
Preferences: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("@/plugins/DailyNotificationPlugin", () => ({
|
||||
DailyNotification: {
|
||||
clearApiNotifications: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("@/utils/logger", () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
log: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const isNativePlatform = Capacitor.isNativePlatform as jest.Mock;
|
||||
const preferencesGet = Preferences.get as jest.Mock;
|
||||
const preferencesSet = Preferences.set as jest.Mock;
|
||||
const clearApiNotifications =
|
||||
DailyNotification.clearApiNotifications as jest.Mock;
|
||||
|
||||
describe("runLegacyApiNotificationsCleanupOnce", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
isNativePlatform.mockReturnValue(true);
|
||||
preferencesGet.mockResolvedValue({ value: null });
|
||||
preferencesSet.mockResolvedValue(undefined);
|
||||
clearApiNotifications.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("no-ops on non-native platforms", async () => {
|
||||
isNativePlatform.mockReturnValue(false);
|
||||
|
||||
await runLegacyApiNotificationsCleanupOnce();
|
||||
|
||||
expect(preferencesGet).not.toHaveBeenCalled();
|
||||
expect(clearApiNotifications).not.toHaveBeenCalled();
|
||||
expect(preferencesSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips cleanup when the migration marker is already set", async () => {
|
||||
preferencesGet.mockResolvedValue({ value: "1" });
|
||||
|
||||
await runLegacyApiNotificationsCleanupOnce();
|
||||
|
||||
expect(clearApiNotifications).not.toHaveBeenCalled();
|
||||
expect(preferencesSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears api_* state and writes the marker only after success", async () => {
|
||||
await runLegacyApiNotificationsCleanupOnce();
|
||||
|
||||
expect(clearApiNotifications).toHaveBeenCalledTimes(1);
|
||||
expect(preferencesSet).toHaveBeenCalledWith({
|
||||
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
||||
value: "1",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not write the marker when clearApiNotifications fails", async () => {
|
||||
clearApiNotifications.mockRejectedValue(new Error("native failure"));
|
||||
|
||||
await runLegacyApiNotificationsCleanupOnce();
|
||||
|
||||
expect(clearApiNotifications).toHaveBeenCalledTimes(1);
|
||||
expect(preferencesSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not write the marker when clearApiNotifications is missing", async () => {
|
||||
const original = DailyNotification.clearApiNotifications;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete (DailyNotification as any).clearApiNotifications;
|
||||
|
||||
await runLegacyApiNotificationsCleanupOnce();
|
||||
|
||||
expect(preferencesSet).not.toHaveBeenCalled();
|
||||
DailyNotification.clearApiNotifications = original;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* One-time upgrade cleanup for legacy API-managed notification schedules
|
||||
* (`api_*`) left behind by the retired WAKEUP_PING → /notifications/refresh path.
|
||||
*
|
||||
* Uses DailyNotification.clearApiNotifications(), which cancels only `api_*`
|
||||
* pending/delivered state (and Android persisted api_* schedule rows). It does
|
||||
* not touch daily reminders, dual/New Activity schedules, AlertSearch, or FCM.
|
||||
*/
|
||||
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { Preferences } from "@capacitor/preferences";
|
||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
/** Preferences key; written only after a successful native cleanup. */
|
||||
export const LEGACY_API_NOTIFICATIONS_CLEANUP_KEY =
|
||||
"legacy_api_notifications_cleanup_v1";
|
||||
|
||||
const DONE_VALUE = "1";
|
||||
|
||||
let inFlight: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Idempotent startup hook. Safe when zero `api_*` schedules exist.
|
||||
* On failure, leaves the marker unset so a later launch can retry.
|
||||
*/
|
||||
export async function runLegacyApiNotificationsCleanupOnce(): Promise<void> {
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
inFlight = runCleanup().finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
async function runCleanup(): Promise<void> {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await Preferences.get({
|
||||
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
||||
});
|
||||
if (existing.value === DONE_VALUE) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"[legacyApiNotificationsCleanup] Could not read migration marker; skipping this launch",
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const clearApiNotifications = (
|
||||
DailyNotification as {
|
||||
clearApiNotifications?: () => Promise<void>;
|
||||
}
|
||||
).clearApiNotifications;
|
||||
|
||||
if (typeof clearApiNotifications !== "function") {
|
||||
logger.warn(
|
||||
"[legacyApiNotificationsCleanup] clearApiNotifications unavailable; will retry on a later launch",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await clearApiNotifications.call(DailyNotification);
|
||||
await Preferences.set({
|
||||
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
||||
value: DONE_VALUE,
|
||||
});
|
||||
logger.info(
|
||||
"[legacyApiNotificationsCleanup] Cleared legacy api_* notification state",
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"[legacyApiNotificationsCleanup] Cleanup failed; will retry on next startup",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { getNotificationDebugOverrideHeaders } from "./NotificationDebugConfig";
|
||||
import { shouldBypassNotificationAuth } from "./notificationApiDebugMode";
|
||||
import { logNotification } from "./NotificationDebugEvents";
|
||||
|
||||
export type NotificationRequestKind = "register" | "refresh";
|
||||
export type NotificationRequestKind = "register";
|
||||
|
||||
export type NotificationApiHeadersResult =
|
||||
| {
|
||||
@@ -133,32 +133,17 @@ export async function getNotificationApiHeaders(
|
||||
};
|
||||
}
|
||||
|
||||
export function logNotificationRequestAuthenticated(
|
||||
kind: NotificationRequestKind,
|
||||
): void {
|
||||
logNotification(
|
||||
kind === "register"
|
||||
? "Register request authenticated"
|
||||
: "Refresh request authenticated",
|
||||
);
|
||||
}
|
||||
|
||||
export function logNotificationAuthFailure(
|
||||
kind: NotificationRequestKind,
|
||||
_kind: NotificationRequestKind,
|
||||
message: string,
|
||||
): void {
|
||||
const verb = kind === "register" ? "Register" : "Refresh";
|
||||
logNotification(`${verb} auth unavailable: ${message}`);
|
||||
logNotification(`Register auth unavailable: ${message}`);
|
||||
}
|
||||
|
||||
export function logWaitingForAuthBeforeRegistration(): void {
|
||||
logNotification("Waiting for auth before registration");
|
||||
}
|
||||
|
||||
export function logSkippingRefreshDueToMissingAuth(): void {
|
||||
logNotification("Skipping refresh due to missing auth");
|
||||
}
|
||||
|
||||
export function httpAuthErrorMessage(status: number): string {
|
||||
if (status === 401) {
|
||||
return "unauthorized (expired or invalid auth)";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Defers notification register/refresh until app auth (active DID + Bearer) is available.
|
||||
* Defers FCM token registration until app auth (active DID + Bearer) is available.
|
||||
* Bounded retries avoid racing startup and prevent infinite loops.
|
||||
*/
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ export function logPushNotificationReceived(notification: {
|
||||
title: notification.title,
|
||||
dataType: type,
|
||||
});
|
||||
if (type === "WAKEUP_PING") {
|
||||
logNotification("WAKEUP_PING received — will trigger refresh");
|
||||
}
|
||||
}
|
||||
|
||||
export function logPushNotificationActionPerformed(action: {
|
||||
@@ -74,44 +71,6 @@ export function logTokenRegistrationFailure(
|
||||
});
|
||||
}
|
||||
|
||||
export function logRefreshStarted(source?: string): void {
|
||||
logNotification(source ? `Refresh started (${source})` : "Refresh started");
|
||||
}
|
||||
|
||||
function elapsedMsSince(startedAt: number): number {
|
||||
return performance.now() - startedAt;
|
||||
}
|
||||
|
||||
export function logRefreshSuccess(
|
||||
startedAt: number,
|
||||
scheduledCount: number,
|
||||
source?: string,
|
||||
): void {
|
||||
const elapsedMs = Math.round(elapsedMsSince(startedAt));
|
||||
const message = source
|
||||
? `Refresh completed (${source}) in ${elapsedMs}ms (scheduled ${scheduledCount})`
|
||||
: `Refresh completed in ${elapsedMs}ms (scheduled ${scheduledCount})`;
|
||||
logNotification(message);
|
||||
}
|
||||
|
||||
export function logRefreshFailure(
|
||||
startedAt: number,
|
||||
errorMessage: string,
|
||||
status?: number,
|
||||
source?: string,
|
||||
): void {
|
||||
const statusPart = status != null ? ` HTTP ${status}` : "";
|
||||
const elapsedMs = Math.round(elapsedMsSince(startedAt));
|
||||
const message = source
|
||||
? `Refresh failed (${source}) in ${elapsedMs}ms: ${errorMessage}${statusPart}`
|
||||
: `Refresh failed in ${elapsedMs}ms: ${errorMessage}${statusPart}`;
|
||||
logNotification(message);
|
||||
}
|
||||
|
||||
export function logNotificationClearing(method: string): void {
|
||||
logNotification(`Clearing notifications via ${method}`);
|
||||
}
|
||||
|
||||
export function logScheduleReplacement(count: number): void {
|
||||
logNotification(`Schedule replacement: ${count} notification(s)`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user