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.