diff --git a/AGENTS.md b/AGENTS.md
index afa4a8081..cff505863 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -106,6 +106,17 @@ dart test test/specific_test.dart
dart test test/specific_test.dart -n "test name pattern"
```
+### Writing Tests
+
+Build tests on the `supabase_test` package instead of writing mocks by hand:
+
+- Answer HTTP calls with `MockSupabaseHttpClient` and its `stub`, `stubHandler`, `stubError` and `stubTable`/`stubRpc`/`stubStorage*` helpers. Do not subclass `BaseClient` in a test. Match methods with `HttpMethod.post.value` and friends rather than raw strings.
+- Assert on what was sent with `requests` and `requestsTo(path, method:)`.
+- Put a client into a signed-in state with `signInTestUser`, and build server payloads with `testUserJson`, `testSessionResponseJson` and `unsignedTestJwt`. Do not hand-assemble JWTs or session JSON.
+- Construct clients with `testSupabaseClient` where a whole `SupabaseClient` is needed.
+
+Fakes for things `supabase_test` does not cover, such as a platform plugin interface, are written in the test file itself.
+
### Package Management
```bash
diff --git a/examples/passkeys/pubspec.yaml b/examples/passkeys/pubspec.yaml
index 53622da39..49607f0e6 100644
--- a/examples/passkeys/pubspec.yaml
+++ b/examples/passkeys/pubspec.yaml
@@ -14,7 +14,7 @@ resolution: workspace
dependencies:
flutter:
sdk: flutter
- passkeys: ^2.21.1
+ passkeys: ^2.23.1
supabase_flutter: ^3.0.0-dev.3
dev_dependencies:
diff --git a/packages/supabase_flutter/README.md b/packages/supabase_flutter/README.md
index efa94b4dc..615e77dcf 100644
--- a/packages/supabase_flutter/README.md
+++ b/packages/supabase_flutter/README.md
@@ -61,6 +61,7 @@ final supabase = Supabase.instance.client;
* [Native Google sign in](#native-google-sign-in)
* [OAuth login](#oauth-login)
* [Passkeys](#passkeys)
+ * [Android Restore Credentials](#android-restore-credentials)
* [Database](#database)
* [Realtime](#realtime)
* [Postgres Changes](#postgres-changes)
@@ -318,6 +319,10 @@ await supabase.auth.passkey.delete(passkeyId: passkeys.first.id);
The platform ceremony is handled by whichever plugin you add. Refer to your plugin's documentation, for example the [`passkeys` package documentation](https://pub.dev/packages/passkeys), for its platform requirements, setup, and how to handle ceremony failures such as the user cancelling.
+### Android Restore Credentials
+
+Android's [Restore Credentials](https://developer.android.com/identity/sign-in/restore-credentials) restore keys are passkeys, so they use the same BETA passkey feature and the same authenticator. The [`passkeys`](https://pub.dev/packages/passkeys) plugin's `PasskeyAuthenticator` implements the `RestoreCredentialInterface` these methods expect (since `passkeys` `2.23.1`). Call `supabase.auth.createRestoreKey(authenticator)` after a non-anonymous sign-in and `supabase.auth.signInWithRestoreKey(authenticator)` on the first launch on a new device. On sign-out, delete the server passkey and call `clearRestoreCredential()` on the authenticator. See the API documentation of `AuthClientRestoreCredential` for the details.
+
### [Database](https://supabase.com/docs/guides/database)
Database methods are used to perform basic CRUD operations using the Supabase REST API. Full list of supported operators can be found [here](https://supabase.com/docs/reference/dart/select).
diff --git a/packages/supabase_flutter/lib/src/supabase_restore_credential.dart b/packages/supabase_flutter/lib/src/supabase_restore_credential.dart
new file mode 100644
index 000000000..97d10342a
--- /dev/null
+++ b/packages/supabase_flutter/lib/src/supabase_restore_credential.dart
@@ -0,0 +1,140 @@
+// This file intentionally builds on supabase_auth's experimental passkey API.
+// ignore_for_file: experimental_member_use
+
+import 'package:meta/meta.dart';
+import 'package:passkeys_platform_interface/passkeys_platform_interface.dart';
+import 'package:supabase_flutter/src/logger.dart';
+import 'package:supabase_flutter/src/passkey/passkey_options_mapper.dart';
+import 'package:supabase_flutter/supabase_flutter.dart';
+
+/// Android Restore Credentials ("zero-tap sign-in") on top of Supabase
+/// passkeys.
+///
+/// A restore key is a passkey that Android creates silently, backs up together
+/// with the app data, and makes available on the user's next device. Because
+/// the server side is the same as for passkeys, Supabase Auth stores and
+/// verifies restore keys as regular passkeys. Signing in with one on the new
+/// device yields a brand new session, independent of the refresh token chain
+/// of the old device, so it is safe to use as the credential restored during
+/// device setup.
+///
+/// Passkeys are a BETA feature and must be enabled for your project in the
+/// Supabase Dashboard under Authentication > Configuration > Passkeys. Android
+/// also requires Digital Asset Links for the relying party ID, exactly as for
+/// passkeys.
+///
+/// The platform calls are delegated to the [RestoreCredentialInterface] you
+/// pass in. The [`passkeys`](https://pub.dev/packages/passkeys) plugin's
+/// `PasskeyAuthenticator` implements it since `passkeys` `2.23.1`, so the same
+/// object serves [AuthClientPasskey.registerPasskey] and these methods.
+///
+/// Restore Credentials only exist on Android. Guard the calls with
+/// `defaultTargetPlatform == TargetPlatform.android`.
+///
+/// Methods rethrow whatever the [RestoreCredentialInterface] throws when the
+/// platform call fails, for example when there is no restore key on the device,
+/// and throw [AuthException] when the Supabase server rejects the credential.
+@experimental
+extension AuthClientRestoreCredential on AuthClient {
+ /// Creates a restore key for the signed in user and registers it as a
+ /// passkey.
+ ///
+ /// Call it right after a non-anonymous user signs in, and on a later launch
+ /// if the user is signed in and no restore key exists yet. Android keeps one
+ /// restore key per app, so remember the returned [Passkey.id] and delete the
+ /// previous key with [AuthPasskeyApi.delete] before creating a new one.
+ ///
+ /// When the user signs out, delete the key on both sides: the server passkey
+ /// with [AuthPasskeyApi.delete] and the key on the device with
+ /// [RestoreCredentialInterface.clearRestoreCredential]. Android does not
+ /// remove the device key on its own, so without the second step the user is
+ /// signed in again on the next launch.
+ ///
+ /// If the server rejects the created credential, the key is removed from the
+ /// device again before the error is rethrown. If only the rename to
+ /// [friendlyName] fails, the registered passkey is returned under the name
+ /// the server gave it and the failure is logged.
+ ///
+ /// [friendlyName] becomes the passkey's friendly name so restore keys can be
+ /// told apart from the passkeys the user created, for example to hide them
+ /// from a passkey management screen. It is also used as the account label
+ /// when the server does not provide a `user.name` in the options, see
+ /// [AuthPasskeyApi.startRegistration].
+ ///
+ /// [isCloudBackupEnabled] backs the restore key up to the cloud when the
+ /// device has end-to-end encrypted backup and stores it locally otherwise.
+ /// Pass `false` to always keep it local.
+ ///
+ /// Requires a signed in (non-anonymous) user.
+ Future createRestoreKey(
+ RestoreCredentialInterface restoreCredential, {
+ String friendlyName = 'Android restore key',
+ bool isCloudBackupEnabled = true,
+ }) async {
+ final registration = await passkey.startRegistration(
+ friendlyName: friendlyName,
+ );
+ final response = await restoreCredential.createRestoreCredential(
+ passkeyRegisterRequestFromOptions(registration.options),
+ isCloudBackupEnabled: isCloudBackupEnabled,
+ );
+ final Passkey registered;
+ try {
+ registered = await passkey.verifyRegistration(
+ challengeId: registration.challengeId,
+ credential: response.toJson(),
+ );
+ } catch (_) {
+ await _clearRestoreCredentialQuietly(restoreCredential);
+ rethrow;
+ }
+ try {
+ return await passkey.update(
+ passkeyId: registered.id,
+ friendlyName: friendlyName,
+ );
+ } catch (error, stackTrace) {
+ flutterLogger.warning(
+ 'Restore key ${registered.id} was registered but could not be renamed',
+ error,
+ stackTrace,
+ );
+ return registered;
+ }
+ }
+
+ Future _clearRestoreCredentialQuietly(
+ RestoreCredentialInterface restoreCredential,
+ ) async {
+ try {
+ await restoreCredential.clearRestoreCredential();
+ } catch (error, stackTrace) {
+ flutterLogger.warning(
+ 'Could not remove the rejected restore key from the device',
+ error,
+ stackTrace,
+ );
+ }
+ }
+
+ /// Signs the user in with the restore key on the device.
+ ///
+ /// Call it on the first launch after the app has been restored on a new
+ /// device. Does not require an existing session. On success the session is
+ /// persisted and an [AuthChangeEvent.signedIn] event is fired.
+ Future signInWithRestoreKey(
+ RestoreCredentialInterface restoreCredential, {
+ String? captchaToken,
+ }) async {
+ final authentication = await passkey.startAuthentication(
+ captchaToken: captchaToken,
+ );
+ final response = await restoreCredential.getRestoreCredential(
+ passkeyAuthenticateRequestFromOptions(authentication.options),
+ );
+ return passkey.verifyAuthentication(
+ challengeId: authentication.challengeId,
+ credential: response.toJson(),
+ );
+ }
+}
diff --git a/packages/supabase_flutter/lib/supabase_flutter.dart b/packages/supabase_flutter/lib/supabase_flutter.dart
index 7022a540b..661c4a95b 100644
--- a/packages/supabase_flutter/lib/supabase_flutter.dart
+++ b/packages/supabase_flutter/lib/supabase_flutter.dart
@@ -12,3 +12,4 @@ export 'src/shared_preferences_auth_async_storage.dart';
export 'src/supabase.dart';
export 'src/supabase_auth.dart' hide SupabaseAuth;
export 'src/supabase_passkey.dart';
+export 'src/supabase_restore_credential.dart';
diff --git a/packages/supabase_flutter/pubspec.yaml b/packages/supabase_flutter/pubspec.yaml
index 9bd0db986..6c7a2ac4b 100644
--- a/packages/supabase_flutter/pubspec.yaml
+++ b/packages/supabase_flutter/pubspec.yaml
@@ -24,7 +24,7 @@ dependencies:
sdk: flutter
http: ^1.6.0
meta: ^1.16.0
- passkeys_platform_interface: ^2.8.0
+ passkeys_platform_interface: ^2.10.0
supabase: 3.0.0-dev.3
supabase_common: 3.0.0-dev.2
url_launcher: ^6.3.2
diff --git a/packages/supabase_flutter/test/restore_credential_test.dart b/packages/supabase_flutter/test/restore_credential_test.dart
new file mode 100644
index 000000000..7dd0629dd
--- /dev/null
+++ b/packages/supabase_flutter/test/restore_credential_test.dart
@@ -0,0 +1,328 @@
+// ignore_for_file: experimental_member_use
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:passkeys_platform_interface/passkeys_platform_interface.dart';
+import 'package:passkeys_platform_interface/types/types.dart';
+import 'package:supabase_flutter/supabase_flutter.dart';
+import 'package:supabase_test/supabase_test.dart';
+
+const _challengeId = 'f9e16464-9ce8-4eb4-b3b3-456a8e95dfa9';
+const _passkeyId = '4b52e9e2-7c1b-44e5-8b5b-d4769ce06f58';
+
+const _registrationOptionsPath = '/passkeys/registration/options';
+const _registrationVerifyPath = '/passkeys/registration/verify';
+const _authenticationOptionsPath = '/passkeys/authentication/options';
+const _authenticationVerifyPath = '/passkeys/authentication/verify';
+const _passkeyPath = '/passkeys/$_passkeyId';
+
+Map _registrationOptions({bool withUserName = true}) => {
+ 'challenge_id': _challengeId,
+ 'options': {
+ 'challenge': 'Y2hhbGxlbmdl',
+ 'rp': {'id': 'example.com', 'name': 'Example'},
+ 'user': {
+ 'id': 'dXNlcg',
+ if (withUserName) 'name': 'jane@example.com',
+ if (withUserName) 'displayName': 'jane@example.com',
+ },
+ 'pubKeyCredParams': [
+ {'type': 'public-key', 'alg': -7},
+ ],
+ },
+ 'expires_at': 1735689900,
+};
+
+const _authenticationOptions = {
+ 'challenge_id': _challengeId,
+ 'options': {
+ 'challenge': 'Y2hhbGxlbmdl',
+ 'rpId': 'example.com',
+ 'userVerification': 'preferred',
+ },
+ 'expires_at': 1735689900,
+};
+
+Map _passkeyJson(String friendlyName) => {
+ 'id': _passkeyId,
+ 'friendly_name': friendlyName,
+ 'created_at': '2025-01-01T00:00:00Z',
+};
+
+class _FakeRestoreCredential implements RestoreCredentialInterface {
+ _FakeRestoreCredential({this.error});
+
+ static const registrationResponse = RegisterResponseType(
+ id: 'credential-id',
+ rawId: 'credential-id',
+ clientDataJSON: 'data',
+ attestationObject: 'data',
+ transports: ['internal'],
+ );
+ static const authenticationResponse = AuthenticateResponseType(
+ id: 'credential-id',
+ rawId: 'credential-id',
+ clientDataJSON: 'data',
+ authenticatorData: 'data',
+ signature: 'signature',
+ userHandle: testUserId,
+ );
+
+ final Object? error;
+ RegisterRequestType? createRequest;
+ bool? createIsCloudBackupEnabled;
+ AuthenticateRequestType? getRequest;
+ int clearCalls = 0;
+
+ @override
+ Future createRestoreCredential(
+ RegisterRequestType request, {
+ bool isCloudBackupEnabled = true,
+ }) async {
+ createRequest = request;
+ createIsCloudBackupEnabled = isCloudBackupEnabled;
+ if (error != null) throw error!;
+ return registrationResponse;
+ }
+
+ @override
+ Future getRestoreCredential(
+ AuthenticateRequestType request,
+ ) async {
+ getRequest = request;
+ if (error != null) throw error!;
+ return authenticationResponse;
+ }
+
+ @override
+ Future clearRestoreCredential() async {
+ clearCalls++;
+ }
+}
+
+void main() {
+ late MockSupabaseHttpClient httpClient;
+ late AuthClient client;
+
+ setUp(() {
+ httpClient = MockSupabaseHttpClient()
+ ..stub(
+ _registrationOptions(),
+ method: HttpMethod.post.value,
+ path: _registrationOptionsPath,
+ )
+ ..stub(
+ _passkeyJson('Google Password Manager'),
+ method: HttpMethod.post.value,
+ path: _registrationVerifyPath,
+ )
+ ..stubHandler(
+ (request) {
+ final body = request.jsonBody as Map;
+ return jsonResponse(_passkeyJson(body['friendly_name'] as String));
+ },
+ method: HttpMethod.patch.value,
+ path: _passkeyPath,
+ )
+ ..stub(
+ _authenticationOptions,
+ method: HttpMethod.post.value,
+ path: _authenticationOptionsPath,
+ )
+ ..stub(
+ testSessionResponseJson(
+ accessToken: unsignedTestJwt({
+ 'sub': testUserId,
+ 'role': 'authenticated',
+ }),
+ ),
+ method: HttpMethod.post.value,
+ path: _authenticationVerifyPath,
+ );
+ client = AuthClient(
+ url: 'http://localhost:9999',
+ httpClient: httpClient,
+ autoRefreshToken: false,
+ asyncStorage: MemoryAuthAsyncStorage(),
+ );
+ });
+
+ tearDown(() => client.dispose());
+
+ Iterable requestedPaths() =>
+ httpClient.requests.map((request) => request.url.path);
+
+ group('createRestoreKey', () {
+ setUp(() => signInTestUser(client));
+
+ test('runs the registration ceremony and names the key', () async {
+ final restore = _FakeRestoreCredential();
+
+ final passkey = await client.createRestoreKey(restore);
+
+ expect(requestedPaths(), [
+ _registrationOptionsPath,
+ _registrationVerifyPath,
+ _passkeyPath,
+ ]);
+ final request = restore.createRequest!;
+ expect(request.challenge, 'Y2hhbGxlbmdl');
+ expect(request.relyingParty.id, 'example.com');
+ expect(request.relyingParty.name, 'Example');
+ expect(request.user.name, 'jane@example.com');
+ expect(restore.createIsCloudBackupEnabled, isTrue);
+
+ final verify = httpClient.requestsTo(_registrationVerifyPath).single;
+ expect(verify.jsonBody, {
+ 'challenge_id': _challengeId,
+ 'credential': _FakeRestoreCredential.registrationResponse.toJson(),
+ });
+
+ final rename = httpClient.requestsTo(_passkeyPath).single;
+ expect(rename.jsonBody, {'friendly_name': 'Android restore key'});
+ expect(passkey.id, _passkeyId);
+ expect(passkey.friendlyName, 'Android restore key');
+ });
+
+ test('uses a custom friendly name for the key and account label', () async {
+ httpClient.stub(
+ _registrationOptions(withUserName: false),
+ method: HttpMethod.post.value,
+ path: _registrationOptionsPath,
+ );
+ final restore = _FakeRestoreCredential();
+
+ final passkey = await client.createRestoreKey(
+ restore,
+ friendlyName: 'Pixel restore key',
+ );
+
+ expect(restore.createRequest?.user.name, 'Pixel restore key');
+ expect(restore.createRequest?.user.displayName, 'Pixel restore key');
+ expect(httpClient.requestsTo(_passkeyPath).single.jsonBody, {
+ 'friendly_name': 'Pixel restore key',
+ });
+ expect(passkey.friendlyName, 'Pixel restore key');
+ });
+
+ test('rethrows platform errors without registering anything', () async {
+ final restore = _FakeRestoreCredential(
+ error: StateError('backup unavailable'),
+ );
+
+ await expectLater(
+ client.createRestoreKey(restore),
+ throwsA(isA()),
+ );
+
+ expect(requestedPaths(), [_registrationOptionsPath]);
+ });
+
+ test('removes the device key again when the server rejects it', () async {
+ httpClient.stub(
+ {
+ 'code': 400,
+ 'error_code': 'validation_failed',
+ 'msg': 'Invalid credential',
+ },
+ method: HttpMethod.post.value,
+ path: _registrationVerifyPath,
+ statusCode: 400,
+ );
+ final restore = _FakeRestoreCredential();
+
+ await expectLater(
+ client.createRestoreKey(restore),
+ throwsA(isA()),
+ );
+
+ expect(restore.clearCalls, 1);
+ expect(requestedPaths(), [
+ _registrationOptionsPath,
+ _registrationVerifyPath,
+ ]);
+ });
+
+ test('keeps the registered key when only the rename fails', () async {
+ httpClient.stub(
+ {
+ 'code': 500,
+ 'error_code': 'unexpected_failure',
+ 'msg': 'Database error',
+ },
+ method: HttpMethod.patch.value,
+ path: _passkeyPath,
+ statusCode: 500,
+ );
+ final restore = _FakeRestoreCredential();
+
+ final passkey = await client.createRestoreKey(restore);
+
+ expect(passkey.id, _passkeyId);
+ expect(passkey.friendlyName, 'Google Password Manager');
+ expect(restore.clearCalls, 0);
+ });
+
+ test('forwards a local-only restore key request', () async {
+ final restore = _FakeRestoreCredential();
+
+ await client.createRestoreKey(restore, isCloudBackupEnabled: false);
+
+ expect(restore.createIsCloudBackupEnabled, isFalse);
+ });
+ });
+
+ group('signInWithRestoreKey', () {
+ test('runs the authentication ceremony and signs in', () async {
+ final signedIn = client.onAuthStateChange.firstWhere(
+ (state) => state.event == AuthChangeEvent.signedIn,
+ );
+ final restore = _FakeRestoreCredential();
+
+ final response = await client.signInWithRestoreKey(
+ restore,
+ captchaToken: 'captcha-token',
+ );
+
+ expect(requestedPaths(), [
+ _authenticationOptionsPath,
+ _authenticationVerifyPath,
+ ]);
+ expect(
+ httpClient.requestsTo(_authenticationOptionsPath).single.jsonBody,
+ {
+ 'gotrue_meta_security': {'captcha_token': 'captcha-token'},
+ },
+ );
+ final request = restore.getRequest!;
+ expect(request.challenge, 'Y2hhbGxlbmdl');
+ expect(request.relyingPartyId, 'example.com');
+ expect(request.userVerification, 'preferred');
+
+ final verify = httpClient.requestsTo(_authenticationVerifyPath).single;
+ expect(verify.jsonBody, {
+ 'challenge_id': _challengeId,
+ 'credential': _FakeRestoreCredential.authenticationResponse.toJson(),
+ });
+
+ expect(response.session, isNotNull);
+ expect(client.currentSession?.accessToken, response.session?.accessToken);
+ expect(client.currentUser?.id, testUserId);
+ expect(
+ (await signedIn).session?.accessToken,
+ response.session?.accessToken,
+ );
+ });
+
+ test('rethrows platform errors without verifying', () async {
+ final restore = _FakeRestoreCredential(error: StateError('no key'));
+
+ await expectLater(
+ client.signInWithRestoreKey(restore),
+ throwsA(isA()),
+ );
+
+ expect(requestedPaths(), [_authenticationOptionsPath]);
+ expect(client.currentSession, isNull);
+ });
+ });
+}
diff --git a/pubspec.lock b/pubspec.lock
index a5c0a34ac..0c09fa65b 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -675,58 +675,58 @@ packages:
dependency: transitive
description:
name: passkeys
- sha256: a8feedc7e575d88bc705fd1c84eacbf65367e3e2e05183b4bbcdb95acbfa4c54
+ sha256: b3230c542246d7f90093e8ce00cf8552006cf5e54dbc1cb9b593f12f85d20705
url: "https://pub.dev"
source: hosted
- version: "2.21.1"
+ version: "2.23.1"
passkeys_android:
dependency: transitive
description:
name: passkeys_android
- sha256: a3e36f0114400ae07376f0327c0600189bd37af2535443ef45f83444c6d94569
+ sha256: "92e7607612a6cf6fc8c6590384ffc25f98e9cad1154b17c84d6ccf251ba25c31"
url: "https://pub.dev"
source: hosted
- version: "2.12.2"
+ version: "2.14.1"
passkeys_darwin:
dependency: transitive
description:
name: passkeys_darwin
- sha256: "07944baf2f18f21bb85cb7f8f50c4b3318a948bdf32fe6c8e737cfc41fbfb639"
+ sha256: ceb245f1c98fa25119fd2795ebe8dc39dd7a6b443de77bf58fde9f5ff76237b9
url: "https://pub.dev"
source: hosted
- version: "0.4.1+1"
+ version: "0.4.5"
passkeys_doctor:
dependency: transitive
description:
name: passkeys_doctor
- sha256: d5c797c1a8307fbfb43bedb35845400c1513d215314dff4116b280ca83b0b4ff
+ sha256: "44eefcc28603cdeb052e7a6b5fd8642f47e54bb5450ffbd7da3dc0e2a728bb7b"
url: "https://pub.dev"
source: hosted
- version: "1.5.1"
+ version: "1.6.2"
passkeys_platform_interface:
dependency: transitive
description:
name: passkeys_platform_interface
- sha256: "9610bd136b3382500390912ddd8517ee99505228b1af7b507b9c907f7e99a47d"
+ sha256: "5fca044a6f1a3c5abff645ca5a5417b613c130d1c4e08513ba9182c7f4834619"
url: "https://pub.dev"
source: hosted
- version: "2.8.0"
+ version: "2.10.0"
passkeys_web:
dependency: transitive
description:
name: passkeys_web
- sha256: "89b2a7124d24560ae8eb5eaaa98e1079e5eb488dfaeeb0f19cea032ff2c42d26"
+ sha256: "5a02f256914eeb5d06d1b5a8e552c6a2837a7364d7591c14b06a992fcc4da051"
url: "https://pub.dev"
source: hosted
- version: "2.9.2"
+ version: "2.10.1"
passkeys_windows:
dependency: transitive
description:
name: passkeys_windows
- sha256: "03f0525ed3acc19f5d8cd6d0251284ac2e6ebd5e1af53aae90e31a3afdddac40"
+ sha256: "43d116e42324788e1cddc1ba6e98a32a28ccc47f68508c4cd4a19e6309daa127"
url: "https://pub.dev"
source: hosted
- version: "0.1.2+1"
+ version: "0.1.5"
path:
dependency: transitive
description:
@@ -1108,14 +1108,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
- ua_client_hints:
- dependency: transitive
- description:
- name: ua_client_hints
- sha256: e6e302bb7c21d6a90023db61e0ec413f0810ce341bb7e2ab447ff5d61ec3d87e
- url: "https://pub.dev"
- source: hosted
- version: "1.7.0"
url_launcher:
dependency: transitive
description:
diff --git a/sdk-compliance.yaml b/sdk-compliance.yaml
index aad444f3a..7d075b69a 100644
--- a/sdk-compliance.yaml
+++ b/sdk-compliance.yaml
@@ -425,9 +425,11 @@ features:
status: implemented
symbols:
- AuthClientPasskey.registerPasskey
+ - AuthClientRestoreCredential.createRestoreKey
- AuthPasskeyApi.startRegistration
- AuthPasskeyApi.verifyRegistration
supporting_symbols:
+ - AuthClientRestoreCredential
- PasskeyRegistrationOptionsResponse
- PasskeyRegistrationOptionsResponse.PasskeyRegistrationOptionsResponse
- PasskeyRegistrationOptionsResponse.challengeId
@@ -438,6 +440,7 @@ features:
status: implemented
symbols:
- AuthClientPasskey.signInWithPasskey
+ - AuthClientRestoreCredential.signInWithRestoreKey
- AuthPasskeyApi.startAuthentication
- AuthPasskeyApi.verifyAuthentication
supporting_symbols: