@fanfare-io/fanfare-sdk-shopify 0.7.0 → 0.7.2

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.
@@ -15,7 +15,7 @@ export interface FanfareCartAdapter<TCart> {
15
15
  createCart(): Promise<TCart>;
16
16
  /** Add one line to the cart AFTER the gate token is written; returns the updated cart. */
17
17
  addLine(cart: TCart, line: GatedLine): Promise<TCart>;
18
- /** Extract the claim-target cart id and the post-add checkout URL. */
18
+ /** Extract the claim-target cart id and checkout URL. `gatedCheckout` needs both before claiming. */
19
19
  readCart(cart: TCart): {
20
20
  id: string;
21
21
  checkoutUrl?: string;
@@ -56,13 +56,14 @@ export declare function gatedLineToRequestedPurchase(line: GatedLine): ShopifyRe
56
56
  * add via `retry()`, never re-claim (that double-reserves).
57
57
  * - `reservation_expired` — the reservation lapsed mid-add; a fresh `gatedCheckout` is required.
58
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).
59
+ * - `checkout_url_missing` — the cart exposed no checkout URL when the claim/redirect needed it (adapter bug).
60
+ * - `checkout_url_mismatch` — the cart checkout URL changed after the claim was signed (adapter bug).
60
61
  * - `busy` — a `gatedCheckout` is already in flight; disable the buy button on `pending`.
61
62
  *
62
63
  * Only `add_line_failed` carries a `retry()`; branch on `retryable` (or the presence
63
64
  * of `retry`), never on `code` alone, to decide whether to retry.
64
65
  */
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
+ 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" | "checkout_url_mismatch" | "busy";
66
67
  /** Which phase the failure came from, for coarse UI/metric bucketing. */
67
68
  export type FanfareCheckoutFailureKind = "create_cart" | "claim" | "add_line" | "busy";
68
69
  /**
@@ -1 +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};
1
+ function t(t,r){const i="Product"===r?"ProductVariant":"Product";if(t.startsWith(`gid://shopify/${i}/`))throw new Error(`Expected a Shopify ${r} id but received a ${i} GID: ${t}`);return/^[1-9]\d*$/.test(t)?`gid://shopify/${r}/${t}`:t}function r(r){return t(r,"Product")}function i(r){return t(r,"ProductVariant")}function n(t){return{productId:t.shopifyProductId,variantId:t.merchandiseId,quantity:t.quantity}}export{n as gatedLineToRequestedPurchase,r as productGid,i as variantGid};
package/dist/claimer.d.ts CHANGED
@@ -17,6 +17,12 @@ export interface ShopifyClaimContext {
17
17
  * that build a claimer directly may instead pass an Ajax cart token incl.
18
18
  * `?key=`; the proxy/cart bridge resolves either form to a cart GID.) */
19
19
  cartId: string;
20
+ /**
21
+ * The Shopify cart checkout URL. When present, the SDK signs the relayed DPoP
22
+ * proof over this URL so the app proxy can ask the admission authority to
23
+ * verify possession for the eventual checkout target.
24
+ */
25
+ checkoutUrl?: string;
20
26
  /**
21
27
  * Optional intended-purchase payload. The cart id names WHERE the gate token is
22
28
  * written; this names WHAT is being bought, so the admission authority can apply
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,...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};
1
+ import{buildAdmissionProofHeaders as t}from"@fanfare-io/fanfare-sdk-core/handoff";function e(e){if(!e.proxyBaseUrl||""===e.proxyBaseUrl.trim())throw new Error("createShopifyCheckoutClaimer: proxyBaseUrl is required");const r=`${e.proxyBaseUrl.replace(/\/+$/,"")}/${(e.endpoint??"admissions/claim").replace(/^\/+/,"")}`,a=e.fetchImpl??globalThis.fetch;return{async claim(){try{const o=e.getClaimContext?await e.getClaimContext():null;if(null==o)return{ok:!1,reason:"no_claim_context"};const n=o.checkoutUrl?.trim(),i=""===n?void 0:n;let c=null;if(void 0!==i)try{c=await t({url:i,method:"POST"})}catch{c=null}const l=await a(r,{method:"POST",headers:{"Content-Type":"application/json",...c??{}},body:JSON.stringify({credential:o.credential,distributionId:o.distributionId,cartId:o.cartId,...void 0===i?{}:{checkoutUrl:i},...void 0===o.requestedPurchase?{}:{requestedPurchase:o.requestedPurchase}}),signal:AbortSignal.timeout(e.timeoutMs??1e4)});if(!l.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}`}(l);return{ok:!1,reason:t}}const s=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}}(l);return null===s?{ok:!1,reason:"claim_malformed"}:{ok:!0,expDate:s}}catch{return{ok:!1,reason:"claim_unreachable"}}}}}export{e as createShopifyCheckoutClaimer};
@@ -1,2 +1,2 @@
1
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};
2
+ import{useState as e,useRef as r,useCallback as t}from"react";import{canonicalizeAdmissionProofUrl as c}from"@fanfare-io/fanfare-sdk-core/handoff";import{useFanfareClaim as a}from"./use-fanfare-claim.js";function n(e){try{return c(e)}catch{return null}}function o(c){const{claim:o}=a(),[i,l]=e(!1),d=r(c);d.current=c;const u=r(null),k=t(e=>{if(null!==u.current){const e={ok:!1,code:"busy",retryable:!0,kind:"busy"};return Promise.resolve(e)}const r=(async()=>{l(!0);try{return await e()}finally{u.current=null,l(!1)}})();return u.current=r,r},[]),s=t(async(e,r,t,c,a,o)=>{const i=d.current;if(function(e){const r=Date.parse(e);return!Number.isNaN(r)&&Date.now()>r}(o))return{ok:!1,code:"reservation_expired",retryable:!1,kind:"add_line",cart:e,expDate:o};let l=e;for(const[n,d]of r.slice(t).entries()){const e=t+n;try{l=await i.addLine(l,d)}catch{const t=l;return{ok:!1,code:"add_line_failed",retryable:!0,kind:"add_line",failedLineIndex:e,cart:t,expDate:o,retry:()=>k(()=>s(t,r,e,c,a,o))}}}const u=i.readCart(l);return u.id!==c?{ok:!1,code:"cart_id_mismatch",retryable:!1,kind:"add_line",cart:l,expDate:o}:void 0===u.checkoutUrl||""===u.checkoutUrl?{ok:!1,code:"checkout_url_missing",retryable:!1,kind:"add_line",cart:l,expDate:o}:n(u.checkoutUrl)!==n(a)?{ok:!1,code:"checkout_url_mismatch",retryable:!1,kind:"add_line",cart:l,expDate:o}:{ok:!0,cart:l,checkoutUrl:u.checkoutUrl,expDate:o}},[k]);return{gatedCheckout:t(e=>{const{gatedLine:r,ungatedLines:t}=e,c=void 0===t?[r]:[r,...t];return k(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 a=e.readCart(t),i=a.id;if(void 0===a.checkoutUrl||""===a.checkoutUrl||!n(a.checkoutUrl))return{ok:!1,code:"checkout_url_missing",retryable:!1,kind:"claim",cart:t};const l=await o({cartId:i,checkoutUrl:a.checkoutUrl,purchase:r});return l.ok?s(t,c,0,i,a.checkoutUrl,l.expDate):"no_claim_context"===(u=l.reason)?{ok:!1,code:"no_claim_context",retryable:!1,kind:"claim"}:"cart_not_materialized"===u?{ok:!1,code:"cart_not_materialized",retryable:!0,kind:"claim"}:{ok:!1,code:"claim_failed",retryable:!0,kind:"claim"};var u})},[o,k,s]),claim:o,pending:i}}export{o as useFanfareCheckout};
@@ -17,10 +17,11 @@ export interface UseFanfareClaimResult {
17
17
  * `no_claim_context` means not-yet-ready). Never rejects — every outcome
18
18
  * resolves to a `ClaimResult`.
19
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.
20
+ * Pass `{ cartId, checkoutUrl, purchase }` to forward the cart checkout URL and
21
+ * product/variant/quantity context to the gate, or a bare `cartId` string for a
22
+ * product-less legacy drop (the body omits the purchase and DPoP checkout URL).
23
+ * `useFanfareCheckout().gatedCheckout` is the recommended path that owns the
24
+ * create→claim→add-line ordering for you.
24
25
  */
25
26
  claim(input: string | FanfareClaimInput): Promise<ClaimResult>;
26
27
  /** `"idle"` → `"claiming"` → `"claimed"` | `"error"`. */
@@ -33,6 +34,7 @@ export interface UseFanfareClaimResult {
33
34
  */
34
35
  export interface FanfareClaimInput {
35
36
  cartId: string;
37
+ checkoutUrl?: string;
36
38
  purchase: GatedLine;
37
39
  }
38
40
  export declare function useFanfareClaim(): UseFanfareClaimResult;
@@ -1,2 +1,2 @@
1
1
  "use client";
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};
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:u,getCheckoutContext:a}=c(),[l,m]=r("idle"),p=t(a);p.current=a;const d=t(void 0),f=t(void 0),v=s.shopify.proxyBaseUrl,y=e(()=>void 0===v||""===v.trim()?null:i({proxyBaseUrl:v,endpoint:"admissions/reserve",getClaimContext:()=>{const r=p.current();if(null===r)return null;const t=d.current,e=f.current;return{...r,...void 0===e?{}:{checkoutUrl:e},...void 0===t?{}:{requestedPurchase:t}}}}),[v]);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),f.current="string"==typeof r?void 0:r.checkoutUrl,u(t),m("claiming");const e=await y.claim();return m(e.ok?"claimed":"error"),e},[y,u]),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.7.0",
3
+ "version": "0.7.2",
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.7.0"
42
+ "@fanfare-io/fanfare-sdk-core": "0.7.2"
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.7.0"
67
+ "@fanfare-io/fanfare-sdk-core": "0.7.2"
68
68
  },
69
69
  "sideEffects": false,
70
70
  "keywords": [