@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
package/src/register-form.tsx
CHANGED
|
@@ -3,24 +3,28 @@
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
import {
|
|
5
5
|
registerCustomer,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
sendRegistrationOtp,
|
|
7
|
+
verifyRegistrationOtp,
|
|
8
8
|
} from "./auth-server"
|
|
9
|
-
import OtpInput from "./otp-input"
|
|
10
9
|
import GoogleAuthSection from "./google-auth-section"
|
|
10
|
+
import OtpVerificationModal from "./otp-verification-modal"
|
|
11
|
+
import type { OtpVerificationComponent } from "./otp-component"
|
|
11
12
|
|
|
12
13
|
type VerificationMethod = "email_verification" | "phone_verification"
|
|
14
|
+
type RegistrationStep = "form" | "otp" | "success"
|
|
13
15
|
|
|
14
16
|
export default function RegisterForm({
|
|
15
17
|
countryCode,
|
|
18
|
+
OtpComponent = OtpVerificationModal,
|
|
16
19
|
onLogin,
|
|
17
20
|
}: {
|
|
18
21
|
countryCode: string
|
|
22
|
+
OtpComponent?: OtpVerificationComponent
|
|
19
23
|
onLogin: () => void
|
|
20
24
|
}) {
|
|
21
|
-
const [step, setStep] = useState<
|
|
25
|
+
const [step, setStep] = useState<RegistrationStep>("form")
|
|
22
26
|
const [verificationMethod, setVerificationMethod] =
|
|
23
|
-
useState<VerificationMethod>("
|
|
27
|
+
useState<VerificationMethod>("email_verification")
|
|
24
28
|
const [form, setForm] = useState({
|
|
25
29
|
full_name: "",
|
|
26
30
|
email: "",
|
|
@@ -32,6 +36,7 @@ export default function RegisterForm({
|
|
|
32
36
|
const [otp, setOtp] = useState("")
|
|
33
37
|
const [error, setError] = useState<string | null>(null)
|
|
34
38
|
const [pending, setPending] = useState(false)
|
|
39
|
+
const [showOtpModal, setShowOtpModal] = useState(false)
|
|
35
40
|
|
|
36
41
|
function updateField(key: keyof typeof form, value: string) {
|
|
37
42
|
setForm((prev) => ({ ...prev, [key]: value }))
|
|
@@ -69,13 +74,15 @@ export default function RegisterForm({
|
|
|
69
74
|
|
|
70
75
|
setCustomerId(id)
|
|
71
76
|
|
|
72
|
-
const otpResult = await
|
|
77
|
+
const otpResult = await sendRegistrationOtp(id, verificationMethod)
|
|
73
78
|
if (!otpResult.success) {
|
|
74
79
|
throw new Error(otpResult.error || "Failed to send verification code")
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
setOtpToken(otpResult.token ?? null)
|
|
83
|
+
setOtp("")
|
|
78
84
|
setStep("otp")
|
|
85
|
+
setShowOtpModal(true)
|
|
79
86
|
} catch (err) {
|
|
80
87
|
setError(err instanceof Error ? err.message : "Registration failed")
|
|
81
88
|
} finally {
|
|
@@ -83,22 +90,30 @@ export default function RegisterForm({
|
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
async function handleVerifyOtp(
|
|
87
|
-
e.preventDefault()
|
|
93
|
+
async function handleVerifyOtp() {
|
|
88
94
|
if (!otp || otp.length < 6) return
|
|
89
95
|
|
|
90
96
|
setPending(true)
|
|
91
97
|
setError(null)
|
|
92
98
|
|
|
93
99
|
try {
|
|
94
|
-
const result = await
|
|
100
|
+
const result = await verifyRegistrationOtp({
|
|
101
|
+
otpToken: otpToken || "",
|
|
102
|
+
code: otp,
|
|
103
|
+
countryCode,
|
|
104
|
+
})
|
|
105
|
+
|
|
95
106
|
if (!result.success) {
|
|
96
107
|
throw new Error(result.error || "Invalid verification code")
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
setStep("success")
|
|
100
|
-
window.setTimeout(() =>
|
|
111
|
+
window.setTimeout(() => {
|
|
112
|
+
setShowOtpModal(false)
|
|
113
|
+
onLogin()
|
|
114
|
+
}, 2000)
|
|
101
115
|
} catch (err) {
|
|
116
|
+
if (isNextRedirect(err)) throw err
|
|
102
117
|
setError(err instanceof Error ? err.message : "Verification failed")
|
|
103
118
|
} finally {
|
|
104
119
|
setPending(false)
|
|
@@ -111,7 +126,7 @@ export default function RegisterForm({
|
|
|
111
126
|
setError(null)
|
|
112
127
|
|
|
113
128
|
try {
|
|
114
|
-
const result = await
|
|
129
|
+
const result = await sendRegistrationOtp(customerId, verificationMethod)
|
|
115
130
|
if (!result.success) {
|
|
116
131
|
throw new Error(result.error || "Failed to resend code")
|
|
117
132
|
}
|
|
@@ -124,49 +139,8 @@ export default function RegisterForm({
|
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<div className="text-center space-y-4 py-6">
|
|
130
|
-
<div className="w-14 h-14 mx-auto rounded-full bg-green-100 flex items-center justify-center text-2xl">
|
|
131
|
-
✓
|
|
132
|
-
</div>
|
|
133
|
-
<h2 className="section-heading text-lg">Account verified</h2>
|
|
134
|
-
<p className="text-sm text-muted">
|
|
135
|
-
Your account is ready. Redirecting you to sign in…
|
|
136
|
-
</p>
|
|
137
|
-
</div>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (step === "otp") {
|
|
142
|
-
return (
|
|
143
|
-
<form onSubmit={handleVerifyOtp} className="space-y-5">
|
|
144
|
-
<div className="text-center space-y-2">
|
|
145
|
-
<h2 className="section-heading text-lg">Verify your account</h2>
|
|
146
|
-
<p className="text-sm text-muted">
|
|
147
|
-
Enter the 6-digit code sent to your{" "}
|
|
148
|
-
{verificationMethod === "phone_verification" ? "phone" : "email"}.
|
|
149
|
-
</p>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
<OtpInput value={otp} onChange={setOtp} autoFocus />
|
|
153
|
-
{error && <p className="text-sm text-brand-sale text-center">{error}</p>}
|
|
154
|
-
|
|
155
|
-
<button type="submit" disabled={pending || otp.length < 6} className="btn-primary w-full disabled:opacity-60">
|
|
156
|
-
{pending ? "Verifying…" : "Verify account"}
|
|
157
|
-
</button>
|
|
158
|
-
|
|
159
|
-
<button
|
|
160
|
-
type="button"
|
|
161
|
-
disabled={pending}
|
|
162
|
-
onClick={handleResendOtp}
|
|
163
|
-
className="text-sm text-muted hover:text-brand-accent w-full text-center disabled:opacity-60"
|
|
164
|
-
>
|
|
165
|
-
Resend code
|
|
166
|
-
</button>
|
|
167
|
-
</form>
|
|
168
|
-
)
|
|
169
|
-
}
|
|
142
|
+
const verificationTarget =
|
|
143
|
+
verificationMethod === "email_verification" ? form.email : form.phone
|
|
170
144
|
|
|
171
145
|
return (
|
|
172
146
|
<div className="space-y-6">
|
|
@@ -181,57 +155,73 @@ export default function RegisterForm({
|
|
|
181
155
|
</div>
|
|
182
156
|
</div>
|
|
183
157
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
158
|
+
{step === "form" && (
|
|
159
|
+
<>
|
|
160
|
+
<div className="flex gap-2 p-1 bg-surface-muted rounded-lg">
|
|
161
|
+
<VerificationToggle
|
|
162
|
+
active={verificationMethod === "email_verification"}
|
|
163
|
+
onClick={() => setVerificationMethod("email_verification")}
|
|
164
|
+
>
|
|
165
|
+
Email OTP
|
|
166
|
+
</VerificationToggle>
|
|
167
|
+
<VerificationToggle
|
|
168
|
+
active={verificationMethod === "phone_verification"}
|
|
169
|
+
onClick={() => setVerificationMethod("phone_verification")}
|
|
170
|
+
>
|
|
171
|
+
Mobile OTP
|
|
172
|
+
</VerificationToggle>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<p className="text-xs text-muted -mt-2">
|
|
176
|
+
{verificationMethod === "email_verification"
|
|
177
|
+
? "We will send a verification code to your email after you register."
|
|
178
|
+
: "We will send a verification code to your mobile after you register."}
|
|
179
|
+
</p>
|
|
199
180
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
181
|
+
<form onSubmit={handleRegister} className="space-y-4">
|
|
182
|
+
<Field
|
|
183
|
+
label="Full name"
|
|
184
|
+
value={form.full_name}
|
|
185
|
+
onChange={(e) => updateField("full_name", e.target.value)}
|
|
186
|
+
autoComplete="name"
|
|
187
|
+
/>
|
|
188
|
+
<Field
|
|
189
|
+
label="Email"
|
|
190
|
+
type="email"
|
|
191
|
+
value={form.email}
|
|
192
|
+
onChange={(e) => updateField("email", e.target.value)}
|
|
193
|
+
autoComplete="email"
|
|
194
|
+
/>
|
|
195
|
+
<Field
|
|
196
|
+
label="Phone"
|
|
197
|
+
type="tel"
|
|
198
|
+
value={form.phone}
|
|
199
|
+
onChange={(e) => updateField("phone", e.target.value)}
|
|
200
|
+
autoComplete="tel"
|
|
201
|
+
required={verificationMethod === "phone_verification"}
|
|
202
|
+
/>
|
|
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"}
|
|
215
|
+
</button>
|
|
216
|
+
</form>
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{step !== "form" && !showOtpModal && (
|
|
221
|
+
<p className="text-sm text-muted text-center">
|
|
222
|
+
Complete verification to activate your account.
|
|
223
|
+
</p>
|
|
224
|
+
)}
|
|
235
225
|
|
|
236
226
|
<p className="text-sm text-center text-muted">
|
|
237
227
|
Already have an account?{" "}
|
|
@@ -239,10 +229,46 @@ export default function RegisterForm({
|
|
|
239
229
|
Sign in
|
|
240
230
|
</button>
|
|
241
231
|
</p>
|
|
232
|
+
|
|
233
|
+
<OtpComponent
|
|
234
|
+
open={showOtpModal}
|
|
235
|
+
preventClose={step === "otp"}
|
|
236
|
+
title={
|
|
237
|
+
step === "success"
|
|
238
|
+
? "Account verified"
|
|
239
|
+
: verificationMethod === "email_verification"
|
|
240
|
+
? "Verify your email"
|
|
241
|
+
: "Verify your mobile"
|
|
242
|
+
}
|
|
243
|
+
description={
|
|
244
|
+
step === "success"
|
|
245
|
+
? "Your account is ready."
|
|
246
|
+
: `We've sent a 6-digit verification code to ${verificationTarget || "your contact"}.`
|
|
247
|
+
}
|
|
248
|
+
otp={otp}
|
|
249
|
+
onOtpChange={setOtp}
|
|
250
|
+
error={error}
|
|
251
|
+
pending={pending}
|
|
252
|
+
success={step === "success"}
|
|
253
|
+
successTitle="Account verified"
|
|
254
|
+
successMessage="Redirecting you to sign in…"
|
|
255
|
+
verifyLabel="Verify & continue"
|
|
256
|
+
onVerify={handleVerifyOtp}
|
|
257
|
+
onResend={handleResendOtp}
|
|
258
|
+
/>
|
|
242
259
|
</div>
|
|
243
260
|
)
|
|
244
261
|
}
|
|
245
262
|
|
|
263
|
+
function isNextRedirect(err: unknown) {
|
|
264
|
+
return (
|
|
265
|
+
typeof err === "object" &&
|
|
266
|
+
err !== null &&
|
|
267
|
+
"digest" in err &&
|
|
268
|
+
String((err as { digest?: string }).digest || "").includes("NEXT_REDIRECT")
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
function VerificationToggle({
|
|
247
273
|
active,
|
|
248
274
|
onClick,
|
package/src/segment.tsx
CHANGED
|
@@ -6,12 +6,24 @@ import LoginForm from "./login-form"
|
|
|
6
6
|
import RegisterForm from "./register-form"
|
|
7
7
|
import ForgotPasswordForm from "./forgot-password-form"
|
|
8
8
|
import AccountOverview from "./account-overview"
|
|
9
|
+
import AccountProfile from "./account-profile"
|
|
10
|
+
import AccountAddresses from "./account-addresses"
|
|
11
|
+
import AccountOrders from "./account-orders"
|
|
12
|
+
import AccountPaymentMethods from "./account-payment-methods"
|
|
13
|
+
import AccountGuestOrders from "./account-guest-orders"
|
|
14
|
+
import { resolveOtpComponent, type OtpVerificationComponent } from "./otp-component"
|
|
15
|
+
import { accountSectionPath } from "./account-utils"
|
|
9
16
|
|
|
10
17
|
type AccountProps = {
|
|
11
18
|
countryCode?: string
|
|
12
19
|
path?: string
|
|
13
20
|
customer?: HttpTypes.StoreCustomer | null
|
|
14
21
|
orders?: HttpTypes.StoreOrder[]
|
|
22
|
+
paymentDetails?: Record<string, unknown>[]
|
|
23
|
+
guestOrders?: HttpTypes.StoreOrder[]
|
|
24
|
+
title?: string
|
|
25
|
+
subtitle?: string
|
|
26
|
+
otpComponent?: OtpVerificationComponent
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
export default function LoginTemplate({
|
|
@@ -19,17 +31,58 @@ export default function LoginTemplate({
|
|
|
19
31
|
path = "",
|
|
20
32
|
customer,
|
|
21
33
|
orders = [],
|
|
34
|
+
paymentDetails = [],
|
|
35
|
+
guestOrders = [],
|
|
36
|
+
title,
|
|
37
|
+
subtitle,
|
|
38
|
+
otpComponent,
|
|
22
39
|
}: AccountProps) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
const OtpComponent = resolveOtpComponent(otpComponent)
|
|
41
|
+
const section = accountSectionPath(path)
|
|
42
|
+
|
|
43
|
+
if (section === "guest-orders") {
|
|
44
|
+
return <AccountGuestOrders orders={guestOrders} countryCode={countryCode} />
|
|
45
|
+
}
|
|
26
46
|
|
|
27
47
|
if (customer && customer.id !== "pending_deletion") {
|
|
28
|
-
|
|
48
|
+
switch (section) {
|
|
49
|
+
case "profile":
|
|
50
|
+
return (
|
|
51
|
+
<AccountProfile
|
|
52
|
+
customer={customer}
|
|
53
|
+
countryCode={countryCode}
|
|
54
|
+
OtpComponent={OtpComponent}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
case "addresses":
|
|
58
|
+
return <AccountAddresses customer={customer} countryCode={countryCode} />
|
|
59
|
+
case "orders":
|
|
60
|
+
return <AccountOrders orders={orders} />
|
|
61
|
+
case "payment-methods":
|
|
62
|
+
return (
|
|
63
|
+
<AccountPaymentMethods
|
|
64
|
+
paymentDetails={paymentDetails as Parameters<typeof AccountPaymentMethods>[0]["paymentDetails"]}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
case "overview":
|
|
68
|
+
default:
|
|
69
|
+
return <AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
|
|
70
|
+
}
|
|
29
71
|
}
|
|
30
72
|
|
|
73
|
+
const [view, setView] = useState<"login" | "register" | "forgot">(
|
|
74
|
+
path === "register" ? "register" : path === "forgot-password" ? "forgot" : "login"
|
|
75
|
+
)
|
|
76
|
+
|
|
31
77
|
return (
|
|
32
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
|
+
|
|
33
86
|
{view !== "forgot" && (
|
|
34
87
|
<div className="flex gap-6 mb-8 border-b border-cart-border">
|
|
35
88
|
<TabButton active={view === "login"} onClick={() => setView("login")}>
|
|
@@ -44,12 +97,17 @@ export default function LoginTemplate({
|
|
|
44
97
|
{view === "login" && (
|
|
45
98
|
<LoginForm
|
|
46
99
|
countryCode={countryCode}
|
|
100
|
+
OtpComponent={OtpComponent}
|
|
47
101
|
onForgot={() => setView("forgot")}
|
|
48
102
|
onRegister={() => setView("register")}
|
|
49
103
|
/>
|
|
50
104
|
)}
|
|
51
105
|
{view === "register" && (
|
|
52
|
-
<RegisterForm
|
|
106
|
+
<RegisterForm
|
|
107
|
+
countryCode={countryCode}
|
|
108
|
+
OtpComponent={OtpComponent}
|
|
109
|
+
onLogin={() => setView("login")}
|
|
110
|
+
/>
|
|
53
111
|
)}
|
|
54
112
|
{view === "forgot" && <ForgotPasswordForm onBack={() => setView("login")} />}
|
|
55
113
|
</div>
|