diff --git a/doc/local-android-testing-analysis.md b/doc/local-android-testing-analysis.md new file mode 100644 index 00000000..36135a17 --- /dev/null +++ b/doc/local-android-testing-analysis.md @@ -0,0 +1,400 @@ +# Android Local Notification Testing — Planning Analysis + +**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 only — does not replace or modify the iOS guide. + +--- + +## Executive summary + +The iOS guide’s **backend + ngrok + in-app debug panel** path is platform-agnostic. Most of sections **1–3**, **6**, **9** (with log tooling swapped), **10** (with `platform: "android"`), **12**, and parts of **11** can be copied or lightly edited. + +Everything involving **APNs, Xcode, Apple Developer, iOS capabilities, and iOS background/silent-push caveats** must be replaced. Android adds **direct FCM delivery** (no APNs hop), **`google-services.json`**, **runtime notification permissions (API 33+)**, **Doze / battery optimization / OEM restrictions**, and different **force-stop / background** semantics. + +Existing related docs to cross-link (not duplicate): + +- [android-physical-device-guide.md](./android-physical-device-guide.md) — USB, `adb`, build/run commands +- [notification-system-overview.md](./notification-system-overview.md) +- [notification-from-api-call.md](./notification-from-api-call.md) +- [notification-permissions-and-rollovers.md](./notification-permissions-and-rollovers.md) + +--- + +## iOS guide structure (reference map) + +| § | iOS doc heading | Reuse for Android | +|---|-----------------|-------------------| +| Intro | Architecture overview | **Adapt** — swap APNs leg for FCM→device | +| — | Prerequisites | **Partial** — drop Xcode/APNs; add Android SDK/device | +| 1 | Install and configure ngrok | **Reuse unchanged** | +| 2 | Start the backend locally | **Reuse unchanged** | +| 3 | Obtain and use ngrok HTTPS URL | **Reuse** — wording: “device” not “iPhone” | +| 4 | Generate and open iOS workspace | **Rewrite** — Android Studio / Capacitor sync | +| 5 | Firebase + APNs setup | **Rewrite** — Firebase Android only; no APNs | +| 6 | Notification Debug Panel override | **Reuse unchanged** | +| 7 | Firebase and Xcode checklist | **Rewrite** — Android manifest / Gradle checklist | +| 8 | iOS-specific testing notes | **Rewrite** — Android delivery caveats | +| 9 | Recommended debug workflow | **Reuse** — replace Xcode console with logcat | +| 10 | Sample curl commands | **Reuse** — change `platform` to `android` | +| 11 | Troubleshooting | **Partial** — keep ngrok/API rows; replace push rows | +| 12 | Key source files | **Reuse unchanged** | +| 13 | Related docs | **Extend** — link Android build/device guides | + +--- + +## Sections reusable unchanged (or near-unchanged) + +These blocks can be carried into `doc/local-android-testing-ngrok.md` (proposed name) with at most global find-replace (“iPhone” → “Android device”, “Mac” tunnel audience unchanged). + +### notification-wakeup-service startup (iOS §1 Terminal A, §2) + +- Clone **notification-wakeup-service**, `npm install`, `.env` from `.env.example` +- `export PORT=3000` (or port from that repo’s README) +- `npm run dev` +- Local verify: `curl -sS http://localhost:3000/health` +- Firebase **Admin** service account for the backend (`GOOGLE_APPLICATION_CREDENTIALS`) — same project can serve iOS and Android apps + +### ngrok setup (iOS §1) + +- `brew install ngrok/ngrok/ngrok` (or download) +- `ngrok http 3000` in a second terminal +- Use **HTTPS** forwarding URL; free tier URL rotation note +- ngrok inspect UI at `http://127.0.0.1:4040` + +### ngrok account creation (iOS §1 “Account and auth token”) + +- Sign up at dashboard.ngrok.com +- `ngrok config add-authtoken YOUR_AUTHTOKEN_HERE` + +### Obtaining HTTPS URL (iOS §3) + +- Copy `https://….ngrok-free.app` from Forwarding line +- No trailing slash in debug panel +- Mac-side tunnel test: `export NGROK_URL=…` and `curl "$NGROK_URL/health"` + +### Backend override configuration (iOS §6) + +- Non-production build required for Notification Debug Panel +- Account → **Show All General Advanced Functions** → `/dev/notifications` +- **Notification Backend URL**, **Save Backend URL** +- `localStorage`: `notificationDebug.backendBaseUrl`, `notificationDebug.testMode` +- Optional programmatic override via `@/services/notifications` (`setBackendBaseUrl`, `setTestMode`, `getNotificationApiBaseUrl`) + +### Debug panel usage (iOS §6 table, §8 “Two Simulate WAKEUP_PING buttons”) + +| Control | Android relevance | +|---------|-------------------| +| Notification Backend URL | Same | +| Test Mode | Same (`testMode: true` on API) | +| Register Token Now | Same (`POST /notifications/register`) | +| Refresh Notifications | Same | +| Simulate WAKEUP_PING (backend) | Same — isolates ngrok + refresh without FCM | +| Wakeup Ping Simulator | Same — exercises `handleCapacitorPushNotificationReceived` path | +| Event Log `[Notifications]` | Same | +| Pending Notification Inspector | Same concept; confirm Android plugin inspector behavior in **daily-notification-plugin** | + +### testMode usage (iOS §6, §10) + +- Default-on when unset in storage (`NotificationDebugConfig.ts`) +- Sent on register and refresh payloads +- Backend/debug endpoints accept `testMode: true` for dev traffic + +### Refresh endpoint testing (iOS §9 steps 5, §11 “Refresh endpoint unreachable”) + +- Panel **Refresh Notifications** → expect Event Log + ngrok `POST /notifications/refresh` +- **Simulate WAKEUP_PING** (backend button) for API-only path +- Troubleshooting table for network error, 404, wrong port, stale URL + +### curl examples (iOS §10) + +Reuse structure; **only payload deltas** for Android doc: + +```bash +export BASE="https://abc123.ngrok-free.app" +``` + +- `$BASE/health` — unchanged +- `$BASE/notifications/register` — set `"platform": "android"` +- `$BASE/notifications/refresh` — set `"platform": "android"` +- `$BASE/debug/send-wakeup` — unchanged shape; confirm deviceId/token contract in **notification-wakeup-service** README + +App still uses `Capacitor.getPlatform()` for `platform` in `NotificationService.ts` (`ios` | `android`). + +### Shared architecture concepts (intro + silent wake sequence) + +Reusable narrative (edit diagram only): + +1. FCM **data** message with `data.type = "WAKEUP_PING"` +2. Capacitor `pushNotificationReceived` → `handleCapacitorPushNotificationReceived()` +3. `POST {backend}/notifications/refresh` with `testMode` +4. `nextNotifications` → `applyNotificationRefreshPayload()` → **daily-notification-plugin** clear + schedule + +Repos table (notification-wakeup-service, crowd-funder-for-time-pwa, daily-notification-plugin) — unchanged. + +### Key source files (iOS §12) + +Same files apply on Android Capacitor builds: + +- `NotificationDebugConfig.ts`, `NotificationDebugEvents.ts`, `notificationLog.ts` +- `NotificationService.ts`, `NativeNotificationService.ts` +- `firebaseMessagingClient.ts`, `NotificationDebugPanel.vue`, `main.capacitor.ts` + +### Recommended debug workflow (iOS §9) — reuse with tooling swap + +Steps 1–5, 8–9 unchanged. Replace step 7: + +- **iOS:** Xcode console → `[Notifications] pushNotificationReceived type=WAKEUP_PING` +- **Android:** `adb logcat` filtered on app tag / `[Notifications]` (document exact filter in Android guide) + +--- + +## iOS-specific sections — must rewrite for Android + +### Architecture diagram (intro) + +**iOS today:** Mac → ngrok → app; FCM → **APNs** → iPhone. + +**Android doc:** FCM → **device directly** (no APNs). Update ASCII diagram and caption (“silent push” on Android is still FCM data; delivery rules differ). + +### Prerequisites (intro list) + +| iOS prerequisite | Android replacement | +|------------------|---------------------| +| Mac with **Xcode** | **Android Studio**, JDK 17+, `ANDROID_HOME`, `adb` — see [android-physical-device-guide.md](./android-physical-device-guide.md) | +| Physical **iPhone** | Physical **Android** device (emulator possible for some steps but **not** representative for Doze/OEM/battery) | +| Firebase with **APNs** for bundle ID | Firebase with **Android app** (`app.timesafari` package name) | +| Non-production build | Same — e.g. `build:android:dev` / `build:android:test` | + +Remove: “simulator is not sufficient for reliable silent push / **APNs**”. + +Add: emulator vs physical device guidance for FCM and background limits. + +### §4 — Generate and open the iOS workspace + +**Replace entirely** with Android equivalent: + +- `npm install` +- `npm run build:android:dev` or `build:android:test` (non-production for debug panel) +- `npx cap sync android` if needed +- Open `android/` in Android Studio +- Run on physical device (USB debugging) +- `VITE_FIREBASE_*` in Capacitor web build +- `initializeNativePushAndFirebaseMessaging()` in `main.capacitor.ts` — same entry point + +Do **not** reference `.xcworkspace`, signing in Xcode, or `build:ios:*` except as cross-link to iOS doc. + +### §5 — Firebase + APNs setup (first-time setup) + +**Keep (Android-relevant portions only):** + +- Firebase account / Spark plan sufficient for FCM +- Create Firebase project +- **Register Android app** in Firebase (package name `app.timesafari` from `capacitor.config.ts`) +- Download **`google-services.json`** → `android/app/` (project may gitignore this file — document secure handling) +- Firebase Admin service account for **notification-wakeup-service** — same as iOS §5 tail + +**Remove entirely:** + +- Register **iOS** app in Firebase (or move to “shared project” sidebar: one Firebase project, two apps) +- **GoogleService-Info.plist** / Xcode drag-and-drop +- **Create APNs Authentication Key** (.p8) +- **Upload APNs key to Firebase** +- **Enable iOS capabilities** (Push Notifications, Background Modes → Remote notifications) + +**Add in Android guide (see next major section):** + +- Gradle plugin / `google-services` classpath if not already in repo +- `POST_NOTIFICATIONS` permission (API 33+) +- Default notification channel / Capacitor Push Notifications Android setup +- SHA-1/SHA-256 only if using Firebase features that require it (note whether wakeup testing needs Play App Signing keys) + +### §5 verify checklist — iOS-only bullets + +Replace: + +- “Xcode without Firebase/plist errors” → Android Studio build; `google-services.json` present +- “iOS push permission prompt” → Android 13+ notification permission + older grant model +- “content-available style payload” → Android **high-priority data message** / FCM options as implemented by **notification-wakeup-service** (document actual payload; no APNs `content-available`) + +### §7 — Firebase and Xcode checklist (iOS) + +**Replace** with Android checklist, e.g.: + +| Item | Action | +|------|--------| +| **Application ID** | `app.timesafari` in `capacitor.config.ts`, `android/app/build.gradle`, Firebase Android app | +| **google-services.json** | In `android/app/`; not committed if gitignored — local copy per developer | +| **Gradle** | Google services plugin applied (verify repo’s current `build.gradle`) | +| **Permissions** | `POST_NOTIFICATIONS` (API 33+); manifest entries for FCM | +| **FCM token** | Debug panel **Register Token Now** + ngrok `POST /notifications/register` | +| **No APNs** | N/A on Android | + +### §8 — iOS-specific testing notes + +**Replace** with Android-specific sections (draft topics below). Do not port: + +- APNs silent delivery / Simulator unreliability (iOS framing) +- **Force-quit** via app switcher (iOS-specific policy) +- **Low Power Mode** (iOS) — Android has different battery saver APIs +- **Focus / Do Not Disturb** (iOS naming) + +Port with Android wording: + +- Two **Simulate WAKEUP_PING** buttons table — unchanged behavior + +### §11 — Troubleshooting (partial) + +**Reuse as-is:** + +- Refresh endpoint unreachable (ngrok, URL, 404, CORS note) +- Stale ngrok URL +- Plugin / JWT errors after refresh + +**Rewrite:** + +| iOS troubleshooting | Android replacement | +|----------------------|---------------------| +| Push permission + `VITE_FIREBASE_*` + **Xcode** log | Permission (runtime POST_NOTIFICATIONS), logcat, Firebase Android config | +| Silent push not waking — **backgrounded not force-quit**, **APNs key**, wait 30–120s | FCM high-priority data, **force-stop** (`STOP` from settings), **Doze**, battery optimization, OEM autostart, token mismatch | +| Physical device + provisioning profile | USB debugging, correct build variant, Play vs debug signing if relevant | + +### §13 — Related docs + +Keep iOS-centric links as “see also”; add: + +- [android-physical-device-guide.md](./android-physical-device-guide.md) +- `BUILDING.md` — Android build commands (`build:android:*`) +- **daily-notification-plugin** Android docs (exact alarm, pending inspector on Android) + +--- + +## Android-Specific Topics Required + +These sections do not exist in the iOS guide (or exist only by analogy) and must be written for the Android notification testing doc. + +### Firebase project setup + +- Use the **same** Firebase project as iOS when testing the same backend, or document a dedicated `timesafari-dev` project. +- Add an **Android** app with package name **`app.timesafari`**. +- Enable **Cloud Messaging** (default on new projects). +- Download **`google-services.json`** and install under `android/app/`. +- Note: `android/.gitignore` may exclude `google-services.json` — developers copy locally; never commit secrets. + +### google-services.json + +- Placement: `android/app/google-services.json` +- Sync after add: `npx cap sync android`, rebuild in Android Studio +- Verify build merges Firebase config (no “missing google-services” Gradle errors) +- Relationship to `VITE_FIREBASE_*` for the web layer / Capacitor JS Firebase initialization + +### Android notification permissions + +- **Android 13+ (API 33):** `POST_NOTIFICATIONS` runtime permission — required for notification **display**; document interaction with **data-only** FCM wake (may still deliver to app code when permission denied — verify against current app behavior and document accurately). +- **Android 12 and below:** install-time grant model; fewer runtime prompts. +- App Settings → Notifications — manual enable path for testers. +- Link [notification-permissions-and-rollovers.md](./notification-permissions-and-rollovers.md) for product-level permission UX. + +### FCM token handling + +- Token obtained via Capacitor Push Notifications + `firebaseMessagingClient.ts` (same JS path as iOS). +- **Register Token Now** in debug panel → `POST /notifications/register` with `platform: "android"`. +- Token rotation: when to re-register; duplicate skip behavior in panel. +- Ensure **notification-wakeup-service** stores/sends to the token shown in the panel for `/debug/send-wakeup`. +- Optional: `adb` cannot easily read FCM token — panel is source of truth (same as iOS). + +### Android background delivery behavior + +- FCM **data** messages handled in foreground/background per Capacitor plugin and `NativeNotificationService.ts`. +- No APNs intermediary — document expected latency vs iOS. +- **High-priority** FCM for wakeup testing (align with backend message options). +- App in **background** vs **foreground** vs **killed** — different from iOS “swipe away” story: + - **Force stop** (Settings → Force stop): delivery often blocked until user launches app again (stricter than iOS “backgrounded”). + - **Recent apps swipe**: behavior varies by OEM/Android version — document “test with Home button background, not force stop.” +- `pushNotificationReceived` / listener registration at startup (`main.capacitor.ts`). + +### Doze Mode + +- Device idle → deferred network and job execution. +- Testing: use `adb shell dumpsys deviceidle` (document safe dev-only commands) or unplugged idle wait. +- Explain why `/debug/send-wakeup` may succeed on server but device wakes late. +- Whitelisting app for tests (developer settings) — use cautiously; note production users won’t do this. + +### Battery optimization + +- Settings → Apps → TimeSafari → Battery → **Unrestricted** vs **Optimized**. +- Manufacturer “battery saver” modes that restrict background network. +- Recommend **Unrestricted** (or equivalent) for local wakeup validation; warn that production users may remain optimized. + +### OEM restrictions (Samsung, Xiaomi, Oppo, etc.) + +- **Autostart** / **Background activity** / **Battery** menus on Samsung, Xiaomi (MIUI), Oppo/ColorOS, Huawei, OnePlus, etc. +- Symptom: FCM works on Pixel but not on OEM device until autostart enabled. +- Provide a short “if wake fails on OEM, check…” checklist without exhaustive per-OEM screenshots (link community docs if needed). +- Physical device testing should include at least one **stock-ish** device (Pixel) and one **OEM** device when possible. + +--- + +## Proposed outline for `doc/local-android-testing-ngrok.md` + +Suggested section order mirroring iOS doc for easy maintenance: + +1. Title, audience, goal (Android physical device + ngrok + wakeup service) +2. Architecture overview (FCM direct to Android) +3. Prerequisites (Android Studio, device, Firebase Android app, non-prod build) +4. ngrok install, account, tunnel (**reuse iOS §1**) +5. Start notification-wakeup-service (**reuse iOS §2**) +6. ngrok HTTPS URL (**reuse iOS §3**) +7. Build and open Android project (**new**, replaces iOS §4) +8. Firebase setup for Android (**new**, replaces iOS §5 — no APNs) +9. Notification Debug Panel (**reuse iOS §6**) +10. Android configuration checklist (**new**, replaces iOS §7) +11. Android-specific testing notes (**new**, replaces iOS §8) +12. Recommended debug workflow (**reuse iOS §9** + logcat) +13. Sample curl commands (**reuse iOS §10** + `platform: "android"`) +14. Troubleshooting (**merge reusable + Android push rows**) +15. Key source files (**reuse iOS §12**) +16. Related docs (**iOS doc + Android device guide + BUILDING**) + +--- + +## Wording and terminology substitutions + +When adapting reused sections: + +| iOS doc term | Android doc term | +|--------------|------------------| +| iPhone | Android phone / device | +| Xcode console | logcat / Android Studio Logcat | +| `build:ios:dev` / `test` | `build:android:dev` / `test` | +| `GoogleService-Info.plist` | `google-services.json` | +| APNs / silent push | FCM data message / high-priority data | +| Bundle ID | Application ID / package name (`app.timesafari`) | +| Physical iPhone required for APNs | Physical device strongly recommended for Doze/OEM/FCM realism | +| `platform: "ios"` in curl | `platform: "android"` | + +--- + +## Gaps to resolve while writing the Android guide + +Research during authoring (code + **notification-wakeup-service** + **daily-notification-plugin**): + +1. Exact FCM Android message priority and payload fields for `WAKEUP_PING` (parity with iOS data message). +2. Whether `POST_NOTIFICATIONS` denial blocks data message delivery to JS listeners on API 33+. +3. Gradle/Firebase plugin versions already in `android/` — document exact files to touch. +4. Android **Pending Notification Inspector** parity with iOS panel section. +5. Whether emulator with Google Play image is acceptable for minimal FCM smoke tests vs mandatory physical device for wakeup SLA testing. + +--- + +## Document maintenance + +| Document | Role | +|----------|------| +| [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) | Canonical iOS + ngrok workflow (unchanged by this analysis) | +| **This file** | Reuse vs rewrite matrix and Android topic backlog | +| *Future* `local-android-testing-ngrok.md` | Operator guide for Android testers | + +When backend or debug panel behavior changes, update **both** platform guides’ shared sections in lockstep (or extract shared “ngrok + debug panel” snippet later — out of scope unless requested). diff --git a/doc/local-android-testing-ngrok.md b/doc/local-android-testing-ngrok.md new file mode 100644 index 00000000..ad35cbe8 --- /dev/null +++ b/doc/local-android-testing-ngrok.md @@ -0,0 +1,961 @@ +# Local Android Testing with ngrok (notification-wakeup-service) + +**Last updated:** 2026-06-02 (verification checklist, end-to-end test) +**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. + +**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. + +--- + +## Architecture overview + +End-to-end flow when testing New Activity / silent wake on a physical Android phone: + +```text +┌─────────────────────┐ HTTPS ┌──────────────────────┐ +│ Mac (localhost) │ ◄───────────── │ ngrok edge │ +│ notification- │ tunnel │ (public HTTPS URL) │ +│ wakeup-service │ └──────────┬───────────┘ +└──────────┬──────────┘ │ + │ │ fetch + │ POST /notifications/refresh │ POST /notifications/register + │ ▼ + │ ┌──────────────────────┐ + │ │ crowd-funder-for- │ + │ │ time-pwa (Capacitor │ + │ │ Android on device) │ + │ └──────────┬───────────┘ + │ │ + │ FCM data message (WAKEUP_PING) │ daily-notification-plugin + ▼ ▼ (local schedule replace) +┌─────────────────────┐ ┌──────────────────────┐ +│ Firebase Cloud │ ──FCM────────► │ Android device │ +│ Messaging │ direct │ app.timesafari.app │ +└─────────────────────┘ └──────────────────────┘ +``` + +Unlike iOS, Android does **not** use APNs. FCM delivers directly to the app via Google Play services on the device. + +### Repos and responsibilities + +| 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 | + +### Android wakeup flow (production path) + +1. **notification-wakeup-service** (or `/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. + +Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`). + +--- + +## Prerequisites + +- Mac (or Linux) with Node.js 18+, **notification-wakeup-service** cloned and runnable +- **Android Studio**, JDK 17+, `ANDROID_HOME`, and `adb` — see [android-physical-device-guide.md](./android-physical-device-guide.md) +- Physical Android device with USB debugging (recommended for realistic FCM, Doze, and OEM behavior) +- ngrok account (free tier is enough for dev) +- Firebase project with an **Android** app registered for the Gradle **application ID** +- Non-production app build (Notification Debug Panel is dev-only) +- `google-services.json` in `android/app/` (not committed; see [`.gitignore`](../android/.gitignore)) + +--- + +## 1. Install and configure ngrok (macOS) + +### Install + +```bash +# Homebrew +brew install ngrok/ngrok/ngrok +``` + +Or download from [https://ngrok.com/download](https://ngrok.com/download). + +### Create ngrok account and configure auth token + +1. Sign up at [https://dashboard.ngrok.com/signup](https://dashboard.ngrok.com/signup). +2. Copy your authtoken from **Your Authtoken** in the dashboard. +3. Configure the CLI: + +```bash +ngrok config add-authtoken YOUR_AUTHTOKEN_HERE +``` + +### Start a tunnel to the wakeup service + +Assume the service listens on port **3000** (confirm in **notification-wakeup-service** `README` or `.env`). + +```bash +# Terminal A — backend +cd /path/to/notification-wakeup-service + +npm install + +# one-time setup if needed +cp .env.example .env + +# configure Firebase service account etc. as required +export PORT=3000 + +npm run dev +``` + +```bash +# Terminal B — ngrok +ngrok http 3000 +``` + +ngrok prints a forwarding URL, for example: + +```text +Forwarding https://abc123.ngrok-free.app -> http://localhost:3000 +``` + +Use the **HTTPS** URL (not `http://127.0.0.1:3000`). The phone cannot reach your Mac’s localhost without the tunnel. + +> **Note:** Free ngrok URLs change every time you restart ngrok unless you use a reserved domain (paid). Update the app debug override whenever the URL changes. + +--- + +## 2. Start the backend locally + +On first setup, copy `.env.example` to `.env` and set Firebase service account, `PORT`, and other variables per **notification-wakeup-service** docs. + +If the backend is not already running from section 1: + +```bash +cd /path/to/notification-wakeup-service +npm run dev +``` + +Verify locally before ngrok: + +```bash +curl -sS http://localhost:3000/health +``` + +Expected: HTTP 200 and a JSON body indicating the service is up (exact shape depends on that repo). + +### Configure Firebase Admin for the backend + +**notification-wakeup-service** uses the Firebase Admin SDK to send FCM messages from your Mac. + +1. Firebase Console → **Project settings** → **Service accounts**. +2. Click **Generate new private key** and download the JSON file. +3. Store the JSON outside the repo (do not commit it). +4. Point the backend at it: + +```bash +export GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/to/service-account.json" +``` + +Set the same variable (or the equivalent documented in **notification-wakeup-service**) in the shell where you run `npm run dev`, or add it to that repo’s `.env`. + +--- + +## 3. Obtain and use the ngrok HTTPS URL + +1. Run `ngrok http `. +2. Copy the `https://….ngrok-free.app` host from the **Forwarding** line. +3. Do **not** add a trailing slash when saving in the app (the debug config trims it). +4. Optional: open `http://127.0.0.1:4040` (ngrok web UI) to inspect requests and responses while testing. + +Test through the tunnel from your Mac: + +```bash +export NGROK_URL="https://abc123.ngrok-free.app" +curl -sS "$NGROK_URL/health" +``` + +--- + +## 4. Build and deploy to a physical Android device + +From **crowd-funder-for-time-pwa**, build a non-production Capacitor bundle and sync Android. Complete [section 5](#5-firebase-setup-for-android-first-time) before expecting FCM to work. + +```bash +npm install +npm run build:android:dev # or build:android:test — non-production for debug panel +``` + +Or use the combined run targets from [android-physical-device-guide.md](./android-physical-device-guide.md): + +```bash +npm run build:android:debug:run +# or +npm run build:android:test:run +``` + +Open `android/` in Android Studio if you need to inspect native logs or signing. Ensure `VITE_FIREBASE_*` variables are set for the build you use (see `.env` / [BUILDING.md](../BUILDING.md)). + +Native push registration runs at startup via `initializeNativePushAndFirebaseMessaging()` in `main.capacitor.ts` once Firebase and `google-services.json` are in place. + +--- + +## 5. Firebase setup for Android (first-time setup) + +Complete this section once before your first physical-device push test. If Firebase is already configured for this Android app, skip to [section 6](#6-configure-the-notification-debug-panel-backend-override). + +### Create or access a Firebase account + +1. Sign in at [https://console.firebase.google.com/](https://console.firebase.google.com/). +2. No paid Firebase plan is required. The free **Spark** plan supports **Firebase Cloud Messaging (FCM)** and local ngrok development. + +### Create a Firebase project + +1. In the Firebase Console, click **Add project** (or **Create a project**). +2. Enter a project name (for example, `timesafari-dev`) and continue through the wizard. +3. **Google Analytics** is optional for this workflow. +4. When the project is created, open it. **Cloud Messaging** is available on all projects — you do not need a separate “enable FCM” step beyond registering the Android app. + +### Add the Android app in Firebase + +1. In the project overview, click the **Android** icon (**Add app** → Android). +2. Enter the **Android package name**. It must **exactly** match the Gradle **applicationId**: + + **`app.timesafari.app`** + + (see `applicationId` in `android/app/build.gradle`. This differs from Capacitor `appId` in `capacitor.config.ts`, which is `app.timesafari`.) + +3. App nickname and SHA-1/SHA-256 are optional for basic FCM wakeup testing; you may add debug keystore fingerprints later if Firebase Console prompts for them. +4. Download **`google-services.json`** when prompted. + +### Place google-services.json + +1. Copy the file to: + + ```text + crowd-funder-for-time-pwa/android/app/google-services.json + ``` + +2. The repo gitignores this file — each developer keeps a local copy; never commit it. + +3. Rebuild so Gradle applies the Google Services plugin (`android/app/build.gradle` applies `com.google.gms.google-services` when the file exists). If the file is missing, the build logs: *google-services.json not found, google-services plugin not applied. Push Notifications won't work*. + +4. Sync Capacitor if you changed native config: + + ```bash + npx cap sync android + ``` + +### Enable Firebase Cloud Messaging + +FCM is enabled by default for Firebase projects. Confirm in **Project settings** → **Cloud Messaging**: + +- **Cloud Messaging API** is available (legacy or HTTP v1 per your backend setup). +- The Android app (`app.timesafari.app`) appears under your apps. + +The **notification-wakeup-service** sends wakeup messages through Firebase Admin using the same project’s service account ([section 2](#2-start-the-backend-locally)). + +### Web / Capacitor JS Firebase config + +In addition to `google-services.json`, the Capacitor web build needs `VITE_FIREBASE_*` env vars (API key, project ID, messaging sender ID, app ID, and optionally `VITE_FIREBASE_VAPID_KEY`). These must refer to the **same Firebase project** as `google-services.json`. + +### Verify Firebase configuration + +Before ngrok end-to-end testing, confirm: + +- [ ] App builds and installs without Gradle errors about `google-services.json`. +- [ ] Push permission is **granted** (see [section 7](#7-android-notification-permissions)). +- [ ] **Notification Debug Panel** shows an FCM token (after permission and registration). +- [ ] **Register Token Now** succeeds and ngrok shows `POST /notifications/register`. +- [ ] Backend health and `GOOGLE_APPLICATION_CREDENTIALS` are set so `/debug/send-wakeup` can run. + +--- + +## 6. Configure the Notification Debug Panel backend override + +The app normally calls `APP_SERVER` (from `VITE_APP_SERVER`). For local wakeup testing, override the notification API base URL without rebuilding. + +### Open the panel + +1. Use a **non-production** build (e.g. `build:android:dev` or `build:android:test`). +2. **Account** → enable **Show All General Advanced Functions**. +3. Open **Notification Debug Panel** (route `/dev/notifications`). + +### Backend Testing section + +| Control | Purpose | +|---------|---------| +| **Notification Backend URL** | Paste ngrok HTTPS URL → **Save Backend URL** | +| **Test Mode** | Sends `testMode: true` on register/refresh (default on when unset in storage) | +| **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** | Calls refresh API directly (no FCM) — quick backend + ngrok test | +| **Event Log** | Shared `[Notifications]` panel log (100 entries) | + +Persistence: `localStorage` keys `notificationDebug.backendBaseUrl` and `notificationDebug.testMode` (`NotificationDebugConfig.ts`). + +### 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. + +### Two “Simulate WAKEUP_PING” buttons + +| 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 without FCM. + +### Programmatic override (optional) + +From Chrome DevTools attached to the WebView (`chrome://inspect` → your app): + +```javascript +import { + setBackendBaseUrl, + setTestMode, + getNotificationApiBaseUrl, +} from "@/services/notifications"; + +setBackendBaseUrl("https://abc123.ngrok-free.app"); +setTestMode(true); +getNotificationApiBaseUrl(); // → ngrok URL +``` + +--- + +## 7. Android notification permissions + +### Android 13+ (API 33+) + +`AndroidManifest.xml` declares: + +```xml + +``` + +On **Android 13 and above**, this is a **runtime** permission. The user must grant it before the system allows notification **display**. The OS shows a system dialog the first time the app requests it. + +### How this app requests permission + +At startup, `initializeNativePushAndFirebaseMessaging()` in `firebaseMessagingClient.ts` calls: + +```typescript +const perm = await PushNotifications.requestPermissions(); +``` + +If `perm.receive !== "granted"`, native push registration does not proceed and you will not get a reliable FCM token in the debug panel. + +Daily notification flows can also request permission via **daily-notification-plugin** (`NativeNotificationService.requestPermissions()` → plugin `POST_NOTIFICATIONS`). See [notification-permissions-and-rollovers.md](./notification-permissions-and-rollovers.md). + +### Android 12 and below + +`POST_NOTIFICATIONS` does not exist as a runtime prompt on older APIs. Notifications are generally allowed at install time; fewer permission dialogs appear during testing. + +### Manual check for testers + +**Settings** → **Apps** → **TimeSafari** → **Notifications** → ensure notifications are allowed. + +### Permission vs FCM data wake + +**WAKEUP_PING** uses FCM **data** messages handled in `pushNotificationReceived`. For local testing: + +- Grant notification permission so **Register Token Now** and token display in the debug panel work. +- If permission is denied, fix permission before debugging FCM wakeup; do not assume data delivery will run the full refresh pipeline. + +--- + +## 8. Android Platform Notes + +### FCM vs iOS silent push + +Android generally delivers **background FCM data messages** more reliably than iOS **silent APNs** (`content-available`), especially when the app is backgrounded (not force-stopped) and Google Play services is healthy. There is no separate APNs hop; FCM talks to the device directly. + +Delivery is still **not guaranteed**. Treat wakeup as best-effort in tests and in production expectations. + +### Force-stop vs backgrounding +| State | Typical FCM behavior | +|-------|----------------------| +| **Foreground** | Data messages handled promptly; `pushNotificationReceived` fires. | +| **Background** (Home) | Data messages usually delivered; handler may run with delay under load. | +| **Force stop** (Settings → Force stop) | Delivery often **blocked** until the user launches the app again. Stricter than iOS “swipe away” in many cases. | +| **Recents swipe** | Varies by OEM/Android version; less predictable than Home. Prefer **Home** for tests. | + +Always validate wakeup with the app **backgrounded**, not force-stopped. + +### Notification permissions (Android 13+) + +On **API 33+**, `POST_NOTIFICATIONS` is a runtime permission ([section 7](#7-android-notification-permissions)). This app requests push permission via `PushNotifications.requestPermissions()` before `register()`. Denied permission blocks token registration in the debug panel and undermines end-to-end testing—fix permission before debugging FCM. + +### 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. + +### 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. + +### Logcat + +```bash +adb logcat | grep -E '\[Notifications\]|\[FirebaseMessaging\]|\[NativeNotificationService\]' +``` + +Expect `pushNotificationReceived type=WAKEUP_PING` and `WAKEUP_PING handler — invoking refresh` after a successful wake. + +--- + +## 9. Battery Optimization Caveats + +Android power management can **delay or batch** FCM delivery and background work even when the server returns success from `/debug/send-wakeup`. + +### Doze Mode + +When the device is **unplugged**, screen off, and idle, Android enters **Doze**. Network access and jobs are deferred to **maintenance windows**. Wakeup messages may arrive minutes late. + +**Testing tip:** Keep the device on charger, or wake the screen briefly, or wait longer (2–5+ minutes) before failing a Doze test. + +### App Standby + +Apps used infrequently move to **standby** buckets with reduced background network. Frequent dev installs reset some of this; long-idle test devices do not. + +**Testing tip:** Open the app before sending wakeup; register token again if the device was idle for days. + +### Adaptive Battery + +**Adaptive Battery** / per-app battery savers learn usage patterns and restrict background activity for “unused” apps. + +**Testing tip:** **Settings** → **Apps** → **TimeSafari** → **Battery** → **Unrestricted** (or **Don’t optimize**) during local validation. + +### 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. + +| Vendor | Where to look (names vary by OS version) | +|--------|------------------------------------------| +| **Samsung** | Device care → Battery → Background usage limits; app → Battery → Unrestricted | +| **Xiaomi** | Security app → Autostart; Battery saver → No restrictions | +| **Oppo** / **Realme** | Battery → App battery management → Allow background activity | +| **Vivo** | iManager / Battery → High background power consumption | +| **Huawei** | App launch → Manage manually → enable Auto-launch, Secondary launch, Run in background | + +If wakeup works on a **Pixel** but fails on an OEM phone, assume battery policy first—not a broken FCM payload. + +### How this affects wakeup testing + +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** in the debug panel to separate FCM delay from refresh/API issues. + +--- + +## 10. Recommended local testing workflow + +1. Start **notification-wakeup-service** on the Mac (`npm run dev`, `GOOGLE_APPLICATION_CREDENTIALS` set). +2. Start **ngrok** and copy the HTTPS URL. +3. Install a **non-production** Android build with `google-services.json` in place. +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. +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** (backend button) to verify ngrok + refresh without FCM. +9. 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 timing lines. +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. + +For a formal pass/fail sequence, use the [Verification Checklist](#11-verification-checklist) below. + +--- + +## 11. Verification Checklist + +Use this checklist during development or QA sign-off. Each step lists **actions**, then **expected outcome**. Prerequisites: non-production Android build, `google-services.json` installed, physical device (recommended), backend + ngrok running. + +| # | Check | Pass criteria | +|---|--------|----------------| +| 1 | Backend via ngrok | §1 below | +| 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 | +| 7 | Manual wakeup sends FCM | §7 | +| 8 | WAKEUP_PING → refresh | §8 | +| 9 | Replace after refresh | §9 | +| 10 | Test mode frequent refreshes | §10 | + +### 1. Backend reachable through ngrok + +**Actions:** + +```bash +export BASE="https://YOUR-NGROK-HOST.ngrok-free.app" +curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health" +``` + +**Expected outcome:** HTTP **200**; JSON body indicates the service is healthy (exact fields per **notification-wakeup-service**). Mac `curl http://localhost:3000/health` must also pass before blaming ngrok. + +--- + +### 2. Device can reach backend override URL + +**Actions:** + +1. Open **Notification Debug Panel** (`/dev/notifications`). +2. Paste ngrok HTTPS URL (no trailing slash) → **Save Backend URL**. +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 production `APP_SERVER`) for `/notifications/register` and `/notifications/refresh`. + +--- + +### 3. FCM token successfully registered + +**Actions:** + +1. Grant notification permission when prompted (Android 13+). +2. Tap **Register Token Now**. +3. Watch Event Log and logcat (`[Notifications]`, `[FirebaseMessaging]`). + +**Expected outcome:** + +- **Current FCM Token** shows a non-empty token (Copy works). +- Event Log: `[Notifications] Token registration success` (not failure / auth unavailable). +- ngrok inspect: `POST /notifications/register` with **200**; body includes `platform: "android"`, `testMode: true` (if Test Mode on), `deviceId`, and `fcmToken`. + +--- + +### 4. Device record appears in notification-wakeup-service + +**Actions:** + +1. In ngrok inspect (`http://127.0.0.1:4040`), open the **register** request → copy `deviceId` and `fcmToken` from the JSON body. +2. In **notification-wakeup-service**, confirm persistence per that repo (server logs, database, admin/list endpoint, or debug CLI). + +**Expected outcome:** The `deviceId` and `fcmToken` from step 3 are stored and retrievable by the service. `/debug/send-wakeup` can target that `deviceId` (or token, per service contract). If register returned 200 but no record exists, fix the wakeup service store/config before FCM tests. + +> **Note:** `deviceId` is created in-app (`Preferences` key `stable_device_id` in `deviceId.ts`). It is not shown in the debug panel; use ngrok request body or logcat `[DeviceId]` lines. + +--- + +### 5. Refresh endpoint returns schedule data + +**Actions:** + +1. Tap **Refresh Notifications** in the panel (or curl below). +2. Inspect ngrok response body. + +```bash +curl -sS -X POST "$BASE/notifications/refresh" \ + -H "Content-Type: application/json" \ + -d '{"platform":"android","testMode":true}' +``` + +**Expected outcome:** HTTP **200**; JSON includes `nextNotifications` array with at least one `{ "timestamp": }` 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. + +--- + +### 6. Local notifications are scheduled + +**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))`. + +**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. + +--- + +### 7. Manual wakeup endpoint sends FCM successfully + +**Actions:** + +1. Use `deviceId` from step 4. +2. From the Mac: + +```bash +curl -sS -X POST "$BASE/debug/send-wakeup" \ + -H "Content-Type: application/json" \ + -d '{"deviceId":"YOUR_DEVICE_ID","testMode":true}' +``` + +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`; server logs indicate FCM message enqueued/sent with `data.type = "WAKEUP_PING"`. This step alone does not prove device delivery (see step 8). + +--- + +### 8. WAKEUP_PING triggers refreshNotifications() + +**Actions:** + +1. **Background** the app (Home — not force-stop). +2. Run step 7 again (or wait for a server-driven wakeup). +3. Filter logcat: + +```bash +adb logcat | grep -E 'WAKEUP_PING|pushNotificationReceived|Refresh completed' +``` + +**Expected outcome (within ~30–120s, longer under Doze/OEM):** + +1. `pushNotificationReceived` / `WAKEUP_PING received` +2. `WAKEUP_PING handler — invoking refresh` +3. ngrok: new `POST /notifications/refresh` +4. `Refresh completed in …ms (scheduled N)` + +**Isolation:** **Wakeup Ping Simulator** on the panel should produce lines 2–4 without FCM. **Backend Testing → Simulate WAKEUP_PING** proves refresh only (no handler). + +--- + +### 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. + +--- + +### 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. + +--- + +## 12. End-to-End Test + +Single scripted run from cold start to scheduled local notification. Time: ~15–30 minutes (plus optional wait for a near `testMode` fire). + +### Phase A — Mac backend and tunnel + +1. Terminal A: + +```bash +cd /path/to/notification-wakeup-service +export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" +export PORT=3000 +npm run dev +``` + +2. Verify: `curl -sS http://localhost:3000/health` → **200**. + +3. Terminal B: `ngrok http 3000` → copy `https://….ngrok-free.app` → `export BASE=…`. + +4. Verify: `curl -sS "$BASE/health"` → **200**. + +### Phase B — Android app + +5. Build and install dev/test APK (`npm run build:android:dev` or `build:android:debug:run`); `google-services.json` in `android/app/`. + +6. Launch app; accept **notification permission**. + +7. **Account** → **Show All General Advanced Functions** → **Notification Debug Panel**. + +8. Paste `BASE` → **Save Backend URL**; enable **Test Mode**. + +### Phase C — Register and schedule + +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`. + +12. **Pending Notification Inspector** → **Refresh** → future alarm(s) listed. + +### Phase D — FCM wakeup path + +13. Background app (Home). + +14. Mac: `curl -X POST "$BASE/debug/send-wakeup" -H "Content-Type: application/json" -d '{"deviceId":"…","testMode":true}'` → server FCM success. + +15. Within 30–120s (longer if unplugged/Doze): logcat shows `WAKEUP_PING` → refresh; 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** (backend) then **Wakeup Ping Simulator** to bisect FCM vs handler; see [Troubleshooting](#14-troubleshooting). + +### End-to-end pass criteria + +| Phase | Pass | +|-------|------| +| 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 | + +--- + +## 13. Sample curl commands + +Set your tunnel base URL: + +```bash +export BASE="https://abc123.ngrok-free.app" +``` + +### Health + +```bash +curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health" +``` + +### Register device (mirror app payload) + +```bash +curl -sS -X POST "$BASE/notifications/register" \ + -H "Content-Type: application/json" \ + -d '{ + "deviceId": "00000000-0000-4000-8000-000000000001", + "fcmToken": "YOUR_FCM_TOKEN_FROM_DEBUG_PANEL", + "platform": "android", + "testMode": true + }' +``` + +### Refresh (mirror app payload) + +```bash +curl -sS -X POST "$BASE/notifications/refresh" \ + -H "Content-Type: application/json" \ + -d '{ + "platform": "android", + "testMode": true + }' +``` + +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`). + +### Send wakeup push (debug) + +Exact path and body depend on **notification-wakeup-service**; typical pattern: + +```bash +curl -sS -X POST "$BASE/debug/send-wakeup" \ + -H "Content-Type: application/json" \ + -d '{ + "deviceId": "00000000-0000-4000-8000-000000000001", + "testMode": true + }' +``` + +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`. + +--- + +## 14. Troubleshooting + +Structured checks for local ngrok + FCM testing. Each item lists **symptoms**, **likely causes**, **verification**, and **fixes**. + +### Token registration failures + +**Symptoms:** Event Log shows token registration failure; no FCM token in debug panel; ngrok has no `POST /notifications/register`; curl register fails. + +**Likely causes:** Notification permission denied; missing `google-services.json`; Firebase package mismatch (`app.timesafari.app`); `VITE_FIREBASE_*` not in build; auth headers missing for register; duplicate-token skip. + +**Verification:** + +1. Settings → app → Notifications → allowed. +2. Logcat: `[FirebaseMessaging] Push permission not granted` or registration errors. +3. Panel shows a token string before **Register Token Now**. +4. ngrok inspect UI (`http://127.0.0.1:4040`) for register request status body. + +**Fixes:** Grant permission and cold-start app; add `android/app/google-services.json` and rebuild; align Firebase Android app ID with Gradle `applicationId`; rebuild with correct `.env`; tap **Register Token Now** (forces re-register); fix DID/auth if register returns 401. + +--- + +### Backend unreachable + +**Symptoms:** Backend Status unhealthy in panel; register/refresh network errors; Mac `curl localhost:3000/health` fails. + +**Likely causes:** `notification-wakeup-service` not running; wrong `PORT`; firewall; typo in saved backend URL (not ngrok). + +**Verification:** + +```bash +curl -sS http://localhost:3000/health +``` + +Compare with panel **Backend Status** and Event Log error text. + +**Fixes:** Start backend with `npm run dev` and `GOOGLE_APPLICATION_CREDENTIALS`; match `PORT` to ngrok target; paste full ngrok **HTTPS** URL into panel (no trailing slash). + +--- + +### Stale ngrok URL + +**Symptoms:** Worked yesterday; today all API calls fail or hit wrong host; ngrok shows no requests. + +**Likely causes:** Free ngrok URL changed after tunnel restart; old URL still in `notificationDebug.backendBaseUrl`. + +**Verification:** Compare panel URL to current `ngrok http` **Forwarding** line; `curl -sS "$NEW_URL/health"`. + +**Fixes:** Copy new HTTPS URL → **Save Backend URL** in panel; or clear override only if intentionally returning to `APP_SERVER`. + +--- + +### Refresh endpoint failures + +**Symptoms:** **Refresh Notifications** fails; Event Log HTTP error; no `scheduled X` line; ngrok missing `POST /notifications/refresh`. + +**Likely causes:** Stale ngrok URL; backend down; 404/wrong path; JWT/native fetcher not configured; refresh auth failure. + +**Verification:** + +1. **Simulate WAKEUP_PING** (backend button) — 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. + +--- + +### FCM message not received + +**Symptoms:** `/debug/send-wakeup` returns success on Mac; no `pushNotificationReceived` / `WAKEUP_PING` in logcat within 2 minutes; refresh never triggered. + +**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. + +**Verification:** + +1. App **backgrounded** (Home), not force-stopped. +2. Panel FCM token matches token used by server/register. +3. **Simulate WAKEUP_PING** (backend button) works → isolates FCM path. +4. Wait 30–120s (longer on Doze/OEM). +5. 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. + +--- + +### Notification permission denied + +**Symptoms:** No permission dialog or user denied; logcat `Push permission not granted`; empty FCM token; register skipped. + +**Likely causes:** User denied prompt; permission revoked in Settings; testing on API 33+ without `POST_NOTIFICATIONS` grant. + +**Verification:** Settings → Apps → TimeSafari → Notifications; logcat after cold start for `requestPermissions` result. + +**Fixes:** Enable notifications in Settings; reinstall to re-prompt if needed; cold-start app so `initializeNativePushAndFirebaseMessaging()` runs again. + +--- + +### Notifications not appearing + +**Symptoms:** Refresh succeeds (`scheduled X` in log) 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). + +**Fixes:** Grant `POST_NOTIFICATIONS`; check `SCHEDULE_EXACT_ALARM` / alarm permission per plugin docs; fix refresh payload; test with nearer timestamps via backend `testMode`. + +--- + +### Duplicate notifications + +**Symptoms:** Multiple identical local notifications; Event Log shows repeated refresh lines. + +**Likely causes:** Multiple `WAKEUP_PING` deliveries; repeated manual **Refresh**; flood test; separate Daily Reminder vs New Activity schedules. + +**Verification:** Event Log count of refresh completions; ngrok inspect for duplicate `POST /notifications/refresh`. + +**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. + +--- + +### Device enters battery-saving mode + +**Symptoms:** Intermittent wakeup; long delays; works on charger but not unplugged; OEM “battery saver” icon on. + +**Likely causes:** Doze; Adaptive Battery; manufacturer saver (Samsung, Xiaomi, Oppo, Vivo, Huawei). + +**Verification:** Settings → Battery; OEM battery app; reproduce unplugged screen-off vs charging. + +**Fixes:** For dev testing set app battery to **Unrestricted**; enable **Autostart** / background activity on OEM; wait for maintenance window or wake device before concluding FCM failure. + +--- + +### Gradle: Push Notifications won't work + +**Symptoms:** Build log: `google-services.json not found, google-services plugin not applied`. + +**Likely causes:** Missing `android/app/google-services.json`. + +**Verification:** File exists on disk; rebuild shows Google Services plugin applied. + +**Fixes:** Download from Firebase Console (package `app.timesafari.app`); place in `android/app/`; `npx cap sync android` and rebuild. + +--- + +## 15. Key source files (crowd-funder-for-time-pwa) + +| File | Purpose | +|------|---------| +| `src/services/notifications/NotificationDebugConfig.ts` | Backend URL + testMode override | +| `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/firebaseMessagingClient.ts` | Capacitor push listeners, permission, token registration | +| `src/components/dev/NotificationDebugPanel.vue` | Dev UI | +| `src/main.capacitor.ts` | Native push init at startup | +| `android/app/build.gradle` | `applicationId`, conditional `google-services` plugin | +| `android/app/src/main/AndroidManifest.xml` | `POST_NOTIFICATIONS` and plugin permissions | + +--- + +## 16. Related docs + +- [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) — iOS + APNs equivalent +- [local-android-testing-analysis.md](./local-android-testing-analysis.md) — reuse matrix used to author this guide +- [android-physical-device-guide.md](./android-physical-device-guide.md) — USB, `adb`, build commands +- [notification-system-overview.md](./notification-system-overview.md) +- [notification-from-api-call.md](./notification-from-api-call.md) +- [notification-permissions-and-rollovers.md](./notification-permissions-and-rollovers.md) +- [BUILDING.md](../BUILDING.md) — Android build commands +- [Notification Debug Panel (README)](../README.md#notification-debug-panel-dev-builds) + +For plugin-native behavior (exact alarms, Android pending inspector), see **daily-notification-plugin** documentation. For FCM payload format and `/debug/send-wakeup` contract, see **notification-wakeup-service**.