@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.
@@ -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 { initiateGoogleAuth } from "@pradip1995/commerce-core/client/actions/customer"
4
- import { GoogleLoginButton } from "@pradip1995/commerce-auth/components/google-login"
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
- <GoogleLoginButton
17
- countryCode={countryCode}
18
- defaultCountryCode="in"
19
- initiateAuth={initiateGoogleAuth}
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
- googleIconSrc="/Google.svg"
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"
@@ -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">{state}</p>
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({ countryCode }: { countryCode: string }) {
120
- const router = useRouter()
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 [step, setStep] = useState<"identifier" | "otp">("identifier")
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
- setStep("otp")
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
- setStep("otp")
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 handleVerifyOtp(e: React.FormEvent) {
169
- e.preventDefault()
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
- router.push(`/${countryCode}/account/guest-orders`)
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
- if (step === "otp") {
208
- return (
209
- <form onSubmit={handleVerifyOtp} className="space-y-4">
210
- <p className="text-sm text-muted text-center">
211
- Enter the 6-digit code sent to <span className="font-medium text-heading">{identifier}</span>
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
- {isNewUser && authMethod !== "guest" && (
215
- <div className="grid grid-cols-2 gap-3">
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="button"
240
- className="text-sm text-muted hover:text-brand-accent w-full text-center"
241
- onClick={() => {
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
- Use a different {authMethod === "phone_auth" ? "phone" : "email"}
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
- <Field
268
- label={authMethod === "phone_auth" ? "Phone" : "Email"}
269
- name="identifier"
270
- type={authMethod === "phone_auth" ? "tel" : "email"}
271
- value={identifier}
272
- onChange={(e) => setIdentifier(e.target.value)}
273
- autoComplete={authMethod === "phone_auth" ? "tel" : "email"}
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&apos;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
+ }