Initialize Firebase at startup so iOS APNs token is captured

Firebase was only initialized lazily when the user toggled push on. On iOS the
firebase_messaging plugin installs its APNs swizzling at app launch, but the
delivered device token can only be forwarded to FCM if a FirebaseApp is already
configured at that point. With lazy init there was none, so getAPNSToken() never
resolved and enabling push failed with "Could not enable push".

Add initFirebaseForPush() (mobile-only, idempotent) and call it from main() at
launch. Extract a shared pushSupportedOnThisPlatform getter so isSupported and
the initializer stay in sync; _ensureFirebase() now delegates to it as a safety
net.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-09 11:51:28 -04:00
co-authored by Claude Opus 4.8
parent 7d7f0197cd
commit 3d190e847e
2 changed files with 35 additions and 14 deletions
+10
View File
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:frontend/theme/catppuccin_mocha_theme.dart';
import 'package:frontend/utils/local_network_permission.dart';
import 'package:frontend/utils/pref_utils.dart';
import 'package:frontend/utils/push_service.dart';
import 'package:provider/provider.dart';
import 'navigation.dart';
import 'theme/gruvbox_theme.dart';
@@ -31,6 +32,15 @@ void main() {
);
}
// Initialize Firebase at launch so the firebase_messaging plugin's iOS
// APNs swizzling has a configured app to forward the device token to;
// without this getAPNSToken() never resolves and enabling push fails.
try {
await initFirebaseForPush();
} catch (e, stack) {
debugPrint('Firebase init for push failed, continuing: $e\n$stack');
}
// Settle iOS's local-network permission now, at launch, so it isn't
// still being decided when the user first taps "Test" in settings.
unawaited(requestLocalNetworkPermission());
+25 -14
View File
@@ -6,6 +6,25 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../firebase_options.dart';
import 'oott_api.dart';
/// Whether push is available on this platform/build (mobile only — FCM/APNs).
/// Shared by [FirebasePushService.isSupported] and [initFirebaseForPush] so the
/// two never drift.
bool get pushSupportedOnThisPlatform =>
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
/// Initializes Firebase at app startup on push-capable platforms. This must run
/// at launch — the firebase_messaging plugin wires up iOS APNs swizzling in the
/// AppDelegate at launch, and it can only forward the APNs device token to FCM
/// if a FirebaseApp is already configured when iOS delivers it. Without this,
/// `getAPNSToken()` never resolves and enabling push fails. No-op on web/desktop
/// and if Firebase is already initialized.
Future<void> initFirebaseForPush() async {
if (!pushSupportedOnThisPlatform || Firebase.apps.isNotEmpty) return;
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
}
/// Per-device push enable/disable, behind an interface so the settings UI can be
/// driven by a fake in tests without pulling in Firebase. Tapping a push only
/// opens the app (no deep-link, no identifier); the in-app notification list
@@ -47,24 +66,16 @@ class FirebasePushService implements PushService {
bool _foregroundDisplayWired = false;
@override
bool get isSupported =>
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
bool get isSupported => pushSupportedOnThisPlatform;
String get _platformName =>
defaultTargetPlatform == TargetPlatform.iOS ? 'ios' : 'android';
Future<void> _ensureFirebase() async {
// Options come from the committed firebase_options.dart rather than native
// config files, so no google-services.json / GoogleService-Info.plist is
// needed in the build.
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
}
// Safety net in case startup init was skipped; normally Firebase is already
// initialized at launch by initFirebaseForPush(). Options come from the
// committed firebase_options.dart rather than native config files, so no
// google-services.json / GoogleService-Info.plist is needed in the build.
Future<void> _ensureFirebase() => initFirebaseForPush();
@override
Future<bool> enable() async {