From 12cea82551aa9b64af334b950b2ea0b3e374cbbf Mon Sep 17 00:00:00 2001 From: "masatada.kurihara" Date: Tue, 14 Apr 2026 19:54:12 +0900 Subject: [PATCH 1/3] fix(passkeys_darwin): map ASAuthorizationError code 1006 to exclude-credentials-match ASAuthorizationError code 1006 (.matchedExcludedCredential, iOS 18+) indicates the device already holds a credential listed in excludeCredentials. Previously this fell through to the default case and was mapped to "unknown", causing a raw PlatformException to surface instead of ExcludeCredentialsCanNotBeRegisteredException. Closes #232 --- .../passkeys_darwin/darwin/Classes/ErrorExtension.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 } From 3cf1804bbddc66520ecaef781de45e875115b024 Mon Sep 17 00:00:00 2001 From: "masatada.kurihara" Date: Tue, 14 Apr 2026 19:54:22 +0900 Subject: [PATCH 2/3] fix(passkeys_android): use substring match for excluded credentials error detection Use case-insensitive substring match (contains) instead of exact string comparison (Objects.equals) for detecting excluded credentials errors. The error message can vary across Android / Google Play Services versions and the previous exact match missed cases where the error arrives as TYPE_INVALID_STATE_ERROR with a different message format. Closes #232 --- .../java/com/corbado/passkeys_android/MessageHandler.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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.")) { From 7b7adc8d0b0def1733c81cf9e448909ae755a003 Mon Sep 17 00:00:00 2001 From: "masatada.kurihara" Date: Tue, 14 Apr 2026 19:54:29 +0900 Subject: [PATCH 3/3] fix(passkeys): wrap unhandled PlatformExceptions in register() with UnhandledAuthenticatorException The register() method's default PlatformException handler used rethrow, leaking raw PlatformExceptions to callers. The authenticate() method already wraps android-unhandled and ios-unhandled codes in UnhandledAuthenticatorException. Apply the same pattern to register() for consistency. Also applies dart format to the file. Closes #232 --- .../passkeys/passkeys/lib/authenticator.dart | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) 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;