create-cartbase 0.1.1 → 0.1.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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/template/app/docs/BUILD-A-STOREFRONT.md +1 -1
  3. package/template/app/docs/README.md +1 -1
  4. package/template/app/docs/auth.md +1 -1
  5. package/template/app/docs/carts.md +3 -3
  6. package/template/app/docs/categories.md +1 -1
  7. package/template/app/docs/checkout.md +125 -74
  8. package/template/app/docs/collections.md +1 -1
  9. package/template/app/docs/components.md +53 -55
  10. package/template/app/docs/consent.md +1 -1
  11. package/template/app/docs/content.md +1 -1
  12. package/template/app/docs/customers.md +1 -1
  13. package/template/app/docs/deploy.md +1 -1
  14. package/template/app/docs/gift-cards.md +1 -1
  15. package/template/app/docs/integrations.md +1 -1
  16. package/template/app/docs/menus.md +1 -1
  17. package/template/app/docs/metaobjects.md +1 -1
  18. package/template/app/docs/orders.md +1 -1
  19. package/template/app/docs/platform.md +1 -1
  20. package/template/app/docs/products.md +1 -1
  21. package/template/app/docs/redirects.md +1 -1
  22. package/template/app/docs/regions.md +1 -1
  23. package/template/app/docs/reviews.md +1 -1
  24. package/template/app/docs/search.md +1 -1
  25. package/template/app/docs/subscriptions.md +1 -1
  26. package/template/app/docs/variables.md +315 -0
  27. package/template/app/next-env.d.ts +6 -0
  28. package/template/app/package.json +1 -1
  29. package/template/app/smoke.mjs +1 -1
  30. package/template/app/src/app/checkout/checkout-page-client.tsx +26 -10
  31. package/template/app/src/app/checkout/mypos-demo-tab.tsx +101 -0
  32. package/template/app/src/app/checkout/page.tsx +48 -51
  33. package/template/app/src/app/order/[id]/confirmed/page.tsx +5 -4
  34. package/template/app/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,315 @@
1
+ <!-- GENERATED FILE. Do not edit by hand.
2
+ Source: src/lib/variables/catalog.ts
3
+ Regenerate: bun x tsx scripts/generate-variables-doc.ts
4
+ Guarded by: tests/docs/variables-doc.test.ts -->
5
+
6
+ # Variables
7
+
8
+ Every value a store can put into a notification, a document or a page, as a
9
+ named token. Write `{{ order.total }}` and the store fills in the amount, in
10
+ its own currency, formatted the way the rest of the platform formats money.
11
+ There are 122 of them across 15 scopes, and this page is generated
12
+ from the same catalog the renderer resolves from, so it cannot drift.
13
+
14
+ ## Syntax
15
+
16
+ Dotted paths, the shape Shopify and Klaviyo both use, so what you already
17
+ know transfers:
18
+
19
+ ```liquid
20
+ Hi {{ customer.first_name }}, order {{ order.number }} is on its way.
21
+ Total {{ order.total }} including {{ order.tax }} VAT.
22
+ Track it: {{ delivery.tracking_number }}
23
+ ```
24
+
25
+ Whitespace inside the braces is optional. A variable the store cannot answer
26
+ renders as nothing, never as the raw `{{ ... }}`: a customer must never see
27
+ template syntax.
28
+
29
+ ## Two rules worth knowing
30
+
31
+ **The platform formats, you place.** Money carries the order's currency,
32
+ dates carry the platform's date format, statuses carry the same words the
33
+ admin shows ("Unpaid", "Partially shipped"). You never format a value
34
+ yourself, and the same amount looks identical in an email, on an invoice and
35
+ in an API response.
36
+
37
+ **Product names, not column names.** The token is `{{ order.number }}`, and
38
+ it resolves to the store's own order number when the store has one. Database
39
+ column names never appear here.
40
+
41
+ ## Scopes
42
+
43
+ A scope is a group of related variables. Which ones a given notification
44
+ carries depends on what that notification is about: an order confirmation
45
+ carries the order, the customer, both addresses, the delivery and the store,
46
+ while a purchase order carries the purchase order, the supplier and the
47
+ store.
48
+
49
+ ### Order
50
+
51
+ Every customer notification carries it. Money is formatted in the order's own currency.
52
+
53
+ | Variable | What it is | Format | Also accepts |
54
+ |---|---|---|---|
55
+ | `{{ order.number }}` | The number the customer sees. The store's own number when it has one, otherwise the platform sequence. | text | `{{display_id}}`, `{{order_number}}` |
56
+ | `{{ order.id }}` | Internal identifier. Useful in links and API calls, not in customer copy. | text | |
57
+ | `{{ order.url }}` | Link to the customer's own order page. Built per store and per order: the store's custom domain when it has one, otherwise its Cartbase address. | url | `{{order_url}}` |
58
+ | `{{ order.email }}` | The address the order was placed with. | email | |
59
+ | `{{ order.date }}` | When the order was placed. | date | |
60
+ | `{{ order.status }}` | Lifecycle state: pending, completed, canceled or archived. | text | |
61
+ | `{{ order.payment_status }}` | Paid, Awaiting payment, Refunded and so on, in the same words the admin uses. | text | |
62
+ | `{{ order.fulfillment_status }}` | Unfulfilled, Shipped, Delivered and so on, in the same words the admin uses. | text | |
63
+ | `{{ order.locale }}` | Language the customer shopped in, when the storefront sends one. | text | |
64
+ | `{{ order.cancelled_at }}` | When the order was cancelled, if it was. | datetime | |
65
+ | `{{ order.item_count }}` | Total number of units in the order. | number | |
66
+ | `{{ order.currency }}` | Currency code the order was placed in. | text | |
67
+ | `{{ order.total }}` | What the customer pays, everything included. | money | `{{order_total}}`, `{{total}}` |
68
+ | `{{ order.subtotal }}` | Items only, before tax, shipping and discounts. | money | |
69
+ | `{{ order.tax }}` | Total tax on the order. | money | |
70
+ | `{{ order.shipping }}` | What delivery cost the customer. | money | |
71
+ | `{{ order.discount }}` | Total discount applied to the order. | money | |
72
+ | `{{ order.payment_method_fee }}` | The fee of the payment method the customer chose (a COD courier fee, a handling fee on a manual method). | money | |
73
+ | `{{ order.paid }}` | How much has actually been captured so far. | money | |
74
+ | `{{ order.refunded }}` | How much has been refunded. | money | |
75
+ | `{{ order.balance_due }}` | What is still outstanding on the order. | money | |
76
+
77
+ ### Customer
78
+
79
+ The email address is the customer's identity here, so every order carries a customer. Names fall back to the shipping then the billing address.
80
+
81
+ | Variable | What it is | Format | Also accepts |
82
+ |---|---|---|---|
83
+ | `{{ customer.name }}` | Full name of the customer. | text | `{{customer_name}}` |
84
+ | `{{ customer.first_name }}` | First name on its own, for a greeting. | text | |
85
+ | `{{ customer.last_name }}` | Last name on its own. | text | |
86
+ | `{{ customer.email }}` | The customer's email address, which is their identity here. | email | |
87
+ | `{{ customer.phone }}` | Phone number on the customer record. | text | |
88
+ | `{{ customer.company }}` | Company name for business customers. | text | |
89
+ | `{{ customer.vat_number }}` | VAT number for business customers, needed on invoices. | text | |
90
+ | `{{ customer.eik }}` | Bulgarian EIK for business customers. | text | |
91
+ | `{{ customer.tags }}` | Tags on the customer, comma separated. | text | |
92
+
93
+ ### Shipping address
94
+
95
+ Every field individually, plus a one-line `formatted` version.
96
+
97
+ | Variable | What it is | Format | Also accepts |
98
+ |---|---|---|---|
99
+ | `{{ shipping_address.formatted }}` | The whole address on one line, ready to drop into a sentence. | text | |
100
+ | `{{ shipping_address.name }}` | Recipient's full name. | text | |
101
+ | `{{ shipping_address.first_name }}` | Recipient's first name. | text | |
102
+ | `{{ shipping_address.last_name }}` | Recipient's last name. | text | |
103
+ | `{{ shipping_address.company }}` | Company on the address, when given. | text | |
104
+ | `{{ shipping_address.line_1 }}` | Street and number. | text | |
105
+ | `{{ shipping_address.line_2 }}` | Apartment, floor, or extra detail. | text | |
106
+ | `{{ shipping_address.city }}` | City or town. | text | |
107
+ | `{{ shipping_address.province }}` | Province, region or state. | text | |
108
+ | `{{ shipping_address.postal_code }}` | Postal code. | text | |
109
+ | `{{ shipping_address.country }}` | Two-letter country code. | text | |
110
+ | `{{ shipping_address.phone }}` | Phone number on the address. | text | |
111
+
112
+ ### Billing address
113
+
114
+ The same field set as the shipping address.
115
+
116
+ | Variable | What it is | Format | Also accepts |
117
+ |---|---|---|---|
118
+ | `{{ billing_address.formatted }}` | The whole address on one line, ready to drop into a sentence. | text | |
119
+ | `{{ billing_address.name }}` | Recipient's full name. | text | |
120
+ | `{{ billing_address.first_name }}` | Recipient's first name. | text | |
121
+ | `{{ billing_address.last_name }}` | Recipient's last name. | text | |
122
+ | `{{ billing_address.company }}` | Company on the address, when given. | text | |
123
+ | `{{ billing_address.line_1 }}` | Street and number. | text | |
124
+ | `{{ billing_address.line_2 }}` | Apartment, floor, or extra detail. | text | |
125
+ | `{{ billing_address.city }}` | City or town. | text | |
126
+ | `{{ billing_address.province }}` | Province, region or state. | text | |
127
+ | `{{ billing_address.postal_code }}` | Postal code. | text | |
128
+ | `{{ billing_address.country }}` | Two-letter country code. | text | |
129
+ | `{{ billing_address.phone }}` | Phone number on the address. | text | |
130
+
131
+ ### Delivery
132
+
133
+ Reads the order's most recent live fulfillment and its first label.
134
+
135
+ | Variable | What it is | Format | Also accepts |
136
+ |---|---|---|---|
137
+ | `{{ delivery.method }}` | The shipping option the customer chose, in the merchant's own words. | text | |
138
+ | `{{ delivery.price }}` | What the chosen delivery method cost. | money | |
139
+ | `{{ delivery.carrier }}` | The courier carrying the parcel. | text | |
140
+ | `{{ delivery.tracking_number }}` | Waybill number the customer can track. | text | `{{tracking_number}}` |
141
+ | `{{ delivery.tracking_url }}` | Direct link to the courier's tracking page for this parcel. | url | |
142
+ | `{{ delivery.pickup_location }}` | Courier office or locker the customer chose. | text | |
143
+ | `{{ delivery.shipped_at }}` | When the parcel left the store. | datetime | |
144
+ | `{{ delivery.delivered_at }}` | When the courier delivered the parcel. | datetime | |
145
+
146
+ ### Store
147
+
148
+ The store's public identity. `store.email` is the address a reply reaches.
149
+
150
+ | Variable | What it is | Format | Also accepts |
151
+ |---|---|---|---|
152
+ | `{{ store.name }}` | The store's name. | text | `{{store_name}}` |
153
+ | `{{ store.url }}` | Link to the storefront: the custom domain when there is one. | url | |
154
+ | `{{ store.email }}` | Where customers reach the store. The sender address when set, otherwise the owner's account email. | email | `{{support_email}}` |
155
+ | `{{ store.address }}` | Registered address of the business. | text | |
156
+ | `{{ store.vat_number }}` | The business VAT number. | text | |
157
+ | `{{ store.eik }}` | Bulgarian EIK of the business. | text | |
158
+ | `{{ store.iban }}` | Bank account customers pay into. | text | |
159
+
160
+ ### Brand
161
+
162
+ | Variable | What it is | Format | Also accepts |
163
+ |---|---|---|---|
164
+ | `{{ brand.logo_url }}` | The store's primary logo image URL, for <img> tags. | url | |
165
+ | `{{ brand.logo_square_url }}` | The square logo mark image URL, for compact placements. | url | |
166
+ | `{{ brand.color_primary }}` | The brand's primary color as #rrggbb, for inline styles. | text | |
167
+ | `{{ brand.color_secondary }}` | The brand's secondary color as #rrggbb, for inline styles. | text | |
168
+ | `{{ brand.slogan }}` | The store's slogan or tagline. | text | |
169
+
170
+ ### Cart
171
+
172
+ For abandoned-cart flows. Supplied by the caller, not by an order.
173
+
174
+ | Variable | What it is | Format | Also accepts |
175
+ |---|---|---|---|
176
+ | `{{ cart.id }}` | Identifier of the customer's cart. | text | |
177
+ | `{{ cart.total }}` | What is in the cart, priced. | money | |
178
+ | `{{ cart.item_count }}` | How many units are in the cart. | number | |
179
+
180
+ ### Document
181
+
182
+ Read from the document's frozen snapshot, never from the live order, because orders change and documents must not.
183
+
184
+ | Variable | What it is | Format | Also accepts |
185
+ |---|---|---|---|
186
+ | `{{ document.number }}` | Number of the invoice or other document. | text | `{{document_number}}` |
187
+ | `{{ document.type }}` | Invoice, proforma, dispatch note or handover protocol. | text | `{{document_type}}` |
188
+ | `{{ document.date }}` | Date the document was issued. | date | |
189
+ | `{{ document.url }}` | Link to download the PDF. | url | |
190
+ | `{{ document.currency }}` | Currency the document is issued in. | text | |
191
+ | `{{ document.payment_method }}` | How the document says payment is made. | text | |
192
+ | `{{ document.due_date }}` | When the invoice is due. | date | |
193
+ | `{{ document.outstanding }}` | How much of the invoice is still unpaid. | money | |
194
+ | `{{ document.paid_at }}` | When the invoice was settled. | datetime | |
195
+ | `{{ document.voided_at }}` | When the document was voided, if it was. | datetime | |
196
+ | `{{ document.issuer.name }}` | Legal name of the issuer. | text | |
197
+ | `{{ document.issuer.address }}` | Registered address of the issuer. | text | |
198
+ | `{{ document.issuer.eik }}` | Bulgarian EIK / company registration number. | text | |
199
+ | `{{ document.issuer.vat_number }}` | VAT registration number. | text | |
200
+ | `{{ document.issuer.mol }}` | Materially responsible person (Bulgarian invoicing). | text | |
201
+ | `{{ document.issuer.iban }}` | Bank account for payment. | text | |
202
+ | `{{ document.recipient.name }}` | Legal name of the recipient. | text | |
203
+ | `{{ document.recipient.address }}` | Registered address of the recipient. | text | |
204
+ | `{{ document.recipient.eik }}` | Bulgarian EIK / company registration number. | text | |
205
+ | `{{ document.recipient.vat_number }}` | VAT registration number. | text | |
206
+ | `{{ document.recipient.mol }}` | Materially responsible person (Bulgarian invoicing). | text | |
207
+ | `{{ document.recipient.iban }}` | Bank account for payment. | text | |
208
+ | `{{ document.totals.subtotal }}` | Line items on the document, before tax. | money | |
209
+ | `{{ document.totals.discount }}` | Discount on the document. | money | |
210
+ | `{{ document.totals.shipping }}` | Shipping on the document. | money | |
211
+ | `{{ document.totals.tax }}` | Tax on the document. | money | |
212
+ | `{{ document.totals.tax_rate_percent }}` | Effective VAT percentage shown on the document. | percent | |
213
+ | `{{ document.totals.total }}` | The amount the document is for. | money | |
214
+
215
+ ### Purchase order
216
+
217
+ | Variable | What it is | Format | Also accepts |
218
+ |---|---|---|---|
219
+ | `{{ purchase_order.number }}` | Number of the purchase order sent to a supplier. | text | `{{po_number}}` |
220
+ | `{{ purchase_order.status }}` | Where the purchase order stands. | text | |
221
+ | `{{ purchase_order.total }}` | Value of the purchase order. | money | |
222
+ | `{{ purchase_order.expected_at }}` | When the goods are expected. | date | |
223
+
224
+ ### Supplier
225
+
226
+ | Variable | What it is | Format | Also accepts |
227
+ |---|---|---|---|
228
+ | `{{ supplier.name }}` | Name of the supplier the purchase order goes to. | text | `{{supplier_name}}` |
229
+ | `{{ supplier.email }}` | Where the purchase order is sent. | email | |
230
+ | `{{ supplier.phone }}` | Supplier phone number. | text | |
231
+
232
+ ### Review
233
+
234
+ | Variable | What it is | Format | Also accepts |
235
+ |---|---|---|---|
236
+ | `{{ review.url }}` | Link to the review form for this order. | url | |
237
+ | `{{ review.product_title }}` | The product being reviewed. | text | `{{product_title}}` |
238
+ | `{{ review.reward_percentage }}` | Discount percentage earned for leaving a review. | number | `{{reward_percentage}}` |
239
+ | `{{ review.reward_code }}` | The single-use discount code earned for a review. | text | `{{reward_code}}` |
240
+
241
+ ### Gift card
242
+
243
+ The amount only. The card CODE is never a variable: it renders as a fixed block so an edited template cannot drop it or leak it.
244
+
245
+ | Variable | What it is | Format | Also accepts |
246
+ |---|---|---|---|
247
+ | `{{ gift_card.amount }}` | Value loaded on the gift card. | money | `{{gift_card_amount}}` |
248
+ | `{{ gift_card.expires_at }}` | When the gift card stops working, if it expires. | date | |
249
+
250
+ ### Subscription
251
+
252
+ | Variable | What it is | Format | Also accepts |
253
+ |---|---|---|---|
254
+ | `{{ subscription.plan_name }}` | The subscription plan the customer is on. | text | `{{plan_name}}` |
255
+ | `{{ subscription.next_charge_date }}` | When the subscription renews next. | text | `{{next_charge_date}}` |
256
+
257
+ ### Alert
258
+
259
+ Staff operational alerts only.
260
+
261
+ | Variable | What it is | Format | Also accepts |
262
+ |---|---|---|---|
263
+ | `{{ alert.title }}` | Headline of the operational alert. | text | `{{alert_title}}` |
264
+ | `{{ alert.detail }}` | What the system check found. | text | |
265
+
266
+ ## What each notification carries
267
+
268
+ | Notification | Scopes |
269
+ |---|---|
270
+ | Order Confirmation | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
271
+ | Order Shipped | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
272
+ | Order Delivered | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
273
+ | Order Refunded | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
274
+ | Order Cancelled | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
275
+ | Admin: New Order | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
276
+ | Welcome Email | Customer, Store, Brand |
277
+ | Password Reset | Customer, Store, Brand |
278
+ | Document Issued | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Document |
279
+ | Sign-in Code | Customer, Store, Brand |
280
+ | B2B Account Approved | Customer, Store, Brand |
281
+ | Review Request | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Review |
282
+ | Review Reward Code | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Review |
283
+ | Out for Delivery | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
284
+ | Ready for Pickup | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
285
+ | Picked Up | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
286
+ | Econt Office Reminder (Day 3) | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
287
+ | Econt Office Reminder (Day 5) | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
288
+ | Admin: System Alert | Alert, Store, Brand |
289
+ | Gift Card Delivery | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Gift card |
290
+ | Purchase Order | Purchase order, Supplier, Store, Brand |
291
+ | Subscription: Upcoming Renewal | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Subscription |
292
+ | Subscription: Payment Failed | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Subscription |
293
+ | Subscription: Paused | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Subscription |
294
+ | Subscription: Canceled | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand, Subscription |
295
+
296
+ Writing a variable from a scope the notification does not carry is refused
297
+ when you save it, with the reason, rather than rendering as a blank in a real
298
+ customer's inbox.
299
+
300
+ ## Not available yet
301
+
302
+ These are real product gaps, listed so they are visible rather than promised.
303
+ They are not offered in the editor and cannot be saved into a template.
304
+
305
+ | Variable | What it would be | What is missing |
306
+ |---|---|---|
307
+ | `{{ order.note }}` | The note the customer left with their order. | not collected yet |
308
+ | `{{ store.phone }}` | The number customers can call. | no column yet |
309
+ | `{{ cart.url }}` | Link that puts the customer back in their abandoned cart. | storefront cart recovery route plus the cart id |
310
+
311
+ ## Never variables
312
+
313
+ The **gift card code** and the **sign-in code** render as fixed blocks and
314
+ are deliberately not tokens. An edited template must not be able to drop them
315
+ (the customer loses their code) or move them somewhere they leak.
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/dev/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "@cartbase/storefront": "^0.2.0",
11
+ "@cartbase/storefront": "^0.3.0",
12
12
  "next": "16.2.4",
13
13
  "react": "19.2.4",
14
14
  "react-dom": "19.2.4"
@@ -106,7 +106,7 @@ try {
106
106
  const codTab = page.getByRole("button", { name: "Cash on delivery" })
107
107
  await codTab.waitFor({ state: "visible" })
108
108
  await codTab.click()
109
- step("selected manual payment (pp_manual offline tab)")
109
+ step("selected the merchant method (offline tab)")
110
110
 
111
111
  // ── Place order ─────────────────────────────────────────────────────
112
112
  const submit = page.getByTestId("submit-order-button")
@@ -3,11 +3,11 @@
3
3
  import { useRouter } from "next/navigation"
4
4
  import type { Cart } from "@cartbase/storefront/api/carts"
5
5
  import type { StoreShippingOption } from "@cartbase/storefront/api/checkout"
6
- import type { StorePaymentProvider } from "@cartbase/storefront/api/checkout"
7
- import type { PublicCodConfig } from "@cartbase/storefront/api/integrations"
6
+ import type { StorePaymentEntry } from "@cartbase/storefront/api/checkout"
8
7
  import { CheckoutProvider } from "@cartbase/storefront/checkout/context"
9
8
  import { CheckoutClient } from "@cartbase/storefront/checkout/checkout-client"
10
9
  import { browserClient } from "@/lib/browser-client"
10
+ import { MyposDemoTab } from "./mypos-demo-tab"
11
11
 
12
12
  /** sessionStorage key the confirmation page reads (guests have no order
13
13
  * read endpoint — the completeCart() response is the only order handle,
@@ -18,12 +18,10 @@ export function CheckoutPageClient({
18
18
  cart,
19
19
  shippingOptions,
20
20
  paymentProviders,
21
- codConfig,
22
21
  }: {
23
22
  cart: Cart
24
23
  shippingOptions: StoreShippingOption[]
25
- paymentProviders: StorePaymentProvider[]
26
- codConfig: PublicCodConfig | null
24
+ paymentProviders: StorePaymentEntry[]
27
25
  }) {
28
26
  const router = useRouter()
29
27
 
@@ -38,13 +36,31 @@ export function CheckoutPageClient({
38
36
  countryCode="bg"
39
37
  countries={[{ iso_2: "bg", display_name: "Bulgaria" }]}
40
38
  // Per-store rule (the documented paymentMethodFilter seam): this
41
- // reference store checks out offline via pp_manual only without
42
- // the filter the hook prefers a tenant-enabled pp_cod for the
43
- // offline tab.
39
+ // reference store checks out offline via its merchant methods only
40
+ // (pp_* kill: method entries carry payment_method_id, processors
41
+ // carry id) — the filter drops any connected processor.
44
42
  paymentMethodFilter={(methods) =>
45
- methods?.filter((m) => m.id === "pp_manual") ?? null
43
+ methods?.filter((m) => "payment_method_id" in m) ?? null
44
+ }
45
+ // Provider-research demo (2026-08-04): the myPOS sandbox as a third
46
+ // radio card, so the embedded-provider seam can be judged inside
47
+ // the real checkout. Off unless NEXT_PUBLIC_MYPOS_DEMO=1.
48
+ extraPaymentTabs={
49
+ process.env.NEXT_PUBLIC_MYPOS_DEMO === "1"
50
+ ? [
51
+ {
52
+ id: "mypos-demo",
53
+ label: "Card via myPOS (demo)",
54
+ content: (
55
+ <MyposDemoTab
56
+ amount={Number(cart.total) || 23.45}
57
+ currency={cart.currency_code}
58
+ />
59
+ ),
60
+ },
61
+ ]
62
+ : undefined
46
63
  }
47
- codConfig={codConfig}
48
64
  onOrderPlaced={(order) => {
49
65
  // Guest order handle = the completeCart() response (orders.md).
50
66
  // Stash it (+ the decorated cart lines for the items list) for
@@ -0,0 +1,101 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef, useState } from "react"
4
+
5
+ /**
6
+ * DEMO ONLY (2026-08-04, provider research): mounts the myPOS Embedded
7
+ * sandbox (their public docs demo store) inside the real checkout's
8
+ * payment section, so the visual seam of a third-party embedded provider
9
+ * can be judged against our own tabs. Enabled by
10
+ * NEXT_PUBLIC_MYPOS_DEMO=1; never ship this to a real store.
11
+ */
12
+
13
+ const SDK_SRC = "https://developers.mypos.com/repository/mypos-embedded-sdk.js"
14
+ const CONTAINER_ID = "mypos-demo-container"
15
+
16
+ declare global {
17
+ interface Window {
18
+ MyPOSEmbedded?: {
19
+ createPayment: (
20
+ containerId: string,
21
+ params: Record<string, unknown>,
22
+ callbacks: Record<string, unknown>
23
+ ) => void
24
+ }
25
+ }
26
+ }
27
+
28
+ export function MyposDemoTab({ amount, currency }: { amount: number; currency: string }) {
29
+ const mounted = useRef(false)
30
+ const [error, setError] = useState<string | null>(null)
31
+
32
+ useEffect(() => {
33
+ if (mounted.current) return
34
+ mounted.current = true
35
+
36
+ const mount = () => {
37
+ const sdk = window.MyPOSEmbedded
38
+ if (!sdk || typeof sdk.createPayment !== "function") {
39
+ setError("myPOS SDK loaded but its global was not found (check the console).")
40
+ return
41
+ }
42
+ sdk.createPayment(
43
+ CONTAINER_ID,
44
+ {
45
+ // Public sandbox store from the myPOS docs sample.
46
+ sid: "000000000000010",
47
+ ipcLanguage: "en",
48
+ walletNumber: "61938166610",
49
+ amount,
50
+ currency: currency.toUpperCase(),
51
+ orderID: `demo_${Math.random().toString(36).slice(2, 11)}`,
52
+ urlNotify: "https://example.com/payment-notify",
53
+ urlOk: window.location.href,
54
+ urlCancel: window.location.href,
55
+ keyIndex: 1,
56
+ cartItems: [{ article: "Cartbase demo order", quantity: 1, price: amount, currency: currency.toUpperCase() }],
57
+ },
58
+ {
59
+ isSandbox: true,
60
+ onSuccess: (data: unknown) => {
61
+ // Real integration: this is where complete-cart would run,
62
+ // AFTER server-side verification via urlNotify.
63
+ // eslint-disable-next-line no-console
64
+ console.log("[mypos-demo] sandbox success", data)
65
+ window.alert("myPOS sandbox reported success (demo only, no order is placed).")
66
+ },
67
+ onError: () => {
68
+ // eslint-disable-next-line no-console
69
+ console.log("[mypos-demo] sandbox payment failed")
70
+ },
71
+ }
72
+ )
73
+ }
74
+
75
+ const existing = document.querySelector(`script[src="${SDK_SRC}"]`)
76
+ if (existing && window.MyPOSEmbedded) {
77
+ mount()
78
+ return
79
+ }
80
+ const script = document.createElement("script")
81
+ script.src = SDK_SRC
82
+ script.async = true
83
+ script.onload = mount
84
+ script.onerror = () => setError("Could not load the myPOS SDK script.")
85
+ document.head.appendChild(script)
86
+ }, [amount, currency])
87
+
88
+ return (
89
+ <div>
90
+ {error ? (
91
+ <p className="text-sm text-red-600">{error}</p>
92
+ ) : (
93
+ <div id={CONTAINER_ID} className="min-h-[620px]" />
94
+ )}
95
+ <p className="mt-2 text-xs text-muted-foreground">
96
+ myPOS sandbox demo. The frame above is myPOS's embedded form with its
97
+ own pay button; everything around it is the Cartbase checkout.
98
+ </p>
99
+ </div>
100
+ )
101
+ }
@@ -1,51 +1,48 @@
1
- import { cookies } from "next/headers"
2
- import { redirect } from "next/navigation"
3
- import { retrieveCart } from "@cartbase/storefront/api/carts"
4
- import {
5
- listPaymentProviders,
6
- listShippingOptions,
7
- } from "@cartbase/storefront/api/checkout"
8
- import { getIntegrationsConfig } from "@cartbase/storefront/api/integrations"
9
- import { readCartCookie } from "@/lib/config"
10
- import { getServerClient } from "@/lib/server-client"
11
- import { CheckoutPageClient } from "./checkout-page-client"
12
-
13
- /**
14
- * Checkout (runbook step 8 / checkout.md): list shipping options and
15
- * payment providers WITH `cart_id` (server-side rule filtering), fetch the
16
- * integrations `cod` block, and hand everything to the orchestrated
17
- * client layout. Redirect completed/missing carts server-side
18
- * (checkout-client mount rule).
19
- */
20
- export default async function CheckoutPage() {
21
- const jar = await cookies()
22
- // Back-compat: prefer `_cartbase_cart`, fall back to the legacy
23
- // `_barter_cart_id` name (platform-fingerprints card).
24
- const cartId = readCartCookie((name) => jar.get(name)?.value)
25
- if (!cartId) redirect("/")
26
-
27
- const client = await getServerClient()
28
- const cart = await retrieveCart(client, cartId)
29
- .then((res) => res.cart)
30
- .catch(() => null)
31
- if (!cart || (cart.items ?? []).length === 0) redirect("/")
32
- if (cart.completed_at) redirect("/")
33
-
34
- const [shippingOptions, paymentProviders, integrations] = await Promise.all([
35
- listShippingOptions(client, { cart_id: cart.id }),
36
- listPaymentProviders(client, {
37
- cart_id: cart.id,
38
- region_id: cart.region_id ?? undefined,
39
- }),
40
- getIntegrationsConfig(client),
41
- ])
42
-
43
- return (
44
- <CheckoutPageClient
45
- cart={cart}
46
- shippingOptions={shippingOptions.shipping_options}
47
- paymentProviders={paymentProviders.payment_providers}
48
- codConfig={integrations.cod}
49
- />
50
- )
51
- }
1
+ import { cookies } from "next/headers"
2
+ import { redirect } from "next/navigation"
3
+ import { retrieveCart } from "@cartbase/storefront/api/carts"
4
+ import {
5
+ listPaymentProviders,
6
+ listShippingOptions,
7
+ } from "@cartbase/storefront/api/checkout"
8
+ import { readCartCookie } from "@/lib/config"
9
+ import { getServerClient } from "@/lib/server-client"
10
+ import { CheckoutPageClient } from "./checkout-page-client"
11
+
12
+ /**
13
+ * Checkout (runbook step 8 / checkout.md): list shipping options and
14
+ * payment providers WITH `cart_id` (server-side rule filtering), fetch the
15
+ * integrations `cod` block, and hand everything to the orchestrated
16
+ * client layout. Redirect completed/missing carts server-side
17
+ * (checkout-client mount rule).
18
+ */
19
+ export default async function CheckoutPage() {
20
+ const jar = await cookies()
21
+ // Back-compat: prefer `_cartbase_cart`, fall back to the legacy
22
+ // `_barter_cart_id` name (platform-fingerprints card).
23
+ const cartId = readCartCookie((name) => jar.get(name)?.value)
24
+ if (!cartId) redirect("/")
25
+
26
+ const client = await getServerClient()
27
+ const cart = await retrieveCart(client, cartId)
28
+ .then((res) => res.cart)
29
+ .catch(() => null)
30
+ if (!cart || (cart.items ?? []).length === 0) redirect("/")
31
+ if (cart.completed_at) redirect("/")
32
+
33
+ const [shippingOptions, paymentProviders] = await Promise.all([
34
+ listShippingOptions(client, { cart_id: cart.id }),
35
+ listPaymentProviders(client, {
36
+ cart_id: cart.id,
37
+ region_id: cart.region_id ?? undefined,
38
+ }),
39
+ ])
40
+
41
+ return (
42
+ <CheckoutPageClient
43
+ cart={cart}
44
+ shippingOptions={shippingOptions.shipping_options}
45
+ paymentProviders={paymentProviders.payment_providers}
46
+ />
47
+ )
48
+ }
@@ -67,10 +67,11 @@ export default function OrderConfirmedPage() {
67
67
  order={orderForTemplate}
68
68
  totals={totals}
69
69
  items={cartItems.map(displayItemFromCartLine)}
70
- // This reference store enables exactly one provider (pp_manual); a
71
- // multi-provider store should stash the chosen provider id from its
72
- // checkout state alongside the order.
73
- paymentProviderId={"pp_manual"}
70
+ // This reference store checks out via one merchant method (pp_* kill:
71
+ // methods are merchant-named, provider-less); a multi-tender store
72
+ // should stash the chosen tender from its checkout state alongside
73
+ // the order.
74
+ paymentMethodName={"Cash on delivery"}
74
75
  storeHref="/"
75
76
  />
76
77
  )