@zappie/sdk 0.1.0 → 0.1.1

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 ADDED
@@ -0,0 +1,19 @@
1
+ # @zappie/sdk
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 38f23ac: `VERSION` (`@zappie/sdk`) and `FOUNDATIONS_VERSION` (`@zappie/foundations`) report the installed version. In 0.1.0 they said `"0.0.0"`. Verify reports now carry the real `verifyVersion`, and the test-mode debug channel the real SDK version.
8
+ - 1e6f420: Each package now has a README written for people installing it from npm: what it does, how to install and use it, its entry points, and links to docs.zappie.ai. `@zappie/kit` gains its first README.
9
+
10
+ ## 0.1.0
11
+
12
+ ### Minor Changes
13
+
14
+ - - `@zappie/tokens`: Publish the semantic storefront token contract, CSS aliases, validation and compilation tools.
15
+ - `@zappie/sdk`: Publish the framework-agnostic storefront client and its React, server, checkout, customer, places, visits and URL entries. Money accepts every ISO 4217 currency the Zappie API accepts, with the API's minor-unit digits.
16
+ - `@zappie/foundations`: Publish accessible, unstyled React primitives for storefront catalogue, item, fulfilment and cart experiences.
17
+ - `@zappie/theme-base`: Publish the reusable base storefront theme and its catalogue, item and cart surfaces.
18
+ - `@zappie/kit`: Publish the built component registry consumed by the Zappie CLI.
19
+ - `zappie`: Publish the component CLI with `add`, `list` and `diff`.
package/README.md CHANGED
@@ -1,338 +1,80 @@
1
1
  # @zappie/sdk
2
2
 
3
- Framework-free storefront reads and a device-local cart: store identity, branch catalogues, items, display estimates and authoritative quotes. The core runs in browsers, Node and Workers without React or host imports. This phase remains private and uses the existing GraphQL host adapter; the future REST API is exercised only through mocks.
3
+ The typed client for Zappie storefronts. Read a store and its menus, keep a cart on the shopper's device, get quoted totals from Zappie and hand payment to Zappie's hosted checkout. It runs in browsers, Node and Cloudflare Workers.
4
4
 
5
- ## Client
6
-
7
- ```ts
8
- import { createZappie, money, isZappieError } from "@zappie/sdk";
9
-
10
- const zappie = createZappie({
11
- store: "my-store",
12
- key: "pk_test_example",
13
- branch: "main",
14
- });
15
- const store = await zappie.store.get();
16
- const menu = await zappie.menu.get();
17
- const item = await zappie.menu.item("item_123");
18
- console.log(zappie.format.money(money(1695800, "NGN"))); // ₦16,958
19
- ```
20
-
21
- This REST example targets the planned API, not a deployed service promised by this package. Use an injected `Transport` or `createMockZappie` today. A REST key identifies one storefront; `store` is the client context slug, not a way to override a key's tenant. Each client owns its credentials, default branch, cache and listeners. Create one client per server request. Never share a server client across users or tenants.
22
-
23
- `createZappie` accepts only `pk_test_…` / `pk_live_…` keys. The server-only import accepts secret keys and rejects browser execution. Config is shallow-frozen. `storage` supplies optional cart persistence. `telemetry` reserves configuration for the later analytics transport; events currently stay within the client.
24
-
25
- Branch selectors accept a branch ID or slug. The configured branch wins; otherwise the server-designated default or first visible branch is selected. Unknown branches produce `branch_not_found`. All resource reads accept cancellation signals. `menu.version()` returns opaque content/availability versions, `menu.peek()` reads the local snapshot, and `menu.hydrate()` seeds SSR data.
26
-
27
- ## Models and naming
28
-
29
- **Storefront** means the overall customer-facing store. **Checkout** means purchase completion and payment. SDK read models describe the storefront; payment methods in `Store` are display capabilities, not a guarantee of checkout eligibility.
30
-
31
- - `Store` holds identity, description, currency, timezone, brand imagery,
32
- fulfilment, payment capabilities and branches. Branch directory entries may
33
- include a phone number, pickup location and social links; missing source
34
- values remain absent.
35
- - `Branch` preserves weekly hours, disabled days, timezone, special hours, closures and next-change metadata when provided. These are service snapshots; this SDK does not recalculate live opening status.
36
- - `Menu` is one normalized branch catalogue with items keyed by ID. Its `menus` sections retain source menu IDs, category membership and schedules when a branch has several menus. A single-menu catalogue retains that menu's ID/name; a multi-menu aggregate uses `branch:<branchId>` as its collection ID, never as a product/variant ID.
37
- - `Item.variants` is a required nonempty list of exact identities. Incomplete data is rejected instead of inventing a variant from a product ID. Prices and availability stay service-owned.
38
- - `OptionGroup` preserves selection bounds, conditional triggers **and effects**, toggle/informational inputs, and variable choice quantities. Attribute choices have zero addon price; their exact price belongs to the selected variant.
39
-
40
- `Money` is an immutable branded value constructed using `money()`. Values must be safe integer minor units. NGN, GHS, KES, USD and GBP are supported; currency exponents come from `Intl`. Formatting defaults to `en-NG` for NGN and `en` otherwise, hides zero decimal places and supports compact notation and explicit signs. No arithmetic is exported.
41
-
42
- The hosted identity Worker currently returns `phone_masked` unchanged from its
43
- identity claim. It may contain a full phone number until Worker masking ships,
44
- so applications must never log it.
45
-
46
- ## Cache and events
47
-
48
- Entries younger than 60 seconds are fresh. Reads between 60 seconds and 10 minutes return stale data and start one background refresh. Older reads await the service. Concurrent reads deduplicate by resource and exact branch. One caller's cancellation does not cancel another caller's shared request; the last foreground cancellation aborts the underlying request. Background refresh errors retain existing data and expose an error snapshot.
49
-
50
- ```ts
51
- const unsubscribe = zappie.on("menu:updated", ({ branchId, version }) => {
52
- console.log(branchId, version);
53
- });
54
- zappie.on("error", ({ error }) => console.error(error.code));
5
+ ```sh
6
+ npm install @zappie/sdk
55
7
  ```
56
8
 
57
- `menu:updated` fires when a revalidation stores a different version, after the new value is visible. Hydration does not emit that event. Consumers should treat returned snapshots as immutable.
9
+ ## Use it
58
10
 
59
- ## React and SSR
11
+ Create a client with your publishable key and the store's slug, then share it with React through `ZappieProvider`:
60
12
 
61
13
  ```tsx
62
- import { ZappieProvider, useMenu } from "@zappie/sdk/react";
63
-
64
- function Catalogue() {
65
- const { data, error, isLoading, isStale } = useMenu();
66
- if (isLoading) {
67
- return <p>Loading…</p>;
68
- }
69
- if (error) {
70
- return <p>{error.message}</p>;
71
- }
72
- return (
73
- <p>
74
- {data?.name}
75
- {isStale ? " (refreshing)" : ""}
76
- </p>
14
+ import { createZappie } from "@zappie/sdk";
15
+ import { ZappieProvider } from "@zappie/sdk/react";
16
+ import { type ReactNode, useMemo } from "react";
17
+
18
+ type StorefrontEnv = { ZAPPIE_KEY: string; ZAPPIE_STORE: string };
19
+
20
+ export function Storefront({
21
+ env,
22
+ children,
23
+ }: {
24
+ env: StorefrontEnv;
25
+ children: ReactNode;
26
+ }) {
27
+ const client = useMemo(
28
+ () => createZappie({ key: env.ZAPPIE_KEY, store: env.ZAPPIE_STORE }),
29
+ [env.ZAPPIE_KEY, env.ZAPPIE_STORE],
77
30
  );
31
+ return <ZappieProvider client={client}>{children}</ZappieProvider>;
78
32
  }
79
-
80
- // Seed both server and browser clients with the same serialized initial data.
81
- <ZappieProvider client={zappie} initialData={{ store, menu }}>
82
- <Catalogue />
83
- </ZappieProvider>;
84
- ```
85
-
86
- Hooks use `useSyncExternalStore` and subscribe to the client cache. `useStore`, `useMenu` and `useItem` return data, loading and error state. `useItem` selects from the branch catalogue, sharing its fetch. Supplying store and menu initial data avoids a hydration refetch. `useZappie()` requires a provider. The React entry preserves its `use client` directive in packed output.
87
-
88
- ## Transport and errors
89
-
90
- `RestTransport` sends `Authorization: Bearer …` and release-pinned `Zappie-Version: 2026-10-01`. Menu reads preserve ETags, follow `next_cursor` pages, reject repeating cursors/mixed page versions, and normalize snake_case wire fields without transforming ID-map keys. Responses require exact variants even though the older PRD 07 example is abridged.
91
-
92
- GET requests retry network failures and 5xx responses twice with jittered backoff. 429 responses honor numeric or HTTP-date `Retry-After`. Cancellation propagates through requests and retry waits. Redirects are refused to keep credentials on the configured origin. `baseUrl` requires HTTPS, with HTTP allowed for localhost development.
93
-
94
- `ZappieError` exposes `code`, `status`, `param`, `docUrl`, `requestId` and `retryable`. Use `isZappieError` to narrow caught errors. Unknown API error codes become `server_error` with the original code retained in the message. Caller-triggered cancellation keeps the AbortSignal reason.
95
-
96
- ```ts
97
- import {
98
- createZappieServer,
99
- type EdgeSnapshotReader,
100
- } from "@zappie/sdk/server";
101
- const server = createZappieServer({
102
- store: "my-store",
103
- secretKey: "sk_test_example",
104
- });
105
- ```
106
-
107
- The server entry provides reads and the `EdgeSnapshotReader.getMenu(storeId, branchId)` interface. Edge infrastructure is implemented in PRD 21; no snapshot backend is included here.
108
-
109
- ### Storefront URL compatibility
110
-
111
- `parseStorefrontUrl` and `buildStorefrontUrl` from `@zappie/sdk/server` describe
112
- the URLs used by Zappie's current storefront. They accept plain web-standard
113
- values and can map the shorter paths used by an installation bound to one
114
- business or branch.
115
-
116
- ```ts
117
- import { buildStorefrontUrl, parseStorefrontUrl } from "@zappie/sdk/server";
118
-
119
- const route = parseStorefrontUrl("/order/acme/main?menu_id=lunch");
120
- const href = buildStorefrontUrl(route);
121
- ```
122
-
123
- This is an optional compatibility default. A template owns its router and may
124
- define other paths. It can use these helpers when it needs to answer existing
125
- Zappie-issued links without depending on the Zappie host or a web framework.
126
-
127
- ## Existing host adapter
128
-
129
- `lib/zappie-sdk/client.ts` exports `createHostZappie({ client, businessTag, branch?, orderContext, mode })`. `client` is the host's request-scoped Apollo client; `mode` is explicitly `test` or `live`. Its synthetic SDK key is never sent as an API credential. Authentication remains with Apollo.
130
-
131
- The adapter reuses `getDeliveryOrderBusinessByTag`, `getProducts`, `buildCatalogueViewModel` and `buildStorefrontIdentityViewModel`. An opt-in loader field selection requests the existing branch payment capabilities and dine-in flag. There are no backend/schema changes. Menus are scoped to the selected order context, and all product pages are loaded. Versions hash the projected content, so price, option and availability edits invalidate the catalogue even when a menu timestamp stays unchanged. The host has no lightweight version endpoint, so its version read currently loads the catalogue.
132
-
133
- The factory is ready for the base-theme migration; this workstream does not mount it in the current storefront or replace legacy providers.
134
-
135
- ## Testing and development
136
-
137
- ```ts
138
- import { createMockZappie, fixtures } from "@zappie/sdk/testing";
139
- const { store, menu } = fixtures.minimal;
140
- const client = createMockZappie(
141
- { store, menus: { [menu.branchId]: menu } },
142
- {
143
- latencyMs: 20,
144
- // failWith: { getMenu: 'server_error' },
145
- },
146
- );
147
- ```
148
-
149
- Fixtures are synthetic and contain no merchant photos or live credentials. `minimal` is one plain item. `defaultTemplate` replaces the Inside Out fixture, following the default-template decision in workstream 02: two branches (one closed for a holiday), an item in two categories, required, conditional and capped options with per-choice quantities, a text input, and attribute variants with an unavailable size. Host parity tests use the existing test restaurant fixtures in `tests/fixtures/menu.ts`.
150
-
151
- ```sh
152
- pnpm vitest run packages/sdk lib/zappie-sdk
153
- pnpm --filter @zappie/sdk build
154
- pnpm sdk:size
155
- pnpm check-types
156
- pnpm lint
157
- ```
158
-
159
- The size gate includes the core entry **and shared build chunks**, with a combined 16 KB gzip limit (raised from 15 KB once the cart joined the core; the quote client still loads lazily). Public package boundaries and the PRD braces/multiline-block rules remain enforced. No runtime dependency is added; React is an optional peer for the separate React entry. Checkout, auth and analytics are later workstreams.
160
-
161
- ## Cart
162
-
163
- Load the store/menu before synchronous edits. React's `useCart()` hydrates after mount; imperative browser integrations call `cart.mount()` and retain its cleanup. Construction and SSR rendering never read localStorage.
164
-
165
- ```ts
166
- await zappie.menu.get();
167
- const unmountCart = zappie.cart.mount();
168
- await zappie.cart.hydrate();
169
- const line = zappie.cart.add("item_123", {
170
- choices: [{ groupId: "size", choiceId: "large" }],
171
- note: "No onions",
172
- });
173
- zappie.cart.update(line.id, { quantity: 2 });
174
- zappie.cart.setFulfilment({ type: "pickup", branchId: "br_main" });
175
- const quote = await zappie.cart.quote({ force: true });
176
- // At teardown: unmountCart(); zappie.cart.dispose();
177
- ```
178
-
179
- `CartFulfilment` is a shopper's selection. The existing `Fulfilment` type remains the store/branch capability flags. `useCart()` returns stable subscribed state plus `actions`; `useCartLine(id)` selects a line. The server snapshot is always empty and `isHydrated` is false until browser hydration completes. Dispose a client when its owner is permanently removed; a disposed cart is not intended for reuse.
180
-
181
- Edits validate against the loaded menu, resolve exact variants and price in safe integer minor units. They throw `ZappieCartError` with actionable issues before altering an existing cart. All snapshots are frozen. Identical item/variant/choices/inputs/note combinations merge on add; notes distinguish separate lines. Updates keep their line ID. Limits are 100 lines, 99 units per line, item-specific limits and 280 characters per note. Unknown items and choices are rejected. Attribute prices never add a second charge to a variant's price.
182
-
183
- Conditional rules override required/min/max bounds in declaration order, matching `resolveEffectiveAddons`; a failed condition leaves the base rule active. Unavailable groups are removed from selections. Optional groups with a minimum remain skippable until selected. Missing required groups are reported before selected-group count errors, matching the host. Text, number, date, time, toggle and informational/hybrid groups are supported; a hybrid accepts a selection or a free response, never both. The cart additionally enforces the PRD's quantity/typed-input limits rather than accepting malformed persisted values.
184
-
185
- ### Persistence and privacy
186
-
187
- Cart keys are `zappie:cart:v1:<store>:<branchId>`. Restore always recomputes prices from the current menu; a saved price is only a hint for the price-change notice. A throwing storage accessor or operation falls back to client-local memory and emits one warning. Corrupt/version-unknown records are discarded. Browser storage events replace state only for the active key and storage area; listeners are reference-counted and removable.
188
-
189
- `cart: { ttl: milliseconds }` defaults to seven days of inactivity. Restore discards expired carts and emits `cart_expired`, drops past scheduled times, and after 24 hours clears delivery addresses and free-text `inputs`. Quote reads and reconciliation do not extend the idle deadline. Other expired branch keys for the same store are swept when storage supports optional `length`/`key(index)` enumeration. Custom adapters without enumeration still expire the active cart. `moveTo(branchId)` validates the current lines against the destination menu, keeps valid line IDs, reports dropped items and moves persistence to the new key; it replaces any saved destination cart.
190
-
191
- Legacy migration is configured only by `createHostZappie({ legacyKey })`. It reads the old basket once, validates and reprices its selections, writes the new key, and leaves the old value untouched. External SDK clients never inspect host legacy keys. Clearing writes an empty v1 cart so the next load does not resurrect the legacy basket.
192
-
193
- ### Quotes and fixes
194
-
195
- Changes debounce quotes by 300 ms. Mutations immediately invalidate/abort an in-flight quote, so even a transport that ignores cancellation cannot overwrite newer state. Identical unexpired payloads reuse their quote; `force: true` fetches again and emits `view_cart`. Requests carry a SHA-256 payload-derived idempotency key, exact branch/variant IDs and `options`, with no prices. REST `/quote` remains a mocked contract until PRD 08; the host adapter uses existing order-preview GraphQL.
196
-
197
- Subtotal is an estimate. Fees, discounts and total come from preview/quote. A host preview provides only aggregate totals: per-line prices are exposed only when the estimates sum to its subtotal; otherwise `linesUnpriced` is true and the line prices are null, following PRD 08. Known backend charge names identify delivery/service/VAT; unclassified positive charges and negative discounts keep their labels under `other`. No invented promo allocation is made. Delivery quoting requires a selected place ID (`DeliveryAddress.placeId`); missing place data yields an address issue. The existing dine-in preview cannot apply promos, so it returns a removable promo warning. Legacy preview item errors without typed codes conservatively become blocking `item_unavailable` issues with the upstream message.
198
-
199
- Only blocking issues prevent `toCheckoutPayload()`. Warning issues still produce `needs_attention`; info issues do not. This method also requires nonempty lines and a fulfilment selection. Checkout must reprice server-side. Offline quote calls reject with `network_error` while keeping the local estimate and local issues.
200
-
201
- `applyFix` accepts only fixes on a current issue. Remove-line, quantity, promo removal and price acceptance can run immediately. Choose-option/address/schedule/fulfilment actions need shopper input: present that choice and call `update` or `setFulfilment`; applying an interactive fix directly throws an explanatory `invalid_request`. Accepting a price does not clear unrelated issues. Menu refreshes on load/focus and `menu:updated` revalidate lines while retaining unresolved price warnings.
202
-
203
- Events are local only: `add_to_cart`, `remove_from_cart`, `update_cart_line`, `view_cart`, `cart_issue`, `quote_failed`, `warning`, `cart_expired`. No analytics request, order creation or checkout session is performed.
204
-
205
- ## Checkout sessions
206
-
207
- `@zappie/sdk/checkout` is a separate browser entry. Customer calls default to
208
- the same-origin `/_zappie/api` proxy, which attaches the customer session. A
209
- self-hosted integration can provide `customerBase`; there is no production URL
210
- or key default.
211
-
212
- ```ts
213
- import { createCheckout } from "@zappie/sdk/checkout";
214
-
215
- const payload = zappie.cart.toCheckoutPayload();
216
- const checkout = createCheckout();
217
- const session = await checkout.open(
218
- {
219
- branch_id: "br_main",
220
- menu_version: payload.menu_version,
221
- lines: payload.lines,
222
- fulfilment: { type: "pickup" },
223
- return_url: "https://shop.example/checkout/return",
224
- },
225
- checkoutAttemptId,
226
- );
227
- location.assign(session.url);
228
33
  ```
229
34
 
230
- `payload.lines` already uses the cart quote-line builder; checkout does not
231
- maintain a second mapper. The caller creates one idempotency key for a cart
232
- attempt and reuses it for every retry. `session(id)` reads the current state,
233
- while `awaitCompletion(id, { signal })` polls every three seconds until the
234
- session completes, expires or is cancelled. `payReadiness` and
235
- `isContactPhoneValid` expose the released site's Pay-button ordering.
35
+ Any component inside can read the cart. Show Zappie's quoted total once it arrives, and the local subtotal until then:
236
36
 
237
- SDK models are camelCase mappings of the API; request and response bodies sent
238
- to the API retain its wire names.
239
-
240
- ## Visit attribution
241
-
242
- `@zappie/sdk/visits` is a separate browser entry for the anonymous visit
243
- counts used by merchant analytics. Call `countPage` after each navigation and
244
- `countEvent("add" | "bag")` for those two steps. It sends only the page path,
245
- cleaned title, visit source and campaign, and first-view/first-step flags to
246
- the same-origin `/_zappie/view` endpoint with `navigator.sendBeacon`. It sends
247
- no personal data and has no fetch fallback.
248
-
249
- The source, campaign and reached steps are stored for the browser session only
250
- under `sessionStorage.zappie_visit`. Storage failures are ignored; no cookie or
251
- local storage is used. Pass `enabled: false` to `countPage` in editors and
252
- previews. The classic site enables counting only when its storefront mode is
253
- `site`, excluding its `draft` and `review` modes.
254
-
255
- ```ts
256
- import { createCheckout } from "@zappie/sdk/checkout";
257
- import { countPage, currentSource } from "@zappie/sdk/visits";
258
-
259
- countPage({ path, search, title, referrer, storeName, enabled: isLiveSite });
260
- await createCheckout().open(
261
- { ...checkoutRequest, ...currentSource() },
262
- checkoutAttemptId,
263
- );
264
- ```
265
-
266
- ### Signed-in synchronization contract (for PRD 13)
267
-
268
- There is no cross-device cart before sign-in, and no sync requests are implemented here. `CartSyncAdapter` specifies the handoff: authenticated reads return a schema-v1 document with an opaque revision and server expiry; writes use compare-and-swap (`expectedRevision`) plus a stable operation ID. A conflict returns the latest document instead of silently overwriting it. PRD 13 supplies the authentication trigger and adapter; its API implementation must support these semantics before enabling sync.
269
-
270
- On sign-in, merge only the same store/branch: match exact item/variant/choices/inputs/note identities, sum quantities and retain separate notes. Preserve remote IDs for matching/remote-only lines and local IDs for new lines. Revalidate against the current menu; limits or unavailable selections become issues requiring a shopper decision, never silent clamping/dropping. A nonempty local fulfilment/promo selection wins; otherwise retain the remote selection after validation. Server expiry is authoritative. On conflict, reapply the original unsynced local delta to the latest revision once, retrying the same operation ID; never add an already-merged local snapshot again. Sign-out stops the adapter and removes authenticated address/input data from the local device; it does not delete the server cart.
271
-
272
- ### Verification
273
-
274
- ```sh
275
- pnpm exec vitest run packages/sdk/test lib/zappie-sdk
276
- pnpm exec vitest bench packages/sdk/test/add.bench.ts --run
277
- pnpm --filter @zappie/sdk build
278
- pnpm --filter @zappie/sdk size
279
- pnpm check-types
280
- ```
281
-
282
- The benchmark includes persistence and cart clearing on a 1,000-item menu; its measured cycle is a conservative upper bound on synchronous add. Tests live outside `src` to keep host-only parity imports out of published declarations and the SDK dependency boundary. No tests make production requests or place orders.
283
-
284
- ## Site configuration
285
-
286
- `@zappie/sdk/config` exports `defineConfig` for editor completion and
287
- `validateConfig` for checking an untrusted `zappie.config.ts` value. The
288
- validator reports paths, stable issue codes, severities and a concrete fix.
289
- It also exports `DEFAULT_BUDGETS` and `GLOBAL_PACKAGE_ALLOWLIST` for the
290
- `zappie check` command. Theme files remain the responsibility of
291
- `@zappie/tokens/validate`.
292
-
293
- ```ts
294
- import { defineConfig } from "@zappie/sdk/config";
295
-
296
- export default defineConfig({
297
- store: "insideoutcafe",
298
- theme: "./theme.json",
299
- });
37
+ ```tsx
38
+ import { format } from "@zappie/sdk";
39
+ import { useCart } from "@zappie/sdk/react";
40
+
41
+ export function CartTotal() {
42
+ const cart = useCart();
43
+ const total =
44
+ cart.state.status === "quoted" ? cart.state.quote.total : cart.subtotal;
45
+ return <span>{total ? format.money(total) : "Add an item"}</span>;
46
+ }
300
47
  ```
301
48
 
302
- ## Address lookup
303
-
304
- `@zappie/sdk/places` is a separate lazy entry for delivery addresses. `createPlaces({ key, baseUrl?, version?, fetch?, customerBase? })` takes the same key, base URL and version as `createZappie`, and calls the API directly with the publishable key:
49
+ Prices always come from Zappie. The cart shows a local estimate while it waits, and Zappie quotes the real total before checkout.
305
50
 
306
- - `autocomplete(query, sessionToken)`, `place(placeId, { branchId, sessionToken })` and `reverse(lat, lng)` return the API's `PlaceSuggestion` and `Place` bodies, with their wire names. `place` includes `route.distance_meters` when a branch is given.
307
- - `zoneCheck({ branch_id, address, delivery_region_id })` returns whether the branch delivers there, the fee, and a `reason` when it doesn't.
308
- - `staticMapUrl(lat, lng, branchId)` returns an image URL under `customerBase` (default `/_zappie/api`), because the site host adds the key. Use it as an `<img src>`.
309
- - `createAddressSearch({ debounceMs = 250, branchId })` keeps one session token per search, so the maps provider bills one lookup. `suggest(query)` is debounced and aborts the previous request. A superseded call rejects with an `AbortError`, which callers should ignore. `select(placeId)` resolves the address with the same token and then starts a new one. Call `dispose()` when the address field unmounts.
51
+ ## Entry points
310
52
 
311
- Errors are `PlacesError` (a `ZappieError` with `apiCode` and `issues`). The API limits address calls to 60 a minute per key, so `rate_limited` is retryable.
53
+ | Import | For |
54
+ | -------------------------------------------- | ---------------------------------------------------------------------- |
55
+ | `@zappie/sdk` | The client, the cart, money formatting and errors |
56
+ | `@zappie/sdk/react` | `ZappieProvider` and hooks such as `useStore`, `useMenu` and `useCart` |
57
+ | `@zappie/sdk/checkout` | Opening Zappie's hosted checkout and checking it completed |
58
+ | `@zappie/sdk/customer` and `/customer/react` | Signed-in customers, their orders and saved addresses |
59
+ | `@zappie/sdk/places` | Address search and delivery-zone checks |
60
+ | `@zappie/sdk/server` | Server-side reads, storefront URLs and `verifyWebhook()` |
61
+ | `@zappie/sdk/template` and `/template/react` | Template settings and responsive images for template authors |
62
+ | `@zappie/sdk/testing` | A mock client for your tests |
312
63
 
313
- ## Item configuration previews
64
+ 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.
314
65
 
315
- `previewItem(item, input)` from `@zappie/sdk` exposes effective option groups, and unit/total estimates using the resolved variant for configuration editors. It reuses the cart's conditional rules and price calculations. It is included in the core size budget. Call `cart.validate(input)` before adding; a preview is not validation and never replaces a server quote. Invalid intermediate quantities remain renderable and are still rejected by cart validation.
66
+ 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.
316
67
 
317
- The typed `search` event carries `{ query, resultCount }`; foundations emits it after a 600ms debounce. This adds no analytics transport.
68
+ ## Docs
318
69
 
319
- `cart.branchId` (also on the React cart snapshot) is the selected branch ID, or undefined before initialization. It remains set after `moveTo()` clears fulfilment, so catalogue and picker components can follow branch changes before the shopper selects delivery/pickup.
70
+ - [SDK reference](https://docs.zappie.ai/sdk/core)
71
+ - [Build a storefront](https://docs.zappie.ai/guides/build-a-storefront)
72
+ - [Keys](https://docs.zappie.ai/keys)
320
73
 
321
- ## Template settings and images
74
+ ## Status
322
75
 
323
- `@zappie/sdk/template` exports the settings merge and Cloudflare image URL
324
- helpers. `@zappie/sdk/template/react` adds `TemplateSettingsProvider` and
325
- `useTemplateSettings`. The app reads its hosting environment and passes
326
- `mode="site"`, `mode="draft"`, or `mode="review"` to the provider. A live site
327
- uses defaults and saved `Store.templateSettings`. Drafts add unsaved editor
328
- values. Reviews use defaults and editor values without the demo store's saved
329
- choices.
76
+ The SDK is at `0.x`. Its API can change in a minor release while it settles. Each change is listed in `CHANGELOG.md`.
330
77
 
331
- Only draft and review modes listen for editor messages or post editor state.
332
- The hosting layer must keep live sites in `site` mode. The current editor
333
- protocol does not carry a fixed dashboard origin, so the bridge posts to the
334
- parent with `"*"` after the mode gate.
78
+ ## License
335
79
 
336
- Use `resized` and `srcSet` for custom image elements. The unstyled `Image`
337
- primitive in `@zappie/foundations` uses the same helpers and accepts
338
- `mode="direct"` where the host cannot serve `/cdn-cgi/image` URLs.
80
+ MIT
@@ -1,2 +1,2 @@
1
- import {a,h,g,d,i,j,k,l,q,p,e,o,m,n,s,r,f,c}from'./chunk-I55A5QXJ.js';var B=class extends a{constructor(o){super("cart_invalid",o[0]?.message??"The cart needs attention");this.issues=o;this.name="ZappieCartError";}};function K(t,e){let o=e.variantId?t.variants.find(i=>i.id===e.variantId):void 0;if(e.variantId&&!o)throw new a("invalid_request","The selected variant is missing");let n=e.choices.flatMap(i=>{let p=t.optionGroups.find(a=>a.id===i.groupId),r=p?.choices.find(a=>a.id===i.choiceId);if(!p||!r)throw new a("invalid_request","The selected option is missing");return p.isAttribute?[]:[h(r.price,Math.max(i.quantity,1))]});return g(n,o?.price??t.price)}function we(t,e,o){let n=t.categories.filter(a=>a.itemIds.includes(e)),i=t.menus??[{id:t.id,categoryIds:t.categories.map(a=>a.id)}],p=t.items[e]?.placements,r=i.filter(a=>(!p||p.some(s=>s.menuId===a.id&&a.categoryIds.includes(s.categoryId)))&&a.categoryIds.some(s=>n.some(c=>c.id===s)));return o!==void 0?typeof o=="string"&&o.trim().length>0&&o.length<=256&&r.some(a=>a.id===o)?o:void 0:r.length===1?r[0].id:void 0}function x(t,e,o=[],n){return {code:t,severity:"blocking",message:e,fixes:o,source:"local",...n?{groupId:n}:{}}}function be(t,e){let o=new Map;for(let n of e.choices??[]){let i=o.get(n.groupId)??new Set;i.add(n.choiceId),o.set(n.groupId,i);}return t.optionGroups.map(n=>{let i=n.required,{min:p,max:r}=n.selection;for(let a of n.conditions??[]){let s=o.get(a.groupId)??new Set;(a.count?a.count.op==="eq"?s.size===a.count.value:a.count.op==="gte"?s.size>=a.count.value:s.size<=a.count.value:s.size>0&&a.choiceIds.length>0&&(a.match==="all"?a.choiceIds.every(g=>s.has(g)):a.choiceIds.some(g=>s.has(g))))&&(i=a.setRequired??i,p=a.setMin??p,a.setMax!=null&&(r=a.setMax>0?a.setMax:void 0));}return {...n,required:i,selection:{min:p,...r!==void 0?{max:r}:{}}}})}function Y(t,e){let o=new Set(t.optionGroups.filter(n=>n.isAvailable===false).map(n=>n.id));return {...e,quantity:e.quantity??t.quantity.min,choices:(e.choices??[]).filter(n=>!o.has(n.groupId)).map(n=>({...n,quantity:n.quantity??1})),inputs:Object.fromEntries(Object.entries(e.inputs??{}).filter(([n])=>!o.has(n)))}}function X(t,e){let o=t.optionGroups.filter(i=>i.isAttribute),n=t.variants.filter(i=>(!e.variantId||i.id===e.variantId)&&o.every(p=>{let r=e.choices?.filter(a=>a.groupId===p.id)??[];return r.length===1&&i.attributeChoices[p.id]===r[0].choiceId}));return n.length===1?n[0]:void 0}function Je(t,e){switch(t){case "number":return Number.isFinite(Number(e));case "date":return /^\d{4}-\d{2}-\d{2}$/.test(e)&&Number.isFinite(Date.parse(e))&&new Date(e).toISOString().slice(0,10)===e;case "time":return /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/.test(e);case "toggle":return ["true","false"].includes(e.toLowerCase());default:return true}}function ee(t,e,o){if(!t||t.id!==e.itemId||!Object.hasOwn(o.menu.items,e.itemId))return [x("item_unavailable","This item is no longer on the menu",[{action:"remove_line"}])];let n=[];we(o.menu,e.itemId,e.menuId)||n.push(x("item_unavailable",e.menuId===void 0?`${t.name} is on more than one menu. Remove it and add it again from the menu you want.`:`${t.name} is no longer on the menu it was added from.`,[{action:"remove_line"}])),t.isAvailable||n.push(x("item_unavailable",`${t.name} is unavailable`,[{action:"remove_line"}]));let i=Y(t,e),p=i.quantity,{min:r,step:a}=t.quantity,s=Math.min(t.quantity.max??99,99);if(!Number.isInteger(p)||p<r||p>s||p%a!==0){let d=Math.ceil(r/a)*a,b=Math.floor(s/a)*a,S=Math.max(d,Math.min(b,Math.round((Number.isFinite(p)?p:r)/a)*a));n.push(x("quantity_limit",`Choose a valid quantity of ${t.name} (${r}\u2013${s}, step ${a})`,d<=b?[{action:"set_quantity",quantity:S}]:[]));}(i.note?.length??0)>280&&n.push(x("option_limit","Notes must be at most 280 characters"));for(let d of i.choices??[])t.optionGroups.some(b=>b.id===d.groupId)||n.push(x("option_unavailable","This option group no longer exists",[{action:"remove_line"}],d.groupId));for(let d of Object.keys(i.inputs??{}))t.optionGroups.some(b=>b.id===d)||n.push(x("option_unavailable","This input group no longer exists",[{action:"remove_line"}],d));let c=be(t,i),g=c.filter(d=>d.isAvailable!==false&&d.required&&!(i.choices??[]).some(b=>b.groupId===d.id)&&!(i.inputs?.[d.id]??"").trim());if(g.length)return [...n,...g.map(d=>x("option_required",`Choose ${d.name}`,[{action:"choose_option",groupId:d.id}],d.id))];for(let d of c){if(d.isAvailable===false)continue;let b=i.choices?.filter(L=>L.groupId===d.id)??[],S=i.inputs?.[d.id]??"",M=S.trim(),F=d.isInformational&&d.kind==="select",P=d.kind!=="select"||F&&!d.choices.length,N=[{action:"choose_option",groupId:d.id}];d.required&&!b.length&&!M&&n.push(x("option_required",`Choose ${d.name}`,N,d.id)),M&&(!P&&!F||b.length>0)&&n.push(x("option_limit",`Choose either an option or a response for ${d.name}`,N,d.id)),(P||F)&&(S.length>(d.input?.maxLength??500)||M&&!Je(d.kind,M))&&n.push(x("option_limit",`Enter a valid response for ${d.name}`,N,d.id));let U=b.reduce((L,f)=>L+(f.quantity??1),0);b.length&&(P||U<d.selection.min||U>(d.selection.max??1/0)||new Set(b.map(L=>L.choiceId)).size!==b.length)&&n.push(x("option_limit",`Check the number of options for ${d.name}`,N,d.id));for(let L of b){let f=d.choices.find(q=>q.id===L.choiceId);f?.isAvailable||n.push(x("option_unavailable",`An option in ${d.name} is unavailable`,[{action:"choose_other_option",groupId:d.id},{action:"remove_line"}],d.id));let w=L.quantity??1;(!Number.isInteger(w)||w<(f?.quantity?.min??1)||w>(f?.quantity?.max??99))&&n.push(x("option_limit",`Check the option quantity for ${d.name}`,N,d.id));}}if(!X(t,i)?.isAvailable){let d=t.optionGroups.find(b=>b.isAttribute)?.id;n.push(x("option_limit","Choose an available variant",d?[{action:"choose_option",groupId:d}]:[{action:"remove_line"}],d));}return n}function V(t){return {itemId:t.itemId,...t.menuId!==void 0?{menuId:t.menuId}:{},quantity:t.quantity,choices:t.choices.map(e=>({...e})),inputs:{...t.inputs},...t.variantId?{variantId:t.variantId}:{},...t.note!==void 0?{note:t.note}:{}}}function Ce(t){return Object.freeze({...t,choices:Object.freeze(t.choices.map(e=>Object.freeze({...e}))),inputs:Object.freeze({...t.inputs})})}function _e(t,e$1,o,n,i=[]){let p=[],r=true,a=t.map(s=>{let c=Object.hasOwn(e$1.items,s.itemId)?e$1.items[s.itemId]:void 0,g={...s,choices:s.choices?.map(M=>({...M})),inputs:{...s.inputs}},I=c?Y(c,g):g;p.push(...ee(c,I,{menu:e$1}).map(M=>({...M,lineId:s.id})));let d=I.variantId??(c?X(c,I)?.id:void 0),b=e(0,n),S=true;try{if(!c)throw new Error("Missing item");b=K(c,{choices:(I.choices??[]).map(M=>({...M,quantity:M.quantity??1})),...d?{variantId:d}:{}});}catch{r=false,S=false;}return s.unitPrice&&s.unitPrice.amountMinor!==b.amountMinor&&c&&S&&p.push({...x("price_changed",`${c.name} is now ${f.money(b)}`,[{action:"accept_price"},{action:"remove_line"}]),severity:"warning",lineId:s.id,data:{previous:s.unitPrice,current:b}}),Ce({id:s.id,itemId:s.itemId,...I.menuId!==void 0?{menuId:I.menuId}:{},name:c?.name??s.name,quantity:I.quantity??1,choices:(I.choices??[]).map(M=>({...M,quantity:M.quantity??1})),inputs:I.inputs??{},...d?{variantId:d}:{},...I.note!==void 0?{note:I.note}:{},unitPrice:b,lineSubtotal:h(b,I.quantity??1),addedAt:s.addedAt})});for(let s of i)s.code==="price_changed"&&a.some(c=>c.id===s.lineId)&&!p.some(c=>c.code===s.code&&c.lineId===s.lineId)&&p.push(s);return {lines:Object.freeze(a),issues:p,priced:r}}function Ke(t){try{return t&&e(t.amountMinor,t.currency)}catch{return}}function de(t){let e=JSON.parse(t);if(!e||e.v!==1||!Array.isArray(e.lines)||e.lines.length>100||!Number.isFinite(e.updatedAt)||typeof e.menuVersion!="string"||e.promoCode!==null&&typeof e.promoCode!="string")throw new Error("Invalid saved cart");let o=new Set;for(let n of e.lines){if(!n||typeof n.id!="string"||o.has(n.id)||typeof n.itemId!="string"||typeof n.name!="string"||!Number.isFinite(Date.parse(n.addedAt))||!Number.isInteger(n.quantity)||n.quantity<1||n.quantity>99||n.note!==void 0&&(typeof n.note!="string"||n.note.length>280)||n.menuId!==void 0&&(typeof n.menuId!="string"||!n.menuId.trim()||n.menuId.length>256)||n.variantId!==void 0&&typeof n.variantId!="string")throw new Error("Invalid saved cart line");if(o.add(n.id),!Array.isArray(n.choices)||n.choices.length>1e3||n.choices.some(i=>!i||typeof i.groupId!="string"||typeof i.choiceId!="string"||!Number.isInteger(i.quantity)||i.quantity<1||i.quantity>99))throw new Error("Invalid saved choices");if(!n.inputs||typeof n.inputs!="object"||Array.isArray(n.inputs)||Object.values(n.inputs).some(i=>typeof i!="string"))throw new Error("Invalid saved inputs")}if(e.fulfilment!==null&&!qe(e.fulfilment))throw new Error("Invalid saved fulfilment");return {v:1,lines:e.lines.map(n=>{let i=Ke(n.unitPrice);return {id:n.id,itemId:n.itemId,...n.menuId!==void 0?{menuId:n.menuId}:{},name:n.name,addedAt:n.addedAt,...i?{unitPrice:i}:{},quantity:n.quantity,choices:n.choices,inputs:n.inputs,...n.variantId?{variantId:n.variantId}:{},...n.note!==void 0?{note:n.note}:{}}}),fulfilment:e.fulfilment,promoCode:e.promoCode,menuVersion:e.menuVersion,updatedAt:e.updatedAt}}function qe(t){if(!t||!["pickup","delivery","dine_in"].includes(t.type)||typeof t.branchId!="string"||"scheduledFor"in t&&t.scheduledFor!==void 0&&!Number.isFinite(Date.parse(t.scheduledFor)))return false;if(t.type==="delivery"){let e=t.address;if(!e||typeof e.line1!="string"||!e.line1.trim()||[e.line2,e.city,e.placeId,e.instructions].some(o=>o!==void 0&&typeof o!="string")||e.coordinates&&(!Number.isFinite(e.coordinates.lat)||!Number.isFinite(e.coordinates.lng)||Math.abs(e.coordinates.lat)>90||Math.abs(e.coordinates.lng)>180))return false}return t.type!=="dine_in"||t.table===void 0||typeof t.table=="string"}function Ze(t,e){let o,n=false,i=false,p=false,r=new Map,a=c=>{p||(p=true,e(c));},s=(c,g)=>{try{if(n||(n=!0,o=t??(typeof window>"u"?void 0:window.localStorage)),o&&!i)return c(o)}catch{i=true,a("Cart storage unavailable; using memory for this client");}return g()};return {warning:a,read(c){return s(g=>{let I=g.getItem(c);return I!==null?r.set(c,I):r.delete(c),I},()=>r.get(c)??null)},write(c,g){r.set(c,g),s(I=>I.setItem(c,g),()=>{});},remove(c){r.delete(c),s(g=>g.removeItem(c),()=>{});},accepts(c){return !i&&(!c||c===o)},sweep(c,g,I){s(d=>{let b=Array.from({length:d.length??0},(S,M)=>d.key?.(M));for(let S of b)if(!(!S||S===g||!S.startsWith(c)))try{let M=d.getItem(S);M&&Date.now()-de(M).updatedAt>I&&d.removeItem(S);}catch{d.removeItem(S);}},()=>{});}}}function Se(t){return {line_id:t.id,item_id:t.itemId,...t.menuId!==void 0?{menu_id:t.menuId}:{},quantity:t.quantity,...t.variantId?{variant_id:t.variantId}:{},options:t.choices.map(e=>({group_id:e.groupId,choice_id:e.choiceId,quantity:e.quantity})),inputs:{...t.inputs},...t.note!==void 0?{note:t.note}:{}}}function ze(t){return {type:t.type,..."scheduledFor"in t&&t.scheduledFor?{scheduled_for:t.scheduledFor}:{},...t.type==="dine_in"&&t.table?{table:t.table}:{},...t.type==="delivery"?{address:{line1:t.address.line1,...t.address.line2?{line2:t.address.line2}:{},...t.address.city?{city:t.address.city}:{},...t.address.placeId?{place_id:t.address.placeId}:{},...t.address.instructions?{instructions:t.address.instructions}:{},...t.address.coordinates?{lat:t.address.coordinates.lat,lng:t.address.coordinates.lng}:{}}}:{}}}var Ye=Object.freeze({lines:Object.freeze([]),count:0,subtotal:null,fulfilment:null,promoCode:null,state:{status:"ready"},issues:Object.freeze([]),isHydrated:false});function Ue(t){if(t&&typeof t=="object"&&!Object.isFrozen(t)){Object.freeze(t);for(let e of Object.values(t))Ue(e);}return t}function ye(t){return JSON.stringify([t.itemId,t.menuId,t.variantId,t.note??"",[...t.choices??[]].sort((e,o)=>e.groupId.localeCompare(o.groupId)||e.choiceId.localeCompare(o.choiceId)),Object.entries(t.inputs??{}).sort(([e],[o])=>e.localeCompare(o))])}function Ve(t){let{config:e$1,menu:o,store:n,events:i}=t,p=e$1.cart?.ttl??10080*60*1e3;if(!Number.isFinite(p)||p<=0)throw new a("invalid_request","Cart ttl must be positive milliseconds");let r=Ye,a$1,s,c$1=e$1.branch,g$1=0,I,d=0,b=0,S=0,M=0,F=0,P,N,U=0,L,f=new Set,w=`zappie:cart:v1:${e$1.store}:`,q=e$1.cart?.storageScope,R=u=>`${w}${u}${q?`:${q}`:""}`,k=()=>R(s.id),A=Ze(e$1.storage,u=>i.emit("warning",{message:u})),j=u=>{let l=A.read(R(u));return l===null&&q&&(l=A.read(w+u),l!==null&&(A.remove(w+u),A.write(R(u),l))),l},ie,xe,Qe=()=>xe??(xe=import('./cart/quote.js').then(({createQuoteClient:u})=>ie=u(t.transport,t.context),u=>{throw xe=void 0,u})),G=u=>{r=Ue({...r,...u,branchId:s?.id});for(let l of [...f])try{l();}catch(m){i.emit("error",{error:c(m)});}},ne=(u,l)=>u.some(m=>m.severity!=="info")?{status:"needs_attention",issues:u,...l?{quote:l}:{}}:l?{status:"quoted",quote:l}:{status:"ready"},Q=()=>{d++,b++,clearTimeout(I),ie?.cancel();},oe=()=>{let u={v:1,lines:r.lines.map(l=>({...V(l),id:l.id,name:l.name,addedAt:l.addedAt,unitPrice:l.unitPrice})),fulfilment:r.fulfilment,promoCode:r.promoCode,menuVersion:a$1.version,updatedAt:g$1};A.write(k(),JSON.stringify(u));},se=()=>{clearTimeout(I),r.lines.length&&(I=setTimeout(()=>{J.quote().catch(()=>{});},300));},me=(u,l=r.issues.filter(v=>v.source==="local"),m={})=>{let v=n.peek().currency,h=_e(u,a$1,s,v,l),y=h.priced?g(h.lines.map(_=>_.lineSubtotal),e(0,v)):null;G({...m,lines:h.lines,count:h.lines.reduce((_,T)=>_+T.quantity,0),subtotal:y,issues:h.issues,state:ne(h.issues)});for(let _ of h.issues)i.emit("cart_issue",{code:_.code});},H=(u=r.lines,l={})=>{Q(),me(u,void 0,l),g$1=Date.now(),oe(),se();},Ie=u=>{let l=n.peek()?.branches.find(m=>m.id===u.branchId);if(!l)throw new a("branch_not_found","Load the storefront branch before using its cart");a$1=u,s=l,c$1=l.id;},pe=(u,l=true)=>{Q();let m;try{m=u?de(u):void 0,m&&Date.now()-m.updatedAt>p&&(m=void 0,A.remove(k()),i.emit("cart_expired",{branchId:s.id}));}catch{A.remove(k()),A.warning("Invalid saved cart discarded");}g$1=m?.updatedAt??Date.now();let v=Date.now()-g$1,h=m?.fulfilment??null;(h?.branchId!==s.id||v>864e5&&h.type==="delivery")&&(h=null),h&&"scheduledFor"in h&&h.scheduledFor&&Date.parse(h.scheduledFor)<Date.now()&&(h={...h},delete h.scheduledFor);let y=(m?.lines??[]).map(T=>({...T,...v>864e5?{inputs:{}}:{}})),_=_e(y,a$1,s,n.peek().currency);me(_.lines,_.issues,{fulfilment:h,promoCode:m?.promoCode??null,isHydrated:true}),u!==null&&l&&oe(),A.sweep(w,k(),p);},Z=()=>{let u=s?.id??n.peek()?.branches.find(h=>h.id===c$1||h.slug===c$1||!c$1&&h.isDefault)?.id??(c$1?void 0:n.peek()?.branches[0]?.id);if(r.isHydrated)return;let l=a$1??(u?t.workingSet.peek(u,[]):void 0);if(!l||!n.peek())throw new a("invalid_request","Load the store and menu before editing the cart");Ie(l);let m=j(s.id),v=[];if(m===null&&L)try{v=L(A.read);}catch{A.warning("Invalid legacy cart discarded");}pe(m),m===null&&L&&oe();for(let h of v.slice(0,100))try{J.add(h.itemId,h);}catch{i.emit("cart_issue",{code:"item_unavailable"});}},fe=async()=>{let u=s?.id,l=S,m=++M,v=d;if(!u||!a$1||!r.lines.length||a$1.completeness!=="partial"&&(await t.transport.getMenuVersion({...t.context(),branchId:u})).version===a$1.version)return;let h=await t.workingSet.read(u,r.lines.map(y=>y.itemId),true);l!==S||m!==M||v!==d||JSON.stringify(h)===JSON.stringify(a$1)||(a$1=h,Q(),me(r.lines),oe(),se());},Re=u=>{let l=t.workingSet.peek(s.id,[...r.lines.map(m=>m.itemId),u]);l.items[u]&&(a$1=l);},he=(u,l,m)=>{Re(u.itemId);let v=Object.hasOwn(a$1.items,u.itemId)?a$1.items[u.itemId]:void 0,h$1=ee(v,u,{menu:a$1});if(h$1.length||!v)throw new B(h$1);let y=Y(v,u),_=X(v,y),T=(y.choices??[]).map(E=>({...E,quantity:E.quantity??1})),O=K(v,{choices:T,variantId:_.id});return Ce({id:l??`line_${Array.from(globalThis.crypto.getRandomValues(new Uint8Array(10)),E=>"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-"[E&63]).join("")}`,itemId:v.id,...y.menuId!==void 0?{menuId:y.menuId}:{},name:v.name,quantity:y.quantity,choices:T,inputs:y.inputs??{},variantId:_.id,...y.note!==void 0?{note:y.note}:{},unitPrice:O,lineSubtotal:h(O,y.quantity),addedAt:m??new Date().toISOString()})},He=i.on("menu:updated",({branchId:u,version:l})=>{if(s?.id===u){let m=o.peek(u);m&&m.completeness!=="partial"&&m.version===l&&m.version!==a$1?.version&&(M++,a$1=m,Q(),me(r.lines),oe(),se());}}),J={get branchId(){return r.branchId},get lines(){return r.lines},get count(){return r.count},get subtotal(){return r.subtotal},get fulfilment(){return r.fulfilment},get promoCode(){return r.promoCode},get state(){return r.state},get issues(){return r.issues},get isHydrated(){return r.isHydrated},getSnapshot:()=>r,subscribe(u){return f.add(u),()=>{f.delete(u);}},async hydrate(){if(r.isHydrated)return fe();let u=U;return N??(N=(async()=>{let l=await n.get(),m=l.branches.find(y=>y.id===c$1||y.slug===c$1)??(c$1?void 0:l.branches.find(y=>y.isDefault)??l.branches[0]);if(!m)throw new a("branch_not_found","Cart branch is unavailable");let v=r.lines;if(!r.isHydrated){let y=j(m.id);try{let _=y?de(y):void 0;v=_&&Date.now()-_.updatedAt<=p?_.lines:y===null?L?.(A.read)??[]:[];}catch{v=[];}}let h=await t.workingSet.read(m.id,v.map(y=>y.itemId));u===U&&(Ie(h),Z(),h.completeness!=="partial"&&await fe());})().finally(()=>{N=void 0;})),N},mount(){if(F++,!P&&typeof window<"u"){let l=v=>{if(s&&v.key===k()&&A.accepts(v.storageArea)){let h=v.newValue,y;try{y=h?de(h).lines.map(z=>z.itemId):[];}catch{pe(h,false);return}let _=t.workingSet.peek(s.id,y);if(_.completeness!=="partial"||y.every(z=>_.items[z])){a$1=_,pe(h,false),se();return}let T=d,O=++M,E=U;t.workingSet.read(s.id,y).then(z=>{E===U&&T===d&&O===M&&(a$1=z,pe(h,false),se());}).catch(z=>i.emit("error",{error:c(z)}));}},m=()=>{fe().catch(v=>i.emit("error",{error:c(v)}));};window.addEventListener("storage",l),window.addEventListener("focus",m),P=()=>{window.removeEventListener("storage",l),window.removeEventListener("focus",m);};}F===1&&J.hydrate().catch(l=>i.emit("error",{error:c(l)}));let u=true;return ()=>{u&&(u=false,F--,F===0&&(P?.(),P=void 0,Q()));}},dispose(){U++,S++,He(),Q(),P?.(),P=void 0,f.clear();},validate(u){Z(),Re(u.itemId);let l=ee(Object.hasOwn(a$1.items,u.itemId)?a$1.items[u.itemId]:void 0,u,{menu:a$1});return l.length?{ok:false,issues:l}:{ok:true}},add(u,l={}){Z();let m=he({...l,itemId:typeof u=="string"?u:u.id}),v=r.lines.find(h=>ye(V(h))===ye(V(m)));if(v)m=he({...V(m),quantity:v.quantity+m.quantity},v.id,v.addedAt);else if(r.lines.length>=100)throw new B([x("quantity_limit","A cart can contain at most 100 lines")]);return H(v?r.lines.map(h=>h.id===v.id?m:h):[...r.lines,m]),i.emit("add_to_cart",{itemId:m.itemId,quantity:m.quantity-(v?.quantity??0),value:h(m.unitPrice,m.quantity-(v?.quantity??0))}),m},update(u,l){Z();let m=r.lines.find(_=>_.id===u);if(!m)throw new B([x("item_not_found","Cart line not found")]);let v={...V(m),...l};l.choices&&!l.variantId&&delete v.variantId;let h=he(v,m.id,m.addedAt),y=r.lines.find(_=>_.id!==u&&ye(V(_))===ye(V(h)));return y&&(h=he({...V(h),quantity:h.quantity+y.quantity},m.id,m.addedAt)),H(r.lines.filter(_=>_.id!==y?.id).map(_=>_.id===u?h:_)),i.emit("update_cart_line",{lineId:u,itemId:h.itemId,quantity:h.quantity}),h},remove(u){Z();let l=r.lines.find(m=>m.id===u);H(r.lines.filter(m=>m.id!==u)),l&&i.emit("remove_from_cart",{itemId:l.itemId,quantity:l.quantity,value:l.lineSubtotal});},clear(){Z(),H([],{fulfilment:null,promoCode:null});},setFulfilment(u){if(Z(),!qe(u)||u.branchId!==s.id)throw new B([x("fulfilment_unavailable","Select fulfilment for the cart's branch")]);H(r.lines,{fulfilment:structuredClone(u)});},setPromoCode(u){Z(),H(r.lines,{promoCode:u?.trim()||null});},async moveTo(u){Z();let l=++S;Q(),G({state:ne(r.issues)});let m=await t.workingSet.read(u,r.lines.map(O=>O.itemId));if(l!==S)return;let v=k(),h=r.lines;Ie(m);let y=h.filter(O=>!ee(m.items[O.itemId],V(O),{menu:m}).length);H(y,{fulfilment:null,promoCode:null});let _=h.filter(O=>!y.includes(O)).map(O=>({...x("item_unavailable",`${O.name} is unavailable at this branch`),severity:"warning",lineId:O.id})),T=[...r.issues,..._];G({issues:T,state:ne(T)}),v!==k()&&A.remove(v);},async quote(u={}){Z(),clearTimeout(I),u.force&&i.emit("view_cart",{});let l={branch_id:s.id,lines:r.lines.map(Se),...r.fulfilment?{fulfilment:ze(r.fulfilment)}:{},...r.promoCode?{promo_code:r.promoCode}:{},menu_version:a$1.version},m=d,v=++b;G({state:{status:"quoting"}});try{return await(await Qe()).get(l,u.force??!1,y=>{if(m!==d)return;let _=r.issues.filter(E=>E.source==="local"),T=y.issues.filter(E=>!_.some(z=>z.code===E.code&&z.lineId===E.lineId&&z.groupId===E.groupId)),O=[..._,...T];G({issues:O,state:ne(O,y)});for(let E of T)i.emit("cart_issue",{code:E.code});(y.menuVersion!==a$1.version||y.issues.some(E=>E.code==="menu_version_stale"))&&fe().catch(E=>i.emit("error",{error:c(E)}));})}catch(h){if(m===d&&v===b){let y=r.issues.filter(_=>_.source==="local");G({issues:y,state:ne(y)}),i.emit("quote_failed",{code:c(h).code});}throw h instanceof Error&&h.name==="AbortError"?h:c(h)}},applyFix(u,l=u.fixes[0]){if(!r.issues.includes(u)||!l||!u.fixes.some(m=>JSON.stringify(m)===JSON.stringify(l)))throw new a("invalid_request","Choose a current cart issue and one of its fixes");switch(l.action){case "remove_line":{u.lineId&&J.remove(u.lineId);return}case "remove_promo":J.setPromoCode(null);return;case "set_quantity":{u.lineId&&J.update(u.lineId,{quantity:l.quantity});return}case "accept_price":{ie?.accept(u);let m=r.issues.filter(y=>y!==u),v="quote"in r.state?r.state.quote:void 0,h=v?{...v,issues:v.issues.filter(y=>!(y.code===u.code&&y.lineId===u.lineId))}:void 0;G({issues:m,state:ne(m,h)});return}default:throw new a("invalid_request",`${l.action} needs the shopper's selection; use update or setFulfilment`)}},toCheckoutPayload(){Z();let u=r.issues.filter(l=>l.severity==="blocking");if((!r.lines.length||!r.fulfilment)&&u.push(x("fulfilment_unavailable","Add items and select fulfilment before checkout")),u.length)throw new B(u);return {lines:r.lines.map(Se),fulfilment:structuredClone(r.fulfilment),...r.promoCode?{promo_code:r.promoCode}:{},menu_version:a$1.version}}};return {cart:Object.freeze(J),cartCatalogue:()=>a$1,setLegacyReader(u){L=u;}}}function De(t){let e=(n,i,p)=>{let r=p??t.menu.peek(n),a=Object.fromEntries([...new Set(i)].flatMap(s=>{let c=t.peekItem(s,n)??r?.items[s];return c?[[s,c]]:[]}));return {id:`branch:${n}`,name:"Cart working set",branchId:n,version:"",...r,kind:"working-set",completeness:"partial",items:a,categories:(r?.categories??[]).map(s=>({...s,itemIds:Object.values(a).filter(c=>c.placements?c.placements.some(g=>g.categoryId===s.id&&(r?.menus??[{id:r?.id,categoryIds:[s.id]}]).some(I=>I.id===g.menuId&&I.categoryIds.includes(s.id))):s.itemIds.includes(c.id)).map(c=>c.id)}))}},o=(n,i)=>{let p=t.menu.peek(n);return p&&p.completeness!=="partial"?p:e(n,i)};return {peek:o,async read(n,i,p=false){let r=o(n,i);if(!i.length||!p&&r.completeness!=="partial")return r;let a$1=p?await t.transport.getMenu({...t.context(),branchId:n,metadataOnly:true}):void 0,s=a$1&&"menu"in a$1?a$1.menu:await t.menu.get({branch:n,metadataOnly:true});if(s.branchId!==n)throw new a("server_error","Cart metadata belongs to another branch");return t.menu.hydrate(s),s.completeness!=="partial"?s:(await Promise.all([...new Set(i)].map(async c=>{try{await t.readItem(c,n,p);}catch(g){if(g.code!=="item_not_found")throw g}})),e(n,i,s))}}}var Me="2026-10-01";function We(t,e=false){if(!t.store?.trim())throw new a("invalid_request","A storefront slug is required");if(e&&typeof window<"u")throw new a("invalid_request","Secret keys cannot be used in a browser");let n=(e?/^sk_(test|live)_.+$/:/^pk_(test|live)_.+$/).exec(t.key);if(!n)throw new a("invalid_request",e?"A secret sk_test_ or sk_live_ key is required":"Use a publishable pk_test_ or pk_live_ key; secret keys are never accepted by createZappie");return n[1]}function re(t){return t?.reason??new DOMException("The request was aborted","AbortError")}function te(t){if(t?.aborted)throw re(t)}function ke(t,e){return new Promise((o,n)=>{te(e);let i=()=>{clearTimeout(p),n(re(e));},p=setTimeout(()=>{e?.removeEventListener("abort",i),o();},t);e?.addEventListener("abort",i,{once:true});})}var Xe=1e4,et=Object.freeze({fetchedAt:0,loading:false}),ve=class{constructor(e){this.onError=e;this.entries=new Map;this.listeners=new Set;this.flights=new Map;this.subscribe=e=>(this.listeners.add(e),()=>{this.listeners.delete(e);});}snapshot(e){return this.entries.get(e)??et}notify(){for(let e of [...this.listeners])e();}set(e,o,n){this.entries.set(e,{value:o,fetchedAt:Date.now(),loading:false,...n!==void 0?{etag:n}:{}}),this.notify();}stale(e){let o=this.snapshot(e);return o.value!==void 0&&Date.now()-o.fetchedAt>=6e4}async get(e,o,n){te(n);let i=this.snapshot(e),p=Date.now()-i.fetchedAt;if(i.value!==void 0&&p<6e4)return i.value;let r=i.value!==void 0&&p<6e5;if(r&&i.failedAt!==void 0&&Date.now()-i.failedAt<Xe)return i.value;let a=this.flights.get(e);if(!a){let c$1=new AbortController;a={controller:c$1,users:0,keepAlive:r,promise:Promise.resolve()};let g=a;this.flights.set(e,g);let I={...i,loading:true};this.entries.set(e,I),this.notify(),g.promise=Promise.resolve().then(()=>(te(c$1.signal),o(c$1.signal,i))).then(d=>(this.entries.get(e)===I&&!c$1.signal.aborted&&(this.set(e,d.value,d.etag),d.onStored?.()),d.value)).catch(d=>{throw this.entries.get(e)===I&&(this.entries.set(e,c$1.signal.aborted?{...i,loading:false}:{...i,loading:false,error:c(d),failedAt:Date.now()}),this.notify(),c$1.signal.aborted||this.onError(c(d))),c$1.signal.aborted?re(c$1.signal):c(d)}).finally(()=>{this.flights.get(e)===g&&this.flights.delete(e);});}if(r)return a.keepAlive=true,a.promise.catch(()=>{}),i.value;let s=a;return s.users++,new Promise((c,g)=>{let I=false,d=S=>{I||(I=true,n?.removeEventListener("abort",b),s.users--,S());},b=()=>{d(()=>g(re(n))),s.users===0&&!s.keepAlive&&(s.controller.abort(re(n)),this.flights.get(e)===s&&this.flights.delete(e));};n?.addEventListener("abort",b,{once:true}),s.promise.then(S=>d(()=>c(S)),S=>d(()=>g(S))),n?.aborted&&b();})}};function Be(){let t=new Map;return {on(e,o){let n=t.get(e)??new Set;return t.set(e,n),n.add(o),()=>{n.delete(o);}},emit(e,o){for(let n of [...t.get(e)??[]])try{n(o);}catch(i){console.error("Zappie event listener failed",i);}}}}var tt=5e3,le=class{constructor(e={}){let o=new URL(e.baseUrl??"https://api.zappie.ai/storefront/v1");if(o.username||o.password||o.search||o.hash||o.protocol!=="https:"&&!(o.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(o.hostname)))throw new a("invalid_request","REST baseUrl requires HTTPS, or HTTP on localhost");this.baseUrl=o.href.replace(/\/$/,""),this.fetcher=e.fetch??((n,i)=>globalThis.fetch(n,i));}async request(e,o,n,i){if(typeof window<"u"&&o.key.startsWith("sk_"))throw new a("invalid_request","Secret keys cannot be sent from a browser");for(let p=0;;p++){te(o.signal);let r;try{r=await this.fetcher(this.baseUrl+e,{method:i?"POST":"GET",...i?{body:JSON.stringify(i)}:{},redirect:"manual",headers:{...i?{"Content-Type":"application/json"}:{},...o.idempotencyKey?{"Idempotency-Key":o.idempotencyKey}:{},Authorization:`Bearer ${o.key}`,"Zappie-Version":o.version,...n?{"If-None-Match":n}:{}},...o.signal?{signal:o.signal}:{}});}catch(d){if(te(o.signal),p>=2)throw new a("network_error","Could not reach the storefront service",{cause:d});await ke(100*2**p+Math.random()*100,o.signal);continue}if(r.ok||r.status===304)return r;let a$1=r.headers.get("Retry-After"),s=a$1!==null?Number(a$1):NaN,c=a$1!==null?Number.isFinite(s)?Math.max(0,s*1e3):Math.max(0,Date.parse(a$1)-Date.now()):NaN,g=Number.isFinite(c)&&c>tt;if(p<2&&!g&&(r.status===429||r.status>=500)){await r.body?.cancel(),await ke(Number.isFinite(c)?c:100*2**p+Math.random()*100,o.signal);continue}let I=await r.json().catch(()=>({}));throw d(I,r.status,r.headers.get("Zappie-Request-Id")??void 0)}}async json(e){try{return await e.json()}catch(o){throw o instanceof Error&&o.name==="AbortError"?o:new a("server_error","The storefront service returned invalid JSON",{status:e.status,cause:o})}}async quote(e,o){let n={...e,idempotencyKey:e.idempotencyKey??await i(JSON.stringify(o))};return await this.json(await this.request("/quote",n,void 0,o))}async getStore(e$1){let o$1=import('./transport/store.js').catch(r=>{throw new a("server_error","Could not load the storefront store mapper",{cause:r})}),n$1=this.request("/store",e$1).then(r=>this.json(r)),[{mapStore:i},p$1]=await Promise.all([o$1,n$1]);return i(p$1,j,k,l,q,p,e,o,m,n)}async getMenu(e){let o,n=new Set,i,p;do{let r=new URLSearchParams({branch:e.branchId,...e.metadataOnly?{metadata_only:"true"}:{},...o?{cursor:o}:{}}),a$1=await this.request(`/menus?${r}`,e,o?void 0:e.ifNoneMatch);if(a$1.status===304){if(o)throw new a("server_error","Unexpected 304 on a continuation page");return {notModified:true,...a$1.headers.get("ETag")?{etag:a$1.headers.get("ETag")}:{}}}let s$1=j(await this.json(a$1)),c=s(s$1,e.branchId);if(e.metadataOnly){if(Object.keys(c.items).length||s$1.next_cursor)throw new a("server_error","Invalid metadata response");return {menu:{...c,completeness:"partial"}}}if(!i)i=c,p=a$1.headers.get("ETag")??void 0;else {if(c.version!==i.version)throw new a("server_error","Menu changed while fetching pages; retry the read");Object.assign(i.items,c.items);for(let g of c.categories){let I=i.categories.find(d=>d.id===g.id);I?I.itemIds=[...new Set([...I.itemIds,...g.itemIds])]:i.categories.push(g);}for(let g of c.menus??[]){let I=i.menus?.find(d=>d.id===g.id);I?I.categoryIds=[...new Set([...I.categoryIds,...g.categoryIds])]:i.menus?.push(g);}}if(o=typeof s$1.next_cursor=="string"&&s$1.next_cursor?s$1.next_cursor:void 0,o){if(n.has(o))throw new a("server_error","Repeated menu pagination cursor");n.add(o);}}while(o);i.menus&&i.menus.length>1&&(i.id=`branch:${e.branchId}`,i.name="Catalogue",delete i.schedule);for(let r of i.categories)for(let a$1 of r.itemIds)if(!Object.prototype.hasOwnProperty.call(i.items,a$1))throw new a("server_error","Category refers to a missing item");return Object.keys(i.items).length>2e3&&console.warn("Zappie menu contains more than 2,000 items"),{menu:i,...p?{etag:p}:{}}}async getSelectedMenu(e){return (await import('./transport/selected-menu.js')).selectedMenu(e,o=>this.request(o,e,e.ifNoneMatch),o=>this.json(o),s,()=>new a("server_error","Invalid selected menu"))}async search(e){return (await import('./transport/search.js')).searchPage(e,async o=>this.json(await this.request(o,e)),s,()=>new a("server_error","Invalid search page"))}async getCategoryPage(e){let o=new URLSearchParams({branch:e.branchId,...e.cursor?{cursor:e.cursor}:{}}),n=await this.json(await this.request(`/menus/${encodeURIComponent(e.menuId)}/categories/${encodeURIComponent(e.categoryId)}/items?${o}`,e));return (await import('./transport/category-page.js')).categoryPage(j(n),e,s,()=>new a("server_error","Invalid category page"))}async getMenuVersion(e){let o=j(await this.json(await this.request(`/menus/version?${new URLSearchParams({branch:e.branchId})}`,e)));if(typeof o.menu_version!="string"||typeof o.availability_hash!="string")throw new a("server_error","Invalid menu version response");return {version:o.menu_version,availabilityHash:o.availability_hash}}async getItem(e){let o=r(await this.json(await this.request(`/items/${encodeURIComponent(e.itemId)}?${new URLSearchParams({branch:e.branchId})}`,e)));if(o.id!==e.itemId)throw new a("server_error","Item response identity does not match the request");return o}};function nt(t,e=false){let o=We(t,e),n=Object.freeze({...t,version:t.version??Me,telemetry:t.telemetry??true}),i=t.transport??new le({...t.baseUrl?{baseUrl:t.baseUrl}:{},...t.fetch?{fetch:t.fetch}:{}}),p=Be(),r=new ve(f=>p.emit("error",{error:f})),a$1=f=>({key:n.key,version:n.version,...f?{signal:f}:{}}),s={get:(f={})=>r.get("store",async w=>({value:await i.getStore(a$1(w))}),f.signal),peek:()=>r.snapshot("store").value},c=(f,w)=>{let q=w??n.branch,R=q?f.branches.find(k=>k.id===q||k.slug===q):f.branches.find(k=>k.isDefault)??f.branches[0];if(!R)throw new a("branch_not_found","No matching storefront branch",{param:"branch"});return R},g,I=f=>{let w=s.peek();if(w)return c(w,f).id;let q=f??n.branch;return g&&(q===void 0||q===n.branch||q===g)?g:q??"default"},d=f=>`menu:${I(f)}`,b={async search(f,w={}){if(!i.search)throw new a("invalid_request","Transport does not support search");let q=c(await s.get(w),w.branch);return i.search({...a$1(w.signal),branchId:q.id,query:f,...w.cursor?{cursor:w.cursor}:{}})},async get(f={}){let w=c(await s.get(f),f.branch);return r.get(`menu:${w.id}${f.metadataOnly?":partial":""}`,async(q,R)=>{let k=await i.getMenu({...a$1(q),branchId:w.id,...f.metadataOnly?{metadataOnly:true}:{},...R.etag?{ifNoneMatch:R.etag}:{}});if("notModified"in k){if(!R.value)throw new a("server_error","Received 304 without a cached menu");return {value:R.value,...k.etag??R.etag?{etag:k.etag??R.etag}:{}}}if(!f.metadataOnly&&k.menu.completeness==="partial")throw new a("server_error","Partial catalogue cannot satisfy a complete read");if(k.menu.branchId!==w.id)throw new a("server_error","Menu belongs to a different branch");return {value:k.menu,etag:k.etag??k.menu.version,onStored:()=>{R.value&&R.value.version!==k.menu.version&&p.emit("menu:updated",{branchId:w.id,version:k.menu.version});}}},f.signal)},peek:f=>r.snapshot(d(f)).value??r.snapshot(`${d(f)}:partial`).value,async page(f,w,q={}){if(!i.getCategoryPage)throw new a("invalid_request","Transport does not support category pages");let R=c(await s.get(q),q.branch);return i.getCategoryPage({...a$1(q.signal),branchId:R.id,menuId:f,categoryId:w,...q.cursor?{cursor:q.cursor}:{}})},async version(f={}){let w=c(await s.get(f),f.branch);return i.getMenuVersion({...a$1(f.signal),branchId:w.id})},async item(f,w={}){let q=c(await s.get(w),w.branch);return (!b.peek(q.id)||b.peek(q.id)?.completeness==="partial")&&await b.get({...w,metadataOnly:true}),F(f,q.id,false,w.signal)},hydrate(f){g??(g=f.branchId);let w=r.snapshot(`menu:${f.branchId}`).value;r.set(`menu:${f.branchId}${f.completeness==="partial"?":partial":""}`,f,f.version),w&&w.version!==f.version&&p.emit("menu:updated",{branchId:f.branchId,version:f.version});}},S=new Map,M=(f,w)=>S.get(`${w}:${f}`),F=async(f,w,q=false,R)=>{let k=r.snapshot(`menu:${w}`).value;if(!q&&k&&Object.hasOwn(k.items,f))return k.items[f];let A=(j=R)=>i.getItem({...a$1(j),branchId:w,itemId:f});try{let j=q?await A():await r.get(`item:${w}:${f}`,async ie=>({value:await A(ie)}),R);return q&&r.set(`item:${w}:${f}`,j),S.set(`${w}:${f}`,j),j}catch(j){throw j.code==="item_not_found"&&(S.delete(`${w}:${f}`),r.set(`item:${w}:${f}`,void 0)),j}},P=De({menu:b,store:s,transport:i,context:a$1,peekItem:M,readItem:F}),{cart:N,setLegacyReader:U,cartCatalogue:L}=Ve({workingSet:P,config:n,menu:b,store:s,transport:i,context:a$1,events:p});return Object.freeze({cart:N,config:n,mode:o,store:s,menu:b,format:f,on:p.on,_internal:{cartCatalogue:L,setLegacyReader:U,transport:i,cache:r,emit:p.emit,menuKey:d,hydrateStore:f=>r.set("store",f)}})}function rt(t){let e=nt(t);return e.mode==="test"&&typeof window<"u"&&import('./debug.js').then(({installDebug:o})=>o(e)),e}function Ge(t){return Number.isInteger(t)&&t>=0&&t<=99?t:0}function it(t,e){let o=Y(t,e),n=be(t,o).filter(s=>s.isAvailable!==false),i=X(t,o)?.id,p=(o.choices??[]).filter(({groupId:s,choiceId:c})=>n.some(g=>g.id===s&&g.choices.some(I=>I.id===c))).map(s=>({...s,quantity:Ge(s.quantity)})),r=K(t,{choices:p,...i?{variantId:i}:{}}),a=h(r,Ge(o.quantity));return {groups:n,unitPrice:r,total:a}}var an="0.0.0";
2
- export{B as a,K as b,we as c,ee as d,ze as e,Ye as f,Me as g,We as h,te as i,ke as j,le as k,nt as l,rt as m,it as n,an as o};//# sourceMappingURL=chunk-VG7NKIKR.js.map
1
+ import {a,h,g,d,i,j,k,l,q,p,e,o,m,n,s,r,f,c}from'./chunk-I55A5QXJ.js';var B=class extends a{constructor(o){super("cart_invalid",o[0]?.message??"The cart needs attention");this.issues=o;this.name="ZappieCartError";}};function J(t,e){let o=e.variantId?t.variants.find(i=>i.id===e.variantId):void 0;if(e.variantId&&!o)throw new a("invalid_request","The selected variant is missing");let n=e.choices.flatMap(i=>{let p=t.optionGroups.find(a=>a.id===i.groupId),r=p?.choices.find(a=>a.id===i.choiceId);if(!p||!r)throw new a("invalid_request","The selected option is missing");return p.isAttribute?[]:[h(r.price,Math.max(i.quantity,1))]});return g(n,o?.price??t.price)}function we(t,e,o){let n=t.categories.filter(a=>a.itemIds.includes(e)),i=t.menus??[{id:t.id,categoryIds:t.categories.map(a=>a.id)}],p=t.items[e]?.placements,r=i.filter(a=>(!p||p.some(s=>s.menuId===a.id&&a.categoryIds.includes(s.categoryId)))&&a.categoryIds.some(s=>n.some(c=>c.id===s)));return o!==void 0?typeof o=="string"&&o.trim().length>0&&o.length<=256&&r.some(a=>a.id===o)?o:void 0:r.length===1?r[0].id:void 0}function E(t,e,o=[],n){return {code:t,severity:"blocking",message:e,fixes:o,source:"local",...n?{groupId:n}:{}}}function be(t,e){let o=new Map;for(let n of e.choices??[]){let i=o.get(n.groupId)??new Set;i.add(n.choiceId),o.set(n.groupId,i);}return t.optionGroups.map(n=>{let i=n.required,{min:p,max:r}=n.selection;for(let a of n.conditions??[]){let s=o.get(a.groupId)??new Set;(a.count?a.count.op==="eq"?s.size===a.count.value:a.count.op==="gte"?s.size>=a.count.value:s.size<=a.count.value:s.size>0&&a.choiceIds.length>0&&(a.match==="all"?a.choiceIds.every(g=>s.has(g)):a.choiceIds.some(g=>s.has(g))))&&(i=a.setRequired??i,p=a.setMin??p,a.setMax!=null&&(r=a.setMax>0?a.setMax:void 0));}return {...n,required:i,selection:{min:p,...r!==void 0?{max:r}:{}}}})}function Y(t,e){let o=new Set(t.optionGroups.filter(n=>n.isAvailable===false).map(n=>n.id));return {...e,quantity:e.quantity??t.quantity.min,choices:(e.choices??[]).filter(n=>!o.has(n.groupId)).map(n=>({...n,quantity:n.quantity??1})),inputs:Object.fromEntries(Object.entries(e.inputs??{}).filter(([n])=>!o.has(n)))}}function X(t,e){let o=t.optionGroups.filter(i=>i.isAttribute),n=t.variants.filter(i=>(!e.variantId||i.id===e.variantId)&&o.every(p=>{let r=e.choices?.filter(a=>a.groupId===p.id)??[];return r.length===1&&i.attributeChoices[p.id]===r[0].choiceId}));return n.length===1?n[0]:void 0}function He(t,e){switch(t){case "number":return Number.isFinite(Number(e));case "date":return /^\d{4}-\d{2}-\d{2}$/.test(e)&&Number.isFinite(Date.parse(e))&&new Date(e).toISOString().slice(0,10)===e;case "time":return /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/.test(e);case "toggle":return ["true","false"].includes(e.toLowerCase());default:return true}}function ee(t,e,o){if(!t||t.id!==e.itemId||!Object.hasOwn(o.menu.items,e.itemId))return [E("item_unavailable","This item is no longer on the menu",[{action:"remove_line"}])];let n=[];we(o.menu,e.itemId,e.menuId)||n.push(E("item_unavailable",e.menuId===void 0?`${t.name} is on more than one menu. Remove it and add it again from the menu you want.`:`${t.name} is no longer on the menu it was added from.`,[{action:"remove_line"}])),t.isAvailable||n.push(E("item_unavailable",`${t.name} is unavailable`,[{action:"remove_line"}]));let i=Y(t,e),p=i.quantity,{min:r,step:a}=t.quantity,s=Math.min(t.quantity.max??99,99);if(!Number.isInteger(p)||p<r||p>s||p%a!==0){let d=Math.ceil(r/a)*a,b=Math.floor(s/a)*a,q=Math.max(d,Math.min(b,Math.round((Number.isFinite(p)?p:r)/a)*a));n.push(E("quantity_limit",`Choose a valid quantity of ${t.name} (${r}\u2013${s}, step ${a})`,d<=b?[{action:"set_quantity",quantity:q}]:[]));}(i.note?.length??0)>280&&n.push(E("option_limit","Notes must be at most 280 characters"));for(let d of i.choices??[])t.optionGroups.some(b=>b.id===d.groupId)||n.push(E("option_unavailable","This option group no longer exists",[{action:"remove_line"}],d.groupId));for(let d of Object.keys(i.inputs??{}))t.optionGroups.some(b=>b.id===d)||n.push(E("option_unavailable","This input group no longer exists",[{action:"remove_line"}],d));let c=be(t,i),g=c.filter(d=>d.isAvailable!==false&&d.required&&!(i.choices??[]).some(b=>b.groupId===d.id)&&!(i.inputs?.[d.id]??"").trim());if(g.length)return [...n,...g.map(d=>E("option_required",`Choose ${d.name}`,[{action:"choose_option",groupId:d.id}],d.id))];for(let d of c){if(d.isAvailable===false)continue;let b=i.choices?.filter(P=>P.groupId===d.id)??[],q=i.inputs?.[d.id]??"",M=q.trim(),F=d.isInformational&&d.kind==="select",T=d.kind!=="select"||F&&!d.choices.length,N=[{action:"choose_option",groupId:d.id}];d.required&&!b.length&&!M&&n.push(E("option_required",`Choose ${d.name}`,N,d.id)),M&&(!T&&!F||b.length>0)&&n.push(E("option_limit",`Choose either an option or a response for ${d.name}`,N,d.id)),(T||F)&&(q.length>(d.input?.maxLength??500)||M&&!He(d.kind,M))&&n.push(E("option_limit",`Enter a valid response for ${d.name}`,N,d.id));let V=b.reduce((P,f)=>P+(f.quantity??1),0);b.length&&(T||V<d.selection.min||V>(d.selection.max??1/0)||new Set(b.map(P=>P.choiceId)).size!==b.length)&&n.push(E("option_limit",`Check the number of options for ${d.name}`,N,d.id));for(let P of b){let f=d.choices.find(S=>S.id===P.choiceId);f?.isAvailable||n.push(E("option_unavailable",`An option in ${d.name} is unavailable`,[{action:"choose_other_option",groupId:d.id},{action:"remove_line"}],d.id));let w=P.quantity??1;(!Number.isInteger(w)||w<(f?.quantity?.min??1)||w>(f?.quantity?.max??99))&&n.push(E("option_limit",`Check the option quantity for ${d.name}`,N,d.id));}}if(!X(t,i)?.isAvailable){let d=t.optionGroups.find(b=>b.isAttribute)?.id;n.push(E("option_limit","Choose an available variant",d?[{action:"choose_option",groupId:d}]:[{action:"remove_line"}],d));}return n}function D(t){return {itemId:t.itemId,...t.menuId!==void 0?{menuId:t.menuId}:{},quantity:t.quantity,choices:t.choices.map(e=>({...e})),inputs:{...t.inputs},...t.variantId?{variantId:t.variantId}:{},...t.note!==void 0?{note:t.note}:{}}}function Ce(t){return Object.freeze({...t,choices:Object.freeze(t.choices.map(e=>Object.freeze({...e}))),inputs:Object.freeze({...t.inputs})})}function _e(t,e$1,o,n,i=[]){let p=[],r=true,a=t.map(s=>{let c=Object.hasOwn(e$1.items,s.itemId)?e$1.items[s.itemId]:void 0,g={...s,choices:s.choices?.map(M=>({...M})),inputs:{...s.inputs}},I=c?Y(c,g):g;p.push(...ee(c,I,{menu:e$1}).map(M=>({...M,lineId:s.id})));let d=I.variantId??(c?X(c,I)?.id:void 0),b=e(0,n),q=true;try{if(!c)throw new Error("Missing item");b=J(c,{choices:(I.choices??[]).map(M=>({...M,quantity:M.quantity??1})),...d?{variantId:d}:{}});}catch{r=false,q=false;}return s.unitPrice&&s.unitPrice.amountMinor!==b.amountMinor&&c&&q&&p.push({...E("price_changed",`${c.name} is now ${f.money(b)}`,[{action:"accept_price"},{action:"remove_line"}]),severity:"warning",lineId:s.id,data:{previous:s.unitPrice,current:b}}),Ce({id:s.id,itemId:s.itemId,...I.menuId!==void 0?{menuId:I.menuId}:{},name:c?.name??s.name,quantity:I.quantity??1,choices:(I.choices??[]).map(M=>({...M,quantity:M.quantity??1})),inputs:I.inputs??{},...d?{variantId:d}:{},...I.note!==void 0?{note:I.note}:{},unitPrice:b,lineSubtotal:h(b,I.quantity??1),addedAt:s.addedAt})});for(let s of i)s.code==="price_changed"&&a.some(c=>c.id===s.lineId)&&!p.some(c=>c.code===s.code&&c.lineId===s.lineId)&&p.push(s);return {lines:Object.freeze(a),issues:p,priced:r}}function Je(t){try{return t&&e(t.amountMinor,t.currency)}catch{return}}function de(t){let e=JSON.parse(t);if(!e||e.v!==1||!Array.isArray(e.lines)||e.lines.length>100||!Number.isFinite(e.updatedAt)||typeof e.menuVersion!="string"||e.promoCode!==null&&typeof e.promoCode!="string")throw new Error("Invalid saved cart");let o=new Set;for(let n of e.lines){if(!n||typeof n.id!="string"||o.has(n.id)||typeof n.itemId!="string"||typeof n.name!="string"||!Number.isFinite(Date.parse(n.addedAt))||!Number.isInteger(n.quantity)||n.quantity<1||n.quantity>99||n.note!==void 0&&(typeof n.note!="string"||n.note.length>280)||n.menuId!==void 0&&(typeof n.menuId!="string"||!n.menuId.trim()||n.menuId.length>256)||n.variantId!==void 0&&typeof n.variantId!="string")throw new Error("Invalid saved cart line");if(o.add(n.id),!Array.isArray(n.choices)||n.choices.length>1e3||n.choices.some(i=>!i||typeof i.groupId!="string"||typeof i.choiceId!="string"||!Number.isInteger(i.quantity)||i.quantity<1||i.quantity>99))throw new Error("Invalid saved choices");if(!n.inputs||typeof n.inputs!="object"||Array.isArray(n.inputs)||Object.values(n.inputs).some(i=>typeof i!="string"))throw new Error("Invalid saved inputs")}if(e.fulfilment!==null&&!Se(e.fulfilment))throw new Error("Invalid saved fulfilment");return {v:1,lines:e.lines.map(n=>{let i=Je(n.unitPrice);return {id:n.id,itemId:n.itemId,...n.menuId!==void 0?{menuId:n.menuId}:{},name:n.name,addedAt:n.addedAt,...i?{unitPrice:i}:{},quantity:n.quantity,choices:n.choices,inputs:n.inputs,...n.variantId?{variantId:n.variantId}:{},...n.note!==void 0?{note:n.note}:{}}}),fulfilment:e.fulfilment,promoCode:e.promoCode,menuVersion:e.menuVersion,updatedAt:e.updatedAt}}function Se(t){if(!t||!["pickup","delivery","dine_in"].includes(t.type)||typeof t.branchId!="string"||"scheduledFor"in t&&t.scheduledFor!==void 0&&!Number.isFinite(Date.parse(t.scheduledFor)))return false;if(t.type==="delivery"){let e=t.address;if(!e||typeof e.line1!="string"||!e.line1.trim()||[e.line2,e.city,e.placeId,e.instructions].some(o=>o!==void 0&&typeof o!="string")||e.coordinates&&(!Number.isFinite(e.coordinates.lat)||!Number.isFinite(e.coordinates.lng)||Math.abs(e.coordinates.lat)>90||Math.abs(e.coordinates.lng)>180))return false}return t.type!=="dine_in"||t.table===void 0||typeof t.table=="string"}function je(t,e){let o,n=false,i=false,p=false,r=new Map,a=c=>{p||(p=true,e(c));},s=(c,g)=>{try{if(n||(n=!0,o=t??(typeof window>"u"?void 0:window.localStorage)),o&&!i)return c(o)}catch{i=true,a("Cart storage unavailable; using memory for this client");}return g()};return {warning:a,read(c){return s(g=>{let I=g.getItem(c);return I!==null?r.set(c,I):r.delete(c),I},()=>r.get(c)??null)},write(c,g){r.set(c,g),s(I=>I.setItem(c,g),()=>{});},remove(c){r.delete(c),s(g=>g.removeItem(c),()=>{});},accepts(c){return !i&&(!c||c===o)},sweep(c,g,I){s(d=>{let b=Array.from({length:d.length??0},(q,M)=>d.key?.(M));for(let q of b)if(!(!q||q===g||!q.startsWith(c)))try{let M=d.getItem(q);M&&Date.now()-de(M).updatedAt>I&&d.removeItem(q);}catch{d.removeItem(q);}},()=>{});}}}function qe(t){return {line_id:t.id,item_id:t.itemId,...t.menuId!==void 0?{menu_id:t.menuId}:{},quantity:t.quantity,...t.variantId?{variant_id:t.variantId}:{},options:t.choices.map(e=>({group_id:e.groupId,choice_id:e.choiceId,quantity:e.quantity})),inputs:{...t.inputs},...t.note!==void 0?{note:t.note}:{}}}function ze(t){return {type:t.type,..."scheduledFor"in t&&t.scheduledFor?{scheduled_for:t.scheduledFor}:{},...t.type==="dine_in"&&t.table?{table:t.table}:{},...t.type==="delivery"?{address:{line1:t.address.line1,...t.address.line2?{line2:t.address.line2}:{},...t.address.city?{city:t.address.city}:{},...t.address.placeId?{place_id:t.address.placeId}:{},...t.address.instructions?{instructions:t.address.instructions}:{},...t.address.coordinates?{lat:t.address.coordinates.lat,lng:t.address.coordinates.lng}:{}}}:{}}}var Ye=Object.freeze({lines:Object.freeze([]),count:0,subtotal:null,fulfilment:null,promoCode:null,state:{status:"ready"},issues:Object.freeze([]),isHydrated:false});function Ve(t){if(t&&typeof t=="object"&&!Object.isFrozen(t)){Object.freeze(t);for(let e of Object.values(t))Ve(e);}return t}function ye(t){return JSON.stringify([t.itemId,t.menuId,t.variantId,t.note??"",[...t.choices??[]].sort((e,o)=>e.groupId.localeCompare(o.groupId)||e.choiceId.localeCompare(o.choiceId)),Object.entries(t.inputs??{}).sort(([e],[o])=>e.localeCompare(o))])}function De(t){let{config:e$1,menu:o,store:n,events:i}=t,p=e$1.cart?.ttl??10080*60*1e3;if(!Number.isFinite(p)||p<=0)throw new a("invalid_request","Cart ttl must be positive milliseconds");let r=Ye,a$1,s,c$1=e$1.branch,g$1=0,I,d=0,b=0,q=0,M=0,F=0,T,N,V=0,P,f=new Set,w=`zappie:cart:v1:${e$1.store}:`,S=e$1.cart?.storageScope,x=u=>`${w}${u}${S?`:${S}`:""}`,k=()=>x(s.id),A=je(e$1.storage,u=>i.emit("warning",{message:u})),Z=u=>{let l=A.read(x(u));return l===null&&S&&(l=A.read(w+u),l!==null&&(A.remove(w+u),A.write(x(u),l))),l},ie,Ee,Ke=()=>Ee??(Ee=import('./cart/quote.js').then(({createQuoteClient:u})=>ie=u(t.transport,t.context),u=>{throw Ee=void 0,u})),G=u=>{r=Ve({...r,...u,branchId:s?.id});for(let l of [...f])try{l();}catch(m){i.emit("error",{error:c(m)});}},ne=(u,l)=>u.some(m=>m.severity!=="info")?{status:"needs_attention",issues:u,...l?{quote:l}:{}}:l?{status:"quoted",quote:l}:{status:"ready"},K=()=>{d++,b++,clearTimeout(I),ie?.cancel();},oe=()=>{let u={v:1,lines:r.lines.map(l=>({...D(l),id:l.id,name:l.name,addedAt:l.addedAt,unitPrice:l.unitPrice})),fulfilment:r.fulfilment,promoCode:r.promoCode,menuVersion:a$1.version,updatedAt:g$1};A.write(k(),JSON.stringify(u));},se=()=>{clearTimeout(I),r.lines.length&&(I=setTimeout(()=>{H.quote().catch(()=>{});},300));},me=(u,l=r.issues.filter(v=>v.source==="local"),m={})=>{let v=n.peek().currency,h=_e(u,a$1,s,v,l),y=h.priced?g(h.lines.map(_=>_.lineSubtotal),e(0,v)):null;G({...m,lines:h.lines,count:h.lines.reduce((_,L)=>_+L.quantity,0),subtotal:y,issues:h.issues,state:ne(h.issues)});for(let _ of h.issues)i.emit("cart_issue",{code:_.code});},Q=(u=r.lines,l={})=>{K(),me(u,void 0,l),g$1=Date.now(),oe(),se();},Ie=u=>{let l=n.peek()?.branches.find(m=>m.id===u.branchId);if(!l)throw new a("branch_not_found","Load the storefront branch before using its cart");a$1=u,s=l,c$1=l.id;},pe=(u,l=true)=>{K();let m;try{m=u?de(u):void 0,m&&Date.now()-m.updatedAt>p&&(m=void 0,A.remove(k()),i.emit("cart_expired",{branchId:s.id}));}catch{A.remove(k()),A.warning("Invalid saved cart discarded");}g$1=m?.updatedAt??Date.now();let v=Date.now()-g$1,h=m?.fulfilment??null;(h?.branchId!==s.id||v>864e5&&h.type==="delivery")&&(h=null),h&&"scheduledFor"in h&&h.scheduledFor&&Date.parse(h.scheduledFor)<Date.now()&&(h={...h},delete h.scheduledFor);let y=(m?.lines??[]).map(L=>({...L,...v>864e5?{inputs:{}}:{}})),_=_e(y,a$1,s,n.peek().currency);me(_.lines,_.issues,{fulfilment:h,promoCode:m?.promoCode??null,isHydrated:true}),u!==null&&l&&oe(),A.sweep(w,k(),p);},j=()=>{let u=s?.id??n.peek()?.branches.find(h=>h.id===c$1||h.slug===c$1||!c$1&&h.isDefault)?.id??(c$1?void 0:n.peek()?.branches[0]?.id);if(r.isHydrated)return;let l=a$1??(u?t.workingSet.peek(u,[]):void 0);if(!l||!n.peek())throw new a("invalid_request","Load the store and menu before editing the cart");Ie(l);let m=Z(s.id),v=[];if(m===null&&P)try{v=P(A.read);}catch{A.warning("Invalid legacy cart discarded");}pe(m),m===null&&P&&oe();for(let h of v.slice(0,100))try{H.add(h.itemId,h);}catch{i.emit("cart_issue",{code:"item_unavailable"});}},fe=async()=>{let u=s?.id,l=q,m=++M,v=d;if(!u||!a$1||!r.lines.length||a$1.completeness!=="partial"&&(await t.transport.getMenuVersion({...t.context(),branchId:u})).version===a$1.version)return;let h=await t.workingSet.read(u,r.lines.map(y=>y.itemId),true);l!==q||m!==M||v!==d||JSON.stringify(h)===JSON.stringify(a$1)||(a$1=h,K(),me(r.lines),oe(),se());},xe=u=>{let l=t.workingSet.peek(s.id,[...r.lines.map(m=>m.itemId),u]);l.items[u]&&(a$1=l);},he=(u,l,m)=>{xe(u.itemId);let v=Object.hasOwn(a$1.items,u.itemId)?a$1.items[u.itemId]:void 0,h$1=ee(v,u,{menu:a$1});if(h$1.length||!v)throw new B(h$1);let y=Y(v,u),_=X(v,y),L=(y.choices??[]).map(R=>({...R,quantity:R.quantity??1})),O=J(v,{choices:L,variantId:_.id});return Ce({id:l??`line_${Array.from(globalThis.crypto.getRandomValues(new Uint8Array(10)),R=>"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-"[R&63]).join("")}`,itemId:v.id,...y.menuId!==void 0?{menuId:y.menuId}:{},name:v.name,quantity:y.quantity,choices:L,inputs:y.inputs??{},variantId:_.id,...y.note!==void 0?{note:y.note}:{},unitPrice:O,lineSubtotal:h(O,y.quantity),addedAt:m??new Date().toISOString()})},Qe=i.on("menu:updated",({branchId:u,version:l})=>{if(s?.id===u){let m=o.peek(u);m&&m.completeness!=="partial"&&m.version===l&&m.version!==a$1?.version&&(M++,a$1=m,K(),me(r.lines),oe(),se());}}),H={get branchId(){return r.branchId},get lines(){return r.lines},get count(){return r.count},get subtotal(){return r.subtotal},get fulfilment(){return r.fulfilment},get promoCode(){return r.promoCode},get state(){return r.state},get issues(){return r.issues},get isHydrated(){return r.isHydrated},getSnapshot:()=>r,subscribe(u){return f.add(u),()=>{f.delete(u);}},async hydrate(){if(r.isHydrated)return fe();let u=V;return N??(N=(async()=>{let l=await n.get(),m=l.branches.find(y=>y.id===c$1||y.slug===c$1)??(c$1?void 0:l.branches.find(y=>y.isDefault)??l.branches[0]);if(!m)throw new a("branch_not_found","Cart branch is unavailable");let v=r.lines;if(!r.isHydrated){let y=Z(m.id);try{let _=y?de(y):void 0;v=_&&Date.now()-_.updatedAt<=p?_.lines:y===null?P?.(A.read)??[]:[];}catch{v=[];}}let h=await t.workingSet.read(m.id,v.map(y=>y.itemId));u===V&&(Ie(h),j(),h.completeness!=="partial"&&await fe());})().finally(()=>{N=void 0;})),N},mount(){if(F++,!T&&typeof window<"u"){let l=v=>{if(s&&v.key===k()&&A.accepts(v.storageArea)){let h=v.newValue,y;try{y=h?de(h).lines.map(z=>z.itemId):[];}catch{pe(h,false);return}let _=t.workingSet.peek(s.id,y);if(_.completeness!=="partial"||y.every(z=>_.items[z])){a$1=_,pe(h,false),se();return}let L=d,O=++M,R=V;t.workingSet.read(s.id,y).then(z=>{R===V&&L===d&&O===M&&(a$1=z,pe(h,false),se());}).catch(z=>i.emit("error",{error:c(z)}));}},m=()=>{fe().catch(v=>i.emit("error",{error:c(v)}));};window.addEventListener("storage",l),window.addEventListener("focus",m),T=()=>{window.removeEventListener("storage",l),window.removeEventListener("focus",m);};}F===1&&H.hydrate().catch(l=>i.emit("error",{error:c(l)}));let u=true;return ()=>{u&&(u=false,F--,F===0&&(T?.(),T=void 0,K()));}},dispose(){V++,q++,Qe(),K(),T?.(),T=void 0,f.clear();},validate(u){j(),xe(u.itemId);let l=ee(Object.hasOwn(a$1.items,u.itemId)?a$1.items[u.itemId]:void 0,u,{menu:a$1});return l.length?{ok:false,issues:l}:{ok:true}},add(u,l={}){j();let m=he({...l,itemId:typeof u=="string"?u:u.id}),v=r.lines.find(h=>ye(D(h))===ye(D(m)));if(v)m=he({...D(m),quantity:v.quantity+m.quantity},v.id,v.addedAt);else if(r.lines.length>=100)throw new B([E("quantity_limit","A cart can contain at most 100 lines")]);return Q(v?r.lines.map(h=>h.id===v.id?m:h):[...r.lines,m]),i.emit("add_to_cart",{itemId:m.itemId,quantity:m.quantity-(v?.quantity??0),value:h(m.unitPrice,m.quantity-(v?.quantity??0))}),m},update(u,l){j();let m=r.lines.find(_=>_.id===u);if(!m)throw new B([E("item_not_found","Cart line not found")]);let v={...D(m),...l};l.choices&&!l.variantId&&delete v.variantId;let h=he(v,m.id,m.addedAt),y=r.lines.find(_=>_.id!==u&&ye(D(_))===ye(D(h)));return y&&(h=he({...D(h),quantity:h.quantity+y.quantity},m.id,m.addedAt)),Q(r.lines.filter(_=>_.id!==y?.id).map(_=>_.id===u?h:_)),i.emit("update_cart_line",{lineId:u,itemId:h.itemId,quantity:h.quantity}),h},remove(u){j();let l=r.lines.find(m=>m.id===u);Q(r.lines.filter(m=>m.id!==u)),l&&i.emit("remove_from_cart",{itemId:l.itemId,quantity:l.quantity,value:l.lineSubtotal});},clear(){j(),Q([],{fulfilment:null,promoCode:null});},setFulfilment(u){if(j(),!Se(u)||u.branchId!==s.id)throw new B([E("fulfilment_unavailable","Select fulfilment for the cart's branch")]);Q(r.lines,{fulfilment:structuredClone(u)});},setPromoCode(u){j(),Q(r.lines,{promoCode:u?.trim()||null});},async moveTo(u){j();let l=++q;K(),G({state:ne(r.issues)});let m=await t.workingSet.read(u,r.lines.map(O=>O.itemId));if(l!==q)return;let v=k(),h=r.lines;Ie(m);let y=h.filter(O=>!ee(m.items[O.itemId],D(O),{menu:m}).length);Q(y,{fulfilment:null,promoCode:null});let _=h.filter(O=>!y.includes(O)).map(O=>({...E("item_unavailable",`${O.name} is unavailable at this branch`),severity:"warning",lineId:O.id})),L=[...r.issues,..._];G({issues:L,state:ne(L)}),v!==k()&&A.remove(v);},async quote(u={}){j(),clearTimeout(I),u.force&&i.emit("view_cart",{});let l={branch_id:s.id,lines:r.lines.map(qe),...r.fulfilment?{fulfilment:ze(r.fulfilment)}:{},...r.promoCode?{promo_code:r.promoCode}:{},menu_version:a$1.version},m=d,v=++b;G({state:{status:"quoting"}});try{return await(await Ke()).get(l,u.force??!1,y=>{if(m!==d)return;let _=r.issues.filter(R=>R.source==="local"),L=y.issues.filter(R=>!_.some(z=>z.code===R.code&&z.lineId===R.lineId&&z.groupId===R.groupId)),O=[..._,...L];G({issues:O,state:ne(O,y)});for(let R of L)i.emit("cart_issue",{code:R.code});(y.menuVersion!==a$1.version||y.issues.some(R=>R.code==="menu_version_stale"))&&fe().catch(R=>i.emit("error",{error:c(R)}));})}catch(h){if(m===d&&v===b){let y=r.issues.filter(_=>_.source==="local");G({issues:y,state:ne(y)}),i.emit("quote_failed",{code:c(h).code});}throw h instanceof Error&&h.name==="AbortError"?h:c(h)}},applyFix(u,l=u.fixes[0]){if(!r.issues.includes(u)||!l||!u.fixes.some(m=>JSON.stringify(m)===JSON.stringify(l)))throw new a("invalid_request","Choose a current cart issue and one of its fixes");switch(l.action){case "remove_line":{u.lineId&&H.remove(u.lineId);return}case "remove_promo":H.setPromoCode(null);return;case "set_quantity":{u.lineId&&H.update(u.lineId,{quantity:l.quantity});return}case "accept_price":{ie?.accept(u);let m=r.issues.filter(y=>y!==u),v="quote"in r.state?r.state.quote:void 0,h=v?{...v,issues:v.issues.filter(y=>!(y.code===u.code&&y.lineId===u.lineId))}:void 0;G({issues:m,state:ne(m,h)});return}default:throw new a("invalid_request",`${l.action} needs the shopper's selection; use update or setFulfilment`)}},toCheckoutPayload(){j();let u=r.issues.filter(l=>l.severity==="blocking");if((!r.lines.length||!r.fulfilment)&&u.push(E("fulfilment_unavailable","Add items and select fulfilment before checkout")),u.length)throw new B(u);return {lines:r.lines.map(qe),fulfilment:structuredClone(r.fulfilment),...r.promoCode?{promo_code:r.promoCode}:{},menu_version:a$1.version}}};return {cart:Object.freeze(H),cartCatalogue:()=>a$1,setLegacyReader(u){P=u;}}}function Ue(t){let e=(n,i,p)=>{let r=p??t.menu.peek(n),a=Object.fromEntries([...new Set(i)].flatMap(s=>{let c=t.peekItem(s,n)??r?.items[s];return c?[[s,c]]:[]}));return {id:`branch:${n}`,name:"Cart working set",branchId:n,version:"",...r,kind:"working-set",completeness:"partial",items:a,categories:(r?.categories??[]).map(s=>({...s,itemIds:Object.values(a).filter(c=>c.placements?c.placements.some(g=>g.categoryId===s.id&&(r?.menus??[{id:r?.id,categoryIds:[s.id]}]).some(I=>I.id===g.menuId&&I.categoryIds.includes(s.id))):s.itemIds.includes(c.id)).map(c=>c.id)}))}},o=(n,i)=>{let p=t.menu.peek(n);return p&&p.completeness!=="partial"?p:e(n,i)};return {peek:o,async read(n,i,p=false){let r=o(n,i);if(!i.length||!p&&r.completeness!=="partial")return r;let a$1=p?await t.transport.getMenu({...t.context(),branchId:n,metadataOnly:true}):void 0,s=a$1&&"menu"in a$1?a$1.menu:await t.menu.get({branch:n,metadataOnly:true});if(s.branchId!==n)throw new a("server_error","Cart metadata belongs to another branch");return t.menu.hydrate(s),s.completeness!=="partial"?s:(await Promise.all([...new Set(i)].map(async c=>{try{await t.readItem(c,n,p);}catch(g){if(g.code!=="item_not_found")throw g}})),e(n,i,s))}}}var Me="2026-10-01";function We(t,e=false){if(!t.store?.trim())throw new a("invalid_request","A storefront slug is required");if(e&&typeof window<"u")throw new a("invalid_request","Secret keys cannot be used in a browser");let n=(e?/^sk_(test|live)_.+$/:/^pk_(test|live)_.+$/).exec(t.key);if(!n)throw new a("invalid_request",e?"A secret sk_test_ or sk_live_ key is required":"Use a publishable pk_test_ or pk_live_ key; secret keys are never accepted by createZappie");return n[1]}function re(t){return t?.reason??new DOMException("The request was aborted","AbortError")}function te(t){if(t?.aborted)throw re(t)}function ke(t,e){return new Promise((o,n)=>{te(e);let i=()=>{clearTimeout(p),n(re(e));},p=setTimeout(()=>{e?.removeEventListener("abort",i),o();},t);e?.addEventListener("abort",i,{once:true});})}var Xe=1e4,et=Object.freeze({fetchedAt:0,loading:false}),ve=class{constructor(e){this.onError=e;this.entries=new Map;this.listeners=new Set;this.flights=new Map;this.subscribe=e=>(this.listeners.add(e),()=>{this.listeners.delete(e);});}snapshot(e){return this.entries.get(e)??et}notify(){for(let e of [...this.listeners])e();}set(e,o,n){this.entries.set(e,{value:o,fetchedAt:Date.now(),loading:false,...n!==void 0?{etag:n}:{}}),this.notify();}stale(e){let o=this.snapshot(e);return o.value!==void 0&&Date.now()-o.fetchedAt>=6e4}async get(e,o,n){te(n);let i=this.snapshot(e),p=Date.now()-i.fetchedAt;if(i.value!==void 0&&p<6e4)return i.value;let r=i.value!==void 0&&p<6e5;if(r&&i.failedAt!==void 0&&Date.now()-i.failedAt<Xe)return i.value;let a=this.flights.get(e);if(!a){let c$1=new AbortController;a={controller:c$1,users:0,keepAlive:r,promise:Promise.resolve()};let g=a;this.flights.set(e,g);let I={...i,loading:true};this.entries.set(e,I),this.notify(),g.promise=Promise.resolve().then(()=>(te(c$1.signal),o(c$1.signal,i))).then(d=>(this.entries.get(e)===I&&!c$1.signal.aborted&&(this.set(e,d.value,d.etag),d.onStored?.()),d.value)).catch(d=>{throw this.entries.get(e)===I&&(this.entries.set(e,c$1.signal.aborted?{...i,loading:false}:{...i,loading:false,error:c(d),failedAt:Date.now()}),this.notify(),c$1.signal.aborted||this.onError(c(d))),c$1.signal.aborted?re(c$1.signal):c(d)}).finally(()=>{this.flights.get(e)===g&&this.flights.delete(e);});}if(r)return a.keepAlive=true,a.promise.catch(()=>{}),i.value;let s=a;return s.users++,new Promise((c,g)=>{let I=false,d=q=>{I||(I=true,n?.removeEventListener("abort",b),s.users--,q());},b=()=>{d(()=>g(re(n))),s.users===0&&!s.keepAlive&&(s.controller.abort(re(n)),this.flights.get(e)===s&&this.flights.delete(e));};n?.addEventListener("abort",b,{once:true}),s.promise.then(q=>d(()=>c(q)),q=>d(()=>g(q))),n?.aborted&&b();})}};function Be(){let t=new Map;return {on(e,o){let n=t.get(e)??new Set;return t.set(e,n),n.add(o),()=>{n.delete(o);}},emit(e,o){for(let n of [...t.get(e)??[]])try{n(o);}catch(i){console.error("Zappie event listener failed",i);}}}}var tt=5e3,le=class{constructor(e={}){let o=new URL(e.baseUrl??"https://api.zappie.ai/storefront/v1");if(o.username||o.password||o.search||o.hash||o.protocol!=="https:"&&!(o.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(o.hostname)))throw new a("invalid_request","REST baseUrl requires HTTPS, or HTTP on localhost");this.baseUrl=o.href.replace(/\/$/,""),this.fetcher=e.fetch??((n,i)=>globalThis.fetch(n,i));}async request(e,o,n,i){if(typeof window<"u"&&o.key.startsWith("sk_"))throw new a("invalid_request","Secret keys cannot be sent from a browser");for(let p=0;;p++){te(o.signal);let r;try{r=await this.fetcher(this.baseUrl+e,{method:i?"POST":"GET",...i?{body:JSON.stringify(i)}:{},redirect:"manual",headers:{...i?{"Content-Type":"application/json"}:{},...o.idempotencyKey?{"Idempotency-Key":o.idempotencyKey}:{},Authorization:`Bearer ${o.key}`,"Zappie-Version":o.version,...n?{"If-None-Match":n}:{}},...o.signal?{signal:o.signal}:{}});}catch(d){if(te(o.signal),p>=2)throw new a("network_error","Could not reach the storefront service",{cause:d});await ke(100*2**p+Math.random()*100,o.signal);continue}if(r.ok||r.status===304)return r;let a$1=r.headers.get("Retry-After"),s=a$1!==null?Number(a$1):NaN,c=a$1!==null?Number.isFinite(s)?Math.max(0,s*1e3):Math.max(0,Date.parse(a$1)-Date.now()):NaN,g=Number.isFinite(c)&&c>tt;if(p<2&&!g&&(r.status===429||r.status>=500)){await r.body?.cancel(),await ke(Number.isFinite(c)?c:100*2**p+Math.random()*100,o.signal);continue}let I=await r.json().catch(()=>({}));throw d(I,r.status,r.headers.get("Zappie-Request-Id")??void 0)}}async json(e){try{return await e.json()}catch(o){throw o instanceof Error&&o.name==="AbortError"?o:new a("server_error","The storefront service returned invalid JSON",{status:e.status,cause:o})}}async quote(e,o){let n={...e,idempotencyKey:e.idempotencyKey??await i(JSON.stringify(o))};return await this.json(await this.request("/quote",n,void 0,o))}async getStore(e$1){let o$1=import('./transport/store.js').catch(r=>{throw new a("server_error","Could not load the storefront store mapper",{cause:r})}),n$1=this.request("/store",e$1).then(r=>this.json(r)),[{mapStore:i},p$1]=await Promise.all([o$1,n$1]);return i(p$1,j,k,l,q,p,e,o,m,n)}async getMenu(e){let o,n=new Set,i,p;do{let r=new URLSearchParams({branch:e.branchId,...e.metadataOnly?{metadata_only:"true"}:{},...o?{cursor:o}:{}}),a$1=await this.request(`/menus?${r}`,e,o?void 0:e.ifNoneMatch);if(a$1.status===304){if(o)throw new a("server_error","Unexpected 304 on a continuation page");return {notModified:true,...a$1.headers.get("ETag")?{etag:a$1.headers.get("ETag")}:{}}}let s$1=j(await this.json(a$1)),c=s(s$1,e.branchId);if(e.metadataOnly){if(Object.keys(c.items).length||s$1.next_cursor)throw new a("server_error","Invalid metadata response");return {menu:{...c,completeness:"partial"}}}if(!i)i=c,p=a$1.headers.get("ETag")??void 0;else {if(c.version!==i.version)throw new a("server_error","Menu changed while fetching pages; retry the read");Object.assign(i.items,c.items);for(let g of c.categories){let I=i.categories.find(d=>d.id===g.id);I?I.itemIds=[...new Set([...I.itemIds,...g.itemIds])]:i.categories.push(g);}for(let g of c.menus??[]){let I=i.menus?.find(d=>d.id===g.id);I?I.categoryIds=[...new Set([...I.categoryIds,...g.categoryIds])]:i.menus?.push(g);}}if(o=typeof s$1.next_cursor=="string"&&s$1.next_cursor?s$1.next_cursor:void 0,o){if(n.has(o))throw new a("server_error","Repeated menu pagination cursor");n.add(o);}}while(o);i.menus&&i.menus.length>1&&(i.id=`branch:${e.branchId}`,i.name="Catalogue",delete i.schedule);for(let r of i.categories)for(let a$1 of r.itemIds)if(!Object.prototype.hasOwnProperty.call(i.items,a$1))throw new a("server_error","Category refers to a missing item");return Object.keys(i.items).length>2e3&&console.warn("Zappie menu contains more than 2,000 items"),{menu:i,...p?{etag:p}:{}}}async getSelectedMenu(e){return (await import('./transport/selected-menu.js')).selectedMenu(e,o=>this.request(o,e,e.ifNoneMatch),o=>this.json(o),s,()=>new a("server_error","Invalid selected menu"))}async search(e){return (await import('./transport/search.js')).searchPage(e,async o=>this.json(await this.request(o,e)),s,()=>new a("server_error","Invalid search page"))}async getCategoryPage(e){let o=new URLSearchParams({branch:e.branchId,...e.cursor?{cursor:e.cursor}:{}}),n=await this.json(await this.request(`/menus/${encodeURIComponent(e.menuId)}/categories/${encodeURIComponent(e.categoryId)}/items?${o}`,e));return (await import('./transport/category-page.js')).categoryPage(j(n),e,s,()=>new a("server_error","Invalid category page"))}async getMenuVersion(e){let o=j(await this.json(await this.request(`/menus/version?${new URLSearchParams({branch:e.branchId})}`,e)));if(typeof o.menu_version!="string"||typeof o.availability_hash!="string")throw new a("server_error","Invalid menu version response");return {version:o.menu_version,availabilityHash:o.availability_hash}}async getItem(e){let o=r(await this.json(await this.request(`/items/${encodeURIComponent(e.itemId)}?${new URLSearchParams({branch:e.branchId})}`,e)));if(o.id!==e.itemId)throw new a("server_error","Item response identity does not match the request");return o}};function nt(t,e=false){let o=We(t,e),n=Object.freeze({...t,version:t.version??Me,telemetry:t.telemetry??true}),i=t.transport??new le({...t.baseUrl?{baseUrl:t.baseUrl}:{},...t.fetch?{fetch:t.fetch}:{}}),p=Be(),r=new ve(f=>p.emit("error",{error:f})),a$1=f=>({key:n.key,version:n.version,...f?{signal:f}:{}}),s={get:(f={})=>r.get("store",async w=>({value:await i.getStore(a$1(w))}),f.signal),peek:()=>r.snapshot("store").value},c=(f,w)=>{let S=w??n.branch,x=S?f.branches.find(k=>k.id===S||k.slug===S):f.branches.find(k=>k.isDefault)??f.branches[0];if(!x)throw new a("branch_not_found","No matching storefront branch",{param:"branch"});return x},g,I=f=>{let w=s.peek();if(w)return c(w,f).id;let S=f??n.branch;return g&&(S===void 0||S===n.branch||S===g)?g:S??"default"},d=f=>`menu:${I(f)}`,b={async search(f,w={}){if(!i.search)throw new a("invalid_request","Transport does not support search");let S=c(await s.get(w),w.branch);return i.search({...a$1(w.signal),branchId:S.id,query:f,...w.cursor?{cursor:w.cursor}:{}})},async get(f={}){let w=c(await s.get(f),f.branch);return r.get(`menu:${w.id}${f.metadataOnly?":partial":""}`,async(S,x)=>{let k=await i.getMenu({...a$1(S),branchId:w.id,...f.metadataOnly?{metadataOnly:true}:{},...x.etag?{ifNoneMatch:x.etag}:{}});if("notModified"in k){if(!x.value)throw new a("server_error","Received 304 without a cached menu");return {value:x.value,...k.etag??x.etag?{etag:k.etag??x.etag}:{}}}if(!f.metadataOnly&&k.menu.completeness==="partial")throw new a("server_error","Partial catalogue cannot satisfy a complete read");if(k.menu.branchId!==w.id)throw new a("server_error","Menu belongs to a different branch");return {value:k.menu,etag:k.etag??k.menu.version,onStored:()=>{x.value&&x.value.version!==k.menu.version&&p.emit("menu:updated",{branchId:w.id,version:k.menu.version});}}},f.signal)},peek:f=>r.snapshot(d(f)).value??r.snapshot(`${d(f)}:partial`).value,async page(f,w,S={}){if(!i.getCategoryPage)throw new a("invalid_request","Transport does not support category pages");let x=c(await s.get(S),S.branch);return i.getCategoryPage({...a$1(S.signal),branchId:x.id,menuId:f,categoryId:w,...S.cursor?{cursor:S.cursor}:{}})},async version(f={}){let w=c(await s.get(f),f.branch);return i.getMenuVersion({...a$1(f.signal),branchId:w.id})},async item(f,w={}){let S=c(await s.get(w),w.branch);return (!b.peek(S.id)||b.peek(S.id)?.completeness==="partial")&&await b.get({...w,metadataOnly:true}),F(f,S.id,false,w.signal)},hydrate(f){g??(g=f.branchId);let w=r.snapshot(`menu:${f.branchId}`).value;r.set(`menu:${f.branchId}${f.completeness==="partial"?":partial":""}`,f,f.version),w&&w.version!==f.version&&p.emit("menu:updated",{branchId:f.branchId,version:f.version});}},q=new Map,M=(f,w)=>q.get(`${w}:${f}`),F=async(f,w,S=false,x)=>{let k=r.snapshot(`menu:${w}`).value;if(!S&&k&&Object.hasOwn(k.items,f))return k.items[f];let A=(Z=x)=>i.getItem({...a$1(Z),branchId:w,itemId:f});try{let Z=S?await A():await r.get(`item:${w}:${f}`,async ie=>({value:await A(ie)}),x);return S&&r.set(`item:${w}:${f}`,Z),q.set(`${w}:${f}`,Z),Z}catch(Z){throw Z.code==="item_not_found"&&(q.delete(`${w}:${f}`),r.set(`item:${w}:${f}`,void 0)),Z}},T=Ue({menu:b,store:s,transport:i,context:a$1,peekItem:M,readItem:F}),{cart:N,setLegacyReader:V,cartCatalogue:P}=De({workingSet:T,config:n,menu:b,store:s,transport:i,context:a$1,events:p});return Object.freeze({cart:N,config:n,mode:o,store:s,menu:b,format:f,on:p.on,_internal:{cartCatalogue:P,setLegacyReader:V,transport:i,cache:r,emit:p.emit,menuKey:d,hydrateStore:f=>r.set("store",f)}})}function rt(t){let e=nt(t);return e.mode==="test"&&typeof window<"u"&&import('./debug.js').then(({installDebug:o})=>o(e)),e}function Ge(t){return Number.isInteger(t)&&t>=0&&t<=99?t:0}function it(t,e){let o=Y(t,e),n=be(t,o).filter(s=>s.isAvailable!==false),i=X(t,o)?.id,p=(o.choices??[]).filter(({groupId:s,choiceId:c})=>n.some(g=>g.id===s&&g.choices.some(I=>I.id===c))).map(s=>({...s,quantity:Ge(s.quantity)})),r=J(t,{choices:p,...i?{variantId:i}:{}}),a=h(r,Ge(o.quantity));return {groups:n,unitPrice:r,total:a}}var an="0.1.1";
2
+ export{B as a,J as b,we as c,ee as d,ze as e,Ye as f,Me as g,We as h,te as i,ke as j,le as k,nt as l,rt as m,it as n,an as o};//# sourceMappingURL=chunk-LFBQICYU.js.map