Flutter - firebase FCM messages not working on Testflight release builds at all
Asked Answered
K

4

4

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.

Knorring answered 16/10, 2021 at 3:48 Comment(6)
Are you sure you're doing an actual release build?. Release build and archive are not the same thing (necessarily) depending on how you structure your builds. Debug doesn't use the same certs as release. Debug uses APNS_SANDBOX instead of APNS. Might even be a misconfiguration with your server or on firebase.Giuseppinagiustina
To be honest I don't know flutter. I only know Push notifications. I don't know what flutter does to the build schemes or configurationsGiuseppinagiustina
But most of what you need to do for PN is on FB and on the server. iOS is only one small piece of the whole PN puzzle. It's actually less complicated on iOS than it is on your BE or on FB. FB has to be configured with the correct certifications (or recently api keys) and Be has to send the pn request to the correct FB app for it to all work.Giuseppinagiustina
Keep android out of the picture. how PN work for android is way different than iOS. in iOS you need specific certs for debug and release and any other build configuration you may have. having PN working locally doesn't mean that firebase is setup correctly. you need to go through your setup on apple developer and on firebase and make sure that everything matches your builds that you ship to TF.Giuseppinagiustina
I’m talking about push notification certificates. They are not managed by Xcode. There are also api keys which are recent. You might be using those instead. Fb has to be configured to send PN to the correct certificate/api key and for APNS. Also make sure your app asks for access to PN properly. If your app doesn’t ask the user to allow push notifications the problem is on iOS. Usually this is done on launch but can also be done at any time such as login.Giuseppinagiustina
Let us continue this discussion in chat.Giuseppinagiustina
K
2

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

Knorring answered 17/10, 2021 at 22:7 Comment(0)
S
1

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: "%02.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

Stenographer answered 16/10, 2021 at 20:45 Comment(2)
Point of clarify on #2 - how will one set this. Are you referring to *.entitlements file, specifically set aps-environment to production?Knorring
aps-environment is set to production, I added the setAPNSToken() function after the apnsToken = deviceToken, this didn't help. Re #1, only 1 APS certificate exists referring to the correct package name (checked on developer site).Knorring
A
0

I tried everything but what worked for me was discovering that App Flyer SDK was preventing my push notifications from coming through. Removing this from pubspec solved it for me. End of a 30 hour debugging journey.

Asparagine answered 31/10, 2022 at 2:18 Comment(0)
I
0

Just solved it.

The fix required 'aps-environment' to be set as 'production' in 'Runner.entitlements' file. During Development / Test, it was set as 'development'.

Please note - TestFlight is considered as 'production'.

enter image description here

Inauspicious answered 30/5, 2023 at 21:43 Comment(2)
In my case, I was using ConnectyCube. They had a flag to set the production type in the configuration, which I did not catch till later. Deploying using testflight ignores the entitlements i.e. it is overriden to 'production' (see this on dialog before uploading archive). You do not have to touch aps-environment for notifications to work in any capacity.Knorring
This did not workFuge

© 2022 - 2024 — McMap. All rights reserved.