@cartbase/storefront 0.1.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 +226 -0
- package/src/api/auth.ts +108 -0
- package/src/api/carts.ts +506 -0
- package/src/api/categories.ts +184 -0
- package/src/api/checkout.ts +440 -0
- package/src/api/collections.ts +130 -0
- package/src/api/consent.ts +75 -0
- package/src/api/content.ts +125 -0
- package/src/api/customers.ts +307 -0
- package/src/api/gift-cards.ts +112 -0
- package/src/api/http.ts +122 -0
- package/src/api/index.ts +29 -0
- package/src/api/integrations.ts +130 -0
- package/src/api/menus.ts +77 -0
- package/src/api/metaobjects.ts +136 -0
- package/src/api/orders.ts +290 -0
- package/src/api/products.ts +303 -0
- package/src/api/redirects.ts +37 -0
- package/src/api/regions.ts +141 -0
- package/src/api/reviews.ts +259 -0
- package/src/api/search.ts +133 -0
- package/src/api/types.ts +91 -0
- package/src/cart-drawer/cart-drawer.tsx +86 -0
- package/src/cart-drawer/context.tsx +569 -0
- package/src/cart-drawer/continue-shopping.tsx +28 -0
- package/src/cart-drawer/cross-sell-carousel.tsx +212 -0
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -0
- package/src/cart-drawer/empty.tsx +52 -0
- package/src/cart-drawer/free-gift.tsx +71 -0
- package/src/cart-drawer/gift-wrap.tsx +83 -0
- package/src/cart-drawer/header.tsx +52 -0
- package/src/cart-drawer/index.ts +69 -0
- package/src/cart-drawer/item/index.tsx +164 -0
- package/src/cart-drawer/item/quantity.tsx +100 -0
- package/src/cart-drawer/item/upsell.tsx +110 -0
- package/src/cart-drawer/item/variant.tsx +46 -0
- package/src/cart-drawer/labels-bg.ts +72 -0
- package/src/cart-drawer/labels.ts +119 -0
- package/src/cart-drawer/notes.tsx +131 -0
- package/src/cart-drawer/payment-badges.tsx +96 -0
- package/src/cart-drawer/promo-banner.tsx +43 -0
- package/src/cart-drawer/rewards-points.tsx +78 -0
- package/src/cart-drawer/sticky-footer.tsx +73 -0
- package/src/cart-drawer/summary-breakdown.tsx +196 -0
- package/src/cart-drawer/template.tsx +225 -0
- package/src/cart-drawer/tiered-progress.tsx +168 -0
- package/src/checkout/address-error-copy.ts +119 -0
- package/src/checkout/address-form.tsx +224 -0
- package/src/checkout/address-select.tsx +79 -0
- package/src/checkout/boxnow-locker-selector.tsx +410 -0
- package/src/checkout/checkout-client.tsx +222 -0
- package/src/checkout/company-details.tsx +94 -0
- package/src/checkout/compare-addresses.ts +40 -0
- package/src/checkout/context.tsx +76 -0
- package/src/checkout/discount-section.tsx +218 -0
- package/src/checkout/econt-office-selector.tsx +332 -0
- package/src/checkout/error-message.tsx +25 -0
- package/src/checkout/geocode.ts +154 -0
- package/src/checkout/gift-card-section.tsx +224 -0
- package/src/checkout/index.ts +74 -0
- package/src/checkout/labels-bg.ts +128 -0
- package/src/checkout/labels.ts +263 -0
- package/src/checkout/line-item-card.tsx +152 -0
- package/src/checkout/order-summary.tsx +524 -0
- package/src/checkout/payment-button.tsx +373 -0
- package/src/checkout/payment-error-copy.ts +269 -0
- package/src/checkout/payment-method-list.tsx +365 -0
- package/src/checkout/payment-wrapper.tsx +102 -0
- package/src/checkout/promotion-error-copy.ts +124 -0
- package/src/checkout/shipping-method-list.tsx +335 -0
- package/src/checkout/stripe-wrapper.tsx +165 -0
- package/src/checkout/use-checkout-orchestration.ts +1504 -0
- package/src/common/cart-button-client.tsx +39 -0
- package/src/common/cart-button.tsx +28 -0
- package/src/common/country-select.tsx +65 -0
- package/src/common/delete-button.tsx +66 -0
- package/src/common/index.ts +17 -0
- package/src/common/language-select.tsx +78 -0
- package/src/common/localized-link.tsx +45 -0
- package/src/common/skeleton.tsx +29 -0
- package/src/index.ts +12 -0
- package/src/lib/cart-helpers.ts +113 -0
- package/src/lib/dual-price.tsx +73 -0
- package/src/lib/get-percentage-diff.ts +5 -0
- package/src/lib/get-product-price.ts +133 -0
- package/src/lib/hooks/use-intersection.ts +30 -0
- package/src/lib/hooks/use-toggle-state.ts +25 -0
- package/src/lib/money.ts +73 -0
- package/src/lib/payment-constants.ts +66 -0
- package/src/lib/product.ts +22 -0
- package/src/lib/sort-products.ts +63 -0
- package/src/lib/store-api-error.ts +36 -0
- package/src/lib/utils.ts +16 -0
- package/src/order/context.tsx +32 -0
- package/src/order/index.ts +63 -0
- package/src/order/labels-bg.ts +39 -0
- package/src/order/labels.ts +79 -0
- package/src/order/order-address-card.tsx +47 -0
- package/src/order/order-completed-template.tsx +165 -0
- package/src/order/order-confirmation-header.tsx +65 -0
- package/src/order/order-delivery-card.tsx +258 -0
- package/src/order/order-help-section.tsx +47 -0
- package/src/order/order-item.tsx +201 -0
- package/src/order/order-items-list.tsx +52 -0
- package/src/order/order-payment-card.tsx +95 -0
- package/src/order/order-timeline.tsx +141 -0
- package/src/order/order-totals.tsx +245 -0
- package/src/primitives/field.tsx +125 -0
- package/src/primitives/select-field.tsx +77 -0
- package/src/primitives/ui/accordion.tsx +61 -0
- package/src/primitives/ui/button.tsx +68 -0
- package/src/primitives/ui/collapsible.tsx +16 -0
- package/src/primitives/ui/dialog.tsx +112 -0
- package/src/primitives/ui/input.tsx +30 -0
- package/src/primitives/ui/label.tsx +31 -0
- package/src/primitives/ui/popover.tsx +38 -0
- package/src/primitives/ui/select.tsx +163 -0
- package/src/primitives/ui/sheet.tsx +131 -0
- package/src/primitives/ui/tabs.tsx +62 -0
- package/src/products/context.tsx +34 -0
- package/src/products/image-gallery.tsx +43 -0
- package/src/products/index.ts +44 -0
- package/src/products/labels-bg.ts +35 -0
- package/src/products/labels.ts +57 -0
- package/src/products/mobile-actions.tsx +180 -0
- package/src/products/option-select.tsx +67 -0
- package/src/products/preview-price.tsx +36 -0
- package/src/products/product-actions-wrapper.tsx +58 -0
- package/src/products/product-actions.tsx +217 -0
- package/src/products/product-info.tsx +43 -0
- package/src/products/product-preview.tsx +49 -0
- package/src/products/product-price.tsx +69 -0
- package/src/products/product-tabs.tsx +169 -0
- package/src/products/product-template.tsx +114 -0
- package/src/products/purchase-options.tsx +130 -0
- package/src/products/related-products.tsx +86 -0
- package/src/products/thumbnail.tsx +71 -0
- package/src/products/variant-matching.ts +71 -0
- package/src/reviews-ui/helpers.ts +174 -0
- package/src/reviews-ui/index.ts +74 -0
- package/src/reviews-ui/labels-bg.ts +91 -0
- package/src/reviews-ui/labels.ts +199 -0
- package/src/reviews-ui/photo-upload.tsx +345 -0
- package/src/reviews-ui/review-list.tsx +249 -0
- package/src/reviews-ui/review-widget.tsx +224 -0
- package/src/reviews-ui/review-wizard.tsx +560 -0
- package/src/reviews-ui/star-badge.tsx +104 -0
- package/src/reviews-ui/wizard-state.ts +81 -0
- package/src/store/category-template.tsx +129 -0
- package/src/store/collection-template.tsx +139 -0
- package/src/store/index.ts +41 -0
- package/src/store/labels-bg.ts +22 -0
- package/src/store/labels.ts +52 -0
- package/src/store/paginated-products.tsx +116 -0
- package/src/store/pagination.tsx +103 -0
- package/src/store/search-params.ts +256 -0
- package/src/store/search-template.tsx +249 -0
- package/src/store/skeleton-product-grid.tsx +26 -0
- package/src/store/sort-select.tsx +81 -0
- package/src/store/store-template.tsx +65 -0
- package/src/tracking/attribution.ts +418 -0
- package/src/tracking/consent-banner.tsx +355 -0
- package/src/tracking/consent-init.tsx +44 -0
- package/src/tracking/consent.ts +243 -0
- package/src/tracking/fbq.ts +168 -0
- package/src/tracking/ga4.tsx +49 -0
- package/src/tracking/get-tracking-attribution.ts +224 -0
- package/src/tracking/get-tracking-config.ts +50 -0
- package/src/tracking/gtag.ts +200 -0
- package/src/tracking/index.ts +133 -0
- package/src/tracking/meta-pixel.tsx +166 -0
- package/src/tracking/rybbit-events.ts +242 -0
- package/src/tracking/rybbit.tsx +40 -0
- package/src/tracking/types.ts +185 -0
- package/src/tracking/use-engagement-time.ts +58 -0
- package/tailwind-preset.cjs +72 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import Image from "next/image"
|
|
2
|
+
import { ImageOff } from "lucide-react"
|
|
3
|
+
import { DualPrice } from "../lib/dual-price"
|
|
4
|
+
import type { StoreOrderItem } from "../api/orders"
|
|
5
|
+
import { defaultOrderLabels, type OrderLabels } from "./labels"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Order line row. Ported from `@1click/ui/src/order/order-item.tsx`
|
|
9
|
+
* (v2.3.1) with the data seam translated to barter:
|
|
10
|
+
*
|
|
11
|
+
* Medusa's `StoreOrderLineItem` carried per-line computed totals
|
|
12
|
+
* (`total` / `original_total`). Barter's store order surface does NOT —
|
|
13
|
+
* `StoreOrderDetail.items` are version-pivot rows (`{quantity, line_item}`)
|
|
14
|
+
* whose embedded `order_line_items` row carries only `unit_price` (see
|
|
15
|
+
* docs/storefront/orders.md). The component therefore renders a normalized
|
|
16
|
+
* `OrderDisplayItem`, produced by one of the pure converters below:
|
|
17
|
+
*
|
|
18
|
+
* - `displayItemFromOrderItem(pivot)` — from the order detail. `total` is
|
|
19
|
+
* `unit_price × quantity` (the pre-adjustment line amount — the order
|
|
20
|
+
* surface exposes no per-line adjustment data; totals rows remain the
|
|
21
|
+
* money authority).
|
|
22
|
+
* - `displayItemFromCartLine(line)` — from the decorated cart the app
|
|
23
|
+
* still holds on the confirmation page (`api/carts` `CartLineItem`);
|
|
24
|
+
* this path keeps the SERVER-computed `total`/`original_total` and so
|
|
25
|
+
* the strike-through discount display. Prefer it right after
|
|
26
|
+
* `completeCart()`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Normalized, render-ready order line. */
|
|
30
|
+
export interface OrderDisplayItem {
|
|
31
|
+
id: string
|
|
32
|
+
title: string | null
|
|
33
|
+
/** Resolved variant caption (options joined, or variant_title). */
|
|
34
|
+
variantLabel: string | null
|
|
35
|
+
thumbnail: string | null
|
|
36
|
+
quantity: number
|
|
37
|
+
/** SERVER line total when available (cart path); else unit_price × qty. */
|
|
38
|
+
total: number
|
|
39
|
+
/** Pre-discount total — only present when the source had it (cart path). */
|
|
40
|
+
originalTotal?: number
|
|
41
|
+
metadata?: Record<string, unknown> | null
|
|
42
|
+
/** Preserved for the items-list newest-first sort, when the source had it. */
|
|
43
|
+
createdAt?: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Variant caption resolution — port of the source's `variantLabel` IIFE:
|
|
48
|
+
* option values joined with " / " when present, else the variant title
|
|
49
|
+
* unless it is the Medusa placeholder "Default variant".
|
|
50
|
+
*/
|
|
51
|
+
export function variantLabelFrom(input: {
|
|
52
|
+
variant_option_values?: Record<string, unknown> | null
|
|
53
|
+
variant_title?: string | null
|
|
54
|
+
}): string | null {
|
|
55
|
+
const values = input.variant_option_values
|
|
56
|
+
if (values && typeof values === "object") {
|
|
57
|
+
const parts = Object.values(values).filter(
|
|
58
|
+
(v): v is string | number => typeof v === "string" || typeof v === "number"
|
|
59
|
+
)
|
|
60
|
+
if (parts.length) return parts.map(String).join(" / ")
|
|
61
|
+
}
|
|
62
|
+
const title = input.variant_title
|
|
63
|
+
if (title && title !== "Default variant") return title
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Pure converter: order-detail pivot row → display line. */
|
|
68
|
+
export function displayItemFromOrderItem(
|
|
69
|
+
item: StoreOrderItem
|
|
70
|
+
): OrderDisplayItem {
|
|
71
|
+
const li = item.line_item
|
|
72
|
+
const quantity = Number(item.quantity ?? 0)
|
|
73
|
+
const unitPrice = Number(li?.unit_price ?? 0)
|
|
74
|
+
return {
|
|
75
|
+
id: item.id,
|
|
76
|
+
title: li?.product_title ?? li?.title ?? null,
|
|
77
|
+
variantLabel: li
|
|
78
|
+
? variantLabelFrom({
|
|
79
|
+
variant_option_values:
|
|
80
|
+
(li["variant_option_values"] as Record<string, unknown> | null) ??
|
|
81
|
+
null,
|
|
82
|
+
variant_title: li.variant_title,
|
|
83
|
+
})
|
|
84
|
+
: null,
|
|
85
|
+
thumbnail: li?.thumbnail ?? null,
|
|
86
|
+
quantity,
|
|
87
|
+
total: unitPrice * quantity,
|
|
88
|
+
metadata: li?.metadata ?? null,
|
|
89
|
+
createdAt: (li?.["created_at"] as string | undefined) ?? null,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Structural cart-line shape (satisfied by `api/carts` `CartLineItem`). */
|
|
94
|
+
export interface CartLineLike {
|
|
95
|
+
id: string
|
|
96
|
+
product_title?: string | null
|
|
97
|
+
title?: string | null
|
|
98
|
+
variant_title?: string | null
|
|
99
|
+
thumbnail?: string | null
|
|
100
|
+
quantity: number
|
|
101
|
+
total?: number | null
|
|
102
|
+
original_total?: number | null
|
|
103
|
+
unit_price?: number | null
|
|
104
|
+
metadata?: Record<string, unknown> | null
|
|
105
|
+
created_at?: string | null
|
|
106
|
+
[key: string]: unknown
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Pure converter: decorated cart line → display line (keeps server totals). */
|
|
110
|
+
export function displayItemFromCartLine(line: CartLineLike): OrderDisplayItem {
|
|
111
|
+
const quantity = Number(line.quantity ?? 0)
|
|
112
|
+
return {
|
|
113
|
+
id: line.id,
|
|
114
|
+
title: line.product_title ?? line.title ?? null,
|
|
115
|
+
variantLabel: variantLabelFrom({
|
|
116
|
+
variant_option_values:
|
|
117
|
+
(line["variant_option_values"] as Record<string, unknown> | null) ??
|
|
118
|
+
null,
|
|
119
|
+
variant_title: line.variant_title,
|
|
120
|
+
}),
|
|
121
|
+
thumbnail: line.thumbnail ?? null,
|
|
122
|
+
quantity,
|
|
123
|
+
total: Number(line.total ?? Number(line.unit_price ?? 0) * quantity),
|
|
124
|
+
originalTotal:
|
|
125
|
+
line.original_total !== null && line.original_total !== undefined
|
|
126
|
+
? Number(line.original_total)
|
|
127
|
+
: undefined,
|
|
128
|
+
metadata: line.metadata ?? null,
|
|
129
|
+
createdAt: line.created_at ?? null,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type OrderItemProps = {
|
|
134
|
+
item: OrderDisplayItem
|
|
135
|
+
currencyCode: string
|
|
136
|
+
labels?: Pick<OrderLabels, "qty">
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function OrderItem({ item, currencyCode, labels }: OrderItemProps) {
|
|
140
|
+
const l = { ...defaultOrderLabels, ...labels }
|
|
141
|
+
const hasDiscount =
|
|
142
|
+
item.originalTotal !== undefined && item.total < item.originalTotal
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="flex gap-4 py-4">
|
|
146
|
+
<div className="w-[64px] h-[64px] rounded-xl overflow-hidden bg-muted relative flex-shrink-0 shadow-sm">
|
|
147
|
+
{item.thumbnail ? (
|
|
148
|
+
<Image
|
|
149
|
+
src={item.thumbnail}
|
|
150
|
+
alt={item.title || ""}
|
|
151
|
+
fill
|
|
152
|
+
className="object-cover"
|
|
153
|
+
sizes="64px"
|
|
154
|
+
/>
|
|
155
|
+
) : (
|
|
156
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
157
|
+
<ImageOff className="w-5 h-5 text-muted-foreground" />
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="flex-1 min-w-0 flex flex-col justify-center">
|
|
163
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
164
|
+
<div className="min-w-0">
|
|
165
|
+
<span className="text-sm font-semibold text-foreground line-clamp-1 leading-snug block">
|
|
166
|
+
{item.title}
|
|
167
|
+
</span>
|
|
168
|
+
{item.variantLabel && (
|
|
169
|
+
<span className="text-xs text-muted-foreground mt-0.5 block">
|
|
170
|
+
{item.variantLabel}
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
<span className="text-xs text-muted-foreground mt-0.5 block">
|
|
174
|
+
{item.quantity} {l.qty}
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
177
|
+
<div className="text-right flex-shrink-0">
|
|
178
|
+
{hasDiscount && (
|
|
179
|
+
<span className="text-xs text-muted-foreground line-through block leading-none mb-0.5">
|
|
180
|
+
<DualPrice
|
|
181
|
+
amount={item.originalTotal ?? 0}
|
|
182
|
+
currencyCode={currencyCode}
|
|
183
|
+
className="text-xs text-muted-foreground"
|
|
184
|
+
/>
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
<DualPrice
|
|
188
|
+
amount={item.total}
|
|
189
|
+
currencyCode={currencyCode}
|
|
190
|
+
className={`text-sm font-bold ${
|
|
191
|
+
hasDiscount ? "text-success" : "text-foreground"
|
|
192
|
+
}`}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { type OrderItemProps }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { OrderItem, type OrderDisplayItem } from "./order-item"
|
|
2
|
+
import { isProductLine } from "../lib/cart-helpers"
|
|
3
|
+
import { defaultOrderLabels, type OrderLabels } from "./labels"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Order items list. Ported from `@1click/ui/src/order/order-items-list.tsx`
|
|
7
|
+
* (v2.3.1); data seam: takes normalized `OrderDisplayItem[]` (see
|
|
8
|
+
* `order-item.tsx` converters) + the currency, since barter's
|
|
9
|
+
* `StoreOrderDetail` carries no order-level computed fields beyond
|
|
10
|
+
* `currency_code`.
|
|
11
|
+
*/
|
|
12
|
+
type OrderItemsListProps = {
|
|
13
|
+
items: OrderDisplayItem[]
|
|
14
|
+
currencyCode: string
|
|
15
|
+
labels?: Pick<OrderLabels, "summary" | "qty">
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function OrderItemsList({
|
|
19
|
+
items,
|
|
20
|
+
currencyCode,
|
|
21
|
+
labels,
|
|
22
|
+
}: OrderItemsListProps) {
|
|
23
|
+
const l = { ...defaultOrderLabels, ...labels }
|
|
24
|
+
// Render product lines only — backend-injected fee lines (legacy COD-fee
|
|
25
|
+
// line items tagged `metadata.is_cod_fee`) are hidden here and surfaced
|
|
26
|
+
// as a dedicated row in OrderTotals instead. barter's NATIVE COD fee is
|
|
27
|
+
// never a line item (order_summaries.totals.cod_fee_total), so on pure
|
|
28
|
+
// barter data this filter is a no-op safety net.
|
|
29
|
+
const visible = (items || []).filter((i) => isProductLine(i))
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<h2 className="text-sm font-semibold text-foreground mb-1">
|
|
34
|
+
{l.summary}
|
|
35
|
+
</h2>
|
|
36
|
+
<div className="divide-y divide-border">
|
|
37
|
+
{visible
|
|
38
|
+
.sort((a, b) => ((a.createdAt ?? "") > (b.createdAt ?? "") ? -1 : 1))
|
|
39
|
+
.map((item) => (
|
|
40
|
+
<OrderItem
|
|
41
|
+
key={item.id}
|
|
42
|
+
item={item}
|
|
43
|
+
currencyCode={currencyCode}
|
|
44
|
+
labels={labels}
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { type OrderItemsListProps }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { CreditCard } from "lucide-react"
|
|
2
|
+
import {
|
|
3
|
+
isManual,
|
|
4
|
+
isPaypal,
|
|
5
|
+
isStripeLike,
|
|
6
|
+
paymentInfoMap,
|
|
7
|
+
} from "../lib/payment-constants"
|
|
8
|
+
import { defaultOrderLabels, type OrderLabels } from "./labels"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Payment-method card. Ported from
|
|
12
|
+
* `@1click/ui/src/order/order-payment-card.tsx` (v2.3.1); data seam:
|
|
13
|
+
* Medusa's `order.payment_collections[0].payments[0]` embed does not exist
|
|
14
|
+
* on barter's `StoreOrderDetail` (payment internals never cross the store
|
|
15
|
+
* surface) — the provider id arrives via props instead: from
|
|
16
|
+
* `prepareCheckout().payment_provider`, the decorated cart's
|
|
17
|
+
* `payment_collection` sessions, or the app's checkout state.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a human-readable, locale-correct title for a payment provider id.
|
|
22
|
+
* Resolution order (verbatim from the source):
|
|
23
|
+
*
|
|
24
|
+
* 1. Bucketed lookup against `labels.paymentMethodTitles` (the locale
|
|
25
|
+
* pack the storefront passes in). This is the canonical localization
|
|
26
|
+
* surface — Bulgarian stores get "Наложен платеж" here, English
|
|
27
|
+
* stores get "Cash on delivery", etc.
|
|
28
|
+
* 2. Hardcoded `paymentInfoMap` fallback for known provider ids (English
|
|
29
|
+
* defaults). Keeps the library safe for storefronts that haven't
|
|
30
|
+
* adopted the labels pack yet.
|
|
31
|
+
* 3. The raw provider id as a last resort, so the card never renders
|
|
32
|
+
* empty if a brand-new provider lands.
|
|
33
|
+
*
|
|
34
|
+
* The bucket is determined by the same `isStripeLike` / `isManual` /
|
|
35
|
+
* `isPaypal` predicates used everywhere else in the library (translated to
|
|
36
|
+
* barter ids in `lib/payment-constants`), so a single change to those
|
|
37
|
+
* propagates here automatically.
|
|
38
|
+
*/
|
|
39
|
+
export function resolvePaymentTitle(
|
|
40
|
+
providerId: string | undefined,
|
|
41
|
+
paymentMethodTitles: OrderLabels["paymentMethodTitles"]
|
|
42
|
+
): string {
|
|
43
|
+
if (!providerId) return ""
|
|
44
|
+
if (isStripeLike(providerId)) return paymentMethodTitles.card
|
|
45
|
+
// COD before manual: pp_cod is its own bucket (payments-registry card) —
|
|
46
|
+
// an offline manual payment must never render as "Cash on delivery".
|
|
47
|
+
if (providerId === "pp_cod") return paymentMethodTitles.cod
|
|
48
|
+
if (isManual(providerId)) return paymentMethodTitles.manual
|
|
49
|
+
if (isPaypal(providerId)) return paymentMethodTitles.paypal
|
|
50
|
+
return paymentInfoMap[providerId]?.title ?? providerId
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type OrderPaymentCardProps = {
|
|
54
|
+
/** Payment provider id (e.g. `pp_stripe`, `pp_system_default`). */
|
|
55
|
+
providerId?: string | null
|
|
56
|
+
/** Last 4 card digits when known (Stripe checkout state). */
|
|
57
|
+
cardLast4?: string | null
|
|
58
|
+
labels?: Pick<OrderLabels, "paymentMethod" | "paymentMethodTitles">
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function OrderPaymentCard({
|
|
62
|
+
providerId,
|
|
63
|
+
cardLast4,
|
|
64
|
+
labels,
|
|
65
|
+
}: OrderPaymentCardProps) {
|
|
66
|
+
const l = { ...defaultOrderLabels, ...labels }
|
|
67
|
+
|
|
68
|
+
if (!providerId) return null
|
|
69
|
+
|
|
70
|
+
const title = resolvePaymentTitle(providerId, l.paymentMethodTitles)
|
|
71
|
+
const icon = paymentInfoMap[providerId]?.icon ?? null
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="bg-card rounded-xl p-5 shadow-sm">
|
|
75
|
+
<h3 className="text-sm font-semibold text-foreground mb-3">
|
|
76
|
+
{l.paymentMethod}
|
|
77
|
+
</h3>
|
|
78
|
+
<div className="flex items-center gap-3">
|
|
79
|
+
<div className="w-9 h-9 rounded-lg bg-muted flex items-center justify-center text-muted-foreground">
|
|
80
|
+
{icon || <CreditCard className="w-[18px] h-[18px]" />}
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<p className="text-sm text-foreground">{title}</p>
|
|
84
|
+
{isStripeLike(providerId) && cardLast4 && (
|
|
85
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
86
|
+
**** **** **** {cardLast4}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { type OrderPaymentCardProps }
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { StoreOrderFulfillment } from "../api/orders"
|
|
4
|
+
import { defaultOrderLabels, type OrderLabels } from "./labels"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fulfillment progress timeline. Ported from
|
|
8
|
+
* `@1click/ui/src/order/order-timeline.tsx` (v2.3.1) — the step state
|
|
9
|
+
* machine and rendering are unchanged. Barter seam: there is no
|
|
10
|
+
* `order.fulfillment_status` column on the store surface; derive the
|
|
11
|
+
* status from the fulfillments' lifecycle timestamps with
|
|
12
|
+
* `deriveFulfillmentStatus(order.fulfillments)` and pass it in.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Derive a Medusa-style fulfillment status from barter's fulfillment
|
|
17
|
+
* lifecycle timestamps (packed_at → fulfilled, shipped_at → shipped,
|
|
18
|
+
* delivered_at → delivered; canceled fulfillments are ignored). Returns
|
|
19
|
+
* `undefined` for "not fulfilled yet" (timeline shows "Placed"). Partial
|
|
20
|
+
* variants are emitted when only SOME active fulfillments reached the
|
|
21
|
+
* stage — the timeline maps both to the same step. Unit-tested.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveFulfillmentStatus(
|
|
24
|
+
fulfillments: readonly StoreOrderFulfillment[] | null | undefined
|
|
25
|
+
): string | undefined {
|
|
26
|
+
const active = (fulfillments ?? [])
|
|
27
|
+
.map((row) => row?.fulfillment)
|
|
28
|
+
.filter((f): f is NonNullable<StoreOrderFulfillment["fulfillment"]> =>
|
|
29
|
+
Boolean(f && !f.canceled_at)
|
|
30
|
+
)
|
|
31
|
+
if (!active.length) return undefined
|
|
32
|
+
|
|
33
|
+
const all = (key: "packed_at" | "shipped_at" | "delivered_at") =>
|
|
34
|
+
active.every((f) => Boolean(f[key]))
|
|
35
|
+
const some = (key: "packed_at" | "shipped_at" | "delivered_at") =>
|
|
36
|
+
active.some((f) => Boolean(f[key]))
|
|
37
|
+
|
|
38
|
+
if (all("delivered_at")) return "delivered"
|
|
39
|
+
if (some("delivered_at")) return "partially_delivered"
|
|
40
|
+
if (all("shipped_at")) return "shipped"
|
|
41
|
+
if (some("shipped_at")) return "partially_shipped"
|
|
42
|
+
if (all("packed_at")) return "fulfilled"
|
|
43
|
+
if (some("packed_at")) return "partially_fulfilled"
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type OrderTimelineProps = {
|
|
48
|
+
fulfillmentStatus?: string
|
|
49
|
+
labels?: Pick<OrderLabels, "orderPlaced" | "processing" | "shipped" | "delivered">
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function OrderTimeline({
|
|
53
|
+
fulfillmentStatus,
|
|
54
|
+
labels,
|
|
55
|
+
}: OrderTimelineProps) {
|
|
56
|
+
const l = { ...defaultOrderLabels, ...labels }
|
|
57
|
+
|
|
58
|
+
const steps = [
|
|
59
|
+
{ label: l.orderPlaced, key: "placed" },
|
|
60
|
+
{ label: l.processing, key: "processing" },
|
|
61
|
+
{ label: l.shipped, key: "shipped" },
|
|
62
|
+
{ label: l.delivered, key: "delivered" },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const getActiveIndex = (): number => {
|
|
66
|
+
switch (fulfillmentStatus) {
|
|
67
|
+
case "delivered":
|
|
68
|
+
case "partially_delivered":
|
|
69
|
+
return 3
|
|
70
|
+
case "shipped":
|
|
71
|
+
case "partially_shipped":
|
|
72
|
+
return 2
|
|
73
|
+
case "fulfilled":
|
|
74
|
+
case "partially_fulfilled":
|
|
75
|
+
return 1
|
|
76
|
+
default:
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const activeIndex = getActiveIndex()
|
|
82
|
+
const lastStep = steps.length - 1
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="px-6 py-5 bg-card rounded-xl shadow-sm">
|
|
86
|
+
<div className="relative">
|
|
87
|
+
<div className="absolute top-[5px] left-[6px] right-[6px] h-[2px] bg-muted rounded-full" />
|
|
88
|
+
<div
|
|
89
|
+
className="absolute top-[5px] left-[6px] h-[2px] bg-success rounded-full transition-all duration-700 ease-out"
|
|
90
|
+
style={{
|
|
91
|
+
width:
|
|
92
|
+
activeIndex === 0
|
|
93
|
+
? "0"
|
|
94
|
+
: `calc((100% - 12px) * ${activeIndex / lastStep})`,
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<div className="relative flex justify-between">
|
|
99
|
+
{steps.map((step, i) => {
|
|
100
|
+
const isActive = i === activeIndex
|
|
101
|
+
const isComplete = i < activeIndex
|
|
102
|
+
const isFuture = i > activeIndex
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
key={step.key}
|
|
107
|
+
className={`flex flex-col gap-2 ${
|
|
108
|
+
i === 0
|
|
109
|
+
? "items-start"
|
|
110
|
+
: i === lastStep
|
|
111
|
+
? "items-end"
|
|
112
|
+
: "items-center"
|
|
113
|
+
}`}
|
|
114
|
+
>
|
|
115
|
+
<div className="relative w-3 h-3">
|
|
116
|
+
{isActive && (
|
|
117
|
+
<span className="absolute inset-0 rounded-full bg-success/40 animate-ping [animation-duration:2s]" />
|
|
118
|
+
)}
|
|
119
|
+
<div
|
|
120
|
+
className={`relative w-3 h-3 rounded-full z-10 ${
|
|
121
|
+
isComplete || isActive ? "bg-success" : "bg-muted"
|
|
122
|
+
}`}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
<span
|
|
126
|
+
className={`text-[11px] sm:text-xs font-medium whitespace-nowrap ${
|
|
127
|
+
isFuture ? "text-muted-foreground" : "text-success"
|
|
128
|
+
}`}
|
|
129
|
+
>
|
|
130
|
+
{step.label}
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { type OrderTimelineProps }
|