Guides and the debug panel still described /notifications/refresh and api_* scheduling as current after Phase 5; keep Phase 4 cleanup and FCM send-wakeup diagnostics.
24 KiB
Local iOS Testing with ngrok (notification-wakeup-service)
Last updated: 2026-09-24 (retired WAKEUP_PING → refresh → api_* consumption)
Audience: Developers on crowd-funder-for-time-pwa, daily-notification-plugin, and notification-wakeup-service
Goal: Exercise FCM token registration and silent-push delivery (WAKEUP_PING via /debug/send-wakeup) against a Mac-hosted backend reachable from a physical iPhone.
Retired (do not expect this in the app):
WAKEUP_PING→POST /notifications/refresh→applyNotificationRefreshPayload()→api_*local schedules. The app logs ignored push types and does not refresh or schedule from wakeup./debug/send-wakeupremains an FCM/APNs diagnostic. Maccurlof/notifications/refreshonly tests the backend.
Architecture overview
End-to-end flow when testing FCM registration and wakeup delivery on a physical iPhone:
┌─────────────────────┐ HTTPS ┌──────────────────────┐
│ Mac (localhost) │ ◄───────────── │ ngrok edge │
│ notification- │ tunnel │ (public HTTPS URL) │
│ wakeup-service │ └──────────┬───────────┘
└──────────┬──────────┘ │
│ │ fetch
│ │ POST /notifications/register
│ ▼
│ ┌──────────────────────┐
│ │ crowd-funder-for- │
│ │ time-pwa (Capacitor │
│ │ iOS on iPhone) │
│ └──────────┬───────────┘
│ │
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
▼ ▼ (Daily Reminder / dual / fetcher)
┌─────────────────────┐ ┌──────────────────────┐
│ Firebase Cloud │ ──APNs──────► │ iPhone (physical) │
│ Messaging │ silent push │ app.timesafari │
└─────────────────────┘ └──────────────────────┘
Repos and responsibilities
| Repo | Role |
|---|---|
| notification-wakeup-service | HTTP API: device registration, health, debug wakeup send; may still expose /notifications/refresh |
| crowd-funder-for-time-pwa | Capacitor app: FCM token, POST /notifications/register; logs WAKEUP_PING without refresh/api_* scheduling |
| daily-notification-plugin | Native iOS/Android: Daily Reminder, New Activity / dual, native fetcher; Phase 4 clearApiNotifications() |
Silent wake sequence (current)
- Backend
/debug/send-wakeupsends an FCM data message withdata.type = "WAKEUP_PING". - APNs delivers to the device (best-effort; see iOS caveats below).
- Capacitor
pushNotificationReceivedfires →handleCapacitorPushNotificationReceived(). - Handler logs
[Notifications] push handler ignored type=WAKEUP_PING. It does not POST/notifications/refreshor scheduleapi_*notifications.
Console and debug panel lines are prefixed with [Notifications] (see NotificationDebugEvents.ts).
Prerequisites
- Mac 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
- Sign up at https://dashboard.ngrok.com/signup.
- Copy your authtoken from Your Authtoken in the dashboard.
- Configure the CLI:
ngrok config add-authtoken YOUR_AUTHTOKEN_HERE
Start a tunnel to the wakeup service
Assume the service listens on port 3000 (confirm in notification-wakeup-service README or .env).
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 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:
# 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
- Run
ngrok http <PORT>. - Copy the
https://….ngrok-free.apphost from the Forwarding line. - Do not add a trailing slash when saving in the app (the debug config trims it).
- Optional: open
http://127.0.0.1:4040(ngrok web UI) to inspect requests and responses while testing.
Test through the tunnel from your Mac:
export NGROK_URL="https://abc123.ngrok-free.app"
curl -sS "$NGROK_URL/health"
4. 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
-
Sign in with a Google account at https://console.firebase.google.com/.
-
If this is your first time using Firebase:
- Accept the Firebase terms.
- Create a new Firebase account/workspace when prompted.
-
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
- In the Firebase Console, click Add project (or Create a project).
- Enter a project name (for example,
timesafari-dev) and continue through the wizard. - Google Analytics is optional for this workflow; you can disable it for a simpler dev project.
- 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
- In the project overview, click the iOS icon (Add app → iOS).
- Enter the Apple bundle ID. It must exactly match the Capacitor / Xcode app ID:
app.timesafari(seeappIdincapacitor.config.tsand the Xcode target Bundle Identifier).
- App nickname and App Store ID are optional for local testing; continue.
- Download
GoogleService-Info.plistwhen prompted and keep it handy for the next step.
Add GoogleService-Info.plist to Xcode
- Open the iOS workspace you generated in section 4 (for example
ios/App/App.xcworkspace). - In the Project Navigator, drag
GoogleService-Info.plistinto the App folder (the same one that contains AppDelegate.swift and Info.plist). - 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.
- 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.
- Sign in to Apple Developer → Certificates, Identifiers & Profiles.
- Open Keys → + (create a new key).
- Name the key (for example,
Timesafari APNs Dev). - Enable Apple Push Notifications service (APNs) and continue.
- Register the key, then Download the
.p8file. You can download it only once — store it securely. - 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
- Firebase Console → your project → Project settings (gear icon).
- Open the Cloud Messaging tab.
- Under Apple app configuration, select your iOS app (
app.timesafari) if prompted. - Under APNs Authentication Key, click Upload.
- Select the
.p8file and enter:- Key ID
- Team ID
- Save. Firebase can now send FCM messages through APNs to your iOS app.
Enable iOS capabilities in Xcode
- Select the App target → Signing & Capabilities.
- Click + Capability and add Push Notifications.
- Click + Capability again and add Background Modes.
- 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.
- Firebase Console → Project settings → Service accounts.
- Click Generate new private key and confirm download of the JSON file.
- Store the JSON outside the repo (do not commit it).
- 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 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-wakeupcan run when you reach that step in the workflow below.
6. Configure the Notification Debug Panel backend override
The app normally calls DEFAULT_NOTIFY_API_SERVER (from VITE_DEFAULT_NOTIFY_API_SERVER, falling back to AppString.PROD_NOTIFY_API_SERVER). That is independent of APP_SERVER. For local wakeup testing, override the notification API base URL in the Debug Panel without rebuilding.
For a full panel reference (configuration, URL resolution order, authentication, and troubleshooting), see notification-debug-panel.md.
Open the panel
- Use a non-production bundle (e.g. dev/test build).
- Account → enable Show All General Advanced Functions.
- Open Notification Debug Panel (route
/dev/notifications).
Backend Testing section
| Control | Purpose |
|---|---|
| Notification Backend URL | Paste ngrok HTTPS URL → Save Backend URL (changes target server only) |
| Test Mode | Sends testMode: true/false in register / send-wakeup JSON bodies (default on when unset in storage) |
| Skip JWT Authentication (Local Development Only) | When on, omits Authorization headers for local servers that accept unauthenticated requests (default off) |
| Register Token Now | POST /notifications/register with current FCM token |
| Upload AlertSearch Authorization | Uploads AlertSearch delegated JWTs |
| Send Real WAKEUP_PING | POST /debug/send-wakeup; FCM/APNs delivery diagnostic only |
| 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 —
testModefield in JSON request bodies only. - Skip JWT Authentication — whether JWT
Authorizationheaders 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 → + 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. The app logs WAKEUP_PING as ignored and does not refresh.
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-wakeupfrom 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).
Send Real WAKEUP_PING (FCM/APNs diagnostic)
Send Real WAKEUP_PING posts /debug/send-wakeup. Delivery is confirmed by push handler ignored type=WAKEUP_PING. Mock refresh, Simulate WAKEUP_PING, and Wakeup Ping Simulator were removed.
9. Recommended debug workflow
- Start notification-wakeup-service on the Mac.
- Start ngrok and copy the HTTPS URL.
- Set URL + Test Mode in the Notification Debug Panel; confirm Backend Status.
- Tap Register Token Now → confirm ngrok request and
[Notifications] Token registration success. - From the backend, call
/debug/send-wakeup(see curl below) with the registereddeviceId/ FCM token as required by that service, or tap Send Real WAKEUP_PING. - Watch Xcode console for
[Notifications] push handler ignored type=WAKEUP_PING. - Open ngrok inspect UI (
http://127.0.0.1:4040) to correlate requests (register and send-wakeup; not app-initiated refresh). - Use Pending Notification Inspector for Daily Reminder / dual / fetcher schedules.
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 (backend only; app does not consume)
curl -sS -X POST "$BASE/notifications/refresh" \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"testMode": true
}'
This exercises notification-wakeup-service only. The app does not call applyNotificationRefreshPayload or schedule api_* from the response.
Send wakeup push (debug)
Exact path and body depend on notification-wakeup-service; typical pattern:
curl -sS -X POST "$BASE/debug/send-wakeup" \
-H "Content-Type: application/json" \
-d '{
"deviceId": "00000000-0000-4000-8000-000000000001",
"testMode": true
}'
Confirm parameters (token vs deviceId, auth headers) in that repo’s README or OpenAPI spec.
11. Troubleshooting
Refresh endpoint unreachable
| Symptom | Checks |
|---|---|
Mac curl of /notifications/refresh fails |
ngrok running? URL saved without typo/trailing slash? |
| HTTP 404 | Tunnel port matches backend PORT; path is /notifications/refresh (backend route; the app does not call it) |
| 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 failurein 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"(logged as ignored inNativeNotificationService.ts) - Server actually sent to the same FCM token shown in the debug panel
- Wait 30–120s — delivery is not instant
- Confirm Xcode shows
push handler ignored type=WAKEUP_PING(there is no Simulate WAKEUP_PING / refresh API in the app)
Notifications duplicating
- Daily Reminder vs New Activity both scheduling — see
doc/notification-new-activity-lay-of-the-land.md - Leftover
api_*before Phase 4 cleanup (startupclearApiNotifications())
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
DEFAULT_NOTIFY_API_SERVERagain
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.mdandnativeFetcherConfig.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 |
Push delivery hook (logs ignored types; no refresh) |
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 — panel controls, authentication, troubleshooting
- Notification Debug Panel (README)
- notification-system-overview.md
- notification-from-api-call.md
- notification-new-activity-lay-of-the-land.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.