@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.
- package/package.json +1 -1
- package/src/account-addresses.tsx +14 -13
- package/src/account-guest-orders.tsx +47 -12
- package/src/account-orders.tsx +90 -38
- package/src/account-overview.tsx +96 -74
- package/src/account-payment-methods.tsx +155 -63
- package/src/account-profile.tsx +12 -11
- package/src/account-theme.ts +79 -0
- package/src/forgot-password-form.tsx +44 -26
- package/src/google-auth-section.tsx +8 -10
- package/src/guest-order-modal.tsx +224 -0
- package/src/login-brand-panel.tsx +96 -0
- package/src/login-form.tsx +207 -146
- package/src/payment-methods-actions.ts +214 -0
- package/src/register-form.tsx +77 -57
- package/src/segment.tsx +144 -50
|
@@ -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
|
+
}
|
package/src/register-form.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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={
|
|
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
|
|
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
|
-
<
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
/>
|
|
322
|
-
</
|
|
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={
|
|
114
|
+
paymentDetails={
|
|
115
|
+
paymentDetails as Parameters<typeof AccountPaymentMethods>[0]["paymentDetails"]
|
|
116
|
+
}
|
|
65
117
|
/>
|
|
66
118
|
)
|
|
67
119
|
case "overview":
|
|
68
120
|
default:
|
|
69
|
-
return
|
|
121
|
+
return (
|
|
122
|
+
<AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
|
|
123
|
+
)
|
|
70
124
|
}
|
|
71
125
|
}
|
|
72
126
|
|
|
73
|
-
const
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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={
|
|
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>
|