@pradip1995/segment-login-template 0.4.1 → 0.4.3

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.
@@ -0,0 +1,214 @@
1
+ "use server"
2
+
3
+ import { getAuthHeaders } from "@pradip1995/commerce-core/data/cookies"
4
+
5
+ type PaymentMethodType = "upi" | "bank" | "card"
6
+
7
+ type ActionResult =
8
+ | { success: true }
9
+ | { success: false; error: string; field_errors?: Record<string, string> }
10
+
11
+ function getBaseUrl() {
12
+ return process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
13
+ }
14
+
15
+ function getPublishableKey() {
16
+ return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ""
17
+ }
18
+
19
+ function trimAll(detail_json: Record<string, string>): Record<string, string> {
20
+ const out: Record<string, string> = {}
21
+ for (const [key, value] of Object.entries(detail_json)) {
22
+ if (typeof value !== "string") continue
23
+ const trimmed = value.trim()
24
+ if (!trimmed) continue
25
+ out[key] = key === "ifsc" ? trimmed.toUpperCase() : trimmed
26
+ }
27
+ return out
28
+ }
29
+
30
+ function validatePaymentDetail(
31
+ type: PaymentMethodType,
32
+ detail_json: Record<string, string>
33
+ ): Record<string, string> {
34
+ const field_errors: Record<string, string> = {}
35
+
36
+ if (type === "upi") {
37
+ const upi = detail_json.upi_id || ""
38
+ if (!upi) {
39
+ field_errors.upi_id = "UPI ID is required"
40
+ } else if (!/^[\w.\-]{2,256}@[a-zA-Z]{2,64}$/.test(upi)) {
41
+ field_errors.upi_id = "Enter a valid UPI ID (e.g. name@upi)"
42
+ }
43
+ }
44
+
45
+ if (type === "bank") {
46
+ const holder = detail_json.account_holder_name || ""
47
+ const bank = detail_json.bank_name || ""
48
+ const account = detail_json.account_number || ""
49
+ const ifsc = detail_json.ifsc || ""
50
+
51
+ if (!holder) {
52
+ field_errors.account_holder_name = "Account holder name is required"
53
+ }
54
+ if (!bank) {
55
+ field_errors.bank_name = "Bank name is required"
56
+ }
57
+ if (!account) {
58
+ field_errors.account_number = "Account number is required"
59
+ } else if (!/^\d{9,18}$/.test(account)) {
60
+ field_errors.account_number = "Account number must be 9–18 digits"
61
+ }
62
+ if (!ifsc) {
63
+ field_errors.ifsc = "IFSC is required"
64
+ } else if (!/^[A-Z]{4}0[A-Z0-9]{6}$/.test(ifsc)) {
65
+ field_errors.ifsc =
66
+ "Enter a valid IFSC (e.g. HDFC0001234) — 4 letters, 0, then 6 characters"
67
+ }
68
+ }
69
+
70
+ if (type === "card") {
71
+ const holder = detail_json.card_holder_name || ""
72
+ const number = detail_json.card_number || ""
73
+ const expiry = detail_json.expiry_date || ""
74
+ const cvv = detail_json.cvv || ""
75
+
76
+ if (!holder) {
77
+ field_errors.card_holder_name = "Card holder name is required"
78
+ }
79
+ if (!number) {
80
+ field_errors.card_number = "Card number is required"
81
+ } else if (!/^\d{13,19}$/.test(number.replace(/\s+/g, ""))) {
82
+ field_errors.card_number = "Enter a valid card number (13–19 digits)"
83
+ }
84
+ if (!expiry) {
85
+ field_errors.expiry_date = "Expiry date is required"
86
+ } else if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(expiry)) {
87
+ field_errors.expiry_date = "Use MM/YY format (e.g. 08/28)"
88
+ }
89
+ if (!cvv) {
90
+ field_errors.cvv = "CVV is required"
91
+ } else if (!/^\d{3,4}$/.test(cvv)) {
92
+ field_errors.cvv = "CVV must be 3 or 4 digits"
93
+ }
94
+ }
95
+
96
+ return field_errors
97
+ }
98
+
99
+ function firstFieldError(field_errors: Record<string, string>): string {
100
+ return Object.values(field_errors)[0] || "Invalid payment details"
101
+ }
102
+
103
+ function parseBackendError(body: unknown, status: number): string {
104
+ if (!body || typeof body !== "object") {
105
+ return status === 400
106
+ ? "Invalid payment details. Check the entered values."
107
+ : `Request failed (${status})`
108
+ }
109
+
110
+ const data = body as {
111
+ message?: unknown
112
+ errors?: Array<{ message?: string; path?: string[]; field?: string }>
113
+ }
114
+
115
+ if (Array.isArray(data.errors) && data.errors.length > 0) {
116
+ const details = data.errors
117
+ .map((item) => {
118
+ const path = item.path?.join(".") || item.field
119
+ const msg = item.message?.trim()
120
+ if (path && msg) return `${path}: ${msg}`
121
+ return msg
122
+ })
123
+ .filter((msg): msg is string => Boolean(msg))
124
+ if (details.length) return details.join("; ")
125
+ }
126
+
127
+ if (typeof data.message === "string" && data.message.trim()) {
128
+ const msg = data.message.trim()
129
+ if (/^invalid request( data)?\.?$/i.test(msg)) {
130
+ return "Invalid payment details. Check required fields and try again."
131
+ }
132
+ return msg
133
+ }
134
+
135
+ return `Request failed (${status})`
136
+ }
137
+
138
+ async function paymentDetailsFetch(
139
+ path: string,
140
+ init: { method: string; body?: Record<string, unknown> }
141
+ ): Promise<ActionResult> {
142
+ const authHeaders = await getAuthHeaders()
143
+ if (!("authorization" in authHeaders)) {
144
+ return { success: false, error: "Please sign in to manage payment methods" }
145
+ }
146
+
147
+ const response = await fetch(`${getBaseUrl()}${path}`, {
148
+ method: init.method,
149
+ headers: {
150
+ Accept: "application/json",
151
+ "Content-Type": "application/json",
152
+ "x-publishable-api-key": getPublishableKey(),
153
+ ...authHeaders,
154
+ },
155
+ body: init.body ? JSON.stringify(init.body) : undefined,
156
+ cache: "no-store",
157
+ })
158
+
159
+ if (response.ok) {
160
+ return { success: true }
161
+ }
162
+
163
+ const body = await response.json().catch(() => null)
164
+ return {
165
+ success: false,
166
+ error: parseBackendError(body, response.status),
167
+ }
168
+ }
169
+
170
+ export async function addPaymentMethodAction(
171
+ type: PaymentMethodType,
172
+ detail_json: Record<string, string>
173
+ ): Promise<ActionResult> {
174
+ const normalized = trimAll(detail_json)
175
+ if (normalized.card_number) {
176
+ normalized.card_number = normalized.card_number.replace(/\s+/g, "")
177
+ }
178
+
179
+ const field_errors = validatePaymentDetail(type, normalized)
180
+ if (Object.keys(field_errors).length > 0) {
181
+ return {
182
+ success: false,
183
+ error: firstFieldError(field_errors),
184
+ field_errors,
185
+ }
186
+ }
187
+
188
+ return paymentDetailsFetch("/store/payment-details", {
189
+ method: "POST",
190
+ body: { type, detail_json: normalized },
191
+ })
192
+ }
193
+
194
+ export async function makeDefaultPaymentMethodAction(
195
+ id: string
196
+ ): Promise<ActionResult> {
197
+ if (!id) {
198
+ return { success: false, error: "Payment method id is required" }
199
+ }
200
+ return paymentDetailsFetch(`/store/payment-details/${id}/make-default`, {
201
+ method: "POST",
202
+ })
203
+ }
204
+
205
+ export async function deletePaymentMethodAction(
206
+ id: string
207
+ ): Promise<ActionResult> {
208
+ if (!id) {
209
+ return { success: false, error: "Payment method id is required" }
210
+ }
211
+ return paymentDetailsFetch(`/store/payment-details/${id}`, {
212
+ method: "DELETE",
213
+ })
214
+ }
@@ -9,6 +9,8 @@ import {
9
9
  import GoogleAuthSection from "./google-auth-section"
10
10
  import OtpVerificationModal from "./otp-verification-modal"
11
11
  import type { OtpVerificationComponent } from "./otp-component"
12
+ import { auth } from "./account-theme"
13
+ import { AuthDivider, AuthField, Ornament } from "./login-form"
12
14
 
13
15
  type VerificationMethod = "email_verification" | "phone_verification"
14
16
  type RegistrationStep = "form" | "otp" | "success"
@@ -37,6 +39,7 @@ export default function RegisterForm({
37
39
  const [error, setError] = useState<string | null>(null)
38
40
  const [pending, setPending] = useState(false)
39
41
  const [showOtpModal, setShowOtpModal] = useState(false)
42
+ const [showPassword, setShowPassword] = useState(false)
40
43
 
41
44
  function updateField(key: keyof typeof form, value: string) {
42
45
  setForm((prev) => ({ ...prev, [key]: value }))
@@ -143,21 +146,22 @@ export default function RegisterForm({
143
146
  verificationMethod === "email_verification" ? form.email : form.phone
144
147
 
145
148
  return (
146
- <div className="space-y-6">
149
+ <div className="space-y-6" data-testid="register-page">
150
+ <div className="text-center lg:hidden">
151
+ <Ornament />
152
+ <h2 className={auth.mobileTitle}>Create account</h2>
153
+ <p className="mt-2 text-[10px] text-muted uppercase tracking-[0.24em]">
154
+ Join our collection
155
+ </p>
156
+ </div>
157
+
147
158
  <GoogleAuthSection countryCode={countryCode} />
148
159
 
149
- <div className="relative">
150
- <div className="absolute inset-0 flex items-center">
151
- <div className="w-full border-t border-cart-border" />
152
- </div>
153
- <div className="relative flex justify-center text-xs uppercase tracking-[var(--letter-spacing-nav)]">
154
- <span className="bg-page-bg px-3 text-muted">Or register with email</span>
155
- </div>
156
- </div>
160
+ <AuthDivider label="Or register with email" />
157
161
 
158
162
  {step === "form" && (
159
163
  <>
160
- <div className="flex gap-2 p-1 bg-surface-muted rounded-lg">
164
+ <div className="flex gap-2 p-1 bg-surface-muted/80">
161
165
  <VerificationToggle
162
166
  active={verificationMethod === "email_verification"}
163
167
  onClick={() => setVerificationMethod("email_verification")}
@@ -179,39 +183,67 @@ export default function RegisterForm({
179
183
  </p>
180
184
 
181
185
  <form onSubmit={handleRegister} className="space-y-4">
182
- <Field
186
+ <AuthField
183
187
  label="Full name"
184
188
  value={form.full_name}
185
189
  onChange={(e) => updateField("full_name", e.target.value)}
186
190
  autoComplete="name"
191
+ placeholder="Your full name"
187
192
  />
188
- <Field
193
+ <AuthField
189
194
  label="Email"
190
195
  type="email"
191
196
  value={form.email}
192
197
  onChange={(e) => updateField("email", e.target.value)}
193
198
  autoComplete="email"
199
+ placeholder="you@example.com"
194
200
  />
195
- <Field
201
+ <AuthField
196
202
  label="Phone"
197
203
  type="tel"
198
204
  value={form.phone}
199
205
  onChange={(e) => updateField("phone", e.target.value)}
200
206
  autoComplete="tel"
201
207
  required={verificationMethod === "phone_verification"}
208
+ placeholder="Phone number"
202
209
  />
203
- <Field
204
- label="Password"
205
- type="password"
206
- value={form.password}
207
- onChange={(e) => updateField("password", e.target.value)}
208
- autoComplete="new-password"
209
- />
210
-
211
- {error && <p className="text-sm text-brand-sale">{error}</p>}
212
-
213
- <button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
214
- {pending ? "Creating account…" : "Create account"}
210
+ <div>
211
+ <span className={`${auth.fieldLabel} mb-1.5 block`}>Password</span>
212
+ <div className="relative">
213
+ <input
214
+ type={showPassword ? "text" : "password"}
215
+ value={form.password}
216
+ onChange={(e) => updateField("password", e.target.value)}
217
+ autoComplete="new-password"
218
+ required
219
+ placeholder="Create a password"
220
+ className={`${auth.input} pr-10`}
221
+ />
222
+ <button
223
+ type="button"
224
+ onClick={() => setShowPassword((v) => !v)}
225
+ className="absolute inset-y-0 right-0 flex items-center text-muted hover:text-heading transition-colors"
226
+ aria-label={showPassword ? "Hide password" : "Show password"}
227
+ >
228
+ {showPassword ? <EyeOffIcon /> : <EyeIcon />}
229
+ </button>
230
+ </div>
231
+ </div>
232
+
233
+ {error && (
234
+ <p className="text-sm text-brand-sale" role="alert">
235
+ {error}
236
+ </p>
237
+ )}
238
+
239
+ <button
240
+ type="submit"
241
+ disabled={pending}
242
+ className={`${auth.btnPrimary} ${auth.btnShine}`}
243
+ >
244
+ <span className="relative z-[1]">
245
+ {pending ? "Creating account…" : "Create account"}
246
+ </span>
215
247
  </button>
216
248
  </form>
217
249
  </>
@@ -225,7 +257,11 @@ export default function RegisterForm({
225
257
 
226
258
  <p className="text-sm text-center text-muted">
227
259
  Already have an account?{" "}
228
- <button type="button" onClick={onLogin} className="text-brand-accent font-semibold hover:underline">
260
+ <button
261
+ type="button"
262
+ onClick={onLogin}
263
+ className="text-heading font-semibold hover:text-brand-accent transition-colors underline underline-offset-2"
264
+ >
229
265
  Sign in
230
266
  </button>
231
267
  </p>
@@ -282,43 +318,27 @@ function VerificationToggle({
282
318
  <button
283
319
  type="button"
284
320
  onClick={onClick}
285
- className={`flex-1 py-2 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] rounded-md transition-colors ${
286
- active ? "bg-page-bg text-heading shadow-sm" : "text-muted hover:text-heading"
287
- }`}
321
+ className={active ? auth.modeActive : auth.modeIdle}
288
322
  >
289
323
  {children}
290
324
  </button>
291
325
  )
292
326
  }
293
327
 
294
- function Field({
295
- label,
296
- type = "text",
297
- value,
298
- onChange,
299
- autoComplete,
300
- required = true,
301
- }: {
302
- label: string
303
- type?: string
304
- value: string
305
- onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
306
- autoComplete?: string
307
- required?: boolean
308
- }) {
328
+ function EyeIcon() {
309
329
  return (
310
- <label className="block">
311
- <span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
312
- {label}
313
- </span>
314
- <input
315
- type={type}
316
- value={value}
317
- onChange={onChange}
318
- autoComplete={autoComplete}
319
- required={required}
320
- className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
321
- />
322
- </label>
330
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden>
331
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
332
+ <circle cx="12" cy="12" r="3" />
333
+ </svg>
334
+ )
335
+ }
336
+
337
+ function EyeOffIcon() {
338
+ return (
339
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden>
340
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
341
+ <line x1="1" y1="1" x2="23" y2="23" />
342
+ </svg>
323
343
  )
324
344
  }
package/src/segment.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
- import { useState } from "react"
3
+ import { useEffect, useState } from "react"
4
+ import { useSearchParams } from "next/navigation"
4
5
  import type { HttpTypes } from "@medusajs/types"
5
6
  import LoginForm from "./login-form"
6
7
  import RegisterForm from "./register-form"
@@ -11,18 +12,26 @@ import AccountAddresses from "./account-addresses"
11
12
  import AccountOrders from "./account-orders"
12
13
  import AccountPaymentMethods from "./account-payment-methods"
13
14
  import AccountGuestOrders from "./account-guest-orders"
15
+ import LoginBrandPanel from "./login-brand-panel"
16
+ import GuestOrderModal from "./guest-order-modal"
14
17
  import { resolveOtpComponent, type OtpVerificationComponent } from "./otp-component"
15
18
  import { accountSectionPath } from "./account-utils"
19
+ import { auth, type BrandPanelConfig } from "./account-theme"
20
+ import { listGuestOrders } from "@pradip1995/commerce-core/data/guest"
16
21
 
17
22
  type AccountProps = {
18
23
  countryCode?: string
19
24
  path?: string
20
25
  customer?: HttpTypes.StoreCustomer | null
21
- orders?: HttpTypes.StoreOrder[]
26
+ orders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
22
27
  paymentDetails?: Record<string, unknown>[]
23
- guestOrders?: HttpTypes.StoreOrder[]
28
+ guestOrders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
24
29
  title?: string
25
30
  subtitle?: string
31
+ registerTitle?: string
32
+ registerSubtitle?: string
33
+ brandPanel?: BrandPanelConfig | null
34
+ guestTrackLabel?: string
26
35
  otpComponent?: OtpVerificationComponent
27
36
  }
28
37
 
@@ -33,12 +42,53 @@ export default function LoginTemplate({
33
42
  orders = [],
34
43
  paymentDetails = [],
35
44
  guestOrders = [],
36
- title,
37
- subtitle,
45
+ title = "Sign in",
46
+ subtitle = "Access your exclusive collection",
47
+ registerTitle = "Create account",
48
+ registerSubtitle = "Join our exclusive collection",
49
+ brandPanel,
50
+ guestTrackLabel = "Track guest order",
38
51
  otpComponent,
39
52
  }: AccountProps) {
40
53
  const OtpComponent = resolveOtpComponent(otpComponent)
41
54
  const section = accountSectionPath(path)
55
+ const searchParams = useSearchParams()
56
+
57
+ const [view, setView] = useState<"login" | "register" | "forgot">(
58
+ path === "register" ? "register" : path === "forgot-password" ? "forgot" : "login"
59
+ )
60
+ const [showGuestModal, setShowGuestModal] = useState(false)
61
+ const [checkingGuest, setCheckingGuest] = useState(false)
62
+ const [guestEmail, setGuestEmail] = useState("")
63
+ const [guestAutoStart, setGuestAutoStart] = useState(false)
64
+
65
+ useEffect(() => {
66
+ const guestView = searchParams.get("view")
67
+ const email = searchParams.get("email")
68
+ if (guestView === "guest") {
69
+ setGuestEmail(email && email !== "undefined" ? email : "")
70
+ setGuestAutoStart(Boolean(email && email !== "undefined"))
71
+ setShowGuestModal(true)
72
+ }
73
+ }, [searchParams])
74
+
75
+ async function handleTrackGuestOrder() {
76
+ // Guest JWT is httpOnly (_medusa_guest_jwt); probe via server action (empty token → cookie).
77
+ setCheckingGuest(true)
78
+ try {
79
+ await listGuestOrders("")
80
+ window.location.href = `/${countryCode}/account/guest-orders`
81
+ return
82
+ } catch {
83
+ // No valid guest session — open OTP modal
84
+ } finally {
85
+ setCheckingGuest(false)
86
+ }
87
+
88
+ setGuestEmail(searchParams.get("email") || "")
89
+ setGuestAutoStart(false)
90
+ setShowGuestModal(true)
91
+ }
42
92
 
43
93
  if (section === "guest-orders") {
44
94
  return <AccountGuestOrders orders={guestOrders} countryCode={countryCode} />
@@ -61,56 +111,104 @@ export default function LoginTemplate({
61
111
  case "payment-methods":
62
112
  return (
63
113
  <AccountPaymentMethods
64
- paymentDetails={paymentDetails as Parameters<typeof AccountPaymentMethods>[0]["paymentDetails"]}
114
+ paymentDetails={
115
+ paymentDetails as Parameters<typeof AccountPaymentMethods>[0]["paymentDetails"]
116
+ }
65
117
  />
66
118
  )
67
119
  case "overview":
68
120
  default:
69
- return <AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
121
+ return (
122
+ <AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
123
+ )
70
124
  }
71
125
  }
72
126
 
73
- const [view, setView] = useState<"login" | "register" | "forgot">(
74
- path === "register" ? "register" : path === "forgot-password" ? "forgot" : "login"
75
- )
127
+ const isSignIn = view === "login"
128
+ const isRegister = view === "register"
129
+ const heading = isSignIn ? title : isRegister ? registerTitle : "Reset password"
130
+ const subheading = isSignIn ? subtitle : isRegister ? registerSubtitle : undefined
76
131
 
77
132
  return (
78
- <div className="w-full max-w-md mx-auto">
79
- {(title || subtitle) && view !== "forgot" && (
80
- <div className="mb-8 text-center">
81
- {title && <h1 className="section-heading text-2xl mb-2">{title}</h1>}
82
- {subtitle && <p className="text-sm text-muted">{subtitle}</p>}
83
- </div>
84
- )}
85
-
86
- {view !== "forgot" && (
87
- <div className="flex gap-6 mb-8 border-b border-cart-border">
88
- <TabButton active={view === "login"} onClick={() => setView("login")}>
89
- Sign in
90
- </TabButton>
91
- <TabButton active={view === "register"} onClick={() => setView("register")}>
92
- Register
93
- </TabButton>
133
+ <>
134
+ <div data-login-viewport className={auth.viewport}>
135
+ <LoginBrandPanel panel={brandPanel} variant="mobile" />
136
+ <LoginBrandPanel panel={brandPanel} variant="desktop" />
137
+
138
+ <div className={auth.formColumn}>
139
+ <div className={auth.formWash} aria-hidden />
140
+ <div className={auth.formGlow} aria-hidden />
141
+
142
+ <div className={auth.formInner}>
143
+ {(isSignIn || isRegister) && (
144
+ <header className={auth.desktopHeader}>
145
+ <div className="flex items-center gap-3 mb-3">
146
+ <span className="h-px flex-1 max-w-[48px] bg-gradient-to-r from-brand-accent to-brand-primary" />
147
+ <span className="text-brand-accent text-[8px] leading-none">◆</span>
148
+ <span className="h-px flex-1 max-w-[48px] bg-gradient-to-l from-brand-accent to-brand-primary" />
149
+ </div>
150
+ <h1 className={auth.title}>{heading}</h1>
151
+ {subheading && <p className={auth.subtitle}>{subheading}</p>}
152
+ </header>
153
+ )}
154
+
155
+ <div className={auth.formCard}>
156
+ {view !== "forgot" && (
157
+ <div className="flex gap-6 mb-8 border-b border-cart-border">
158
+ <TabButton active={view === "login"} onClick={() => setView("login")}>
159
+ Sign in
160
+ </TabButton>
161
+ <TabButton active={view === "register"} onClick={() => setView("register")}>
162
+ Register
163
+ </TabButton>
164
+ </div>
165
+ )}
166
+
167
+ {view === "login" && (
168
+ <LoginForm
169
+ countryCode={countryCode}
170
+ OtpComponent={OtpComponent}
171
+ onForgot={() => setView("forgot")}
172
+ onRegister={() => setView("register")}
173
+ onTrackGuest={() => {
174
+ if (checkingGuest) return
175
+ void handleTrackGuestOrder()
176
+ }}
177
+ />
178
+ )}
179
+ {view === "register" && (
180
+ <RegisterForm
181
+ countryCode={countryCode}
182
+ OtpComponent={OtpComponent}
183
+ onLogin={() => setView("login")}
184
+ />
185
+ )}
186
+ {view === "forgot" && (
187
+ <ForgotPasswordForm onBack={() => setView("login")} />
188
+ )}
189
+ </div>
190
+
191
+ {checkingGuest && (
192
+ <p className="mt-4 text-xs text-muted uppercase tracking-[0.2em]">
193
+ {guestTrackLabel}…
194
+ </p>
195
+ )}
196
+ </div>
94
197
  </div>
95
- )}
96
-
97
- {view === "login" && (
98
- <LoginForm
99
- countryCode={countryCode}
100
- OtpComponent={OtpComponent}
101
- onForgot={() => setView("forgot")}
102
- onRegister={() => setView("register")}
103
- />
104
- )}
105
- {view === "register" && (
106
- <RegisterForm
107
- countryCode={countryCode}
108
- OtpComponent={OtpComponent}
109
- onLogin={() => setView("login")}
110
- />
111
- )}
112
- {view === "forgot" && <ForgotPasswordForm onBack={() => setView("login")} />}
113
- </div>
198
+ </div>
199
+
200
+ <GuestOrderModal
201
+ open={showGuestModal}
202
+ onClose={() => {
203
+ setShowGuestModal(false)
204
+ setGuestAutoStart(false)
205
+ }}
206
+ countryCode={countryCode}
207
+ initialEmail={guestEmail}
208
+ autoStart={guestAutoStart}
209
+ OtpComponent={OtpComponent}
210
+ />
211
+ </>
114
212
  )
115
213
  }
116
214
 
@@ -127,11 +225,7 @@ function TabButton({
127
225
  <button
128
226
  type="button"
129
227
  onClick={onClick}
130
- className={`pb-3 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] border-b-2 -mb-px transition-colors ${
131
- active
132
- ? "border-brand-accent text-brand-accent"
133
- : "border-transparent text-muted hover:text-heading"
134
- }`}
228
+ className={active ? auth.tabActive : auth.tabIdle}
135
229
  >
136
230
  {children}
137
231
  </button>