Skip to content

Commit

Permalink
Merge pull request #57 from nimblehq/feature/10-integrate-sign-in
Browse files Browse the repository at this point in the history
[#10] [Integrate] As a user, I can sign in with email and password
  • Loading branch information
nkhanh44 authored Aug 18, 2023
2 parents be5b16a + 8343044 commit 62cdcbc
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 73 deletions.
14 changes: 8 additions & 6 deletions lib/di/provider/dio_provider.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:survey_flutter/di/interceptor/app_interceptor.dart';
import 'package:survey_flutter/env.dart';

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

class DioProvider {
Dio? _dio;
Expand Down Expand Up @@ -31,9 +32,10 @@ class DioProvider {
}

return dio
..options.connectTimeout = const Duration(seconds: 3000)
..options.receiveTimeout = const Duration(seconds: 5000)
..options.headers = {headerContentType: defaultContentType}
..interceptors.addAll(interceptors);
..options.connectTimeout = const Duration(seconds: 3)
..options.receiveTimeout = const Duration(seconds: 5)
..options.headers = {_headerContentType: _defaultContentType}
..interceptors.addAll(interceptors)
..options.baseUrl = Env.restApiEndpoint;
}
}
5 changes: 5 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter_config/flutter_config.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
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/theme/app_theme.dart';
Expand Down Expand Up @@ -34,6 +35,10 @@ class App extends StatelessWidget {
child: LoginScreen(),
),
),
GoRoute(
path: routePathHomeScreen,
builder: (_, __) => const HomeScreen(),
),
],
);

Expand Down
11 changes: 9 additions & 2 deletions lib/repositories/authentication_repository.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:survey_flutter/api/authentication_api_service.dart';
import 'package:survey_flutter/api/exception/network_exceptions.dart';
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:injectable/injectable.dart';

const String _grantType = "password";

final authenticationRepositoryProvider =
Provider<AuthenticationRepository>((_) {
return AuthenticationRepositoryImpl(
AuthenticationApiService(DioProvider().getDio()),
);
});

abstract class AuthenticationRepository {
Future<LoginModel> login({
required String email,
required String password,
});
}

@Singleton(as: AuthenticationRepository)
class AuthenticationRepositoryImpl extends AuthenticationRepository {
final AuthenticationApiService _authenticationApiService;

Expand Down
2 changes: 2 additions & 0 deletions lib/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'package:survey_flutter/screens/home/home_header_widget.dart';
import 'package:survey_flutter/screens/home/home_pages_widget.dart';
import 'package:survey_flutter/screens/home/home_page_indicator_widget.dart';

const routePathHomeScreen = '/home';

class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);

Expand Down
16 changes: 15 additions & 1 deletion lib/screens/login/login_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:survey_flutter/theme/primary_text_field_decoration.dart';
import 'package:survey_flutter/utils/build_context_ext.dart';

const _fieldSpacing = 20.0;
const _loadingIndicatorSize = 28.0;

class LoginForm extends ConsumerStatefulWidget {
const LoginForm({Key? key}) : super(key: key);
Expand Down Expand Up @@ -55,7 +56,20 @@ class _LoginFormState extends ConsumerState<LoginForm> {
ElevatedButton get _loginButton => ElevatedButton(
style: PrimaryButtonStyle(hintTextStyle: context.textTheme.labelMedium),
onPressed: _submit,
child: Text(context.localizations.loginButton),
child: Consumer(
builder: (_, widgetRef, __) {
final loginVievModel = widgetRef.watch(loginViewModelProvider);
return (loginVievModel.isLoading)
? const SizedBox(
width: _loadingIndicatorSize,
height: _loadingIndicatorSize,
child: CircularProgressIndicator(
color: Colors.black45,
),
)
: Text(context.localizations.loginButton);
},
),
);

String? _validateEmail(String? email) {
Expand Down
6 changes: 3 additions & 3 deletions lib/screens/login/login_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:survey_flutter/gen/assets.gen.dart';
import 'package:survey_flutter/screens/home/home_screen.dart';
import 'package:survey_flutter/screens/login/login_form.dart';
import 'package:survey_flutter/screens/login/login_view_model.dart';
import 'package:survey_flutter/theme/app_constants.dart';
Expand Down Expand Up @@ -115,9 +117,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_setUpListener(BuildContext context) {
ref.listen<AsyncValue<void>>(loginViewModelProvider, (_, next) {
next.maybeWhen(
data: (_) {
// TODO: Navigate to the Home screen
},
data: (_) => context.go(routePathHomeScreen),
error: (error, _) {
showAlertDialog(
context: context,
Expand Down
57 changes: 38 additions & 19 deletions lib/screens/login/login_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:survey_flutter/api/exception/network_exceptions.dart';
import 'package:survey_flutter/uimodels/app_error.dart';
import 'package:survey_flutter/usecases/base/base_use_case.dart';
import 'package:survey_flutter/usecases/login_use_case.dart';
import 'package:survey_flutter/utils/internet_connection_manager.dart';

final loginViewModelProvider =
AsyncNotifierProvider.autoDispose<LoginViewModel, void>(LoginViewModel.new);

class LoginViewModel extends AutoDisposeAsyncNotifier<void> {
late InternetConnectionManager internetConnectionManager;

bool isValidEmail(String? email) {
// Just use a simple rule, no fancy Regex!
return !(email == null || !email.contains('@'));
Expand All @@ -24,31 +25,49 @@ class LoginViewModel extends AutoDisposeAsyncNotifier<void> {
required String password,
}) async {
state = const AsyncLoading();
// TODO: Integrate with API

// Handling error part:
final loginUseCase = ref.read(loginUseCaseProvider);
final result = await loginUseCase(
LoginParams(
email: email,
password: password,
),
);

// If it returns unauthorized error (401)
//state = const AsyncError(
// AppError.unauthorized,
// StackTrace.empty,
//);
if (result is Failed) {
final error = result as Failed;
final exception = error.exception.actualException as NetworkExceptions;

// If it returns timeout error, then check Internet connection
internetConnectionManager = ref.read(internetConnectionManagerProvider);
final isConnected = await internetConnectionManager.hasConnection();
if (exception is BadRequest || exception is UnauthorisedRequest) {
state = const AsyncError(
AppError.unauthorized,
StackTrace.empty,
);
return;
} else if (exception is RequestTimeout) {
final isConnected = await _hasInternetConnection();
if (!isConnected) {
state = const AsyncError(
AppError.noInternetConnection,
StackTrace.empty,
);
return;
}
}

if (!isConnected) {
state = const AsyncError(
AppError.noInternetConnection,
StackTrace.empty,
);
} else {
state = const AsyncError(
AppError.generic,
StackTrace.empty,
);
return;
}

state = const AsyncData(null);
}

Future<bool> _hasInternetConnection() async {
final internetConnectionManager =
ref.read(internetConnectionManagerProvider);
return await internetConnectionManager.hasConnection();
}

@override
Expand Down
7 changes: 5 additions & 2 deletions lib/usecases/login_use_case.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
import 'package:survey_flutter/model/login_model.dart';
import 'package:survey_flutter/repositories/authentication_repository.dart';
import 'package:survey_flutter/usecases/base/base_use_case.dart';
import 'package:injectable/injectable.dart';

final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
return LoginUseCase(ref.watch(authenticationRepositoryProvider));
});

class LoginParams {
final String email;
Expand All @@ -14,7 +18,6 @@ class LoginParams {
});
}

@Injectable()
class LoginUseCase extends UseCase<LoginModel, LoginParams> {
final AuthenticationRepository _repository;

Expand Down
16 changes: 0 additions & 16 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
get_it:
dependency: transitive
description:
name: get_it
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468"
url: "https://pub.dev"
source: hosted
version: "7.6.0"
glob:
dependency: transitive
description:
Expand Down Expand Up @@ -391,14 +383,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
injectable:
dependency: "direct main"
description:
name: injectable
sha256: f71eb879124ed286cbd2210337b91ff5f345f146187c1f1891c172e0ac06443a
url: "https://pub.dev"
source: hosted
version: "1.5.4"
integration_test:
dependency: "direct dev"
description: flutter
Expand Down
1 change: 0 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ dependencies:
retrofit: ^4.0.1
japx: ^2.0.5
equatable: ^2.0.0
injectable: ^1.5.0
internet_connection_checker: ^1.0.0+1
page_view_dot_indicator: ^2.1.0

Expand Down
10 changes: 10 additions & 0 deletions test/mocks/dummy_models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:survey_flutter/model/login_model.dart';

extension LoginModelDummy on LoginModel {
static LoginModel instance = const LoginModel(
id: '',
accessToken: '',
expiresIn: 0,
refreshToken: '',
);
}
2 changes: 2 additions & 0 deletions test/mocks/generate_mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
import 'package:mockito/annotations.dart';
import 'package:survey_flutter/api/authentication_api_service.dart';
import 'package:survey_flutter/repositories/authentication_repository.dart';
import 'package:survey_flutter/usecases/login_use_case.dart';
import 'package:survey_flutter/utils/internet_connection_manager.dart';

import '../utils/async_listener.dart';
Expand All @@ -12,6 +13,7 @@ import '../utils/async_listener.dart';
AuthenticationRepository,
DioError,
InternetConnectionManager,
LoginUseCase,
])
main() {
// empty class to generate mock repository classes
Expand Down
Loading

0 comments on commit 62cdcbc

Please sign in to comment.