Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 +538,8 @@ the upgrade still completes after it.
The pkce flow, which is the default, keeps its code verifiers in the `asyncStorage` of
`AuthClientOptions`. A `SupabaseClient` or `AuthClient` created without one used to fail an assert,
so `dart run` and AOT builds only failed at the first pkce call. Both now throw an `ArgumentError`
when they are created, in debug and release builds alike. So does an `AuthClient` that persists the
session without an `asyncStorage`.
when they are created, in debug and release builds alike. So does either client when it persists
the session without an `asyncStorage`.

`Supabase.initialize` configures a storage for you. A standalone script or a server builds its own
client and has to pass one, or use the implicit flow:
Expand Down
6 changes: 5 additions & 1 deletion examples/realtime_room/integration_test/room_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ void main() {
await _pumpUntil(tester, find.text('hello from alice'));

// A second client stands in for another person in the room.
final bobClient = SupabaseClient(supabaseUrl, supabasePublishableKey);
final bobClient = SupabaseClient(
supabaseUrl,
supabasePublishableKey,
authOptions: AuthClientOptions(asyncStorage: MemoryAuthAsyncStorage()),
);
addTearDown(bobClient.dispose);
final bobRepository = RoomRepository(bobClient);
final bob = bobRepository.joinRoom(username: 'bob');
Expand Down
6 changes: 5 additions & 1 deletion packages/supabase/example/web/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import 'package:web/web.dart' as web;
void main() {
const supabaseUrl = 'YOUR_SUPABASE_URL';
const supabaseKey = 'YOUR_SUPABASE_KEY';
final supabase = SupabaseClient(supabaseUrl, supabaseKey);
final supabase = SupabaseClient(
supabaseUrl,
supabaseKey,
authOptions: AuthClientOptions(asyncStorage: MemoryAuthAsyncStorage()),
);

final element = web.document.querySelector('#output');
element?.textContent = 'Supabase Dart Web Example';
Expand Down
35 changes: 19 additions & 16 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ import 'trace_http_client.dart';
/// 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. Without an `asyncStorage` the
/// constructor throws an [ArgumentError], unless the implicit flow or an
/// [accessToken] is used.
/// constructor throws an [ArgumentError] for the pkce flow or a persisted
/// session, unless an [accessToken] is used.
/// {@endtemplate}
class SupabaseClient {
/// {@macro supabase_client}
Expand Down Expand Up @@ -104,12 +104,10 @@ class SupabaseClient {
_jsonCodec = jsonCodec ?? (YAJsonIsolate()..initialize()),
_ownsJsonCodec = jsonCodec == null {
if (accessToken == null &&
authOptions.authFlowType == AuthFlowType.pkce &&
authOptions.asyncStorage == null) {
if (_ownsJsonCodec) {
unawaited(_jsonCodec.dispose());
}
throw ArgumentError(_missingAsyncStorageMessage());
authOptions.asyncStorage == null &&
(authOptions.authFlowType == AuthFlowType.pkce ||
authOptions.persistSession)) {
throw ArgumentError(_missingAsyncStorageMessage(authOptions));
}
final baseHttpClient = httpClient ?? createDefaultHttpClient();
final tracedHttpClient = tracePropagationOptions == null
Expand Down Expand Up @@ -498,14 +496,19 @@ class SupabaseClient {
}
}

String _missingAsyncStorageMessage() {
final message =
'SupabaseClient uses the pkce flow by default, which needs an '
'asyncStorage to keep its code verifiers in. Pass one through '
'authOptions, for example '
'`authOptions: AuthClientOptions(asyncStorage: '
'MemoryAuthAsyncStorage())` when the verifiers do not need to outlive '
'the process, or set `authFlowType: AuthFlowType.implicit`.';
String _missingAsyncStorageMessage(AuthClientOptions authOptions) {
final message = authOptions.authFlowType == AuthFlowType.pkce
? 'SupabaseClient uses the pkce flow by default, which needs an '
'asyncStorage to keep its code verifiers in. Pass one through '
'authOptions, for example '
'`authOptions: AuthClientOptions(asyncStorage: '
'MemoryAuthAsyncStorage())` when the verifiers do not need to '
'outlive the process, or set '
'`authFlowType: AuthFlowType.implicit`.'
: 'SupabaseClient needs an asyncStorage to persist the session in. Pass '
'one through authOptions, for example '
'`authOptions: AuthClientOptions(asyncStorage: '
'MemoryAuthAsyncStorage())`, or set `persistSession: false`.';
if (Zone.current[#test.invoker] == null) {
return message;
}
Expand Down
20 changes: 20 additions & 0 deletions packages/supabase/test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,26 @@ void main() {
);
});

test('a persisted session throws when no asyncStorage is given', () {
expect(
() => real.SupabaseClient(
'http://localhost:1',
'supabaseKey',
authOptions: const AuthClientOptions(
authFlowType: AuthFlowType.implicit,
persistSession: true,
),
),
throwsA(
isA<ArgumentError>().having(
(error) => error.message,
'message',
contains('SupabaseClient needs an asyncStorage to persist'),
),
),
);
});

test('the implicit flow needs no asyncStorage', () async {
final supabase = real.SupabaseClient(
'http://localhost:1',
Expand Down
20 changes: 20 additions & 0 deletions packages/supabase_auth/test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,26 @@ void main() {
returnsNormally,
);
});

test('throws an AuthException when exchanging a code', () async {
final client = AuthClient(
url: authUrl,
headers: {'apikey': anonToken},
flowType: AuthFlowType.implicit,
);
addTearDown(client.dispose);

await expectLater(
() => client.exchangeCodeForSession('code'),
throwsA(
isA<AuthException>().having(
(error) => error.message,
'message',
contains('Exchanging a code needs the asyncStorage'),
),
),
);
});
});
}

Expand Down
Loading