fix: otp launch review issues (#7730)

* fix: add error text under text field

* chore: update translation
This commit is contained in:
Lucas 2025-04-14 10:29:46 +08:00 committed by GitHub
parent 2e295e6891
commit 8c4324ee9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 269 additions and 149 deletions

View File

@ -67,13 +67,11 @@ extension CommonOperations on WidgetTester {
} else { } else {
// cloud version // cloud version
final anonymousButton = find.byType(SignInAnonymousButtonV2); 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<void> tapContinousAnotherWay() async { Future<void> tapContinousAnotherWay() async {
// local version // local version

View File

@ -199,7 +199,9 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
emit( emit(
result.fold( result.fold(
(userProfile) => state.copyWith(isSubmitting: true), (userProfile) => state.copyWith(
isSubmitting: false,
),
(error) => _stateFromCode(error), (error) => _stateFromCode(error),
), ),
); );
@ -282,8 +284,9 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
case ErrorCode.UserUnauthorized: case ErrorCode.UserUnauthorized:
final errorMsg = error.msg; final errorMsg = error.msg;
String msg = LocaleKeys.signIn_generalError.tr(); String msg = LocaleKeys.signIn_generalError.tr();
if (errorMsg.contains('rate limit')) { if (errorMsg.contains('rate limit') ||
msg = LocaleKeys.signIn_limitRateError.tr(); errorMsg.contains('For security purposes')) {
msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr();
} else if (errorMsg.contains('invalid')) { } else if (errorMsg.contains('invalid')) {
msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr();
} }

View File

@ -23,7 +23,6 @@ class DesktopSignInScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
const indicatorMinHeight = 4.0;
return BlocBuilder<SignInBloc, SignInState>( return BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
@ -55,15 +54,6 @@ class DesktopSignInScreen extends StatelessWidget {
// sign in agreement // sign in agreement
const SignInAgreement(), const SignInAgreement(),
// loading status
const VSpace(indicatorMinHeight),
state.isSubmitting
? const LinearProgressIndicator(
minHeight: indicatorMinHeight,
)
: const VSpace(indicatorMinHeight),
const VSpace(20),
const Spacer(), const Spacer(),
// anonymous sign in and settings // anonymous sign in and settings

View File

@ -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/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_loading_screen.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart'; import 'package:universal_platform/universal_platform.dart';
@ -37,10 +38,17 @@ class SignInScreen extends StatelessWidget {
void _showSignInError(BuildContext context, SignInState state) { void _showSignInError(BuildContext context, SignInState state) {
final successOrFail = state.successOrFail; final successOrFail = state.successOrFail;
if (successOrFail != null) { if (successOrFail != null) {
handleUserProfileResult( successOrFail.fold(
successOrFail, (userProfile) {
context, if (userProfile.encryptionType == EncryptionTypePB.Symmetric) {
getIt<AuthRouter>(), getIt<AuthRouter>().pushEncryptionScreen(context, userProfile);
} else {
getIt<AuthRouter>().goHomeScreen(context, userProfile);
}
},
(error) {
handleOpenWorkspaceError(context, error);
},
); );
} }
} }

View File

@ -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/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_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/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:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
class ContinueWithEmailAndPassword extends StatefulWidget { class ContinueWithEmailAndPassword extends StatefulWidget {
const ContinueWithEmailAndPassword({super.key}); const ContinueWithEmailAndPassword({super.key});
@ -23,6 +21,7 @@ class _ContinueWithEmailAndPasswordState
extends State<ContinueWithEmailAndPassword> { extends State<ContinueWithEmailAndPassword> {
final controller = TextEditingController(); final controller = TextEditingController();
final focusNode = FocusNode(); final focusNode = FocusNode();
final emailKey = GlobalKey<AFTextFieldState>();
@override @override
void dispose() { void dispose() {
@ -36,28 +35,43 @@ class _ContinueWithEmailAndPasswordState
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
return Column( return BlocListener<SignInBloc, SignInState>(
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: [ children: [
SizedBox( AFTextField(
height: UniversalPlatform.isMobile ? 38.0 : 40.0, key: emailKey,
child: AFTextField(
controller: controller, controller: controller,
hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),
radius: 10, radius: 10,
onSubmitted: (value) => _pushContinueWithMagicLinkOrPasscodePage( onSubmitted: (value) => _signInWithEmail(
context, context,
value, value,
), ),
), ),
),
VSpace(theme.spacing.l), VSpace(theme.spacing.l),
ContinueWithEmail( ContinueWithEmail(
onTap: () => _pushContinueWithMagicLinkOrPasscodePage( onTap: () => _signInWithEmail(
context, context,
controller.text, controller.text,
), ),
), ),
// Hide password sign in until we implement the reset password / forgot password
// VSpace(theme.spacing.l), // VSpace(theme.spacing.l),
// ContinueWithPassword( // ContinueWithPassword(
// onTap: () => _pushContinueWithPasswordPage( // onTap: () => _pushContinueWithPasswordPage(
@ -66,25 +80,29 @@ class _ContinueWithEmailAndPasswordState
// ), // ),
// ), // ),
], ],
),
); );
} }
void _signInWithEmail(BuildContext context, String email) {
if (!isEmail(email)) {
emailKey.currentState?.syncError(
errorText: LocaleKeys.signIn_invalidEmail.tr(),
);
return;
}
context
.read<SignInBloc>()
.add(SignInEvent.signInWithMagicLink(email: email));
}
void _pushContinueWithMagicLinkOrPasscodePage( void _pushContinueWithMagicLinkOrPasscodePage(
BuildContext context, BuildContext context,
String email, String email,
) { ) {
if (!isEmail(email)) {
showToastNotification(
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
return;
}
final signInBloc = context.read<SignInBloc>(); final signInBloc = context.read<SignInBloc>();
signInBloc.add(SignInEvent.signInWithMagicLink(email: email));
// push the a continue with magic link or passcode screen // push the a continue with magic link or passcode screen
Navigator.push( Navigator.push(
context, context,
@ -114,7 +132,9 @@ class _ContinueWithEmailAndPasswordState
// Navigator.push( // Navigator.push(
// context, // context,
// MaterialPageRoute( // MaterialPageRoute(
// builder: (context) => ContinueWithPasswordPage( // builder: (context) => BlocProvider.value(
// value: signInBloc,
// child: ContinueWithPasswordPage(
// email: email, // email: email,
// backToLogin: () => Navigator.pop(context), // backToLogin: () => Navigator.pop(context),
// onEnterPassword: (password) => signInBloc.add( // onEnterPassword: (password) => signInBloc.add(
@ -128,6 +148,7 @@ class _ContinueWithEmailAndPasswordState
// }, // },
// ), // ),
// ), // ),
// ),
// ); // );
// } // }
} }

View File

@ -33,6 +33,8 @@ class _ContinueWithMagicLinkOrPasscodePageState
ToastificationItem? toastificationItem; ToastificationItem? toastificationItem;
final inputPasscodeKey = GlobalKey<AFTextFieldState>();
@override @override
void dispose() { void dispose() {
passcodeController.dispose(); passcodeController.dispose();
@ -44,10 +46,13 @@ class _ContinueWithMagicLinkOrPasscodePageState
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocListener<SignInBloc, SignInState>( return BlocListener<SignInBloc, SignInState>(
listener: (context, state) { listener: (context, state) {
if (state.isSubmitting) { final successOrFail = state.successOrFail;
_showLoadingDialog(); if (successOrFail != null && successOrFail.isFailure) {
} else { successOrFail.onFailure((error) {
_dismissLoadingDialog(); inputPasscodeKey.currentState?.syncError(
errorText: LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(),
);
});
} }
}, },
child: Scaffold( child: Scaffold(
@ -91,24 +96,39 @@ class _ContinueWithMagicLinkOrPasscodePageState
return [ return [
// Enter code manually // Enter code manually
SizedBox( AFTextField(
height: 40, key: inputPasscodeKey,
child: AFTextField(
controller: passcodeController, controller: passcodeController,
hintText: LocaleKeys.signIn_enterCode.tr(), hintText: LocaleKeys.signIn_enterCode.tr(),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
radius: 10, radius: 10,
autoFocus: true, autoFocus: true,
onSubmitted: widget.onEnterPasscode, onSubmitted: (passcode) {
), if (passcode.isEmpty) {
inputPasscodeKey.currentState?.syncError(
errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),
);
} else {
widget.onEnterPasscode(passcode);
}
},
), ),
// todo: ask designer to provide the spacing // todo: ask designer to provide the spacing
VSpace(12), VSpace(12),
// continue to login // continue to login
AFFilledTextButton.primary( AFFilledTextButton.primary(
text: 'Continue to sign in', text: LocaleKeys.signIn_continueToSignIn.tr(),
onTap: () => widget.onEnterPasscode(passcodeController.text), onTap: () {
final passcode = passcodeController.text;
if (passcode.isEmpty) {
inputPasscodeKey.currentState?.syncError(
errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),
);
} else {
widget.onEnterPasscode(passcode);
}
},
size: AFButtonSize.l, size: AFButtonSize.l,
alignment: Alignment.center, alignment: Alignment.center,
), ),
@ -123,6 +143,7 @@ class _ContinueWithMagicLinkOrPasscodePageState
text: LocaleKeys.signIn_backToLogin.tr(), text: LocaleKeys.signIn_backToLogin.tr(),
size: AFButtonSize.s, size: AFButtonSize.s,
onTap: widget.backToLogin, onTap: widget.backToLogin,
padding: EdgeInsets.zero,
textColor: (context, isHovering, disabled) { textColor: (context, isHovering, disabled) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (isHovering) { if (isHovering) {
@ -137,6 +158,7 @@ class _ContinueWithMagicLinkOrPasscodePageState
List<Widget> _buildLogoTitleAndDescription() { List<Widget> _buildLogoTitleAndDescription() {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
final spacing = VSpace(theme.spacing.xxl); final spacing = VSpace(theme.spacing.xxl);
if (!isEnteringPasscode) {
return [ return [
// logo // logo
const AFLogo(), const AFLogo(),
@ -153,7 +175,39 @@ class _ContinueWithMagicLinkOrPasscodePageState
// description // description
Text( Text(
LocaleKeys.signIn_temporaryVerificationSent.tr(), LocaleKeys.signIn_temporaryVerificationLinkSent.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,
];
} else {
return [
// logo
const AFLogo(),
spacing,
// title
Text(
LocaleKeys.signIn_enterCode.tr(),
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
spacing,
// description
Text(
LocaleKeys.signIn_temporaryVerificationCodeSent.tr(),
style: theme.textStyle.body.standard( style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary, color: theme.textColorScheme.primary,
), ),
@ -169,19 +223,5 @@ class _ContinueWithMagicLinkOrPasscodePageState
spacing, spacing,
]; ];
} }
void _showLoadingDialog() {
_dismissLoadingDialog();
toastificationItem = showToastNotification(
message: LocaleKeys.signIn_signingIn.tr(),
);
}
void _dismissLoadingDialog() {
final toastificationItem = this.toastificationItem;
if (toastificationItem != null) {
toastification.dismiss(toastificationItem);
}
} }
} }

View File

@ -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/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ContinueWithPasswordPage extends StatefulWidget { class ContinueWithPasswordPage extends StatefulWidget {
const ContinueWithPasswordPage({ const ContinueWithPasswordPage({
@ -25,6 +27,7 @@ class ContinueWithPasswordPage extends StatefulWidget {
class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> { class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
final passwordController = TextEditingController(); final passwordController = TextEditingController();
final inputPasswordKey = GlobalKey<AFTextFieldState>();
@override @override
void dispose() { void dispose() {
@ -38,6 +41,16 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
body: Center( body: Center(
child: SizedBox( child: SizedBox(
width: 320, width: 320,
child: BlocListener<SignInBloc, SignInState>(
listener: (context, state) {
if (state.passwordError != null) {
inputPasswordKey.currentState?.syncError(
errorText: 'Incorrect password. Please try again.',
);
} else {
inputPasswordKey.currentState?.clearError();
}
},
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -53,6 +66,7 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
), ),
), ),
), ),
),
); );
} }
@ -100,25 +114,33 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
return [ return [
// Password input // Password input
AFTextField( AFTextField(
key: inputPasswordKey,
controller: passwordController, controller: passwordController,
hintText: 'Enter password', hintText: 'Enter password',
autoFocus: true, autoFocus: true,
onSubmitted: widget.onEnterPassword, onSubmitted: widget.onEnterPassword,
), ),
// todo: ask designer to provide the spacing // todo: ask designer to provide the spacing
VSpace(12), VSpace(8),
// todo: forgot password is not implemented yet
// Forgot password button // Forgot password button
// AFGhostTextButton( Align(
// text: 'Forget password?', alignment: Alignment.centerLeft,
// size: AFButtonSize.s, child: AFGhostTextButton(
// onTap: widget.onForgotPassword, text: 'Forget password?',
// textColor: (context, isHovering, disabled) { size: AFButtonSize.s,
// return theme.textColorScheme.theme; padding: EdgeInsets.zero,
// }, onTap: widget.onForgotPassword,
// ), textColor: (context, isHovering, disabled) {
VSpace(12), final theme = AppFlowyTheme.of(context);
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
return theme.textColorScheme.theme;
},
),
),
VSpace(20),
// Continue button // Continue button
AFFilledTextButton.primary( AFFilledTextButton.primary(
@ -137,8 +159,12 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
text: 'Back to Login', text: 'Back to Login',
size: AFButtonSize.s, size: AFButtonSize.s,
onTap: widget.backToLogin, onTap: widget.backToLogin,
padding: EdgeInsets.zero,
textColor: (context, isHovering, disabled) { textColor: (context, isHovering, disabled) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
return theme.textColorScheme.theme; return theme.textColorScheme.theme;
}, },
), ),

View File

@ -26,8 +26,8 @@ class SignInAgreement extends StatelessWidget {
children: [ children: [
TextSpan( TextSpan(
text: isLocalAuthEnabled text: isLocalAuthEnabled
? '${LocaleKeys.web_signInLocalAgreement.tr()} \n' ? LocaleKeys.web_signInLocalAgreement.tr()
: '${LocaleKeys.web_signInAgreement.tr()} \n', : LocaleKeys.web_signInAgreement.tr(),
style: textStyle, style: textStyle,
), ),
TextSpan( TextSpan(

View File

@ -101,6 +101,7 @@ class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> {
VSpace(theme.spacing.l), VSpace(theme.spacing.l),
AFGhostTextButton( AFGhostTextButton(
text: 'More options', text: 'More options',
padding: EdgeInsets.zero,
textColor: (context, isHovering, disabled) { textColor: (context, isHovering, disabled) {
if (isHovering) { if (isHovering) {
return theme.fillColorScheme.themeThickHover; return theme.fillColorScheme.themeThickHover;

View File

@ -5,6 +5,11 @@ typedef AFTextFieldValidator = (bool result, String errorText) Function(
TextEditingController controller, TextEditingController controller,
); );
abstract class AFTextFieldState extends State<AFTextField> {
void syncError({required String errorText}) {}
void clearError() {}
}
class AFTextField extends StatefulWidget { class AFTextField extends StatefulWidget {
const AFTextField({ const AFTextField({
super.key, super.key,
@ -17,8 +22,12 @@ class AFTextField extends StatefulWidget {
this.onChanged, this.onChanged,
this.onSubmitted, this.onSubmitted,
this.autoFocus, 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. /// The hint text to display when the text field is empty.
final String? hintText; final String? hintText;
@ -52,7 +61,7 @@ class AFTextField extends StatefulWidget {
State<AFTextField> createState() => _AFTextFieldState(); State<AFTextField> createState() => _AFTextFieldState();
} }
class _AFTextFieldState extends State<AFTextField> { class _AFTextFieldState extends AFTextFieldState {
late final TextEditingController effectiveController; late final TextEditingController effectiveController;
bool hasError = false; bool hasError = false;
@ -146,8 +155,11 @@ class _AFTextFieldState extends State<AFTextField> {
), ),
); );
child = SizedBox(height: widget.height, child: child);
if (hasError && errorText.isNotEmpty) { if (hasError && errorText.isNotEmpty) {
child = Column( child = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
child, child,
SizedBox(height: theme.spacing.xs), SizedBox(height: theme.spacing.xs),
@ -174,4 +186,22 @@ class _AFTextFieldState extends State<AFTextField> {
}); });
} }
} }
@override
void syncError({
required String errorText,
}) {
setState(() {
hasError = true;
this.errorText = errorText;
});
}
@override
void clearError() {
setState(() {
hasError = false;
errorText = '';
});
}
} }

View File

@ -48,7 +48,7 @@
"repeatPasswordEmptyError": "Repeat password can't be empty", "repeatPasswordEmptyError": "Repeat password can't be empty",
"unmatchedPasswordError": "Repeat password is not the same as password", "unmatchedPasswordError": "Repeat password is not the same as password",
"syncPromptMessage": "Syncing the data might take a while. Please don't close this page", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page",
"or": "OR", "or": "or",
"signInWithGoogle": "Continue with Google", "signInWithGoogle": "Continue with Google",
"signInWithGithub": "Continue with GitHub", "signInWithGithub": "Continue with GitHub",
"signInWithDiscord": "Continue with Discord", "signInWithDiscord": "Continue with Discord",
@ -70,15 +70,18 @@
"generalError": "Something went wrong. Please try again later", "generalError": "Something went wrong. Please try again later",
"limitRateError": "For security reasons, you can only request a magic link every 60 seconds", "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.", "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...", "signingIn": "Signing in...",
"checkYourEmail": "Check your email", "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", "continueToSignIn": "Continue to sign in",
"backToLogin": "Back to login", "backToLogin": "Back to login",
"enterCode": "Enter code", "enterCode": "Enter code",
"enterCodeManually": "Enter code manually", "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": { "workspace": {
"chooseWorkspace": "Choose your workspace", "chooseWorkspace": "Choose your workspace",
@ -2814,8 +2817,8 @@
"continueWithApple": "Continue with Apple ", "continueWithApple": "Continue with Apple ",
"moreOptions": "More options", "moreOptions": "More options",
"collapse": "Collapse", "collapse": "Collapse",
"signInAgreement": "By clicking \"Continue\" 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 AppFlowy's", "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to \nAppFlowy's ",
"and": "and", "and": "and",
"termOfUse": "Terms", "termOfUse": "Terms",
"privacyPolicy": "Privacy Policy", "privacyPolicy": "Privacy Policy",