Skip to content

Commit e594202

Browse files
author
linzhengyu
committed
Merge remote-tracking branch 'origin/main' into lzy-dev
2 parents 7a9cf25 + 847e2c5 commit e594202

File tree

20 files changed

+416
-90
lines changed

20 files changed

+416
-90
lines changed

app/components/auth/Authenticator.tsx

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
useErrorNotification,
2222
} from '@/hooks/useGlobalNotification'
2323
import { useTranslation } from 'react-i18next'
24+
import GloTooltip from '@/components/common/GlobalTooltip'
2425
/**
2526
* Props interface for the Authenticator component.
2627
*/
@@ -32,14 +33,13 @@ interface AuthenticatorProps {
3233
}
3334

3435
/*
35-
Authenticator Component
36-
37-
A React component that provides user authentication with email and password input, including
38-
registration, verification code flow, and login.
39-
40-
@param onAuthSuccess Type: () => void. Callback invoked on successful authentication.
41-
42-
@returns JSX.Element The authenticator UI.
36+
Authenticator component.
37+
38+
Provides user authentication UI and flow including login, registration, and email verification code.
39+
40+
@param onAuthSuccess Function. Callback invoked after a successful authentication flow. No default.
41+
42+
@returns JSX.Element The rendered authenticator UI.
4343
*/
4444
export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
4545
const [email, setEmail] = useState('')
@@ -56,7 +56,18 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
5656
const { showErrorNotification } = useErrorNotification()
5757
const [codeErrorMessage, setCodeErrorMessage] = useState('')
5858
const { t, i18n } = useTranslation()
59-
59+
const [needCoolDown, setNeedCoolDown] = useState(false)
60+
/*
61+
Get current geolocation with a timeout guard.
62+
63+
Resolves with latitude and longitude if available, otherwise rejects on timeout or geolocation error.
64+
65+
@param options PositionOptions. Options passed to navigator.geolocation.getCurrentPosition. Default timeout is 5000 ms if not provided.
66+
67+
@returns Promise<{ latitude: number; longitude: number }> The resolved location coordinates.
68+
69+
@throws {Error} When the location request times out or geolocation reports an error.
70+
*/
6071
const getCurrentPositionAsync = (options: PositionOptions) => {
6172
return new Promise((resolve, reject) => {
6273
let resolved = false
@@ -88,12 +99,12 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
8899
})
89100
}
90101
/*
91-
Verify user credentials and handle registration or login.
92-
93-
@param email Type: string. The user's email address.
94-
@param password Type: string. The user's password.
95-
96-
@returns Promise<void> Resolves when the flow completes.
102+
Verify user credentials and handle registration or login based on the active tab.
103+
104+
@param email string. The user's email address.
105+
@param password string. The user's password.
106+
107+
@returns Promise<void> Resolves when the verification/login flow is completed.
97108
*/
98109
const loginVerify = async (email: string, password: string) => {
99110
setIsLoading(true)
@@ -109,6 +120,7 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
109120
setIsLoading(false)
110121
if (response.auth_code === 200) {
111122
setNeedCode(response.confirmation_required)
123+
setNeedCoolDown(response.confirmation_required)
112124
if (!response.confirmation_required) {
113125
showSuccessNotification('Registration Successful!')
114126
setActiveTab('login')
@@ -202,11 +214,11 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
202214
}
203215

204216
/*
205-
Handle form submission for user authentication.
206-
207-
@param e Type: React.FormEvent. The form submit event.
208-
209-
@returns Promise<void> Resolves after submit handling is complete.
217+
Handle form submission for user authentication (login or register).
218+
219+
@param e React.FormEvent. The form submit event.
220+
221+
@returns Promise<void> Resolves after submit handling completes.
210222
*/
211223
const handleSubmit = async (e: React.FormEvent) => {
212224
e.preventDefault()
@@ -231,10 +243,10 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
231243

232244
/*
233245
Switch between login and register tabs.
234-
235-
@param event Type: React.SyntheticEvent. The tab change event.
236-
@param value Type: 'login' | 'register'. The target tab value.
237-
246+
247+
@param event React.SyntheticEvent. The tab change event.
248+
@param value 'login' | 'register'. The target tab value to activate.
249+
238250
@returns void
239251
*/
240252
const handleTabChange = (
@@ -244,11 +256,11 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
244256
setActiveTab(value)
245257
}
246258
/*
247-
Submit the verification code in register flow.
248-
249-
@param inputCode Type: string. The 6-digit verification code.
250-
251-
@returns Promise<void> Resolves when the action completes.
259+
Submit the verification code during the registration flow.
260+
261+
@param inputCode string. The 6-digit verification code provided by the user.
262+
263+
@returns Promise<void> Resolves when the verification handling completes.
252264
*/
253265
const handleCodeSubmit = async (inputCode: string) => {
254266
setCodeErrorMessage('')
@@ -288,6 +300,20 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
288300
}
289301
}
290302

303+
/*
304+
Request sending or resending a verification code to the provided email.
305+
306+
No action is taken if the email field is empty.
307+
308+
@returns Promise<void> Resolves after the resend request completes.
309+
*/
310+
const handleNeedCode = async () => {
311+
if (!email) {
312+
return
313+
}
314+
setNeedCode(true)
315+
}
316+
291317
const buttonText = (() => {
292318
if (activeTab === 'login') {
293319
return isLoading ? t('auth.signingIn') : t('auth.signIn')
@@ -504,7 +530,24 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
504530
</button>
505531
</div>
506532
{/** code */}
507-
533+
<div
534+
style={{
535+
display: 'flex',
536+
alignItems: 'center',
537+
justifyContent: 'end',
538+
color: '#fff',
539+
fontSize: '14px',
540+
fontWeight: '500',
541+
marginBottom: '16px',
542+
textAlign: 'left',
543+
cursor: 'pointer',
544+
}}
545+
onClick={handleNeedCode}
546+
>
547+
<GloTooltip content={t('auth.enterTheVerificationCodeDescription')}>
548+
<div>{t('auth.needToEnterTheVerificationCode')}</div>
549+
</GloTooltip>
550+
</div>
508551
{/* Sign In Button */}
509552
<button
510553
type="submit"
@@ -543,6 +586,7 @@ export default function Authenticator({ onAuthSuccess }: AuthenticatorProps) {
543586
onSubmit={handleCodeSubmit}
544587
isSubmitting={isLoading}
545588
errorMessage={codeErrorMessage}
589+
needCoolDown={needCoolDown}
546590
/>
547591
</div>
548592
)

app/components/auth/EmailCodeModal.tsx

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,114 @@
33
import React, { useEffect, useState } from 'react'
44
import { Dialog } from '@/components/common/Dialog'
55
import { useTranslation } from 'react-i18next'
6-
6+
import GloTooltip from '@/components/common/GlobalTooltip'
7+
import { fetchResendConfirmationCode } from '@/request/api'
8+
import {
9+
useSuccessNotification,
10+
useErrorNotification,
11+
} from '@/hooks/useGlobalNotification'
12+
/*
13+
Props for the EmailCodeModal component.
14+
15+
Displays and manages the email verification code modal including code input,
16+
resend logic with cooldown, and submission handling.
17+
*/
718
interface EmailCodeModalProps {
819
isOpen: boolean
9-
email?: string
20+
email: string
1021
onClose: () => void
1122
onSubmit?: (code: string) => void | Promise<void>
1223
onResend?: () => void | Promise<void>
1324
isSubmitting?: boolean
1425
errorMessage?: string
26+
needCoolDown?: boolean
1527
}
28+
/*
29+
EmailCodeModal component.
30+
31+
Renders a dialog to enter a 6-digit email verification code and supports resending
32+
the code with a cooldown period.
33+
34+
@param isOpen boolean. Whether the dialog is open.
35+
@param email string. The email address to which the code was sent.
36+
@param onClose Function. Called when the dialog should close.
37+
@param onSubmit Function | Promise<void>. Optional submit handler for the entered code. No default.
38+
@param onResend Function | Promise<void>. Optional external resend handler (unused here). No default.
39+
@param isSubmitting boolean. Whether submission is in progress. Default: false.
40+
@param errorMessage string | undefined. Optional error message to display. No default.
41+
@param needCoolDown boolean. If true, initializes resend cooldown. Default: false.
42+
43+
@returns JSX.Element The verification code dialog UI.
44+
*/
1645
export default function EmailCodeModal({
1746
isOpen,
1847
email,
1948
onClose,
2049
onSubmit,
2150
isSubmitting = false,
2251
errorMessage,
52+
needCoolDown = false,
2353
}: EmailCodeModalProps) {
24-
/*
25-
Handle verification code form submission.
26-
27-
@param e React.FormEvent - The form submit event.
28-
29-
@returns Promise<void> Resolves when the submit completes.
30-
*/
54+
const { showSuccessNotification } = useSuccessNotification()
55+
const { showErrorNotification } = useErrorNotification()
3156
const [code, setCode] = useState('')
32-
const { t } = useTranslation()
57+
const { t, i18n } = useTranslation()
3358
useEffect(() => {
3459
if (isOpen) {
3560
setCode('')
61+
if (needCoolDown) {
62+
setResendCooldown(60)
63+
}
3664
}
3765
}, [isOpen])
3866

67+
/*
68+
Handle verification code form submission.
69+
70+
@param e React.FormEvent. The form submit event.
71+
72+
@returns Promise<void> Resolves when the submit completes.
73+
*/
3974
const handleSubmit = async (e: React.FormEvent) => {
4075
e.preventDefault()
4176
if (!code.trim()) return
4277
await onSubmit?.(code.trim())
4378
}
4479

80+
const [resendCooldown, setResendCooldown] = useState(0)
81+
useEffect(() => {
82+
if (resendCooldown <= 0) return
83+
const timer = setTimeout(() => setResendCooldown(prev => prev - 1), 1000)
84+
return () => clearTimeout(timer)
85+
}, [resendCooldown])
86+
87+
/*
88+
Resend a verification code and start/reset the cooldown timer.
89+
90+
No action is taken if the cooldown is still active.
91+
92+
@returns Promise<void> Resolves after the resend attempt completes.
93+
*/
94+
const handleResend = async () => {
95+
if (resendCooldown > 0) return
96+
const response = await fetchResendConfirmationCode(email, i18n.language)
97+
if (response.auth_code === 200) {
98+
showSuccessNotification(t('notification.verificationCodeResentSuccessfully'))
99+
setResendCooldown(60)
100+
} else {
101+
showErrorNotification(response.auth_msg)
102+
}
103+
}
104+
45105
return (
46106
<Dialog
47107
isOpen={isOpen}
48108
onClose={onClose}
49109
title={t('auth.emailVerificationCode')}
50110
maxWidth="420px"
51111
className="email-code-dialog"
112+
closeOnEscape={false}
113+
closeOnBackdropClick={false}
52114
>
53115
<form
54116
onSubmit={handleSubmit}
@@ -61,7 +123,23 @@ export default function EmailCodeModal({
61123
</span>{' '}
62124
{t('auth.pleaseEnterThe6DigitCodeInTheEmailToCompleteTheVerification')}
63125
</div>
64-
126+
<div
127+
style={{
128+
display: 'flex',
129+
alignItems: 'center',
130+
justifyContent: 'end',
131+
color: '#fff',
132+
fontSize: '14px',
133+
fontWeight: '500',
134+
marginBottom: '16px',
135+
textAlign: 'left',
136+
cursor: 'pointer',
137+
}}
138+
>
139+
<GloTooltip content={t('auth.resendVerificationCodeDescription')}>
140+
<div>{t('auth.didNotReceiveTheVerificationCode')}</div>
141+
</GloTooltip>
142+
</div>
65143
<label style={{ color: '#8b8ea8', fontSize: '14px' }}>
66144
{t('auth.verificationCode')}
67145
</label>
@@ -74,7 +152,6 @@ export default function EmailCodeModal({
74152
onChange={e => setCode(e.target.value)}
75153
placeholder={t('auth.enterTheVerificationCode')}
76154
style={{
77-
width: '100%',
78155
height: '48px',
79156
padding: '0 14px',
80157
backgroundColor: 'transparent',
@@ -91,11 +168,9 @@ export default function EmailCodeModal({
91168
e.currentTarget.style.borderColor = '#333652'
92169
}}
93170
/>
94-
95171
{errorMessage ? (
96172
<div style={{ color: '#ff6b6b', fontSize: '13px' }}>{errorMessage}</div>
97173
) : null}
98-
99174
<div
100175
style={{
101176
display: 'flex',
@@ -105,6 +180,35 @@ export default function EmailCodeModal({
105180
}}
106181
>
107182
<button
183+
type="button"
184+
onClick={handleResend}
185+
style={{
186+
padding: '10px 16px',
187+
borderRadius: '8px',
188+
background: 'transparent',
189+
color: '#aaa',
190+
border: '1px solid #333652',
191+
cursor: resendCooldown > 0 ? 'not-allowed' : 'pointer',
192+
opacity: resendCooldown > 0 ? 0.6 : 1,
193+
}}
194+
onMouseEnter={e => {
195+
if (resendCooldown === 0) {
196+
e.currentTarget.style.backgroundColor = '#333652'
197+
e.currentTarget.style.color = '#ffffff'
198+
}
199+
}}
200+
onMouseLeave={e => {
201+
e.currentTarget.style.backgroundColor = 'transparent'
202+
e.currentTarget.style.color = '#aaa'
203+
}}
204+
disabled={resendCooldown > 0}
205+
>
206+
{resendCooldown > 0
207+
? `${t('auth.resendVerificationCode')} (${resendCooldown}s)`
208+
: t('auth.resendVerificationCode')}
209+
</button>
210+
211+
{/* <button
108212
type="button"
109213
onClick={onClose}
110214
style={{
@@ -125,7 +229,7 @@ export default function EmailCodeModal({
125229
}}
126230
>
127231
{t('common.cancel')}
128-
</button>
232+
</button> */}
129233

130234
<button
131235
type="submit"

0 commit comments

Comments
 (0)