@pradip1995/segment-login-template 0.2.5 → 0.4.0
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 +9 -2
- 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 +111 -0
- 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 +307 -0
- package/src/commerce-auth-otp-modal.tsx +104 -0
- package/src/forgot-password-form.tsx +89 -0
- package/src/google-auth-section.tsx +86 -0
- package/src/index.ts +5 -0
- package/src/login-form.tsx +411 -0
- package/src/otp-component.ts +14 -0
- package/src/otp-input.tsx +46 -0
- package/src/otp-verification-modal.tsx +112 -0
- package/src/register-form.tsx +324 -0
- package/src/reset-password-form.tsx +110 -0
- package/src/reset-password-page.tsx +12 -0
- package/src/segment.tsx +81 -200
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
registerCustomer,
|
|
6
|
+
sendRegistrationOtp,
|
|
7
|
+
verifyRegistrationOtp,
|
|
8
|
+
} from "./auth-server"
|
|
9
|
+
import GoogleAuthSection from "./google-auth-section"
|
|
10
|
+
import OtpVerificationModal from "./otp-verification-modal"
|
|
11
|
+
import type { OtpVerificationComponent } from "./otp-component"
|
|
12
|
+
|
|
13
|
+
type VerificationMethod = "email_verification" | "phone_verification"
|
|
14
|
+
type RegistrationStep = "form" | "otp" | "success"
|
|
15
|
+
|
|
16
|
+
export default function RegisterForm({
|
|
17
|
+
countryCode,
|
|
18
|
+
OtpComponent = OtpVerificationModal,
|
|
19
|
+
onLogin,
|
|
20
|
+
}: {
|
|
21
|
+
countryCode: string
|
|
22
|
+
OtpComponent?: OtpVerificationComponent
|
|
23
|
+
onLogin: () => void
|
|
24
|
+
}) {
|
|
25
|
+
const [step, setStep] = useState<RegistrationStep>("form")
|
|
26
|
+
const [verificationMethod, setVerificationMethod] =
|
|
27
|
+
useState<VerificationMethod>("email_verification")
|
|
28
|
+
const [form, setForm] = useState({
|
|
29
|
+
full_name: "",
|
|
30
|
+
email: "",
|
|
31
|
+
phone: "",
|
|
32
|
+
password: "",
|
|
33
|
+
})
|
|
34
|
+
const [customerId, setCustomerId] = useState<string | null>(null)
|
|
35
|
+
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
36
|
+
const [otp, setOtp] = useState("")
|
|
37
|
+
const [error, setError] = useState<string | null>(null)
|
|
38
|
+
const [pending, setPending] = useState(false)
|
|
39
|
+
const [showOtpModal, setShowOtpModal] = useState(false)
|
|
40
|
+
|
|
41
|
+
function updateField(key: keyof typeof form, value: string) {
|
|
42
|
+
setForm((prev) => ({ ...prev, [key]: value }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleRegister(e: React.FormEvent) {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
setPending(true)
|
|
48
|
+
setError(null)
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (verificationMethod === "phone_verification" && !form.phone.trim()) {
|
|
52
|
+
throw new Error("Phone number is required for mobile verification")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const [first_name, ...rest] = form.full_name.trim().split(/\s+/)
|
|
56
|
+
const last_name = rest.join(" ") || "."
|
|
57
|
+
|
|
58
|
+
const result = await registerCustomer({
|
|
59
|
+
email: form.email.trim(),
|
|
60
|
+
first_name: first_name || "",
|
|
61
|
+
last_name,
|
|
62
|
+
phone: form.phone || undefined,
|
|
63
|
+
password: form.password,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
throw new Error(result.error || "Registration failed")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const id = result.customer?.id
|
|
71
|
+
if (!id) {
|
|
72
|
+
throw new Error("Account was created but customer id is missing")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setCustomerId(id)
|
|
76
|
+
|
|
77
|
+
const otpResult = await sendRegistrationOtp(id, verificationMethod)
|
|
78
|
+
if (!otpResult.success) {
|
|
79
|
+
throw new Error(otpResult.error || "Failed to send verification code")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setOtpToken(otpResult.token ?? null)
|
|
83
|
+
setOtp("")
|
|
84
|
+
setStep("otp")
|
|
85
|
+
setShowOtpModal(true)
|
|
86
|
+
} catch (err) {
|
|
87
|
+
setError(err instanceof Error ? err.message : "Registration failed")
|
|
88
|
+
} finally {
|
|
89
|
+
setPending(false)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handleVerifyOtp() {
|
|
94
|
+
if (!otp || otp.length < 6) return
|
|
95
|
+
|
|
96
|
+
setPending(true)
|
|
97
|
+
setError(null)
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await verifyRegistrationOtp({
|
|
101
|
+
otpToken: otpToken || "",
|
|
102
|
+
code: otp,
|
|
103
|
+
countryCode,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
throw new Error(result.error || "Invalid verification code")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setStep("success")
|
|
111
|
+
window.setTimeout(() => {
|
|
112
|
+
setShowOtpModal(false)
|
|
113
|
+
onLogin()
|
|
114
|
+
}, 2000)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (isNextRedirect(err)) throw err
|
|
117
|
+
setError(err instanceof Error ? err.message : "Verification failed")
|
|
118
|
+
} finally {
|
|
119
|
+
setPending(false)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handleResendOtp() {
|
|
124
|
+
if (!customerId) return
|
|
125
|
+
setPending(true)
|
|
126
|
+
setError(null)
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await sendRegistrationOtp(customerId, verificationMethod)
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
throw new Error(result.error || "Failed to resend code")
|
|
132
|
+
}
|
|
133
|
+
setOtpToken(result.token ?? null)
|
|
134
|
+
setOtp("")
|
|
135
|
+
} catch (err) {
|
|
136
|
+
setError(err instanceof Error ? err.message : "Failed to resend code")
|
|
137
|
+
} finally {
|
|
138
|
+
setPending(false)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const verificationTarget =
|
|
143
|
+
verificationMethod === "email_verification" ? form.email : form.phone
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="space-y-6">
|
|
147
|
+
<GoogleAuthSection countryCode={countryCode} />
|
|
148
|
+
|
|
149
|
+
<div className="relative">
|
|
150
|
+
<div className="absolute inset-0 flex items-center">
|
|
151
|
+
<div className="w-full border-t border-cart-border" />
|
|
152
|
+
</div>
|
|
153
|
+
<div className="relative flex justify-center text-xs uppercase tracking-[var(--letter-spacing-nav)]">
|
|
154
|
+
<span className="bg-page-bg px-3 text-muted">Or register with email</span>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
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>
|
|
180
|
+
|
|
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
|
+
)}
|
|
225
|
+
|
|
226
|
+
<p className="text-sm text-center text-muted">
|
|
227
|
+
Already have an account?{" "}
|
|
228
|
+
<button type="button" onClick={onLogin} className="text-brand-accent font-semibold hover:underline">
|
|
229
|
+
Sign in
|
|
230
|
+
</button>
|
|
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
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
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
|
+
|
|
272
|
+
function VerificationToggle({
|
|
273
|
+
active,
|
|
274
|
+
onClick,
|
|
275
|
+
children,
|
|
276
|
+
}: {
|
|
277
|
+
active: boolean
|
|
278
|
+
onClick: () => void
|
|
279
|
+
children: React.ReactNode
|
|
280
|
+
}) {
|
|
281
|
+
return (
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={onClick}
|
|
285
|
+
className={`flex-1 py-2 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] rounded-md transition-colors ${
|
|
286
|
+
active ? "bg-page-bg text-heading shadow-sm" : "text-muted hover:text-heading"
|
|
287
|
+
}`}
|
|
288
|
+
>
|
|
289
|
+
{children}
|
|
290
|
+
</button>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function Field({
|
|
295
|
+
label,
|
|
296
|
+
type = "text",
|
|
297
|
+
value,
|
|
298
|
+
onChange,
|
|
299
|
+
autoComplete,
|
|
300
|
+
required = true,
|
|
301
|
+
}: {
|
|
302
|
+
label: string
|
|
303
|
+
type?: string
|
|
304
|
+
value: string
|
|
305
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
306
|
+
autoComplete?: string
|
|
307
|
+
required?: boolean
|
|
308
|
+
}) {
|
|
309
|
+
return (
|
|
310
|
+
<label className="block">
|
|
311
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
312
|
+
{label}
|
|
313
|
+
</span>
|
|
314
|
+
<input
|
|
315
|
+
type={type}
|
|
316
|
+
value={value}
|
|
317
|
+
onChange={onChange}
|
|
318
|
+
autoComplete={autoComplete}
|
|
319
|
+
required={required}
|
|
320
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
321
|
+
/>
|
|
322
|
+
</label>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { useSearchParams } from "next/navigation"
|
|
5
|
+
import { completePasswordResetAction } from "./auth-server"
|
|
6
|
+
|
|
7
|
+
export default function ResetPasswordForm({ countryCode }: { countryCode: string }) {
|
|
8
|
+
const searchParams = useSearchParams()
|
|
9
|
+
const token = searchParams.get("token") || ""
|
|
10
|
+
const email = searchParams.get("email") || ""
|
|
11
|
+
|
|
12
|
+
const [password, setPassword] = useState("")
|
|
13
|
+
const [confirmPassword, setConfirmPassword] = useState("")
|
|
14
|
+
const [error, setError] = useState<string | null>(null)
|
|
15
|
+
const [pending, setPending] = useState(false)
|
|
16
|
+
|
|
17
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
18
|
+
e.preventDefault()
|
|
19
|
+
setError(null)
|
|
20
|
+
|
|
21
|
+
if (!token || !email) {
|
|
22
|
+
setError("This reset link is invalid or expired. Request a new one from the sign-in page.")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (password.length < 8) {
|
|
27
|
+
setError("Password must be at least 8 characters.")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (password !== confirmPassword) {
|
|
32
|
+
setError("Passwords do not match.")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setPending(true)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = await completePasswordResetAction({
|
|
40
|
+
email,
|
|
41
|
+
password,
|
|
42
|
+
token,
|
|
43
|
+
countryCode,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (result && !result.success) {
|
|
47
|
+
setError(result.error || "Failed to reset password")
|
|
48
|
+
setPending(false)
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (isNextRedirect(err)) throw err
|
|
52
|
+
setError(err instanceof Error ? err.message : "Failed to reset password")
|
|
53
|
+
setPending(false)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="w-full max-w-md mx-auto space-y-6">
|
|
59
|
+
<div className="text-center space-y-2">
|
|
60
|
+
<h1 className="section-heading text-xl">Set a new password</h1>
|
|
61
|
+
<p className="text-sm text-muted">Choose a strong password for {email || "your account"}.</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
65
|
+
<label className="block">
|
|
66
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
67
|
+
New password
|
|
68
|
+
</span>
|
|
69
|
+
<input
|
|
70
|
+
type="password"
|
|
71
|
+
value={password}
|
|
72
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
73
|
+
required
|
|
74
|
+
autoComplete="new-password"
|
|
75
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
76
|
+
/>
|
|
77
|
+
</label>
|
|
78
|
+
|
|
79
|
+
<label className="block">
|
|
80
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
81
|
+
Confirm password
|
|
82
|
+
</span>
|
|
83
|
+
<input
|
|
84
|
+
type="password"
|
|
85
|
+
value={confirmPassword}
|
|
86
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
87
|
+
required
|
|
88
|
+
autoComplete="new-password"
|
|
89
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
90
|
+
/>
|
|
91
|
+
</label>
|
|
92
|
+
|
|
93
|
+
{error && <p className="text-sm text-brand-sale">{error}</p>}
|
|
94
|
+
|
|
95
|
+
<button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
|
|
96
|
+
{pending ? "Updating…" : "Update password"}
|
|
97
|
+
</button>
|
|
98
|
+
</form>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isNextRedirect(err: unknown) {
|
|
104
|
+
return (
|
|
105
|
+
typeof err === "object" &&
|
|
106
|
+
err !== null &&
|
|
107
|
+
"digest" in err &&
|
|
108
|
+
String((err as { digest?: string }).digest || "").includes("NEXT_REDIRECT")
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Suspense } from "react"
|
|
4
|
+
import ResetPasswordForm from "./reset-password-form"
|
|
5
|
+
|
|
6
|
+
export default function ResetPasswordPage({ countryCode = "in" }: { countryCode?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<Suspense fallback={<p className="text-sm text-muted text-center py-8">Loading…</p>}>
|
|
9
|
+
<ResetPasswordForm countryCode={countryCode} />
|
|
10
|
+
</Suspense>
|
|
11
|
+
)
|
|
12
|
+
}
|