firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.52k stars 3.92k forks source link

[firebase_messaging] Data only message is handled background on Android, but not on iOS #2718

Closed leejh10003 closed 4 years ago

leejh10003 commented 4 years ago

Describe the bug I want to implement Propagate Remote Config updates in real time on flutter, and implemented it on Android. But on iOS, failed.

To Reproduce Steps to reproduce the behavior:

  1. Code like this:

pubspec.yaml

name: my_app_name
flutter:
  uses-material-design: true
  assets:
  - graphics/logo.png
  - graphics/placeholder.png
description: Dib mobile application
version: current_version+current_build_number
module:
  androidX: true
environment:
  sdk: ^2.4.0
  flutter: ^1.7.0

dependencies:
  flutter_inappwebview: ^2.1.0+1
  flutter_web_auth: ^0.1.3
  matrix_gesture_detector: ^0.1.0
  firebase_crashlytics: ^0.1.3
  flutter_markdown: ^0.3.3
  url_launcher: ^5.4.1
  package_info: ^0.4.0+13
  flutter_auth_buttons: ^0.8.0
  version: ^1.0.0
  flutter:
    sdk: flutter
  http: ^0.12.0+2
  share: ^0.6.3+5
  json_serializable: ^3.2.2
  firebase_core: ^0.4.0+9
  device_info: ^0.4.2+1
  firebase_analytics: ^5.0.2
  apple_sign_in: ^0.1.0
  firebase_auth: ^0.15.5+3
  graphql_flutter: ^3.0.1
  cupertino_icons: ^0.1.2
  iamport_flutter: ^0.9.9
  carousel_slider: ^1.3.0
  flutter_kakao_login: "^0.8.1"
  flutter_secure_storage: ^3.3.1+1
  firebase_dynamic_links: ^0.5.0+11
  firebase_remote_config: ^0.3.0+1
  flutter_webview_plugin: ^0.3.11
  autocomplete_textfield: ^1.6.4
  native_widgets: ^1.3.5
  flutter_typeahead: ^1.7.0
  webview_flutter: ^0.3.19+9
  badges: ^1.1.0
  tuple: ^1.0.3
  firebase_messaging: ^6.0.16
  shared_preferences: ^0.5.7+3
  flutter_datetime_picker: ^1.2.8
  loadmore: ^1.0.4
  validators: ^2.0.0+1

dev_dependencies:
  flutter_launcher_icons: "^0.7.3"
  pedantic: ^1.8.0
  icapps_license: ^0.0.6
flutter_icons:
  android: "launcher_icon" 
  ios: true
  image_path: "graphics/icon.png"

info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleDisplayName</key>
    <string>my_app_name</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>app</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>$(FLUTTER_BUILD_NAME)</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string><info_for_another_app></string>
            </array>
        </dict>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLName</key>
            <string><bundle_name></string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string><bundle_name></string>
            </array>
        </dict>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLName</key>
            <string>auth</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string><custom_url_scheme></string>
            </array>
        </dict>
    </array>
    <key>CFBundleVersion</key>
    <string>$(FLUTTER_BUILD_NUMBER)</string>
    <key>FirebaseAppDelegateProxyEnabled</key>
    <false/>
    <key>ANOTHER_APP_INFO</key>
    <string>my_app_info</string>
    <key>LSApplicationQueriesSchemes</key>
    <array>
        <INFOS_FROM_ANOTHER_APPS>
    </array>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
        <key>NSAllowsArbitraryLoadsInWebContent</key>
        <true/>
    </dict>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>This app does not access your location.</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>This app does not access your location.</string>
    <key>UIBackgroundModes</key>
    <array>
        <string>fetch</string>
        <string>remote-notification</string>
    </array>
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UIStatusBarStyle</key>
    <string>UIStatusBarStyleDarkContent</string>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UIViewControllerBasedStatusBarAppearance</key>
    <false/>
    <key>io.flutter.embedded_views_preview</key>
    <true/>
</dict>
</plist>

app.dart

import 'dart:async';
import 'dart:io' show Platform;
import 'tab/recommend/recommend.dart';
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'tab/comment/tab.dart';
import 'package:package_info/package_info.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:device_info/device_info.dart';
import 'package:shared_preferences/shared_preferences.dart';

Future<void> getAppConfigs() async {
  StaticObject.remoteConfig = await RemoteConfig.instance;
  PackageInfo packageInfo = await PackageInfo.fromPlatform();
  String version = packageInfo.version;
  String build = packageInfo.buildNumber;
  final defaults = <String, dynamic>{
    //default values
  };
  try{
    if (Platform.isAndroid){
    var instance = await SharedPreferences.getInstance();
    var needToUpdate = instance.getBool('need_to_update');
      if (needToUpdate == null || needToUpdate == true){
        await StaticObject.remoteConfig.fetch(expiration: const Duration(seconds: 1));
        await StaticObject.remoteConfig.activateFetched();
        var values = StaticObject.remoteConfig.getAll().map<String, dynamic>((key, value) => MapEntry(key, value.asString()));
        var toUpdate = jsonEncode(values);
        await instance.setString('default_config', toUpdate);
        await instance.setBool('need_to_update', false);
      }
    } else {
      await StaticObject.remoteConfig.fetch(expiration: const Duration(hours: 12));
      await StaticObject.remoteConfig.activateFetched();
    }
  } catch (err) {
    var instance = await SharedPreferences.getInstance();
    var cachedConfig = instance.getString('default_config');
    if (cachedConfig != null){
      await StaticObject.remoteConfig.setDefaults(jsonDecode(cachedConfig));
    } else {
      await StaticObject.remoteConfig.setDefaults(defaults);
    }
  }
}

class LoginCheck extends StatefulWidget{
  @override
  LoginCheckState createState() => LoginCheckState();
}
class LoginCheckState extends State<LoginCheck> 
    with SingleTickerProviderStateMixin {
  @override
  initState() {
    super.initState();
    TabsContext.controller = TabController(vsync: this, initialIndex: 0, length: 4);
  }
  final storage = new FlutterSecureStorage();
  Future<dynamic> keyCheck() async {
    String refreshToken = await storage.read(key: "refreshToken");
    if (refreshToken != null) {
      dynamic refreshInfoRetrieved = await http.post(
          StaticObject.remoteConfig.getString('rest_api_endpoint') + '/jwt/app/refresh',
          body: {"refreshToken": refreshToken});
      dynamic result = jsonDecode(refreshInfoRetrieved.body);
      if (result['success'] == false) {
        await storage.delete(key: 'refreshToken');
        GraphQlObject.isAnonymous = true;
        GraphQlObject.accessToken = null;
        return false;
      } else {
        String accessToken = jsonDecode(refreshInfoRetrieved.body)['token'];
        await Future.delayed(Duration(seconds: 1));
        Map<String, dynamic> jwt = parseJwt(refreshToken);
        var userId = jwt['userId'] is String ? jwt['userId'] : "${jwt['userId']}";
        DDIBFirebaseAnalytics.analytics.setUserId(userId);
        GraphQlObject.isAnonymous = false;
        return accessToken;
      }
    } else {
      GraphQlObject.isAnonymous = true;
      return false;
    }
  }

  @override
  Widget build(BuildContext context) {
    //build logic
  }
class Tabs extends StatefulWidget {
  @override
  TabsPushState createState() => TabsPushState();
}
Future<dynamic> onBackgroundMessage(Map<String, dynamic> message) async {
  print("$message");
  if ((Platform.isAndroid && message['data']['remote_config_update'] == 'true')){
    var instance = await SharedPreferences.getInstance();
    await instance.setBool('need_to_update', true);
  }
  return Future<void>.value();
}
class TabsPushState extends State<Tabs> with WidgetsBindingObserver{
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
  int _selectedIndex;
  void iOSPermission() {
    _firebaseMessaging.requestNotificationPermissions(
        IosNotificationSettings(sound: true, badge: true, alert: true, provisional: false)
    );
    _firebaseMessaging.onIosSettingsRegistered
        .listen((IosNotificationSettings settings)
    {
    });
  }
  @override
  initState() {
    super.initState();
    if (Platform.isIOS) iOSPermission();
    if (GraphQlObject.isAnonymous == false){
      _firebaseMessaging.getToken().then((token){
        var client = GraphQLProvider.of(context).value;
        FlutterSecureStorage storage = new FlutterSecureStorage();
        return storage.read(key: 'refreshToken').then((refreshToken){
          return client.mutate(MutationOptions(
            variables: {
              "userId": parseJwt(refreshToken)['userId'],
              "token": token
            },
            documentNode: gql(StaticObject.remoteConfig.getString('update_refresh_token'))
          ));
        });
      }).then((response){
            _firebaseMessaging.configure(
              onMessage: (message) async {
                print("onMessage $message");
                if ((Platform.isAndroid && message['data']['remote_config_update'] == 'true') || (Platform.isIOS && message['remote_config_update'] == 'true')){
                  var instance = await SharedPreferences.getInstance();
                  await instance.setBool('need_to_update', true);
                  await getAppConfigs();
                }
              },
              onLaunch: (message) async {
                print("onMessage $message");
                if ((Platform.isAndroid && message['data']['remote_config_update'] == 'true') || (Platform.isIOS && message['remote_config_update'] == 'true')){
                  var instance = await SharedPreferences.getInstance();
                  await instance.setBool('need_to_update', true);
                  await getAppConfigs();
                }
              },
              onResume: (message) async {
                print("onMessage $message");
                if ((Platform.isAndroid && message['data']['remote_config_update'] == 'true') || (Platform.isIOS && message['remote_config_update'] == 'true')){
                  var instance = await SharedPreferences.getInstance();
                  await instance.setBool('need_to_update', true);
                  await getAppConfigs();
                }
              },
              onBackgroundMessage: Platform.isAndroid ? onBackgroundMessage : null
            );
            _firebaseMessaging.requestNotificationPermissions(
                const IosNotificationSettings(sound: true, badge: true, alert: true));
            _firebaseMessaging.onIosSettingsRegistered.listen((settings) {});
            _firebaseMessaging.subscribeToTopic('PUSH_RC_UPDATE').then((result){
              print("topic subscribed");
            });
      });
    }
    setState(() {
      _selectedIndex = 0;
    });
  }
  @override
  Widget build(BuildContext context) {
    //build logic
  }
}

on server side, sent message with this node.js code

admin.messaging().send({
    data:{
        click_action: 'FLUTTER_NOTIFICATION_CLICK',
        remote_config_update: 'true'
    },
    topic: 'PUSH_RC_UPDATE'
})

Expected behavior onBackgroundMessageHandler to be called when data message sent to Android when app is in background, and onMessage to be called when app comes back from background or terminated to foreground as described here

Additional context Add any other context about the problem here.

Flutter doctor Run flutter doctor and paste the output below:

[✓] Flutter (Channel stable, v1.17.3, on Mac OS X 10.15.5 19F101, locale en-KR)
    • Flutter version 1.17.3 at /Users/leejunhyuk/Tools/flutter
    • Framework revision b041144f83 (3 days ago), 2020-06-04 09:26:11 -0700
    • Engine revision ee76268252
    • Dart version 2.8.4

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/leejunhyuk/Library/Android/sdk
    • Platform android-29, build-tools 29.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.5)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.5, Build version 11E608c
    • CocoaPods version 1.9.1

[✓] Android Studio (version 4.0)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 46.0.2
    • Dart plugin version 193.7361
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)

[✓] VS Code (version 1.45.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.11.0

[✓] Connected device (2 available)
    • SM G920S    • 06157df671de8425          • android-arm64 • Android 7.0 (API 24)
    • 이준혁의 iPhone • 00008020-001619AE1A81002E • ios           • iOS 13.5.1
leejh10003 commented 4 years ago

After deleting this swift native code from AppDelegate.application function, it works.

  if #available(iOS 10.0, *) {
    UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
  }

Now my AppDelegate.swift looks like this:

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}