@amboras-dev/meta-shops-checkout 0.2.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.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @amboras-dev/meta-shops-checkout
2
+
3
+ Storefront half of the **Facebook & Instagram Shop** plugin (`facebook-instagram-shop`).
4
+
5
+ When a shopper checks out from a Meta Shop configured with **"Checkout on another
6
+ website"**, Meta sends them to a deep-link on the merchant's storefront:
7
+
8
+ ```
9
+ https://store.example.com/checkout?products=variant_x:4,variant_y:2&coupon=SAVE10&cart_origin=meta_shops
10
+ ```
11
+
12
+ This package exports a single `rootProviders` slot component,
13
+ `MetaShopsCheckoutProvider`, that:
14
+
15
+ 1. Detects the deep-link (only on the checkout path, only when
16
+ `cart_origin=meta_shops`).
17
+ 2. Builds a **fresh cart** with exactly the items Meta sent (`variantId:qty`).
18
+ The retailer_id Meta returns is the Medusa variant id, so it maps straight to
19
+ `createLineItem`.
20
+ 3. Applies the `coupon` as a cart promotion (a bad coupon is skipped, not fatal).
21
+ 4. Makes it the storefront's active cart (`localStorage.medusa_cart_id` +
22
+ invalidates the shared `['cart']` query) and strips the URL params.
23
+
24
+ It is **idempotency-guarded** (a refresh won't re-add items) and fully inert on
25
+ every other page/URL.
26
+
27
+ ### What it does NOT do
28
+
29
+ Payment-session initialization is left to the storefront's `useCheckout`, which
30
+ auto-initializes it once the shopper provides a shipping address + method —
31
+ Medusa ties the payment amount to shipping, and Meta does not send an address,
32
+ so payment cannot (and should not) be pre-initialized.
33
+
34
+ ### Installation
35
+
36
+ Installed automatically by the Amboras plugin pipeline as part of the
37
+ `facebook-instagram-shop` plugin (it is the manifest's `npm_package`). It is a
38
+ Class A integration — declarative `rootProviders` binding, no storefront source
39
+ edits.
@@ -0,0 +1,79 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ interface MetaShopsCheckoutProviderProps {
4
+ /**
5
+ * Medusa backend base URL. Defaults to `NEXT_PUBLIC_MEDUSA_BACKEND_URL`
6
+ * (falling back to `http://localhost:9000`). The `/store/*` API resolves the
7
+ * tenant from the publishable key, so no store-environment header is needed.
8
+ */
9
+ baseUrl?: string;
10
+ /**
11
+ * Publishable API key sent as `x-publishable-api-key`. Defaults to
12
+ * `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY`. Routes the `/store/*` calls to the
13
+ * correct tenant.
14
+ */
15
+ publishableApiKey?: string;
16
+ /**
17
+ * The `cart_origin` query value that marks a Meta Shops deep-link. The
18
+ * provider stays completely inert unless this exact value is present, so it
19
+ * is safe to mount on every page. Defaults to `'meta_shops'`.
20
+ */
21
+ cartOrigin?: string;
22
+ /**
23
+ * Path the deep-link lands on. The provider only seeds when the current
24
+ * pathname matches this (so a stray `?cart_origin=meta_shops` elsewhere does
25
+ * nothing). Defaults to `'/checkout'`.
26
+ */
27
+ checkoutPath?: string;
28
+ /** Master switch. When false the provider renders null and does nothing. */
29
+ enabled?: boolean;
30
+ }
31
+ /** A single `variantId:quantity` pair parsed out of the `products` param. */
32
+ interface ParsedItem {
33
+ variant_id: string;
34
+ quantity: number;
35
+ }
36
+
37
+ /**
38
+ * Null-rendering side-effect component for the `rootProviders` slot.
39
+ *
40
+ * When a Meta Shops "checkout on another website" deep-link lands on the
41
+ * checkout page:
42
+ *
43
+ * /checkout?products=variant_x:4,variant_y:2&coupon=SAVE10&cart_origin=meta_shops
44
+ *
45
+ * it builds a FRESH cart containing exactly the items Meta sent, applies the
46
+ * coupon, makes that cart the storefront's active cart, and strips the params.
47
+ * Payment is intentionally NOT initialized here — the storefront's useCheckout
48
+ * auto-initializes the payment session once the shopper provides shipping
49
+ * (Medusa ties the pay amount to shipping, which Meta does not send).
50
+ *
51
+ * Why a fresh cart: the buyer agreed to a specific set of items on Meta, so we
52
+ * never merge into a stale browse-cart. Writing our cart id to localStorage
53
+ * overrides any empty cart the header's useCart may have just auto-created; the
54
+ * orphaned empty cart is abandoned harmlessly.
55
+ *
56
+ * Safe to mount globally: it is fully inert unless pathname === checkoutPath
57
+ * AND cart_origin === cartOrigin AND a non-empty products param is present.
58
+ */
59
+ declare function MetaShopsCheckoutProvider({ baseUrl, publishableApiKey, cartOrigin, checkoutPath, enabled, }: MetaShopsCheckoutProviderProps): ReactElement | null;
60
+
61
+ /**
62
+ * Parse Meta's `products` deep-link param into line items.
63
+ *
64
+ * Format (already percent-decoded by URLSearchParams):
65
+ * `variant_123:4,variant_456:2`
66
+ *
67
+ * The retailer_id Meta sends back is the Medusa variant id (the Meta Shop sync
68
+ * mapper sets each catalog item's `id` to `variant.id`), so these map straight
69
+ * to `createLineItem({ variant_id })` — no translation needed.
70
+ *
71
+ * Defensive rules:
72
+ * - missing/!positive qty → 1
73
+ * - a bare `variant_x` (no `:qty`) → qty 1
74
+ * - duplicate variants are merged (quantities summed)
75
+ * - blank/garbage segments are skipped, never thrown
76
+ */
77
+ declare function parseProducts(raw: string): ParsedItem[];
78
+
79
+ export { MetaShopsCheckoutProvider, type MetaShopsCheckoutProviderProps, type ParsedItem, parseProducts };
@@ -0,0 +1,79 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ interface MetaShopsCheckoutProviderProps {
4
+ /**
5
+ * Medusa backend base URL. Defaults to `NEXT_PUBLIC_MEDUSA_BACKEND_URL`
6
+ * (falling back to `http://localhost:9000`). The `/store/*` API resolves the
7
+ * tenant from the publishable key, so no store-environment header is needed.
8
+ */
9
+ baseUrl?: string;
10
+ /**
11
+ * Publishable API key sent as `x-publishable-api-key`. Defaults to
12
+ * `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY`. Routes the `/store/*` calls to the
13
+ * correct tenant.
14
+ */
15
+ publishableApiKey?: string;
16
+ /**
17
+ * The `cart_origin` query value that marks a Meta Shops deep-link. The
18
+ * provider stays completely inert unless this exact value is present, so it
19
+ * is safe to mount on every page. Defaults to `'meta_shops'`.
20
+ */
21
+ cartOrigin?: string;
22
+ /**
23
+ * Path the deep-link lands on. The provider only seeds when the current
24
+ * pathname matches this (so a stray `?cart_origin=meta_shops` elsewhere does
25
+ * nothing). Defaults to `'/checkout'`.
26
+ */
27
+ checkoutPath?: string;
28
+ /** Master switch. When false the provider renders null and does nothing. */
29
+ enabled?: boolean;
30
+ }
31
+ /** A single `variantId:quantity` pair parsed out of the `products` param. */
32
+ interface ParsedItem {
33
+ variant_id: string;
34
+ quantity: number;
35
+ }
36
+
37
+ /**
38
+ * Null-rendering side-effect component for the `rootProviders` slot.
39
+ *
40
+ * When a Meta Shops "checkout on another website" deep-link lands on the
41
+ * checkout page:
42
+ *
43
+ * /checkout?products=variant_x:4,variant_y:2&coupon=SAVE10&cart_origin=meta_shops
44
+ *
45
+ * it builds a FRESH cart containing exactly the items Meta sent, applies the
46
+ * coupon, makes that cart the storefront's active cart, and strips the params.
47
+ * Payment is intentionally NOT initialized here — the storefront's useCheckout
48
+ * auto-initializes the payment session once the shopper provides shipping
49
+ * (Medusa ties the pay amount to shipping, which Meta does not send).
50
+ *
51
+ * Why a fresh cart: the buyer agreed to a specific set of items on Meta, so we
52
+ * never merge into a stale browse-cart. Writing our cart id to localStorage
53
+ * overrides any empty cart the header's useCart may have just auto-created; the
54
+ * orphaned empty cart is abandoned harmlessly.
55
+ *
56
+ * Safe to mount globally: it is fully inert unless pathname === checkoutPath
57
+ * AND cart_origin === cartOrigin AND a non-empty products param is present.
58
+ */
59
+ declare function MetaShopsCheckoutProvider({ baseUrl, publishableApiKey, cartOrigin, checkoutPath, enabled, }: MetaShopsCheckoutProviderProps): ReactElement | null;
60
+
61
+ /**
62
+ * Parse Meta's `products` deep-link param into line items.
63
+ *
64
+ * Format (already percent-decoded by URLSearchParams):
65
+ * `variant_123:4,variant_456:2`
66
+ *
67
+ * The retailer_id Meta sends back is the Medusa variant id (the Meta Shop sync
68
+ * mapper sets each catalog item's `id` to `variant.id`), so these map straight
69
+ * to `createLineItem({ variant_id })` — no translation needed.
70
+ *
71
+ * Defensive rules:
72
+ * - missing/!positive qty → 1
73
+ * - a bare `variant_x` (no `:qty`) → qty 1
74
+ * - duplicate variants are merged (quantities summed)
75
+ * - blank/garbage segments are skipped, never thrown
76
+ */
77
+ declare function parseProducts(raw: string): ParsedItem[];
78
+
79
+ export { MetaShopsCheckoutProvider, type MetaShopsCheckoutProviderProps, type ParsedItem, parseProducts };
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ MetaShopsCheckoutProvider: () => MetaShopsCheckoutProvider,
25
+ parseProducts: () => parseProducts
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/provider.tsx
30
+ var import_react = require("react");
31
+ var import_react_query = require("@tanstack/react-query");
32
+
33
+ // src/lib/fetch.ts
34
+ function resolveEnv(key) {
35
+ if (key === "NEXT_PUBLIC_MEDUSA_BACKEND_URL") return process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL;
36
+ if (key === "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY") return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY;
37
+ return void 0;
38
+ }
39
+ async function storeFetch(path, options = {}) {
40
+ const {
41
+ baseUrl = resolveEnv("NEXT_PUBLIC_MEDUSA_BACKEND_URL") ?? "http://localhost:9000",
42
+ publishableApiKey = resolveEnv("NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY"),
43
+ method = "GET",
44
+ body
45
+ } = options;
46
+ const headers = { Accept: "application/json" };
47
+ if (publishableApiKey) headers["x-publishable-api-key"] = publishableApiKey;
48
+ if (body !== void 0) headers["Content-Type"] = "application/json";
49
+ const res = await fetch(`${baseUrl}${path}`, {
50
+ method,
51
+ headers,
52
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
53
+ });
54
+ if (!res.ok) {
55
+ const err = await res.json().catch(() => ({}));
56
+ throw new Error(err.message ?? err.error ?? `Store request failed (${res.status})`);
57
+ }
58
+ return res.json();
59
+ }
60
+
61
+ // src/lib/parse.ts
62
+ function parseProducts(raw) {
63
+ const merged = /* @__PURE__ */ new Map();
64
+ for (const segment of raw.split(",")) {
65
+ const pair = segment.trim();
66
+ if (!pair) continue;
67
+ const sep = pair.lastIndexOf(":");
68
+ const rawId = sep === -1 ? pair : pair.slice(0, sep);
69
+ const rawQty = sep === -1 ? "" : pair.slice(sep + 1);
70
+ let variantId = rawId.trim();
71
+ try {
72
+ variantId = decodeURIComponent(variantId).trim();
73
+ } catch {
74
+ }
75
+ if (!variantId) continue;
76
+ const parsed = parseInt(rawQty, 10);
77
+ const quantity = Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
78
+ merged.set(variantId, (merged.get(variantId) ?? 0) + quantity);
79
+ }
80
+ return Array.from(merged, ([variant_id, quantity]) => ({ variant_id, quantity }));
81
+ }
82
+
83
+ // src/provider.tsx
84
+ var CART_ID_KEY = "medusa_cart_id";
85
+ var CART_QUERY_KEY = ["cart"];
86
+ var SEEDED_PREFIX = "meta_shops_seeded:";
87
+ function MetaShopsCheckoutProvider({
88
+ baseUrl,
89
+ publishableApiKey,
90
+ cartOrigin = "meta_shops",
91
+ checkoutPath = "/checkout",
92
+ enabled = true
93
+ }) {
94
+ const queryClient = (0, import_react_query.useQueryClient)();
95
+ const ranRef = (0, import_react.useRef)(false);
96
+ (0, import_react.useEffect)(() => {
97
+ if (!enabled || typeof window === "undefined" || ranRef.current) return;
98
+ let params;
99
+ try {
100
+ params = new URLSearchParams(window.location.search);
101
+ } catch {
102
+ return;
103
+ }
104
+ const norm = (p) => p.replace(/\/+$/, "");
105
+ if (norm(window.location.pathname) !== norm(checkoutPath)) return;
106
+ if (params.get("cart_origin") !== cartOrigin) return;
107
+ const productsRaw = params.get("products");
108
+ if (!productsRaw) return;
109
+ const items = parseProducts(productsRaw);
110
+ if (items.length === 0) return;
111
+ const couponRaw = params.get("coupon");
112
+ const coupon = couponRaw && couponRaw.trim() ? couponRaw.trim().toUpperCase() : null;
113
+ const cleanUrl = () => {
114
+ try {
115
+ window.history.replaceState(window.history.state, "", checkoutPath);
116
+ } catch {
117
+ }
118
+ };
119
+ const seedKey = `${SEEDED_PREFIX}${productsRaw}|${coupon ?? ""}`;
120
+ try {
121
+ if (sessionStorage.getItem(seedKey)) {
122
+ cleanUrl();
123
+ return;
124
+ }
125
+ } catch {
126
+ }
127
+ ranRef.current = true;
128
+ const opts = { baseUrl, publishableApiKey };
129
+ const seed = async () => {
130
+ try {
131
+ const { regions } = await storeFetch(
132
+ "/store/regions",
133
+ opts
134
+ );
135
+ const regionId = regions?.[0]?.id;
136
+ if (!regionId) throw new Error("no region available");
137
+ const { cart: created } = await storeFetch("/store/carts", {
138
+ ...opts,
139
+ method: "POST",
140
+ body: { region_id: regionId }
141
+ });
142
+ const cartId = created.id;
143
+ let latest = created;
144
+ for (const item of items) {
145
+ const { cart } = await storeFetch(
146
+ `/store/carts/${cartId}/line-items`,
147
+ { ...opts, method: "POST", body: { variant_id: item.variant_id, quantity: item.quantity } }
148
+ );
149
+ latest = cart;
150
+ }
151
+ if (coupon) {
152
+ try {
153
+ const { cart } = await storeFetch(`/store/carts/${cartId}`, {
154
+ ...opts,
155
+ method: "POST",
156
+ body: { promo_codes: [coupon] }
157
+ });
158
+ latest = cart;
159
+ } catch (e) {
160
+ console.warn(`[meta-shops-checkout] could not apply coupon "${coupon}":`, e);
161
+ }
162
+ }
163
+ try {
164
+ localStorage.setItem(CART_ID_KEY, cartId);
165
+ } catch {
166
+ }
167
+ try {
168
+ sessionStorage.setItem(seedKey, "1");
169
+ } catch {
170
+ }
171
+ queryClient.setQueryData(CART_QUERY_KEY, latest);
172
+ void queryClient.invalidateQueries({ queryKey: CART_QUERY_KEY });
173
+ cleanUrl();
174
+ } catch (e) {
175
+ console.error("[meta-shops-checkout] failed to seed cart from Meta Shops deep-link:", e);
176
+ ranRef.current = false;
177
+ }
178
+ };
179
+ void seed();
180
+ }, [enabled, baseUrl, publishableApiKey, cartOrigin, checkoutPath]);
181
+ return null;
182
+ }
183
+ // Annotate the CommonJS export names for ESM import in node:
184
+ 0 && (module.exports = {
185
+ MetaShopsCheckoutProvider,
186
+ parseProducts
187
+ });
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/lib/fetch.ts","../src/lib/parse.ts"],"sourcesContent":["// Provider (client component — rootProviders slot): seeds a fresh cart from a\n// Meta Shops \"checkout on another website\" deep-link, then hands off to the\n// storefront's checkout flow.\nexport { MetaShopsCheckoutProvider } from './provider'\n\n// Public types\nexport type { MetaShopsCheckoutProviderProps, ParsedItem } from './types'\n\n// Exposed for reuse / testing.\nexport { parseProducts } from './lib/parse'\n","'use client'\n\nimport { useEffect, useRef, type ReactElement } from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { storeFetch } from './lib/fetch'\nimport { parseProducts } from './lib/parse'\nimport type { MetaShopsCheckoutProviderProps } from './types'\n\n// Same key + query the storefront's useCart owns, so the seeded cart becomes\n// THE active cart and the checkout page renders it.\nconst CART_ID_KEY = 'medusa_cart_id'\nconst CART_QUERY_KEY = ['cart'] as const\nconst SEEDED_PREFIX = 'meta_shops_seeded:'\n\n/**\n * Null-rendering side-effect component for the `rootProviders` slot.\n *\n * When a Meta Shops \"checkout on another website\" deep-link lands on the\n * checkout page:\n *\n * /checkout?products=variant_x:4,variant_y:2&coupon=SAVE10&cart_origin=meta_shops\n *\n * it builds a FRESH cart containing exactly the items Meta sent, applies the\n * coupon, makes that cart the storefront's active cart, and strips the params.\n * Payment is intentionally NOT initialized here — the storefront's useCheckout\n * auto-initializes the payment session once the shopper provides shipping\n * (Medusa ties the pay amount to shipping, which Meta does not send).\n *\n * Why a fresh cart: the buyer agreed to a specific set of items on Meta, so we\n * never merge into a stale browse-cart. Writing our cart id to localStorage\n * overrides any empty cart the header's useCart may have just auto-created; the\n * orphaned empty cart is abandoned harmlessly.\n *\n * Safe to mount globally: it is fully inert unless pathname === checkoutPath\n * AND cart_origin === cartOrigin AND a non-empty products param is present.\n */\nexport function MetaShopsCheckoutProvider({\n baseUrl,\n publishableApiKey,\n cartOrigin = 'meta_shops',\n checkoutPath = '/checkout',\n enabled = true,\n}: MetaShopsCheckoutProviderProps): ReactElement | null {\n const queryClient = useQueryClient()\n // Guards a single mount (incl. React StrictMode's double-invoke in dev).\n const ranRef = useRef(false)\n\n useEffect(() => {\n if (!enabled || typeof window === 'undefined' || ranRef.current) return\n\n let params: URLSearchParams\n try {\n params = new URLSearchParams(window.location.search)\n } catch {\n return\n }\n\n const norm = (p: string) => p.replace(/\\/+$/, '')\n if (norm(window.location.pathname) !== norm(checkoutPath)) return\n if (params.get('cart_origin') !== cartOrigin) return\n\n const productsRaw = params.get('products')\n if (!productsRaw) return\n\n const items = parseProducts(productsRaw)\n if (items.length === 0) return\n\n const couponRaw = params.get('coupon')\n const coupon = couponRaw && couponRaw.trim() ? couponRaw.trim().toUpperCase() : null\n\n const cleanUrl = () => {\n try {\n window.history.replaceState(window.history.state, '', checkoutPath)\n } catch {\n /* ignore */\n }\n }\n\n // Idempotency: never re-seed the same deep-link (refresh, back-nav, re-mount).\n const seedKey = `${SEEDED_PREFIX}${productsRaw}|${coupon ?? ''}`\n try {\n if (sessionStorage.getItem(seedKey)) {\n cleanUrl()\n return\n }\n } catch {\n /* sessionStorage blocked → ranRef + cleanUrl still prevent most repeats */\n }\n\n ranRef.current = true\n const opts = { baseUrl, publishableApiKey }\n const seed = async () => {\n try {\n // 1. Region — mirror the storefront's useCart (first region).\n const { regions } = await storeFetch<{ regions: Array<{ id: string }> }>(\n '/store/regions',\n opts,\n )\n const regionId = regions?.[0]?.id\n if (!regionId) throw new Error('no region available')\n\n // 2. Fresh cart for this Meta order.\n const { cart: created } = await storeFetch<{ cart: { id: string } }>('/store/carts', {\n ...opts,\n method: 'POST',\n body: { region_id: regionId },\n })\n const cartId = created.id\n\n // 3. Line items — sequential to keep the cart's optimistic version sane.\n let latest: unknown = created\n for (const item of items) {\n const { cart } = await storeFetch<{ cart: unknown }>(\n `/store/carts/${cartId}/line-items`,\n { ...opts, method: 'POST', body: { variant_id: item.variant_id, quantity: item.quantity } },\n )\n latest = cart\n }\n\n // 4. Discount — an invalid/expired coupon must NOT abort the cart.\n if (coupon) {\n try {\n const { cart } = await storeFetch<{ cart: unknown }>(`/store/carts/${cartId}`, {\n ...opts,\n method: 'POST',\n body: { promo_codes: [coupon] },\n })\n latest = cart\n } catch (e) {\n console.warn(`[meta-shops-checkout] could not apply coupon \"${coupon}\":`, e)\n }\n }\n\n // 5. Activate: make this the storefront's cart and reveal it on checkout.\n try {\n localStorage.setItem(CART_ID_KEY, cartId)\n } catch {\n /* ignore */\n }\n try {\n sessionStorage.setItem(seedKey, '1')\n } catch {\n /* ignore */\n }\n queryClient.setQueryData(CART_QUERY_KEY, latest)\n // Force a refetch so useCart re-reads localStorage (our cart) and wins\n // any race with a concurrently auto-created empty cart.\n void queryClient.invalidateQueries({ queryKey: CART_QUERY_KEY })\n\n cleanUrl()\n } catch (e) {\n // Non-fatal: leave the shopper on /checkout with whatever cart exists.\n console.error('[meta-shops-checkout] failed to seed cart from Meta Shops deep-link:', e)\n ranRef.current = false // allow a retry if the component re-mounts\n }\n }\n\n void seed()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, baseUrl, publishableApiKey, cartOrigin, checkoutPath])\n\n return null\n}\n","'use client'\n\nfunction resolveEnv(key: string): string | undefined {\n if (key === 'NEXT_PUBLIC_MEDUSA_BACKEND_URL') return process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL\n if (key === 'NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY') return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY\n return undefined\n}\n\nexport interface StoreFetchOptions {\n baseUrl?: string\n publishableApiKey?: string\n method?: string\n body?: unknown\n}\n\n/**\n * Minimal `/store/*` fetch helper, mirroring the storefront's medusa-client\n * (publishable key in the header, JSON in/out). Intentionally raw fetch rather\n * than the JS SDK: this package must NOT bundle a second `@medusajs/js-sdk`\n * copy, and the three cart endpoints we touch are stable.\n */\nexport async function storeFetch<T>(path: string, options: StoreFetchOptions = {}): Promise<T> {\n const {\n baseUrl = resolveEnv('NEXT_PUBLIC_MEDUSA_BACKEND_URL') ?? 'http://localhost:9000',\n publishableApiKey = resolveEnv('NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY'),\n method = 'GET',\n body,\n } = options\n\n const headers: Record<string, string> = { Accept: 'application/json' }\n if (publishableApiKey) headers['x-publishable-api-key'] = publishableApiKey\n if (body !== undefined) headers['Content-Type'] = 'application/json'\n\n const res = await fetch(`${baseUrl}${path}`, {\n method,\n headers,\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n })\n\n if (!res.ok) {\n const err = (await res.json().catch(() => ({}))) as { message?: string; error?: string }\n throw new Error(err.message ?? err.error ?? `Store request failed (${res.status})`)\n }\n\n return res.json() as Promise<T>\n}\n","import type { ParsedItem } from '../types'\n\n/**\n * Parse Meta's `products` deep-link param into line items.\n *\n * Format (already percent-decoded by URLSearchParams):\n * `variant_123:4,variant_456:2`\n *\n * The retailer_id Meta sends back is the Medusa variant id (the Meta Shop sync\n * mapper sets each catalog item's `id` to `variant.id`), so these map straight\n * to `createLineItem({ variant_id })` — no translation needed.\n *\n * Defensive rules:\n * - missing/!positive qty → 1\n * - a bare `variant_x` (no `:qty`) → qty 1\n * - duplicate variants are merged (quantities summed)\n * - blank/garbage segments are skipped, never thrown\n */\nexport function parseProducts(raw: string): ParsedItem[] {\n const merged = new Map<string, number>()\n\n for (const segment of raw.split(',')) {\n const pair = segment.trim()\n if (!pair) continue\n\n // lastIndexOf(':') so a variant id that itself contained a colon survives.\n const sep = pair.lastIndexOf(':')\n const rawId = sep === -1 ? pair : pair.slice(0, sep)\n const rawQty = sep === -1 ? '' : pair.slice(sep + 1)\n\n let variantId = rawId.trim()\n try {\n variantId = decodeURIComponent(variantId).trim()\n } catch {\n // already-decoded / malformed escape → keep as-is\n }\n if (!variantId) continue\n\n const parsed = parseInt(rawQty, 10)\n const quantity = Number.isFinite(parsed) && parsed > 0 ? parsed : 1\n\n merged.set(variantId, (merged.get(variantId) ?? 0) + quantity)\n }\n\n return Array.from(merged, ([variant_id, quantity]) => ({ variant_id, quantity }))\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ // src/provider.tsx
4
+ import { useEffect, useRef } from "react";
5
+ import { useQueryClient } from "@tanstack/react-query";
6
+
7
+ // src/lib/fetch.ts
8
+ function resolveEnv(key) {
9
+ if (key === "NEXT_PUBLIC_MEDUSA_BACKEND_URL") return process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL;
10
+ if (key === "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY") return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY;
11
+ return void 0;
12
+ }
13
+ async function storeFetch(path, options = {}) {
14
+ const {
15
+ baseUrl = resolveEnv("NEXT_PUBLIC_MEDUSA_BACKEND_URL") ?? "http://localhost:9000",
16
+ publishableApiKey = resolveEnv("NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY"),
17
+ method = "GET",
18
+ body
19
+ } = options;
20
+ const headers = { Accept: "application/json" };
21
+ if (publishableApiKey) headers["x-publishable-api-key"] = publishableApiKey;
22
+ if (body !== void 0) headers["Content-Type"] = "application/json";
23
+ const res = await fetch(`${baseUrl}${path}`, {
24
+ method,
25
+ headers,
26
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
27
+ });
28
+ if (!res.ok) {
29
+ const err = await res.json().catch(() => ({}));
30
+ throw new Error(err.message ?? err.error ?? `Store request failed (${res.status})`);
31
+ }
32
+ return res.json();
33
+ }
34
+
35
+ // src/lib/parse.ts
36
+ function parseProducts(raw) {
37
+ const merged = /* @__PURE__ */ new Map();
38
+ for (const segment of raw.split(",")) {
39
+ const pair = segment.trim();
40
+ if (!pair) continue;
41
+ const sep = pair.lastIndexOf(":");
42
+ const rawId = sep === -1 ? pair : pair.slice(0, sep);
43
+ const rawQty = sep === -1 ? "" : pair.slice(sep + 1);
44
+ let variantId = rawId.trim();
45
+ try {
46
+ variantId = decodeURIComponent(variantId).trim();
47
+ } catch {
48
+ }
49
+ if (!variantId) continue;
50
+ const parsed = parseInt(rawQty, 10);
51
+ const quantity = Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
52
+ merged.set(variantId, (merged.get(variantId) ?? 0) + quantity);
53
+ }
54
+ return Array.from(merged, ([variant_id, quantity]) => ({ variant_id, quantity }));
55
+ }
56
+
57
+ // src/provider.tsx
58
+ var CART_ID_KEY = "medusa_cart_id";
59
+ var CART_QUERY_KEY = ["cart"];
60
+ var SEEDED_PREFIX = "meta_shops_seeded:";
61
+ function MetaShopsCheckoutProvider({
62
+ baseUrl,
63
+ publishableApiKey,
64
+ cartOrigin = "meta_shops",
65
+ checkoutPath = "/checkout",
66
+ enabled = true
67
+ }) {
68
+ const queryClient = useQueryClient();
69
+ const ranRef = useRef(false);
70
+ useEffect(() => {
71
+ if (!enabled || typeof window === "undefined" || ranRef.current) return;
72
+ let params;
73
+ try {
74
+ params = new URLSearchParams(window.location.search);
75
+ } catch {
76
+ return;
77
+ }
78
+ const norm = (p) => p.replace(/\/+$/, "");
79
+ if (norm(window.location.pathname) !== norm(checkoutPath)) return;
80
+ if (params.get("cart_origin") !== cartOrigin) return;
81
+ const productsRaw = params.get("products");
82
+ if (!productsRaw) return;
83
+ const items = parseProducts(productsRaw);
84
+ if (items.length === 0) return;
85
+ const couponRaw = params.get("coupon");
86
+ const coupon = couponRaw && couponRaw.trim() ? couponRaw.trim().toUpperCase() : null;
87
+ const cleanUrl = () => {
88
+ try {
89
+ window.history.replaceState(window.history.state, "", checkoutPath);
90
+ } catch {
91
+ }
92
+ };
93
+ const seedKey = `${SEEDED_PREFIX}${productsRaw}|${coupon ?? ""}`;
94
+ try {
95
+ if (sessionStorage.getItem(seedKey)) {
96
+ cleanUrl();
97
+ return;
98
+ }
99
+ } catch {
100
+ }
101
+ ranRef.current = true;
102
+ const opts = { baseUrl, publishableApiKey };
103
+ const seed = async () => {
104
+ try {
105
+ const { regions } = await storeFetch(
106
+ "/store/regions",
107
+ opts
108
+ );
109
+ const regionId = regions?.[0]?.id;
110
+ if (!regionId) throw new Error("no region available");
111
+ const { cart: created } = await storeFetch("/store/carts", {
112
+ ...opts,
113
+ method: "POST",
114
+ body: { region_id: regionId }
115
+ });
116
+ const cartId = created.id;
117
+ let latest = created;
118
+ for (const item of items) {
119
+ const { cart } = await storeFetch(
120
+ `/store/carts/${cartId}/line-items`,
121
+ { ...opts, method: "POST", body: { variant_id: item.variant_id, quantity: item.quantity } }
122
+ );
123
+ latest = cart;
124
+ }
125
+ if (coupon) {
126
+ try {
127
+ const { cart } = await storeFetch(`/store/carts/${cartId}`, {
128
+ ...opts,
129
+ method: "POST",
130
+ body: { promo_codes: [coupon] }
131
+ });
132
+ latest = cart;
133
+ } catch (e) {
134
+ console.warn(`[meta-shops-checkout] could not apply coupon "${coupon}":`, e);
135
+ }
136
+ }
137
+ try {
138
+ localStorage.setItem(CART_ID_KEY, cartId);
139
+ } catch {
140
+ }
141
+ try {
142
+ sessionStorage.setItem(seedKey, "1");
143
+ } catch {
144
+ }
145
+ queryClient.setQueryData(CART_QUERY_KEY, latest);
146
+ void queryClient.invalidateQueries({ queryKey: CART_QUERY_KEY });
147
+ cleanUrl();
148
+ } catch (e) {
149
+ console.error("[meta-shops-checkout] failed to seed cart from Meta Shops deep-link:", e);
150
+ ranRef.current = false;
151
+ }
152
+ };
153
+ void seed();
154
+ }, [enabled, baseUrl, publishableApiKey, cartOrigin, checkoutPath]);
155
+ return null;
156
+ }
157
+ export {
158
+ MetaShopsCheckoutProvider,
159
+ parseProducts
160
+ };
161
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider.tsx","../src/lib/fetch.ts","../src/lib/parse.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef, type ReactElement } from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { storeFetch } from './lib/fetch'\nimport { parseProducts } from './lib/parse'\nimport type { MetaShopsCheckoutProviderProps } from './types'\n\n// Same key + query the storefront's useCart owns, so the seeded cart becomes\n// THE active cart and the checkout page renders it.\nconst CART_ID_KEY = 'medusa_cart_id'\nconst CART_QUERY_KEY = ['cart'] as const\nconst SEEDED_PREFIX = 'meta_shops_seeded:'\n\n/**\n * Null-rendering side-effect component for the `rootProviders` slot.\n *\n * When a Meta Shops \"checkout on another website\" deep-link lands on the\n * checkout page:\n *\n * /checkout?products=variant_x:4,variant_y:2&coupon=SAVE10&cart_origin=meta_shops\n *\n * it builds a FRESH cart containing exactly the items Meta sent, applies the\n * coupon, makes that cart the storefront's active cart, and strips the params.\n * Payment is intentionally NOT initialized here — the storefront's useCheckout\n * auto-initializes the payment session once the shopper provides shipping\n * (Medusa ties the pay amount to shipping, which Meta does not send).\n *\n * Why a fresh cart: the buyer agreed to a specific set of items on Meta, so we\n * never merge into a stale browse-cart. Writing our cart id to localStorage\n * overrides any empty cart the header's useCart may have just auto-created; the\n * orphaned empty cart is abandoned harmlessly.\n *\n * Safe to mount globally: it is fully inert unless pathname === checkoutPath\n * AND cart_origin === cartOrigin AND a non-empty products param is present.\n */\nexport function MetaShopsCheckoutProvider({\n baseUrl,\n publishableApiKey,\n cartOrigin = 'meta_shops',\n checkoutPath = '/checkout',\n enabled = true,\n}: MetaShopsCheckoutProviderProps): ReactElement | null {\n const queryClient = useQueryClient()\n // Guards a single mount (incl. React StrictMode's double-invoke in dev).\n const ranRef = useRef(false)\n\n useEffect(() => {\n if (!enabled || typeof window === 'undefined' || ranRef.current) return\n\n let params: URLSearchParams\n try {\n params = new URLSearchParams(window.location.search)\n } catch {\n return\n }\n\n const norm = (p: string) => p.replace(/\\/+$/, '')\n if (norm(window.location.pathname) !== norm(checkoutPath)) return\n if (params.get('cart_origin') !== cartOrigin) return\n\n const productsRaw = params.get('products')\n if (!productsRaw) return\n\n const items = parseProducts(productsRaw)\n if (items.length === 0) return\n\n const couponRaw = params.get('coupon')\n const coupon = couponRaw && couponRaw.trim() ? couponRaw.trim().toUpperCase() : null\n\n const cleanUrl = () => {\n try {\n window.history.replaceState(window.history.state, '', checkoutPath)\n } catch {\n /* ignore */\n }\n }\n\n // Idempotency: never re-seed the same deep-link (refresh, back-nav, re-mount).\n const seedKey = `${SEEDED_PREFIX}${productsRaw}|${coupon ?? ''}`\n try {\n if (sessionStorage.getItem(seedKey)) {\n cleanUrl()\n return\n }\n } catch {\n /* sessionStorage blocked → ranRef + cleanUrl still prevent most repeats */\n }\n\n ranRef.current = true\n const opts = { baseUrl, publishableApiKey }\n const seed = async () => {\n try {\n // 1. Region — mirror the storefront's useCart (first region).\n const { regions } = await storeFetch<{ regions: Array<{ id: string }> }>(\n '/store/regions',\n opts,\n )\n const regionId = regions?.[0]?.id\n if (!regionId) throw new Error('no region available')\n\n // 2. Fresh cart for this Meta order.\n const { cart: created } = await storeFetch<{ cart: { id: string } }>('/store/carts', {\n ...opts,\n method: 'POST',\n body: { region_id: regionId },\n })\n const cartId = created.id\n\n // 3. Line items — sequential to keep the cart's optimistic version sane.\n let latest: unknown = created\n for (const item of items) {\n const { cart } = await storeFetch<{ cart: unknown }>(\n `/store/carts/${cartId}/line-items`,\n { ...opts, method: 'POST', body: { variant_id: item.variant_id, quantity: item.quantity } },\n )\n latest = cart\n }\n\n // 4. Discount — an invalid/expired coupon must NOT abort the cart.\n if (coupon) {\n try {\n const { cart } = await storeFetch<{ cart: unknown }>(`/store/carts/${cartId}`, {\n ...opts,\n method: 'POST',\n body: { promo_codes: [coupon] },\n })\n latest = cart\n } catch (e) {\n console.warn(`[meta-shops-checkout] could not apply coupon \"${coupon}\":`, e)\n }\n }\n\n // 5. Activate: make this the storefront's cart and reveal it on checkout.\n try {\n localStorage.setItem(CART_ID_KEY, cartId)\n } catch {\n /* ignore */\n }\n try {\n sessionStorage.setItem(seedKey, '1')\n } catch {\n /* ignore */\n }\n queryClient.setQueryData(CART_QUERY_KEY, latest)\n // Force a refetch so useCart re-reads localStorage (our cart) and wins\n // any race with a concurrently auto-created empty cart.\n void queryClient.invalidateQueries({ queryKey: CART_QUERY_KEY })\n\n cleanUrl()\n } catch (e) {\n // Non-fatal: leave the shopper on /checkout with whatever cart exists.\n console.error('[meta-shops-checkout] failed to seed cart from Meta Shops deep-link:', e)\n ranRef.current = false // allow a retry if the component re-mounts\n }\n }\n\n void seed()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, baseUrl, publishableApiKey, cartOrigin, checkoutPath])\n\n return null\n}\n","'use client'\n\nfunction resolveEnv(key: string): string | undefined {\n if (key === 'NEXT_PUBLIC_MEDUSA_BACKEND_URL') return process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL\n if (key === 'NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY') return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY\n return undefined\n}\n\nexport interface StoreFetchOptions {\n baseUrl?: string\n publishableApiKey?: string\n method?: string\n body?: unknown\n}\n\n/**\n * Minimal `/store/*` fetch helper, mirroring the storefront's medusa-client\n * (publishable key in the header, JSON in/out). Intentionally raw fetch rather\n * than the JS SDK: this package must NOT bundle a second `@medusajs/js-sdk`\n * copy, and the three cart endpoints we touch are stable.\n */\nexport async function storeFetch<T>(path: string, options: StoreFetchOptions = {}): Promise<T> {\n const {\n baseUrl = resolveEnv('NEXT_PUBLIC_MEDUSA_BACKEND_URL') ?? 'http://localhost:9000',\n publishableApiKey = resolveEnv('NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY'),\n method = 'GET',\n body,\n } = options\n\n const headers: Record<string, string> = { Accept: 'application/json' }\n if (publishableApiKey) headers['x-publishable-api-key'] = publishableApiKey\n if (body !== undefined) headers['Content-Type'] = 'application/json'\n\n const res = await fetch(`${baseUrl}${path}`, {\n method,\n headers,\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n })\n\n if (!res.ok) {\n const err = (await res.json().catch(() => ({}))) as { message?: string; error?: string }\n throw new Error(err.message ?? err.error ?? `Store request failed (${res.status})`)\n }\n\n return res.json() as Promise<T>\n}\n","import type { ParsedItem } from '../types'\n\n/**\n * Parse Meta's `products` deep-link param into line items.\n *\n * Format (already percent-decoded by URLSearchParams):\n * `variant_123:4,variant_456:2`\n *\n * The retailer_id Meta sends back is the Medusa variant id (the Meta Shop sync\n * mapper sets each catalog item's `id` to `variant.id`), so these map straight\n * to `createLineItem({ variant_id })` — no translation needed.\n *\n * Defensive rules:\n * - missing/!positive qty → 1\n * - a bare `variant_x` (no `:qty`) → qty 1\n * - duplicate variants are merged (quantities summed)\n * - blank/garbage segments are skipped, never thrown\n */\nexport function parseProducts(raw: string): ParsedItem[] {\n const merged = new Map<string, number>()\n\n for (const segment of raw.split(',')) {\n const pair = segment.trim()\n if (!pair) continue\n\n // lastIndexOf(':') so a variant id that itself contained a colon survives.\n const sep = pair.lastIndexOf(':')\n const rawId = sep === -1 ? pair : pair.slice(0, sep)\n const rawQty = sep === -1 ? '' : pair.slice(sep + 1)\n\n let variantId = rawId.trim()\n try {\n variantId = decodeURIComponent(variantId).trim()\n } catch {\n // already-decoded / malformed escape → keep as-is\n }\n if (!variantId) continue\n\n const parsed = parseInt(rawQty, 10)\n const quantity = Number.isFinite(parsed) && parsed > 0 ? parsed : 1\n\n merged.set(variantId, (merged.get(variantId) ?? 0) + quantity)\n }\n\n return Array.from(merged, ([variant_id, quantity]) => ({ variant_id, quantity }))\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@amboras-dev/meta-shops-checkout",
3
+ "version": "0.2.0",
4
+ "description": "Meta Shops 'checkout on another website' handoff for Amboras storefronts — builds a fresh cart (line items + discount) from the deep-link URL params and activates it for checkout",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "peerDependencies": {
20
+ "react": ">=18",
21
+ "@tanstack/react-query": ">=5"
22
+ },
23
+ "devDependencies": {
24
+ "tsup": "^8.0.0",
25
+ "typescript": "^5.0.0",
26
+ "@tanstack/react-query": "^5.0.0",
27
+ "react": "^19.0.0",
28
+ "@types/react": "^19.0.0",
29
+ "@types/node": "^20.0.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "typecheck": "tsc --noEmit"
37
+ }
38
+ }