Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 74 additions & 30 deletions app/components/auth/Authenticator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
useErrorNotification,
} from '@/hooks/useGlobalNotification'
import { useTranslation } from 'react-i18next'
import GloTooltip from '@/components/common/GlobalTooltip'
/**
* Props interface for the Authenticator component.
*/
Expand All @@ -32,14 +33,13 @@ interface AuthenticatorProps {
}

/*
Authenticator Component

A React component that provides user authentication with email and password input, including
registration, verification code flow, and login.

@param onAuthSuccess Type: () => void. Callback invoked on successful authentication.

@returns JSX.Element The authenticator UI.
Authenticator component.

Provides user authentication UI and flow including login, registration, and email verification code.

@param onAuthSuccess Function. Callback invoked after a successful authentication flow. No default.

@returns JSX.Element The rendered authenticator UI.
*/
export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
const [email, setEmail] = useState('')
Expand All @@ -56,7 +56,18 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
const { showErrorNotification } = useErrorNotification()
const [codeErrorMessage, setCodeErrorMessage] = useState('')
const { t, i18n } = useTranslation()

const [needCoolDown, setNeedCoolDown] = useState(false)
/*
Get current geolocation with a timeout guard.

Resolves with latitude and longitude if available, otherwise rejects on timeout or geolocation error.

@param options PositionOptions. Options passed to navigator.geolocation.getCurrentPosition. Default timeout is 5000 ms if not provided.

@returns Promise<{ latitude: number; longitude: number }> The resolved location coordinates.

@throws {Error} When the location request times out or geolocation reports an error.
*/
const getCurrentPositionAsync = (options: PositionOptions) => {
return new Promise((resolve, reject) => {
let resolved = false
Expand Down Expand Up @@ -88,12 +99,12 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
})
}
/*
Verify user credentials and handle registration or login.

@param email Type: string. The user's email address.
@param password Type: string. The user's password.

@returns Promise<void> Resolves when the flow completes.
Verify user credentials and handle registration or login based on the active tab.
@param email string. The user's email address.
@param password string. The user's password.
@returns Promise<void> Resolves when the verification/login flow is completed.
*/
const loginVerify = async (email: string, password: string) => {
setIsLoading(true)
Expand All @@ -109,6 +120,7 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
setIsLoading(false)
if (response.auth_code === 200) {
setNeedCode(response.confirmation_required)
setNeedCoolDown(response.confirmation_required)
if (!response.confirmation_required) {
showSuccessNotification('Registration Successful!')
setActiveTab('login')
Expand Down Expand Up @@ -202,11 +214,11 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
}

/*
Handle form submission for user authentication.

@param e Type: React.FormEvent. The form submit event.

@returns Promise<void> Resolves after submit handling is complete.
Handle form submission for user authentication (login or register).
@param e React.FormEvent. The form submit event.
@returns Promise<void> Resolves after submit handling completes.
*/
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
Expand All @@ -231,10 +243,10 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {

/*
Switch between login and register tabs.

@param event Type: React.SyntheticEvent. The tab change event.
@param value Type: 'login' | 'register'. The target tab value.

@param event React.SyntheticEvent. The tab change event.
@param value 'login' | 'register'. The target tab value to activate.
@returns void
*/
const handleTabChange = (
Expand All @@ -244,11 +256,11 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
setActiveTab(value)
}
/*
Submit the verification code in register flow.

@param inputCode Type: string. The 6-digit verification code.

@returns Promise<void> Resolves when the action completes.
Submit the verification code during the registration flow.
@param inputCode string. The 6-digit verification code provided by the user.
@returns Promise<void> Resolves when the verification handling completes.
*/
const handleCodeSubmit = async (inputCode: string) => {
setCodeErrorMessage('')
Expand Down Expand Up @@ -288,6 +300,20 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
}
}

/*
Request sending or resending a verification code to the provided email.

No action is taken if the email field is empty.

@returns Promise<void> Resolves after the resend request completes.
*/
const handleNeedCode = async () => {
if (!email) {
return
}
setNeedCode(true)
}

const buttonText = (() => {
if (activeTab === 'login') {
return isLoading ? t('auth.signingIn') : t('auth.signIn')
Expand Down Expand Up @@ -504,7 +530,24 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
</button>
</div>
{/** code */}

<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
color: '#fff',
fontSize: '14px',
fontWeight: '500',
marginBottom: '16px',
textAlign: 'left',
cursor: 'pointer',
}}
onClick={handleNeedCode}
>
<GloTooltip content={t('auth.enterTheVerificationCodeDescription')}>
<div>{t('auth.needToEnterTheVerificationCode')}</div>
</GloTooltip>
</div>
{/* Sign In Button */}
<button
type="submit"
Expand Down Expand Up @@ -543,6 +586,7 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
onSubmit={handleCodeSubmit}
isSubmitting={isLoading}
errorMessage={codeErrorMessage}
needCoolDown={needCoolDown}
/>
</div>
)
Expand Down
134 changes: 119 additions & 15 deletions app/components/auth/EmailCodeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,114 @@
import React, { useEffect, useState } from 'react'
import { Dialog } from '@/components/common/Dialog'
import { useTranslation } from 'react-i18next'

import GloTooltip from '@/components/common/GlobalTooltip'
import { fetchResendConfirmationCode } from '@/request/api'
import {
useSuccessNotification,
useErrorNotification,
} from '@/hooks/useGlobalNotification'
/*
Props for the EmailCodeModal component.

Displays and manages the email verification code modal including code input,
resend logic with cooldown, and submission handling.
*/
interface EmailCodeModalProps {
isOpen: boolean
email?: string
email: string
onClose: () => void
onSubmit?: (code: string) => void | Promise<void>
onResend?: () => void | Promise<void>
isSubmitting?: boolean
errorMessage?: string
needCoolDown?: boolean
}
/*
EmailCodeModal component.

Renders a dialog to enter a 6-digit email verification code and supports resending
the code with a cooldown period.

@param isOpen boolean. Whether the dialog is open.
@param email string. The email address to which the code was sent.
@param onClose Function. Called when the dialog should close.
@param onSubmit Function | Promise<void>. Optional submit handler for the entered code. No default.
@param onResend Function | Promise<void>. Optional external resend handler (unused here). No default.
@param isSubmitting boolean. Whether submission is in progress. Default: false.
@param errorMessage string | undefined. Optional error message to display. No default.
@param needCoolDown boolean. If true, initializes resend cooldown. Default: false.

@returns JSX.Element The verification code dialog UI.
*/
export default function EmailCodeModal({
isOpen,
email,
onClose,
onSubmit,
isSubmitting = false,
errorMessage,
needCoolDown = false,
}: EmailCodeModalProps) {
/*
Handle verification code form submission.

@param e React.FormEvent - The form submit event.

@returns Promise<void> Resolves when the submit completes.
*/
const { showSuccessNotification } = useSuccessNotification()
const { showErrorNotification } = useErrorNotification()
const [code, setCode] = useState('')
const { t } = useTranslation()
const { t, i18n } = useTranslation()
useEffect(() => {
if (isOpen) {
setCode('')
if (needCoolDown) {
setResendCooldown(60)
}
}
}, [isOpen])

/*
Handle verification code form submission.

@param e React.FormEvent. The form submit event.

@returns Promise<void> Resolves when the submit completes.
*/
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!code.trim()) return
await onSubmit?.(code.trim())
}

const [resendCooldown, setResendCooldown] = useState(0)
useEffect(() => {
if (resendCooldown <= 0) return
const timer = setTimeout(() => setResendCooldown(prev => prev - 1), 1000)
return () => clearTimeout(timer)
}, [resendCooldown])

/*
Resend a verification code and start/reset the cooldown timer.

No action is taken if the cooldown is still active.

@returns Promise<void> Resolves after the resend attempt completes.
*/
const handleResend = async () => {
if (resendCooldown > 0) return
const response = await fetchResendConfirmationCode(email, i18n.language)
if (response.auth_code === 200) {
showSuccessNotification(t('notification.verificationCodeResentSuccessfully'))
setResendCooldown(60)
} else {
showErrorNotification(response.auth_msg)
}
}

return (
<Dialog
isOpen={isOpen}
onClose={onClose}
title={t('auth.emailVerificationCode')}
maxWidth="420px"
className="email-code-dialog"
closeOnEscape={false}
closeOnBackdropClick={false}
>
<form
onSubmit={handleSubmit}
Expand All @@ -61,7 +123,23 @@ export default function EmailCodeModal({
</span>{' '}
{t('auth.pleaseEnterThe6DigitCodeInTheEmailToCompleteTheVerification')}
</div>

<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
color: '#fff',
fontSize: '14px',
fontWeight: '500',
marginBottom: '16px',
textAlign: 'left',
cursor: 'pointer',
}}
>
<GloTooltip content={t('auth.resendVerificationCodeDescription')}>
<div>{t('auth.didNotReceiveTheVerificationCode')}</div>
</GloTooltip>
</div>
<label style={{ color: '#8b8ea8', fontSize: '14px' }}>
{t('auth.verificationCode')}
</label>
Expand All @@ -74,7 +152,6 @@ export default function EmailCodeModal({
onChange={e => setCode(e.target.value)}
placeholder={t('auth.enterTheVerificationCode')}
style={{
width: '100%',
height: '48px',
padding: '0 14px',
backgroundColor: 'transparent',
Expand All @@ -91,11 +168,9 @@ export default function EmailCodeModal({
e.currentTarget.style.borderColor = '#333652'
}}
/>

{errorMessage ? (
<div style={{ color: '#ff6b6b', fontSize: '13px' }}>{errorMessage}</div>
) : null}

<div
style={{
display: 'flex',
Expand All @@ -105,6 +180,35 @@ export default function EmailCodeModal({
}}
>
<button
type="button"
onClick={handleResend}
style={{
padding: '10px 16px',
borderRadius: '8px',
background: 'transparent',
color: '#aaa',
border: '1px solid #333652',
cursor: resendCooldown > 0 ? 'not-allowed' : 'pointer',
opacity: resendCooldown > 0 ? 0.6 : 1,
}}
onMouseEnter={e => {
if (resendCooldown === 0) {
e.currentTarget.style.backgroundColor = '#333652'
e.currentTarget.style.color = '#ffffff'
}
}}
onMouseLeave={e => {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = '#aaa'
}}
disabled={resendCooldown > 0}
>
{resendCooldown > 0
? `${t('auth.resendVerificationCode')} (${resendCooldown}s)`
: t('auth.resendVerificationCode')}
</button>

{/* <button
type="button"
onClick={onClose}
style={{
Expand All @@ -125,7 +229,7 @@ export default function EmailCodeModal({
}}
>
{t('common.cancel')}
</button>
</button> */}

<button
type="submit"
Expand Down
Loading