@pradip1995/segment-login-template 0.5.8 → 0.5.10
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 +3 -1
- package/src/account-profile-logic.ts +214 -0
- package/src/account-profile.tsx +41 -120
- package/src/auth-server.ts +32 -11
- package/src/profile-phone-field.tsx +83 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pradip1995/segment-login-template",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"./commerce-auth-otp-modal": "./src/commerce-auth-otp-modal.tsx",
|
|
18
18
|
"./otp-component": "./src/otp-component.ts",
|
|
19
19
|
"./auth-server": "./src/auth-server.ts",
|
|
20
|
+
"./account-profile-logic": "./src/account-profile-logic.ts",
|
|
21
|
+
"./profile-phone-field": "./src/profile-phone-field.tsx",
|
|
20
22
|
"./google-auth-section": "./src/google-auth-section.tsx",
|
|
21
23
|
"./otp-input": "./src/otp-input.tsx"
|
|
22
24
|
},
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useActionState, useState } from "react"
|
|
4
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import { updateCustomer } from "@pradip1995/commerce-core/data/customer"
|
|
6
|
+
import {
|
|
7
|
+
combinePhoneParts,
|
|
8
|
+
isValidPhoneParts,
|
|
9
|
+
parsePhoneParts,
|
|
10
|
+
} from "@pradip1995/commerce-core/util/phone"
|
|
11
|
+
import {
|
|
12
|
+
sendRegistrationOtp,
|
|
13
|
+
verifyRegistrationOtp,
|
|
14
|
+
} from "./auth-server"
|
|
15
|
+
import { isNextRedirect, safeErrorMessage } from "./is-next-redirect"
|
|
16
|
+
|
|
17
|
+
export function customerPhoneToInputValue(phone?: string | null): string {
|
|
18
|
+
if (!phone?.trim()) return ""
|
|
19
|
+
const parts = parsePhoneParts(phone)
|
|
20
|
+
return combinePhoneParts(parts.dialCode, parts.localNumber)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function phoneDigits(value: string): string {
|
|
24
|
+
return value.replace(/\D/g, "")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function phoneToE164(value: string): string {
|
|
28
|
+
const parts = parsePhoneParts(value)
|
|
29
|
+
const local = parts.localNumber.replace(/\D/g, "")
|
|
30
|
+
const dial = parts.dialCode.replace(/\D/g, "")
|
|
31
|
+
if (!local) return ""
|
|
32
|
+
return `+${dial}${local}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readVerified(
|
|
36
|
+
customer: HttpTypes.StoreCustomer,
|
|
37
|
+
field: "email_verified" | "phone_verified"
|
|
38
|
+
): boolean {
|
|
39
|
+
const direct = (customer as Record<string, unknown>)[field]
|
|
40
|
+
if (direct === true || direct === "true") return true
|
|
41
|
+
const meta = customer.metadata as Record<string, unknown> | undefined
|
|
42
|
+
const fromMeta = meta?.[field]
|
|
43
|
+
return fromMeta === true || fromMeta === "true"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useAccountProfileLogic(
|
|
47
|
+
customer: HttpTypes.StoreCustomer,
|
|
48
|
+
countryCode: string
|
|
49
|
+
) {
|
|
50
|
+
const [message, setMessage] = useState<string | null>(null)
|
|
51
|
+
const [phone, setPhone] = useState(() => customerPhoneToInputValue(customer.phone))
|
|
52
|
+
const [phoneError, setPhoneError] = useState<string | null>(null)
|
|
53
|
+
const [otpOpen, setOtpOpen] = useState(false)
|
|
54
|
+
const [otp, setOtp] = useState("")
|
|
55
|
+
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
56
|
+
const [otpType, setOtpType] = useState<"email_verification" | "phone_verification">(
|
|
57
|
+
"email_verification"
|
|
58
|
+
)
|
|
59
|
+
const [otpError, setOtpError] = useState<string | null>(null)
|
|
60
|
+
const [otpPending, setOtpPending] = useState(false)
|
|
61
|
+
const [phoneVerified, setPhoneVerified] = useState(() =>
|
|
62
|
+
readVerified(customer, "phone_verified")
|
|
63
|
+
)
|
|
64
|
+
const [emailVerified] = useState(() => readVerified(customer, "email_verified"))
|
|
65
|
+
|
|
66
|
+
const phoneParts = parsePhoneParts(phone)
|
|
67
|
+
const phoneReady = isValidPhoneParts(phoneParts.dialCode, phoneParts.localNumber)
|
|
68
|
+
|
|
69
|
+
async function saveProfile(_prev: unknown, formData: FormData) {
|
|
70
|
+
try {
|
|
71
|
+
const fullName = String(formData.get("full_name") || "").trim()
|
|
72
|
+
const [first_name, ...rest] = fullName.split(/\s+/)
|
|
73
|
+
const e164 = phoneToE164(phone)
|
|
74
|
+
await updateCustomer({
|
|
75
|
+
first_name: first_name || "",
|
|
76
|
+
last_name: rest.join(" ") || ".",
|
|
77
|
+
phone: e164 || undefined,
|
|
78
|
+
})
|
|
79
|
+
if (e164 && phoneDigits(e164) !== phoneDigits(customer.phone || "")) {
|
|
80
|
+
setPhoneVerified(false)
|
|
81
|
+
}
|
|
82
|
+
return { ok: true as const, error: null as string | null }
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (isNextRedirect(err)) throw err
|
|
85
|
+
return {
|
|
86
|
+
ok: false as const,
|
|
87
|
+
error: safeErrorMessage(err, "Failed to update profile"),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [state, formAction, pending] = useActionState(saveProfile, {
|
|
93
|
+
ok: false as const,
|
|
94
|
+
error: null as string | null,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
async function ensurePhoneSavedForVerification(): Promise<boolean> {
|
|
98
|
+
setPhoneError(null)
|
|
99
|
+
if (!phoneReady) {
|
|
100
|
+
setPhoneError("Please enter a valid mobile number before verifying.")
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const e164 = phoneToE164(phone)
|
|
105
|
+
if (phoneDigits(e164) === phoneDigits(customer.phone || "")) {
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await updateCustomer({ phone: e164 })
|
|
111
|
+
return true
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (isNextRedirect(err)) throw err
|
|
114
|
+
setPhoneError(
|
|
115
|
+
safeErrorMessage(err, "Failed to save mobile number. Please try again.")
|
|
116
|
+
)
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function sendVerifyOtp(type: "email_verification" | "phone_verification") {
|
|
122
|
+
setOtpType(type)
|
|
123
|
+
setOtpPending(true)
|
|
124
|
+
setOtpError(null)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
if (type === "phone_verification") {
|
|
128
|
+
const saved = await ensurePhoneSavedForVerification()
|
|
129
|
+
if (!saved) return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const res = await sendRegistrationOtp(customer.id, type)
|
|
133
|
+
if (!res?.success) {
|
|
134
|
+
throw new Error(res?.error || "Failed to send code")
|
|
135
|
+
}
|
|
136
|
+
setOtpToken(res.token ?? null)
|
|
137
|
+
setOtp("")
|
|
138
|
+
setOtpOpen(true)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (isNextRedirect(err)) throw err
|
|
141
|
+
const msg = safeErrorMessage(err, "Failed to send code")
|
|
142
|
+
setOtpError(msg)
|
|
143
|
+
if (type === "phone_verification") {
|
|
144
|
+
setPhoneError(msg)
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
setOtpPending(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function verifyOtp() {
|
|
152
|
+
if (otp.length < 6) return
|
|
153
|
+
setOtpPending(true)
|
|
154
|
+
setOtpError(null)
|
|
155
|
+
try {
|
|
156
|
+
const res = await verifyRegistrationOtp({
|
|
157
|
+
otpToken: otpToken || "",
|
|
158
|
+
code: otp,
|
|
159
|
+
countryCode,
|
|
160
|
+
skipRedirect: true,
|
|
161
|
+
})
|
|
162
|
+
if (!res?.success) {
|
|
163
|
+
throw new Error(res?.error || "Invalid code")
|
|
164
|
+
}
|
|
165
|
+
setOtpOpen(false)
|
|
166
|
+
if (otpType === "phone_verification") {
|
|
167
|
+
setPhoneVerified(true)
|
|
168
|
+
}
|
|
169
|
+
setMessage("Verification successful.")
|
|
170
|
+
window.location.reload()
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (isNextRedirect(err)) throw err
|
|
173
|
+
setOtpError(safeErrorMessage(err, "Verification failed"))
|
|
174
|
+
setOtpPending(false)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handlePhoneChange(value: string) {
|
|
179
|
+
setPhone(value)
|
|
180
|
+
setPhoneError(null)
|
|
181
|
+
const nextE164 = phoneToE164(value)
|
|
182
|
+
if (
|
|
183
|
+
nextE164 &&
|
|
184
|
+
phoneDigits(nextE164) !== phoneDigits(customer.phone || "")
|
|
185
|
+
) {
|
|
186
|
+
setPhoneVerified(false)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const fullName = [customer.first_name, customer.last_name].filter(Boolean).join(" ")
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
fullName,
|
|
194
|
+
phone,
|
|
195
|
+
handlePhoneChange,
|
|
196
|
+
phoneReady,
|
|
197
|
+
phoneError,
|
|
198
|
+
phoneVerified,
|
|
199
|
+
emailVerified,
|
|
200
|
+
message,
|
|
201
|
+
state,
|
|
202
|
+
formAction,
|
|
203
|
+
pending,
|
|
204
|
+
otpOpen,
|
|
205
|
+
setOtpOpen,
|
|
206
|
+
otp,
|
|
207
|
+
setOtp,
|
|
208
|
+
otpType,
|
|
209
|
+
otpError,
|
|
210
|
+
otpPending,
|
|
211
|
+
sendVerifyOtp,
|
|
212
|
+
verifyOtp,
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/account-profile.tsx
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useActionState, useState } from "react"
|
|
4
3
|
import type { HttpTypes } from "@medusajs/types"
|
|
5
|
-
import { updateCustomer } from "@pradip1995/commerce-core/data/customer"
|
|
6
|
-
import {
|
|
7
|
-
sendRegistrationOtp,
|
|
8
|
-
verifyRegistrationOtp,
|
|
9
|
-
} from "./auth-server"
|
|
10
4
|
import type { OtpVerificationComponent } from "./otp-component"
|
|
11
5
|
import OtpVerificationModal from "./otp-verification-modal"
|
|
6
|
+
import { useAccountProfileLogic } from "./account-profile-logic"
|
|
7
|
+
import ProfilePhoneField from "./profile-phone-field"
|
|
12
8
|
import { acct } from "./account-theme"
|
|
13
|
-
import { isNextRedirect, safeErrorMessage } from "./is-next-redirect"
|
|
14
9
|
|
|
15
10
|
export default function AccountProfile({
|
|
16
11
|
customer,
|
|
@@ -21,80 +16,7 @@ export default function AccountProfile({
|
|
|
21
16
|
countryCode: string
|
|
22
17
|
OtpComponent?: OtpVerificationComponent
|
|
23
18
|
}) {
|
|
24
|
-
const
|
|
25
|
-
const [otpOpen, setOtpOpen] = useState(false)
|
|
26
|
-
const [otp, setOtp] = useState("")
|
|
27
|
-
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
28
|
-
const [otpType, setOtpType] = useState<"email_verification" | "phone_verification">(
|
|
29
|
-
"email_verification"
|
|
30
|
-
)
|
|
31
|
-
const [otpError, setOtpError] = useState<string | null>(null)
|
|
32
|
-
const [otpPending, setOtpPending] = useState(false)
|
|
33
|
-
|
|
34
|
-
async function saveProfile(_prev: unknown, formData: FormData) {
|
|
35
|
-
try {
|
|
36
|
-
const fullName = String(formData.get("full_name") || "").trim()
|
|
37
|
-
const [first_name, ...rest] = fullName.split(/\s+/)
|
|
38
|
-
await updateCustomer({
|
|
39
|
-
first_name: first_name || "",
|
|
40
|
-
last_name: rest.join(" ") || ".",
|
|
41
|
-
phone: String(formData.get("phone") || "").replace(/[^\d+]/g, "") || undefined,
|
|
42
|
-
})
|
|
43
|
-
return { ok: true as const, error: null as string | null }
|
|
44
|
-
} catch (err) {
|
|
45
|
-
if (isNextRedirect(err)) throw err
|
|
46
|
-
return {
|
|
47
|
-
ok: false as const,
|
|
48
|
-
error: safeErrorMessage(err, "Failed to update profile"),
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const [state, formAction, pending] = useActionState(saveProfile, { ok: false, error: null })
|
|
54
|
-
|
|
55
|
-
async function sendVerifyOtp(type: "email_verification" | "phone_verification") {
|
|
56
|
-
setOtpType(type)
|
|
57
|
-
setOtpPending(true)
|
|
58
|
-
setOtpError(null)
|
|
59
|
-
try {
|
|
60
|
-
const res = await sendRegistrationOtp(customer.id, type)
|
|
61
|
-
if (!res.success) throw new Error(res.error || "Failed to send code")
|
|
62
|
-
setOtpToken(res.token ?? null)
|
|
63
|
-
setOtp("")
|
|
64
|
-
setOtpOpen(true)
|
|
65
|
-
} catch (err) {
|
|
66
|
-
if (isNextRedirect(err)) throw err
|
|
67
|
-
setOtpError(safeErrorMessage(err, "Failed to send code"))
|
|
68
|
-
} finally {
|
|
69
|
-
setOtpPending(false)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function verifyOtp() {
|
|
74
|
-
if (otp.length < 6) return
|
|
75
|
-
setOtpPending(true)
|
|
76
|
-
setOtpError(null)
|
|
77
|
-
try {
|
|
78
|
-
const res = await verifyRegistrationOtp({
|
|
79
|
-
otpToken: otpToken || "",
|
|
80
|
-
code: otp,
|
|
81
|
-
countryCode,
|
|
82
|
-
skipRedirect: true,
|
|
83
|
-
})
|
|
84
|
-
if (!res.success) throw new Error(res.error || "Invalid code")
|
|
85
|
-
setOtpOpen(false)
|
|
86
|
-
setMessage("Verification successful.")
|
|
87
|
-
window.location.reload()
|
|
88
|
-
} catch (err) {
|
|
89
|
-
if (isNextRedirect(err)) throw err
|
|
90
|
-
setOtpError(safeErrorMessage(err, "Verification failed"))
|
|
91
|
-
setOtpPending(false)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const fullName = [customer.first_name, customer.last_name].filter(Boolean).join(" ")
|
|
96
|
-
const emailVerified = (customer as { email_verified?: boolean }).email_verified === true
|
|
97
|
-
const phoneVerified = (customer as { phone_verified?: boolean }).phone_verified === true
|
|
19
|
+
const profile = useAccountProfileLogic(customer, countryCode)
|
|
98
20
|
|
|
99
21
|
return (
|
|
100
22
|
<div className="space-y-8">
|
|
@@ -103,8 +25,8 @@ export default function AccountProfile({
|
|
|
103
25
|
<p className={`text-sm ${acct.muted}`}>Update your name and contact information.</p>
|
|
104
26
|
</div>
|
|
105
27
|
|
|
106
|
-
<form action={formAction} className={`${acct.card} p-6 space-y-4`}>
|
|
107
|
-
<Field label="Full name" name="full_name" defaultValue={fullName} />
|
|
28
|
+
<form action={profile.formAction} className={`${acct.card} p-6 space-y-4`}>
|
|
29
|
+
<Field label="Full name" name="full_name" defaultValue={profile.fullName} />
|
|
108
30
|
<div>
|
|
109
31
|
<label className="block text-xs font-semibold uppercase tracking-wider text-muted mb-1.5">
|
|
110
32
|
Email
|
|
@@ -115,62 +37,61 @@ export default function AccountProfile({
|
|
|
115
37
|
readOnly
|
|
116
38
|
className="flex-1 min-w-[200px] border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface-muted text-muted"
|
|
117
39
|
/>
|
|
118
|
-
{!emailVerified && (
|
|
40
|
+
{!profile.emailVerified && (
|
|
119
41
|
<button
|
|
120
42
|
type="button"
|
|
121
|
-
onClick={() => sendVerifyOtp("email_verification")}
|
|
122
|
-
disabled={otpPending}
|
|
43
|
+
onClick={() => profile.sendVerifyOtp("email_verification")}
|
|
44
|
+
disabled={profile.otpPending}
|
|
123
45
|
className={`${acct.btnOutline} !py-2 !px-3`}
|
|
124
46
|
>
|
|
125
47
|
Verify email
|
|
126
48
|
</button>
|
|
127
49
|
)}
|
|
128
|
-
{emailVerified && (
|
|
129
|
-
<span className="text-xs text-green-600 font-medium">Verified</span>
|
|
130
|
-
)}
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
<div>
|
|
134
|
-
<Field label="Phone" name="phone" type="tel" defaultValue={customer.phone || ""} />
|
|
135
|
-
<div className="mt-2 flex items-center gap-3">
|
|
136
|
-
{!phoneVerified && customer.phone && (
|
|
137
|
-
<button
|
|
138
|
-
type="button"
|
|
139
|
-
onClick={() => sendVerifyOtp("phone_verification")}
|
|
140
|
-
disabled={otpPending}
|
|
141
|
-
className={`${acct.btnOutline} !py-2 !px-3`}
|
|
142
|
-
>
|
|
143
|
-
Verify phone
|
|
144
|
-
</button>
|
|
145
|
-
)}
|
|
146
|
-
{phoneVerified && customer.phone && (
|
|
50
|
+
{profile.emailVerified && (
|
|
147
51
|
<span className="text-xs text-green-600 font-medium">Verified</span>
|
|
148
52
|
)}
|
|
149
53
|
</div>
|
|
150
54
|
</div>
|
|
151
55
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
56
|
+
<ProfilePhoneField
|
|
57
|
+
phone={profile.phone}
|
|
58
|
+
onPhoneChange={profile.handlePhoneChange}
|
|
59
|
+
phoneVerified={profile.phoneVerified}
|
|
60
|
+
phoneReady={profile.phoneReady}
|
|
61
|
+
phoneError={profile.phoneError}
|
|
62
|
+
verifyPending={profile.otpPending}
|
|
63
|
+
onVerify={() => profile.sendVerifyOtp("phone_verification")}
|
|
64
|
+
verifyButtonClassName={`${acct.btnOutline} !py-2 !px-3`}
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
{(profile.state.error || profile.message) && (
|
|
68
|
+
<p
|
|
69
|
+
className={`text-sm ${profile.state.ok || profile.message ? "text-green-600" : "text-red-600"}`}
|
|
70
|
+
>
|
|
71
|
+
{profile.message || profile.state.error}
|
|
155
72
|
</p>
|
|
156
73
|
)}
|
|
157
74
|
|
|
158
|
-
<button type="submit" disabled={pending} className={acct.btnPrimary}>
|
|
159
|
-
{pending ? "Saving…" : "Save changes"}
|
|
75
|
+
<button type="submit" disabled={profile.pending} className={acct.btnPrimary}>
|
|
76
|
+
{profile.pending ? "Saving…" : "Save changes"}
|
|
160
77
|
</button>
|
|
161
78
|
</form>
|
|
162
79
|
|
|
163
80
|
<OtpComponent
|
|
164
|
-
open={otpOpen}
|
|
165
|
-
title={
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
81
|
+
open={profile.otpOpen}
|
|
82
|
+
title={
|
|
83
|
+
profile.otpType === "email_verification"
|
|
84
|
+
? "Verify your email"
|
|
85
|
+
: "Verify your phone"
|
|
86
|
+
}
|
|
87
|
+
description={`Enter the code sent to your ${profile.otpType === "email_verification" ? "email" : "phone"}.`}
|
|
88
|
+
otp={profile.otp}
|
|
89
|
+
onOtpChange={profile.setOtp}
|
|
90
|
+
error={profile.otpError}
|
|
91
|
+
pending={profile.otpPending}
|
|
92
|
+
onVerify={profile.verifyOtp}
|
|
93
|
+
onResend={() => profile.sendVerifyOtp(profile.otpType)}
|
|
94
|
+
onClose={() => profile.setOtpOpen(false)}
|
|
174
95
|
/>
|
|
175
96
|
</div>
|
|
176
97
|
)
|
package/src/auth-server.ts
CHANGED
|
@@ -258,21 +258,42 @@ export async function sendRegistrationOtp(
|
|
|
258
258
|
customerId: string,
|
|
259
259
|
type: "email_verification" | "phone_verification"
|
|
260
260
|
) {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
const response = await fetch(`${getBaseUrl()}/store/customers/otp/send`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: getHeaders(),
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
customer_id: customerId,
|
|
266
|
+
type,
|
|
267
|
+
}),
|
|
268
|
+
cache: "no-store",
|
|
269
|
+
})
|
|
265
270
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
success: false as const,
|
|
270
|
-
error:
|
|
271
|
-
"Verification email is not configured on the backend. Rebuild and restart the Medusa server with customer-registration OTP settings.",
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
const err = (await response.json().catch(() => ({}))) as {
|
|
273
|
+
message?: string
|
|
272
274
|
}
|
|
275
|
+
const message = err.message || "Failed to send verification code"
|
|
276
|
+
if (message.includes("Channel configuration not found")) {
|
|
277
|
+
return {
|
|
278
|
+
success: false as const,
|
|
279
|
+
error:
|
|
280
|
+
"Verification is not configured on the backend. Rebuild and restart the Medusa server with customer-registration OTP settings.",
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { success: false as const, error: message }
|
|
273
284
|
}
|
|
274
285
|
|
|
275
|
-
|
|
286
|
+
const data = (await response.json()) as {
|
|
287
|
+
token?: string
|
|
288
|
+
expires_at?: string
|
|
289
|
+
message?: string
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
success: true as const,
|
|
294
|
+
token: data.token,
|
|
295
|
+
message: data.message || "OTP sent successfully",
|
|
296
|
+
}
|
|
276
297
|
}
|
|
277
298
|
|
|
278
299
|
export async function initiateGoogleAuth(countryCode?: string) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { SplitPhoneInput } from "@pradip1995/commerce-core/components/split-phone-input"
|
|
4
|
+
|
|
5
|
+
export type ProfilePhoneFieldProps = {
|
|
6
|
+
phone: string
|
|
7
|
+
onPhoneChange: (value: string) => void
|
|
8
|
+
phoneVerified: boolean
|
|
9
|
+
phoneReady: boolean
|
|
10
|
+
phoneError: string | null
|
|
11
|
+
verifyPending: boolean
|
|
12
|
+
onVerify: () => void
|
|
13
|
+
label?: string
|
|
14
|
+
wrapperClassName?: string
|
|
15
|
+
labelClassName?: string
|
|
16
|
+
rowClassName?: string
|
|
17
|
+
verifyButtonClassName?: string
|
|
18
|
+
verifiedClassName?: string
|
|
19
|
+
errorClassName?: string
|
|
20
|
+
hintClassName?: string
|
|
21
|
+
phoneInputClassName?: string
|
|
22
|
+
numberInputId?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function ProfilePhoneField({
|
|
26
|
+
phone,
|
|
27
|
+
onPhoneChange,
|
|
28
|
+
phoneVerified,
|
|
29
|
+
phoneReady,
|
|
30
|
+
phoneError,
|
|
31
|
+
verifyPending,
|
|
32
|
+
onVerify,
|
|
33
|
+
label = "Phone",
|
|
34
|
+
wrapperClassName = "",
|
|
35
|
+
labelClassName = "text-xs font-semibold uppercase tracking-wider text-muted mb-1.5 block",
|
|
36
|
+
rowClassName = "flex flex-wrap items-center gap-3",
|
|
37
|
+
verifyButtonClassName = "",
|
|
38
|
+
verifiedClassName = "text-xs text-green-600 font-medium",
|
|
39
|
+
errorClassName = "text-sm text-red-600 mt-1.5",
|
|
40
|
+
hintClassName = "text-xs text-muted mt-1.5",
|
|
41
|
+
phoneInputClassName = "",
|
|
42
|
+
numberInputId = "account-profile-phone",
|
|
43
|
+
}: ProfilePhoneFieldProps) {
|
|
44
|
+
return (
|
|
45
|
+
<div className={wrapperClassName}>
|
|
46
|
+
<span className={labelClassName}>{label}</span>
|
|
47
|
+
<div className={rowClassName}>
|
|
48
|
+
<div className={`flex-1 min-w-[200px] ${phoneInputClassName}`}>
|
|
49
|
+
<SplitPhoneInput
|
|
50
|
+
value={phone}
|
|
51
|
+
onChange={onPhoneChange}
|
|
52
|
+
label="Mobile number"
|
|
53
|
+
numberInputId={numberInputId}
|
|
54
|
+
data-testid="account-profile-phone"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
{phoneVerified ? (
|
|
58
|
+
<span className={verifiedClassName}>Verified</span>
|
|
59
|
+
) : phoneReady ? (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={onVerify}
|
|
63
|
+
disabled={verifyPending}
|
|
64
|
+
className={verifyButtonClassName}
|
|
65
|
+
data-testid="account-profile-verify-phone"
|
|
66
|
+
>
|
|
67
|
+
{verifyPending ? "Sending…" : "Verify phone"}
|
|
68
|
+
</button>
|
|
69
|
+
) : null}
|
|
70
|
+
</div>
|
|
71
|
+
{phoneError ? (
|
|
72
|
+
<p className={errorClassName} role="alert">
|
|
73
|
+
{phoneError}
|
|
74
|
+
</p>
|
|
75
|
+
) : null}
|
|
76
|
+
{!phoneVerified && phoneReady ? (
|
|
77
|
+
<p className={hintClassName}>
|
|
78
|
+
Tap <strong>Verify phone</strong> to receive an OTP on this number.
|
|
79
|
+
</p>
|
|
80
|
+
) : null}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|