Skip to content
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

refactor: Remove hive dependency and add instructinos to migration from v1 while persisting the auth state #823

Merged
merged 6 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions packages/supabase_flutter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,160 @@ Supabase.initialize(
);
```

### Persisting the user session from supabase_flutter v1

supabase_flutter v1 used hive to persist the user session. In the current versino of supabase_flutter it uses shared_preferences. If you are updating your app from v1 to v2, you can use the following custom `LocalStorage` implementation to automatically migrate the user session from hive to shared_preferences.
dshukertjr marked this conversation as resolved.
Show resolved Hide resolved
dshukertjr marked this conversation as resolved.
Show resolved Hide resolved

```dart
const _hiveBoxName = 'supabase_authentication';

class MigrationLocalStorage extends LocalStorage {
final SharedPreferencesLocalStorage sharedPreferencesLocalStorage;
late final HiveLocalStorage hiveLocalStorage;

MigrationLocalStorage({required String persistSessionKey})
: sharedPreferencesLocalStorage =
SharedPreferencesLocalStorage(persistSessionKey: persistSessionKey);

@override
Future<void> initialize() async {
await Hive.initFlutter('auth');
hiveLocalStorage = const HiveLocalStorage();
await sharedPreferencesLocalStorage.initialize();
try {
await migrate();
} on TimeoutException {
// Ignore TimeoutException thrown by Hive methods
// https://github.com/supabase/supabase-flutter/issues/794
}
}

@visibleForTesting
Future<void> migrate() async {
// Migrate from Hive to SharedPreferences
if (await Hive.boxExists(_hiveBoxName)) {
await hiveLocalStorage.initialize();

final hasHive = await hiveLocalStorage.hasAccessToken();
if (hasHive) {
final accessToken = await hiveLocalStorage.accessToken();
final session =
Session.fromJson(jsonDecode(accessToken!)['currentSession']);
if (session == null) {
return;
}
await sharedPreferencesLocalStorage
.persistSession(jsonEncode(session.toJson()));
await hiveLocalStorage.removePersistedSession();
}
if (Hive.box(_hiveBoxName).isEmpty) {
final boxPath = Hive.box(_hiveBoxName).path;
await Hive.deleteBoxFromDisk(_hiveBoxName);

//Delete `auth` folder if it's empty
if (!kIsWeb && boxPath != null) {
final boxDir = File(boxPath).parent;
final dirIsEmpty = await boxDir.list().length == 0;
if (dirIsEmpty) {
await boxDir.delete();
}
}
}
}
}

@override
Future<String?> accessToken() {
return sharedPreferencesLocalStorage.accessToken();
}

@override
Future<bool> hasAccessToken() {
return sharedPreferencesLocalStorage.hasAccessToken();
}

@override
Future<void> persistSession(String persistSessionString) {
return sharedPreferencesLocalStorage.persistSession(persistSessionString);
}

@override
Future<void> removePersistedSession() {
return sharedPreferencesLocalStorage.removePersistedSession();
}
}

/// A [LocalStorage] implementation that implements Hive as the
/// storage method.
class HiveLocalStorage extends LocalStorage {
/// Creates a LocalStorage instance that implements the Hive Database
const HiveLocalStorage();

/// The encryption key used by Hive. If null, the box is not encrypted
///
/// This value should not be redefined in runtime, otherwise the user may
/// not be fetched correctly
///
/// See also:
///
/// * <https://docs.hivedb.dev/#/advanced/encrypted_box?id=encrypted-box>
static String? encryptionKey;

@override
Future<void> initialize() async {
HiveCipher? encryptionCipher;
if (encryptionKey != null) {
encryptionCipher = HiveAesCipher(base64Url.decode(encryptionKey!));
}
await Hive.initFlutter('auth');
await Hive.openBox(_hiveBoxName, encryptionCipher: encryptionCipher)
.timeout(const Duration(seconds: 1));
}

@override
Future<bool> hasAccessToken() {
return Future.value(
Hive.box(_hiveBoxName).containsKey(
supabasePersistSessionKey,
),
);
}

@override
Future<String?> accessToken() {
return Future.value(
Hive.box(_hiveBoxName).get(supabasePersistSessionKey) as String?,
);
}

@override
Future<void> removePersistedSession() {
return Hive.box(_hiveBoxName).delete(supabasePersistSessionKey);
}

@override
Future<void> persistSession(String persistSessionString) {
// Flush after X amount of writes
return Hive.box(_hiveBoxName)
.put(supabasePersistSessionKey, persistSessionString);
}
}
```

You can then initialize Supabase with `MigrationLocalStorage` and it will automatically migrate the sessino from Hive to SharedPreferences.
dshukertjr marked this conversation as resolved.
Show resolved Hide resolved

```dart
Supabase.initialize(
// ...
authOptions: FlutterAuthClientOptions(
localStorage: const MigrationLocalStorage(
persistSessionKey:
"sb-${Uri.parse(url).host.split(".").first}-auth-token",
),
),
);
```

---

## Contributing
Expand Down
136 changes: 0 additions & 136 deletions packages/supabase_flutter/lib/src/local_storage.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import './local_storage_stub.dart'
if (dart.library.html) './local_storage_web.dart' as web;

const _hiveBoxName = 'supabase_authentication';
const supabasePersistSessionKey = 'SUPABASE_PERSIST_SESSION_KEY';

/// LocalStorage is used to persist the user session in the device.
Expand Down Expand Up @@ -64,62 +60,6 @@ class EmptyLocalStorage extends LocalStorage {
Future<void> persistSession(persistSessionString) async {}
}

/// A [LocalStorage] implementation that implements Hive as the
/// storage method.
class HiveLocalStorage extends LocalStorage {
/// Creates a LocalStorage instance that implements the Hive Database
const HiveLocalStorage();

/// The encryption key used by Hive. If null, the box is not encrypted
///
/// This value should not be redefined in runtime, otherwise the user may
/// not be fetched correctly
///
/// See also:
///
/// * <https://docs.hivedb.dev/#/advanced/encrypted_box?id=encrypted-box>
static String? encryptionKey;

@override
Future<void> initialize() async {
HiveCipher? encryptionCipher;
if (encryptionKey != null) {
encryptionCipher = HiveAesCipher(base64Url.decode(encryptionKey!));
}
await Hive.initFlutter('auth');
await Hive.openBox(_hiveBoxName, encryptionCipher: encryptionCipher)
.timeout(const Duration(seconds: 1));
}

@override
Future<bool> hasAccessToken() {
return Future.value(
Hive.box(_hiveBoxName).containsKey(
supabasePersistSessionKey,
),
);
}

@override
Future<String?> accessToken() {
return Future.value(
Hive.box(_hiveBoxName).get(supabasePersistSessionKey) as String?,
);
}

@override
Future<void> removePersistedSession() {
return Hive.box(_hiveBoxName).delete(supabasePersistSessionKey);
}

@override
Future<void> persistSession(String persistSessionString) {
// Flush after X amount of writes
return Hive.box(_hiveBoxName)
.put(supabasePersistSessionKey, persistSessionString);
}
}

/// A [LocalStorage] implementation that implements SharedPreferences as the
/// storage method.
class SharedPreferencesLocalStorage extends LocalStorage {
Expand Down Expand Up @@ -173,82 +113,6 @@ class SharedPreferencesLocalStorage extends LocalStorage {
}
}

class MigrationLocalStorage extends LocalStorage {
final SharedPreferencesLocalStorage sharedPreferencesLocalStorage;
late final HiveLocalStorage hiveLocalStorage;

MigrationLocalStorage({required String persistSessionKey})
: sharedPreferencesLocalStorage =
SharedPreferencesLocalStorage(persistSessionKey: persistSessionKey);

@override
Future<void> initialize() async {
await Hive.initFlutter('auth');
hiveLocalStorage = const HiveLocalStorage();
await sharedPreferencesLocalStorage.initialize();
try {
await migrate();
} on TimeoutException {
// Ignore TimeoutException thrown by Hive methods
// https://github.com/supabase/supabase-flutter/issues/794
}
}

@visibleForTesting
Future<void> migrate() async {
// Migrate from Hive to SharedPreferences
if (await Hive.boxExists(_hiveBoxName)) {
await hiveLocalStorage.initialize();

final hasHive = await hiveLocalStorage.hasAccessToken();
if (hasHive) {
final accessToken = await hiveLocalStorage.accessToken();
final session =
Session.fromJson(jsonDecode(accessToken!)['currentSession']);
if (session == null) {
return;
}
await sharedPreferencesLocalStorage
.persistSession(jsonEncode(session.toJson()));
await hiveLocalStorage.removePersistedSession();
}
if (Hive.box(_hiveBoxName).isEmpty) {
final boxPath = Hive.box(_hiveBoxName).path;
await Hive.deleteBoxFromDisk(_hiveBoxName);

//Delete `auth` folder if it's empty
if (!kIsWeb && boxPath != null) {
final boxDir = File(boxPath).parent;
final dirIsEmpty = await boxDir.list().length == 0;
if (dirIsEmpty) {
await boxDir.delete();
}
}
}
}
}

@override
Future<String?> accessToken() {
return sharedPreferencesLocalStorage.accessToken();
}

@override
Future<bool> hasAccessToken() {
return sharedPreferencesLocalStorage.hasAccessToken();
}

@override
Future<void> persistSession(String persistSessionString) {
return sharedPreferencesLocalStorage.persistSession(persistSessionString);
}

@override
Future<void> removePersistedSession() {
return sharedPreferencesLocalStorage.removePersistedSession();
}
}

/// local storage to store pkce flow code verifier.
class SharedPreferencesGotrueAsyncStorage extends GotrueAsyncStorage {
SharedPreferencesGotrueAsyncStorage() {
Expand Down
2 changes: 1 addition & 1 deletion packages/supabase_flutter/lib/src/supabase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class Supabase {
}
if (authOptions.localStorage == null) {
authOptions = authOptions.copyWith(
localStorage: MigrationLocalStorage(
localStorage: SharedPreferencesLocalStorage(
persistSessionKey:
"sb-${Uri.parse(url).host.split(".").first}-auth-token",
),
Expand Down
2 changes: 0 additions & 2 deletions packages/supabase_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ dependencies:
crypto: ^3.0.2
flutter:
sdk: flutter
hive: ^2.2.1
hive_flutter: ^1.1.0
http: '>=0.13.4 <2.0.0'
meta: ^1.7.0
supabase: ^2.0.7
Expand Down
29 changes: 0 additions & 29 deletions packages/supabase_flutter/test/widget_test.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
import 'package:supabase_flutter/supabase_flutter.dart';

import 'utils.dart';
import 'widget_test_stubs.dart';

void main() {
Expand All @@ -33,28 +28,4 @@ void main() {
await tester.pump();
expect(find.text('You have signed out'), findsOneWidget);
});

test(
"Migrates from Hive to SharedPreferences",
() async {
final hiveLocalStorage = TestHiveLocalStorage();
await hiveLocalStorage.initialize();
final (:accessToken, :sessionString) = getSessionData(DateTime.now());
await hiveLocalStorage
.persistSession('{"currentSession":$sessionString}');
final boxFile =
File("${path.current}/auth_test/supabase_authentication.hive");
expect(await boxFile.exists(), true);

final migrationLocalStorage = TestMigrationLocalStorage();
await migrationLocalStorage.initialize();

final migratedSessionString = await migrationLocalStorage.accessToken();
final migratedSession =
Session.fromJson(jsonDecode(migratedSessionString!));
expect(await boxFile.exists(), false);
expect(accessToken, migratedSession!.accessToken);
expect(await boxFile.parent.exists(), false);
},
);
}
Loading
Loading