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.
46 KiB
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)
- notification-wakeup-service (or
/debug/send-wakeup) sends an FCM data message withdata.type = "WAKEUP_PING". - FCM delivers to the device (best-effort; see Android Platform Notes and Battery Optimization Caveats).
- Capacitor
pushNotificationReceivedfires →handleCapacitorPushNotificationReceived()inNativeNotificationService.ts. - Handler calls
refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" }), whichPOSTs{backend}/notifications/refreshwithtestModefrom the debug config. - Backend returns
nextNotifications: [{ timestamp }, ...]. - 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, andadb— 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.jsoninandroid/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
- Sign up at https://dashboard.ngrok.com/signup.
- Copy your authtoken from Your Authtoken in the dashboard.
- 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 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:
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.
- Firebase Console → Project settings → Service accounts.
- Click Generate new private key and download the JSON file.
- Store the JSON outside the repo (do not commit it).
- 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 repo’s .env.
3. Obtain and use the ngrok HTTPS URL
- Run
ngrok http <PORT>. - Copy the
https://….ngrok-free.apphost from the Forwarding line. - Do not add a trailing slash when saving in the app (the debug config trims it).
- 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
- Sign in at https://console.firebase.google.com/.
- No paid Firebase plan is required. The free Spark plan supports Firebase Cloud Messaging (FCM) and local ngrok development.
Create a Firebase project
- In the Firebase Console, click Add project (or Create a project).
- Enter a project name (for example,
timesafari-dev) and continue through the wizard. - Google Analytics is optional for this workflow.
- 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
-
In the project overview, click the Android icon (Add app → Android).
-
Enter the Android package name. It must exactly match the Gradle applicationId:
app.timesafari.app(see
applicationIdinandroid/app/build.gradle. This differs from CapacitorappIdincapacitor.config.ts, which isapp.timesafari.) -
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.
-
Download
google-services.jsonwhen prompted.
Place google-services.json
-
Copy the file to:
crowd-funder-for-time-pwa/android/app/google-services.json -
The repo gitignores this file — each developer keeps a local copy; never commit it.
-
Rebuild so Gradle applies the Google Services plugin (
android/app/build.gradleappliescom.google.gms.google-serviceswhen 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. -
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 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).
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_CREDENTIALSare set so/debug/send-wakeupcan 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
- Use a non-production build (e.g.
build:android:devorbuild:android:test). - Account → enable Show All General Advanced Functions.
- 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 panel’s 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:
- 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.
POSTs to{backend override}/debug/send-wakeupwithdeviceId,fcmToken,platform: "android", andtestModefrom the panel config (NotificationDebugService.sendRealWakeupPing()).- On HTTP success, the notification-wakeup-service enqueues a real FCM data message with
data.type = "WAKEUP_PING"to that device. - When FCM delivers the message to the app, Capacitor fires
pushNotificationReceived→handleCapacitorPushNotificationReceived()→refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })→POST /notifications/refresh→applyNotificationRefreshPayload()(clear + reschedule via daily-notification-plugin).
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 ~30–120s), expect these key lines in order:
[Notifications] Real WAKEUP_PING requested
[Notifications] Real WAKEUP_PING success
[Notifications] pushNotificationReceived type=WAKEUP_PING
[Notifications] WAKEUP_PING handler — invoking refresh
[Notifications] Refresh started (WAKEUP_PING)
[Notifications] Refresh completed (WAKEUP_PING) in …ms (scheduled N)
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
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). 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
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 (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
- Server accepts
/debug/send-wakeup→ FCM enqueue succeeds. - Device may hold the message until Doze maintenance or OEM policy allows delivery.
pushNotificationReceivedruns →refreshNotificationsWithDiagnostics()→ ngrokPOST /notifications/refresh.- 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).
10. Recommended local testing workflow
- Start notification-wakeup-service on the Mac (
npm run dev,GOOGLE_APPLICATION_CREDENTIALSset). - Start ngrok and copy the HTTPS URL.
- Install a non-production Android build with
google-services.jsonin place. - Grant notification permission when prompted (or enable in Settings).
- Open Notification Debug Panel → paste ngrok URL → Save Backend URL; confirm Test Mode is on.
- Tap Register Token Now → confirm ngrok
POST /notifications/registerand[Notifications] Token registration successin Event Log / logcat. - Tap Refresh Notifications → confirm
POST /notifications/refreshandRefresh completed in Nms (scheduled X)in Event Log. - Optional: tap Simulate WAKEUP_PING (Local) to verify ngrok + refresh without FCM.
- Background the app (Home), then tap Send Real WAKEUP_PING (or from the Mac, call
/debug/send-wakeup— see curl examples) with the registereddeviceId/ token as required by notification-wakeup-service. - Watch logcat for
WAKEUP_PINGandRefresh completed (WAKEUP_PING)lines (expected output). - Open ngrok inspect UI (
http://127.0.0.1:4040) to correlate HTTP traffic. - 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:
- Open Notification Debug Panel (
/dev/notifications). - Paste ngrok HTTPS URL (no trailing slash) → Save Backend URL.
- Confirm Backend Status → URL matches the saved ngrok host.
- 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:
- Grant notification permission when prompted (Android 13+).
- Tap Register Token Now.
- 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/registerwith 200; body includesplatform: "android",testMode: true(if Test Mode on),deviceId, andfcmToken.
4. Device record appears in notification-wakeup-service
Actions:
- In ngrok inspect (
http://127.0.0.1:4040), open the register request → copydeviceIdandfcmTokenfrom the JSON body. - 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:
deviceIdis created in-app (Preferenceskeystable_device_idindeviceId.ts). It is not shown in the debug panel; use ngrok request body or logcat[DeviceId]lines.
5. Refresh endpoint returns schedule data
Actions:
- Tap Refresh Notifications in the panel (or curl below).
- 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:
- After a successful refresh (step 5), open Pending Notification Inspector → Refresh (list button).
- 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:
- Use
deviceIdfrom step 4. - 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}'
- 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:
- Background the app (Home — not force-stop).
- Run step 7 again (panel Send Real WAKEUP_PING or Mac
curl), or wait for a server-driven wakeup. - Filter logcat:
adb logcat | grep -E 'WAKEUP_PING|pushNotificationReceived|Refresh completed'
Expected outcome (within ~30–120s, longer under Doze/OEM):
pushNotificationReceived type=WAKEUP_PING/WAKEUP_PING receivedWAKEUP_PING handler — invoking refreshRefresh started (WAKEUP_PING)- ngrok: new
POST /notifications/refresh Refresh completed (WAKEUP_PING) in …ms (scheduled N)
Isolation: Wakeup Ping Simulator on the panel should produce lines 2–5 without FCM. Simulate WAKEUP_PING (Local) proves refresh only (no handler, source WAKEUP_PING simulation). Full expected logcat for Send Real WAKEUP_PING: §6.
9. Existing notifications are replaced after refresh
Actions:
- Note pending count and identifiers in Pending Notification Inspector.
- Tap Refresh Notifications again (or trigger step 8).
- 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:
- Confirm Test Mode checked; Backend Status → testMode: true.
- Call refresh twice (panel or curl) with
testMode: true. - Compare
nextNotificationstimestamps 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
- Terminal A:
cd /path/to/notification-wakeup-service
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
export PORT=3000
npm run dev
-
Verify:
curl -sS http://localhost:3000/health→ 200. -
Terminal B:
ngrok http 3000→ copyhttps://….ngrok-free.app→export BASE=…. -
Verify:
curl -sS "$BASE/health"→ 200.
Phase B — Android app
-
Build and install dev/test APK (
npm run build:android:devorbuild:android:debug:run);google-services.jsoninandroid/app/. -
Launch app; accept notification permission.
-
Account → Show All General Advanced Functions → Notification Debug Panel.
-
Paste
BASE→ Save Backend URL; enable Test Mode.
Phase C — Register and schedule
-
Register Token Now → Event Log success; ngrok
POST /notifications/register200; copydeviceIdfrom ngrok request body. -
Confirm device in notification-wakeup-service (logs/DB per that repo).
-
Refresh Notifications → Event Log
scheduled N, N ≥ 1; ngrok refresh 200 withnextNotifications. -
Pending Notification Inspector → Refresh → future alarm(s) listed.
Phase D — FCM wakeup path
-
Background app (Home).
-
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}') → panelReal WAKEUP_PING successand server FCM enqueue success. -
Within 30–120s (longer if unplugged/Doze): logcat shows
pushNotificationReceived→Refresh completed (WAKEUP_PING); ngrok shows secondPOST /notifications/refresh. -
Pending Inspector Refresh → timestamps updated (replacement, not duplicate stack).
Phase E — Optional delivery proof
-
If
testModereturned a trigger within a few minutes, wait for wall-clock fire with app backgrounded; confirm notification appears (permission + channel + exact alarm rules). -
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 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:
- Settings → app → Notifications → allowed.
- Logcat:
[FirebaseMessaging] Push permission not grantedor registration errors. - Panel shows a token string before Register Token Now.
- 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:
- Simulate WAKEUP_PING (Local) — if this fails, problem is ngrok/refresh API, not FCM.
- ngrok inspect for refresh status code and response body.
- Logcat:
refreshNotifications failedor JWT errors fromconfigureNativeFetcherIfReady().
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:
- App backgrounded (Home), not force-stopped.
- Panel FCM token matches token used by server/register.
- Simulate WAKEUP_PING (Local) works → isolates FCM path from refresh/API.
- Wait 30–120s (longer on Doze/OEM).
- 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:
- Permission granted (section 7).
- Pending Notification Inspector after refresh.
- Logcat:
Schedule replacement appliedvs aborted messages. - Confirm
nextNotificationstimestamps 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 |
16. Related docs
- local-ios-testing-ngrok.md — iOS + APNs equivalent
- local-android-testing-analysis.md — reuse matrix used to author this guide
- android-physical-device-guide.md — USB,
adb, build commands - notification-system-overview.md
- notification-from-api-call.md
- notification-permissions-and-rollovers.md
- BUILDING.md — Android build commands
- Notification Debug Panel (README)
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.