Skip to content

Commit

Permalink
fix: Support custom access token (#1073)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vinzent03 authored Nov 11, 2024
1 parent ba5ccd4 commit fc9ad2c
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 85 deletions.
17 changes: 10 additions & 7 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class SupabaseClient {
final Client? _httpClient;
late final Client _authHttpClient;

late final GoTrueClient _authInstance;
GoTrueClient? _authInstance;

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

GoTrueClient get auth {
if (accessToken == null) {
return _authInstance;
return _authInstance!;
} else {
throw AuthException(
'Supabase Client is configured with the accessToken option, accessing supabase.auth is not possible.',
Expand Down Expand Up @@ -240,11 +240,13 @@ class SupabaseClient {
return await accessToken!();
}

if (_authInstance.currentSession?.isExpired ?? false) {
final authInstance = _authInstance!;

if (authInstance.currentSession?.isExpired ?? false) {
try {
await _authInstance.refreshSession();
await authInstance.refreshSession();
} catch (error, stackTrace) {
final expiresAt = _authInstance.currentSession?.expiresAt;
final expiresAt = authInstance.currentSession?.expiresAt;
if (expiresAt != null) {
// Failed to refresh the token.
final isExpiredWithoutMargin = DateTime.now()
Expand All @@ -261,14 +263,14 @@ class SupabaseClient {
}
}
}
return _authInstance.currentSession?.accessToken;
return authInstance.currentSession?.accessToken;
}

Future<void> dispose() async {
_log.fine('Dispose SupabaseClient');
await _authStateSubscription?.cancel();
await _isolate.dispose();
auth.dispose();
_authInstance?.dispose();
}

GoTrueClient _initSupabaseAuthClient({
Expand Down Expand Up @@ -333,6 +335,7 @@ class SupabaseClient {
);
}

/// Requires the `auth` instance, so no custom `accessToken` is allowed.
Map<String, String> _getAuthHeaders() {
final authBearer = auth.currentSession?.accessToken ?? _supabaseKey;
final defaultHeaders = {
Expand Down
102 changes: 90 additions & 12 deletions packages/supabase_flutter/lib/src/supabase.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:async';
import 'dart:developer' as dev;

import 'package:async/async.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart';
import 'package:logging/logging.dart';
import 'package:supabase/supabase.dart';
Expand Down Expand Up @@ -32,7 +32,7 @@ final _log = Logger('supabase.supabase_flutter');
/// See also:
///
/// * [SupabaseAuth]
class Supabase {
class Supabase with WidgetsBindingObserver {
/// Gets the current supabase instance.
///
/// An [AssertionError] is thrown if supabase isn't initialized yet.
Expand Down Expand Up @@ -126,15 +126,18 @@ class Supabase {
accessToken: accessToken,
);

_instance._supabaseAuth = SupabaseAuth();
await _instance._supabaseAuth.initialize(options: authOptions);
if (accessToken == null) {
final supabaseAuth = SupabaseAuth();
_instance._supabaseAuth = supabaseAuth;
await supabaseAuth.initialize(options: authOptions);

// Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
// if still in progress
_instance._restoreSessionCancellableOperation =
CancelableOperation.fromFuture(
_instance._supabaseAuth.recoverSession(),
);
// Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
// if still in progress
_instance._restoreSessionCancellableOperation =
CancelableOperation.fromFuture(
supabaseAuth.recoverSession(),
);
}

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

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

static WidgetsBinding? get _widgetsBindingInstance => WidgetsBinding.instance;

bool _initialized = false;

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

late SupabaseAuth _supabaseAuth;
SupabaseAuth? _supabaseAuth;

bool _debugEnable = false;

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

CancelableOperation<void>? _realtimeReconnectOperation;

StreamSubscription? _logSubscription;

/// Dispose the instance to free up resources.
Future<void> dispose() async {
await _restoreSessionCancellableOperation.cancel();
_logSubscription?.cancel();
client.dispose();
_instance._supabaseAuth.dispose();
_instance._supabaseAuth?.dispose();
_widgetsBindingInstance?.removeObserver(this);
_initialized = false;
}

Expand Down Expand Up @@ -195,6 +203,76 @@ class Supabase {
authOptions: authOptions,
accessToken: accessToken,
);
_widgetsBindingInstance?.addObserver(this);
_initialized = true;
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
onResumed();
case AppLifecycleState.detached:
case AppLifecycleState.paused:
_realtimeReconnectOperation?.cancel();
Supabase.instance.client.realtime.disconnect();
default:
}
}

Future<void> onResumed() async {
final realtime = Supabase.instance.client.realtime;
if (realtime.channels.isNotEmpty) {
if (realtime.connState == SocketStates.disconnecting) {
// If the socket is still disconnecting from e.g.
// [AppLifecycleState.paused] we should wait for it to finish before
// reconnecting.

bool cancel = false;
final connectFuture = realtime.conn!.sink.done.then(
(_) async {
// Make this connect cancelable so that it does not connect if the
// disconnect took so long that the app is already in background
// again.

if (!cancel) {
// ignore: invalid_use_of_internal_member
await realtime.connect();
for (final channel in realtime.channels) {
// ignore: invalid_use_of_internal_member
if (channel.isJoined) {
// ignore: invalid_use_of_internal_member
channel.forceRejoin();
}
}
}
},
onError: (error) {},
);
_realtimeReconnectOperation = CancelableOperation.fromFuture(
connectFuture,
onCancel: () => cancel = true,
);
} else if (!realtime.isConnected) {
// Reconnect if the socket is currently not connected.
// When coming from [AppLifecycleState.paused] this should be the case,
// but when coming from [AppLifecycleState.inactive] no disconnect
// happened and therefore connection should still be intanct and we
// should not reconnect.

// ignore: invalid_use_of_internal_member
await realtime.connect();
for (final channel in realtime.channels) {
// Only rejoin channels that think they are still joined and not
// which were manually unsubscribed by the user while in background

// ignore: invalid_use_of_internal_member
if (channel.isJoined) {
// ignore: invalid_use_of_internal_member
channel.forceRejoin();
}
}
}
}
}
}
68 changes: 3 additions & 65 deletions packages/supabase_flutter/lib/src/supabase_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'dart:io' show Platform;
import 'dart:math';

import 'package:app_links/app_links.dart';
import 'package:async/async.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Expand All @@ -31,8 +30,6 @@ class SupabaseAuth with WidgetsBindingObserver {

StreamSubscription<Uri?>? _deeplinkSubscription;

CancelableOperation<void>? _realtimeReconnectOperation;

final _appLinks = AppLinks();

final _log = Logger('supabase.supabase_flutter');
Expand Down Expand Up @@ -118,77 +115,18 @@ class SupabaseAuth with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
onResumed();
if (_autoRefreshToken) {
Supabase.instance.client.auth.startAutoRefresh();
}
case AppLifecycleState.detached:
case AppLifecycleState.paused:
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
Supabase.instance.client.auth.stopAutoRefresh();
_realtimeReconnectOperation?.cancel();
Supabase.instance.client.realtime.disconnect();
}
default:
}
}

Future<void> onResumed() async {
if (_autoRefreshToken) {
Supabase.instance.client.auth.startAutoRefresh();
}
final realtime = Supabase.instance.client.realtime;
if (realtime.channels.isNotEmpty) {
if (realtime.connState == SocketStates.disconnecting) {
// If the socket is still disconnecting from e.g.
// [AppLifecycleState.paused] we should wait for it to finish before
// reconnecting.

bool cancel = false;
final connectFuture = realtime.conn!.sink.done.then(
(_) async {
// Make this connect cancelable so that it does not connect if the
// disconnect took so long that the app is already in background
// again.

if (!cancel) {
// ignore: invalid_use_of_internal_member
await realtime.connect();
for (final channel in realtime.channels) {
// ignore: invalid_use_of_internal_member
if (channel.isJoined) {
// ignore: invalid_use_of_internal_member
channel.forceRejoin();
}
}
}
},
onError: (error) {},
);
_realtimeReconnectOperation = CancelableOperation.fromFuture(
connectFuture,
onCancel: () => cancel = true,
);
} else if (!realtime.isConnected) {
// Reconnect if the socket is currently not connected.
// When coming from [AppLifecycleState.paused] this should be the case,
// but when coming from [AppLifecycleState.inactive] no disconnect
// happened and therefore connection should still be intanct and we
// should not reconnect.

// ignore: invalid_use_of_internal_member
await realtime.connect();
for (final channel in realtime.channels) {
// Only rejoin channels that think they are still joined and not
// which were manually unsubscribed by the user while in background

// ignore: invalid_use_of_internal_member
if (channel.isJoined) {
// ignore: invalid_use_of_internal_member
channel.forceRejoin();
}
}
}
}
}

void _onAuthStateChange(AuthChangeEvent event, Session? session) {
if (session != null) {
_localStorage.persistSession(jsonEncode(session.toJson()));
Expand Down
23 changes: 22 additions & 1 deletion packages/supabase_flutter/test/supabase_flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ void main() {
const supabaseKey = '';
tearDown(() async => await Supabase.instance.dispose());

group("Valid session", () {
group("Initialize", () {
setUp(() async {
mockAppLink();
// Initialize the Supabase singleton
Expand Down Expand Up @@ -48,6 +48,27 @@ void main() {
});
});

test('with custom access token', () async {
final supabase = await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseUrl,
debug: false,
authOptions: FlutterAuthClientOptions(
localStorage: MockLocalStorage(),
pkceAsyncStorage: MockAsyncStorage(),
),
accessToken: () async => 'my-access-token',
);

// print(supabase.client.auth.runtimeType);

void accessAuth() {
supabase.client.auth;
}

expect(accessAuth, throwsA(isA<AuthException>()));
});

group("Expired session", () {
setUp(() async {
mockAppLink();
Expand Down

0 comments on commit fc9ad2c

Please sign in to comment.