Files
crowd-funder-for-time-pwa/doc/local-ios-testing-ngrok.md
Jose Olarte III f23fe65078 docs(notifications): document debug panel auth settings and bypassAuth
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.
2026-07-07 19:21:18 +08:00

24 KiB
Raw Blame History

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:

┌─────────────────────┐     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

# Homebrew
brew install ngrok/ngrok/ngrok

Or download from https://ngrok.com/download.

Account and auth token

  1. Sign up at https://dashboard.ngrok.com/signup.
  2. Copy your authtoken from Your Authtoken in the dashboard.
  3. Configure the CLI:
ngrok config add-authtoken YOUR_AUTHTOKEN_HERE

Start a tunnel to the wakeup service

Assume the service listens on port 3000 (confirm in notification-wakeup-service README or .env).

If the service already defaults to port 3000 internally, you may not need to export PORT manually.

# Terminal A — backend
cd /path/to/notification-wakeup-service

npm install

# one-time setup if needed
cp .env.example .env

# configure Firebase/service account/etc as required
export PORT=3000

npm run dev
# Terminal B — ngrok
ngrok http 3000

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:

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 Macs 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:

# If not already running from the previous step:
cd /path/to/notification-wakeup-service
npm run dev

Verify locally before ngrok:

curl -sS http://localhost:3000/health

Expected: HTTP 200 and a JSON body indicating the service is up (exact shape depends on that repo).


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:

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 (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.

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.

Create or access a Firebase account

  1. Sign in with a Google account at 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, 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 (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 PhasesCopy 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 DeveloperCertificates, 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 settingsService 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:
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 repos .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.

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 ModetestMode 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:

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+ CapabilityPush 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.


  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:

export BASE="https://abc123.ngrok-free.app"

Health

curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"

Register device (mirror app payload)

curl -sS -X POST "$BASE/notifications/register" \
  -H "Content-Type: application/json" \
  -d '{
    "deviceId": "00000000-0000-4000-8000-000000000001",
    "fcmToken": "YOUR_FCM_TOKEN_FROM_DEBUG_PANEL",
    "platform": "ios",
    "testMode": true
  }'

Refresh (mirror app payload)

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):

{
  "shouldNotify": true,
  "nextNotifications": [
    { "timestamp": 1710000000000 },
    { "timestamp": 1710003600000 }
  ]
}

The app schedules those timestamps via daily-notification-plugin (applyNotificationRefreshPayload in NativeNotificationService.ts).

Send wakeup push (debug)

Exact path and body depend on notification-wakeup-service; typical pattern:

curl -sS -X POST "$BASE/debug/send-wakeup" \
  -H "Content-Type: application/json" \
  -d '{
    "deviceId": "00000000-0000-4000-8000-000000000001",
    "testMode": true
  }'

Confirm parameters (token vs deviceId, auth headers) in that repos 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 30120s — 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

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.