@zappie/kit 0.1.8 → 0.1.9

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @zappie/kit
2
2
 
3
+ ## 0.1.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 20160ee: `contactLinks(branch)` in `@zappie/foundations` lists the ways a shopper can reach a branch: a call to its phone and its messaging link. The kit's account page takes `branch` and `version` and shows a "Contact support" row with those links and a "Version" row. The starter's account page shows the branch's contact links.
8
+
3
9
  ## 0.1.8
4
10
 
5
11
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":"2026-10-10","items":[{"name":"account-page","type":"block","version":"1.0.0","title":"Account page","description":"Show your profile and manage saved addresses."},{"name":"address-picker","type":"component","version":"1.0.0","title":"Address picker","description":"Find delivery addresses and show them on a map."},{"name":"cart-button","type":"component","version":"1.1.1","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal."},{"name":"cart-drawer","type":"component","version":"1.1.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer."},{"name":"cart-line","type":"component","version":"1.0.0","title":"Cart line","description":"Show options and let shoppers change quantity or remove a line."},{"name":"category-nav","type":"component","version":"1.0.0","title":"Category navigation","description":"Navigate categories with tabs that follow scrolling."},{"name":"checkout-button","type":"component","version":"1.1.0","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution."},{"name":"checkout-page","type":"block","version":"1.0.1","title":"Checkout page","description":"Collect contact and delivery details, resolve cart issues and continue to payment."},{"name":"checkout-return","type":"block","version":"1.0.0","title":"Checkout return","description":"Confirm payment and link to the completed order."},{"name":"consent","type":"component","version":"1.0.0","title":"Consent","description":"Save your privacy choice and withdraw consent when you choose."},{"name":"fulfilment-switch","type":"component","version":"1.0.0","title":"Fulfilment switch","description":"Choose pickup or delivery from the branch options."},{"name":"item-card","type":"component","version":"1.1.0","title":"Item card","description":"Show an item, its price, availability and cart feedback."},{"name":"item-sheet","type":"component","version":"1.0.0","title":"Item sheet","description":"Let shoppers configure an item, with optional v2 URL navigation."},{"name":"menu-page","type":"block","version":"1.0.0","title":"Menu page","description":"Combine the catalogue, item view and cart in your framework."},{"name":"money","type":"component","version":"1.0.0","title":"Money","description":"Format SDK money with your theme styles."},{"name":"open-status","type":"component","version":"1.0.0","title":"Open status","description":"Show whether a branch is open and when that changes."},{"name":"order-page","type":"block","version":"1.0.0","title":"Order page","description":"Show order progress and your support links."},{"name":"order-status","type":"component","version":"1.0.0","title":"Order status","description":"Show live order progress, items and totals."},{"name":"orders-page","type":"block","version":"1.0.0","title":"Orders page","description":"Browse your orders and load older orders."},{"name":"provider","type":"component","version":"1.0.0","title":"Zappie provider","description":"Create a storefront client and apply scoped tokens and fonts."},{"name":"quantity","type":"component","version":"1.0.0","title":"Quantity","description":"Adjust quantity within its limits."},{"name":"schedule-picker","type":"component","version":"1.0.0","title":"Schedule picker","description":"Choose ASAP or an available time for the cart."},{"name":"search","type":"component","version":"1.0.0","title":"Search","description":"Search the menu as shoppers type and discard outdated results."},{"name":"sign-in-button","type":"component","version":"1.0.0","title":"Sign-in button","description":"Show customer sign-in and sign-out controls."},{"name":"store-footer","type":"component","version":"1.0.0","title":"Store footer","description":"Show branch contacts, social links and your legal links."},{"name":"store-header","type":"component","version":"1.1.0","title":"Store header","description":"Show store details, opening status and fulfilment."}]}
1
+ {"version":"2026-10-10","items":[{"name":"account-page","type":"block","version":"1.1.0","title":"Account page","description":"Show your profile, manage saved addresses and contact the branch."},{"name":"address-picker","type":"component","version":"1.0.0","title":"Address picker","description":"Find delivery addresses and show them on a map."},{"name":"cart-button","type":"component","version":"1.1.1","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal."},{"name":"cart-drawer","type":"component","version":"1.1.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer."},{"name":"cart-line","type":"component","version":"1.0.0","title":"Cart line","description":"Show options and let shoppers change quantity or remove a line."},{"name":"category-nav","type":"component","version":"1.0.0","title":"Category navigation","description":"Navigate categories with tabs that follow scrolling."},{"name":"checkout-button","type":"component","version":"1.1.1","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution."},{"name":"checkout-page","type":"block","version":"1.0.1","title":"Checkout page","description":"Collect contact and delivery details, resolve cart issues and continue to payment."},{"name":"checkout-return","type":"block","version":"1.0.0","title":"Checkout return","description":"Confirm payment and link to the completed order."},{"name":"consent","type":"component","version":"1.0.0","title":"Consent","description":"Save your privacy choice and withdraw consent when you choose."},{"name":"fulfilment-switch","type":"component","version":"1.0.0","title":"Fulfilment switch","description":"Choose pickup or delivery from the branch options."},{"name":"item-card","type":"component","version":"1.1.0","title":"Item card","description":"Show an item, its price, availability and cart feedback."},{"name":"item-sheet","type":"component","version":"1.0.0","title":"Item sheet","description":"Let shoppers configure an item, with optional v2 URL navigation."},{"name":"menu-page","type":"block","version":"1.0.0","title":"Menu page","description":"Combine the catalogue, item view and cart in your framework."},{"name":"money","type":"component","version":"1.0.0","title":"Money","description":"Format SDK money with your theme styles."},{"name":"open-status","type":"component","version":"1.0.0","title":"Open status","description":"Show whether a branch is open and when that changes."},{"name":"order-page","type":"block","version":"1.0.0","title":"Order page","description":"Show order progress and your support links."},{"name":"order-status","type":"component","version":"1.0.0","title":"Order status","description":"Show live order progress, items and totals."},{"name":"orders-page","type":"block","version":"1.0.0","title":"Orders page","description":"Browse your orders and load older orders."},{"name":"provider","type":"component","version":"1.0.0","title":"Zappie provider","description":"Create a storefront client and apply scoped tokens and fonts."},{"name":"quantity","type":"component","version":"1.0.0","title":"Quantity","description":"Adjust quantity within its limits."},{"name":"schedule-picker","type":"component","version":"1.0.0","title":"Schedule picker","description":"Choose ASAP or an available time for the cart."},{"name":"search","type":"component","version":"1.0.0","title":"Search","description":"Search the menu as shoppers type and discard outdated results."},{"name":"sign-in-button","type":"component","version":"1.0.0","title":"Sign-in button","description":"Show customer sign-in and sign-out controls."},{"name":"store-footer","type":"component","version":"1.0.0","title":"Store footer","description":"Show branch contacts, social links and your legal links."},{"name":"store-header","type":"component","version":"1.1.0","title":"Store header","description":"Show store details, opening status and fulfilment."}]}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"account-page","type":"block","version":"1.0.0","title":"Account page","description":"Show your profile and manage saved addresses.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["sign-in-button"],"files":[{"path":"account-page.css","target":"components/zappie/account-page.css","sha256":"6bfc66e08b88126e9f59eeb4c58c872629efe7a5d7f31a15176e707ebc9ba8c7","content":".z-kit-account-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-account-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-account-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-account-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-account-page ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"account-page.tsx","target":"components/zappie/account-page.tsx","sha256":"844ed8a5fb5df3353120ab4fca0d7c0786ab55f76b2d5c604b0d39f1fe983511","content":"\"use client\";\n\nimport { SignInButton } from \"@/components/zappie/sign-in-button\";\nimport type {\n CustomerClient,\n CustomerProfile,\n SavedAddress,\n} from \"@zappie/sdk/customer\";\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./account-page.css\";\n\n/** Show your profile and let you save or remove delivery addresses. */\nexport function AccountPage({\n customer,\n ordersHref = \"/orders\",\n}: {\n /** Use the same client as your CustomerProvider. */\n customer: CustomerClient;\n ordersHref?: string;\n}) {\n const auth = useAuth();\n return (\n <section className=\"z-kit-account-page\" aria-label=\"Your account\">\n <h2>Your account</h2>\n {auth.state === \"unknown\" ? (\n <p role=\"status\">Checking your session…</p>\n ) : auth.customer ? (\n <Account key={auth.customer.id} customer={customer} />\n ) : (\n <p>Sign in to see your profile and saved addresses.</p>\n )}\n <SignInButton />\n <a href={ordersHref}>Your orders</a>\n </section>\n );\n}\n\nfunction Account({ customer }: { customer: CustomerClient }) {\n const [profile, setProfile] = useState<CustomerProfile>();\n const [addresses, setAddresses] = useState<SavedAddress[]>([]);\n const [loading, setLoading] = useState(true);\n const [attempt, setAttempt] = useState(0);\n const [error, setError] = useState(\"\");\n const [busy, setBusy] = useState(false);\n useEffect(() => {\n let active = true;\n setLoading(true);\n setError(\"\");\n void Promise.all([customer.profile(), customer.addresses()])\n .then(\n ([next, saved]) => {\n if (active) {\n setProfile(next);\n setAddresses(saved);\n }\n },\n () => {\n if (active) {\n setError(\"We could not load your account. Try again.\");\n }\n },\n )\n .finally(() => {\n if (active) {\n setLoading(false);\n }\n });\n return () => {\n active = false;\n };\n }, [customer, attempt]);\n const remove = async (id: string) => {\n setBusy(true);\n setError(\"\");\n try {\n await customer.removeAddress(id);\n setAddresses((current) => current.filter((address) => address.id !== id));\n } catch {\n setError(\"We could not remove that address. Try again.\");\n } finally {\n setBusy(false);\n }\n };\n return (\n <div>\n {loading && <p role=\"status\">Loading your account…</p>}\n {error && (\n <div role=\"alert\">\n <p>{error}</p>\n <button\n type=\"button\"\n onClick={() => setAttempt((value) => value + 1)}\n >\n Reload account\n </button>\n </div>\n )}\n {profile && (\n <div>\n <p>\n {[profile.given_name, profile.family_name]\n .filter(Boolean)\n .join(\" \")}\n </p>\n <p>{profile.phone_masked}</p>\n </div>\n )}\n <h3>Saved addresses</h3>\n {!loading && addresses.length === 0 && (\n <p>You have no saved addresses.</p>\n )}\n <ul>\n {addresses.map((address) => (\n <li key={address.id}>\n <p>\n {address.label && `${address.label}: `}\n {address.line1}\n </p>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => void remove(address.id)}\n aria-label={`Remove ${address.line1}`}\n >\n Remove\n </button>\n </li>\n ))}\n </ul>\n <form\n onSubmit={(event) => {\n event.preventDefault();\n const form = event.currentTarget;\n const data = new FormData(form);\n const line1 = String(data.get(\"line1\") ?? \"\").trim();\n if (!line1 || busy) {\n return;\n }\n setBusy(true);\n setError(\"\");\n void customer\n .addAddress({\n line1,\n label: \"other\",\n instructions: String(data.get(\"instructions\") ?? \"\").trim(),\n })\n .then(\n (address) => {\n setAddresses((current) => [...current, address]);\n form.reset();\n },\n () => setError(\"We could not save that address. Try again.\"),\n )\n .finally(() => setBusy(false));\n }}\n >\n <label>\n Street and address\n <input name=\"line1\" autoComplete=\"street-address\" required />\n </label>\n <label>\n Delivery instructions\n <input name=\"instructions\" maxLength={200} />\n </label>\n <button type=\"submit\" disabled={busy || loading}>\n {busy ? \"Saving…\" : \"Save address\"}\n </button>\n </form>\n </div>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/account-page"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"account-page","type":"block","version":"1.1.0","title":"Account page","description":"Show your profile, manage saved addresses and contact the branch.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.2.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["sign-in-button"],"files":[{"path":"account-page.css","target":"components/zappie/account-page.css","sha256":"6bfc66e08b88126e9f59eeb4c58c872629efe7a5d7f31a15176e707ebc9ba8c7","content":".z-kit-account-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-account-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-account-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-account-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-account-page ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"account-page.tsx","target":"components/zappie/account-page.tsx","sha256":"c87d6d13a7ab954fbc2ae706eb54be3ec1821b160a8b20ad45ad4df4c1ed96d4","content":"\"use client\";\n\nimport { SignInButton } from \"@/components/zappie/sign-in-button\";\nimport { contactLinks } from \"@zappie/foundations\";\nimport type { Branch } from \"@zappie/sdk\";\nimport type {\n CustomerClient,\n CustomerProfile,\n SavedAddress,\n} from \"@zappie/sdk/customer\";\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./account-page.css\";\n\n/**\n * Show your profile and let you save or remove delivery addresses, with links to your orders and\n * to the branch's phone and messaging link, and your site's version.\n */\nexport function AccountPage({\n customer,\n ordersHref = \"/orders\",\n branch,\n version,\n}: {\n /** Use the same client as your CustomerProvider. */\n customer: CustomerClient;\n ordersHref?: string;\n /** The branch whose phone and messaging link the \"Contact support\" row offers. */\n branch?: Pick<Branch, \"phone\" | \"socialLinks\">;\n /** Your site's version, shown as the last row when you pass it. */\n version?: string;\n}) {\n const auth = useAuth();\n const contacts = branch ? contactLinks(branch) : [];\n return (\n <section className=\"z-kit-account-page\" aria-label=\"Your account\">\n <h2>Your account</h2>\n {auth.state === \"unknown\" ? (\n <p role=\"status\">Checking your session…</p>\n ) : auth.customer ? (\n <Account key={auth.customer.id} customer={customer} />\n ) : (\n <p>Sign in to see your profile and saved addresses.</p>\n )}\n <SignInButton />\n <a href={ordersHref}>Your orders</a>\n {contacts.length > 0 && (\n <nav aria-label=\"Contact support\">\n <h3>Contact support</h3>\n {contacts.map((link) => (\n <a key={link.href} href={link.href} data-contact={link.type}>\n {link.label}\n </a>\n ))}\n </nav>\n )}\n {version && <p>Version: {version}</p>}\n </section>\n );\n}\n\nfunction Account({ customer }: { customer: CustomerClient }) {\n const [profile, setProfile] = useState<CustomerProfile>();\n const [addresses, setAddresses] = useState<SavedAddress[]>([]);\n const [loading, setLoading] = useState(true);\n const [attempt, setAttempt] = useState(0);\n const [error, setError] = useState(\"\");\n const [busy, setBusy] = useState(false);\n useEffect(() => {\n let active = true;\n setLoading(true);\n setError(\"\");\n void Promise.all([customer.profile(), customer.addresses()])\n .then(\n ([next, saved]) => {\n if (active) {\n setProfile(next);\n setAddresses(saved);\n }\n },\n () => {\n if (active) {\n setError(\"We could not load your account. Try again.\");\n }\n },\n )\n .finally(() => {\n if (active) {\n setLoading(false);\n }\n });\n return () => {\n active = false;\n };\n }, [customer, attempt]);\n const remove = async (id: string) => {\n setBusy(true);\n setError(\"\");\n try {\n await customer.removeAddress(id);\n setAddresses((current) => current.filter((address) => address.id !== id));\n } catch {\n setError(\"We could not remove that address. Try again.\");\n } finally {\n setBusy(false);\n }\n };\n return (\n <div>\n {loading && <p role=\"status\">Loading your account…</p>}\n {error && (\n <div role=\"alert\">\n <p>{error}</p>\n <button\n type=\"button\"\n onClick={() => setAttempt((value) => value + 1)}\n >\n Reload account\n </button>\n </div>\n )}\n {profile && (\n <div>\n <p>\n {[profile.given_name, profile.family_name]\n .filter(Boolean)\n .join(\" \")}\n </p>\n <p>{profile.phone_masked}</p>\n </div>\n )}\n <h3>Saved addresses</h3>\n {!loading && addresses.length === 0 && (\n <p>You have no saved addresses.</p>\n )}\n <ul>\n {addresses.map((address) => (\n <li key={address.id}>\n <p>\n {address.label && `${address.label}: `}\n {address.line1}\n </p>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => void remove(address.id)}\n aria-label={`Remove ${address.line1}`}\n >\n Remove\n </button>\n </li>\n ))}\n </ul>\n <form\n onSubmit={(event) => {\n event.preventDefault();\n const form = event.currentTarget;\n const data = new FormData(form);\n const line1 = String(data.get(\"line1\") ?? \"\").trim();\n if (!line1 || busy) {\n return;\n }\n setBusy(true);\n setError(\"\");\n void customer\n .addAddress({\n line1,\n label: \"other\",\n instructions: String(data.get(\"instructions\") ?? \"\").trim(),\n })\n .then(\n (address) => {\n setAddresses((current) => [...current, address]);\n form.reset();\n },\n () => setError(\"We could not save that address. Try again.\"),\n )\n .finally(() => setBusy(false));\n }}\n >\n <label>\n Street and address\n <input name=\"line1\" autoComplete=\"street-address\" required />\n </label>\n <label>\n Delivery instructions\n <input name=\"instructions\" maxLength={200} />\n </label>\n <button type=\"submit\" disabled={busy || loading}>\n {busy ? \"Saving…\" : \"Save address\"}\n </button>\n </form>\n </div>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/account-page"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.1.0","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-button.tsx","target":"components/zappie/checkout-button.tsx","sha256":"779ae1c956b15a9527f45c16422341f157bc33d7f1c5f17733fd20de4c919697","content":"\"use client\";\n\nimport type { CartModule } from \"@zappie/sdk\";\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { CheckoutError, isContactPhoneValid } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { redirectToCheckout } from \"@zappie/sdk/redirect\";\nimport { currentSource } from \"@zappie/sdk/visits\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { createCheckoutAttempt } from \"./checkout-attempt\";\nimport \"./checkout-button.css\";\nimport { checkoutButtonState } from \"./checkout-state\";\n\nexport type CheckoutButtonProps = {\n checkout: CheckoutClient;\n branchId: string;\n returnUrl?: string;\n cancelUrl?: string;\n contactPhone?: string;\n /** The rider's note for a delivery. It goes with payment and never asks for a new quote. */\n instructions?: string;\n attribution?: () => { source: string; campaign: string };\n onSignIn?: () => void;\n /** Show a recoverable error when opening payment fails. */\n onError?: (failure: unknown) => void;\n onContactPhoneRequired?: () => void;\n redirect?: (url: string) => void;\n};\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\n\nfunction withInstructions(payload: Payload, note: string | undefined) {\n const f = payload.fulfilment;\n if (note === undefined || f.type !== \"delivery\") {\n return payload;\n }\n const { instructions: _old, ...address } = f.address;\n const value = note.trim();\n return {\n ...payload,\n fulfilment: {\n ...f,\n address: value ? { ...address, instructions: value } : address,\n },\n };\n}\n\nexport function CheckoutButton({\n checkout,\n branchId,\n returnUrl,\n cancelUrl,\n contactPhone = \"\",\n instructions,\n attribution = currentSource,\n onSignIn,\n onError,\n onContactPhoneRequired,\n redirect = (url) => redirectToCheckout(url, window.top ?? window),\n}: CheckoutButtonProps) {\n const cart = useCart();\n const attempt = useMemo(() => createCheckoutAttempt(), []);\n const [busy, setBusy] = useState(false);\n const quoted = useRef(\"\");\n const quoteKey = JSON.stringify([\n cart.lines,\n cart.fulfilment,\n cart.promoCode,\n ]);\n const [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\" &&\n quoted.current !== quoteKey\n ) {\n quoted.current = quoteKey;\n void cart.actions.quote().catch((failure: unknown) => onError?.(failure));\n }\n }, [\n cart.actions,\n cart.isHydrated,\n cart.lines.length,\n cart.state.status,\n quoteKey,\n onError,\n ]);\n const state = checkoutButtonState(\n cart.state,\n cart.fulfilment,\n isContactPhoneValid(contactPhone),\n busy,\n forced,\n );\n const activate = async () => {\n if (state.status === \"sign_in\") {\n onSignIn?.();\n return;\n }\n if (state.status === \"phone\") {\n onContactPhoneRequired?.();\n return;\n }\n if (state.status === \"retry\") {\n try {\n await cart.actions.quote({ force: true });\n } catch (failure) {\n onError?.(failure);\n }\n return;\n }\n if (state.status !== \"pay\") {\n return;\n }\n setBusy(true);\n try {\n const checkoutAttempt = attempt({\n payload: withInstructions(\n cart.actions.toCheckoutPayload(),\n instructions,\n ),\n branchId,\n contactPhone,\n returnUrl: returnUrl ?? `${location.origin}/checkout/return`,\n ...((cancelUrl ?? location.href)\n ? { cancelUrl: cancelUrl ?? location.href }\n : {}),\n attribution: attribution(),\n });\n const session = await checkout.open(\n checkoutAttempt.request,\n checkoutAttempt.key,\n );\n redirect(session.url);\n } catch (failure) {\n if (\n failure instanceof CheckoutError &&\n failure.apiCode === \"customer_required\"\n ) {\n setForced(\"sign_in\");\n onSignIn?.();\n return;\n }\n if (onError) {\n onError(failure);\n return;\n }\n throw failure;\n } finally {\n setBusy(false);\n }\n };\n return (\n <button\n type=\"button\"\n className=\"z-kit-checkout-button\"\n data-state={state.status}\n data-zappie-checkout=\"\"\n disabled={!state.enabled}\n onClick={() => void activate()}\n >\n {state.label}\n </button>\n );\n}\n"},{"path":"checkout-attempt.ts","target":"components/zappie/checkout-attempt.ts","sha256":"cefb578068eac7d86f343d2ae5f7f9e2feda13bced3f05b365b78a71252d1bca","content":"import type { CartModule } from \"@zappie/sdk\";\nimport { checkoutFulfilment, type CheckoutRequest } from \"@zappie/sdk/checkout\";\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\ntype Attribution = { source: string; campaign: string };\nexport type CheckoutAttempt = { key: string; request: CheckoutRequest };\n\nfunction fulfilment(payload: Payload) {\n const value = checkoutFulfilment(payload.fulfilment);\n if (value.type === \"pickup\") {\n return {\n type: \"pickup\" as const,\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n if (value.type === \"delivery\") {\n return {\n type: \"delivery\" as const,\n ...(value.address !== undefined ? { address: value.address } : {}),\n ...(value.delivery_region_id !== undefined\n ? { delivery_region_id: value.delivery_region_id }\n : {}),\n ...(value.instructions !== undefined\n ? { instructions: value.instructions }\n : {}),\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n throw new Error(\"Dine-in checkout is unavailable\");\n}\n\nexport function createCheckoutAttempt() {\n let current: { digest: string; attempt: CheckoutAttempt } | undefined;\n return ({\n payload,\n branchId,\n contactPhone,\n returnUrl,\n cancelUrl,\n attribution,\n }: {\n payload: Payload;\n branchId: string;\n contactPhone: string;\n returnUrl: string;\n cancelUrl?: string;\n attribution: Attribution;\n }) => {\n const normalizedPhone = contactPhone.trim().replace(/\\s+/g, \"\");\n const digest = JSON.stringify({ payload, contactPhone: normalizedPhone });\n if (current?.digest === digest) {\n return current.attempt;\n }\n const request: CheckoutRequest = {\n branch_id: branchId,\n menu_version: payload.menu_version,\n lines: payload.lines,\n fulfilment: fulfilment(payload),\n ...(normalizedPhone ? { contact_phone: normalizedPhone } : {}),\n ...(payload.promo_code ? { promo_code: payload.promo_code } : {}),\n ...(attribution.source ? { source: attribution.source } : {}),\n ...(attribution.campaign ? { campaign: attribution.campaign } : {}),\n return_url: returnUrl,\n ...(cancelUrl ? { cancel_url: cancelUrl } : {}),\n };\n current = { digest, attempt: { key: crypto.randomUUID(), request } };\n return current.attempt;\n };\n}\n"},{"path":"checkout-state.ts","target":"components/zappie/checkout-state.ts","sha256":"e8e2dc8f88d513c124d44516e3d18012d4fd7e2c069e31454d4426dd6c2d4e43","content":"import type { CartFulfilment, CartState } from \"@zappie/sdk\";\nimport {\n payReadiness,\n type PayReadiness,\n type PayReadinessQuoteState,\n} from \"@zappie/sdk/checkout\";\n\nexport type CheckoutButtonStatus = PayReadiness | \"busy\";\nconst labels: Record<CheckoutButtonStatus, string> = {\n address: \"Add a delivery address\",\n quoting: \"Updating total…\",\n retry: \"Update totals to continue\",\n choose_area: \"Choose a delivery area\",\n fix_issues: \"Review your cart\",\n sign_in: \"Sign in to continue\",\n phone: \"Check your phone number\",\n pay: \"Continue to payment\",\n busy: \"Please wait…\",\n};\n\nfunction quoteState(state: CartState): PayReadinessQuoteState {\n if (state.status === \"quoting\") {\n return { status: \"loading\" };\n }\n if (state.status === \"quoted\") {\n return { status: \"ready\", quote: state.quote };\n }\n if (state.status === \"needs_attention\") {\n return state.quote\n ? { status: \"ready\", quote: state.quote }\n : { status: \"error\" };\n }\n return { status: \"idle\" };\n}\n\nexport function checkoutButtonState(\n state: CartState,\n fulfilment: CartFulfilment | null,\n contactPhoneValid: boolean,\n busy: boolean,\n forced?: \"sign_in\",\n) {\n let status: CheckoutButtonStatus;\n if (busy) {\n status = \"busy\";\n } else if (forced) {\n status = forced;\n } else {\n const addressValid =\n fulfilment?.type !== \"delivery\" ||\n Boolean(\n fulfilment.address.line1 ||\n fulfilment.address.placeId ||\n fulfilment.address.coordinates,\n );\n status = payReadiness(quoteState(state), contactPhoneValid, addressValid);\n if (status === \"pay\" && state.status === \"needs_attention\") {\n status = \"fix_issues\";\n }\n }\n return {\n status,\n label: labels[status],\n enabled:\n status === \"pay\" ||\n status === \"sign_in\" ||\n status === \"phone\" ||\n status === \"retry\",\n };\n}\n"},{"path":"checkout-button.css","target":"components/zappie/checkout-button.css","sha256":"9e6bb8afe1bb1e5cd55ed458e435d90b4b3d546fb4276d3fd9432d022844ca22","content":".z-kit-checkout-button {\n width: 100%;\n border: 0;\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-checkout-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-checkout-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-radius-md","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-button"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.1.1","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-button.tsx","target":"components/zappie/checkout-button.tsx","sha256":"779ae1c956b15a9527f45c16422341f157bc33d7f1c5f17733fd20de4c919697","content":"\"use client\";\n\nimport type { CartModule } from \"@zappie/sdk\";\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { CheckoutError, isContactPhoneValid } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { redirectToCheckout } from \"@zappie/sdk/redirect\";\nimport { currentSource } from \"@zappie/sdk/visits\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { createCheckoutAttempt } from \"./checkout-attempt\";\nimport \"./checkout-button.css\";\nimport { checkoutButtonState } from \"./checkout-state\";\n\nexport type CheckoutButtonProps = {\n checkout: CheckoutClient;\n branchId: string;\n returnUrl?: string;\n cancelUrl?: string;\n contactPhone?: string;\n /** The rider's note for a delivery. It goes with payment and never asks for a new quote. */\n instructions?: string;\n attribution?: () => { source: string; campaign: string };\n onSignIn?: () => void;\n /** Show a recoverable error when opening payment fails. */\n onError?: (failure: unknown) => void;\n onContactPhoneRequired?: () => void;\n redirect?: (url: string) => void;\n};\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\n\nfunction withInstructions(payload: Payload, note: string | undefined) {\n const f = payload.fulfilment;\n if (note === undefined || f.type !== \"delivery\") {\n return payload;\n }\n const { instructions: _old, ...address } = f.address;\n const value = note.trim();\n return {\n ...payload,\n fulfilment: {\n ...f,\n address: value ? { ...address, instructions: value } : address,\n },\n };\n}\n\nexport function CheckoutButton({\n checkout,\n branchId,\n returnUrl,\n cancelUrl,\n contactPhone = \"\",\n instructions,\n attribution = currentSource,\n onSignIn,\n onError,\n onContactPhoneRequired,\n redirect = (url) => redirectToCheckout(url, window.top ?? window),\n}: CheckoutButtonProps) {\n const cart = useCart();\n const attempt = useMemo(() => createCheckoutAttempt(), []);\n const [busy, setBusy] = useState(false);\n const quoted = useRef(\"\");\n const quoteKey = JSON.stringify([\n cart.lines,\n cart.fulfilment,\n cart.promoCode,\n ]);\n const [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\" &&\n quoted.current !== quoteKey\n ) {\n quoted.current = quoteKey;\n void cart.actions.quote().catch((failure: unknown) => onError?.(failure));\n }\n }, [\n cart.actions,\n cart.isHydrated,\n cart.lines.length,\n cart.state.status,\n quoteKey,\n onError,\n ]);\n const state = checkoutButtonState(\n cart.state,\n cart.fulfilment,\n isContactPhoneValid(contactPhone),\n busy,\n forced,\n );\n const activate = async () => {\n if (state.status === \"sign_in\") {\n onSignIn?.();\n return;\n }\n if (state.status === \"phone\") {\n onContactPhoneRequired?.();\n return;\n }\n if (state.status === \"retry\") {\n try {\n await cart.actions.quote({ force: true });\n } catch (failure) {\n onError?.(failure);\n }\n return;\n }\n if (state.status !== \"pay\") {\n return;\n }\n setBusy(true);\n try {\n const checkoutAttempt = attempt({\n payload: withInstructions(\n cart.actions.toCheckoutPayload(),\n instructions,\n ),\n branchId,\n contactPhone,\n returnUrl: returnUrl ?? `${location.origin}/checkout/return`,\n ...((cancelUrl ?? location.href)\n ? { cancelUrl: cancelUrl ?? location.href }\n : {}),\n attribution: attribution(),\n });\n const session = await checkout.open(\n checkoutAttempt.request,\n checkoutAttempt.key,\n );\n redirect(session.url);\n } catch (failure) {\n if (\n failure instanceof CheckoutError &&\n failure.apiCode === \"customer_required\"\n ) {\n setForced(\"sign_in\");\n onSignIn?.();\n return;\n }\n if (onError) {\n onError(failure);\n return;\n }\n throw failure;\n } finally {\n setBusy(false);\n }\n };\n return (\n <button\n type=\"button\"\n className=\"z-kit-checkout-button\"\n data-state={state.status}\n data-zappie-checkout=\"\"\n disabled={!state.enabled}\n onClick={() => void activate()}\n >\n {state.label}\n </button>\n );\n}\n"},{"path":"checkout-attempt.ts","target":"components/zappie/checkout-attempt.ts","sha256":"cefb578068eac7d86f343d2ae5f7f9e2feda13bced3f05b365b78a71252d1bca","content":"import type { CartModule } from \"@zappie/sdk\";\nimport { checkoutFulfilment, type CheckoutRequest } from \"@zappie/sdk/checkout\";\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\ntype Attribution = { source: string; campaign: string };\nexport type CheckoutAttempt = { key: string; request: CheckoutRequest };\n\nfunction fulfilment(payload: Payload) {\n const value = checkoutFulfilment(payload.fulfilment);\n if (value.type === \"pickup\") {\n return {\n type: \"pickup\" as const,\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n if (value.type === \"delivery\") {\n return {\n type: \"delivery\" as const,\n ...(value.address !== undefined ? { address: value.address } : {}),\n ...(value.delivery_region_id !== undefined\n ? { delivery_region_id: value.delivery_region_id }\n : {}),\n ...(value.instructions !== undefined\n ? { instructions: value.instructions }\n : {}),\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n throw new Error(\"Dine-in checkout is unavailable\");\n}\n\nexport function createCheckoutAttempt() {\n let current: { digest: string; attempt: CheckoutAttempt } | undefined;\n return ({\n payload,\n branchId,\n contactPhone,\n returnUrl,\n cancelUrl,\n attribution,\n }: {\n payload: Payload;\n branchId: string;\n contactPhone: string;\n returnUrl: string;\n cancelUrl?: string;\n attribution: Attribution;\n }) => {\n const normalizedPhone = contactPhone.trim().replace(/\\s+/g, \"\");\n const digest = JSON.stringify({ payload, contactPhone: normalizedPhone });\n if (current?.digest === digest) {\n return current.attempt;\n }\n const request: CheckoutRequest = {\n branch_id: branchId,\n menu_version: payload.menu_version,\n lines: payload.lines,\n fulfilment: fulfilment(payload),\n ...(normalizedPhone ? { contact_phone: normalizedPhone } : {}),\n ...(payload.promo_code ? { promo_code: payload.promo_code } : {}),\n ...(attribution.source ? { source: attribution.source } : {}),\n ...(attribution.campaign ? { campaign: attribution.campaign } : {}),\n return_url: returnUrl,\n ...(cancelUrl ? { cancel_url: cancelUrl } : {}),\n };\n current = { digest, attempt: { key: crypto.randomUUID(), request } };\n return current.attempt;\n };\n}\n"},{"path":"checkout-state.ts","target":"components/zappie/checkout-state.ts","sha256":"e8e2dc8f88d513c124d44516e3d18012d4fd7e2c069e31454d4426dd6c2d4e43","content":"import type { CartFulfilment, CartState } from \"@zappie/sdk\";\nimport {\n payReadiness,\n type PayReadiness,\n type PayReadinessQuoteState,\n} from \"@zappie/sdk/checkout\";\n\nexport type CheckoutButtonStatus = PayReadiness | \"busy\";\nconst labels: Record<CheckoutButtonStatus, string> = {\n address: \"Add a delivery address\",\n quoting: \"Updating total…\",\n retry: \"Update totals to continue\",\n choose_area: \"Choose a delivery area\",\n fix_issues: \"Review your cart\",\n sign_in: \"Sign in to continue\",\n phone: \"Check your phone number\",\n pay: \"Continue to payment\",\n busy: \"Please wait…\",\n};\n\nfunction quoteState(state: CartState): PayReadinessQuoteState {\n if (state.status === \"quoting\") {\n return { status: \"loading\" };\n }\n if (state.status === \"quoted\") {\n return { status: \"ready\", quote: state.quote };\n }\n if (state.status === \"needs_attention\") {\n return state.quote\n ? { status: \"ready\", quote: state.quote }\n : { status: \"error\" };\n }\n return { status: \"idle\" };\n}\n\nexport function checkoutButtonState(\n state: CartState,\n fulfilment: CartFulfilment | null,\n contactPhoneValid: boolean,\n busy: boolean,\n forced?: \"sign_in\",\n) {\n let status: CheckoutButtonStatus;\n if (busy) {\n status = \"busy\";\n } else if (forced) {\n status = forced;\n } else {\n const addressValid =\n fulfilment?.type !== \"delivery\" ||\n Boolean(\n fulfilment.address.line1 ||\n fulfilment.address.placeId ||\n fulfilment.address.coordinates,\n );\n status = payReadiness(quoteState(state), contactPhoneValid, addressValid);\n if (status === \"pay\" && state.status === \"needs_attention\") {\n status = \"fix_issues\";\n }\n }\n return {\n status,\n label: labels[status],\n enabled:\n status === \"pay\" ||\n status === \"sign_in\" ||\n status === \"phone\" ||\n status === \"retry\",\n };\n}\n"},{"path":"checkout-button.css","target":"components/zappie/checkout-button.css","sha256":"9e6bb8afe1bb1e5cd55ed458e435d90b4b3d546fb4276d3fd9432d022844ca22","content":".z-kit-checkout-button {\n width: 100%;\n border: 0;\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-checkout-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-checkout-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-radius-md","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-button"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zappie/kit",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "The registry of copy-in Zappie storefront components, installed with the zappie CLI.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "vite": "8.3.5",
21
21
  "vitest": "^4.1.12",
22
22
  "zod": "^4.6.7",
23
- "@zappie/sdk": "^0.2.0",
24
- "@zappie/foundations": "^0.1.12",
23
+ "@zappie/foundations": "^0.2.0",
24
+ "@zappie/sdk": "^0.2.1",
25
25
  "@zappie/tokens": "^0.1.3"
26
26
  },
27
27
  "license": "MIT",