Skip to content

fix: Adds support for SharedPreferencesAsync #1164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

romaingyh
Copy link

What kind of change does this PR introduce?

It's both a bug fix and feature.

9 months ago with version 2.3.0, shared_preferences package added SharedPreferencesAsync and SharedPreferencesWithCache to replace the legacy (and deprecated in future) SharedPreferences. See here

Supabase flutter uses by default the legacy one with SharedPreferencesLocalStorage.

Problem is that on some platform like Windows, the legacy one and new one are not compatible. If the developer made the migration to SharedPreferencesAsync then the supabase's local storage is broken.

Example :
I'm using SharedPreferencesAsync in my app. After sign in, the sb-x-auth-token is saved in saved-preferences.json by supabase's SharedPreferences. After that, any call to my app's SharedPreferencesAsync will overwrite the token and user have to sign in on next app launch. In practice it can lead to sign in every time he opens the app if you use shared preferences frequently in your code.

What is the new behavior?

I added a boolean flag useSharedPreferencesAsync to SharedPreferencesLocalStorage. There wasn't very much changes as every methods are already async in LocalStorage interface.

If you prefer two distinct classes SharedPreferencesLocalStorage and SharedPreferencesAsyncLocalStorage I can do this

@Vinzent03
Copy link
Collaborator

Vinzent03 commented May 5, 2025

I can confirm that this issue exists, as I encountered this as well in a recent project, but didn't have the time to investigate more and come up with a pr. At that time, I solved it by manually providing my own storage implementation using the async shared preferences.
But I'm really unsure about the current design of the solution and will have to think about it a bit.

@coveralls
Copy link

coveralls commented May 5, 2025

Pull Request Test Coverage Report for Build 15304311527

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 0 of 16 (0.0%) changed or added relevant lines in 1 file are covered.
  • 16 unchanged lines in 4 files lost coverage.
  • Overall coverage decreased (-0.3%) to 75.085%

Changes Missing Coverage Covered Lines Changed/Added Lines %
packages/supabase_flutter/lib/src/local_storage.dart 0 16 0.0%
Files with Coverage Reduction New Missed Lines %
packages/supabase/lib/src/supabase_client.dart 2 67.74%
packages/realtime_client/lib/src/realtime_client.dart 4 90.4%
packages/gotrue/lib/src/gotrue_admin_api.dart 5 92.96%
packages/realtime_client/lib/src/realtime_channel.dart 5 66.78%
Totals Coverage Status
Change from base Build 14730510657: -0.3%
Covered Lines: 2881
Relevant Lines: 3837

💛 - Coveralls

@romaingyh romaingyh changed the title Adds support for SharedPreferencesAsync fix: Adds support for SharedPreferencesAsync May 5, 2025
@romaingyh
Copy link
Author

romaingyh commented May 5, 2025

I can confirm that this issue exists, as I encountered this as well in a recent project, but didn't have the time to investigate more and come up with a pr. At that time, I solved it by manually providing my own storage implementation using the async shared preferences. But I'm really unsure about the current design of the solution and will have to think about it a bit.

I thought of another solution like this :

Code
/// A [LocalStorage] implementation that implements SharedPreferences as the
/// storage method.
class SharedPreferencesLocalStorage extends LocalStorage {
  late final SharedPreferences _prefs;

  SharedPreferencesLocalStorage({required this.persistSessionKey});

  final String persistSessionKey;

  static const _useWebLocalStorage =
      kIsWeb && bool.fromEnvironment("dart.library.js_interop");

  @override
  Future<void> initialize() async {
    if (!_useWebLocalStorage) {
      WidgetsFlutterBinding.ensureInitialized();
      _prefs = await SharedPreferences.getInstance();
    }
  }

  @override
  Future<bool> hasAccessToken() async {
    if (_useWebLocalStorage) {
      return web.hasAccessToken(persistSessionKey);
    }
    return _prefs.containsKey(persistSessionKey);
  }

  @override
  Future<String?> accessToken() async {
    if (_useWebLocalStorage) {
      return web.accessToken(persistSessionKey);
    }
    return _prefs.getString(persistSessionKey);
  }

  @override
  Future<void> removePersistedSession() async {
    if (_useWebLocalStorage) {
      web.removePersistedSession(persistSessionKey);
    } else {
      await _prefs.remove(persistSessionKey);
    }
  }

  @override
  Future<void> persistSession(String persistSessionString) {
    if (_useWebLocalStorage) {
      return web.persistSession(persistSessionKey, persistSessionString);
    }
    return _prefs.setString(persistSessionKey, persistSessionString);
  }
}

/// A [LocalStorage] implementation that implements SharedPreferencesAsync as the
/// storage method.
class SharedPreferencesAsyncLocalStorage extends LocalStorage {
  late final SharedPreferencesAsync _prefs;

  SharedPreferencesAsyncLocalStorage({required this.persistSessionKey});

  final String persistSessionKey;

  static const _useWebLocalStorage =
      kIsWeb && bool.fromEnvironment("dart.library.js_interop");

  @override
  Future<void> initialize() async {
    if (!_useWebLocalStorage) {
      WidgetsFlutterBinding.ensureInitialized();
      _prefs = SharedPreferencesAsync();
    }
  }

  @override
  Future<bool> hasAccessToken() async {
    if (_useWebLocalStorage) {
      return web.hasAccessToken(persistSessionKey);
    }
    return _prefs.containsKey(persistSessionKey);
  }

  @override
  Future<String?> accessToken() async {
    if (_useWebLocalStorage) {
      return web.accessToken(persistSessionKey);
    }
    return _prefs.getString(persistSessionKey);
  }

  @override
  Future<void> removePersistedSession() async {
    if (_useWebLocalStorage) {
      web.removePersistedSession(persistSessionKey);
    } else {
      await _prefs.remove(persistSessionKey);
    }
  }

  @override
  Future<void> persistSession(String persistSessionString) {
    if (_useWebLocalStorage) {
      return web.persistSession(persistSessionKey, persistSessionString);
    }
    return _prefs.setString(persistSessionKey, persistSessionString);
  }
}

where the dev has to opt-in for SharedPreferencesAsyncLocalStorage when he initializes supabase.

I didn't choose this implementation in my PR because the two classes are pretty much the same except for shared prefs init.

@romaingyh
Copy link
Author

I think CI test fails because with Flutter 3.19 the resolvable shared_preferences package version is < 2.3.0 but not sure about this

@sai-chandra22
Copy link

@Vinzent03 , Can you exactly tell me what is the solution you have done for this solution

@sai-chandra22
Copy link

sai-chandra22 commented May 28, 2025

await sb.Supabase.initialize(
    url: ApiKeys.supabaseUrl,
    anonKey: ApiKeys.supabaseAnonKey,
    authOptions: sb.FlutterAuthClientOptions(
        autoRefreshToken: true,
        localStorage: sb.SharedPreferencesLocalStorage(
            persistSessionKey: sb.supabasePersistSessionKey)
        ),
  );

the refresh_token_already_used error is coming, though supabase is handling auto refresh - This comes when user again opens the app (Cold start) after the token expiry time, then instead of emitting token refreshed state, the error is coming!

@Vinzent03
Copy link
Collaborator

If we want to provide a LocalStorage implementation that uses SharedPreferencesAsync we need shared_preferences v2.3.0, which requires Dart v3.4.0, but we currently test and support until Dart v3.3.0. So we would need to bump Flutter to v3.22.0 for Dart v3.4.0. But this version is already one year old and this is an actual issue, because the session persistence is broken.
The only other way would be to only provide an example implementation for users to copy and add themselves.
But because we can't detect whether they use the new or old api we would have to stick with the old api for the moment and mention it in the docs that if they use the new shared_preferences api they need the new LocalStorage implementation anyway.
@dshukertjr What is your opinion?

Either way @romaingyh I prefer the two distinct classes implementation.

…: SharedPreferencesLocalStorage & SharedPreferencesAsyncLocalStorage
@dshukertjr
Copy link
Member

@Vinzent03

But this version is already one year old and this is an actual issue, because the session persistence is broken.

Broken in what way?

@romaingyh
Copy link
Author

Either way @romaingyh I prefer the two distinct classes implementation.

Done, PR has now two classes : SharedPreferencesLocalStorage and SharedPreferencesAsyncLocalStorage

@Vinzent03
Copy link
Collaborator

@dshukertjr The way it's described in the pr description. Writes to SharedPreferencesAsync overwrite values written by SharedPreferences.getInstance(), so restoring the session fails at startup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants