@zappie/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +337 -2
  3. package/dist/cart/item.d.ts +10 -0
  4. package/dist/cart/item.js +1 -0
  5. package/dist/cart/item.js.map +1 -0
  6. package/dist/cart/quote.d.ts +15 -0
  7. package/dist/cart/quote.js +1 -0
  8. package/dist/cart/quote.js.map +1 -0
  9. package/dist/checkout/index.d.ts +141 -0
  10. package/dist/checkout/index.js +1 -0
  11. package/dist/checkout/index.js.map +1 -0
  12. package/dist/chunk-I55A5QXJ.js +2 -0
  13. package/dist/chunk-I55A5QXJ.js.map +1 -0
  14. package/dist/chunk-VG7NKIKR.js +2 -0
  15. package/dist/chunk-VG7NKIKR.js.map +1 -0
  16. package/dist/client-BctSrEhO.d.ts +725 -0
  17. package/dist/config/index.d.ts +83 -0
  18. package/dist/config/index.js +1 -0
  19. package/dist/customer/index.d.ts +175 -0
  20. package/dist/customer/index.js +3 -0
  21. package/dist/customer/index.js.map +1 -0
  22. package/dist/customer/react.d.ts +31 -0
  23. package/dist/customer/react.js +3 -0
  24. package/dist/customer/react.js.map +1 -0
  25. package/dist/debug.d.ts +45 -0
  26. package/dist/debug.js +1 -0
  27. package/dist/debug.js.map +1 -0
  28. package/dist/index.d.ts +84 -0
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/internal.d.ts +14 -0
  32. package/dist/internal.js +1 -0
  33. package/dist/internal.js.map +1 -0
  34. package/dist/models-Dj-VkdU1.d.ts +230 -0
  35. package/dist/persist/catalogue.d.ts +2 -0
  36. package/dist/persist/catalogue.js +2 -0
  37. package/dist/persist/catalogue.js.map +1 -0
  38. package/dist/places/index.d.ts +121 -0
  39. package/dist/places/index.js +1 -0
  40. package/dist/places/index.js.map +1 -0
  41. package/dist/react/index.d.ts +79 -0
  42. package/dist/react/index.js +2 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/server/index.d.ts +235 -0
  45. package/dist/server/index.js +2 -0
  46. package/dist/server/index.js.map +1 -0
  47. package/dist/template/index.d.ts +16 -0
  48. package/dist/template/index.js +3 -0
  49. package/dist/template/index.js.map +1 -0
  50. package/dist/template/react.d.ts +13 -0
  51. package/dist/template/react.js +3 -0
  52. package/dist/template/react.js.map +1 -0
  53. package/dist/testing/index.d.ts +64 -0
  54. package/dist/testing/index.js +2 -0
  55. package/dist/testing/index.js.map +1 -0
  56. package/dist/transport/category-page.d.ts +17 -0
  57. package/dist/transport/category-page.js +2 -0
  58. package/dist/transport/category-page.js.map +1 -0
  59. package/dist/transport/search.d.ts +7 -0
  60. package/dist/transport/search.js +2 -0
  61. package/dist/transport/search.js.map +1 -0
  62. package/dist/transport/selected-menu.d.ts +2 -0
  63. package/dist/transport/selected-menu.js +2 -0
  64. package/dist/transport/selected-menu.js.map +1 -0
  65. package/dist/transport/store.d.ts +10 -0
  66. package/dist/transport/store.js +2 -0
  67. package/dist/transport/store.js.map +1 -0
  68. package/dist/url/index.d.ts +157 -0
  69. package/dist/url/index.js +2 -0
  70. package/dist/url/index.js.map +1 -0
  71. package/dist/visits/index.d.ts +89 -0
  72. package/dist/visits/index.js +1 -0
  73. package/dist/visits/index.js.map +1 -0
  74. package/package.json +104 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zappie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,338 @@
1
- # Temporary Holding Version
1
+ # @zappie/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
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));
55
+ ```
56
+
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.
58
+
59
+ ## React and SSR
60
+
61
+ ```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>
77
+ );
78
+ }
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
+ ```
229
+
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.
236
+
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
+ });
300
+ ```
301
+
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:
305
+
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.
310
+
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.
312
+
313
+ ## Item configuration previews
314
+
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.
316
+
317
+ The typed `search` event carries `{ query, resultCount }`; foundations emits it after a 600ms debounce. This adds no analytics transport.
318
+
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.
320
+
321
+ ## Template settings and images
322
+
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.
330
+
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.
335
+
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.
@@ -0,0 +1,10 @@
1
+ import { Z as Zappie } from '../client-BctSrEhO.js';
2
+ import { I as Item } from '../models-Dj-VkdU1.js';
3
+
4
+ /** Register an item already read by a partial/selected catalogue, without fetching it again. */
5
+ declare function prepareItem(client: Zappie, item: Item, options: {
6
+ branch: string;
7
+ signal?: AbortSignal;
8
+ }): Promise<Item>;
9
+
10
+ export { prepareItem };
@@ -0,0 +1 @@
1
+ async function a(r,t,e){return e.signal?.throwIfAborted(),await r.menu.get({...e,metadataOnly:true}),e.signal?.throwIfAborted(),r._internal.cache.set(`item:${e.branch}:${t.id}`,t),r.menu.item(t.id,e)}export{a as prepareItem};//# sourceMappingURL=item.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cart/item.ts"],"names":["prepareItem","client","item","options"],"mappings":"AAIA,eAAsBA,CAAAA,CACpBC,EACAC,CAAAA,CACAC,CAAAA,CACe,CACf,OAAAA,CAAAA,CAAQ,QAAQ,cAAA,EAAe,CAC/B,MAAMF,CAAAA,CAAO,IAAA,CAAK,IAAI,CAAE,GAAGE,EAAS,YAAA,CAAc,IAAK,CAAC,CAAA,CACxDA,CAAAA,CAAQ,QAAQ,cAAA,EAAe,CAC/BF,EAAO,SAAA,CAAU,KAAA,CAAM,IAAI,CAAA,KAAA,EAAQE,CAAAA,CAAQ,MAAM,CAAA,CAAA,EAAID,CAAAA,CAAK,EAAE,CAAA,CAAA,CAAIA,CAAI,EAG7DD,CAAAA,CAAO,IAAA,CAAK,KAAKC,CAAAA,CAAK,EAAA,CAAIC,CAAO,CAC1C","file":"item.js","sourcesContent":["import type { Zappie } from \"../client\";\nimport type { Item } from \"../models\";\n\n/** Register an item already read by a partial/selected catalogue, without fetching it again. */\nexport async function prepareItem(\n client: Zappie,\n item: Item,\n options: { branch: string; signal?: AbortSignal },\n): Promise<Item> {\n options.signal?.throwIfAborted();\n await client.menu.get({ ...options, metadataOnly: true });\n options.signal?.throwIfAborted();\n client._internal.cache.set(`item:${options.branch}:${item.id}`, item);\n // menu.item owns the working-set item registry. Its fresh cache entry means\n // this registration does no item read and never promotes a partial menu.\n return client.menu.item(item.id, options);\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { T as Transport, R as RequestCtx, Q as QuoteRequest, a as Quote } from '../client-BctSrEhO.js';
2
+ import '../models-Dj-VkdU1.js';
3
+
4
+ declare function createQuoteClient(transport: Transport, context: () => RequestCtx): {
5
+ cancel: () => void;
6
+ get(body: QuoteRequest, force: boolean, apply: (quote: Quote) => void): Promise<Quote>;
7
+ accept(issue: CartIssueForAcceptance): void;
8
+ };
9
+ type CartIssueForAcceptance = {
10
+ code: string;
11
+ lineId?: string;
12
+ };
13
+ type QuoteClient = ReturnType<typeof createQuoteClient>;
14
+
15
+ export { type QuoteClient, createQuoteClient };
@@ -0,0 +1 @@
1
+ import {i as i$1,a,j,p,g,e}from'../chunk-I55A5QXJ.js';function d(){throw new a("server_error","Invalid quote response")}function i(c){return typeof c!="string"?d():c}function y(c){return Array.isArray(c)?c:d()}function M(c){let r=j(c);switch(r.action){case "remove_line":case "accept_price":case "remove_promo":case "change_address":return {action:r.action};case "choose_option":case "choose_other_option":return {action:r.action,groupId:i(r.group_id)};case "set_quantity":return !Number.isInteger(r.quantity)||Number(r.quantity)<1||Number(r.quantity)>99?d():{action:r.action,quantity:r.quantity};case "schedule":return {action:r.action,...r.next_open_at?{nextOpenAt:i(r.next_open_at)}:{},...r.slots?{slots:y(r.slots).map(i)}:{}};case "switch_fulfilment":{let t=y(r.types).map(i).filter(s=>["pickup","delivery","dine_in"].includes(s));return t.length?{action:r.action,types:t}:void 0}default:return}}var N=["item_unavailable","item_not_found","option_unavailable","option_required","option_limit","quantity_limit","price_changed","branch_closed","fulfilment_unavailable","outside_delivery_zone","delivery_region_required","schedule_invalid","promo_invalid","promo_min_spend","min_order","menu_version_stale"];function A(c,r){let t=j(c);if(t.branch_id!==r||!Number.isFinite(Date.parse(i(t.expires_at))))return d();let s=p(t.subtotal),o=j(t.checkout);if(typeof t.can_checkout!="boolean"||typeof o.ready!="boolean"||o.reason!==void 0&&o.reason!=="issues"&&o.reason!=="customer_required")return d();let l=n=>{let e=p(n);return e.currency!==s.currency?d():e},q=n=>{let e=j(n);return {code:i(e.code),label:i(e.label),amount:l(e.amount)}},a=y(t.charges).map(q),p$1=y(t.discounts).map(q),w=l(t.total);if(g([...a,...p$1].map(n=>n.amount),s).amountMinor!==w.amountMinor)return d();let m=a.filter(n=>n.code==="delivery"),v=y(t.lines).map(n=>{let e=j(n);return (e.unit_price==null||e.line_total==null)&&t.lines_unpriced!==true?d():{lineId:i(e.line_id),unitPrice:e.unit_price==null?null:l(e.unit_price),total:e.line_total==null?null:l(e.line_total)}});return t.lines_unpriced!==true&&g(v.map(n=>n.total),e(0,s.currency)).amountMinor!==s.amountMinor?d():{lines:v,...t.lines_unpriced===true?{linesUnpriced:true}:{},subtotal:s,total:w,fees:a.filter(n=>n.code!=="delivery"),discounts:p$1,...m.length?{delivery:g(m.map(n=>n.amount),e(0,s.currency))}:{},menuVersion:i(t.menu_version),expiresAt:i(t.expires_at),issues:y(t.issues).flatMap(n=>{let e=j(n),h=i(e.code),g=i(e.severity),u=N.includes(h)&&["blocking","warning","info"].includes(g),k=y(e.fixes).map(M).filter(F=>F!==void 0),_=e.line_id?i(e.line_id):void 0;return !u&&(g==="warning"||g==="info")?(console.warn(`Zappie quote issue "${h}" is newer than this SDK and was skipped`),[]):[{code:u?h:"item_unavailable",severity:u?g:"blocking",message:i(e.message),source:"quote",..._?{lineId:_}:{},...e.group_id?{groupId:i(e.group_id)}:{},...e.data||!u?{data:{...e.data?j(e.data):{},...u?{}:{code:h}}}:{},fixes:!u&&!k.length&&_?[{action:"remove_line"}]:k}]}),canCheckout:t.can_checkout,checkout:{ready:o.ready,...o.reason!==void 0?{reason:o.reason}:{}}}}function E(c,r){let t,s=0,o,l,q=()=>{s++,t?.abort(),l=void 0;};return {cancel:q,async get(a$1,p,w){let m=JSON.stringify(a$1);if(!p&&o?.key===m&&Date.parse(o.quote.expiresAt)>Date.now())return w(o.quote),o.quote;if(!p&&l?.key===m)return l.promise;q();let v=s;t=new AbortController;let n=t.signal,e=(async()=>{let h=await i$1(m);n.throwIfAborted();let g=await c.quote({...r(),signal:n,idempotencyKey:h},a$1);n.throwIfAborted();let u=A(g,a$1.branch_id),k=new Set(u.lines.map(_=>_.lineId));if(k.size!==u.lines.length||u.lines.length!==a$1.lines.length||a$1.lines.some(_=>!k.has(_.line_id)))throw new a("server_error","Quote line identities do not match the cart");return v===s&&(o={key:m,quote:u},w(u)),u})();l={key:m,promise:e};try{return await e}finally{v===s&&(l=void 0);}},accept(a){o&&(o.quote={...o.quote,issues:o.quote.issues.filter(p=>!(p.code===a.code&&p.lineId===a.lineId))});}}}export{E as createQuoteClient};//# sourceMappingURL=quote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cart/quote-response.ts","../../src/cart/quote-client.ts"],"names":["fail","ZappieError","text","value","list","fix","v","record","types","t","codes","mapQuote","branchId","subtotal","mapMoney","checkout","amount","raw","result","charge","c","charges","discounts","total","sumMoney","deliveryCharges","lines","l","line","money","i","code","severity","known","fixes","f","lineId","createQuoteClient","transport","context","controller","sequence","cached","flight","cancel","body","force","apply","key","seq","signal","promise","idempotencyKey","payloadKey","quote","ids","issue"],"mappings":"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","file":"quote.js","sourcesContent":["// Quote responses load lazily with the quote client, outside the synchronous core.\nimport { ZappieError } from \"../errors\";\nimport { sumMoney } from \"../internal/money-math\";\nimport { money } from \"../money\";\nimport { mapMoney, record } from \"../transport/mappers\";\nimport type { CartFix, CartFulfilment, CartIssue, Quote } from \"./types\";\nfunction fail(): never {\n throw new ZappieError(\"server_error\", \"Invalid quote response\");\n}\nfunction text(value: unknown): string {\n if (typeof value !== \"string\") {\n return fail();\n }\n return value;\n}\nfunction list(value: unknown): unknown[] {\n if (!Array.isArray(value)) {\n return fail();\n }\n return value;\n}\n/** A fix newer than this SDK is dropped (undefined); a malformed known fix still fails. */\nfunction fix(value: unknown): CartFix | undefined {\n const v = record(value);\n switch (v.action) {\n case \"remove_line\":\n case \"accept_price\":\n case \"remove_promo\":\n case \"change_address\":\n return { action: v.action };\n case \"choose_option\":\n case \"choose_other_option\":\n return { action: v.action, groupId: text(v.group_id) };\n case \"set_quantity\": {\n if (\n !Number.isInteger(v.quantity) ||\n Number(v.quantity) < 1 ||\n Number(v.quantity) > 99\n ) {\n return fail();\n }\n return { action: v.action, quantity: v.quantity as number };\n }\n case \"schedule\":\n return {\n action: v.action,\n ...(v.next_open_at ? { nextOpenAt: text(v.next_open_at) } : {}),\n ...(v.slots ? { slots: list(v.slots).map(text) } : {}),\n };\n case \"switch_fulfilment\": {\n const types = list(v.types)\n .map(text)\n .filter((t) => [\"pickup\", \"delivery\", \"dine_in\"].includes(t));\n return types.length\n ? { action: v.action, types: types as CartFulfilment[\"type\"][] }\n : undefined;\n }\n default:\n return undefined;\n }\n}\nconst codes = [\n \"item_unavailable\",\n \"item_not_found\",\n \"option_unavailable\",\n \"option_required\",\n \"option_limit\",\n \"quantity_limit\",\n \"price_changed\",\n \"branch_closed\",\n \"fulfilment_unavailable\",\n \"outside_delivery_zone\",\n \"delivery_region_required\",\n \"schedule_invalid\",\n \"promo_invalid\",\n \"promo_min_spend\",\n \"min_order\",\n \"menu_version_stale\",\n];\nexport function mapQuote(value: unknown, branchId: string): Quote {\n const v = record(value);\n if (\n v.branch_id !== branchId ||\n !Number.isFinite(Date.parse(text(v.expires_at)))\n ) {\n return fail();\n }\n const subtotal = mapMoney(v.subtotal);\n const checkout = record(v.checkout);\n if (\n typeof v.can_checkout !== \"boolean\" ||\n typeof checkout.ready !== \"boolean\" ||\n (checkout.reason !== undefined &&\n checkout.reason !== \"issues\" &&\n checkout.reason !== \"customer_required\")\n ) {\n return fail();\n }\n const amount = (raw: unknown) => {\n const result = mapMoney(raw);\n if (result.currency !== subtotal.currency) {\n return fail();\n }\n return result;\n };\n const charge = (raw: unknown) => {\n const c = record(raw);\n return {\n code: text(c.code),\n label: text(c.label),\n amount: amount(c.amount),\n };\n };\n const charges = list(v.charges).map(charge);\n const discounts = list(v.discounts).map(charge);\n const total = amount(v.total);\n if (\n sumMoney(\n [...charges, ...discounts].map((c) => c.amount),\n subtotal,\n ).amountMinor !== total.amountMinor\n ) {\n return fail();\n }\n const deliveryCharges = charges.filter((c) => c.code === \"delivery\");\n const lines = list(v.lines).map((raw) => {\n const l = record(raw);\n if (\n (l.unit_price == null || l.line_total == null) &&\n v.lines_unpriced !== true\n ) {\n return fail();\n }\n return {\n lineId: text(l.line_id),\n unitPrice: l.unit_price == null ? null : amount(l.unit_price),\n total: l.line_total == null ? null : amount(l.line_total),\n };\n });\n if (\n v.lines_unpriced !== true &&\n sumMoney(\n lines.map((line) => line.total!),\n money(0, subtotal.currency),\n ).amountMinor !== subtotal.amountMinor\n ) {\n return fail();\n }\n return {\n lines,\n ...(v.lines_unpriced === true ? { linesUnpriced: true } : {}),\n subtotal,\n total,\n fees: charges.filter((c) => c.code !== \"delivery\"),\n discounts,\n ...(deliveryCharges.length\n ? {\n delivery: sumMoney(\n deliveryCharges.map((c) => c.amount),\n money(0, subtotal.currency),\n ),\n }\n : {}),\n menuVersion: text(v.menu_version),\n expiresAt: text(v.expires_at),\n issues: list(v.issues).flatMap((raw): CartIssue[] => {\n const i = record(raw);\n const code = text(i.code);\n const severity = text(i.severity);\n const known =\n codes.includes(code) &&\n [\"blocking\", \"warning\", \"info\"].includes(severity);\n const fixes = list(i.fixes)\n .map(fix)\n .filter((f): f is CartFix => f !== undefined);\n const lineId = i.line_id ? text(i.line_id) : undefined;\n if (!known && (severity === \"warning\" || severity === \"info\")) {\n console.warn(\n `Zappie quote issue \"${code}\" is newer than this SDK and was skipped`,\n );\n return [];\n }\n // A blocking (or unknown-severity) issue newer than the SDK keeps checkout blocked.\n return [\n {\n code: known ? (code as CartIssue[\"code\"]) : \"item_unavailable\",\n severity: known ? (severity as CartIssue[\"severity\"]) : \"blocking\",\n message: text(i.message),\n source: \"quote\",\n ...(lineId ? { lineId } : {}),\n ...(i.group_id ? { groupId: text(i.group_id) } : {}),\n ...(i.data || !known\n ? {\n data: {\n ...(i.data ? record(i.data) : {}),\n ...(known ? {} : { code }),\n },\n }\n : {}),\n fixes:\n !known && !fixes.length && lineId\n ? [{ action: \"remove_line\" }]\n : fixes,\n },\n ];\n }),\n canCheckout: v.can_checkout,\n checkout: {\n ready: checkout.ready,\n ...(checkout.reason !== undefined ? { reason: checkout.reason } : {}),\n },\n };\n}\n","import { ZappieError } from \"../errors\";\nimport { payloadKey } from \"../internal/digest\";\nimport type { RequestCtx, Transport } from \"../transport/types\";\nimport { mapQuote } from \"./quote-response\";\nimport type { Quote, QuoteRequest } from \"./types\";\nexport function createQuoteClient(\n transport: Transport,\n context: () => RequestCtx,\n) {\n let controller: AbortController | undefined;\n let sequence = 0;\n let cached: { key: string; quote: Quote } | undefined;\n let flight: { key: string; promise: Promise<Quote> } | undefined;\n const cancel = () => {\n sequence++;\n controller?.abort();\n flight = undefined;\n };\n return {\n cancel,\n async get(\n body: QuoteRequest,\n force: boolean,\n apply: (quote: Quote) => void,\n ): Promise<Quote> {\n const key = JSON.stringify(body);\n if (\n !force &&\n cached?.key === key &&\n Date.parse(cached.quote.expiresAt) > Date.now()\n ) {\n apply(cached.quote);\n return cached.quote;\n }\n if (!force && flight?.key === key) {\n return flight.promise;\n }\n cancel();\n const seq = sequence;\n controller = new AbortController();\n const signal = controller.signal;\n const promise = (async () => {\n const idempotencyKey = await payloadKey(key);\n signal.throwIfAborted();\n const raw = await transport.quote(\n { ...context(), signal, idempotencyKey },\n body,\n );\n signal.throwIfAborted();\n const quote = mapQuote(raw, body.branch_id);\n const ids = new Set(quote.lines.map((line) => line.lineId));\n if (\n ids.size !== quote.lines.length ||\n quote.lines.length !== body.lines.length ||\n body.lines.some((line) => !ids.has(line.line_id))\n ) {\n throw new ZappieError(\n \"server_error\",\n \"Quote line identities do not match the cart\",\n );\n }\n if (seq === sequence) {\n cached = { key, quote };\n apply(quote);\n }\n return quote;\n })();\n flight = { key, promise };\n try {\n return await promise;\n } finally {\n if (seq === sequence) {\n flight = undefined;\n }\n }\n },\n accept(issue: CartIssueForAcceptance) {\n if (cached) {\n cached.quote = {\n ...cached.quote,\n issues: cached.quote.issues.filter(\n (i) => !(i.code === issue.code && i.lineId === issue.lineId),\n ),\n };\n }\n },\n };\n}\ntype CartIssueForAcceptance = { code: string; lineId?: string };\n\nexport type QuoteClient = ReturnType<typeof createQuoteClient>;\n"]}
@@ -0,0 +1,141 @@
1
+ import { WireLine, WireMoney, ZappieError, Quote } from '@zappie/sdk';
2
+ export { wireFulfilment as checkoutFulfilment } from '@zappie/sdk/internal';
3
+
4
+ /**
5
+ * Address fields you can send with a delivery checkout request.
6
+ */
7
+ type CheckoutAddress = {
8
+ line1?: string;
9
+ city?: string;
10
+ place_id?: string;
11
+ lat?: number;
12
+ lng?: number;
13
+ };
14
+ /**
15
+ * Pickup or delivery details for a checkout session, including an optional schedule.
16
+ */
17
+ type CheckoutFulfilment = {
18
+ /** Dine-in orders are not checked out through a session. */
19
+ type: "pickup" | "delivery";
20
+ address?: CheckoutAddress;
21
+ scheduled_for?: string | null;
22
+ delivery_region_id?: string;
23
+ instructions?: string;
24
+ };
25
+ /**
26
+ * Cart lines, branch, fulfilment and return URLs you send to open checkout.
27
+ */
28
+ type CheckoutRequest = {
29
+ branch_id: string;
30
+ menu_version: string;
31
+ lines: WireLine[];
32
+ fulfilment: CheckoutFulfilment;
33
+ contact_phone?: string;
34
+ promo_code?: string;
35
+ return_url: string;
36
+ cancel_url?: string;
37
+ source?: string;
38
+ campaign?: string;
39
+ };
40
+ /**
41
+ * A checkout session returned by the service, with its amount, expiry and order status.
42
+ */
43
+ type CheckoutSession = {
44
+ id: string;
45
+ status: "open" | "completed" | "expired" | "cancelled";
46
+ url?: string;
47
+ amount: WireMoney;
48
+ order_id: string | null;
49
+ return_url: string;
50
+ cancel_url?: string;
51
+ expires_at: string;
52
+ };
53
+ /**
54
+ * A checkout session with a payment URL you can use to send the shopper to checkout.
55
+ */
56
+ type OpenCheckoutSession = CheckoutSession & {
57
+ url: string;
58
+ };
59
+ /**
60
+ * A terminal checkout result: a completed order, an expired session or a cancelled session.
61
+ */
62
+ type CheckoutCompletion = (CheckoutSession & {
63
+ status: "completed";
64
+ order_id: string;
65
+ }) | (CheckoutSession & {
66
+ status: "expired" | "cancelled";
67
+ });
68
+ /**
69
+ * Optional customer API base URL and fetch implementation for the checkout client.
70
+ */
71
+ type CheckoutClientOptions = {
72
+ customerBase?: string;
73
+ fetch?: typeof fetch;
74
+ };
75
+ /**
76
+ * Cancellation signal and polling interval in milliseconds for awaiting checkout completion.
77
+ */
78
+ type AwaitCompletionOptions = {
79
+ signal?: AbortSignal;
80
+ intervalMs?: number;
81
+ };
82
+
83
+ /**
84
+ * Create a client that opens checkout sessions, reads them and polls for completion.
85
+ *
86
+ * @param options - Override the default `/_zappie/api` base or fetch implementation.
87
+ * @returns A checkout client. Retain each idempotency key when retrying the same cart.
88
+ */
89
+ declare function createCheckout(options?: CheckoutClientOptions): {
90
+ /** The caller retains this key and reuses it when retrying the same cart. */
91
+ open(body: CheckoutRequest, idempotencyKey: string): Promise<OpenCheckoutSession>;
92
+ session(id: string): Promise<CheckoutSession>;
93
+ awaitCompletion(id: string, options?: AwaitCompletionOptions): Promise<CheckoutCompletion>;
94
+ };
95
+ /**
96
+ * The session creation, lookup and completion methods returned by createCheckout.
97
+ */
98
+ type CheckoutClient = ReturnType<typeof createCheckout>;
99
+
100
+ /**
101
+ * A checkout API failure with its HTTP status, API code and optional validation issues.
102
+ * Statuses 409, 429 and 5xx are marked retryable.
103
+ */
104
+ declare class CheckoutError extends ZappieError {
105
+ readonly apiCode: string;
106
+ readonly issues?: unknown;
107
+ constructor(status: number, apiCode: string, message: string, param?: string, issues?: unknown);
108
+ }
109
+
110
+ /**
111
+ * The next action your payment UI should present, from fixing an address to paying.
112
+ */
113
+ type PayReadiness = "address" | "quoting" | "retry" | "choose_area" | "fix_issues" | "sign_in" | "phone" | "pay";
114
+ /**
115
+ * The quote loading state and, when ready, the quote used to decide payment readiness.
116
+ */
117
+ type PayReadinessQuoteState = {
118
+ status: "idle" | "loading";
119
+ } | {
120
+ status: "error";
121
+ } | {
122
+ status: "ready";
123
+ quote: Quote;
124
+ };
125
+ /**
126
+ * Check whether a phone is blank or contains 7 to 15 digits with an optional leading plus.
127
+ * Whitespace within a nonblank phone is ignored. This does not verify that the number exists.
128
+ *
129
+ * @param phone - The contact phone entered by the shopper.
130
+ * @returns Whether the phone passes this optional-field format check.
131
+ */
132
+ declare function isContactPhoneValid(phone: string): boolean;
133
+ /**
134
+ * Choose the next payment action from address validity, quote state and contact phone validity.
135
+ * Address and quote issues take priority over sign-in and phone prompts.
136
+ *
137
+ * @returns The action you should display in your payment UI.
138
+ */
139
+ declare function payReadiness(state: PayReadinessQuoteState, contactPhoneValid: boolean, addressValid?: boolean): PayReadiness;
140
+
141
+ export { type AwaitCompletionOptions, type CheckoutAddress, type CheckoutClient, type CheckoutClientOptions, type CheckoutCompletion, CheckoutError, type CheckoutFulfilment, type CheckoutRequest, type CheckoutSession, type OpenCheckoutSession, type PayReadiness, type PayReadinessQuoteState, createCheckout, isContactPhoneValid, payReadiness };
@@ -0,0 +1 @@
1
+ import {ZappieError,money}from'@zappie/sdk';export{wireFulfilment as checkoutFulfilment}from'@zappie/sdk/internal';function l(e){return e===400||e===422?"invalid_request":e===401?"unauthorized":e===404?"not_found":e===429?"rate_limited":"server_error"}async function h(e,r){let t=await e.json().catch(()=>({})),s=t&&typeof t=="object"?t:{},o="error"in s&&s.error&&typeof s.error=="object"?s.error:{};return {apiCode:"code"in o&&typeof o.code=="string"?o.code:"server_error",message:"message"in o&&typeof o.message=="string"?o.message:r,..."param"in o&&typeof o.param=="string"?{param:o.param}:{},..."issues"in o?{issues:o.issues}:{}}}function m(e,r){return r?.throwIfAborted(),new Promise((t,s)=>{let i=setTimeout(()=>{r?.removeEventListener("abort",n),t();},e),n=()=>{clearTimeout(i),r?.removeEventListener("abort",n),s(r?.reason??new DOMException("Aborted","AbortError"));};r?.addEventListener("abort",n,{once:true});})}async function y(e,r,t,s){for(let o=0;o<2;o++)try{return await e(r,{...t,credentials:"same-origin",headers:{Accept:"application/json",...t.headers}})}catch(i){if(t.signal?.aborted)throw t.signal.reason??i;if(o===1)throw new ZappieError("network_error",s,{cause:i,retryable:true});await m(400,t.signal??void 0);}throw new ZappieError("network_error",s,{retryable:true})}var p=class extends ZappieError{constructor(t,s,o,i,n){super(l(t),o,{status:t,retryable:t===409||t===429||t>=500,...i!==void 0?{param:i}:{}});this.apiCode=s;n!==void 0&&(this.issues=n);}};function k(e){if(!e||typeof e!="object"||Array.isArray(e))throw new ZappieError("server_error","Invalid checkout session response");return e}function d(e){if(typeof e!="string")throw new ZappieError("server_error","Invalid checkout session response");return e}function C(e){try{return money(0,d(e)).currency}catch{throw new ZappieError("server_error","Invalid checkout session response")}}function g(e){let r=k(e),t=k(r.amount),s=d(r.status);if(!["open","completed","expired","cancelled"].includes(s))throw new ZappieError("server_error","Invalid checkout session response");let o=d(r.expires_at);if(!Number.isFinite(Date.parse(o)))throw new ZappieError("server_error","Invalid checkout session response");if(!Number.isSafeInteger(t.amount_minor))throw new ZappieError("server_error","Invalid checkout session response");if(r.order_id!==null&&typeof r.order_id!="string")throw new ZappieError("server_error","Invalid checkout session response");return {id:d(r.id),status:s,...typeof r.url=="string"?{url:r.url}:{},amount:{amount_minor:t.amount_minor,currency:C(t.currency)},order_id:r.order_id,return_url:d(r.return_url),...typeof r.cancel_url=="string"?{cancel_url:r.cancel_url}:{},expires_at:o}}var b="/_zappie/api";async function x(e){let r=await h(e,`The checkout service answered ${e.status}`);return new p(e.status,r.apiCode,r.message,r.param,r.issues)}function v(e={}){let r=(e.customerBase??b).replace(/\/+$/,""),t=e.fetch??globalThis.fetch,s=async(i,n)=>{let c=await y(t,r+i,n,"Could not reach checkout");if(!c.ok)throw await x(c);return g(await c.json())},o=(i,n)=>s(`/checkout/sessions/${encodeURIComponent(i)}`,{method:"GET",...n?{signal:n}:{}});return {async open(i,n){let c=await s("/checkout/sessions",{method:"POST",headers:{"Content-Type":"application/json","Idempotency-Key":n},body:JSON.stringify(i)});if(!c.url)throw new ZappieError("server_error","Checkout session has no payment URL");return globalThis.__zappieEmit?.("checkout:open",c),{...c,url:c.url}},session(i){return o(i)},async awaitCompletion(i,n={}){let c=n.intervalMs??3e3;if(!Number.isFinite(c)||c<0)throw new ZappieError("invalid_request","intervalMs cannot be negative");for(;;){n.signal?.throwIfAborted();try{let a=await o(i,n.signal);if(a.status==="completed"&&a.order_id)return globalThis.__zappieEmit?.("checkout:completed",a),{...a,status:"completed",order_id:a.order_id};if(a.status==="expired"||a.status==="cancelled")return {...a,status:a.status}}catch(a){if(n.signal?.throwIfAborted(),a instanceof ZappieError&&a.status!==void 0&&a.status>=400&&a.status<500)throw a}await m(c,n.signal);}}}}function E(e){return e.trim()===""||/^\+?[0-9]{7,15}$/.test(e.replace(/\s+/g,""))}function R(e,r,t=true){if(!t)return "address";switch(e.status){case "idle":case "loading":return "quoting";case "error":return "retry"}return e.quote.issues.some(s=>s.code==="delivery_region_required")?"choose_area":!e.quote.canCheckout||e.quote.checkout.reason==="issues"?"fix_issues":e.quote.checkout.reason==="customer_required"?"sign_in":r?"pay":"phone"}export{p as CheckoutError,v as createCheckout,E as isContactPhoneValid,R as payReadiness};//# sourceMappingURL=index.js.map