Files
crowd-funder-for-time-pwa/doc/local-android-testing-ngrok.md
Jose Olarte III 6a7f341990 docs(android): document Send Real WAKEUP_PING debug panel flow
Add §6 coverage for the full backend→FCM→refresh pipeline, expected
Logcat lines, and troubleshooting that distinguishes backend success
from end-to-end delivery; cross-link checklist, workflow, and §14.
2026-06-12 18:23:19 +08:00

46 KiB
Raw Blame History

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 for the iOS workflow (APNs, Xcode). 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:

┌─────────────────────┐     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 and Battery Optimization Caveats).
  3. Capacitor pushNotificationReceived fires → handleCapacitorPushNotificationReceived() in NativeNotificationService.ts.
  4. Handler calls refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" }), which POSTs {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
  • 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)

1. Install and configure ngrok (macOS)

Install

# Homebrew
brew install ngrok/ngrok/ngrok

Or download from https://ngrok.com/download.

Create ngrok account and configure auth token

  1. Sign up at https://dashboard.ngrok.com/signup.
  2. Copy your authtoken from Your Authtoken in the dashboard.
  3. Configure the CLI:
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).

# 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
# Terminal B — ngrok
ngrok http 3000

ngrok prints a forwarding URL, for example:

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 Macs 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:

cd /path/to/notification-wakeup-service
npm run dev

Verify locally before ngrok:

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 settingsService 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:
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 repos .env.


3. Obtain and use the ngrok HTTPS URL

  1. Run ngrok http <PORT>.
  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:

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 before expecting FCM to work.

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:

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).

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.

Create or access a Firebase account

  1. Sign in at 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:

    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:

    npx cap sync android
    

Enable Firebase Cloud Messaging

FCM is enabled by default for Firebase projects. Confirm in Project settingsCloud 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 projects service account (section 2).

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).
  • 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 (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 panels current FCM token (see below)
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.

WAKEUP_PING debug controls

Three panel actions exercise different segments of the wakeup pipeline. Use them to bisect failures (see Troubleshooting §14).

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.

Send Real WAKEUP_PING

Send Real WAKEUP_PING is the in-app equivalent of calling POST /debug/send-wakeup from the Mac (see Verification Checklist step 7). It is intended for local testing and diagnostics on non-production builds only.

What it does:

  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. POSTs 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 pushNotificationReceivedhandleCapacitorPushNotificationReceived()refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })POST /notifications/refreshapplyNotificationRefreshPayload() (clear + reschedule via daily-notification-plugin).

That exercises the full backend → FCM → Capacitor push listener → refresh request → notification rescheduling path on Android without manual curl on the Mac.

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).

Expected Logcat output

Filter logcat (prefix is always [Notifications]):

adb logcat | grep -E '\[Notifications\].*(Real WAKEUP_PING|pushNotificationReceived|WAKEUP_PING|Refresh started|Refresh completed)'

On a successful end-to-end run (HTTP success from the panel, then FCM delivery within ~30120s), 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)

Intermediate lines (e.g. WAKEUP_PING received — will trigger refresh, Schedule replacement: …, auth bypass messages) are normal. ngrok should also show a new POST /notifications/refresh after the push is handled.

Send Real WAKEUP_PING vs end-to-end 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.

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), not a backend enqueue failure.

Programmatic override (optional)

From Chrome DevTools attached to the WebView (chrome://inspect → your app):

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:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

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:

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.

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

SettingsAppsTimeSafariNotifications → 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). 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 Googles FCM endpoints (mobile data or WiFi).
  • After wake, the app must reach the ngrok HTTPS URL for POST /notifications/refresh.
  • Airplane mode, captive portals, VPNs, or flaky WiFi 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

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) instead of manual curl.


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 (25+ 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: SettingsAppsTimeSafariBatteryUnrestricted (or Dont 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 (Local) in the debug panel to separate FCM delay from refresh/API issues; use Send Real WAKEUP_PING for the full FCM path (§6).

  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 (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) 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).
  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 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:

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.
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": <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.


6. Local notifications are scheduled

Actions:

  1. After a successful refresh (step 5), open Pending Notification InspectorRefresh (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. Either:
    • On device: background the app (Home), open Notification Debug Panel, tap Send Real WAKEUP_PING; or
    • From the Mac:
curl -sS -X POST "$BASE/debug/send-wakeup" \
  -H "Content-Type: application/json" \
  -d '{"deviceId":"YOUR_DEVICE_ID","testMode":true}'
  1. 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).


8. WAKEUP_PING triggers refreshNotifications()

Actions:

  1. Background the app (Home — not force-stop).
  2. Run step 7 again (panel Send Real WAKEUP_PING or Mac curl), or wait for a server-driven wakeup.
  3. Filter logcat:
adb logcat | grep -E 'WAKEUP_PING|pushNotificationReceived|Refresh completed'

Expected outcome (within ~30120s, 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)

Isolation: Wakeup Ping Simulator on the panel should produce lines 25 without FCM. Simulate WAKEUP_PING (Local) proves refresh only (no handler, source WAKEUP_PING simulation). Full expected logcat for Send Real WAKEUP_PING: §6.


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: ~1530 minutes (plus optional wait for a near testMode fire).

Phase A — Mac backend and tunnel

  1. Terminal A:
cd /path/to/notification-wakeup-service
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
export PORT=3000
npm run dev
  1. Verify: curl -sS http://localhost:3000/health200.

  2. Terminal B: ngrok http 3000 → copy https://….ngrok-free.appexport BASE=….

  3. Verify: curl -sS "$BASE/health"200.

Phase B — Android app

  1. Build and install dev/test APK (npm run build:android:dev or build:android:debug:run); google-services.json in android/app/.

  2. Launch app; accept notification permission.

  3. AccountShow All General Advanced FunctionsNotification Debug Panel.

  4. Paste BASESave Backend URL; enable Test Mode.

Phase C — Register and schedule

  1. Register Token Now → Event Log success; ngrok POST /notifications/register 200; copy deviceId from ngrok request body.

  2. Confirm device in notification-wakeup-service (logs/DB per that repo).

  3. Refresh Notifications → Event Log scheduled N, N ≥ 1; ngrok refresh 200 with nextNotifications.

  4. Pending Notification InspectorRefresh → future alarm(s) listed.

Phase D — FCM wakeup path

  1. Background app (Home).

  2. 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.

  3. Within 30120s (longer if unplugged/Doze): logcat shows pushNotificationReceivedRefresh completed (WAKEUP_PING); ngrok shows second POST /notifications/refresh.

  4. Pending Inspector Refresh → timestamps updated (replacement, not duplicate stack).

Phase E — Optional delivery proof

  1. 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).

  2. 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 and Send Real WAKEUP_PING vs end-to-end success.

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:

export BASE="https://abc123.ngrok-free.app"

Health

curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"

Register device (mirror app payload)

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)

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):

{
  "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:

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 repos 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:

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 (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); confirm testMode if backend requires it.


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.

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).

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 30120s (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); 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).
  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 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

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.