@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.
- package/package.json +7 -4
- 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 +61 -19
- 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 +157 -2
- package/src/commerce-auth-otp-modal.tsx +104 -0
- package/src/google-auth-section.tsx +69 -8
- package/src/index.ts +4 -0
- package/src/login-form.tsx +143 -87
- package/src/otp-component.ts +14 -0
- package/src/otp-verification-modal.tsx +112 -0
- package/src/register-form.tsx +130 -104
- package/src/segment.tsx +63 -5
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
createPaymentDetail,
|
|
6
|
+
deletePaymentDetail,
|
|
7
|
+
makeDefaultPaymentDetail,
|
|
8
|
+
} from "@pradip1995/commerce-core/data/payment-details"
|
|
9
|
+
|
|
10
|
+
type PaymentDetail = {
|
|
11
|
+
id: string
|
|
12
|
+
type: "upi" | "bank" | "card"
|
|
13
|
+
detail_json?: Record<string, string>
|
|
14
|
+
is_default?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function AccountPaymentMethods({
|
|
18
|
+
paymentDetails,
|
|
19
|
+
}: {
|
|
20
|
+
paymentDetails: PaymentDetail[]
|
|
21
|
+
}) {
|
|
22
|
+
const [showForm, setShowForm] = useState(false)
|
|
23
|
+
const [type, setType] = useState<"upi" | "bank" | "card">("upi")
|
|
24
|
+
const [pending, setPending] = useState(false)
|
|
25
|
+
const [error, setError] = useState<string | null>(null)
|
|
26
|
+
const [form, setForm] = useState({
|
|
27
|
+
upi_id: "",
|
|
28
|
+
account_holder: "",
|
|
29
|
+
account_number: "",
|
|
30
|
+
ifsc: "",
|
|
31
|
+
card_last4: "",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
async function handleAdd(e: React.FormEvent) {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
setPending(true)
|
|
37
|
+
setError(null)
|
|
38
|
+
try {
|
|
39
|
+
let detail_json: Record<string, string> = {}
|
|
40
|
+
if (type === "upi") {
|
|
41
|
+
detail_json = { upi_id: form.upi_id.trim() }
|
|
42
|
+
} else if (type === "bank") {
|
|
43
|
+
detail_json = {
|
|
44
|
+
account_holder: form.account_holder.trim(),
|
|
45
|
+
account_number: form.account_number.trim(),
|
|
46
|
+
ifsc: form.ifsc.trim(),
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
detail_json = { card_last4: form.card_last4.trim() }
|
|
50
|
+
}
|
|
51
|
+
await createPaymentDetail(type, detail_json)
|
|
52
|
+
window.location.reload()
|
|
53
|
+
} catch (err) {
|
|
54
|
+
setError(err instanceof Error ? err.message : "Failed to add payment method")
|
|
55
|
+
} finally {
|
|
56
|
+
setPending(false)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleDefault(id: string) {
|
|
61
|
+
setPending(true)
|
|
62
|
+
try {
|
|
63
|
+
await makeDefaultPaymentDetail(id)
|
|
64
|
+
window.location.reload()
|
|
65
|
+
} finally {
|
|
66
|
+
setPending(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handleDelete(id: string) {
|
|
71
|
+
if (!confirm("Remove this payment method?")) return
|
|
72
|
+
setPending(true)
|
|
73
|
+
try {
|
|
74
|
+
await deletePaymentDetail(id)
|
|
75
|
+
window.location.reload()
|
|
76
|
+
} finally {
|
|
77
|
+
setPending(false)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function labelFor(detail: PaymentDetail) {
|
|
82
|
+
const json = detail.detail_json ?? {}
|
|
83
|
+
if (detail.type === "upi") return `UPI · ${json.upi_id || "—"}`
|
|
84
|
+
if (detail.type === "bank")
|
|
85
|
+
return `Bank · ${json.account_holder || "—"} (${json.account_number?.slice(-4) || "****"})`
|
|
86
|
+
return `Card · **** ${json.card_last4 || "****"}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-6">
|
|
91
|
+
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
92
|
+
<div>
|
|
93
|
+
<h2 className="section-heading text-xl mb-1">Payment methods</h2>
|
|
94
|
+
<p className="text-sm text-muted">Refund destinations for returns and exchanges.</p>
|
|
95
|
+
</div>
|
|
96
|
+
<button type="button" onClick={() => setShowForm((v) => !v)} className="btn-primary text-sm">
|
|
97
|
+
{showForm ? "Cancel" : "Add method"}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{showForm && (
|
|
102
|
+
<form onSubmit={handleAdd} className="card-surface rounded-2xl p-6 space-y-4">
|
|
103
|
+
<div className="flex flex-wrap gap-2">
|
|
104
|
+
{(["upi", "bank", "card"] as const).map((t) => (
|
|
105
|
+
<button
|
|
106
|
+
key={t}
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => setType(t)}
|
|
109
|
+
className={`px-3 py-1.5 text-xs font-medium rounded-full border ${
|
|
110
|
+
type === t
|
|
111
|
+
? "border-brand-accent text-brand-accent bg-brand-accent/5"
|
|
112
|
+
: "border-cart-border text-muted"
|
|
113
|
+
}`}
|
|
114
|
+
>
|
|
115
|
+
{t.toUpperCase()}
|
|
116
|
+
</button>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{type === "upi" && (
|
|
121
|
+
<Field
|
|
122
|
+
label="UPI ID"
|
|
123
|
+
value={form.upi_id}
|
|
124
|
+
onChange={(v) => setForm((f) => ({ ...f, upi_id: v }))}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
{type === "bank" && (
|
|
128
|
+
<>
|
|
129
|
+
<Field
|
|
130
|
+
label="Account holder"
|
|
131
|
+
value={form.account_holder}
|
|
132
|
+
onChange={(v) => setForm((f) => ({ ...f, account_holder: v }))}
|
|
133
|
+
/>
|
|
134
|
+
<Field
|
|
135
|
+
label="Account number"
|
|
136
|
+
value={form.account_number}
|
|
137
|
+
onChange={(v) => setForm((f) => ({ ...f, account_number: v }))}
|
|
138
|
+
/>
|
|
139
|
+
<Field
|
|
140
|
+
label="IFSC"
|
|
141
|
+
value={form.ifsc}
|
|
142
|
+
onChange={(v) => setForm((f) => ({ ...f, ifsc: v }))}
|
|
143
|
+
/>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
{type === "card" && (
|
|
147
|
+
<Field
|
|
148
|
+
label="Last 4 digits"
|
|
149
|
+
value={form.card_last4}
|
|
150
|
+
onChange={(v) => setForm((f) => ({ ...f, card_last4: v }))}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{error && <p className="text-sm text-brand-sale">{error}</p>}
|
|
155
|
+
|
|
156
|
+
<button type="submit" disabled={pending} className="btn-primary text-sm disabled:opacity-60">
|
|
157
|
+
{pending ? "Saving…" : "Save payment method"}
|
|
158
|
+
</button>
|
|
159
|
+
</form>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{paymentDetails.length === 0 ? (
|
|
163
|
+
<div className="card-surface rounded-2xl p-8 text-center text-sm text-muted">
|
|
164
|
+
No refund payment methods saved.
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<ul className="space-y-3">
|
|
168
|
+
{paymentDetails.map((detail) => (
|
|
169
|
+
<li
|
|
170
|
+
key={detail.id}
|
|
171
|
+
className="card-surface rounded-2xl p-5 flex flex-wrap justify-between gap-4 items-center"
|
|
172
|
+
>
|
|
173
|
+
<div>
|
|
174
|
+
<p className="font-medium text-heading text-sm">{labelFor(detail)}</p>
|
|
175
|
+
{detail.is_default && (
|
|
176
|
+
<p className="text-xs text-brand-accent font-medium mt-1">Default</p>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex gap-3 text-xs">
|
|
180
|
+
{!detail.is_default && (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={() => handleDefault(detail.id)}
|
|
184
|
+
disabled={pending}
|
|
185
|
+
className="text-brand-accent hover:underline disabled:opacity-60"
|
|
186
|
+
>
|
|
187
|
+
Make default
|
|
188
|
+
</button>
|
|
189
|
+
)}
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={() => handleDelete(detail.id)}
|
|
193
|
+
disabled={pending}
|
|
194
|
+
className="text-brand-sale hover:underline disabled:opacity-60"
|
|
195
|
+
>
|
|
196
|
+
Remove
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</li>
|
|
200
|
+
))}
|
|
201
|
+
</ul>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function Field({
|
|
208
|
+
label,
|
|
209
|
+
value,
|
|
210
|
+
onChange,
|
|
211
|
+
}: {
|
|
212
|
+
label: string
|
|
213
|
+
value: string
|
|
214
|
+
onChange: (value: string) => void
|
|
215
|
+
}) {
|
|
216
|
+
return (
|
|
217
|
+
<label className="block">
|
|
218
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
219
|
+
{label}
|
|
220
|
+
</span>
|
|
221
|
+
<input
|
|
222
|
+
value={value}
|
|
223
|
+
onChange={(e) => onChange(e.target.value)}
|
|
224
|
+
required
|
|
225
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
226
|
+
/>
|
|
227
|
+
</label>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
sendRegistrationOtp,
|
|
8
|
+
verifyRegistrationOtp,
|
|
9
|
+
} from "./auth-server"
|
|
10
|
+
import type { OtpVerificationComponent } from "./otp-component"
|
|
11
|
+
import OtpVerificationModal from "./otp-verification-modal"
|
|
12
|
+
|
|
13
|
+
export default function AccountProfile({
|
|
14
|
+
customer,
|
|
15
|
+
countryCode,
|
|
16
|
+
OtpComponent = OtpVerificationModal,
|
|
17
|
+
}: {
|
|
18
|
+
customer: HttpTypes.StoreCustomer
|
|
19
|
+
countryCode: string
|
|
20
|
+
OtpComponent?: OtpVerificationComponent
|
|
21
|
+
}) {
|
|
22
|
+
const [message, setMessage] = useState<string | null>(null)
|
|
23
|
+
const [otpOpen, setOtpOpen] = useState(false)
|
|
24
|
+
const [otp, setOtp] = useState("")
|
|
25
|
+
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
26
|
+
const [otpType, setOtpType] = useState<"email_verification" | "phone_verification">(
|
|
27
|
+
"email_verification"
|
|
28
|
+
)
|
|
29
|
+
const [otpError, setOtpError] = useState<string | null>(null)
|
|
30
|
+
const [otpPending, setOtpPending] = useState(false)
|
|
31
|
+
|
|
32
|
+
async function saveProfile(_prev: unknown, formData: FormData) {
|
|
33
|
+
try {
|
|
34
|
+
const fullName = String(formData.get("full_name") || "").trim()
|
|
35
|
+
const [first_name, ...rest] = fullName.split(/\s+/)
|
|
36
|
+
await updateCustomer({
|
|
37
|
+
first_name: first_name || "",
|
|
38
|
+
last_name: rest.join(" ") || ".",
|
|
39
|
+
phone: String(formData.get("phone") || "").replace(/[^\d+]/g, "") || undefined,
|
|
40
|
+
})
|
|
41
|
+
return { ok: true as const, error: null as string | null }
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false as const,
|
|
45
|
+
error: err instanceof Error ? err.message : "Failed to update profile",
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [state, formAction, pending] = useActionState(saveProfile, { ok: false, error: null })
|
|
51
|
+
|
|
52
|
+
async function sendVerifyOtp(type: "email_verification" | "phone_verification") {
|
|
53
|
+
setOtpType(type)
|
|
54
|
+
setOtpPending(true)
|
|
55
|
+
setOtpError(null)
|
|
56
|
+
try {
|
|
57
|
+
const res = await sendRegistrationOtp(customer.id, type)
|
|
58
|
+
if (!res.success) throw new Error(res.error || "Failed to send code")
|
|
59
|
+
setOtpToken(res.token ?? null)
|
|
60
|
+
setOtp("")
|
|
61
|
+
setOtpOpen(true)
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setOtpError(err instanceof Error ? err.message : "Failed to send code")
|
|
64
|
+
} finally {
|
|
65
|
+
setOtpPending(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function verifyOtp() {
|
|
70
|
+
if (otp.length < 6) return
|
|
71
|
+
setOtpPending(true)
|
|
72
|
+
setOtpError(null)
|
|
73
|
+
try {
|
|
74
|
+
const res = await verifyRegistrationOtp({
|
|
75
|
+
otpToken: otpToken || "",
|
|
76
|
+
code: otp,
|
|
77
|
+
countryCode,
|
|
78
|
+
})
|
|
79
|
+
if (!res.success) throw new Error(res.error || "Invalid code")
|
|
80
|
+
setOtpOpen(false)
|
|
81
|
+
setMessage("Verification successful.")
|
|
82
|
+
window.location.reload()
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setOtpError(err instanceof Error ? err.message : "Verification failed")
|
|
85
|
+
} finally {
|
|
86
|
+
setOtpPending(false)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fullName = [customer.first_name, customer.last_name].filter(Boolean).join(" ")
|
|
91
|
+
const emailVerified = (customer as { email_verified?: boolean }).email_verified === true
|
|
92
|
+
const phoneVerified = (customer as { phone_verified?: boolean }).phone_verified === true
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-8">
|
|
96
|
+
<div>
|
|
97
|
+
<h2 className="section-heading text-xl mb-1">Account details</h2>
|
|
98
|
+
<p className="text-sm text-muted">Update your name and contact information.</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<form action={formAction} className="card-surface rounded-2xl p-6 space-y-4">
|
|
102
|
+
<Field label="Full name" name="full_name" defaultValue={fullName} />
|
|
103
|
+
<div>
|
|
104
|
+
<label className="block text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5">
|
|
105
|
+
Email
|
|
106
|
+
</label>
|
|
107
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
108
|
+
<input
|
|
109
|
+
value={customer.email}
|
|
110
|
+
readOnly
|
|
111
|
+
className="flex-1 min-w-[200px] border border-cart-border rounded px-3 py-2.5 text-sm bg-surface-muted text-muted"
|
|
112
|
+
/>
|
|
113
|
+
{!emailVerified && (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => sendVerifyOtp("email_verification")}
|
|
117
|
+
disabled={otpPending}
|
|
118
|
+
className="btn-outline text-xs shrink-0"
|
|
119
|
+
>
|
|
120
|
+
Verify email
|
|
121
|
+
</button>
|
|
122
|
+
)}
|
|
123
|
+
{emailVerified && (
|
|
124
|
+
<span className="text-xs text-green-600 font-medium">Verified</span>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<Field label="Phone" name="phone" type="tel" defaultValue={customer.phone || ""} />
|
|
130
|
+
<div className="mt-2 flex items-center gap-3">
|
|
131
|
+
{!phoneVerified && customer.phone && (
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={() => sendVerifyOtp("phone_verification")}
|
|
135
|
+
disabled={otpPending}
|
|
136
|
+
className="btn-outline text-xs"
|
|
137
|
+
>
|
|
138
|
+
Verify phone
|
|
139
|
+
</button>
|
|
140
|
+
)}
|
|
141
|
+
{phoneVerified && customer.phone && (
|
|
142
|
+
<span className="text-xs text-green-600 font-medium">Verified</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{(state.error || message) && (
|
|
148
|
+
<p className={`text-sm ${state.ok || message ? "text-green-600" : "text-brand-sale"}`}>
|
|
149
|
+
{message || state.error}
|
|
150
|
+
</p>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
<button type="submit" disabled={pending} className="btn-primary disabled:opacity-60">
|
|
154
|
+
{pending ? "Saving…" : "Save changes"}
|
|
155
|
+
</button>
|
|
156
|
+
</form>
|
|
157
|
+
|
|
158
|
+
<OtpComponent
|
|
159
|
+
open={otpOpen}
|
|
160
|
+
title={otpType === "email_verification" ? "Verify your email" : "Verify your phone"}
|
|
161
|
+
description={`Enter the code sent to your ${otpType === "email_verification" ? "email" : "phone"}.`}
|
|
162
|
+
otp={otp}
|
|
163
|
+
onOtpChange={setOtp}
|
|
164
|
+
error={otpError}
|
|
165
|
+
pending={otpPending}
|
|
166
|
+
onVerify={verifyOtp}
|
|
167
|
+
onResend={() => sendVerifyOtp(otpType)}
|
|
168
|
+
onClose={() => setOtpOpen(false)}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Field({
|
|
175
|
+
label,
|
|
176
|
+
name,
|
|
177
|
+
type = "text",
|
|
178
|
+
defaultValue,
|
|
179
|
+
}: {
|
|
180
|
+
label: string
|
|
181
|
+
name: string
|
|
182
|
+
type?: string
|
|
183
|
+
defaultValue?: string
|
|
184
|
+
}) {
|
|
185
|
+
return (
|
|
186
|
+
<label className="block">
|
|
187
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
188
|
+
{label}
|
|
189
|
+
</span>
|
|
190
|
+
<input
|
|
191
|
+
name={name}
|
|
192
|
+
type={type}
|
|
193
|
+
defaultValue={defaultValue}
|
|
194
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
195
|
+
/>
|
|
196
|
+
</label>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
2
|
+
|
|
3
|
+
export function getProfileCompletion(customer: HttpTypes.StoreCustomer): number {
|
|
4
|
+
let score = 0
|
|
5
|
+
if (customer.first_name?.trim()) score += 25
|
|
6
|
+
if (customer.last_name?.trim()) score += 25
|
|
7
|
+
if (customer.email?.trim()) score += 25
|
|
8
|
+
if (customer.phone?.trim()) score += 25
|
|
9
|
+
return score
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function customerDisplayName(customer: HttpTypes.StoreCustomer): string {
|
|
13
|
+
return [customer.first_name, customer.last_name].filter(Boolean).join(" ") || customer.email || "Account"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function accountSectionPath(path?: string): string {
|
|
17
|
+
if (!path || path === "login") return "overview"
|
|
18
|
+
const base = path.split("/")[0]
|
|
19
|
+
return base || "overview"
|
|
20
|
+
}
|
package/src/auth-server.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
checkEmailRegistered,
|
|
5
5
|
completePasswordReset,
|
|
6
|
-
registerCustomer,
|
|
6
|
+
registerCustomer as registerCustomerBase,
|
|
7
7
|
requestPasswordReset,
|
|
8
8
|
sendCustomerOTP,
|
|
9
9
|
verifyCustomerOTP,
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
import { transferCart } from "@pradip1995/commerce-core/data/customer"
|
|
12
12
|
import { getCacheTag, setAuthToken } from "@pradip1995/commerce-core/data/cookies"
|
|
13
13
|
import { sendOTP, verifyOTP } from "@pradip1995/commerce-core/data/guest"
|
|
14
|
+
import { getGoogleOAuthCallbackUrl } from "@pradip1995/commerce-core/util/google-oauth"
|
|
14
15
|
import { revalidateTag } from "next/cache"
|
|
15
16
|
import { redirect } from "next/navigation"
|
|
16
17
|
|
|
17
18
|
export {
|
|
18
|
-
registerCustomer,
|
|
19
19
|
sendCustomerOTP,
|
|
20
20
|
verifyCustomerOTP,
|
|
21
21
|
requestPasswordReset,
|
|
@@ -25,6 +25,38 @@ export {
|
|
|
25
25
|
verifyOTP,
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function normalizeRegistrationError(message: string): string {
|
|
29
|
+
const lower = message.toLowerCase()
|
|
30
|
+
if (
|
|
31
|
+
lower.includes("identity with email already exists") ||
|
|
32
|
+
lower.includes("email already exists") ||
|
|
33
|
+
lower.includes("email is already registered")
|
|
34
|
+
) {
|
|
35
|
+
return "Email already exists"
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
lower.includes("identity with phone already exists") ||
|
|
39
|
+
lower.includes("phone already exists")
|
|
40
|
+
) {
|
|
41
|
+
return "Phone number already exists"
|
|
42
|
+
}
|
|
43
|
+
return message
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function registerCustomer(data: {
|
|
47
|
+
email: string
|
|
48
|
+
first_name: string
|
|
49
|
+
last_name: string
|
|
50
|
+
phone?: string
|
|
51
|
+
password: string
|
|
52
|
+
}) {
|
|
53
|
+
const result = await registerCustomerBase(data)
|
|
54
|
+
if (!result.success && result.error) {
|
|
55
|
+
return { ...result, error: normalizeRegistrationError(result.error) }
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
function getHeaders() {
|
|
29
61
|
return {
|
|
30
62
|
"Content-Type": "application/json",
|
|
@@ -150,3 +182,126 @@ export async function completePasswordResetAction(input: {
|
|
|
150
182
|
|
|
151
183
|
redirect(`/${input.countryCode || "in"}/account`)
|
|
152
184
|
}
|
|
185
|
+
|
|
186
|
+
export async function verifyRegistrationOtp(input: {
|
|
187
|
+
otpToken: string
|
|
188
|
+
code: string
|
|
189
|
+
countryCode: string
|
|
190
|
+
}) {
|
|
191
|
+
const response = await fetch(`${getBaseUrl()}/store/customers/otp/verify`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: getHeaders(),
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
token: input.otpToken,
|
|
196
|
+
code: input.code,
|
|
197
|
+
}),
|
|
198
|
+
cache: "no-store",
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const data = (await response.json().catch(() => ({}))) as {
|
|
202
|
+
message?: string
|
|
203
|
+
token?: string | null
|
|
204
|
+
email_verified?: boolean
|
|
205
|
+
phone_verified?: boolean
|
|
206
|
+
needs_login?: boolean
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
return {
|
|
211
|
+
success: false as const,
|
|
212
|
+
error: data.message || "Invalid verification code",
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const loginToken = data.token
|
|
217
|
+
if (loginToken) {
|
|
218
|
+
await setAuthToken(loginToken)
|
|
219
|
+
try {
|
|
220
|
+
await transferCart(loginToken)
|
|
221
|
+
} catch {
|
|
222
|
+
// non-fatal
|
|
223
|
+
}
|
|
224
|
+
const customerCacheTag = await getCacheTag("customers")
|
|
225
|
+
revalidateTag(customerCacheTag)
|
|
226
|
+
redirect(`/${input.countryCode || "in"}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
success: true as const,
|
|
231
|
+
emailVerified: data.email_verified,
|
|
232
|
+
phoneVerified: data.phone_verified,
|
|
233
|
+
needsLogin: data.needs_login ?? true,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function sendRegistrationOtp(
|
|
238
|
+
customerId: string,
|
|
239
|
+
type: "email_verification" | "phone_verification"
|
|
240
|
+
) {
|
|
241
|
+
const result = await sendCustomerOTP(customerId, type)
|
|
242
|
+
if (result.success) {
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const message = result.error || "Failed to send verification code"
|
|
247
|
+
if (message.includes("Channel configuration not found")) {
|
|
248
|
+
return {
|
|
249
|
+
success: false as const,
|
|
250
|
+
error:
|
|
251
|
+
"Verification email is not configured on the backend. Rebuild and restart the Medusa server with customer-registration OTP settings.",
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { success: false as const, error: message }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function initiateGoogleAuth(countryCode?: string) {
|
|
259
|
+
try {
|
|
260
|
+
const backendUrl = getBaseUrl()
|
|
261
|
+
|
|
262
|
+
if (!backendUrl) {
|
|
263
|
+
return { error: "Backend URL not configured" }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!process.env.GOOGLE_CLIENT_ID && !process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) {
|
|
267
|
+
return { error: "Google sign-in is not configured on the storefront" }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const callbackUrl = getGoogleOAuthCallbackUrl(countryCode)
|
|
271
|
+
|
|
272
|
+
const response = await fetch(`${backendUrl}/auth/customer/google`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "Content-Type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ callback_url: callbackUrl }),
|
|
276
|
+
cache: "no-store",
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
const err = (await response.json().catch(() => ({}))) as {
|
|
281
|
+
message?: string
|
|
282
|
+
type?: string
|
|
283
|
+
}
|
|
284
|
+
const message = err.message?.trim()
|
|
285
|
+
if (message?.includes("Unable to retrieve the auth provider")) {
|
|
286
|
+
return {
|
|
287
|
+
error:
|
|
288
|
+
"Google sign-in is not enabled on the backend. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in backend/.env and restart the Medusa server.",
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { error: message || "Google sign-in failed. Check backend Google OAuth configuration." }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = await response.json()
|
|
295
|
+
const redirectUrl =
|
|
296
|
+
typeof result === "string" ? result : (result as { location?: string })?.location
|
|
297
|
+
|
|
298
|
+
if (!redirectUrl) {
|
|
299
|
+
return { error: "No redirect URL received from Google OAuth" }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { redirectUrl }
|
|
303
|
+
} catch (error: unknown) {
|
|
304
|
+
const message = error instanceof Error ? error.message : "Internal server error"
|
|
305
|
+
return { error: message }
|
|
306
|
+
}
|
|
307
|
+
}
|