diff --git a/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart index 9af6341c8823..db9f90b0508f 100644 --- a/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart +++ b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart @@ -140,6 +140,38 @@ void main() { skip: defaultTargetPlatform != TargetPlatform.iOS, ); + test( + 'appAttestWithDeviceCheckFallback falls back rather than erroring', + () async { + await FirebaseAppCheck.instance.activate( + providerApple: + const AppleAppAttestWithDeviceCheckFallbackProvider(), + ); + + // On devices without App Attest support — most Macs, and every + // simulator — the provider has to fall back to DeviceCheck. It used + // to pick App Attest purely on OS version and fail with "The + // attestation provider AppAttestProvider is not supported on current + // platform and OS version", so App Attest must not be the provider + // named in any error. Fetching a token can still fail beyond that + // (simulators do not support DeviceCheck either, and there is no + // debug token configured), which is fine here. + try { + await FirebaseAppCheck.instance.getToken(true); + } on FirebaseException catch (e) { + expect( + '${e.message}', + isNot(contains('AppAttestProvider')), + reason: 'the DeviceCheck fallback did not engage', + ); + } + }, + skip: defaultTargetPlatform != TargetPlatform.macOS && + defaultTargetPlatform != TargetPlatform.iOS + ? 'Apple platforms only.' + : null, + ); + test( 'uses Apple debug token when both Android and Apple debug tokens are configured', () async { 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 7c7cb4514293..9b702d03aef0 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 @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import DeviceCheck import FirebaseAppCheck import FirebaseCore @@ -371,11 +372,16 @@ class AppCheckProviderWrapper: NSObject, AppCheckProvider { delegateProvider = AppCheckDebugProvider(app: app) } case "appAttestWithDeviceCheckFallback": - if #available(iOS 14.0, macOS 14.0, *) { - delegateProvider = AppAttestProvider(app: app) - } else { - delegateProvider = DeviceCheckProvider(app: app) + // A new enough OS is not sufficient: the device must also support App + // Attest, which is often false on macOS. The SDK only reports that when a + // token is first requested, so check the same capability it checks — + // otherwise this provider never actually falls back. Typed as the + // protocol to keep the availability-annotated type inside `#available`. + var appAttestProvider: (any AppCheckProvider)? + if #available(iOS 14.0, macOS 14.0, *), DCAppAttestService.shared.isSupported { + appAttestProvider = AppAttestProvider(app: app) } + delegateProvider = appAttestProvider ?? DeviceCheckProvider(app: app) case "recaptcha": #if os(iOS) if let recaptchaSiteKey {