@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 +10 -7
- package/src/account-guest-orders.tsx +41 -12
- package/src/account-payment-methods.tsx +7 -0
- package/src/account-theme.ts +64 -1
- package/src/auth-server.ts +23 -7
- 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 +158 -0
- package/src/login-form.tsx +211 -147
- package/src/otp-unified-form.tsx +340 -0
- package/src/payment-methods-actions.ts +16 -4
- package/src/register-form.tsx +77 -57
- package/src/segment.tsx +181 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pradip1995/segment-login-template",
|
|
3
|
-
"version": "0.
|
|
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
|
|
18
|
-
|
|
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
|
-
<
|
|
31
|
-
|
|
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="
|
|
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="
|
|
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">
|
|
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)
|
package/src/account-theme.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
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
|
+
}
|
package/src/auth-server.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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-
|
|
46
|
-
<
|
|
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=
|
|
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=
|
|
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-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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'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
|
|
47
|
+
<div>
|
|
50
48
|
{error && (
|
|
51
|
-
<p className="mb-3 text-sm text-
|
|
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={
|
|
60
|
-
|
|
57
|
+
className={auth.btnOutline}
|
|
58
|
+
aria-label="Continue with Google"
|
|
61
59
|
>
|
|
62
|
-
<svg width="
|
|
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-
|
|
81
|
-
{loading ? "
|
|
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'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
|
+
}
|