Skip to content

Commit 23c3c80

Browse files
authored
Merge pull request #59 from nimblehq/feature/11-integrate-saving-token-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
2 parents 3a1cc91 + f67adba commit 23c3c80

File tree

11 files changed

+203
-4
lines changed

11 files changed

+203
-4
lines changed

ios/Podfile.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ PODS:
22
- Flutter (1.0.0)
33
- flutter_config (0.0.1):
44
- Flutter
5+
- flutter_secure_storage (6.0.0):
6+
- Flutter
57
- integration_test (0.0.1):
68
- Flutter
79
- package_info_plus (0.4.5):
@@ -12,6 +14,7 @@ PODS:
1214
DEPENDENCIES:
1315
- Flutter (from `Flutter`)
1416
- flutter_config (from `.symlinks/plugins/flutter_config/ios`)
17+
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
1518
- integration_test (from `.symlinks/plugins/integration_test/ios`)
1619
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
1720
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
@@ -21,6 +24,8 @@ EXTERNAL SOURCES:
2124
:path: Flutter
2225
flutter_config:
2326
:path: ".symlinks/plugins/flutter_config/ios"
27+
flutter_secure_storage:
28+
:path: ".symlinks/plugins/flutter_secure_storage/ios"
2429
integration_test:
2530
:path: ".symlinks/plugins/integration_test/ios"
2631
package_info_plus:
@@ -31,6 +36,7 @@ EXTERNAL SOURCES:
3136
SPEC CHECKSUMS:
3237
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
3338
flutter_config: 2226c1df19c78fe34a05eb7f1363445f18e76fc1
39+
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
3440
integration_test: 13825b8a9334a850581300559b8839134b124670
3541
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
3642
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
2+
3+
class FlutterSecureStorageProvider {
4+
FlutterSecureStorage? _storage;
5+
6+
FlutterSecureStorage getStorage() {
7+
_storage ??= _createStorage();
8+
return _storage!;
9+
}
10+
11+
FlutterSecureStorage _createStorage() {
12+
return const FlutterSecureStorage(
13+
aOptions: AndroidOptions(
14+
encryptedSharedPreferences: true,
15+
),
16+
);
17+
}
18+
}

lib/model/api_token.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
import 'package:survey_flutter/storage/secure_storage.dart';
3+
4+
part 'api_token.g.dart';
5+
6+
@JsonSerializable()
7+
class ApiToken extends SecureStorageModel {
8+
@JsonKey(name: 'access_token')
9+
final String accessToken;
10+
@JsonKey(name: 'refresh_token')
11+
final String refreshToken;
12+
@JsonKey(name: 'token_type')
13+
final String tokenType;
14+
15+
ApiToken({
16+
required this.accessToken,
17+
required this.refreshToken,
18+
required this.tokenType,
19+
});
20+
21+
factory ApiToken.fromJson(Map<String, dynamic> json) =>
22+
_$ApiTokenFromJson(json);
23+
24+
Map<String, dynamic> toJson() => _$ApiTokenToJson(this);
25+
26+
@override
27+
bool operator ==(Object other) =>
28+
other is ApiToken &&
29+
accessToken == other.accessToken &&
30+
refreshToken == other.refreshToken &&
31+
tokenType == other.tokenType;
32+
33+
@override
34+
int get hashCode => (accessToken + refreshToken + tokenType).hashCode;
35+
}

lib/model/response/login_response.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:json_annotation/json_annotation.dart';
22
import 'package:survey_flutter/api/response_decoder.dart';
3+
import 'package:survey_flutter/model/api_token.dart';
34
import 'package:survey_flutter/model/login_model.dart';
45

56
part 'login_response.g.dart';
@@ -32,6 +33,12 @@ class LoginResponse {
3233
refreshToken: refreshToken,
3334
);
3435

36+
ApiToken toApiToken() => ApiToken(
37+
accessToken: accessToken,
38+
refreshToken: refreshToken,
39+
tokenType: tokenType,
40+
);
41+
3542
static LoginResponse dummy() {
3643
return LoginResponse(
3744
id: "",

lib/repositories/authentication_repository.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import 'package:survey_flutter/di/provider/dio_provider.dart';
55
import 'package:survey_flutter/env.dart';
66
import 'package:survey_flutter/model/login_model.dart';
77
import 'package:survey_flutter/model/request/login_request.dart';
8+
import 'package:survey_flutter/storage/secure_storage.dart';
9+
import 'package:survey_flutter/storage/secure_storage_impl.dart';
810

911
const String _grantType = "password";
1012

1113
final authenticationRepositoryProvider =
12-
Provider<AuthenticationRepository>((_) {
14+
Provider<AuthenticationRepository>((ref) {
1315
return AuthenticationRepositoryImpl(
1416
AuthenticationApiService(DioProvider().getDio()),
17+
ref.watch(secureStorageProvider),
1518
);
1619
});
1720

@@ -24,8 +27,12 @@ abstract class AuthenticationRepository {
2427

2528
class AuthenticationRepositoryImpl extends AuthenticationRepository {
2629
final AuthenticationApiService _authenticationApiService;
30+
final SecureStorage _secureStorage;
2731

28-
AuthenticationRepositoryImpl(this._authenticationApiService);
32+
AuthenticationRepositoryImpl(
33+
this._authenticationApiService,
34+
this._secureStorage,
35+
);
2936

3037
@override
3138
Future<LoginModel> login({
@@ -40,6 +47,10 @@ class AuthenticationRepositoryImpl extends AuthenticationRepository {
4047
clientSecret: Env.clientSecret,
4148
grantType: _grantType,
4249
));
50+
await _secureStorage.save(
51+
value: response.toApiToken(),
52+
key: SecureStorageKey.apiToken,
53+
);
4354
return response.toLoginModel();
4455
} catch (exception) {
4556
throw NetworkExceptions.fromDioException(exception);

lib/storage/secure_storage.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
enum SecureStorageKey {
2+
apiToken,
3+
}
4+
5+
extension SecureStorageKeyExt on SecureStorageKey {
6+
String get string {
7+
switch (this) {
8+
case SecureStorageKey.apiToken:
9+
return 'API_TOKEN_KEY';
10+
}
11+
}
12+
}
13+
14+
abstract class SecureStorageModel {}
15+
16+
enum SecureStorageError {
17+
failToGetValue,
18+
}
19+
20+
abstract class SecureStorage {
21+
Future<void> save<M extends SecureStorageModel>(
22+
{required M value, required SecureStorageKey key});
23+
Future<M> getValue<M extends SecureStorageModel>(
24+
{required SecureStorageKey key});
25+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter_riverpod/flutter_riverpod.dart';
4+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
5+
import 'package:survey_flutter/storage/secure_storage.dart';
6+
7+
import '../di/provider/flutter_secure_storage.dart';
8+
9+
final secureStorageProvider = Provider<SecureStorage>((_) {
10+
return SecureStorageImpl(FlutterSecureStorageProvider().getStorage());
11+
});
12+
13+
class SecureStorageImpl extends SecureStorage {
14+
final FlutterSecureStorage _storage;
15+
SecureStorageImpl(this._storage);
16+
17+
@override
18+
Future<M> getValue<M extends SecureStorageModel>(
19+
{required SecureStorageKey key}) async {
20+
final rawValue = await _storage.read(key: key.string);
21+
if (rawValue == null) {
22+
throw SecureStorageError.failToGetValue;
23+
}
24+
25+
return await jsonDecode(rawValue);
26+
}
27+
28+
@override
29+
Future<void> save<M extends SecureStorageModel>(
30+
{required M value, required SecureStorageKey key}) async {
31+
final encodedValue = jsonEncode(value);
32+
await _storage.write(key: key.string, value: encodedValue);
33+
}
34+
}

pubspec.lock

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,54 @@ packages:
288288
url: "https://pub.dev"
289289
source: hosted
290290
version: "2.3.6"
291+
flutter_secure_storage:
292+
dependency: "direct main"
293+
description:
294+
name: flutter_secure_storage
295+
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5"
296+
url: "https://pub.dev"
297+
source: hosted
298+
version: "8.0.0"
299+
flutter_secure_storage_linux:
300+
dependency: transitive
301+
description:
302+
name: flutter_secure_storage_linux
303+
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
304+
url: "https://pub.dev"
305+
source: hosted
306+
version: "1.1.3"
307+
flutter_secure_storage_macos:
308+
dependency: transitive
309+
description:
310+
name: flutter_secure_storage_macos
311+
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50"
312+
url: "https://pub.dev"
313+
source: hosted
314+
version: "3.0.0"
315+
flutter_secure_storage_platform_interface:
316+
dependency: transitive
317+
description:
318+
name: flutter_secure_storage_platform_interface
319+
sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b
320+
url: "https://pub.dev"
321+
source: hosted
322+
version: "1.0.1"
323+
flutter_secure_storage_web:
324+
dependency: transitive
325+
description:
326+
name: flutter_secure_storage_web
327+
sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe"
328+
url: "https://pub.dev"
329+
source: hosted
330+
version: "1.1.1"
331+
flutter_secure_storage_windows:
332+
dependency: transitive
333+
description:
334+
name: flutter_secure_storage_windows
335+
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee
336+
url: "https://pub.dev"
337+
source: hosted
338+
version: "2.0.0"
291339
flutter_svg:
292340
dependency: "direct main"
293341
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies:
4747
equatable: ^2.0.0
4848
internet_connection_checker: ^1.0.0+1
4949
page_view_dot_indicator: ^2.1.0
50+
flutter_secure_storage: ^8.0.0
5051

5152
dev_dependencies:
5253
build_runner: ^2.4.4

test/api/repositories/authentication_repository_test.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import 'package:flutter_config/flutter_config.dart';
22
import 'package:flutter_test/flutter_test.dart';
33
import 'package:mockito/mockito.dart';
4-
import 'package:survey_flutter/repositories/authentication_repository.dart';
54
import 'package:survey_flutter/api/exception/network_exceptions.dart';
65
import 'package:survey_flutter/model/response/login_response.dart';
6+
import 'package:survey_flutter/repositories/authentication_repository.dart';
7+
import 'package:survey_flutter/storage/secure_storage.dart';
78

89
import '../../mocks/generate_mocks.mocks.dart';
910

1011
void main() {
1112
group('AuthenticationRepositoryTest', () {
1213
late MockAuthenticationApiService mockAuthApiService;
14+
late MockSecureStorage mockSecureStorage;
1315
late AuthenticationRepositoryImpl authRepository;
1416

1517
const email = "email";
@@ -24,7 +26,11 @@ void main() {
2426

2527
setUp(() {
2628
mockAuthApiService = MockAuthenticationApiService();
27-
authRepository = AuthenticationRepositoryImpl(mockAuthApiService);
29+
mockSecureStorage = MockSecureStorage();
30+
authRepository = AuthenticationRepositoryImpl(
31+
mockAuthApiService,
32+
mockSecureStorage,
33+
);
2834
});
2935

3036
test('When login successfully, it returns correct model', () async {
@@ -37,6 +43,12 @@ void main() {
3743
await authRepository.login(email: email, password: password);
3844

3945
expect(result, loginResponse.toLoginModel());
46+
verify(
47+
mockSecureStorage.save(
48+
value: loginResponse.toApiToken(),
49+
key: SecureStorageKey.apiToken,
50+
),
51+
).called(1);
4052
});
4153

4254
test('When login fail, it returns failed exception', () async {

0 commit comments

Comments
 (0)