create-brainerce-store 1.77.0 → 1.79.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/dist/index.js +12 -2
- package/messages/en.json +8 -2
- package/messages/he.json +8 -2
- package/package.json +1 -1
- package/templates/nextjs/base/.eslintrc.json +2 -0
- package/templates/nextjs/base/AGENTS.md.ejs +23 -5
- package/templates/nextjs/base/CLAUDE.md.ejs +23 -5
- package/templates/nextjs/base/src/app/checkout/page.tsx +43 -31
- package/templates/nextjs/base/src/app/order-status/page.tsx +18 -3
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +220 -214
- package/templates/nextjs/base/src/components/account/order-history.tsx +11 -4
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -2
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +343 -328
- package/templates/nextjs/base/src/core/lib/kit.ts +88 -0
- package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +87 -12
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +205 -197
- package/templates/nextjs/base/src/ui/product/product-card.tsx +230 -221
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +524 -493
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +117 -108
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +23 -3
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +210 -202
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +251 -242
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +540 -509
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +110 -101
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +22 -2
- package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +41 -11
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +182 -174
- package/templates/nextjs/ui-canvas/product/product-card.tsx +174 -165
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +31 -0
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +114 -105
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +22 -2
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.79.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -305,7 +305,17 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
305
305
|
// bare 0, so an older SDK fails type-check at scaffold time. This floor is
|
|
306
306
|
// load-bearing now; it is no longer only about provably getting the
|
|
307
307
|
// corrected README.
|
|
308
|
-
|
|
308
|
+
//
|
|
309
|
+
// 2.5.0 is the first SDK that carries the KIT fields, and this floor is
|
|
310
|
+
// load-bearing for exactly the reason the ones above are. `2.4.0` already
|
|
311
|
+
// typed `Product.type` as `'SIMPLE' | 'VARIABLE' | 'KIT'` — so
|
|
312
|
+
// `product.type === 'KIT'` compiles against it and looks fine — but it has no
|
|
313
|
+
// `kitComponents` and no `kitAvailable`. The scaffolded product card reads
|
|
314
|
+
// `product.kitAvailable` and the product page reads `product.kitComponents`
|
|
315
|
+
// off an SDK-typed `Product`, so on an older SDK a scaffolded store fails
|
|
316
|
+
// type-check at creation. Verified against the published 2.4.0 tarball: zero
|
|
317
|
+
// files mention either field.
|
|
318
|
+
brainerce: "^2.5.0",
|
|
309
319
|
"isomorphic-dompurify": "^3.8.0"
|
|
310
320
|
});
|
|
311
321
|
|
package/messages/en.json
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"no": "No",
|
|
12
12
|
"total": "Total",
|
|
13
13
|
"subtotal": "Subtotal",
|
|
14
|
-
"subtotalExclTax": "Subtotal (excl. tax)",
|
|
15
14
|
"discount": "Discount",
|
|
16
15
|
"generalDiscount": "General Discount",
|
|
17
16
|
"couponDiscount": "Coupon",
|
|
@@ -123,6 +122,7 @@
|
|
|
123
122
|
"addSelectedToCart": "Add Selected to Cart",
|
|
124
123
|
"totalPrice": "Total: {price}",
|
|
125
124
|
"addingAll": "Adding...",
|
|
125
|
+
"whatsInTheBox": "What's in the box",
|
|
126
126
|
"by": "By"
|
|
127
127
|
},
|
|
128
128
|
"reviews": {
|
|
@@ -244,6 +244,7 @@
|
|
|
244
244
|
"paymentNotConfigured": "Payment Not Configured",
|
|
245
245
|
"paymentNotConfiguredDesc": "Payment has not been set up for this store yet. Please contact the store owner.",
|
|
246
246
|
"paymentError": "Payment Error",
|
|
247
|
+
"paymentUnavailable": "Payment is unavailable for this store right now. Please contact the store.",
|
|
247
248
|
"paymentRedirectBlocked": "Payment redirect blocked for security reasons. Please contact support.",
|
|
248
249
|
"sandboxTitle": "Sandbox Mode",
|
|
249
250
|
"sandboxDescription": "This is a test order. No real payment will be processed.",
|
|
@@ -434,7 +435,7 @@
|
|
|
434
435
|
"orderPrefix": "Order",
|
|
435
436
|
"productFallback": "Product",
|
|
436
437
|
"statusDraft": "Draft",
|
|
437
|
-
"statusPending": "
|
|
438
|
+
"statusPending": "Awaiting processing",
|
|
438
439
|
"statusProcessing": "Processing",
|
|
439
440
|
"statusOnHold": "On hold",
|
|
440
441
|
"statusPaid": "Paid",
|
|
@@ -516,6 +517,8 @@
|
|
|
516
517
|
"loading": "Loading your order...",
|
|
517
518
|
"errorTitle": "Order not found",
|
|
518
519
|
"notFound": "We couldn't find an order matching this link.",
|
|
520
|
+
"unavailableTitle": "We could not load your order",
|
|
521
|
+
"unavailableBody": "Something went wrong on our side. Your order is safe. Please try again in a moment.",
|
|
519
522
|
"missingCheckoutInfo": "Missing order information.",
|
|
520
523
|
"returnHome": "Return Home"
|
|
521
524
|
},
|
|
@@ -602,6 +605,9 @@
|
|
|
602
605
|
"giftCard": {
|
|
603
606
|
"placeholder": "Gift card code",
|
|
604
607
|
"invalidCode": "That gift card code cannot be used on this order.",
|
|
608
|
+
"applyFailed": "We could not apply that gift card right now. Please try again.",
|
|
609
|
+
"locked": "Payment has already started, so this order can no longer take a gift card.",
|
|
610
|
+
"lockedHint": "A gift card is added before the payment step.",
|
|
605
611
|
"removeFailed": "We could not remove that gift card.",
|
|
606
612
|
"applied": "Gift card applied — {amount}"
|
|
607
613
|
}
|
package/messages/he.json
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"no": "לא",
|
|
12
12
|
"total": "סה\"כ",
|
|
13
13
|
"subtotal": "סיכום ביניים",
|
|
14
|
-
"subtotalExclTax": "סיכום ביניים (לפני מע\"מ)",
|
|
15
14
|
"discount": "הנחה",
|
|
16
15
|
"generalDiscount": "הנחה כללית",
|
|
17
16
|
"couponDiscount": "קופון",
|
|
@@ -123,6 +122,7 @@
|
|
|
123
122
|
"addSelectedToCart": "הוסף הנבחרים לעגלה",
|
|
124
123
|
"totalPrice": "סה\"כ: {price}",
|
|
125
124
|
"addingAll": "מוסיף...",
|
|
125
|
+
"whatsInTheBox": "מה יש בקופסה",
|
|
126
126
|
"by": "מאת"
|
|
127
127
|
},
|
|
128
128
|
"reviews": {
|
|
@@ -244,6 +244,7 @@
|
|
|
244
244
|
"paymentNotConfigured": "תשלום לא מוגדר",
|
|
245
245
|
"paymentNotConfiguredDesc": "התשלום עדיין לא הוגדר לחנות זו. אנא פנו לבעל החנות.",
|
|
246
246
|
"paymentError": "שגיאת תשלום",
|
|
247
|
+
"paymentUnavailable": "לא ניתן לשלם בחנות זו כרגע. ניתן לפנות לחנות.",
|
|
247
248
|
"paymentRedirectBlocked": "הפניית התשלום נחסמה מטעמי אבטחה. אנא פנו לתמיכה.",
|
|
248
249
|
"sandboxTitle": "מצב בדיקה",
|
|
249
250
|
"sandboxDescription": "זוהי הזמנת בדיקה. לא יבוצע תשלום אמיתי.",
|
|
@@ -434,7 +435,7 @@
|
|
|
434
435
|
"orderPrefix": "הזמנה",
|
|
435
436
|
"productFallback": "מוצר",
|
|
436
437
|
"statusDraft": "טיוטה",
|
|
437
|
-
"statusPending": "ממתין",
|
|
438
|
+
"statusPending": "ממתין לטיפול",
|
|
438
439
|
"statusProcessing": "בטיפול",
|
|
439
440
|
"statusOnHold": "מושהה",
|
|
440
441
|
"statusPaid": "שולם",
|
|
@@ -516,6 +517,8 @@
|
|
|
516
517
|
"loading": "טוען את ההזמנה שלך...",
|
|
517
518
|
"errorTitle": "ההזמנה לא נמצאה",
|
|
518
519
|
"notFound": "לא הצלחנו למצוא הזמנה שתואמת לקישור הזה.",
|
|
520
|
+
"unavailableTitle": "לא הצלחנו לטעון את ההזמנה",
|
|
521
|
+
"unavailableBody": "משהו השתבש אצלנו. ההזמנה שלכם שמורה. נסו שוב בעוד רגע.",
|
|
519
522
|
"missingCheckoutInfo": "חסר מידע הזמנה.",
|
|
520
523
|
"returnHome": "חזרה לדף הבית"
|
|
521
524
|
},
|
|
@@ -602,6 +605,9 @@
|
|
|
602
605
|
"giftCard": {
|
|
603
606
|
"placeholder": "קוד כרטיס מתנה",
|
|
604
607
|
"invalidCode": "לא ניתן להשתמש בקוד הזה בהזמנה הזאת.",
|
|
608
|
+
"applyFailed": "לא הצלחנו לממש את כרטיס המתנה כרגע. נסו שוב.",
|
|
609
|
+
"locked": "התשלום כבר התחיל, אז אי אפשר להוסיף כרטיס מתנה להזמנה הזאת.",
|
|
610
|
+
"lockedHint": "כרטיס מתנה מוסיפים לפני שלב התשלום.",
|
|
605
611
|
"removeFailed": "לא הצלחנו להסיר את כרטיס המתנה.",
|
|
606
612
|
"applied": "כרטיס מתנה — {amount}"
|
|
607
613
|
}
|
package/package.json
CHANGED
|
@@ -91,11 +91,12 @@ and handlers, never JSX. Never hardcode catalog content.
|
|
|
91
91
|
|
|
92
92
|
### Rebuilding the look is free. Dropping a feature is not.
|
|
93
93
|
|
|
94
|
-
Some files under `src/ui/` are the ONLY place a
|
|
95
|
-
exists in this project. Delete one and the capability
|
|
96
|
-
nothing to notice it by: no type error, no console
|
|
97
|
-
either, because these components auto-hide while
|
|
98
|
-
switched off, so a deleted one and an idle one
|
|
94
|
+
Some files under `src/ui/` (and one under `src/core/`) are the ONLY place a
|
|
95
|
+
mandatory checklist entry exists in this project. Delete one and the capability
|
|
96
|
+
leaves the store with nothing to notice it by: no type error, no console
|
|
97
|
+
warning, and no visual hole either, because these components auto-hide while
|
|
98
|
+
the merchant has the feature switched off, so a deleted one and an idle one
|
|
99
|
+
look identical on the page.
|
|
99
100
|
These are the usual casualties of a redesign, because no art-direction brief
|
|
100
101
|
asks for them by name:
|
|
101
102
|
|
|
@@ -103,6 +104,8 @@ asks for them by name:
|
|
|
103
104
|
product/ back-in-stock-form · customization-fields · modifier-group-selector
|
|
104
105
|
review-form · reviews-section · frequently-bought-together
|
|
105
106
|
discount-badge · stock-badge
|
|
107
|
+
product-client-section → the KIT "what's in the box" block
|
|
108
|
+
core/ lib/kit.ts → the KIT stock resolver every badge and button reads
|
|
106
109
|
cart/ reservation-countdown · coupon-input · cart-upgrade-banner
|
|
107
110
|
cart-bundle-offer · tax-estimate-line
|
|
108
111
|
home/ discount-banner-strip
|
|
@@ -115,6 +118,21 @@ split them in half: all fine. What has to survive a rebuild is the SDK call
|
|
|
115
118
|
each one makes and the states it handles (loading, empty, failed, and the
|
|
116
119
|
merchant-has-it-off state that renders nothing).
|
|
117
120
|
|
|
121
|
+
⛔ **The two KIT entries are the ones a redesign silently breaks.** A `KIT` is
|
|
122
|
+
one product assembled from other catalog products, and three things have to
|
|
123
|
+
survive whatever you do to the markup. (1) It is added to the cart as ONE line
|
|
124
|
+
using the kit's own `productId`, with no `variantId` and no modifier
|
|
125
|
+
selections; never loop `product.kitComponents` into `addToCart`, which charges
|
|
126
|
+
the shopper twice and reserves the stock twice. (2) A kit has NO `inventory`
|
|
127
|
+
object, so every badge and buy button must go through `resolveStockInfo` /
|
|
128
|
+
`canPurchaseProduct` from `@/core/lib/kit`; reading `product.inventory`
|
|
129
|
+
directly gives a kit a red "out of stock" badge beside an ENABLED buy button,
|
|
130
|
+
or worse, renders a sold-out kit as buyable. (3) `kitComponents` is display
|
|
131
|
+
only and arrives on the by-slug read alone, never on list responses. And do not
|
|
132
|
+
assume a kit price is fixed: `kitPricingMode` may be `SUM` or
|
|
133
|
+
`SUM_MINUS_PERCENT`, in which case the price is recomputed from the components
|
|
134
|
+
on every read, so never cache one.
|
|
135
|
+
|
|
118
136
|
The list is short on purpose and it is NOT the specification. It names the
|
|
119
137
|
files people lose, not every mandatory entry. `get-required-features` is the
|
|
120
138
|
specification, and step 1 of "Verify before declaring done" is what actually
|
|
@@ -93,11 +93,12 @@ return state and handlers, never JSX. Never hardcode catalog content.
|
|
|
93
93
|
|
|
94
94
|
### Rebuilding the look is free. Dropping a feature is not.
|
|
95
95
|
|
|
96
|
-
Some files under `src/ui/` are the ONLY place a
|
|
97
|
-
exists in this project. Delete one and the capability
|
|
98
|
-
nothing to notice it by: no type error, no console
|
|
99
|
-
either, because these components auto-hide while
|
|
100
|
-
switched off, so a deleted one and an idle one
|
|
96
|
+
Some files under `src/ui/` (and one under `src/core/`) are the ONLY place a
|
|
97
|
+
mandatory checklist entry exists in this project. Delete one and the capability
|
|
98
|
+
leaves the store with nothing to notice it by: no type error, no console
|
|
99
|
+
warning, and no visual hole either, because these components auto-hide while
|
|
100
|
+
the merchant has the feature switched off, so a deleted one and an idle one
|
|
101
|
+
look identical on the page.
|
|
101
102
|
These are the usual casualties of a redesign, because no art-direction brief
|
|
102
103
|
asks for them by name:
|
|
103
104
|
|
|
@@ -105,6 +106,8 @@ asks for them by name:
|
|
|
105
106
|
product/ back-in-stock-form · customization-fields · modifier-group-selector
|
|
106
107
|
review-form · reviews-section · frequently-bought-together
|
|
107
108
|
discount-badge · stock-badge
|
|
109
|
+
product-client-section → the KIT "what's in the box" block
|
|
110
|
+
core/ lib/kit.ts → the KIT stock resolver every badge and button reads
|
|
108
111
|
cart/ reservation-countdown · coupon-input · cart-upgrade-banner
|
|
109
112
|
cart-bundle-offer · tax-estimate-line
|
|
110
113
|
home/ discount-banner-strip
|
|
@@ -117,6 +120,21 @@ split them in half: all fine. What has to survive a rebuild is the SDK call
|
|
|
117
120
|
each one makes and the states it handles (loading, empty, failed, and the
|
|
118
121
|
merchant-has-it-off state that renders nothing).
|
|
119
122
|
|
|
123
|
+
⛔ **The two KIT entries are the ones a redesign silently breaks.** A `KIT` is
|
|
124
|
+
one product assembled from other catalog products, and three things have to
|
|
125
|
+
survive whatever you do to the markup. (1) It is added to the cart as ONE line
|
|
126
|
+
using the kit's own `productId`, with no `variantId` and no modifier
|
|
127
|
+
selections; never loop `product.kitComponents` into `addToCart`, which charges
|
|
128
|
+
the shopper twice and reserves the stock twice. (2) A kit has NO `inventory`
|
|
129
|
+
object, so every badge and buy button must go through `resolveStockInfo` /
|
|
130
|
+
`canPurchaseProduct` from `@/core/lib/kit`; reading `product.inventory`
|
|
131
|
+
directly gives a kit a red "out of stock" badge beside an ENABLED buy button,
|
|
132
|
+
or worse, renders a sold-out kit as buyable. (3) `kitComponents` is display
|
|
133
|
+
only and arrives on the by-slug read alone, never on list responses. And do not
|
|
134
|
+
assume a kit price is fixed: `kitPricingMode` may be `SUM` or
|
|
135
|
+
`SUM_MINUS_PERCENT`, in which case the price is recomputed from the components
|
|
136
|
+
on every read, so never cache one.
|
|
137
|
+
|
|
120
138
|
The list is short on purpose and it is NOT the specification. It names the
|
|
121
139
|
files people lose, not every mandatory entry. `get-required-features` is the
|
|
122
140
|
specification, and step 1 of "Verify before declaring done" is what actually
|
|
@@ -132,8 +132,18 @@ function CheckoutContent() {
|
|
|
132
132
|
try {
|
|
133
133
|
setPlacing(true);
|
|
134
134
|
setPlaceError(null);
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
await getClient().completeCheckout(checkout.id);
|
|
136
|
+
// `checkout_id`, NOT the order id. The confirmation page reads
|
|
137
|
+
// `checkout_id` and every other caller (payment-step, all three of its
|
|
138
|
+
// redirect paths) sends that. Sending the order id instead meant the page
|
|
139
|
+
// found no parameter it recognised, made zero requests, and rendered its
|
|
140
|
+
// "missing payment information" state over an order that had just been
|
|
141
|
+
// placed and paid for — with the cart still full.
|
|
142
|
+
//
|
|
143
|
+
// The order id `completeCheckout` returns is deliberately not used: the
|
|
144
|
+
// page resolves the order from the checkout, which is also what makes a
|
|
145
|
+
// reload of the confirmation URL work.
|
|
146
|
+
router.push(`/order-confirmation?checkout_id=${encodeURIComponent(checkout.id)}`);
|
|
137
147
|
} catch (err) {
|
|
138
148
|
// Said out loud. A silent failure here leaves someone staring at a button
|
|
139
149
|
// that did nothing, holding a card they believe they have already spent.
|
|
@@ -1056,6 +1066,13 @@ function CheckoutContent() {
|
|
|
1056
1066
|
className="mt-3"
|
|
1057
1067
|
checkoutId={checkout.id}
|
|
1058
1068
|
tenders={checkout.tenders ?? []}
|
|
1069
|
+
// Mirrors the server's own guard. Once an intent exists
|
|
1070
|
+
// the checkout takes no more tender changes, so the field
|
|
1071
|
+
// must stop offering them.
|
|
1072
|
+
locked={
|
|
1073
|
+
checkout.status === 'PAYMENT_PENDING' ||
|
|
1074
|
+
checkout.status === 'PAYMENT_PROCESSING'
|
|
1075
|
+
}
|
|
1059
1076
|
formatAmount={(value) =>
|
|
1060
1077
|
formatPrice(parseFloat(value), { currency }) as string
|
|
1061
1078
|
}
|
|
@@ -1068,36 +1085,31 @@ function CheckoutContent() {
|
|
|
1068
1085
|
{/* Totals */}
|
|
1069
1086
|
{checkout &&
|
|
1070
1087
|
(() => {
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1088
|
+
// Every row in this column is GROSS, and the tax row below is
|
|
1089
|
+
// informational. That is how a VAT receipt reads, it is what
|
|
1090
|
+
// `TaxDisplay` already says on its own line ("VAT 18% included"),
|
|
1091
|
+
// and it is exactly what the order-history summary renders — one
|
|
1092
|
+
// convention across all three, so a shopper who compares the
|
|
1093
|
+
// checkout against the order they later open sees the same
|
|
1094
|
+
// arithmetic twice.
|
|
1095
|
+
//
|
|
1096
|
+
// This deliberately drops an earlier attempt to show a NET
|
|
1097
|
+
// subtotal for inclusive stores. Two things were wrong with it.
|
|
1098
|
+
// It read `taxBreakdown.subtotal`, which is the net of every
|
|
1099
|
+
// TAXED LINE — shipping included — and then added
|
|
1100
|
+
// `shippingAmount` below, counting shipping twice: a real
|
|
1101
|
+
// checkout printed 54.06 + 9.99 + 7.93 beside a 61.99 total.
|
|
1102
|
+
// And a net subtotal cannot coexist with a tax row labelled
|
|
1103
|
+
// "included": either the shopper adds the tax, contradicting the
|
|
1104
|
+
// label, or they do not, and the column falls short of the total
|
|
1105
|
+
// by the whole VAT. Subtracting the shipping net back out fixes
|
|
1106
|
+
// the arithmetic and leaves the contradiction, so the row that
|
|
1107
|
+
// has to go is the special case itself.
|
|
1084
1108
|
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
// `shippingAmount` minus a guess: subtracting the GROSS is only
|
|
1090
|
-
// correct when shipping is untaxed, and a store that taxes
|
|
1091
|
-
// shipping would silently show a short subtotal.
|
|
1092
|
-
const shippingNet =
|
|
1093
|
-
typeof checkout.taxBreakdown?.shippingNet === 'number'
|
|
1094
|
-
? checkout.taxBreakdown.shippingNet
|
|
1095
|
-
: parseFloat(checkout.shippingAmount) || 0;
|
|
1096
|
-
const displayedSubtotal =
|
|
1097
|
-
isInclusive && typeof checkout.taxBreakdown?.subtotal === 'number'
|
|
1098
|
-
? checkout.taxBreakdown.subtotal - shippingNet
|
|
1099
|
-
: parseFloat(checkout.subtotal);
|
|
1100
|
-
const subtotalLabel = isInclusive ? tc('subtotalExclTax') : tc('subtotal');
|
|
1109
|
+
// Gross + gross = total, with the VAT stated beneath it:
|
|
1110
|
+
// 52.00 + 9.99 = 61.99, "VAT 18% included 7.93".
|
|
1111
|
+
const displayedSubtotal = parseFloat(checkout.subtotal);
|
|
1112
|
+
const subtotalLabel = tc('subtotal');
|
|
1101
1113
|
return (
|
|
1102
1114
|
<div className="border-border space-y-2 border-t pt-4 text-sm">
|
|
1103
1115
|
<div className="flex items-center justify-between">
|
|
@@ -4,6 +4,7 @@ import { Suspense, useEffect, useState } from 'react';
|
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import { Link } from '@/core/lib/navigation';
|
|
6
6
|
import type { Order } from 'brainerce';
|
|
7
|
+
import { BrainerceError } from 'brainerce';
|
|
7
8
|
import { getClient } from '@/core/lib/brainerce';
|
|
8
9
|
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
9
10
|
import { OrderHistory } from '@/components/account/order-history';
|
|
@@ -23,6 +24,8 @@ function OrderStatusContent() {
|
|
|
23
24
|
const [order, setOrder] = useState<Order | null>(null);
|
|
24
25
|
const [loading, setLoading] = useState(true);
|
|
25
26
|
const [error, setError] = useState<string | null>(null);
|
|
27
|
+
/** Separates our own outage from a link that names no order. */
|
|
28
|
+
const [unavailable, setUnavailable] = useState(false);
|
|
26
29
|
|
|
27
30
|
useEffect(() => {
|
|
28
31
|
if (!checkoutId) {
|
|
@@ -39,8 +42,18 @@ function OrderStatusContent() {
|
|
|
39
42
|
if (!cancelled) setOrder(result);
|
|
40
43
|
} catch (err) {
|
|
41
44
|
if (!cancelled) {
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// "We could not find your order" and "we could not reach the server"
|
|
46
|
+
// are different facts, and only one of them is about the customer.
|
|
47
|
+
// A backend outage rendered as ORDER NOT FOUND on the page people
|
|
48
|
+
// open from their receipt email -- seen for real during a redeploy.
|
|
49
|
+
// Telling someone the order they just paid for does not exist is
|
|
50
|
+
// worse than telling them nothing.
|
|
51
|
+
//
|
|
52
|
+
// A 4xx really is "no order for this link". Anything else is us.
|
|
53
|
+
const missing = err instanceof BrainerceError && err.statusCode < 500;
|
|
54
|
+
if (!missing) console.error('[order-status] could not load order', err);
|
|
55
|
+
setUnavailable(!missing);
|
|
56
|
+
setError(missing ? t('notFound') : t('unavailableBody'));
|
|
44
57
|
}
|
|
45
58
|
} finally {
|
|
46
59
|
if (!cancelled) setLoading(false);
|
|
@@ -78,7 +91,9 @@ function OrderStatusContent() {
|
|
|
78
91
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
79
92
|
/>
|
|
80
93
|
</svg>
|
|
81
|
-
<h1 className="text-foreground text-2xl font-bold">
|
|
94
|
+
<h1 className="text-foreground text-2xl font-bold">
|
|
95
|
+
{unavailable ? t('unavailableTitle') : t('errorTitle')}
|
|
96
|
+
</h1>
|
|
82
97
|
<p className="text-muted-foreground mt-2">{error || t('notFound')}</p>
|
|
83
98
|
<Link
|
|
84
99
|
href="/"
|