Skip to content

Commit 5322ad8

Browse files
committed
fix: support custom access token
1 parent be998fd commit 5322ad8

File tree

4 files changed

+122
-84
lines changed

4 files changed

+122
-84
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.',
@@ -239,11 +239,13 @@ class SupabaseClient {
239239
return await accessToken!();
240240
}
241241

242-
if (_authInstance.currentSession?.isExpired ?? false) {
242+
final authInstance = _authInstance!;
243+
244+
if (authInstance.currentSession?.isExpired ?? false) {
243245
try {
244-
await _authInstance.refreshSession();
246+
await authInstance.refreshSession();
245247
} catch (error, stackTrace) {
246-
final expiresAt = _authInstance.currentSession?.expiresAt;
248+
final expiresAt = authInstance.currentSession?.expiresAt;
247249
if (expiresAt != null) {
248250
// Failed to refresh the token.
249251
final isExpiredWithoutMargin = DateTime.now()
@@ -260,14 +262,14 @@ class SupabaseClient {
260262
}
261263
}
262264
}
263-
return _authInstance.currentSession?.accessToken;
265+
return authInstance.currentSession?.accessToken;
264266
}
265267

266268
Future<void> dispose() async {
267269
_log.fine('Dispose SupabaseClient');
268270
await _authStateSubscription?.cancel();
269271
await _isolate.dispose();
270-
auth.dispose();
272+
_authInstance?.dispose();
271273
}
272274

273275
GoTrueClient _initSupabaseAuthClient({
@@ -332,6 +334,7 @@ class SupabaseClient {
332334
);
333335
}
334336

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

packages/supabase_flutter/lib/src/supabase.dart

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'dart:async';
22
import 'dart:developer' as dev;
3+
import 'dart:io';
34

45
import 'package:async/async.dart';
56
import 'package:flutter/foundation.dart';
7+
import 'package:flutter/widgets.dart';
68
import 'package:http/http.dart';
79
import 'package:logging/logging.dart';
810
import 'package:supabase/supabase.dart';
@@ -32,7 +34,7 @@ final _log = Logger('supabase.supabase_flutter');
3234
/// See also:
3335
///
3436
/// * [SupabaseAuth]
35-
class Supabase {
37+
class Supabase with WidgetsBindingObserver {
3638
/// Gets the current supabase instance.
3739
///
3840
/// An [AssertionError] is thrown if supabase isn't initialized yet.
@@ -126,15 +128,18 @@ class Supabase {
126128
accessToken: accessToken,
127129
);
128130

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

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-
);
136+
// Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
137+
// if still in progress
138+
_instance._restoreSessionCancellableOperation =
139+
CancelableOperation.fromFuture(
140+
supabaseAuth.recoverSession(),
141+
);
142+
}
138143

139144
_log.info('***** Supabase init completed *****');
140145

@@ -144,28 +149,33 @@ class Supabase {
144149
Supabase._();
145150
static final Supabase _instance = Supabase._();
146151

152+
static WidgetsBinding? get _widgetsBindingInstance => WidgetsBinding.instance;
153+
147154
bool _initialized = false;
148155

149156
/// The supabase client for this instance
150157
///
151158
/// Throws an error if [Supabase.initialize] was not called.
152159
late SupabaseClient client;
153160

154-
late SupabaseAuth _supabaseAuth;
161+
SupabaseAuth? _supabaseAuth;
155162

156163
bool _debugEnable = false;
157164

158165
/// Wraps the `recoverSession()` call so that it can be terminated when `dispose()` is called
159166
late CancelableOperation _restoreSessionCancellableOperation;
160167

168+
CancelableOperation<void>? _realtimeReconnectOperation;
169+
161170
StreamSubscription? _logSubscription;
162171

163172
/// Dispose the instance to free up resources.
164173
Future<void> dispose() async {
165174
await _restoreSessionCancellableOperation.cancel();
166175
_logSubscription?.cancel();
167176
client.dispose();
168-
_instance._supabaseAuth.dispose();
177+
_instance._supabaseAuth?.dispose();
178+
_widgetsBindingInstance?.removeObserver(this);
169179
_initialized = false;
170180
}
171181

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

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: 18 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,23 @@ 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+
accessToken: () async => 'my-access-token',
57+
);
58+
59+
// print(supabase.client.auth.runtimeType);
60+
61+
void accessAuth() {
62+
supabase.client.auth;
63+
}
64+
65+
expect(accessAuth, throwsA(isA<AuthException>()));
66+
});
67+
5168
group("Expired session", () {
5269
setUp(() async {
5370
mockAppLink();

0 commit comments

Comments
 (0)