@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.
@@ -3,24 +3,28 @@
3
3
  import { useState } from "react"
4
4
  import {
5
5
  registerCustomer,
6
- sendCustomerOTP,
7
- verifyCustomerOTP,
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<"form" | "otp" | "success">("form")
25
+ const [step, setStep] = useState<RegistrationStep>("form")
22
26
  const [verificationMethod, setVerificationMethod] =
23
- useState<VerificationMethod>("phone_verification")
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 sendCustomerOTP(id, verificationMethod)
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(e: React.FormEvent) {
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 verifyCustomerOTP(otpToken || "", otp)
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(() => onLogin(), 2000)
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 sendCustomerOTP(customerId, verificationMethod)
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
- if (step === "success") {
128
- return (
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
- <form onSubmit={handleRegister} className="space-y-4">
185
- <div className="flex gap-2 p-1 bg-surface-muted rounded-lg">
186
- <VerificationToggle
187
- active={verificationMethod === "phone_verification"}
188
- onClick={() => setVerificationMethod("phone_verification")}
189
- >
190
- Mobile
191
- </VerificationToggle>
192
- <VerificationToggle
193
- active={verificationMethod === "email_verification"}
194
- onClick={() => setVerificationMethod("email_verification")}
195
- >
196
- Email OTP
197
- </VerificationToggle>
198
- </div>
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
- <Field
201
- label="Full name"
202
- value={form.full_name}
203
- onChange={(e) => updateField("full_name", e.target.value)}
204
- autoComplete="name"
205
- />
206
- <Field
207
- label="Email"
208
- type="email"
209
- value={form.email}
210
- onChange={(e) => updateField("email", e.target.value)}
211
- autoComplete="email"
212
- />
213
- <Field
214
- label="Phone"
215
- type="tel"
216
- value={form.phone}
217
- onChange={(e) => updateField("phone", e.target.value)}
218
- autoComplete="tel"
219
- required={verificationMethod === "phone_verification"}
220
- />
221
- <Field
222
- label="Password"
223
- type="password"
224
- value={form.password}
225
- onChange={(e) => updateField("password", e.target.value)}
226
- autoComplete="new-password"
227
- />
228
-
229
- {error && <p className="text-sm text-brand-sale">{error}</p>}
230
-
231
- <button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
232
- {pending ? "Creating account…" : "Create account"}
233
- </button>
234
- </form>
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 [view, setView] = useState<"login" | "register" | "forgot">(
24
- path === "register" ? "register" : path === "forgot-password" ? "forgot" : "login"
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
- return <AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
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 countryCode={countryCode} onLogin={() => setView("login")} />
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>