@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.
@@ -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
+ }