create-brainerce-store 1.78.0 → 1.80.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 +27 -2
- package/messages/en.json +12 -3
- package/messages/he.json +12 -3
- package/package.json +10 -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 +19 -2
- 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/account/profile-section.tsx +7 -1
- package/templates/nextjs/base/src/components/auth/register-form.tsx +18 -1
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -2
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +1 -1
- 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/layout/newsletter-signup.tsx +59 -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/layout/newsletter-signup.tsx +50 -8
- 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,8 +31,17 @@ 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.80.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
|
+
homepage: "https://brainerce.com",
|
|
37
|
+
repository: {
|
|
38
|
+
type: "git",
|
|
39
|
+
url: "https://github.com/brainerce/brainerce.git",
|
|
40
|
+
directory: "packages/create-brainerce-store"
|
|
41
|
+
},
|
|
42
|
+
bugs: {
|
|
43
|
+
url: "https://github.com/brainerce/brainerce/issues"
|
|
44
|
+
},
|
|
36
45
|
bin: {
|
|
37
46
|
"create-brainerce-store": "dist/index.js"
|
|
38
47
|
},
|
|
@@ -305,7 +314,23 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
305
314
|
// bare 0, so an older SDK fails type-check at scaffold time. This floor is
|
|
306
315
|
// load-bearing now; it is no longer only about provably getting the
|
|
307
316
|
// corrected README.
|
|
308
|
-
|
|
317
|
+
//
|
|
318
|
+
// 2.5.0 is the first SDK that carries the KIT fields, and this floor is
|
|
319
|
+
// load-bearing for exactly the reason the ones above are. `2.4.0` already
|
|
320
|
+
// typed `Product.type` as `'SIMPLE' | 'VARIABLE' | 'KIT'` — so
|
|
321
|
+
// `product.type === 'KIT'` compiles against it and looks fine — but it has no
|
|
322
|
+
// `kitComponents` and no `kitAvailable`. The scaffolded product card reads
|
|
323
|
+
// `product.kitAvailable` and the product page reads `product.kitComponents`
|
|
324
|
+
// off an SDK-typed `Product`, so on an older SDK a scaffolded store fails
|
|
325
|
+
// type-check at creation. Verified against the published 2.4.0 tarball: zero
|
|
326
|
+
// files mention either field.
|
|
327
|
+
//
|
|
328
|
+
// 2.7.0 is the first SDK with `marketing.getBenefit()` and
|
|
329
|
+
// `PublicNewsletterBenefitOffer`, and the scaffolded newsletter signup reads
|
|
330
|
+
// both to show the merchant's welcome offer beside the field. On an older
|
|
331
|
+
// SDK a scaffolded store fails type-check at creation, exactly like the KIT
|
|
332
|
+
// floor above — so this floor is load-bearing, not cosmetic.
|
|
333
|
+
brainerce: "^2.7.0",
|
|
309
334
|
"isomorphic-dompurify": "^3.8.0"
|
|
310
335
|
});
|
|
311
336
|
|
package/messages/en.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"birthday": "Birthday",
|
|
43
43
|
"birthdayMonth": "Month",
|
|
44
44
|
"birthdayDay": "Day",
|
|
45
|
-
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day.",
|
|
45
|
+
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day, as long as you are signed up for our emails.",
|
|
46
46
|
"birthdayIncomplete": "Choose both a month and a day, or leave both empty.",
|
|
47
47
|
"birthdayRequired": "Choose your birthday month and day to continue.",
|
|
48
48
|
"birthdayPlaceholder": "Choose your birthday",
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
"addSelectedToCart": "Add Selected to Cart",
|
|
123
123
|
"totalPrice": "Total: {price}",
|
|
124
124
|
"addingAll": "Adding...",
|
|
125
|
+
"whatsInTheBox": "What's in the box",
|
|
125
126
|
"by": "By"
|
|
126
127
|
},
|
|
127
128
|
"reviews": {
|
|
@@ -243,6 +244,7 @@
|
|
|
243
244
|
"paymentNotConfigured": "Payment Not Configured",
|
|
244
245
|
"paymentNotConfiguredDesc": "Payment has not been set up for this store yet. Please contact the store owner.",
|
|
245
246
|
"paymentError": "Payment Error",
|
|
247
|
+
"paymentUnavailable": "Payment is unavailable for this store right now. Please contact the store.",
|
|
246
248
|
"paymentRedirectBlocked": "Payment redirect blocked for security reasons. Please contact support.",
|
|
247
249
|
"sandboxTitle": "Sandbox Mode",
|
|
248
250
|
"sandboxDescription": "This is a test order. No real payment will be processed.",
|
|
@@ -433,7 +435,7 @@
|
|
|
433
435
|
"orderPrefix": "Order",
|
|
434
436
|
"productFallback": "Product",
|
|
435
437
|
"statusDraft": "Draft",
|
|
436
|
-
"statusPending": "
|
|
438
|
+
"statusPending": "Awaiting processing",
|
|
437
439
|
"statusProcessing": "Processing",
|
|
438
440
|
"statusOnHold": "On hold",
|
|
439
441
|
"statusPaid": "Paid",
|
|
@@ -515,6 +517,8 @@
|
|
|
515
517
|
"loading": "Loading your order...",
|
|
516
518
|
"errorTitle": "Order not found",
|
|
517
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.",
|
|
518
522
|
"missingCheckoutInfo": "Missing order information.",
|
|
519
523
|
"returnHome": "Return Home"
|
|
520
524
|
},
|
|
@@ -565,7 +569,9 @@
|
|
|
565
569
|
"submitting": "Sending...",
|
|
566
570
|
"checkEmailTitle": "Almost there — check your email",
|
|
567
571
|
"checkEmailBody": "We've sent you a link to confirm. Look in your spam folder too; you won't get another copy for 24 hours.",
|
|
568
|
-
"
|
|
572
|
+
"offerPercent": "{value}% off your first order",
|
|
573
|
+
"offerAmount": "{value} off your first order",
|
|
574
|
+
"offerByEmail": "Your personal code is in that email too. It is one per address.",
|
|
569
575
|
"genericError": "Something went wrong. Please try again."
|
|
570
576
|
},
|
|
571
577
|
"loyalty": {
|
|
@@ -601,6 +607,9 @@
|
|
|
601
607
|
"giftCard": {
|
|
602
608
|
"placeholder": "Gift card code",
|
|
603
609
|
"invalidCode": "That gift card code cannot be used on this order.",
|
|
610
|
+
"applyFailed": "We could not apply that gift card right now. Please try again.",
|
|
611
|
+
"locked": "Payment has already started, so this order can no longer take a gift card.",
|
|
612
|
+
"lockedHint": "A gift card is added before the payment step.",
|
|
604
613
|
"removeFailed": "We could not remove that gift card.",
|
|
605
614
|
"applied": "Gift card applied — {amount}"
|
|
606
615
|
}
|
package/messages/he.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"birthday": "יום הולדת",
|
|
43
43
|
"birthdayMonth": "חודש",
|
|
44
44
|
"birthdayDay": "יום",
|
|
45
|
-
"birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום
|
|
45
|
+
"birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום עצמו, בתנאי שאתם רשומים לדיוור שלנו.",
|
|
46
46
|
"birthdayIncomplete": "בחרו גם חודש וגם יום, או השאירו את שניהם ריקים.",
|
|
47
47
|
"birthdayRequired": "בחרו את חודש ויום ההולדת כדי להמשיך.",
|
|
48
48
|
"birthdayPlaceholder": "בחרו את יום ההולדת",
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
"addSelectedToCart": "הוסף הנבחרים לעגלה",
|
|
123
123
|
"totalPrice": "סה\"כ: {price}",
|
|
124
124
|
"addingAll": "מוסיף...",
|
|
125
|
+
"whatsInTheBox": "מה יש בקופסה",
|
|
125
126
|
"by": "מאת"
|
|
126
127
|
},
|
|
127
128
|
"reviews": {
|
|
@@ -243,6 +244,7 @@
|
|
|
243
244
|
"paymentNotConfigured": "תשלום לא מוגדר",
|
|
244
245
|
"paymentNotConfiguredDesc": "התשלום עדיין לא הוגדר לחנות זו. אנא פנו לבעל החנות.",
|
|
245
246
|
"paymentError": "שגיאת תשלום",
|
|
247
|
+
"paymentUnavailable": "לא ניתן לשלם בחנות זו כרגע. ניתן לפנות לחנות.",
|
|
246
248
|
"paymentRedirectBlocked": "הפניית התשלום נחסמה מטעמי אבטחה. אנא פנו לתמיכה.",
|
|
247
249
|
"sandboxTitle": "מצב בדיקה",
|
|
248
250
|
"sandboxDescription": "זוהי הזמנת בדיקה. לא יבוצע תשלום אמיתי.",
|
|
@@ -433,7 +435,7 @@
|
|
|
433
435
|
"orderPrefix": "הזמנה",
|
|
434
436
|
"productFallback": "מוצר",
|
|
435
437
|
"statusDraft": "טיוטה",
|
|
436
|
-
"statusPending": "ממתין",
|
|
438
|
+
"statusPending": "ממתין לטיפול",
|
|
437
439
|
"statusProcessing": "בטיפול",
|
|
438
440
|
"statusOnHold": "מושהה",
|
|
439
441
|
"statusPaid": "שולם",
|
|
@@ -515,6 +517,8 @@
|
|
|
515
517
|
"loading": "טוען את ההזמנה שלך...",
|
|
516
518
|
"errorTitle": "ההזמנה לא נמצאה",
|
|
517
519
|
"notFound": "לא הצלחנו למצוא הזמנה שתואמת לקישור הזה.",
|
|
520
|
+
"unavailableTitle": "לא הצלחנו לטעון את ההזמנה",
|
|
521
|
+
"unavailableBody": "משהו השתבש אצלנו. ההזמנה שלכם שמורה. נסו שוב בעוד רגע.",
|
|
518
522
|
"missingCheckoutInfo": "חסר מידע הזמנה.",
|
|
519
523
|
"returnHome": "חזרה לדף הבית"
|
|
520
524
|
},
|
|
@@ -565,7 +569,9 @@
|
|
|
565
569
|
"submitting": "שולח...",
|
|
566
570
|
"checkEmailTitle": "כמעט סיימנו — בדקו את המייל",
|
|
567
571
|
"checkEmailBody": "שלחנו לכם קישור לאישור. בדקו גם בתיקיית הספאם; עותק נוסף לא יישלח ב-24 השעות הקרובות.",
|
|
568
|
-
"
|
|
572
|
+
"offerPercent": "{value}% הנחה על ההזמנה הראשונה",
|
|
573
|
+
"offerAmount": "{value} הנחה על ההזמנה הראשונה",
|
|
574
|
+
"offerByEmail": "הקוד האישי שלכם נמצא באותו מייל. הטבה אחת לכל כתובת.",
|
|
569
575
|
"genericError": "משהו השתבש. נסו שוב."
|
|
570
576
|
},
|
|
571
577
|
"loyalty": {
|
|
@@ -601,6 +607,9 @@
|
|
|
601
607
|
"giftCard": {
|
|
602
608
|
"placeholder": "קוד כרטיס מתנה",
|
|
603
609
|
"invalidCode": "לא ניתן להשתמש בקוד הזה בהזמנה הזאת.",
|
|
610
|
+
"applyFailed": "לא הצלחנו לממש את כרטיס המתנה כרגע. נסו שוב.",
|
|
611
|
+
"locked": "התשלום כבר התחיל, אז אי אפשר להוסיף כרטיס מתנה להזמנה הזאת.",
|
|
612
|
+
"lockedHint": "כרטיס מתנה מוסיפים לפני שלב התשלום.",
|
|
604
613
|
"removeFailed": "לא הצלחנו להסיר את כרטיס המתנה.",
|
|
605
614
|
"applied": "כרטיס מתנה — {amount}"
|
|
606
615
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-brainerce-store",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.80.0",
|
|
4
4
|
"description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
5
|
+
"homepage": "https://brainerce.com",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/brainerce/brainerce.git",
|
|
9
|
+
"directory": "packages/create-brainerce-store"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/brainerce/brainerce/issues"
|
|
13
|
+
},
|
|
5
14
|
"bin": {
|
|
6
15
|
"create-brainerce-store": "dist/index.js"
|
|
7
16
|
},
|
|
@@ -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
|
}
|
|
@@ -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="/"
|