@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/commerce-auth",
3
- "version": "3.0.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 { sendOTP, verifyOTP, listGuestOrders } from "@core/data/guest"
17
- import { Button, Input, Heading, Text, clx } from "@medusajs/ui"
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="relative flex justify-center gap-x-2.5 mt-8">
474
- {[0, 1, 2, 3, 4, 5].map((index) => (
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 {