diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fd60f01..2cc4ac1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,6 +2,8 @@ PODS: - Flutter (1.0.0) - flutter_config (0.0.1): - Flutter + - flutter_secure_storage (6.0.0): + - Flutter - integration_test (0.0.1): - Flutter - package_info_plus (0.4.5): @@ -12,6 +14,7 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) - flutter_config (from `.symlinks/plugins/flutter_config/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -21,6 +24,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_config: :path: ".symlinks/plugins/flutter_config/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" package_info_plus: @@ -31,6 +36,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_config: 2226c1df19c78fe34a05eb7f1363445f18e76fc1 + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be integration_test: 13825b8a9334a850581300559b8839134b124670 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 diff --git a/lib/di/provider/flutter_secure_storage.dart b/lib/di/provider/flutter_secure_storage.dart new file mode 100644 index 0000000..dae9543 --- /dev/null +++ b/lib/di/provider/flutter_secure_storage.dart @@ -0,0 +1,18 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class FlutterSecureStorageProvider { + FlutterSecureStorage? _storage; + + FlutterSecureStorage getStorage() { + _storage ??= _createStorage(); + return _storage!; + } + + FlutterSecureStorage _createStorage() { + return const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + ); + } +} diff --git a/lib/model/api_token.dart b/lib/model/api_token.dart new file mode 100644 index 0000000..41d07f4 --- /dev/null +++ b/lib/model/api_token.dart @@ -0,0 +1,35 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:survey_flutter/storage/secure_storage.dart'; + +part 'api_token.g.dart'; + +@JsonSerializable() +class ApiToken extends SecureStorageModel { + @JsonKey(name: 'access_token') + final String accessToken; + @JsonKey(name: 'refresh_token') + final String refreshToken; + @JsonKey(name: 'token_type') + final String tokenType; + + ApiToken({ + required this.accessToken, + required this.refreshToken, + required this.tokenType, + }); + + factory ApiToken.fromJson(Map json) => + _$ApiTokenFromJson(json); + + Map toJson() => _$ApiTokenToJson(this); + + @override + bool operator ==(Object other) => + other is ApiToken && + accessToken == other.accessToken && + refreshToken == other.refreshToken && + tokenType == other.tokenType; + + @override + int get hashCode => (accessToken + refreshToken + tokenType).hashCode; +} diff --git a/lib/model/response/login_response.dart b/lib/model/response/login_response.dart index ad026ef..0a02f44 100644 --- a/lib/model/response/login_response.dart +++ b/lib/model/response/login_response.dart @@ -1,5 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:survey_flutter/api/response_decoder.dart'; +import 'package:survey_flutter/model/api_token.dart'; import 'package:survey_flutter/model/login_model.dart'; part 'login_response.g.dart'; @@ -32,6 +33,12 @@ class LoginResponse { refreshToken: refreshToken, ); + ApiToken toApiToken() => ApiToken( + accessToken: accessToken, + refreshToken: refreshToken, + tokenType: tokenType, + ); + static LoginResponse dummy() { return LoginResponse( id: "", diff --git a/lib/repositories/authentication_repository.dart b/lib/repositories/authentication_repository.dart index 4ff6a17..7f9a449 100644 --- a/lib/repositories/authentication_repository.dart +++ b/lib/repositories/authentication_repository.dart @@ -5,13 +5,16 @@ import 'package:survey_flutter/di/provider/dio_provider.dart'; import 'package:survey_flutter/env.dart'; import 'package:survey_flutter/model/login_model.dart'; import 'package:survey_flutter/model/request/login_request.dart'; +import 'package:survey_flutter/storage/secure_storage.dart'; +import 'package:survey_flutter/storage/secure_storage_impl.dart'; const String _grantType = "password"; final authenticationRepositoryProvider = - Provider((_) { + Provider((ref) { return AuthenticationRepositoryImpl( AuthenticationApiService(DioProvider().getDio()), + ref.watch(secureStorageProvider), ); }); @@ -24,8 +27,12 @@ abstract class AuthenticationRepository { class AuthenticationRepositoryImpl extends AuthenticationRepository { final AuthenticationApiService _authenticationApiService; + final SecureStorage _secureStorage; - AuthenticationRepositoryImpl(this._authenticationApiService); + AuthenticationRepositoryImpl( + this._authenticationApiService, + this._secureStorage, + ); @override Future login({ @@ -40,6 +47,10 @@ class AuthenticationRepositoryImpl extends AuthenticationRepository { clientSecret: Env.clientSecret, grantType: _grantType, )); + await _secureStorage.save( + value: response.toApiToken(), + key: SecureStorageKey.apiToken, + ); return response.toLoginModel(); } catch (exception) { throw NetworkExceptions.fromDioException(exception); diff --git a/lib/storage/secure_storage.dart b/lib/storage/secure_storage.dart new file mode 100644 index 0000000..d441c3d --- /dev/null +++ b/lib/storage/secure_storage.dart @@ -0,0 +1,25 @@ +enum SecureStorageKey { + apiToken, +} + +extension SecureStorageKeyExt on SecureStorageKey { + String get string { + switch (this) { + case SecureStorageKey.apiToken: + return 'API_TOKEN_KEY'; + } + } +} + +abstract class SecureStorageModel {} + +enum SecureStorageError { + failToGetValue, +} + +abstract class SecureStorage { + Future save( + {required M value, required SecureStorageKey key}); + Future getValue( + {required SecureStorageKey key}); +} diff --git a/lib/storage/secure_storage_impl.dart b/lib/storage/secure_storage_impl.dart new file mode 100644 index 0000000..00f4741 --- /dev/null +++ b/lib/storage/secure_storage_impl.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:survey_flutter/storage/secure_storage.dart'; + +import '../di/provider/flutter_secure_storage.dart'; + +final secureStorageProvider = Provider((_) { + return SecureStorageImpl(FlutterSecureStorageProvider().getStorage()); +}); + +class SecureStorageImpl extends SecureStorage { + final FlutterSecureStorage _storage; + SecureStorageImpl(this._storage); + + @override + Future getValue( + {required SecureStorageKey key}) async { + final rawValue = await _storage.read(key: key.string); + if (rawValue == null) { + throw SecureStorageError.failToGetValue; + } + + return await jsonDecode(rawValue); + } + + @override + Future save( + {required M value, required SecureStorageKey key}) async { + final encodedValue = jsonEncode(value); + await _storage.write(key: key.string, value: encodedValue); + } +} diff --git a/pubspec.lock b/pubspec.lock index 2b226e7..4cffea3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -288,6 +288,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + url: "https://pub.dev" + source: hosted + version: "1.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + url: "https://pub.dev" + source: hosted + version: "2.0.0" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cdbe306..69832e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: equatable: ^2.0.0 internet_connection_checker: ^1.0.0+1 page_view_dot_indicator: ^2.1.0 + flutter_secure_storage: ^8.0.0 dev_dependencies: build_runner: ^2.4.4 diff --git a/test/api/repositories/authentication_repository_test.dart b/test/api/repositories/authentication_repository_test.dart index 0a257c7..28ed4fd 100644 --- a/test/api/repositories/authentication_repository_test.dart +++ b/test/api/repositories/authentication_repository_test.dart @@ -1,15 +1,17 @@ import 'package:flutter_config/flutter_config.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:survey_flutter/repositories/authentication_repository.dart'; import 'package:survey_flutter/api/exception/network_exceptions.dart'; import 'package:survey_flutter/model/response/login_response.dart'; +import 'package:survey_flutter/repositories/authentication_repository.dart'; +import 'package:survey_flutter/storage/secure_storage.dart'; import '../../mocks/generate_mocks.mocks.dart'; void main() { group('AuthenticationRepositoryTest', () { late MockAuthenticationApiService mockAuthApiService; + late MockSecureStorage mockSecureStorage; late AuthenticationRepositoryImpl authRepository; const email = "email"; @@ -24,7 +26,11 @@ void main() { setUp(() { mockAuthApiService = MockAuthenticationApiService(); - authRepository = AuthenticationRepositoryImpl(mockAuthApiService); + mockSecureStorage = MockSecureStorage(); + authRepository = AuthenticationRepositoryImpl( + mockAuthApiService, + mockSecureStorage, + ); }); test('When login successfully, it returns correct model', () async { @@ -37,6 +43,12 @@ void main() { await authRepository.login(email: email, password: password); expect(result, loginResponse.toLoginModel()); + verify( + mockSecureStorage.save( + value: loginResponse.toApiToken(), + key: SecureStorageKey.apiToken, + ), + ).called(1); }); test('When login fail, it returns failed exception', () async { diff --git a/test/mocks/generate_mocks.dart b/test/mocks/generate_mocks.dart index f4fc561..f130b4b 100644 --- a/test/mocks/generate_mocks.dart +++ b/test/mocks/generate_mocks.dart @@ -3,6 +3,7 @@ import 'package:mockito/annotations.dart'; import 'package:survey_flutter/api/authentication_api_service.dart'; import 'package:survey_flutter/api/survey_api_service.dart'; import 'package:survey_flutter/repositories/authentication_repository.dart'; +import 'package:survey_flutter/storage/secure_storage.dart'; import 'package:survey_flutter/usecases/login_use_case.dart'; import 'package:survey_flutter/utils/internet_connection_manager.dart'; import 'package:survey_flutter/repositories/survey_repository.dart'; @@ -16,6 +17,7 @@ import '../utils/async_listener.dart'; DioError, InternetConnectionManager, LoginUseCase, + SecureStorage, SurveyRepository, SurveyApiService, ])