fix(register): auto re-register with proxy on every launch#15
Merged
Conversation
Real tester (npub1whale...) hit a silent push-delivery failure today:
their signer pubkey had ZERO APNs tokens registered with the proxy
(`[PRIMARY] Event for pubkey 75fbfcd7... — no registered tokens,
skipping` repeated 63 times in 4h). Foreground L1 worked because it
catches via WebSocket and bypasses APNs entirely; everything else
silently failed.
Root cause: registration with the proxy only happens in
`AppState.importKey()` / `generateKey()` and the manual Settings →
Register button. Three real failure modes lead to a stale or absent
token on the proxy:
1. iOS rotates the device token (Apple does this periodically,
especially after iOS upgrades). New token from
`didRegisterForRemoteNotificationsWithDeviceToken` was being saved
to UserDefaults but never pushed to the proxy.
2. User reinstalls Clave from TestFlight — fresh APNs token, but the
existing nsec in Keychain means we never re-hit the
importKey/generateKey path.
3. Proxy-side token loss (the 2026-04-20 tokens.json migration wiped
legacy entries; future bugs may do similar) — we had no way to
recover without manual intervention.
The previous design comment in `ClaveApp.swift` claimed AppDelegate
"doesn't have access to the signer nsec without duplicating
LightEvent.signNip98 logic" — true, but solvable by signaling AppState
via NotificationCenter and letting it do the actual NIP-98-signed POST.
Changes:
* `Clave/ClaveApp.swift` `didRegisterForRemoteNotificationsWithDeviceToken`
now posts `NotificationCenter.default.post(name: .apnsDeviceTokenAvailable, object: token)`
every time iOS hands us a token. New `.apnsDeviceTokenAvailable`
Notification.Name added to the existing extension.
* `Clave/AppState.swift`:
- `init` adds a NotificationCenter observer for
`.apnsDeviceTokenAvailable` that updates `deviceToken` and calls
`registerWithProxy()` if `isKeyImported`.
- `loadState()` calls `registerWithProxy()` if both key and stored
token are present. Belt-and-suspenders for the cold-launch ordering
case where iOS handed us the token via the AppDelegate path *before*
`loadState()` ran (so the observer's `isKeyImported` check failed
at that moment because the key hadn't loaded from Keychain yet).
Both paths are idempotent on the proxy side (the /register endpoint
upserts), so the common case of "token+pubkey unchanged" is harmless.
Verification:
- xcodebuild -scheme Clave -destination 'generic/platform=iOS' build →
BUILD SUCCEEDED
- xcodebuild test → TEST SUCCEEDED
- Real tester confirmation pending: build 26 ships → tester reinstalls
or just opens app → proxy tokens.json gains their entry without manual
Settings → Register tap.
Closes the BACKLOG item: "Auto-register on launch: loadState() doesn't
re-register with the proxy — only importKey()/generateKey() do."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b7c5fa5 to
f60f873
Compare
DocNR
added a commit
that referenced
this pull request
Apr 28, 2026
Rolls up PR #13 (pending-approval refresh + banner + UI bundle, already shipped as build 25 from the branch) and PR #15 (auto re-register with proxy on every launch). Build 25 was archived from PR #13 branch without merging to main; this commit catches main up to where build 25 already was, plus adds PR #15 on top for build 26. Tagging policy: tag v0.1.0-build26 immediately after archive as Pre-release on GitHub; flip to Latest once Apple clears external review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
DocNR
added a commit
that referenced
this pull request
Apr 28, 2026
…C sweep (#16) Two real bugs from build 26 testing today: 1. **registerWithProxy silently drops failures on bad cellular.** User launched Clave on weak cellular signal; the auto-register HTTP POST (10s timeout) failed, the failure was dropped because the auto- register call sites in `AppState.init` (`.apnsDeviceTokenAvailable` observer) and `loadState()` pass `completion: nil`. Token never reached proxy. Symptom: signing silently failed until user moved to wifi and tapped Settings → Register manually (the manual path surfaces the failure to the UI). PR #15 was correct on the happy path but didn't self-heal when the initial POST failed. 2. **Blank Notification Center entries weren't being swept.** Build 26 shipped `MainTabView.sweepBlankNotifications()` filtering on `title.isEmpty`. But the proxy's APNs payload sets `alert: { title: " ", body: " " }` (single SPACE characters, so NSE has something to override). When NSE doesn't run (cold-launch race, timeout, force-quit recovery), iOS keeps the proxy's original payload — title is " " (single space), NOT empty. Sweep never matched these. User reported 8 blank entries accumulating in NC while Clave was backgrounded. Changes: * New `Clave/Views/Components/NotificationCenterSweep.swift` extracts `sweepBlankNotifications()` to a top-level free function so both `MainTabView` (scenePhase observer) and `ForegroundRelaySubscription` (L1 event dispatch) can call it. Filter now trims whitespace and checks BOTH title AND body, catching the proxy single-space fallback. * `MainTabView.handleScenePhase` now sweeps on `.inactive` too (catches the case where a user opens NC via swipe-down while Clave is the most-recent foreground app but isn't currently `.active`). Also calls `appState.ensureRegisteredFresh()` on `.active`. * `Shared/ForegroundRelaySubscription.swift` calls `sweepBlankNotifications()` after each in-process event. Catches the case where Clave is foregrounded and a parallel APNs push leaves a blank NC entry from NSE's `.noEvents` return path. * `Clave/AppState.swift` `registerWithProxy()` now records `lastRegisterSucceededAtKey` / `lastRegisterFailedAtKey` timestamps in `SharedConstants.sharedDefaults`. New `ensureRegisteredFresh()` method gates re-registers: skips if last success < 30 min ago, applies a 60s cooldown after failures so a dead proxy doesn't get hammered on every foreground. * `Shared/SharedConstants.swift` adds the two new defaults keys. Verification: - xcodebuild -scheme Clave -destination 'generic/platform=iOS' build → BUILD SUCCEEDED - xcodebuild test on iPhone 17 Pro Max sim (iOS 26.4) → TEST SUCCEEDED - Device test (build 27): on bad cellular, launch Clave, verify ensureRegisteredFresh() retries on next foreground after network recovery. Verify NC stays clean of blank entries even with Clave backgrounded for ~30 min during a session. Closes BACKLOG: "registerWithProxy() retry on transient network failure" (opened today, fixed same day). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Real tester (npub1whale...) hit a silent push-delivery failure today: their signer pubkey had zero APNs tokens registered with the proxy (`[PRIMARY] Event for pubkey 75fbfcd7... — no registered tokens, skipping` repeated 63 times in 4h). Foreground L1 worked because it catches via WebSocket and bypasses APNs entirely; everything else silently failed. Their workaround was "toggle Clave to foreground every time" — fine as a stopgap, broken as a product.
Root cause: registration with the proxy only happens in `AppState.importKey()` / `generateKey()` and the manual Settings → Register button. Three real failure modes leave the proxy with a stale or absent token:
The previous design comment in `ClaveApp.swift` claimed AppDelegate "doesn't have access to the signer nsec without duplicating LightEvent.signNip98 logic" — true, but solvable by signaling AppState via NotificationCenter and letting it do the actual NIP-98-signed POST.
Changes
Both paths are idempotent on the proxy side (the /register endpoint upserts), so the common case of "token+pubkey unchanged" is harmless.
Test plan
Closes
BACKLOG: "Auto-register on launch (low priority): loadState() doesn't re-register with the proxy — only importKey()/generateKey() do." Promoted to High after this incident.
Why this didn't make build 25
Build 25 just shipped (PR #13 merged + archived). This is a follow-up for build 26. Independent enough to merge separately; no conflicts with PR #14 (proxy resilience, server-side).
🤖 Generated with Claude Code