diff --git a/apps/mobile/app/components/ui/input/form-input.tsx b/apps/mobile/app/components/ui/input/form-input.tsx index 3818ed356..7d510ea0b 100644 --- a/apps/mobile/app/components/ui/input/form-input.tsx +++ b/apps/mobile/app/components/ui/input/form-input.tsx @@ -22,6 +22,7 @@ import { ColorValue, TextInput, TextInputProps, + TextStyle, TouchableOpacity, View, ViewStyle @@ -220,6 +221,7 @@ interface FormInputProps extends TextInputProps { inputStyle?: TextInputProps["style"]; containerStyle?: ViewStyle; wrapperStyle?: ViewStyle; + errorStyle?: TextStyle; } export function FormInput({ @@ -245,6 +247,7 @@ export function FormInput({ onBlur, onPress, onChangeText, + errorStyle, ...restProps }: FormInputProps) { const { colors, isDark } = useThemeColors(); @@ -391,7 +394,7 @@ export function FormInput({ {fieldError ? ( { const { colors } = useThemeColors(); const user = useUserStore((state) => state.user); + const methodId = method?.id; + const formRef = useRef( + createFormRef({ + target: "", + code: "" + }) + ); + const targetInputRef = useRef(null); + const codeInputRef = useRef(null); const [authenticatorDetails, setAuthenticatorDetails] = useState({ sharedKey: null, authenticatorUri: null }); - const code = useRef(undefined); - const phoneNumber = useRef(undefined); const { seconds, setId, start } = useTimer(method?.id); const [loading, setLoading] = useState(method?.id === "app" ? true : false); const [enabling, setEnabling] = useState(false); const [sending, setSending] = useState(false); - const [error, setError] = useState(false); + const [generalError, setGeneralError] = useState(); useEffect(() => { - if (method?.id === "app") { - db.mfa?.setup("app").then((data) => { - setAuthenticatorDetails(data); - setLoading(false); - }); + if (methodId === "app") { + setLoading(true); + db.mfa + ?.setup("app") + .then((data) => { + setAuthenticatorDetails(data); + setLoading(false); + }) + .catch((error: Error) => { + setLoading(false); + setGeneralError(error.message); + }); + return; } - }, [method?.id]); + + setLoading(false); + }, [methodId]); + + useEffect(() => { + if (!methodId) return; + + formRef.current.clearErrors(); + formRef.current.setValue( + "target", + methodId === "email" + ? user?.email || "" + : methodId === "app" + ? authenticatorDetails.sharedKey || "" + : formRef.current.getValue("target") + ); + formRef.current.setValue("code", ""); + setGeneralError(undefined); + }, [authenticatorDetails.sharedKey, methodId, user?.email]); const codeHelpText = { app: "After putting the above code in authenticator app, the app will display a code that you can enter below.", @@ -212,15 +255,43 @@ export const MFASetup = ({ "You will receive a 2FA code on your email address which you can enter below" }; + const targetValidators = + method?.id === "sms" + ? [ + validators.required(strings.phoneNumberNotEntered()), + (value: string) => + isMobilePhone(value, "any", { + strictMode: true + }) + ? undefined + : strings.enterValidPhone() + ] + : method?.id === "email" + ? [ + validators.required(strings.emailRequired()), + validators.email(strings.enterValidEmail()) + ] + : []; + + const codeValidators = [ + validators.required(strings.enterSixDigitCode()), + (value: string) => + /^\d{6}$/.test(value.trim()) ? undefined : strings.enterSixDigitCode() + ]; + const onNext = async () => { - if (!code.current || code.current.length !== 6) return; + if (formRef.current.validateField("code")) return; + try { if (!method) return; + const code = formRef.current.getValue("code").trim(); + + setGeneralError(undefined); setEnabling(true); if (recovery) { - await db.mfa.enableFallback(method.id, code.current); + await db.mfa.enableFallback(method.id, code); } else { - await db.mfa.enable(method.id, code.current); + await db.mfa.enable(method.id, code); } const user = await db.user.fetchUser(); @@ -229,14 +300,18 @@ export const MFASetup = ({ setEnabling(false); } catch (e) { const error = e as Error; - ToastManager.error(error, "Error submitting 2fa code"); + formRef.current.setError("code", error.message); setEnabling(false); } }; const onSendCode = async () => { - if (error) return; if (!method || sending) return; + + if (method.id !== "app" && formRef.current.validateField("target")) { + return; + } + if (method.id === "app" && authenticatorDetails.sharedKey) { Clipboard.setString(authenticatorDetails.sharedKey); if (authenticatorDetails.authenticatorUri) { @@ -254,30 +329,37 @@ export const MFASetup = ({ } try { - if (seconds) throw new Error(strings.resendCodeWait()); - if (method.id === "sms" && !phoneNumber.current) - throw new Error(strings.phoneNumberNotEntered()); + const target = formRef.current.getValue("target").trim(); + + setGeneralError(undefined); + if (seconds) { + setGeneralError(strings.resendCodeWait()); + return; + } + setSending(true); - await db.mfa.setup(method?.id, phoneNumber.current); + await db.mfa.setup(method.id, method.id === "sms" ? target : undefined); if (method.id === "sms") { - setId(method.id + phoneNumber.current); + setId(method.id + target); } await sleep(300); - start( - 60, - method.id === "sms" ? method.id + phoneNumber.current : method.id - ); + start(60, method.id === "sms" ? method.id + target : method.id); setSending(false); ToastManager.show({ heading: strings["2faCodeSentVia"](method.id), type: "success", context: "local" }); + codeInputRef.current?.focus(); } catch (e) { setSending(false); const error = e as Error; - ToastManager.error(error, strings.errorSend2fa()); + if (method.id === "sms" || method.id === "email") { + formRef.current.setError("target", error.message); + } else { + setGeneralError(error.message); + } } }; @@ -316,59 +398,54 @@ export const MFASetup = ({ ) : ( <> - { - phoneNumber.current = value; + onChangeText={() => { + setGeneralError(undefined); }} placeholder={ - method?.id === "email" + method.id === "email" ? strings.enterEmailAddress() : "+1234567890" } - onSubmit={() => { - onSendCode(); - }} - onErrorCheck={(e) => setError(e)} - validationType={method?.id === "email" ? "email" : "phonenumber"} + onSubmitEditing={onSendCode} + validators={targetValidators} keyboardType={ - method.id == "email" ? "email-address" : "phone-pad" - } - errorMessage={ - method?.id === "email" - ? strings.enterValidEmail() - : strings.enterValidPhone() + method.id === "email" ? "email-address" : "phone-pad" } buttons={ - error ? null : ( -