# Local iOS Testing with ngrok (notification-wakeup-service) **Last updated:** 2026-05-18 **Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service** **Goal:** Exercise silent push wake (`WAKEUP_PING`), FCM token registration, and notification refresh against a Mac-hosted backend reachable from a physical iPhone. --- ## Architecture overview End-to-end flow when testing New Activity / silent wake on a physical iPhone: ```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 │ │ │ iOS on iPhone) │ │ └──────────┬───────────┘ │ │ │ FCM data message (WAKEUP_PING) │ daily-notification-plugin ▼ ▼ (local schedule replace) ┌─────────────────────┐ ┌──────────────────────┐ │ Firebase Cloud │ ──APNs──────► │ iPhone (physical) │ │ Messaging │ silent push │ app.timesafari │ └─────────────────────┘ └──────────────────────┘ ``` ### 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 iOS/Android: clear + reschedule local notifications from refresh timestamps | ### Silent wake sequence (production path) 1. Backend (or `/debug/send-wakeup`) sends an FCM **data** message with `data.type = "WAKEUP_PING"`. 2. APNs delivers to the device (best-effort; see iOS caveats below). 3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()`. 4. App calls `POST {backend}/notifications/refresh` with `testMode` (from debug config). 5. Backend returns `nextNotifications: [{ timestamp }, ...]`. 6. App calls `applyNotificationRefreshPayload()` → plugin clears and schedules new local alarms. Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`). --- ## Prerequisites - Mac with Xcode, Node.js 18+, and the **notification-wakeup-service** repo cloned and runnable - Physical iPhone (USB or wireless debugging) — **simulator is not sufficient** for reliable silent push / APNs behavior - ngrok account (free tier is enough for dev) - Firebase project with APNs configured for the iOS app bundle ID - Non-production app build (Notification Debug Panel is dev-only) --- ## 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). ### Account and 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`). If the service already defaults to port 3000 internally, you may not need to export PORT manually. ```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 ``` The backend only needs to be started once. The dedicated backend section below exists for verification and troubleshooting details, not as a second startup step. 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 iPhone 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 Example (adjust to match **notification-wakeup-service**). On first setup, copy `.env.example` to `.env` and set Firebase service account, `PORT`, and other variables per that repo's docs. If the backend is not already running from section 1: ```bash # If not already running from the previous step: 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). --- ## 3. Obtain and use the ngrok HTTPS URL 1. Run `ngrok http `. 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. Generate and open the iOS workspace From **crowd-funder-for-time-pwa**, generate the Capacitor iOS project and open it in Xcode. **[Section 5](#5-firebase--apns-setup-first-time-setup) (Firebase + APNs)** needs this workspace—for example to add `GoogleService-Info.plist` and enable Push Notifications in the app target. The app does not need Firebase or push fully configured yet; the goal here is a buildable Xcode project on your Mac. ```bash npm install npm run build:ios:dev # or build:ios:test — non-production for debug panel ``` Open the generated Xcode workspace (for example `ios/App/App.xcworkspace`), select your **physical iPhone**, enable signing, and Run when you are ready to verify the app launches. Ensure `VITE_FIREBASE_*` variables are set for the Capacitor build you use (see `.env` / build docs). Native push registration runs at startup via `initializeNativePushAndFirebaseMessaging()` in `main.capacitor.ts` once Firebase is configured in the next section. --- ## 5. Firebase + APNs setup (first-time setup) Complete this section once before your first physical-device push test. If Firebase and APNs are already configured for this app, skip to [section 6](#6-configure-the-notification-debug-panel-backend-override). ### Create or access a Firebase account 1. Sign in with a Google account at [https://console.firebase.google.com/](https://console.firebase.google.com/). 2. If this is your first time using Firebase: - Accept the Firebase terms. - Create a new Firebase account/workspace when prompted. 3. No paid Firebase plan is required for local iOS notification testing. The free **Spark** plan is sufficient for: - Firebase Cloud Messaging (FCM) - APNs silent push testing - local ngrok-based development ### Create a Firebase project 1. In the [Firebase Console](https://console.firebase.google.com/), 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; you can disable it for a simpler dev project. 4. When the project is created, open it. **Cloud Messaging** is available on all projects — you do not need a separate enable step for FCM. ### Register the iOS app in Firebase 1. In the project overview, click the **iOS** icon (**Add app** → iOS). 2. Enter the **Apple bundle ID**. It must **exactly** match the Capacitor / Xcode app ID: - **`app.timesafari`** (see `appId` in `capacitor.config.ts` and the Xcode target **Bundle Identifier**). 3. App nickname and App Store ID are optional for local testing; continue. 4. Download **`GoogleService-Info.plist`** when prompted and keep it handy for the next step. ### Add GoogleService-Info.plist to Xcode 1. Open the iOS workspace you generated in [section 4](#4-generate-and-open-the-ios-workspace) (for example `ios/App/App.xcworkspace`). 2. In the Project Navigator, drag **`GoogleService-Info.plist`** into the **App** folder (the same one that contains AppDelegate.swift and Info.plist). 3. In the dialog that appears: - Check **Copy items if needed** (so the file is copied into the project tree). - Under **Add to targets**, ensure the main app target (not only the share extension) is checked. 4. Confirm the file appears under the app target in Xcode and is listed in **Build Phases** → **Copy Bundle Resources** if your project uses that phase for plists. ### Create an APNs Authentication Key Apple uses APNs to deliver pushes to devices; Firebase needs an APNs key to talk to Apple on your behalf. 1. Sign in to [Apple Developer](https://developer.apple.com/account/) → **Certificates, Identifiers & Profiles**. 2. Open **Keys** → **+** (create a new key). 3. Name the key (for example, `Timesafari APNs Dev`). 4. Enable **Apple Push Notifications service (APNs)** and continue. 5. Register the key, then **Download** the `.p8` file. **You can download it only once** — store it securely. 6. Note: - **Key ID** (shown on the key detail page) - **Team ID** (top right of the developer portal, or **Membership** details) ### Upload APNs key to Firebase 1. Firebase Console → your project → **Project settings** (gear icon). 2. Open the **Cloud Messaging** tab. 3. Under **Apple app configuration**, select your iOS app (`app.timesafari`) if prompted. 4. Under **APNs Authentication Key**, click **Upload**. 5. Select the `.p8` file and enter: - **Key ID** - **Team ID** 6. Save. Firebase can now send FCM messages through APNs to your iOS app. ### Enable iOS capabilities in Xcode 1. Select the **App** target → **Signing & Capabilities**. 2. Click **+ Capability** and add **Push Notifications**. 3. Click **+ Capability** again and add **Background Modes**. 4. Under Background Modes, enable **Remote notifications**. These match what silent / data wake flows expect for background delivery. ### Configure Firebase Admin for the backend **notification-wakeup-service** uses the Firebase Admin SDK to send FCM (and thus APNs) messages from your Mac. 1. Firebase Console → **Project settings** → **Service accounts**. 2. Click **Generate new private key** and confirm download of the JSON file. 3. Store the JSON outside the repo (do not commit it). 4. Point the backend at it, for example: ```bash export GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/to/service-account.json" ``` The backend uses this credential to authenticate with Firebase when calling endpoints such as `/debug/send-wakeup`. Set the same variable (or the equivalent env var documented in **notification-wakeup-service**) in the shell where you run `npm run dev`, or add it to that repo’s `.env` per its README. ### Verify Firebase configuration Before ngrok end-to-end testing, confirm: - [ ] App builds and launches on a **physical** iPhone without Firebase/plist errors in Xcode. - [ ] iOS shows the push **permission** prompt (or Settings → app → Notifications is enabled). - [ ] **Notification Debug Panel** shows an FCM token (after permission). - [ ] **Register Token Now** succeeds and ngrok (or local backend) shows `POST /notifications/register`. - [ ] Backend health and Firebase Admin env are set so `/debug/send-wakeup` can run when you reach that step in the workflow below. --- ## 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** bundle (e.g. dev/test build). 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 | | **Refresh Notifications** | `POST /notifications/refresh` (same as post-wakeup flow) | | **Simulate WAKEUP_PING (Local)** | Calls refresh API directly (no FCM) — quick backend test | | **Send Real WAKEUP_PING** | `POST /debug/send-wakeup`; server sends real FCM `WAKEUP_PING` (Android doc has full flow) | | **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 notification API calls. - **Test Mode** — `testMode` field in JSON request bodies only. - **Skip JWT Authentication** — whether JWT `Authorization` headers are sent. For a **hosted shared test server**: set the backend URL, keep **Test Mode** on if required, leave **Skip JWT Authentication** **off**, and ensure an active DID exists. For **local ngrok**: set the backend URL; enable **Skip JWT Authentication** only if your local backend accepts unauthenticated requests. ### Programmatic override (optional) From Safari Web Inspector or a dev console attached to the WebView: ```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. Firebase and Xcode checklist (iOS) This section is a quick verification checklist for the detailed Firebase/APNs setup steps above. | Item | Action | |------|--------| | **Bundle ID** | Match Capacitor `appId` (`app.timesafari` in `capacitor.config.ts`) to Firebase iOS app and Xcode target | | **APNs auth key** | Firebase Console → Project Settings → Cloud Messaging → upload **APNs Authentication Key** (.p8) or certificates | | **Push Notifications** | Xcode target → **Signing & Capabilities** → **+ Capability** → **Push Notifications** | | **Background Modes** | Enable **Remote notifications** (and any others required by your plugin docs) | | **GoogleService-Info.plist** | Present in the iOS target if using Firebase iOS SDK paths in your build | | **FCM token** | Confirm **Register Token Now** succeeds in the debug panel and ngrok shows `POST /notifications/register` | Silent/data pushes used for wake typically use a **content-available** style payload; confirm **notification-wakeup-service** and Firebase message format match what `handleCapacitorPushNotificationReceived` expects (`data.type === "WAKEUP_PING"`). --- ## 8. iOS-specific testing notes ### Physical device required - APNs silent delivery and background wake behavior are **not** representative on the iOS Simulator. - Always validate on a plugged-in or trusted wireless device with a development provisioning profile. ### Silent push is best-effort - iOS may **delay or coalesce** background pushes, especially on battery saver or under load. - A successful `/debug/send-wakeup` from the server does not guarantee immediate app wake. ### Force-quit limitations - If the user **swipes the app away** from the app switcher, iOS often **will not** deliver background notifications until the user launches the app again. - Test with the app **backgrounded** (home button / gesture), not force-quit, when validating wake. ### Low Power Mode and Focus - **Low Power Mode** can reduce background execution. - **Focus / Do Not Disturb** may affect notification presentation (separate from silent data wake, but confusing during tests). ### Two “Simulate WAKEUP_PING” buttons | Button | Behavior | |--------|----------| | **Backend Testing → Simulate WAKEUP_PING** | Skips FCM; calls refresh API only (ngrok path test) | | **Wakeup Ping Simulator** (lower on panel) | Runs production handler with synthetic `WAKEUP_PING` payload | Use the backend button to verify ngrok + refresh; use the simulator to verify handler + refresh chaining. --- ## 9. Recommended debug workflow 1. Start **notification-wakeup-service** on the Mac. 2. Start **ngrok** and copy the HTTPS URL. 3. Set URL + **Test Mode** in the Notification Debug Panel; confirm **Backend Status**. 4. Tap **Register Token Now** → confirm ngrok request and `[Notifications] Token registration success`. 5. Tap **Refresh Notifications** → confirm `Refresh completed in Nms (scheduled X)` in Event Log and ngrok `POST /notifications/refresh`. 6. From the backend, call **`/debug/send-wakeup`** (see curl below) with the registered `deviceId` / FCM token as required by that service. 7. Watch **Xcode console** for `[Notifications] pushNotificationReceived type=WAKEUP_PING` and refresh timing lines. 8. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate requests. 9. Use **Pending Notification Inspector** on the panel to see locally scheduled fires after refresh. --- ## 10. 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": "ios", "testMode": true }' ``` ### Refresh (mirror app payload) ```bash curl -sS -X POST "$BASE/notifications/refresh" \ -H "Content-Type: application/json" \ -d '{ "platform": "ios", "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. --- ## 11. Troubleshooting ### Refresh endpoint unreachable | Symptom | Checks | |---------|--------| | Network error in Event Log | ngrok running? URL saved without typo/trailing slash? | | HTTP 404 | Tunnel port matches backend `PORT`; path is `/notifications/refresh` | | CORS (web only) | Native Capacitor fetch usually avoids browser CORS; if testing in Safari PWA, configure CORS on the service | | ngrok browser warning | Free tier may show an interstitial for browser clients; native `fetch` from the app is usually unaffected | ### Token registration failures - Push permission granted on the device? - Firebase `VITE_FIREBASE_*` env vars baked into the build? - `[Notifications] Token registration failure` in Xcode — read HTTP status in ngrok inspect - Duplicate token skip: panel may show “skipped (duplicate)”; use **Register Token Now** to force re-register ### Silent push not waking the app - App **backgrounded**, not force-quit - Physical device, correct provisioning profile - APNs key uploaded to Firebase; bundle ID matches - FCM message includes `data.type = "WAKEUP_PING"` (see `NativeNotificationService.ts`) - Server actually sent to the **same** FCM token shown in the debug panel - Wait 30–120s — delivery is not instant - Try **Simulate WAKEUP_PING** (refresh API) to isolate app/plugin from FCM/APNs ### Notifications duplicating - Multiple refresh calls (flood test, repeated wakeups) each **replace** schedule via clear + schedule — check Event Log for repeated refreshes - Separate issue: Daily Reminder vs New Activity both scheduling — see `doc/notification-new-activity-lay-of-the-land.md` ### Stale ngrok URL - After restarting ngrok, update **Notification Backend URL** in the panel and tap **Save** - Or clear override (empty field + Save) only if you intend to hit `APP_SERVER` again ### Plugin / JWT errors after refresh - Refresh calls `configureNativeFetcherIfReady()` before scheduling — ensure an **active DID** and endorser API settings exist in the app DB - See `doc/notification-from-api-call.md` and `nativeFetcherConfig.ts` --- ## 12. 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` | Refresh, `WAKEUP_PING`, schedule replace | | `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners | | `src/components/dev/NotificationDebugPanel.vue` | Dev UI | | `src/main.capacitor.ts` | Native push init at startup | --- ## 13. Related docs - [notification-debug-panel.md](./notification-debug-panel.md) — panel controls, authentication, troubleshooting - [Notification Debug Panel (README)](../README.md#notification-debug-panel-dev-builds) - [notification-system-overview.md](./notification-system-overview.md) - [notification-from-api-call.md](./notification-from-api-call.md) - [notification-new-activity-lay-of-the-land.md](./notification-new-activity-lay-of-the-land.md) - [BUILDING.md](../BUILDING.md) — iOS build commands For plugin-native behavior (exact alarm, iOS pending inspector), see **daily-notification-plugin** documentation. For FCM payload format and `/debug/send-wakeup` contract, see **notification-wakeup-service**.