diff --git a/.github/workflows/login_client_flutter-publish.yml b/.github/workflows/login_client_flutter-publish.yml index e3c61b4a..c64fa6cf 100644 --- a/.github/workflows/login_client_flutter-publish.yml +++ b/.github/workflows/login_client_flutter-publish.yml @@ -24,12 +24,12 @@ jobs: - name: Set up Dart uses: dart-lang/setup-dart@v1 with: - sdk: 3.3 + sdk: 3.7 - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.19.x' + flutter-version: '3.29.x' cache: true - name: Publish diff --git a/.github/workflows/login_client_flutter-test.yml b/.github/workflows/login_client_flutter-test.yml index 67c4c74f..909724fb 100644 --- a/.github/workflows/login_client_flutter-test.yml +++ b/.github/workflows/login_client_flutter-test.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: include: - - version: '3.19.x' + - version: '3.29.x' defaults: run: diff --git a/packages/login_client_flutter/CHANGELOG.md b/packages/login_client_flutter/CHANGELOG.md index 2937af25..8ac7ab91 100644 --- a/packages/login_client_flutter/CHANGELOG.md +++ b/packages/login_client_flutter/CHANGELOG.md @@ -1,7 +1,9 @@ -# Unreleased +# 3.1.0 -- Bump `leancode_lint` dev dependency to `12.0.0`. -- Bump `custom_lint` dev dependency to `0.6.4`. +- Add configurable `FlutterSecureStorage` support to `FlutterSecureCredentialsStorage`. The constructor now accepts an optional `storage` parameter, allowing users to provide a custom `FlutterSecureStorage` instance with platform-specific configuration options (e.g., iOS KeyChain accessibility settings). +- Bump minimum Dart SDK version to `3.7.0`. +- Bump minimum Flutter version to `3.29.0`. +- Bump `leancode_lint` dev dependency to `16.0.0`. # 3.0.0 diff --git a/packages/login_client_flutter/README.md b/packages/login_client_flutter/README.md index a0ac735c..bcb1aab7 100644 --- a/packages/login_client_flutter/README.md +++ b/packages/login_client_flutter/README.md @@ -8,12 +8,33 @@ ## Usage ```dart +// Default usage final loginClient = LoginClient( credentialsStorage: const FlutterSecureCredentialsStorage(), // ... ); ``` +### Custom Configuration + +You can provide a custom `FlutterSecureStorage` instance to configure platform-specific options: + +```dart +final loginClient = LoginClient( + credentialsStorage: const FlutterSecureCredentialsStorage( + storage: FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + iOptions: IOSOptions( + accessibility: KeychainAccessibility.first_unlock_this_device, + ), + ), + ), + // ... +); +``` + ## Android `javax.crypto.BadPaddingException` Exclude Flutter Secure Storage from Android full backup. diff --git a/packages/login_client_flutter/lib/login_client_flutter.dart b/packages/login_client_flutter/lib/login_client_flutter.dart index b38ab37d..5e72c652 100644 --- a/packages/login_client_flutter/lib/login_client_flutter.dart +++ b/packages/login_client_flutter/lib/login_client_flutter.dart @@ -16,6 +16,6 @@ /// /// See also: /// - https://github.com/mogol/flutter_secure_storage -library login_client_flutter; +library; export 'src/flutter_secure_credentials_storage.dart'; diff --git a/packages/login_client_flutter/lib/src/flutter_secure_credentials_storage.dart b/packages/login_client_flutter/lib/src/flutter_secure_credentials_storage.dart index f6d5b1ef..8372ded2 100644 --- a/packages/login_client_flutter/lib/src/flutter_secure_credentials_storage.dart +++ b/packages/login_client_flutter/lib/src/flutter_secure_credentials_storage.dart @@ -15,13 +15,21 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:login_client/login_client.dart'; +export 'package:flutter_secure_storage/flutter_secure_storage.dart'; + /// A `flutter_secure_storage` implementation of the [CredentialsStorage]. class FlutterSecureCredentialsStorage implements CredentialsStorage { /// Creates the [CredentialsStorage]. - const FlutterSecureCredentialsStorage(); + /// + /// The optional [storage] parameter allows you to provide a custom + /// [FlutterSecureStorage] instance with specific configuration options. + /// If not provided, a default instance will be used. + const FlutterSecureCredentialsStorage({ + FlutterSecureStorage storage = const FlutterSecureStorage(), + }) : _storage = storage; static const _key = 'login_client_flutter_credentials'; - FlutterSecureStorage get _storage => const FlutterSecureStorage(); + final FlutterSecureStorage _storage; @override Future read() async { diff --git a/packages/login_client_flutter/pubspec.yaml b/packages/login_client_flutter/pubspec.yaml index 6df696f2..d9f87072 100644 --- a/packages/login_client_flutter/pubspec.yaml +++ b/packages/login_client_flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: login_client_flutter -version: 3.0.0 +version: 3.1.0 homepage: https://github.com/leancodepl/flutter_corelibrary/tree/master/packages/login_client_flutter repository: https://github.com/leancodepl/flutter_corelibrary description: >- @@ -7,8 +7,8 @@ description: >- for the login_client package. environment: - sdk: '>=3.0.0 <4.0.0' - flutter: '>=3.10.0' + sdk: '>=3.7.0 <4.0.0' + flutter: '>=3.29.0' dependencies: flutter: @@ -17,5 +17,6 @@ dependencies: login_client: ^3.0.0 dev_dependencies: - custom_lint: ^0.6.4 - leancode_lint: ^12.0.0 + flutter_test: + sdk: flutter + leancode_lint: ^16.0.0 diff --git a/packages/login_client_flutter/test/flutter_secure_credentials_storage_test.dart b/packages/login_client_flutter/test/flutter_secure_credentials_storage_test.dart new file mode 100644 index 00000000..6b62a21c --- /dev/null +++ b/packages/login_client_flutter/test/flutter_secure_credentials_storage_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_client_flutter/login_client_flutter.dart'; +// for testing credentials +// ignore: depend_on_referenced_packages +import 'package:oauth2/oauth2.dart'; + +// Mock FlutterSecureStorage for testing +class MockFlutterSecureStorage extends FlutterSecureStorage { + final Map _storage = {}; + + @override + Future read({ + required String key, + IOSOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + MacOsOptions? mOptions, + WindowsOptions? wOptions, + }) async { + return _storage[key]; + } + + @override + Future write({ + required String key, + required String? value, + IOSOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + MacOsOptions? mOptions, + WindowsOptions? wOptions, + }) async { + if (value == null) { + _storage.remove(key); + } else { + _storage[key] = value; + } + } + + @override + Future delete({ + required String key, + IOSOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + MacOsOptions? mOptions, + WindowsOptions? wOptions, + }) async { + _storage.remove(key); + } +} + +void main() { + group('FlutterSecureCredentialsStorage', () { + test('uses default storage when none provided', () { + const storage = FlutterSecureCredentialsStorage(); + + expect(storage, isA()); + }); + + test('accepts custom storage instance', () { + final customStorage = MockFlutterSecureStorage(); + final storage = FlutterSecureCredentialsStorage(storage: customStorage); + + expect(storage, isA()); + }); + + test('works with custom storage - read/write/clear operations', () async { + final customStorage = MockFlutterSecureStorage(); + final storage = FlutterSecureCredentialsStorage(storage: customStorage); + + expect(await storage.read(), null); + + final credentials = Credentials('test_token'); + await storage.save(credentials); + + final readCredentials = await storage.read(); + expect(readCredentials?.accessToken, equals('test_token')); + + await storage.clear(); + expect(await storage.read(), null); + }); + }); +}