@fanfare-io/fanfare-sdk-shopify 0.5.0 → 0.7.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.
@@ -0,0 +1,23 @@
1
+ import { GatedLine } from './checkout-types';
2
+ /**
3
+ * Integrator-supplied cart I/O for the recommended `gatedCheckout` path. `TCart`
4
+ * is the integrator's own Storefront cart type — the SDK never inspects it except
5
+ * through `readCart`. The adapter is passed to `useFanfareCheckout`, so the SDK
6
+ * owns the create→claim→add-line ordering while the integrator owns the Shopify
7
+ * Storefront calls (cart create / `cartLinesAdd`).
8
+ */
9
+ export interface FanfareCartAdapter<TCart> {
10
+ /**
11
+ * Create or locate an EMPTY cart. Takes NO line — there is deliberately nothing
12
+ * to add the gated product with here, so it cannot precede the claim that writes
13
+ * the gate token.
14
+ */
15
+ createCart(): Promise<TCart>;
16
+ /** Add one line to the cart AFTER the gate token is written; returns the updated cart. */
17
+ addLine(cart: TCart, line: GatedLine): Promise<TCart>;
18
+ /** Extract the claim-target cart id and the post-add checkout URL. */
19
+ readCart(cart: TCart): {
20
+ id: string;
21
+ checkoutUrl?: string;
22
+ };
23
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Public checkout vocabulary shared by the low-level `claim` and the recommended
3
+ * `gatedCheckout`. Isomorphic (no React) so the package root can export it.
4
+ *
5
+ * The two Shopify identifiers a gated line carries are BOTH opaque
6
+ * `gid://shopify/...` strings, so they are branded to make a transposition a
7
+ * compile error rather than a silent admission-authority mismatch:
8
+ * - `shopifyProductId` (a `ProductGID`) is the gate's policy anchor.
9
+ * - `merchandiseId` (a `VariantGID`) is Shopify's Storefront `cartLinesAdd` id.
10
+ */
11
+ declare const gidBrand: unique symbol;
12
+ /** A Shopify Product GID, e.g. `gid://shopify/Product/123`. Construct via `productGid`. */
13
+ export type ProductGID = string & {
14
+ readonly [gidBrand]: "Product";
15
+ };
16
+ /** A Shopify ProductVariant GID, e.g. `gid://shopify/ProductVariant/456`. Construct via `variantGid`. */
17
+ export type VariantGID = string & {
18
+ readonly [gidBrand]: "ProductVariant";
19
+ };
20
+ export declare function productGid(gid: string): ProductGID;
21
+ export declare function variantGid(gid: string): VariantGID;
22
+ /**
23
+ * One purchase intent the gate evaluates. `shopifyProductId` drives the gate's
24
+ * (currently server-deferred) product policy; `merchandiseId` is the variant the
25
+ * integrator's `cartLinesAdd` consumes. Both required — the admission authority
26
+ * keys on the product and has no variant→product derivation path.
27
+ */
28
+ export interface GatedLine {
29
+ shopifyProductId: ProductGID;
30
+ merchandiseId: VariantGID;
31
+ quantity: number;
32
+ }
33
+ /**
34
+ * The intended-purchase payload relayed to the app proxy. INTERNAL wire shape:
35
+ * `merchandiseId` maps to the authority's `variantId`. The fields stay branded so
36
+ * the mapping below cannot transpose them.
37
+ */
38
+ export interface ShopifyRequestedPurchase {
39
+ productId: ProductGID;
40
+ variantId?: VariantGID;
41
+ quantity: number;
42
+ }
43
+ /** Map a `GatedLine` to the authority's requested-purchase wire shape. */
44
+ export declare function gatedLineToRequestedPurchase(line: GatedLine): ShopifyRequestedPurchase;
45
+ /**
46
+ * Closed refusal vocabulary for `gatedCheckout`. The load-bearing distinction is
47
+ * whether a fresh `gatedCheckout` is safe (nothing reserved) or would
48
+ * double-reserve (a slot is already held) — see `kind` + the docs on
49
+ * `FanfareGatedCheckoutResult`.
50
+ *
51
+ * - `cart_create_failed` — `createCart` threw; nothing claimed, safe to restart.
52
+ * - `no_claim_context` — not yet ready (no grant/distribution, or demo mode).
53
+ * - `cart_not_materialized` — the proxy could not find a real cart; safe to restart.
54
+ * - `claim_failed` — the claim was refused/unreachable; nothing reserved, safe to restart.
55
+ * - `add_line_failed` — RESERVED. The token is minted and the slot held; resume the
56
+ * add via `retry()`, never re-claim (that double-reserves).
57
+ * - `reservation_expired` — the reservation lapsed mid-add; a fresh `gatedCheckout` is required.
58
+ * - `cart_id_mismatch` — the cart `addLine` returned is not the one claimed (adapter bug).
59
+ * - `checkout_url_missing` — every line was added but the cart exposed no checkout URL (adapter bug).
60
+ * - `busy` — a `gatedCheckout` is already in flight; disable the buy button on `pending`.
61
+ *
62
+ * Only `add_line_failed` carries a `retry()`; branch on `retryable` (or the presence
63
+ * of `retry`), never on `code` alone, to decide whether to retry.
64
+ */
65
+ export type FanfareCheckoutCode = "cart_create_failed" | "no_claim_context" | "cart_not_materialized" | "claim_failed" | "add_line_failed" | "reservation_expired" | "cart_id_mismatch" | "checkout_url_missing" | "busy";
66
+ /** Which phase the failure came from, for coarse UI/metric bucketing. */
67
+ export type FanfareCheckoutFailureKind = "create_cart" | "claim" | "add_line" | "busy";
68
+ /**
69
+ * The closed result of `gatedCheckout`. Never rejects. On `{ ok: true }` the cart
70
+ * carries the gate token plus every line, and `checkoutUrl` is ready to redirect.
71
+ *
72
+ * On `{ ok: false }`, branch on `code`. The only RESUMABLE failure is
73
+ * `add_line_failed`: the claim already minted the token and reserved the slot, so
74
+ * `retry()` re-runs the remaining `addLine`s on the SAME cart — it never re-claims.
75
+ * `cart` is the half-built cart and `expDate` the reservation expiry, so a caller
76
+ * can decide to `retry()` or, once expired (`reservation_expired`), start fresh.
77
+ */
78
+ export type FanfareGatedCheckoutResult<TCart> = {
79
+ ok: true;
80
+ cart: TCart;
81
+ checkoutUrl: string;
82
+ expDate: string;
83
+ } | {
84
+ ok: false;
85
+ code: FanfareCheckoutCode;
86
+ retryable: boolean;
87
+ kind: FanfareCheckoutFailureKind;
88
+ /** The reservation expiry, when a claim succeeded before the failure. */
89
+ expDate?: string;
90
+ /** The index into `[gatedLine, ...ungatedLines]` whose `addLine` failed. */
91
+ failedLineIndex?: number;
92
+ /** The claimed (possibly half-built) cart, when one exists. */
93
+ cart?: TCart;
94
+ /** Present only on `add_line_failed`: resume the remaining adds on the same cart. */
95
+ retry?: () => Promise<FanfareGatedCheckoutResult<TCart>>;
96
+ };
97
+ export {};
@@ -0,0 +1 @@
1
+ function t(t,r){const n="Product"===r?"ProductVariant":"Product";if(t.startsWith(`gid://shopify/${n}/`))throw new Error(`Expected a Shopify ${r} id but received a ${n} GID: ${t}`);return t}function r(r){return t(r,"Product")}function n(r){return t(r,"ProductVariant")}function i(t){return{productId:t.shopifyProductId,variantId:t.merchandiseId,quantity:t.quantity}}export{i as gatedLineToRequestedPurchase,r as productGid,n as variantGid};
package/dist/claimer.d.ts CHANGED
@@ -1,40 +1,4 @@
1
- /**
2
- * Shopify checkout CLAIMER — the on-grant, NON-CONSUMING sibling of
3
- * `createShopifyCheckoutVerifier`.
4
- *
5
- * The claimer and the verifier relay the same `{ credential, distributionId,
6
- * cartId }` body to the same proxy host, but they are deliberately SEPARATE
7
- * factories — not one factory with an endpoint flag — because their result
8
- * shapes and readiness policies diverge (see below). They differ in WHICH route
9
- * they POST to, and that route is configurable per caller:
10
- *
11
- * - The CLAIMER's factory default is the `admissions/claim` route, which
12
- * validates the credential, mints a short-lived gate token, and writes it to
13
- * the cart metafield (mint-only; reserves and spends nothing). It returns the
14
- * minted token's `expDate` so the storefront can surface a continuity signal
15
- * — so its result carries a body the caller reads. Direct callers (e.g. the
16
- * Liquid theme path) use this default.
17
- * - The React adapter's headless buy action OVERRIDES `endpoint` to
18
- * `admissions/reserve` (the #29 reserve-fold — see `react/use-fanfare-claim`).
19
- * Reserve is a strict superset: it validates + mints + writes the gate token
20
- * AND reserves the slot, so the slot can't be re-drawn mid-checkout. Its body
21
- * is shape-compatible — the claimer reads only `expDate` (reserve's extra
22
- * `reservationId` is ignored here), so `ClaimResult` is unchanged.
23
- * - The VERIFIER fires *at checkout entry* against the `admissions/reserve`
24
- * route as a separate factory and returns a bare gate decision whose body it
25
- * ignores. (Reserve is not exclusively the verifier's route — the reserve-fold
26
- * above points the claimer at it for the headless mint+reserve round-trip.)
27
- *
28
- * The claimer is NOT a `CheckoutVerifier`: different result shape (carries
29
- * `expDate`), different context-readiness policy (a missing context is a benign
30
- * no-op, not a construction error — the claimer fires automatically on every
31
- * grant), and it is invoked directly by the storefront, never threaded through
32
- * the React adapter's `recordGrant` seam.
33
- *
34
- * Fails closed by construction: a missing/`null` context, a non-2xx proxy
35
- * response, a 2xx body without a parseable `expDate`, and any transport throw all
36
- * resolve to a denied `{ ok:false, reason }`. It NEVER rejects.
37
- */
1
+ import { ShopifyRequestedPurchase } from './checkout-types';
38
2
  /** The live context a claim needs. INTERNAL — the React adapter fills it for you
39
3
  * from provider state; you never construct one. Separate from
40
4
  * `ShopifyCheckoutContext` so the claim and verify seams can diverge without
@@ -46,20 +10,30 @@ export interface ShopifyClaimContext {
46
10
  distributionId: string;
47
11
  /**
48
12
  * The cart identifier the gate token is written to. For the React adapter's
49
- * headless path this is the Storefront cart GID you materialized and handed to
50
- * `useFanfareClaim().claim(cartId)`. (Liquid-theme callers that build a claimer
51
- * directly may instead pass an Ajax cart token incl. `?key=`; the proxy/cart
52
- * bridge resolves either form to a cart GID.) */
13
+ * headless path this is the real Storefront cart GID you materialized and
14
+ * handed to `useFanfareClaim().claim(cartId)`. This identifies the cart
15
+ * metafield write target; product/variant/quantity policy must still be
16
+ * represented by the integration's purchase context. (Liquid-theme callers
17
+ * that build a claimer directly may instead pass an Ajax cart token incl.
18
+ * `?key=`; the proxy/cart bridge resolves either form to a cart GID.) */
53
19
  cartId: string;
20
+ /**
21
+ * Optional intended-purchase payload. The cart id names WHERE the gate token is
22
+ * written; this names WHAT is being bought, so the admission authority can apply
23
+ * product/quantity policy. Omitted by the bare `claim(cartId)` form. The server
24
+ * ignores it today (order-limit enforcement is deferred), but the SDK forwards
25
+ * it so the contract is in place.
26
+ */
27
+ requestedPurchase?: ShopifyRequestedPurchase;
54
28
  }
55
29
  /**
56
30
  * A machine-readable refusal code on `{ ok: false }`. The codes split into two
57
31
  * groups for the integrator deciding whether to retry:
58
32
  *
59
33
  * - `cart_not_materialized` — RECOVERABLE. The proxy could not find a real cart
60
- * behind your `cartId`. Materialize the Storefront cart (with the gated line on
61
- * it), then call `claim(cart.id)` again. This is the one reason that maps to a
62
- * concrete retry the integrator owns.
34
+ * behind your `cartId`. Materialize a real Storefront cart, then call
35
+ * `claim(cart.id)` again. This is the one reason that maps to a concrete
36
+ * retry the integrator owns.
63
37
  * - `no_claim_context` — NOT-YET-READY (not a bug in your `cartId`). The provider
64
38
  * has no admission credential / distribution id to claim with: the shopper
65
39
  * hasn't cleared the drop, the journey hasn't routed, or you're in demo mode
@@ -78,9 +52,10 @@ export type ClaimRefusalReason = "cart_not_materialized" | "no_claim_context" |
78
52
  * `reason` is a stable `ClaimRefusalReason` code — see that type for which codes
79
53
  * are retryable (`cart_not_materialized` → re-materialize + re-claim) vs terminal.
80
54
  *
81
- * Note: this result does NOT carry a checkout URL. You already hold
82
- * `cart.checkoutUrl` from the cart you materialized; on `{ ok: true }` redirect
83
- * the shopper there to enter the gated checkout.
55
+ * Note: this result does NOT carry a checkout URL. You own the cart mutation and
56
+ * redirect: on `{ ok: true }`, redirect to the checkout URL for the same cart
57
+ * after your integration has added any gated lines that were waiting for the
58
+ * gate token.
84
59
  */
85
60
  export type ClaimResult = {
86
61
  ok: true;
@@ -126,6 +101,8 @@ export interface ShopifyCheckoutClaimer {
126
101
  * `proxy_409`) so the storefront can materialize a real cart and re-claim — the
127
102
  * claimer does NOT own materialization, which is context-specific (the Ajax cart
128
103
  * bridge for themes vs the Storefront cart GID for headless) and lives in the
129
- * caller that holds that knowledge.
104
+ * caller that holds that knowledge. For headless, the cart reference identifies
105
+ * where to write the gate token; it does not replace the intended product/
106
+ * variant/quantity context needed to validate purchase policy.
130
107
  */
131
108
  export declare function createShopifyCheckoutClaimer(config: ShopifyClaimConfig): ShopifyCheckoutClaimer;
package/dist/claimer.js CHANGED
@@ -1 +1 @@
1
- function t(t){if(!t.proxyBaseUrl||""===t.proxyBaseUrl.trim())throw new Error("createShopifyCheckoutClaimer: proxyBaseUrl is required");const e=`${t.proxyBaseUrl.replace(/\/+$/,"")}/${(t.endpoint??"admissions/claim").replace(/^\/+/,"")}`,r=t.fetchImpl??globalThis.fetch;return{async claim(){try{const a=t.getClaimContext?await t.getClaimContext():null;if(null==a)return{ok:!1,reason:"no_claim_context"};const n=await r(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({credential:a.credential,distributionId:a.distributionId,cartId:a.cartId}),signal:AbortSignal.timeout(t.timeoutMs??1e4)});if(!n.ok){const t=await async function(t){if(409===t.status)try{const e=await t.json();if("cart_not_materialized"===e?.error)return"cart_not_materialized"}catch{}return`proxy_${t.status}`}(n);return{ok:!1,reason:t}}const o=await async function(t){try{const e=await t.json();return"string"==typeof e?.expDate&&e.expDate.length>0?e.expDate:null}catch{return null}}(n);return null===o?{ok:!1,reason:"claim_malformed"}:{ok:!0,expDate:o}}catch{return{ok:!1,reason:"claim_unreachable"}}}}}export{t as createShopifyCheckoutClaimer};
1
+ function t(t){if(!t.proxyBaseUrl||""===t.proxyBaseUrl.trim())throw new Error("createShopifyCheckoutClaimer: proxyBaseUrl is required");const e=`${t.proxyBaseUrl.replace(/\/+$/,"")}/${(t.endpoint??"admissions/claim").replace(/^\/+/,"")}`,r=t.fetchImpl??globalThis.fetch;return{async claim(){try{const a=t.getClaimContext?await t.getClaimContext():null;if(null==a)return{ok:!1,reason:"no_claim_context"};const n=await r(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({credential:a.credential,distributionId:a.distributionId,cartId:a.cartId,...void 0===a.requestedPurchase?{}:{requestedPurchase:a.requestedPurchase}}),signal:AbortSignal.timeout(t.timeoutMs??1e4)});if(!n.ok){const t=await async function(t){if(409===t.status)try{const e=await t.json();if("cart_not_materialized"===e?.error)return"cart_not_materialized"}catch{}return`proxy_${t.status}`}(n);return{ok:!1,reason:t}}const o=await async function(t){try{const e=await t.json();return"string"==typeof e?.expDate&&e.expDate.length>0?e.expDate:null}catch{return null}}(n);return null===o?{ok:!1,reason:"claim_malformed"}:{ok:!0,expDate:o}}catch{return{ok:!1,reason:"claim_unreachable"}}}}}export{t as createShopifyCheckoutClaimer};
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  * public surface here is the `experienceIds` metafield primitives,
13
13
  * `FanfareConfigurationError`, and the framework-free seam vocabulary.
14
14
  */
15
+ export { productGid, variantGid } from './checkout-types';
16
+ export type { FanfareCheckoutCode, FanfareCheckoutFailureKind, FanfareGatedCheckoutResult, GatedLine, ProductGID, VariantGID, } from './checkout-types';
15
17
  export type { ClaimRefusalReason, ClaimResult } from './claimer';
16
18
  export type { ExperienceSource, FanfareMode, ShopifyAdapterConfig, ShopifyProductRef } from './config';
17
19
  export { FanfareConfigurationError } from './errors';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{FanfareConfigurationError as r}from"./errors.js";import{FANFARE_PRODUCT_METAFIELD as o,parseExperienceIds as f,parseFirstExperienceId as e}from"./metafield.js";import{ACCESS_STATUSES as m,snapshotToAccessStatus as t}from"@fanfare-io/fanfare-sdk-core/storefront";export{m as ACCESS_STATUSES,o as FANFARE_PRODUCT_METAFIELD,r as FanfareConfigurationError,f as parseExperienceIds,e as parseFirstExperienceId,t as snapshotToAccessStatus};
1
+ import{productGid as r,variantGid as o}from"./checkout-types.js";import{FanfareConfigurationError as e}from"./errors.js";import{FANFARE_PRODUCT_METAFIELD as f,parseExperienceIds as t,parseFirstExperienceId as m}from"./metafield.js";import{ACCESS_STATUSES as s,snapshotToAccessStatus as i}from"@fanfare-io/fanfare-sdk-core/storefront";export{s as ACCESS_STATUSES,f as FANFARE_PRODUCT_METAFIELD,e as FanfareConfigurationError,t as parseExperienceIds,m as parseFirstExperienceId,r as productGid,i as snapshotToAccessStatus,o as variantGid};
@@ -7,12 +7,16 @@
7
7
  * store facts. `useFanfareExperience` is the Shopify→experience mapping the PDP
8
8
  * calls. `useFanfareWidgetBridge` connects the drop journey widget to the provider
9
9
  * (spread its `onJourneyChange` onto your `<ExperienceWidget>`), so the grant the
10
- * shopper earns reaches the gate + claim. `useFanfareClaim` is the on-buy claim
11
- * that writes the gate token onto a cart you materialized. `<FanfareCheckoutGate>`
12
- * is the drop-in checkout guard, with `useFanfareGrant` exposed for manual control
13
- * of the loading/allowed/blocked states. The internal `useShopifyAdapter` hook they
14
- * build on is intentionally NOT exported.
10
+ * shopper earns reaches the gate + checkout. `useFanfareCheckout` is the recommended
11
+ * on-buy path — its `gatedCheckout` owns the create→claim→add-line ordering — and it
12
+ * also exposes the low-level `claim` (= `useFanfareClaim`) for bring-your-own-cart.
13
+ * `<FanfareCheckoutGate>` is the drop-in checkout guard, with `useFanfareGrant` exposed
14
+ * for manual control of the loading/allowed/blocked states. The internal
15
+ * `useShopifyAdapter` hook they build on is intentionally NOT exported.
15
16
  */
17
+ export type { FanfareCartAdapter } from '../cart-adapter';
18
+ export { productGid, variantGid } from '../checkout-types';
19
+ export type { FanfareCheckoutCode, FanfareCheckoutFailureKind, FanfareGatedCheckoutResult, GatedLine, ProductGID, VariantGID, } from '../checkout-types';
16
20
  export type { ClaimRefusalReason, ClaimResult } from '../claimer';
17
21
  export type { ExperienceSource, FanfareMode, ShopifyAdapterConfig, ShopifyProductRef } from '../config';
18
22
  export { FanfareConfigurationError } from '../errors';
@@ -20,8 +24,10 @@ export { FanfareCheckoutGate } from './fanfare-checkout-gate';
20
24
  export type { FanfareCheckoutGateProps } from './fanfare-checkout-gate';
21
25
  export { FanfareShopifyProvider } from './shopify-provider';
22
26
  export type { FanfareShopifyProviderProps } from './shopify-provider';
27
+ export { useFanfareCheckout } from './use-fanfare-checkout';
28
+ export type { FanfareGatedCheckoutInput, UseFanfareCheckoutResult } from './use-fanfare-checkout';
23
29
  export { useFanfareClaim } from './use-fanfare-claim';
24
- export type { FanfareClaimState, UseFanfareClaimResult } from './use-fanfare-claim';
30
+ export type { FanfareClaimInput, FanfareClaimState, UseFanfareClaimResult } from './use-fanfare-claim';
25
31
  export { useFanfareExperience } from './use-fanfare-experience';
26
32
  export type { UseFanfareExperienceResult } from './use-fanfare-experience';
27
33
  export { useFanfareGrant } from './use-fanfare-grant';
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- import{FanfareConfigurationError as r}from"../errors.js";import{FanfareCheckoutGate as e}from"./fanfare-checkout-gate.js";import{FanfareShopifyProvider as o}from"./shopify-provider.js";import{useFanfareClaim as f}from"./use-fanfare-claim.js";import{useFanfareExperience as m}from"./use-fanfare-experience.js";import{useFanfareGrant as a}from"./use-fanfare-grant.js";import{useFanfareWidgetBridge as i}from"./use-fanfare-widget-bridge.js";export{e as FanfareCheckoutGate,r as FanfareConfigurationError,o as FanfareShopifyProvider,f as useFanfareClaim,m as useFanfareExperience,a as useFanfareGrant,i as useFanfareWidgetBridge};
2
+ import{productGid as r,variantGid as e}from"../checkout-types.js";import{FanfareConfigurationError as o}from"../errors.js";import{FanfareCheckoutGate as f}from"./fanfare-checkout-gate.js";import{FanfareShopifyProvider as m}from"./shopify-provider.js";import{useFanfareCheckout as s}from"./use-fanfare-checkout.js";import{useFanfareClaim as t}from"./use-fanfare-claim.js";import{useFanfareExperience as a}from"./use-fanfare-experience.js";import{useFanfareGrant as i}from"./use-fanfare-grant.js";import{useFanfareWidgetBridge as p}from"./use-fanfare-widget-bridge.js";export{f as FanfareCheckoutGate,o as FanfareConfigurationError,m as FanfareShopifyProvider,r as productGid,s as useFanfareCheckout,t as useFanfareClaim,a as useFanfareExperience,i as useFanfareGrant,p as useFanfareWidgetBridge,e as variantGid};
@@ -0,0 +1,18 @@
1
+ import { FanfareCartAdapter } from '../cart-adapter';
2
+ import { FanfareGatedCheckoutResult, GatedLine } from '../checkout-types';
3
+ import { UseFanfareClaimResult } from './use-fanfare-claim';
4
+ /** What to check out: the one gated line, plus optional convenience lines that ride the same cart. */
5
+ export interface FanfareGatedCheckoutInput {
6
+ gatedLine: GatedLine;
7
+ /** Added to the cart but NOT policy-checked today — the gate covers `gatedLine` only. */
8
+ ungatedLines?: GatedLine[];
9
+ }
10
+ export interface UseFanfareCheckoutResult<TCart> {
11
+ /** Recommended one-call path: create → claim → add line(s) → checkout URL. Never rejects. */
12
+ gatedCheckout(input: FanfareGatedCheckoutInput): Promise<FanfareGatedCheckoutResult<TCart>>;
13
+ /** Advanced escape hatch — the low-level claim (see `useFanfareClaim`). */
14
+ claim: UseFanfareClaimResult["claim"];
15
+ /** True while a `gatedCheckout` run is in flight (drives the buy-button spinner). */
16
+ pending: boolean;
17
+ }
18
+ export declare function useFanfareCheckout<TCart>(adapter: FanfareCartAdapter<TCart>): UseFanfareCheckoutResult<TCart>;
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ import{useState as e,useRef as r,useCallback as t}from"react";import{useFanfareClaim as a}from"./use-fanfare-claim.js";function c(c){const{claim:n}=a(),[i,o]=e(!1),d=r(c);d.current=c;const l=r(null),u=t(e=>{if(null!==l.current){const e={ok:!1,code:"busy",retryable:!0,kind:"busy"};return Promise.resolve(e)}const r=(async()=>{o(!0);try{return await e()}finally{l.current=null,o(!1)}})();return l.current=r,r},[]),s=t(async(e,r,t,a,c)=>{const n=d.current;if(function(e){const r=Date.parse(e);return!Number.isNaN(r)&&Date.now()>r}(c))return{ok:!1,code:"reservation_expired",retryable:!1,kind:"add_line",cart:e,expDate:c};let i=e;for(const[d,l]of r.slice(t).entries()){const e=t+d;try{i=await n.addLine(i,l)}catch{const t=i;return{ok:!1,code:"add_line_failed",retryable:!0,kind:"add_line",failedLineIndex:e,cart:t,expDate:c,retry:()=>u(()=>s(t,r,e,a,c))}}}const o=n.readCart(i);return o.id!==a?{ok:!1,code:"cart_id_mismatch",retryable:!1,kind:"add_line",cart:i,expDate:c}:void 0===o.checkoutUrl||""===o.checkoutUrl?{ok:!1,code:"checkout_url_missing",retryable:!1,kind:"add_line",cart:i,expDate:c}:{ok:!0,cart:i,checkoutUrl:o.checkoutUrl,expDate:c}},[u]);return{gatedCheckout:t(e=>{const{gatedLine:r,ungatedLines:t}=e,a=void 0===t?[r]:[r,...t];return u(async()=>{const e=d.current;let t;try{t=await e.createCart()}catch{return{ok:!1,code:"cart_create_failed",retryable:!0,kind:"create_cart"}}const c=e.readCart(t).id,i=await n({cartId:c,purchase:r});return i.ok?s(t,a,0,c,i.expDate):"no_claim_context"===(o=i.reason)?{ok:!1,code:"no_claim_context",retryable:!1,kind:"claim"}:"cart_not_materialized"===o?{ok:!1,code:"cart_not_materialized",retryable:!0,kind:"claim"}:{ok:!1,code:"claim_failed",retryable:!0,kind:"claim"};var o})},[n,u,s]),claim:n,pending:i}}export{c as useFanfareCheckout};
@@ -1,21 +1,38 @@
1
+ import { GatedLine } from '../checkout-types';
1
2
  import { ClaimResult } from '../claimer';
2
3
  /** The claim lifecycle, for driving buy-button UI (e.g. a spinner). */
3
4
  export type FanfareClaimState = "idle" | "claiming" | "claimed" | "error";
4
5
  export interface UseFanfareClaimResult {
5
6
  /**
6
- * Claim the spot for a cart you already materialized. In one post-cart
7
- * round-trip this validates the admission, mints + writes the gate token to the
8
- * cart, AND reserves the slot (so it can't be re-drawn mid-checkout). `cartId`
9
- * is your Storefront cart's GID (the cart carrying the gated line). On
10
- * `{ ok: true }`, redirect the shopper to that cart's `checkoutUrl`; `expDate`
11
- * is the token's expiry (the reserve route's `reservationId` is handled
12
- * internally and not surfaced). On `{ ok: false }`, branch on `result.reason`
13
- * (a `ClaimRefusalReason` — `cart_not_materialized` is retryable;
7
+ * Claim the spot for a cart you already materialized. In one round-trip this
8
+ * validates the admission, mints + writes the gate token to the cart, AND
9
+ * reserves the slot (so it can't be re-drawn mid-checkout). `cartId` is your
10
+ * Storefront cart's GID: it identifies where the gate token is written, but it
11
+ * does not by itself validate product/variant/quantity policy. On `{ ok: true
12
+ * }`, add the gated line to that same cart if it is not already present, then
13
+ * redirect the shopper to that cart's `checkoutUrl`; `expDate` is the token's
14
+ * expiry (the reserve route's `reservationId` is handled internally and not
15
+ * surfaced). On `{ ok: false }`, branch on `result.reason` (a
16
+ * `ClaimRefusalReason` — `cart_not_materialized` is retryable;
14
17
  * `no_claim_context` means not-yet-ready). Never rejects — every outcome
15
18
  * resolves to a `ClaimResult`.
19
+ *
20
+ * Pass `{ cartId, purchase }` to forward product/variant/quantity context to the
21
+ * gate, or a bare `cartId` string for a product-less drop (the body omits the
22
+ * purchase). `useFanfareCheckout().gatedCheckout` is the recommended path that
23
+ * owns the create→claim→add-line ordering for you.
16
24
  */
17
- claim(cartId: string): Promise<ClaimResult>;
25
+ claim(input: string | FanfareClaimInput): Promise<ClaimResult>;
18
26
  /** `"idle"` → `"claiming"` → `"claimed"` | `"error"`. */
19
27
  state: FanfareClaimState;
20
28
  }
29
+ /**
30
+ * The object form of `claim`. `purchase` is REQUIRED here so the gate receives the
31
+ * intended `GatedLine`; for a genuinely product-less drop use the bare
32
+ * `claim(cartId: string)` form instead.
33
+ */
34
+ export interface FanfareClaimInput {
35
+ cartId: string;
36
+ purchase: GatedLine;
37
+ }
21
38
  export declare function useFanfareClaim(): UseFanfareClaimResult;
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- import{useState as r,useRef as t,useMemo as o,useCallback as e}from"react";import{createShopifyCheckoutClaimer as i}from"../claimer.js";import{useShopifyAdapter as n}from"./shopify-provider.js";function c(){const{config:c,setActiveCartId:a,getCheckoutContext:s}=n(),[m,l]=r("idle"),p=t(s);p.current=s;const u=c.shopify.proxyBaseUrl,f=o(()=>void 0===u||""===u.trim()?null:i({proxyBaseUrl:u,endpoint:"admissions/reserve",getClaimContext:()=>p.current()}),[u]);return{claim:e(async r=>{if(null===f)return l("error"),{ok:!1,reason:"no_claim_context"};a(r),l("claiming");const t=await f.claim();return l(t.ok?"claimed":"error"),t},[f,a]),state:m}}export{c as useFanfareClaim};
2
+ import{useState as r,useRef as t,useMemo as e,useCallback as o}from"react";import{gatedLineToRequestedPurchase as n}from"../checkout-types.js";import{createShopifyCheckoutClaimer as i}from"../claimer.js";import{useShopifyAdapter as c}from"./shopify-provider.js";function s(){const{config:s,setActiveCartId:a,getCheckoutContext:u}=c(),[l,m]=r("idle"),p=t(u);p.current=u;const d=t(void 0),f=s.shopify.proxyBaseUrl,y=e(()=>void 0===f||""===f.trim()?null:i({proxyBaseUrl:f,endpoint:"admissions/reserve",getClaimContext:()=>{const r=p.current();if(null===r)return null;const t=d.current;return void 0===t?r:{...r,requestedPurchase:t}}}),[f]);return{claim:o(async r=>{if(null===y)return m("error"),{ok:!1,reason:"no_claim_context"};const t="string"==typeof r?r:r.cartId;d.current="string"==typeof r?void 0:n(r.purchase),a(t),m("claiming");const e=await y.claim();return m(e.ok?"claimed":"error"),e},[y,a]),state:l}}export{s as useFanfareClaim};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanfare-io/fanfare-sdk-shopify",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Shopify storefront adapter for Fanfare SDK: product→experience resolver, checkout verifier, and experienceIds metafield primitives",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -39,7 +39,7 @@
39
39
  "peerDependencies": {
40
40
  "react": "^18.0.0 || ^19.1.1",
41
41
  "react-dom": "^18.0.0 || ^19.1.1",
42
- "@fanfare-io/fanfare-sdk-core": "0.5.0"
42
+ "@fanfare-io/fanfare-sdk-core": "0.7.0"
43
43
  },
44
44
  "peerDependenciesMeta": {
45
45
  "react": {
@@ -64,7 +64,7 @@
64
64
  "vite-plugin-dts": "^4.5.4",
65
65
  "vite-tsconfig-paths": "^5.1.4",
66
66
  "vitest": "^3.2.4",
67
- "@fanfare-io/fanfare-sdk-core": "0.5.0"
67
+ "@fanfare-io/fanfare-sdk-core": "0.7.0"
68
68
  },
69
69
  "sideEffects": false,
70
70
  "keywords": [