From 0ae2b7149286d4719bbaafdf1ecb3d76c8b4eb7a Mon Sep 17 00:00:00 2001
From: Vinzent <vinzent03@proton.me>
Date: Thu, 21 Nov 2024 21:38:31 +0100
Subject: [PATCH 1/5] refactor: store session and pkce in the same storage in
 gotrue_client

---
 packages/gotrue/lib/src/gotrue_client.dart    | 136 +++++++++++++-----
 .../lib/src/types/gotrue_async_storage.dart   |   3 +
 .../supabase/lib/src/supabase_client.dart     |  21 ++-
 .../lib/src/supabase_client_options.dart      |   9 ++
 .../src/flutter_go_true_client_options.dart   |  13 ++
 .../lib/src/local_storage.dart                |  49 +++++++
 .../supabase_flutter/lib/src/supabase.dart    |  31 ++--
 .../lib/src/supabase_auth.dart                |  63 --------
 8 files changed, 201 insertions(+), 124 deletions(-)

diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart
index 63f5c85f..b93db808 100644
--- a/packages/gotrue/lib/src/gotrue_client.dart
+++ b/packages/gotrue/lib/src/gotrue_client.dart
@@ -87,8 +87,14 @@ class GoTrueClient {
   Stream<AuthState> get onAuthStateChangeSync =>
       _onAuthStateChangeControllerSync.stream;
 
+  final Completer<void> _initalizedStorage = Completer<void>();
+
   final AuthFlowType _flowType;
 
+  final bool _persistSession;
+
+  final String _storageKey;
+
   final _log = Logger('supabase.auth');
 
   /// Proxy to the web BroadcastChannel API. Should be null on non-web platforms.
@@ -101,8 +107,10 @@ class GoTrueClient {
     String? url,
     Map<String, String>? headers,
     bool? autoRefreshToken,
+    bool? persistSession,
     Client? httpClient,
     GotrueAsyncStorage? asyncStorage,
+    String? storageKey,
     AuthFlowType flowType = AuthFlowType.pkce,
   })  : _url = url ?? Constants.defaultGotrueUrl,
         _headers = {
@@ -111,7 +119,9 @@ class GoTrueClient {
         },
         _httpClient = httpClient,
         _asyncStorage = asyncStorage,
-        _flowType = flowType {
+        _flowType = flowType,
+        _persistSession = persistSession ?? false,
+        _storageKey = storageKey ?? Constants.defaultStorageKey {
     _autoRefreshToken = autoRefreshToken ?? true;
 
     final gotrueUrl = url ?? Constants.defaultGotrueUrl;
@@ -127,10 +137,19 @@ class GoTrueClient {
       client: this,
       fetch: _fetch,
     );
+
+    assert(asyncStorage != null || !_persistSession,
+        'You need to provide asyncStorage to persist session.');
+    if (asyncStorage != null) {
+      _initalizedStorage.complete(
+          asyncStorage.initialize().catchError((e) => notifyException(e)));
+    }
+
     if (_autoRefreshToken) {
       startAutoRefresh();
     }
 
+    _initialize();
     _mayStartBroadcastChannel();
   }
 
@@ -148,6 +167,37 @@ class GoTrueClient {
   /// Returns the current session, if any;
   Session? get currentSession => _currentSession;
 
+  /// This method should not throw as it is called from the constructor.
+  Future<void> _initialize() async {
+    try {
+      if (_persistSession && _asyncStorage != null) {
+        await _initalizedStorage.future;
+        final jsonStr = await _asyncStorage!.getItem(key: _storageKey);
+        var shouldEmitInitialSession = true;
+        if (jsonStr != null) {
+          await setInitialSession(jsonStr);
+          shouldEmitInitialSession = false;
+
+          // Only try to recover session if the session got set in [setInitialSession]
+          // because if not the session is missing data and already notified an
+          // exception.
+          if (currentSession != null) {
+            // [notifyException] gets already called here if needed, so we can
+            // catch any error.
+            recoverSession(jsonStr).then((_) {}, onError: (_) {});
+          }
+        }
+        if (shouldEmitInitialSession) {
+          // Emit a null session if the user did not have persisted session
+          notifyAllSubscribers(AuthChangeEvent.initialSession);
+        }
+      }
+    } catch (error, stackTrace) {
+      _log.warning('Error while loading initial session', error, stackTrace);
+      notifyException(error, stackTrace);
+    }
+  }
+
   /// Creates a new anonymous user.
   ///
   /// Returns An `AuthResponse` with a session where the `is_anonymous` claim
@@ -172,7 +222,7 @@ class GoTrueClient {
 
     final session = authResponse.session;
     if (session != null) {
-      _saveSession(session);
+      await _saveSession(session);
       notifyAllSubscribers(AuthChangeEvent.signedIn);
     }
 
@@ -217,9 +267,8 @@ class GoTrueClient {
         assert(_asyncStorage != null,
             'You need to provide asyncStorage to perform pkce flow.');
         final codeVerifier = generatePKCEVerifier();
-        await _asyncStorage!.setItem(
-            key: '${Constants.defaultStorageKey}-code-verifier',
-            value: codeVerifier);
+        await _asyncStorage!
+            .setItem(key: '$_storageKey-code-verifier', value: codeVerifier);
         codeChallenge = generatePKCEChallenge(codeVerifier);
       }
 
@@ -259,7 +308,7 @@ class GoTrueClient {
 
     final session = authResponse.session;
     if (session != null) {
-      _saveSession(session);
+      await _saveSession(session);
       notifyAllSubscribers(AuthChangeEvent.signedIn);
     }
 
@@ -312,7 +361,7 @@ class GoTrueClient {
     final authResponse = AuthResponse.fromJson(response);
 
     if (authResponse.session?.accessToken != null) {
-      _saveSession(authResponse.session!);
+      await _saveSession(authResponse.session!);
       notifyAllSubscribers(AuthChangeEvent.signedIn);
     }
     return authResponse;
@@ -339,8 +388,8 @@ class GoTrueClient {
     assert(_asyncStorage != null,
         'You need to provide asyncStorage to perform pkce flow.');
 
-    final codeVerifierRawString = await _asyncStorage!
-        .getItem(key: '${Constants.defaultStorageKey}-code-verifier');
+    final codeVerifierRawString =
+        await _asyncStorage!.getItem(key: '$_storageKey-code-verifier');
     if (codeVerifierRawString == null) {
       throw AuthException('Code verifier could not be found in local storage.');
     }
@@ -363,14 +412,13 @@ class GoTrueClient {
       ),
     );
 
-    await _asyncStorage!
-        .removeItem(key: '${Constants.defaultStorageKey}-code-verifier');
+    await _asyncStorage!.removeItem(key: '$_storageKey-code-verifier');
 
     final authSessionUrlResponse = AuthSessionUrlResponse(
         session: Session.fromJson(response)!, redirectType: redirectType?.name);
 
     final session = authSessionUrlResponse.session;
-    _saveSession(session);
+    await _saveSession(session);
     if (redirectType == AuthChangeEvent.passwordRecovery) {
       notifyAllSubscribers(AuthChangeEvent.passwordRecovery);
     } else {
@@ -434,7 +482,7 @@ class GoTrueClient {
       );
     }
 
-    _saveSession(authResponse.session!);
+    await _saveSession(authResponse.session!);
     notifyAllSubscribers(AuthChangeEvent.signedIn);
 
     return authResponse;
@@ -472,9 +520,8 @@ class GoTrueClient {
         assert(_asyncStorage != null,
             'You need to provide asyncStorage to perform pkce flow.');
         final codeVerifier = generatePKCEVerifier();
-        await _asyncStorage!.setItem(
-            key: '${Constants.defaultStorageKey}-code-verifier',
-            value: codeVerifier);
+        await _asyncStorage!
+            .setItem(key: '$_storageKey-code-verifier', value: codeVerifier);
         codeChallenge = generatePKCEChallenge(codeVerifier);
       }
       await _fetch.request(
@@ -559,7 +606,7 @@ class GoTrueClient {
       );
     }
 
-    _saveSession(authResponse.session!);
+    await _saveSession(authResponse.session!);
     notifyAllSubscribers(type == OtpType.recovery
         ? AuthChangeEvent.passwordRecovery
         : AuthChangeEvent.signedIn);
@@ -594,9 +641,8 @@ class GoTrueClient {
       assert(_asyncStorage != null,
           'You need to provide asyncStorage to perform pkce flow.');
       final codeVerifier = generatePKCEVerifier();
-      await _asyncStorage!.setItem(
-          key: '${Constants.defaultStorageKey}-code-verifier',
-          value: codeVerifier);
+      await _asyncStorage!
+          .setItem(key: '$_storageKey-code-verifier', value: codeVerifier);
       codeChallenge = generatePKCEChallenge(codeVerifier);
       codeChallengeMethod = codeVerifier == codeChallenge ? 'plain' : 's256';
     }
@@ -832,7 +878,7 @@ class GoTrueClient {
     final redirectType = url.queryParameters['type'];
 
     if (storeSession == true) {
-      _saveSession(session);
+      await _saveSession(session);
       if (redirectType == 'recovery') {
         notifyAllSubscribers(AuthChangeEvent.passwordRecovery);
       } else {
@@ -855,9 +901,8 @@ class GoTrueClient {
     final accessToken = currentSession?.accessToken;
 
     if (scope != SignOutScope.others) {
-      _removeSession();
-      await _asyncStorage?.removeItem(
-          key: '${Constants.defaultStorageKey}-code-verifier');
+      await _removeSession();
+      await _asyncStorage?.removeItem(key: '$_storageKey-code-verifier');
       notifyAllSubscribers(AuthChangeEvent.signedOut);
     }
 
@@ -889,7 +934,7 @@ class GoTrueClient {
           'You need to provide asyncStorage to perform pkce flow.');
       final codeVerifier = generatePKCEVerifier();
       await _asyncStorage!.setItem(
-        key: '${Constants.defaultStorageKey}-code-verifier',
+        key: '$_storageKey-code-verifier',
         value: '$codeVerifier/${AuthChangeEvent.passwordRecovery.name}',
       );
       codeChallenge = generatePKCEChallenge(codeVerifier);
@@ -978,9 +1023,7 @@ class GoTrueClient {
       if (session == null) {
         _log.warning("Can't recover session from string, session is null");
         await signOut();
-        throw notifyException(
-          AuthException('Current session is missing data.'),
-        );
+        throw AuthException('Session to restore is missing data.');
       }
 
       if (session.isExpired) {
@@ -995,7 +1038,7 @@ class GoTrueClient {
       } else {
         final shouldEmitEvent = _currentSession == null ||
             _currentSession?.user.id != session.user.id;
-        _saveSession(session);
+        await _saveSession(session);
 
         if (shouldEmitEvent) {
           notifyAllSubscribers(AuthChangeEvent.tokenRefreshed);
@@ -1126,7 +1169,7 @@ class GoTrueClient {
           'You need to provide asyncStorage to perform pkce flow.');
       final codeVerifier = generatePKCEVerifier();
       await _asyncStorage!.setItem(
-        key: '${Constants.defaultStorageKey}-code-verifier',
+        key: '$_storageKey-code-verifier',
         value: codeVerifier,
       );
 
@@ -1146,17 +1189,36 @@ class GoTrueClient {
   }
 
   /// set currentSession and currentUser
-  void _saveSession(Session session) {
+  Future<void> _saveSession(Session session) async {
     _log.finest('Saving session: $session');
     _log.fine('Saving session');
     _currentSession = session;
     _currentUser = session.user;
+
+    if (_persistSession && _asyncStorage != null) {
+      if (!_initalizedStorage.isCompleted) {
+        await _initalizedStorage.future;
+      }
+      _asyncStorage!.setItem(
+        key: _storageKey,
+        value: jsonEncode(session.toJson()),
+      );
+    }
   }
 
-  void _removeSession() {
+  Future<void> _removeSession() async {
     _log.fine('Removing session');
     _currentSession = null;
     _currentUser = null;
+
+    if (_persistSession && _asyncStorage != null) {
+      if (!_initalizedStorage.isCompleted) {
+        await _initalizedStorage.future;
+      }
+      _asyncStorage!.removeItem(
+        key: _storageKey,
+      );
+    }
   }
 
   void _mayStartBroadcastChannel() {
@@ -1170,7 +1232,7 @@ class GoTrueClient {
       try {
         _broadcastChannel = web.getBroadcastChannel(broadcastKey);
         _broadcastChannelSubscription =
-            _broadcastChannel?.onMessage.listen((messageEvent) {
+            _broadcastChannel?.onMessage.listen((messageEvent) async {
           final rawEvent = messageEvent['event'];
           _log.finest('Received broadcast message: $messageEvent');
           _log.info('Received broadcast event: $rawEvent');
@@ -1195,9 +1257,9 @@ class GoTrueClient {
               session = Session.fromJson(messageEvent['session']);
             }
             if (session != null) {
-              _saveSession(session);
+              await _saveSession(session);
             } else {
-              _removeSession();
+              await _removeSession();
             }
             notifyAllSubscribers(event, session: session, broadcast: false);
           }
@@ -1247,14 +1309,14 @@ class GoTrueClient {
         throw AuthSessionMissingException();
       }
 
-      _saveSession(session);
+      await _saveSession(session);
       notifyAllSubscribers(AuthChangeEvent.tokenRefreshed);
 
       _refreshTokenCompleter?.complete(data);
       return data;
     } on AuthException catch (error, stack) {
       if (error is! AuthRetryableFetchException) {
-        _removeSession();
+        await _removeSession();
         notifyAllSubscribers(AuthChangeEvent.signedOut);
       } else {
         notifyException(error, stack);
diff --git a/packages/gotrue/lib/src/types/gotrue_async_storage.dart b/packages/gotrue/lib/src/types/gotrue_async_storage.dart
index 29ce2b15..b4a1304e 100644
--- a/packages/gotrue/lib/src/types/gotrue_async_storage.dart
+++ b/packages/gotrue/lib/src/types/gotrue_async_storage.dart
@@ -2,6 +2,9 @@
 abstract class GotrueAsyncStorage {
   const GotrueAsyncStorage();
 
+  /// May be implemented to allow for initialization of the storage before use.
+  Future<void> initialize() async {}
+
   /// Retrieves an item asynchronously from the storage with the key.
   Future<String?> getItem({required String key});
 
diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart
index 4f500b8b..5382ec9e 100644
--- a/packages/supabase/lib/src/supabase_client.dart
+++ b/packages/supabase/lib/src/supabase_client.dart
@@ -137,11 +137,7 @@ class SupabaseClient {
         },
         _httpClient = httpClient,
         _isolate = isolate ?? (YAJsonIsolate()..initialize()) {
-    _authInstance = _initSupabaseAuthClient(
-      autoRefreshToken: authOptions.autoRefreshToken,
-      gotrueAsyncStorage: authOptions.pkceAsyncStorage,
-      authFlowType: authOptions.authFlowType,
-    );
+    _authInstance = _initSupabaseAuthClient(authOptions: authOptions);
     _authHttpClient =
         AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken);
     rest = _initRestClient();
@@ -273,11 +269,8 @@ class SupabaseClient {
     _authInstance?.dispose();
   }
 
-  GoTrueClient _initSupabaseAuthClient({
-    bool? autoRefreshToken,
-    required GotrueAsyncStorage? gotrueAsyncStorage,
-    required AuthFlowType authFlowType,
-  }) {
+  GoTrueClient _initSupabaseAuthClient(
+      {required AuthClientOptions authOptions}) {
     final authHeaders = {...headers};
     authHeaders['apikey'] = _supabaseKey;
     authHeaders['Authorization'] = 'Bearer $_supabaseKey';
@@ -285,10 +278,12 @@ class SupabaseClient {
     return GoTrueClient(
       url: _authUrl,
       headers: authHeaders,
-      autoRefreshToken: autoRefreshToken,
+      autoRefreshToken: authOptions.autoRefreshToken,
       httpClient: _httpClient,
-      asyncStorage: gotrueAsyncStorage,
-      flowType: authFlowType,
+      asyncStorage: authOptions.asyncStorage,
+      storageKey: authOptions.storageKey,
+      persistSession: authOptions.persistSession,
+      flowType: authOptions.authFlowType,
     );
   }
 
diff --git a/packages/supabase/lib/src/supabase_client_options.dart b/packages/supabase/lib/src/supabase_client_options.dart
index 3587e618..fe326a4b 100644
--- a/packages/supabase/lib/src/supabase_client_options.dart
+++ b/packages/supabase/lib/src/supabase_client_options.dart
@@ -8,13 +8,22 @@ class PostgrestClientOptions {
 
 class AuthClientOptions {
   final bool autoRefreshToken;
+
+  @Deprecated(
+      "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad")
   final GotrueAsyncStorage? pkceAsyncStorage;
+  final GotrueAsyncStorage? asyncStorage;
   final AuthFlowType authFlowType;
+  final String? storageKey;
+  final bool? persistSession;
 
   const AuthClientOptions({
     this.autoRefreshToken = true,
     this.pkceAsyncStorage,
+    this.asyncStorage,
     this.authFlowType = AuthFlowType.pkce,
+    this.storageKey,
+    this.persistSession,
   });
 }
 
diff --git a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
index 2c82a0f7..956dc4b8 100644
--- a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
+++ b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
@@ -1,6 +1,8 @@
 import 'package:supabase_flutter/supabase_flutter.dart';
 
 class FlutterAuthClientOptions extends AuthClientOptions {
+  @Deprecated(
+      "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad")
   final LocalStorage? localStorage;
 
   /// If true, the client will start the deep link observer and obtain sessions
@@ -11,6 +13,9 @@ class FlutterAuthClientOptions extends AuthClientOptions {
     super.authFlowType,
     super.autoRefreshToken,
     super.pkceAsyncStorage,
+    super.asyncStorage,
+    super.storageKey,
+    super.persistSession,
     this.localStorage,
     this.detectSessionInUri = true,
   });
@@ -20,13 +25,21 @@ class FlutterAuthClientOptions extends AuthClientOptions {
     bool? autoRefreshToken,
     LocalStorage? localStorage,
     GotrueAsyncStorage? pkceAsyncStorage,
+    GotrueAsyncStorage? asyncStorage,
+    String? storageKey,
+    bool? persistSession,
     bool? detectSessionInUri,
   }) {
     return FlutterAuthClientOptions(
       authFlowType: authFlowType ?? this.authFlowType,
       autoRefreshToken: autoRefreshToken ?? this.autoRefreshToken,
+      // ignore: deprecated_member_use_from_same_package
       localStorage: localStorage ?? this.localStorage,
+      // ignore: deprecated_member_use
       pkceAsyncStorage: pkceAsyncStorage ?? this.pkceAsyncStorage,
+      asyncStorage: asyncStorage,
+      storageKey: storageKey,
+      persistSession: persistSession,
       detectSessionInUri: detectSessionInUri ?? this.detectSessionInUri,
     );
   }
diff --git a/packages/supabase_flutter/lib/src/local_storage.dart b/packages/supabase_flutter/lib/src/local_storage.dart
index 7b201584..11bdef21 100644
--- a/packages/supabase_flutter/lib/src/local_storage.dart
+++ b/packages/supabase_flutter/lib/src/local_storage.dart
@@ -3,6 +3,7 @@ import 'dart:async';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:supabase_flutter/src/supabase_auth.dart';
 import 'package:supabase_flutter/supabase_flutter.dart';
 
 import './local_storage_stub.dart'
@@ -148,3 +149,51 @@ class SharedPreferencesGotrueAsyncStorage extends GotrueAsyncStorage {
     await _prefs.setString(key, value);
   }
 }
+
+/// Combines the storage for pkce and session into one.
+///
+/// Previously the session got stored by [SupabaseAuth] and the pkce flow by
+/// [GoTrueClient] in a separate storage and with different interface.
+/// This combiens both into one.
+///
+/// This introduces another level of abstraction for the actual
+/// session storage, but is necessary to prevent breaking changes.
+class PkceAndSessionLocalStorage extends GotrueAsyncStorage {
+  final LocalStorage sessionLocalStorage;
+  final GotrueAsyncStorage pkceAsyncStorage;
+
+  PkceAndSessionLocalStorage(this.sessionLocalStorage, this.pkceAsyncStorage);
+  @override
+  Future<void> initialize() async {
+    await sessionLocalStorage.initialize();
+    await pkceAsyncStorage.initialize();
+    super.initialize();
+  }
+
+  @override
+  Future<String?> getItem({required String key}) {
+    if (key.endsWith("-code-verifier")) {
+      return pkceAsyncStorage.getItem(key: key);
+    } else {
+      return sessionLocalStorage.accessToken();
+    }
+  }
+
+  @override
+  Future<void> removeItem({required String key}) async {
+    if (key.endsWith("-code-verifier")) {
+      await pkceAsyncStorage.removeItem(key: key);
+    } else {
+      await sessionLocalStorage.removePersistedSession();
+    }
+  }
+
+  @override
+  Future<void> setItem({required String key, required String value}) async {
+    if (key.endsWith("-code-verifier")) {
+      await pkceAsyncStorage.setItem(key: key, value: value);
+    } else {
+      await sessionLocalStorage.persistSession(value);
+    }
+  }
+}
diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart
index 952460b0..77e5d389 100644
--- a/packages/supabase_flutter/lib/src/supabase.dart
+++ b/packages/supabase_flutter/lib/src/supabase.dart
@@ -101,19 +101,39 @@ class Supabase with WidgetsBindingObserver {
 
     _log.config("Initialize Supabase v$version");
 
+    // ignore: deprecated_member_use
     if (authOptions.pkceAsyncStorage == null) {
       authOptions = authOptions.copyWith(
         pkceAsyncStorage: SharedPreferencesGotrueAsyncStorage(),
       );
     }
+    // ignore: deprecated_member_use_from_same_package
     if (authOptions.localStorage == null) {
       authOptions = authOptions.copyWith(
         localStorage: SharedPreferencesLocalStorage(
           persistSessionKey:
               "sb-${Uri.parse(url).host.split(".").first}-auth-token",
+          // For now we don't set the above key that is used by supabase-js too
+          // as [AuthClientOptions.storageKey], because this would change
+          // the key for exsting pkce items. For v3 we should change this.
         ),
       );
     }
+    if (authOptions.persistSession == null) {
+      authOptions = authOptions.copyWith(
+        persistSession: true,
+      );
+    }
+
+    if (authOptions.asyncStorage == null) {
+      authOptions = authOptions.copyWith(
+          asyncStorage: PkceAndSessionLocalStorage(
+        // ignore: deprecated_member_use_from_same_package
+        authOptions.localStorage!,
+        // ignore: deprecated_member_use
+        authOptions.pkceAsyncStorage!,
+      ));
+    }
     _instance._init(
       url,
       anonKey,
@@ -130,13 +150,6 @@ class Supabase with WidgetsBindingObserver {
       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(
-        supabaseAuth.recoverSession(),
-      );
     }
 
     _log.info('***** Supabase init completed *****');
@@ -160,16 +173,12 @@ class Supabase with WidgetsBindingObserver {
 
   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();
diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart
index ada94a24..13215b5d 100644
--- a/packages/supabase_flutter/lib/src/supabase_auth.dart
+++ b/packages/supabase_flutter/lib/src/supabase_auth.dart
@@ -15,7 +15,6 @@ import 'package:url_launcher/url_launcher.dart';
 class SupabaseAuth with WidgetsBindingObserver {
   static WidgetsBinding? get _widgetsBindingInstance => WidgetsBinding.instance;
 
-  late LocalStorage _localStorage;
   late AuthFlowType _authFlowType;
 
   /// Whether to automatically refresh the token
@@ -26,8 +25,6 @@ class SupabaseAuth with WidgetsBindingObserver {
   /// throughout your app's life.
   static bool _initialDeeplinkIsHandled = false;
 
-  StreamSubscription<AuthState>? _authSubscription;
-
   StreamSubscription<Uri?>? _deeplinkSubscription;
 
   final _appLinks = AppLinks();
@@ -40,65 +37,14 @@ class SupabaseAuth with WidgetsBindingObserver {
   Future<void> initialize({
     required FlutterAuthClientOptions options,
   }) async {
-    _localStorage = options.localStorage!;
     _authFlowType = options.authFlowType;
     _autoRefreshToken = options.autoRefreshToken;
 
-    _authSubscription = Supabase.instance.client.auth.onAuthStateChange.listen(
-      (data) {
-        _onAuthStateChange(data.event, data.session);
-      },
-      onError: (error, stackTrace) {},
-    );
-
-    await _localStorage.initialize();
-
-    final hasPersistedSession = await _localStorage.hasAccessToken();
-    var shouldEmitInitialSession = true;
-    if (hasPersistedSession) {
-      final persistedSession = await _localStorage.accessToken();
-      if (persistedSession != null) {
-        try {
-          await Supabase.instance.client.auth
-              .setInitialSession(persistedSession);
-          shouldEmitInitialSession = false;
-        } catch (error, stackTrace) {
-          _log.warning(
-              'Error while setting initial session', error, stackTrace);
-        }
-      }
-    }
-    if (shouldEmitInitialSession) {
-      Supabase.instance.client.auth
-          // ignore: invalid_use_of_internal_member
-          .notifyAllSubscribers(AuthChangeEvent.initialSession);
-    }
     _widgetsBindingInstance?.addObserver(this);
 
     if (options.detectSessionInUri) {
       await _startDeeplinkObserver();
     }
-
-    // Emit a null session if the user did not have persisted session
-  }
-
-  /// Recovers the session from local storage.
-  ///
-  /// Called lazily after `.initialize()` by `Supabase` instance
-  Future<void> recoverSession() async {
-    try {
-      final hasPersistedSession = await _localStorage.hasAccessToken();
-      if (hasPersistedSession) {
-        final persistedSession = await _localStorage.accessToken();
-        if (persistedSession != null) {
-          await Supabase.instance.client.auth.recoverSession(persistedSession);
-        }
-      }
-    } on AuthException catch (error, stackTrace) {
-      _log.warning(error.message, error, stackTrace);
-    } catch (error, stackTrace) {
-      _log.warning("Error while recovering session", error, stackTrace);
-    }
   }
 
   /// Dispose the instance to free up resources
@@ -106,7 +52,6 @@ class SupabaseAuth with WidgetsBindingObserver {
     if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) {
       _initialDeeplinkIsHandled = false;
     }
-    _authSubscription?.cancel();
     _stopDeeplinkObserver();
     _widgetsBindingInstance?.removeObserver(this);
   }
@@ -127,14 +72,6 @@ class SupabaseAuth with WidgetsBindingObserver {
     }
   }
 
-  void _onAuthStateChange(AuthChangeEvent event, Session? session) {
-    if (session != null) {
-      _localStorage.persistSession(jsonEncode(session.toJson()));
-    } else if (event == AuthChangeEvent.signedOut) {
-      _localStorage.removePersistedSession();
-    }
-  }
-
   /// If _authCallbackUrlHost not init, we treat all deep links as auth callback
   bool _isAuthCallbackDeeplink(Uri uri) {
     return (uri.fragment.contains('access_token') &&

From dd698d65ca414682add8d5fed6fa3c83dc12cd4b Mon Sep 17 00:00:00 2001
From: Vinzent <vinzent03@proton.me>
Date: Thu, 21 Nov 2024 22:09:45 +0100
Subject: [PATCH 2/5] fix: properly copyWith

---
 packages/gotrue/lib/src/gotrue_client.dart                  | 2 +-
 .../lib/src/flutter_go_true_client_options.dart             | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart
index b93db808..b86e7546 100644
--- a/packages/gotrue/lib/src/gotrue_client.dart
+++ b/packages/gotrue/lib/src/gotrue_client.dart
@@ -126,7 +126,7 @@ class GoTrueClient {
 
     final gotrueUrl = url ?? Constants.defaultGotrueUrl;
     _log.config(
-        'Initialize GoTrueClient v$version with url: $_url, autoRefreshToken: $_autoRefreshToken, flowType: $_flowType, tickDuration: ${Constants.autoRefreshTickDuration}, tickThreshold: ${Constants.autoRefreshTickThreshold}');
+        'Initialize GoTrueClient v$version with url: $_url, persistSession: $_persistSession, _storageKey: $storageKey, autoRefreshToken: $_autoRefreshToken, flowType: $_flowType, tickDuration: ${Constants.autoRefreshTickDuration}, tickThreshold: ${Constants.autoRefreshTickThreshold}');
     _log.finest('Initialize with headers: $_headers');
     admin = GoTrueAdminApi(
       gotrueUrl,
diff --git a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
index 956dc4b8..e82db799 100644
--- a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
+++ b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
@@ -37,9 +37,9 @@ class FlutterAuthClientOptions extends AuthClientOptions {
       localStorage: localStorage ?? this.localStorage,
       // ignore: deprecated_member_use
       pkceAsyncStorage: pkceAsyncStorage ?? this.pkceAsyncStorage,
-      asyncStorage: asyncStorage,
-      storageKey: storageKey,
-      persistSession: persistSession,
+      asyncStorage: asyncStorage ?? this.asyncStorage,
+      storageKey: storageKey ?? this.storageKey,
+      persistSession: persistSession ?? this.persistSession,
       detectSessionInUri: detectSessionInUri ?? this.detectSessionInUri,
     );
   }

From 4e150de14b2cc74498b68439d44c25da122bfe10 Mon Sep 17 00:00:00 2001
From: Vinzent <vinzent03@proton.me>
Date: Fri, 22 Nov 2024 00:44:16 +0100
Subject: [PATCH 3/5] fix: only broadcast session when persisting session

---
 packages/gotrue/lib/src/gotrue_client.dart | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart
index b86e7546..3baa3097 100644
--- a/packages/gotrue/lib/src/gotrue_client.dart
+++ b/packages/gotrue/lib/src/gotrue_client.dart
@@ -1222,7 +1222,7 @@ class GoTrueClient {
   }
 
   void _mayStartBroadcastChannel() {
-    if (const bool.fromEnvironment('dart.library.html')) {
+    if (_persistSession && const bool.fromEnvironment('dart.library.html')) {
       // Used by the js library as well
       final broadcastKey =
           "sb-${Uri.parse(_url).host.split(".").first}-auth-token";

From 0094ec010be3815d7638f95c6fcfb49dffcae31c Mon Sep 17 00:00:00 2001
From: Vinzent <vinzent03@proton.me>
Date: Sat, 23 Nov 2024 15:37:24 +0100
Subject: [PATCH 4/5] refactor: update docs and default values

---
 packages/gotrue/lib/src/gotrue_client.dart    | 10 +++++-
 packages/gotrue/lib/src/types/types.dart      |  2 ++
 .../supabase/lib/src/supabase_client.dart     | 13 +++-----
 .../lib/src/supabase_client_options.dart      | 32 +++++++++++++++++--
 .../src/flutter_go_true_client_options.dart   | 29 +++++++++++++++--
 .../supabase_flutter/lib/src/supabase.dart    | 31 +++++++-----------
 6 files changed, 84 insertions(+), 33 deletions(-)

diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart
index 3baa3097..03df2bd3 100644
--- a/packages/gotrue/lib/src/gotrue_client.dart
+++ b/packages/gotrue/lib/src/gotrue_client.dart
@@ -29,9 +29,17 @@ part 'gotrue_mfa_api.dart';
 ///
 /// [autoRefreshToken] whether to refresh the token automatically or not. Defaults to true.
 ///
+/// [persistSession] whether to persist the session via [asyncStorage] or not.
+/// Defaults to false. Session is only broadcasted via [BroadcastChannel] if
+/// set to true.
+///
 /// [httpClient] custom http client.
 ///
-/// [asyncStorage] local storage to store pkce code verifiers. Required when using the pkce flow.
+/// [asyncStorage] local storage to store sessions and pkce code verifiers.
+/// Required when using the pkce flow and persisting sessions.
+///
+/// [storageKey] key to store the session with in [asyncStorage].
+/// The pkce code verifiers are suffixed with `-code-verifier`
 ///
 /// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow.
 /// {@endtemplate}
diff --git a/packages/gotrue/lib/src/types/types.dart b/packages/gotrue/lib/src/types/types.dart
index c1001433..abf300cc 100644
--- a/packages/gotrue/lib/src/types/types.dart
+++ b/packages/gotrue/lib/src/types/types.dart
@@ -1,3 +1,5 @@
+/// An interface to use [html.BroadcastChannel] on web to broadcast sessions to
+/// other tabs.
 typedef BroadcastChannel = ({
   Stream<Map<String, dynamic>> onMessage,
   void Function(Map) postMessage,
diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart
index 5382ec9e..6f2c7416 100644
--- a/packages/supabase/lib/src/supabase_client.dart
+++ b/packages/supabase/lib/src/supabase_client.dart
@@ -11,20 +11,19 @@ import 'auth_http_client.dart';
 import 'counter.dart';
 
 /// {@template supabase_client}
+///
 /// Creates a Supabase client to interact with your Supabase instance.
 ///
 /// [supabaseUrl] and [supabaseKey] can be found on your Supabase dashboard.
 ///
-/// You can access none public schema by passing different [schema].
-///
 /// Default headers can be overridden by specifying [headers].
 ///
 /// Custom http client can be used by passing [httpClient] parameter.
 ///
-/// [storageRetryAttempts] specifies how many retry attempts there should be to
-///  upload a file to Supabase storage when failed due to network interruption.
-///
-/// [realtimeClientOptions] specifies different options you can pass to `RealtimeClient`.
+/// [realtimeClientOptions], [authOptions], [storageOptions],
+/// [postgrestOptions] specify different options you can pass to
+/// [RealtimeClient], [GoTrueClient], [SupabaseStorageClient],
+/// [PostgrestClient].
 ///
 /// [accessToken] Optional function for using a third-party authentication system with Supabase.
 /// The function should return an access token or ID token (JWT) by obtaining
@@ -36,8 +35,6 @@ import 'counter.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 [gotrueAsyncStorage] and set the [authFlowType] to
-/// `AuthFlowType.pkce`in order to perform auth actions with pkce flow.
 /// {@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 fe326a4b..52b23db4 100644
--- a/packages/supabase/lib/src/supabase_client_options.dart
+++ b/packages/supabase/lib/src/supabase_client_options.dart
@@ -6,6 +6,28 @@ class PostgrestClientOptions {
   const PostgrestClientOptions({this.schema = 'public'});
 }
 
+/// {@template supabase_auth_client_options}
+///
+/// Configuration for the auth client with appropriate default values when using
+/// the `supabase` package. For usage via `supabase_flutter` use
+/// [FlutterAuthClientOptions] instead
+///
+/// [autoRefreshToken] whether to refresh the token automatically or not. Defaults to true.
+///
+/// [asyncStorage] a storage interface to store sessions
+/// (if [persistSession] is `true`) and pkce code verifiers
+/// (if [authFlowType] is [AuthFlowType.pkce]), which is the default.
+///
+/// [storageKey] key to store the session with in [asyncStorage].
+/// The pkce code verifiers are suffixed with `-code-verifier`
+///
+/// [persistSession] whether to persist the session via [asyncStorage] or not.
+/// Session is only broadcasted via [BroadcastChannel] if set to true.
+///
+/// Set [authFlowType] to [AuthFlowType.implicit] to use the old implicit flow for authentication
+/// involving deep links.
+///
+/// {@endtemplate}
 class AuthClientOptions {
   final bool autoRefreshToken;
 
@@ -13,22 +35,28 @@ class AuthClientOptions {
       "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad")
   final GotrueAsyncStorage? pkceAsyncStorage;
   final GotrueAsyncStorage? asyncStorage;
+
   final AuthFlowType authFlowType;
+
   final String? storageKey;
-  final bool? persistSession;
+  final bool persistSession;
 
+  /// {@macro supabase_auth_client_options}
   const AuthClientOptions({
     this.autoRefreshToken = true,
     this.pkceAsyncStorage,
     this.asyncStorage,
     this.authFlowType = AuthFlowType.pkce,
     this.storageKey,
-    this.persistSession,
+    this.persistSession = false,
   });
 }
 
 class StorageClientOptions {
   final int retryAttempts;
 
+  /// [retryAttempts] specifies how many retry attempts there should be
+  /// to upload a file to Supabase storage when failed due to network
+  /// interruption.
   const StorageClientOptions({this.retryAttempts = 0});
 }
diff --git a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
index e82db799..72e29c3b 100644
--- a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
+++ b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart
@@ -1,21 +1,44 @@
 import 'package:supabase_flutter/supabase_flutter.dart';
 
+/// {@template supabase_flutter_auth_client_options}
+///
+/// [autoRefreshToken] whether to refresh the token automatically or not. Defaults to true.
+///
+/// [asyncStorage] a storage interface to store sessions
+/// (if [persistSession] is `true`) and pkce code verifiers
+/// (if [authFlowType] is [AuthFlowType.pkce])
+///
+/// [storageKey] key to store the session with in [asyncStorage].
+/// The pkce code verifiers are suffixed with `-code-verifier`
+///
+/// [persistSession] whether to persist the session via [asyncStorage] or not.
+/// Session is only broadcasted via [BroadcastChannel] if set to true.
+///
+/// Set [authFlowType] to [AuthFlowType.implicit] to use the old implicit flow for authentication
+/// involving deep links.
+///
+/// [detectSessionInUri] If true, the client will start the deep link observer and obtain sessions
+/// when a valid URI is detected.
+///
+/// PKCE flow uses shared preferences for storing the code verifier by default.
+/// Pass a custom storage to [asyncStorage] to override the behavior.
+///
+/// {@endtemplate}
 class FlutterAuthClientOptions extends AuthClientOptions {
   @Deprecated(
       "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad")
   final LocalStorage? localStorage;
 
-  /// If true, the client will start the deep link observer and obtain sessions
-  /// when a valid URI is detected.
   final bool detectSessionInUri;
 
+  /// {@macro supabase_flutter_auth_client_options}
   const FlutterAuthClientOptions({
     super.authFlowType,
     super.autoRefreshToken,
     super.pkceAsyncStorage,
     super.asyncStorage,
     super.storageKey,
-    super.persistSession,
+    super.persistSession = true,
     this.localStorage,
     this.detectSessionInUri = true,
   });
diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart
index 77e5d389..c7b58b96 100644
--- a/packages/supabase_flutter/lib/src/supabase.dart
+++ b/packages/supabase_flutter/lib/src/supabase.dart
@@ -48,28 +48,26 @@ class Supabase with WidgetsBindingObserver {
   /// Initialize the current supabase instance
   ///
   /// This must be called only once. If called more than once, an
-  /// [AssertionError] is thrown
+  /// [AssertionError] is thrown.
+  /// (after calling [dispose], [initialize] can be called again)
   ///
   /// [url] and [anonKey] can be found on your Supabase dashboard.
   ///
-  /// You can access none public schema by passing different [schema].
-  ///
   /// Default headers can be overridden by specifying [headers].
   ///
-  /// Pass [localStorage] to override the default local storage option used to
-  /// persist auth.
-  ///
   /// Custom http client can be used by passing [httpClient] parameter.
   ///
-  /// [storageRetryAttempts] specifies how many retry attempts there should be
-  /// to upload a file to Supabase storage when failed due to network
-  /// interruption.
-  ///
-  /// Set [authFlowType] to [AuthFlowType.implicit] to use the old implicit flow for authentication
-  /// involving deep links.
+  /// [realtimeClientOptions], [authOptions], [storageOptions],
+  /// [postgrestOptions] specify different options you can pass to
+  /// [RealtimeClient], [GoTrueClient], [SupabaseStorageClient],
+  /// [PostgrestClient].
   ///
-  /// PKCE flow uses shared preferences for storing the code verifier by default.
-  /// Pass a custom storage to [pkceAsyncStorage] to override the behavior.
+  /// [accessToken] Optional function for using a third-party authentication system with Supabase.
+  /// The function should return an access token or ID token (JWT) by obtaining
+  /// it from the third-party auth client library. Note that this function may be
+  /// called concurrently and many times. Use memoization and locking techniques
+  /// if this is not supported by the client libraries. When set, the `auth`
+  /// namespace of the Supabase client cannot be used.
   ///
   /// If [debug] is set to `true`, debug logs will be printed in debug console. Default is `kDebugMode`.
   static Future<Supabase> initialize({
@@ -119,11 +117,6 @@ class Supabase with WidgetsBindingObserver {
         ),
       );
     }
-    if (authOptions.persistSession == null) {
-      authOptions = authOptions.copyWith(
-        persistSession: true,
-      );
-    }
 
     if (authOptions.asyncStorage == null) {
       authOptions = authOptions.copyWith(

From d62da8feb248729b64d2a883fd4690c31ab31256 Mon Sep 17 00:00:00 2001
From: Vinzent <vinzent03@proton.me>
Date: Sat, 23 Nov 2024 16:54:21 +0100
Subject: [PATCH 5/5] refactor: fix typo and add documentation

---
 packages/gotrue/lib/src/gotrue_client.dart | 30 +++++++++++++---------
 1 file changed, 18 insertions(+), 12 deletions(-)

diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart
index 03df2bd3..67cb8814 100644
--- a/packages/gotrue/lib/src/gotrue_client.dart
+++ b/packages/gotrue/lib/src/gotrue_client.dart
@@ -39,7 +39,7 @@ part 'gotrue_mfa_api.dart';
 /// Required when using the pkce flow and persisting sessions.
 ///
 /// [storageKey] key to store the session with in [asyncStorage].
-/// The pkce code verifiers are suffixed with `-code-verifier`
+/// The pkce code verifiers are suffixed with `-code-verifier`.
 ///
 /// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow.
 /// {@endtemplate}
@@ -73,7 +73,9 @@ class GoTrueClient {
   final _onAuthStateChangeControllerSync =
       BehaviorSubject<AuthState>(sync: true);
 
-  /// Local storage to store pkce code verifiers.
+  /// Local storage to store session and pkce code verifiers.
+  ///
+  /// check [_initializedStorage] before usage.
   final GotrueAsyncStorage? _asyncStorage;
 
   /// Receive a notification every time an auth event happens.
@@ -95,12 +97,18 @@ class GoTrueClient {
   Stream<AuthState> get onAuthStateChangeSync =>
       _onAuthStateChangeControllerSync.stream;
 
-  final Completer<void> _initalizedStorage = Completer<void>();
+  /// Completes when the [_asyncStorage] is initialized.
+  ///
+  /// Initialization is started in the constructor and should be awaited before
+  /// accessing the storage.
+  final Completer<void> _initializedStorage = Completer<void>();
 
   final AuthFlowType _flowType;
 
   final bool _persistSession;
 
+  /// Key to store the session with in [_asyncStorage].
+  /// The pkce code verifiers are suffixed with `-code-verifier`.
   final String _storageKey;
 
   final _log = Logger('supabase.auth');
@@ -149,7 +157,7 @@ class GoTrueClient {
     assert(asyncStorage != null || !_persistSession,
         'You need to provide asyncStorage to persist session.');
     if (asyncStorage != null) {
-      _initalizedStorage.complete(
+      _initializedStorage.complete(
           asyncStorage.initialize().catchError((e) => notifyException(e)));
     }
 
@@ -179,7 +187,7 @@ class GoTrueClient {
   Future<void> _initialize() async {
     try {
       if (_persistSession && _asyncStorage != null) {
-        await _initalizedStorage.future;
+        await _initializedStorage.future;
         final jsonStr = await _asyncStorage!.getItem(key: _storageKey);
         var shouldEmitInitialSession = true;
         if (jsonStr != null) {
@@ -1204,8 +1212,8 @@ class GoTrueClient {
     _currentUser = session.user;
 
     if (_persistSession && _asyncStorage != null) {
-      if (!_initalizedStorage.isCompleted) {
-        await _initalizedStorage.future;
+      if (!_initializedStorage.isCompleted) {
+        await _initializedStorage.future;
       }
       _asyncStorage!.setItem(
         key: _storageKey,
@@ -1220,12 +1228,10 @@ class GoTrueClient {
     _currentUser = null;
 
     if (_persistSession && _asyncStorage != null) {
-      if (!_initalizedStorage.isCompleted) {
-        await _initalizedStorage.future;
+      if (!_initializedStorage.isCompleted) {
+        await _initializedStorage.future;
       }
-      _asyncStorage!.removeItem(
-        key: _storageKey,
-      );
+      _asyncStorage!.removeItem(key: _storageKey);
     }
   }