Guides and the debug panel still described /notifications/refresh and api_* scheduling as current after Phase 5; keep Phase 4 cleanup and FCM send-wakeup diagnostics.
44 KiB
Local Android Testing with ngrok (notification-wakeup-service)
Last updated: 2026-09-24 (retired WAKEUP_PING → refresh → api_* consumption)
Audience: Developers on crowd-funder-for-time-pwa, daily-notification-plugin, and notification-wakeup-service
Goal: Exercise FCM token registration and FCM wakeup delivery (WAKEUP_PING via /debug/send-wakeup) against a Mac-hosted backend reachable from a physical Android device.
Retired (do not expect this in the app):
WAKEUP_PING→POST /notifications/refresh→applyNotificationRefreshPayload()→api_*local schedules. The app logs ignored push types and does not refresh or schedule from wakeup./debug/send-wakeupremains an FCM diagnostic. Maccurlof/notifications/refreshonly tests the backend. Local schedules come from Static Daily Reminder, New Activity / dual, and the native fetcher.
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 FCM registration and wakeup delivery on a physical Android phone:
┌─────────────────────┐ HTTPS ┌──────────────────────┐
│ Mac (localhost) │ ◄───────────── │ ngrok edge │
│ notification- │ tunnel │ (public HTTPS URL) │
│ wakeup-service │ └──────────┬───────────┘
└──────────┬──────────┘ │
│ │ fetch
│ │ POST /notifications/register
│ ▼
│ ┌──────────────────────┐
│ │ crowd-funder-for- │
│ │ time-pwa (Capacitor │
│ │ Android on device) │
│ └──────────┬───────────┘
│ │
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
▼ ▼ (Daily Reminder / dual / fetcher)
┌─────────────────────┐ ┌──────────────────────┐
│ 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, health, debug wakeup send; may still expose /notifications/refresh for the backend |
| crowd-funder-for-time-pwa | Capacitor app: FCM token, POST /notifications/register; logs WAKEUP_PING without refresh/api_* scheduling |
| daily-notification-plugin | Native Android: Daily Reminder, New Activity / dual, native fetcher; Phase 4 clearApiNotifications() |
Android wakeup flow (current)
- notification-wakeup-service
/debug/send-wakeupsends 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 logs
[Notifications] push handler ignored type=WAKEUP_PING. It does not POST/notifications/refreshor scheduleapi_*notifications.
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 DEFAULT_NOTIFY_API_SERVER (from VITE_DEFAULT_NOTIFY_API_SERVER, falling back to AppString.PROD_NOTIFY_API_SERVER). That is independent of APP_SERVER. For local wakeup testing, override the notification API base URL in the Debug Panel without rebuilding.
For a full panel reference (configuration, URL resolution order, authentication, and troubleshooting), see notification-debug-panel.md.
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 (changes target server only) |
| Test Mode | Sends testMode: true/false in register / send-wakeup JSON bodies (default on when unset in storage) |
| Skip JWT Authentication (Local Development Only) | When on, omits Authorization headers for local servers that accept unauthenticated requests (default off) |
| Register Token Now | POST /notifications/register with current FCM token and platform: "android" |
| Upload AlertSearch Authorization | Uploads AlertSearch delegated JWTs (/notifications/alert-authorization) |
| Send Real WAKEUP_PING | POST /debug/send-wakeup; FCM delivery diagnostic only (no app-side refresh/api_* scheduling) |
| Event Log | Shared [Notifications] panel log (100 entries) |
Persistence: localStorage keys notificationDebug.backendBaseUrl, notificationDebug.testMode, and notificationDebug.bypassAuth (NotificationDebugConfig.ts).
Authentication vs backend URL
These settings are independent:
- Backend URL — which server receives
/notifications/*and/debug/*calls. - Test Mode —
testModefield in JSON request bodies only. - Skip JWT Authentication — whether
Authorization: Bearer …is sent (viagetNotificationApiHeaders()).
For a hosted shared test server (for example https://test-notify-api.timesafari.app): set the backend URL, keep Test Mode on if the server requires it, and leave Skip JWT Authentication off. Ensure an active DID and endorser session exist in the app.
For local ngrok / localhost: set the backend URL to your tunnel or http://127.0.0.1:PORT. Enable Skip JWT Authentication only if your local notification-wakeup-service intentionally accepts unauthenticated requests.
testMode
When Test Mode is on (default if never saved), register and send-wakeup requests include "testMode": true. The backend can route dev traffic separately from production. Turn it off in the panel only if you intentionally want production-mode API behavior against your tunnel.
WAKEUP_PING debug controls
Send Real WAKEUP_PING is the remaining panel control for wakeup. Mock refresh, Simulate WAKEUP_PING, Wakeup Ping Simulator, and Flood Test were removed with the retired refresh pipeline.
Use Send Real WAKEUP_PING to confirm backend → FCM → Capacitor listener delivery. Do not expect /notifications/refresh or api_* rescheduling.
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(), which logspush handler ignored type=WAKEUP_PING. There is norefreshNotificationsWithDiagnosticsand noapplyNotificationRefreshPayload.
That exercises backend → FCM → Capacitor push listener on Android without manual curl on the Mac. It does not reschedule local notifications.
Prerequisites: ngrok backend override saved, Test Mode as needed, notification permission granted, Register Token Now succeeded, app backgrounded (Home — not force-stop) before expecting FCM delivery (§8).
Expected Logcat output
Filter logcat (prefix is always [Notifications]):
adb logcat | grep -E '\[Notifications\].*(Real WAKEUP_PING|push handler ignored)'
On a successful FCM delivery run (HTTP success from the panel, then FCM delivery within ~30–120s), expect these key lines in order:
[Notifications] Real WAKEUP_PING requested
[Notifications] Real WAKEUP_PING success
[Notifications] push handler ignored type=WAKEUP_PING
Auth bypass messages when Skip JWT Authentication is on are normal. ngrok should not show a new POST /notifications/refresh from the app after the push.
Send Real WAKEUP_PING vs delivery success
The panel status “Real WAKEUP_PING sent via backend.” and the Event Log line Real WAKEUP_PING success only confirm that the backend accepted the request and attempted FCM delivery. They do not prove the device received the push.
Successful delivery is confirmed when push handler ignored type=WAKEUP_PING appears in logcat (or Event Log) within the delivery window. If you see Real WAKEUP_PING success but not that line, treat it as an FCM delivery problem (FCM message not received), not a backend enqueue failure.
Programmatic override (optional)
From Chrome DevTools attached to the WebView (chrome://inspect → your app):
import {
setBackendBaseUrl,
setTestMode,
setBypassAuth,
getNotificationApiBaseUrl,
} from "@/services/notifications";
setBackendBaseUrl("https://abc123.ngrok-free.app");
setTestMode(true);
setBypassAuth(true); // local dev only — omit for hosted servers that require JWT
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 needs network to Google’s endpoints. Registration needs the ngrok HTTPS URL for POST /notifications/register. Airplane mode, captive portals, VPNs, or flaky Wi‑Fi cause “server sent wakeup but app never logged ignored type” symptoms even when FCM eventually arrives.
Physical device vs emulator
Doze, App Standby, and OEM battery menus are weak or absent on many emulators. Use a physical device for wakeup SLA testing; emulators are fine for panel-only register smoke tests.
Logcat
adb logcat | grep -E '\[Notifications\]|\[FirebaseMessaging\]|\[NativeNotificationService\]'
Expect push handler ignored type=WAKEUP_PING 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.
| 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 →push handler ignored type=WAKEUP_PING.- Any step can lag under battery savers; use Send Real WAKEUP_PING for the 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; enable Skip JWT Authentication only if your local backend accepts unauthenticated requests.
- Tap Register Token Now → confirm ngrok
POST /notifications/registerand[Notifications] Token registration successin Event Log / logcat. - 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
push handler ignored type=WAKEUP_PING(expected output). - Open ngrok inspect UI (
http://127.0.0.1:4040) to correlate HTTP traffic (register and send-wakeup; not app-initiated refresh). - Use Pending Notification Inspector for Daily Reminder / New Activity / dual schedules — not for retired
api_*refresh.
For a formal pass/fail sequence, use the Verification Checklist 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 | Optional backend /notifications/refresh curl |
§5 (backend only; app does not call this) |
| 6 | Locals scheduled | §6 (Daily Reminder / dual / fetcher — not refresh) |
| 7 | Manual wakeup sends FCM | §7 |
| 8 | WAKEUP_PING delivered | §8 |
| 9 | (retired) Replace after refresh | skipped |
| 10 | (retired) Test mode frequent refreshes | skipped |
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 the default DEFAULT_NOTIFY_API_SERVER) for /notifications/register and /debug/send-wakeup.
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 (backend only; app does not call this)
Actions:
Mac-side curl against notification-wakeup-service if you need to confirm the backend still answers. The app has no Refresh Notifications button and does not schedule from this payload.
curl -sS -X POST "$BASE/notifications/refresh" \
-H "Content-Type: application/json" \
-d '{"platform":"android","testMode":true}'
Expected outcome: HTTP 200 from the service if that route is still deployed. Do not expect Event Log Refresh completed or api_* schedules in the app.
6. Local notifications are scheduled (not from refresh)
Actions:
- Configure Static Daily Reminder and/or New Activity (dual) in the app, or rely on the native fetcher.
- Open Pending Notification Inspector → Refresh (list button).
Expected outcome: Inspector lists pending Daily Reminder / dual / fetcher items. Do not expect api_* identifiers after Phase 4 startup cleanup. There is no Schedule replacement applied from WAKEUP_PING.
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 delivery success).
8. WAKEUP_PING is delivered (no refresh)
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|push handler ignored'
Expected outcome (within ~30–120s, longer under Doze/OEM):
Real WAKEUP_PING success(panel path)push handler ignored type=WAKEUP_PING- ngrok: no app-initiated
POST /notifications/refresh
Full expected logcat for Send Real WAKEUP_PING: §6.
9. Existing notifications are replaced after refresh
Retired. The app no longer replaces local schedules from /notifications/refresh. Pending list changes come from Daily Reminder / dual / native fetcher, or from Phase 4 clearApiNotifications().
10. Test mode produces frequent notification refreshes
Retired. testMode still goes on register and send-wakeup bodies. It does not drive app-side api_* refresh cadences.
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
-
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).
Phase D — FCM wakeup delivery
-
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
push handler ignored type=WAKEUP_PING; ngrok does not show an appPOST /notifications/refresh.
Phase E — Optional local notification proof
- Enable Daily Reminder or New Activity; wait for wall-clock fire with app backgrounded; confirm the OS notification is not the retired generic
api_*“Reminder” copy.
End-to-end pass criteria
| Phase | Pass |
|---|---|
| A | Health OK local + ngrok |
| B | Override URL + testMode active |
| C | Register succeeded |
| D | Wakeup curl/panel OK → logcat ignored-type line |
| E | Optional visible Daily Reminder / New Activity notification |
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 (backend only; app does not consume)
curl -sS -X POST "$BASE/notifications/refresh" \
-H "Content-Type: application/json" \
-d '{
"platform": "android",
"testMode": true
}'
This exercises notification-wakeup-service only. The app does not call applyNotificationRefreshPayload or schedule api_* from the response.
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 may still send FCM data including type: "WAKEUP_PING"; the app logs it as ignored and does not refresh.
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; JWT auth failure or Skip JWT Authentication mismatch with server expectations; 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); for JWT-required servers, ensure active DID/endorser session and Skip JWT Authentication off; for local unauthenticated backends, enable Skip JWT Authentication (see notification-debug-panel.md).
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 DEFAULT_NOTIFY_API_SERVER.
Refresh endpoint failures
Symptoms: Mac curl of /notifications/refresh fails; ngrok missing that path. This is a backend issue. The app does not POST refresh.
Verification: curl health and refresh from the Mac; confirm the service still ships that route.
Fixes: Fix backend URL and health. App scheduling uses Daily Reminder / dual / native fetcher, not this payload.
FCM message not received
Symptoms: /debug/send-wakeup or panel Send Real WAKEUP_PING shows success (Real WAKEUP_PING success in Event Log); no push handler ignored type=WAKEUP_PING in logcat within 2 minutes.
Likely causes: Force-stopped app; wrong FCM token on server; Doze/OEM delay; no Google Play services; payload missing data.type = "WAKEUP_PING"; device offline.
Note: Real WAKEUP_PING success only means the backend accepted and sent the FCM request. Missing the ignored-type log 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.
- 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: Daily Reminder or New Activity configured but no visible notification at fire time; Pending Inspector empty or stale.
Likely causes: Permission denied (display blocked); exact alarm permission on Android 12+; timestamps in past; plugin schedule error; DND/channel settings.
Verification:
- Permission granted (section 7).
- Pending Notification Inspector for Daily Reminder / dual identifiers (not
api_*after Phase 4 cleanup).
Fixes: Grant POST_NOTIFICATIONS; check SCHEDULE_EXACT_ALARM / alarm permission per plugin docs. Do not expect wakeup refresh payloads to populate the inspector.
Duplicate notifications
Symptoms: Multiple identical local notifications.
Likely causes: Separate Daily Reminder vs New Activity schedules; duplicate dual config; leftover api_* before Phase 4 cleanup.
Verification: Pending Inspector identifiers; see notification-new-activity-lay-of-the-land.md for product-level double-schedule issues.
Fixes: Confirm Daily Reminder vs dual settings. WAKEUP_PING no longer stacks api_* refreshes.
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, and bypassAuth overrides |
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 |
Push delivery hook (logs ignored types; no refresh) |
src/services/notifications/firebaseMessagingClient.ts |
Capacitor push listeners, permission, token registration |
src/components/dev/NotificationDebugPanel.vue |
Dev UI |
src/main.capacitor.ts |
Native push init at startup |
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.md — panel controls, authentication, troubleshooting
- 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.