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 `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](./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 the default `DEFAULT_NOTIFY_API_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 `DEFAULT_NOTIFY_API_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**.
|