diff --git a/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/providers/login_controller.dart b/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/providers/login_controller.dart index c6092171..1c9866b1 100644 --- a/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/providers/login_controller.dart +++ b/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/providers/login_controller.dart @@ -12,7 +12,7 @@ import 'package:sf_tracking/sf_tracking.dart'; part 'login_controller.g.dart'; -const int resendCooldownSeconds = 30; +const int resendCooldownSeconds = 90; @Riverpod(keepAlive: true) class LoginController extends _$LoginController with LoginFormValidation { diff --git a/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_bottom_sheet.dart b/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_bottom_sheet.dart index 5c6f7cd4..8149c2a3 100644 --- a/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_bottom_sheet.dart +++ b/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_bottom_sheet.dart @@ -11,9 +11,9 @@ class LegacyTwoFactorBottomSheetView extends StatelessWidget { super.key, required this.title, required this.subtitle, + required this.maskedEmail, required this.verifyText, required this.resendText, - required this.closeText, required this.isOtpLoading, required this.canResend, required this.otpCode, @@ -21,14 +21,13 @@ class LegacyTwoFactorBottomSheetView extends StatelessWidget { required this.onChanged, required this.onVerify, required this.onResend, - required this.onClose, }); final String title; final String subtitle; + final String maskedEmail; final String verifyText; final String resendText; - final String closeText; final bool isOtpLoading; final bool canResend; @@ -38,20 +37,26 @@ class LegacyTwoFactorBottomSheetView extends StatelessWidget { final ValueChanged onChanged; final Future Function() onVerify; final Future Function() onResend; - final VoidCallback onClose; bool get _isValidOtp => otpCode.trim().length == 6; + bool get _canVerify => _isValidOtp && !isOtpLoading; @override Widget build(BuildContext context) { final bottomInset = MediaQuery.of(context).viewInsets.bottom; + final primaryColor = context.sfColors.legacyPrimary; return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), - padding: EdgeInsets.fromLTRB(12, 12, 12, 24 + bottomInset), + padding: EdgeInsets.fromLTRB( + 20, + 12, + 20, + 24 + bottomInset + MediaQuery.of(context).padding.bottom, + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -76,6 +81,18 @@ class LegacyTwoFactorBottomSheetView extends StatelessWidget { textAlign: TextAlign.center, style: const TextStyle(fontSize: 14), ), + if (maskedEmail.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + maskedEmail, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: primaryColor, + ), + ), + ], const SizedBox(height: 20), LegacyOtpCodeFields( @@ -91,36 +108,36 @@ class LegacyTwoFactorBottomSheetView extends StatelessWidget { ), const SizedBox(height: 20), - PrimaryButton( - onPressed: (isOtpLoading || !_isValidOtp) - ? () {} - : () => unawaited(onVerify()), - text: verifyText, - color: context.sfColors.legacyPrimary, - leading: isOtpLoading - ? const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : null, + Opacity( + opacity: _canVerify ? 1.0 : 0.5, + child: PrimaryButton( + onPressed: _canVerify ? () => unawaited(onVerify()) : () {}, + text: verifyText, + color: primaryColor, + leading: isOtpLoading + ? const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : null, + ), ), - const SizedBox(height: 12), + const SizedBox(height: 16), TextButton( onPressed: canResend ? () => unawaited(onResend()) : null, - child: Text(resendText), - ), - - const SizedBox(height: 4), - - TextButton( - onPressed: isOtpLoading ? null : onClose, - child: Text(closeText), + child: Text( + resendText, + style: TextStyle( + fontWeight: FontWeight.w600, + color: canResend ? primaryColor : Colors.grey, + ), + ), ), ], ), diff --git a/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_sheet_launcher.dart b/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_sheet_launcher.dart index ce88f6ec..9e0e586c 100644 --- a/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_sheet_launcher.dart +++ b/modules/legacy/modules/legacy_auth/lib/src/features/login/presentation/widgets/two_factor_sheet_launcher.dart @@ -1,82 +1,52 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:legacy_auth/src/features/login/domain/entities/legacy_auth_error_event.dart'; import 'package:legacy_auth/src/features/login/presentation/providers/login_controller.dart'; import 'package:legacy_auth/src/features/login/presentation/widgets/two_factor_bottom_sheet.dart'; import 'package:sf_localizations/sf_localizations.dart'; -import 'package:sf_shared/sf_shared.dart'; - -String _twoFactorErrorI18nKey(LegacyAuthErrorEvent event) { - return switch (event) { - LegacyAuthErrorEvent.invalidCode => I18n.errorTwoFactorCodeInvalid, - LegacyAuthErrorEvent.invalidToken => I18n.authErrorInvalidToken, - LegacyAuthErrorEvent.tooManyAttempts => I18n.authErrorTooManyAttempts, - LegacyAuthErrorEvent.noMethodsAvailable => I18n.errorTwoFactorNoMethods, - LegacyAuthErrorEvent.network => I18n.authErrorNetwork, - _ => I18n.errorGeneric, - }; -} void showTwoFactorSheet(BuildContext context) { + final container = ProviderScope.containerOf(context); + showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, isDismissible: false, - enableDrag: false, + enableDrag: true, backgroundColor: Colors.transparent, builder: (sheetContext) { return Consumer( builder: (sheetContext, ref, _) { final notifier = ref.read(loginControllerProvider.notifier); + final state = ref.watch(loginControllerProvider); - final otpErrorKey = ref - .watch(loginControllerProvider.select((s) => s.codeError)); - final isLoading = ref - .watch(loginControllerProvider.select((s) => s.isLoading)); - final code = - ref.watch(loginControllerProvider.select((s) => s.code)); - final resendCooldown = ref - .watch(loginControllerProvider.select((s) => s.resendCooldown)); - - final otpErrorText = otpErrorKey.isEmpty + final otpErrorText = state.codeError.isEmpty ? '' - : sheetContext.translate(otpErrorKey); + : sheetContext.translate(state.codeError); ref.listen( - loginControllerProvider.select( - (s) => (errorEvent: s.errorEvent, verified: s.twoFAVerified), - ), - (previous, next) async { - final errorEvent = next.errorEvent; - if (errorEvent != null && errorEvent != previous?.errorEvent) { - await showErrorDialog( - sheetContext, - _twoFactorErrorI18nKey(errorEvent), - ); - ref.read(loginControllerProvider.notifier).clearErrorEvent(); - } - if (next.verified) { - if (sheetContext.mounted) { - Navigator.of(sheetContext).pop(); - } + loginControllerProvider.select((s) => s.twoFAVerified), + (_, verified) { + if (verified && sheetContext.mounted) { + Navigator.of(sheetContext).pop(); } }, ); - final canResend = !isLoading && resendCooldown == 0; + final canResend = !state.isLoading && state.resendCooldown == 0; + final maskedEmail = _maskEmail(state.email); return LegacyTwoFactorBottomSheetView( title: sheetContext.translate(I18n.twoFactorTitle), subtitle: sheetContext.translate(I18n.twoFactorSubtitle), + maskedEmail: maskedEmail, verifyText: sheetContext.translate(I18n.twoFactorVerify), - resendText: resendCooldown > 0 - ? '${sheetContext.translate(I18n.twoFactorResend)} (${resendCooldown}s)' + resendText: state.resendCooldown > 0 + ? '${sheetContext.translate(I18n.twoFactorResend)} (${_formatCooldown(state.resendCooldown)})' : sheetContext.translate(I18n.twoFactorResend), - closeText: sheetContext.translate(I18n.close), - isOtpLoading: isLoading, + isOtpLoading: state.isLoading, canResend: canResend, - otpCode: code, + otpCode: state.code, otpErrorText: otpErrorText, onChanged: notifier.setCode, onVerify: () async { @@ -84,13 +54,28 @@ void showTwoFactorSheet(BuildContext context) { await notifier.submitTwoFACode(); }, onResend: () => notifier.resendCode(), - onClose: () { - notifier.dismissTwoFA(); - Navigator.of(sheetContext).pop(); - }, ); }, ); }, - ); + ).then((_) { + container.read(loginControllerProvider.notifier).dismissTwoFA(); + }); +} + +String _formatCooldown(int seconds) { + final min = seconds ~/ 60; + final sec = seconds % 60; + if (min <= 0) return '${sec}s'; + return '$min:${sec.toString().padLeft(2, '0')}'; +} + +String _maskEmail(String email) { + if (email.isEmpty) return ''; + final parts = email.split('@'); + if (parts.length != 2) return email; + final name = parts[0]; + final domain = parts[1]; + if (name.length <= 2) return '${name[0]}***@$domain'; + return '${name[0]}${'*' * (name.length - 2)}${name[name.length - 1]}@$domain'; } diff --git a/modules/legacy/modules/legacy_auth/lib/src/features/sign_up/presentation/screens/account_created_screen.dart b/modules/legacy/modules/legacy_auth/lib/src/features/sign_up/presentation/screens/account_created_screen.dart index afdbd63d..9ae52e1d 100644 --- a/modules/legacy/modules/legacy_auth/lib/src/features/sign_up/presentation/screens/account_created_screen.dart +++ b/modules/legacy/modules/legacy_auth/lib/src/features/sign_up/presentation/screens/account_created_screen.dart @@ -6,6 +6,33 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; +class _BrandHintText extends StatelessWidget { + final String text; + final String brand; + + const _BrandHintText({required this.text, required this.brand}); + + @override + Widget build(BuildContext context) { + final parts = text.split(brand); + if (parts.length < 2) return Text(text, textAlign: TextAlign.center); + + return Text.rich( + TextSpan( + children: [ + TextSpan(text: parts[0]), + TextSpan( + text: brand, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: parts[1]), + ], + ), + textAlign: TextAlign.center, + ); + } +} + class LegacyAccountCreatedScreen extends ConsumerWidget { final NavigationContract navigationContract; @@ -19,8 +46,8 @@ class LegacyAccountCreatedScreen extends ConsumerWidget { final state = ref.watch(signUpControllerProvider); - final String email = state.email; final String fullName = '${state.firstName} ${state.lastName}'.trim(); + final String email = state.email; return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, @@ -59,24 +86,16 @@ class LegacyAccountCreatedScreen extends ConsumerWidget { ), const SizedBox(height: 16), - Text.rich( + Text( + email, textAlign: TextAlign.center, - TextSpan( - text: - '${context.translate(I18n.accountCreatedEmailVerificationSentLabel)}\n', - children: [ - TextSpan( - text: email, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), + style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 20), - Text( - context.translate(I18n.accountCreatedChildSetupHint), - textAlign: TextAlign.center, + _BrandHintText( + text: context.translate(I18n.accountCreatedChildSetupHint), + brand: 'SaveFamily', ), const SizedBox(height: 20), diff --git a/modules/legacy/modules/legacy_auth/lib/src/widgets/layouts/sign_up_layout.dart b/modules/legacy/modules/legacy_auth/lib/src/widgets/layouts/sign_up_layout.dart index 2e61525a..ea8289e7 100644 --- a/modules/legacy/modules/legacy_auth/lib/src/widgets/layouts/sign_up_layout.dart +++ b/modules/legacy/modules/legacy_auth/lib/src/widgets/layouts/sign_up_layout.dart @@ -37,7 +37,7 @@ class LegacySignUpLayout extends StatelessWidget { if (isLoading) const ColoredBox( color: Colors.black26, - child: Center(child: AppLoadingIndicator()), + child: Center(child: LegacyLoadingIndicator()), ), ], ); @@ -45,10 +45,10 @@ class LegacySignUpLayout extends StatelessWidget { Widget _buildScaffold(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( - child: Container( - color: Theme.of(context).colorScheme.surface, + child: Padding( padding: const EdgeInsets.only(left: 24, right: 24), child: Column( children: [ @@ -85,7 +85,9 @@ class LegacySignUpLayout extends StatelessWidget { Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 16), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + 16, + ), child: body, ), ),