Skip to content

Commit 20e9072

Browse files
committed
add signup form
1 parent 2b4975e commit 20e9072

File tree

15 files changed

+309
-16
lines changed

15 files changed

+309
-16
lines changed

app/devtools_options.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
description: This file stores settings for Dart & Flutter DevTools.
2+
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3+
extensions:

app/lib/presentation/navigation/routers.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ enum Routes {
2121
String get path => '/$name';
2222
String get subPath => name;
2323

24-
void nav(BuildContext context, {Object? extra}) {
24+
void go(BuildContext context, {Object? extra}) {
2525
context.router.goNamed(
2626
name,
2727
extra: extra,
@@ -63,7 +63,7 @@ class Routers {
6363
return;
6464
}
6565
debugPrint('Navigating to app route');
66-
Routes.app.nav(context);
66+
Routes.app.go(context);
6767
break;
6868
case AuthStateUnauthenticated _:
6969
debugPrint(
@@ -74,7 +74,7 @@ class Routers {
7474
return;
7575
}
7676
debugPrint('Navigating to auth route');
77-
Routes.auth.nav(context);
77+
Routes.auth.go(context);
7878
break;
7979
case _:
8080
}

app/lib/presentation/resources/dim.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Dimen {
77
static const loadingSpinnerSize = 32.0;
88
static const loadingSpinnerSizeS = 16.0;
99

10-
static const double loginFormMaxWidth = 400.0;
10+
static const double authFormMaxWidth = 400.0;
1111

1212
static const spacingXxs = 2.0;
1313
static const spacingXs = 4.0;

app/lib/presentation/resources/locale/generated/intl/messages_en.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class MessageLookup extends MessageLookupByLibrary {
3131
"This website uses cookies",
3232
),
3333
"ctaLogin": MessageLookupByLibrary.simpleMessage("Login"),
34+
"ctaSignUp": MessageLookupByLibrary.simpleMessage("Sign Up"),
3435
"errorEmailInvalid": MessageLookupByLibrary.simpleMessage(
3536
"Please enter a valid email address.",
3637
),
@@ -43,9 +44,15 @@ class MessageLookup extends MessageLookupByLibrary {
4344
"errorPasswordWeak": MessageLookupByLibrary.simpleMessage(
4445
"Password is too weak.",
4546
),
47+
"errorPasswordsDoNotMatch": MessageLookupByLibrary.simpleMessage(
48+
"Passwords do not match.",
49+
),
4650
"labelAgreeToTerms": MessageLookupByLibrary.simpleMessage(
4751
"I agree to the Terms and Conditions",
4852
),
53+
"labelConfirmPassword": MessageLookupByLibrary.simpleMessage(
54+
"Confirm Password",
55+
),
4956
"labelEmail": MessageLookupByLibrary.simpleMessage("Email"),
5057
"labelPassword": MessageLookupByLibrary.simpleMessage("Password"),
5158
"loginErrorInvalidCredentials": MessageLookupByLibrary.simpleMessage(
@@ -67,5 +74,9 @@ class MessageLookup extends MessageLookupByLibrary {
6774
"titleLoginSubtitle": MessageLookupByLibrary.simpleMessage(
6875
"Use your email and password to login to your account.",
6976
),
77+
"titleSignUp": MessageLookupByLibrary.simpleMessage("Sign Up"),
78+
"titleSignUpSubtitle": MessageLookupByLibrary.simpleMessage(
79+
"Create an account using your email and password.",
80+
),
7081
};
7182
}

app/lib/presentation/resources/locale/generated/l10n.dart

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/lib/presentation/resources/locale/intl_en.arb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@
88
"pleaseTryAgainLaterWeArenworkingToFixTheIssue": "Please try again later, we are\nworking to fix the issue.",
99
"sorryWeDidntFindAnyProduct": "Sorry we didn't find any product",
1010
"ctaLogin": "Login",
11+
"ctaSignUp": "Sign Up",
1112
"labelEmail": "Email",
1213
"labelPassword": "Password",
14+
"labelConfirmPassword": "Confirm Password",
15+
"errorPasswordsDoNotMatch": "Passwords do not match.",
1316
"passwordInstructions": "Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.",
1417
"labelAgreeToTerms": "I agree to the Terms and Conditions",
1518
"errorEmailRequired": "Email is required.",
1619
"errorPasswordRequired": "Password is required.",
1720
"titleLogin": "Login",
21+
"titleSignUp": "Sign Up",
1822
"titleLoginSubtitle": "Use your email and password to login to your account.",
23+
"titleSignUpSubtitle": "Create an account using your email and password.",
1924
"errorEmailInvalid": "Please enter a valid email address.",
2025
"errorPasswordWeak": "Password is too weak.",
2126
"loginErrorInvalidCredentials": "Invalid email or password."

app/lib/presentation/ui/pages/auth/login/login_form.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:app/presentation/navigation/routers.dart';
12
import 'package:app/presentation/resources/locale/generated/l10n.dart';
23
import 'package:app/presentation/resources/resources.dart';
34
import 'package:app/presentation/ui/components/primary_button.dart';
@@ -52,6 +53,7 @@ class _LoginFormState extends State<LoginForm> {
5253
labelText: S.of(context).labelEmail,
5354
),
5455
keyboardType: TextInputType.emailAddress,
56+
autofillHints: const [AutofillHints.username, AutofillHints.email],
5557
controller: emailController,
5658
validator: (value) {
5759
if (!FormValidator.isEmail(value)) {
@@ -147,6 +149,26 @@ class _LoginFormState extends State<LoginForm> {
147149
);
148150
},
149151
),
152+
const Gap(Dimen.spacingL),
153+
const Row(
154+
mainAxisAlignment: MainAxisAlignment.center,
155+
children: [
156+
Expanded(child: Divider()),
157+
Padding(
158+
padding: EdgeInsets.symmetric(horizontal: Dimen.spacingS),
159+
child: Text("OR"),
160+
),
161+
Expanded(child: Divider()),
162+
],
163+
),
164+
const Gap(Dimen.spacingM),
165+
SizedBox(
166+
width: double.infinity,
167+
child: TextButton(
168+
onPressed: () => Routes.signup.go(context),
169+
child: Text(S.of(context).ctaSignUp.toUpperCase()),
170+
),
171+
),
150172
],
151173
),
152174
);

app/lib/presentation/ui/pages/auth/login/login_page.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:app/presentation/resources/resources.dart';
22
import 'package:app/presentation/ui/pages/auth/login/login_form.dart';
3+
import 'package:flutter/foundation.dart';
34
import 'package:flutter/material.dart';
45

56
class LoginPage extends StatelessWidget {
@@ -11,7 +12,7 @@ class LoginPage extends StatelessWidget {
1112
body: Center(
1213
child: Container(
1314
constraints: const BoxConstraints(
14-
maxWidth: Dimen.loginFormMaxWidth,
15+
maxWidth: kIsWeb ? Dimen.authFormMaxWidth : double.infinity,
1516
),
1617
child: const Card(
1718
child: Padding(
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import 'package:app/presentation/resources/locale/generated/l10n.dart';
2+
import 'package:app/presentation/resources/resources.dart';
3+
import 'package:app/presentation/ui/components/primary_button.dart';
4+
import 'package:common/core/resource.dart';
5+
import 'package:common/validators/form_validator.dart';
6+
import 'package:domain/bloc/auth/auth_cubit.dart';
7+
import 'package:flutter/material.dart';
8+
import 'package:flutter_bloc/flutter_bloc.dart';
9+
import 'package:gap/gap.dart';
10+
11+
class SignUpForm extends StatefulWidget {
12+
const SignUpForm({super.key});
13+
14+
@override
15+
State<SignUpForm> createState() => _SignUpFormState();
16+
}
17+
18+
class _SignUpFormState extends State<SignUpForm> {
19+
TextEditingController emailController = TextEditingController();
20+
TextEditingController passwordController = TextEditingController();
21+
TextEditingController confirmPasswordController = TextEditingController();
22+
bool agreeToTerms = false;
23+
24+
final _formKey = GlobalKey<FormState>();
25+
26+
@override
27+
void dispose() {
28+
emailController.dispose();
29+
passwordController.dispose();
30+
confirmPasswordController.dispose();
31+
super.dispose();
32+
}
33+
34+
@override
35+
Widget build(BuildContext context) {
36+
return Form(
37+
key: _formKey,
38+
child: Column(
39+
mainAxisSize: MainAxisSize.min,
40+
crossAxisAlignment: CrossAxisAlignment.start,
41+
children: [
42+
Text(
43+
S.of(context).titleSignUp,
44+
style: Theme.of(context).textTheme.titleMedium,
45+
),
46+
const Gap(Dimen.spacingL),
47+
Text(
48+
S.of(context).titleSignUpSubtitle,
49+
style: Theme.of(context).textTheme.titleSmall,
50+
),
51+
const Gap(Dimen.spacingL),
52+
TextFormField(
53+
decoration: InputDecoration(
54+
labelText: S.of(context).labelEmail,
55+
),
56+
keyboardType: TextInputType.emailAddress,
57+
autofillHints: const [AutofillHints.username, AutofillHints.email],
58+
controller: emailController,
59+
validator: (value) {
60+
if (!FormValidator.isEmail(value)) {
61+
return S.of(context).errorEmailInvalid;
62+
}
63+
64+
return null;
65+
},
66+
),
67+
const Gap(Dimen.spacingM),
68+
TextFormField(
69+
decoration: InputDecoration(
70+
labelText: S.of(context).labelPassword,
71+
),
72+
obscureText: true,
73+
controller: passwordController,
74+
validator: (value) {
75+
if (!FormValidator.isStrongPassword(value)) {
76+
return S.of(context).errorPasswordWeak;
77+
}
78+
return null;
79+
},
80+
),
81+
const Gap(Dimen.spacingM),
82+
Text(
83+
S.of(context).passwordInstructions,
84+
style: Theme.of(context).textTheme.bodySmall,
85+
),
86+
const Gap(Dimen.spacingM),
87+
TextFormField(
88+
decoration: InputDecoration(
89+
labelText: S.of(context).labelConfirmPassword,
90+
),
91+
obscureText: true,
92+
controller: confirmPasswordController,
93+
validator: (value) {
94+
if (value != passwordController.text) {
95+
return S.of(context).errorPasswordsDoNotMatch;
96+
}
97+
return null;
98+
},
99+
),
100+
const Gap(Dimen.spacingM),
101+
TextButton(
102+
onPressed: () => setState(() {
103+
agreeToTerms = !agreeToTerms;
104+
}),
105+
child: Row(
106+
children: [
107+
Checkbox(
108+
value: agreeToTerms,
109+
onChanged: (value) => setState(() {
110+
agreeToTerms = value ?? false;
111+
}),
112+
),
113+
Text(
114+
S.of(context).labelAgreeToTerms,
115+
style: Theme.of(context).textTheme.titleSmall,
116+
),
117+
const Gap(Dimen.spacingS),
118+
IconButton(
119+
onPressed: () {
120+
ScaffoldMessenger.of(context).showSnackBar(
121+
const SnackBar(
122+
content: Text(
123+
"This should open the terms and conditions URL."),
124+
),
125+
);
126+
},
127+
icon: const Icon(Icons.info),
128+
)
129+
],
130+
),
131+
),
132+
const Gap(Dimen.spacingM),
133+
BlocBuilder<AuthCubit, Resource>(
134+
builder: (context, state) {
135+
return Column(
136+
mainAxisSize: MainAxisSize.min,
137+
children: [
138+
if (state is RError) ...[
139+
Text(
140+
S.of(context).loginErrorInvalidCredentials,
141+
style:
142+
Theme.of(context).textTheme.headlineMedium?.copyWith(
143+
color: Theme.of(context).colorScheme.error,
144+
),
145+
),
146+
const Gap(Dimen.spacingM),
147+
],
148+
PrimaryButton(
149+
label: S.of(context).ctaSignUp,
150+
onPressed: () {
151+
if ((_formKey.currentState?.validate() ?? false) &&
152+
agreeToTerms) {
153+
context.read<AuthCubit>().signUp(
154+
email: emailController.text,
155+
password: passwordController.text,
156+
);
157+
}
158+
},
159+
isEnabled: agreeToTerms,
160+
isLoading: state is RLoading,
161+
trailingIcon: const Icon(Icons.login),
162+
)
163+
],
164+
);
165+
},
166+
),
167+
],
168+
),
169+
);
170+
}
171+
}

0 commit comments

Comments
 (0)