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 1 commit 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

Pull Request Test Coverage Report for Build 14823893141

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 18 (0.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.2%) to 75.138%

Changes Missing Coverage Covered Lines Changed/Added Lines %
packages/supabase_flutter/lib/src/local_storage.dart 0 18 0.0%
Totals Coverage Status
Change from base Build 14730510657: -0.2%
Covered Lines: 2868
Relevant Lines: 3817

💛 - 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

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.

3 participants