@pradip1995/commerce-auth 3.0.0 → 3.0.2
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/package.json +7 -2
- package/src/components/guest-order-modal/index.tsx +131 -0
- package/src/components/login/index.tsx +6 -213
- package/src/components/otp-input/index.tsx +57 -0
- package/src/components/otp-verification-modal/index.tsx +94 -0
- package/src/components/register/index.tsx +3 -25
- package/src/hooks/use-customer-otp.ts +90 -0
- package/src/hooks/use-guest-otp.ts +125 -0
- package/src/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pradip1995/commerce-auth",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Medusa storefront authentication — login, register, OTP, Google OAuth",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
"./components/google-auth-shell": "./src/components/google-auth-shell/index.tsx",
|
|
34
34
|
"./components/google-identity-script": "./src/components/google-identity-script/index.tsx",
|
|
35
35
|
"./components/google-one-tap": "./src/components/google-one-tap/index.tsx",
|
|
36
|
-
"./components/google-credential-button": "./src/components/google-credential-button/index.tsx"
|
|
36
|
+
"./components/google-credential-button": "./src/components/google-credential-button/index.tsx",
|
|
37
|
+
"./components/otp-input": "./src/components/otp-input/index.tsx",
|
|
38
|
+
"./components/guest-order-modal": "./src/components/guest-order-modal/index.tsx",
|
|
39
|
+
"./components/otp-verification-modal": "./src/components/otp-verification-modal/index.tsx",
|
|
40
|
+
"./hooks/use-guest-otp": "./src/hooks/use-guest-otp.ts",
|
|
41
|
+
"./hooks/use-customer-otp": "./src/hooks/use-customer-otp.ts"
|
|
37
42
|
},
|
|
38
43
|
"peerDependencies": {
|
|
39
44
|
"@pradip1995/commerce-core": "^3.0.0",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Envelope from "@modules/common/icons/envelope"
|
|
4
|
+
import Spinner from "@modules/common/icons/spinner"
|
|
5
|
+
import Modal from "@modules/common/components/modal"
|
|
6
|
+
import OtpInput from "../otp-input"
|
|
7
|
+
import { useGuestOtp } from "../../hooks/use-guest-otp"
|
|
8
|
+
|
|
9
|
+
type GuestOrderModalProps = {
|
|
10
|
+
isOpen: boolean
|
|
11
|
+
close: () => void
|
|
12
|
+
onVerified: () => void
|
|
13
|
+
initialEmail?: string
|
|
14
|
+
autoStart?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function GuestOrderModal({
|
|
18
|
+
isOpen,
|
|
19
|
+
close,
|
|
20
|
+
onVerified,
|
|
21
|
+
initialEmail = "",
|
|
22
|
+
autoStart = false,
|
|
23
|
+
}: GuestOrderModalProps) {
|
|
24
|
+
const {
|
|
25
|
+
email,
|
|
26
|
+
setEmail,
|
|
27
|
+
otp,
|
|
28
|
+
setOtp,
|
|
29
|
+
step,
|
|
30
|
+
setStep,
|
|
31
|
+
isLoading,
|
|
32
|
+
error,
|
|
33
|
+
success,
|
|
34
|
+
sendGuestOtp,
|
|
35
|
+
verifyGuestOtp,
|
|
36
|
+
reset,
|
|
37
|
+
} = useGuestOtp({ initialEmail, autoStart, isOpen })
|
|
38
|
+
|
|
39
|
+
const handleClose = () => {
|
|
40
|
+
reset()
|
|
41
|
+
close()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleVerify = async () => {
|
|
45
|
+
const token = await verifyGuestOtp()
|
|
46
|
+
if (token) {
|
|
47
|
+
onVerified()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Modal isOpen={isOpen} close={handleClose} size="small">
|
|
53
|
+
<Modal.Body>
|
|
54
|
+
<div className="flex flex-col gap-6 py-4 px-2">
|
|
55
|
+
<div className="text-center">
|
|
56
|
+
<h2 className="text-2xl font-bold text-heading mb-2">
|
|
57
|
+
{step === "email" ? "Track Your Order" : "Verify OTP"}
|
|
58
|
+
</h2>
|
|
59
|
+
<p className="text-sm text-gray-500 leading-relaxed max-w-[300px] mx-auto">
|
|
60
|
+
{step === "email"
|
|
61
|
+
? "Enter your email address to track your guest orders. We will send you a One-Time Password (OTP)."
|
|
62
|
+
: `Enter the 6-digit code sent to ${email}`}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{success ? (
|
|
67
|
+
<div className="flex flex-col items-center gap-4 py-6">
|
|
68
|
+
<div className="text-brand-accent animate-pulse font-bold">
|
|
69
|
+
Verification Successful!
|
|
70
|
+
</div>
|
|
71
|
+
<Spinner />
|
|
72
|
+
</div>
|
|
73
|
+
) : step === "email" ? (
|
|
74
|
+
<div className="space-y-4">
|
|
75
|
+
<div className="relative">
|
|
76
|
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
|
|
77
|
+
<Envelope size="20" />
|
|
78
|
+
</div>
|
|
79
|
+
<input
|
|
80
|
+
placeholder="Email"
|
|
81
|
+
value={email}
|
|
82
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
83
|
+
autoComplete="email"
|
|
84
|
+
type="email"
|
|
85
|
+
required
|
|
86
|
+
className="w-full pl-12 pr-4 py-3 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-accent transition-all text-sm"
|
|
87
|
+
style={{ borderRadius: "30px" }}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
|
92
|
+
|
|
93
|
+
<button
|
|
94
|
+
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:bg-brand-accent-hover transition-colors flex justify-center items-center gap-2"
|
|
95
|
+
style={{ borderRadius: "30px" }}
|
|
96
|
+
onClick={() => void sendGuestOtp()}
|
|
97
|
+
disabled={isLoading || !email}
|
|
98
|
+
>
|
|
99
|
+
{isLoading ? <Spinner size={16} /> : "Send OTP"}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
) : (
|
|
103
|
+
<div className="space-y-4">
|
|
104
|
+
<OtpInput value={otp} onChange={setOtp} autoFocus />
|
|
105
|
+
|
|
106
|
+
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
|
107
|
+
|
|
108
|
+
<div className="space-y-3">
|
|
109
|
+
<button
|
|
110
|
+
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:bg-brand-accent-hover transition-colors flex justify-center items-center gap-2"
|
|
111
|
+
style={{ borderRadius: "30px" }}
|
|
112
|
+
onClick={() => void handleVerify()}
|
|
113
|
+
disabled={isLoading || otp.length < 6}
|
|
114
|
+
>
|
|
115
|
+
{isLoading ? <Spinner size={16} /> : "Verify & View Orders"}
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => setStep("email")}
|
|
120
|
+
className="text-xs text-gray-500 hover:text-brand-accent w-full text-center underline font-medium"
|
|
121
|
+
>
|
|
122
|
+
Change Email
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</Modal.Body>
|
|
129
|
+
</Modal>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -13,10 +13,8 @@ import { useRouter, useParams, useSearchParams } from "next/navigation"
|
|
|
13
13
|
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
14
14
|
import { useServerAction } from "@core/hooks/use-server-action"
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import Modal from "@modules/common/components/modal"
|
|
19
|
-
import { signout } from "@core/data/customer"
|
|
16
|
+
import { listGuestOrders } from "@core/data/guest"
|
|
17
|
+
import GuestOrderModal from "../guest-order-modal"
|
|
20
18
|
import DeletionPendingModal from "@modules/account/components/deletion-pending-modal"
|
|
21
19
|
import Spinner from "@modules/common/icons/spinner"
|
|
22
20
|
|
|
@@ -340,220 +338,15 @@ const Login = ({ setCurrentView }: Props) => {
|
|
|
340
338
|
<GuestOrderModal
|
|
341
339
|
isOpen={showGuestModal}
|
|
342
340
|
close={() => setShowGuestModal(false)}
|
|
343
|
-
router={router}
|
|
344
|
-
params={params}
|
|
345
341
|
initialEmail={searchParams.get("email") || ""}
|
|
346
342
|
autoStart={searchParams.get("view") === "guest" && !!searchParams.get("email")}
|
|
343
|
+
onVerified={() => {
|
|
344
|
+
const countryCode = (params?.countryCode as string) || "in"
|
|
345
|
+
router.push(`/${countryCode}/guest-orders`)
|
|
346
|
+
}}
|
|
347
347
|
/>
|
|
348
348
|
</div>
|
|
349
349
|
)
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
const GuestOrderModal = ({
|
|
353
|
-
isOpen,
|
|
354
|
-
close,
|
|
355
|
-
router,
|
|
356
|
-
params,
|
|
357
|
-
initialEmail = "",
|
|
358
|
-
autoStart = false,
|
|
359
|
-
}: {
|
|
360
|
-
isOpen: boolean
|
|
361
|
-
close: () => void
|
|
362
|
-
router: any
|
|
363
|
-
params: any
|
|
364
|
-
initialEmail?: string
|
|
365
|
-
autoStart?: boolean
|
|
366
|
-
}) => {
|
|
367
|
-
const [email, setEmail] = useState(initialEmail)
|
|
368
|
-
const [otp, setOtp] = useState("")
|
|
369
|
-
const [step, setStep] = useState<"email" | "otp">("email")
|
|
370
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
371
|
-
const [error, setError] = useState<string | null>(null)
|
|
372
|
-
const [verificationToken, setVerificationToken] = useState<string | null>(null)
|
|
373
|
-
const [success, setSuccess] = useState(false)
|
|
374
|
-
const autoStartedRef = useRef(false)
|
|
375
|
-
|
|
376
|
-
useEffect(() => {
|
|
377
|
-
if (initialEmail) setEmail(initialEmail)
|
|
378
|
-
}, [initialEmail])
|
|
379
|
-
|
|
380
|
-
useEffect(() => {
|
|
381
|
-
// If autoStart is true and we have an email, trigger OTP send immediately
|
|
382
|
-
if (isOpen && autoStart && initialEmail && !autoStartedRef.current) {
|
|
383
|
-
autoStartedRef.current = true
|
|
384
|
-
setIsLoading(true)
|
|
385
|
-
handleSendOtp(initialEmail)
|
|
386
|
-
}
|
|
387
|
-
}, [isOpen, autoStart, initialEmail])
|
|
388
|
-
|
|
389
|
-
const handleSendOtp = async (emailToSend = email) => {
|
|
390
|
-
if (!emailToSend) return
|
|
391
|
-
setIsLoading(true)
|
|
392
|
-
setError(null)
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const res = (await sendOTP(emailToSend, "email_verification")) as any
|
|
396
|
-
if (res.token || res.success !== false) {
|
|
397
|
-
setVerificationToken(res.token || "temp-token")
|
|
398
|
-
setStep("otp")
|
|
399
|
-
} else {
|
|
400
|
-
setError((res as any).error || "Failed to send OTP")
|
|
401
|
-
setStep("email")
|
|
402
|
-
}
|
|
403
|
-
} catch (e: any) {
|
|
404
|
-
setError(e.message)
|
|
405
|
-
setStep("email")
|
|
406
|
-
} finally {
|
|
407
|
-
setIsLoading(false)
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const handleVerifyOtp = async () => {
|
|
412
|
-
if (!otp) return
|
|
413
|
-
setIsLoading(true)
|
|
414
|
-
setError(null)
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
// Pass email as customerId since it's a guest flow
|
|
418
|
-
const res = (await verifyOTP(email, verificationToken || "", otp)) as any
|
|
419
|
-
if (res.success !== false) {
|
|
420
|
-
// Assuming success if no explicit error field
|
|
421
|
-
setSuccess(true)
|
|
422
|
-
|
|
423
|
-
// Store token in cookie
|
|
424
|
-
// Note: For a real app, use a server action to set httpOnly cookie.
|
|
425
|
-
// Here we simulate for client-side usage or basic auth header.
|
|
426
|
-
const token = res.token || "guest_access_token"
|
|
427
|
-
document.cookie = `_medusa_guest_token=${token}; path=/; max-age=3600; SameSite=Lax`
|
|
428
|
-
|
|
429
|
-
// Redirect to guest order view
|
|
430
|
-
const countryCode = (params?.countryCode as string) || "in"
|
|
431
|
-
router.push(`/${countryCode}/guest-orders`)
|
|
432
|
-
} else {
|
|
433
|
-
setError(res.error || "Invalid OTP")
|
|
434
|
-
}
|
|
435
|
-
} catch (e: any) {
|
|
436
|
-
setError(e.message)
|
|
437
|
-
} finally {
|
|
438
|
-
setIsLoading(false)
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const reset = () => {
|
|
443
|
-
setEmail("")
|
|
444
|
-
setOtp("")
|
|
445
|
-
setStep("email")
|
|
446
|
-
setError(null)
|
|
447
|
-
setSuccess(false)
|
|
448
|
-
close()
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return (
|
|
452
|
-
<Modal isOpen={isOpen} close={reset} size="small">
|
|
453
|
-
<Modal.Body>
|
|
454
|
-
<div className="flex flex-col gap-6 py-4 px-2">
|
|
455
|
-
{/* Header */}
|
|
456
|
-
<div className="text-center">
|
|
457
|
-
<h2 className="text-2xl font-bold text-heading mb-2">
|
|
458
|
-
{step === "email" ? "Track Your Order" : "Verify OTP"}
|
|
459
|
-
</h2>
|
|
460
|
-
<p className="text-sm text-gray-500 leading-relaxed max-w-[300px] mx-auto">
|
|
461
|
-
{step === "email"
|
|
462
|
-
? "Enter your email address to track your guest orders. We will send you a One-Time Password (OTP)."
|
|
463
|
-
: `Enter the 6-digit code sent to ${email}`}
|
|
464
|
-
</p>
|
|
465
|
-
</div>
|
|
466
|
-
|
|
467
|
-
{success ? (
|
|
468
|
-
<div className="flex flex-col items-center gap-4 py-6">
|
|
469
|
-
<div className="text-brand-accent animate-pulse font-bold">
|
|
470
|
-
Verification Successful!
|
|
471
|
-
</div>
|
|
472
|
-
<Spinner />
|
|
473
|
-
</div>
|
|
474
|
-
) : step === "email" ? (
|
|
475
|
-
<div className="space-y-4">
|
|
476
|
-
<div className="relative">
|
|
477
|
-
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
|
|
478
|
-
<Envelope size="20" />
|
|
479
|
-
</div>
|
|
480
|
-
<input
|
|
481
|
-
placeholder="Email"
|
|
482
|
-
value={email}
|
|
483
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
484
|
-
autoComplete="email"
|
|
485
|
-
type="email"
|
|
486
|
-
required
|
|
487
|
-
className="w-full pl-12 pr-4 py-3 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-accent transition-all text-sm"
|
|
488
|
-
style={{ borderRadius: "30px" }}
|
|
489
|
-
/>
|
|
490
|
-
</div>
|
|
491
|
-
|
|
492
|
-
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
|
493
|
-
|
|
494
|
-
<button
|
|
495
|
-
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:bg-brand-accent-hover transition-colors flex justify-center items-center gap-2"
|
|
496
|
-
style={{ borderRadius: "30px" }}
|
|
497
|
-
onClick={() => handleSendOtp()}
|
|
498
|
-
disabled={isLoading || !email}
|
|
499
|
-
>
|
|
500
|
-
{isLoading ? <Spinner size={16} /> : "Send OTP"}
|
|
501
|
-
</button>
|
|
502
|
-
</div>
|
|
503
|
-
) : (
|
|
504
|
-
<div className="space-y-4">
|
|
505
|
-
<div className="relative flex justify-center gap-x-2.5 mt-6">
|
|
506
|
-
{[0, 1, 2, 3, 4, 5].map((index) => (
|
|
507
|
-
<div
|
|
508
|
-
key={index}
|
|
509
|
-
className={`w-10 h-12 flex items-center justify-center border-2 rounded-lg text-xl font-bold transition-all duration-200 ${
|
|
510
|
-
otp.length === index
|
|
511
|
-
? "border-brand-accent ring-2 ring-brand-accent/10 bg-page-bg"
|
|
512
|
-
: otp.length > index
|
|
513
|
-
? "border-gray-400 bg-page-bg shadow-sm"
|
|
514
|
-
: "border-gray-200 bg-gray-50/50"
|
|
515
|
-
}`}
|
|
516
|
-
>
|
|
517
|
-
{otp[index] || ""}
|
|
518
|
-
</div>
|
|
519
|
-
))}
|
|
520
|
-
<input
|
|
521
|
-
type="text"
|
|
522
|
-
maxLength={6}
|
|
523
|
-
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
524
|
-
value={otp}
|
|
525
|
-
onChange={(e) =>
|
|
526
|
-
setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))
|
|
527
|
-
}
|
|
528
|
-
autoFocus
|
|
529
|
-
/>
|
|
530
|
-
</div>
|
|
531
|
-
|
|
532
|
-
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
|
533
|
-
|
|
534
|
-
<div className="space-y-3">
|
|
535
|
-
<button
|
|
536
|
-
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:bg-brand-accent-hover transition-colors flex justify-center items-center gap-2"
|
|
537
|
-
style={{ borderRadius: "30px" }}
|
|
538
|
-
onClick={handleVerifyOtp}
|
|
539
|
-
disabled={isLoading || otp.length < 6}
|
|
540
|
-
>
|
|
541
|
-
{isLoading ? <Spinner size={16} /> : "Verify & View Orders"}
|
|
542
|
-
</button>
|
|
543
|
-
|
|
544
|
-
<button
|
|
545
|
-
onClick={() => setStep("email")}
|
|
546
|
-
className="text-xs text-gray-500 hover:text-brand-accent w-full text-center underline font-medium"
|
|
547
|
-
>
|
|
548
|
-
Change Email
|
|
549
|
-
</button>
|
|
550
|
-
</div>
|
|
551
|
-
</div>
|
|
552
|
-
)}
|
|
553
|
-
</div>
|
|
554
|
-
</Modal.Body>
|
|
555
|
-
</Modal>
|
|
556
|
-
)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
352
|
export default Login
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
type OtpInputProps = {
|
|
4
|
+
value: string
|
|
5
|
+
onChange: (value: string) => void
|
|
6
|
+
length?: number
|
|
7
|
+
autoFocus?: boolean
|
|
8
|
+
activeClassName?: string
|
|
9
|
+
filledClassName?: string
|
|
10
|
+
emptyClassName?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_ACTIVE =
|
|
14
|
+
"border-brand-accent ring-2 ring-brand-accent/10 bg-page-bg"
|
|
15
|
+
const DEFAULT_FILLED = "border-gray-400 bg-page-bg shadow-sm"
|
|
16
|
+
const DEFAULT_EMPTY = "border-gray-200 bg-gray-50/50"
|
|
17
|
+
|
|
18
|
+
export default function OtpInput({
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
length = 6,
|
|
22
|
+
autoFocus = false,
|
|
23
|
+
activeClassName = DEFAULT_ACTIVE,
|
|
24
|
+
filledClassName = DEFAULT_FILLED,
|
|
25
|
+
emptyClassName = DEFAULT_EMPTY,
|
|
26
|
+
}: OtpInputProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="relative flex justify-center gap-x-2.5">
|
|
29
|
+
{Array.from({ length }, (_, index) => (
|
|
30
|
+
<div
|
|
31
|
+
key={index}
|
|
32
|
+
className={`w-10 h-12 flex items-center justify-center border-2 rounded-lg text-xl font-bold transition-all duration-200 ${
|
|
33
|
+
value.length === index
|
|
34
|
+
? activeClassName
|
|
35
|
+
: value.length > index
|
|
36
|
+
? filledClassName
|
|
37
|
+
: emptyClassName
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
{value[index] || ""}
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
<input
|
|
44
|
+
type="text"
|
|
45
|
+
inputMode="numeric"
|
|
46
|
+
maxLength={length}
|
|
47
|
+
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
48
|
+
value={value}
|
|
49
|
+
onChange={(event) =>
|
|
50
|
+
onChange(event.target.value.replace(/\D/g, "").slice(0, length))
|
|
51
|
+
}
|
|
52
|
+
autoFocus={autoFocus}
|
|
53
|
+
aria-label="One-time password"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Heading, Text, Button, clx } from "@medusajs/ui"
|
|
4
|
+
import Modal from "@modules/common/components/modal"
|
|
5
|
+
import OtpInput from "../otp-input"
|
|
6
|
+
|
|
7
|
+
type OtpVerificationModalProps = {
|
|
8
|
+
isOpen: boolean
|
|
9
|
+
close: () => void
|
|
10
|
+
title?: string
|
|
11
|
+
description: string
|
|
12
|
+
otp: string
|
|
13
|
+
onOtpChange: (value: string) => void
|
|
14
|
+
onVerify: () => void
|
|
15
|
+
onResend?: () => void
|
|
16
|
+
isLoading?: boolean
|
|
17
|
+
error?: string | null
|
|
18
|
+
success?: boolean
|
|
19
|
+
successMessage?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function OtpVerificationModal({
|
|
23
|
+
isOpen,
|
|
24
|
+
close,
|
|
25
|
+
title = "Verify OTP",
|
|
26
|
+
description,
|
|
27
|
+
otp,
|
|
28
|
+
onOtpChange,
|
|
29
|
+
onVerify,
|
|
30
|
+
onResend,
|
|
31
|
+
isLoading = false,
|
|
32
|
+
error,
|
|
33
|
+
success = false,
|
|
34
|
+
successMessage = "Verification successful!",
|
|
35
|
+
}: OtpVerificationModalProps) {
|
|
36
|
+
return (
|
|
37
|
+
<Modal isOpen={isOpen} close={close} size="small">
|
|
38
|
+
<Modal.Body>
|
|
39
|
+
<div className="flex flex-col gap-6 py-4 px-2">
|
|
40
|
+
<div className="text-center">
|
|
41
|
+
<Heading level="h2" className="text-2xl font-bold text-heading mb-2">
|
|
42
|
+
{title}
|
|
43
|
+
</Heading>
|
|
44
|
+
<Text className="text-sm text-gray-500 leading-relaxed max-w-[320px] mx-auto">
|
|
45
|
+
{description}
|
|
46
|
+
</Text>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{success ? (
|
|
50
|
+
<div className="text-center py-6">
|
|
51
|
+
<Text className="text-brand-accent font-bold animate-pulse">
|
|
52
|
+
{successMessage}
|
|
53
|
+
</Text>
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<>
|
|
57
|
+
<OtpInput value={otp} onChange={onOtpChange} autoFocus />
|
|
58
|
+
|
|
59
|
+
{error && (
|
|
60
|
+
<Text className="text-red-500 text-xs text-center">{error}</Text>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<div className="space-y-3">
|
|
64
|
+
<Button
|
|
65
|
+
onClick={onVerify}
|
|
66
|
+
isLoading={isLoading}
|
|
67
|
+
disabled={otp.length < 6}
|
|
68
|
+
className={clx(
|
|
69
|
+
"w-full py-3 bg-brand-accent text-inverse font-bold hover:bg-brand-accent-hover",
|
|
70
|
+
"transition-colors"
|
|
71
|
+
)}
|
|
72
|
+
style={{ borderRadius: "30px" }}
|
|
73
|
+
>
|
|
74
|
+
Verify
|
|
75
|
+
</Button>
|
|
76
|
+
|
|
77
|
+
{onResend && (
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={onResend}
|
|
81
|
+
disabled={isLoading}
|
|
82
|
+
className="text-xs text-gray-500 hover:text-brand-accent w-full text-center underline font-medium"
|
|
83
|
+
>
|
|
84
|
+
Resend code
|
|
85
|
+
</button>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</Modal.Body>
|
|
92
|
+
</Modal>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
20
20
|
import { Input, Button, Heading, Text, clx } from "@medusajs/ui"
|
|
21
21
|
import Modal from "@modules/common/components/modal"
|
|
22
|
+
import OtpInput from "../otp-input"
|
|
22
23
|
import PhoneInput from "react-phone-input-2"
|
|
23
24
|
import "react-phone-input-2/lib/style.css"
|
|
24
25
|
import Spinner from "@modules/common/icons/spinner"
|
|
@@ -470,31 +471,8 @@ const Register = ({ setCurrentView }: Props) => {
|
|
|
470
471
|
</strong>
|
|
471
472
|
</Text>
|
|
472
473
|
|
|
473
|
-
<div className="
|
|
474
|
-
{
|
|
475
|
-
<div
|
|
476
|
-
key={index}
|
|
477
|
-
className={`w-10 h-12 flex items-center justify-center border-2 rounded-lg text-xl font-bold transition-all duration-200 ${
|
|
478
|
-
otp.length === index
|
|
479
|
-
? "border-brand-accent ring-2 ring-brand-accent/10 bg-page-bg"
|
|
480
|
-
: otp.length > index
|
|
481
|
-
? "border-gray-400 bg-page-bg shadow-sm"
|
|
482
|
-
: "border-gray-200 bg-gray-50/50"
|
|
483
|
-
}`}
|
|
484
|
-
>
|
|
485
|
-
{otp[index] || ""}
|
|
486
|
-
</div>
|
|
487
|
-
))}
|
|
488
|
-
<input
|
|
489
|
-
type="text"
|
|
490
|
-
maxLength={6}
|
|
491
|
-
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
492
|
-
value={otp}
|
|
493
|
-
onChange={(e) =>
|
|
494
|
-
setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))
|
|
495
|
-
}
|
|
496
|
-
autoFocus
|
|
497
|
-
/>
|
|
474
|
+
<div className="mt-8">
|
|
475
|
+
<OtpInput value={otp} onChange={setOtp} autoFocus />
|
|
498
476
|
</div>
|
|
499
477
|
|
|
500
478
|
{actionError && (
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react"
|
|
4
|
+
import { sendCustomerOTP, verifyCustomerOTP } from "@core/data/customer-registration"
|
|
5
|
+
|
|
6
|
+
type OtpVerificationType = "email_verification" | "phone_verification"
|
|
7
|
+
|
|
8
|
+
export function useCustomerOtpVerification(customerId?: string | null) {
|
|
9
|
+
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
10
|
+
const [otp, setOtp] = useState("")
|
|
11
|
+
const [isSending, setIsSending] = useState(false)
|
|
12
|
+
const [isVerifying, setIsVerifying] = useState(false)
|
|
13
|
+
const [error, setError] = useState<string | null>(null)
|
|
14
|
+
|
|
15
|
+
const sendOtp = useCallback(
|
|
16
|
+
async (type: OtpVerificationType = "phone_verification") => {
|
|
17
|
+
if (!customerId) {
|
|
18
|
+
setError("Customer account is not ready yet.")
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setIsSending(true)
|
|
23
|
+
setError(null)
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result = await sendCustomerOTP(customerId, type)
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
setError(result.error || "Failed to send OTP")
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setOtpToken(result.token ?? null)
|
|
33
|
+
return true
|
|
34
|
+
} catch (err: unknown) {
|
|
35
|
+
setError(err instanceof Error ? err.message : "Failed to send OTP")
|
|
36
|
+
return false
|
|
37
|
+
} finally {
|
|
38
|
+
setIsSending(false)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
[customerId]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const verifyOtp = useCallback(async () => {
|
|
45
|
+
if (!otpToken || otp.length < 6) {
|
|
46
|
+
setError("Please enter the 6-digit code.")
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setIsVerifying(true)
|
|
51
|
+
setError(null)
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await verifyCustomerOTP(otpToken, otp)
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
setError(result.error || "Invalid OTP")
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
setError(err instanceof Error ? err.message : "Failed to verify OTP")
|
|
63
|
+
return false
|
|
64
|
+
} finally {
|
|
65
|
+
setIsVerifying(false)
|
|
66
|
+
}
|
|
67
|
+
}, [otp, otpToken])
|
|
68
|
+
|
|
69
|
+
const reset = useCallback(() => {
|
|
70
|
+
setOtp("")
|
|
71
|
+
setOtpToken(null)
|
|
72
|
+
setError(null)
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
otp,
|
|
77
|
+
setOtp,
|
|
78
|
+
otpToken,
|
|
79
|
+
isSending,
|
|
80
|
+
isVerifying,
|
|
81
|
+
isLoading: isSending || isVerifying,
|
|
82
|
+
error,
|
|
83
|
+
sendOtp,
|
|
84
|
+
verifyOtp,
|
|
85
|
+
reset,
|
|
86
|
+
setError,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type { OtpVerificationType }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
4
|
+
import { sendOTP, verifyOTP } from "@core/data/guest"
|
|
5
|
+
|
|
6
|
+
type UseGuestOtpOptions = {
|
|
7
|
+
initialEmail?: string
|
|
8
|
+
autoStart?: boolean
|
|
9
|
+
isOpen?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useGuestOtp({
|
|
13
|
+
initialEmail = "",
|
|
14
|
+
autoStart = false,
|
|
15
|
+
isOpen = false,
|
|
16
|
+
}: UseGuestOtpOptions = {}) {
|
|
17
|
+
const [email, setEmail] = useState(initialEmail)
|
|
18
|
+
const [otp, setOtp] = useState("")
|
|
19
|
+
const [step, setStep] = useState<"email" | "otp">("email")
|
|
20
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
21
|
+
const [error, setError] = useState<string | null>(null)
|
|
22
|
+
const [verificationToken, setVerificationToken] = useState<string | null>(null)
|
|
23
|
+
const [success, setSuccess] = useState(false)
|
|
24
|
+
const autoStartedRef = useRef(false)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (initialEmail) {
|
|
28
|
+
setEmail(initialEmail)
|
|
29
|
+
}
|
|
30
|
+
}, [initialEmail])
|
|
31
|
+
|
|
32
|
+
const reset = useCallback(() => {
|
|
33
|
+
setEmail(initialEmail)
|
|
34
|
+
setOtp("")
|
|
35
|
+
setStep("email")
|
|
36
|
+
setError(null)
|
|
37
|
+
setSuccess(false)
|
|
38
|
+
setVerificationToken(null)
|
|
39
|
+
autoStartedRef.current = false
|
|
40
|
+
}, [initialEmail])
|
|
41
|
+
|
|
42
|
+
const sendGuestOtp = useCallback(async (emailToSend = email) => {
|
|
43
|
+
if (!emailToSend) return false
|
|
44
|
+
|
|
45
|
+
setIsLoading(true)
|
|
46
|
+
setError(null)
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const res = (await sendOTP(emailToSend, "email_verification")) as {
|
|
50
|
+
token?: string
|
|
51
|
+
success?: boolean
|
|
52
|
+
error?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (res.token || res.success !== false) {
|
|
56
|
+
setVerificationToken(res.token || "temp-token")
|
|
57
|
+
setStep("otp")
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setError(res.error || "Failed to send OTP")
|
|
62
|
+
setStep("email")
|
|
63
|
+
return false
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
setError(err instanceof Error ? err.message : "Failed to send OTP")
|
|
66
|
+
setStep("email")
|
|
67
|
+
return false
|
|
68
|
+
} finally {
|
|
69
|
+
setIsLoading(false)
|
|
70
|
+
}
|
|
71
|
+
}, [email])
|
|
72
|
+
|
|
73
|
+
const verifyGuestOtp = useCallback(async () => {
|
|
74
|
+
if (!otp) return null
|
|
75
|
+
|
|
76
|
+
setIsLoading(true)
|
|
77
|
+
setError(null)
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const res = (await verifyOTP(email, verificationToken || "", otp)) as {
|
|
81
|
+
success?: boolean
|
|
82
|
+
token?: string
|
|
83
|
+
error?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (res.success !== false) {
|
|
87
|
+
setSuccess(true)
|
|
88
|
+
const token = res.token || "guest_access_token"
|
|
89
|
+
document.cookie = `_medusa_guest_token=${token}; path=/; max-age=3600; SameSite=Lax`
|
|
90
|
+
return token
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setError(res.error || "Invalid OTP")
|
|
94
|
+
return null
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
setError(err instanceof Error ? err.message : "Failed to verify OTP")
|
|
97
|
+
return null
|
|
98
|
+
} finally {
|
|
99
|
+
setIsLoading(false)
|
|
100
|
+
}
|
|
101
|
+
}, [email, otp, verificationToken])
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (isOpen && autoStart && initialEmail && !autoStartedRef.current) {
|
|
105
|
+
autoStartedRef.current = true
|
|
106
|
+
setIsLoading(true)
|
|
107
|
+
void sendGuestOtp(initialEmail)
|
|
108
|
+
}
|
|
109
|
+
}, [isOpen, autoStart, initialEmail, sendGuestOtp])
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
email,
|
|
113
|
+
setEmail,
|
|
114
|
+
otp,
|
|
115
|
+
setOtp,
|
|
116
|
+
step,
|
|
117
|
+
setStep,
|
|
118
|
+
isLoading,
|
|
119
|
+
error,
|
|
120
|
+
success,
|
|
121
|
+
sendGuestOtp,
|
|
122
|
+
verifyGuestOtp,
|
|
123
|
+
reset,
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,12 @@ export type {
|
|
|
9
9
|
export { default as Login } from "./components/login"
|
|
10
10
|
export { default as Register } from "./components/register"
|
|
11
11
|
export { default as ForgotPassword } from "./components/forgot-password"
|
|
12
|
+
export { default as OtpInput } from "./components/otp-input"
|
|
13
|
+
export { default as GuestOrderModal } from "./components/guest-order-modal"
|
|
14
|
+
export { default as OtpVerificationModal } from "./components/otp-verification-modal"
|
|
15
|
+
export { useGuestOtp } from "./hooks/use-guest-otp"
|
|
16
|
+
export { useCustomerOtpVerification } from "./hooks/use-customer-otp"
|
|
17
|
+
export type { OtpVerificationType } from "./hooks/use-customer-otp"
|
|
12
18
|
export { default as LoginTemplate } from "./templates/login-template"
|
|
13
19
|
|
|
14
20
|
export {
|