@pradip1995/commerce-auth 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +54 -0
- package/src/components/forgot-password/index.tsx +179 -0
- package/src/components/login/index.tsx +606 -0
- package/src/components/register/index.tsx +581 -0
- package/src/data/customer-registration.ts +365 -0
- package/src/data/customer.ts +638 -0
- package/src/data/guest.ts +357 -0
- package/src/index.ts +12 -0
- package/src/templates/login-template.tsx +44 -0
- package/src/types/account.ts +21 -0
- package/src/util-google-oauth.ts +28 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { useParams, useRouter } from "next/navigation"
|
|
5
|
+
import { LOGIN_VIEW } from "@core/types/account"
|
|
6
|
+
import ErrorMessage from "@modules/checkout/components/error-message"
|
|
7
|
+
import Envelope from "@modules/common/icons/envelope"
|
|
8
|
+
import Lock from "@modules/common/icons/lock"
|
|
9
|
+
import User from "@modules/common/icons/user"
|
|
10
|
+
import PhoneIcon from "@modules/common/icons/phone"
|
|
11
|
+
import Eye from "@modules/common/icons/eye"
|
|
12
|
+
import EyeOff from "@modules/common/icons/eye-off"
|
|
13
|
+
import Image from "next/image"
|
|
14
|
+
import { initiateGoogleAuth } from "@core/data/customer"
|
|
15
|
+
import {
|
|
16
|
+
registerCustomer,
|
|
17
|
+
sendCustomerOTP,
|
|
18
|
+
verifyCustomerOTP,
|
|
19
|
+
} from "@core/data/customer-registration"
|
|
20
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
21
|
+
import { Input, Button, Heading, Text, clx } from "@medusajs/ui"
|
|
22
|
+
import Modal from "@modules/common/components/modal"
|
|
23
|
+
import PhoneInput from "react-phone-input-2"
|
|
24
|
+
import "react-phone-input-2/lib/style.css"
|
|
25
|
+
import Spinner from "@modules/common/icons/spinner"
|
|
26
|
+
import { useServerAction } from "@core/hooks/use-server-action"
|
|
27
|
+
|
|
28
|
+
type Props = {
|
|
29
|
+
setCurrentView: (view: LOGIN_VIEW) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type RegistrationStep = "form" | "otp" | "success"
|
|
33
|
+
|
|
34
|
+
type RegistrationFormData = {
|
|
35
|
+
full_name: string
|
|
36
|
+
email: string
|
|
37
|
+
phone: string
|
|
38
|
+
password: string
|
|
39
|
+
verificationMethod: "email_verification" | "phone_verification"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Register = ({ setCurrentView }: Props) => {
|
|
43
|
+
const params = useParams()
|
|
44
|
+
const router = useRouter()
|
|
45
|
+
const [showPassword, setShowPassword] = useState(false)
|
|
46
|
+
const [googleLoading, setGoogleLoading] = useState(false)
|
|
47
|
+
const [error, setError] = useState<string | null>(null)
|
|
48
|
+
|
|
49
|
+
// Multi-step registration state
|
|
50
|
+
const [step, setStep] = useState<RegistrationStep>("form")
|
|
51
|
+
const [formData, setFormData] = useState({
|
|
52
|
+
full_name: "",
|
|
53
|
+
email: "",
|
|
54
|
+
phone: "",
|
|
55
|
+
password: "",
|
|
56
|
+
})
|
|
57
|
+
const [customerId, setCustomerId] = useState<string | null>(null)
|
|
58
|
+
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
59
|
+
const [otp, setOtp] = useState("")
|
|
60
|
+
const [showOtpModal, setShowOtpModal] = useState(false)
|
|
61
|
+
const [verificationMethod, setVerificationMethod] = useState<
|
|
62
|
+
"email_verification" | "phone_verification"
|
|
63
|
+
>("phone_verification")
|
|
64
|
+
|
|
65
|
+
const {
|
|
66
|
+
execute: submitRegistration,
|
|
67
|
+
isPending: isSubmitting,
|
|
68
|
+
error: submitError,
|
|
69
|
+
} = useServerAction(async (payload: RegistrationFormData) => {
|
|
70
|
+
const [first_name, ...last_name_parts] = payload.full_name.split(" ")
|
|
71
|
+
const last_name = last_name_parts.join(" ") || "."
|
|
72
|
+
|
|
73
|
+
if (payload.verificationMethod === "phone_verification" && !payload.phone) {
|
|
74
|
+
throw new Error("Please enter your phone number for mobile verification")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await registerCustomer({
|
|
78
|
+
email: payload.email,
|
|
79
|
+
first_name: first_name || "",
|
|
80
|
+
last_name: last_name,
|
|
81
|
+
phone: payload.phone || undefined,
|
|
82
|
+
password: payload.password,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
throw new Error(result.error || "Registration failed")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setCustomerId(result.customer?.id ?? null)
|
|
90
|
+
|
|
91
|
+
const otpResult = await sendCustomerOTP(
|
|
92
|
+
result.customer?.id,
|
|
93
|
+
payload.verificationMethod
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if (!otpResult.success) {
|
|
97
|
+
throw new Error(otpResult.error || "Failed to send verification code")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setOtpToken(otpResult.token ?? null)
|
|
101
|
+
setShowOtpModal(true)
|
|
102
|
+
setStep("otp")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const {
|
|
106
|
+
execute: verifyRegistrationOtp,
|
|
107
|
+
isPending: otpLoading,
|
|
108
|
+
error: verifyError,
|
|
109
|
+
} = useServerAction(async (payload: { token: string; otp: string }) => {
|
|
110
|
+
const result = await verifyCustomerOTP(payload.token, payload.otp)
|
|
111
|
+
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
throw new Error(result.error || "Invalid verification code")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setStep("success")
|
|
117
|
+
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
setShowOtpModal(false)
|
|
120
|
+
setCurrentView(LOGIN_VIEW.SIGN_IN)
|
|
121
|
+
}, 2000)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const {
|
|
125
|
+
execute: resendRegistrationOtp,
|
|
126
|
+
isPending: isResendingOtp,
|
|
127
|
+
error: resendError,
|
|
128
|
+
} = useServerAction(
|
|
129
|
+
async (payload: {
|
|
130
|
+
customerId: string
|
|
131
|
+
method: "email_verification" | "phone_verification"
|
|
132
|
+
}) => {
|
|
133
|
+
const result = await sendCustomerOTP(payload.customerId, payload.method)
|
|
134
|
+
|
|
135
|
+
if (!result.success) {
|
|
136
|
+
throw new Error(result.error || "Failed to resend code")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setOtpToken(result.token ?? null)
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
144
|
+
setFormData((prev) => ({
|
|
145
|
+
...prev,
|
|
146
|
+
[e.target.name]: e.target.value,
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const handleFormSubmit = (e: React.FormEvent) => {
|
|
151
|
+
e.preventDefault()
|
|
152
|
+
setError(null)
|
|
153
|
+
submitRegistration({
|
|
154
|
+
...formData,
|
|
155
|
+
verificationMethod,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const handleVerifyOtp = () => {
|
|
160
|
+
if (!otp || otp.length < 6) return
|
|
161
|
+
setError(null)
|
|
162
|
+
verifyRegistrationOtp({
|
|
163
|
+
token: otpToken || "",
|
|
164
|
+
otp,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const handleResendOtp = () => {
|
|
169
|
+
if (!customerId) return
|
|
170
|
+
setError(null)
|
|
171
|
+
resendRegistrationOtp({
|
|
172
|
+
customerId,
|
|
173
|
+
method: verificationMethod,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const actionError =
|
|
178
|
+
error || submitError?.message || verifyError?.message || resendError?.message
|
|
179
|
+
|
|
180
|
+
const closeOtpModal = () => {
|
|
181
|
+
setShowOtpModal(false)
|
|
182
|
+
setOtp("")
|
|
183
|
+
setError(null)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="w-full flex flex-col" data-testid="register-page">
|
|
188
|
+
{/* Register Title */}
|
|
189
|
+
<h1 className="text-xl min-[340px]:text-2xl min-[550px]:text-3xl font-bold text-heading mb-1 min-[340px]:mb-2">
|
|
190
|
+
Register to Chocomelon
|
|
191
|
+
</h1>
|
|
192
|
+
<p className="text-sm min-[340px]:text-base text-gray-500 mb-6 text-center min-[550px]:text-left">
|
|
193
|
+
Create an account! Please enter your details
|
|
194
|
+
</p>
|
|
195
|
+
|
|
196
|
+
{/* Google Signup at Top */}
|
|
197
|
+
<div className="mb-6">
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={async (e) => {
|
|
201
|
+
e.preventDefault()
|
|
202
|
+
e.stopPropagation()
|
|
203
|
+
try {
|
|
204
|
+
setGoogleLoading(true)
|
|
205
|
+
|
|
206
|
+
// Save current country code before redirecting to Google OAuth
|
|
207
|
+
const countryCode =
|
|
208
|
+
(params?.countryCode as string) ||
|
|
209
|
+
window.location.pathname.split("/")[1] ||
|
|
210
|
+
"us"
|
|
211
|
+
localStorage.setItem("googleLoginCountryCode", countryCode)
|
|
212
|
+
|
|
213
|
+
document.cookie =
|
|
214
|
+
"_medusa_jwt=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
215
|
+
document.cookie =
|
|
216
|
+
"_medusa_cache_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
217
|
+
|
|
218
|
+
// Use server-side action instead of direct backend call
|
|
219
|
+
const result = await initiateGoogleAuth(countryCode)
|
|
220
|
+
|
|
221
|
+
if (result.error) {
|
|
222
|
+
throw new Error(result.error)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const redirectUrl = result.redirectUrl
|
|
226
|
+
|
|
227
|
+
if (redirectUrl) {
|
|
228
|
+
window.location.href = redirectUrl
|
|
229
|
+
} else {
|
|
230
|
+
setGoogleLoading(false)
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
setGoogleLoading(false)
|
|
234
|
+
}
|
|
235
|
+
}}
|
|
236
|
+
disabled={googleLoading}
|
|
237
|
+
className="w-full flex items-center justify-center gap-2.5 py-3 px-4 border border-gray-200 bg-white hover:bg-gray-50 transition-all shadow-sm hover:shadow-md active:scale-[0.98]"
|
|
238
|
+
style={{ borderRadius: "30px" }}
|
|
239
|
+
>
|
|
240
|
+
<Image src="/Google.svg" alt="Google" width={20} height={20} />
|
|
241
|
+
<span className="text-sm text-gray-700 font-bold">
|
|
242
|
+
{googleLoading ? "Connecting..." : "Continue with Google"}
|
|
243
|
+
</span>
|
|
244
|
+
</button>
|
|
245
|
+
|
|
246
|
+
{/* Quick Link at Top */}
|
|
247
|
+
<div className="mt-4 text-center">
|
|
248
|
+
<span className="text-gray-700 text-xs min-[340px]:text-sm">
|
|
249
|
+
Already have an account?{" "}
|
|
250
|
+
<button
|
|
251
|
+
onClick={() => setCurrentView(LOGIN_VIEW.SIGN_IN)}
|
|
252
|
+
className="text-brand-accent font-bold hover:underline"
|
|
253
|
+
>
|
|
254
|
+
Login
|
|
255
|
+
</button>
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Registration Toggle */}
|
|
261
|
+
<div className="flex justify-center mb-8">
|
|
262
|
+
<div className="flex bg-[#F3F4F6] p-1.5 rounded-full w-full max-w-[420px] border border-gray-100 shadow-inner">
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
onClick={() => setVerificationMethod("phone_verification")}
|
|
266
|
+
className={clx(
|
|
267
|
+
"flex-1 py-2.5 text-sm font-bold rounded-full transition-all duration-300",
|
|
268
|
+
verificationMethod === "phone_verification"
|
|
269
|
+
? "bg-white text-brand-accent shadow-md border-2 border-black"
|
|
270
|
+
: "text-gray-400 hover:text-gray-600"
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
Mobile Registration
|
|
274
|
+
</button>
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
onClick={() => setVerificationMethod("email_verification")}
|
|
278
|
+
className={clx(
|
|
279
|
+
"flex-1 py-2.5 text-sm font-bold rounded-full transition-all duration-300",
|
|
280
|
+
verificationMethod === "email_verification"
|
|
281
|
+
? "bg-white text-brand-accent shadow-md border-2 border-black"
|
|
282
|
+
: "text-gray-400 hover:text-gray-600"
|
|
283
|
+
)}
|
|
284
|
+
>
|
|
285
|
+
Email Registration
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className="relative mb-6">
|
|
291
|
+
<div className="absolute inset-0 flex items-center">
|
|
292
|
+
<div className="w-full border-t border-gray-100"></div>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="relative flex justify-center text-xs uppercase tracking-widest">
|
|
295
|
+
<span className="px-3 bg-page-bg text-gray-400 font-bold">
|
|
296
|
+
Or sign up with
|
|
297
|
+
</span>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<form className="w-full" onSubmit={handleFormSubmit}>
|
|
302
|
+
{/* Full Name Input */}
|
|
303
|
+
<div className="mb-4">
|
|
304
|
+
<div className="relative">
|
|
305
|
+
<div className="absolute inset-y-0 left-0 pl-3 min-[340px]:pl-4 flex items-center pointer-events-none">
|
|
306
|
+
<User size="20" color="#9CA3AF" />
|
|
307
|
+
</div>
|
|
308
|
+
<input
|
|
309
|
+
type="text"
|
|
310
|
+
name="full_name"
|
|
311
|
+
placeholder="Full name"
|
|
312
|
+
autoComplete="name"
|
|
313
|
+
required
|
|
314
|
+
value={formData.full_name}
|
|
315
|
+
onChange={handleInputChange}
|
|
316
|
+
className="w-full pl-10 min-[340px]:pl-12 pr-3 min-[340px]:pr-4 py-2 min-[340px]:py-2.5 min-[550px]:py-3 text-sm min-[340px]:text-base border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-accent focus:border-transparent"
|
|
317
|
+
style={{ borderRadius: "30px" }}
|
|
318
|
+
data-testid="full-name-input"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{/* Email Input */}
|
|
324
|
+
<div className="mb-4">
|
|
325
|
+
<div className="relative">
|
|
326
|
+
<div className="absolute inset-y-0 left-0 pl-3 min-[340px]:pl-4 flex items-center pointer-events-none">
|
|
327
|
+
<Envelope size="20" color="#9CA3AF" />
|
|
328
|
+
</div>
|
|
329
|
+
<input
|
|
330
|
+
type="email"
|
|
331
|
+
name="email"
|
|
332
|
+
placeholder="Email"
|
|
333
|
+
autoComplete="email"
|
|
334
|
+
required
|
|
335
|
+
value={formData.email}
|
|
336
|
+
onChange={handleInputChange}
|
|
337
|
+
className="w-full pl-10 min-[340px]:pl-12 pr-3 min-[340px]:pr-4 py-2 min-[340px]:py-2.5 min-[550px]:py-3 text-sm min-[340px]:text-base border border-gray-200 focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent transition-all"
|
|
338
|
+
style={{ borderRadius: "30px" }}
|
|
339
|
+
data-testid="email-input"
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Phone Input */}
|
|
345
|
+
<div className="mb-4 phone-input-register">
|
|
346
|
+
<PhoneInput
|
|
347
|
+
country={"in"}
|
|
348
|
+
value={formData.phone}
|
|
349
|
+
onChange={(value) => {
|
|
350
|
+
setFormData((prev) => ({
|
|
351
|
+
...prev,
|
|
352
|
+
phone: value.startsWith("+") ? value : `+${value}`,
|
|
353
|
+
}))
|
|
354
|
+
}}
|
|
355
|
+
inputProps={{
|
|
356
|
+
name: "phone",
|
|
357
|
+
required: verificationMethod === "phone_verification",
|
|
358
|
+
autoComplete: "tel",
|
|
359
|
+
}}
|
|
360
|
+
placeholder="Mobile Number"
|
|
361
|
+
containerStyle={{
|
|
362
|
+
width: "100%",
|
|
363
|
+
}}
|
|
364
|
+
inputStyle={{
|
|
365
|
+
width: "100%",
|
|
366
|
+
height: "46px",
|
|
367
|
+
fontSize: "15px",
|
|
368
|
+
paddingLeft: "55px",
|
|
369
|
+
borderRadius: "30px",
|
|
370
|
+
border: "1px solid #d1d5db",
|
|
371
|
+
backgroundColor: "var(--color-surface)",
|
|
372
|
+
transition: "all 0.2s",
|
|
373
|
+
}}
|
|
374
|
+
buttonStyle={{
|
|
375
|
+
backgroundColor: "transparent",
|
|
376
|
+
border: "none",
|
|
377
|
+
borderRadius: "30px 0 0 30px",
|
|
378
|
+
paddingLeft: "12px",
|
|
379
|
+
}}
|
|
380
|
+
dropdownStyle={{
|
|
381
|
+
width: "280px",
|
|
382
|
+
borderRadius: "15px",
|
|
383
|
+
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
|
|
384
|
+
zIndex: 50,
|
|
385
|
+
}}
|
|
386
|
+
/>
|
|
387
|
+
<style jsx global>{`
|
|
388
|
+
.phone-input-register .react-tel-input .form-control:focus {
|
|
389
|
+
outline: none !important;
|
|
390
|
+
ring: 2px !important;
|
|
391
|
+
border-color: var(--color-brand-accent) !important;
|
|
392
|
+
box-shadow: 0 0 0 2px #f3e8ff !important;
|
|
393
|
+
}
|
|
394
|
+
`}</style>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{/* Password Input */}
|
|
398
|
+
<div className="mb-4">
|
|
399
|
+
<div className="relative">
|
|
400
|
+
<div className="absolute inset-y-0 left-0 pl-3 min-[340px]:pl-4 flex items-center pointer-events-none">
|
|
401
|
+
<Lock size="20" color="#9CA3AF" />
|
|
402
|
+
</div>
|
|
403
|
+
<input
|
|
404
|
+
type={showPassword ? "text" : "password"}
|
|
405
|
+
name="password"
|
|
406
|
+
placeholder="Password"
|
|
407
|
+
autoComplete="new-password"
|
|
408
|
+
required
|
|
409
|
+
value={formData.password}
|
|
410
|
+
onChange={handleInputChange}
|
|
411
|
+
className="w-full pl-10 min-[340px]:pl-12 pr-10 min-[340px]:pr-12 py-2 min-[340px]:py-2.5 min-[550px]:py-3 text-sm min-[340px]:text-base border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-accent focus:border-transparent"
|
|
412
|
+
style={{ borderRadius: "30px" }}
|
|
413
|
+
data-testid="password-input"
|
|
414
|
+
/>
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
418
|
+
className="absolute inset-y-0 right-0 pr-3 min-[340px]:pr-4 flex items-center text-gray-500 hover:text-gray-700"
|
|
419
|
+
>
|
|
420
|
+
{showPassword ? <EyeOff size="20" /> : <Eye size="20" />}
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
{/* Error Message */}
|
|
426
|
+
{actionError && !showOtpModal && (
|
|
427
|
+
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
428
|
+
<p className="text-red-600 text-sm">{actionError}</p>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{/* Terms and Conditions */}
|
|
433
|
+
<div className="mb-4 min-[340px]:mb-5 min-[550px]:mb-6">
|
|
434
|
+
<span className="text-center text-gray-700 text-xs min-[340px]:text-sm">
|
|
435
|
+
By creating an account, you agree to Chocomelon's{" "}
|
|
436
|
+
<LocalizedClientLink
|
|
437
|
+
href="/privacy-policy"
|
|
438
|
+
className="text-brand-accent font-semibold hover:underline"
|
|
439
|
+
>
|
|
440
|
+
Privacy Policy
|
|
441
|
+
</LocalizedClientLink>{" "}
|
|
442
|
+
and{" "}
|
|
443
|
+
<LocalizedClientLink
|
|
444
|
+
href="/terms-of-use"
|
|
445
|
+
className="text-brand-accent font-semibold hover:underline"
|
|
446
|
+
>
|
|
447
|
+
Terms of Use
|
|
448
|
+
</LocalizedClientLink>
|
|
449
|
+
.
|
|
450
|
+
</span>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* Register Button */}
|
|
454
|
+
<button
|
|
455
|
+
type="submit"
|
|
456
|
+
disabled={isSubmitting}
|
|
457
|
+
className="w-full py-2.5 min-[340px]:py-3 px-3 min-[340px]:px-4 bg-brand-accent text-sm min-[340px]:text-base text-inverse font-bold hover:bg-brand-accent-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed mb-4 min-[340px]:mb-5 min-[550px]:mb-6"
|
|
458
|
+
style={{ borderRadius: "30px" }}
|
|
459
|
+
data-testid="register-button"
|
|
460
|
+
>
|
|
461
|
+
{isSubmitting ? "Registering..." : "Register"}
|
|
462
|
+
</button>
|
|
463
|
+
</form>
|
|
464
|
+
|
|
465
|
+
{/* OTP Verification Modal */}
|
|
466
|
+
|
|
467
|
+
{/* OTP Verification Modal */}
|
|
468
|
+
<Modal isOpen={showOtpModal} close={closeOtpModal} size="small">
|
|
469
|
+
<Modal.Title>
|
|
470
|
+
<Heading className="text-xl font-bold text-center">
|
|
471
|
+
{step === "success"
|
|
472
|
+
? "Account Verified! 🎉"
|
|
473
|
+
: verificationMethod === "email_verification"
|
|
474
|
+
? "Verify Your Email"
|
|
475
|
+
: "Verify Your Mobile"}
|
|
476
|
+
</Heading>
|
|
477
|
+
</Modal.Title>
|
|
478
|
+
<Modal.Body>
|
|
479
|
+
<div className="flex flex-col gap-4 py-4">
|
|
480
|
+
{step === "success" ? (
|
|
481
|
+
<div className="flex flex-col items-center text-center py-6 animate-in zoom-in duration-300">
|
|
482
|
+
<div className="w-20 h-20 bg-green-50 rounded-full flex items-center justify-center mb-6 shadow-sm border border-green-100">
|
|
483
|
+
<svg
|
|
484
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
485
|
+
width="32"
|
|
486
|
+
height="32"
|
|
487
|
+
viewBox="0 0 24 24"
|
|
488
|
+
fill="none"
|
|
489
|
+
stroke="#10b981"
|
|
490
|
+
strokeWidth="3"
|
|
491
|
+
strokeLinecap="round"
|
|
492
|
+
strokeLinejoin="round"
|
|
493
|
+
>
|
|
494
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
495
|
+
</svg>
|
|
496
|
+
</div>
|
|
497
|
+
<Heading level="h2" className="text-2xl font-bold text-heading mb-2">
|
|
498
|
+
Account Verified! 🎉
|
|
499
|
+
</Heading>
|
|
500
|
+
<Text className="text-gray-600">
|
|
501
|
+
Your account has been verified successfully!
|
|
502
|
+
</Text>
|
|
503
|
+
<div className="flex items-center gap-3 mt-8 bg-gray-50 px-4 py-2 rounded-full border border-gray-100">
|
|
504
|
+
<Spinner className="animate-spin text-brand-accent" />
|
|
505
|
+
<Text className="text-gray-500 text-sm font-medium">
|
|
506
|
+
Redirecting to login...
|
|
507
|
+
</Text>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
) : (
|
|
511
|
+
<>
|
|
512
|
+
<Text className="text-gray-500 text-center text-sm">
|
|
513
|
+
We've sent a 6-digit verification code to{" "}
|
|
514
|
+
<strong>
|
|
515
|
+
{verificationMethod === "email_verification"
|
|
516
|
+
? formData.email
|
|
517
|
+
: formData.phone}
|
|
518
|
+
</strong>
|
|
519
|
+
</Text>
|
|
520
|
+
|
|
521
|
+
<div className="relative flex justify-center gap-x-2.5 mt-8">
|
|
522
|
+
{[0, 1, 2, 3, 4, 5].map((index) => (
|
|
523
|
+
<div
|
|
524
|
+
key={index}
|
|
525
|
+
className={`w-10 h-12 flex items-center justify-center border-2 rounded-lg text-xl font-bold transition-all duration-200 ${
|
|
526
|
+
otp.length === index
|
|
527
|
+
? "border-brand-accent ring-2 ring-brand-accent/10 bg-page-bg"
|
|
528
|
+
: otp.length > index
|
|
529
|
+
? "border-gray-400 bg-page-bg shadow-sm"
|
|
530
|
+
: "border-gray-200 bg-gray-50/50"
|
|
531
|
+
}`}
|
|
532
|
+
>
|
|
533
|
+
{otp[index] || ""}
|
|
534
|
+
</div>
|
|
535
|
+
))}
|
|
536
|
+
<input
|
|
537
|
+
type="text"
|
|
538
|
+
maxLength={6}
|
|
539
|
+
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
540
|
+
value={otp}
|
|
541
|
+
onChange={(e) =>
|
|
542
|
+
setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))
|
|
543
|
+
}
|
|
544
|
+
autoFocus
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
{actionError && (
|
|
549
|
+
<Text className="text-red-500 text-sm text-center">
|
|
550
|
+
{actionError}
|
|
551
|
+
</Text>
|
|
552
|
+
)}
|
|
553
|
+
|
|
554
|
+
<Button
|
|
555
|
+
className="w-full bg-brand-accent hover:bg-brand-accent-hover text-inverse"
|
|
556
|
+
onClick={handleVerifyOtp}
|
|
557
|
+
isLoading={otpLoading || isResendingOtp}
|
|
558
|
+
disabled={otp.length < 6 || otpLoading || isResendingOtp}
|
|
559
|
+
>
|
|
560
|
+
Verify & Continue
|
|
561
|
+
</Button>
|
|
562
|
+
|
|
563
|
+
<div className="text-center">
|
|
564
|
+
<button
|
|
565
|
+
onClick={handleResendOtp}
|
|
566
|
+
disabled={otpLoading || isResendingOtp}
|
|
567
|
+
className="text-xs text-gray-500 underline hover:text-gray-700 disabled:opacity-50"
|
|
568
|
+
>
|
|
569
|
+
Didn't receive code? Resend
|
|
570
|
+
</button>
|
|
571
|
+
</div>
|
|
572
|
+
</>
|
|
573
|
+
)}
|
|
574
|
+
</div>
|
|
575
|
+
</Modal.Body>
|
|
576
|
+
</Modal>
|
|
577
|
+
</div>
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export default Register
|