Push registers with registerPush, falling back on servers without it
MergedThis switches APNs registration and withdrawal to social.grain.notification.registerPush and unregisterPush. It falls back to the old registerToken and unregisterToken calls only when PushManager.isMethodMissing sees a 404, a 501, or a MethodNotImplemented or MethodNotFound error. I found nothing that must change.
- Fallback and error handling: other failures, such as a 401 or a 400
InvalidRequest, are logged and swallowed without a retry. The old payloads are unchanged. - Tests: they cover the request shape and each fallback and non-fallback case for both register and unregister, plus
isMethodMissingon its own. - Known gaps: the body states the gaps itself. The constant
appIdand the lost body on the DPoP nonce-retry path are acceptable for now. A development build under another bundle id will register as the shipping app, so revisit that once the new server is the only one.
I only read the diff. I did not run the tests, and the PR notes that no workflow builds or tests the app.
Carried forward: the change this pull makes at 9b73f8213dfba1580fa2ad4e2a94c563e1a1d60b is the one reviewed at 68116de9d8b0469c2a90b413ca524f5910da335f, so that review stands. What is new comes from its base.
Carried forward: the change this pull makes at eeaa856dc2450c659e9efc1114249c5426be57d4 is the one reviewed at 9b73f8213dfba1580fa2ad4e2a94c563e1a1d60b, so that review stands. What is new comes from its base.

What changes
The app registers its APNs token with
social.grain.notification.registerPush, and withdraws it withsocial.grain.notification.unregisterPush. Both take the input of Bluesky'sapp.bsky.notification.registerPush:serviceDid(did:web:grain.social),token(the hex APNs token, as before),platform(ios) andappId(social.grain.grain).When the server answers 404, 501, or an XRPC error named
MethodNotImplementedorMethodNotFound,PushManagersends the olderregisterToken/unregisterTokencall exactly as it is sent today ({token, platform: "apns"}and{token}). Any other failure, such as a 401 or a 400InvalidRequest, is the answer: it is logged and swallowed as before, and the older call is not tried.Auth is unchanged: the new call goes through the same client and auth context as the old one. The triggers are unchanged too (token arrival after sign-in and on account switch, unregister on sign-out and on switching away).
Why
The Rust server serves only
registerPush/unregisterPush, shaped as Bluesky's. Production still runs the old server until cutover, and that has noregisterPush(it answers 404 today), so the app has to work against both until then.How it was checked
NetworkSideEffectTestscovers:registerPushandunregisterPushrequest: path, POST, JSON content type, and a body of exactly the four fields;registerTokenand tounregisterTokenon each of 404, 501, 400MethodNotImplementedand 400MethodNotFound, with the old body exactly as before;InvalidRequestor 500, for both register and unregister;PushManager.isMethodMissingon its own.Ran
NetworkSideEffectTestsandAppLifecycleTestslocally on the iPhone 17 Pro Max simulator: 25 tests, 0 failures. swiftformat and swiftlint are clean on the touched files.Shortcomings
registerPushis untested.appIdis the constantsocial.grain.grain, notBundle.main.bundleIdentifier, so a development build under another bundle id still registers as the shipping app.MethodNotImplemented400 seen only after a nonce retry would not fall back. 404 and 501 do in every path, and production answers 404.🤖 Generated with Claude Code