Skip to content

Commit fc9ad2c

Browse files
authored
fix: Support custom access token (#1073)
1 parent ba5ccd4 commit fc9ad2c

File tree

4 files changed

+125
-85
lines changed

4 files changed

+125
-85
lines changed

packages/supabase/lib/src/supabase_client.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class SupabaseClient {
5252
final Client? _httpClient;
5353
late final Client _authHttpClient;
5454

55-
late final GoTrueClient _authInstance;
55+
GoTrueClient? _authInstance;
5656

5757
/// Supabase Functions allows you to deploy and invoke edge functions.
5858
late final FunctionsClient functions;
@@ -160,7 +160,7 @@ class SupabaseClient {
160160

161161
GoTrueClient get auth {
162162
if (accessToken == null) {
163-
return _authInstance;
163+
return _authInstance!;
164164
} else {
165165
throw AuthException(
166166
'Supabase Client is configured with the accessToken option, accessing supabase.auth is not possible.',
@@ -240,11 +240,13 @@ class SupabaseClient {
240240
return await accessToken!();
241241
}
242242

243-
if (_authInstance.currentSession?.isExpired ?? false) {
243+
final authInstance = _authInstance!;
244+
245+
if (authInstance.currentSession?.isExpired ?? false) {
244246
try {
245-
await _authInstance.refreshSession();
247+
await authInstance.refreshSession();
246248
} catch (error, stackTrace) {
247-
final expiresAt = _authInstance.currentSession?.expiresAt;
249+
final expiresAt = authInstance.currentSession?.expiresAt;
248250
if (expiresAt != null) {
249251
// Failed to refresh the token.
250252
final isExpiredWithoutMargin = DateTime.now()
@@ -261,14 +263,14 @@ class SupabaseClient {
261263
}
262264
}
263265
}
264-
return _authInstance.currentSession?.accessToken;
266+
return authInstance.currentSession?.accessToken;
265267
}
266268

267269
Future<void> dispose() async {
268270
_log.fine('Dispose SupabaseClient');
269271
await _authStateSubscription?.cancel();
270272
await _isolate.dispose();
271-
auth.dispose();
273+
_authInstance?.dispose();
272274
}
273275

274276
GoTrueClient _initSupabaseAuthClient({
@@ -333,6 +335,7 @@ class SupabaseClient {
333335
);
334336
}
335337

338+
/// Requires the `auth` instance, so no custom `accessToken` is allowed.
336339
Map<String, String> _getAuthHeaders() {
337340
final authBearer = auth.currentSession?.accessToken ?? _supabaseKey;
338341
final defaultHeaders = {

packages/supabase_flutter/lib/src/supabase.dart

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import 'dart:async';
2-
import 'dart:developer' as dev;
32

43
import 'package:async/async.dart';
54
import 'package:flutter/foundation.dart';
5+
import 'package:flutter/widgets.dart';
66
import 'package:http/http.dart';
77
import 'package:logging/logging.dart';
88
import 'package:supabase/supabase.dart';
@@ -32,7 +32,7 @@ final _log = Logger('supabase.supabase_flutter');
3232
/// See also:
3333
///
3434
/// * [SupabaseAuth]
35-
class Supabase {
35+
class Supabase with WidgetsBindingObserver {
3636
/// Gets the current supabase instance.
3737
///
3838
/// An [AssertionError] is thrown if supabase isn't initialized yet.
@@ -126,15 +126,18 @@ class Supabase {
126126
accessToken: accessToken,
127127
);
128128

129-
_instance._supabaseAuth = SupabaseAuth();
130-
await _instance._supabaseAuth.initialize(options: authOptions);
129+
if (accessToken == null) {
130+
final supabaseAuth = SupabaseAuth();
131+
_instance._supabaseAuth = supabaseAuth;
132+
await supabaseAuth.initialize(options: authOptions);
131133

132-
// Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
133-
// if still in progress
134-
_instance._restoreSessionCancellableOperation =
135-
CancelableOperation.fromFuture(
136-
_instance._supabaseAuth.recoverSession(),
137-
);
134+
// Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
135+
// if still in progress
136+
_instance._restoreSessionCancellableOperation =
137+
CancelableOperation.fromFuture(
138+
supabaseAuth.recoverSession(),
139+
);
140+
}
138141

139142
_log.info('***** Supabase init completed *****');
140143

@@ -144,28 +147,33 @@ class Supabase {
144147
Supabase._();
145148
static final Supabase _instance = Supabase._();
146149

150+
static WidgetsBinding? get _widgetsBindingInstance => WidgetsBinding.instance;
151+
147152
bool _initialized = false;
148153

149154
/// The supabase client for this instance
150155
///
151156
/// Throws an error if [Supabase.initialize] was not called.
152157
late SupabaseClient client;
153158

154-
late SupabaseAuth _supabaseAuth;
159+
SupabaseAuth? _supabaseAuth;
155160

156161
bool _debugEnable = false;
157162

158163
/// Wraps the `recoverSession()` call so that it can be terminated when `dispose()` is called
159164
late CancelableOperation _restoreSessionCancellableOperation;
160165

166+
CancelableOperation<void>? _realtimeReconnectOperation;
167+
161168
StreamSubscription? _logSubscription;
162169

163170
/// Dispose the instance to free up resources.
164171
Future<void> dispose() async {
165172
await _restoreSessionCancellableOperation.cancel();
166173
_logSubscription?.cancel();
167174
client.dispose();
168-
_instance._supabaseAuth.dispose();
175+
_instance._supabaseAuth?.dispose();
176+
_widgetsBindingInstance?.removeObserver(this);
169177
_initialized = false;
170178
}
171179

@@ -195,6 +203,76 @@ class Supabase {
195203
authOptions: authOptions,
196204
accessToken: accessToken,
197205
);
206+
_widgetsBindingInstance?.addObserver(this);
198207
_initialized = true;
199208
}
209+
210+
@override
211+
void didChangeAppLifecycleState(AppLifecycleState state) {
212+
switch (state) {
213+
case AppLifecycleState.resumed:
214+
onResumed();
215+
case AppLifecycleState.detached:
216+
case AppLifecycleState.paused:
217+
_realtimeReconnectOperation?.cancel();
218+
Supabase.instance.client.realtime.disconnect();
219+
default:
220+
}
221+
}
222+
223+
Future<void> onResumed() async {
224+
final realtime = Supabase.instance.client.realtime;
225+
if (realtime.channels.isNotEmpty) {
226+
if (realtime.connState == SocketStates.disconnecting) {
227+
// If the socket is still disconnecting from e.g.
228+
// [AppLifecycleState.paused] we should wait for it to finish before
229+
// reconnecting.
230+
231+
bool cancel = false;
232+
final connectFuture = realtime.conn!.sink.done.then(
233+
(_) async {
234+
// Make this connect cancelable so that it does not connect if the
235+
// disconnect took so long that the app is already in background
236+
// again.
237+
238+
if (!cancel) {
239+
// ignore: invalid_use_of_internal_member
240+
await realtime.connect();
241+
for (final channel in realtime.channels) {
242+
// ignore: invalid_use_of_internal_member
243+
if (channel.isJoined) {
244+
// ignore: invalid_use_of_internal_member
245+
channel.forceRejoin();
246+
}
247+
}
248+
}
249+
},
250+
onError: (error) {},
251+
);
252+
_realtimeReconnectOperation = CancelableOperation.fromFuture(
253+
connectFuture,
254+
onCancel: () => cancel = true,
255+
);
256+
} else if (!realtime.isConnected) {
257+
// Reconnect if the socket is currently not connected.
258+
// When coming from [AppLifecycleState.paused] this should be the case,
259+
// but when coming from [AppLifecycleState.inactive] no disconnect
260+
// happened and therefore connection should still be intanct and we
261+
// should not reconnect.
262+
263+
// ignore: invalid_use_of_internal_member
264+
await realtime.connect();
265+
for (final channel in realtime.channels) {
266+
// Only rejoin channels that think they are still joined and not
267+
// which were manually unsubscribed by the user while in background
268+
269+
// ignore: invalid_use_of_internal_member
270+
if (channel.isJoined) {
271+
// ignore: invalid_use_of_internal_member
272+
channel.forceRejoin();
273+
}
274+
}
275+
}
276+
}
277+
}
200278
}

packages/supabase_flutter/lib/src/supabase_auth.dart

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import 'dart:io' show Platform;
44
import 'dart:math';
55

66
import 'package:app_links/app_links.dart';
7-
import 'package:async/async.dart';
87
import 'package:flutter/foundation.dart' show kIsWeb;
98
import 'package:flutter/material.dart';
109
import 'package:flutter/services.dart';
@@ -31,8 +30,6 @@ class SupabaseAuth with WidgetsBindingObserver {
3130

3231
StreamSubscription<Uri?>? _deeplinkSubscription;
3332

34-
CancelableOperation<void>? _realtimeReconnectOperation;
35-
3633
final _appLinks = AppLinks();
3734

3835
final _log = Logger('supabase.supabase_flutter');
@@ -118,77 +115,18 @@ class SupabaseAuth with WidgetsBindingObserver {
118115
void didChangeAppLifecycleState(AppLifecycleState state) {
119116
switch (state) {
120117
case AppLifecycleState.resumed:
121-
onResumed();
118+
if (_autoRefreshToken) {
119+
Supabase.instance.client.auth.startAutoRefresh();
120+
}
122121
case AppLifecycleState.detached:
123122
case AppLifecycleState.paused:
124123
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
125124
Supabase.instance.client.auth.stopAutoRefresh();
126-
_realtimeReconnectOperation?.cancel();
127-
Supabase.instance.client.realtime.disconnect();
128125
}
129126
default:
130127
}
131128
}
132129

133-
Future<void> onResumed() async {
134-
if (_autoRefreshToken) {
135-
Supabase.instance.client.auth.startAutoRefresh();
136-
}
137-
final realtime = Supabase.instance.client.realtime;
138-
if (realtime.channels.isNotEmpty) {
139-
if (realtime.connState == SocketStates.disconnecting) {
140-
// If the socket is still disconnecting from e.g.
141-
// [AppLifecycleState.paused] we should wait for it to finish before
142-
// reconnecting.
143-
144-
bool cancel = false;
145-
final connectFuture = realtime.conn!.sink.done.then(
146-
(_) async {
147-
// Make this connect cancelable so that it does not connect if the
148-
// disconnect took so long that the app is already in background
149-
// again.
150-
151-
if (!cancel) {
152-
// ignore: invalid_use_of_internal_member
153-
await realtime.connect();
154-
for (final channel in realtime.channels) {
155-
// ignore: invalid_use_of_internal_member
156-
if (channel.isJoined) {
157-
// ignore: invalid_use_of_internal_member
158-
channel.forceRejoin();
159-
}
160-
}
161-
}
162-
},
163-
onError: (error) {},
164-
);
165-
_realtimeReconnectOperation = CancelableOperation.fromFuture(
166-
connectFuture,
167-
onCancel: () => cancel = true,
168-
);
169-
} else if (!realtime.isConnected) {
170-
// Reconnect if the socket is currently not connected.
171-
// When coming from [AppLifecycleState.paused] this should be the case,
172-
// but when coming from [AppLifecycleState.inactive] no disconnect
173-
// happened and therefore connection should still be intanct and we
174-
// should not reconnect.
175-
176-
// ignore: invalid_use_of_internal_member
177-
await realtime.connect();
178-
for (final channel in realtime.channels) {
179-
// Only rejoin channels that think they are still joined and not
180-
// which were manually unsubscribed by the user while in background
181-
182-
// ignore: invalid_use_of_internal_member
183-
if (channel.isJoined) {
184-
// ignore: invalid_use_of_internal_member
185-
channel.forceRejoin();
186-
}
187-
}
188-
}
189-
}
190-
}
191-
192130
void _onAuthStateChange(AuthChangeEvent event, Session? session) {
193131
if (session != null) {
194132
_localStorage.persistSession(jsonEncode(session.toJson()));

packages/supabase_flutter/test/supabase_flutter_test.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ void main() {
99
const supabaseKey = '';
1010
tearDown(() async => await Supabase.instance.dispose());
1111

12-
group("Valid session", () {
12+
group("Initialize", () {
1313
setUp(() async {
1414
mockAppLink();
1515
// Initialize the Supabase singleton
@@ -48,6 +48,27 @@ void main() {
4848
});
4949
});
5050

51+
test('with custom access token', () async {
52+
final supabase = await Supabase.initialize(
53+
url: supabaseUrl,
54+
anonKey: supabaseUrl,
55+
debug: false,
56+
authOptions: FlutterAuthClientOptions(
57+
localStorage: MockLocalStorage(),
58+
pkceAsyncStorage: MockAsyncStorage(),
59+
),
60+
accessToken: () async => 'my-access-token',
61+
);
62+
63+
// print(supabase.client.auth.runtimeType);
64+
65+
void accessAuth() {
66+
supabase.client.auth;
67+
}
68+
69+
expect(accessAuth, throwsA(isA<AuthException>()));
70+
});
71+
5172
group("Expired session", () {
5273
setUp(() async {
5374
mockAppLink();

0 commit comments

Comments
 (0)