diff --git a/packages/firebase_app_installations/analysis_options.yaml b/packages/firebase_app_installations/analysis_options.yaml new file mode 100644 index 000000000000..23846daf2324 --- /dev/null +++ b/packages/firebase_app_installations/analysis_options.yaml @@ -0,0 +1,10 @@ +# Copyright 2026 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +include: ../../analysis_options.yaml + +analyzer: + exclude: + - firebase_app_installations_platform_interface/lib/src/pigeon/messages.pigeon.dart + - firebase_app_installations_platform_interface/pigeons/messages.dart diff --git a/packages/firebase_app_installations/firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/FirebaseInstallationsPlugin.kt b/packages/firebase_app_installations/firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/FirebaseInstallationsPlugin.kt index 0b55bf25178e..32a420de276d 100644 --- a/packages/firebase_app_installations/firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/FirebaseInstallationsPlugin.kt +++ b/packages/firebase_app_installations/firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/FirebaseInstallationsPlugin.kt @@ -12,128 +12,95 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugins.firebase.core.FlutterFirebasePlugin import io.flutter.plugins.firebase.core.FlutterFirebasePluginRegistry /** FirebaseInstallationsPlugin */ -class FirebaseInstallationsPlugin : FlutterFirebasePlugin, FlutterPlugin, MethodCallHandler { - private var channel: MethodChannel? = null +class FirebaseInstallationsPlugin : + FlutterFirebasePlugin, FlutterPlugin, FirebaseAppInstallationsHostApi { private var messenger: BinaryMessenger? = null private val streamHandlers = mutableMapOf() override fun onAttachedToEngine(binding: FlutterPluginBinding) { messenger = binding.binaryMessenger - channel = - MethodChannel(binding.binaryMessenger, METHOD_CHANNEL_NAME).also { - it.setMethodCallHandler(this) - } - + FirebaseAppInstallationsHostApi.setUp(binding.binaryMessenger, this) FlutterFirebasePluginRegistry.registerPlugin(METHOD_CHANNEL_NAME, this) } override fun onDetachedFromEngine(binding: FlutterPluginBinding) { - channel?.setMethodCallHandler(null) - channel = null + FirebaseAppInstallationsHostApi.setUp(binding.binaryMessenger, null) messenger = null removeEventListeners() } - private fun getInstallations(arguments: Map): FirebaseInstallations { - val appName = requireNotNull(arguments["appName"] as? String) + private fun getInstallations(appName: String): FirebaseInstallations { return FirebaseInstallations.getInstance(FirebaseApp.getInstance(appName)) } - private fun getId(arguments: Map): Task { - val taskCompletionSource = TaskCompletionSource() - + override fun delete(appName: String, callback: (Result) -> Unit) { FlutterFirebasePlugin.cachedThreadPool.execute { try { - taskCompletionSource.setResult(Tasks.await(getInstallations(arguments).id)) + Tasks.await(getInstallations(appName).delete()) + callback(Result.success(Unit)) } catch (exception: Exception) { - taskCompletionSource.setException(exception) + callback( + Result.failure( + FlutterError( + "firebase_app_installations", + exception.message, + getExceptionDetails(exception)))) } } - - return taskCompletionSource.task } - private fun getToken(arguments: Map): Task { - val taskCompletionSource = TaskCompletionSource() - + override fun getId(appName: String, callback: (Result) -> Unit) { FlutterFirebasePlugin.cachedThreadPool.execute { try { - val forceRefresh = requireNotNull(arguments["forceRefresh"] as? Boolean) - val tokenResult = Tasks.await(getInstallations(arguments).getToken(forceRefresh)) - taskCompletionSource.setResult(tokenResult.token) + callback(Result.success(Tasks.await(getInstallations(appName).id))) } catch (exception: Exception) { - taskCompletionSource.setException(exception) + callback( + Result.failure( + FlutterError( + "firebase_app_installations", + exception.message, + getExceptionDetails(exception)))) } } - - return taskCompletionSource.task } - private fun registerIdChangeListener(arguments: Map): Task { - val taskCompletionSource = TaskCompletionSource() - + override fun getToken( + appName: String, + forceRefresh: Boolean, + callback: (Result) -> Unit + ) { FlutterFirebasePlugin.cachedThreadPool.execute { try { - val appName = requireNotNull(arguments["appName"] as? String) - val handler = TokenChannelStreamHandler(getInstallations(arguments)) - val name = "$METHOD_CHANNEL_NAME/token/$appName" - val eventChannel = EventChannel(requireNotNull(messenger), name) - eventChannel.setStreamHandler(handler) - streamHandlers[eventChannel] = handler - taskCompletionSource.setResult(name) + val tokenResult = Tasks.await(getInstallations(appName).getToken(forceRefresh)) + callback(Result.success(tokenResult.token)) } catch (exception: Exception) { - taskCompletionSource.setException(exception) + callback( + Result.failure( + FlutterError( + "firebase_app_installations", + exception.message, + getExceptionDetails(exception)))) } } - - return taskCompletionSource.task } - private fun deleteId(arguments: Map): Task { - val taskCompletionSource = TaskCompletionSource() - - FlutterFirebasePlugin.cachedThreadPool.execute { - try { - Tasks.await(getInstallations(arguments).delete()) - taskCompletionSource.setResult(null) - } catch (exception: Exception) { - taskCompletionSource.setException(exception) - } - } - - return taskCompletionSource.task - } - - override fun onMethodCall(call: MethodCall, result: Result) { - val arguments = requireNotNull(call.arguments>()) - val methodCallTask = - when (call.method) { - "FirebaseInstallations#getId" -> getId(arguments) - "FirebaseInstallations#getToken" -> getToken(arguments) - "FirebaseInstallations#delete" -> deleteId(arguments) - "FirebaseInstallations#registerIdChangeListener" -> registerIdChangeListener(arguments) - else -> { - result.notImplemented() - return - } - } - - methodCallTask.addOnCompleteListener { task -> - if (task.isSuccessful) { - result.success(task.result) - } else { - val exception = task.exception - result.error( - "firebase_app_installations", exception?.message, getExceptionDetails(exception)) - } + override fun registerIdChangeListener(appName: String, callback: (Result) -> Unit) { + try { + val handler = TokenChannelStreamHandler(getInstallations(appName)) + val name = "$METHOD_CHANNEL_NAME/token/$appName" + val eventChannel = EventChannel(requireNotNull(messenger), name) + eventChannel.setStreamHandler(handler) + streamHandlers[eventChannel] = handler + callback(Result.success(name)) + } catch (exception: Exception) { + callback( + Result.failure( + FlutterError( + "firebase_app_installations", exception.message, getExceptionDetails(exception)))) } } diff --git a/packages/firebase_app_installations/firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/GeneratedAndroidFirebaseAppInstallations.g.kt b/packages/firebase_app_installations/firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/GeneratedAndroidFirebaseAppInstallations.g.kt new file mode 100644 index 000000000000..9576a1cee448 --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/GeneratedAndroidFirebaseAppInstallations.g.kt @@ -0,0 +1,182 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package io.flutter.plugins.firebase.installations.firebase_app_installations + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private object GeneratedAndroidFirebaseAppInstallationsPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf(exception.code, exception.message, exception.details) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) + } + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError( + val code: String, + override val message: String? = null, + val details: Any? = null +) : RuntimeException() + +private open class GeneratedAndroidFirebaseAppInstallationsPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return super.readValueOfType(type, buffer) + } + + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + super.writeValue(stream, value) + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface FirebaseAppInstallationsHostApi { + fun delete(appName: String, callback: (Result) -> Unit) + + fun getId(appName: String, callback: (Result) -> Unit) + + fun getToken(appName: String, forceRefresh: Boolean, callback: (Result) -> Unit) + + fun registerIdChangeListener(appName: String, callback: (Result) -> Unit) + + companion object { + /** The codec used by FirebaseAppInstallationsHostApi. */ + val codec: MessageCodec by lazy { GeneratedAndroidFirebaseAppInstallationsPigeonCodec() } + /** + * Sets up an instance of `FirebaseAppInstallationsHostApi` to handle messages through the + * `binaryMessenger`. + */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: FirebaseAppInstallationsHostApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.delete$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val appNameArg = args[0] as String + api.delete(appNameArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapError(error)) + } else { + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.getId$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val appNameArg = args[0] as String + api.getId(appNameArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.getToken$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val appNameArg = args[0] as String + val forceRefreshArg = args[1] as Boolean + api.getToken(appNameArg, forceRefreshArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.registerIdChangeListener$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val appNameArg = args[0] as String + api.registerIdChangeListener(appNameArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeneratedAndroidFirebaseAppInstallationsPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift b/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift new file mode 100644 index 000000000000..42ad7966b4ad --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift @@ -0,0 +1,201 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(Swift.type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +private class FirebaseAppInstallationsMessagesPigeonCodecReader: FlutterStandardReader {} + +private class FirebaseAppInstallationsMessagesPigeonCodecWriter: FlutterStandardWriter {} + +private class FirebaseAppInstallationsMessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + FirebaseAppInstallationsMessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + FirebaseAppInstallationsMessagesPigeonCodecWriter(data: data) + } +} + +class FirebaseAppInstallationsMessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable +{ + static let shared = FirebaseAppInstallationsMessagesPigeonCodec( + readerWriter: FirebaseAppInstallationsMessagesPigeonCodecReaderWriter() + ) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol FirebaseAppInstallationsHostApi { + func delete(appName: String, completion: @escaping (Result) -> Void) + func getId(appName: String, completion: @escaping (Result) -> Void) + func getToken( + appName: String, forceRefresh: Bool, + completion: @escaping (Result) -> Void) + func registerIdChangeListener( + appName: String, + completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class FirebaseAppInstallationsHostApiSetup { + static var codec: FlutterStandardMessageCodec { + FirebaseAppInstallationsMessagesPigeonCodec.shared + } + + /// Sets up an instance of `FirebaseAppInstallationsHostApi` to handle messages through the + /// `binaryMessenger`. + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: FirebaseAppInstallationsHostApi?, + messageChannelSuffix: String = "" + ) { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let deleteChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.delete\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec + ) + if let api { + deleteChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let appNameArg = args[0] as! String + api.delete(appName: appNameArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + deleteChannel.setMessageHandler(nil) + } + let getIdChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.getId\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec + ) + if let api { + getIdChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let appNameArg = args[0] as! String + api.getId(appName: appNameArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getIdChannel.setMessageHandler(nil) + } + let getTokenChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.getToken\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec + ) + if let api { + getTokenChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let appNameArg = args[0] as! String + let forceRefreshArg = args[1] as! Bool + api.getToken(appName: appNameArg, forceRefresh: forceRefreshArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getTokenChannel.setMessageHandler(nil) + } + let registerIdChangeListenerChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.registerIdChangeListener\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec + ) + if let api { + registerIdChangeListenerChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let appNameArg = args[0] as! String + api.registerIdChangeListener(appName: appNameArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + registerIdChangeListenerChannel.setMessageHandler(nil) + } + } +} diff --git a/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseInstallationsPlugin.swift b/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseInstallationsPlugin.swift index eb1d365a1f7e..7f3703a25177 100644 --- a/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseInstallationsPlugin.swift +++ b/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseInstallationsPlugin.swift @@ -18,8 +18,12 @@ import FirebaseInstallations let kFLTFirebaseInstallationsChannelName = "plugins.flutter.io/firebase_app_installations" -public class FirebaseInstallationsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin { - private var eventSink: FlutterEventSink? +// swift-format-ignore: AvoidRetroactiveConformances +extension FlutterError: @retroactive Error {} + +public class FirebaseInstallationsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin, + FirebaseAppInstallationsHostApi +{ private var messenger: FlutterBinaryMessenger private var streamHandler = [String: IdChangedStreamHandler?]() @@ -36,13 +40,9 @@ public class FirebaseInstallationsPlugin: NSObject, FLTFirebasePluginProtocol, F binaryMessenger = registrar.messenger() #endif - let channel = FlutterMethodChannel( - name: kFLTFirebaseInstallationsChannelName, - binaryMessenger: binaryMessenger - ) let instance = FirebaseInstallationsPlugin(messenger: binaryMessenger) FLTFirebasePluginRegistry.sharedInstance().register(instance) - registrar.addMethodCallDelegate(instance, channel: channel) + FirebaseAppInstallationsHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: instance) } public func firebaseLibraryVersion() -> String { @@ -66,96 +66,12 @@ public class FirebaseInstallationsPlugin: NSObject, FLTFirebasePluginProtocol, F } /// Gets Installations instance for a Firebase App. - /// - Returns: a Firebase Installations instance for the passed app from Dart private func getInstallations(appName: String) -> Installations { let app: FirebaseApp = FLTFirebasePlugin.firebaseAppNamed(appName)! return Installations.installations(app: app) } - /// Gets Installations Id for an instance. - /// - Parameter arguments: the arguments passed by the Dart calling method - /// - Parameter result: the result instance used to send the result to Dart. - /// - Parameter errorBlock: the error block used to send the error to Dart. - private func getId( - arguments: NSDictionary, result: @escaping FlutterResult, - errorBlock: @escaping FLTFirebaseMethodCallErrorBlock - ) { - let instance = getInstallations(appName: arguments["appName"] as! String) - instance.installationID { (id: String?, error: Error?) in - if let error { - errorBlock(nil, nil, nil, error) - } else { - result(id) - } - } - } - - /// Deletes the Installations Id for an instance. - /// - Parameter arguments: the arguments passed by the Dart calling method - /// - Parameter result: the result instance used to send the result to Dart. - /// - Parameter errorBlock: the error block used to send the error to Dart. - private func deleteId( - arguments: NSDictionary, result: @escaping FlutterResult, - errorBlock: @escaping FLTFirebaseMethodCallErrorBlock - ) { - let instance = getInstallations(appName: arguments["appName"] as! String) - instance.delete { (error: Error?) in - if let error { - errorBlock(nil, nil, nil, error) - } else { - result(nil) - } - } - } - - /// Gets the Auth Token for an instance. - /// - Parameter arguments: the arguments passed by the Dart calling method - /// - Parameter result: the result instance used to send the result to Dart. - /// - Parameter errorBlock: the error block used to send the error to Dart. - private func getToken( - arguments: NSDictionary, result: @escaping FlutterResult, - errorBlock: @escaping FLTFirebaseMethodCallErrorBlock - ) { - let instance = getInstallations(appName: arguments["appName"] as! String) - let forceRefresh = arguments["forceRefresh"] as? Bool ?? false - instance - .authTokenForcingRefresh(forceRefresh) { - ( - tokenResult: InstallationsAuthTokenResult?, - error: Error? - ) in - if let error { - errorBlock(nil, nil, nil, error) - } else { - result(tokenResult?.authToken) - } - } - } - - /// Registers a listener for changes in the Installations Id. - /// - Parameter arguments: the arguments passed by the Dart calling method - /// - Parameter result: the result instance used to send the result to Dart. - /// - Parameter errorBlock: the error block used to send the error to Dart. - private func registerIdChangeListener( - arguments: NSDictionary, result: @escaping FlutterResult, - errorBlock: @escaping FLTFirebaseMethodCallErrorBlock - ) { - let instance = getInstallations(appName: arguments["appName"] as! String) - let appName = arguments["appName"] as! String - let eventChannelName = kFLTFirebaseInstallationsChannelName + "/token/" + appName - - let eventChannel = FlutterEventChannel(name: eventChannelName, binaryMessenger: messenger) - - if streamHandler[eventChannelName] == nil { - streamHandler[eventChannelName] = IdChangedStreamHandler(instance: instance) - } - - eventChannel.setStreamHandler(streamHandler[eventChannelName]!) - - result(eventChannelName) - } - - private func mapInstallationsErrorCodes(code: UInt) -> NSString { + private func mapInstallationsErrorCodes(code: UInt) -> String { let error = InstallationsErrorCode( InstallationsErrorCode .Code(rawValue: Int(code)) ?? InstallationsErrorCode.unknown @@ -175,64 +91,97 @@ public class FirebaseInstallationsPlugin: NSObject, FLTFirebasePluginProtocol, F } } - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let args = call.arguments as? NSDictionary else { - result( - FlutterError( - code: "invalid-arguments", - message: "Arguments are not a dictionary", - details: nil + private func createFlutterError(_ error: Error) -> FlutterError { + let nsError = error as NSError + return FlutterError( + code: mapInstallationsErrorCodes(code: UInt(nsError.code)), + message: nsError.localizedDescription, + details: nil + ) + } + + public func delete( + appName: String, + completion: @escaping (Result) -> Void + ) { + let instance = getInstallations(appName: appName) + instance.delete { (error: Error?) in + if let error { + completion(.failure(self.createFlutterError(error))) + } else { + completion(.success(())) + } + } + } + + public func getId( + appName: String, + completion: @escaping (Result) -> Void + ) { + let instance = getInstallations(appName: appName) + instance.installationID { (id: String?, error: Error?) in + if let error { + completion(.failure(self.createFlutterError(error))) + } else if let id { + completion(.success(id)) + } else { + completion( + .failure( + FlutterError( + code: "unknown", + message: "Installation ID was nil", + details: nil + ) + ) ) - ) - return + } } + } - let errorBlock: FLTFirebaseMethodCallErrorBlock = { + public func getToken( + appName: String, + forceRefresh: Bool, + completion: @escaping (Result) -> Void + ) { + let instance = getInstallations(appName: appName) + instance.authTokenForcingRefresh(forceRefresh) { ( - code, message, details, + tokenResult: InstallationsAuthTokenResult?, error: Error? ) in - var errorDetails = [String: Any?]() - - errorDetails["code"] = - code - ?? self - .mapInstallationsErrorCodes(code: UInt((error! as NSError).code)) - errorDetails["message"] = - message ?? error? - .localizedDescription ?? "An unknown error has occurred." - errorDetails["additionalData"] = details - - if code == "unknown" { - NSLog( - "FLTFirebaseInstallations: An error occurred while calling method %@", - call.method + if let error { + completion(.failure(self.createFlutterError(error))) + } else if let token = tokenResult?.authToken { + completion(.success(token)) + } else { + completion( + .failure( + FlutterError( + code: "unknown", + message: "Installation token was nil", + details: nil + ) + ) ) } - - result( - FLTFirebasePlugin.createFlutterError( - fromCode: errorDetails["code"] as! String, - message: errorDetails["message"] as! String, - optionalDetails: errorDetails[ - "additionalData" - ] as? [AnyHashable: Any], - andOptionalNSError: error - ) - ) } + } - switch call.method { - case "FirebaseInstallations#getId": - getId(arguments: args, result: result, errorBlock: errorBlock) - case "FirebaseInstallations#delete": - deleteId(arguments: args, result: result, errorBlock: errorBlock) - case "FirebaseInstallations#getToken": - getToken(arguments: args, result: result, errorBlock: errorBlock) - case "FirebaseInstallations#registerIdChangeListener": - registerIdChangeListener(arguments: args, result: result, errorBlock: errorBlock) - default: - result(FlutterMethodNotImplemented) + public func registerIdChangeListener( + appName: String, + completion: @escaping (Result) -> Void + ) { + let instance = getInstallations(appName: appName) + let eventChannelName = kFLTFirebaseInstallationsChannelName + "/token/" + appName + + let eventChannel = FlutterEventChannel(name: eventChannelName, binaryMessenger: messenger) + + if streamHandler[eventChannelName] == nil { + streamHandler[eventChannelName] = IdChangedStreamHandler(instance: instance) } + + eventChannel.setStreamHandler(streamHandler[eventChannelName]!) + + completion(.success(eventChannelName)) } } diff --git a/packages/firebase_app_installations/firebase_app_installations/macos/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift b/packages/firebase_app_installations/firebase_app_installations/macos/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift new file mode 120000 index 000000000000..3992a0860aab --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations/macos/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift @@ -0,0 +1 @@ +../../../../ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift \ No newline at end of file diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/lib/src/method_channel/method_channel_firebase_app_installations.dart b/packages/firebase_app_installations/firebase_app_installations_platform_interface/lib/src/method_channel/method_channel_firebase_app_installations.dart index 8ab904811d42..b6286cb80325 100644 --- a/packages/firebase_app_installations/firebase_app_installations_platform_interface/lib/src/method_channel/method_channel_firebase_app_installations.dart +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/lib/src/method_channel/method_channel_firebase_app_installations.dart @@ -9,6 +9,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_app_installations_platform_interface/firebase_app_installations_platform_interface.dart'; import 'package:flutter/services.dart'; +import '../pigeon/messages.pigeon.dart'; import 'utils/exception.dart'; class MethodChannelFirebaseAppInstallations @@ -19,10 +20,8 @@ class MethodChannelFirebaseAppInstallations return MethodChannelFirebaseAppInstallations._(); } - /// The [MethodChannelFirebaseFunctions] method channel. - static const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/firebase_app_installations', - ); + static final FirebaseAppInstallationsHostApi _api = + FirebaseAppInstallationsHostApi(); static final Map> _idTokenChangesListeners = >{}; @@ -33,11 +32,8 @@ class MethodChannelFirebaseAppInstallations final controller = _idTokenChangesListeners[app.name] = StreamController.broadcast(); - channel.invokeMethod( - 'FirebaseInstallations#registerIdChangeListener', { - 'appName': app.name, - }).then((channelName) { - final events = EventChannel(channelName!, channel.codec); + _api.registerIdChangeListener(app.name).then((channelName) { + final events = EventChannel(channelName); events .receiveGuardedBroadcastStream(onError: convertPlatformException) @@ -45,6 +41,10 @@ class MethodChannelFirebaseAppInstallations (Object? arguments) => controller.add((arguments as Map)['token']), onError: controller.addError, ); + // ignore: avoid_catches_without_on_clauses + }).catchError((_) { + // Silently ignore errors during listener registration. + // This can happen in test environments where the host API is not set up. }); } @@ -62,9 +62,7 @@ class MethodChannelFirebaseAppInstallations @override Future delete() async { try { - await channel.invokeMethod('FirebaseInstallations#delete', { - 'appName': app!.name, - }); + await _api.delete(app!.name); } catch (e, s) { convertPlatformException(e, s); } @@ -73,12 +71,7 @@ class MethodChannelFirebaseAppInstallations @override Future getId() async { try { - final id = (await channel.invokeMethod( - 'FirebaseInstallations#getId', - {'appName': app!.name}, - ))!; - - return id; + return await _api.getId(app!.name); } catch (e, s) { convertPlatformException(e, s); } @@ -87,12 +80,7 @@ class MethodChannelFirebaseAppInstallations @override Future getToken(bool forceRefresh) async { try { - final id = (await channel.invokeMethod( - 'FirebaseInstallations#getToken', - {'appName': app!.name, 'forceRefresh': forceRefresh}, - ))!; - - return id; + return await _api.getToken(app!.name, forceRefresh); } catch (e, s) { convertPlatformException(e, s); } diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_app_installations/firebase_app_installations_platform_interface/lib/src/pigeon/messages.pigeon.dart new file mode 100644 index 000000000000..4b1471f70f57 --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -0,0 +1,154 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +class FirebaseAppInstallationsHostApi { + /// Constructor for [FirebaseAppInstallationsHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FirebaseAppInstallationsHostApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future delete(String appName) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.delete$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([appName]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future getId(String appName) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.getId$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([appName]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as String; + } + + Future getToken(String appName, bool forceRefresh) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.getToken$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([appName, forceRefresh]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as String; + } + + Future registerIdChangeListener(String appName) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi.registerIdChangeListener$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([appName]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as String; + } +} diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/pigeons/copyright.txt b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pigeons/copyright.txt new file mode 100644 index 000000000000..c507a9c1b050 --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2026, the Chromium project authors. Please see the AUTHORS file +for details. All rights reserved. Use of this source code is governed by a +BSD-style license that can be found in the LICENSE file. diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/pigeons/messages.dart b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pigeons/messages.dart new file mode 100644 index 000000000000..a0497ff0d9fc --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pigeons/messages.dart @@ -0,0 +1,35 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/pigeon/messages.pigeon.dart', + dartPackageName: 'firebase_app_installations_platform_interface', + kotlinOut: + '../firebase_app_installations/android/src/main/kotlin/io/flutter/plugins/firebase/installations/firebase_app_installations/GeneratedAndroidFirebaseAppInstallations.g.kt', + kotlinOptions: KotlinOptions( + package: + 'io.flutter.plugins.firebase.installations.firebase_app_installations', + ), + swiftOut: + '../firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/FirebaseAppInstallationsMessages.g.swift', + copyrightHeader: 'pigeons/copyright.txt', + ), +) +@HostApi() +abstract class FirebaseAppInstallationsHostApi { + @async + void delete(String appName); + + @async + String getId(String appName); + + @async + String getToken(String appName, bool forceRefresh); + + @async + String registerIdChangeListener(String appName); +} diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml index 4e62d9074c84..419aadfe1d7a 100644 --- a/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml @@ -22,3 +22,4 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 + pigeon: 26.3.4 diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/test/method_channel/method_channel_firebase_app_installations_test.dart b/packages/firebase_app_installations/firebase_app_installations_platform_interface/test/method_channel/method_channel_firebase_app_installations_test.dart new file mode 100644 index 000000000000..3f7f41f8664c --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/test/method_channel/method_channel_firebase_app_installations_test.dart @@ -0,0 +1,137 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_app_installations_platform_interface/src/method_channel/method_channel_firebase_app_installations.dart'; +import 'package:firebase_app_installations_platform_interface/src/pigeon/messages.pigeon.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../mock.dart'; + +const String _hostApiPrefix = + 'dev.flutter.pigeon.firebase_app_installations_platform_interface.FirebaseAppInstallationsHostApi'; + +void main() { + setupFirebaseAppInstallationsMocks(); + + late FirebaseApp app; + late MethodChannelFirebaseAppInstallations installations; + + String? lastDeleteAppName; + String? lastGetIdAppName; + String? lastGetTokenAppName; + bool? lastForceRefresh; + String? lastRegisterAppName; + + ByteData? encodeSuccess([Object? value]) { + return FirebaseAppInstallationsHostApi.pigeonChannelCodec.encodeMessage( + [value], + ); + } + + setUpAll(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('$_hostApiPrefix.registerIdChangeListener', ( + ByteData? message, + ) async { + final List args = + FirebaseAppInstallationsHostApi.pigeonChannelCodec.decodeMessage( + message, + ) as List; + lastRegisterAppName = args[0]! as String; + return encodeSuccess( + 'plugins.flutter.io/firebase_app_installations/token/$lastRegisterAppName', + ); + }); + + app = await Firebase.initializeApp(); + installations = MethodChannelFirebaseAppInstallations(app: app); + await Future.delayed(Duration.zero); + }); + + tearDownAll(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler( + '$_hostApiPrefix.registerIdChangeListener', + null, + ); + }); + + setUp(() { + lastDeleteAppName = null; + lastGetIdAppName = null; + lastGetTokenAppName = null; + lastForceRefresh = null; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('$_hostApiPrefix.delete', + (ByteData? message) async { + final List args = + FirebaseAppInstallationsHostApi.pigeonChannelCodec.decodeMessage( + message, + ) as List; + lastDeleteAppName = args[0]! as String; + return encodeSuccess(); + }); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('$_hostApiPrefix.getId', + (ByteData? message) async { + final List args = + FirebaseAppInstallationsHostApi.pigeonChannelCodec.decodeMessage( + message, + ) as List; + lastGetIdAppName = args[0]! as String; + return encodeSuccess('test-installation-id'); + }); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('$_hostApiPrefix.getToken', ( + ByteData? message, + ) async { + final List args = + FirebaseAppInstallationsHostApi.pigeonChannelCodec.decodeMessage( + message, + ) as List; + lastGetTokenAppName = args[0]! as String; + lastForceRefresh = args[1]! as bool; + return encodeSuccess('test-installation-token'); + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('$_hostApiPrefix.delete', null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('$_hostApiPrefix.getId', null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('$_hostApiPrefix.getToken', null); + }); + + test('delete forwards the app name', () async { + await installations.delete(); + + expect(lastDeleteAppName, app.name); + }); + + test('getId forwards the app name', () async { + final id = await installations.getId(); + + expect(lastGetIdAppName, app.name); + expect(id, 'test-installation-id'); + }); + + test('getToken forwards the app name and forceRefresh', () async { + final token = await installations.getToken(true); + + expect(lastGetTokenAppName, app.name); + expect(lastForceRefresh, isTrue); + expect(token, 'test-installation-token'); + }); + + test('registerIdChangeListener is invoked for onIdChange setup', () { + expect(lastRegisterAppName, app.name); + }); +} diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/test/mock.dart b/packages/firebase_app_installations/firebase_app_installations_platform_interface/test/mock.dart new file mode 100644 index 000000000000..b25a3dd7f5c8 --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/test/mock.dart @@ -0,0 +1,12 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_core_platform_interface/test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void setupFirebaseAppInstallationsMocks() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setupFirebaseCoreMocks(); +}