fix(mobile): explain an invalid email once typing pauses

The Forgot Password and Edit Email hints waited for the field to lose focus.
Android keeps it focused when Back closes the keyboard, as does tapping the
Edit Email header, so Continue or Submit stayed disabled without a reason.
The hint now also shows a second after typing stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DIYgod
2026-10-12 21:47:45 +08:00
parent f56d18558e
commit d284517c12
3 changed files with 27 additions and 2 deletions
@@ -0,0 +1,21 @@
import { useEffect } from "react"
// Long enough that a value still being typed is not flagged
const TYPING_PAUSE_MS = 1000
/**
* Marks a field as touched once typing pauses on an invalid value. A hint that waits for the field
* to lose focus never shows on Android when Back closes the keyboard, as the field keeps focus, nor
* while the field stays focused and a disabled header button is tapped.
*/
export const useTouchOnTypingPause = (
value: string,
isInvalid: boolean,
setTouched: (touched: boolean) => void,
) => {
useEffect(() => {
if (!value || !isInvalid) return
const timer = setTimeout(() => setTouched(true), TYPING_PAUSE_MS)
return () => clearTimeout(timer)
}, [value, isInvalid, setTouched])
}
@@ -18,6 +18,7 @@ import {
GroupedPlainButtonCell,
} from "@/src/components/ui/grouped/GroupedList"
import { Text } from "@/src/components/ui/typography/Text"
import { useTouchOnTypingPause } from "@/src/hooks/useTouchOnTypingPause"
import { isValidEmail } from "@/src/lib/email"
import { useNavigation } from "@/src/lib/navigation/hooks"
import type { NavigationControllerView } from "@/src/lib/navigation/types"
@@ -31,11 +32,12 @@ export const EditEmailScreen: NavigationControllerView = () => {
const [email, setEmail] = useState(whoami?.email ?? "")
const [isDirty, setIsDirty] = useState(false)
// Set once the field is left, so the hint does not interrupt typing
// Set once the field is left or typing pauses, so the hint does not interrupt typing
const [isEmailTouched, setIsEmailTouched] = useState(false)
const isValidate = whoami?.emailVerified
const newEmailIsValid = isValidEmail(email)
useTouchOnTypingPause(isDirty ? email : "", !newEmailIsValid, setIsEmailTouched)
const navigation = useNavigation()
const { mutate: updateEmail, isPending } = useMutation({
@@ -9,6 +9,7 @@ import { SubmitButton } from "@/src/components/common/SubmitButton"
import { HeaderCloseOnly } from "@/src/components/layouts/header/HeaderElements"
import { PlainTextField } from "@/src/components/ui/form/TextField"
import { Text } from "@/src/components/ui/typography/Text"
import { useTouchOnTypingPause } from "@/src/hooks/useTouchOnTypingPause"
import { forgetPassword } from "@/src/lib/auth"
import { isValidEmail } from "@/src/lib/email"
import { useNavigation } from "@/src/lib/navigation/hooks"
@@ -20,10 +21,11 @@ export const ForgetPasswordScreen: NavigationControllerView = () => {
const insets = useSafeAreaInsets()
const { t } = useTranslation()
const [email, setEmail] = useState("")
// Set once the field is left or submitted, so the hint does not interrupt typing
// Set once the field is left or submitted or typing pauses, so the hint does not interrupt typing
const [isEmailTouched, setIsEmailTouched] = useState(false)
const trimmedEmail = email.trim()
const isEmailValid = isValidEmail(trimmedEmail)
useTouchOnTypingPause(trimmedEmail, !isEmailValid, setIsEmailTouched)
const navigation = useNavigation()
const contentContainerStyle = {
flexGrow: 1,