@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.
- package/package.json +9 -2
- 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 +111 -0
- 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 +307 -0
- package/src/commerce-auth-otp-modal.tsx +104 -0
- package/src/forgot-password-form.tsx +89 -0
- package/src/google-auth-section.tsx +86 -0
- package/src/index.ts +5 -0
- package/src/login-form.tsx +411 -0
- package/src/otp-component.ts +14 -0
- package/src/otp-input.tsx +46 -0
- package/src/otp-verification-modal.tsx +112 -0
- package/src/register-form.tsx +324 -0
- package/src/reset-password-form.tsx +110 -0
- package/src/reset-password-page.tsx +12 -0
- package/src/segment.tsx +81 -200
|
@@ -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
|
+
}
|