Preface:
My App is Flutter based - but native code implementation is required to get FCM messages working, see below for more details
GitHub issue #154 for reference.
I'm having immense trouble getting FCM notifications working on iOS, specifically on my app published to Testflight. I have been stuck on this problem for a week and have absolutely no idea how to proceed.
Problem
When running locally using debug/release builds on my devices using Xcode/Android Studio, notifications are received in the background, foreground, etc. When uploading the exact same app to Testflight, not a single notification will come through via FCM.
This is crucial as FCM delivers VoIP notifications, these aren't being received on Testflight which is extremely distressing
Questions & Solutions?
There are 2 questions I found (here & here), both seemed to indicate it is a APNS certificate problem (APNS -> Firebase). I have recreated my certificate and added it to the Firebase console (using the same .csr
file for all certificate generating operations)
Setup/Configuration:
APNS Key generated & added to Firebase
Capabilities:
Tried with:
<key>FirebaseAppDelegateProxyEnabled</key>
<string>NO</string>
with:
<key>FirebaseAppDelegateProxyEnabled</key>
<string>0</string>
and with :
<key>FirebaseAppDelegateProxyEnabled</key>
<boolean>false</boolean>
- Background modes:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>bluetooth-central</string>
<string>external-accessory</string>
<string>fetch</string>
<string>location</string>
<string>processing</string>
<string>remote-notification</string>
<string>voip</string>
<string>remote-notification</string>
</array>
Tutorials/sources:
Swift Code: (targeting >=10.0)
import UIKit
import CallKit
import Flutter
import Firebase
import UserNotifications
import GoogleMaps
import PushKit
import flutter_voip_push_notification
import flutter_call_kit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// run firebase app
FirebaseApp.configure()
// setup Google Maps
GMSServices.provideAPIKey("google-maps-api-key")
// register notification delegate
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
GeneratedPluginRegistrant.register(with: self)
// register VOIP
self.voipRegistration()
// register notifications
application.registerForRemoteNotifications();
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Handle updated push credentials
public func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
// Process the received pushCredentials
FlutterVoipPushNotificationPlugin.didUpdate(pushCredentials, forType: type.rawValue);
}
// Handle incoming pushes
public func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Swift.Void){
FlutterVoipPushNotificationPlugin.didReceiveIncomingPush(with: payload, forType: type.rawValue)
let signalType = payload.dictionaryPayload["signal_type"] as! String
if(signalType == "endCall" || signalType == "rejectCall"){
return
}
let uuid = payload.dictionaryPayload["session_id"] as! String
let uID = payload.dictionaryPayload["caller_id"] as! Int
let callerName = payload.dictionaryPayload["caller_name"] as! String
let isVideo = payload.dictionaryPayload["call_type"] as! Int == 1;
FlutterCallKitPlugin.reportNewIncomingCall(
uuid,
handle: String(uID),
handleType: "generic",
hasVideo: isVideo,
localizedCallerName: callerName,
fromPushKit: true
)
completion()
}
// Register for VoIP notifications
func voipRegistration(){
// Create a push registry object
let voipRegistry: PKPushRegistry = PKPushRegistry(queue: DispatchQueue.main)
// Set the registry's delegate to self
voipRegistry.delegate = self
// Set the push type to VoIP
voipRegistry.desiredPushTypes = [PKPushType.voIP]
}
}
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if #available(iOS 14.0, *) {
completionHandler([ .banner, .alert, .sound, .badge])
} else {
completionHandler([.alert, .sound, .badge])
}
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
print(deviceToken)
Messaging.messaging().apnsToken = deviceToken;
}
Flutter main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await initializeDateFormatting();
setupLocator();
var fcmService = locator<FCMService>();
FirebaseMessaging.onBackgroundMessage(FCMService.handleFirebaseBackgroundMessage);
FirebaseMessaging.onMessage.listen((event) {
print("Foreground message");
Fluttertoast.showToast(msg: "Received onMessage event");
FCMService.processCallNotification(event.data);
});
FirebaseMessaging.onMessageOpenedApp.listen((event) {
print("On message opened app");
Fluttertoast.showToast(msg: "Received onMessageOpenedAppEvent");
FCMService.handleInitialMessage(event);
});
FirebaseMessaging.instance.getInitialMessage().then((value) {
Fluttertoast.showToast(msg: "Received onLaunch event");
if (value != null) {
FCMService.handleInitialMessage(value);
}
});
initConnectyCube();
runApp(AppProviders());
}
FCMService.dart
// handle any firebase message
static Future<void> handleFirebaseBackgroundMessage(RemoteMessage message) async {
print("Received background message");
Fluttertoast.showToast(msg: "Received Firebase background message");
await Firebase.initializeApp();
setupLocator();
var fcmService = locator<FCMService>();
fcmService.init();
_handleMessage(message, launchMessage: true);
}
Testing:
Testing is done on 2 physical iPhones (6s & 8). Both work with Firebase FCM when building directly from Mac (Android Studio & XCode) using (debug & release) modes. Neither works when downloading the same from TestFlight.
If any can provide insight into a misconfiguration, an error in setup or missing/incorrect Swift code, or simply a mistake or omission, it would be much appreciated.
CodePudding user response:
This problem sometimes drive people crazy even they apply everything in the correct scenario, so please try to check the following:
1- in your apple developer account make sure that you have only one Apple Push Services Certificate
assigned to the app identifier ( Bundle ID ), please avoid duplication.
2- If you are using the APNs key to receive notification you have to make sure its set on the production mode when your app is uploaded to TestFlight or AppStore
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: ".2hhx", $0) }.joined()
print("APNs Device Token: \(token)")
Messaging.messaging().apnsToken = deviceToken
Messaging.messaging().setAPNSToken(deviceToken, type: .prod)
}
Note: TestFlight considered as a release (Production) mode not as sandbox mode
CodePudding user response:
Preface: the issue was mine.
TL;DR
changed only 1 reference of CubeEnvironment
to PRODUCTION
.
There are multiple locations to change CubeEnvironment
:
Suggestion to use, even better to add this in your "CallManagerService"'s init()
method:
bool isProduction = bool.fromEnvironment('dart.vm.product');
parameters.environment = isProduction ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
Debugging (process):
The debugging process (being somewhat unfamiliar with Swift & XCode) could have been better. I considered various provisioning profiles, aps-environment
settings, etc.
Since the issue only occurred on Testflight, it made debugging alot more challenging and time consuming as uploading a debug build had its own set of issues
Finally I added a bunch of logging, the one that was crucial was the CB-SDK debug entry (when a notification is received):
[
{
"subscription": {
"id": sub id,
"_id": "insert sub id",
"user_id": cube_user_id,
"bundle_identifier": "insert bundle id",
"client_identification_sequence": "insert client id",
"notification_channel_id": 6,
"udid": "insert-uuid",
"platform_id": 1,
"environment": "development",
"notification_channel": {
"name": "apns_voip"
},
"device": {
"udid": "insert-uuid",
"platform": {
"name": "ios"
}
}
}
}
]
specifically, the following entry.
environment": "development
This is due to APS used 2 different push notification environments, each with its own certificates (certificate is assigned to unique URL's where push notifications can come from). This, aps-environment
is set to 'production
(see on upload Archive screen right before you start uploading) but I'm receiving development
environment notifications - that needed fixing.
Reviewing my code, I finally found the issue (and fix mentioned above).