Skip to content

Commit

Permalink
Merge pull request #68 from nimblehq/release/0.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
nkhanh44 authored Aug 25, 2023
2 parents d5e3bdf + 5aa9335 commit 7e01764
Show file tree
Hide file tree
Showing 48 changed files with 1,136 additions and 154 deletions.
Binary file removed assets/images/dummy_background.png
Binary file not shown.
Binary file added assets/images/placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ PODS:
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.1.1):
- Flutter

Expand All @@ -17,6 +20,7 @@ DEPENDENCIES:
- 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`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)

EXTERNAL SOURCES:
Expand All @@ -30,6 +34,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"

Expand All @@ -39,6 +45,7 @@ SPEC CHECKSUMS:
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
integration_test: 13825b8a9334a850581300559b8839134b124670
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6

PODFILE CHECKSUM: 632d6ac0b577d6e268ff7a13a105bbc4f7941989
Expand Down
14 changes: 10 additions & 4 deletions lib/api/authentication_api_service.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'package:dio/dio.dart';
import 'package:survey_flutter/model/request/login_request.dart';
import 'package:survey_flutter/model/response/login_response.dart';
import 'package:retrofit/retrofit.dart';
import 'package:survey_flutter/model/request/login_request.dart';
import 'package:survey_flutter/model/request/refresh_token_request.dart';
import 'package:survey_flutter/model/response/token_response.dart';

part 'authentication_api_service.g.dart';

Expand All @@ -11,7 +12,12 @@ abstract class AuthenticationApiService {
_AuthenticationApiService;

@POST('/oauth/token')
Future<LoginResponse> login(
@Body() LoginRequest body,
Future<TokenResponse> login(
@Body() LoginRequest loginRequest,
);

@POST('/oauth/token')
Future<TokenResponse> refreshToken(
@Body() RefreshTokenRequest refreshTokenRequest,
);
}
62 changes: 62 additions & 0 deletions lib/api/data_sources/token_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:survey_flutter/api/authentication_api_service.dart';
import 'package:survey_flutter/di/provider/dio_provider.dart';
import 'package:survey_flutter/env.dart';
import 'package:survey_flutter/model/api_token.dart';
import 'package:survey_flutter/model/request/refresh_token_request.dart';
import 'package:survey_flutter/storage/secure_storage.dart';
import 'package:survey_flutter/storage/secure_storage_impl.dart';
import 'package:survey_flutter/utils/serializer/api_token_serializer.dart';

final tokenDataSourceProvider = Provider<TokenDataSource>((ref) {
return TokenDataSourceImpl(ref.watch(secureStorageProvider),
AuthenticationApiService(DioProvider().getDio()));
});

abstract class TokenDataSource {
Future<ApiToken> getToken({bool forceRefresh});
Future<void> setToken(ApiToken token);
}

class TokenDataSourceImpl extends TokenDataSource {
final SecureStorage _secureStorage;
final AuthenticationApiService _authenticationApiService;
final String _grantType = 'refresh_token';

TokenDataSourceImpl(
this._secureStorage,
this._authenticationApiService,
);

@override
Future<ApiToken> getToken({bool forceRefresh = false}) async {
final currentToken = await _secureStorage.getValue(
key: SecureStorageKey.apiToken,
serializer: ApiTokenSerializer(),
);

if (!forceRefresh) {
return currentToken;
}

final tokenResponse = await _authenticationApiService.refreshToken(
RefreshTokenRequest(
grantType: _grantType,
refreshToken: currentToken.refreshToken,
clientId: Env.clientId,
clientSecret: Env.clientSecret,
),
);
final newToken = tokenResponse.toApiToken();
await _secureStorage.save(
value: newToken,
key: SecureStorageKey.apiToken,
);
return newToken;
}

@override
Future<void> setToken(ApiToken token) async {
await _secureStorage.save(value: token, key: SecureStorageKey.apiToken);
}
}
58 changes: 58 additions & 0 deletions lib/api/interceptor/auth_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:survey_flutter/api/data_sources/token_data_source.dart';

const String _headerAuthorization = 'Authorization';
const String _retryCountOption = 'Retry-Count';

class AuthInterceptor extends Interceptor {
final Dio _dio;
final TokenDataSource _tokenDataSource;

AuthInterceptor(
this._dio,
this._tokenDataSource,
);

@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final token = await _tokenDataSource.getToken();
options.headers.putIfAbsent(
_headerAuthorization, () => "${token.tokenType} ${token.accessToken}");
super.onRequest(options, handler);
}

@override
void onError(
DioError err,
ErrorInterceptorHandler handler,
) {
final statusCode = err.response?.statusCode;
final requestOptions = err.requestOptions;

if (statusCode != HttpStatus.forbidden &&
statusCode != HttpStatus.unauthorized &&
requestOptions.extra[_retryCountOption] != 1) {
handler.next(err);
return;
}

requestOptions.extra[_retryCountOption] = 1;
_refreshTokenAndRetry(requestOptions, handler);
}

Future<void> _refreshTokenAndRetry(
RequestOptions options,
ErrorInterceptorHandler handler,
) async {
final token = await _tokenDataSource.getToken(forceRefresh: true);
final headers = options.headers;
headers[_headerAuthorization] = "${token.tokenType} ${token.accessToken}";
final newOptions = options.copyWith(headers: headers);
await _dio.fetch(newOptions).then((response) => handler.resolve(response));
}
}
7 changes: 4 additions & 3 deletions lib/api/response_decoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import 'package:japx/japx.dart';

class ResponseDecoder {
static Map<String, dynamic> decodeData(Map<String, dynamic> json) {
return Japx.decode(json)['data'];
return json.containsKey('data') ? Japx.decode(json)['data'] : json;
}

static Map<String, dynamic> decode(Map<String, dynamic> json) =>
Japx.decode(json);
static Map<String, dynamic> decode(Map<String, dynamic> json) {
return Japx.decode(json);
}
}
28 changes: 21 additions & 7 deletions lib/di/provider/dio_provider.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:survey_flutter/di/interceptor/app_interceptor.dart';
import 'package:survey_flutter/api/data_sources/token_data_source.dart';
import 'package:survey_flutter/api/interceptor/auth_interceptor.dart';
import 'package:survey_flutter/env.dart';

const String _headerContentType = 'Content-Type';
const String _defaultContentType = 'application/json; charset=utf-8';

class DioProvider {
Dio? _dio;
Dio? _authorizedDio;
TokenDataSource? _tokenDataSource;

Dio getDio() {
_dio ??= _createDio();
return _dio!;
}

Dio _createDio({bool requireAuthenticate = false}) {
Dio getAuthorizedDio({
required TokenDataSource tokenDataSource,
}) {
_tokenDataSource = tokenDataSource;
_authorizedDio ??= _createDio(requireAuthentication: true);
return _authorizedDio!;
}

Dio _createDio({bool requireAuthentication = false}) {
final dio = Dio();
final appInterceptor = AppInterceptor(
requireAuthenticate,
dio,
);

final interceptors = <Interceptor>[];
interceptors.add(appInterceptor);
if (requireAuthentication) {
final authInterceptor = AuthInterceptor(
dio,
_tokenDataSource!,
);
interceptors.add(authInterceptor);
}
if (!kReleaseMode) {
interceptors.add(LogInterceptor(
request: true,
Expand Down
3 changes: 2 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"noInternetConnectionError": "You seem to be offline.\nPlease try again!",
"genericError": "Something went wrong.\nPlease try again!",
"loginFailAlertTitle": "Unable to log in",
"today": "Today"
"today": "Today",
"errorText": "Error"
}
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import 'package:go_router/go_router.dart';
import 'package:survey_flutter/screens/home/home_screen.dart';
import 'package:survey_flutter/screens/login/login_screen.dart';
import 'package:survey_flutter/screens/splash/splash_screen.dart';
import 'package:survey_flutter/storage/survey_storage.dart';
import 'package:survey_flutter/theme/app_theme.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterConfig.loadEnvVariables();
await setupHive();
runApp(
ProviderScope(
child: App(),
Expand Down
4 changes: 2 additions & 2 deletions lib/model/api_token.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:survey_flutter/storage/secure_storage.dart';
import 'package:survey_flutter/utils/serializer/serializable.dart';

part 'api_token.g.dart';

@JsonSerializable()
class ApiToken extends SecureStorageModel {
class ApiToken extends Serializable {
@JsonKey(name: 'access_token')
final String accessToken;
@JsonKey(name: 'refresh_token')
Expand Down
24 changes: 24 additions & 0 deletions lib/model/request/refresh_token_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'refresh_token_request.g.dart';

@JsonSerializable()
class RefreshTokenRequest {
@JsonKey(name: 'grant_type')
final String grantType;
@JsonKey(name: 'refresh_token')
final String refreshToken;
@JsonKey(name: 'client_id')
final String clientId;
@JsonKey(name: 'client_secret')
final String clientSecret;

RefreshTokenRequest({
required this.grantType,
required this.refreshToken,
required this.clientId,
required this.clientSecret,
});

Map<String, dynamic> toJson() => _$RefreshTokenRequestToJson(this);
}
2 changes: 1 addition & 1 deletion lib/model/response/meta_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class MetaResponse {
});

factory MetaResponse.fromJson(Map<String, dynamic> json) =>
_$MetaResponseFromJson(ResponseDecoder.decode(json));
_$MetaResponseFromJson(ResponseDecoder.decodeData(json));

static MetaResponse dummy() {
return MetaResponse(
Expand Down
5 changes: 4 additions & 1 deletion lib/model/response/survey_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class SurveyResponse {
id: id ?? '',
title: title ?? '',
description: description ?? '',
coverImageUrl: coverImageUrl ?? '',
coverImageUrl: highResolutionCoverImageUrl,
);

String get highResolutionCoverImageUrl =>
(coverImageUrl != null) ? "${coverImageUrl}l" : "";
}
5 changes: 2 additions & 3 deletions lib/model/response/surveys_container_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ class SurveysContainerResponse {
required this.meta,
});

factory SurveysContainerResponse.fromJson(Map<String, dynamic> json) {
return _$SurveysContainerResponseFromJson(ResponseDecoder.decode(json));
}
factory SurveysContainerResponse.fromJson(Map<String, dynamic> json) =>
_$SurveysContainerResponseFromJson(ResponseDecoder.decode(json));

SurveysContainerModel toSurveysContainerModel() => SurveysContainerModel(
surveys:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ 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';
part 'token_response.g.dart';

@JsonSerializable()
class LoginResponse {
class TokenResponse {
final String id;
final String accessToken;
final String tokenType;
final double expiresIn;
final String refreshToken;
final int createdAt;

LoginResponse({
TokenResponse({
required this.id,
required this.accessToken,
required this.tokenType,
Expand All @@ -23,8 +23,8 @@ class LoginResponse {
required this.createdAt,
});

factory LoginResponse.fromJson(Map<String, dynamic> json) =>
_$LoginResponseFromJson(ResponseDecoder.decodeData(json));
factory TokenResponse.fromJson(Map<String, dynamic> json) =>
_$TokenResponseFromJson(ResponseDecoder.decodeData(json));

LoginModel toLoginModel() => LoginModel(
id: id,
Expand All @@ -39,8 +39,8 @@ class LoginResponse {
tokenType: tokenType,
);

static LoginResponse dummy() {
return LoginResponse(
static TokenResponse dummy() {
return TokenResponse(
id: "",
accessToken: "",
tokenType: "",
Expand Down
Loading

0 comments on commit 7e01764

Please sign in to comment.