shopgpt 0.2.0 → 0.3.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 CHANGED
@@ -1,94 +1,95 @@
1
- # shopgpt — Shoppable CTV Ad SDK
2
-
3
- TV ad plays → viewer presses the CTA → cart converts → TV updates live.
4
-
5
- Works on any HTML5 CTV (Samsung Tizen, LG webOS, FireTV, AndroidTV, Roku webview). No native code.
6
-
7
- ## Quickstart
8
-
9
- ```ts
10
- import { ShopGPTCTV } from "shopgpt";
11
-
12
- const ctv = new ShopGPTCTV({ placementId: "AD-01" }); // ← /embed ad spot id
13
-
14
- // listen for conversions
15
- ctv.onEvent((e) => {
16
- if (e.type === "added_to_cart") showBadge(e.session);
17
- });
18
-
19
- // when the ad spot starts — any /embed ad
20
- const { session } = await ctv.createSession(product);
21
-
22
- // CTA press → raises the added_to_cart flag
23
- await ctv.addToCart(session.id);
24
- ```
25
-
26
- ## CTA (fully customisable)
27
-
28
- Two built-in presets, or any custom text:
29
-
30
- ```tsx
31
- import { ShopGPTAddToCartButton } from "shopgpt/react";
32
-
33
- // preset 1 (default)
34
- <ShopGPTAddToCartButton session={session} cta="add_to_cart" />
35
-
36
- // preset 2
37
- <ShopGPTAddToCartButton session={session} cta="send_to_phone" />
38
-
39
- // fully custom — change it as you want
40
- <ShopGPTAddToCartButton
41
- session={session}
42
- label="Buy Now"
43
- className="my-cta-class"
44
- onCta={(s) => analytics.track("ctv_cta", { sessionId: s.id })}
45
- />
46
- ```
47
-
48
- ## React
49
-
50
- ```tsx
51
- import { ShopGPTCTVPlayer, useShopGPTShoppableAd } from "shopgpt/react";
52
-
53
- <ShopGPTCTVPlayer
54
- videoSrc="https://…/ad.mp4"
55
- product={product}
56
- placementId="hero"
57
- cta="send_to_phone" // or ctaLabel="Buy Now"
58
- onConverted={(s) => console.log("sold!", s.cartCount)}
59
- />
60
- ```
61
-
62
- Hooks: `useShopGPTShoppableAd(product, opts)` (create + poll automatically),
63
- `useShopGPTSession(sessionId)` (follow a live session).
64
-
65
- ## Backend (included in this repo)
66
-
67
- | Method | Route | Purpose |
68
- |---|---|---|
69
- | POST | `/api/ctv/session` | create session `{ product, placementId, campaignId }` |
70
- | GET | `/api/ctv/session/:id` | poll session (no-store) |
71
- | PATCH | `/api/ctv/session/:id` | `{ status: "scanned" \| "viewing" \| "completed" }` |
72
- | POST | `/api/ctv/cart` | `{ sessionId, qty }` → marks `added_to_cart` |
73
- | GET | `/api/ctv/cart?sessionId=` | list cart items |
74
- | GET | `/api/ctv/products` | catalog products shaped for TV |
75
-
76
- Demo: `/ctv-demo/tv` (TV) → press the CTA → spot flips to converted live.
77
-
78
- ## ShopGPT handoff (CTV → shopgpt.rankup.diy)
79
-
80
- Sessions carry a ShopGPT deep link (`session.mobileUrl`):
81
-
82
- ```
83
- https://shopgpt.rankup.diy/?product_id=<acp_item_id>&ctv=<sessionId>
84
- ```
85
-
86
- `?product_id=` opens the ShopGPT product dialog (chat + add to cart);
87
- `&ctv=` lets ShopGPT beacon `scanned` / `added_to_cart` back to
88
- `/api/ctv/session/:id` and `/api/ctv/cart` (CORS open) so the TV updates live.
89
-
90
- ## Production notes
91
-
92
- - Current store is in-memory (`lib/ctv/store.ts`). Move to Supabase `ctv_sessions` + `ctv_cart_items` or Upstash Redis for multi-instance.
93
- - Wire `product` from Shopify / Woo / ACP feed (`lib/acp`, `app/shop/[slug]/[itemId]`).
94
- - Analytics: `ctv.track("impression", { sessionId })` → POST `/api/ctv/session/track` (stub, add table).
1
+ # shopgpt — Shoppable CTV Ad SDK
2
+
3
+ TV ad plays → viewer presses the CTA → cart converts → TV updates live.
4
+
5
+ Works on any HTML5 CTV (Samsung Tizen, LG webOS, FireTV, AndroidTV, Roku webview). No native code.
6
+
7
+ ## Quickstart
8
+
9
+ ```ts
10
+ import { ShopGPTCTV } from "shopgpt";
11
+
12
+ const ctv = new ShopGPTCTV({ apiBase: "https://app.rankup.diy" });
13
+
14
+ // listen for conversions
15
+ ctv.onEvent((e) => {
16
+ if (e.type === "added_to_cart") showBadge(e.session);
17
+ });
18
+
19
+ // when the ad starts — pass the 3 VAST params as-is.
20
+ // The product (title, image, price) is looked up server-side from rankupid.
21
+ const { session } = await ctv.createSession({ shoppable, rankupid, cta });
22
+ if (!session) return; // shoppable: false → plain ad
23
+
24
+ // CTA press → raises the added_to_cart flag
25
+ await ctv.addToCart(session.id);
26
+ ```
27
+
28
+ ## CTA (fully customisable)
29
+
30
+ Two built-in presets, or any custom text:
31
+
32
+ ```tsx
33
+ import { ShopGPTAddToCartButton } from "shopgpt/react";
34
+
35
+ // preset 1 (default)
36
+ <ShopGPTAddToCartButton session={session} cta="add_to_cart" />
37
+
38
+ // preset 2
39
+ <ShopGPTAddToCartButton session={session} cta="send_to_phone" />
40
+
41
+ // fully custom — change it as you want
42
+ <ShopGPTAddToCartButton
43
+ session={session}
44
+ label="Buy Now"
45
+ className="my-cta-class"
46
+ onCta={(s) => analytics.track("ctv_cta", { sessionId: s.id })}
47
+ />
48
+ ```
49
+
50
+ ## React
51
+
52
+ ```tsx
53
+ import { ShopGPTCTVPlayer, useShopGPTShoppableAd } from "shopgpt/react";
54
+
55
+ <ShopGPTCTVPlayer
56
+ videoSrc="https://…/ad.mp4"
57
+ ad={{ shoppable: true, rankupid: 10403886137619, cta: "Scan to buy" }}
58
+ cta="send_to_phone" // or ctaLabel="Buy Now"
59
+ onConverted={(s) => console.log("sold!", s.cartCount)}
60
+ />
61
+ ```
62
+
63
+ Hooks: `useShopGPTShoppableAd(ad, opts)` (create + poll automatically),
64
+ `useShopGPTSession(sessionId)` (follow a live session).
65
+
66
+ ## Backend (included in this repo)
67
+
68
+ | Method | Route | Purpose |
69
+ |---|---|---|
70
+ | POST | `/api/ctv/session` | create session `{ shoppable, rankupid, cta }` → product resolved server-side (404 if unknown) |
71
+ | GET | `/api/ctv/session/:id` | poll session (no-store) |
72
+ | PATCH | `/api/ctv/session/:id` | `{ status: "scanned" \| "viewing" \| "completed" }` |
73
+ | POST | `/api/ctv/cart` | `{ sessionId, qty }` → marks `added_to_cart` |
74
+ | GET | `/api/ctv/cart?sessionId=` | list cart items |
75
+ | GET | `/api/ctv/products` | catalog products shaped for TV |
76
+
77
+ Demo: `/ctv-demo/tv` (TV) → press the CTA → spot flips to converted live.
78
+
79
+ ## ShopGPT handoff (CTV → shopgpt.rankup.diy)
80
+
81
+ Sessions carry a ShopGPT deep link (`session.mobileUrl`):
82
+
83
+ ```
84
+ https://shopgpt.rankup.diy/?product_id=<acp_item_id>&ctv=<sessionId>
85
+ ```
86
+
87
+ `?product_id=` opens the ShopGPT product dialog (chat + add to cart);
88
+ `&ctv=` lets ShopGPT beacon `scanned` / `added_to_cart` back to
89
+ `/api/ctv/session/:id` and `/api/ctv/cart` (CORS open) so the TV updates live.
90
+
91
+ ## Production notes
92
+
93
+ - Current store is in-memory (`lib/ctv/store.ts`). Move to Supabase `ctv_sessions` + `ctv_cart_items` or Upstash Redis for multi-instance.
94
+ - Wire `product` from Shopify / Woo / ACP feed (`lib/acp`, `app/shop/[slug]/[itemId]`).
95
+ - Analytics: `ctv.track("impression", { sessionId })` → POST `/api/ctv/session/track` (stub, add table).
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ShopGPTConfig, ShopGPTEvent, ShopGPTProduct, ShopGPTSession } from "./types";
1
+ import type { ShopGPTAdParams, ShopGPTConfig, ShopGPTEvent, ShopGPTProduct, ShopGPTSession } from "./types";
2
2
  export * from "./types";
3
3
  type Listener = (e: ShopGPTEvent) => void;
4
4
  /**
@@ -8,8 +8,8 @@ type Listener = (e: ShopGPTEvent) => void;
8
8
  * Phone side: getSession() → markViewed() → addToCart()
9
9
  *
10
10
  * @example
11
- * const ctv = new ShopGPTCTV({ placementId: "living-room-hero" });
12
- * const { session } = await ctv.createSession(product);
11
+ * const ctv = new ShopGPTCTV({ apiBase: "https://app.rankup.diy" });
12
+ * const { session } = await ctv.createSession({ shoppable: true, rankupid: 10403886137619, cta: "Scan to buy" });
13
13
  * ctv.onEvent((e) => { if (e.type === "added_to_cart") showBadge(e.session); });
14
14
  */
15
15
  export declare class ShopGPTCTV {
@@ -21,13 +21,9 @@ export declare class ShopGPTCTV {
21
21
  onEvent(fn: Listener): () => void;
22
22
  private emit;
23
23
  private f;
24
- createSession(product: ShopGPTProduct, opts?: {
25
- campaignId?: string;
26
- placementId?: string;
27
- ttlSec?: number;
28
- }): Promise<{
29
- session: ShopGPTSession;
30
- mobileUrl: string;
24
+ createSession(ad: ShopGPTAdParams): Promise<{
25
+ session: ShopGPTSession | null;
26
+ mobileUrl: string | null;
31
27
  }>;
32
28
  getSession(sessionId: string): Promise<ShopGPTSession>;
33
29
  pollSession(sessionId: string, onUpdate?: (s: ShopGPTSession) => void): () => void;
@@ -42,5 +38,5 @@ export declare class ShopGPTCTV {
42
38
  }>;
43
39
  track(event: string, props?: Record<string, unknown>): void;
44
40
  }
45
- /** Vanilla script-tag helper: <div data-shopgpt-ctv data-product-id="..."> */
41
+ /** Vanilla script-tag helper: marks [data-shopgpt-ctv] elements ready. */
46
42
  export declare function autoInit(selector?: string): ShopGPTCTV;
package/dist/index.js CHANGED
@@ -13,8 +13,8 @@ function resolveBase(cfg) {
13
13
  * Phone side: getSession() → markViewed() → addToCart()
14
14
  *
15
15
  * @example
16
- * const ctv = new ShopGPTCTV({ placementId: "living-room-hero" });
17
- * const { session } = await ctv.createSession(product);
16
+ * const ctv = new ShopGPTCTV({ apiBase: "https://app.rankup.diy" });
17
+ * const { session } = await ctv.createSession({ shoppable: true, rankupid: 10403886137619, cta: "Scan to buy" });
18
18
  * ctv.onEvent((e) => { if (e.type === "added_to_cart") showBadge(e.session); });
19
19
  */
20
20
  export class ShopGPTCTV {
@@ -43,24 +43,28 @@ export class ShopGPTCTV {
43
43
  f() {
44
44
  return this.cfg.fetchImpl ?? fetch.bind(globalThis);
45
45
  }
46
- // ── TV: create a shoppable session ──────────────────────────────
47
- async createSession(product, opts) {
46
+ // ── TV: create a shoppable session from the VAST ad params ─────
47
+ // Only { shoppable, rankupid, cta } are needed — the server looks up the
48
+ // product. `session` is null when the ad isn't shoppable.
49
+ async createSession(ad) {
50
+ if (ad.shoppable === false)
51
+ return { session: null, mobileUrl: null };
48
52
  const res = await this.f()(`${this.apiBase}/api/ctv/session`, {
49
53
  method: "POST",
50
54
  headers: { "content-type": "application/json" },
51
55
  body: JSON.stringify({
52
- product,
53
- placementId: opts?.placementId ?? this.cfg.placementId,
54
- campaignId: opts?.campaignId ?? this.cfg.campaignId,
55
- ttlSec: opts?.ttlSec ?? 600,
56
+ shoppable: ad.shoppable ?? true,
57
+ rankupid: ad.rankupid,
58
+ cta: ad.cta,
56
59
  }),
57
60
  });
58
61
  if (!res.ok)
59
62
  throw new Error(`createSession failed: ${res.status}`);
60
63
  const session = (await res.json()).session;
61
- const mobileUrl = session.mobileUrl;
64
+ if (!session)
65
+ return { session: null, mobileUrl: null };
62
66
  this.emit({ type: "session_created", session });
63
- return { session, mobileUrl };
67
+ return { session, mobileUrl: session.mobileUrl };
64
68
  }
65
69
  // ── Shared: fetch one session ───────────────────────────────────
66
70
  async getSession(sessionId) {
@@ -142,7 +146,7 @@ export class ShopGPTCTV {
142
146
  }
143
147
  }
144
148
  }
145
- /** Vanilla script-tag helper: <div data-shopgpt-ctv data-product-id="..."> */
149
+ /** Vanilla script-tag helper: marks [data-shopgpt-ctv] elements ready. */
146
150
  export function autoInit(selector = "[data-shopgpt-ctv]") {
147
151
  const client = new ShopGPTCTV();
148
152
  if (typeof document !== "undefined") {
package/dist/react.d.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  import { ShopGPTCTV } from "./index";
2
- import type { ShopGPTProduct, ShopGPTSession } from "./types";
2
+ import type { ShopGPTAdParams, ShopGPTSession } from "./types";
3
3
  export declare function useShopGPTSession(sessionId: string | null, apiBase?: string): {
4
4
  session: ShopGPTSession | null;
5
5
  error: string | null;
6
6
  };
7
- export declare function useShopGPTShoppableAd(product: ShopGPTProduct | null, opts?: {
8
- placementId?: string;
9
- campaignId?: string;
7
+ export declare function useShopGPTShoppableAd(ad: ShopGPTAdParams | null, opts?: {
10
8
  apiBase?: string;
11
9
  autoCreate?: boolean;
12
10
  }): {
@@ -30,15 +28,14 @@ export declare function ShopGPTAddToCartButton({ session, client, apiBase, qty,
30
28
  onAdded?: (s: ShopGPTSession) => void;
31
29
  className?: string;
32
30
  }): import("react/jsx-runtime").JSX.Element | null;
33
- export declare function ShopGPTCTVPlayer({ videoSrc, product, placementId, campaignId, apiBase, cta, ctaLabel, onConverted, }: {
31
+ export declare function ShopGPTCTVPlayer({ videoSrc, ad, apiBase, cta, ctaLabel, onConverted, }: {
34
32
  videoSrc: string;
35
- product: ShopGPTProduct;
36
- placementId?: string;
37
- campaignId?: string;
33
+ /** VAST ad params: { shoppable, rankupid, cta } */
34
+ ad: ShopGPTAdParams;
38
35
  apiBase?: string;
39
36
  /** CTA preset passed to the button: "add_to_cart" | "send_to_phone" */
40
37
  cta?: ShopGPTCta;
41
- /** Fully custom CTA text (overrides the preset) */
38
+ /** Fully custom CTA text (overrides the preset and the ad's `cta`) */
42
39
  ctaLabel?: string;
43
40
  onConverted?: (s: ShopGPTSession) => void;
44
41
  }): import("react/jsx-runtime").JSX.Element;
package/dist/react.js CHANGED
@@ -22,35 +22,33 @@ export function useShopGPTSession(sessionId, apiBase) {
22
22
  }, [sessionId, apiBase]);
23
23
  return { session, error };
24
24
  }
25
- // ── Hook: create session for a product (TV ad start) ──────────────
26
- export function useShopGPTShoppableAd(product, opts) {
25
+ // ── Hook: create session from the VAST ad params (TV ad start) ────
26
+ export function useShopGPTShoppableAd(ad, opts) {
27
27
  const [session, setSession] = useState(null);
28
28
  const [loading, setLoading] = useState(false);
29
29
  const clientRef = useRef(null);
30
30
  if (!clientRef.current)
31
31
  clientRef.current = new ShopGPTCTV(opts?.apiBase ? { apiBase: opts.apiBase } : {});
32
32
  const create = useCallback(async () => {
33
- if (!product)
33
+ if (!ad)
34
34
  return null;
35
35
  setLoading(true);
36
36
  try {
37
- const r = await clientRef.current.createSession(product, {
38
- placementId: opts?.placementId,
39
- campaignId: opts?.campaignId,
40
- });
37
+ const r = await clientRef.current.createSession(ad);
41
38
  setSession(r.session);
42
- clientRef.current.pollSession(r.session.id, setSession);
39
+ if (r.session)
40
+ clientRef.current.pollSession(r.session.id, setSession);
43
41
  return r.session;
44
42
  }
45
43
  finally {
46
44
  setLoading(false);
47
45
  }
48
- }, [product, opts?.placementId, opts?.campaignId]);
46
+ }, [ad]);
49
47
  useEffect(() => {
50
- if (opts?.autoCreate !== false && product && !session)
48
+ if (opts?.autoCreate !== false && ad && !session)
51
49
  void create();
52
50
  // eslint-disable-next-line react-hooks/exhaustive-deps
53
- }, [product?.id]);
51
+ }, [ad?.rankupid, ad?.shoppable]);
54
52
  useEffect(() => () => clientRef.current?.stopPolling(), []);
55
53
  return { session, loading, create };
56
54
  }
@@ -86,16 +84,12 @@ export function ShopGPTAddToCartButton({ session, client, apiBase, qty = 1, cta
86
84
  // ── Component: minimal CTV player wrapper ─────────────────────────
87
85
  // Remote CTA button (raises `added_to_cart` flag). Overlay shows the
88
86
  // product name only — no other chrome.
89
- export function ShopGPTCTVPlayer({ videoSrc, product, placementId, campaignId, apiBase, cta = "add_to_cart", ctaLabel, onConverted, }) {
90
- const { session } = useShopGPTShoppableAd(product, {
91
- placementId,
92
- campaignId,
93
- apiBase,
94
- });
87
+ export function ShopGPTCTVPlayer({ videoSrc, ad, apiBase, cta = "add_to_cart", ctaLabel, onConverted, }) {
88
+ const { session } = useShopGPTShoppableAd(ad, { apiBase });
95
89
  useEffect(() => {
96
90
  if (session &&
97
91
  (session.status === "added_to_cart" || session.status === "completed"))
98
92
  onConverted?.(session);
99
93
  }, [session, onConverted]);
100
- return (_jsxs("div", { className: "relative aspect-video w-full overflow-hidden rounded-2xl bg-black", children: [_jsx("video", { src: videoSrc, controls: true, autoPlay: true, muted: true, loop: true, playsInline: true, className: "h-full w-full object-cover" }), session && (_jsx("div", { className: "absolute bottom-6 right-6 z-20 rounded-2xl bg-black/70 p-4 text-white shadow-2xl backdrop-blur", children: _jsxs("div", { className: "pr-2", children: [_jsx("p", { className: "max-w-[240px] truncate text-sm font-semibold", children: session.product.title }), _jsx("div", { className: "mt-2", children: _jsx(ShopGPTAddToCartButton, { session: session, apiBase: apiBase, cta: cta, label: ctaLabel }) })] }) }))] }));
94
+ return (_jsxs("div", { className: "relative aspect-video w-full overflow-hidden rounded-2xl bg-black", children: [_jsx("video", { src: videoSrc, controls: true, autoPlay: true, muted: true, loop: true, playsInline: true, className: "h-full w-full object-cover" }), session && (_jsx("div", { className: "absolute bottom-6 right-6 z-20 rounded-2xl bg-black/70 p-4 text-white shadow-2xl backdrop-blur", children: _jsxs("div", { className: "pr-2", children: [_jsx("p", { className: "max-w-[240px] truncate text-sm font-semibold", children: session.product.title }), _jsx("div", { className: "mt-2", children: _jsx(ShopGPTAddToCartButton, { session: session, apiBase: apiBase, cta: cta, label: ctaLabel ?? session.cta }) })] }) }))] }));
101
95
  }
package/dist/types.d.ts CHANGED
@@ -8,13 +8,22 @@ export interface ShopGPTProduct {
8
8
  url?: string;
9
9
  description?: string;
10
10
  }
11
+ /** The three params a VAST ad carries. The product is resolved server-side from `rankupid`. */
12
+ export interface ShopGPTAdParams {
13
+ /** false → plain ad, no session is created */
14
+ shoppable?: boolean;
15
+ /** Our product id (unique identifier) */
16
+ rankupid: number | string;
17
+ /** CTA label shown on the TV, e.g. "Scan to buy" */
18
+ cta?: string;
19
+ }
11
20
  export type ShopGPTSessionStatus = "created" | "scanned" | "viewing" | "added_to_cart" | "completed" | "expired";
12
21
  export interface ShopGPTSession {
13
22
  id: string;
14
23
  product: ShopGPTProduct;
15
24
  status: ShopGPTSessionStatus;
16
- placementId?: string;
17
- campaignId?: string;
25
+ rankupid?: string;
26
+ cta?: string;
18
27
  mobileUrl: string;
19
28
  cartCount: number;
20
29
  scanCount: number;
@@ -24,8 +33,6 @@ export interface ShopGPTSession {
24
33
  export interface ShopGPTConfig {
25
34
  /** e.g. "https://shop.example.com" — base the SDK falls back to for links. Defaults to window.location.origin */
26
35
  apiBase?: string;
27
- placementId?: string;
28
- campaignId?: string;
29
36
  pollIntervalMs?: number;
30
37
  fetchImpl?: typeof fetch;
31
38
  }
package/package.json CHANGED
@@ -1,23 +1,28 @@
1
- {
2
- "name": "shopgpt",
3
- "version": "0.2.0",
4
- "description": "ShopGPT Shoppable CTV Ad SDK — remote CTA to cart with live TV conversion events. Works on any HTML5 CTV player (Tizen, webOS, FireTV, AndroidTV).",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "files": ["dist", "README.md"],
8
- "type": "module",
9
- "exports": {
10
- ".": "./dist/index.js",
11
- "./react": "./dist/react.js"
12
- },
13
- "scripts": {
14
- "build": "tsc -p tsconfig.json"
15
- },
16
- "peerDependencies": {
17
- "react": ">=18"
18
- },
19
- "peerDependenciesMeta": {
20
- "react": { "optional": true }
21
- },
22
- "license": "MIT"
23
- }
1
+ {
2
+ "name": "shopgpt",
3
+ "version": "0.3.0",
4
+ "description": "ShopGPT Shoppable CTV Ad SDK — remote CTA to cart with live TV conversion events. Works on any HTML5 CTV player (Tizen, webOS, FireTV, AndroidTV).",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "type": "module",
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./react": "./dist/react.js"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json"
18
+ },
19
+ "peerDependencies": {
20
+ "react": ">=18"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "react": {
24
+ "optional": true
25
+ }
26
+ },
27
+ "license": "MIT"
28
+ }