diff --git a/packages/passkeys/passkeys/lib/authenticator.dart b/packages/passkeys/passkeys/lib/authenticator.dart index 005c318c..41ae5b45 100644 --- a/packages/passkeys/passkeys/lib/authenticator.dart +++ b/packages/passkeys/passkeys/lib/authenticator.dart @@ -23,8 +23,10 @@ class PasskeyAuthenticator { final bool debugMode; /// Returns true only if passkeys are supported by the platform. - @Deprecated('Use PasskeyAvailability.isAvailable instead. ' - 'This method will be removed in a future release.') + @Deprecated( + 'Use PasskeyAvailability.isAvailable instead. ' + 'This method will be removed in a future release.', + ) Future canAuthenticate() { return _platform.canAuthenticate(); } @@ -86,7 +88,13 @@ class PasskeyAuthenticator { case 'ios-security-key-timeout': throw TimeoutException(e.message); default: - rethrow; + if (e.code.startsWith('android-unhandled')) { + throw UnhandledAuthenticatorException(e.code, e.message, e.details); + } else if (e.code.startsWith('ios-unhandled')) { + throw UnhandledAuthenticatorException(e.code, e.message, e.details); + } else { + rethrow; + } } } } @@ -229,8 +237,10 @@ class PasskeyAuthenticator { if (!base64UrlRegex.hasMatch(input)) return false; try { - String normalized = - input.padRight(input.length + (4 - input.length % 4) % 4, '='); + String normalized = input.padRight( + input.length + (4 - input.length % 4) % 4, + '=', + ); base64Url.decode(normalized); return true; diff --git a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java index 0d7d5270..cb2bccc0 100644 --- a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java +++ b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java @@ -192,8 +192,10 @@ public void onError(CreateCredentialException e) { } else if (Objects.equals(e.getMessage(), "Unable to get sync account.")) { platformException = new Messages.FlutterError("android-sync-account-not-available", e.getMessage(), SYNC_ACCOUNT_NOT_AVAILABLE_ERROR); - } else if (Objects.equals(e.getMessage(), - "One of the excluded credentials exists on the local device")) { + } else if (e.getMessage() != null && e.getMessage().toLowerCase().contains("excluded credentials")) { + // Match excluded credentials error by substring instead of exact match, + // as the error message may vary across Android / Google Play Services versions. + // Also handles TYPE_INVALID_STATE_ERROR with excluded credentials message. platformException = new Messages.FlutterError("exclude-credentials-match", e.getMessage(), EXCLUDE_CREDENTIALS_MATCH_ERROR); } else if (Objects.equals(e.getMessage(), "[15] Flow has timed out.")) { diff --git a/packages/passkeys/passkeys_darwin/darwin/Classes/ErrorExtension.swift b/packages/passkeys/passkeys_darwin/darwin/Classes/ErrorExtension.swift index 159a479f..e912919a 100644 --- a/packages/passkeys/passkeys_darwin/darwin/Classes/ErrorExtension.swift +++ b/packages/passkeys/passkeys_darwin/darwin/Classes/ErrorExtension.swift @@ -38,7 +38,13 @@ extension FlutterError: Error { } break default: - code = "unknown" + // ASAuthorizationError code 1006 (.matchedExcludedCredential, iOS 18+) + // indicates the device already holds a credential listed in excludeCredentials. + if error.code.rawValue == 1006 { + code = "exclude-credentials-match" + } else { + code = "unknown" + } break }