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; | |