Skip to content

Commit

Permalink
[#8] Show unauthenticated error dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
Thieurom committed Aug 14, 2023
1 parent 1a2fee7 commit 6515054
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 20 deletions.
5 changes: 4 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"okText": "OK",
"emailInputHint": "Email",
"passwordInputHint": "Password",
"loginButton": "Log in",
"invalidEmailError": "Please enter the valid email format.",
"invalidPasswordError": "Password must be at least 8 characters long."
"invalidPasswordError": "Password must be at least 8 characters long.",
"unauthenticatedError": "Your email or password is incorrect.\nPlease try again.",
"loginFailAlertTitle": "Unable to log in"
}
30 changes: 21 additions & 9 deletions lib/screens/login/login_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,25 @@ class LoginForm extends ConsumerStatefulWidget {
const LoginForm({Key? key}) : super(key: key);

@override
ConsumerState<ConsumerStatefulWidget> createState() => _LoginFormState();
ConsumerState<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends ConsumerState<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();

bool _isFormSubmitted = false;

TextFormField get _emailTextField => TextFormField(
keyboardType: TextInputType.emailAddress,
autocorrect: false,
decoration: PrimaryTextFieldDecoration(
hintText: context.localizations?.emailInputHint,
hintText: context.localizations.emailInputHint,
hintTextStyle: context.textTheme.bodyMedium,
),
style: context.textTheme.bodyMedium,
controller: _emailController,
validator: _validateEmail,
autovalidateMode: _isFormSubmitted
? AutovalidateMode.onUserInteraction
Expand All @@ -37,10 +41,11 @@ class _LoginFormState extends ConsumerState<LoginForm> {
autocorrect: false,
obscureText: true,
decoration: PrimaryTextFieldDecoration(
hintText: context.localizations?.passwordInputHint,
hintText: context.localizations.passwordInputHint,
hintTextStyle: context.textTheme.bodyMedium,
),
style: context.textTheme.bodyMedium,
controller: _passwordController,
validator: _validatePassword,
autovalidateMode: _isFormSubmitted
? AutovalidateMode.onUserInteraction
Expand All @@ -50,29 +55,36 @@ class _LoginFormState extends ConsumerState<LoginForm> {
ElevatedButton get _loginButton => ElevatedButton(
style: PrimaryButtonStyle(hintTextStyle: context.textTheme.labelMedium),
onPressed: _submit,
child: Text(context.localizations?.loginButton ?? ''),
child: Text(context.localizations.loginButton),
);

String? _validateEmail(String? email) {
if (!ref.read(loginViewModelProvider.notifier).isValidEmail(email)) {
return context.localizations?.invalidEmailError;
return context.localizations.invalidEmailError;
}
return null;
}

String? _validatePassword(String? password) {
if (!ref.read(loginViewModelProvider.notifier).isValidPassword(password)) {
return context.localizations?.invalidPasswordError;
return context.localizations.invalidPasswordError;
}
return null;
}

void _submit() {
setState(() => _isFormSubmitted = true);
if (_formKey.currentState?.validate() == true) {
context.dismissKeyboard();
// TODO: Integrate with API
bool isFormValidated = _formKey.currentState?.validate() ?? false;

if (!isFormValidated) {
return;
}

context.dismissKeyboard();
ref.read(loginViewModelProvider.notifier).login(
email: _emailController.text,
password: _passwordController.text,
);
}

@override
Expand Down
35 changes: 32 additions & 3 deletions lib/screens/login/login_screen.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:survey_flutter/gen/assets.gen.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';
import 'package:survey_flutter/uimodels/app_error.dart';
import 'package:survey_flutter/utils/build_context_ext.dart';
import 'package:survey_flutter/widgets/alert_dialog.dart';

const routePathLoginScreen = '/login';

class LoginScreen extends StatefulWidget {
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({Key? key}) : super(key: key);

@override
State<StatefulWidget> createState() => _LoginScreenState();
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen>
class _LoginScreenState extends ConsumerState<LoginScreen>
with TickerProviderStateMixin {
final _animationDuration = const Duration(milliseconds: 600);

Expand Down Expand Up @@ -110,6 +114,31 @@ class _LoginScreenState extends State<LoginScreen>

@override
Widget build(BuildContext context) {
ref.listen<AsyncValue<void>>(loginViewModelProvider, (_, next) {
next.maybeWhen(
data: (_) {
// TODO: Navigate to the Home screen
},
error: (error, _) {
showAlertDialog(
context: context,
title: context.localizations.loginFailAlertTitle,
message: (error as AppError?)?.description(context) ?? '',
actions: [
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.black),
),
child: Text(context.localizations.okText),
onPressed: () => Navigator.pop(context),
)
],
);
},
orElse: () {},
);
});

return GestureDetector(
onTap: () => context.dismissKeyboard(),
child: Scaffold(
Expand Down
22 changes: 16 additions & 6 deletions lib/screens/login/login_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:survey_flutter/uimodels/app_error.dart';

final loginViewModelProvider =
StateNotifierProvider.autoDispose<LoginViewModel, void>((_) {
return LoginViewModel();
});

class LoginViewModel extends StateNotifier<void> {
LoginViewModel() : super([]);
AsyncNotifierProvider.autoDispose<LoginViewModel, void>(LoginViewModel.new);

class LoginViewModel extends AutoDisposeAsyncNotifier<void> {
bool isValidEmail(String? email) {
// Just use a simple rule, no fancy Regex!
return !(email == null || !email.contains('@'));
Expand All @@ -16,4 +15,15 @@ class LoginViewModel extends StateNotifier<void> {
bool isValidPassword(String? password) {
return !(password == null || password.length < 8);
}

login({required String email, required String password}) async {

Check warning on line 19 in lib/screens/login/login_view_model.dart

View check run for this annotation

Codecov / codecov/patch

lib/screens/login/login_view_model.dart#L19

Added line #L19 was not covered by tests
// TODO: Integrate with API
state = const AsyncError(

Check warning on line 21 in lib/screens/login/login_view_model.dart

View check run for this annotation

Codecov / codecov/patch

lib/screens/login/login_view_model.dart#L21

Added line #L21 was not covered by tests
AppError.unauthenticated,
StackTrace.empty,
);
}

@override
FutureOr<void> build() {}
}
5 changes: 5 additions & 0 deletions lib/theme/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class AppTheme {
fontSize: 17,
fontWeight: FontWeight.bold,
),
labelLarge: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
textSelectionTheme: const TextSelectionThemeData(
cursorColor: Colors.white,
Expand Down
13 changes: 13 additions & 0 deletions lib/uimodels/app_error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:survey_flutter/utils/build_context_ext.dart';

enum AppError { unauthenticated }

extension AppErrorExtension on AppError {
String description(BuildContext context) {

Check warning on line 7 in lib/uimodels/app_error.dart

View check run for this annotation

Codecov / codecov/patch

lib/uimodels/app_error.dart#L7

Added line #L7 was not covered by tests
switch (this) {
case AppError.unauthenticated:
return context.localizations.unauthenticatedError;

Check warning on line 10 in lib/uimodels/app_error.dart

View check run for this annotation

Codecov / codecov/patch

lib/uimodels/app_error.dart#L9-L10

Added lines #L9 - L10 were not covered by tests
}
}
}
4 changes: 3 additions & 1 deletion lib/utils/build_context_ext.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';

extension BuildContextExtension on BuildContext {
TextTheme get textTheme => Theme.of(this).textTheme;

AppLocalizations? get localizations => AppLocalizations.of(this);
AppLocalizations get localizations =>
AppLocalizations.of(this) ?? AppLocalizationsEn();

Check warning on line 9 in lib/utils/build_context_ext.dart

View check run for this annotation

Codecov / codecov/patch

lib/utils/build_context_ext.dart#L8-L9

Added lines #L8 - L9 were not covered by tests

dismissKeyboard() {
FocusNode currentFocusNode = FocusScope.of(this);
Expand Down
39 changes: 39 additions & 0 deletions lib/widgets/alert_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:survey_flutter/utils/build_context_ext.dart';

Future<void> showAlertDialog({
required BuildContext context,
required String title,
required String message,
required List<Widget> actions,
}) {
if (Platform.isIOS) {
return showCupertinoDialog(
context: context,
builder: (BuildContext context) => CupertinoAlertDialog(
title: Text(title),
content: Text(message),
actions: actions,
),
);
} else if (Platform.isAndroid) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => AlertDialog(
title: Text(title),
titleTextStyle:
context.textTheme.labelLarge?.copyWith(color: Colors.black),
content: Text(message),
contentTextStyle:
context.textTheme.labelMedium?.copyWith(color: Colors.black),
actions: actions,
),
);
} else {
return Future(() => {});
}
}

0 comments on commit 6515054

Please sign in to comment.