From 8c4324ee9d60b8a0fef7ed5cb5026cf881e0e7d6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 14 Apr 2025 10:29:46 +0800 Subject: [PATCH] fix: otp launch review issues (#7730) * fix: add error text under text field * chore: update translation --- .../shared/common_operations.dart | 6 +- .../lib/user/application/sign_in_bloc.dart | 9 +- .../desktop_sign_in_screen.dart | 10 -- .../sign_in_screen/sign_in_screen.dart | 16 +- .../continue_with_email_and_password.dart | 109 +++++++------ ...inue_with_magic_link_or_passcode_page.dart | 148 +++++++++++------- .../continue_with_password_page.dart | 68 +++++--- .../widgets/sign_in_agreement.dart | 4 +- .../third_party_sign_in_buttons.dart | 1 + .../src/component/textfield/textfield.dart | 32 +++- frontend/resources/translations/en.json | 15 +- 11 files changed, 269 insertions(+), 149 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 4a117a71ff..d7a505d152 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -67,12 +67,10 @@ extension CommonOperations on WidgetTester { } else { // cloud version final anonymousButton = find.byType(SignInAnonymousButtonV2); - await tapButton(anonymousButton); + await tapButton(anonymousButton, warnIfMissed: true); } - if (Platform.isWindows) { - await pumpAndSettle(const Duration(milliseconds: 200)); - } + await pumpAndSettle(const Duration(milliseconds: 200)); } Future tapContinousAnotherWay() async { diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 54a60702c8..82269d1f1f 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -199,7 +199,9 @@ class SignInBloc extends Bloc { emit( result.fold( - (userProfile) => state.copyWith(isSubmitting: true), + (userProfile) => state.copyWith( + isSubmitting: false, + ), (error) => _stateFromCode(error), ), ); @@ -282,8 +284,9 @@ class SignInBloc extends Bloc { case ErrorCode.UserUnauthorized: final errorMsg = error.msg; String msg = LocaleKeys.signIn_generalError.tr(); - if (errorMsg.contains('rate limit')) { - msg = LocaleKeys.signIn_limitRateError.tr(); + if (errorMsg.contains('rate limit') || + errorMsg.contains('For security purposes')) { + msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr(); } else if (errorMsg.contains('invalid')) { msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 3a906ee0c4..65e8ee550b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -23,7 +23,6 @@ class DesktopSignInScreen extends StatelessWidget { Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - const indicatorMinHeight = 4.0; return BlocBuilder( builder: (context, state) { return Scaffold( @@ -55,15 +54,6 @@ class DesktopSignInScreen extends StatelessWidget { // sign in agreement const SignInAgreement(), - // loading status - const VSpace(indicatorMinHeight), - state.isSubmitting - ? const LinearProgressIndicator( - minHeight: indicatorMinHeight, - ) - : const VSpace(indicatorMinHeight), - const VSpace(20), - const Spacer(), // anonymous sign in and settings diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index 5b99ad83f3..a428545d8d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -4,6 +4,7 @@ import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -37,10 +38,17 @@ class SignInScreen extends StatelessWidget { void _showSignInError(BuildContext context, SignInState state) { final successOrFail = state.successOrFail; if (successOrFail != null) { - handleUserProfileResult( - successOrFail, - context, - getIt(), + successOrFail.fold( + (userProfile) { + if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { + getIt().pushEncryptionScreen(context, userProfile); + } else { + getIt().goHomeScreen(context, userProfile); + } + }, + (error) { + handleOpenWorkspaceError(context, error); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index e918c3f4f1..a0dda40a36 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -2,14 +2,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; class ContinueWithEmailAndPassword extends StatefulWidget { const ContinueWithEmailAndPassword({super.key}); @@ -23,6 +21,7 @@ class _ContinueWithEmailAndPasswordState extends State { final controller = TextEditingController(); final focusNode = FocusNode(); + final emailKey = GlobalKey(); @override void dispose() { @@ -36,55 +35,74 @@ class _ContinueWithEmailAndPasswordState Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - return Column( - children: [ - SizedBox( - height: UniversalPlatform.isMobile ? 38.0 : 40.0, - child: AFTextField( + return BlocListener( + listener: (context, state) { + final successOrFail = state.successOrFail; + // only push the continue with magic link or passcode page if the magic link is sent successfully + if (successOrFail != null) { + successOrFail.fold( + (_) => emailKey.currentState?.clearError(), + (error) => emailKey.currentState?.syncError( + errorText: error.msg, + ), + ); + } else if (successOrFail == null && !state.isSubmitting) { + _pushContinueWithMagicLinkOrPasscodePage( + context, + controller.text, + ); + } + }, + child: Column( + children: [ + AFTextField( + key: emailKey, controller: controller, hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), radius: 10, - onSubmitted: (value) => _pushContinueWithMagicLinkOrPasscodePage( + onSubmitted: (value) => _signInWithEmail( context, value, ), ), - ), - VSpace(theme.spacing.l), - ContinueWithEmail( - onTap: () => _pushContinueWithMagicLinkOrPasscodePage( - context, - controller.text, + VSpace(theme.spacing.l), + ContinueWithEmail( + onTap: () => _signInWithEmail( + context, + controller.text, + ), ), - ), - // Hide password sign in until we implement the reset password / forgot password - // VSpace(theme.spacing.l), - // ContinueWithPassword( - // onTap: () => _pushContinueWithPasswordPage( - // context, - // controller.text, - // ), - // ), - ], + // VSpace(theme.spacing.l), + // ContinueWithPassword( + // onTap: () => _pushContinueWithPasswordPage( + // context, + // controller.text, + // ), + // ), + ], + ), ); } + void _signInWithEmail(BuildContext context, String email) { + if (!isEmail(email)) { + emailKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidEmail.tr(), + ); + return; + } + + context + .read() + .add(SignInEvent.signInWithMagicLink(email: email)); + } + void _pushContinueWithMagicLinkOrPasscodePage( BuildContext context, String email, ) { - if (!isEmail(email)) { - showToastNotification( - message: LocaleKeys.signIn_invalidEmail.tr(), - type: ToastificationType.error, - ); - return; - } - final signInBloc = context.read(); - signInBloc.add(SignInEvent.signInWithMagicLink(email: email)); - // push the a continue with magic link or passcode screen Navigator.push( context, @@ -114,18 +132,21 @@ class _ContinueWithEmailAndPasswordState // Navigator.push( // context, // MaterialPageRoute( - // builder: (context) => ContinueWithPasswordPage( - // email: email, - // backToLogin: () => Navigator.pop(context), - // onEnterPassword: (password) => signInBloc.add( - // SignInEvent.signInWithEmailAndPassword( - // email: email, - // password: password, + // builder: (context) => BlocProvider.value( + // value: signInBloc, + // child: ContinueWithPasswordPage( + // email: email, + // backToLogin: () => Navigator.pop(context), + // onEnterPassword: (password) => signInBloc.add( + // SignInEvent.signInWithEmailAndPassword( + // email: email, + // password: password, + // ), // ), + // onForgotPassword: () { + // // todo: implement forgot password + // }, // ), - // onForgotPassword: () { - // // todo: implement forgot password - // }, // ), // ), // ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart index ea7ff087ff..5be30ef84c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -33,6 +33,8 @@ class _ContinueWithMagicLinkOrPasscodePageState ToastificationItem? toastificationItem; + final inputPasscodeKey = GlobalKey(); + @override void dispose() { passcodeController.dispose(); @@ -44,10 +46,13 @@ class _ContinueWithMagicLinkOrPasscodePageState Widget build(BuildContext context) { return BlocListener( listener: (context, state) { - if (state.isSubmitting) { - _showLoadingDialog(); - } else { - _dismissLoadingDialog(); + final successOrFail = state.successOrFail; + if (successOrFail != null && successOrFail.isFailure) { + successOrFail.onFailure((error) { + inputPasscodeKey.currentState?.syncError( + errorText: LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(), + ); + }); } }, child: Scaffold( @@ -91,24 +96,39 @@ class _ContinueWithMagicLinkOrPasscodePageState return [ // Enter code manually - SizedBox( - height: 40, - child: AFTextField( - controller: passcodeController, - hintText: LocaleKeys.signIn_enterCode.tr(), - keyboardType: TextInputType.number, - radius: 10, - autoFocus: true, - onSubmitted: widget.onEnterPasscode, - ), + AFTextField( + key: inputPasscodeKey, + controller: passcodeController, + hintText: LocaleKeys.signIn_enterCode.tr(), + keyboardType: TextInputType.number, + radius: 10, + autoFocus: true, + onSubmitted: (passcode) { + if (passcode.isEmpty) { + inputPasscodeKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), + ); + } else { + widget.onEnterPasscode(passcode); + } + }, ), // todo: ask designer to provide the spacing VSpace(12), // continue to login AFFilledTextButton.primary( - text: 'Continue to sign in', - onTap: () => widget.onEnterPasscode(passcodeController.text), + text: LocaleKeys.signIn_continueToSignIn.tr(), + onTap: () { + final passcode = passcodeController.text; + if (passcode.isEmpty) { + inputPasscodeKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), + ); + } else { + widget.onEnterPasscode(passcode); + } + }, size: AFButtonSize.l, alignment: Alignment.center, ), @@ -123,6 +143,7 @@ class _ContinueWithMagicLinkOrPasscodePageState text: LocaleKeys.signIn_backToLogin.tr(), size: AFButtonSize.s, onTap: widget.backToLogin, + padding: EdgeInsets.zero, textColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); if (isHovering) { @@ -137,51 +158,70 @@ class _ContinueWithMagicLinkOrPasscodePageState List _buildLogoTitleAndDescription() { final theme = AppFlowyTheme.of(context); final spacing = VSpace(theme.spacing.xxl); - return [ - // logo - const AFLogo(), - spacing, + if (!isEnteringPasscode) { + return [ + // logo + const AFLogo(), + spacing, - // title - Text( - LocaleKeys.signIn_checkYourEmail.tr(), - style: theme.textStyle.heading.h3( - color: theme.textColorScheme.primary, + // title + Text( + LocaleKeys.signIn_checkYourEmail.tr(), + style: theme.textStyle.heading.h3( + color: theme.textColorScheme.primary, + ), ), - ), - spacing, + spacing, - // description - Text( - LocaleKeys.signIn_temporaryVerificationSent.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, + // description + Text( + LocaleKeys.signIn_temporaryVerificationLinkSent.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - Text( - widget.email, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, + Text( + widget.email, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - spacing, - ]; - } + spacing, + ]; + } else { + return [ + // logo + const AFLogo(), + spacing, - void _showLoadingDialog() { - _dismissLoadingDialog(); + // title + Text( + LocaleKeys.signIn_enterCode.tr(), + style: theme.textStyle.heading.h3( + color: theme.textColorScheme.primary, + ), + ), + spacing, - toastificationItem = showToastNotification( - message: LocaleKeys.signIn_signingIn.tr(), - ); - } - - void _dismissLoadingDialog() { - final toastificationItem = this.toastificationItem; - if (toastificationItem != null) { - toastification.dismiss(toastificationItem); + // description + Text( + LocaleKeys.signIn_temporaryVerificationCodeSent.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, + ), + Text( + widget.email, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, + ), + spacing, + ]; } } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart index 3a281889ad..4ab40011d2 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart @@ -1,8 +1,10 @@ +import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class ContinueWithPasswordPage extends StatefulWidget { const ContinueWithPasswordPage({ @@ -25,6 +27,7 @@ class ContinueWithPasswordPage extends StatefulWidget { class _ContinueWithPasswordPageState extends State { final passwordController = TextEditingController(); + final inputPasswordKey = GlobalKey(); @override void dispose() { @@ -38,18 +41,29 @@ class _ContinueWithPasswordPageState extends State { body: Center( child: SizedBox( width: 320, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo and title - ..._buildLogoAndTitle(), + child: BlocListener( + listener: (context, state) { + if (state.passwordError != null) { + inputPasswordKey.currentState?.syncError( + errorText: 'Incorrect password. Please try again.', + ); + } else { + inputPasswordKey.currentState?.clearError(); + } + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo and title + ..._buildLogoAndTitle(), - // Password input and buttons - ..._buildPasswordSection(), + // Password input and buttons + ..._buildPasswordSection(), - // Back to login - ..._buildBackToLogin(), - ], + // Back to login + ..._buildBackToLogin(), + ], + ), ), ), ), @@ -100,25 +114,33 @@ class _ContinueWithPasswordPageState extends State { return [ // Password input AFTextField( + key: inputPasswordKey, controller: passwordController, hintText: 'Enter password', autoFocus: true, onSubmitted: widget.onEnterPassword, ), // todo: ask designer to provide the spacing - VSpace(12), + VSpace(8), - // todo: forgot password is not implemented yet // Forgot password button - // AFGhostTextButton( - // text: 'Forget password?', - // size: AFButtonSize.s, - // onTap: widget.onForgotPassword, - // textColor: (context, isHovering, disabled) { - // return theme.textColorScheme.theme; - // }, - // ), - VSpace(12), + Align( + alignment: Alignment.centerLeft, + child: AFGhostTextButton( + text: 'Forget password?', + size: AFButtonSize.s, + padding: EdgeInsets.zero, + onTap: widget.onForgotPassword, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } + return theme.textColorScheme.theme; + }, + ), + ), + VSpace(20), // Continue button AFFilledTextButton.primary( @@ -137,8 +159,12 @@ class _ContinueWithPasswordPageState extends State { text: 'Back to Login', size: AFButtonSize.s, onTap: widget.backToLogin, + padding: EdgeInsets.zero, textColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } return theme.textColorScheme.theme; }, ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart index 4ea819d997..19a5ab9cf6 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -26,8 +26,8 @@ class SignInAgreement extends StatelessWidget { children: [ TextSpan( text: isLocalAuthEnabled - ? '${LocaleKeys.web_signInLocalAgreement.tr()} \n' - : '${LocaleKeys.web_signInAgreement.tr()} \n', + ? LocaleKeys.web_signInLocalAgreement.tr() + : LocaleKeys.web_signInAgreement.tr(), style: textStyle, ), TextSpan( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart index ed2e060c49..8d27846c46 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart @@ -101,6 +101,7 @@ class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { VSpace(theme.spacing.l), AFGhostTextButton( text: 'More options', + padding: EdgeInsets.zero, textColor: (context, isHovering, disabled) { if (isHovering) { return theme.fillColorScheme.themeThickHover; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 9fed31027f..595d4bb859 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -5,6 +5,11 @@ typedef AFTextFieldValidator = (bool result, String errorText) Function( TextEditingController controller, ); +abstract class AFTextFieldState extends State { + void syncError({required String errorText}) {} + void clearError() {} +} + class AFTextField extends StatefulWidget { const AFTextField({ super.key, @@ -17,8 +22,12 @@ class AFTextField extends StatefulWidget { this.onChanged, this.onSubmitted, this.autoFocus, + this.height = 40.0, }); + /// The height of the text field. + final double height; + /// The hint text to display when the text field is empty. final String? hintText; @@ -52,7 +61,7 @@ class AFTextField extends StatefulWidget { State createState() => _AFTextFieldState(); } -class _AFTextFieldState extends State { +class _AFTextFieldState extends AFTextFieldState { late final TextEditingController effectiveController; bool hasError = false; @@ -146,8 +155,11 @@ class _AFTextFieldState extends State { ), ); + child = SizedBox(height: widget.height, child: child); + if (hasError && errorText.isNotEmpty) { child = Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ child, SizedBox(height: theme.spacing.xs), @@ -174,4 +186,22 @@ class _AFTextFieldState extends State { }); } } + + @override + void syncError({ + required String errorText, + }) { + setState(() { + hasError = true; + this.errorText = errorText; + }); + } + + @override + void clearError() { + setState(() { + hasError = false; + errorText = ''; + }); + } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 29473d2e00..91f9ce7743 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -48,7 +48,7 @@ "repeatPasswordEmptyError": "Repeat password can't be empty", "unmatchedPasswordError": "Repeat password is not the same as password", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page", - "or": "OR", + "or": "or", "signInWithGoogle": "Continue with Google", "signInWithGithub": "Continue with GitHub", "signInWithDiscord": "Continue with Discord", @@ -70,15 +70,18 @@ "generalError": "Something went wrong. Please try again later", "limitRateError": "For security reasons, you can only request a magic link every 60 seconds", "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.", - "tokenHasExpiredOrInvalid": "The token has expired or is invalid. Please try again.", + "tokenHasExpiredOrInvalid": "The code has expired or is invalid. Please try again.", "signingIn": "Signing in...", "checkYourEmail": "Check your email", - "temporaryVerificationSent": "A temporary verification link has been sent. Please check your inbox at", + "temporaryVerificationLinkSent": "A temporary verification link has been sent.\nPlease check your inbox at", + "temporaryVerificationCodeSent": "A temporary verification code has been sent.\nPlease check your inbox at", "continueToSignIn": "Continue to sign in", "backToLogin": "Back to login", "enterCode": "Enter code", "enterCodeManually": "Enter code manually", - "continueWithEmail": "Continue with email" + "continueWithEmail": "Continue with email", + "invalidVerificationCode": "Please enter a valid verification code", + "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later." }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -2814,8 +2817,8 @@ "continueWithApple": "Continue with Apple ", "moreOptions": "More options", "collapse": "Collapse", - "signInAgreement": "By clicking \"Continue\" above, you agreed to AppFlowy's", - "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to AppFlowy's", + "signInAgreement": "By clicking \"Continue\" above, you agreed to \nAppFlowy's ", + "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to \nAppFlowy's ", "and": "and", "termOfUse": "Terms", "privacyPolicy": "Privacy Policy",