diff --git a/bccm_core/lib/src/features/auth/implementations/auth_state_notifier_mobile.dart b/bccm_core/lib/src/features/auth/implementations/auth_state_notifier_mobile.dart index a540898..b6ff443 100644 --- a/bccm_core/lib/src/features/auth/implementations/auth_state_notifier_mobile.dart +++ b/bccm_core/lib/src/features/auth/implementations/auth_state_notifier_mobile.dart @@ -15,8 +15,25 @@ import 'package:unleash_proxy_client_flutter/id_generator.dart'; import '../../../utils/constants.dart'; +/// `encryptedSharedPreferences` is deliberately absent: v10 ignores the parameter +/// outright and migrates any Jetpack EncryptedSharedPreferences data to its own ciphers +/// on first access. If that migration throws it falls back to the old store rather than +/// discarding anything, which is why the defaults below are left alone — +/// `resetOnError: true` (erases on unrecoverable errors, matching the reasoning behind +/// [kMinimumCredentialsTTL]) and `migrateWithBackup: false`. Do **not** set +/// `migrateWithBackup: true` here: it skips the direct ESP migration in favour of a +/// backup-protected path that only runs when a cipher key-mismatch is detected, which a +/// user whose data is still in EncryptedSharedPreferences may never trigger. +/// +/// `sharedPreferencesName` is deprecated in favour of `storageNamespace` and removed in +/// v11. They are **not** interchangeable: switching moves the wrapped-key prefs +/// (`FlutterSecureKeyStorage` -> `FlutterSecureKeyStorage:auth`) and the KeyStore alias +/// (suffix `.auth`) while leaving the ciphertext where it is, and nothing migrates +/// across that change. Swapping it blind would leave every Android user with data they +/// cannot decrypt, which `resetOnError` then erases. That migration needs its own +/// release, on v10, before v11 removes this parameter. AndroidOptions _getAndroidSecureStorageOptions() => const AndroidOptions( - encryptedSharedPreferences: true, // https://github.com/juliansteenbakker/flutter_secure_storage/issues/354 + // ignore: deprecated_member_use sharedPreferencesName: 'auth', ); diff --git a/bccm_core/pubspec.yaml b/bccm_core/pubspec.yaml index 3e35b7a..ebde283 100644 --- a/bccm_core/pubspec.yaml +++ b/bccm_core/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: extended_image: ^10.1.0 flutter_local_notifications: ^17.2.3 flutter_riverpod: ^2.5.3 - flutter_secure_storage: ^9.2.2 + flutter_secure_storage: ^10.3.1 freezed_annotation: ^3.1.0 graphql: ^5.2.1 graphql_flutter: ^5.3.0 diff --git a/bccm_core/test/auth_test.dart b/bccm_core/test/auth_test.dart index 443e2a4..951517e 100644 --- a/bccm_core/test/auth_test.dart +++ b/bccm_core/test/auth_test.dart @@ -143,9 +143,20 @@ MockFlutterSecureStorage storageWithSession({String? accessToken, String? userPr /// the app actually read — see the note on `_getIOSSecureStorageOptions`. final kExpectedIOSOptions = const IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device).params; -/// The app's own encrypted store, not the plugin's default one. Changing either -/// value orphans every credential already on disk. -final kExpectedAndroidOptions = const AndroidOptions(encryptedSharedPreferences: true, sharedPreferencesName: 'auth').params; +/// The app's own encrypted store, not the plugin's default one. Changing this +/// orphans every credential already on disk. +/// +/// Two specific ways to break it: +/// - Dropping `sharedPreferencesName` moves the data prefs from `auth` to the plugin +/// default `FlutterSecureStorage`. +/// - Swapping it for `storageNamespace` keeps the data prefs but moves the wrapped-key +/// prefs and the KeyStore alias, so the ciphertext is found and cannot be decrypted. +/// +/// Neither is migrated by the plugin. `resetOnError` (on by default) then erases. +final kExpectedAndroidOptions = const AndroidOptions( + // ignore: deprecated_member_use + sharedPreferencesName: 'auth', +).params; /// `params` hands back plain [Map]s, and Map doesn't implement value equality — /// a `Set` of them dedupes by identity, so every element always looks distinct. diff --git a/bccm_core/test/auth_test.mocks.dart b/bccm_core/test/auth_test.mocks.dart index ff11e90..590589e 100644 --- a/bccm_core/test/auth_test.mocks.dart +++ b/bccm_core/test/auth_test.mocks.dart @@ -82,8 +82,8 @@ class _FakeWebOptions_8 extends _i1.SmartFake implements _i3.WebOptions { : super(parent, parentInvocation); } -class _FakeMacOsOptions_9 extends _i1.SmartFake implements _i3.MacOsOptions { - _FakeMacOsOptions_9(Object parent, Invocation parentInvocation) +class _FakeAppleOptions_9 extends _i1.SmartFake implements _i3.AppleOptions { + _FakeAppleOptions_9(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } @@ -228,14 +228,22 @@ class MockFlutterSecureStorage extends _i1.Mock ) as _i3.WebOptions); @override - _i3.MacOsOptions get mOptions => (super.noSuchMethod( + _i3.AppleOptions get mOptions => (super.noSuchMethod( Invocation.getter(#mOptions), - returnValue: _FakeMacOsOptions_9(this, Invocation.getter(#mOptions)), - returnValueForMissingStub: _FakeMacOsOptions_9( + returnValue: _FakeAppleOptions_9(this, Invocation.getter(#mOptions)), + returnValueForMissingStub: _FakeAppleOptions_9( this, Invocation.getter(#mOptions), ), - ) as _i3.MacOsOptions); + ) as _i3.AppleOptions); + + @override + Map>> get getListeners => + (super.noSuchMethod( + Invocation.getter(#getListeners), + returnValue: >>{}, + returnValueForMissingStub: >>{}, + ) as Map>>); @override void registerListener({ @@ -275,11 +283,11 @@ class MockFlutterSecureStorage extends _i1.Mock _i6.Future write({ required String? key, required String? value, - _i3.IOSOptions? iOptions, + _i3.AppleOptions? iOptions, _i3.AndroidOptions? aOptions, _i3.LinuxOptions? lOptions, _i3.WebOptions? webOptions, - _i3.MacOsOptions? mOptions, + _i3.AppleOptions? mOptions, _i3.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method(#write, [], { @@ -299,11 +307,11 @@ class MockFlutterSecureStorage extends _i1.Mock @override _i6.Future read({ required String? key, - _i3.IOSOptions? iOptions, + _i3.AppleOptions? iOptions, _i3.AndroidOptions? aOptions, _i3.LinuxOptions? lOptions, _i3.WebOptions? webOptions, - _i3.MacOsOptions? mOptions, + _i3.AppleOptions? mOptions, _i3.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method(#read, [], { @@ -322,11 +330,11 @@ class MockFlutterSecureStorage extends _i1.Mock @override _i6.Future containsKey({ required String? key, - _i3.IOSOptions? iOptions, + _i3.AppleOptions? iOptions, _i3.AndroidOptions? aOptions, _i3.LinuxOptions? lOptions, _i3.WebOptions? webOptions, - _i3.MacOsOptions? mOptions, + _i3.AppleOptions? mOptions, _i3.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method(#containsKey, [], { @@ -345,11 +353,11 @@ class MockFlutterSecureStorage extends _i1.Mock @override _i6.Future delete({ required String? key, - _i3.IOSOptions? iOptions, + _i3.AppleOptions? iOptions, _i3.AndroidOptions? aOptions, _i3.LinuxOptions? lOptions, _i3.WebOptions? webOptions, - _i3.MacOsOptions? mOptions, + _i3.AppleOptions? mOptions, _i3.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method(#delete, [], { @@ -367,11 +375,11 @@ class MockFlutterSecureStorage extends _i1.Mock @override _i6.Future> readAll({ - _i3.IOSOptions? iOptions, + _i3.AppleOptions? iOptions, _i3.AndroidOptions? aOptions, _i3.LinuxOptions? lOptions, _i3.WebOptions? webOptions, - _i3.MacOsOptions? mOptions, + _i3.AppleOptions? mOptions, _i3.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method(#readAll, [], { @@ -390,11 +398,11 @@ class MockFlutterSecureStorage extends _i1.Mock @override _i6.Future deleteAll({ - _i3.IOSOptions? iOptions, + _i3.AppleOptions? iOptions, _i3.AndroidOptions? aOptions, _i3.LinuxOptions? lOptions, _i3.WebOptions? webOptions, - _i3.MacOsOptions? mOptions, + _i3.AppleOptions? mOptions, _i3.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method(#deleteAll, [], {