import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:sxchnge_analytics/sxchnge_analytics.dart'; import 'package:sxchnge_auth/sxchnge_auth.dart'; import '../routing/routes.dart'; import '../security/challenge.dart'; import 'feedback.dart'; import 'validation.dart'; import '../generated/i18n/app_localizations.dart'; class AuthForm extends StatefulWidget { const AuthForm({super.key}); @override State createState() => _AuthFormState(); } class _AuthFormState extends State { final _form = GlobalKey(); final _password = TextEditingController(); final _confirmation = TextEditingController(); bool _signUp = false; bool _showPassword = false; bool _showConfirmation = false; bool _submitted = false; final _email = TextEditingController(); final _name = TextEditingController(); AuthProvider get _provider => _signUp ? context.read() : context.read(); Locale? _lastLocale; @override void didChangeDependencies() { super.didChangeDependencies(); final locale = Localizations.localeOf(context); if (_lastLocale != null && _lastLocale != locale && _submitted) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _form.currentState?.validate(); }); } _lastLocale = locale; } @override void dispose() { _email.dispose(); _name.dispose(); _password.dispose(); _confirmation.dispose(); super.dispose(); } void _switch(bool signUp) { if (signUp == _signUp) return; context.read().abandon(JourneyExit.modeChange); context.read().reset(); context.read().reset(); _form.currentState?.reset(); _password.clear(); _confirmation.clear(); setState(() { _signUp = signUp; _submitted = false; _showPassword = false; _showConfirmation = false; }); } Future _submit() async { if (_provider.state.isLoading) return; setState(() => _submitted = true); if (!_form.currentState!.validate()) return; FocusScope.of(context).unfocus(); final locale = Localizations.localeOf(context).toLanguageTag(); if (_signUp) { final signup = context.read(); await signup.signup( name: _name.text, email: _email.text, password: _password.text, locale: locale, ); if (!mounted || !_signUp || signup.state.status != AuthStatus.success) { return; } final email = _email.text.trim(); _password.clear(); _confirmation.clear(); context.goNamed(AppRoutes.verificationSent, extra: email); } else { final login = context.read(); await login.login( email: _email.text, password: _password.text, locale: locale, ); if (!mounted || _signUp) return; if (login.state.error == AuthError.accountNotVerified) { _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, service: context.read(), recorder: context.read(), journey: JourneyKind.login, ); if (!mounted || _signUp) return; if (grant == null) { login.reset(); context.read().abandon(JourneyExit.navigation); return; } await login.complete(challenge, grant); if (mounted && login.state.status == AuthStatus.success) { context.go('/accounts'); } } } } @override Widget build(BuildContext context) { final login = context.watch(); final signup = context.watch(); final state = (_signUp ? signup : login).state; final l = AppLocalizations.of(context)!; final theme = Theme.of(context); final colors = theme.colorScheme; final text = theme.textTheme; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: ShapeDecoration( color: colors.tertiaryContainer, shape: const StadiumBorder(), ), child: Text( l.nextChapter, style: text.labelSmall!.copyWith( color: colors.onTertiaryContainer, letterSpacing: 0.8, ), ), ), ), const SizedBox(height: 24), Text( _signUp ? l.signupHeading : l.loginHeading, style: text.displayMedium, ), const SizedBox(height: 16), Text( _signUp ? l.signupIntro : l.loginIntro, style: text.bodyLarge!.copyWith(color: colors.onSurfaceVariant), ), const SizedBox(height: 32), SegmentedButton( showSelectedIcon: false, segments: [ ButtonSegment(value: false, label: Text(l.login)), ButtonSegment(value: true, label: Text(l.signup)), ], selected: {_signUp}, onSelectionChanged: (selected) => _switch(selected.single), 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) ...[ 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, textCapitalization: TextCapitalization.words, validator: (value) => AuthValidation.name(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'), enabled: !state.isLoading, controller: _confirmation, obscureText: !_showConfirmation, 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, ), ), ), validator: (value) => AuthValidation.confirmation(value, _password.text, l), ), ], const SizedBox(height: 28), FilledButton( key: const ValueKey('submit'), onPressed: state.isLoading ? 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 || state.status == AuthStatus.error) ...[ const SizedBox(height: 20), AuthFeedback(state: state, signUp: _signUp), ], const SizedBox(height: 20), if (context.read().isSimulated) TextButton( onPressed: () => context.go('/accounts'), child: Text(l.wEnter), ), Wrap( alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( _signUp ? l.alreadyMember : l.newHere, style: text.bodySmall!.copyWith(color: colors.onSurfaceVariant), ), TextButton( onPressed: () => _switch(!_signUp), child: Text(_signUp ? l.loginInstead : l.createAnAccount), ), ], ), ], ); } }