Surface native iOS APNs registration outcome for diagnostics

Enabling push still fails on iOS with getAPNSToken() returning null, so capture
the APNs registration result natively to find out why. AppDelegate now overrides
didRegister/didFailToRegister, logs the outcome, and exposes it over a
oott/push_diagnostics method channel (super still calls through so Firebase
swizzling is unaffected).

apnsRegistrationStatus() reads that channel, and the settings push toggle now
shows Apple's actual rejection reason in the error message when enabling fails,
so it can be diagnosed without a Mac to read the device console.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-09 12:06:11 -04:00
co-authored by Claude Opus 4.8
parent 3d190e847e
commit c7c5b45ae8
3 changed files with 72 additions and 1 deletions
+45
View File
@@ -3,11 +3,56 @@ import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
// The most recent APNs registration outcome. Surfaced to Dart over a method
// channel for in-app diagnostics, because reading the device console requires
// a Mac and OOTT is built/shipped via Codemagic + TestFlight.
private var apnsStatus = "APNs: registration not completed yet"
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
if let controller = window?.rootViewController as? FlutterViewController {
let channel = FlutterMethodChannel(
name: "oott/push_diagnostics",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { [weak self] call, result in
if call.method == "apnsStatus" {
result(self?.apnsStatus ?? "APNs: status unavailable")
} else {
result(FlutterMethodNotImplemented)
}
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Logged for diagnostics; super still forwards the token to the
// firebase_messaging plugin (method swizzling is enabled).
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
apnsStatus = "APNs: registered OK (token bytes=\(deviceToken.count))"
NSLog("[OOTT] \(apnsStatus)")
super.application(
application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
// This fires instead of the success callback when iOS refuses to issue an APNs
// token; error.localizedDescription is the reason we need (e.g. "no valid
// 'aps-environment' entitlement string found for application").
override func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
apnsStatus = "APNs: registration FAILED - \(error.localizedDescription)"
NSLog("[OOTT] \(apnsStatus)")
super.application(
application, didFailToRegisterForRemoteNotificationsWithError: error)
}
}
+8 -1
View File
@@ -104,9 +104,16 @@ class _SettingsState extends State<Settings> {
);
} else {
setState(() => _pushEnabled = false);
// On iOS, surface the native APNs registration reason (if any) so the
// failure can be diagnosed without a Mac to read the device console.
final apnsStatus = await apnsRegistrationStatus();
if (!mounted) return;
UISnackbars.showError(
context,
'Could not enable push. Check notification permission for OOTT.',
apnsStatus != null
? 'Could not enable push. $apnsStatus'
: 'Could not enable push. Check notification permission for '
'OOTT.',
);
}
} else {
+19
View File
@@ -1,6 +1,7 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../firebase_options.dart';
@@ -14,6 +15,24 @@ bool get pushSupportedOnThisPlatform =>
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
// Channel exposing the native iOS APNs registration outcome (see AppDelegate).
const MethodChannel _pushDiagnosticsChannel = MethodChannel(
'oott/push_diagnostics',
);
/// Reads the most recent native iOS APNs registration outcome for in-app
/// diagnostics, since reading the device console requires a Mac. Returns a short
/// status string on iOS, or null elsewhere or when the channel is unavailable
/// (e.g. in tests).
Future<String?> apnsRegistrationStatus() async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.iOS) return null;
try {
return await _pushDiagnosticsChannel.invokeMethod<String>('apnsStatus');
} catch (_) {
return null;
}
}
/// 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