create-cartbase 0.1.6 → 0.1.7
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/README.md +13 -8
- package/dist/index.js +28 -20
- package/package.json +21 -21
- package/template/app/AGENTS.md +29 -0
- package/template/app/CLAUDE.md +19 -8
- package/template/app/docs/BUILD-A-STOREFRONT.md +233 -226
- package/template/app/docs/README.md +1 -0
- package/template/app/docs/deploy.md +15 -10
- package/template/app/docs/platform.md +1 -1
- package/template/app/docs/store.md +47 -0
- package/template/app/docs/variables.md +3 -3
- package/template/app/next.config.ts +10 -14
- package/template/app/package.json +4 -3
- package/template/app/src/app/checkout/checkout-page-client.tsx +0 -20
- package/template/app/src/app/globals.css +1 -1
- package/template/app/src/app/layout.tsx +59 -18
- package/template/app/src/lib/config.ts +30 -21
- package/template/app/next-env.d.ts +0 -6
- package/template/app/smoke.mjs +0 -158
- package/template/app/src/app/checkout/mypos-demo-tab.tsx +0 -101
- package/template/app/src/app/gallery/[slug]/page.tsx +0 -172
- package/template/app/src/app/gallery/_components/specimens.tsx +0 -254
- package/template/app/src/app/gallery/_components/status.tsx +0 -47
- package/template/app/src/app/gallery/_lib/catalog.ts +0 -59
- package/template/app/src/app/gallery/_lib/registry.ts +0 -401
- package/template/app/src/app/gallery/design-system/page.tsx +0 -353
- package/template/app/src/app/gallery/layout.tsx +0 -83
- package/template/app/src/app/gallery/page.tsx +0 -81
- package/template/app/tsconfig.tsbuildinfo +0 -1
|
@@ -58,7 +58,7 @@ Every customer notification carries it. Money is formatted in the order's own cu
|
|
|
58
58
|
| `{{ order.email }}` | The address the order was placed with. | email | |
|
|
59
59
|
| `{{ order.date }}` | When the order was placed. | date | |
|
|
60
60
|
| `{{ order.status }}` | Lifecycle state: pending, completed, canceled or archived. | text | |
|
|
61
|
-
| `{{ order.payment_status }}` | Paid,
|
|
61
|
+
| `{{ order.payment_status }}` | Paid, Unpaid, Partially paid, Refunded and so on, in the same words the admin uses. | text | |
|
|
62
62
|
| `{{ order.fulfillment_status }}` | Unfulfilled, Shipped, Delivered and so on, in the same words the admin uses. | text | |
|
|
63
63
|
| `{{ order.locale }}` | Language the customer shopped in, when the storefront sends one. | text | |
|
|
64
64
|
| `{{ order.cancelled_at }}` | When the order was cancelled, if it was. | datetime | |
|
|
@@ -70,7 +70,7 @@ Every customer notification carries it. Money is formatted in the order's own cu
|
|
|
70
70
|
| `{{ order.shipping }}` | What delivery cost the customer. | money | |
|
|
71
71
|
| `{{ order.discount }}` | Total discount applied to the order. | money | |
|
|
72
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
|
|
73
|
+
| `{{ order.paid }}` | How much has actually been received so far. | money | |
|
|
74
74
|
| `{{ order.refunded }}` | How much has been refunded. | money | |
|
|
75
75
|
| `{{ order.balance_due }}` | What is still outstanding on the order. | money | |
|
|
76
76
|
|
|
@@ -271,7 +271,7 @@ Staff operational alerts only.
|
|
|
271
271
|
| Order Shipped | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
|
|
272
272
|
| Order Delivered | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
|
|
273
273
|
| Order Refunded | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
|
|
274
|
-
| Order
|
|
274
|
+
| Order Canceled | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
|
|
275
275
|
| Admin: New Order | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
|
|
276
276
|
| Welcome Email | Customer, Store, Brand |
|
|
277
277
|
| Password Reset | Customer, Store, Brand |
|
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
1
|
import type { NextConfig } from "next"
|
|
3
2
|
|
|
4
3
|
const nextConfig: NextConfig = {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
outputFileTracingRoot: path.join(__dirname, "../.."),
|
|
8
|
-
// The smoke driver browses via 127.0.0.1; allow dev-resource access.
|
|
9
|
-
allowedDevOrigins: ["127.0.0.1"],
|
|
10
|
-
// @cartbase/storefront is source-shipped TypeScript (workspace package) —
|
|
11
|
-
// the app's Next build transpiles it.
|
|
4
|
+
// @cartbase/storefront is source-shipped TypeScript — the app's Next
|
|
5
|
+
// build transpiles it.
|
|
12
6
|
transpilePackages: ["@cartbase/storefront"],
|
|
13
7
|
images: {
|
|
14
|
-
//
|
|
15
|
-
//
|
|
8
|
+
// Product images come from the Cartbase media CDN and, until you
|
|
9
|
+
// allowlist your own hosts under remotePatterns, from anywhere the
|
|
10
|
+
// catalog points. Tighten this when the media domain is known.
|
|
16
11
|
unoptimized: true,
|
|
17
12
|
},
|
|
18
13
|
// The Cartbase store API ships NO CORS headers — browser-side SDK calls
|
|
19
14
|
// must be same-origin. Proxy them through the app origin (the browser
|
|
20
15
|
// client's baseUrl is window.location.origin); server-side SDK calls go
|
|
21
|
-
// straight to
|
|
16
|
+
// straight to the platform origin and are unaffected. The origin is the
|
|
17
|
+
// platform's constant; NEXT_PUBLIC_CARTBASE_URL only overrides it for a
|
|
18
|
+
// local or staging platform.
|
|
22
19
|
async rewrites() {
|
|
23
|
-
const
|
|
24
|
-
if (!barterUrl) return []
|
|
20
|
+
const cartbaseUrl = process.env.NEXT_PUBLIC_CARTBASE_URL || "https://admin.cartbase.ai"
|
|
25
21
|
return [
|
|
26
22
|
{
|
|
27
23
|
source: "/api/store/:path*",
|
|
28
|
-
destination: `${
|
|
24
|
+
destination: `${cartbaseUrl}/api/store/:path*`,
|
|
29
25
|
},
|
|
30
26
|
]
|
|
31
27
|
},
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"name": "cartbase-storefront",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "Reference storefront built from docs/storefront/BUILD-A-STOREFRONT.md alone — the adoption proof for @cartbase/storefront.",
|
|
6
5
|
"scripts": {
|
|
7
|
-
"
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start",
|
|
8
|
+
"dev": "next dev",
|
|
8
9
|
"typecheck": "tsc --noEmit"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {
|
|
11
|
-
"@cartbase/storefront": "^0.
|
|
12
|
+
"@cartbase/storefront": "^0.8.0",
|
|
12
13
|
"next": "16.2.4",
|
|
13
14
|
"react": "19.2.4",
|
|
14
15
|
"react-dom": "19.2.4"
|
|
@@ -7,7 +7,6 @@ import type { StorePaymentEntry } from "@cartbase/storefront/api/checkout"
|
|
|
7
7
|
import { CheckoutProvider } from "@cartbase/storefront/checkout/context"
|
|
8
8
|
import { CheckoutClient } from "@cartbase/storefront/checkout/checkout-client"
|
|
9
9
|
import { browserClient } from "@/lib/browser-client"
|
|
10
|
-
import { MyposDemoTab } from "./mypos-demo-tab"
|
|
11
10
|
|
|
12
11
|
/** sessionStorage key the confirmation page reads (guests have no order
|
|
13
12
|
* read endpoint — the completeCart() response is the only order handle,
|
|
@@ -42,25 +41,6 @@ export function CheckoutPageClient({
|
|
|
42
41
|
paymentMethodFilter={(methods) =>
|
|
43
42
|
methods?.filter((m) => "payment_method_id" in m) ?? null
|
|
44
43
|
}
|
|
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
|
|
63
|
-
}
|
|
64
44
|
onOrderPlaced={(order) => {
|
|
65
45
|
// Guest order handle = the completeCart() response (orders.md).
|
|
66
46
|
// Stash it (+ the decorated cart lines for the items list) for
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* to scan it to know which classes exist. Without this the components render
|
|
18
18
|
* with no styles at all — the classes are in the markup but never generated.
|
|
19
19
|
*/
|
|
20
|
-
@source "
|
|
20
|
+
@source "../../node_modules/@cartbase/storefront/src";
|
|
21
21
|
|
|
22
22
|
/*
|
|
23
23
|
* Dark mode is class-based: put `.dark` on <html> and every token swaps.
|
|
@@ -17,10 +17,56 @@ import "./globals.css"
|
|
|
17
17
|
// createStorefrontMetadata (platform-fingerprints card) stamps
|
|
18
18
|
// `<meta name="generator" content="Cartbase" />` on every page — never
|
|
19
19
|
// hand-write `generator` here.
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
21
|
+
const store = await fetchStore()
|
|
22
|
+
return createStorefrontMetadata({
|
|
23
|
+
title: store.name,
|
|
24
|
+
description: store.brand.slogan ?? undefined,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The store's own identity (store.md): name, slug, brand. Read once per
|
|
30
|
+
* request; a storefront never hardcodes its name. Until @cartbase/storefront
|
|
31
|
+
* 0.8.0 ships `api/store`, the call goes through the client's low-level
|
|
32
|
+
* request with the documented shape.
|
|
33
|
+
*/
|
|
34
|
+
type StoreIdentity = {
|
|
35
|
+
/** The store's public id: what PlatformInit mounts (one key: never an env input). */
|
|
36
|
+
id: string
|
|
37
|
+
name: string
|
|
38
|
+
slug: string
|
|
39
|
+
brand: {
|
|
40
|
+
logo_url: string | null
|
|
41
|
+
logo_square_url: string | null
|
|
42
|
+
color_primary: string | null
|
|
43
|
+
color_secondary: string | null
|
|
44
|
+
slogan: string | null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchStore(): Promise<StoreIdentity> {
|
|
49
|
+
const client = await getServerClient()
|
|
50
|
+
try {
|
|
51
|
+
const { store } = await client.request<{ store: StoreIdentity }>("/api/store/store")
|
|
52
|
+
return store
|
|
53
|
+
} catch {
|
|
54
|
+
// Never crash the layout on a transient API error; the name is
|
|
55
|
+
// decoration, the store still renders.
|
|
56
|
+
return {
|
|
57
|
+
id: BARTER_CLIENT_ID ?? "",
|
|
58
|
+
name: "Store",
|
|
59
|
+
slug: "",
|
|
60
|
+
brand: {
|
|
61
|
+
logo_url: null,
|
|
62
|
+
logo_square_url: null,
|
|
63
|
+
color_primary: null,
|
|
64
|
+
color_secondary: null,
|
|
65
|
+
slogan: null,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
24
70
|
|
|
25
71
|
/**
|
|
26
72
|
* Runbook steps 3–4: bootstrap store config at layout level, then mount
|
|
@@ -67,7 +113,8 @@ export default async function RootLayout({
|
|
|
67
113
|
// the rename (platform-fingerprints card).
|
|
68
114
|
const cartId = readCartCookie((name) => jar.get(name)?.value) ?? null
|
|
69
115
|
|
|
70
|
-
const [consentRes, mainMenu, footerMenu, cart] = await Promise.all([
|
|
116
|
+
const [store, consentRes, mainMenu, footerMenu, cart] = await Promise.all([
|
|
117
|
+
fetchStore(),
|
|
71
118
|
getConsent(client),
|
|
72
119
|
fetchMenu("main-menu"),
|
|
73
120
|
fetchMenu("footer"),
|
|
@@ -94,12 +141,12 @@ export default async function RootLayout({
|
|
|
94
141
|
wbraid) that exist ONLY on an ad's landing URL — miss them
|
|
95
142
|
here and no platform can attribute the order to the click. */}
|
|
96
143
|
<TrackInit />
|
|
97
|
-
<PlatformInit storeId={
|
|
144
|
+
<PlatformInit storeId={store.id} />
|
|
98
145
|
<Providers cart={cart} consent={consentRes.consent}>
|
|
99
146
|
<header className="border-b border-border">
|
|
100
147
|
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
|
|
101
148
|
<Link href="/" className="font-semibold text-lg">
|
|
102
|
-
|
|
149
|
+
{store.name}
|
|
103
150
|
</Link>
|
|
104
151
|
<div className="flex items-center gap-6">
|
|
105
152
|
<MenuNav menu={mainMenu} />
|
|
@@ -110,17 +157,11 @@ export default async function RootLayout({
|
|
|
110
157
|
Search
|
|
111
158
|
</Link>
|
|
112
159
|
{/*
|
|
113
|
-
The workbench
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
160
|
+
The component workbench lives at /gallery in THIS app only
|
|
161
|
+
(the seed excludes it, src/lib/storefront/seed.ts), so the
|
|
162
|
+
shop's own nav never links to it: a merchant's store must
|
|
163
|
+
not carry a dead link.
|
|
117
164
|
*/}
|
|
118
|
-
<Link
|
|
119
|
-
href="/gallery"
|
|
120
|
-
className="text-sm text-muted-foreground hover:text-foreground"
|
|
121
|
-
>
|
|
122
|
-
Library
|
|
123
|
-
</Link>
|
|
124
165
|
<CartButtonClient cart={cart} />
|
|
125
166
|
</div>
|
|
126
167
|
</div>
|
|
@@ -128,7 +169,7 @@ export default async function RootLayout({
|
|
|
128
169
|
<main>{children}</main>
|
|
129
170
|
<footer className="border-t border-border mt-12">
|
|
130
171
|
<div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
|
|
131
|
-
<span>
|
|
172
|
+
<span>{store.name}</span>
|
|
132
173
|
<MenuNav menu={footerMenu} />
|
|
133
174
|
</div>
|
|
134
175
|
</footer>
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
/** Shared storefront configuration (BUILD-A-STOREFRONT.md —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
/** Shared storefront configuration (BUILD-A-STOREFRONT.md — the one input). */
|
|
2
|
+
|
|
3
|
+
import { cartbaseApiOrigin } from "@cartbase/storefront/lib/platform"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The store's publishable key names the store and scopes its catalog: the
|
|
7
|
+
* ONE value a storefront needs (one key, 2026-09-07). The origin is the
|
|
8
|
+
* platform's, overridable for a local or staging platform; the client id is
|
|
9
|
+
* the platform's own door (hosted builds carry it) and never something to
|
|
10
|
+
* copy by hand.
|
|
11
|
+
*/
|
|
12
|
+
export const BARTER_PUBLISHABLE_KEY =
|
|
13
|
+
process.env.NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY || undefined
|
|
14
|
+
export const BARTER_URL = cartbaseApiOrigin(process.env.NEXT_PUBLIC_CARTBASE_URL)
|
|
15
|
+
export const BARTER_CLIENT_ID = process.env.NEXT_PUBLIC_CARTBASE_CLIENT_ID || undefined
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cart-id cookie — owned by the app (the SDK never persists the cart);
|
|
19
|
+
* the NAME is owned by the library so every consumer emits the same wire
|
|
20
|
+
* fingerprint (platform-fingerprints card). `readCartCookie` is the
|
|
21
|
+
* backward-compat reader for the legacy `_barter_cart_id` name.
|
|
22
|
+
*/
|
|
23
|
+
export { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"
|
|
24
|
+
export const CART_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
|
|
25
|
+
|
|
26
|
+
/** Locale cookie read by the clients' `getLocale`. */
|
|
27
|
+
export const LOCALE_COOKIE = "_barter_locale"
|
|
28
|
+
|
|
29
|
+
/** Pricing context for every catalog surface (runbook step 5). EUR only. */
|
|
30
|
+
export const PRICING_CONTEXT = { currency_code: "eur" } as const
|
package/template/app/smoke.mjs
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adoption-proof smoke: drives the REAL example-storefront UI from home to
|
|
3
|
-
* a completed pp_manual checkout against the barter backend.
|
|
4
|
-
*
|
|
5
|
-
* Prereqs: backend on :4777, example app on :4778 (see examples/storefront
|
|
6
|
-
* package.json). Run: `node examples/storefront/smoke.mjs` from the repo
|
|
7
|
-
* root (Playwright is a root devDependency). Exits 0 only when the
|
|
8
|
-
* confirmation page shows an order number and totals.
|
|
9
|
-
*/
|
|
10
|
-
import { chromium } from "@playwright/test"
|
|
11
|
-
|
|
12
|
-
const BASE = process.env.SMOKE_BASE_URL ?? "http://127.0.0.1:4778"
|
|
13
|
-
const trail = []
|
|
14
|
-
const step = (msg) => {
|
|
15
|
-
trail.push(msg)
|
|
16
|
-
console.log(`[smoke] ${msg}`)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const browser = await chromium.launch()
|
|
20
|
-
const page = await browser.newPage()
|
|
21
|
-
page.setDefaultTimeout(30_000)
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
// ── Home ────────────────────────────────────────────────────────────
|
|
25
|
-
await page.goto(`${BASE}/`, { waitUntil: "domcontentloaded" })
|
|
26
|
-
step("home loaded")
|
|
27
|
-
|
|
28
|
-
// Consent banner (builtin modal is the server default) — accept.
|
|
29
|
-
const acceptButton = page.getByRole("button", { name: "Accept", exact: true })
|
|
30
|
-
try {
|
|
31
|
-
await acceptButton.waitFor({ state: "visible", timeout: 10_000 })
|
|
32
|
-
await acceptButton.click()
|
|
33
|
-
step("consent banner accepted")
|
|
34
|
-
} catch {
|
|
35
|
-
step("no consent banner rendered (disabled or external mode)")
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await page.locator('a[href^="/products/"]').first().waitFor()
|
|
39
|
-
step("home renders product cards")
|
|
40
|
-
|
|
41
|
-
// ── PDP (seeded handle) ─────────────────────────────────────────────
|
|
42
|
-
await page.goto(`${BASE}/products/linen-shirt`, {
|
|
43
|
-
waitUntil: "domcontentloaded",
|
|
44
|
-
})
|
|
45
|
-
await page.getByTestId("product-container").waitFor()
|
|
46
|
-
step("PDP linen-shirt rendered")
|
|
47
|
-
|
|
48
|
-
// Pick the REAL Size option's "M" (the shared dev tenant accretes junk
|
|
49
|
-
// options; variant match needs exactly the Size choice). Dev-mode
|
|
50
|
-
// hydration can lag the SSR paint — retry until the selection sticks.
|
|
51
|
-
const sizeRow = page
|
|
52
|
-
.locator('div.flex.flex-col:has(> span:text-is("Select Size"))')
|
|
53
|
-
.first()
|
|
54
|
-
const mButton = sizeRow.getByRole("button", { name: "M", exact: true })
|
|
55
|
-
let selected = false
|
|
56
|
-
for (let i = 0; i < 30 && !selected; i++) {
|
|
57
|
-
await mButton.click()
|
|
58
|
-
await page.waitForTimeout(700)
|
|
59
|
-
selected = ((await mButton.getAttribute("class")) ?? "").includes(
|
|
60
|
-
"border-primary"
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
if (!selected) throw new Error("Size M never became selected")
|
|
64
|
-
step("selected Size M")
|
|
65
|
-
|
|
66
|
-
const addButton = page.getByTestId("add-product-button").first()
|
|
67
|
-
await addButton.click()
|
|
68
|
-
step("clicked Add to cart")
|
|
69
|
-
|
|
70
|
-
// ── Cart drawer → checkout ──────────────────────────────────────────
|
|
71
|
-
// The drawer auto-opens only on a rise from a nonzero base (first add
|
|
72
|
-
// never auto-opens — components.md); open it via the header cart
|
|
73
|
-
// button once the badge confirms the server-side add landed.
|
|
74
|
-
await page
|
|
75
|
-
.getByTestId("nav-cart-link")
|
|
76
|
-
.locator("span")
|
|
77
|
-
.filter({ hasText: /^\d+$/ })
|
|
78
|
-
.waitFor({ timeout: 20_000 })
|
|
79
|
-
step("header cart badge shows the added item")
|
|
80
|
-
await page.getByTestId("nav-cart-link").click()
|
|
81
|
-
const checkoutLink = page.getByRole("link", { name: "Checkout" })
|
|
82
|
-
await checkoutLink.click()
|
|
83
|
-
step("cart drawer opened via header cart button")
|
|
84
|
-
await page.waitForURL("**/checkout")
|
|
85
|
-
step("navigated to /checkout")
|
|
86
|
-
|
|
87
|
-
// ── Address form ────────────────────────────────────────────────────
|
|
88
|
-
const fill = async (name, value) => {
|
|
89
|
-
await page.locator(`[name="${name}"]`).fill(value)
|
|
90
|
-
}
|
|
91
|
-
await fill("email", "smoke-adoption@example.test")
|
|
92
|
-
await fill("shipping_address.first_name", "Ivan")
|
|
93
|
-
await fill("shipping_address.last_name", "Petrov")
|
|
94
|
-
await fill("shipping_address.address_1", "ul. Vitosha 15")
|
|
95
|
-
await fill("shipping_address.postal_code", "1000")
|
|
96
|
-
await fill("shipping_address.city", "Sofia")
|
|
97
|
-
await fill("shipping_address.phone", "+359888123456")
|
|
98
|
-
await page.locator('[name="shipping_address.phone"]').blur()
|
|
99
|
-
step("address form filled (BG address, EUR cart)")
|
|
100
|
-
|
|
101
|
-
// ── Shipping method ─────────────────────────────────────────────────
|
|
102
|
-
await page.getByText("Flat Rate (Bulgaria)", { exact: false }).first().click()
|
|
103
|
-
step('selected shipping "Flat Rate (Bulgaria)"')
|
|
104
|
-
|
|
105
|
-
// ── Payment: manual/offline tab (no Stripe env) ─────────────────────
|
|
106
|
-
const codTab = page.getByRole("button", { name: "Cash on delivery" })
|
|
107
|
-
await codTab.waitFor({ state: "visible" })
|
|
108
|
-
await codTab.click()
|
|
109
|
-
step("selected the merchant method (offline tab)")
|
|
110
|
-
|
|
111
|
-
// ── Place order ─────────────────────────────────────────────────────
|
|
112
|
-
const submit = page.getByTestId("submit-order-button")
|
|
113
|
-
await submit.waitFor({ state: "visible" })
|
|
114
|
-
// The Buy button enables once the debounced address autosave lands.
|
|
115
|
-
await page.waitForFunction(
|
|
116
|
-
() =>
|
|
117
|
-
!document.querySelector('[data-testid="submit-order-button"]')?.disabled,
|
|
118
|
-
undefined,
|
|
119
|
-
{ timeout: 20_000 }
|
|
120
|
-
)
|
|
121
|
-
await submit.click()
|
|
122
|
-
step("clicked Place order")
|
|
123
|
-
|
|
124
|
-
// ── Confirmation ────────────────────────────────────────────────────
|
|
125
|
-
await page.waitForURL("**/order/**/confirmed", { timeout: 60_000 })
|
|
126
|
-
step(`confirmation URL: ${new URL(page.url()).pathname}`)
|
|
127
|
-
|
|
128
|
-
const orderNumberText = await page
|
|
129
|
-
.getByText(/#\d+/)
|
|
130
|
-
.first()
|
|
131
|
-
.innerText({ timeout: 30_000 })
|
|
132
|
-
const displayNumber = orderNumberText.match(/#(\d+)/)?.[1]
|
|
133
|
-
if (!displayNumber) throw new Error("no order display number rendered")
|
|
134
|
-
step(`order number rendered: #${displayNumber}`)
|
|
135
|
-
|
|
136
|
-
// Totals: the order family renders a Total row with a EUR amount.
|
|
137
|
-
const totalRow = page
|
|
138
|
-
.locator("div,li")
|
|
139
|
-
.filter({ hasText: /^Total/ })
|
|
140
|
-
.filter({ hasText: "€" })
|
|
141
|
-
.first()
|
|
142
|
-
const totalText = (await totalRow.innerText()).replace(/\s+/g, " ").trim()
|
|
143
|
-
if (!/€\s?\d/.test(totalText)) throw new Error("no EUR total rendered")
|
|
144
|
-
step(`totals rendered: ${totalText}`)
|
|
145
|
-
|
|
146
|
-
console.log("\n[smoke] PASS — completed checkout, order #" + displayNumber)
|
|
147
|
-
await browser.close()
|
|
148
|
-
process.exit(0)
|
|
149
|
-
} catch (err) {
|
|
150
|
-
console.error("\n[smoke] FAIL after steps:\n - " + trail.join("\n - "))
|
|
151
|
-
console.error(err)
|
|
152
|
-
try {
|
|
153
|
-
await page.screenshot({ path: "examples/storefront/smoke-failure.png" })
|
|
154
|
-
console.error("[smoke] screenshot: examples/storefront/smoke-failure.png")
|
|
155
|
-
} catch {}
|
|
156
|
-
await browser.close()
|
|
157
|
-
process.exit(1)
|
|
158
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
}
|