diff --git a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt index 99aab95ea256..f390a7d472f3 100644 --- a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt +++ b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt @@ -93,6 +93,24 @@ class FirebaseAppCheckPlugin : FlutterFirebasePlugin, FlutterPlugin, FirebaseApp } } + override fun getTokenResult( + appName: String, + forceRefresh: Boolean, + callback: (Result) -> Unit + ) { + val firebaseAppCheck = getAppCheck(appName) + firebaseAppCheck.getAppCheckToken(forceRefresh).addOnCompleteListener { task -> + if (task.isSuccessful) { + val token = task.result + callback( + Result.success( + token?.let { InternalAppCheckTokenResult(it.token, it.expireTimeMillis) })) + } else { + callback(Result.failure(FlutterError("firebase_app_check", task.exception?.message, null))) + } + } + } + override fun setTokenAutoRefreshEnabled( appName: String, isTokenAutoRefreshEnabled: Boolean, diff --git a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt index 4cd7a39bc1b4..f1162a5cafa7 100644 --- a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt +++ b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt @@ -31,6 +31,150 @@ private object GeneratedAndroidFirebaseAppCheckPigeonUtils { "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) } } + + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true + } + if (a is Array<*> && b is Array<*>) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true + } + if (a is List<*> && b is List<*>) { + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true + } + if (a is Map<*, *> && b is Map<*, *>) { + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false + } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) + } + return a == b + } + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } } /** @@ -46,13 +190,63 @@ class FlutterError( val details: Any? = null ) : RuntimeException() +/** Generated class from Pigeon that represents data sent in messages. */ +data class InternalAppCheckTokenResult(val token: String, val expirationTimestamp: Long? = null) { + companion object { + fun fromList(pigeonVar_list: List): InternalAppCheckTokenResult { + val token = pigeonVar_list[0] as String + val expirationTimestamp = pigeonVar_list[1] as Long? + return InternalAppCheckTokenResult(token, expirationTimestamp) + } + } + + fun toList(): List { + return listOf( + token, + expirationTimestamp, + ) + } + + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as InternalAppCheckTokenResult + return GeneratedAndroidFirebaseAppCheckPigeonUtils.deepEquals(this.token, other.token) && + GeneratedAndroidFirebaseAppCheckPigeonUtils.deepEquals( + this.expirationTimestamp, other.expirationTimestamp) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + GeneratedAndroidFirebaseAppCheckPigeonUtils.deepHash(this.token) + result = + 31 * result + GeneratedAndroidFirebaseAppCheckPigeonUtils.deepHash(this.expirationTimestamp) + return result + } +} + private open class GeneratedAndroidFirebaseAppCheckPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { InternalAppCheckTokenResult.fromList(it) } + } + else -> super.readValueOfType(type, buffer) + } } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) + when (value) { + is InternalAppCheckTokenResult -> { + stream.write(129) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } } } @@ -68,6 +262,12 @@ interface FirebaseAppCheckHostApi { fun getToken(appName: String, forceRefresh: Boolean, callback: (Result) -> Unit) + fun getTokenResult( + appName: String, + forceRefresh: Boolean, + callback: (Result) -> Unit + ) + fun setTokenAutoRefreshEnabled( appName: String, isTokenAutoRefreshEnabled: Boolean, @@ -145,6 +345,32 @@ interface FirebaseAppCheckHostApi { channel.setMessageHandler(null) } } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getTokenResult$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.getTokenResult(appNameArg, forceRefreshArg) { + result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedAndroidFirebaseAppCheckPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeneratedAndroidFirebaseAppCheckPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel( diff --git a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift index d8f3a100c89b..e3b063889660 100644 --- a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift +++ b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift @@ -66,9 +66,177 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } -private class FirebaseAppCheckMessagesPigeonCodecReader: FlutterStandardReader {} +private func doubleEqualsFirebaseAppCheckMessages(_ lhs: Double, _ rhs: Double) -> Bool { + (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashFirebaseAppCheckMessages(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8_0000_0000_0000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + +func deepEqualsFirebaseAppCheckMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + + case is (Void, Void): + return true + + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsFirebaseAppCheckMessages(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsFirebaseAppCheckMessages(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsFirebaseAppCheckMessages(lhsKey, rhsKey) { + if deepEqualsFirebaseAppCheckMessages(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true -private class FirebaseAppCheckMessagesPigeonCodecWriter: FlutterStandardWriter {} + case (let lhs as Double, let rhs as Double): + return doubleEqualsFirebaseAppCheckMessages(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + + default: + return false + } +} + +func deepHashFirebaseAppCheckMessages(value: Any?, hasher: inout Hasher) { + let cleanValue = nilOrValue(value) as Any? + if let cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashFirebaseAppCheckMessages(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashFirebaseAppCheckMessages(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashFirebaseAppCheckMessages(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashFirebaseAppCheckMessages(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashFirebaseAppCheckMessages(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) + } + } else { + hasher.combine(0) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct InternalAppCheckTokenResult: Hashable { + var token: String + var expirationTimestamp: Int64? + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> InternalAppCheckTokenResult? { + let token = pigeonVar_list[0] as! String + let expirationTimestamp: Int64? = nilOrValue(pigeonVar_list[1]) + + return InternalAppCheckTokenResult( + token: token, + expirationTimestamp: expirationTimestamp + ) + } + + func toList() -> [Any?] { + [ + token, + expirationTimestamp, + ] + } + + static func == (lhs: InternalAppCheckTokenResult, rhs: InternalAppCheckTokenResult) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsFirebaseAppCheckMessages(lhs.token, rhs.token) + && deepEqualsFirebaseAppCheckMessages( + lhs.expirationTimestamp, + rhs.expirationTimestamp + ) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("InternalAppCheckTokenResult") + deepHashFirebaseAppCheckMessages(value: token, hasher: &hasher) + deepHashFirebaseAppCheckMessages(value: expirationTimestamp, hasher: &hasher) + } +} + +private class FirebaseAppCheckMessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return InternalAppCheckTokenResult.fromList(readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class FirebaseAppCheckMessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? InternalAppCheckTokenResult { + super.writeByte(129) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} private class FirebaseAppCheckMessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { override func reader(with data: Data) -> FlutterStandardReader { @@ -81,20 +249,23 @@ private class FirebaseAppCheckMessagesPigeonCodecReaderWriter: FlutterStandardRe } class FirebaseAppCheckMessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = FirebaseAppCheckMessagesPigeonCodec( - readerWriter: FirebaseAppCheckMessagesPigeonCodecReaderWriter() - ) + static let shared = + FirebaseAppCheckMessagesPigeonCodec( + readerWriter: FirebaseAppCheckMessagesPigeonCodecReaderWriter() + ) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol FirebaseAppCheckHostApi { func activate( appName: String, androidProvider: String?, appleProvider: String?, - debugToken: String?, - completion: @escaping (Result) -> Void) + debugToken: String?, completion: @escaping (Result) -> Void) func getToken( appName: String, forceRefresh: Bool, completion: @escaping (Result) -> Void) + func getTokenResult( + appName: String, forceRefresh: Bool, + completion: @escaping (Result) -> Void) func setTokenAutoRefreshEnabled( appName: String, isTokenAutoRefreshEnabled: Bool, completion: @escaping (Result) -> Void) @@ -120,7 +291,8 @@ class FirebaseAppCheckHostApiSetup { let activateChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + binaryMessenger: binaryMessenger, + codec: codec ) if let api { activateChannel.setMessageHandler { message, reply in @@ -130,7 +302,9 @@ class FirebaseAppCheckHostApiSetup { let appleProviderArg: String? = nilOrValue(args[2]) let debugTokenArg: String? = nilOrValue(args[3]) api.activate( - appName: appNameArg, androidProvider: androidProviderArg, appleProvider: appleProviderArg, + appName: appNameArg, + androidProvider: androidProviderArg, + appleProvider: appleProviderArg, debugToken: debugTokenArg ) { result in switch result { @@ -147,7 +321,8 @@ class FirebaseAppCheckHostApiSetup { let getTokenChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getToken\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + binaryMessenger: binaryMessenger, + codec: codec ) if let api { getTokenChannel.setMessageHandler { message, reply in @@ -166,10 +341,34 @@ class FirebaseAppCheckHostApiSetup { } else { getTokenChannel.setMessageHandler(nil) } + let getTokenResultChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getTokenResult\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + getTokenResultChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let appNameArg = args[0] as! String + let forceRefreshArg = args[1] as! Bool + api.getTokenResult(appName: appNameArg, forceRefresh: forceRefreshArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getTokenResultChannel.setMessageHandler(nil) + } let setTokenAutoRefreshEnabledChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.setTokenAutoRefreshEnabled\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + binaryMessenger: binaryMessenger, + codec: codec ) if let api { setTokenAutoRefreshEnabledChannel.setMessageHandler { message, reply in @@ -177,7 +376,8 @@ class FirebaseAppCheckHostApiSetup { let appNameArg = args[0] as! String let isTokenAutoRefreshEnabledArg = args[1] as! Bool api.setTokenAutoRefreshEnabled( - appName: appNameArg, isTokenAutoRefreshEnabled: isTokenAutoRefreshEnabledArg + appName: appNameArg, + isTokenAutoRefreshEnabled: isTokenAutoRefreshEnabledArg ) { result in switch result { case .success: @@ -193,7 +393,8 @@ class FirebaseAppCheckHostApiSetup { let registerTokenListenerChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.registerTokenListener\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + binaryMessenger: binaryMessenger, + codec: codec ) if let api { registerTokenListenerChannel.setMessageHandler { message, reply in @@ -214,7 +415,8 @@ class FirebaseAppCheckHostApiSetup { let getLimitedUseAppCheckTokenChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getLimitedUseAppCheckToken\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + binaryMessenger: binaryMessenger, + codec: codec ) if let api { getLimitedUseAppCheckTokenChannel.setMessageHandler { message, reply in diff --git a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift index 2d63a7652e18..898906d6f90a 100644 --- a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift +++ b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift @@ -114,6 +114,41 @@ public class FirebaseAppCheckPlugin: NSObject, FlutterPlugin, } } + func getTokenResult( + appName: String, forceRefresh: Bool, + completion: @escaping (Result) -> Void + ) { + guard let app = FLTFirebasePlugin.firebaseAppNamed(appName), + let appCheck = AppCheck.appCheck(app: app) + else { + completion( + .failure( + FlutterError( + code: "unknown", message: "App Check not available for app: \(appName)", details: nil + ) + ) + ) + return + } + + appCheck.token(forcingRefresh: forceRefresh) { token, error in + if let error { + completion(.failure(self.createFlutterError(error))) + } else { + completion( + .success( + token.map { + InternalAppCheckTokenResult( + token: $0.token, + expirationTimestamp: Int64($0.expirationDate.timeIntervalSince1970 * 1000) + ) + } + ) + ) + } + } + } + func setTokenAutoRefreshEnabled( appName: String, isTokenAutoRefreshEnabled: Bool, completion: @escaping (Result) -> Void diff --git a/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart index 5e6a8cc98b81..83b47d6af96f 100644 --- a/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart @@ -9,6 +9,7 @@ import 'package:firebase_core_platform_interface/firebase_core_platform_interfac export 'package:firebase_app_check_platform_interface/firebase_app_check_platform_interface.dart' show + AppCheckTokenResult, AndroidProvider, AndroidAppCheckProvider, AndroidDebugProvider, diff --git a/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart index 0553fa116e03..2cc14f2e9d33 100644 --- a/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart @@ -133,6 +133,17 @@ class FirebaseAppCheck extends FirebasePlugin implements FirebaseService { return _delegate.getToken(forceRefresh ?? false); } + /// Get the current App Check token and its associated metadata. + /// + /// Attaches to the most recent in-flight request if one is present. Returns + /// null if no token is present and no token requests are in-flight. + /// + /// If `forceRefresh` is true, will always try to fetch a fresh token. If + /// false, will use a cached token if found in storage. + Future getTokenResult([bool? forceRefresh]) { + return _delegate.getTokenResult(forceRefresh ?? false); + } + /// If true, the SDK automatically refreshes App Check tokens as needed. Future setTokenAutoRefreshEnabled(bool isTokenAutoRefreshEnabled) { return _delegate.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled); diff --git a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp index d9a0a72d7014..9fa87de94200 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp +++ b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp @@ -200,6 +200,30 @@ void FirebaseAppCheckPlugin::GetToken( }); } +void FirebaseAppCheckPlugin::GetTokenResult( + const std::string& app_name, bool force_refresh, + std::function< + void(ErrorOr> reply)> + result) { + AppCheck* app_check = GetAppCheckFromPigeon(app_name); + + Future future = app_check->GetAppCheckToken(force_refresh); + future.OnCompletion([result](const Future& completed_future) { + if (completed_future.error() != 0) { + result(ParseError(completed_future)); + } else { + const AppCheckToken* token = completed_future.result(); + if (token) { + int64_t expiration_timestamp = token->expire_time_millis; + result(std::optional( + InternalAppCheckTokenResult(token->token, &expiration_timestamp))); + } else { + result(std::optional(std::nullopt)); + } + } + }); +} + void FirebaseAppCheckPlugin::SetTokenAutoRefreshEnabled( const std::string& app_name, bool is_token_auto_refresh_enabled, std::function reply)> result) { diff --git a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h index baabf2bd5931..b2ca0c4319b6 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h +++ b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h @@ -47,6 +47,11 @@ class FirebaseAppCheckPlugin : public flutter::Plugin, void GetToken(const std::string& app_name, bool force_refresh, std::function> reply)> result) override; + void GetTokenResult( + const std::string& app_name, bool force_refresh, + std::function< + void(ErrorOr> reply)> + result) override; void SetTokenAutoRefreshEnabled( const std::string& app_name, bool is_token_auto_refresh_enabled, std::function reply)> result) override; diff --git a/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp b/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp index 0da3e3c1ded5..92bc0a0b28eb 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp +++ b/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp @@ -239,16 +239,110 @@ size_t PigeonInternalDeepHash(const ::flutter::EncodableValue& v) { } } // namespace +// InternalAppCheckTokenResult + +InternalAppCheckTokenResult::InternalAppCheckTokenResult( + const std::string& token) + : token_(token) {} + +InternalAppCheckTokenResult::InternalAppCheckTokenResult( + const std::string& token, const int64_t* expiration_timestamp) + : token_(token), + expiration_timestamp_(expiration_timestamp + ? std::optional(*expiration_timestamp) + : std::nullopt) {} + +const std::string& InternalAppCheckTokenResult::token() const { return token_; } + +void InternalAppCheckTokenResult::set_token(std::string_view value_arg) { + token_ = value_arg; +} + +const int64_t* InternalAppCheckTokenResult::expiration_timestamp() const { + return expiration_timestamp_ ? &(*expiration_timestamp_) : nullptr; +} + +void InternalAppCheckTokenResult::set_expiration_timestamp( + const int64_t* value_arg) { + expiration_timestamp_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void InternalAppCheckTokenResult::set_expiration_timestamp(int64_t value_arg) { + expiration_timestamp_ = value_arg; +} + +EncodableList InternalAppCheckTokenResult::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(token_)); + list.push_back(expiration_timestamp_ ? EncodableValue(*expiration_timestamp_) + : EncodableValue()); + return list; +} + +InternalAppCheckTokenResult InternalAppCheckTokenResult::FromEncodableList( + const EncodableList& list) { + InternalAppCheckTokenResult decoded(std::get(list[0])); + auto& encodable_expiration_timestamp = list[1]; + if (!encodable_expiration_timestamp.IsNull()) { + decoded.set_expiration_timestamp( + std::get(encodable_expiration_timestamp)); + } + return decoded; +} + +bool InternalAppCheckTokenResult::operator==( + const InternalAppCheckTokenResult& other) const { + return PigeonInternalDeepEquals(token_, other.token_) && + PigeonInternalDeepEquals(expiration_timestamp_, + other.expiration_timestamp_); +} + +bool InternalAppCheckTokenResult::operator!=( + const InternalAppCheckTokenResult& other) const { + return !(*this == other); +} + +size_t InternalAppCheckTokenResult::Hash() const { + size_t result = 1; + result = result * 31 + PigeonInternalDeepHash(token_); + result = result * 31 + PigeonInternalDeepHash(expiration_timestamp_); + return result; +} + +size_t PigeonInternalDeepHash(const InternalAppCheckTokenResult& v) { + return v.Hash(); +} PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( uint8_t type, ::flutter::ByteStreamReader* stream) const { - return ::flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + switch (type) { + case 129: { + return CustomEncodableValue( + InternalAppCheckTokenResult::FromEncodableList( + std::get(ReadValue(stream)))); + } + default: + return ::flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } } void PigeonInternalCodecSerializer::WriteValue( const EncodableValue& value, ::flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(InternalAppCheckTokenResult)) { + stream->WriteByte(129); + WriteValue(EncodableValue( + std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + } ::flutter::StandardCodecSerializer::WriteValue(value, stream); } @@ -373,6 +467,59 @@ void FirebaseAppCheckHostApi::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_app_check_platform_interface." + "FirebaseAppCheckHostApi.getTokenResult" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const ::flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_name_arg = args.at(0); + if (encodable_app_name_arg.IsNull()) { + reply(WrapError("app_name_arg unexpectedly null.")); + return; + } + const auto& app_name_arg = + std::get(encodable_app_name_arg); + const auto& encodable_force_refresh_arg = args.at(1); + if (encodable_force_refresh_arg.IsNull()) { + reply(WrapError("force_refresh_arg unexpectedly null.")); + return; + } + const auto& force_refresh_arg = + std::get(encodable_force_refresh_arg); + api->GetTokenResult( + app_name_arg, force_refresh_arg, + [reply](ErrorOr>&& + output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + auto output_optional = std::move(output).TakeValue(); + if (output_optional) { + wrapped.push_back(CustomEncodableValue( + std::move(output_optional).value())); + } else { + wrapped.push_back(EncodableValue()); + } + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } { BasicMessageChannel<> channel( binary_messenger, diff --git a/packages/firebase_app_check/firebase_app_check/windows/messages.g.h b/packages/firebase_app_check/firebase_app_check/windows/messages.g.h index 50ae482963dc..143daacf98c8 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/messages.g.h +++ b/packages/firebase_app_check/firebase_app_check/windows/messages.g.h @@ -58,6 +58,39 @@ class ErrorOr { std::variant v_; }; +// Generated class from Pigeon that represents data sent in messages. +class InternalAppCheckTokenResult { + public: + // Constructs an object setting all non-nullable fields. + explicit InternalAppCheckTokenResult(const std::string& token); + + // Constructs an object setting all fields. + explicit InternalAppCheckTokenResult(const std::string& token, + const int64_t* expiration_timestamp); + + const std::string& token() const; + void set_token(std::string_view value_arg); + + const int64_t* expiration_timestamp() const; + void set_expiration_timestamp(const int64_t* value_arg); + void set_expiration_timestamp(int64_t value_arg); + + bool operator==(const InternalAppCheckTokenResult& other) const; + bool operator!=(const InternalAppCheckTokenResult& other) const; + /// Returns a hash code value for the object. This method is supported for the + /// benefit of hash tables. + size_t Hash() const; + + private: + static InternalAppCheckTokenResult FromEncodableList( + const ::flutter::EncodableList& list); + ::flutter::EncodableList ToEncodableList() const; + friend class FirebaseAppCheckHostApi; + friend class PigeonInternalCodecSerializer; + std::string token_; + std::optional expiration_timestamp_; +}; + class PigeonInternalCodecSerializer : public ::flutter::StandardCodecSerializer { public: @@ -90,6 +123,11 @@ class FirebaseAppCheckHostApi { const std::string& app_name, bool force_refresh, std::function> reply)> result) = 0; + virtual void GetTokenResult( + const std::string& app_name, bool force_refresh, + std::function< + void(ErrorOr> reply)> + result) = 0; virtual void SetTokenAutoRefreshEnabled( const std::string& app_name, bool is_token_auto_refresh_enabled, std::function reply)> result) = 0; diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart index 53a285e48729..18a23f08cdba 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart @@ -5,6 +5,7 @@ export 'src/android_provider.dart'; export 'src/android_providers.dart'; +export 'src/app_check_token_result.dart'; export 'src/apple_provider.dart'; export 'src/apple_providers.dart'; export 'src/method_channel/method_channel_firebase_app_check.dart'; diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/app_check_token_result.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/app_check_token_result.dart new file mode 100644 index 000000000000..bbe2a05dbe89 --- /dev/null +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/app_check_token_result.dart @@ -0,0 +1,26 @@ +// 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. + +/// An App Check token and its associated metadata. +class AppCheckTokenResult { + /// Creates an App Check token result. + const AppCheckTokenResult({ + required this.token, + this.expirationTime, + }); + + /// The App Check token JWT string. + final String token; + + /// The time when the App Check token expires. + /// + /// This is `null` on platforms whose native SDK does not expose token + /// expiration metadata. + final DateTime? expirationTime; + + @override + String toString() { + return '$AppCheckTokenResult(token: $token, expirationTime: $expirationTime)'; + } +} diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart index b7f2d035d212..e367cb95a023 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart @@ -148,6 +148,25 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { } } + @override + Future getTokenResult(bool forceRefresh) async { + try { + final result = await _pigeonApi.getTokenResult(app.name, forceRefresh); + if (result == null) { + return null; + } + + return AppCheckTokenResult( + token: result.token, + expirationTime: result.expirationTimestamp == null + ? null + : DateTime.fromMillisecondsSinceEpoch(result.expirationTimestamp!), + ); + } on PlatformException catch (e, s) { + convertPlatformException(e, s); + } + } + @override Future setTokenAutoRefreshEnabled( bool isTokenAutoRefreshEnabled, diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart index ca154bea1029..e44fa71de539 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -37,6 +37,116 @@ Object? _extractReplyValueOrThrow( return replyList.firstOrNull; } +bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == b; + } + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; + } + return a == b; +} + +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + +class InternalAppCheckTokenResult { + InternalAppCheckTokenResult({ + required this.token, + this.expirationTimestamp, + }); + + String token; + + int? expirationTimestamp; + + List _toList() { + return [ + token, + expirationTimestamp, + ]; + } + + Object encode() { + return _toList(); + } + + static InternalAppCheckTokenResult decode(Object result) { + result as List; + return InternalAppCheckTokenResult( + token: result[0]! as String, + expirationTimestamp: result[1] as int?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! InternalAppCheckTokenResult || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(token, other.token) && + _deepEquals(expirationTimestamp, other.expirationTimestamp); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -44,6 +154,9 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); + } else if (value is InternalAppCheckTokenResult) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -52,6 +165,8 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { + case 129: + return InternalAppCheckTokenResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -113,6 +228,27 @@ class FirebaseAppCheckHostApi { return pigeonVar_replyValue as String?; } + Future getTokenResult( + String appName, bool forceRefresh) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getTokenResult$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: true, + ); + return pigeonVar_replyValue as InternalAppCheckTokenResult?; + } + Future setTokenAutoRefreshEnabled( String appName, bool isTokenAutoRefreshEnabled) async { final pigeonVar_channelName = diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/platform_interface/platform_interface_firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/platform_interface/platform_interface_firebase_app_check.dart index 3346544a3bed..597929b7c9af 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/platform_interface/platform_interface_firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/platform_interface/platform_interface_firebase_app_check.dart @@ -111,6 +111,14 @@ abstract class FirebaseAppCheckPlatform extends PlatformInterface { throw UnimplementedError('getToken() is not implemented'); } + /// Get the current App Check token and its associated metadata. + /// + /// If `forceRefresh` is true, will always try to fetch a fresh token. If + /// false, will use a cached token if found in storage. + Future getTokenResult(bool forceRefresh) async { + throw UnimplementedError('getTokenResult() is not implemented'); + } + /// If true, the SDK automatically refreshes App Check tokens as needed. Future setTokenAutoRefreshEnabled(bool isTokenAutoRefreshEnabled) { throw UnimplementedError('setTokenAutoRefreshEnabled() is not implemented'); diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart index e84ff78ab5f4..25e024623cc1 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart @@ -4,6 +4,17 @@ import 'package:pigeon/pigeon.dart'; +class InternalAppCheckTokenResult { + InternalAppCheckTokenResult({ + required this.token, + this.expirationTimestamp, + }); + + String token; + + int? expirationTimestamp; +} + @ConfigurePigeon( PigeonOptions( dartOut: 'lib/src/pigeon/messages.pigeon.dart', @@ -34,6 +45,12 @@ abstract class FirebaseAppCheckHostApi { @async String? getToken(String appName, bool forceRefresh); + @async + InternalAppCheckTokenResult? getTokenResult( + String appName, + bool forceRefresh, + ); + @async void setTokenAutoRefreshEnabled( String appName, diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/test/app_check_token_result_test.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/test/app_check_token_result_test.dart new file mode 100644 index 000000000000..1c3aa3b60d85 --- /dev/null +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/test/app_check_token_result_test.dart @@ -0,0 +1,34 @@ +// 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_check_platform_interface/firebase_app_check_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$AppCheckTokenResult', () { + final expirationTime = DateTime.fromMillisecondsSinceEpoch(1234567890); + final result = AppCheckTokenResult( + token: 'test-token', + expirationTime: expirationTime, + ); + + test('exposes token metadata', () { + expect(result.token, 'test-token'); + expect(result.expirationTime, expirationTime); + }); + + test('supports unavailable expiration metadata', () { + const result = AppCheckTokenResult(token: 'test-token'); + + expect(result.expirationTime, isNull); + }); + + test('toString()', () { + expect( + result.toString(), + '$AppCheckTokenResult(token: test-token, expirationTime: $expirationTime)', + ); + }); + }); +} diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart index 7c60c253ff3d..2c969ee4c1ec 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart @@ -36,6 +36,16 @@ void main() { 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate', null, ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getToken', + null, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getTokenResult', + null, + ); }); group('delegateFor()', () { @@ -57,6 +67,55 @@ void main() { }); }); + group('getToken()', () { + const expirationTimestamp = 1234567890; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getToken', + (ByteData? message) async { + return FirebaseAppCheckHostApi.pigeonChannelCodec.encodeMessage( + ['test-token'], + ); + }, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getTokenResult', + (ByteData? message) async { + return FirebaseAppCheckHostApi.pigeonChannelCodec.encodeMessage( + [ + InternalAppCheckTokenResult( + token: 'test-token', + expirationTimestamp: expirationTimestamp, + ), + ], + ); + }, + ); + }); + + test('returns the token string without changing the existing API', + () async { + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + expect(await appCheck.getToken(true), 'test-token'); + }); + + test('returns token metadata', () async { + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + final result = await appCheck.getTokenResult(true); + + expect(result?.token, 'test-token'); + expect( + result?.expirationTime?.millisecondsSinceEpoch, + expirationTimestamp, + ); + }); + }); + group('activate()', () { test('passes the Apple debug token on Apple platforms', () async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/test/platform_interface_tests/platform_interface_app_check_test.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/test/platform_interface_tests/platform_interface_app_check_test.dart index eac0597bd8c7..21f7050abd23 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/test/platform_interface_tests/platform_interface_app_check_test.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/test/platform_interface_tests/platform_interface_app_check_test.dart @@ -104,6 +104,19 @@ void main() { ); }); + test('throws if .getTokenResult() not implemented', () async { + await expectLater( + () => firebaseAppCheckPlatform.getTokenResult(true), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'getTokenResult() is not implemented', + ), + ), + ); + }); + test('throws if .tokenChanges() not implemented', () async { await expectLater( () => firebaseAppCheckPlatform.onTokenChange, diff --git a/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart b/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart index f4cd415164c8..62a0395edca4 100644 --- a/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart +++ b/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart @@ -194,10 +194,15 @@ class FirebaseAppCheckWeb extends FirebaseAppCheckPlatform { @override Future getToken(bool forceRefresh) async { - return convertWebExceptions>(() async { + return (await getTokenResult(forceRefresh))?.token; + } + + @override + Future getTokenResult(bool forceRefresh) async { + return convertWebExceptions>(() async { app_check_interop.AppCheckTokenResultJsImpl result = await _delegate!.getToken(forceRefresh); - return result.token.toDart; + return AppCheckTokenResult(token: result.token.toDart); }); } diff --git a/tests/integration_test/firebase_app_check/firebase_app_check_e2e_test.dart b/tests/integration_test/firebase_app_check/firebase_app_check_e2e_test.dart index 777444ae5d64..06358a44d4f9 100644 --- a/tests/integration_test/firebase_app_check/firebase_app_check_e2e_test.dart +++ b/tests/integration_test/firebase_app_check/firebase_app_check_e2e_test.dart @@ -54,6 +54,25 @@ void main() { }, ); + test( + 'getTokenResult', + () async { + try { + final result = await FirebaseAppCheck.instance.getTokenResult(true); + if (result != null) { + expect(result.token, isNotEmpty); + if (!kIsWeb) { + expect(result.expirationTime, isNotNull); + expect(result.expirationTime!.isAfter(DateTime.now()), isTrue); + } + } + } catch (exception) { + // Needs a debug token pasted in the Firebase console to work so we catch the exception. + expect(exception, isA()); + } + }, + ); + test( 'setTokenAutoRefreshEnabled', () async {