@pradip1995/segment-login-template 0.4.2 → 0.5.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/segment-login-template",
3
- "version": "0.4.2",
3
+ "version": "0.5.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,7 +15,14 @@
15
15
  "./reset-password": "./src/reset-password-page.tsx",
16
16
  "./otp-verification-modal": "./src/otp-verification-modal.tsx",
17
17
  "./commerce-auth-otp-modal": "./src/commerce-auth-otp-modal.tsx",
18
- "./otp-component": "./src/otp-component.ts"
18
+ "./otp-component": "./src/otp-component.ts",
19
+ "./auth-server": "./src/auth-server.ts",
20
+ "./google-auth-section": "./src/google-auth-section.tsx",
21
+ "./otp-input": "./src/otp-input.tsx"
22
+ },
23
+ "scripts": {
24
+ "typecheck": "tsc --noEmit",
25
+ "lint": "tsc --noEmit"
19
26
  },
20
27
  "peerDependencies": {
21
28
  "@pradip1995/commerce-auth": "^4.0.0",
@@ -36,9 +43,5 @@
36
43
  "@types/react": "^19",
37
44
  "react": "19.0.3",
38
45
  "typescript": "^5.7.2"
39
- },
40
- "scripts": {
41
- "typecheck": "tsc --noEmit",
42
- "lint": "tsc --noEmit"
43
46
  }
44
- }
47
+ }
@@ -4,6 +4,7 @@ import type { HttpTypes } from "@medusajs/types"
4
4
  import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
5
5
  import { formatPrice } from "@pradip1995/segment-primitives/format-price"
6
6
  import { logoutGuest } from "@pradip1995/commerce-core/data/guest"
7
+ import { auth } from "./account-theme"
7
8
 
8
9
  export default function AccountGuestOrders({
9
10
  orders,
@@ -14,30 +15,56 @@ export default function AccountGuestOrders({
14
15
  }) {
15
16
  const safeOrders: HttpTypes.StoreOrder[] = Array.isArray(orders)
16
17
  ? orders
17
- : Array.isArray((orders as any)?.orders)
18
- ? (orders as any).orders
19
- : []
18
+ : Array.isArray((orders as { orders?: HttpTypes.StoreOrder[] })?.orders)
19
+ ? ((orders as { orders: HttpTypes.StoreOrder[] }).orders)
20
+ : []
20
21
 
21
22
  async function handleSignOut() {
22
23
  await logoutGuest()
23
24
  window.location.href = `/${countryCode}/account`
24
25
  }
25
26
 
27
+ async function handleCheckDifferent() {
28
+ await logoutGuest()
29
+ window.location.href = `/${countryCode}/account?view=guest`
30
+ }
31
+
26
32
  return (
27
33
  <div className="space-y-6">
28
34
  <div className="flex flex-wrap items-start justify-between gap-4">
29
35
  <div>
30
- <h2 className="section-heading text-xl mb-1">Guest orders</h2>
31
- <p className="text-sm text-muted">Orders linked to your verified email.</p>
36
+ <div className="flex items-center gap-2 mb-2" aria-hidden>
37
+ <span className="h-px w-6 bg-brand-accent/70" />
38
+ <span className="text-brand-accent text-[7px]">◆</span>
39
+ <span className="h-px w-6 bg-brand-accent/70" />
40
+ </div>
41
+ <h2 className="font-heading text-2xl font-bold text-heading uppercase tracking-[0.08em] mb-1">
42
+ Guest orders
43
+ </h2>
44
+ <p className="text-sm text-muted">
45
+ Track orders linked to your verified email.
46
+ </p>
47
+ </div>
48
+ <div className="flex flex-wrap gap-2">
49
+ <button
50
+ type="button"
51
+ onClick={handleCheckDifferent}
52
+ className="btn-outline text-xs"
53
+ >
54
+ Check different email
55
+ </button>
56
+ <button type="button" onClick={handleSignOut} className="btn-outline text-xs">
57
+ End guest session
58
+ </button>
32
59
  </div>
33
- <button type="button" onClick={handleSignOut} className="btn-outline text-xs">
34
- End guest session
35
- </button>
36
60
  </div>
37
61
 
38
62
  {safeOrders.length === 0 ? (
39
- <div className="card-surface rounded-2xl p-8 text-center text-sm text-muted">
40
- No guest orders found for this session.
63
+ <div className="border border-cart-border bg-surface p-8 text-center">
64
+ <p className="text-sm text-muted mb-4">No guest orders found for this session.</p>
65
+ <LocalizedLink href="/store" className={`${auth.btnPrimary} ${auth.btnShine} inline-flex`}>
66
+ <span className="relative z-[1]">Continue shopping</span>
67
+ </LocalizedLink>
41
68
  </div>
42
69
  ) : (
43
70
  <ul className="space-y-3">
@@ -45,10 +72,12 @@ export default function AccountGuestOrders({
45
72
  <li key={order.id}>
46
73
  <LocalizedLink
47
74
  href={`/orders/${order.id}`}
48
- className="card-surface rounded-2xl p-5 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
75
+ className="border border-cart-border bg-surface p-5 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors duration-200 block"
49
76
  >
50
77
  <div>
51
- <p className="font-semibold text-heading text-sm">Order #{order.display_id}</p>
78
+ <p className="font-semibold text-heading text-sm">
79
+ Order #{order.display_id}
80
+ </p>
52
81
  <p className="text-xs text-muted mt-1 capitalize">
53
82
  {order.status?.replace(/_/g, " ")}
54
83
  </p>
@@ -92,6 +92,13 @@ export default function AccountPaymentMethods({
92
92
  }
93
93
 
94
94
  async function handleDelete(id: string) {
95
+ const target = paymentDetails.find((d) => d.id === id)
96
+ if (target?.is_default && paymentDetails.length === 1) {
97
+ setError(
98
+ "The default payment method cannot be removed. Please add another payment method and set it as default before removing this one."
99
+ )
100
+ return
101
+ }
95
102
  if (!confirm("Remove this payment method?")) return
96
103
  setPending(true)
97
104
  setError(null)
@@ -1,4 +1,4 @@
1
- /** Account / order visual tokens — maps to segment-tokens CSS variables */
1
+ /** Logged-in account visual tokens — maps to segment-tokens CSS variables */
2
2
  export const acct = {
3
3
  pageBg: "bg-page-bg",
4
4
  card: "bg-surface border border-cart-border rounded-none shadow-card",
@@ -26,3 +26,66 @@ export const acct = {
26
26
  badge:
27
27
  "inline-flex items-center px-3 py-1 text-xs font-semibold border rounded-none",
28
28
  } as const
29
+
30
+ /** Auth / login visual tokens — maps to segment-tokens CSS variables (theme-safe) */
31
+ export const auth = {
32
+ viewport:
33
+ "w-full flex flex-col lg:flex-row overflow-hidden min-h-[calc(100svh-6.25rem)] lg:h-[calc(100svh-6.25rem)] lg:max-h-[calc(100svh-6.25rem)] bg-page-bg",
34
+ formColumn: "relative flex flex-1 flex-col min-h-0 w-full lg:w-1/2 bg-page-bg",
35
+ formWash:
36
+ "pointer-events-none absolute inset-y-0 -left-20 w-40 bg-gradient-to-r from-transparent to-page-bg hidden lg:block z-[1]",
37
+ formGlow:
38
+ "pointer-events-none absolute inset-0 bg-gradient-to-l from-brand-primary/[0.03] via-transparent to-transparent hidden lg:block",
39
+ formInner:
40
+ "relative z-[2] flex flex-1 flex-col justify-center min-h-0 overflow-y-auto overscroll-contain px-5 sm:px-10 md:px-12 lg:px-14 xl:px-20 py-5 sm:py-6 lg:py-8",
41
+ formCard:
42
+ "relative w-full max-w-[440px] shrink-0 border border-cart-border/60 bg-surface shadow-[0_10px_40px_-16px_rgba(0,0,0,0.12)] p-7 sm:p-8",
43
+ desktopHeader: "hidden lg:block mb-6 max-w-[440px] shrink-0",
44
+ title:
45
+ "font-heading text-2xl xl:text-[32px] font-bold text-heading uppercase tracking-[0.08em] leading-tight",
46
+ subtitle: "mt-2 text-[11px] font-medium text-muted uppercase tracking-[0.28em]",
47
+ mobileTitle:
48
+ "font-heading text-base font-bold text-heading uppercase tracking-[0.16em] lg:hidden",
49
+ fieldLabel:
50
+ "text-[10px] font-semibold text-heading/80 uppercase tracking-[0.22em]",
51
+ input:
52
+ "w-full px-0 py-3 text-sm text-heading bg-transparent border-0 border-b-2 border-cart-border rounded-none placeholder:text-muted/50 focus:outline-none focus:border-brand-primary focus:ring-0 transition-colors duration-200",
53
+ btnPrimary:
54
+ "group relative w-full py-3.5 bg-brand-primary text-inverse text-[11px] font-bold uppercase tracking-[0.28em] shadow-[0_8px_28px_-6px_rgba(0,0,0,0.25)] hover:bg-brand-accent hover:shadow-[0_12px_32px_-6px_rgba(0,0,0,0.3)] transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden active:scale-[0.98]",
55
+ btnShine:
56
+ "before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/15 before:to-transparent before:-translate-x-full hover:before:translate-x-full before:transition-transform before:duration-700",
57
+ btnOutline:
58
+ "w-full flex items-center justify-center gap-2.5 py-3 border border-brand-primary/15 bg-surface/80 hover:bg-page-bg hover:border-brand-primary/35 shadow-sm transition-all duration-200 disabled:opacity-50",
59
+ guestCta:
60
+ "w-full py-3 border border-brand-primary/20 bg-gradient-to-r from-surface to-page-bg text-heading text-[10px] font-bold uppercase tracking-[0.18em] hover:border-brand-accent hover:shadow-[0_4px_16px_-4px_rgba(0,0,0,0.15)] transition-all duration-300 flex items-center justify-center gap-2 disabled:opacity-50 active:scale-[0.98]",
61
+ tabActive:
62
+ "pb-3 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] border-b-2 -mb-px border-brand-accent text-brand-accent transition-colors duration-200",
63
+ tabIdle:
64
+ "pb-3 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] border-b-2 -mb-px border-transparent text-muted hover:text-heading transition-colors duration-200",
65
+ modeActive:
66
+ "flex-1 py-2 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] rounded-md bg-surface text-heading shadow-sm transition-colors duration-200",
67
+ modeIdle:
68
+ "flex-1 py-2 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] rounded-md text-muted hover:text-heading transition-colors duration-200",
69
+ chipActive:
70
+ "px-3 py-1.5 text-xs font-medium border border-brand-accent text-brand-accent bg-brand-accent/5 transition-colors duration-200",
71
+ chipIdle:
72
+ "px-3 py-1.5 text-xs font-medium border border-cart-border text-muted hover:text-heading transition-colors duration-200",
73
+ } as const
74
+
75
+ export type BrandFeatureCard = {
76
+ imageSrc?: string | null
77
+ title: string
78
+ text: string
79
+ }
80
+
81
+ export type BrandPanelConfig = {
82
+ image?: string | null
83
+ logo?: string | null
84
+ eyebrow?: string | null
85
+ headline?: string | null
86
+ /** Sahsha alias for headline */
87
+ title?: string | null
88
+ description?: string | null
89
+ featuresLabel?: string | null
90
+ featureCards?: BrandFeatureCard[] | null
91
+ }
@@ -112,6 +112,8 @@ export async function verifyAuthOtpAndLogin(input: {
112
112
  countryCode: string
113
113
  first_name?: string
114
114
  last_name?: string
115
+ /** When true, return success instead of redirecting (used by login popup). */
116
+ skipRedirect?: boolean
115
117
  }) {
116
118
  const baseUrl = getBaseUrl()
117
119
 
@@ -128,9 +130,17 @@ export async function verifyAuthOtpAndLogin(input: {
128
130
 
129
131
  if (!response.ok) {
130
132
  const err = await response.json().catch(() => ({}))
133
+ const raw =
134
+ (err as { message?: string }).message || "Invalid verification code"
135
+ const lower = raw.toLowerCase()
136
+ const error =
137
+ lower.includes("unable to retrieve the auth provider") &&
138
+ (lower.includes("emailotp") || lower.includes("phoneotp"))
139
+ ? "OTP sign-in is not configured on the backend. Ask the shop admin to enable emailotp/phoneotp auth providers and redeploy."
140
+ : raw.replace(/\s+/g, " ").trim()
131
141
  return {
132
142
  success: false as const,
133
- error: (err as { message?: string }).message || "Invalid verification code",
143
+ error,
134
144
  }
135
145
  }
136
146
 
@@ -151,14 +161,20 @@ export async function verifyAuthOtpAndLogin(input: {
151
161
 
152
162
  await setAuthToken(jwt)
153
163
 
154
- try {
155
- await transferCart(jwt)
156
- } catch {
157
- // non-fatal
158
- }
164
+ // Don't block login on cart merge — middleware recovers guest cart after redirect.
165
+ void transferCart(jwt).catch(() => {})
159
166
 
160
167
  const customerCacheTag = await getCacheTag("customers")
161
- revalidateTag(customerCacheTag)
168
+ if (customerCacheTag) {
169
+ revalidateTag(customerCacheTag)
170
+ }
171
+
172
+ if (input.skipRedirect) {
173
+ return {
174
+ success: true as const,
175
+ isNewUser: data.is_new_user ?? false,
176
+ }
177
+ }
162
178
 
163
179
  const countryCode = input.countryCode || "in"
164
180
  redirect(`/${countryCode}`)
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useState } from "react"
4
4
  import { checkEmailRegistered, requestPasswordReset } from "./auth-server"
5
+ import { auth } from "./account-theme"
6
+ import { AuthField, Ornament } from "./login-form"
5
7
 
6
8
  export default function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
7
9
  const [email, setEmail] = useState("")
@@ -42,13 +44,17 @@ export default function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
42
44
 
43
45
  if (success) {
44
46
  return (
45
- <div className="space-y-4 text-center py-4">
46
- <div className="w-14 h-14 mx-auto rounded-full bg-green-100 flex items-center justify-center text-2xl">
47
-
47
+ <div className="space-y-5 text-center py-4">
48
+ <Ornament />
49
+ <div className="w-14 h-14 mx-auto rounded-full bg-brand-accent/10 flex items-center justify-center text-brand-accent">
50
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" aria-hidden>
51
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
52
+ <polyline points="22,6 12,13 2,6" />
53
+ </svg>
48
54
  </div>
49
- <h2 className="section-heading text-lg">Check your email</h2>
55
+ <h2 className={`${auth.title} text-xl`}>Check your email</h2>
50
56
  <p className="text-sm text-muted">{message}</p>
51
- <button type="button" onClick={onBack} className="btn-outline w-full">
57
+ <button type="button" onClick={onBack} className={auth.btnOutline}>
52
58
  Back to sign in
53
59
  </button>
54
60
  </div>
@@ -56,31 +62,43 @@ export default function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
56
62
  }
57
63
 
58
64
  return (
59
- <form onSubmit={handleSubmit} className="space-y-4">
60
- <p className="text-sm text-muted">
61
- Enter your email and we&apos;ll send a link to reset your password.
62
- </p>
63
- <label className="block">
64
- <span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
65
- Email
66
- </span>
67
- <input
68
- name="email"
69
- type="email"
70
- value={email}
71
- onChange={(e) => setEmail(e.target.value)}
72
- required
73
- className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
74
- />
75
- </label>
76
- {error && <p className="text-sm text-brand-sale">{error}</p>}
77
- <button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
78
- {pending ? "Sending…" : "Send reset link"}
65
+ <form onSubmit={handleSubmit} className="space-y-5">
66
+ <div className="text-center lg:text-left">
67
+ <Ornament />
68
+ <h2 className={`${auth.title} text-xl lg:text-2xl`}>Reset password</h2>
69
+ <p className="mt-2 text-sm text-muted">
70
+ Enter your email and we&apos;ll send a link to reset your password.
71
+ </p>
72
+ </div>
73
+
74
+ <AuthField
75
+ label="Email"
76
+ name="email"
77
+ type="email"
78
+ value={email}
79
+ onChange={(e) => setEmail(e.target.value)}
80
+ autoComplete="email"
81
+ placeholder="you@example.com"
82
+ />
83
+
84
+ {error && (
85
+ <p className="text-sm text-brand-sale" role="alert">
86
+ {error}
87
+ </p>
88
+ )}
89
+
90
+ <button
91
+ type="submit"
92
+ disabled={pending}
93
+ className={`${auth.btnPrimary} ${auth.btnShine}`}
94
+ >
95
+ <span className="relative z-[1]">{pending ? "Sending…" : "Send reset link"}</span>
79
96
  </button>
97
+
80
98
  <button
81
99
  type="button"
82
100
  onClick={onBack}
83
- className="text-sm text-muted hover:text-brand-accent w-full text-center"
101
+ className="text-sm text-muted hover:text-brand-accent w-full text-center transition-colors"
84
102
  >
85
103
  Back to sign in
86
104
  </button>
@@ -6,9 +6,7 @@ import {
6
6
  GOOGLE_LOGIN_COUNTRY_CODE_KEY,
7
7
  } from "@pradip1995/commerce-auth/util/google-auth-client"
8
8
  import { initiateGoogleAuth } from "./auth-server"
9
-
10
- const BUTTON_CLASS =
11
- "btn-outline w-full flex items-center justify-center gap-2.5 py-3 px-4"
9
+ import { auth } from "./account-theme"
12
10
 
13
11
  export default function GoogleAuthSection({ countryCode }: { countryCode: string }) {
14
12
  const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID?.trim()
@@ -46,9 +44,9 @@ export default function GoogleAuthSection({ countryCode }: { countryCode: string
46
44
  }
47
45
 
48
46
  return (
49
- <div className="mb-6">
47
+ <div>
50
48
  {error && (
51
- <p className="mb-3 text-sm text-red-600" role="alert">
49
+ <p className="mb-3 text-sm text-brand-sale" role="alert">
52
50
  {error}
53
51
  </p>
54
52
  )}
@@ -56,10 +54,10 @@ export default function GoogleAuthSection({ countryCode }: { countryCode: string
56
54
  type="button"
57
55
  onClick={handleClick}
58
56
  disabled={loading}
59
- className={BUTTON_CLASS}
60
- style={{ borderRadius: "30px" }}
57
+ className={auth.btnOutline}
58
+ aria-label="Continue with Google"
61
59
  >
62
- <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
60
+ <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
63
61
  <path
64
62
  fill="#4285F4"
65
63
  d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
@@ -77,8 +75,8 @@ export default function GoogleAuthSection({ countryCode }: { countryCode: string
77
75
  d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
78
76
  />
79
77
  </svg>
80
- <span className="text-sm text-gray-700 font-bold">
81
- {loading ? "Connecting..." : "Continue with Google"}
78
+ <span className="text-xs text-heading font-medium tracking-wide">
79
+ {loading ? "Redirecting…" : "Continue with Google"}
82
80
  </span>
83
81
  </button>
84
82
  </div>
@@ -0,0 +1,224 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef, useState } from "react"
4
+ import { sendOTP, verifyOTP } from "./auth-server"
5
+ import OtpVerificationModal from "./otp-verification-modal"
6
+ import type { OtpVerificationComponent } from "./otp-component"
7
+ import { auth } from "./account-theme"
8
+
9
+ type GuestOrderModalProps = {
10
+ open: boolean
11
+ onClose: () => void
12
+ countryCode: string
13
+ initialEmail?: string
14
+ autoStart?: boolean
15
+ OtpComponent?: OtpVerificationComponent
16
+ }
17
+
18
+ export default function GuestOrderModal({
19
+ open,
20
+ onClose,
21
+ countryCode,
22
+ initialEmail = "",
23
+ autoStart = false,
24
+ OtpComponent = OtpVerificationModal,
25
+ }: GuestOrderModalProps) {
26
+ const [email, setEmail] = useState(initialEmail)
27
+ const [otp, setOtp] = useState("")
28
+ const [otpToken, setOtpToken] = useState<string | null>(null)
29
+ const [step, setStep] = useState<"email" | "otp">("email")
30
+ const [pending, setPending] = useState(false)
31
+ const [error, setError] = useState<string | null>(null)
32
+ const [success, setSuccess] = useState(false)
33
+ const autoStartedRef = useRef(false)
34
+
35
+ useEffect(() => {
36
+ if (initialEmail) setEmail(initialEmail)
37
+ }, [initialEmail])
38
+
39
+ useEffect(() => {
40
+ if (open && autoStart && initialEmail && !autoStartedRef.current) {
41
+ autoStartedRef.current = true
42
+ void handleSendOtp(initialEmail)
43
+ }
44
+ if (!open) {
45
+ autoStartedRef.current = false
46
+ }
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [open, autoStart, initialEmail])
49
+
50
+ async function handleSendOtp(emailToSend = email) {
51
+ if (!emailToSend.trim()) return
52
+ setPending(true)
53
+ setError(null)
54
+
55
+ try {
56
+ const res = await sendOTP(emailToSend.trim())
57
+ if (!res.success && (res as { error?: string }).error) {
58
+ throw new Error((res as { error?: string }).error)
59
+ }
60
+ setOtpToken((res as { token?: string }).token || "guest")
61
+ setOtp("")
62
+ setStep("otp")
63
+ } catch (err) {
64
+ setError(err instanceof Error ? err.message : "Failed to send code")
65
+ setStep("email")
66
+ } finally {
67
+ setPending(false)
68
+ }
69
+ }
70
+
71
+ async function handleVerifyOtp() {
72
+ if (!otp || otp.length < 6) return
73
+ setPending(true)
74
+ setError(null)
75
+
76
+ try {
77
+ const res = await verifyOTP(email.trim(), otpToken || "", otp)
78
+ if (!res.success) {
79
+ throw new Error((res as { error?: string }).error || "Invalid code")
80
+ }
81
+ // verifyOTP sets httpOnly `_medusa_guest_jwt` via the server action
82
+ setSuccess(true)
83
+ window.setTimeout(() => {
84
+ window.location.href = `/${countryCode}/account/guest-orders`
85
+ }, 900)
86
+ } catch (err) {
87
+ setError(err instanceof Error ? err.message : "Verification failed")
88
+ setPending(false)
89
+ }
90
+ }
91
+
92
+ async function handleResend() {
93
+ await handleSendOtp(email)
94
+ }
95
+
96
+ function handleClose() {
97
+ setEmail(initialEmail || "")
98
+ setOtp("")
99
+ setOtpToken(null)
100
+ setStep("email")
101
+ setError(null)
102
+ setSuccess(false)
103
+ setPending(false)
104
+ autoStartedRef.current = false
105
+ onClose()
106
+ }
107
+
108
+ if (!open) return null
109
+
110
+ if (step === "otp" || success) {
111
+ return (
112
+ <OtpComponent
113
+ open
114
+ preventClose={!success}
115
+ title={success ? "Verified" : "Verify your email"}
116
+ description={
117
+ success
118
+ ? "Opening your guest orders…"
119
+ : `Enter the 6-digit code sent to ${email || "your email"}.`
120
+ }
121
+ otp={otp}
122
+ onOtpChange={setOtp}
123
+ error={error}
124
+ pending={pending}
125
+ success={success}
126
+ successTitle="Verification successful"
127
+ successMessage="Redirecting to your guest orders…"
128
+ verifyLabel="Verify & view orders"
129
+ onVerify={handleVerifyOtp}
130
+ onResend={handleResend}
131
+ onClose={handleClose}
132
+ extraContent={
133
+ !success ? (
134
+ <button
135
+ type="button"
136
+ onClick={() => {
137
+ setStep("email")
138
+ setOtp("")
139
+ setError(null)
140
+ }}
141
+ className="text-xs text-muted hover:text-brand-accent mb-4 w-full text-center"
142
+ >
143
+ Change email
144
+ </button>
145
+ ) : undefined
146
+ }
147
+ />
148
+ )
149
+ }
150
+
151
+ return (
152
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
153
+ <button
154
+ type="button"
155
+ className="absolute inset-0 bg-black/40"
156
+ aria-label="Close guest order dialog"
157
+ onClick={handleClose}
158
+ />
159
+ <div className="relative w-full max-w-md border border-cart-border/60 bg-surface shadow-[0_10px_40px_-16px_rgba(0,0,0,0.2)] p-7 sm:p-8">
160
+ <form
161
+ onSubmit={(e) => {
162
+ e.preventDefault()
163
+ void handleSendOtp()
164
+ }}
165
+ className="space-y-5"
166
+ >
167
+ <div className="text-center">
168
+ <Ornament />
169
+ <h2 className={`${auth.title} text-xl xl:text-2xl`}>Track your order</h2>
170
+ <p className="mt-2 text-sm text-muted leading-relaxed">
171
+ Enter the email used at checkout. We&apos;ll send a one-time code to view guest
172
+ orders.
173
+ </p>
174
+ </div>
175
+
176
+ <label className="block">
177
+ <span className={`${auth.fieldLabel} mb-1.5 block`}>Email</span>
178
+ <input
179
+ type="email"
180
+ value={email}
181
+ onChange={(e) => setEmail(e.target.value)}
182
+ required
183
+ autoComplete="email"
184
+ placeholder="you@example.com"
185
+ className={auth.input}
186
+ />
187
+ </label>
188
+
189
+ {error && (
190
+ <p className="text-sm text-brand-sale" role="alert">
191
+ {error}
192
+ </p>
193
+ )}
194
+
195
+ <button
196
+ type="submit"
197
+ disabled={pending || !email.trim()}
198
+ className={`${auth.btnPrimary} ${auth.btnShine}`}
199
+ >
200
+ <span className="relative z-[1]">{pending ? "Sending…" : "Send verification code"}</span>
201
+ </button>
202
+
203
+ <button
204
+ type="button"
205
+ onClick={handleClose}
206
+ className="text-sm text-muted hover:text-brand-accent w-full text-center transition-colors"
207
+ >
208
+ Cancel
209
+ </button>
210
+ </form>
211
+ </div>
212
+ </div>
213
+ )
214
+ }
215
+
216
+ function Ornament() {
217
+ return (
218
+ <div className="flex items-center justify-center gap-2 mb-4" aria-hidden>
219
+ <span className="h-px w-8 bg-gradient-to-r from-transparent to-brand-accent" />
220
+ <span className="text-brand-accent text-[8px] leading-none">◆</span>
221
+ <span className="h-px w-8 bg-gradient-to-l from-transparent to-brand-accent" />
222
+ </div>
223
+ )
224
+ }