@pradip1995/segment-login-template 0.3.0 → 0.4.1
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 +7 -4
- package/src/account-addresses.tsx +216 -0
- package/src/account-guest-orders.tsx +60 -0
- package/src/account-orders.tsx +81 -0
- package/src/account-overview.tsx +61 -19
- package/src/account-payment-methods.tsx +229 -0
- package/src/account-profile.tsx +198 -0
- package/src/account-utils.ts +20 -0
- package/src/auth-server.ts +157 -2
- package/src/commerce-auth-otp-modal.tsx +104 -0
- package/src/google-auth-section.tsx +69 -8
- package/src/index.ts +4 -0
- package/src/login-form.tsx +143 -87
- package/src/otp-component.ts +14 -0
- package/src/otp-verification-modal.tsx +112 -0
- package/src/register-form.tsx +130 -104
- package/src/segment.tsx +63 -5
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import OtpInput from "@pradip1995/commerce-auth/components/otp-input"
|
|
4
|
+
import type { OtpVerificationModalProps } from "./otp-verification-modal"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OTP modal styled like commerce-auth/chocomelon, compatible with segment-login-template props.
|
|
8
|
+
* Use in pages.config: `"otpComponent": "@pradip1995/segment-login-template/commerce-auth-otp-modal"`
|
|
9
|
+
*/
|
|
10
|
+
export default function CommerceAuthOtpModal({
|
|
11
|
+
open,
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
otp,
|
|
15
|
+
onOtpChange,
|
|
16
|
+
error,
|
|
17
|
+
pending,
|
|
18
|
+
success = false,
|
|
19
|
+
successTitle = "Verified",
|
|
20
|
+
successMessage = "Verification successful!",
|
|
21
|
+
verifyLabel = "Verify",
|
|
22
|
+
onVerify,
|
|
23
|
+
onResend,
|
|
24
|
+
onClose,
|
|
25
|
+
preventClose = false,
|
|
26
|
+
extraContent,
|
|
27
|
+
}: OtpVerificationModalProps) {
|
|
28
|
+
if (!open) return null
|
|
29
|
+
|
|
30
|
+
function handleBackdropClick() {
|
|
31
|
+
if (preventClose || pending || success) return
|
|
32
|
+
onClose?.()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className="absolute inset-0 bg-black/40"
|
|
40
|
+
aria-label="Close verification dialog"
|
|
41
|
+
onClick={handleBackdropClick}
|
|
42
|
+
/>
|
|
43
|
+
<div className="relative w-full max-w-md rounded-2xl bg-page-bg shadow-xl p-6">
|
|
44
|
+
<div className="flex flex-col gap-6 py-4 px-2">
|
|
45
|
+
<div className="text-center">
|
|
46
|
+
<h2 className="text-2xl font-bold text-heading mb-2">
|
|
47
|
+
{success ? successTitle : title}
|
|
48
|
+
</h2>
|
|
49
|
+
{!success && (
|
|
50
|
+
<p className="text-sm text-gray-500 leading-relaxed max-w-[320px] mx-auto">
|
|
51
|
+
{description}
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{success ? (
|
|
57
|
+
<div className="text-center py-6">
|
|
58
|
+
<p className="text-brand-accent font-bold animate-pulse">{successMessage}</p>
|
|
59
|
+
</div>
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
{extraContent}
|
|
63
|
+
|
|
64
|
+
<OtpInput value={otp} onChange={onOtpChange} autoFocus />
|
|
65
|
+
|
|
66
|
+
{error && (
|
|
67
|
+
<p className="text-red-500 text-xs text-center" role="alert">
|
|
68
|
+
{error}
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
<div className="space-y-3">
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={onVerify}
|
|
76
|
+
disabled={pending || otp.length < 6}
|
|
77
|
+
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:opacity-90 rounded-[30px] transition-colors disabled:opacity-60"
|
|
78
|
+
>
|
|
79
|
+
{pending ? "Verifying…" : verifyLabel}
|
|
80
|
+
</button>
|
|
81
|
+
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={onResend}
|
|
85
|
+
disabled={pending}
|
|
86
|
+
className="text-xs text-gray-500 hover:text-brand-accent w-full text-center underline font-medium disabled:opacity-60"
|
|
87
|
+
>
|
|
88
|
+
Resend code
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{preventClose && (
|
|
93
|
+
<p className="text-xs text-muted text-center">
|
|
94
|
+
Verification is required to complete{" "}
|
|
95
|
+
{verifyLabel.toLowerCase().includes("sign in") ? "sign in" : "registration"}.
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -1,25 +1,86 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
clearMedusaAuthCookies,
|
|
6
|
+
GOOGLE_LOGIN_COUNTRY_CODE_KEY,
|
|
7
|
+
} from "@pradip1995/commerce-auth/util/google-auth-client"
|
|
8
|
+
import { initiateGoogleAuth } from "./auth-server"
|
|
5
9
|
|
|
6
10
|
const BUTTON_CLASS =
|
|
7
11
|
"btn-outline w-full flex items-center justify-center gap-2.5 py-3 px-4"
|
|
8
12
|
|
|
9
13
|
export default function GoogleAuthSection({ countryCode }: { countryCode: string }) {
|
|
10
14
|
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID?.trim()
|
|
15
|
+
const [loading, setLoading] = useState(false)
|
|
16
|
+
const [error, setError] = useState<string | null>(null)
|
|
11
17
|
|
|
12
18
|
if (!clientId) return null
|
|
13
19
|
|
|
20
|
+
const handleClick = async () => {
|
|
21
|
+
setError(null)
|
|
22
|
+
setLoading(true)
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
localStorage.setItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY, countryCode)
|
|
26
|
+
clearMedusaAuthCookies()
|
|
27
|
+
|
|
28
|
+
const result = await initiateGoogleAuth(countryCode)
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
setError(result.error)
|
|
32
|
+
setLoading(false)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (result.redirectUrl) {
|
|
37
|
+
window.location.href = result.redirectUrl
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setLoading(false)
|
|
42
|
+
} catch {
|
|
43
|
+
setError("Google sign-in failed. Please try again.")
|
|
44
|
+
setLoading(false)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
return (
|
|
15
49
|
<div className="mb-6">
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
50
|
+
{error && (
|
|
51
|
+
<p className="mb-3 text-sm text-red-600" role="alert">
|
|
52
|
+
{error}
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={handleClick}
|
|
58
|
+
disabled={loading}
|
|
20
59
|
className={BUTTON_CLASS}
|
|
21
|
-
|
|
22
|
-
|
|
60
|
+
style={{ borderRadius: "30px" }}
|
|
61
|
+
>
|
|
62
|
+
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
|
|
63
|
+
<path
|
|
64
|
+
fill="#4285F4"
|
|
65
|
+
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"
|
|
66
|
+
/>
|
|
67
|
+
<path
|
|
68
|
+
fill="#34A853"
|
|
69
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
70
|
+
/>
|
|
71
|
+
<path
|
|
72
|
+
fill="#FBBC05"
|
|
73
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
74
|
+
/>
|
|
75
|
+
<path
|
|
76
|
+
fill="#EA4335"
|
|
77
|
+
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
|
+
/>
|
|
79
|
+
</svg>
|
|
80
|
+
<span className="text-sm text-gray-700 font-bold">
|
|
81
|
+
{loading ? "Connecting..." : "Continue with Google"}
|
|
82
|
+
</span>
|
|
83
|
+
</button>
|
|
23
84
|
</div>
|
|
24
85
|
)
|
|
25
86
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { default } from "./segment"
|
|
2
2
|
export { default as manifest } from "./manifest"
|
|
3
3
|
export { default as ResetPasswordPage } from "./reset-password-page"
|
|
4
|
+
export { default as OtpVerificationModal } from "./otp-verification-modal"
|
|
5
|
+
export { default as CommerceAuthOtpModal } from "./commerce-auth-otp-modal"
|
|
6
|
+
export type { OtpVerificationModalProps } from "./otp-verification-modal"
|
|
7
|
+
export { resolveOtpComponent, type OtpVerificationComponent } from "./otp-component"
|
package/src/login-form.tsx
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { useActionState, useState } from "react"
|
|
4
|
-
import { useRouter } from "next/navigation"
|
|
5
4
|
import { login } from "@pradip1995/commerce-core/client/actions/customer"
|
|
6
5
|
import { sendAuthOtp, sendOTP, verifyAuthOtpAndLogin, verifyOTP } from "./auth-server"
|
|
7
|
-
import OtpInput from "./otp-input"
|
|
8
6
|
import GoogleAuthSection from "./google-auth-section"
|
|
7
|
+
import OtpVerificationModal from "./otp-verification-modal"
|
|
8
|
+
import type { OtpVerificationComponent } from "./otp-component"
|
|
9
9
|
|
|
10
10
|
type LoginMode = "password" | "otp"
|
|
11
11
|
|
|
12
12
|
export default function LoginForm({
|
|
13
13
|
countryCode,
|
|
14
|
+
OtpComponent = OtpVerificationModal,
|
|
14
15
|
onForgot,
|
|
15
16
|
onRegister,
|
|
16
17
|
}: {
|
|
17
18
|
countryCode: string
|
|
19
|
+
OtpComponent?: OtpVerificationComponent
|
|
18
20
|
onForgot: () => void
|
|
19
21
|
onRegister: () => void
|
|
20
22
|
}) {
|
|
@@ -45,7 +47,7 @@ export default function LoginForm({
|
|
|
45
47
|
{mode === "password" ? (
|
|
46
48
|
<PasswordLoginForm countryCode={countryCode} onForgot={onForgot} />
|
|
47
49
|
) : (
|
|
48
|
-
<OtpLoginForm countryCode={countryCode} />
|
|
50
|
+
<OtpLoginForm countryCode={countryCode} OtpComponent={OtpComponent} />
|
|
49
51
|
)}
|
|
50
52
|
|
|
51
53
|
<p className="text-sm text-center text-muted">
|
|
@@ -95,7 +97,11 @@ function PasswordLoginForm({
|
|
|
95
97
|
<Field label="Email or phone" name="email_or_phone" type="text" autoComplete="username" />
|
|
96
98
|
<Field label="Password" name="password" type="password" autoComplete="current-password" />
|
|
97
99
|
{state && typeof state === "string" && state !== "ACCOUNT_DELETION_PENDING" && (
|
|
98
|
-
<p className="text-sm text-brand-sale">
|
|
100
|
+
<p className="text-sm text-brand-sale">
|
|
101
|
+
{state === "Email not verified." || state.includes("not verified")
|
|
102
|
+
? "Your email is not verified yet. Complete OTP verification during registration, or use OTP sign-in below."
|
|
103
|
+
: state}
|
|
104
|
+
</p>
|
|
99
105
|
)}
|
|
100
106
|
{state === "ACCOUNT_DELETION_PENDING" && (
|
|
101
107
|
<p className="text-sm text-brand-sale">
|
|
@@ -116,13 +122,18 @@ function PasswordLoginForm({
|
|
|
116
122
|
)
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
function OtpLoginForm({
|
|
120
|
-
|
|
125
|
+
function OtpLoginForm({
|
|
126
|
+
countryCode,
|
|
127
|
+
OtpComponent,
|
|
128
|
+
}: {
|
|
129
|
+
countryCode: string
|
|
130
|
+
OtpComponent: OtpVerificationComponent
|
|
131
|
+
}) {
|
|
121
132
|
const [authMethod, setAuthMethod] = useState<"email_auth" | "phone_auth" | "guest">("email_auth")
|
|
122
133
|
const [identifier, setIdentifier] = useState("")
|
|
123
134
|
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
124
135
|
const [otp, setOtp] = useState("")
|
|
125
|
-
const [
|
|
136
|
+
const [showOtpModal, setShowOtpModal] = useState(false)
|
|
126
137
|
const [error, setError] = useState<string | null>(null)
|
|
127
138
|
const [pending, setPending] = useState(false)
|
|
128
139
|
const [isNewUser, setIsNewUser] = useState(false)
|
|
@@ -141,14 +152,15 @@ function OtpLoginForm({ countryCode }: { countryCode: string }) {
|
|
|
141
152
|
throw new Error((res as { error?: string }).error)
|
|
142
153
|
}
|
|
143
154
|
setOtpToken("guest")
|
|
144
|
-
|
|
155
|
+
setOtp("")
|
|
156
|
+
setShowOtpModal(true)
|
|
145
157
|
return
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
const res = await sendAuthOtp(
|
|
149
161
|
authMethod === "email_auth"
|
|
150
|
-
? { email: identifier, type: "email_auth" }
|
|
151
|
-
: { phone: identifier, type: "phone_auth" }
|
|
162
|
+
? { email: identifier.trim(), type: "email_auth" }
|
|
163
|
+
: { phone: identifier.replace(/[^\d+]/g, ""), type: "phone_auth" }
|
|
152
164
|
)
|
|
153
165
|
|
|
154
166
|
if (!res.success) {
|
|
@@ -157,7 +169,8 @@ function OtpLoginForm({ countryCode }: { countryCode: string }) {
|
|
|
157
169
|
|
|
158
170
|
setOtpToken(res.token ?? null)
|
|
159
171
|
setIsNewUser(res.isNewUser ?? false)
|
|
160
|
-
|
|
172
|
+
setOtp("")
|
|
173
|
+
setShowOtpModal(true)
|
|
161
174
|
} catch (err) {
|
|
162
175
|
setError(err instanceof Error ? err.message : "Failed to send code")
|
|
163
176
|
} finally {
|
|
@@ -165,8 +178,40 @@ function OtpLoginForm({ countryCode }: { countryCode: string }) {
|
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
180
|
|
|
168
|
-
async function
|
|
169
|
-
|
|
181
|
+
async function handleResendOtp() {
|
|
182
|
+
setPending(true)
|
|
183
|
+
setError(null)
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
if (authMethod === "guest") {
|
|
187
|
+
const res = await sendOTP(identifier)
|
|
188
|
+
if (!res.success && (res as { error?: string }).error) {
|
|
189
|
+
throw new Error((res as { error?: string }).error)
|
|
190
|
+
}
|
|
191
|
+
setOtp("")
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const res = await sendAuthOtp(
|
|
196
|
+
authMethod === "email_auth"
|
|
197
|
+
? { email: identifier.trim(), type: "email_auth" }
|
|
198
|
+
: { phone: identifier.replace(/[^\d+]/g, ""), type: "phone_auth" }
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if (!res.success) {
|
|
202
|
+
throw new Error(res.error)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setOtpToken(res.token ?? null)
|
|
206
|
+
setOtp("")
|
|
207
|
+
} catch (err) {
|
|
208
|
+
setError(err instanceof Error ? err.message : "Failed to resend code")
|
|
209
|
+
} finally {
|
|
210
|
+
setPending(false)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function handleVerifyOtp() {
|
|
170
215
|
if (!otp || otp.length < 6) return
|
|
171
216
|
|
|
172
217
|
setPending(true)
|
|
@@ -182,7 +227,7 @@ function OtpLoginForm({ countryCode }: { countryCode: string }) {
|
|
|
182
227
|
if (token) {
|
|
183
228
|
document.cookie = `_medusa_guest_token=${token}; path=/; max-age=86400; SameSite=Lax`
|
|
184
229
|
}
|
|
185
|
-
|
|
230
|
+
window.location.href = `/${countryCode}/account/guest-orders`
|
|
186
231
|
return
|
|
187
232
|
}
|
|
188
233
|
|
|
@@ -204,87 +249,98 @@ function OtpLoginForm({ countryCode }: { countryCode: string }) {
|
|
|
204
249
|
}
|
|
205
250
|
}
|
|
206
251
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
252
|
+
const otpTitle =
|
|
253
|
+
authMethod === "guest"
|
|
254
|
+
? "Verify guest order access"
|
|
255
|
+
: authMethod === "phone_auth"
|
|
256
|
+
? "Verify your phone"
|
|
257
|
+
: "Verify your email"
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<>
|
|
261
|
+
<form onSubmit={handleSendOtp} className="space-y-4">
|
|
262
|
+
<div className="flex flex-wrap gap-2">
|
|
263
|
+
<Chip active={authMethod === "email_auth"} onClick={() => setAuthMethod("email_auth")}>
|
|
264
|
+
Email OTP
|
|
265
|
+
</Chip>
|
|
266
|
+
<Chip active={authMethod === "phone_auth"} onClick={() => setAuthMethod("phone_auth")}>
|
|
267
|
+
Phone OTP
|
|
268
|
+
</Chip>
|
|
269
|
+
<Chip active={authMethod === "guest"} onClick={() => setAuthMethod("guest")}>
|
|
270
|
+
Guest orders
|
|
271
|
+
</Chip>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<Field
|
|
275
|
+
label={authMethod === "phone_auth" ? "Phone" : "Email"}
|
|
276
|
+
name="identifier"
|
|
277
|
+
type={authMethod === "phone_auth" ? "tel" : "email"}
|
|
278
|
+
value={identifier}
|
|
279
|
+
onChange={(e) => setIdentifier(e.target.value)}
|
|
280
|
+
autoComplete={authMethod === "phone_auth" ? "tel" : "email"}
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
<p className="text-xs text-muted">
|
|
284
|
+
{authMethod === "guest"
|
|
285
|
+
? "We will send a one-time code to view your guest orders."
|
|
286
|
+
: "We will send a one-time code to sign in or create your account."}
|
|
212
287
|
</p>
|
|
213
288
|
|
|
214
|
-
{
|
|
215
|
-
|
|
216
|
-
<Field
|
|
217
|
-
label="First name"
|
|
218
|
-
name="first_name"
|
|
219
|
-
type="text"
|
|
220
|
-
value={firstName}
|
|
221
|
-
onChange={(e) => setFirstName(e.target.value)}
|
|
222
|
-
/>
|
|
223
|
-
<Field
|
|
224
|
-
label="Last name"
|
|
225
|
-
name="last_name"
|
|
226
|
-
type="text"
|
|
227
|
-
value={lastName}
|
|
228
|
-
onChange={(e) => setLastName(e.target.value)}
|
|
229
|
-
/>
|
|
230
|
-
</div>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
<OtpInput value={otp} onChange={setOtp} autoFocus />
|
|
234
|
-
{error && <p className="text-sm text-brand-sale text-center">{error}</p>}
|
|
235
|
-
<button type="submit" disabled={pending || otp.length < 6} className="btn-primary w-full disabled:opacity-60">
|
|
236
|
-
{pending ? "Verifying…" : "Verify & continue"}
|
|
237
|
-
</button>
|
|
289
|
+
{error && !showOtpModal && <p className="text-sm text-brand-sale">{error}</p>}
|
|
290
|
+
|
|
238
291
|
<button
|
|
239
|
-
type="
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
setStep("identifier")
|
|
243
|
-
setOtp("")
|
|
244
|
-
setError(null)
|
|
245
|
-
}}
|
|
292
|
+
type="submit"
|
|
293
|
+
disabled={pending || !identifier.trim()}
|
|
294
|
+
className="btn-primary w-full disabled:opacity-60"
|
|
246
295
|
>
|
|
247
|
-
|
|
296
|
+
{pending ? "Sending…" : "Send verification code"}
|
|
248
297
|
</button>
|
|
249
298
|
</form>
|
|
250
|
-
)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return (
|
|
254
|
-
<form onSubmit={handleSendOtp} className="space-y-4">
|
|
255
|
-
<div className="flex flex-wrap gap-2">
|
|
256
|
-
<Chip active={authMethod === "email_auth"} onClick={() => setAuthMethod("email_auth")}>
|
|
257
|
-
Email OTP
|
|
258
|
-
</Chip>
|
|
259
|
-
<Chip active={authMethod === "phone_auth"} onClick={() => setAuthMethod("phone_auth")}>
|
|
260
|
-
Phone OTP
|
|
261
|
-
</Chip>
|
|
262
|
-
<Chip active={authMethod === "guest"} onClick={() => setAuthMethod("guest")}>
|
|
263
|
-
Guest orders
|
|
264
|
-
</Chip>
|
|
265
|
-
</div>
|
|
266
299
|
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
300
|
+
<OtpComponent
|
|
301
|
+
open={showOtpModal}
|
|
302
|
+
preventClose
|
|
303
|
+
title={otpTitle}
|
|
304
|
+
description={`Enter the 6-digit code sent to ${identifier || "your contact"}.`}
|
|
305
|
+
otp={otp}
|
|
306
|
+
onOtpChange={setOtp}
|
|
307
|
+
error={error}
|
|
308
|
+
pending={pending}
|
|
309
|
+
verifyLabel="Verify & sign in"
|
|
310
|
+
onVerify={handleVerifyOtp}
|
|
311
|
+
onResend={handleResendOtp}
|
|
312
|
+
extraContent={
|
|
313
|
+
isNewUser && authMethod !== "guest" ? (
|
|
314
|
+
<div className="grid grid-cols-2 gap-3 mb-6">
|
|
315
|
+
<label className="block">
|
|
316
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
317
|
+
First name
|
|
318
|
+
</span>
|
|
319
|
+
<input
|
|
320
|
+
type="text"
|
|
321
|
+
value={firstName}
|
|
322
|
+
onChange={(e) => setFirstName(e.target.value)}
|
|
323
|
+
required
|
|
324
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
325
|
+
/>
|
|
326
|
+
</label>
|
|
327
|
+
<label className="block">
|
|
328
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
329
|
+
Last name
|
|
330
|
+
</span>
|
|
331
|
+
<input
|
|
332
|
+
type="text"
|
|
333
|
+
value={lastName}
|
|
334
|
+
onChange={(e) => setLastName(e.target.value)}
|
|
335
|
+
required
|
|
336
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
337
|
+
/>
|
|
338
|
+
</label>
|
|
339
|
+
</div>
|
|
340
|
+
) : undefined
|
|
341
|
+
}
|
|
274
342
|
/>
|
|
275
|
-
|
|
276
|
-
<p className="text-xs text-muted">
|
|
277
|
-
{authMethod === "guest"
|
|
278
|
-
? "Track a guest order with a one-time code — no account required."
|
|
279
|
-
: "We will send a one-time code to sign in or create your account."}
|
|
280
|
-
</p>
|
|
281
|
-
|
|
282
|
-
{error && <p className="text-sm text-brand-sale">{error}</p>}
|
|
283
|
-
|
|
284
|
-
<button type="submit" disabled={pending || !identifier.trim()} className="btn-primary w-full disabled:opacity-60">
|
|
285
|
-
{pending ? "Sending…" : "Send code"}
|
|
286
|
-
</button>
|
|
287
|
-
</form>
|
|
343
|
+
</>
|
|
288
344
|
)
|
|
289
345
|
}
|
|
290
346
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentType } from "react"
|
|
2
|
+
import type { OtpVerificationModalProps } from "./otp-verification-modal"
|
|
3
|
+
import DefaultOtpVerificationModal from "./otp-verification-modal"
|
|
4
|
+
|
|
5
|
+
export type OtpVerificationComponent = ComponentType<OtpVerificationModalProps>
|
|
6
|
+
|
|
7
|
+
export function resolveOtpComponent(
|
|
8
|
+
component?: OtpVerificationComponent | string
|
|
9
|
+
): OtpVerificationComponent {
|
|
10
|
+
if (!component || typeof component === "string") {
|
|
11
|
+
return DefaultOtpVerificationModal
|
|
12
|
+
}
|
|
13
|
+
return component
|
|
14
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import OtpInput from "./otp-input"
|
|
4
|
+
|
|
5
|
+
export type OtpVerificationModalProps = {
|
|
6
|
+
open: boolean
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
otp: string
|
|
10
|
+
onOtpChange: (value: string) => void
|
|
11
|
+
error: string | null
|
|
12
|
+
pending: boolean
|
|
13
|
+
success?: boolean
|
|
14
|
+
successTitle?: string
|
|
15
|
+
successMessage?: string
|
|
16
|
+
verifyLabel?: string
|
|
17
|
+
onVerify: () => void
|
|
18
|
+
onResend: () => void
|
|
19
|
+
onClose?: () => void
|
|
20
|
+
/** When true, backdrop click does not dismiss the modal (registration OTP). */
|
|
21
|
+
preventClose?: boolean
|
|
22
|
+
extraContent?: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function OtpVerificationModal({
|
|
26
|
+
open,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
otp,
|
|
30
|
+
onOtpChange,
|
|
31
|
+
error,
|
|
32
|
+
pending,
|
|
33
|
+
success = false,
|
|
34
|
+
successTitle = "Verified",
|
|
35
|
+
successMessage = "Redirecting you…",
|
|
36
|
+
verifyLabel = "Verify & continue",
|
|
37
|
+
onVerify,
|
|
38
|
+
onResend,
|
|
39
|
+
onClose,
|
|
40
|
+
preventClose = false,
|
|
41
|
+
extraContent,
|
|
42
|
+
}: OtpVerificationModalProps) {
|
|
43
|
+
if (!open) return null
|
|
44
|
+
|
|
45
|
+
function handleBackdropClick() {
|
|
46
|
+
if (preventClose || pending || success) return
|
|
47
|
+
onClose?.()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="absolute inset-0 bg-black/40"
|
|
55
|
+
aria-label="Close verification dialog"
|
|
56
|
+
onClick={handleBackdropClick}
|
|
57
|
+
/>
|
|
58
|
+
<div className="relative w-full max-w-md rounded-2xl bg-page-bg border border-cart-border shadow-xl p-6">
|
|
59
|
+
{success ? (
|
|
60
|
+
<div className="text-center space-y-4 py-4">
|
|
61
|
+
<div className="w-14 h-14 mx-auto rounded-full bg-green-100 flex items-center justify-center text-2xl text-green-600">
|
|
62
|
+
✓
|
|
63
|
+
</div>
|
|
64
|
+
<h2 className="section-heading text-lg">{successTitle}</h2>
|
|
65
|
+
<p className="text-sm text-muted">{successMessage}</p>
|
|
66
|
+
</div>
|
|
67
|
+
) : (
|
|
68
|
+
<>
|
|
69
|
+
<h2 className="section-heading text-lg text-center mb-2">{title}</h2>
|
|
70
|
+
<p className="text-sm text-muted text-center mb-6">{description}</p>
|
|
71
|
+
|
|
72
|
+
{extraContent}
|
|
73
|
+
|
|
74
|
+
<div className="mb-6">
|
|
75
|
+
<OtpInput value={otp} onChange={onOtpChange} autoFocus />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{error && (
|
|
79
|
+
<p className="text-sm text-brand-sale text-center mb-4" role="alert">
|
|
80
|
+
{error}
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={onVerify}
|
|
87
|
+
disabled={pending || otp.length < 6}
|
|
88
|
+
className="btn-primary w-full disabled:opacity-60 mb-3"
|
|
89
|
+
>
|
|
90
|
+
{pending ? "Verifying…" : verifyLabel}
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={onResend}
|
|
96
|
+
disabled={pending}
|
|
97
|
+
className="text-sm text-muted hover:text-brand-accent w-full text-center disabled:opacity-60"
|
|
98
|
+
>
|
|
99
|
+
Didn't receive the code? Resend
|
|
100
|
+
</button>
|
|
101
|
+
|
|
102
|
+
{preventClose && (
|
|
103
|
+
<p className="text-xs text-muted text-center mt-4">
|
|
104
|
+
Verification is required to complete {verifyLabel.includes("sign in") ? "sign in" : "registration"}.
|
|
105
|
+
</p>
|
|
106
|
+
)}
|
|
107
|
+
</>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|