Enable credential autofill for signup and login (#56)
This commit is contained in:
1 parent
bd675357bb
commit
86b6aafe7d
6 files changed
+461
-117
No files matched your search
+146
-117
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -63,6 +64,8 @@ class _AuthFormState extends State<AuthForm> {
|
||||
|
||||
void _switch(bool signUp) {
|
||||
if (signUp == _signUp) return;
|
||||
FocusScope.of(context).unfocus();
|
||||
TextInput.finishAutofillContext(shouldSave: false);
|
||||
context.read<JourneyRecorder>().abandon(JourneyExit.modeChange);
|
||||
context.read<LoginProvider>().reset();
|
||||
context.read<SignupProvider>().reset();
|
||||
@@ -95,6 +98,7 @@ class _AuthFormState extends State<AuthForm> {
|
||||
return;
|
||||
}
|
||||
final email = _email.text.trim();
|
||||
TextInput.finishAutofillContext();
|
||||
_password.clear();
|
||||
_confirmation.clear();
|
||||
context.goNamed(AppRoutes.verificationSent, extra: email);
|
||||
@@ -107,13 +111,13 @@ class _AuthFormState extends State<AuthForm> {
|
||||
);
|
||||
if (!mounted || _signUp) return;
|
||||
if (login.state.error == AuthError.accountNotVerified) {
|
||||
TextInput.finishAutofillContext(shouldSave: false);
|
||||
_password.clear();
|
||||
context.goNamed(AppRoutes.verificationSent, extra: _email.text.trim());
|
||||
return;
|
||||
}
|
||||
final challenge = login.state.result?.challenge;
|
||||
if (challenge != null) {
|
||||
_password.clear();
|
||||
final grant = await verifyFactor(
|
||||
context,
|
||||
challenge: challenge,
|
||||
@@ -123,6 +127,8 @@ class _AuthFormState extends State<AuthForm> {
|
||||
);
|
||||
if (!mounted || _signUp) return;
|
||||
if (grant == null) {
|
||||
TextInput.finishAutofillContext(shouldSave: false);
|
||||
_password.clear();
|
||||
login.reset();
|
||||
context.read<JourneyRecorder>().abandon(JourneyExit.navigation);
|
||||
return;
|
||||
@@ -137,9 +143,13 @@ class _AuthFormState extends State<AuthForm> {
|
||||
!_signUp &&
|
||||
identical(login.state.result, accepted) &&
|
||||
login.state.status == AuthStatus.success) {
|
||||
TextInput.finishAutofillContext();
|
||||
_password.clear();
|
||||
context.go(offer ? '/identity?onboarding=1' : '/accounts');
|
||||
}
|
||||
}
|
||||
} else if (login.state.status == AuthStatus.success) {
|
||||
TextInput.finishAutofillContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,150 +205,169 @@ class _AuthFormState extends State<AuthForm> {
|
||||
expandedInsets: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Form(
|
||||
key: _form,
|
||||
autovalidateMode: _submitted
|
||||
? AutovalidateMode.onUserInteraction
|
||||
: AutovalidateMode.disabled,
|
||||
onChanged: () {
|
||||
if (_provider.state.status == AuthStatus.success ||
|
||||
_provider.state.status == AuthStatus.error) {
|
||||
_provider.reset();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_signUp) ...[
|
||||
AutofillGroup(
|
||||
// Leaving an unfinished form must not offer to save its credentials.
|
||||
onDisposeAction: AutofillContextAction.cancel,
|
||||
child: Form(
|
||||
key: _form,
|
||||
autovalidateMode: _submitted
|
||||
? AutovalidateMode.onUserInteraction
|
||||
: AutovalidateMode.disabled,
|
||||
onChanged: () {
|
||||
if (_provider.state.status == AuthStatus.success ||
|
||||
_provider.state.status == AuthStatus.error) {
|
||||
_provider.reset();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_signUp) ...[
|
||||
TextFormField(
|
||||
key: const ValueKey('name'),
|
||||
controller: _name,
|
||||
enabled: !state.isLoading,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.name,
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.name,
|
||||
autofillHints: const [AutofillHints.name],
|
||||
textCapitalization: TextCapitalization.words,
|
||||
validator: (value) => AuthValidation.name(value, l),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
TextFormField(
|
||||
key: const ValueKey('name'),
|
||||
controller: _name,
|
||||
key: const ValueKey('email'),
|
||||
controller: _email,
|
||||
enabled: !state.isLoading,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.name,
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
labelText: l.email,
|
||||
hintText: l.emailHint,
|
||||
prefixIcon: Icon(Icons.alternate_email),
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
AutofillHints.email,
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
validator: (value) => AuthValidation.name(value, l),
|
||||
autocorrect: false,
|
||||
validator: (value) => AuthValidation.email(value, l),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
TextFormField(
|
||||
key: const ValueKey('email'),
|
||||
controller: _email,
|
||||
enabled: !state.isLoading,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.email,
|
||||
hintText: l.emailHint,
|
||||
prefixIcon: Icon(Icons.alternate_email),
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
autocorrect: false,
|
||||
validator: (value) => AuthValidation.email(value, l),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
key: const ValueKey('password'),
|
||||
enabled: !state.isLoading,
|
||||
controller: _password,
|
||||
obscureText: !_showPassword,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
textInputAction: _signUp
|
||||
? TextInputAction.next
|
||||
: TextInputAction.done,
|
||||
onFieldSubmitted: (_) {
|
||||
if (!_signUp) _submit();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: l.password,
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
errorMaxLines: 3,
|
||||
helperText: _signUp ? l.passwordHint : null,
|
||||
helperMaxLines: 3,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: _showPassword ? l.hidePassword : l.showPassword,
|
||||
onPressed: () =>
|
||||
setState(() => _showPassword = !_showPassword),
|
||||
icon: Icon(
|
||||
_showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
AuthValidation.password(value, l, signUp: _signUp),
|
||||
),
|
||||
if (_signUp) ...[
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
key: const ValueKey('confirmation'),
|
||||
key: const ValueKey('password'),
|
||||
enabled: !state.isLoading,
|
||||
controller: _confirmation,
|
||||
obscureText: !_showConfirmation,
|
||||
controller: _password,
|
||||
obscureText: !_showPassword,
|
||||
autofillHints: [
|
||||
_signUp
|
||||
? AutofillHints.newPassword
|
||||
: AutofillHints.password,
|
||||
],
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (_) => _submit(),
|
||||
textInputAction: _signUp
|
||||
? TextInputAction.next
|
||||
: TextInputAction.done,
|
||||
onFieldSubmitted: (_) {
|
||||
if (!_signUp) _submit();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: l.confirmPassword,
|
||||
labelText: l.password,
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
errorMaxLines: 3,
|
||||
helperText: _signUp ? l.passwordHint : null,
|
||||
helperMaxLines: 3,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: _showConfirmation
|
||||
? l.hideConfirmation
|
||||
: l.showConfirmation,
|
||||
onPressed: () => setState(
|
||||
() => _showConfirmation = !_showConfirmation,
|
||||
),
|
||||
tooltip: _showPassword ? l.hidePassword : l.showPassword,
|
||||
onPressed: () =>
|
||||
setState(() => _showPassword = !_showPassword),
|
||||
icon: Icon(
|
||||
_showConfirmation
|
||||
_showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
AuthValidation.confirmation(value, _password.text, l),
|
||||
AuthValidation.password(value, l, signUp: _signUp),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 28),
|
||||
FilledButton(
|
||||
key: const ValueKey('submit'),
|
||||
onPressed: state.isLoading || state.status == AuthStatus.success
|
||||
? null
|
||||
: _submit,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
state.isLoading
|
||||
? l.authWorking
|
||||
: (_signUp ? l.createAccount : l.letsGo),
|
||||
textAlign: TextAlign.center,
|
||||
if (_signUp) ...[
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
key: const ValueKey('confirmation'),
|
||||
enabled: !state.isLoading,
|
||||
controller: _confirmation,
|
||||
obscureText: !_showConfirmation,
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (_) => _submit(),
|
||||
decoration: InputDecoration(
|
||||
labelText: l.confirmPassword,
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
errorMaxLines: 3,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: _showConfirmation
|
||||
? l.hideConfirmation
|
||||
: l.showConfirmation,
|
||||
onPressed: () => setState(
|
||||
() => _showConfirmation = !_showConfirmation,
|
||||
),
|
||||
icon: Icon(
|
||||
_showConfirmation
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
state.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.arrow_forward, size: 20),
|
||||
],
|
||||
),
|
||||
validator: (value) =>
|
||||
AuthValidation.confirmation(value, _password.text, l),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 28),
|
||||
FilledButton(
|
||||
key: const ValueKey('submit'),
|
||||
onPressed:
|
||||
state.isLoading || state.status == AuthStatus.success
|
||||
? null
|
||||
: _submit,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
state.isLoading
|
||||
? l.authWorking
|
||||
: (_signUp ? l.createAccount : l.letsGo),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
state.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.arrow_forward, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.status == AuthStatus.success ||
|
||||
|
||||
Reference in new issue
Block a user