@pradip1995/segment-login-template 0.4.0 → 0.4.2
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 -3
- package/src/account-addresses.tsx +14 -13
- package/src/account-guest-orders.tsx +9 -3
- package/src/account-orders.tsx +90 -38
- package/src/account-overview.tsx +96 -74
- package/src/account-payment-methods.tsx +155 -63
- package/src/account-profile.tsx +12 -11
- package/src/account-theme.ts +28 -0
- package/src/payment-methods-actions.ts +214 -0
- package/src/segment.tsx +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pradip1995/segment-login-template",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@pradip1995/commerce-auth": "^4.0.0",
|
|
30
30
|
"@pradip1995/commerce-core": "^4.0.0",
|
|
31
|
-
"@pradip1995/segment-primitives": "^0.
|
|
32
|
-
"@pradip1995/segment-tokens": "^0.3.
|
|
31
|
+
"@pradip1995/segment-primitives": "^0.4.0",
|
|
32
|
+
"@pradip1995/segment-tokens": "^0.3.7"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@pradip1995/plugin-sdk": "^0.2.0",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
deleteCustomerAddress,
|
|
8
8
|
updateCustomerAddress,
|
|
9
9
|
} from "@pradip1995/commerce-core/data/customer"
|
|
10
|
+
import { acct } from "./account-theme"
|
|
10
11
|
|
|
11
12
|
export default function AccountAddresses({
|
|
12
13
|
customer,
|
|
@@ -23,23 +24,23 @@ export default function AccountAddresses({
|
|
|
23
24
|
<div className="space-y-8">
|
|
24
25
|
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
25
26
|
<div>
|
|
26
|
-
<h2 className=
|
|
27
|
-
<p className=
|
|
27
|
+
<h2 className={acct.title}>Saved addresses</h2>
|
|
28
|
+
<p className={`text-sm ${acct.muted}`}>Manage shipping and billing addresses.</p>
|
|
28
29
|
</div>
|
|
29
|
-
<button type="button" onClick={() => setShowAdd(true)} className=
|
|
30
|
+
<button type="button" onClick={() => setShowAdd(true)} className={acct.btnPrimary}>
|
|
30
31
|
Add address
|
|
31
32
|
</button>
|
|
32
33
|
</div>
|
|
33
34
|
|
|
34
35
|
{addresses.length === 0 && !showAdd && (
|
|
35
|
-
<div className=
|
|
36
|
+
<div className={`${acct.cardMuted} p-8 text-center text-sm ${acct.muted}`}>
|
|
36
37
|
No saved addresses yet.
|
|
37
38
|
</div>
|
|
38
39
|
)}
|
|
39
40
|
|
|
40
41
|
<ul className="space-y-4">
|
|
41
42
|
{addresses.map((address) => (
|
|
42
|
-
<li key={address.id} className=
|
|
43
|
+
<li key={address.id} className={`${acct.card} p-5`}>
|
|
43
44
|
{editingId === address.id ? (
|
|
44
45
|
<AddressForm
|
|
45
46
|
countryCode={countryCode}
|
|
@@ -75,7 +76,7 @@ export default function AccountAddresses({
|
|
|
75
76
|
<button
|
|
76
77
|
type="button"
|
|
77
78
|
onClick={() => setEditingId(address.id!)}
|
|
78
|
-
className=
|
|
79
|
+
className={`${acct.btnOutline} !py-2 !px-3`}
|
|
79
80
|
>
|
|
80
81
|
Edit
|
|
81
82
|
</button>
|
|
@@ -88,7 +89,7 @@ export default function AccountAddresses({
|
|
|
88
89
|
</ul>
|
|
89
90
|
|
|
90
91
|
{showAdd && (
|
|
91
|
-
<div className=
|
|
92
|
+
<div className={`${acct.card} p-5`}>
|
|
92
93
|
<h3 className="text-sm font-semibold text-heading mb-4">New address</h3>
|
|
93
94
|
<AddressForm
|
|
94
95
|
countryCode={countryCode}
|
|
@@ -146,13 +147,13 @@ function AddressForm({
|
|
|
146
147
|
/>
|
|
147
148
|
<Field label="Phone" name="phone" type="tel" defaultValue={address?.phone || ""} />
|
|
148
149
|
{state?.error && (
|
|
149
|
-
<p className="sm:col-span-2 text-sm text-
|
|
150
|
+
<p className="sm:col-span-2 text-sm text-red-600">{String(state.error)}</p>
|
|
150
151
|
)}
|
|
151
152
|
<div className="sm:col-span-2 flex gap-3">
|
|
152
|
-
<button type="submit" disabled={pending} className=
|
|
153
|
+
<button type="submit" disabled={pending} className={acct.btnPrimary}>
|
|
153
154
|
{pending ? "Saving…" : "Save address"}
|
|
154
155
|
</button>
|
|
155
|
-
<button type="button" onClick={onCancel} className=
|
|
156
|
+
<button type="button" onClick={onCancel} className={acct.btnOutline}>
|
|
156
157
|
Cancel
|
|
157
158
|
</button>
|
|
158
159
|
</div>
|
|
@@ -179,7 +180,7 @@ function DeleteAddressButton({ addressId }: { addressId: string }) {
|
|
|
179
180
|
type="button"
|
|
180
181
|
onClick={handleDelete}
|
|
181
182
|
disabled={pending}
|
|
182
|
-
className="text-xs text-
|
|
183
|
+
className="text-xs text-red-600 hover:underline disabled:opacity-60"
|
|
183
184
|
>
|
|
184
185
|
{pending ? "Deleting…" : "Delete"}
|
|
185
186
|
</button>
|
|
@@ -201,7 +202,7 @@ function Field({
|
|
|
201
202
|
}) {
|
|
202
203
|
return (
|
|
203
204
|
<label className={`block ${className}`}>
|
|
204
|
-
<span className="text-xs font-semibold uppercase tracking-
|
|
205
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-muted mb-1.5 block">
|
|
205
206
|
{label}
|
|
206
207
|
</span>
|
|
207
208
|
<input
|
|
@@ -209,7 +210,7 @@ function Field({
|
|
|
209
210
|
type={type}
|
|
210
211
|
defaultValue={defaultValue}
|
|
211
212
|
required={name !== "address_2"}
|
|
212
|
-
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
213
|
+
className="w-full border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface text-heading focus:outline-none focus:border-brand-accent"
|
|
213
214
|
/>
|
|
214
215
|
</label>
|
|
215
216
|
)
|
|
@@ -9,9 +9,15 @@ export default function AccountGuestOrders({
|
|
|
9
9
|
orders,
|
|
10
10
|
countryCode,
|
|
11
11
|
}: {
|
|
12
|
-
orders
|
|
12
|
+
orders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
|
|
13
13
|
countryCode: string
|
|
14
14
|
}) {
|
|
15
|
+
const safeOrders: HttpTypes.StoreOrder[] = Array.isArray(orders)
|
|
16
|
+
? orders
|
|
17
|
+
: Array.isArray((orders as any)?.orders)
|
|
18
|
+
? (orders as any).orders
|
|
19
|
+
: []
|
|
20
|
+
|
|
15
21
|
async function handleSignOut() {
|
|
16
22
|
await logoutGuest()
|
|
17
23
|
window.location.href = `/${countryCode}/account`
|
|
@@ -29,13 +35,13 @@ export default function AccountGuestOrders({
|
|
|
29
35
|
</button>
|
|
30
36
|
</div>
|
|
31
37
|
|
|
32
|
-
{
|
|
38
|
+
{safeOrders.length === 0 ? (
|
|
33
39
|
<div className="card-surface rounded-2xl p-8 text-center text-sm text-muted">
|
|
34
40
|
No guest orders found for this session.
|
|
35
41
|
</div>
|
|
36
42
|
) : (
|
|
37
43
|
<ul className="space-y-3">
|
|
38
|
-
{
|
|
44
|
+
{safeOrders.map((order) => (
|
|
39
45
|
<li key={order.id}>
|
|
40
46
|
<LocalizedLink
|
|
41
47
|
href={`/orders/${order.id}`}
|
package/src/account-orders.tsx
CHANGED
|
@@ -4,29 +4,60 @@ import { useMemo, useState } from "react"
|
|
|
4
4
|
import type { HttpTypes } from "@medusajs/types"
|
|
5
5
|
import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
|
|
6
6
|
import { formatPrice } from "@pradip1995/segment-primitives/format-price"
|
|
7
|
+
import { acct } from "./account-theme"
|
|
8
|
+
|
|
9
|
+
function statusStyle(status?: string, fulfillment?: string) {
|
|
10
|
+
const s = (fulfillment || status || "").toLowerCase()
|
|
11
|
+
if (s.includes("cancel")) {
|
|
12
|
+
return "bg-[#FFF1F2] text-[#BE123C] border-[#BE123C]/20"
|
|
13
|
+
}
|
|
14
|
+
if (s === "delivered" || s === "complete" || s === "completed") {
|
|
15
|
+
return "bg-[#E6F4F0] text-[#008A5D] border-[#008A5D]/20"
|
|
16
|
+
}
|
|
17
|
+
if (s.includes("ship")) {
|
|
18
|
+
return "bg-[#EBF5FF] text-[#0068D2] border-[#0068D2]/20"
|
|
19
|
+
}
|
|
20
|
+
if (s.includes("fulfill")) {
|
|
21
|
+
return "bg-surface-muted text-brand-accent border-brand-accent"
|
|
22
|
+
}
|
|
23
|
+
return "bg-surface-muted text-muted border-cart-border"
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
export default function AccountOrders({
|
|
9
27
|
orders,
|
|
10
28
|
}: {
|
|
11
|
-
orders
|
|
29
|
+
orders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
|
|
12
30
|
}) {
|
|
13
31
|
const [query, setQuery] = useState("")
|
|
14
32
|
|
|
33
|
+
const safeOrders: HttpTypes.StoreOrder[] = useMemo(() => {
|
|
34
|
+
if (Array.isArray(orders)) return orders
|
|
35
|
+
if (Array.isArray((orders as any)?.orders)) return (orders as any).orders
|
|
36
|
+
return []
|
|
37
|
+
}, [orders])
|
|
38
|
+
|
|
15
39
|
const filtered = useMemo(() => {
|
|
16
40
|
const q = query.trim().toLowerCase()
|
|
17
|
-
if (!q) return
|
|
18
|
-
return
|
|
41
|
+
if (!q) return safeOrders
|
|
42
|
+
return safeOrders.filter((order) => {
|
|
19
43
|
const id = String(order.display_id ?? order.id)
|
|
20
44
|
const status = String(order.status ?? "").replace(/_/g, " ")
|
|
21
|
-
|
|
45
|
+
const fulfillment = String(order.fulfillment_status ?? "").replace(/_/g, " ")
|
|
46
|
+
return (
|
|
47
|
+
id.includes(q) ||
|
|
48
|
+
status.toLowerCase().includes(q) ||
|
|
49
|
+
fulfillment.toLowerCase().includes(q)
|
|
50
|
+
)
|
|
22
51
|
})
|
|
23
|
-
}, [
|
|
52
|
+
}, [safeOrders, query])
|
|
24
53
|
|
|
25
54
|
return (
|
|
26
55
|
<div className="space-y-6">
|
|
27
|
-
<div>
|
|
28
|
-
<h2 className=
|
|
29
|
-
<p className=
|
|
56
|
+
<div className="pb-6 border-b border-cart-border">
|
|
57
|
+
<h2 className={acct.title}>Order history</h2>
|
|
58
|
+
<p className={`text-sm ${acct.muted} mt-2`}>
|
|
59
|
+
View and track your past orders.
|
|
60
|
+
</p>
|
|
30
61
|
</div>
|
|
31
62
|
|
|
32
63
|
<input
|
|
@@ -34,46 +65,67 @@ export default function AccountOrders({
|
|
|
34
65
|
value={query}
|
|
35
66
|
onChange={(e) => setQuery(e.target.value)}
|
|
36
67
|
placeholder="Search by order number or status"
|
|
37
|
-
className=
|
|
68
|
+
className={acct.input}
|
|
38
69
|
/>
|
|
39
70
|
|
|
40
71
|
{filtered.length === 0 ? (
|
|
41
|
-
<div className=
|
|
42
|
-
<p className=
|
|
43
|
-
{
|
|
72
|
+
<div className={`${acct.cardMuted} p-8 text-center`}>
|
|
73
|
+
<p className={`${acct.muted} text-sm mb-4`}>
|
|
74
|
+
{safeOrders.length === 0
|
|
75
|
+
? "No orders yet."
|
|
76
|
+
: "No orders match your search."}
|
|
44
77
|
</p>
|
|
45
|
-
{
|
|
46
|
-
<LocalizedLink href="/store" className=
|
|
78
|
+
{safeOrders.length === 0 && (
|
|
79
|
+
<LocalizedLink href="/store" className={acct.btnPrimary}>
|
|
47
80
|
Start shopping
|
|
48
81
|
</LocalizedLink>
|
|
49
82
|
)}
|
|
50
83
|
</div>
|
|
51
84
|
) : (
|
|
52
|
-
<ul className="space-y-
|
|
53
|
-
{filtered.map((order) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
<ul className="space-y-4">
|
|
86
|
+
{filtered.map((order) => {
|
|
87
|
+
const label = (
|
|
88
|
+
order.fulfillment_status ||
|
|
89
|
+
order.status ||
|
|
90
|
+
""
|
|
91
|
+
).replace(/_/g, " ")
|
|
92
|
+
return (
|
|
93
|
+
<li key={order.id}>
|
|
94
|
+
<LocalizedLink
|
|
95
|
+
href={`/orders/${order.id}`}
|
|
96
|
+
className={`${acct.card} p-5 flex flex-wrap items-center justify-between gap-4 hover:border-brand-accent transition-colors block`}
|
|
97
|
+
>
|
|
98
|
+
<div className="space-y-2 min-w-0">
|
|
99
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
100
|
+
<p className={`font-bold ${acct.heading}`}>
|
|
101
|
+
Order #{order.display_id}
|
|
102
|
+
</p>
|
|
103
|
+
<span
|
|
104
|
+
className={`${acct.badge} capitalize ${statusStyle(
|
|
105
|
+
order.status,
|
|
106
|
+
order.fulfillment_status
|
|
107
|
+
)}`}
|
|
108
|
+
>
|
|
109
|
+
{label}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
<p className={`text-xs ${acct.muted}`}>
|
|
113
|
+
{order.created_at &&
|
|
114
|
+
new Date(order.created_at).toLocaleDateString("en-IN", {
|
|
115
|
+
dateStyle: "medium",
|
|
116
|
+
})}
|
|
117
|
+
{" · "}
|
|
118
|
+
{order.items?.length ?? 0} item
|
|
119
|
+
{(order.items?.length ?? 0) === 1 ? "" : "s"}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
<p className={`text-base font-bold ${acct.accent} shrink-0`}>
|
|
123
|
+
{formatPrice(order.total, order.currency_code)}
|
|
69
124
|
</p>
|
|
70
|
-
</
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</LocalizedLink>
|
|
75
|
-
</li>
|
|
76
|
-
))}
|
|
125
|
+
</LocalizedLink>
|
|
126
|
+
</li>
|
|
127
|
+
)
|
|
128
|
+
})}
|
|
77
129
|
</ul>
|
|
78
130
|
)}
|
|
79
131
|
</div>
|
package/src/account-overview.tsx
CHANGED
|
@@ -4,108 +4,130 @@ import type { HttpTypes } from "@medusajs/types"
|
|
|
4
4
|
import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
|
|
5
5
|
import { formatPrice } from "@pradip1995/segment-primitives/format-price"
|
|
6
6
|
import { getProfileCompletion } from "./account-utils"
|
|
7
|
+
import { acct } from "./account-theme"
|
|
7
8
|
|
|
8
9
|
export default function AccountOverview({
|
|
9
10
|
customer,
|
|
10
11
|
orders,
|
|
11
|
-
countryCode,
|
|
12
12
|
}: {
|
|
13
13
|
customer: HttpTypes.StoreCustomer
|
|
14
|
-
orders
|
|
14
|
+
orders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
|
|
15
15
|
countryCode: string
|
|
16
16
|
}) {
|
|
17
|
-
const name = [customer.first_name, customer.last_name].filter(Boolean).join(" ") || customer.email
|
|
18
17
|
const completion = getProfileCompletion(customer)
|
|
19
18
|
const addressCount = customer.addresses?.length ?? 0
|
|
20
19
|
|
|
20
|
+
const safeOrders: HttpTypes.StoreOrder[] = Array.isArray(orders)
|
|
21
|
+
? orders
|
|
22
|
+
: Array.isArray((orders as any)?.orders)
|
|
23
|
+
? (orders as any).orders
|
|
24
|
+
: []
|
|
25
|
+
|
|
21
26
|
return (
|
|
22
|
-
<div
|
|
23
|
-
<div>
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
<div>
|
|
28
|
+
<div className="flex justify-between items-start gap-4 mb-6 pb-6 border-b border-cart-border">
|
|
29
|
+
<span className={`font-serif text-2xl font-extrabold ${acct.heading}`}>
|
|
30
|
+
Hello {customer.first_name}
|
|
31
|
+
</span>
|
|
32
|
+
<span className={`text-xs ${acct.muted} text-right shrink-0 max-w-[50%]`}>
|
|
33
|
+
Signed in as:{" "}
|
|
34
|
+
<span className={`font-semibold ${acct.accent} block sm:inline`}>
|
|
35
|
+
{customer.email}
|
|
36
|
+
</span>
|
|
37
|
+
</span>
|
|
27
38
|
</div>
|
|
28
39
|
|
|
29
|
-
<div className="
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
40
|
+
<div className="flex items-start gap-x-12 sm:gap-x-16 mb-8 pb-8 border-b border-cart-border">
|
|
41
|
+
<LocalizedLink href="/account/profile" className="flex flex-col gap-y-2 group">
|
|
42
|
+
<h3 className={acct.subsection}>Profile</h3>
|
|
43
|
+
<div className="flex items-end gap-x-2">
|
|
44
|
+
<span className="text-4xl font-serif font-extrabold text-heading group-hover:text-brand-accent transition-colors">
|
|
45
|
+
{completion}%
|
|
46
|
+
</span>
|
|
47
|
+
<span className={`uppercase text-xs font-medium tracking-wider ${acct.muted} pb-1`}>
|
|
48
|
+
Completed
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</LocalizedLink>
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
</
|
|
53
|
+
<LocalizedLink href="/account/addresses" className="flex flex-col gap-y-2 group">
|
|
54
|
+
<h3 className={acct.subsection}>Addresses</h3>
|
|
55
|
+
<div className="flex items-end gap-x-2">
|
|
56
|
+
<span className="text-4xl font-serif font-extrabold text-heading group-hover:text-brand-accent transition-colors">
|
|
57
|
+
{addressCount}
|
|
58
|
+
</span>
|
|
59
|
+
<span className={`uppercase text-xs font-medium tracking-wider ${acct.muted} pb-1`}>
|
|
60
|
+
Saved
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
</LocalizedLink>
|
|
64
|
+
</div>
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
<div className="flex flex-col gap-y-4">
|
|
67
|
+
<h3 className={acct.sectionTitle}>Recent orders</h3>
|
|
68
|
+
{safeOrders.length === 0 ? (
|
|
69
|
+
<div className={`${acct.cardMuted} p-8 text-center`}>
|
|
70
|
+
<p className={`${acct.muted} text-sm mb-4`}>No orders yet.</p>
|
|
71
|
+
<LocalizedLink href="/store" className={acct.btnPrimary}>
|
|
60
72
|
Start shopping
|
|
61
73
|
</LocalizedLink>
|
|
62
74
|
</div>
|
|
63
75
|
) : (
|
|
64
|
-
<ul className="
|
|
65
|
-
{
|
|
76
|
+
<ul className="flex flex-col gap-y-3">
|
|
77
|
+
{safeOrders.slice(0, 5).map((order) => (
|
|
66
78
|
<li key={order.id}>
|
|
67
|
-
<LocalizedLink
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
<LocalizedLink href={`/orders/${order.id}`} className="block group">
|
|
80
|
+
<div
|
|
81
|
+
className={`${acct.cardMuted} flex justify-between items-center p-4 transition-colors group-hover:border-brand-accent group-hover:bg-surface`}
|
|
82
|
+
>
|
|
83
|
+
<div className="grid grid-cols-3 gap-x-4 flex-1 text-sm">
|
|
84
|
+
<div>
|
|
85
|
+
<span className={`font-semibold ${acct.muted} text-xs uppercase tracking-wide block`}>
|
|
86
|
+
Date placed
|
|
87
|
+
</span>
|
|
88
|
+
<span className={acct.heading}>
|
|
89
|
+
{order.created_at
|
|
90
|
+
? new Date(order.created_at).toLocaleDateString("en-IN", {
|
|
91
|
+
dateStyle: "medium",
|
|
92
|
+
})
|
|
93
|
+
: "—"}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<span className={`font-semibold ${acct.muted} text-xs uppercase tracking-wide block`}>
|
|
98
|
+
Order
|
|
99
|
+
</span>
|
|
100
|
+
<span className={`font-bold ${acct.accent}`}>
|
|
101
|
+
#{order.display_id}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<span className={`font-semibold ${acct.muted} text-xs uppercase tracking-wide block`}>
|
|
106
|
+
Total
|
|
107
|
+
</span>
|
|
108
|
+
<span className={`font-bold ${acct.heading}`}>
|
|
109
|
+
{formatPrice(order.total, order.currency_code)}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<span className={`${acct.accent} text-lg font-bold group-hover:translate-x-0.5 transition-transform`}>
|
|
114
|
+
›
|
|
115
|
+
</span>
|
|
76
116
|
</div>
|
|
77
|
-
<p className="text-sm font-semibold text-brand-accent shrink-0">
|
|
78
|
-
{formatPrice(order.total, order.currency_code)}
|
|
79
|
-
</p>
|
|
80
117
|
</LocalizedLink>
|
|
81
118
|
</li>
|
|
82
119
|
))}
|
|
83
120
|
</ul>
|
|
84
121
|
)}
|
|
122
|
+
{safeOrders.length > 0 && (
|
|
123
|
+
<LocalizedLink
|
|
124
|
+
href="/account/orders"
|
|
125
|
+
className={`text-xs font-bold uppercase tracking-widest ${acct.accent} hover:underline mt-2`}
|
|
126
|
+
>
|
|
127
|
+
View all orders
|
|
128
|
+
</LocalizedLink>
|
|
129
|
+
)}
|
|
85
130
|
</div>
|
|
86
131
|
</div>
|
|
87
132
|
)
|
|
88
133
|
}
|
|
89
|
-
|
|
90
|
-
function StatCard({
|
|
91
|
-
title,
|
|
92
|
-
value,
|
|
93
|
-
subtitle,
|
|
94
|
-
href,
|
|
95
|
-
}: {
|
|
96
|
-
title: string
|
|
97
|
-
value: string
|
|
98
|
-
subtitle: string
|
|
99
|
-
href: string
|
|
100
|
-
}) {
|
|
101
|
-
return (
|
|
102
|
-
<LocalizedLink
|
|
103
|
-
href={href}
|
|
104
|
-
className="card-surface rounded-2xl p-5 hover:border-brand-accent transition-colors block"
|
|
105
|
-
>
|
|
106
|
-
<p className="text-xs uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-2">{title}</p>
|
|
107
|
-
<p className="text-3xl font-semibold text-heading leading-none">{value}</p>
|
|
108
|
-
<p className="text-xs text-muted mt-2 uppercase">{subtitle}</p>
|
|
109
|
-
</LocalizedLink>
|
|
110
|
-
)
|
|
111
|
-
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "
|
|
5
|
+
addPaymentMethodAction,
|
|
6
|
+
deletePaymentMethodAction,
|
|
7
|
+
makeDefaultPaymentMethodAction,
|
|
8
|
+
} from "./payment-methods-actions"
|
|
9
|
+
import { acct } from "./account-theme"
|
|
9
10
|
|
|
10
11
|
type PaymentDetail = {
|
|
11
12
|
id: string
|
|
@@ -14,6 +15,19 @@ type PaymentDetail = {
|
|
|
14
15
|
is_default?: boolean
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
const emptyForm = {
|
|
19
|
+
upi_id: "",
|
|
20
|
+
account_holder_name: "",
|
|
21
|
+
bank_name: "",
|
|
22
|
+
account_number: "",
|
|
23
|
+
ifsc: "",
|
|
24
|
+
branch_name: "",
|
|
25
|
+
card_holder_name: "",
|
|
26
|
+
card_number: "",
|
|
27
|
+
expiry_date: "",
|
|
28
|
+
cvv: "",
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export default function AccountPaymentMethods({
|
|
18
32
|
paymentDetails,
|
|
19
33
|
}: {
|
|
@@ -23,92 +37,118 @@ export default function AccountPaymentMethods({
|
|
|
23
37
|
const [type, setType] = useState<"upi" | "bank" | "card">("upi")
|
|
24
38
|
const [pending, setPending] = useState(false)
|
|
25
39
|
const [error, setError] = useState<string | null>(null)
|
|
26
|
-
const [
|
|
27
|
-
|
|
28
|
-
account_holder: "",
|
|
29
|
-
account_number: "",
|
|
30
|
-
ifsc: "",
|
|
31
|
-
card_last4: "",
|
|
32
|
-
})
|
|
40
|
+
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
|
|
41
|
+
const [form, setForm] = useState(emptyForm)
|
|
33
42
|
|
|
34
43
|
async function handleAdd(e: React.FormEvent) {
|
|
35
44
|
e.preventDefault()
|
|
36
45
|
setPending(true)
|
|
37
46
|
setError(null)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
setFieldErrors({})
|
|
48
|
+
|
|
49
|
+
let detail_json: Record<string, string> = {}
|
|
50
|
+
if (type === "upi") {
|
|
51
|
+
detail_json = { upi_id: form.upi_id.trim() }
|
|
52
|
+
} else if (type === "bank") {
|
|
53
|
+
detail_json = {
|
|
54
|
+
account_holder_name: form.account_holder_name.trim(),
|
|
55
|
+
bank_name: form.bank_name.trim(),
|
|
56
|
+
account_number: form.account_number.trim(),
|
|
57
|
+
ifsc: form.ifsc.trim(),
|
|
58
|
+
}
|
|
59
|
+
if (form.branch_name.trim()) {
|
|
60
|
+
detail_json.branch_name = form.branch_name.trim()
|
|
50
61
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
} else {
|
|
63
|
+
detail_json = {
|
|
64
|
+
card_holder_name: form.card_holder_name.trim(),
|
|
65
|
+
card_number: form.card_number.trim(),
|
|
66
|
+
expiry_date: form.expiry_date.trim(),
|
|
67
|
+
cvv: form.cvv.trim(),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await addPaymentMethodAction(type, detail_json)
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
setError(result.error)
|
|
74
|
+
setFieldErrors(result.field_errors || {})
|
|
56
75
|
setPending(false)
|
|
76
|
+
return
|
|
57
77
|
}
|
|
78
|
+
window.location.reload()
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
async function handleDefault(id: string) {
|
|
61
82
|
setPending(true)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
setError(null)
|
|
84
|
+
setFieldErrors({})
|
|
85
|
+
const result = await makeDefaultPaymentMethodAction(id)
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
setError(result.error)
|
|
66
88
|
setPending(false)
|
|
89
|
+
return
|
|
67
90
|
}
|
|
91
|
+
window.location.reload()
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
async function handleDelete(id: string) {
|
|
71
95
|
if (!confirm("Remove this payment method?")) return
|
|
72
96
|
setPending(true)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
setError(null)
|
|
98
|
+
setFieldErrors({})
|
|
99
|
+
const result = await deletePaymentMethodAction(id)
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
setError(result.error)
|
|
77
102
|
setPending(false)
|
|
103
|
+
return
|
|
78
104
|
}
|
|
105
|
+
window.location.reload()
|
|
79
106
|
}
|
|
80
107
|
|
|
81
108
|
function labelFor(detail: PaymentDetail) {
|
|
82
109
|
const json = detail.detail_json ?? {}
|
|
83
110
|
if (detail.type === "upi") return `UPI · ${json.upi_id || "—"}`
|
|
84
|
-
if (detail.type === "bank")
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
if (detail.type === "bank") {
|
|
112
|
+
const holder = json.account_holder_name || json.account_holder || "—"
|
|
113
|
+
const last4 = json.account_number?.slice(-4) || "****"
|
|
114
|
+
const bank = json.bank_name ? ` · ${json.bank_name}` : ""
|
|
115
|
+
return `Bank · ${holder}${bank} (····${last4})`
|
|
116
|
+
}
|
|
117
|
+
const holder = json.card_holder_name || "—"
|
|
118
|
+
const last4 =
|
|
119
|
+
json.card_number?.replace(/\s+/g, "").slice(-4) ||
|
|
120
|
+
json.card_last4 ||
|
|
121
|
+
"****"
|
|
122
|
+
return `Card · ${holder} (····${last4})`
|
|
87
123
|
}
|
|
88
124
|
|
|
89
125
|
return (
|
|
90
126
|
<div className="space-y-6">
|
|
91
127
|
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
92
128
|
<div>
|
|
93
|
-
<h2 className=
|
|
94
|
-
<p className=
|
|
129
|
+
<h2 className={acct.title}>Payment methods</h2>
|
|
130
|
+
<p className={`text-sm ${acct.muted}`}>Refund destinations for returns and exchanges.</p>
|
|
95
131
|
</div>
|
|
96
|
-
<button type="button" onClick={() => setShowForm((v) => !v)} className=
|
|
132
|
+
<button type="button" onClick={() => setShowForm((v) => !v)} className={acct.btnPrimary}>
|
|
97
133
|
{showForm ? "Cancel" : "Add method"}
|
|
98
134
|
</button>
|
|
99
135
|
</div>
|
|
100
136
|
|
|
101
137
|
{showForm && (
|
|
102
|
-
<form onSubmit={handleAdd} className=
|
|
138
|
+
<form onSubmit={handleAdd} className={`${acct.card} p-6 space-y-4`}>
|
|
103
139
|
<div className="flex flex-wrap gap-2">
|
|
104
140
|
{(["upi", "bank", "card"] as const).map((t) => (
|
|
105
141
|
<button
|
|
106
142
|
key={t}
|
|
107
143
|
type="button"
|
|
108
|
-
onClick={() =>
|
|
109
|
-
|
|
144
|
+
onClick={() => {
|
|
145
|
+
setType(t)
|
|
146
|
+
setError(null)
|
|
147
|
+
setFieldErrors({})
|
|
148
|
+
}}
|
|
149
|
+
className={`px-3 py-1.5 text-xs font-medium rounded-none border ${
|
|
110
150
|
type === t
|
|
111
|
-
? "border-brand-accent text-brand-accent bg-brand-accent
|
|
151
|
+
? "border-brand-accent text-brand-accent bg-brand-accent-muted"
|
|
112
152
|
: "border-cart-border text-muted"
|
|
113
153
|
}`}
|
|
114
154
|
>
|
|
@@ -121,46 +161,87 @@ export default function AccountPaymentMethods({
|
|
|
121
161
|
<Field
|
|
122
162
|
label="UPI ID"
|
|
123
163
|
value={form.upi_id}
|
|
164
|
+
error={fieldErrors.upi_id}
|
|
124
165
|
onChange={(v) => setForm((f) => ({ ...f, upi_id: v }))}
|
|
125
166
|
/>
|
|
126
167
|
)}
|
|
168
|
+
|
|
127
169
|
{type === "bank" && (
|
|
128
170
|
<>
|
|
129
171
|
<Field
|
|
130
|
-
label="Account holder"
|
|
131
|
-
value={form.
|
|
132
|
-
|
|
172
|
+
label="Account holder name"
|
|
173
|
+
value={form.account_holder_name}
|
|
174
|
+
error={fieldErrors.account_holder_name}
|
|
175
|
+
onChange={(v) => setForm((f) => ({ ...f, account_holder_name: v }))}
|
|
176
|
+
/>
|
|
177
|
+
<Field
|
|
178
|
+
label="Bank name"
|
|
179
|
+
value={form.bank_name}
|
|
180
|
+
error={fieldErrors.bank_name}
|
|
181
|
+
onChange={(v) => setForm((f) => ({ ...f, bank_name: v }))}
|
|
133
182
|
/>
|
|
134
183
|
<Field
|
|
135
184
|
label="Account number"
|
|
136
185
|
value={form.account_number}
|
|
186
|
+
error={fieldErrors.account_number}
|
|
137
187
|
onChange={(v) => setForm((f) => ({ ...f, account_number: v }))}
|
|
138
188
|
/>
|
|
139
189
|
<Field
|
|
140
190
|
label="IFSC"
|
|
141
191
|
value={form.ifsc}
|
|
192
|
+
error={fieldErrors.ifsc}
|
|
142
193
|
onChange={(v) => setForm((f) => ({ ...f, ifsc: v }))}
|
|
143
194
|
/>
|
|
195
|
+
<Field
|
|
196
|
+
label="Branch name (optional)"
|
|
197
|
+
value={form.branch_name}
|
|
198
|
+
required={false}
|
|
199
|
+
error={fieldErrors.branch_name}
|
|
200
|
+
onChange={(v) => setForm((f) => ({ ...f, branch_name: v }))}
|
|
201
|
+
/>
|
|
144
202
|
</>
|
|
145
203
|
)}
|
|
204
|
+
|
|
146
205
|
{type === "card" && (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
206
|
+
<>
|
|
207
|
+
<Field
|
|
208
|
+
label="Card holder name"
|
|
209
|
+
value={form.card_holder_name}
|
|
210
|
+
error={fieldErrors.card_holder_name}
|
|
211
|
+
onChange={(v) => setForm((f) => ({ ...f, card_holder_name: v }))}
|
|
212
|
+
/>
|
|
213
|
+
<Field
|
|
214
|
+
label="Card number"
|
|
215
|
+
value={form.card_number}
|
|
216
|
+
error={fieldErrors.card_number}
|
|
217
|
+
onChange={(v) => setForm((f) => ({ ...f, card_number: v }))}
|
|
218
|
+
/>
|
|
219
|
+
<Field
|
|
220
|
+
label="Expiry (MM/YY)"
|
|
221
|
+
value={form.expiry_date}
|
|
222
|
+
error={fieldErrors.expiry_date}
|
|
223
|
+
placeholder="08/28"
|
|
224
|
+
onChange={(v) => setForm((f) => ({ ...f, expiry_date: v }))}
|
|
225
|
+
/>
|
|
226
|
+
<Field
|
|
227
|
+
label="CVV"
|
|
228
|
+
value={form.cvv}
|
|
229
|
+
error={fieldErrors.cvv}
|
|
230
|
+
onChange={(v) => setForm((f) => ({ ...f, cvv: v }))}
|
|
231
|
+
/>
|
|
232
|
+
</>
|
|
152
233
|
)}
|
|
153
234
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<button type="submit" disabled={pending} className="btn-primary text-sm disabled:opacity-60">
|
|
235
|
+
<button type="submit" disabled={pending} className={acct.btnPrimary}>
|
|
157
236
|
{pending ? "Saving…" : "Save payment method"}
|
|
158
237
|
</button>
|
|
159
238
|
</form>
|
|
160
239
|
)}
|
|
161
240
|
|
|
241
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
242
|
+
|
|
162
243
|
{paymentDetails.length === 0 ? (
|
|
163
|
-
<div className=
|
|
244
|
+
<div className={`${acct.cardMuted} p-8 text-center text-sm ${acct.muted}`}>
|
|
164
245
|
No refund payment methods saved.
|
|
165
246
|
</div>
|
|
166
247
|
) : (
|
|
@@ -168,7 +249,7 @@ export default function AccountPaymentMethods({
|
|
|
168
249
|
{paymentDetails.map((detail) => (
|
|
169
250
|
<li
|
|
170
251
|
key={detail.id}
|
|
171
|
-
className=
|
|
252
|
+
className={`${acct.card} p-5 flex flex-wrap justify-between gap-4 items-center`}
|
|
172
253
|
>
|
|
173
254
|
<div>
|
|
174
255
|
<p className="font-medium text-heading text-sm">{labelFor(detail)}</p>
|
|
@@ -191,7 +272,7 @@ export default function AccountPaymentMethods({
|
|
|
191
272
|
type="button"
|
|
192
273
|
onClick={() => handleDelete(detail.id)}
|
|
193
274
|
disabled={pending}
|
|
194
|
-
className="text-
|
|
275
|
+
className="text-red-600 hover:underline disabled:opacity-60"
|
|
195
276
|
>
|
|
196
277
|
Remove
|
|
197
278
|
</button>
|
|
@@ -208,22 +289,33 @@ function Field({
|
|
|
208
289
|
label,
|
|
209
290
|
value,
|
|
210
291
|
onChange,
|
|
292
|
+
error,
|
|
293
|
+
required = true,
|
|
294
|
+
placeholder,
|
|
211
295
|
}: {
|
|
212
296
|
label: string
|
|
213
297
|
value: string
|
|
214
298
|
onChange: (value: string) => void
|
|
299
|
+
error?: string
|
|
300
|
+
required?: boolean
|
|
301
|
+
placeholder?: string
|
|
215
302
|
}) {
|
|
216
303
|
return (
|
|
217
304
|
<label className="block">
|
|
218
|
-
<span className="text-xs font-semibold uppercase tracking-
|
|
305
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-muted mb-1.5 block">
|
|
219
306
|
{label}
|
|
220
307
|
</span>
|
|
221
308
|
<input
|
|
222
309
|
value={value}
|
|
223
310
|
onChange={(e) => onChange(e.target.value)}
|
|
224
|
-
required
|
|
225
|
-
|
|
311
|
+
required={required}
|
|
312
|
+
placeholder={placeholder}
|
|
313
|
+
aria-invalid={Boolean(error)}
|
|
314
|
+
className={`w-full border rounded-none px-3 py-2.5 text-sm bg-surface text-heading focus:outline-none focus:border-brand-accent ${
|
|
315
|
+
error ? "border-red-500" : "border-cart-border"
|
|
316
|
+
}`}
|
|
226
317
|
/>
|
|
318
|
+
{error && <span className="mt-1 block text-xs text-red-600">{error}</span>}
|
|
227
319
|
</label>
|
|
228
320
|
)
|
|
229
321
|
}
|
package/src/account-profile.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./auth-server"
|
|
10
10
|
import type { OtpVerificationComponent } from "./otp-component"
|
|
11
11
|
import OtpVerificationModal from "./otp-verification-modal"
|
|
12
|
+
import { acct } from "./account-theme"
|
|
12
13
|
|
|
13
14
|
export default function AccountProfile({
|
|
14
15
|
customer,
|
|
@@ -94,28 +95,28 @@ export default function AccountProfile({
|
|
|
94
95
|
return (
|
|
95
96
|
<div className="space-y-8">
|
|
96
97
|
<div>
|
|
97
|
-
<h2 className=
|
|
98
|
-
<p className=
|
|
98
|
+
<h2 className={acct.title}>Account details</h2>
|
|
99
|
+
<p className={`text-sm ${acct.muted}`}>Update your name and contact information.</p>
|
|
99
100
|
</div>
|
|
100
101
|
|
|
101
|
-
<form action={formAction} className=
|
|
102
|
+
<form action={formAction} className={`${acct.card} p-6 space-y-4`}>
|
|
102
103
|
<Field label="Full name" name="full_name" defaultValue={fullName} />
|
|
103
104
|
<div>
|
|
104
|
-
<label className="block text-xs font-semibold uppercase tracking-
|
|
105
|
+
<label className="block text-xs font-semibold uppercase tracking-wider text-muted mb-1.5">
|
|
105
106
|
Email
|
|
106
107
|
</label>
|
|
107
108
|
<div className="flex flex-wrap items-center gap-3">
|
|
108
109
|
<input
|
|
109
110
|
value={customer.email}
|
|
110
111
|
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
|
+
className="flex-1 min-w-[200px] border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface-muted text-muted"
|
|
112
113
|
/>
|
|
113
114
|
{!emailVerified && (
|
|
114
115
|
<button
|
|
115
116
|
type="button"
|
|
116
117
|
onClick={() => sendVerifyOtp("email_verification")}
|
|
117
118
|
disabled={otpPending}
|
|
118
|
-
className=
|
|
119
|
+
className={`${acct.btnOutline} !py-2 !px-3`}
|
|
119
120
|
>
|
|
120
121
|
Verify email
|
|
121
122
|
</button>
|
|
@@ -133,7 +134,7 @@ export default function AccountProfile({
|
|
|
133
134
|
type="button"
|
|
134
135
|
onClick={() => sendVerifyOtp("phone_verification")}
|
|
135
136
|
disabled={otpPending}
|
|
136
|
-
className=
|
|
137
|
+
className={`${acct.btnOutline} !py-2 !px-3`}
|
|
137
138
|
>
|
|
138
139
|
Verify phone
|
|
139
140
|
</button>
|
|
@@ -145,12 +146,12 @@ export default function AccountProfile({
|
|
|
145
146
|
</div>
|
|
146
147
|
|
|
147
148
|
{(state.error || message) && (
|
|
148
|
-
<p className={`text-sm ${state.ok || message ? "text-green-600" : "text-
|
|
149
|
+
<p className={`text-sm ${state.ok || message ? "text-green-600" : "text-red-600"}`}>
|
|
149
150
|
{message || state.error}
|
|
150
151
|
</p>
|
|
151
152
|
)}
|
|
152
153
|
|
|
153
|
-
<button type="submit" disabled={pending} className=
|
|
154
|
+
<button type="submit" disabled={pending} className={acct.btnPrimary}>
|
|
154
155
|
{pending ? "Saving…" : "Save changes"}
|
|
155
156
|
</button>
|
|
156
157
|
</form>
|
|
@@ -184,14 +185,14 @@ function Field({
|
|
|
184
185
|
}) {
|
|
185
186
|
return (
|
|
186
187
|
<label className="block">
|
|
187
|
-
<span className="text-xs font-semibold uppercase tracking-
|
|
188
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-muted mb-1.5 block">
|
|
188
189
|
{label}
|
|
189
190
|
</span>
|
|
190
191
|
<input
|
|
191
192
|
name={name}
|
|
192
193
|
type={type}
|
|
193
194
|
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
|
+
className="w-full border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface text-heading focus:outline-none focus:border-brand-accent"
|
|
195
196
|
/>
|
|
196
197
|
</label>
|
|
197
198
|
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Account / order visual tokens — maps to segment-tokens CSS variables */
|
|
2
|
+
export const acct = {
|
|
3
|
+
pageBg: "bg-page-bg",
|
|
4
|
+
card: "bg-surface border border-cart-border rounded-none shadow-card",
|
|
5
|
+
cardMuted: "bg-surface-muted border border-cart-border rounded-none",
|
|
6
|
+
title:
|
|
7
|
+
"font-heading text-2xl sm:text-3xl font-extrabold tracking-tight text-heading",
|
|
8
|
+
sectionTitle:
|
|
9
|
+
"text-base sm:text-lg font-bold uppercase tracking-wider text-heading",
|
|
10
|
+
subsection: "text-sm font-semibold uppercase tracking-wider text-muted",
|
|
11
|
+
accent: "text-brand-accent",
|
|
12
|
+
muted: "text-muted",
|
|
13
|
+
heading: "text-heading",
|
|
14
|
+
body: "text-sm text-body",
|
|
15
|
+
border: "border-cart-border",
|
|
16
|
+
btnPrimary:
|
|
17
|
+
"inline-flex items-center justify-center bg-brand-primary hover:bg-brand-accent text-inverse rounded-none px-6 py-3 font-bold text-xs uppercase tracking-widest transition-all disabled:opacity-50",
|
|
18
|
+
btnAccent:
|
|
19
|
+
"inline-flex items-center justify-center bg-brand-accent hover:bg-brand-accent-hover text-inverse rounded-none px-6 py-3 font-bold text-xs uppercase tracking-widest transition-all disabled:opacity-50",
|
|
20
|
+
btnOutline:
|
|
21
|
+
"inline-flex items-center justify-center border border-cart-border text-brand-accent rounded-none px-6 py-3 font-bold text-xs uppercase tracking-widest hover:bg-surface-muted transition-all disabled:opacity-50",
|
|
22
|
+
input:
|
|
23
|
+
"w-full border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface text-heading focus:outline-none focus:border-brand-accent",
|
|
24
|
+
label:
|
|
25
|
+
"block text-xs font-semibold uppercase tracking-wider text-muted mb-1.5",
|
|
26
|
+
badge:
|
|
27
|
+
"inline-flex items-center px-3 py-1 text-xs font-semibold border rounded-none",
|
|
28
|
+
} as const
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import { getAuthHeaders } from "@pradip1995/commerce-core/data/cookies"
|
|
4
|
+
|
|
5
|
+
type PaymentMethodType = "upi" | "bank" | "card"
|
|
6
|
+
|
|
7
|
+
type ActionResult =
|
|
8
|
+
| { success: true }
|
|
9
|
+
| { success: false; error: string; field_errors?: Record<string, string> }
|
|
10
|
+
|
|
11
|
+
function getBaseUrl() {
|
|
12
|
+
return process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getPublishableKey() {
|
|
16
|
+
return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ""
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function trimAll(detail_json: Record<string, string>): Record<string, string> {
|
|
20
|
+
const out: Record<string, string> = {}
|
|
21
|
+
for (const [key, value] of Object.entries(detail_json)) {
|
|
22
|
+
if (typeof value !== "string") continue
|
|
23
|
+
const trimmed = value.trim()
|
|
24
|
+
if (!trimmed) continue
|
|
25
|
+
out[key] = key === "ifsc" ? trimmed.toUpperCase() : trimmed
|
|
26
|
+
}
|
|
27
|
+
return out
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validatePaymentDetail(
|
|
31
|
+
type: PaymentMethodType,
|
|
32
|
+
detail_json: Record<string, string>
|
|
33
|
+
): Record<string, string> {
|
|
34
|
+
const field_errors: Record<string, string> = {}
|
|
35
|
+
|
|
36
|
+
if (type === "upi") {
|
|
37
|
+
const upi = detail_json.upi_id || ""
|
|
38
|
+
if (!upi) {
|
|
39
|
+
field_errors.upi_id = "UPI ID is required"
|
|
40
|
+
} else if (!/^[\w.\-]{2,256}@[a-zA-Z]{2,64}$/.test(upi)) {
|
|
41
|
+
field_errors.upi_id = "Enter a valid UPI ID (e.g. name@upi)"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (type === "bank") {
|
|
46
|
+
const holder = detail_json.account_holder_name || ""
|
|
47
|
+
const bank = detail_json.bank_name || ""
|
|
48
|
+
const account = detail_json.account_number || ""
|
|
49
|
+
const ifsc = detail_json.ifsc || ""
|
|
50
|
+
|
|
51
|
+
if (!holder) {
|
|
52
|
+
field_errors.account_holder_name = "Account holder name is required"
|
|
53
|
+
}
|
|
54
|
+
if (!bank) {
|
|
55
|
+
field_errors.bank_name = "Bank name is required"
|
|
56
|
+
}
|
|
57
|
+
if (!account) {
|
|
58
|
+
field_errors.account_number = "Account number is required"
|
|
59
|
+
} else if (!/^\d{9,18}$/.test(account)) {
|
|
60
|
+
field_errors.account_number = "Account number must be 9–18 digits"
|
|
61
|
+
}
|
|
62
|
+
if (!ifsc) {
|
|
63
|
+
field_errors.ifsc = "IFSC is required"
|
|
64
|
+
} else if (!/^[A-Z]{4}0[A-Z0-9]{6}$/.test(ifsc)) {
|
|
65
|
+
field_errors.ifsc =
|
|
66
|
+
"Enter a valid IFSC (e.g. HDFC0001234) — 4 letters, 0, then 6 characters"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (type === "card") {
|
|
71
|
+
const holder = detail_json.card_holder_name || ""
|
|
72
|
+
const number = detail_json.card_number || ""
|
|
73
|
+
const expiry = detail_json.expiry_date || ""
|
|
74
|
+
const cvv = detail_json.cvv || ""
|
|
75
|
+
|
|
76
|
+
if (!holder) {
|
|
77
|
+
field_errors.card_holder_name = "Card holder name is required"
|
|
78
|
+
}
|
|
79
|
+
if (!number) {
|
|
80
|
+
field_errors.card_number = "Card number is required"
|
|
81
|
+
} else if (!/^\d{13,19}$/.test(number.replace(/\s+/g, ""))) {
|
|
82
|
+
field_errors.card_number = "Enter a valid card number (13–19 digits)"
|
|
83
|
+
}
|
|
84
|
+
if (!expiry) {
|
|
85
|
+
field_errors.expiry_date = "Expiry date is required"
|
|
86
|
+
} else if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(expiry)) {
|
|
87
|
+
field_errors.expiry_date = "Use MM/YY format (e.g. 08/28)"
|
|
88
|
+
}
|
|
89
|
+
if (!cvv) {
|
|
90
|
+
field_errors.cvv = "CVV is required"
|
|
91
|
+
} else if (!/^\d{3,4}$/.test(cvv)) {
|
|
92
|
+
field_errors.cvv = "CVV must be 3 or 4 digits"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return field_errors
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function firstFieldError(field_errors: Record<string, string>): string {
|
|
100
|
+
return Object.values(field_errors)[0] || "Invalid payment details"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseBackendError(body: unknown, status: number): string {
|
|
104
|
+
if (!body || typeof body !== "object") {
|
|
105
|
+
return status === 400
|
|
106
|
+
? "Invalid payment details. Check the entered values."
|
|
107
|
+
: `Request failed (${status})`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = body as {
|
|
111
|
+
message?: unknown
|
|
112
|
+
errors?: Array<{ message?: string; path?: string[]; field?: string }>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Array.isArray(data.errors) && data.errors.length > 0) {
|
|
116
|
+
const details = data.errors
|
|
117
|
+
.map((item) => {
|
|
118
|
+
const path = item.path?.join(".") || item.field
|
|
119
|
+
const msg = item.message?.trim()
|
|
120
|
+
if (path && msg) return `${path}: ${msg}`
|
|
121
|
+
return msg
|
|
122
|
+
})
|
|
123
|
+
.filter((msg): msg is string => Boolean(msg))
|
|
124
|
+
if (details.length) return details.join("; ")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof data.message === "string" && data.message.trim()) {
|
|
128
|
+
const msg = data.message.trim()
|
|
129
|
+
if (/^invalid request( data)?\.?$/i.test(msg)) {
|
|
130
|
+
return "Invalid payment details. Check required fields and try again."
|
|
131
|
+
}
|
|
132
|
+
return msg
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return `Request failed (${status})`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function paymentDetailsFetch(
|
|
139
|
+
path: string,
|
|
140
|
+
init: { method: string; body?: Record<string, unknown> }
|
|
141
|
+
): Promise<ActionResult> {
|
|
142
|
+
const authHeaders = await getAuthHeaders()
|
|
143
|
+
if (!("authorization" in authHeaders)) {
|
|
144
|
+
return { success: false, error: "Please sign in to manage payment methods" }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const response = await fetch(`${getBaseUrl()}${path}`, {
|
|
148
|
+
method: init.method,
|
|
149
|
+
headers: {
|
|
150
|
+
Accept: "application/json",
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"x-publishable-api-key": getPublishableKey(),
|
|
153
|
+
...authHeaders,
|
|
154
|
+
},
|
|
155
|
+
body: init.body ? JSON.stringify(init.body) : undefined,
|
|
156
|
+
cache: "no-store",
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (response.ok) {
|
|
160
|
+
return { success: true }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const body = await response.json().catch(() => null)
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: parseBackendError(body, response.status),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function addPaymentMethodAction(
|
|
171
|
+
type: PaymentMethodType,
|
|
172
|
+
detail_json: Record<string, string>
|
|
173
|
+
): Promise<ActionResult> {
|
|
174
|
+
const normalized = trimAll(detail_json)
|
|
175
|
+
if (normalized.card_number) {
|
|
176
|
+
normalized.card_number = normalized.card_number.replace(/\s+/g, "")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const field_errors = validatePaymentDetail(type, normalized)
|
|
180
|
+
if (Object.keys(field_errors).length > 0) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: firstFieldError(field_errors),
|
|
184
|
+
field_errors,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return paymentDetailsFetch("/store/payment-details", {
|
|
189
|
+
method: "POST",
|
|
190
|
+
body: { type, detail_json: normalized },
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function makeDefaultPaymentMethodAction(
|
|
195
|
+
id: string
|
|
196
|
+
): Promise<ActionResult> {
|
|
197
|
+
if (!id) {
|
|
198
|
+
return { success: false, error: "Payment method id is required" }
|
|
199
|
+
}
|
|
200
|
+
return paymentDetailsFetch(`/store/payment-details/${id}/make-default`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function deletePaymentMethodAction(
|
|
206
|
+
id: string
|
|
207
|
+
): Promise<ActionResult> {
|
|
208
|
+
if (!id) {
|
|
209
|
+
return { success: false, error: "Payment method id is required" }
|
|
210
|
+
}
|
|
211
|
+
return paymentDetailsFetch(`/store/payment-details/${id}`, {
|
|
212
|
+
method: "DELETE",
|
|
213
|
+
})
|
|
214
|
+
}
|
package/src/segment.tsx
CHANGED
|
@@ -18,9 +18,9 @@ type AccountProps = {
|
|
|
18
18
|
countryCode?: string
|
|
19
19
|
path?: string
|
|
20
20
|
customer?: HttpTypes.StoreCustomer | null
|
|
21
|
-
orders?: HttpTypes.StoreOrder[]
|
|
21
|
+
orders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
|
|
22
22
|
paymentDetails?: Record<string, unknown>[]
|
|
23
|
-
guestOrders?: HttpTypes.StoreOrder[]
|
|
23
|
+
guestOrders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
|
|
24
24
|
title?: string
|
|
25
25
|
subtitle?: string
|
|
26
26
|
otpComponent?: OtpVerificationComponent
|