@wix/agent-skills 1.27.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +1 -1
- package/skills/wix-app/references/APP_VALIDATION.md +5 -1
- package/skills/wix-app/references/SDK_MODULE_MAP.md +1 -1
- package/skills/wix-app/references/stores/GET_PRODUCT.md +9 -1
- package/skills/wix-app/references/stores/INVENTORY.md +2 -0
- package/skills/wix-app/references/stores/QUERY.md +2 -2
- package/skills/wix-app/references/stores/WRITE.md +1 -1
- package/skills/wix-headless-kit/SKILL.md +4 -0
- package/skills/wix-headless-kit/guides/no-machine.md +115 -0
- package/skills/wix-headless-kit/install/attach.mjs +5 -3
- package/skills/wix-headless-kit/install/bootstrap.mjs +3 -1
- package/skills/wix-headless-kit/install/context.mjs +4 -2
- package/skills/wix-headless-kit/install/deploy.mjs +4 -2
- package/skills/wix-headless-kit/install/pins.json +8 -0
- package/skills/wix-headless-kit/install/setup.mjs +10 -8
- package/skills/wix-headless-kit/install/templates.mjs +18 -8
- package/skills/wix-headless-templates/donations/INSTRUCTIONS.md +8 -9
- package/skills/wix-headless-templates/donations/app/wix/donations/donate.ts +18 -45
- package/skills/wix-headless-templates/donations/app/wix/donations/donations-core.ts +18 -19
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donate.ts +18 -45
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donations-core.ts +18 -19
- package/skills/wix-headless-templates/donations/rest/donate.ts +13 -39
- package/skills/wix-headless-templates/pricing-plans/INSTRUCTIONS.md +4 -2
- package/skills/wix-headless-templates/shared/capabilities/site-search/seed/install.mjs +2 -2
- package/skills/wix-headless-templates/shared/seed/wix-cli.mjs +6 -3
- package/skills/wix-headless-templates/storefront/app/wix/storefront/cart.ts +1 -2
- package/skills/wix-headless-templates/storefront/project/src/wix/storefront/cart.ts +1 -2
- package/skills/wix-manage/references/sites/upload-static-site.md +61 -42
package/package.json
CHANGED
package/skills/wix-app/SKILL.md
CHANGED
|
@@ -492,7 +492,7 @@ Execute these steps sequentially after all implementation is complete. See [APP_
|
|
|
492
492
|
1. **Package Installation** — Detect package manager, run install
|
|
493
493
|
2. **TypeScript Compilation** — `npx tsc --noEmit -p .`
|
|
494
494
|
3. **Build** — `npx wix build`
|
|
495
|
-
4. **Preview** — `npx wix preview`, in the foreground: it uploads, prints the preview URLs and exits on its own, so no `timeout`, backgrounding or `
|
|
495
|
+
4. **Preview** — `npx wix preview`, in the foreground and unpiped: it uploads, prints the preview URLs and exits on its own, so no `timeout`, backgrounding, `sleep` or `| tail`. If your shell moves it to the background anyway, it stalled: read what it printed and `.wix/debug.log` once, then report that the preview did not complete. Don't poll, and don't promise a URL
|
|
496
496
|
|
|
497
497
|
Stop and report errors if any step fails. Check `.wix/debug.log` on failures.
|
|
498
498
|
|
|
@@ -85,14 +85,18 @@ npx wix build
|
|
|
85
85
|
|
|
86
86
|
### Step 4: Preview Deployment
|
|
87
87
|
|
|
88
|
-
`npx wix preview` is a one-shot command, not a server: it uploads a preview build, prints the preview URLs and **exits on its own**, in seconds. Run it in the foreground and read its output:
|
|
88
|
+
`npx wix preview` is a one-shot command, not a server: it uploads a preview build, prints the preview URLs and **exits on its own**, in seconds. Run it in the foreground, exactly as written, and read its output:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
npx wix preview
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
Don't pipe it (`| tail`, `| head`, `| grep`). The output is short, and a pipe holds back everything until the command exits, so a slow preview shows nothing at all.
|
|
95
|
+
|
|
94
96
|
Don't wrap it in `timeout` (macOS has none, so the call fails before preview runs), background it, `sleep` or poll a log for it, or `kill` it afterwards. There is no process left to wait for or stop.
|
|
95
97
|
|
|
98
|
+
**If it doesn't return:** your shell may move a long call to the background on its own (for example, after a 120s limit). That means the preview stalled, not that it's still working normally. Read the output it left and `.wix/debug.log` **once**. If neither has the preview URLs, report "preview did not complete" with whatever they show and stop validation. Don't sleep, poll, schedule a wake-up or start the preview again, and don't tell the user a URL is coming.
|
|
99
|
+
|
|
96
100
|
**Success criteria:**
|
|
97
101
|
- The command exits 0
|
|
98
102
|
- It printed `✔ Preview created successfully!` and the preview URLs
|
|
@@ -15,7 +15,7 @@ Data owned by an existing Wix business app is read and written through that app'
|
|
|
15
15
|
| pricing plans / subscriptions | `@wix/pricing-plans` |
|
|
16
16
|
| bookings / services / staff / time slots | `@wix/bookings` |
|
|
17
17
|
| calendar events / schedules | `@wix/calendar` |
|
|
18
|
-
| table reservations | `@wix/table-reservations` |
|
|
18
|
+
| table reservations | `@wix/table-reservations` (namespace `reservations`) — not `@wix/restaurants` |
|
|
19
19
|
| restaurant menus / online orders | `@wix/restaurants` |
|
|
20
20
|
| blog posts | `@wix/blog` |
|
|
21
21
|
| site events / tickets / RSVPs | `@wix/events` |
|
|
@@ -4,13 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
6
|
if (v === 'V3_CATALOG') {
|
|
7
|
-
const product = await productsV3.getProduct(id); // returns
|
|
7
|
+
const product = await productsV3.getProduct(id); // returns V3Product directly
|
|
8
8
|
return product;
|
|
9
9
|
}
|
|
10
10
|
const { product } = await products.getProduct(id); // V1 wraps in { product }
|
|
11
11
|
return product;
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
The V3 entity type is `productsV3.V3Product`.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Product page URL
|
|
19
|
+
|
|
20
|
+
V3 `product.url` is populated only when the request's `fields` includes `'URL'` — `getProduct(id, { fields: ['URL'] })`, or the direct-call `searchProducts` / `queryProducts(query, { fields: ['URL'] })` (the fluent builder's `.find()` takes no options). V1 `productPageUrl` is an object, `{ base, path }`, not a string.
|
|
21
|
+
|
|
14
22
|
---
|
|
15
23
|
|
|
16
24
|
## Read product options and choices (V1 vs V3)
|
|
@@ -18,6 +18,8 @@ if (v === 'V3_CATALOG') {
|
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
V3 increments work only for inventory items with `trackQuantity: true`; the entries are `{ inventoryItemId, incrementBy }` with `incrementBy` 1 to 99999.
|
|
22
|
+
|
|
21
23
|
To find a V3 inventory item ID, use `inventoryItemsV3.searchInventoryItems` filtered by `productId` / `variantId`.
|
|
22
24
|
|
|
23
25
|
---
|
|
@@ -8,7 +8,7 @@ Both versions expose a fluent query builder, but the paging method differs:
|
|
|
8
8
|
|--------|--------------------------------|----------------------------------|
|
|
9
9
|
| API style | Fluent builder: `.skip().limit().find()` | Fluent builder: `.skipTo(cursor).limit().find()` |
|
|
10
10
|
| Pagination | Offset (`.skip(n)`) | Cursor (`.skipTo(cursor)`) |
|
|
11
|
-
| Result `items` | `res.items` (V1 `Product[]`) | `res.items` (V3 `
|
|
11
|
+
| Result `items` | `res.items` (V1 `Product[]`) | `res.items` (V3 `productsV3.V3Product[]`) |
|
|
12
12
|
| Total count | `res.totalCount` | **None** — V3 only has `cursors.next` + `hasNext()` |
|
|
13
13
|
| `hasNext` | `res.hasNext()` (method) | `res.hasNext()` (method) |
|
|
14
14
|
| Next cursor | n/a | `res.cursors.next` (string) |
|
|
@@ -35,7 +35,7 @@ export async function listProductsPage(
|
|
|
35
35
|
if (v === 'V3_CATALOG') {
|
|
36
36
|
let builder = productsV3.queryProducts().limit(limit);
|
|
37
37
|
if (typeof cursorOrSkip === 'string') builder = builder.skipTo(cursorOrSkip);
|
|
38
|
-
// Do NOT chain a sort — see
|
|
38
|
+
// Do NOT chain a sort — see [STORES_VERSIONING.md](../STORES_VERSIONING.md) gotcha #10.
|
|
39
39
|
const res = await builder.find();
|
|
40
40
|
return {
|
|
41
41
|
products: res.items,
|
|
@@ -32,7 +32,7 @@ return product;
|
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
34
|
if (v === 'V3_CATALOG') {
|
|
35
|
-
// Signature: updateProduct(_id, productFields). Returns
|
|
35
|
+
// Signature: updateProduct(_id, productFields). Returns V3Product directly.
|
|
36
36
|
const current = await productsV3.getProduct(id);
|
|
37
37
|
if (current.revision == null) throw new Error(`Product ${id} has no revision`);
|
|
38
38
|
return await productsV3.updateProduct(id, {
|
|
@@ -302,6 +302,10 @@ browser versus the server, the OAuth allow-list for a self-hosted origin, pre-re
|
|
|
302
302
|
how to close such a run. A public site is still better served by managed Astro; say so when you
|
|
303
303
|
close.
|
|
304
304
|
|
|
305
|
+
Without a machine at all, or when the install, the CLI or the login is blocked where you are,
|
|
306
|
+
read `<SKILL_ROOT>/guides/no-machine.md`: the same run, step by step, as the Wix API calls the
|
|
307
|
+
scripts make and the files beside this skill that carry the contracts.
|
|
308
|
+
|
|
305
309
|
## Verticals
|
|
306
310
|
|
|
307
311
|
The shortlist. Match the brief against the first column; when it names a Wix product or a
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# No machine: the kit's run through the Wix APIs
|
|
2
|
+
|
|
3
|
+
Read when you cannot run the commands this skill is built on: no shell, no file system, no Node, a
|
|
4
|
+
sandbox that blocks the install, the CLI or the login, or a host that only talks to Wix through
|
|
5
|
+
the Wix MCP. Nothing in this guide installs, downloads or runs anything. It walks the run of
|
|
6
|
+
SKILL.md step by step and names, for each, the Wix API call the CLI or the script performs, and
|
|
7
|
+
the file beside this skill that carries the contract.
|
|
8
|
+
|
|
9
|
+
## Where these skills are
|
|
10
|
+
|
|
11
|
+
This skill is one of a set, and the guide reads from three of them:
|
|
12
|
+
|
|
13
|
+
- `<SKILL_ROOT>` — this skill, `wix-headless-kit`: the run, its guides, the Verticals table.
|
|
14
|
+
- `<TEMPLATES>` — `wix-headless-templates`: the shipped, verified code per Wix Business Solution,
|
|
15
|
+
one folder each, with its `INSTRUCTIONS.md` (the contracts), `seed/` (the seed script and
|
|
16
|
+
`SEED.md`) and `rest/` (the data layer over plain `fetch`). Installed beside this skill, or
|
|
17
|
+
fetched to `<SKILL_ROOT>/templates/`.
|
|
18
|
+
- `<MANAGE>` — `wix-manage`: REST recipes to configure and manage a site's business solutions, one
|
|
19
|
+
recipe per operation with the exact endpoint and payload; its `SKILL.md` is the index.
|
|
20
|
+
- `<DOCS>` — `wix-docs`: how to look up the Wix API and SDK documentation and confirm a method's
|
|
21
|
+
exact shape before writing a call.
|
|
22
|
+
|
|
23
|
+
They install together and sit beside each other. When you are reading this skill online rather
|
|
24
|
+
than from an install, the others are published alongside it, by those names, and each skill's
|
|
25
|
+
manifest lists its files. Read each file in full: a fetch tool that summarizes drops the exact
|
|
26
|
+
calls and shapes; a tool that returns the file's text, or one that runs code and can fetch a URL,
|
|
27
|
+
does not.
|
|
28
|
+
|
|
29
|
+
Two things such a run does not produce, and never claims: a frontend built from the shipped `app/`
|
|
30
|
+
code (that needs a build), and a release of one. Everything else the kit does to a site, it does
|
|
31
|
+
through calls you can make.
|
|
32
|
+
|
|
33
|
+
## 0. Identity
|
|
34
|
+
|
|
35
|
+
Every call below runs as the site owner, with whatever identity your host gives you: the Wix
|
|
36
|
+
MCP's `ExecuteWixAPI` carries the user's login; an API key from the user's account goes in the
|
|
37
|
+
`Authorization` header of any call, as `<MANAGE>/SKILL.md` describes; a site token from the Wix CLI
|
|
38
|
+
works the same way when the user runs the CLI for you. The site's OAuth app (`client_id`) mints
|
|
39
|
+
visitor tokens only, which cover what a visitor may see and nothing here.
|
|
40
|
+
|
|
41
|
+
## 1. The brief → the solutions
|
|
42
|
+
|
|
43
|
+
Unchanged from SKILL.md step 2 and its Verticals table: the brief names the business, the table
|
|
44
|
+
names the solutions it needs. Read each solution's `<TEMPLATES>/<solution>/INSTRUCTIONS.md` for
|
|
45
|
+
what it covers and `<TEMPLATES>/<solution>/seed/SEED.md` for what a seeded site holds.
|
|
46
|
+
|
|
47
|
+
## 2. The site
|
|
48
|
+
|
|
49
|
+
The CLI's `npm create @wix/new` is one call: `POST /headless-business-setup/v1/headless-business/provision`,
|
|
50
|
+
with `origin: agent-<your-id>`, a name, and the solutions to install as `seedOptions`. The recipe
|
|
51
|
+
is `<MANAGE>/references/sites/create-headless-site.md`; it returns the `metaSiteId` and the
|
|
52
|
+
site's OAuth client `appId`. Do not create a site any other way: a project from Create Project or
|
|
53
|
+
a template is neither headless nor publishable, and a later run with a machine cannot attach to it.
|
|
54
|
+
|
|
55
|
+
A site that already exists (the brief names it) is read first, never seeded unasked: SKILL.md
|
|
56
|
+
step 2 and `<SKILL_ROOT>/guides/existing-site.md` apply as written.
|
|
57
|
+
|
|
58
|
+
## 3. The apps
|
|
59
|
+
|
|
60
|
+
A solution the provision call did not install, or a capability's app (Members Area, Forms,
|
|
61
|
+
Restaurants, Donations, FAQ), goes on with the Apps Installer:
|
|
62
|
+
`<MANAGE>/references/app-installation/install-wix-apps.md`. The app ids are in each
|
|
63
|
+
`seed-<solution>.mjs`, which installs them before seeding.
|
|
64
|
+
|
|
65
|
+
## 4. The seed
|
|
66
|
+
|
|
67
|
+
`seed-<solution>.mjs` is a sequence of REST calls, and `SEED.md` is its contract: the plan shape
|
|
68
|
+
(what to create, how many, which images), the order the entities need, and the traps the script
|
|
69
|
+
encodes (a Bookings service needs a category to be visible and takes resource ids, not staff ids;
|
|
70
|
+
a Forms field is registered by its `validation` block; a product's choice photos are linked after
|
|
71
|
+
the gallery holds them). Read `SEED.md`, read the script for any call `SEED.md` only names, and make the same
|
|
72
|
+
calls through `<MANAGE>`'s recipe for each (services, products, posts, events, collections). Keep
|
|
73
|
+
the script's rules: create, never delete, and report what the site already held. Image prompts
|
|
74
|
+
need the Media Manager; without it, products and services stay text-only, say so.
|
|
75
|
+
|
|
76
|
+
## 5. The frontend
|
|
77
|
+
|
|
78
|
+
The shipped `app/` code cannot be built here, and the Astro pages cannot be released. A page can
|
|
79
|
+
still be shipped: one self-contained `index.html` that loads the Wix SDK from a package CDN and
|
|
80
|
+
talks to the site as a visitor, dropped onto the site from step 2 through
|
|
81
|
+
`<MANAGE>/references/sites/upload-static-site.md`. The kit's own frontend for this site is a run
|
|
82
|
+
with a machine, which attaches to this site (`guides/existing-site.md`) rather than making a new one.
|
|
83
|
+
|
|
84
|
+
**The page.** One file, a few hundred lines, written in full before the first drop: the drop takes
|
|
85
|
+
the complete file set in one call, and a response has a hard size limit, so there is no second
|
|
86
|
+
pass. The SDK comes from a pinned package URL, `https://esm.sh/@wix/sdk@<version>` and the
|
|
87
|
+
solution's package (`@wix/bookings`, `@wix/stores`, `@wix/blog`, …); the client is
|
|
88
|
+
`createClient({ modules: { … }, auth: OAuthStrategy({ clientId }) })` with the `appId` the provision
|
|
89
|
+
call returned, and the SDK mints and refreshes the visitor token itself. Markup, a render function
|
|
90
|
+
per state, the event handlers: that is what you write. Keep what the page shows to what the site
|
|
91
|
+
holds; nothing invented, as in SKILL.md step 4.
|
|
92
|
+
|
|
93
|
+
**The calls.** Take every SDK call's shape from the kit's own transport for the solution,
|
|
94
|
+
`<TEMPLATES>/<solution>/app/wix/<solution>/*.ts` (`services.ts`, `booking.ts`, `catalog.ts`, …,
|
|
95
|
+
with `<TEMPLATES>/shared/app/wix/sdk.ts` for the client wiring): those files call the same SDK
|
|
96
|
+
modules the page imports, and they are verified. Copy the form of the call, not the file. The
|
|
97
|
+
cores beside them (`*-core.ts`) hold the rules the page needs and nothing else does: which fields
|
|
98
|
+
to read, how a price or a duration is formatted, which slot is bookable, how an id is read.
|
|
99
|
+
|
|
100
|
+
**A call those files don't cover** is confirmed before it is written, the way `<DOCS>` describes:
|
|
101
|
+
the method's SDK page, and when the page and the package disagree, the package's own type
|
|
102
|
+
declarations (the `@wix/auto_sdk_<solution>_<module>` package the solution package depends on,
|
|
103
|
+
its `index.d.ts`) are the truth. Two shapes that recur, as examples of what the types settle and
|
|
104
|
+
the kit's files already encode: SDK query methods return a query builder finished with `.find()`;
|
|
105
|
+
and an entity's id arrives as `_id` on some objects and `id` on others (the kit reads both,
|
|
106
|
+
`rawId`). Dates travel as local
|
|
107
|
+
wall-clock strings in the business time zone. A page cannot be run here, so a wrong shape fails
|
|
108
|
+
silently in the browser; the file beside the skill is the check that is available.
|
|
109
|
+
|
|
110
|
+
## 6. Closing
|
|
111
|
+
|
|
112
|
+
As in SKILL.md step 5, minus the live URL when nothing was released: what exists on the site, by
|
|
113
|
+
name; the dashboard link `https://manage.wix.com/dashboard/<metaSiteId>` and each solution's own
|
|
114
|
+
pages from its INSTRUCTIONS.md table; what is placeholder; payments, when the site takes money;
|
|
115
|
+
and what this run could not do, with the site id, so a run with a machine continues here.
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
// business instead.
|
|
45
45
|
//
|
|
46
46
|
// Emits ONE JSON event per line (attached, scaffolded, deployed, install_started,
|
|
47
|
-
// ready_for_brand_layer, or error). Requires a logged-in Wix CLI (`npx @wix/cli
|
|
47
|
+
// ready_for_brand_layer, or error). Requires a logged-in Wix CLI (`npx @wix/cli@<pinned> whoami`, the version in pins.json)
|
|
48
48
|
// whose account owns or co-manages the site.
|
|
49
49
|
import { spawn, spawnSync, execFileSync } from "node:child_process";
|
|
50
50
|
import { cpSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
@@ -53,6 +53,8 @@ import { fileURLToPath } from "node:url";
|
|
|
53
53
|
import { writeAgentsMd } from "./agents-md.mjs";
|
|
54
54
|
import { frontendPresent, siteContext } from "./context.mjs";
|
|
55
55
|
import { listVerticals, templatesDir } from "./templates.mjs";
|
|
56
|
+
const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
|
|
57
|
+
const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
|
|
56
58
|
|
|
57
59
|
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
58
60
|
const MANAGE = "https://manage.wix.com";
|
|
@@ -135,9 +137,9 @@ emit("folder", { mode, stack, hosting: hosting0, project: hasProject, config: cw
|
|
|
135
137
|
|
|
136
138
|
// ---- http ---------------------------------------------------------------------------------------
|
|
137
139
|
const cliToken = (site) => {
|
|
138
|
-
const r = spawnSync("npx", ["-y",
|
|
140
|
+
const r = spawnSync("npx", ["-y", WIX_CLI, "token", ...(site ? ["--site", site] : [])], { encoding: "utf8", timeout: 120_000 });
|
|
139
141
|
const t = (r.stdout || "").trim();
|
|
140
|
-
if (r.status !== 0 || !t) fail("auth", (r.stderr || r.stdout ||
|
|
142
|
+
if (r.status !== 0 || !t) fail("auth", (r.stderr || r.stdout || `no token — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-400));
|
|
141
143
|
return t;
|
|
142
144
|
};
|
|
143
145
|
async function call(base, path, { method = "POST", token, site, body, query } = {}) {
|
|
@@ -3,6 +3,8 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
|
|
7
|
+
const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
|
|
6
8
|
|
|
7
9
|
// ── tiny event protocol (one JSON object per line) ───────────────────────────
|
|
8
10
|
const emit = (event, extra = {}) =>
|
|
@@ -15,7 +17,7 @@ const fail = (event, extra = {}) => {
|
|
|
15
17
|
// ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────
|
|
16
18
|
const isWin = process.platform === 'win32';
|
|
17
19
|
const bin = (name) => (isWin ? `${name}.cmd` : name);
|
|
18
|
-
const WIX = [bin('npx'), '-y',
|
|
20
|
+
const WIX = [bin('npx'), '-y', WIX_CLI]; // run the CLI via npx — no global install/mutation
|
|
19
21
|
|
|
20
22
|
// Force the CLI into non-interactive "agent" mode. Without an agent signal in
|
|
21
23
|
// the env, `wix login` renders an interactive Ink TUI (device code + keypress)
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import { spawnSync } from "node:child_process";
|
|
20
20
|
import { existsSync, readFileSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
+
const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
|
|
23
|
+
const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
|
|
22
24
|
|
|
23
25
|
// The variables the editor-to-headless migration writes into the child project's `prod`
|
|
24
26
|
// environment (headless-bo, editorMigrations/environmentMetadata.ts), which `wix env pull` copies
|
|
@@ -57,7 +59,7 @@ export function readWixConfig(cwd) {
|
|
|
57
59
|
try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; }
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
const ENV_PULL = ["-y",
|
|
62
|
+
const ENV_PULL = ["-y", WIX_CLI, "env", "pull"];
|
|
61
63
|
const runPull = (dir) => spawnSync("npx", ENV_PULL, { cwd: dir, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 180_000 });
|
|
62
64
|
|
|
63
65
|
/**
|
|
@@ -70,7 +72,7 @@ export function pullEnv(cwd) {
|
|
|
70
72
|
const envFile = join(cwd, ".env.local");
|
|
71
73
|
const r = runPull(cwd);
|
|
72
74
|
if (r.status === 0 && existsSync(envFile)) return { ok: true, via: "in place" };
|
|
73
|
-
return { ok: false, error: (r.stderr || r.stdout ||
|
|
75
|
+
return { ok: false, error: (r.stderr || r.stdout || `env pull produced no .env.local — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).trim().slice(-400) };
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/**
|
|
@@ -46,6 +46,8 @@ import { fileURLToPath } from "node:url";
|
|
|
46
46
|
import { templatesDir } from "./templates.mjs";
|
|
47
47
|
import { syncLockRoot } from "./lock.mjs";
|
|
48
48
|
import { siteContext } from "./context.mjs";
|
|
49
|
+
const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
|
|
50
|
+
const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
|
|
49
51
|
|
|
50
52
|
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
51
53
|
// The shipped code: the repository's templates/ (a checkout, the cache, or fetched now).
|
|
@@ -275,7 +277,7 @@ if (stack === "static") {
|
|
|
275
277
|
// .env.local (`wix env pull`; on a migration preview the parent site's app), else the config's appId.
|
|
276
278
|
let clientId = clientIdFlag ?? (existsSync(join(PROJECT, "wix.config.json")) ? siteContext({ cwd: PROJECT }).content.clientId : null);
|
|
277
279
|
if (!clientId) {
|
|
278
|
-
console.log(JSON.stringify({ error:
|
|
280
|
+
console.log(JSON.stringify({ error: `static stack needs the public OAuth client id — run \`npm create @wix/new@${PINS["@wix/create-new"]} init\` here first, or pass --client-id` }));
|
|
279
281
|
process.exit(1);
|
|
280
282
|
}
|
|
281
283
|
const missing = requested.filter((v) => !existsSync(join(REF, v, "rest")));
|
|
@@ -336,7 +338,7 @@ if (stack === "static") {
|
|
|
336
338
|
const sources = readdirSync(JS).filter((f) => f.endsWith(".ts")).map((f) => join(JS, f));
|
|
337
339
|
const tsc = spawnSync(
|
|
338
340
|
"npx",
|
|
339
|
-
["-y", "-p",
|
|
341
|
+
["-y", "-p", `typescript@${PINS.typescript}`, "tsc", ...sources, "--outDir", JS, "--module", "esnext", "--target", "es2022",
|
|
340
342
|
"--moduleResolution", "bundler", "--lib", "es2022,dom", "--strict", "--skipLibCheck", "--removeComments", "false"],
|
|
341
343
|
{ encoding: "utf8", timeout: 180_000 },
|
|
342
344
|
);
|
|
@@ -37,6 +37,8 @@ import { fileURLToPath } from "node:url";
|
|
|
37
37
|
import { AGENT_CONFIG_FILES, writeAgentsMd } from "./agents-md.mjs";
|
|
38
38
|
import { listVerticals, templatesDir } from "./templates.mjs";
|
|
39
39
|
import { folderShape, siteContext } from "./context.mjs";
|
|
40
|
+
const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
|
|
41
|
+
const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
|
|
40
42
|
|
|
41
43
|
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
42
44
|
|
|
@@ -134,14 +136,14 @@ if (mode === "create") {
|
|
|
134
136
|
// commit" is noise here, and becomes a nested-repo (submodule gitlink) hazard
|
|
135
137
|
// if the project is later placed inside an existing repo. --skip-install for the
|
|
136
138
|
// same reason: deps install in a detached step below.
|
|
137
|
-
["create", "@wix/new
|
|
139
|
+
["create", `@wix/new@${PINS["@wix/create-new"]}`, "--", "headless",
|
|
138
140
|
"--folder-name", folderName, "--business-name", businessName,
|
|
139
141
|
"--site-template", "blank", ...(template ? ["--template-path", template] : []),
|
|
140
142
|
"--skip-install", "--skip-git", "--no-publish"],
|
|
141
143
|
{ env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 },
|
|
142
144
|
);
|
|
143
145
|
if (scaffold.status !== 0 || !existsSync(join(projectDir, "wix.config.json"))) {
|
|
144
|
-
fail("scaffold", (scaffold.stderr || scaffold.stdout ||
|
|
146
|
+
fail("scaffold", (scaffold.stderr || scaffold.stdout || `scaffold produced no wix.config.json — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-600));
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
} else if (mode === "migrate") {
|
|
@@ -177,10 +179,10 @@ if (mode === "create") {
|
|
|
177
179
|
// wix.config.json and .env.local, touches nothing else. Non-interactive under CI=1; the site is
|
|
178
180
|
// named after the folder (rename it in the dashboard).
|
|
179
181
|
emit("adopting", { folder: cwd });
|
|
180
|
-
const init = spawnSync("npm", ["create", "@wix/new
|
|
182
|
+
const init = spawnSync("npm", ["create", `@wix/new@${PINS["@wix/create-new"]}`, "--", "init"],
|
|
181
183
|
{ cwd, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 });
|
|
182
184
|
if (init.status !== 0 || !has("wix.config.json")) {
|
|
183
|
-
fail("init", (init.stderr || init.stdout ||
|
|
185
|
+
fail("init", (init.stderr || init.stdout || `init produced no wix.config.json — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-600));
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
188
|
const wixConfig = JSON.parse(readFileSync(join(projectDir, "wix.config.json"), "utf8"));
|
|
@@ -296,10 +298,10 @@ if (planPath && madeTheSite) {
|
|
|
296
298
|
|
|
297
299
|
// ---- done ----------------------------------------------------------------------------------------
|
|
298
300
|
const release = {
|
|
299
|
-
astro:
|
|
300
|
-
react:
|
|
301
|
-
lib:
|
|
302
|
-
static: `npx
|
|
301
|
+
astro: `npx ${WIX_CLI} build, then npx ${WIX_CLI} release`,
|
|
302
|
+
react: `the project's own build, then npx ${WIX_CLI} release of the build folder named in wix.config.json (what Wix hosting serves and how routes must be shaped: SKILL.md step 1)`,
|
|
303
|
+
lib: `the project's own build, then npx ${WIX_CLI} release of the build folder named in wix.config.json (SKILL.md step 1)`,
|
|
304
|
+
static: `npx ${WIX_CLI} release — no build; it uploads ${STATIC_OUT}/ whole (wix.config.json site.outputDirectory), so the pages, styles and assets move into ${STATIC_OUT}/ first and import the modules from ./js/wix/ there; the root keeps the config, the plan, the seed output and the skills`,
|
|
303
305
|
}[stack];
|
|
304
306
|
// On a migration preview the links and `siteId` are the PARENT's — the site whose content the pages
|
|
305
307
|
// show and whose dashboard manages it; `deploySiteId` is where `wix release` goes.
|
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
// 2. the cache `<SKILL_ROOT>/templates/`, filled by an earlier call (`--refresh` refetches).
|
|
12
12
|
// 3. a fetch: a sparse, shallow clone of `skills/wix-headless-templates/` from the repository the skill was installed
|
|
13
13
|
// from (skills-lock.json's `source`, default wix/skills), at the branch or tag the install
|
|
14
|
-
// named (its `ref`, falling back to the default branch when that ref no longer exists)
|
|
15
|
-
// repository's default branch
|
|
16
|
-
//
|
|
14
|
+
// named (its `ref`, falling back to the default branch when that ref no longer exists), else
|
|
15
|
+
// the repository's default branch: a skill installed from the repository tracks the repository,
|
|
16
|
+
// so the templates match the kit beside them. A kit that came from a package (no skills-lock.json
|
|
17
|
+
// above it) fetches at the release tag in `install/pins.json` (`templates.ref`, kept equal to the
|
|
18
|
+
// package version by the release flow), the code it was released with; the package carries the
|
|
19
|
+
// templates skill beside the kit anyway, so that fetch is the rare path.
|
|
20
|
+
// `WIX_HEADLESS_FAST_TEMPLATES_REF=<branch|tag|sha>` overrides all of it.
|
|
17
21
|
// The cache stays with the project: its `.gitignore` leaves out only the composed `project/`
|
|
18
22
|
// folders (the scaffolds with their lockfiles, read once, at create or attach) and the repository
|
|
19
23
|
// tooling, so the code layers, playbooks, seeds and readers are committed at the commit the project
|
|
@@ -29,6 +33,8 @@ export const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
|
29
33
|
export const DEFAULT_REPO = "https://github.com/wix/skills.git";
|
|
30
34
|
/** The templates skill's folder in the repository. */
|
|
31
35
|
export const TEMPLATES_PATH = "skills/wix-headless-templates";
|
|
36
|
+
/** Pinned versions, shared by the install scripts; `templates.ref` is the release tag the fetch defaults to. */
|
|
37
|
+
export const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
|
|
32
38
|
|
|
33
39
|
const git = (args, opts = {}) => spawnSync("git", args, { encoding: "utf8", timeout: 180_000, ...opts });
|
|
34
40
|
|
|
@@ -46,7 +52,7 @@ export function installSource() {
|
|
|
46
52
|
if (typeof entry?.source === "string" && entry.source) {
|
|
47
53
|
const parsed = parseSource(entry.source);
|
|
48
54
|
// `ref` is the branch or tag the install named (skills-lock.json v1 keeps it beside `source`)
|
|
49
|
-
return { ...parsed, ref: parsed.ref ?? (typeof entry.ref === "string" && entry.ref ? entry.ref : null) };
|
|
55
|
+
return { ...parsed, ref: parsed.ref ?? (typeof entry.ref === "string" && entry.ref ? entry.ref : null), fromLock: true };
|
|
50
56
|
}
|
|
51
57
|
} catch { /* fall through to the default */ }
|
|
52
58
|
break;
|
|
@@ -55,7 +61,7 @@ export function installSource() {
|
|
|
55
61
|
if (up === dir) break;
|
|
56
62
|
dir = up;
|
|
57
63
|
}
|
|
58
|
-
return { repo: DEFAULT_REPO, ref: null };
|
|
64
|
+
return { repo: DEFAULT_REPO, ref: null, fromLock: false };
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
function parseSource(src) {
|
|
@@ -112,15 +118,19 @@ export function templatesSource(dir) {
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
function fetchTemplates(cache) {
|
|
115
|
-
const { repo, ref: lockRef } = installSource();
|
|
121
|
+
const { repo, ref: lockRef, fromLock } = installSource();
|
|
116
122
|
const envRef = process.env.WIX_HEADLESS_FAST_TEMPLATES_REF || null;
|
|
117
|
-
|
|
123
|
+
// Installed from the repository (a lock above the skill) and no ref named: the default branch, so
|
|
124
|
+
// the templates match the kit. No lock at all is a package install: the release tag it shipped with.
|
|
125
|
+
const pinnedRef = fromLock ? null : (PINS.templates?.ref || null);
|
|
126
|
+
let ref = envRef || lockRef || pinnedRef || null;
|
|
118
127
|
let r = cloneSparse(repo, ref);
|
|
119
128
|
// The branch the skill was installed from can be gone by the time the code is first needed
|
|
120
129
|
// (merged and deleted). The lock's ref then falls back to the default branch; an explicit env
|
|
121
130
|
// ref does not.
|
|
122
131
|
let fellBack = false;
|
|
123
132
|
if (r.status !== 0 && ref && !envRef && /not found|couldn't find remote ref|unknown revision/i.test(r.stderr || "")) {
|
|
133
|
+
// (the pinned tag, too: a fork or a pre-release checkout may not carry it)
|
|
124
134
|
rmSync(r.tmp, { recursive: true, force: true });
|
|
125
135
|
fellBack = true; ref = null;
|
|
126
136
|
r = cloneSparse(repo, null);
|
|
@@ -141,7 +151,7 @@ function fetchTemplates(cache) {
|
|
|
141
151
|
catch { cpSync(join(tmp, TEMPLATES_PATH), cache, { recursive: true }); }
|
|
142
152
|
rmSync(tmp, { recursive: true, force: true });
|
|
143
153
|
writeFileSync(join(cache, ".gitignore"), IGNORED.join("\n") + "\n");
|
|
144
|
-
writeFileSync(join(cache, ".source"), JSON.stringify({ repo, ref: ref ?? (fellBack ? `default branch (${lockRef} not found)` : "default branch"), commit, fetchedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
154
|
+
writeFileSync(join(cache, ".source"), JSON.stringify({ repo, ref: ref ?? (fellBack ? `default branch (${lockRef || pinnedRef} not found)` : "default branch"), commit, fetchedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
// A sparse, shallow clone of the templates skill's folder at a ref (branch, tag, or commit id) into a temp dir.
|
|
@@ -6,8 +6,9 @@ typed end-to-end. **The presentation is yours**: you design and implement the ca
|
|
|
6
6
|
campaigns index, the campaign page's layout around the shipped form, the home page and the brand. You
|
|
7
7
|
never write checkout logic; you never skip designing.
|
|
8
8
|
|
|
9
|
-
A donation is an eCom
|
|
10
|
-
|
|
9
|
+
A donation is an eCom cart (Cart V2) with one line that references the campaign; in V2 the cart IS the
|
|
10
|
+
checkout, so the cart's id is the id the redirect session takes — there is no separate checkout entity.
|
|
11
|
+
Wix's hosted checkout takes the payment and the donor's details, and the eCom order is the receipt. The campaign entity carries
|
|
11
12
|
**no description and no slug**: the story on a campaign page is the site's own copy, and routes key by
|
|
12
13
|
the campaign id (`/donate/<id>`).
|
|
13
14
|
|
|
@@ -29,7 +30,7 @@ Astro markup), your campaign page layout, and your home page.
|
|
|
29
30
|
| `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes, ratio)` — every `<img>` attribute for a DTO image: `<img {...imgAttrs(c.imageUrl, "50vw", 9 / 16)} alt={c.name} />`; `imgSrc()` / `imgSrcSet()` underneath |
|
|
30
31
|
| `wix/donations/types.ts` | the DTOs (`CampaignSummary`, `CampaignDetail`, `GoalProgress`, `DonationOptions`, `DonationReceipt`) — contracts inlined below |
|
|
31
32
|
| `wix/donations/campaigns.ts` | `fetchCampaigns`, `fetchCampaign(id)`, `fetchDefaultCampaign` — the transport; the rules and DTO mappers are in `donations-core.ts` beside it (shared with the REST layer) |
|
|
32
|
-
| `wix/donations/donate.ts` | `donationCheckoutUrl(campaignId, input)` — the
|
|
33
|
+
| `wix/donations/donate.ts` | `donationCheckoutUrl(campaignId, input)` — the donation cart + hosted redirect session; `fetchDonationReceipt(orderId)` — the thank-you page's order read; bodies in `donations-core.ts` |
|
|
33
34
|
| `wix/donations/donation-store.ts` · `campaigns-store.ts` · `receipt-store.ts` | the form, listing and receipt state machines, framework-free (`createDonationStore(campaign)`, `createCampaignsStore()`, `createReceiptStore()` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
|
|
34
35
|
| `hooks/donations/useDonation.ts` | React binding of `donation-store.ts`: the donate form's state and actions — contract below |
|
|
35
36
|
| `hooks/donations/useCampaigns.ts` | React binding of `campaigns-store.ts`: the listing — contract below |
|
|
@@ -328,12 +329,10 @@ first real site and read the owning file if one bites.
|
|
|
328
329
|
shows) and `options.currency` "" (the button reads "Donate" without an amount; preset labels still
|
|
329
330
|
carry the API's own formatted amounts). If the campaign query itself is refused, the pages render
|
|
330
331
|
their empty state.
|
|
331
|
-
- **
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
can answer `CURRENT_CART_ALREADY_EXISTS` for a visitor who already holds a cart — surfaced as the
|
|
336
|
-
form's error.
|
|
332
|
+
- **Create Cart is the whole path (Cart V2).** `donationCheckoutUrl` creates one fresh cart with the
|
|
333
|
+
donation line (`cart.source.channelType: WEB`, the donor note as `cart.note`) and hands that cart's
|
|
334
|
+
id straight to the redirect session — the cart is the checkout, so there is no separate Create
|
|
335
|
+
Checkout call and nothing merges into a storefront visitor's current cart.
|
|
337
336
|
- **Site currency.** Comes from the metrics response ("returns only the site's default currency");
|
|
338
337
|
On a campaign nobody has donated to yet the metrics carry no currency either, so the transports read the site's currency once from the eCommerce settings (BUSINESS_INFO); unknown → "".
|
|
339
338
|
- **Reading the order on the thank-you page.** `GET /ecom/v1/orders/{id}` with the visitor's token is
|
|
@@ -1,29 +1,19 @@
|
|
|
1
|
-
// The donation flow over the SDK: a visitor's choice → an eCom
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// createDonationCheckout — Create Checkout with the line (a donation never merges into a
|
|
9
|
-
// storefront visitor's current cart). The default.
|
|
10
|
-
// createDonationCheckoutViaCart — the Wix widget's own sequence: Create Cart → Create Checkout
|
|
11
|
-
// from the cart, channelType OTHER_PLATFORM. The fallback when
|
|
12
|
-
// the direct route is refused; also reachable by name.
|
|
13
|
-
// Whether the Donations catalog plugin prices a line created directly is not yet verified live,
|
|
14
|
-
// hence the automatic fallback.
|
|
15
|
-
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/checkout/create-checkout.md
|
|
1
|
+
// The donation flow over the SDK: a visitor's choice → an eCom cart with one donation line → the
|
|
2
|
+
// Wix-hosted checkout URL; and the thank-you page's order read. In Cart V2 there is no separate
|
|
3
|
+
// checkout entity: the cart IS the checkout, so its id is the id the redirect session takes.
|
|
4
|
+
// Payment, donor details and receipts are Wix's hosted checkout and the eCom order — nothing here
|
|
5
|
+
// creates an order or builds a URL by hand. Bodies and result readers live in ./donations-core
|
|
6
|
+
// (shared with the REST twin); this file is the transport only. Copy as-is.
|
|
7
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
|
|
16
8
|
// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
|
|
17
9
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/orders/get-order.md
|
|
18
|
-
import {
|
|
10
|
+
import { cartV2 as cartV2Module, orders as ordersModule } from "@wix/ecom";
|
|
19
11
|
import { redirects as redirectsModule } from "@wix/redirects";
|
|
20
12
|
import { wixModule } from "../sdk";
|
|
21
13
|
import {
|
|
22
14
|
cartBody,
|
|
23
|
-
|
|
15
|
+
cartIdOf,
|
|
24
16
|
checkoutError,
|
|
25
|
-
checkoutIdOf,
|
|
26
|
-
rawId,
|
|
27
17
|
redirectBody,
|
|
28
18
|
redirectUrl,
|
|
29
19
|
toReceipt,
|
|
@@ -34,8 +24,7 @@ import type { DonationInput, DonationReceipt } from "./types";
|
|
|
34
24
|
|
|
35
25
|
export type { DonatePaths };
|
|
36
26
|
|
|
37
|
-
const
|
|
38
|
-
const cartApi = wixModule(cartModule);
|
|
27
|
+
const cartV2Api = wixModule(cartV2Module);
|
|
39
28
|
const ordersApi = wixModule(ordersModule);
|
|
40
29
|
const redirects = wixModule(redirectsModule);
|
|
41
30
|
|
|
@@ -46,37 +35,21 @@ export interface DonateOptions {
|
|
|
46
35
|
paths?: DonatePaths;
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
/** Create Checkout with the donation line → checkout id. */
|
|
50
|
-
export async function createDonationCheckout(campaignId: string, input: DonationInput): Promise<string> {
|
|
51
|
-
const checkout: Raw = await checkoutApi.createCheckout(checkoutBody(campaignId, input) as any);
|
|
52
|
-
return checkoutIdOf(checkout);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** The Wix widget's route: Create Cart (the note as buyerNote) → Create Checkout from the cart → checkout id. */
|
|
56
|
-
export async function createDonationCheckoutViaCart(campaignId: string, input: DonationInput): Promise<string> {
|
|
57
|
-
const cart: Raw = await cartApi.createCart(cartBody(campaignId, input) as any);
|
|
58
|
-
const res: Raw = await cartApi.createCheckout(rawId(cart), { channelType: "OTHER_PLATFORM" });
|
|
59
|
-
return checkoutIdOf(res);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
38
|
/**
|
|
63
39
|
* Start the hosted checkout for a donation; resolves to the URL to navigate the FULL document to.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
40
|
+
* One Create Cart (the cart is the checkout) → one redirect session whose thank-you callback lands
|
|
41
|
+
* on `/donate/thank-you?orderId=`.
|
|
66
42
|
*/
|
|
67
43
|
export async function donationCheckoutUrl(campaignId: string, input: DonationInput, { origin, paths }: DonateOptions = {}): Promise<string> {
|
|
68
44
|
const site = origin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
69
|
-
let
|
|
45
|
+
let cartId: string;
|
|
70
46
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} catch {
|
|
76
|
-
throw checkoutError(direct);
|
|
77
|
-
}
|
|
47
|
+
const cart: Raw = await cartV2Api.createCart(cartBody(campaignId, input) as any);
|
|
48
|
+
cartId = cartIdOf(cart);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
throw checkoutError(e);
|
|
78
51
|
}
|
|
79
|
-
const session: Raw = await redirects.createRedirectSession(redirectBody(
|
|
52
|
+
const session: Raw = await redirects.createRedirectSession(redirectBody(cartId, campaignId, site, paths) as any);
|
|
80
53
|
return redirectUrl(session);
|
|
81
54
|
}
|
|
82
55
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Donation rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./campaigns.ts
|
|
2
2
|
// + ./donate.ts (the SDK, managed Astro and React) and the REST twins in templates/donations/rest/
|
|
3
3
|
// (fetch, a static site or a port to another language). Every rule about amounts, fees, frequency,
|
|
4
|
-
// goal math, validation, the
|
|
4
|
+
// goal math, validation, the cart line and the redirect body lives HERE, once. A raw campaign
|
|
5
5
|
// may come from the SDK (`_id`, coverImage as a `wix:image://` string) or from REST (`id`,
|
|
6
6
|
// coverImage as { id, url }); the mappers accept both. Has its own formatMoney so it stands alone
|
|
7
7
|
// when stripped. Imports are type-only so a strip to JS emits no imports.
|
|
@@ -351,10 +351,10 @@ export function toInput(options: DonationOptions, sel: DonationSelection): Donat
|
|
|
351
351
|
return { amount, frequency: sel.frequency, coverFee: options.askCoverFee && sel.coverFee, note: sel.note.trim() };
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
// ----
|
|
354
|
+
// ---- cart -----------------------------------------------------------------------------------------
|
|
355
355
|
|
|
356
356
|
/**
|
|
357
|
-
* The one
|
|
357
|
+
* The one catalog item a donation cart carries. `amount` is a NUMBER and `frequency` the enum
|
|
358
358
|
* string; `donorCoveringFees` is sent only when the donor opted in (the Donations catalog plugin
|
|
359
359
|
* prices the line from these options — never a customLineItem, never a computed price).
|
|
360
360
|
*/
|
|
@@ -370,27 +370,26 @@ export function donationLineItem(campaignId: string, input: DonationInput): Raw
|
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
/**
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
...(input.note ? { checkoutInfo: { buyerNote: input.note } } : {}),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Create Cart body — the Wix widget's own route (fallback): the note as the cart's buyerNote. */
|
|
373
|
+
/**
|
|
374
|
+
* Create Cart body (Cart V2): the single donation catalog item, the WEB channel on the cart's
|
|
375
|
+
* `source`, the donor note as the cart's `note`. In V2 the created cart IS the checkout — its id is
|
|
376
|
+
* the id the redirect session takes; there is no separate Create Checkout step.
|
|
377
|
+
*/
|
|
383
378
|
export function cartBody(campaignId: string, input: DonationInput): Raw {
|
|
384
379
|
return {
|
|
385
|
-
|
|
386
|
-
|
|
380
|
+
cart: { source: { channelType: "WEB" }, ...(input.note ? { note: input.note } : {}) },
|
|
381
|
+
catalogItems: [donationLineItem(campaignId, input)],
|
|
387
382
|
};
|
|
388
383
|
}
|
|
389
384
|
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
385
|
+
/**
|
|
386
|
+
* The cart id out of the Create Cart response — the id a V2 donation hands the redirect session
|
|
387
|
+
* (the cart is the checkout). The SDK returns a Cart directly (`_id`); REST wraps it as
|
|
388
|
+
* `{ cart: { id } }`.
|
|
389
|
+
*/
|
|
390
|
+
export function cartIdOf(res: Raw | null | undefined): string {
|
|
391
|
+
const id = res?._id ?? res?.id ?? res?.cart?._id ?? res?.cart?.id ?? "";
|
|
392
|
+
if (!id) throw new Error("Checkout couldn't start: no cart id returned.");
|
|
394
393
|
return String(id);
|
|
395
394
|
}
|
|
396
395
|
|
|
@@ -1,29 +1,19 @@
|
|
|
1
|
-
// The donation flow over the SDK: a visitor's choice → an eCom
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// createDonationCheckout — Create Checkout with the line (a donation never merges into a
|
|
9
|
-
// storefront visitor's current cart). The default.
|
|
10
|
-
// createDonationCheckoutViaCart — the Wix widget's own sequence: Create Cart → Create Checkout
|
|
11
|
-
// from the cart, channelType OTHER_PLATFORM. The fallback when
|
|
12
|
-
// the direct route is refused; also reachable by name.
|
|
13
|
-
// Whether the Donations catalog plugin prices a line created directly is not yet verified live,
|
|
14
|
-
// hence the automatic fallback.
|
|
15
|
-
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/checkout/create-checkout.md
|
|
1
|
+
// The donation flow over the SDK: a visitor's choice → an eCom cart with one donation line → the
|
|
2
|
+
// Wix-hosted checkout URL; and the thank-you page's order read. In Cart V2 there is no separate
|
|
3
|
+
// checkout entity: the cart IS the checkout, so its id is the id the redirect session takes.
|
|
4
|
+
// Payment, donor details and receipts are Wix's hosted checkout and the eCom order — nothing here
|
|
5
|
+
// creates an order or builds a URL by hand. Bodies and result readers live in ./donations-core
|
|
6
|
+
// (shared with the REST twin); this file is the transport only. Copy as-is.
|
|
7
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
|
|
16
8
|
// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
|
|
17
9
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/orders/get-order.md
|
|
18
|
-
import {
|
|
10
|
+
import { cartV2 as cartV2Module, orders as ordersModule } from "@wix/ecom";
|
|
19
11
|
import { redirects as redirectsModule } from "@wix/redirects";
|
|
20
12
|
import { wixModule } from "../sdk";
|
|
21
13
|
import {
|
|
22
14
|
cartBody,
|
|
23
|
-
|
|
15
|
+
cartIdOf,
|
|
24
16
|
checkoutError,
|
|
25
|
-
checkoutIdOf,
|
|
26
|
-
rawId,
|
|
27
17
|
redirectBody,
|
|
28
18
|
redirectUrl,
|
|
29
19
|
toReceipt,
|
|
@@ -34,8 +24,7 @@ import type { DonationInput, DonationReceipt } from "./types";
|
|
|
34
24
|
|
|
35
25
|
export type { DonatePaths };
|
|
36
26
|
|
|
37
|
-
const
|
|
38
|
-
const cartApi = wixModule(cartModule);
|
|
27
|
+
const cartV2Api = wixModule(cartV2Module);
|
|
39
28
|
const ordersApi = wixModule(ordersModule);
|
|
40
29
|
const redirects = wixModule(redirectsModule);
|
|
41
30
|
|
|
@@ -46,37 +35,21 @@ export interface DonateOptions {
|
|
|
46
35
|
paths?: DonatePaths;
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
/** Create Checkout with the donation line → checkout id. */
|
|
50
|
-
export async function createDonationCheckout(campaignId: string, input: DonationInput): Promise<string> {
|
|
51
|
-
const checkout: Raw = await checkoutApi.createCheckout(checkoutBody(campaignId, input) as any);
|
|
52
|
-
return checkoutIdOf(checkout);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** The Wix widget's route: Create Cart (the note as buyerNote) → Create Checkout from the cart → checkout id. */
|
|
56
|
-
export async function createDonationCheckoutViaCart(campaignId: string, input: DonationInput): Promise<string> {
|
|
57
|
-
const cart: Raw = await cartApi.createCart(cartBody(campaignId, input) as any);
|
|
58
|
-
const res: Raw = await cartApi.createCheckout(rawId(cart), { channelType: "OTHER_PLATFORM" });
|
|
59
|
-
return checkoutIdOf(res);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
38
|
/**
|
|
63
39
|
* Start the hosted checkout for a donation; resolves to the URL to navigate the FULL document to.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
40
|
+
* One Create Cart (the cart is the checkout) → one redirect session whose thank-you callback lands
|
|
41
|
+
* on `/donate/thank-you?orderId=`.
|
|
66
42
|
*/
|
|
67
43
|
export async function donationCheckoutUrl(campaignId: string, input: DonationInput, { origin, paths }: DonateOptions = {}): Promise<string> {
|
|
68
44
|
const site = origin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
69
|
-
let
|
|
45
|
+
let cartId: string;
|
|
70
46
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} catch {
|
|
76
|
-
throw checkoutError(direct);
|
|
77
|
-
}
|
|
47
|
+
const cart: Raw = await cartV2Api.createCart(cartBody(campaignId, input) as any);
|
|
48
|
+
cartId = cartIdOf(cart);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
throw checkoutError(e);
|
|
78
51
|
}
|
|
79
|
-
const session: Raw = await redirects.createRedirectSession(redirectBody(
|
|
52
|
+
const session: Raw = await redirects.createRedirectSession(redirectBody(cartId, campaignId, site, paths) as any);
|
|
80
53
|
return redirectUrl(session);
|
|
81
54
|
}
|
|
82
55
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Donation rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./campaigns.ts
|
|
2
2
|
// + ./donate.ts (the SDK, managed Astro and React) and the REST twins in templates/donations/rest/
|
|
3
3
|
// (fetch, a static site or a port to another language). Every rule about amounts, fees, frequency,
|
|
4
|
-
// goal math, validation, the
|
|
4
|
+
// goal math, validation, the cart line and the redirect body lives HERE, once. A raw campaign
|
|
5
5
|
// may come from the SDK (`_id`, coverImage as a `wix:image://` string) or from REST (`id`,
|
|
6
6
|
// coverImage as { id, url }); the mappers accept both. Has its own formatMoney so it stands alone
|
|
7
7
|
// when stripped. Imports are type-only so a strip to JS emits no imports.
|
|
@@ -351,10 +351,10 @@ export function toInput(options: DonationOptions, sel: DonationSelection): Donat
|
|
|
351
351
|
return { amount, frequency: sel.frequency, coverFee: options.askCoverFee && sel.coverFee, note: sel.note.trim() };
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
// ----
|
|
354
|
+
// ---- cart -----------------------------------------------------------------------------------------
|
|
355
355
|
|
|
356
356
|
/**
|
|
357
|
-
* The one
|
|
357
|
+
* The one catalog item a donation cart carries. `amount` is a NUMBER and `frequency` the enum
|
|
358
358
|
* string; `donorCoveringFees` is sent only when the donor opted in (the Donations catalog plugin
|
|
359
359
|
* prices the line from these options — never a customLineItem, never a computed price).
|
|
360
360
|
*/
|
|
@@ -370,27 +370,26 @@ export function donationLineItem(campaignId: string, input: DonationInput): Raw
|
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
/**
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
...(input.note ? { checkoutInfo: { buyerNote: input.note } } : {}),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Create Cart body — the Wix widget's own route (fallback): the note as the cart's buyerNote. */
|
|
373
|
+
/**
|
|
374
|
+
* Create Cart body (Cart V2): the single donation catalog item, the WEB channel on the cart's
|
|
375
|
+
* `source`, the donor note as the cart's `note`. In V2 the created cart IS the checkout — its id is
|
|
376
|
+
* the id the redirect session takes; there is no separate Create Checkout step.
|
|
377
|
+
*/
|
|
383
378
|
export function cartBody(campaignId: string, input: DonationInput): Raw {
|
|
384
379
|
return {
|
|
385
|
-
|
|
386
|
-
|
|
380
|
+
cart: { source: { channelType: "WEB" }, ...(input.note ? { note: input.note } : {}) },
|
|
381
|
+
catalogItems: [donationLineItem(campaignId, input)],
|
|
387
382
|
};
|
|
388
383
|
}
|
|
389
384
|
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
385
|
+
/**
|
|
386
|
+
* The cart id out of the Create Cart response — the id a V2 donation hands the redirect session
|
|
387
|
+
* (the cart is the checkout). The SDK returns a Cart directly (`_id`); REST wraps it as
|
|
388
|
+
* `{ cart: { id } }`.
|
|
389
|
+
*/
|
|
390
|
+
export function cartIdOf(res: Raw | null | undefined): string {
|
|
391
|
+
const id = res?._id ?? res?.id ?? res?.cart?._id ?? res?.cart?.id ?? "";
|
|
392
|
+
if (!id) throw new Error("Checkout couldn't start: no cart id returned.");
|
|
394
393
|
return String(id);
|
|
395
394
|
}
|
|
396
395
|
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// The donation flow over REST — the twin of app/wix/donations/donate.ts. Same exports, same
|
|
2
2
|
// signatures; the bodies and result readers come from donations-core (the SAME file the SDK
|
|
3
|
-
// transport uses). A donation is an eCom
|
|
4
|
-
// payment and donor details.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/
|
|
3
|
+
// transport uses). A donation is an eCom cart with one catalog line; the hosted checkout takes
|
|
4
|
+
// payment and donor details. In Cart V2 the cart IS the checkout — its id is the id the redirect
|
|
5
|
+
// session takes, so there is one route and no separate Create Checkout call. Never create the
|
|
6
|
+
// order yourself, never hand-build a checkout URL.
|
|
7
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
|
|
8
8
|
// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
|
|
9
9
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/orders/get-order.md
|
|
10
10
|
import { wixRequest } from "./client.js";
|
|
11
11
|
import {
|
|
12
12
|
cartBody,
|
|
13
|
-
|
|
13
|
+
cartIdOf,
|
|
14
14
|
checkoutError,
|
|
15
|
-
checkoutIdOf,
|
|
16
|
-
rawId,
|
|
17
15
|
redirectBody,
|
|
18
16
|
redirectUrl,
|
|
19
17
|
toReceipt,
|
|
@@ -31,46 +29,22 @@ export interface DonateOptions {
|
|
|
31
29
|
paths?: DonatePaths;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
/**
|
|
35
|
-
* Create Checkout with the donation line → checkout id.
|
|
36
|
-
* POST /ecom/v1/checkouts { lineItems: [{ quantity: 1, catalogReference: { appId, catalogItemId, options: { amount, frequency, donorCoveringFees? } } }], channelType: "WEB", checkoutInfo?: { buyerNote } } → { checkout: { id } }
|
|
37
|
-
*/
|
|
38
|
-
export async function createDonationCheckout(campaignId: string, input: DonationInput): Promise<string> {
|
|
39
|
-
const res = await wixRequest<Raw>("/ecom/v1/checkouts", { body: checkoutBody(campaignId, input) });
|
|
40
|
-
return checkoutIdOf(res);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The Wix widget's route → checkout id.
|
|
45
|
-
* POST /ecom/v1/carts { lineItems, cartInfo?: { buyerNote } } → { cart: { id } }
|
|
46
|
-
* POST /ecom/v1/carts/{id}/create-checkout { channelType: "OTHER_PLATFORM" } → { checkoutId }
|
|
47
|
-
*/
|
|
48
|
-
export async function createDonationCheckoutViaCart(campaignId: string, input: DonationInput): Promise<string> {
|
|
49
|
-
const created = await wixRequest<Raw>("/ecom/v1/carts", { body: cartBody(campaignId, input) });
|
|
50
|
-
const cartId = rawId(created?.cart);
|
|
51
|
-
if (!cartId) throw new Error("Checkout couldn't start: no cart id returned.");
|
|
52
|
-
const res = await wixRequest<Raw>(`/ecom/v1/carts/${cartId}/create-checkout`, { body: { channelType: "OTHER_PLATFORM" } });
|
|
53
|
-
return checkoutIdOf(res);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
32
|
/**
|
|
57
33
|
* Start the hosted checkout for a donation; resolves to the URL to navigate the FULL document to.
|
|
58
34
|
* `origin` must be the site's real https origin as registered on the OAuth app's allowed domains.
|
|
35
|
+
* POST /ecom/v2/carts { cart: { source: { channelType: "WEB" }, note? }, catalogItems: [{ quantity: 1, catalogReference: { appId, catalogItemId, options: { amount, frequency, donorCoveringFees? } } }] } → { cart: { id } } (the cart is the checkout)
|
|
59
36
|
* POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } } → { redirectSession: { fullUrl } }
|
|
60
37
|
*/
|
|
61
38
|
export async function donationCheckoutUrl(campaignId: string, input: DonationInput, { origin, paths }: DonateOptions = {}): Promise<string> {
|
|
62
39
|
const site = origin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
63
|
-
let
|
|
40
|
+
let cartId: string;
|
|
64
41
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} catch {
|
|
70
|
-
throw checkoutError(direct);
|
|
71
|
-
}
|
|
42
|
+
const res = await wixRequest<Raw>("/ecom/v2/carts", { body: cartBody(campaignId, input) });
|
|
43
|
+
cartId = cartIdOf(res);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
throw checkoutError(e);
|
|
72
46
|
}
|
|
73
|
-
const session = await wixRequest<Raw>("/headless/v1/redirect-session", { body: redirectBody(
|
|
47
|
+
const session = await wixRequest<Raw>("/headless/v1/redirect-session", { body: redirectBody(cartId, campaignId, site, paths) });
|
|
74
48
|
return redirectUrl(session);
|
|
75
49
|
}
|
|
76
50
|
|
|
@@ -336,8 +336,10 @@ other road — an eCom checkout with one PLAN line item (`catalogItemId: plan.id
|
|
|
336
336
|
must share one checkout with products (a cart holding a membership and a T-shirt). Both end on a
|
|
337
337
|
Wix-hosted checkout with coupons, notes, and login handled there. The DTO carries
|
|
338
338
|
`pricingVariantId` so the eCom path is reachable without touching the DTO if a brief needs it;
|
|
339
|
-
building it means
|
|
340
|
-
`
|
|
339
|
+
building it means adding the plan line (and any product lines) to the current cart with `@wix/ecom`
|
|
340
|
+
`currentCartV2` — in Cart V2 the cart IS the checkout, so the cart's id is the id passed to a
|
|
341
|
+
redirect session with `ecomCheckout: { checkoutId }` — a new function beside `purchase.ts`, not a
|
|
342
|
+
change to it.
|
|
341
343
|
|
|
342
344
|
## Gated content — requires the `members` vertical
|
|
343
345
|
|
|
@@ -29,9 +29,9 @@ if (!siteId) {
|
|
|
29
29
|
console.log(JSON.stringify({ error: "no site: pass --site <siteId> or run in a folder with wix.config.json" }));
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
|
-
const token = execFileSync("npx", ["-y", "@wix/cli@
|
|
32
|
+
const token = execFileSync("npx", ["-y", "@wix/cli@1.1.257", "token", "--site", siteId], { encoding: "utf8" }).trim();
|
|
33
33
|
if (!token) {
|
|
34
|
-
console.log(JSON.stringify({ error: "the Wix CLI returned no token — run `npx @wix/cli@
|
|
34
|
+
console.log(JSON.stringify({ error: "the Wix CLI returned no token — run `npx @wix/cli@1.1.257 login` first" }));
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
try {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// The Wix CLI the seeds call for a site token and for `wix env pull`.
|
|
2
2
|
//
|
|
3
3
|
// Order: the project's own copy (node_modules/.bin/wix, installed with the template's lock), then
|
|
4
|
-
// a `wix` on PATH, then `npx -y @wix/cli
|
|
4
|
+
// a `wix` on PATH, then `npx -y @wix/cli@<pinned>` — which installs the CLI before running it, 20 to
|
|
5
5
|
// 60 s and a wall of npm output on a fresh machine. The first two take under two seconds.
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { delimiter, join } from "node:path";
|
|
8
8
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
9
9
|
|
|
10
|
+
// The CLI version the fallback installs; equal to `@wix/cli` in wix-headless-kit/install/pins.json.
|
|
11
|
+
const WIX_CLI_VERSION = "1.1.257";
|
|
12
|
+
|
|
10
13
|
export function resolveWixCli(cwd = process.cwd()) {
|
|
11
14
|
const bin = process.platform === "win32" ? "wix.cmd" : "wix";
|
|
12
15
|
const local = join(cwd, "node_modules", ".bin", bin);
|
|
@@ -19,7 +22,7 @@ export function resolveWixCli(cwd = process.cwd()) {
|
|
|
19
22
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
20
23
|
if (dir && existsSync(join(dir, bin))) return { file: join(dir, bin), prefix: [] };
|
|
21
24
|
}
|
|
22
|
-
return { file: "npx", prefix: ["-y",
|
|
25
|
+
return { file: "npx", prefix: ["-y", `@wix/cli@${WIX_CLI_VERSION}`] };
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -33,7 +36,7 @@ export function wixToken(siteId, cwd = process.cwd()) {
|
|
|
33
36
|
// An older CLI starts its "update available" box on the token's own line, so a line split is
|
|
34
37
|
// not enough: the token is the first long run of token characters.
|
|
35
38
|
const token = out.match(/[A-Za-z0-9._-]{40,}/)?.[0];
|
|
36
|
-
if (!token) throw new Error(
|
|
39
|
+
if (!token) throw new Error(`The Wix CLI returned no token — run \`wix login\` (or \`npx @wix/cli@${WIX_CLI_VERSION} login\`) first.`);
|
|
37
40
|
return token;
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -167,8 +167,7 @@ export async function setNote(note: string): Promise<Cart> {
|
|
|
167
167
|
/**
|
|
168
168
|
* Start the Wix-hosted checkout for the current cart and return the URL to navigate to.
|
|
169
169
|
* The cart's id IS the checkout id — no separate checkout-creation call (Wix's own storefront
|
|
170
|
-
* does the same
|
|
171
|
-
* before redirecting).
|
|
170
|
+
* does the same).
|
|
172
171
|
* Call from the browser: the return origin must be the site's real https origin
|
|
173
172
|
* (window.location.origin), never a server-derived request origin.
|
|
174
173
|
*/
|
|
@@ -167,8 +167,7 @@ export async function setNote(note: string): Promise<Cart> {
|
|
|
167
167
|
/**
|
|
168
168
|
* Start the Wix-hosted checkout for the current cart and return the URL to navigate to.
|
|
169
169
|
* The cart's id IS the checkout id — no separate checkout-creation call (Wix's own storefront
|
|
170
|
-
* does the same
|
|
171
|
-
* before redirecting).
|
|
170
|
+
* does the same).
|
|
172
171
|
* Call from the browser: the return origin must be the site's real https origin
|
|
173
172
|
* (window.location.origin), never a server-derived request origin.
|
|
174
173
|
*/
|
|
@@ -16,7 +16,7 @@ Five ways to get the files live; what you have decides which are open to you.
|
|
|
16
16
|
| Option | Needs | Carries | The user ends up with |
|
|
17
17
|
| --- | --- | --- | --- |
|
|
18
18
|
| **A.** `curl` + CLI token → [into the account](#publish-into-the-users-account) | A shell; a Wix CLI login | Anything on disk | A site in their account, final URL |
|
|
19
|
-
| **B.** `ExecuteWixAPI` → [into the account](#publish-into-the-users-account) | The Wix MCP |
|
|
19
|
+
| **B.** `ExecuteWixAPI` → [into the account](#publish-into-the-users-account) | The Wix MCP | Files already in the conversation — text as is, small binaries as base64 — and anything downloaded from the site | A site in their account, final URL |
|
|
20
20
|
| **C.** `curl` → [anonymous](#publish-anonymously) | A shell | Anything on disk | A live site for one hour; kept by a [claim](#claim-it-into-the-users-account) (through the Wix MCP or a CLI token) or the save link |
|
|
21
21
|
| **D.** [The drop page](#the-drop-page) | Nothing | Whatever the user uploads | The same, after they upload it themselves |
|
|
22
22
|
| **E.** [The headless skill](#keep-building-add-a-backend-when-you-need-one) | A shell; Node; a Wix CLI login | A project folder, source included (built for you) | A site in their account as a Wix Headless project, released with the Wix CLI, ready for Wix Business Solutions |
|
|
@@ -30,8 +30,13 @@ What sets them apart:
|
|
|
30
30
|
byte is written out inside the call. That costs nothing extra for a page you
|
|
31
31
|
generated or the user pasted (it's already in the conversation); for files on
|
|
32
32
|
disk it means reading them in and writing them back out, and each change
|
|
33
|
-
resends all of it.
|
|
34
|
-
|
|
33
|
+
resends all of it. Files travel in the tool's `files` param — text raw, a
|
|
34
|
+
binary file (PNG, JPG, fonts) as base64 — and `wix.multipart()` builds the
|
|
35
|
+
upload body (below). Base64 costs about a third more than the file and every
|
|
36
|
+
byte is tokens, so it suits small assets (an icon, a logo, a font) and files
|
|
37
|
+
you downloaded from the site to change; a photo goes in by absolute URL
|
|
38
|
+
(`<img src="https://…">`) or with `curl` from a shell. The bundle is capped at
|
|
39
|
+
4M characters.
|
|
35
40
|
- **A CLI login** is one approval by the user in the browser: run
|
|
36
41
|
`npx @wix/cli login` and have them approve; `npx @wix/cli token` then prints a
|
|
37
42
|
token (see [Before the calls](#before-the-calls)). It also unlocks later
|
|
@@ -43,8 +48,8 @@ What sets them apart:
|
|
|
43
48
|
the only one that takes framework source as is and leaves a project ready for a
|
|
44
49
|
backend. A drop site can still move to it [later](#keep-building-add-a-backend-when-you-need-one).
|
|
45
50
|
|
|
46
|
-
So: a
|
|
47
|
-
shell or not. Static files on disk — A with a CLI login, else C (claimed through
|
|
51
|
+
So: a site already in the conversation — pages, styles, a logo — with the Wix
|
|
52
|
+
MCP connected — B, shell or not. Static files on disk — A with a CLI login, else C (claimed through
|
|
48
53
|
the Wix MCP when it's connected). A framework project (a `package.json`), or a
|
|
49
54
|
site that needs stores, bookings, a CMS or members from the start — E. Files out
|
|
50
55
|
of your reach, or nothing above fits — D.
|
|
@@ -108,8 +113,27 @@ curl -sS -X POST \
|
|
|
108
113
|
"siteUrl": "https://headless-zjfqzddjtww-northwind-1406.wix-site-host.com" }
|
|
109
114
|
```
|
|
110
115
|
|
|
111
|
-
Option B — the same two calls as one `ExecuteWixAPI` script, for
|
|
112
|
-
|
|
116
|
+
Option B — the same two calls as one `ExecuteWixAPI` script, for files already
|
|
117
|
+
in the conversation. The files go in the tool's **`files` param**, not in
|
|
118
|
+
`code`: one bundle, each file introduced by a line `=== FILE: <path> ===` and
|
|
119
|
+
followed by its raw text, `<path>` relative to the site root. A binary file is
|
|
120
|
+
introduced by `=== FILE: <path> base64 ===` and followed by its base64. Nothing
|
|
121
|
+
in it is escaped, so quotes, backticks, `${}` and backslashes arrive as written.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
=== FILE: index.html ===
|
|
125
|
+
<!doctype html><html><head><title>Northwind Studio</title>
|
|
126
|
+
<link rel="stylesheet" href="assets/styles.css"></head><body><h1>Northwind Studio</h1><img src="assets/logo.png"></body></html>
|
|
127
|
+
=== FILE: assets/styles.css ===
|
|
128
|
+
body { font-family: sans-serif; margin: 0; padding: 4rem; }
|
|
129
|
+
=== FILE: assets/logo.png base64 ===
|
|
130
|
+
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
In `code`, the bundle is the `files` global (`[{ path, content, encoding? }]`,
|
|
134
|
+
`encoding: 'base64'` on the binary entries) and `wix.multipart()` turns it into
|
|
135
|
+
the upload body: one part named `files` per file, the path as its filename, a
|
|
136
|
+
content type guessed from the extension, base64 entries decoded to their bytes.
|
|
113
137
|
|
|
114
138
|
```javascript
|
|
115
139
|
async function run() {
|
|
@@ -117,31 +141,21 @@ async function run() {
|
|
|
117
141
|
url: 'https://www.wixapis.com/headless-business-setup/v1/headless-business/provision',
|
|
118
142
|
body: { origin: 'drop', newMetasite: { namingStrategy: { metaSiteName: 'Northwind Studio' }, seedOptions: [] },
|
|
119
143
|
synchronousSteps: ['SET_METASITE_NAME', 'CONFIGURE_HEADLESS_APP'] } });
|
|
120
|
-
|
|
121
|
-
// (an unescaped \ is dropped or reinterpreted: /\d+/ would arrive as /d+/).
|
|
122
|
-
const files = {
|
|
123
|
-
'index.html': `<!doctype html><html><head><title>Northwind Studio</title>
|
|
124
|
-
<link rel="stylesheet" href="assets/styles.css"></head><body><h1>Northwind Studio</h1></body></html>`,
|
|
125
|
-
'assets/styles.css': `body { font-family: sans-serif; margin: 0; padding: 4rem; }`,
|
|
126
|
-
};
|
|
127
|
-
const boundary = '----wixdropboundary';
|
|
128
|
-
const body = Object.entries(files).flatMap(([path, text]) => [
|
|
129
|
-
'--' + boundary,
|
|
130
|
-
`Content-Disposition: form-data; name="files"; filename="${path}"`,
|
|
131
|
-
'', text,
|
|
132
|
-
]).concat('--' + boundary + '--', '').join('\r\n');
|
|
144
|
+
const mp = wix.multipart(); // the `files` param; or pass [{ path, content | base64 | bytes }]
|
|
133
145
|
return await wix.request({ scope: 'account', method: 'POST',
|
|
134
146
|
url: `https://www.wixapis.com/headless-business-setup/v1/headless-business/${created.data.metaSiteId}/drop`,
|
|
135
|
-
headers: { 'Content-Type':
|
|
136
|
-
body });
|
|
147
|
+
headers: { 'Content-Type': mp.contentType },
|
|
148
|
+
body: mp.body });
|
|
137
149
|
}
|
|
138
150
|
```
|
|
139
151
|
|
|
140
152
|
`wix.request` returns `{ status, data }`; read a response's fields from `data`
|
|
141
|
-
(`created.data.metaSiteId`).
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
(`created.data.metaSiteId`). Don't build the multipart body by hand — a missing
|
|
154
|
+
`\r\n` or a boundary mismatch is a bare `500` — and don't put file text inside
|
|
155
|
+
`code` as string literals: that second layer of escaping is what corrupts
|
|
156
|
+
backslashes and `${}`. Don't put image bytes in `code` either — they're a
|
|
157
|
+
`base64` entry in `files`. The one thing `files` cannot carry is a file with a
|
|
158
|
+
line that reads exactly `=== FILE: … ===`.
|
|
145
159
|
|
|
146
160
|
`siteUrl` is the site's final address — it's already in the user's account. Give
|
|
147
161
|
the user two links: `siteUrl`, and its dashboard at
|
|
@@ -169,24 +183,28 @@ unzip -o current.zip -d current # the site's files + wix.config.json
|
|
|
169
183
|
```
|
|
170
184
|
|
|
171
185
|
In an `ExecuteWixAPI` script, request the same URL with `responseType: 'base64'`
|
|
172
|
-
and
|
|
186
|
+
and unzip it in memory with `wix.unzip()`. Each entry is `{ path, bytes, text() }`:
|
|
187
|
+
`bytes` is the file as stored, `text()` decodes it when it's text. Don't decode
|
|
188
|
+
every entry — an image run through a text decoder is corrupted.
|
|
173
189
|
|
|
174
190
|
```javascript
|
|
175
191
|
const r = await wix.request({ scope: 'account', method: 'GET', responseType: 'base64',
|
|
176
192
|
url: `https://www.wix.com/_api/wixstro-deployments/v1/instant-sites/${metaSiteId}/download.zip` });
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
193
|
+
const entries = await wix.unzip(r.data);
|
|
194
|
+
// entries: [{ path: 'index.html', bytes, text() }, { path: 'assets/logo.png', bytes, text() }, { path: 'wix.config.json', … }]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Edit in memory — text through `text()`, binaries left as they are — then drop
|
|
198
|
+
the full set back in the same script, leaving `wix.config.json` out. Entries go
|
|
199
|
+
to `wix.multipart()` as they are; an edited file replaces its entry with
|
|
200
|
+
`{ path, content }`:
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
const edited = entries.filter((e) => e.path !== 'wix.config.json').map((e) =>
|
|
204
|
+
e.path === 'index.html' ? { path: e.path, content: e.text().replace('Northwind Studio', 'Northwind Studio — Est. 1998') } : e);
|
|
205
|
+
const mp = wix.multipart(edited);
|
|
206
|
+
await wix.request({ scope: 'account', method: 'POST', headers: { 'Content-Type': mp.contentType }, body: mp.body,
|
|
207
|
+
url: `https://www.wixapis.com/headless-business-setup/v1/headless-business/${metaSiteId}/drop` });
|
|
190
208
|
```
|
|
191
209
|
|
|
192
210
|
The live URL itself (`*.wix-site-host.com`) can't be read from a script; this
|
|
@@ -219,7 +237,8 @@ Keep both: `metaSiteId` addresses the site, `projectId` builds the save link.
|
|
|
219
237
|
### 2. Upload the files
|
|
220
238
|
|
|
221
239
|
Same multipart shape as the [drop](#2-drop-the-files--the-site-goes-live), to the
|
|
222
|
-
upload path, with the attribution parameters:
|
|
240
|
+
upload path, with the attribution parameters (in a script: `wix.multipart()` with
|
|
241
|
+
the `files` param, posted to this URL):
|
|
223
242
|
|
|
224
243
|
```bash
|
|
225
244
|
curl -sS -X POST \
|
|
@@ -323,7 +342,7 @@ Failures come back as HTTP 400 with a code in `details.applicationError.code`:
|
|
|
323
342
|
- **`403 PERMISSION_DENIED` on a drop** — the token expired or is missing (see
|
|
324
343
|
[Before the calls](#before-the-calls)), or the site isn't the caller's. A site
|
|
325
344
|
that's still anonymous is changed by upload + release, not by a drop.
|
|
326
|
-
- **`500` on a drop or upload** — the multipart body is malformed (see the shape
|
|
345
|
+
- **`500` on a drop or upload** — the multipart body was built by hand and is malformed (see the shape
|
|
327
346
|
above). Rebuild it from the example; sending it again unchanged fails the
|
|
328
347
|
same way.
|
|
329
348
|
- **`404` on the anonymous route after step 1** — the hour passed or the site was
|