@zappie/sdk 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +22 -6
  3. package/dist/cart/branch.d.ts +1 -1
  4. package/dist/cart/branch.js +6 -2
  5. package/dist/cart/branch.js.map +2 -2
  6. package/dist/cart/item.d.ts +1 -1
  7. package/dist/cart/quote.d.ts +1 -1
  8. package/dist/checkout-defaults/index.d.ts +17 -11
  9. package/dist/checkout-defaults/index.js +17 -2
  10. package/dist/checkout-defaults/index.js.map +2 -2
  11. package/dist/config/index.d.ts +10 -0
  12. package/dist/customer/navigation.js +2 -7
  13. package/dist/customer/navigation.js.map +2 -2
  14. package/dist/debug.d.ts +1 -1
  15. package/dist/debug.js +1 -1
  16. package/dist/index.d.ts +43 -43
  17. package/dist/index.js +6 -6
  18. package/dist/index.js.map +2 -2
  19. package/dist/integrations/index.d.ts +1 -1
  20. package/dist/internal/return-path.js +17 -0
  21. package/dist/internal/return-path.js.map +7 -0
  22. package/dist/internal.d.ts +1 -1
  23. package/dist/persist/catalogue.d.ts +1 -1
  24. package/dist/react/index.d.ts +1 -1
  25. package/dist/redirect/index.d.ts +15 -0
  26. package/dist/redirect/index.js +22 -0
  27. package/dist/redirect/index.js.map +7 -0
  28. package/dist/server/index.d.ts +87 -2
  29. package/dist/server/index.js +8 -0
  30. package/dist/server/index.js.map +2 -2
  31. package/dist/server/security.js +136 -0
  32. package/dist/server/security.js.map +7 -0
  33. package/dist/server/storefront-url/parse-v2.js +13 -5
  34. package/dist/server/storefront-url/parse-v2.js.map +2 -2
  35. package/dist/server/storefront-url/url.js +3 -1
  36. package/dist/server/storefront-url/url.js.map +2 -2
  37. package/dist/site-config/helpers.js +1 -1
  38. package/dist/site-config/helpers.js.map +2 -2
  39. package/dist/site-config/types.js.map +1 -1
  40. package/dist/site-config/validate.js +25 -9
  41. package/dist/site-config/validate.js.map +2 -2
  42. package/dist/testing/index.d.ts +1 -1
  43. package/dist/transport/search.d.ts +1 -1
  44. package/dist/transport/selected-menu.d.ts +1 -1
  45. package/package.json +9 -5
  46. package/dist/{client-DzUauU-b.d.ts → client-CSOZOwQW.d.ts} +24 -24
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @zappie/sdk
2
2
 
3
+ ## 0.1.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 9b25eb7: The packages now live in zappie/storefront-packages, and each package's repository, homepage and issues links point there. This release is the first one published from that repository.
8
+
9
+ ## 0.1.12
10
+
11
+ ### Patch Changes
12
+
13
+ - 2b672c9: `planBranchSwitch()` gives each kept line the new branch's `unitPrice` and `lineSubtotal`. Before, a kept line moved with the old branch's prices, though the plan's subtotal used the new ones.
14
+ - 9b5b412: A remembered checkout address belongs to the shopper who used it. `rememberAddress(address, storage, customerId)` now stores the signed-in customer's ID with the address, and `defaultAddress()` and `checkoutDefaults()` offer it only to that customer, or only to a guest when no ID was stored. A signed-in customer's own saved addresses now lead over an address a guest or another customer used on the same device. `watchCheckoutDefaults()` forgets the customer's address when they sign out. Pass the signed-in customer's ID to `rememberAddress` so they see their address again on their next visit.
15
+ - 32ecccf: Add report-only storefront security headers, per-request nonces, prerendered-script hashes and V18 security-header verification. The starter now applies the policy to every document response.
16
+ - afb46d4: Add storefront security lint rules, pass outbound hosts from `zappie.config.ts` to ESLint, and provide `safeRedirect` from `@zappie/sdk/redirect` for untrusted navigation targets.
17
+ - 6a8e227: `createCustomer().signIn()` refuses a return path that a browser would turn into another site. A path such as `/\t/evil.example` used to pass, because browsers strip the tab and read it as `//evil.example`. The return path is now resolved the way the browser resolves it and kept only when it stays on the same site. Otherwise the shopper returns to `/`.
18
+ - c0171ae: Storefront URLs keep only the known ad click IDs again: `fbclid`, `gclid`, `dclid`, `ttclid`, `twclid`, `msclkid`, `gbraid`, `wbraid`, `li_fat_id` and `ScCid`, with `ref` and every `utm_*` parameter. Ordinary keys such as `doc_id`, `src_id` and `braid` are dropped from redirects as other unknown parameters are.
19
+ - 0cbc4da: `parseStorefrontUrl()` leaves `returnTo` out of a sign-in route when the `return` parameter does not resolve to a path on the same site, such as `//evil.example` or `https://evil.example`. A same-site path keeps its query and hash.
20
+
3
21
  ## 0.1.11
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -63,9 +63,21 @@ Prices always come from Zappie. The cart shows a local estimate while it waits,
63
63
  | `@zappie/sdk/server` | Server-side reads, storefront URLs and `verifyWebhook()` |
64
64
  | `@zappie/sdk/template` and `/template/react` | Template settings and responsive images for template authors |
65
65
  | `@zappie/sdk/testing` | A mock client for your tests |
66
+ | `@zappie/sdk/url` | Storefront URL parsing and canonical redirects |
67
+ | `@zappie/sdk/redirect` | Same-origin validation for untrusted redirect targets |
66
68
 
67
69
  Customer sign-in runs through Zappie's site host, so `@zappie/sdk/customer` works on sites Zappie hosts. Sign-in for a site you host yourself isn't available yet.
68
70
 
71
+ Pass any redirect target read from a query parameter through `safeRedirect`.
72
+ It keeps same-origin relative paths and returns your trusted fallback for an
73
+ external, malformed or unsupported target.
74
+
75
+ ```ts
76
+ import { safeRedirect } from "@zappie/sdk/redirect";
77
+
78
+ return redirect(safeRedirect(searchParams.get("returnTo"), "/"));
79
+ ```
80
+
69
81
  Use a publishable key (`pk_test_…` or `pk_live_…`) in browsers. Secret keys work only in `@zappie/sdk/server`. `@zappie/sdk/internal` is for Zappie's own packages and can change in any release.
70
82
 
71
83
  ## Docs
@@ -137,12 +149,15 @@ default storefront does, so every template follows one rule.
137
149
  `fromAccount: true` and no `contactPhone`. Leave `contact_phone` out of the
138
150
  checkout and Zappie uses the customer's verified number. Show the masked
139
151
  number as a line or a placeholder, not as typed text.
140
- - **Address.** `defaultAddress()` returns the last delivery address used on
141
- this device, otherwise the customer's first saved address. Unless the fee
152
+ - **Address.** `defaultAddress()` returns the last delivery address this
153
+ shopper used on this device, otherwise the customer's first saved address.
154
+ An address a guest used is offered only to a guest, and one a customer used
155
+ only to that customer. Unless the fee
142
156
  type is `"fixed"`, an address without a place ID counts as none. The cart's
143
157
  own delivery address always wins in `checkoutDefaults()`.
144
- - **Remembering.** `rememberAddress(address)` keeps the address line, place ID
145
- and coordinates in `localStorage`, on this device only.
158
+ - **Remembering.** `rememberAddress(address, storage, customerId)` keeps the
159
+ address line, place ID, coordinates and the signed-in customer's ID in
160
+ `localStorage`, on this device only. Leave `customerId` out for a guest.
146
161
  `rememberAddress(null)` forgets them. Both do nothing where storage is
147
162
  blocked.
148
163
  - **Region.** `defaultRegion(branch, chosen)` keeps a chosen region and
@@ -154,7 +169,8 @@ default storefront does, so every template follows one rule.
154
169
  returns the same object when nothing changes.
155
170
  - **Sign-in.** `watchCheckoutDefaults(customer, read, listener)` publishes
156
171
  `checkoutDefaults()` now and again each time the shopper signs in or out. On
157
- sign-in it fetches `/me/addresses` first.
172
+ sign-in it fetches `/me/addresses` first. On sign-out it forgets the address
173
+ the customer used on this device.
158
174
 
159
175
  ```ts
160
176
  import {
@@ -177,5 +193,5 @@ const stop = watchCheckoutDefaults(
177
193
  );
178
194
 
179
195
  // When the shopper picks a delivery address:
180
- rememberAddress(address);
196
+ rememberAddress(address, undefined, customer.customer?.id);
181
197
  ```
@@ -1,5 +1,5 @@
1
- import { M as Money, I as Item, a as Menu } from '../models-D__QbqEd.js';
2
1
  import { C as CartLine } from '../types-gQXU2gEA.js';
2
+ import { M as Money, I as Item, a as Menu } from '../models-D__QbqEd.js';
3
3
 
4
4
  /** A line that can move to the destination branch, with its old and new estimates. */
5
5
  type BranchSwitchKeptLine = {
@@ -60,8 +60,12 @@ function planBranchSwitch(lines, from, to) {
60
60
  plan.dropped.push({ line, name });
61
61
  continue;
62
62
  }
63
- const moved = { ...line, menuId };
64
- const next = estimateLine(moved, after);
63
+ const unitPrice = estimateUnitPrice(after, line);
64
+ const next = money(
65
+ unitPrice.amountMinor * line.quantity,
66
+ unitPrice.currency
67
+ );
68
+ const moved = { ...line, menuId, unitPrice, lineSubtotal: next };
65
69
  const previous = before ? estimateLine(line, before) : next;
66
70
  plan.kept.push({ line: moved, name, before: previous, after: next });
67
71
  plan.lines.push(moved);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/cart/branch.ts"],
4
- "sourcesContent": ["// This isolated entry imports the installed core so it does not bundle another copy.\n// eslint-disable-next-line no-restricted-imports\nimport {\n estimateUnitPrice,\n money,\n type CartLine,\n type Item,\n type Menu,\n type Money,\n} from \"@zappie/sdk\";\n\n/** A line that can move to the destination branch, with its old and new estimates. */\nexport type BranchSwitchKeptLine = {\n line: CartLine;\n name: string;\n before: Money;\n after: Money;\n};\n\n/** The lines that can and cannot move to another branch, with the new subtotal. */\nexport type BranchSwitchPlan = {\n kept: BranchSwitchKeptLine[];\n dropped: { line: CartLine; name: string }[];\n lines: CartLine[];\n subtotal: Money | null;\n};\n\n/** Estimate a cart line from its exact variant and selected choices. */\nexport function estimateLine(line: CartLine, item: Item): Money {\n const unit = estimateUnitPrice(item, line);\n return money(unit.amountMinor * line.quantity, unit.currency);\n}\n\nfunction destinationMenu(menu: Menu, line: CartLine) {\n const categories = menu.categories.filter((category) =>\n category.itemIds.includes(line.itemId),\n );\n const sources = menu.menus ?? [\n {\n id: menu.id,\n categoryIds: menu.categories.map((category) => category.id),\n },\n ];\n const placements = menu.items[line.itemId]?.placements;\n const candidates = sources.filter(\n (source) =>\n (!placements ||\n placements.some(\n (placement) =>\n placement.menuId === source.id &&\n source.categoryIds.includes(placement.categoryId),\n )) &&\n source.categoryIds.some((id) =>\n categories.some((category) => category.id === id),\n ),\n );\n return (\n candidates.find((candidate) => candidate.id === line.menuId) ??\n candidates[0]\n )?.id;\n}\n\nfunction sells(line: CartLine, item: Item | undefined): item is Item {\n if (!item?.isAvailable) {\n return false;\n }\n if (\n line.variantId &&\n !item.variants.some(\n (variant) => variant.id === line.variantId && variant.isAvailable,\n )\n ) {\n return false;\n }\n return line.choices.every((selected) =>\n item.optionGroups.some(\n (group) =>\n group.id === selected.groupId &&\n group.choices.some(\n (choice) => choice.id === selected.choiceId && choice.isAvailable,\n ),\n ),\n );\n}\n\n/** Plan which cart lines move to another branch without changing either cart. */\nexport function planBranchSwitch(\n lines: readonly CartLine[],\n from: Menu,\n to: Menu,\n): BranchSwitchPlan {\n const plan: BranchSwitchPlan = {\n kept: [],\n dropped: [],\n lines: [],\n subtotal: null,\n };\n for (const line of lines) {\n const before = from.items[line.itemId];\n const after = to.items[line.itemId];\n const name = before?.name ?? after?.name ?? \"An item\";\n const menuId = destinationMenu(to, line);\n if (!sells(line, after) || !menuId) {\n plan.dropped.push({ line, name });\n continue;\n }\n const moved = { ...line, menuId };\n const next = estimateLine(moved, after);\n const previous = before ? estimateLine(line, before) : next;\n plan.kept.push({ line: moved, name, before: previous, after: next });\n plan.lines.push(moved);\n plan.subtotal = money(\n (plan.subtotal?.amountMinor ?? 0) + next.amountMinor,\n next.currency,\n );\n }\n return plan;\n}\n"],
5
- "mappings": ";AAEA;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AAmBA,SAAS,aAAa,MAAgB,MAAmB;AAC9D,QAAM,OAAO,kBAAkB,MAAM,IAAI;AACzC,SAAO,MAAM,KAAK,cAAc,KAAK,UAAU,KAAK,QAAQ;AAC9D;AAEA,SAAS,gBAAgB,MAAY,MAAgB;AACnD,QAAM,aAAa,KAAK,WAAW;AAAA,IAAO,CAAC,aACzC,SAAS,QAAQ,SAAS,KAAK,MAAM;AAAA,EACvC;AACA,QAAM,UAAU,KAAK,SAAS;AAAA,IAC5B;AAAA,MACE,IAAI,KAAK;AAAA,MACT,aAAa,KAAK,WAAW,IAAI,CAAC,aAAa,SAAS,EAAE;AAAA,IAC5D;AAAA,EACF;AACA,QAAM,aAAa,KAAK,MAAM,KAAK,MAAM,GAAG;AAC5C,QAAM,aAAa,QAAQ;AAAA,IACzB,CAAC,YACE,CAAC,cACA,WAAW;AAAA,MACT,CAAC,cACC,UAAU,WAAW,OAAO,MAC5B,OAAO,YAAY,SAAS,UAAU,UAAU;AAAA,IACpD,MACF,OAAO,YAAY;AAAA,MAAK,CAAC,OACvB,WAAW,KAAK,CAAC,aAAa,SAAS,OAAO,EAAE;AAAA,IAClD;AAAA,EACJ;AACA,UACE,WAAW,KAAK,CAAC,cAAc,UAAU,OAAO,KAAK,MAAM,KAC3D,WAAW,CAAC,IACX;AACL;AAEA,SAAS,MAAM,MAAgB,MAAsC;AACnE,MAAI,CAAC,MAAM,aAAa;AACtB,WAAO;AAAA,EACT;AACA,MACE,KAAK,aACL,CAAC,KAAK,SAAS;AAAA,IACb,CAAC,YAAY,QAAQ,OAAO,KAAK,aAAa,QAAQ;AAAA,EACxD,GACA;AACA,WAAO;AAAA,EACT;AACA,SAAO,KAAK,QAAQ;AAAA,IAAM,CAAC,aACzB,KAAK,aAAa;AAAA,MAChB,CAAC,UACC,MAAM,OAAO,SAAS,WACtB,MAAM,QAAQ;AAAA,QACZ,CAAC,WAAW,OAAO,OAAO,SAAS,YAAY,OAAO;AAAA,MACxD;AAAA,IACJ;AAAA,EACF;AACF;AAGO,SAAS,iBACd,OACA,MACA,IACkB;AAClB,QAAM,OAAyB;AAAA,IAC7B,MAAM,CAAC;AAAA,IACP,SAAS,CAAC;AAAA,IACV,OAAO,CAAC;AAAA,IACR,UAAU;AAAA,EACZ;AACA,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,KAAK,MAAM,KAAK,MAAM;AACrC,UAAM,QAAQ,GAAG,MAAM,KAAK,MAAM;AAClC,UAAM,OAAO,QAAQ,QAAQ,OAAO,QAAQ;AAC5C,UAAM,SAAS,gBAAgB,IAAI,IAAI;AACvC,QAAI,CAAC,MAAM,MAAM,KAAK,KAAK,CAAC,QAAQ;AAClC,WAAK,QAAQ,KAAK,EAAE,MAAM,KAAK,CAAC;AAChC;AAAA,IACF;AACA,UAAM,QAAQ,EAAE,GAAG,MAAM,OAAO;AAChC,UAAM,OAAO,aAAa,OAAO,KAAK;AACtC,UAAM,WAAW,SAAS,aAAa,MAAM,MAAM,IAAI;AACvD,SAAK,KAAK,KAAK,EAAE,MAAM,OAAO,MAAM,QAAQ,UAAU,OAAO,KAAK,CAAC;AACnE,SAAK,MAAM,KAAK,KAAK;AACrB,SAAK,WAAW;AAAA,OACb,KAAK,UAAU,eAAe,KAAK,KAAK;AAAA,MACzC,KAAK;AAAA,IACP;AAAA,EACF;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["// This isolated entry imports the installed core so it does not bundle another copy.\n// eslint-disable-next-line no-restricted-imports\nimport {\n estimateUnitPrice,\n money,\n type CartLine,\n type Item,\n type Menu,\n type Money,\n} from \"@zappie/sdk\";\n\n/** A line that can move to the destination branch, with its old and new estimates. */\nexport type BranchSwitchKeptLine = {\n line: CartLine;\n name: string;\n before: Money;\n after: Money;\n};\n\n/** The lines that can and cannot move to another branch, with the new subtotal. */\nexport type BranchSwitchPlan = {\n kept: BranchSwitchKeptLine[];\n dropped: { line: CartLine; name: string }[];\n lines: CartLine[];\n subtotal: Money | null;\n};\n\n/** Estimate a cart line from its exact variant and selected choices. */\nexport function estimateLine(line: CartLine, item: Item): Money {\n const unit = estimateUnitPrice(item, line);\n return money(unit.amountMinor * line.quantity, unit.currency);\n}\n\nfunction destinationMenu(menu: Menu, line: CartLine) {\n const categories = menu.categories.filter((category) =>\n category.itemIds.includes(line.itemId),\n );\n const sources = menu.menus ?? [\n {\n id: menu.id,\n categoryIds: menu.categories.map((category) => category.id),\n },\n ];\n const placements = menu.items[line.itemId]?.placements;\n const candidates = sources.filter(\n (source) =>\n (!placements ||\n placements.some(\n (placement) =>\n placement.menuId === source.id &&\n source.categoryIds.includes(placement.categoryId),\n )) &&\n source.categoryIds.some((id) =>\n categories.some((category) => category.id === id),\n ),\n );\n return (\n candidates.find((candidate) => candidate.id === line.menuId) ??\n candidates[0]\n )?.id;\n}\n\nfunction sells(line: CartLine, item: Item | undefined): item is Item {\n if (!item?.isAvailable) {\n return false;\n }\n if (\n line.variantId &&\n !item.variants.some(\n (variant) => variant.id === line.variantId && variant.isAvailable,\n )\n ) {\n return false;\n }\n return line.choices.every((selected) =>\n item.optionGroups.some(\n (group) =>\n group.id === selected.groupId &&\n group.choices.some(\n (choice) => choice.id === selected.choiceId && choice.isAvailable,\n ),\n ),\n );\n}\n\n/** Plan which cart lines move to another branch without changing either cart. */\nexport function planBranchSwitch(\n lines: readonly CartLine[],\n from: Menu,\n to: Menu,\n): BranchSwitchPlan {\n const plan: BranchSwitchPlan = {\n kept: [],\n dropped: [],\n lines: [],\n subtotal: null,\n };\n for (const line of lines) {\n const before = from.items[line.itemId];\n const after = to.items[line.itemId];\n const name = before?.name ?? after?.name ?? \"An item\";\n const menuId = destinationMenu(to, line);\n if (!sells(line, after) || !menuId) {\n plan.dropped.push({ line, name });\n continue;\n }\n // A kept line carries the destination's price, so the moved cart never shows the old one.\n const unitPrice = estimateUnitPrice(after, line);\n const next = money(\n unitPrice.amountMinor * line.quantity,\n unitPrice.currency,\n );\n const moved = { ...line, menuId, unitPrice, lineSubtotal: next };\n const previous = before ? estimateLine(line, before) : next;\n plan.kept.push({ line: moved, name, before: previous, after: next });\n plan.lines.push(moved);\n plan.subtotal = money(\n (plan.subtotal?.amountMinor ?? 0) + next.amountMinor,\n next.currency,\n );\n }\n return plan;\n}\n"],
5
+ "mappings": ";AAEA;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AAmBA,SAAS,aAAa,MAAgB,MAAmB;AAC9D,QAAM,OAAO,kBAAkB,MAAM,IAAI;AACzC,SAAO,MAAM,KAAK,cAAc,KAAK,UAAU,KAAK,QAAQ;AAC9D;AAEA,SAAS,gBAAgB,MAAY,MAAgB;AACnD,QAAM,aAAa,KAAK,WAAW;AAAA,IAAO,CAAC,aACzC,SAAS,QAAQ,SAAS,KAAK,MAAM;AAAA,EACvC;AACA,QAAM,UAAU,KAAK,SAAS;AAAA,IAC5B;AAAA,MACE,IAAI,KAAK;AAAA,MACT,aAAa,KAAK,WAAW,IAAI,CAAC,aAAa,SAAS,EAAE;AAAA,IAC5D;AAAA,EACF;AACA,QAAM,aAAa,KAAK,MAAM,KAAK,MAAM,GAAG;AAC5C,QAAM,aAAa,QAAQ;AAAA,IACzB,CAAC,YACE,CAAC,cACA,WAAW;AAAA,MACT,CAAC,cACC,UAAU,WAAW,OAAO,MAC5B,OAAO,YAAY,SAAS,UAAU,UAAU;AAAA,IACpD,MACF,OAAO,YAAY;AAAA,MAAK,CAAC,OACvB,WAAW,KAAK,CAAC,aAAa,SAAS,OAAO,EAAE;AAAA,IAClD;AAAA,EACJ;AACA,UACE,WAAW,KAAK,CAAC,cAAc,UAAU,OAAO,KAAK,MAAM,KAC3D,WAAW,CAAC,IACX;AACL;AAEA,SAAS,MAAM,MAAgB,MAAsC;AACnE,MAAI,CAAC,MAAM,aAAa;AACtB,WAAO;AAAA,EACT;AACA,MACE,KAAK,aACL,CAAC,KAAK,SAAS;AAAA,IACb,CAAC,YAAY,QAAQ,OAAO,KAAK,aAAa,QAAQ;AAAA,EACxD,GACA;AACA,WAAO;AAAA,EACT;AACA,SAAO,KAAK,QAAQ;AAAA,IAAM,CAAC,aACzB,KAAK,aAAa;AAAA,MAChB,CAAC,UACC,MAAM,OAAO,SAAS,WACtB,MAAM,QAAQ;AAAA,QACZ,CAAC,WAAW,OAAO,OAAO,SAAS,YAAY,OAAO;AAAA,MACxD;AAAA,IACJ;AAAA,EACF;AACF;AAGO,SAAS,iBACd,OACA,MACA,IACkB;AAClB,QAAM,OAAyB;AAAA,IAC7B,MAAM,CAAC;AAAA,IACP,SAAS,CAAC;AAAA,IACV,OAAO,CAAC;AAAA,IACR,UAAU;AAAA,EACZ;AACA,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,KAAK,MAAM,KAAK,MAAM;AACrC,UAAM,QAAQ,GAAG,MAAM,KAAK,MAAM;AAClC,UAAM,OAAO,QAAQ,QAAQ,OAAO,QAAQ;AAC5C,UAAM,SAAS,gBAAgB,IAAI,IAAI;AACvC,QAAI,CAAC,MAAM,MAAM,KAAK,KAAK,CAAC,QAAQ;AAClC,WAAK,QAAQ,KAAK,EAAE,MAAM,KAAK,CAAC;AAChC;AAAA,IACF;AAEA,UAAM,YAAY,kBAAkB,OAAO,IAAI;AAC/C,UAAM,OAAO;AAAA,MACX,UAAU,cAAc,KAAK;AAAA,MAC7B,UAAU;AAAA,IACZ;AACA,UAAM,QAAQ,EAAE,GAAG,MAAM,QAAQ,WAAW,cAAc,KAAK;AAC/D,UAAM,WAAW,SAAS,aAAa,MAAM,MAAM,IAAI;AACvD,SAAK,KAAK,KAAK,EAAE,MAAM,OAAO,MAAM,QAAQ,UAAU,OAAO,KAAK,CAAC;AACnE,SAAK,MAAM,KAAK,KAAK;AACrB,SAAK,WAAW;AAAA,OACb,KAAK,UAAU,eAAe,KAAK,KAAK;AAAA,MACzC,KAAK;AAAA,IACP;AAAA,EACF;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import { Z as Zappie } from '../client-DzUauU-b.js';
1
+ import { Z as Zappie } from '../client-CSOZOwQW.js';
2
2
  import { I as Item } from '../models-D__QbqEd.js';
3
3
  import '../types-gQXU2gEA.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { T as Transport, R as RequestCtx } from '../client-DzUauU-b.js';
1
+ import { T as Transport, R as RequestCtx } from '../client-CSOZOwQW.js';
2
2
  import { Q as QuoteRequest, a as Quote } from '../types-gQXU2gEA.js';
3
3
  import '../models-D__QbqEd.js';
4
4
 
@@ -42,7 +42,7 @@ type CheckoutDefaultsInput = {
42
42
  */
43
43
  type CheckoutDefaults = {
44
44
  phone: PhoneDefault;
45
- /** The cart's delivery address, else the last one used on this device, else the first saved one. */
45
+ /** The cart's delivery address, else the last one this shopper used on this device, else the first saved one. */
46
46
  address?: DeliveryAddress;
47
47
  /** The cart's region, else the branch's only region. */
48
48
  deliveryRegionId?: string;
@@ -58,33 +58,39 @@ type CheckoutDefaults = {
58
58
  */
59
59
  declare function defaultPhone(typed: string | undefined, customer: SessionCustomer | null | undefined): PhoneDefault;
60
60
  /**
61
- * The last delivery address used on this device, or undefined. Under distance or region fees,
62
- * an entry without a place ID counts as none and is removed.
61
+ * The last delivery address this shopper used on this device, or undefined. An address a guest
62
+ * used is offered only to a guest, and one a customer used only to that customer. Under
63
+ * distance or region fees, an entry without a place ID counts as none and is removed.
63
64
  *
64
- * @param options - The fee type and storage.
65
+ * @param options - The fee type, storage and signed-in customer's ID.
65
66
  */
66
67
  declare function rememberedAddress(options?: {
67
68
  feeType?: DeliveryFeeType;
68
69
  storage?: AddressStorage | null;
70
+ customerId?: string | null;
69
71
  }): DeliveryAddress | undefined;
70
72
  /**
71
73
  * Remember the delivery address the shopper used, on this device only. Stores the address
72
- * line, place ID and coordinates, nothing else. Does nothing where storage is blocked.
74
+ * line, place ID, coordinates and the customer's ID, nothing else. Does nothing where storage
75
+ * is blocked.
73
76
  *
74
77
  * @param value - The address, or null to forget it.
75
78
  * @param storage - Defaults to `localStorage`.
79
+ * @param customerId - The signed-in customer's ID. Leave it out for a guest.
76
80
  */
77
- declare function rememberAddress(value: DeliveryAddress | null, storage?: AddressStorage | null): void;
81
+ declare function rememberAddress(value: DeliveryAddress | null, storage?: AddressStorage | null, customerId?: string | null): void;
78
82
  /**
79
- * The address to start from: the last one used on this device, otherwise the customer's
80
- * first saved address. Under distance or region fees, an address without a place ID counts as none.
83
+ * The address to start from: the last one this shopper used on this device, otherwise the
84
+ * customer's first saved address. Under distance or region fees, an address without a place
85
+ * ID counts as none.
81
86
  *
82
- * @param options - Saved addresses, fee type and storage.
87
+ * @param options - Saved addresses, fee type, storage and the signed-in customer's ID.
83
88
  */
84
89
  declare function defaultAddress(options?: {
85
90
  savedAddresses?: readonly SavedAddress[];
86
91
  feeType?: DeliveryFeeType;
87
92
  storage?: AddressStorage | null;
93
+ customerId?: string | null;
88
94
  }): DeliveryAddress | undefined;
89
95
  /**
90
96
  * The region to start from: the one already chosen, otherwise the branch's only region.
@@ -118,8 +124,8 @@ declare function checkoutDefaults(input: CheckoutDefaultsInput): CheckoutDefault
118
124
  declare function prefillFulfilment<T extends CartFulfilment | null>(fulfilment: T, branch: Branch): T;
119
125
  /**
120
126
  * Work out the defaults now and again whenever the shopper signs in or out. On sign-in it
121
- * fetches the saved addresses first. A failed fetch counts as none. A slower, older run never
122
- * lands last.
127
+ * fetches the saved addresses first. On sign-out it forgets the address the customer used on
128
+ * this device. A failed fetch counts as none. A slower, older run never lands last.
123
129
  *
124
130
  * @param customer - The customer client.
125
131
  * @param read - Returns the current inputs, read on each run.
@@ -41,6 +41,9 @@ function rememberedAddress(options = {}) {
41
41
  return void 0;
42
42
  }
43
43
  const stored = attempt(() => JSON.parse(raw));
44
+ if (text(stored?.customerId) !== (options.customerId || void 0)) {
45
+ return void 0;
46
+ }
44
47
  const placeId = text(stored?.placeId);
45
48
  if (!placeId && options.feeType !== "fixed") {
46
49
  attempt(() => storage?.removeItem(KEY));
@@ -53,7 +56,7 @@ function rememberedAddress(options = {}) {
53
56
  stored?.longitude
54
57
  );
55
58
  }
56
- function rememberAddress(value, storage) {
59
+ function rememberAddress(value, storage, customerId) {
57
60
  const target = local(storage);
58
61
  if (!target) {
59
62
  return;
@@ -66,6 +69,7 @@ function rememberAddress(value, storage) {
66
69
  () => target.setItem(
67
70
  KEY,
68
71
  JSON.stringify({
72
+ ...customerId ? { customerId } : {},
69
73
  address: value.line1,
70
74
  placeId: value.placeId ?? "",
71
75
  latitude: value.coordinates?.lat,
@@ -74,6 +78,14 @@ function rememberAddress(value, storage) {
74
78
  )
75
79
  );
76
80
  }
81
+ function forget(customerId, storage) {
82
+ const target = local(storage);
83
+ const raw = target ? attempt(() => target.getItem(KEY)) : void 0;
84
+ const stored = raw ? attempt(() => JSON.parse(raw)) : void 0;
85
+ if (text(stored?.customerId) === customerId) {
86
+ attempt(() => target?.removeItem(KEY));
87
+ }
88
+ }
77
89
  function defaultAddress(options = {}) {
78
90
  const remembered = rememberedAddress(options);
79
91
  if (remembered) {
@@ -99,7 +111,7 @@ function restoreSchedule(fulfilment, branchId) {
99
111
  }
100
112
  function checkoutDefaults(input) {
101
113
  const delivery = input.fulfilment?.type === "delivery" ? input.fulfilment : void 0;
102
- const address2 = delivery?.address ?? defaultAddress(input);
114
+ const address2 = delivery?.address ?? defaultAddress({ ...input, customerId: input.customer?.id ?? null });
103
115
  const deliveryRegionId = defaultRegion(
104
116
  input.branch,
105
117
  delivery?.deliveryRegionId
@@ -148,6 +160,9 @@ function watchCheckoutDefaults(customer, read, listener) {
148
160
  const refresh = () => {
149
161
  const current = ++run;
150
162
  const id = customer.state === "authenticated" ? customer.customer?.id : void 0;
163
+ if (signedIn && signedIn !== id) {
164
+ forget(signedIn, read().storage);
165
+ }
151
166
  signedIn = id;
152
167
  if (!id) {
153
168
  publish(void 0);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/checkout-defaults/index.ts"],
4
- "sourcesContent": ["// The declarations import the installed core so shared types keep one identity.\n// eslint-disable-next-line no-restricted-imports\nimport type { Branch, CartFulfilment, DeliveryAddress } from \"@zappie/sdk\";\n// eslint-disable-next-line no-restricted-imports\nimport type {\n CustomerClient,\n SavedAddress,\n SessionCustomer,\n} from \"@zappie/sdk/customer\";\n\n/**\n * How the branch prices delivery. Only `\"fixed\"` lets an address without a place ID stand.\n */\nexport type DeliveryFeeType = \"region\" | \"distance\" | \"fixed\";\n\n/**\n * The browser storage the remembered address uses. Pass `null` to remember nothing.\n */\nexport type AddressStorage = Pick<\n Storage,\n \"getItem\" | \"setItem\" | \"removeItem\"\n>;\n\n/**\n * The phone a checkout shows, and the `contact_phone` to send.\n */\nexport type PhoneDefault = {\n /** The typed number, else the signed-in customer's masked number, else \"\". */\n display: string;\n /** The typed number. Absent when the account's number applies: checkout then uses the verified number. */\n contactPhone?: string;\n /** True when `display` is the signed-in customer's masked number. */\n fromAccount: boolean;\n};\n\n/**\n * What the defaults are worked out from.\n */\nexport type CheckoutDefaultsInput = {\n branch: Branch;\n /** The cart's fulfilment. Its type and its address are the shopper's choice and are kept. */\n fulfilment: CartFulfilment | null;\n customer?: SessionCustomer | null;\n /** The signed-in customer's saved addresses, in the order `/me/addresses` returns them. */\n savedAddresses?: readonly SavedAddress[];\n /** The phone typed in this session. */\n typedPhone?: string;\n /** Defaults to `\"distance\"`, as the default storefront does. */\n feeType?: DeliveryFeeType;\n /** Defaults to `localStorage`. */\n storage?: AddressStorage | null;\n};\n\n/**\n * The contact details a checkout starts from.\n */\nexport type CheckoutDefaults = {\n phone: PhoneDefault;\n /** The cart's delivery address, else the last one used on this device, else the first saved one. */\n address?: DeliveryAddress;\n /** The cart's region, else the branch's only region. */\n deliveryRegionId?: string;\n /** The cart's scheduled time when it belongs to this branch and parses. */\n scheduledFor?: string;\n};\n\n// The default storefront's key and shape, so a host page and its templates share one entry.\nconst KEY = \"zappie_delivery_address\";\n\ntype Stored = {\n address?: unknown;\n placeId?: unknown;\n latitude?: unknown;\n longitude?: unknown;\n};\n\nfunction local(storage: AddressStorage | null | undefined) {\n if (storage !== undefined) {\n return storage;\n }\n try {\n return typeof window === \"undefined\" ? null : window.localStorage;\n } catch {\n return null;\n }\n}\n\nfunction attempt<T>(fn: () => T): T | undefined {\n try {\n return fn();\n } catch {\n return undefined;\n }\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\nfunction point(lat: unknown, lng: unknown) {\n return typeof lat === \"number\" &&\n typeof lng === \"number\" &&\n Number.isFinite(lat) &&\n Number.isFinite(lng)\n ? { coordinates: { lat, lng } }\n : {};\n}\n\nfunction address(\n line1: string | undefined,\n placeId: string | undefined,\n lat: unknown,\n lng: unknown,\n): DeliveryAddress | undefined {\n return line1\n ? { line1, ...(placeId ? { placeId } : {}), ...point(lat, lng) }\n : undefined;\n}\n\n/**\n * The phone to show: the one typed in this session, otherwise the signed-in customer's.\n * The session holds only a masked number, so send no `contact_phone` for it.\n *\n * @param typed - The phone typed in this session.\n * @param customer - The signed-in customer, or null.\n */\nexport function defaultPhone(\n typed: string | undefined,\n customer: SessionCustomer | null | undefined,\n): PhoneDefault {\n const value = typed?.trim();\n if (value) {\n return { display: value, contactPhone: value, fromAccount: false };\n }\n const masked = customer?.phone_masked;\n return masked\n ? { display: masked, fromAccount: true }\n : { display: \"\", fromAccount: false };\n}\n\n/**\n * The last delivery address used on this device, or undefined. Under distance or region fees,\n * an entry without a place ID counts as none and is removed.\n *\n * @param options - The fee type and storage.\n */\nexport function rememberedAddress(\n options: { feeType?: DeliveryFeeType; storage?: AddressStorage | null } = {},\n): DeliveryAddress | undefined {\n const storage = local(options.storage);\n const raw = storage ? attempt(() => storage.getItem(KEY)) : undefined;\n if (!raw) {\n return undefined;\n }\n const stored = attempt(() => JSON.parse(raw) as Stored);\n const placeId = text(stored?.placeId);\n if (!placeId && options.feeType !== \"fixed\") {\n attempt(() => storage?.removeItem(KEY));\n return undefined;\n }\n return address(\n text(stored?.address),\n placeId,\n stored?.latitude,\n stored?.longitude,\n );\n}\n\n/**\n * Remember the delivery address the shopper used, on this device only. Stores the address\n * line, place ID and coordinates, nothing else. Does nothing where storage is blocked.\n *\n * @param value - The address, or null to forget it.\n * @param storage - Defaults to `localStorage`.\n */\nexport function rememberAddress(\n value: DeliveryAddress | null,\n storage?: AddressStorage | null,\n): void {\n const target = local(storage);\n if (!target) {\n return;\n }\n if (!value?.line1.trim()) {\n attempt(() => target.removeItem(KEY));\n return;\n }\n attempt(() =>\n target.setItem(\n KEY,\n JSON.stringify({\n address: value.line1,\n placeId: value.placeId ?? \"\",\n latitude: value.coordinates?.lat,\n longitude: value.coordinates?.lng,\n }),\n ),\n );\n}\n\n/**\n * The address to start from: the last one used on this device, otherwise the customer's\n * first saved address. Under distance or region fees, an address without a place ID counts as none.\n *\n * @param options - Saved addresses, fee type and storage.\n */\nexport function defaultAddress(\n options: {\n savedAddresses?: readonly SavedAddress[];\n feeType?: DeliveryFeeType;\n storage?: AddressStorage | null;\n } = {},\n): DeliveryAddress | undefined {\n const remembered = rememberedAddress(options);\n if (remembered) {\n return remembered;\n }\n const first = options.savedAddresses?.[0];\n const placeId = text(first?.place_id);\n if (!first || (!placeId && options.feeType !== \"fixed\")) {\n return undefined;\n }\n return address(text(first.line1), placeId, first.lat, first.lng);\n}\n\n/**\n * The region to start from: the one already chosen, otherwise the branch's only region.\n *\n * @param branch - The cart's branch.\n * @param chosen - The region already chosen.\n */\nexport function defaultRegion(\n branch: Branch,\n chosen?: string,\n): string | undefined {\n if (chosen) {\n return chosen;\n }\n const regions = branch.deliveryRegions ?? [];\n return regions.length === 1 ? regions[0]?.id : undefined;\n}\n\n/**\n * The cart's scheduled time if it belongs to this branch and parses, otherwise undefined.\n * Drop a time this returns undefined for: it can never become valid here.\n *\n * @param fulfilment - The cart's fulfilment.\n * @param branchId - The branch the shopper is ordering from.\n */\nexport function restoreSchedule(\n fulfilment: CartFulfilment | null,\n branchId: string,\n): string | undefined {\n const at =\n fulfilment && fulfilment.type !== \"dine_in\"\n ? fulfilment.scheduledFor\n : undefined;\n return at &&\n fulfilment?.branchId === branchId &&\n Number.isFinite(Date.parse(at))\n ? at\n : undefined;\n}\n\n/**\n * The contact details a checkout starts from, by the default storefront's rule.\n * It reads the cart and never changes it, so the fulfilment type stays the shopper's.\n *\n * @param input - The branch, cart fulfilment, customer and what was typed.\n */\nexport function checkoutDefaults(\n input: CheckoutDefaultsInput,\n): CheckoutDefaults {\n const delivery =\n input.fulfilment?.type === \"delivery\" ? input.fulfilment : undefined;\n const address = delivery?.address ?? defaultAddress(input);\n const deliveryRegionId = defaultRegion(\n input.branch,\n delivery?.deliveryRegionId,\n );\n const scheduledFor = restoreSchedule(input.fulfilment, input.branch.id);\n return {\n phone: defaultPhone(input.typedPhone, input.customer),\n ...(address ? { address } : {}),\n ...(deliveryRegionId ? { deliveryRegionId } : {}),\n ...(scheduledFor ? { scheduledFor } : {}),\n };\n}\n\n/**\n * The cart fulfilment with the defaults applied: the only region filled in and a stale\n * schedule dropped. Same type, same address. Returns the same object when nothing changes.\n *\n * @param fulfilment - The cart's fulfilment.\n * @param branch - The cart's branch.\n */\nexport function prefillFulfilment<T extends CartFulfilment | null>(\n fulfilment: T,\n branch: Branch,\n): T {\n if (!fulfilment || fulfilment.type === \"dine_in\") {\n return fulfilment;\n }\n const scheduledFor = restoreSchedule(fulfilment, branch.id);\n const regionId =\n fulfilment.type === \"delivery\"\n ? defaultRegion(branch, fulfilment.deliveryRegionId)\n : undefined;\n const dropSchedule = fulfilment.scheduledFor !== undefined && !scheduledFor;\n const addRegion =\n fulfilment.type === \"delivery\" && regionId !== fulfilment.deliveryRegionId;\n if (!dropSchedule && !addRegion) {\n return fulfilment;\n }\n const next: Exclude<CartFulfilment, { type: \"dine_in\" }> = { ...fulfilment };\n if (dropSchedule) {\n delete next.scheduledFor;\n }\n if (addRegion && next.type === \"delivery\" && regionId) {\n next.deliveryRegionId = regionId;\n }\n return next as T;\n}\n\n/**\n * Work out the defaults now and again whenever the shopper signs in or out. On sign-in it\n * fetches the saved addresses first. A failed fetch counts as none. A slower, older run never\n * lands last.\n *\n * @param customer - The customer client.\n * @param read - Returns the current inputs, read on each run.\n * @param listener - Receives each result.\n * @returns Stops listening.\n */\nexport function watchCheckoutDefaults(\n customer: Pick<\n CustomerClient,\n \"state\" | \"customer\" | \"onChange\" | \"addresses\"\n >,\n read: () => Omit<CheckoutDefaultsInput, \"customer\" | \"savedAddresses\">,\n listener: (defaults: CheckoutDefaults) => void,\n): () => void {\n let run = 0;\n let stopped = false;\n let signedIn: string | undefined;\n const publish = (savedAddresses: readonly SavedAddress[] | undefined) => {\n listener(\n checkoutDefaults({\n ...read(),\n customer: customer.customer,\n ...(savedAddresses ? { savedAddresses } : {}),\n }),\n );\n };\n const refresh = () => {\n const current = ++run;\n const id =\n customer.state === \"authenticated\" ? customer.customer?.id : undefined;\n signedIn = id;\n if (!id) {\n publish(undefined);\n return;\n }\n void customer.addresses().then(\n (saved) => {\n if (!stopped && current === run) {\n publish(saved);\n }\n },\n () => {\n if (!stopped && current === run) {\n publish(undefined);\n }\n },\n );\n };\n const off = customer.onChange((state, next) => {\n const id = state === \"authenticated\" ? next?.id : undefined;\n if (id !== signedIn) {\n refresh();\n }\n });\n refresh();\n return () => {\n stopped = true;\n off();\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// The declarations import the installed core so shared types keep one identity.\n// eslint-disable-next-line no-restricted-imports\nimport type { Branch, CartFulfilment, DeliveryAddress } from \"@zappie/sdk\";\n// eslint-disable-next-line no-restricted-imports\nimport type {\n CustomerClient,\n SavedAddress,\n SessionCustomer,\n} from \"@zappie/sdk/customer\";\n\n/**\n * How the branch prices delivery. Only `\"fixed\"` lets an address without a place ID stand.\n */\nexport type DeliveryFeeType = \"region\" | \"distance\" | \"fixed\";\n\n/**\n * The browser storage the remembered address uses. Pass `null` to remember nothing.\n */\nexport type AddressStorage = Pick<\n Storage,\n \"getItem\" | \"setItem\" | \"removeItem\"\n>;\n\n/**\n * The phone a checkout shows, and the `contact_phone` to send.\n */\nexport type PhoneDefault = {\n /** The typed number, else the signed-in customer's masked number, else \"\". */\n display: string;\n /** The typed number. Absent when the account's number applies: checkout then uses the verified number. */\n contactPhone?: string;\n /** True when `display` is the signed-in customer's masked number. */\n fromAccount: boolean;\n};\n\n/**\n * What the defaults are worked out from.\n */\nexport type CheckoutDefaultsInput = {\n branch: Branch;\n /** The cart's fulfilment. Its type and its address are the shopper's choice and are kept. */\n fulfilment: CartFulfilment | null;\n customer?: SessionCustomer | null;\n /** The signed-in customer's saved addresses, in the order `/me/addresses` returns them. */\n savedAddresses?: readonly SavedAddress[];\n /** The phone typed in this session. */\n typedPhone?: string;\n /** Defaults to `\"distance\"`, as the default storefront does. */\n feeType?: DeliveryFeeType;\n /** Defaults to `localStorage`. */\n storage?: AddressStorage | null;\n};\n\n/**\n * The contact details a checkout starts from.\n */\nexport type CheckoutDefaults = {\n phone: PhoneDefault;\n /** The cart's delivery address, else the last one this shopper used on this device, else the first saved one. */\n address?: DeliveryAddress;\n /** The cart's region, else the branch's only region. */\n deliveryRegionId?: string;\n /** The cart's scheduled time when it belongs to this branch and parses. */\n scheduledFor?: string;\n};\n\n// The default storefront's key and shape, so a host page and its templates share one entry.\nconst KEY = \"zappie_delivery_address\";\n\ntype Stored = {\n /** The signed-in customer who used the address. Absent for a guest. */\n customerId?: unknown;\n address?: unknown;\n placeId?: unknown;\n latitude?: unknown;\n longitude?: unknown;\n};\n\nfunction local(storage: AddressStorage | null | undefined) {\n if (storage !== undefined) {\n return storage;\n }\n try {\n return typeof window === \"undefined\" ? null : window.localStorage;\n } catch {\n return null;\n }\n}\n\nfunction attempt<T>(fn: () => T): T | undefined {\n try {\n return fn();\n } catch {\n return undefined;\n }\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\nfunction point(lat: unknown, lng: unknown) {\n return typeof lat === \"number\" &&\n typeof lng === \"number\" &&\n Number.isFinite(lat) &&\n Number.isFinite(lng)\n ? { coordinates: { lat, lng } }\n : {};\n}\n\nfunction address(\n line1: string | undefined,\n placeId: string | undefined,\n lat: unknown,\n lng: unknown,\n): DeliveryAddress | undefined {\n return line1\n ? { line1, ...(placeId ? { placeId } : {}), ...point(lat, lng) }\n : undefined;\n}\n\n/**\n * The phone to show: the one typed in this session, otherwise the signed-in customer's.\n * The session holds only a masked number, so send no `contact_phone` for it.\n *\n * @param typed - The phone typed in this session.\n * @param customer - The signed-in customer, or null.\n */\nexport function defaultPhone(\n typed: string | undefined,\n customer: SessionCustomer | null | undefined,\n): PhoneDefault {\n const value = typed?.trim();\n if (value) {\n return { display: value, contactPhone: value, fromAccount: false };\n }\n const masked = customer?.phone_masked;\n return masked\n ? { display: masked, fromAccount: true }\n : { display: \"\", fromAccount: false };\n}\n\n/**\n * The last delivery address this shopper used on this device, or undefined. An address a guest\n * used is offered only to a guest, and one a customer used only to that customer. Under\n * distance or region fees, an entry without a place ID counts as none and is removed.\n *\n * @param options - The fee type, storage and signed-in customer's ID.\n */\nexport function rememberedAddress(\n options: {\n feeType?: DeliveryFeeType;\n storage?: AddressStorage | null;\n customerId?: string | null;\n } = {},\n): DeliveryAddress | undefined {\n const storage = local(options.storage);\n const raw = storage ? attempt(() => storage.getItem(KEY)) : undefined;\n if (!raw) {\n return undefined;\n }\n const stored = attempt(() => JSON.parse(raw) as Stored);\n if (text(stored?.customerId) !== (options.customerId || undefined)) {\n return undefined;\n }\n const placeId = text(stored?.placeId);\n if (!placeId && options.feeType !== \"fixed\") {\n attempt(() => storage?.removeItem(KEY));\n return undefined;\n }\n return address(\n text(stored?.address),\n placeId,\n stored?.latitude,\n stored?.longitude,\n );\n}\n\n/**\n * Remember the delivery address the shopper used, on this device only. Stores the address\n * line, place ID, coordinates and the customer's ID, nothing else. Does nothing where storage\n * is blocked.\n *\n * @param value - The address, or null to forget it.\n * @param storage - Defaults to `localStorage`.\n * @param customerId - The signed-in customer's ID. Leave it out for a guest.\n */\nexport function rememberAddress(\n value: DeliveryAddress | null,\n storage?: AddressStorage | null,\n customerId?: string | null,\n): void {\n const target = local(storage);\n if (!target) {\n return;\n }\n if (!value?.line1.trim()) {\n attempt(() => target.removeItem(KEY));\n return;\n }\n attempt(() =>\n target.setItem(\n KEY,\n JSON.stringify({\n ...(customerId ? { customerId } : {}),\n address: value.line1,\n placeId: value.placeId ?? \"\",\n latitude: value.coordinates?.lat,\n longitude: value.coordinates?.lng,\n }),\n ),\n );\n}\n\n// Remove the remembered address if this customer used it, so the next shopper never sees it.\nfunction forget(\n customerId: string,\n storage: AddressStorage | null | undefined,\n) {\n const target = local(storage);\n const raw = target ? attempt(() => target.getItem(KEY)) : undefined;\n const stored = raw ? attempt(() => JSON.parse(raw) as Stored) : undefined;\n if (text(stored?.customerId) === customerId) {\n attempt(() => target?.removeItem(KEY));\n }\n}\n\n/**\n * The address to start from: the last one this shopper used on this device, otherwise the\n * customer's first saved address. Under distance or region fees, an address without a place\n * ID counts as none.\n *\n * @param options - Saved addresses, fee type, storage and the signed-in customer's ID.\n */\nexport function defaultAddress(\n options: {\n savedAddresses?: readonly SavedAddress[];\n feeType?: DeliveryFeeType;\n storage?: AddressStorage | null;\n customerId?: string | null;\n } = {},\n): DeliveryAddress | undefined {\n const remembered = rememberedAddress(options);\n if (remembered) {\n return remembered;\n }\n const first = options.savedAddresses?.[0];\n const placeId = text(first?.place_id);\n if (!first || (!placeId && options.feeType !== \"fixed\")) {\n return undefined;\n }\n return address(text(first.line1), placeId, first.lat, first.lng);\n}\n\n/**\n * The region to start from: the one already chosen, otherwise the branch's only region.\n *\n * @param branch - The cart's branch.\n * @param chosen - The region already chosen.\n */\nexport function defaultRegion(\n branch: Branch,\n chosen?: string,\n): string | undefined {\n if (chosen) {\n return chosen;\n }\n const regions = branch.deliveryRegions ?? [];\n return regions.length === 1 ? regions[0]?.id : undefined;\n}\n\n/**\n * The cart's scheduled time if it belongs to this branch and parses, otherwise undefined.\n * Drop a time this returns undefined for: it can never become valid here.\n *\n * @param fulfilment - The cart's fulfilment.\n * @param branchId - The branch the shopper is ordering from.\n */\nexport function restoreSchedule(\n fulfilment: CartFulfilment | null,\n branchId: string,\n): string | undefined {\n const at =\n fulfilment && fulfilment.type !== \"dine_in\"\n ? fulfilment.scheduledFor\n : undefined;\n return at &&\n fulfilment?.branchId === branchId &&\n Number.isFinite(Date.parse(at))\n ? at\n : undefined;\n}\n\n/**\n * The contact details a checkout starts from, by the default storefront's rule.\n * It reads the cart and never changes it, so the fulfilment type stays the shopper's.\n *\n * @param input - The branch, cart fulfilment, customer and what was typed.\n */\nexport function checkoutDefaults(\n input: CheckoutDefaultsInput,\n): CheckoutDefaults {\n const delivery =\n input.fulfilment?.type === \"delivery\" ? input.fulfilment : undefined;\n const address =\n delivery?.address ??\n defaultAddress({ ...input, customerId: input.customer?.id ?? null });\n const deliveryRegionId = defaultRegion(\n input.branch,\n delivery?.deliveryRegionId,\n );\n const scheduledFor = restoreSchedule(input.fulfilment, input.branch.id);\n return {\n phone: defaultPhone(input.typedPhone, input.customer),\n ...(address ? { address } : {}),\n ...(deliveryRegionId ? { deliveryRegionId } : {}),\n ...(scheduledFor ? { scheduledFor } : {}),\n };\n}\n\n/**\n * The cart fulfilment with the defaults applied: the only region filled in and a stale\n * schedule dropped. Same type, same address. Returns the same object when nothing changes.\n *\n * @param fulfilment - The cart's fulfilment.\n * @param branch - The cart's branch.\n */\nexport function prefillFulfilment<T extends CartFulfilment | null>(\n fulfilment: T,\n branch: Branch,\n): T {\n if (!fulfilment || fulfilment.type === \"dine_in\") {\n return fulfilment;\n }\n const scheduledFor = restoreSchedule(fulfilment, branch.id);\n const regionId =\n fulfilment.type === \"delivery\"\n ? defaultRegion(branch, fulfilment.deliveryRegionId)\n : undefined;\n const dropSchedule = fulfilment.scheduledFor !== undefined && !scheduledFor;\n const addRegion =\n fulfilment.type === \"delivery\" && regionId !== fulfilment.deliveryRegionId;\n if (!dropSchedule && !addRegion) {\n return fulfilment;\n }\n const next: Exclude<CartFulfilment, { type: \"dine_in\" }> = { ...fulfilment };\n if (dropSchedule) {\n delete next.scheduledFor;\n }\n if (addRegion && next.type === \"delivery\" && regionId) {\n next.deliveryRegionId = regionId;\n }\n return next as T;\n}\n\n/**\n * Work out the defaults now and again whenever the shopper signs in or out. On sign-in it\n * fetches the saved addresses first. On sign-out it forgets the address the customer used on\n * this device. A failed fetch counts as none. A slower, older run never lands last.\n *\n * @param customer - The customer client.\n * @param read - Returns the current inputs, read on each run.\n * @param listener - Receives each result.\n * @returns Stops listening.\n */\nexport function watchCheckoutDefaults(\n customer: Pick<\n CustomerClient,\n \"state\" | \"customer\" | \"onChange\" | \"addresses\"\n >,\n read: () => Omit<CheckoutDefaultsInput, \"customer\" | \"savedAddresses\">,\n listener: (defaults: CheckoutDefaults) => void,\n): () => void {\n let run = 0;\n let stopped = false;\n let signedIn: string | undefined;\n const publish = (savedAddresses: readonly SavedAddress[] | undefined) => {\n listener(\n checkoutDefaults({\n ...read(),\n customer: customer.customer,\n ...(savedAddresses ? { savedAddresses } : {}),\n }),\n );\n };\n const refresh = () => {\n const current = ++run;\n const id =\n customer.state === \"authenticated\" ? customer.customer?.id : undefined;\n if (signedIn && signedIn !== id) {\n forget(signedIn, read().storage);\n }\n signedIn = id;\n if (!id) {\n publish(undefined);\n return;\n }\n void customer.addresses().then(\n (saved) => {\n if (!stopped && current === run) {\n publish(saved);\n }\n },\n () => {\n if (!stopped && current === run) {\n publish(undefined);\n }\n },\n );\n };\n const off = customer.onChange((state, next) => {\n const id = state === \"authenticated\" ? next?.id : undefined;\n if (id !== signedIn) {\n refresh();\n }\n });\n refresh();\n return () => {\n stopped = true;\n off();\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["address"]
7
7
  }
@@ -35,6 +35,16 @@ type ZappieSiteConfig = {
35
35
  outbound?: {
36
36
  allow?: string[];
37
37
  };
38
+ /** Add sources to the fixed storefront Content Security Policy. */
39
+ security?: {
40
+ csp?: {
41
+ imgSrc?: string[];
42
+ connectSrc?: string[];
43
+ fontSrc?: string[];
44
+ styleSrc?: string[];
45
+ frameSrc?: string[];
46
+ };
47
+ };
38
48
  /** Each budget left out uses DEFAULT_BUDGETS. */
39
49
  budgets?: {
40
50
  jsKbGz?: number;
@@ -1,12 +1,7 @@
1
1
  // src/customer/navigation.ts
2
+ import { sameOriginPath } from "../internal/return-path.js";
2
3
  function safeReturnTo(value) {
3
- if (!value || !value.startsWith("/") || value.startsWith("//")) {
4
- return "/";
5
- }
6
- if (value.startsWith("/\\")) {
7
- return "/";
8
- }
9
- return value;
4
+ return sameOriginPath(value) ?? "/";
10
5
  }
11
6
  function currentPath() {
12
7
  if (typeof location === "undefined") {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/customer/navigation.ts"],
4
- "sourcesContent": ["export function safeReturnTo(value: string | undefined): string {\n if (!value || !value.startsWith(\"/\") || value.startsWith(\"//\")) {\n return \"/\";\n }\n if (value.startsWith(\"/\\\\\")) {\n return \"/\";\n }\n return value;\n}\n\nexport function currentPath(): string {\n if (typeof location === \"undefined\") {\n return \"/\";\n }\n return location.pathname + location.search;\n}\n\nexport function signIn(path: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const link = document.createElement(\"a\");\n link.href = `/_zappie/login?return=${encodeURIComponent(path)}`;\n link.target = \"_top\";\n link.click();\n}\n\nexport function signOut(path: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const form = document.createElement(\"form\");\n form.method = \"POST\";\n form.action = path;\n form.target = \"_top\";\n form.hidden = true;\n document.body.appendChild(form);\n form.submit();\n form.remove();\n}\n"],
5
- "mappings": ";AAAO,SAAS,aAAa,OAAmC;AAC9D,MAAI,CAAC,SAAS,CAAC,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,GAAG;AAC9D,WAAO;AAAA,EACT;AACA,MAAI,MAAM,WAAW,KAAK,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,cAAsB;AACpC,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AACA,SAAO,SAAS,WAAW,SAAS;AACtC;AAEO,SAAS,OAAO,MAAoB;AACzC,MAAI,OAAO,aAAa,aAAa;AACnC;AAAA,EACF;AACA,QAAM,OAAO,SAAS,cAAc,GAAG;AACvC,OAAK,OAAO,yBAAyB,mBAAmB,IAAI,CAAC;AAC7D,OAAK,SAAS;AACd,OAAK,MAAM;AACb;AAEO,SAAS,QAAQ,MAAoB;AAC1C,MAAI,OAAO,aAAa,aAAa;AACnC;AAAA,EACF;AACA,QAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,OAAK,SAAS;AACd,OAAK,SAAS;AACd,OAAK,SAAS;AACd,OAAK,SAAS;AACd,WAAS,KAAK,YAAY,IAAI;AAC9B,OAAK,OAAO;AACZ,OAAK,OAAO;AACd;",
4
+ "sourcesContent": ["import { sameOriginPath } from \"../internal/return-path\";\n\nexport function safeReturnTo(value: string | undefined): string {\n return sameOriginPath(value) ?? \"/\";\n}\n\nexport function currentPath(): string {\n if (typeof location === \"undefined\") {\n return \"/\";\n }\n return location.pathname + location.search;\n}\n\nexport function signIn(path: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const link = document.createElement(\"a\");\n link.href = `/_zappie/login?return=${encodeURIComponent(path)}`;\n link.target = \"_top\";\n link.click();\n}\n\nexport function signOut(path: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const form = document.createElement(\"form\");\n form.method = \"POST\";\n form.action = path;\n form.target = \"_top\";\n form.hidden = true;\n document.body.appendChild(form);\n form.submit();\n form.remove();\n}\n"],
5
+ "mappings": ";AAAA,SAAS,sBAAsB;AAExB,SAAS,aAAa,OAAmC;AAC9D,SAAO,eAAe,KAAK,KAAK;AAClC;AAEO,SAAS,cAAsB;AACpC,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AACA,SAAO,SAAS,WAAW,SAAS;AACtC;AAEO,SAAS,OAAO,MAAoB;AACzC,MAAI,OAAO,aAAa,aAAa;AACnC;AAAA,EACF;AACA,QAAM,OAAO,SAAS,cAAc,GAAG;AACvC,OAAK,OAAO,yBAAyB,mBAAmB,IAAI,CAAC;AAC7D,OAAK,SAAS;AACd,OAAK,MAAM;AACb;AAEO,SAAS,QAAQ,MAAoB;AAC1C,MAAI,OAAO,aAAa,aAAa;AACnC;AAAA,EACF;AACA,QAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,OAAK,SAAS;AACd,OAAK,SAAS;AACd,OAAK,SAAS;AACd,OAAK,SAAS;AACd,WAAS,KAAK,YAAY,IAAI;AAC9B,OAAK,OAAO;AACZ,OAAK,OAAO;AACd;",
6
6
  "names": []
7
7
  }
package/dist/debug.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { b as CartLineInput, a as Quote } from './types-gQXU2gEA.js';
2
2
  import { M as Money, a as Menu, B as Branch } from './models-D__QbqEd.js';
3
- import { Z as Zappie } from './client-DzUauU-b.js';
3
+ import { Z as Zappie } from './client-CSOZOwQW.js';
4
4
 
5
5
  type StoredLine = CartLineInput & {
6
6
  id: string;
package/dist/debug.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/debug.ts
2
- var VERSION = true ? "0.1.11" : "0.0.0-development";
2
+ var VERSION = true ? "0.1.13" : "0.0.0-development";
3
3
  function installDebug(client) {
4
4
  if (typeof window === "undefined" || client.mode !== "test") {
5
5
  return;
package/dist/index.d.ts CHANGED
@@ -1,47 +1,9 @@
1
- import { T as Transport, R as RequestCtx, f as MenuFulfilment, g as MenuResponse, h as SelectedMenuRequest, i as SelectedMenuResponse, j as SearchRequest, k as SearchPage, l as MenuVersion, a as ZappieError } from './client-DzUauU-b.js';
2
- export { A as API_VERSION, E as ErrorDetails, m as MenuReadOptions, M as MenuResource, n as ReadOptions, S as StorageLike, c as StoreResource, Z as Zappie, b as ZappieClientOptions, b as ZappieConfig, e as ZappieErrorCode, d as ZappieEvents, o as createZappie, p as isZappieError } from './client-DzUauU-b.js';
3
- import { S as Store, C as CategoryPage, I as Item, O as OptionGroup, M as Money, a as Menu, B as Branch } from './models-D__QbqEd.js';
4
- export { b as Category, c as Choice, d as Currency, F as Formatters, e as Fulfilment, f as MenuSchedule, g as MenuSection, h as MoneyFormatOptions, i as OpeningTime, j as OptionCondition, P as PaymentMethodKind, k as SpecialHours, V as Variant, l as format, m as money } from './models-D__QbqEd.js';
5
- import { Q as QuoteRequest, g as WireQuote, f as CartIssue, b as CartLineInput, C as CartLine } from './types-gQXU2gEA.js';
1
+ import { a as ZappieError, T as Transport, R as RequestCtx, f as MenuFulfilment, g as MenuResponse, h as SelectedMenuRequest, i as SelectedMenuResponse, j as SearchRequest, k as SearchPage, l as MenuVersion } from './client-CSOZOwQW.js';
2
+ export { A as API_VERSION, E as ErrorDetails, m as MenuReadOptions, M as MenuResource, n as ReadOptions, S as StorageLike, c as StoreResource, Z as Zappie, b as ZappieClientOptions, b as ZappieConfig, e as ZappieErrorCode, d as ZappieEvents, o as createZappie, p as isZappieError } from './client-CSOZOwQW.js';
3
+ import { f as CartIssue, b as CartLineInput, C as CartLine, Q as QuoteRequest, g as WireQuote } from './types-gQXU2gEA.js';
6
4
  export { h as CartFix, c as CartFulfilment, i as CartIssueCode, d as CartModule, j as CartSnapshot, e as CartState, k as CartSyncAdapter, l as CartSyncDocument, D as DeliveryAddress, a as Quote, m as WireFix, W as WireFulfilment, n as WireIssue, o as WireLine, p as WireMoney } from './types-gQXU2gEA.js';
7
-
8
- /** REST connection options. A custom fetch can provide tracing or mock responses. */
9
- type RestTransportOptions = {
10
- baseUrl?: string;
11
- fetch?: typeof fetch;
12
- };
13
- /** REST read adapter for the versioned storefront API. */
14
- declare class RestTransport implements Transport {
15
- readonly baseUrl: string;
16
- private fetcher;
17
- constructor(opts?: RestTransportOptions);
18
- private request;
19
- private json;
20
- quote(ctx: RequestCtx, body: QuoteRequest): Promise<WireQuote>;
21
- getStore(ctx: RequestCtx): Promise<Store>;
22
- getMenu(ctx: RequestCtx & {
23
- branchId: string;
24
- ifNoneMatch?: string;
25
- metadataOnly?: boolean;
26
- fulfilment?: MenuFulfilment;
27
- }): Promise<MenuResponse>;
28
- getSelectedMenu(ctx: SelectedMenuRequest): Promise<SelectedMenuResponse>;
29
- search(ctx: SearchRequest): Promise<SearchPage>;
30
- getCategoryPage(ctx: RequestCtx & {
31
- branchId: string;
32
- menuId: string;
33
- categoryId: string;
34
- cursor?: string;
35
- }): Promise<CategoryPage>;
36
- getMenuVersion(ctx: RequestCtx & {
37
- branchId: string;
38
- fulfilment?: MenuFulfilment;
39
- }): Promise<MenuVersion>;
40
- getItem(ctx: RequestCtx & {
41
- branchId: string;
42
- itemId: string;
43
- }): Promise<Item>;
44
- }
5
+ import { I as Item, O as OptionGroup, M as Money, a as Menu, B as Branch, S as Store, C as CategoryPage } from './models-D__QbqEd.js';
6
+ export { b as Category, c as Choice, d as Currency, F as Formatters, e as Fulfilment, f as MenuSchedule, g as MenuSection, h as MoneyFormatOptions, i as OpeningTime, j as OptionCondition, P as PaymentMethodKind, k as SpecialHours, V as Variant, l as format, m as money } from './models-D__QbqEd.js';
45
7
 
46
8
  declare class ZappieCartError extends ZappieError {
47
9
  readonly issues: CartIssue[];
@@ -82,6 +44,44 @@ declare function validateLine(item: Item | undefined, raw: CartLineInput, ctx: {
82
44
  branch: Branch;
83
45
  }): CartIssue[];
84
46
 
47
+ /** REST connection options. A custom fetch can provide tracing or mock responses. */
48
+ type RestTransportOptions = {
49
+ baseUrl?: string;
50
+ fetch?: typeof fetch;
51
+ };
52
+ /** REST read adapter for the versioned storefront API. */
53
+ declare class RestTransport implements Transport {
54
+ readonly baseUrl: string;
55
+ private fetcher;
56
+ constructor(opts?: RestTransportOptions);
57
+ private request;
58
+ private json;
59
+ quote(ctx: RequestCtx, body: QuoteRequest): Promise<WireQuote>;
60
+ getStore(ctx: RequestCtx): Promise<Store>;
61
+ getMenu(ctx: RequestCtx & {
62
+ branchId: string;
63
+ ifNoneMatch?: string;
64
+ metadataOnly?: boolean;
65
+ fulfilment?: MenuFulfilment;
66
+ }): Promise<MenuResponse>;
67
+ getSelectedMenu(ctx: SelectedMenuRequest): Promise<SelectedMenuResponse>;
68
+ search(ctx: SearchRequest): Promise<SearchPage>;
69
+ getCategoryPage(ctx: RequestCtx & {
70
+ branchId: string;
71
+ menuId: string;
72
+ categoryId: string;
73
+ cursor?: string;
74
+ }): Promise<CategoryPage>;
75
+ getMenuVersion(ctx: RequestCtx & {
76
+ branchId: string;
77
+ fulfilment?: MenuFulfilment;
78
+ }): Promise<MenuVersion>;
79
+ getItem(ctx: RequestCtx & {
80
+ branchId: string;
81
+ itemId: string;
82
+ }): Promise<Item>;
83
+ }
84
+
85
85
  /** The installed @zappie/sdk version. */
86
86
  declare const VERSION: string;
87
87
 
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  // src/index.ts
2
+ import { ZappieCartError } from "./cart/errors.js";
3
+ import { previewItem } from "./cart/preview.js";
4
+ import { estimateUnitPrice } from "./cart/price.js";
5
+ import { resolveSourceMenu } from "./cart/source-menu.js";
6
+ import { validateLine } from "./cart/validate.js";
2
7
  import { createZappie } from "./client.js";
3
8
  import { API_VERSION } from "./config.js";
4
9
  import { ZappieError, isZappieError } from "./errors.js";
5
10
  export * from "./models.js";
6
11
  import { format, money } from "./money.js";
7
12
  import { RestTransport } from "./transport/rest.js";
8
- import { ZappieCartError } from "./cart/errors.js";
9
- import { previewItem } from "./cart/preview.js";
10
- import { estimateUnitPrice } from "./cart/price.js";
11
- import { resolveSourceMenu } from "./cart/source-menu.js";
12
- import { validateLine } from "./cart/validate.js";
13
- var VERSION = true ? "0.1.11" : "0.0.0-development";
13
+ var VERSION = true ? "0.1.13" : "0.0.0-development";
14
14
  export {
15
15
  API_VERSION,
16
16
  RestTransport,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["// tsup replaces __ZAPPIE_SDK_VERSION__ with package.json's version. Source run\n// without a build, as in a plain Node script, gets the development label.\n// Kept inline, not in a shared module, so the core chunks don't split differently.\ndeclare const __ZAPPIE_SDK_VERSION__: string | undefined;\n\n/** The installed @zappie/sdk version. */\nexport const VERSION: string =\n typeof __ZAPPIE_SDK_VERSION__ === \"string\"\n ? __ZAPPIE_SDK_VERSION__\n : \"0.0.0-development\";\nexport { createZappie } from \"./client\";\nexport type {\n MenuFulfilment,\n MenuReadOptions,\n MenuResource,\n ReadOptions,\n StoreResource,\n Zappie,\n ZappieEvents,\n} from \"./client\";\nexport { API_VERSION } from \"./config\";\nexport type {\n StorageLike,\n ZappieConfig as ZappieClientOptions,\n ZappieConfig,\n} from \"./config\";\nexport { ZappieError, isZappieError } from \"./errors\";\nexport type { ErrorDetails, ZappieErrorCode } from \"./errors\";\nexport * from \"./models\";\nexport { format, money } from \"./money\";\nexport type { Currency, Formatters, Money, MoneyFormatOptions } from \"./money\";\nexport { RestTransport } from \"./transport/rest\";\nexport type { RestTransportOptions } from \"./transport/rest\";\nexport type {\n MenuResponse,\n MenuVersion,\n RequestCtx,\n SearchPage,\n SearchRequest,\n Transport,\n} from \"./transport/types\";\n\nexport { ZappieCartError } from \"./cart/errors\";\nexport { previewItem } from \"./cart/preview\";\nexport { estimateUnitPrice } from \"./cart/price\";\nexport { resolveSourceMenu } from \"./cart/source-menu\";\nexport type * from \"./cart/types\";\nexport { validateLine } from \"./cart/validate\";\n"],
5
- "mappings": ";AAUA,SAAS,oBAAoB;AAU7B,SAAS,mBAAmB;AAM5B,SAAS,aAAa,qBAAqB;AAE3C,cAAc;AACd,SAAS,QAAQ,aAAa;AAE9B,SAAS,qBAAqB;AAW9B,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAElC,SAAS,oBAAoB;AAzCtB,IAAM,UACX,OACI,WACA;",
4
+ "sourcesContent": ["// tsup replaces __ZAPPIE_SDK_VERSION__ with package.json's version. Source run\n// without a build, as in a plain Node script, gets the development label.\n// Kept inline, not in a shared module, so the core chunks don't split differently.\ndeclare const __ZAPPIE_SDK_VERSION__: string | undefined;\n\n/** The installed @zappie/sdk version. */\nexport const VERSION: string =\n typeof __ZAPPIE_SDK_VERSION__ === \"string\"\n ? __ZAPPIE_SDK_VERSION__\n : \"0.0.0-development\";\nexport { ZappieCartError } from \"./cart/errors\";\nexport { previewItem } from \"./cart/preview\";\nexport { estimateUnitPrice } from \"./cart/price\";\nexport { resolveSourceMenu } from \"./cart/source-menu\";\nexport type * from \"./cart/types\";\nexport { validateLine } from \"./cart/validate\";\nexport { createZappie } from \"./client\";\nexport type {\n MenuFulfilment,\n MenuReadOptions,\n MenuResource,\n ReadOptions,\n StoreResource,\n Zappie,\n ZappieEvents,\n} from \"./client\";\nexport { API_VERSION } from \"./config\";\nexport type {\n StorageLike,\n ZappieConfig as ZappieClientOptions,\n ZappieConfig,\n} from \"./config\";\nexport { ZappieError, isZappieError } from \"./errors\";\nexport type { ErrorDetails, ZappieErrorCode } from \"./errors\";\nexport * from \"./models\";\nexport { format, money } from \"./money\";\nexport type { Currency, Formatters, Money, MoneyFormatOptions } from \"./money\";\nexport { RestTransport } from \"./transport/rest\";\nexport type { RestTransportOptions } from \"./transport/rest\";\nexport type {\n MenuResponse,\n MenuVersion,\n RequestCtx,\n SearchPage,\n SearchRequest,\n Transport,\n} from \"./transport/types\";\n"],
5
+ "mappings": ";AAUA,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAElC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAU7B,SAAS,mBAAmB;AAM5B,SAAS,aAAa,qBAAqB;AAE3C,cAAc;AACd,SAAS,QAAQ,aAAa;AAE9B,SAAS,qBAAqB;AA/BvB,IAAM,UACX,OACI,WACA;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import { R as RequestCtx } from '../client-DzUauU-b.js';
1
+ import { R as RequestCtx } from '../client-CSOZOwQW.js';
2
2
  import '../models-D__QbqEd.js';
3
3
  import '../types-gQXU2gEA.js';
4
4
 
@@ -0,0 +1,17 @@
1
+ // src/internal/return-path.ts
2
+ var ORIGIN = "https://return.invalid";
3
+ function sameOriginPath(value) {
4
+ if (!value?.startsWith("/")) {
5
+ return void 0;
6
+ }
7
+ try {
8
+ const target = new URL(value, ORIGIN);
9
+ return target.origin === ORIGIN ? `${target.pathname}${target.search}${target.hash}` : void 0;
10
+ } catch {
11
+ return void 0;
12
+ }
13
+ }
14
+ export {
15
+ sameOriginPath
16
+ };
17
+ //# sourceMappingURL=return-path.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/internal/return-path.ts"],
4
+ "sourcesContent": ["const ORIGIN = \"https://return.invalid\";\n\n// Resolve the path the way a browser resolves a Location header (it strips\n// tabs and newlines and reads \"\\\" as \"/\"), and keep it only if it stays on this site.\nexport function sameOriginPath(\n value: string | null | undefined,\n): string | undefined {\n if (!value?.startsWith(\"/\")) {\n return undefined;\n }\n try {\n const target = new URL(value, ORIGIN);\n return target.origin === ORIGIN\n ? `${target.pathname}${target.search}${target.hash}`\n : undefined;\n } catch {\n return undefined;\n }\n}\n"],
5
+ "mappings": ";AAAA,IAAM,SAAS;AAIR,SAAS,eACd,OACoB;AACpB,MAAI,CAAC,OAAO,WAAW,GAAG,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,OAAO,MAAM;AACpC,WAAO,OAAO,WAAW,SACrB,GAAG,OAAO,QAAQ,GAAG,OAAO,MAAM,GAAG,OAAO,IAAI,KAChD;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }