Home > Blockchain >  Flutter - firebase FCM messages not working on Testflight release builds at all
Flutter - firebase FCM messages not working on Testflight release builds at all

Time:10-18

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).

  • Related