Add doc/notification-debug-panel.md as the canonical panel reference. Update ngrok guides, README, and analysis doc to describe independent Backend URL, Test Mode, and Skip JWT Authentication settings, including recommended configs for hosted test servers vs local ngrok.
1031 lines
48 KiB
Markdown
1031 lines
48 KiB
Markdown
# Local Android Testing with ngrok (notification-wakeup-service)
|
||
|
||
**Last updated:** 2026-06-02 (verification checklist, end-to-end test)
|
||
**Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service**
|
||
**Goal:** Exercise FCM wakeup (`WAKEUP_PING`), FCM token registration, and notification refresh against a Mac-hosted backend reachable from a physical Android device.
|
||
|
||
**See also:** [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) for the iOS workflow (APNs, Xcode). [android-physical-device-guide.md](./android-physical-device-guide.md) for USB debugging and build/run commands.
|
||
|
||
---
|
||
|
||
## Architecture overview
|
||
|
||
End-to-end flow when testing New Activity / silent wake on a physical Android phone:
|
||
|
||
```text
|
||
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
||
│ Mac (localhost) │ ◄───────────── │ ngrok edge │
|
||
│ notification- │ tunnel │ (public HTTPS URL) │
|
||
│ wakeup-service │ └──────────┬───────────┘
|
||
└──────────┬──────────┘ │
|
||
│ │ fetch
|
||
│ POST /notifications/refresh │ POST /notifications/register
|
||
│ ▼
|
||
│ ┌──────────────────────┐
|
||
│ │ crowd-funder-for- │
|
||
│ │ time-pwa (Capacitor │
|
||
│ │ Android on device) │
|
||
│ └──────────┬───────────┘
|
||
│ │
|
||
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
||
▼ ▼ (local schedule replace)
|
||
┌─────────────────────┐ ┌──────────────────────┐
|
||
│ Firebase Cloud │ ──FCM────────► │ Android device │
|
||
│ Messaging │ direct │ app.timesafari.app │
|
||
└─────────────────────┘ └──────────────────────┘
|
||
```
|
||
|
||
Unlike iOS, Android does **not** use APNs. FCM delivers directly to the app via Google Play services on the device.
|
||
|
||
### Repos and responsibilities
|
||
|
||
| Repo | Role |
|
||
|------|------|
|
||
| **notification-wakeup-service** | HTTP API: device registration, refresh payload (`nextNotifications`), health, debug wakeup send |
|
||
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register` & `/refresh`, handles `WAKEUP_PING` push |
|
||
| **daily-notification-plugin** | Native Android: clear + reschedule local notifications from refresh timestamps |
|
||
|
||
### Android wakeup flow (production path)
|
||
|
||
1. **notification-wakeup-service** (or `/debug/send-wakeup`) sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
||
2. FCM delivers to the device (best-effort; see [Android Platform Notes](#8-android-platform-notes) and [Battery Optimization Caveats](#9-battery-optimization-caveats)).
|
||
3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()` in `NativeNotificationService.ts`.
|
||
4. Handler calls `refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })`, which `POST`s `{backend}/notifications/refresh` with `testMode` from the debug config.
|
||
5. Backend returns `nextNotifications: [{ timestamp }, ...]`.
|
||
6. App calls `applyNotificationRefreshPayload()` → **daily-notification-plugin** clears existing local alarms and schedules new ones.
|
||
|
||
Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`).
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
- Mac (or Linux) with Node.js 18+, **notification-wakeup-service** cloned and runnable
|
||
- **Android Studio**, JDK 17+, `ANDROID_HOME`, and `adb` — see [android-physical-device-guide.md](./android-physical-device-guide.md)
|
||
- Physical Android device with USB debugging (recommended for realistic FCM, Doze, and OEM behavior)
|
||
- ngrok account (free tier is enough for dev)
|
||
- Firebase project with an **Android** app registered for the Gradle **application ID**
|
||
- Non-production app build (Notification Debug Panel is dev-only)
|
||
- `google-services.json` in `android/app/` (not committed; see [`.gitignore`](../android/.gitignore))
|
||
|
||
---
|
||
|
||
## 1. Install and configure ngrok (macOS)
|
||
|
||
### Install
|
||
|
||
```bash
|
||
# Homebrew
|
||
brew install ngrok/ngrok/ngrok
|
||
```
|
||
|
||
Or download from [https://ngrok.com/download](https://ngrok.com/download).
|
||
|
||
### Create ngrok account and configure auth token
|
||
|
||
1. Sign up at [https://dashboard.ngrok.com/signup](https://dashboard.ngrok.com/signup).
|
||
2. Copy your authtoken from **Your Authtoken** in the dashboard.
|
||
3. Configure the CLI:
|
||
|
||
```bash
|
||
ngrok config add-authtoken YOUR_AUTHTOKEN_HERE
|
||
```
|
||
|
||
### Start a tunnel to the wakeup service
|
||
|
||
Assume the service listens on port **3000** (confirm in **notification-wakeup-service** `README` or `.env`).
|
||
|
||
```bash
|
||
# Terminal A — backend
|
||
cd /path/to/notification-wakeup-service
|
||
|
||
npm install
|
||
|
||
# one-time setup if needed
|
||
cp .env.example .env
|
||
|
||
# configure Firebase service account etc. as required
|
||
export PORT=3000
|
||
|
||
npm run dev
|
||
```
|
||
|
||
```bash
|
||
# Terminal B — ngrok
|
||
ngrok http 3000
|
||
```
|
||
|
||
ngrok prints a forwarding URL, for example:
|
||
|
||
```text
|
||
Forwarding https://abc123.ngrok-free.app -> http://localhost:3000
|
||
```
|
||
|
||
Use the **HTTPS** URL (not `http://127.0.0.1:3000`). The phone cannot reach your Mac’s localhost without the tunnel.
|
||
|
||
> **Note:** Free ngrok URLs change every time you restart ngrok unless you use a reserved domain (paid). Update the app debug override whenever the URL changes.
|
||
|
||
---
|
||
|
||
## 2. Start the backend locally
|
||
|
||
On first setup, copy `.env.example` to `.env` and set Firebase service account, `PORT`, and other variables per **notification-wakeup-service** docs.
|
||
|
||
If the backend is not already running from section 1:
|
||
|
||
```bash
|
||
cd /path/to/notification-wakeup-service
|
||
npm run dev
|
||
```
|
||
|
||
Verify locally before ngrok:
|
||
|
||
```bash
|
||
curl -sS http://localhost:3000/health
|
||
```
|
||
|
||
Expected: HTTP 200 and a JSON body indicating the service is up (exact shape depends on that repo).
|
||
|
||
### Configure Firebase Admin for the backend
|
||
|
||
**notification-wakeup-service** uses the Firebase Admin SDK to send FCM messages from your Mac.
|
||
|
||
1. Firebase Console → **Project settings** → **Service accounts**.
|
||
2. Click **Generate new private key** and download the JSON file.
|
||
3. Store the JSON outside the repo (do not commit it).
|
||
4. Point the backend at it:
|
||
|
||
```bash
|
||
export GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/to/service-account.json"
|
||
```
|
||
|
||
Set the same variable (or the equivalent documented in **notification-wakeup-service**) in the shell where you run `npm run dev`, or add it to that repo’s `.env`.
|
||
|
||
---
|
||
|
||
## 3. Obtain and use the ngrok HTTPS URL
|
||
|
||
1. Run `ngrok http <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:
|
||
|
||
```bash
|
||
export NGROK_URL="https://abc123.ngrok-free.app"
|
||
curl -sS "$NGROK_URL/health"
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Build and deploy to a physical Android device
|
||
|
||
From **crowd-funder-for-time-pwa**, build a non-production Capacitor bundle and sync Android. Complete [section 5](#5-firebase-setup-for-android-first-time) before expecting FCM to work.
|
||
|
||
```bash
|
||
npm install
|
||
npm run build:android:dev # or build:android:test — non-production for debug panel
|
||
```
|
||
|
||
Or use the combined run targets from [android-physical-device-guide.md](./android-physical-device-guide.md):
|
||
|
||
```bash
|
||
npm run build:android:debug:run
|
||
# or
|
||
npm run build:android:test:run
|
||
```
|
||
|
||
Open `android/` in Android Studio if you need to inspect native logs or signing. Ensure `VITE_FIREBASE_*` variables are set for the build you use (see `.env` / [BUILDING.md](../BUILDING.md)).
|
||
|
||
Native push registration runs at startup via `initializeNativePushAndFirebaseMessaging()` in `main.capacitor.ts` once Firebase and `google-services.json` are in place.
|
||
|
||
---
|
||
|
||
## 5. Firebase setup for Android (first-time setup)
|
||
|
||
Complete this section once before your first physical-device push test. If Firebase is already configured for this Android app, skip to [section 6](#6-configure-the-notification-debug-panel-backend-override).
|
||
|
||
### Create or access a Firebase account
|
||
|
||
1. Sign in at [https://console.firebase.google.com/](https://console.firebase.google.com/).
|
||
2. No paid Firebase plan is required. The free **Spark** plan supports **Firebase Cloud Messaging (FCM)** and local ngrok development.
|
||
|
||
### Create a Firebase project
|
||
|
||
1. In the Firebase Console, click **Add project** (or **Create a project**).
|
||
2. Enter a project name (for example, `timesafari-dev`) and continue through the wizard.
|
||
3. **Google Analytics** is optional for this workflow.
|
||
4. When the project is created, open it. **Cloud Messaging** is available on all projects — you do not need a separate “enable FCM” step beyond registering the Android app.
|
||
|
||
### Add the Android app in Firebase
|
||
|
||
1. In the project overview, click the **Android** icon (**Add app** → Android).
|
||
2. Enter the **Android package name**. It must **exactly** match the Gradle **applicationId**:
|
||
|
||
**`app.timesafari.app`**
|
||
|
||
(see `applicationId` in `android/app/build.gradle`. This differs from Capacitor `appId` in `capacitor.config.ts`, which is `app.timesafari`.)
|
||
|
||
3. App nickname and SHA-1/SHA-256 are optional for basic FCM wakeup testing; you may add debug keystore fingerprints later if Firebase Console prompts for them.
|
||
4. Download **`google-services.json`** when prompted.
|
||
|
||
### Place google-services.json
|
||
|
||
1. Copy the file to:
|
||
|
||
```text
|
||
crowd-funder-for-time-pwa/android/app/google-services.json
|
||
```
|
||
|
||
2. The repo gitignores this file — each developer keeps a local copy; never commit it.
|
||
|
||
3. Rebuild so Gradle applies the Google Services plugin (`android/app/build.gradle` applies `com.google.gms.google-services` when the file exists). If the file is missing, the build logs: *google-services.json not found, google-services plugin not applied. Push Notifications won't work*.
|
||
|
||
4. Sync Capacitor if you changed native config:
|
||
|
||
```bash
|
||
npx cap sync android
|
||
```
|
||
|
||
### Enable Firebase Cloud Messaging
|
||
|
||
FCM is enabled by default for Firebase projects. Confirm in **Project settings** → **Cloud Messaging**:
|
||
|
||
- **Cloud Messaging API** is available (legacy or HTTP v1 per your backend setup).
|
||
- The Android app (`app.timesafari.app`) appears under your apps.
|
||
|
||
The **notification-wakeup-service** sends wakeup messages through Firebase Admin using the same project’s service account ([section 2](#2-start-the-backend-locally)).
|
||
|
||
### Web / Capacitor JS Firebase config
|
||
|
||
In addition to `google-services.json`, the Capacitor web build needs `VITE_FIREBASE_*` env vars (API key, project ID, messaging sender ID, app ID, and optionally `VITE_FIREBASE_VAPID_KEY`). These must refer to the **same Firebase project** as `google-services.json`.
|
||
|
||
### Verify Firebase configuration
|
||
|
||
Before ngrok end-to-end testing, confirm:
|
||
|
||
- [ ] App builds and installs without Gradle errors about `google-services.json`.
|
||
- [ ] Push permission is **granted** (see [section 7](#7-android-notification-permissions)).
|
||
- [ ] **Notification Debug Panel** shows an FCM token (after permission and registration).
|
||
- [ ] **Register Token Now** succeeds and ngrok shows `POST /notifications/register`.
|
||
- [ ] Backend health and `GOOGLE_APPLICATION_CREDENTIALS` are set so `/debug/send-wakeup` can run.
|
||
|
||
---
|
||
|
||
## 6. Configure the Notification Debug Panel backend override
|
||
|
||
The app normally calls `APP_SERVER` (from `VITE_APP_SERVER`). For local wakeup testing, override the notification API base URL without rebuilding.
|
||
|
||
For a full panel reference (configuration, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
||
|
||
### 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** (changes target server only) |
|
||
| **Test Mode** | Sends `testMode: true/false` in register/refresh JSON bodies (default on when unset in storage) |
|
||
| **Skip JWT Authentication (Local Development Only)** | When on, omits `Authorization` headers for local servers that accept unauthenticated requests (default **off**) |
|
||
| **Register Token Now** | `POST /notifications/register` with current FCM token and `platform: "android"` |
|
||
| **Refresh Notifications** | `POST /notifications/refresh` (same as post-wakeup flow) |
|
||
| **Simulate WAKEUP_PING (Local)** | Calls refresh API directly (no FCM) — quick backend + ngrok test |
|
||
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup` on the backend override URL; server sends a real FCM `WAKEUP_PING` to the panel’s current FCM token (see below) |
|
||
| **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** — `testMode` field in JSON request bodies only.
|
||
- **Skip JWT Authentication** — whether `Authorization: Bearer …` is sent (via `getNotificationApiHeaders()`).
|
||
|
||
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 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](#fcm-message-not-received)).
|
||
|
||
| 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](#7-manual-wakeup-endpoint-sends-fcm-successfully)). 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. `POST`s to `{backend override}/debug/send-wakeup` with `deviceId`, `fcmToken`, `platform: "android"`, and `testMode` from the panel config (`NotificationDebugService.sendRealWakeupPing()`).
|
||
3. On HTTP success, the **notification-wakeup-service** enqueues a real FCM **data** message with `data.type = "WAKEUP_PING"` to that device.
|
||
4. When FCM delivers the message to the app, Capacitor fires `pushNotificationReceived` → `handleCapacitorPushNotificationReceived()` → `refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })` → `POST /notifications/refresh` → `applyNotificationRefreshPayload()` (clear + reschedule via **daily-notification-plugin**).
|
||
|
||
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](#8-android-platform-notes)).
|
||
|
||
#### Expected Logcat output
|
||
|
||
Filter logcat (prefix is always `[Notifications]`):
|
||
|
||
```bash
|
||
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: …`, `Using authenticated notification request` or auth bypass messages when **Skip JWT Authentication** is on) are normal. ngrok should also show a new `POST /notifications/refresh` after the push is handled.
|
||
|
||
#### 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](#fcm-message-not-received)), not a backend enqueue failure.
|
||
|
||
### Programmatic override (optional)
|
||
|
||
From Chrome DevTools attached to the WebView (`chrome://inspect` → your app):
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```typescript
|
||
const perm = await PushNotifications.requestPermissions();
|
||
```
|
||
|
||
If `perm.receive !== "granted"`, native push registration does not proceed and you will not get a reliable FCM token in the debug panel.
|
||
|
||
Daily notification flows can also request permission via **daily-notification-plugin** (`NativeNotificationService.requestPermissions()` → plugin `POST_NOTIFICATIONS`). See [notification-permissions-and-rollovers.md](./notification-permissions-and-rollovers.md).
|
||
|
||
### Android 12 and below
|
||
|
||
`POST_NOTIFICATIONS` does not exist as a runtime prompt on older APIs. Notifications are generally allowed at install time; fewer permission dialogs appear during testing.
|
||
|
||
### Manual check for testers
|
||
|
||
**Settings** → **Apps** → **TimeSafari** → **Notifications** → ensure notifications are allowed.
|
||
|
||
### Permission vs FCM data wake
|
||
|
||
**WAKEUP_PING** uses FCM **data** messages handled in `pushNotificationReceived`. For local testing:
|
||
|
||
- Grant notification permission so **Register Token Now** and token display in the debug panel work.
|
||
- If permission is denied, fix permission before debugging FCM wakeup; do not assume data delivery will run the full refresh pipeline.
|
||
|
||
---
|
||
|
||
## 8. Android Platform Notes
|
||
|
||
### FCM vs iOS silent push
|
||
|
||
Android generally delivers **background FCM data messages** more reliably than iOS **silent APNs** (`content-available`), especially when the app is backgrounded (not force-stopped) and Google Play services is healthy. There is no separate APNs hop; FCM talks to the device directly.
|
||
|
||
Delivery is still **not guaranteed**. Treat wakeup as best-effort in tests and in production expectations.
|
||
|
||
### Force-stop vs backgrounding
|
||
| State | Typical FCM behavior |
|
||
|-------|----------------------|
|
||
| **Foreground** | Data messages handled promptly; `pushNotificationReceived` fires. |
|
||
| **Background** (Home) | Data messages usually delivered; handler may run with delay under load. |
|
||
| **Force stop** (Settings → Force stop) | Delivery often **blocked** until the user launches the app again. Stricter than iOS “swipe away” in many cases. |
|
||
| **Recents swipe** | Varies by OEM/Android version; less predictable than Home. Prefer **Home** for tests. |
|
||
|
||
Always validate wakeup with the app **backgrounded**, not force-stopped.
|
||
|
||
### Notification permissions (Android 13+)
|
||
|
||
On **API 33+**, `POST_NOTIFICATIONS` is a runtime permission ([section 7](#7-android-notification-permissions)). This app requests push permission via `PushNotifications.requestPermissions()` before `register()`. Denied permission blocks token registration in the debug panel and undermines end-to-end testing—fix permission before debugging FCM.
|
||
|
||
### Network connectivity
|
||
|
||
FCM and your ngrok-backed refresh both need network:
|
||
|
||
- Device must reach **Google’s FCM endpoints** (mobile data or Wi‑Fi).
|
||
- After wake, the app must reach the **ngrok HTTPS URL** for `POST /notifications/refresh`.
|
||
- Airplane mode, captive portals, VPNs, or flaky Wi‑Fi cause “server sent wakeup but app never refreshed” symptoms even when FCM eventually arrives.
|
||
|
||
### Physical device vs emulator
|
||
|
||
Doze, App Standby, and OEM battery menus are weak or absent on many emulators. Use a **physical device** for wakeup SLA testing; emulators are fine for panel-only register/refresh smoke tests.
|
||
|
||
### Logcat
|
||
|
||
```bash
|
||
adb logcat | grep -E '\[Notifications\]|\[FirebaseMessaging\]|\[NativeNotificationService\]'
|
||
```
|
||
|
||
Expect `pushNotificationReceived type=WAKEUP_PING` and `WAKEUP_PING handler — invoking refresh` after a successful wake. For a panel-driven test, use **Send Real WAKEUP_PING** ([§6](#send-real-wakeup_ping)) 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
|
||
|
||
1. Server accepts `/debug/send-wakeup` → FCM enqueue succeeds.
|
||
2. Device may **hold** the message until Doze maintenance or OEM policy allows delivery.
|
||
3. `pushNotificationReceived` runs → `refreshNotificationsWithDiagnostics()` → ngrok `POST /notifications/refresh`.
|
||
4. Any step can lag under battery savers; use **Simulate WAKEUP_PING (Local)** in the debug panel to separate FCM delay from refresh/API issues; use **Send Real WAKEUP_PING** for the full FCM path ([§6](#send-real-wakeup_ping)).
|
||
|
||
---
|
||
|
||
## 10. Recommended local testing workflow
|
||
|
||
1. Start **notification-wakeup-service** on the Mac (`npm run dev`, `GOOGLE_APPLICATION_CREDENTIALS` set).
|
||
2. Start **ngrok** and copy the HTTPS URL.
|
||
3. Install a **non-production** Android build with `google-services.json` in place.
|
||
4. Grant notification permission when prompted (or enable in Settings).
|
||
5. Open **Notification Debug Panel** → paste ngrok URL → **Save Backend URL**; confirm **Test Mode** is on; enable **Skip JWT Authentication** only if your local backend accepts unauthenticated requests.
|
||
6. Tap **Register Token Now** → confirm ngrok `POST /notifications/register` and `[Notifications] Token registration success` in Event Log / logcat.
|
||
7. Tap **Refresh Notifications** → confirm `POST /notifications/refresh` and `Refresh completed in Nms (scheduled X)` in Event Log.
|
||
8. Optional: tap **Simulate WAKEUP_PING (Local)** to verify ngrok + refresh without FCM.
|
||
9. Background the app (Home), then tap **Send Real WAKEUP_PING** (or from the Mac, call **`/debug/send-wakeup`** — see [curl examples](#13-sample-curl-commands)) with the registered `deviceId` / token as required by **notification-wakeup-service**.
|
||
10. Watch logcat for `WAKEUP_PING` and `Refresh completed (WAKEUP_PING)` lines ([expected output](#expected-logcat-output)).
|
||
11. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate HTTP traffic.
|
||
12. Use **Pending Notification Inspector** on the panel to confirm locally scheduled fires after refresh.
|
||
|
||
For a formal pass/fail sequence, use the [Verification Checklist](#11-verification-checklist) below.
|
||
|
||
---
|
||
|
||
## 11. Verification Checklist
|
||
|
||
Use this checklist during development or QA sign-off. Each step lists **actions**, then **expected outcome**. Prerequisites: non-production Android build, `google-services.json` installed, physical device (recommended), backend + ngrok running.
|
||
|
||
| # | Check | Pass criteria |
|
||
|---|--------|----------------|
|
||
| 1 | Backend via ngrok | §1 below |
|
||
| 2 | Device → backend override | §2 |
|
||
| 3 | FCM token registered | §3 |
|
||
| 4 | Device record in wakeup service | §4 |
|
||
| 5 | Refresh returns schedule data | §5 |
|
||
| 6 | Locals scheduled | §6 |
|
||
| 7 | Manual wakeup sends FCM | §7 |
|
||
| 8 | WAKEUP_PING → refresh | §8 |
|
||
| 9 | Replace after refresh | §9 |
|
||
| 10 | Test mode frequent refreshes | §10 |
|
||
|
||
### 1. Backend reachable through ngrok
|
||
|
||
**Actions:**
|
||
|
||
```bash
|
||
export BASE="https://YOUR-NGROK-HOST.ngrok-free.app"
|
||
curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
|
||
```
|
||
|
||
**Expected outcome:** HTTP **200**; JSON body indicates the service is healthy (exact fields per **notification-wakeup-service**). Mac `curl http://localhost:3000/health` must also pass before blaming ngrok.
|
||
|
||
---
|
||
|
||
### 2. Device can reach backend override URL
|
||
|
||
**Actions:**
|
||
|
||
1. Open **Notification Debug Panel** (`/dev/notifications`).
|
||
2. Paste ngrok HTTPS URL (no trailing slash) → **Save Backend URL**.
|
||
3. Confirm **Backend Status → URL** matches the saved ngrok host.
|
||
4. Enable **Test Mode** if using dev backend behavior.
|
||
|
||
**Expected outcome:** **Active** URL in the panel equals your ngrok `https://…` host. Subsequent app requests use that base (not production `APP_SERVER`) for `/notifications/register` and `/notifications/refresh`.
|
||
|
||
---
|
||
|
||
### 3. FCM token successfully registered
|
||
|
||
**Actions:**
|
||
|
||
1. Grant notification permission when prompted (Android 13+).
|
||
2. Tap **Register Token Now**.
|
||
3. Watch Event Log and logcat (`[Notifications]`, `[FirebaseMessaging]`).
|
||
|
||
**Expected outcome:**
|
||
|
||
- **Current FCM Token** shows a non-empty token (Copy works).
|
||
- Event Log: `[Notifications] Token registration success` (not failure / auth unavailable).
|
||
- ngrok inspect: `POST /notifications/register` with **200**; body includes `platform: "android"`, `testMode: true` (if Test Mode on), `deviceId`, and `fcmToken`.
|
||
|
||
---
|
||
|
||
### 4. Device record appears in notification-wakeup-service
|
||
|
||
**Actions:**
|
||
|
||
1. In ngrok inspect (`http://127.0.0.1:4040`), open the **register** request → copy `deviceId` and `fcmToken` from the JSON body.
|
||
2. In **notification-wakeup-service**, confirm persistence per that repo (server logs, database, admin/list endpoint, or debug CLI).
|
||
|
||
**Expected outcome:** The `deviceId` and `fcmToken` from step 3 are stored and retrievable by the service. `/debug/send-wakeup` can target that `deviceId` (or token, per service contract). If register returned 200 but no record exists, fix the wakeup service store/config before FCM tests.
|
||
|
||
> **Note:** `deviceId` is created in-app (`Preferences` key `stable_device_id` in `deviceId.ts`). It is not shown in the debug panel; use ngrok request body or logcat `[DeviceId]` lines.
|
||
|
||
---
|
||
|
||
### 5. Refresh endpoint returns schedule data
|
||
|
||
**Actions:**
|
||
|
||
1. Tap **Refresh Notifications** in the panel (or curl below).
|
||
2. Inspect ngrok response body.
|
||
|
||
```bash
|
||
curl -sS -X POST "$BASE/notifications/refresh" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"platform":"android","testMode":true}'
|
||
```
|
||
|
||
**Expected outcome:** HTTP **200**; JSON includes `nextNotifications` array with at least one `{ "timestamp": <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 Inspector** → **Refresh** (list button).
|
||
2. Optionally cross-check logcat for `Schedule replacement applied (N timestamp(s))`.
|
||
|
||
**Expected outcome:** Inspector lists **N** pending item(s) matching refresh count; each row has a **future** `nextTriggerDate` / wall-clock time. No `Schedule replacement aborted` or `skipped (no valid timestamps)` in Event Log.
|
||
|
||
---
|
||
|
||
### 7. Manual wakeup endpoint sends FCM successfully
|
||
|
||
**Actions:**
|
||
|
||
1. Use `deviceId` from step 4.
|
||
2. Either:
|
||
- **On device:** background the app (Home), open **Notification Debug Panel**, tap **Send Real WAKEUP_PING**; or
|
||
- **From the Mac:**
|
||
|
||
```bash
|
||
curl -sS -X POST "$BASE/debug/send-wakeup" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"deviceId":"YOUR_DEVICE_ID","testMode":true}'
|
||
```
|
||
|
||
3. Check **notification-wakeup-service** logs for FCM send success (no Admin SDK / token errors).
|
||
|
||
**Expected outcome:** HTTP **200** (or documented success code) from `/debug/send-wakeup`; Event Log / logcat: `Real WAKEUP_PING success` when using the panel button; server logs indicate FCM message enqueued/sent with `data.type = "WAKEUP_PING"`. This step alone does not prove device delivery (see step 8 and [Send Real WAKEUP_PING vs end-to-end success](#send-real-wakeup_ping-vs-end-to-end-success)).
|
||
|
||
---
|
||
|
||
### 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:
|
||
|
||
```bash
|
||
adb logcat | grep -E 'WAKEUP_PING|pushNotificationReceived|Refresh completed'
|
||
```
|
||
|
||
**Expected outcome (within ~30–120s, longer under Doze/OEM):**
|
||
|
||
1. `pushNotificationReceived type=WAKEUP_PING` / `WAKEUP_PING received`
|
||
2. `WAKEUP_PING handler — invoking refresh`
|
||
3. `Refresh started (WAKEUP_PING)`
|
||
4. ngrok: new `POST /notifications/refresh`
|
||
5. `Refresh completed (WAKEUP_PING) in …ms (scheduled N)`
|
||
|
||
**Isolation:** **Wakeup Ping Simulator** on the panel should produce lines 2–5 without FCM. **Simulate WAKEUP_PING (Local)** proves refresh only (no handler, source `WAKEUP_PING simulation`). Full expected logcat for **Send Real WAKEUP_PING**: [§6](#expected-logcat-output).
|
||
|
||
---
|
||
|
||
### 9. Existing notifications are replaced after refresh
|
||
|
||
**Actions:**
|
||
|
||
1. Note pending count and identifiers in **Pending Notification Inspector**.
|
||
2. Tap **Refresh Notifications** again (or trigger step 8).
|
||
3. Refresh inspector; compare IDs/times to step 1.
|
||
|
||
**Expected outcome:** Event Log shows `clearAllNotifications` or `cancelAllNotifications` then `Schedule replacement applied`; pending list reflects **new** timestamps (old alarms not accumulated). Total pending count should match latest refresh `N`, not double from duplicate refreshes unless backend returned more slots.
|
||
|
||
---
|
||
|
||
### 10. Test mode produces frequent notification refreshes
|
||
|
||
**Actions:**
|
||
|
||
1. Confirm **Test Mode** checked; **Backend Status → testMode: true**.
|
||
2. Call refresh twice (panel or curl) with `testMode: true`.
|
||
3. Compare `nextNotifications` timestamps in ngrok responses.
|
||
|
||
**Expected outcome:** With **testMode: true**, **notification-wakeup-service** returns **dev-friendly** schedule data—typically **sooner** fire times than production mode (shorter horizons). Pending Inspector updates to nearer triggers after each refresh. With Test Mode **off**, timestamps should be farther out (production-like); use that contrast to confirm the flag is wired end-to-end.
|
||
|
||
---
|
||
|
||
## 12. End-to-End Test
|
||
|
||
Single scripted run from cold start to scheduled local notification. Time: ~15–30 minutes (plus optional wait for a near `testMode` fire).
|
||
|
||
### Phase A — Mac backend and tunnel
|
||
|
||
1. Terminal A:
|
||
|
||
```bash
|
||
cd /path/to/notification-wakeup-service
|
||
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
|
||
export PORT=3000
|
||
npm run dev
|
||
```
|
||
|
||
2. Verify: `curl -sS http://localhost:3000/health` → **200**.
|
||
|
||
3. Terminal B: `ngrok http 3000` → copy `https://….ngrok-free.app` → `export BASE=…`.
|
||
|
||
4. Verify: `curl -sS "$BASE/health"` → **200**.
|
||
|
||
### Phase B — Android app
|
||
|
||
5. Build and install dev/test APK (`npm run build:android:dev` or `build:android:debug:run`); `google-services.json` in `android/app/`.
|
||
|
||
6. Launch app; accept **notification permission**.
|
||
|
||
7. **Account** → **Show All General Advanced Functions** → **Notification Debug Panel**.
|
||
|
||
8. Paste `BASE` → **Save Backend URL**; enable **Test Mode**.
|
||
|
||
### Phase C — Register and schedule
|
||
|
||
9. **Register Token Now** → Event Log success; ngrok `POST /notifications/register` **200**; copy `deviceId` from ngrok request body.
|
||
|
||
10. Confirm device in **notification-wakeup-service** (logs/DB per that repo).
|
||
|
||
11. **Refresh Notifications** → Event Log `scheduled N`, N ≥ 1; ngrok refresh **200** with `nextNotifications`.
|
||
|
||
12. **Pending Notification Inspector** → **Refresh** → future alarm(s) listed.
|
||
|
||
### Phase D — FCM wakeup path
|
||
|
||
13. Background app (Home).
|
||
|
||
14. 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.
|
||
|
||
15. Within 30–120s (longer if unplugged/Doze): logcat shows `pushNotificationReceived` → `Refresh completed (WAKEUP_PING)`; ngrok shows second `POST /notifications/refresh`.
|
||
|
||
16. Pending Inspector **Refresh** → timestamps updated (replacement, not duplicate stack).
|
||
|
||
### Phase E — Optional delivery proof
|
||
|
||
17. If `testMode` returned a trigger within a few minutes, wait for wall-clock fire with app backgrounded; confirm notification appears (permission + channel + exact alarm rules).
|
||
|
||
18. If FCM step 15 failed but step 11 passed: run **Simulate WAKEUP_PING (Local)** then **Wakeup Ping Simulator** to bisect FCM vs handler; see [Troubleshooting](#14-troubleshooting) and [Send Real WAKEUP_PING vs end-to-end success](#send-real-wakeup_ping-vs-end-to-end-success).
|
||
|
||
### End-to-end pass criteria
|
||
|
||
| Phase | Pass |
|
||
|-------|------|
|
||
| A | Health OK local + ngrok |
|
||
| B | Override URL + testMode active |
|
||
| C | Register + refresh + pending list populated |
|
||
| D | Wakeup curl OK → logcat refresh chain → pending updated |
|
||
| E | Optional visible notification at scheduled time |
|
||
|
||
---
|
||
|
||
## 13. Sample curl commands
|
||
|
||
Set your tunnel base URL:
|
||
|
||
```bash
|
||
export BASE="https://abc123.ngrok-free.app"
|
||
```
|
||
|
||
### Health
|
||
|
||
```bash
|
||
curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
|
||
```
|
||
|
||
### Register device (mirror app payload)
|
||
|
||
```bash
|
||
curl -sS -X POST "$BASE/notifications/register" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"deviceId": "00000000-0000-4000-8000-000000000001",
|
||
"fcmToken": "YOUR_FCM_TOKEN_FROM_DEBUG_PANEL",
|
||
"platform": "android",
|
||
"testMode": true
|
||
}'
|
||
```
|
||
|
||
### Refresh (mirror app payload)
|
||
|
||
```bash
|
||
curl -sS -X POST "$BASE/notifications/refresh" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"platform": "android",
|
||
"testMode": true
|
||
}'
|
||
```
|
||
|
||
Example success body shape (actual fields may vary by service version):
|
||
|
||
```json
|
||
{
|
||
"shouldNotify": true,
|
||
"nextNotifications": [
|
||
{ "timestamp": 1710000000000 },
|
||
{ "timestamp": 1710003600000 }
|
||
]
|
||
}
|
||
```
|
||
|
||
The app schedules those timestamps via **daily-notification-plugin** (`applyNotificationRefreshPayload` in `NativeNotificationService.ts`).
|
||
|
||
### Send wakeup push (debug)
|
||
|
||
Exact path and body depend on **notification-wakeup-service**; typical pattern:
|
||
|
||
```bash
|
||
curl -sS -X POST "$BASE/debug/send-wakeup" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"deviceId": "00000000-0000-4000-8000-000000000001",
|
||
"testMode": true
|
||
}'
|
||
```
|
||
|
||
Confirm parameters (token vs `deviceId`, auth headers) in that repo’s README or OpenAPI spec. The server must send FCM data including `type: "WAKEUP_PING"` to match `handleCapacitorPushNotificationReceived`.
|
||
|
||
---
|
||
|
||
## 14. Troubleshooting
|
||
|
||
Structured checks for local ngrok + FCM testing. Each item lists **symptoms**, **likely causes**, **verification**, and **fixes**.
|
||
|
||
### Token registration failures
|
||
|
||
**Symptoms:** Event Log shows token registration failure; no FCM token in debug panel; ngrok has no `POST /notifications/register`; curl register fails.
|
||
|
||
**Likely causes:** Notification permission denied; missing `google-services.json`; Firebase package mismatch (`app.timesafari.app`); `VITE_FIREBASE_*` not in build; JWT auth failure or **Skip JWT Authentication** mismatch with server expectations; 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); 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](./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:**
|
||
|
||
```bash
|
||
curl -sS http://localhost:3000/health
|
||
```
|
||
|
||
Compare with panel **Backend Status** and Event Log error text.
|
||
|
||
**Fixes:** Start backend with `npm run dev` and `GOOGLE_APPLICATION_CREDENTIALS`; match `PORT` to ngrok target; paste full ngrok **HTTPS** URL into panel (no trailing slash).
|
||
|
||
---
|
||
|
||
### Stale ngrok URL
|
||
|
||
**Symptoms:** Worked yesterday; today all API calls fail or hit wrong host; ngrok shows no requests.
|
||
|
||
**Likely causes:** Free ngrok URL changed after tunnel restart; old URL still in `notificationDebug.backendBaseUrl`.
|
||
|
||
**Verification:** Compare panel URL to current `ngrok http` **Forwarding** line; `curl -sS "$NEW_URL/health"`.
|
||
|
||
**Fixes:** Copy new HTTPS URL → **Save Backend URL** in panel; or clear override only if intentionally returning to `APP_SERVER`.
|
||
|
||
---
|
||
|
||
### Refresh endpoint failures
|
||
|
||
**Symptoms:** **Refresh Notifications** fails; Event Log HTTP error; no `scheduled X` line; ngrok missing `POST /notifications/refresh`.
|
||
|
||
**Likely causes:** Stale ngrok URL; backend down; 404/wrong path; JWT/native fetcher not configured; refresh auth failure.
|
||
|
||
**Verification:**
|
||
|
||
1. **Simulate WAKEUP_PING (Local)** — if this fails, problem is ngrok/refresh API, not FCM.
|
||
2. ngrok inspect for refresh status code and response body.
|
||
3. Logcat: `refreshNotifications failed` or JWT errors from `configureNativeFetcherIfReady()`.
|
||
|
||
**Fixes:** Fix backend URL and health; ensure active DID and endorser settings ([notification-from-api-call.md](./notification-from-api-call.md)); confirm `testMode` if backend requires it.
|
||
|
||
---
|
||
|
||
### 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](#send-real-wakeup_ping-vs-end-to-end-success)).
|
||
|
||
**Verification:**
|
||
|
||
1. App **backgrounded** (Home), not force-stopped.
|
||
2. Panel FCM token matches token used by server/register.
|
||
3. **Simulate WAKEUP_PING (Local)** works → isolates FCM path from refresh/API.
|
||
4. Wait 30–120s (longer on Doze/OEM).
|
||
5. Battery **Unrestricted** and OEM autostart enabled for test device.
|
||
|
||
**Fixes:** Re-register token; relaunch app; relax battery settings ([section 9](#9-battery-optimization-caveats)); confirm **notification-wakeup-service** message format; test on Pixel vs suspect OEM policy.
|
||
|
||
---
|
||
|
||
### Notification permission denied
|
||
|
||
**Symptoms:** No permission dialog or user denied; logcat `Push permission not granted`; empty FCM token; register skipped.
|
||
|
||
**Likely causes:** User denied prompt; permission revoked in Settings; testing on API 33+ without `POST_NOTIFICATIONS` grant.
|
||
|
||
**Verification:** Settings → Apps → TimeSafari → Notifications; logcat after cold start for `requestPermissions` result.
|
||
|
||
**Fixes:** Enable notifications in Settings; reinstall to re-prompt if needed; cold-start app so `initializeNativePushAndFirebaseMessaging()` runs again.
|
||
|
||
---
|
||
|
||
### Notifications not appearing
|
||
|
||
**Symptoms:** Refresh succeeds (`scheduled X` in log) but no visible notification at fire time; Pending Inspector empty or stale.
|
||
|
||
**Likely causes:** Permission denied (display blocked); exact alarm permission on Android 12+; timestamps in past; plugin schedule error; DND/channel settings.
|
||
|
||
**Verification:**
|
||
|
||
1. Permission granted ([section 7](#7-android-notification-permissions)).
|
||
2. Pending Notification Inspector after refresh.
|
||
3. Logcat: `Schedule replacement applied` vs aborted messages.
|
||
4. Confirm `nextNotifications` timestamps are in the future (curl refresh response).
|
||
|
||
**Fixes:** Grant `POST_NOTIFICATIONS`; check `SCHEDULE_EXACT_ALARM` / alarm permission per plugin docs; fix refresh payload; test with nearer timestamps via backend `testMode`.
|
||
|
||
---
|
||
|
||
### Duplicate notifications
|
||
|
||
**Symptoms:** Multiple identical local notifications; Event Log shows repeated refresh lines.
|
||
|
||
**Likely causes:** Multiple `WAKEUP_PING` deliveries; repeated manual **Refresh**; flood test; separate Daily Reminder vs New Activity schedules.
|
||
|
||
**Verification:** Event Log count of refresh completions; ngrok inspect for duplicate `POST /notifications/refresh`.
|
||
|
||
**Fixes:** Each refresh should **replace** schedule (clear + schedule)—if duplicates persist, check plugin logs; see [notification-new-activity-lay-of-the-land.md](./notification-new-activity-lay-of-the-land.md) for product-level double-schedule issues.
|
||
|
||
---
|
||
|
||
### Device enters battery-saving mode
|
||
|
||
**Symptoms:** Intermittent wakeup; long delays; works on charger but not unplugged; OEM “battery saver” icon on.
|
||
|
||
**Likely causes:** Doze; Adaptive Battery; manufacturer saver (Samsung, Xiaomi, Oppo, Vivo, Huawei).
|
||
|
||
**Verification:** Settings → Battery; OEM battery app; reproduce unplugged screen-off vs charging.
|
||
|
||
**Fixes:** For dev testing set app battery to **Unrestricted**; enable **Autostart** / background activity on OEM; wait for maintenance window or wake device before concluding FCM failure.
|
||
|
||
---
|
||
|
||
### Gradle: Push Notifications won't work
|
||
|
||
**Symptoms:** Build log: `google-services.json not found, google-services plugin not applied`.
|
||
|
||
**Likely causes:** Missing `android/app/google-services.json`.
|
||
|
||
**Verification:** File exists on disk; rebuild shows Google Services plugin applied.
|
||
|
||
**Fixes:** Download from Firebase Console (package `app.timesafari.app`); place in `android/app/`; `npx cap sync android` and rebuild.
|
||
|
||
---
|
||
|
||
## 15. Key source files (crowd-funder-for-time-pwa)
|
||
|
||
| File | Purpose |
|
||
|------|---------|
|
||
| `src/services/notifications/NotificationDebugConfig.ts` | Backend URL, testMode, 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` | `refreshNotifications`, `WAKEUP_PING`, `applyNotificationRefreshPayload` |
|
||
| `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners, permission, token registration |
|
||
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
||
| `src/main.capacitor.ts` | Native push init at startup |
|
||
| `android/app/build.gradle` | `applicationId`, conditional `google-services` plugin |
|
||
| `android/app/src/main/AndroidManifest.xml` | `POST_NOTIFICATIONS` and plugin permissions |
|
||
|
||
---
|
||
|
||
## 16. Related docs
|
||
|
||
- [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) — iOS + APNs equivalent
|
||
- [local-android-testing-analysis.md](./local-android-testing-analysis.md) — reuse matrix used to author this guide
|
||
- [android-physical-device-guide.md](./android-physical-device-guide.md) — USB, `adb`, build commands
|
||
- [notification-system-overview.md](./notification-system-overview.md)
|
||
- [notification-from-api-call.md](./notification-from-api-call.md)
|
||
- [notification-permissions-and-rollovers.md](./notification-permissions-and-rollovers.md)
|
||
- [BUILDING.md](../BUILDING.md) — Android build commands
|
||
- [notification-debug-panel.md](./notification-debug-panel.md) — panel controls, authentication, troubleshooting
|
||
- [Notification Debug Panel (README)](../README.md#notification-debug-panel-dev-builds)
|
||
|
||
For plugin-native behavior (exact alarms, Android pending inspector), see **daily-notification-plugin** documentation. For FCM payload format and `/debug/send-wakeup` contract, see **notification-wakeup-service**.
|