From 7d7f0197cd5965c99e048acff39802a5bae73df9 Mon Sep 17 00:00:00 2001 From: rzuasti Date: Tue, 9 Jun 2026 11:19:46 -0400 Subject: [PATCH] Wait for iOS APNs token before requesting FCM token On iOS, FirebaseMessaging.getToken() throws apns-token-not-set when called before Apple has asynchronously delivered the APNs token, which happens after the permission prompt. This surfaced as "Failed to update push settings" with the toggle stuck off. Poll getAPNSToken() with a short bounded wait first, and return false (toggle stays off) rather than throwing if it never arrives. Co-Authored-By: Claude Opus 4.8 --- frontend/lib/utils/push_service.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/lib/utils/push_service.dart b/frontend/lib/utils/push_service.dart index d49b0fe..2db00d2 100644 --- a/frontend/lib/utils/push_service.dart +++ b/frontend/lib/utils/push_service.dart @@ -76,6 +76,16 @@ class FirebasePushService implements PushService { return false; } + // On iOS, FCM can only mint a token once Apple has delivered the APNs token + // to the app, which happens asynchronously after permission is granted. + // Calling getToken() before then throws `apns-token-not-set`, so wait for + // the APNs token first. Returns false (rather than throwing) if it never + // arrives, so the toggle simply stays off instead of erroring. + if (defaultTargetPlatform == TargetPlatform.iOS && + !await _awaitApnsToken()) { + return false; + } + final token = await FirebaseMessaging.instance.getToken(); if (token == null) return false; @@ -102,6 +112,17 @@ class FirebasePushService implements PushService { await FirebaseMessaging.instance.deleteToken(); } + // Polls for the iOS APNs token, which Apple delivers asynchronously after the + // user grants permission. Returns true once it is available, or false if it + // has not arrived after a short bounded wait (e.g. no network on first run). + Future _awaitApnsToken() async { + for (var attempt = 0; attempt < 10; attempt++) { + if (await FirebaseMessaging.instance.getAPNSToken() != null) return true; + await Future.delayed(const Duration(milliseconds: 500)); + } + return false; + } + // Configure the local-notifications plugin and render foreground messages // ourselves (the OS displays them directly when the app is backgrounded or // terminated). Taps just open the app, so no tap handler is wired.