diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index cd94490c4..8c43734e5 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -44,9 +44,13 @@ import 'trace_http_client.dart'; /// Pass an instance of `YAJsonIsolate` to [isolate] to use your own persisted /// isolate instance. A new instance will be created if [isolate] is omitted. /// -/// Pass an instance of `AuthAsyncStorage` to the `pkceAsyncStorage` field of -/// [authOptions] and set its `authFlowType` field to `AuthFlowType.pkce` in -/// order to perform auth actions with pkce flow. +/// The pkce flow is used by default and keeps its code verifiers in the +/// `AuthAsyncStorage` passed to the `pkceAsyncStorage` field of [authOptions]. +/// Pass a persistent implementation whenever the flow can leave the process +/// before the code comes back, which covers every email link and every +/// redirect to an OAuth provider. `MemoryAuthAsyncStorage` only suits flows +/// that start and complete in the same process, such as tests and command line +/// tools that keep a redirect listener open. /// {@endtemplate} class SupabaseClient { final String _supabaseKey; diff --git a/packages/supabase/lib/src/supabase_client_options.dart b/packages/supabase/lib/src/supabase_client_options.dart index 35fa7f5e0..18fd85cfb 100644 --- a/packages/supabase/lib/src/supabase_client_options.dart +++ b/packages/supabase/lib/src/supabase_client_options.dart @@ -34,6 +34,21 @@ class PostgrestClientOptions { class AuthClientOptions { final bool autoRefreshToken; + + /// Storage for the code verifiers of the pkce flow, required when + /// [authFlowType] is [AuthFlowType.pkce]. + /// + /// A persistent implementation is needed whenever the flow can leave the + /// process before the code comes back. Email links do so by definition, and + /// so does a redirect to an OAuth provider, since the app may be reaped + /// while it waits and the page context is gone after a web redirect. + /// `supabase_flutter` therefore defaults this to shared preferences. + /// + /// [MemoryAuthAsyncStorage] only suits flows that start and complete in the + /// same process, such as tests and command line tools that keep a redirect + /// listener open. It is also unfit for a server handling more than one user + /// at a time, because the verifier is held under a single key that + /// concurrent sign-ins overwrite. final AuthAsyncStorage? pkceAsyncStorage; final AuthFlowType authFlowType; diff --git a/packages/supabase/test/client_test.dart b/packages/supabase/test/client_test.dart index 8a62859ac..93fc9edb3 100644 --- a/packages/supabase/test/client_test.dart +++ b/packages/supabase/test/client_test.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:supabase/supabase.dart'; +import 'package:supabase/src/supabase_client.dart' as real; +import 'package:supabase/supabase.dart' hide SupabaseClient; import 'package:supabase_common/supabase_common.dart'; import 'package:test/test.dart'; import 'package:yet_another_json_isolate/yet_another_json_isolate.dart'; @@ -183,6 +184,36 @@ void main() { }); group('auth', () { + test('the pkce flow asserts when no pkceAsyncStorage is given', () { + expect( + () => real.SupabaseClient('http://localhost:1', 'supabaseKey'), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('You need to provide asyncStorage to perform pkce flow.'), + ), + ), + ); + }); + + test('the pkce flow works with a MemoryAuthAsyncStorage', () async { + final supabase = real.SupabaseClient( + 'http://localhost:1', + 'supabaseKey', + authOptions: AuthClientOptions( + pkceAsyncStorage: MemoryAuthAsyncStorage(), + ), + ); + addTearDown(supabase.dispose); + + final response = await supabase.auth.getOAuthSignInUrl( + provider: OAuthProvider.github, + ); + + expect(response.url.queryParameters, contains('code_challenge')); + }); + test('properly set Authorization header', () async { final (:sessionString, :accessToken) = getSessionData( DateTime.now().add(Duration(hours: 1)), @@ -469,3 +500,28 @@ void main() { }); }); } + +/// A [real.SupabaseClient] that falls back to an in-memory pkce storage, so the +/// tests below do not have to pass one at every construction site. +class SupabaseClient extends real.SupabaseClient { + SupabaseClient( + super.supabaseUrl, + super.supabaseKey, { + super.postgrestOptions, + AuthClientOptions authOptions = const AuthClientOptions(), + super.storageOptions, + super.functionsOptions, + super.realtimeClientOptions, + super.accessToken, + super.headers, + super.httpClient, + super.isolate, + }) : super( + authOptions: AuthClientOptions( + autoRefreshToken: authOptions.autoRefreshToken, + pkceAsyncStorage: + authOptions.pkceAsyncStorage ?? MemoryAuthAsyncStorage(), + authFlowType: authOptions.authFlowType, + ), + ); +} diff --git a/packages/supabase/test/mock_test.dart b/packages/supabase/test/mock_test.dart index c3ece9f0c..4d8812aa6 100644 --- a/packages/supabase/test/mock_test.dart +++ b/packages/supabase/test/mock_test.dart @@ -372,11 +372,13 @@ void main() { headers: { 'X-Client-Info': 'supabase-flutter/0.0.0', }, + authOptions: const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); customHeadersClient = SupabaseClient( 'http://${mockServer.address.host}:${mockServer.port}', apiKey, headers: {'X-Client-Info': 'supabase-flutter/0.0.0', ...customHeaders}, + authOptions: const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); hasListener = false; }); @@ -848,6 +850,9 @@ void main() { 'http://${errorServer.address.host}:${errorServer.port}', 'test-key', headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + authOptions: const AuthClientOptions( + authFlowType: AuthFlowType.implicit, + ), ); final stream = errorClient.from('todos').stream(primaryKey: ['id']); @@ -879,6 +884,9 @@ void main() { throw Exception('Token retrieval failed'); }, headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + authOptions: const AuthClientOptions( + authFlowType: AuthFlowType.implicit, + ), ); // Should handle token errors gracefully @@ -897,6 +905,9 @@ void main() { 'http://${mockServer.address.host}:${mockServer.port}', 'test-key', headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + authOptions: const AuthClientOptions( + authFlowType: AuthFlowType.implicit, + ), ); // First dispose should succeed diff --git a/packages/supabase/test/realtime_test.dart b/packages/supabase/test/realtime_test.dart index 774842aa5..4cca2e9ac 100644 --- a/packages/supabase/test/realtime_test.dart +++ b/packages/supabase/test/realtime_test.dart @@ -27,6 +27,9 @@ void main() { supabase = SupabaseClient( 'http://${mockServer.address.host}:${mockServer.port}', 'supabaseKey', + authOptions: const AuthClientOptions( + authFlowType: AuthFlowType.implicit, + ), ); channel = supabase.channel('realtime'); diff --git a/packages/supabase/test/stream_filter_test.dart b/packages/supabase/test/stream_filter_test.dart index acfe4fb19..5abd50680 100644 --- a/packages/supabase/test/stream_filter_test.dart +++ b/packages/supabase/test/stream_filter_test.dart @@ -8,6 +8,8 @@ import 'package:web_socket_channel/io.dart'; import 'package:supabase_common/testing.dart'; +import 'utils.dart'; + /// Asserts how the filters of `stream()` are sent to the realtime server and to /// PostgREST, without needing either of them to be running. void main() { @@ -36,6 +38,7 @@ void main() { supabase = SupabaseClient( 'http://${InternetAddress.loopbackIPv4.address}:${mockServer.port}', localStackServiceRoleKey, + authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()), ); }); diff --git a/packages/supabase/test/stream_integration_test.dart b/packages/supabase/test/stream_integration_test.dart index 0b08287a7..782db0068 100644 --- a/packages/supabase/test/stream_integration_test.dart +++ b/packages/supabase/test/stream_integration_test.dart @@ -8,6 +8,8 @@ import 'package:test/test.dart'; import 'package:supabase_common/testing.dart'; +import 'utils.dart'; + late SupabaseClient _supabase; void main() { @@ -541,8 +543,11 @@ void main() { const _streamTimeout = Duration(seconds: 10); const _warmUpPrefix = 'warm_up_'; -SupabaseClient _createClient() => - SupabaseClient(localStackUrl, localStackServiceRoleKey); +SupabaseClient _createClient() => SupabaseClient( + localStackUrl, + localStackServiceRoleKey, + authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()), +); /// Listens to [stream] and asserts that it emits [expectedSnapshots] in order, /// where every snapshot is the result of [project] applied to the emitted rows. diff --git a/packages/supabase/test/trace_propagation_test.dart b/packages/supabase/test/trace_propagation_test.dart index cbef486a0..989ba31ef 100644 --- a/packages/supabase/test/trace_propagation_test.dart +++ b/packages/supabase/test/trace_propagation_test.dart @@ -4,6 +4,8 @@ import 'package:supabase/src/trace_http_client.dart'; import 'package:supabase/supabase.dart'; import 'package:test/test.dart'; +import 'utils.dart'; + const _sampledTraceparent = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'; const _unsampledTraceparent = @@ -132,6 +134,7 @@ void main() { _supabaseUrl, 'anon-key', tracePropagationOptions: optionsWith(() => context), + authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()), httpClient: MockClient((request) async { restRequest = request; return Response( @@ -156,6 +159,7 @@ void main() { final supabase = SupabaseClient( _supabaseUrl, 'anon-key', + authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()), httpClient: MockClient((request) async { restRequest = request; return Response( diff --git a/packages/supabase/test/utils.dart b/packages/supabase/test/utils.dart index 678d9ca81..9ffe4ff9d 100644 --- a/packages/supabase/test/utils.dart +++ b/packages/supabase/test/utils.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:supabase/supabase.dart'; + /// Construct session data for a given expiration date ({String accessToken, String sessionString}) getSessionData(DateTime dateTime) { final expiresAt = dateTime.millisecondsSinceEpoch ~/ 1000; @@ -26,3 +28,5 @@ import 'dart:convert'; 'at":"2023-04-01T08:35:05.226938Z"}}'; return (accessToken: accessToken, sessionString: sessionString); } + +class TestAsyncStorage extends MemoryAuthAsyncStorage {} diff --git a/packages/supabase_auth/example/main.dart b/packages/supabase_auth/example/main.dart index 2c2ec09d7..f498bcf2a 100644 --- a/packages/supabase_auth/example/main.dart +++ b/packages/supabase_auth/example/main.dart @@ -12,6 +12,9 @@ Future main() async { 'Authorization': 'Bearer $supabaseKey', 'apikey': supabaseKey, }, + // The pkce flow needs somewhere to keep its code verifiers. Swap this for + // a persistent storage when the code exchange can happen after a restart. + asyncStorage: MemoryAuthAsyncStorage(), ); try { diff --git a/packages/supabase_auth/lib/src/auth_client.dart b/packages/supabase_auth/lib/src/auth_client.dart index 46b28ee9a..6c92b8103 100644 --- a/packages/supabase_auth/lib/src/auth_client.dart +++ b/packages/supabase_auth/lib/src/auth_client.dart @@ -45,7 +45,8 @@ class _SessionState { /// [httpClient] custom http client. /// /// [asyncStorage] local storage to store pkce code verifiers. Required when -/// using the pkce flow. +/// using the pkce flow. Pass a [MemoryAuthAsyncStorage] when the verifiers +/// do not need to outlive the process. /// /// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow. /// {@endtemplate} @@ -157,7 +158,13 @@ class AuthClient { Client? httpClient, AuthAsyncStorage? asyncStorage, AuthFlowType flowType = AuthFlowType.pkce, - }) : _url = url ?? AuthConstants.defaultAuthUrl, + }) : assert( + flowType != AuthFlowType.pkce || asyncStorage != null, + 'You need to provide asyncStorage to perform pkce flow. Pass a ' + 'MemoryAuthAsyncStorage when the code verifiers do not need to ' + 'outlive the process.', + ), + _url = url ?? AuthConstants.defaultAuthUrl, _headers = {...AuthConstants.defaultHeaders, ...?headers}, _httpClient = httpClient, _asyncStorage = asyncStorage, diff --git a/packages/supabase_auth/lib/src/types/auth_async_storage.dart b/packages/supabase_auth/lib/src/types/auth_async_storage.dart index d944c9a55..02c75d56b 100644 --- a/packages/supabase_auth/lib/src/types/auth_async_storage.dart +++ b/packages/supabase_auth/lib/src/types/auth_async_storage.dart @@ -14,3 +14,26 @@ abstract class AuthAsyncStorage { /// Removes an item asynchronously from the storage for the given key. Future removeItem({required String key}); } + +/// A [AuthAsyncStorage] that keeps the pkce code verifiers in memory only. +/// +/// Everything it holds is lost when the process exits, so a pkce flow started +/// before a restart can no longer be completed. Use a persistent +/// implementation when the code exchange happens after the app was closed, +/// which is what `supabase_flutter` does with shared preferences. +class MemoryAuthAsyncStorage extends AuthAsyncStorage { + final _items = {}; + + @override + Future getItem({required String key}) async => _items[key]; + + @override + Future setItem({required String key, required String value}) async { + _items[key] = value; + } + + @override + Future removeItem({required String key}) async { + _items.remove(key); + } +} diff --git a/packages/supabase_auth/test/admin_delete_user_test.dart b/packages/supabase_auth/test/admin_delete_user_test.dart index 0a4c01e80..804c38882 100644 --- a/packages/supabase_auth/test/admin_delete_user_test.dart +++ b/packages/supabase_auth/test/admin_delete_user_test.dart @@ -4,6 +4,8 @@ import 'package:supabase_auth/supabase_auth.dart'; import 'package:http/http.dart'; import 'package:test/test.dart'; +import 'utils.dart'; + class _CapturingHttpClient extends BaseClient { Request? lastRequest; @@ -25,6 +27,7 @@ void main() { client = AuthClient( url: 'http://localhost:9999', httpClient: httpClient, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/admin_list_users_test.dart b/packages/supabase_auth/test/admin_list_users_test.dart index 410f9b628..eb7d4f8d4 100644 --- a/packages/supabase_auth/test/admin_list_users_test.dart +++ b/packages/supabase_auth/test/admin_list_users_test.dart @@ -4,6 +4,8 @@ import 'package:supabase_auth/supabase_auth.dart'; import 'package:http/http.dart'; import 'package:test/test.dart'; +import 'utils.dart'; + /// Serves a fixed list-users response with the pagination headers the GoTrue /// server sends alongside it. class ListUsersMockClient extends BaseClient { @@ -52,6 +54,7 @@ void main() { AuthClient clientWith(ListUsersMockClient mockClient) => AuthClient( url: 'http://localhost:9999', httpClient: mockClient, + asyncStorage: TestAsyncStorage(), ); test('listUsers() returns the metadata of a middle page', () async { diff --git a/packages/supabase_auth/test/admin_test.dart b/packages/supabase_auth/test/admin_test.dart index 1b0e4909a..856ccfef8 100644 --- a/packages/supabase_auth/test/admin_test.dart +++ b/packages/supabase_auth/test/admin_test.dart @@ -32,6 +32,7 @@ void main() { 'Authorization': 'Bearer ${getServiceRoleToken(env)}', 'apikey': getServiceRoleToken(env), }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/client_test.dart b/packages/supabase_auth/test/client_test.dart index 452cc291b..cd2e437f8 100644 --- a/packages/supabase_auth/test/client_test.dart +++ b/packages/supabase_auth/test/client_test.dart @@ -293,6 +293,7 @@ void main() { final newClient = AuthClient( url: authUrl, headers: {'apikey': anonToken}, + asyncStorage: TestAsyncStorage(), ); expect(newClient.currentSession?.refreshToken ?? '', isEmpty); @@ -326,6 +327,7 @@ void main() { final newClient = AuthClient( url: authUrl, headers: {'apikey': anonToken}, + asyncStorage: TestAsyncStorage(), ); expect(newClient.currentSession, isNull); @@ -369,6 +371,7 @@ void main() { final newClient = AuthClient( url: authUrl, headers: {'apikey': anonToken}, + asyncStorage: TestAsyncStorage(), ); // Should fall back to _callRefreshToken and succeed. @@ -420,6 +423,7 @@ void main() { final newClient = AuthClient( url: authUrl, headers: {'apikey': anonToken}, + asyncStorage: TestAsyncStorage(), ); expect(newClient.currentSession, isNull); @@ -666,7 +670,11 @@ void main() { late AuthClient client; setUpAll(() { - client = AuthClient(url: authUrl, httpClient: CustomHttpClient()); + client = AuthClient( + url: authUrl, + httpClient: CustomHttpClient(), + asyncStorage: TestAsyncStorage(), + ); }); test('signIn()', () async { @@ -699,7 +707,11 @@ void main() { setUpAll(() { httpClient = RetryTestHttpClient(); - client = AuthClient(url: authUrl, httpClient: httpClient); + client = AuthClient( + url: authUrl, + httpClient: httpClient, + asyncStorage: TestAsyncStorage(), + ); }); test('Session recovery succeeds after retries', () async { @@ -956,6 +968,32 @@ void main() { }, ); }); + + group('Constructing a client without an asyncStorage', () { + test('asserts when the pkce flow is used', () { + expect( + () => AuthClient(url: authUrl, headers: {'apikey': anonToken}), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('You need to provide asyncStorage to perform pkce flow.'), + ), + ), + ); + }); + + test('is allowed when the implicit flow is used', () { + expect( + () => AuthClient( + url: authUrl, + headers: {'apikey': anonToken}, + flowType: AuthFlowType.implicit, + ), + returnsNormally, + ); + }); + }); } /// Reads the email-change confirmation link that GoTrue delivered to diff --git a/packages/supabase_auth/test/header_isolation_test.dart b/packages/supabase_auth/test/header_isolation_test.dart index ce9f52908..04f0efb32 100644 --- a/packages/supabase_auth/test/header_isolation_test.dart +++ b/packages/supabase_auth/test/header_isolation_test.dart @@ -4,6 +4,8 @@ import 'package:supabase_auth/supabase_auth.dart'; import 'package:http/http.dart'; import 'package:test/test.dart'; +import 'utils.dart'; + /// Records the headers of every request it receives and always answers with a /// minimal user payload, so we can inspect what the client actually sent. class _RecordingHttpClient extends BaseClient { @@ -46,6 +48,7 @@ void main() { url: 'http://localhost', headers: {'apikey': 'anon-key'}, httpClient: http, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/memory_async_storage_test.dart b/packages/supabase_auth/test/memory_async_storage_test.dart new file mode 100644 index 000000000..21532093b --- /dev/null +++ b/packages/supabase_auth/test/memory_async_storage_test.dart @@ -0,0 +1,36 @@ +import 'package:supabase_auth/supabase_auth.dart'; +import 'package:test/test.dart'; + +void main() { + late MemoryAuthAsyncStorage storage; + + setUp(() { + storage = MemoryAuthAsyncStorage(); + }); + + test('returns null for a key that was never stored', () async { + expect(await storage.getItem(key: 'code-verifier'), isNull); + }); + + test('returns the value that was stored last', () async { + await storage.setItem(key: 'code-verifier', value: 'first'); + await storage.setItem(key: 'code-verifier', value: 'second'); + + expect(await storage.getItem(key: 'code-verifier'), 'second'); + }); + + test('forgets a removed key', () async { + await storage.setItem(key: 'code-verifier', value: 'value'); + await storage.removeItem(key: 'code-verifier'); + + expect(await storage.getItem(key: 'code-verifier'), isNull); + }); + + test('keeps the entries of two instances apart', () async { + await storage.setItem(key: 'code-verifier', value: 'value'); + + final other = MemoryAuthAsyncStorage(); + + expect(await other.getItem(key: 'code-verifier'), isNull); + }); +} diff --git a/packages/supabase_auth/test/mfa_enroll_test.dart b/packages/supabase_auth/test/mfa_enroll_test.dart index 6f0ff375c..bdb109ff0 100644 --- a/packages/supabase_auth/test/mfa_enroll_test.dart +++ b/packages/supabase_auth/test/mfa_enroll_test.dart @@ -4,6 +4,8 @@ import 'package:supabase_auth/supabase_auth.dart'; import 'package:http/http.dart'; import 'package:test/test.dart'; +import 'utils.dart'; + /// Records the body of every request it receives and answers with a minimal /// enroll payload, so we can inspect what the client actually sent without a /// live server. @@ -47,6 +49,7 @@ void main() { url: 'http://localhost', headers: {'apikey': 'anon-key'}, httpClient: http, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/src/auth_admin_custom_providers_api_test.dart b/packages/supabase_auth/test/src/auth_admin_custom_providers_api_test.dart index daacf0eb3..4dc9f3e47 100644 --- a/packages/supabase_auth/test/src/auth_admin_custom_providers_api_test.dart +++ b/packages/supabase_auth/test/src/auth_admin_custom_providers_api_test.dart @@ -57,6 +57,7 @@ void main() { 'apikey': serviceRoleToken, 'x-forwarded-for': '127.0.0.1', }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/src/auth_admin_mfa_api_test.dart b/packages/supabase_auth/test/src/auth_admin_mfa_api_test.dart index 14cb9bf0e..5ada6b574 100644 --- a/packages/supabase_auth/test/src/auth_admin_mfa_api_test.dart +++ b/packages/supabase_auth/test/src/auth_admin_mfa_api_test.dart @@ -33,6 +33,7 @@ void main() { 'apikey': serviceRoleToken, 'x-forwarded-for': '127.0.0.1', }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/src/auth_admin_oauth_api_test.dart b/packages/supabase_auth/test/src/auth_admin_oauth_api_test.dart index 9e20da2f2..4e6f54275 100644 --- a/packages/supabase_auth/test/src/auth_admin_oauth_api_test.dart +++ b/packages/supabase_auth/test/src/auth_admin_oauth_api_test.dart @@ -33,6 +33,7 @@ void main() { 'apikey': serviceRoleToken, 'x-forwarded-for': '127.0.0.1', }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/src/auth_mfa_api_test.dart b/packages/supabase_auth/test/src/auth_mfa_api_test.dart index 20f573f3c..cfcda86a9 100644 --- a/packages/supabase_auth/test/src/auth_mfa_api_test.dart +++ b/packages/supabase_auth/test/src/auth_mfa_api_test.dart @@ -61,6 +61,7 @@ void main() { 'apikey': anonToken, 'x-forwarded-for': '127.0.0.1', }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase_auth/test/src/auth_oauth_api_test.dart b/packages/supabase_auth/test/src/auth_oauth_api_test.dart index 426c2d969..cc32e8bdc 100644 --- a/packages/supabase_auth/test/src/auth_oauth_api_test.dart +++ b/packages/supabase_auth/test/src/auth_oauth_api_test.dart @@ -369,6 +369,7 @@ class AuthOauthApiFixture { 'apikey': _serviceRoleToken, 'x-forwarded-for': '127.0.0.1', }, + asyncStorage: TestAsyncStorage(), ); } diff --git a/packages/supabase_auth/test/utils.dart b/packages/supabase_auth/test/utils.dart index 0854d4ff4..f517a530f 100644 --- a/packages/supabase_auth/test/utils.dart +++ b/packages/supabase_auth/test/utils.dart @@ -90,20 +90,4 @@ const sessionDataUserId = '4d2583da-8de4-49d3-9cd1-37a9a74f55bd'; return (accessToken: accessToken, sessionString: sessionString); } -class TestAsyncStorage extends AuthAsyncStorage { - final Map _map = {}; - @override - Future getItem({required String key}) async { - return _map[key]; - } - - @override - Future removeItem({required String key}) async { - _map.remove(key); - } - - @override - Future setItem({required String key, required String value}) async { - _map[key] = value; - } -} +class TestAsyncStorage extends MemoryAuthAsyncStorage {} diff --git a/sdk-compliance.yaml b/sdk-compliance.yaml index 468b58cdd..da65fc667 100644 --- a/sdk-compliance.yaml +++ b/sdk-compliance.yaml @@ -1993,6 +1993,10 @@ features: - LocalStorage.initialize - LocalStorage.persistSession - LocalStorage.removePersistedSession + - MemoryAuthAsyncStorage + - MemoryAuthAsyncStorage.getItem + - MemoryAuthAsyncStorage.removeItem + - MemoryAuthAsyncStorage.setItem - SharedPreferencesAuthAsyncStorage - SharedPreferencesAuthAsyncStorage.SharedPreferencesAuthAsyncStorage - SharedPreferencesAuthAsyncStorage.getItem