Skip to content

Commit

Permalink
Merge pull request #59 from nimblehq/feature/11-integrate-saving-toke…
Browse files Browse the repository at this point in the history
…n-in-local-storage

[#11] [Integrate] As a user, once I have logged in, I can see the app stores the access token in the local storage
  • Loading branch information
nkhanh44 authored Aug 18, 2023
2 parents 3a1cc91 + f67adba commit 23c3c80
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 4 deletions.
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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`)
Expand All @@ -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:
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions lib/di/provider/flutter_secure_storage.dart
Original file line number Diff line number Diff line change
@@ -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,
),
);
}
}
35 changes: 35 additions & 0 deletions lib/model/api_token.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> json) =>
_$ApiTokenFromJson(json);

Map<String, dynamic> 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;
}
7 changes: 7 additions & 0 deletions lib/model/response/login_response.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -32,6 +33,12 @@ class LoginResponse {
refreshToken: refreshToken,
);

ApiToken toApiToken() => ApiToken(
accessToken: accessToken,
refreshToken: refreshToken,
tokenType: tokenType,
);

static LoginResponse dummy() {
return LoginResponse(
id: "",
Expand Down
15 changes: 13 additions & 2 deletions lib/repositories/authentication_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthenticationRepository>((_) {
Provider<AuthenticationRepository>((ref) {
return AuthenticationRepositoryImpl(
AuthenticationApiService(DioProvider().getDio()),
ref.watch(secureStorageProvider),
);
});

Expand All @@ -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<LoginModel> login({
Expand All @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions lib/storage/secure_storage.dart
Original file line number Diff line number Diff line change
@@ -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<void> save<M extends SecureStorageModel>(
{required M value, required SecureStorageKey key});
Future<M> getValue<M extends SecureStorageModel>(
{required SecureStorageKey key});
}
34 changes: 34 additions & 0 deletions lib/storage/secure_storage_impl.dart
Original file line number Diff line number Diff line change
@@ -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<SecureStorage>((_) {
return SecureStorageImpl(FlutterSecureStorageProvider().getStorage());
});

class SecureStorageImpl extends SecureStorage {
final FlutterSecureStorage _storage;
SecureStorageImpl(this._storage);

@override
Future<M> getValue<M extends SecureStorageModel>(
{required SecureStorageKey key}) async {
final rawValue = await _storage.read(key: key.string);
if (rawValue == null) {
throw SecureStorageError.failToGetValue;
}

return await jsonDecode(rawValue);
}

@override
Future<void> save<M extends SecureStorageModel>(
{required M value, required SecureStorageKey key}) async {
final encodedValue = jsonEncode(value);
await _storage.write(key: key.string, value: encodedValue);
}
}
48 changes: 48 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions test/api/repositories/authentication_repository_test.dart
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions test/mocks/generate_mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,6 +17,7 @@ import '../utils/async_listener.dart';
DioError,
InternetConnectionManager,
LoginUseCase,
SecureStorage,
SurveyRepository,
SurveyApiService,
])
Expand Down

0 comments on commit 23c3c80

Please sign in to comment.