local-storefront 0.3.0-rc.2 → 0.3.0-rc.3
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/CHANGELOG.md +42 -0
- package/docs/admin-reference.md +5 -2
- package/docs/admin.md +20 -2
- package/docs/checkout.md +57 -2
- package/docs/control-api.md +1 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,48 @@
|
|
|
3
3
|
What changed in each version of `lsf`, for someone who uses it. The section of a version is
|
|
4
4
|
what its [GitHub release](https://github.com/0xtlt/local-storefront/releases) says.
|
|
5
5
|
|
|
6
|
+
## 0.3.0-rc.3 - 2026-11-12
|
|
7
|
+
|
|
8
|
+
Third release candidate of 0.3.0. The local checkout applies the code discounts a backend makes
|
|
9
|
+
through the Admin API, and the session token of checkout and customer account UI extensions is
|
|
10
|
+
the one Shopify gives, signed with the client secret. A backend that read lsf's old token needs
|
|
11
|
+
a change: see Breaking changes.
|
|
12
|
+
|
|
13
|
+
### Breaking changes
|
|
14
|
+
|
|
15
|
+
What to change when you upgrade from 0.3.0-rc.2.
|
|
16
|
+
|
|
17
|
+
- **The session token of a UI extension has Shopify's claims.** `dest` is the shop's bare
|
|
18
|
+
domain (`local-supply-co.myshopify.com`), no longer lsf's origin; there is no `iss` and no
|
|
19
|
+
`sid`; it lives five minutes; `sub` is there only when a customer is logged in. It is signed
|
|
20
|
+
with HS256 and the client secret, so a backend now verifies it as it verifies Shopify's, and
|
|
21
|
+
one that skipped the verification for lsf can stop doing so.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
#### The checkout of an app
|
|
26
|
+
|
|
27
|
+
- **`DiscountCodeBasic` and `DiscountCodeFreeShipping` codes apply at the checkout.** A code made
|
|
28
|
+
with `discountCodeBasicCreate` or `discountCodeFreeShippingCreate` takes an amount or a
|
|
29
|
+
percentage off the order, off listed products, variants or collections, or off each item, or
|
|
30
|
+
makes the rates up to `maximumShippingPrice` free in the countries of `destination`. It is taken
|
|
31
|
+
while the discount is active by its dates, under its `usageLimit`, for a buyer of its `context`
|
|
32
|
+
(all, listed customers, members of listed segments) who has not used it when it
|
|
33
|
+
`appliesOncePerCustomer`, and for a cart that meets its `minimumRequirement`. One such code
|
|
34
|
+
applies per checkout, with the Function discounts. The order and its `orders/create` and
|
|
35
|
+
`orders/paid` carry it in `discount_codes` and `discount_applications`, and its
|
|
36
|
+
`asyncUsageCount` goes up. `combinesWith`, buy X get Y codes and a `context` of markets are not
|
|
37
|
+
read.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
#### The checkout of an app
|
|
42
|
+
|
|
43
|
+
- **`shopify.sessionToken.get()` gives a token a backend can verify.** The page asks lsf for it
|
|
44
|
+
(`POST /__lsf/extensions/<handle>/session-token`), and lsf signs it with the client secret of
|
|
45
|
+
the Admin API (`--client-secret`, `SHOPIFY_API_SECRET`, the app's `.env`), which stays on the
|
|
46
|
+
server. `get()` rejects with the reason when lsf gives no token.
|
|
47
|
+
|
|
6
48
|
## 0.3.0-rc.2 - 2026-11-12
|
|
7
49
|
|
|
8
50
|
Second release candidate of 0.3.0. It fixes what was reported on 0.2.0 and 0.3.0-rc.1, each
|
package/docs/admin-reference.md
CHANGED
|
@@ -1079,8 +1079,11 @@ Function by `functionHandle` (or the deprecated `functionId`), which must be a d
|
|
|
1079
1079
|
of the app; their `metafields` are the Function's configuration, checked as `metafieldsSet`
|
|
1080
1080
|
checks them (a definition of the `DISCOUNT` owner type applies). The store sync writes an active
|
|
1081
1081
|
app discount back to the store data's `checkout.discounts` (its Function, title, first code,
|
|
1082
|
-
classes and metafields), so the local checkout runs it. Shopify's own types
|
|
1083
|
-
|
|
1082
|
+
classes and metafields), so the local checkout runs it. Of Shopify's own types, the local
|
|
1083
|
+
checkout applies the codes of `DiscountCodeBasic` and `DiscountCodeFreeShipping` itself, and
|
|
1084
|
+
counts the orders placed with them in `asyncUsageCount`
|
|
1085
|
+
([what the checkout sees](admin.md#what-the-storefront-and-the-checkout-see)); buy X get Y and
|
|
1086
|
+
the automatic ones are kept as records.
|
|
1084
1087
|
Their `summary` is the admin's: what the discount takes off, then its minimum, in the shop's
|
|
1085
1088
|
money format (`$5.00 off entire order • Minimum purchase of $50.00`, `10% off 2 products`, `Buy
|
|
1086
1089
|
2 items, get 1 item free`, `Free shipping on all products`), and `shortSummary` its first part.
|
package/docs/admin.md
CHANGED
|
@@ -655,7 +655,7 @@ and delivery customizations, validations, cart transforms, fulfillment constrain
|
|
|
655
655
|
app discounts (`DiscountAutomaticApp`, `DiscountCodeApp`), whether the app created them or the
|
|
656
656
|
store data declared them (`checkout.functions.<handle>.enabled`, `checkout.discounts`). The
|
|
657
657
|
answer counts them (`removedFunctionOwners`). The discounts of Shopify's own types (a
|
|
658
|
-
`DiscountCodeBasic` the app made) stay. While the app is uninstalled, the shop's checkout runs
|
|
658
|
+
`DiscountCodeBasic` the app made) stay, and their codes still apply at the checkout. While the app is uninstalled, the shop's checkout runs
|
|
659
659
|
none of its Functions, whatever a scenario declares or names in its `extensions`: the
|
|
660
660
|
checkout's Functions dialog lists each with "the app is not installed on the shop". A reinstall
|
|
661
661
|
gets none of those objects back, as on Shopify, where a reinstalled app "had to register them
|
|
@@ -1624,6 +1624,24 @@ What the backend changes through the Admin API is what the storefront and the
|
|
|
1624
1624
|
extensions on the order status page (`appMetafields`, `owner_type = "ORDER"`);
|
|
1625
1625
|
- an app discount it creates (`discountAutomaticAppCreate`, `discountCodeAppCreate`) runs its
|
|
1626
1626
|
Function at the checkout, with the metafields it was given, while it is active;
|
|
1627
|
+
- a code discount of Shopify's own types it creates (`discountCodeBasicCreate`,
|
|
1628
|
+
`discountCodeFreeShippingCreate`) applies at the checkout once the buyer enters one of its
|
|
1629
|
+
codes, without a Function: an amount or a percentage off the order, off the products,
|
|
1630
|
+
variants and collections it lists (`appliesOnEachItem` included), or free shipping. The
|
|
1631
|
+
checkout takes the code while the discount is `ACTIVE` by its dates, its `usageLimit` is not
|
|
1632
|
+
reached, the buyer is one of its `context` (all buyers, the customers it lists, the members of
|
|
1633
|
+
the segments it lists: a guest is the customer whose email they gave), has not used it when it
|
|
1634
|
+
`appliesOncePerCustomer`, and the cart meets its `minimumRequirement` (the subtotal or the
|
|
1635
|
+
quantity of the items it applies to, before discounts); free shipping makes free the rates up
|
|
1636
|
+
to its `maximumShippingPrice`, to the countries of its `destination`. Otherwise the page says
|
|
1637
|
+
"Enter a valid discount code", and the state's `discountCodes` says why (`reason`). The order
|
|
1638
|
+
carries the code (`discountCodes`, a `DiscountCodeApplication`, the allocations), so do the
|
|
1639
|
+
`discount_codes` and `discount_applications` of `orders/create` and `orders/paid`, and the
|
|
1640
|
+
discount's `asyncUsageCount` (and its code's) goes up by one at once. One code of these types
|
|
1641
|
+
applies per checkout (lsf does not read `combinesWith`), and it stacks with the discounts of
|
|
1642
|
+
the Functions; of the shipping discounts, the best one applies. Not read: a `context` of
|
|
1643
|
+
markets (every buyer), `appliesOnOneTimePurchase` and `appliesOnSubscription`. Buy X get Y
|
|
1644
|
+
codes and Shopify's automatic discounts stay records;
|
|
1627
1645
|
- the shipping rates of the general delivery profile it changes are the checkout's delivery
|
|
1628
1646
|
options (see [delivery](admin-reference.md#inventory-transfers-fulfillment-and-delivery));
|
|
1629
1647
|
- a theme file it writes to the main theme (`themeFilesUpsert`, `themeFilesCopy`,
|
|
@@ -1637,7 +1655,7 @@ What the backend changes through the Admin API is what the storefront and the
|
|
|
1637
1655
|
| Syncs | Does not sync |
|
|
1638
1656
|
|---|---|
|
|
1639
1657
|
| Products: title, handle, description, vendor, type, tags, status and publication, options, variants (price, SKU, barcode, inventory policy, weight), stock per location, media from `files/`, collections and selling plan groups they are in | Orders and what belongs to them (`orderCreate`, draft orders, fulfillments, refunds, returns): the account pages show the orders of the data and of the checkout |
|
|
1640
|
-
| Collections: title, handle, description, image, products in order | Discounts
|
|
1658
|
+
| Collections: title, handle, description, image, products in order | Discounts of Shopify's own types: the local checkout reads the code ones (`DiscountCodeBasic`, `DiscountCodeFreeShipping`) from the Admin API itself (above), and leaves the others (`…Bxgy`, the automatic ones) out; of an app discount, `combinesWith`, usage limits and every code but the first |
|
|
1641
1659
|
| Customers: email, names, phone, tags, marketing consent, account state, addresses, their company | Gift cards the Admin API creates (changes to the data's cards sync) |
|
|
1642
1660
|
| Companies and their locations, pages, blogs and articles, menus, locations, metaobjects and their translations, selling plan groups | SEO, product categories, bundle components and combined listings (a bundle is a product of its own there), collection rules and sources (a changed automated collection becomes a manual one), product feeds, point of sale |
|
|
1643
1661
|
| Metafields of all of these, of the shop and of markets; markets, their countries and their currency; the published languages | Metafield and metaobject definitions, files, translations of other resources or for a market, publications other than the Online Store; catalogs, price lists, quantity rules, web presences, channels |
|
package/docs/checkout.md
CHANGED
|
@@ -1117,6 +1117,22 @@ discounts on the subtotal they left, then shipping. Within a class:
|
|
|
1117
1117
|
off −€0.00" while its settings are not set. A candidate of more than 0 that finds nothing left
|
|
1118
1118
|
to take off (a line another discount made free) is not applied.
|
|
1119
1119
|
|
|
1120
|
+
**Codes of Shopify's own discount types.** With the [Admin API](admin.md), a code of a
|
|
1121
|
+
`DiscountCodeBasic` or a `DiscountCodeFreeShipping` the backend created applies without a
|
|
1122
|
+
Function ([what the checkout sees](admin.md#what-the-storefront-and-the-checkout-see)): an
|
|
1123
|
+
amount off the order is an order discount, one off listed products, variants or collections (or
|
|
1124
|
+
off each item) a product discount, free shipping a shipping discount, and each takes its place
|
|
1125
|
+
among the discounts of the Functions by the rules above. The code is checked when the
|
|
1126
|
+
checkout is computed: the discount's dates and usage limit, its buyers, one use per customer,
|
|
1127
|
+
its minimum (on the items it applies to, before discounts), and for free shipping the country
|
|
1128
|
+
of the address (a code is taken before there is one). A refused code shows "Enter a valid
|
|
1129
|
+
discount code", and its entry of `discountCodes` in the state has a `reason`
|
|
1130
|
+
(`"its minimum purchase is 50.00 USD, and the items it applies to come to 32.00 USD"`). One code
|
|
1131
|
+
of these types applies per checkout, the first one entered that qualifies: a second is refused,
|
|
1132
|
+
as on Shopify when a discount combines with no other (lsf does not read `combinesWith`). Its
|
|
1133
|
+
`DiscountApplication` is `ACROSS` (`EACH` off each item), `ALL` for the order and `ENTITLED`
|
|
1134
|
+
for listed items; free shipping is 100% off the shipping line, `EACH`.
|
|
1135
|
+
|
|
1120
1136
|
An amount is shared between the lines in proportion to what they cost, to the cent, and the
|
|
1121
1137
|
allocations of the lines (`discountAllocations`, in the page and for the Functions that run
|
|
1122
1138
|
later) add up to the discount.
|
|
@@ -1298,6 +1314,39 @@ of checkout UI extensions has no such type): an `ORDER` declaration reads nothin
|
|
|
1298
1314
|
a warning. Shopify lists `CART` as an owner type of checkout UI extensions only and `ORDER` of
|
|
1299
1315
|
customer account UI extensions only; lsf takes both on either, as one extension can target both.
|
|
1300
1316
|
|
|
1317
|
+
### Session tokens
|
|
1318
|
+
|
|
1319
|
+
`shopify.sessionToken.get()`, in a checkout or a customer account extension, gives the token
|
|
1320
|
+
Shopify gives, which the backend verifies as it verifies Shopify's (`authenticate.public.checkout`
|
|
1321
|
+
and `authenticate.public.customerAccount` of Shopify's libraries, `decodeSessionToken`): an
|
|
1322
|
+
HS256 JWT signed with the client secret of the [Admin API](admin.md#start)
|
|
1323
|
+
(`--client-secret`, `SHOPIFY_API_SECRET` of the environment or of the app's `.env`, else
|
|
1324
|
+
`lsf-local-client-secret`; the same without the admin part, without `--client-secret`). Its
|
|
1325
|
+
claims are those of shopify.dev's Session Token API, for
|
|
1326
|
+
[checkout](https://shopify.dev/docs/api/checkout-ui-extensions/latest/target-apis/platform-apis/session-token-api)
|
|
1327
|
+
and [customer account](https://shopify.dev/docs/api/customer-account-ui-extensions/latest/target-apis/platform-apis/session-token-api)
|
|
1328
|
+
extensions:
|
|
1329
|
+
|
|
1330
|
+
```json
|
|
1331
|
+
{
|
|
1332
|
+
"dest": "local-supply-co.myshopify.com",
|
|
1333
|
+
"aud": "<client id>",
|
|
1334
|
+
"exp": 1794511949,
|
|
1335
|
+
"nbf": 1794511649,
|
|
1336
|
+
"iat": 1794511649,
|
|
1337
|
+
"jti": "7f63880b-b45d-49d4-beb1-af926c85653c",
|
|
1338
|
+
"sub": "gid://shopify/Customer/6523834743460"
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
`dest` is the shop's domain, bare, as in shopify.dev's example (the session token of the admin
|
|
1343
|
+
has `https://`): the `permanent_domain` of the store data, the default shop of the Admin API.
|
|
1344
|
+
`aud` is the client id, the token lives five minutes, `jti` is a new UUID each time, and `sub`
|
|
1345
|
+
is there only when a customer is logged in (whether the app may read customer data is not
|
|
1346
|
+
checked). There is no `iss` and no `sid`. The page asks lsf for the token
|
|
1347
|
+
(`POST /__lsf/extensions/<handle>/session-token`, [control API](control-api.md#apps-checkout-and-admin-api))
|
|
1348
|
+
and hands it to the extension: the secret never reaches the browser.
|
|
1349
|
+
|
|
1301
1350
|
## Run a Function without a server
|
|
1302
1351
|
|
|
1303
1352
|
```bash
|
|
@@ -1505,7 +1554,7 @@ customization runs on them.
|
|
|
1505
1554
|
| `shippingAddress` | `address`: `firstName`, `lastName`, `company`, `address1`, `address2`, `city`, `countryCode`, `provinceCode`, `zip`, `phone` | Sets the address. A country of the store becomes the country of the session. A `countryCode` that is not a country (ISO 3166-1 alpha-2, or `XK`, `AC`, `TA` as Shopify takes them; not `ZZ`) is refused and changes nothing. The store data has no shipping zones: any real country is taken. |
|
|
1506
1555
|
| `deliveryOption` | `group` (`group-1`), `handle` | Chooses a shipping rate. |
|
|
1507
1556
|
| `paymentMethod` | `handle` | Chooses a payment method. No handle clears it. |
|
|
1508
|
-
| `discountCode` | `action` (`add` or `remove`), `code` | Enters or takes back a code. |
|
|
1557
|
+
| `discountCode` | `action` (`add` or `remove`), `code` | Enters or takes back a code. Each entered code is in the `discountCodes` of the state, `applicable` or not; a code of a discount of Shopify's own types that was refused has a `reason` ([how discounts stack](#how-discounts-stack)). |
|
|
1509
1558
|
| `cartLines` | `change`: `addCartLine`, `removeCartLine` or `updateCartLine` of the UI extension API | Changes the cart of the session, the one the theme shows. |
|
|
1510
1559
|
| `attribute` | `change`: `updateAttribute` or `removeAttribute`, with `key` and `value` | Changes the attributes of the cart. |
|
|
1511
1560
|
| `note` | `change`: `updateNote` (`note`) or `removeNote` | Changes the note of the cart. |
|
|
@@ -1564,7 +1613,10 @@ The browser tests of a theme in CI: [testing](testing.md#in-ci).
|
|
|
1564
1613
|
- Discounts have no `conditions` and no combinations: all of them stack, as described in
|
|
1565
1614
|
[how discounts stack](#how-discounts-stack). Shopify lets the merchant choose which discounts
|
|
1566
1615
|
combine, and applies the best of those that cannot; here all the product and order discounts
|
|
1567
|
-
combine, and of the shipping discounts the best one applies.
|
|
1616
|
+
combine, and of the shipping discounts the best one applies. Of Shopify's own discount types,
|
|
1617
|
+
only the codes of `DiscountCodeBasic` and `DiscountCodeFreeShipping` apply, one per checkout,
|
|
1618
|
+
and only with the Admin API (which makes them): not buy X get Y, not the automatic ones, not a
|
|
1619
|
+
`context` of markets nor `appliesOnSubscription`.
|
|
1568
1620
|
- Tax is one rate on the discounted subtotal, with no tax lines.
|
|
1569
1621
|
- No Storefront API: `query` in an extension rejects. No Customer Account API either: a customer
|
|
1570
1622
|
account extension reads the account through its `shopify` global only.
|
|
@@ -1592,6 +1644,9 @@ store before a release:
|
|
|
1592
1644
|
|
|
1593
1645
|
- **Discount combinations and conditions**: lsf combines every product and order discount
|
|
1594
1646
|
([how discounts stack](#how-discounts-stack)).
|
|
1647
|
+
- **Codes of Shopify's own discount types**: one applies per checkout whatever its
|
|
1648
|
+
`combinesWith`, its minimum is checked on the items it applies to before any discount, and a
|
|
1649
|
+
refused code says lsf's reason, where Shopify's checkout has messages of its own.
|
|
1595
1650
|
- **Currencies and price lists**: a buyer pays in the currency of the country of the shipping
|
|
1596
1651
|
address (else of the one selected on the storefront), at its market's `rate` or fixed `prices`
|
|
1597
1652
|
([localization](data-format.md#localization)); the Functions read that rate as
|
package/docs/control-api.md
CHANGED
|
@@ -52,6 +52,7 @@ backend's tests use, then the rest. How a test uses them:
|
|
|
52
52
|
| `GET /checkout?scenario=<name>` | Opens a checkout for a scenario, in a new session whose cookie it sets. With `--app`. | A redirect |
|
|
53
53
|
| `GET /__lsf/checkout/assets/<file>` | The files of the checkout page, which are in the binary. With `--app`. | The file |
|
|
54
54
|
| `GET /__lsf/extensions/<handle>/<file>` | A file of the build (`dist/`) of a checkout UI extension. With `--app`. | The file |
|
|
55
|
+
| `POST /__lsf/extensions/<handle>/session-token` | What `shopify.sessionToken.get()` gives a checkout or customer account UI extension of the app: a token for the shop (`dest`, its bare domain), the app (`aud`) and the customer logged in in the caller's session (`sub`, absent logged out), valid five minutes, signed with HS256 and the client secret ([session tokens](checkout.md#session-tokens)). The checkout page and the customer account ask it for their extensions; a test can call the backend with it. No CORS: another site cannot read it. `404` for an extension the app does not serve. With `--app`. | `{ "token", "authorization", "shop", "claims" }` |
|
|
55
56
|
| `GET /__lsf/admin` | The [Admin API](admin.md): its shops, their tokens, record counts and webhook subscriptions, the app, the throttle, the mutations, each shop's outbox. | JSON, [below](#admin-api) |
|
|
56
57
|
| `GET /__lsf/graphiql` | GraphiQL on the [Admin API](admin.md#graphiql-in-the-browser): the shops, their tokens and the served versions filled in. | HTML |
|
|
57
58
|
| `DELETE /__lsf/admin` | Puts every shop of the Admin API back to the store data (its clock and its outbox too), forgets the subscriptions the backend made and the tokens OAuth issued, and installs the app again (the printed token works). It revokes every token of the app, those of token exchange too, and sends no `app/uninstalled`: the backend's stored tokens are answered `401` ([reset between runs](admin.md#reset-between-runs)). | `{ "ok": true, "shops": [...] }` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-storefront",
|
|
3
|
-
"version": "0.3.0-rc.
|
|
3
|
+
"version": "0.3.0-rc.3",
|
|
4
4
|
"description": "Run Shopify themes and apps locally, without Shopify: the storefront, a local checkout for Functions and checkout UI extensions, and a local GraphQL Admin API with webhooks, from JSON store data. Made for tests in CI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shopify",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"node": ">=18"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"local-storefront-darwin-arm64": "0.3.0-rc.
|
|
42
|
-
"local-storefront-darwin-x64": "0.3.0-rc.
|
|
43
|
-
"local-storefront-linux-arm64": "0.3.0-rc.
|
|
44
|
-
"local-storefront-linux-x64": "0.3.0-rc.
|
|
45
|
-
"local-storefront-win32-arm64": "0.3.0-rc.
|
|
46
|
-
"local-storefront-win32-x64": "0.3.0-rc.
|
|
41
|
+
"local-storefront-darwin-arm64": "0.3.0-rc.3",
|
|
42
|
+
"local-storefront-darwin-x64": "0.3.0-rc.3",
|
|
43
|
+
"local-storefront-linux-arm64": "0.3.0-rc.3",
|
|
44
|
+
"local-storefront-linux-x64": "0.3.0-rc.3",
|
|
45
|
+
"local-storefront-win32-arm64": "0.3.0-rc.3",
|
|
46
|
+
"local-storefront-win32-x64": "0.3.0-rc.3"
|
|
47
47
|
}
|
|
48
48
|
}
|