@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.
@@ -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
+ }