create-cartbase 0.1.0 → 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.
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/template/app/docs/BUILD-A-STOREFRONT.md +6 -6
- package/template/app/docs/README.md +77 -76
- package/template/app/docs/auth.md +1 -1
- package/template/app/docs/carts.md +6 -6
- package/template/app/docs/categories.md +1 -1
- package/template/app/docs/checkout.md +127 -76
- package/template/app/docs/collections.md +1 -1
- package/template/app/docs/components.md +59 -60
- package/template/app/docs/consent.md +1 -1
- package/template/app/docs/content.md +1 -1
- package/template/app/docs/customers.md +1 -1
- package/template/app/docs/deploy.md +5 -5
- package/template/app/docs/gift-cards.md +1 -1
- package/template/app/docs/integrations.md +1 -1
- package/template/app/docs/menus.md +1 -1
- package/template/app/docs/metaobjects.md +1 -1
- package/template/app/docs/orders.md +1 -1
- package/template/app/docs/platform.md +126 -0
- package/template/app/docs/products.md +1 -1
- package/template/app/docs/redirects.md +1 -1
- package/template/app/docs/regions.md +5 -6
- package/template/app/docs/reviews.md +1 -1
- package/template/app/docs/search.md +1 -1
- package/template/app/docs/subscriptions.md +1 -1
- package/template/app/docs/variables.md +315 -0
- package/template/app/next-env.d.ts +6 -0
- package/template/app/next.config.ts +3 -3
- package/template/app/package.json +1 -1
- package/template/app/smoke.mjs +1 -1
- package/template/app/src/app/checkout/checkout-page-client.tsx +26 -10
- package/template/app/src/app/checkout/mypos-demo-tab.tsx +101 -0
- package/template/app/src/app/checkout/page.tsx +5 -6
- package/template/app/src/app/layout.tsx +113 -105
- package/template/app/src/app/order/[id]/confirmed/page.tsx +5 -4
- package/template/app/src/lib/cart-actions.ts +47 -43
- package/template/app/src/lib/config.ts +10 -5
- 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.
|
|
@@ -15,12 +15,12 @@ const nextConfig: NextConfig = {
|
|
|
15
15
|
// A real store should allowlist its media domain via remotePatterns.
|
|
16
16
|
unoptimized: true,
|
|
17
17
|
},
|
|
18
|
-
// The
|
|
18
|
+
// The Cartbase store API ships NO CORS headers — browser-side SDK calls
|
|
19
19
|
// must be same-origin. Proxy them through the app origin (the browser
|
|
20
20
|
// client's baseUrl is window.location.origin); server-side SDK calls go
|
|
21
|
-
// straight to
|
|
21
|
+
// straight to NEXT_PUBLIC_CARTBASE_URL and are unaffected.
|
|
22
22
|
async rewrites() {
|
|
23
|
-
const barterUrl = process.env.
|
|
23
|
+
const barterUrl = process.env.NEXT_PUBLIC_CARTBASE_URL
|
|
24
24
|
if (!barterUrl) return []
|
|
25
25
|
return [
|
|
26
26
|
{
|
package/template/app/smoke.mjs
CHANGED
|
@@ -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
|
|
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 {
|
|
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:
|
|
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
|
|
42
|
-
//
|
|
43
|
-
//
|
|
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) =>
|
|
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
|
+
}
|
|
@@ -5,8 +5,7 @@ import {
|
|
|
5
5
|
listPaymentProviders,
|
|
6
6
|
listShippingOptions,
|
|
7
7
|
} from "@cartbase/storefront/api/checkout"
|
|
8
|
-
import {
|
|
9
|
-
import { CART_COOKIE } from "@/lib/config"
|
|
8
|
+
import { readCartCookie } from "@/lib/config"
|
|
10
9
|
import { getServerClient } from "@/lib/server-client"
|
|
11
10
|
import { CheckoutPageClient } from "./checkout-page-client"
|
|
12
11
|
|
|
@@ -19,7 +18,9 @@ import { CheckoutPageClient } from "./checkout-page-client"
|
|
|
19
18
|
*/
|
|
20
19
|
export default async function CheckoutPage() {
|
|
21
20
|
const jar = await cookies()
|
|
22
|
-
|
|
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)
|
|
23
24
|
if (!cartId) redirect("/")
|
|
24
25
|
|
|
25
26
|
const client = await getServerClient()
|
|
@@ -29,13 +30,12 @@ export default async function CheckoutPage() {
|
|
|
29
30
|
if (!cart || (cart.items ?? []).length === 0) redirect("/")
|
|
30
31
|
if (cart.completed_at) redirect("/")
|
|
31
32
|
|
|
32
|
-
const [shippingOptions, paymentProviders
|
|
33
|
+
const [shippingOptions, paymentProviders] = await Promise.all([
|
|
33
34
|
listShippingOptions(client, { cart_id: cart.id }),
|
|
34
35
|
listPaymentProviders(client, {
|
|
35
36
|
cart_id: cart.id,
|
|
36
37
|
region_id: cart.region_id ?? undefined,
|
|
37
38
|
}),
|
|
38
|
-
getIntegrationsConfig(client),
|
|
39
39
|
])
|
|
40
40
|
|
|
41
41
|
return (
|
|
@@ -43,7 +43,6 @@ export default async function CheckoutPage() {
|
|
|
43
43
|
cart={cart}
|
|
44
44
|
shippingOptions={shippingOptions.shipping_options}
|
|
45
45
|
paymentProviders={paymentProviders.payment_providers}
|
|
46
|
-
codConfig={integrations.cod}
|
|
47
46
|
/>
|
|
48
47
|
)
|
|
49
48
|
}
|