Spaces:
Build error
Build error
import React, { useState, useEffect, useRef } from "react"; | |
import { useTranslation } from "react-i18next"; | |
import { useQueryClient } from "@tanstack/react-query"; | |
import { useSettings } from "#/hooks/query/use-settings"; | |
import { openHands } from "#/api/open-hands-axios"; | |
import { displaySuccessToast } from "#/utils/custom-toast-handlers"; | |
// Email validation regex pattern | |
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; | |
function EmailInputSection({ | |
email, | |
onEmailChange, | |
onSaveEmail, | |
onResendVerification, | |
isSaving, | |
isResendingVerification, | |
isEmailChanged, | |
emailVerified, | |
isEmailValid, | |
children, | |
}: { | |
email: string; | |
onEmailChange: (e: React.ChangeEvent<HTMLInputElement>) => void; | |
onSaveEmail: () => void; | |
onResendVerification: () => void; | |
isSaving: boolean; | |
isResendingVerification: boolean; | |
isEmailChanged: boolean; | |
emailVerified?: boolean; | |
isEmailValid: boolean; | |
children: React.ReactNode; | |
}) { | |
const { t } = useTranslation(); | |
return ( | |
<div className="flex flex-col gap-4"> | |
<div className="flex flex-col gap-2"> | |
<label className="text-sm">{t("SETTINGS$USER_EMAIL")}</label> | |
<div className="flex items-center gap-3"> | |
<input | |
type="email" | |
value={email} | |
onChange={onEmailChange} | |
className={`text-base text-white p-2 bg-base-tertiary rounded border ${ | |
isEmailChanged && !isEmailValid | |
? "border-red-500" | |
: "border-tertiary" | |
} flex-grow focus:outline-none focus:border-transparent focus:ring-0`} | |
placeholder={t("SETTINGS$USER_EMAIL_LOADING")} | |
data-testid="email-input" | |
/> | |
</div> | |
{isEmailChanged && !isEmailValid && ( | |
<div | |
className="text-red-500 text-sm mt-1" | |
data-testid="email-validation-error" | |
> | |
{t("SETTINGS$INVALID_EMAIL_FORMAT")} | |
</div> | |
)} | |
<div className="flex items-center gap-3 mt-2"> | |
<button | |
type="button" | |
onClick={onSaveEmail} | |
disabled={!isEmailChanged || isSaving || !isEmailValid} | |
className="px-4 py-2 rounded bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]" | |
data-testid="save-email-button" | |
> | |
{isSaving ? t("SETTINGS$SAVING") : t("SETTINGS$SAVE")} | |
</button> | |
{emailVerified === false && ( | |
<button | |
type="button" | |
onClick={onResendVerification} | |
disabled={isResendingVerification} | |
className="px-4 py-2 rounded bg-primary text-white hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed disabled:text-[#0D0F11]" | |
data-testid="resend-verification-button" | |
> | |
{isResendingVerification | |
? t("SETTINGS$SENDING") | |
: t("SETTINGS$RESEND_VERIFICATION")} | |
</button> | |
)} | |
</div> | |
{children} | |
</div> | |
</div> | |
); | |
} | |
function VerificationAlert() { | |
const { t } = useTranslation(); | |
return ( | |
<div | |
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mt-4" | |
role="alert" | |
> | |
<p className="font-bold">{t("SETTINGS$EMAIL_VERIFICATION_REQUIRED")}</p> | |
<p className="text-sm"> | |
{t("SETTINGS$EMAIL_VERIFICATION_RESTRICTION_MESSAGE")} | |
</p> | |
</div> | |
); | |
} | |
// These components have been replaced with toast notifications | |
function UserSettingsScreen() { | |
const { t } = useTranslation(); | |
const { data: settings, isLoading, refetch } = useSettings(); | |
const [email, setEmail] = useState(""); | |
const [originalEmail, setOriginalEmail] = useState(""); | |
const [isSaving, setIsSaving] = useState(false); | |
const [isResendingVerification, setIsResendingVerification] = useState(false); | |
const [isEmailValid, setIsEmailValid] = useState(true); | |
const queryClient = useQueryClient(); | |
const pollingIntervalRef = useRef<number | null>(null); | |
const prevVerificationStatusRef = useRef<boolean | undefined>(undefined); | |
useEffect(() => { | |
if (settings?.EMAIL) { | |
setEmail(settings.EMAIL); | |
setOriginalEmail(settings.EMAIL); | |
setIsEmailValid(EMAIL_REGEX.test(settings.EMAIL)); | |
} | |
}, [settings?.EMAIL]); | |
useEffect(() => { | |
if (pollingIntervalRef.current) { | |
window.clearInterval(pollingIntervalRef.current); | |
pollingIntervalRef.current = null; | |
} | |
if ( | |
prevVerificationStatusRef.current === false && | |
settings?.EMAIL_VERIFIED === true | |
) { | |
// Display toast notification instead of setting state | |
displaySuccessToast(t("SETTINGS$EMAIL_VERIFIED_SUCCESSFULLY")); | |
setTimeout(() => { | |
queryClient.invalidateQueries({ queryKey: ["settings"] }); | |
}, 2000); | |
} | |
prevVerificationStatusRef.current = settings?.EMAIL_VERIFIED; | |
if (settings?.EMAIL_VERIFIED === false) { | |
pollingIntervalRef.current = window.setInterval(() => { | |
refetch(); | |
}, 5000); | |
} | |
return () => { | |
if (pollingIntervalRef.current) { | |
window.clearInterval(pollingIntervalRef.current); | |
pollingIntervalRef.current = null; | |
} | |
}; | |
}, [settings?.EMAIL_VERIFIED, refetch, queryClient, t]); | |
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
const newEmail = e.target.value; | |
setEmail(newEmail); | |
setIsEmailValid(EMAIL_REGEX.test(newEmail)); | |
}; | |
const handleSaveEmail = async () => { | |
if (email === originalEmail || !isEmailValid) return; | |
try { | |
setIsSaving(true); | |
await openHands.post("/api/email", { email }, { withCredentials: true }); | |
setOriginalEmail(email); | |
// Display toast notification instead of setting state | |
displaySuccessToast(t("SETTINGS$EMAIL_SAVED_SUCCESSFULLY")); | |
queryClient.invalidateQueries({ queryKey: ["settings"] }); | |
} catch (error) { | |
// eslint-disable-next-line no-console | |
console.error(t("SETTINGS$FAILED_TO_SAVE_EMAIL"), error); | |
} finally { | |
setIsSaving(false); | |
} | |
}; | |
const handleResendVerification = async () => { | |
try { | |
setIsResendingVerification(true); | |
await openHands.put("/api/email/verify", {}, { withCredentials: true }); | |
// Display toast notification instead of setting state | |
displaySuccessToast(t("SETTINGS$VERIFICATION_EMAIL_SENT")); | |
} catch (error) { | |
// eslint-disable-next-line no-console | |
console.error(t("SETTINGS$FAILED_TO_RESEND_VERIFICATION"), error); | |
} finally { | |
setIsResendingVerification(false); | |
} | |
}; | |
const isEmailChanged = email !== originalEmail; | |
return ( | |
<div data-testid="user-settings-screen" className="flex flex-col h-full"> | |
<div className="p-9 flex flex-col gap-6"> | |
{isLoading ? ( | |
<div className="animate-pulse h-8 w-64 bg-tertiary rounded" /> | |
) : ( | |
<EmailInputSection | |
email={email} | |
onEmailChange={handleEmailChange} | |
onSaveEmail={handleSaveEmail} | |
onResendVerification={handleResendVerification} | |
isSaving={isSaving} | |
isResendingVerification={isResendingVerification} | |
isEmailChanged={isEmailChanged} | |
emailVerified={settings?.EMAIL_VERIFIED} | |
isEmailValid={isEmailValid} | |
> | |
{settings?.EMAIL_VERIFIED === false && <VerificationAlert />} | |
</EmailInputSection> | |
)} | |
</div> | |
</div> | |
); | |
} | |
export default UserSettingsScreen; | |