@auphere/embed 0.1.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,116 @@
1
+ # @auphere/embed
2
+
3
+ Send WhatsApp template broadcasts from **your own product**, powered by
4
+ [Auphere](https://auphere.com). This SDK is a thin loader: it mounts a
5
+ secure Auphere iframe and hands it a short-lived session token. Your
6
+ bundle never contains Auphere or WhatsApp credentials, and the modal UI
7
+ updates without you redeploying.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @auphere/embed
13
+ ```
14
+
15
+ ## 1. Mint session tokens in YOUR backend
16
+
17
+ Your secret API key (`ak_live_…`) must never reach the browser. Expose a
18
+ tiny endpoint that exchanges it for a short-lived widget session:
19
+
20
+ ```ts
21
+ // e.g. app/api/auphere/session/route.ts (Next.js)
22
+ export async function POST() {
23
+ const response = await fetch("https://api.auphere.com/v1/widget-sessions", {
24
+ method: "POST",
25
+ headers: {
26
+ Authorization: `Bearer ${process.env.AUPHERE_SECRET_KEY}`,
27
+ "Content-Type": "application/json",
28
+ },
29
+ // your OWN id for the signed-in client (tenant mapping is automatic)
30
+ body: JSON.stringify({ external_client_ref: currentClient.id }),
31
+ });
32
+ return Response.json(await response.json());
33
+ }
34
+ ```
35
+
36
+ > The endpoint is called on open **and again ~every 15 minutes** while a
37
+ > modal is open, so it must work without user interaction.
38
+
39
+ Register your client once (idempotent) with
40
+ `POST /v1/partners/clients { external_client_ref, name }`.
41
+
42
+ ## 2. Create the client
43
+
44
+ ```ts
45
+ import { createAuphere } from "@auphere/embed";
46
+
47
+ const auphere = createAuphere({
48
+ partnerSlug: "acme", // shown in your Auphere dashboard
49
+ fetchSession: async () =>
50
+ (await fetch("/api/auphere/session", { method: "POST" })).json(),
51
+ appearance: { colorPrimary: "#25D366", radius: "8px" },
52
+ locale: "es",
53
+ });
54
+ ```
55
+
56
+ ## 3a. Broadcast button (React)
57
+
58
+ The button renders **only when the client's WhatsApp is connected**:
59
+
60
+ ```tsx
61
+ import { AuphereBroadcastButton } from "@auphere/embed/react";
62
+
63
+ <AuphereBroadcastButton
64
+ auphere={auphere}
65
+ recipients={[
66
+ { phone: "+56912345678", variables: { cliente: "Ana", saldo_pendiente: "$12.000" } },
67
+ { phone: "+56987654321", variables: { cliente: "Luis", saldo_pendiente: "$8.000" } },
68
+ ]}
69
+ className="your-button-class"
70
+ onDone={({ broadcastId, accepted }) => console.log(broadcastId, accepted)}
71
+ >
72
+ Campañas WhatsApp
73
+ </AuphereBroadcastButton>;
74
+ ```
75
+
76
+ `variables` keys must match the template's **named parameters** — you
77
+ map your schema to them in your backend. Recipients always come from
78
+ your data; the modal previews and sends, it never invents an audience.
79
+
80
+ ## 3b. Vanilla JS
81
+
82
+ ```ts
83
+ auphere.getStatus(); // "connected" | "not_connected" | "unknown"
84
+ auphere.onStatusChange((s) => { ... }); // subscription
85
+
86
+ await auphere.openBroadcast({ recipients });
87
+ ```
88
+
89
+ ## 4. Connect WhatsApp (self-serve signup)
90
+
91
+ Call it from your settings/onboarding page — it opens Meta's Embedded
92
+ Signup inside the Auphere iframe:
93
+
94
+ ```ts
95
+ await auphere.connectWhatsApp({
96
+ onConnected: ({ displayPhoneNumber }) => refreshUI(),
97
+ });
98
+ ```
99
+
100
+ ## Security model (short version)
101
+
102
+ - Your secret key lives only in your backend; the browser only ever
103
+ holds a 15-minute JWT scoped to one client.
104
+ - The modal runs on `embed.auphere.com`, isolated from your page's JS.
105
+ - Tokens travel via `postMessage` with strict origin checks — never in
106
+ URLs or cookies.
107
+ - Allowed embedding origins are configured per API key in the Auphere
108
+ dashboard; other origins are blocked by CSP.
109
+
110
+ ## Content-Security-Policy
111
+
112
+ If your site sets a CSP, allow:
113
+
114
+ ```
115
+ frame-src https://embed.auphere.com;
116
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var DEFAULT_EMBED_ORIGIN = "https://embed.auphere.com";
5
+ var MSG = {
6
+ // iframe → loader
7
+ READY: "auph:ready",
8
+ CLOSE: "auph:close",
9
+ STATUS: "auph:status",
10
+ BROADCAST_DONE: "auph:broadcast:done",
11
+ TOKEN_EXPIRING: "auph:token:expiring",
12
+ CONNECTED: "auph:connected",
13
+ ERROR: "auph:error",
14
+ // loader → iframe
15
+ INIT: "auph:init",
16
+ TOKEN: "auph:token"
17
+ };
18
+
19
+ // src/iframe-manager.ts
20
+ var OVERLAY_Z = 2147483646;
21
+ function openOverlay(options) {
22
+ const { url, embedOrigin, title, onMessage, onClose } = options;
23
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
24
+ const previousOverflow = document.body.style.overflow;
25
+ const backdrop = document.createElement("div");
26
+ backdrop.setAttribute("data-auphere-embed", "");
27
+ backdrop.style.cssText = [
28
+ "position:fixed",
29
+ "inset:0",
30
+ `z-index:${OVERLAY_Z}`,
31
+ "background:rgba(0,0,0,0.45)",
32
+ "opacity:0",
33
+ "transition:opacity 200ms ease"
34
+ ].join(";");
35
+ const iframe = document.createElement("iframe");
36
+ iframe.src = url;
37
+ iframe.title = title;
38
+ iframe.setAttribute("role", "dialog");
39
+ iframe.setAttribute("aria-modal", "true");
40
+ iframe.allow = "clipboard-write";
41
+ iframe.style.cssText = [
42
+ "position:fixed",
43
+ "inset:0",
44
+ "width:100%",
45
+ "height:100%",
46
+ "border:0",
47
+ `z-index:${OVERLAY_Z + 1}`,
48
+ "background:transparent",
49
+ "color-scheme:normal"
50
+ ].join(";");
51
+ let closed = false;
52
+ function handleMessage(event) {
53
+ if (event.origin !== embedOrigin) return;
54
+ if (event.source !== iframe.contentWindow) return;
55
+ const data = event.data;
56
+ if (!data || typeof data.type !== "string" || !data.type.startsWith("auph:")) return;
57
+ onMessage(data.type, data);
58
+ }
59
+ function handleKeydown(event) {
60
+ if (event.key === "Escape") close();
61
+ }
62
+ function close() {
63
+ if (closed) return;
64
+ closed = true;
65
+ window.removeEventListener("message", handleMessage);
66
+ document.removeEventListener("keydown", handleKeydown);
67
+ document.body.style.overflow = previousOverflow;
68
+ backdrop.remove();
69
+ iframe.remove();
70
+ previousFocus?.focus?.();
71
+ onClose();
72
+ }
73
+ window.addEventListener("message", handleMessage);
74
+ document.addEventListener("keydown", handleKeydown);
75
+ document.body.style.overflow = "hidden";
76
+ document.body.append(backdrop, iframe);
77
+ requestAnimationFrame(() => {
78
+ backdrop.style.opacity = "1";
79
+ });
80
+ iframe.focus();
81
+ return {
82
+ post(type, payload = {}) {
83
+ iframe.contentWindow?.postMessage({ type, ...payload }, embedOrigin);
84
+ },
85
+ close
86
+ };
87
+ }
88
+
89
+ // src/index.ts
90
+ function normalizeSession(raw) {
91
+ if ("sessionToken" in raw && typeof raw.sessionToken === "string") return raw;
92
+ const wire = raw;
93
+ return {
94
+ sessionToken: wire.session_token,
95
+ expiresIn: wire.expires_in,
96
+ whatsapp: wire.whatsapp ? {
97
+ status: wire.whatsapp.status,
98
+ displayPhoneNumber: wire.whatsapp.display_phone_number ?? null
99
+ } : void 0
100
+ };
101
+ }
102
+ function createAuphere(config) {
103
+ if (typeof config?.fetchSession !== "function") {
104
+ throw new Error("@auphere/embed: `fetchSession` is required");
105
+ }
106
+ if (!config.partnerSlug) {
107
+ throw new Error("@auphere/embed: `partnerSlug` is required");
108
+ }
109
+ const embedOrigin = (config.embedOrigin ?? DEFAULT_EMBED_ORIGIN).replace(/\/+$/, "");
110
+ let status = "unknown";
111
+ const listeners = /* @__PURE__ */ new Set();
112
+ let activeOverlay = null;
113
+ function setStatus(next) {
114
+ if (next === status) return;
115
+ status = next;
116
+ listeners.forEach((listener) => listener(status));
117
+ }
118
+ async function mintSession() {
119
+ const session = normalizeSession(await config.fetchSession());
120
+ if (!session.sessionToken) {
121
+ throw new Error("@auphere/embed: fetchSession returned no session token");
122
+ }
123
+ if (session.whatsapp?.status) setStatus(session.whatsapp.status);
124
+ return session;
125
+ }
126
+ function iframeUrl(route) {
127
+ return `${embedOrigin}/${route}?p=${encodeURIComponent(config.partnerSlug)}`;
128
+ }
129
+ async function open(route, handlers) {
130
+ if (activeOverlay) activeOverlay.close();
131
+ const session = await mintSession();
132
+ await new Promise((resolve, reject) => {
133
+ let settled = false;
134
+ const overlay = openOverlay({
135
+ url: iframeUrl(route),
136
+ embedOrigin,
137
+ title: route === "broadcast" ? "Auphere \u2014 Campa\xF1as WhatsApp" : "Auphere \u2014 Conectar WhatsApp",
138
+ onClose: () => {
139
+ activeOverlay = null;
140
+ handlers.onExit?.();
141
+ if (!settled) {
142
+ settled = true;
143
+ resolve();
144
+ }
145
+ },
146
+ onMessage: (type, payload) => {
147
+ switch (type) {
148
+ case MSG.READY:
149
+ overlay.post(MSG.INIT, {
150
+ mode: route,
151
+ token: session.sessionToken,
152
+ appearance: config.appearance ?? {},
153
+ locale: config.locale ?? document.documentElement.lang ?? "es",
154
+ recipients: handlers.recipients ?? []
155
+ });
156
+ if (!settled) {
157
+ settled = true;
158
+ resolve();
159
+ }
160
+ break;
161
+ case MSG.TOKEN_EXPIRING:
162
+ void mintSession().then((fresh) => overlay.post(MSG.TOKEN, { token: fresh.sessionToken })).catch(() => overlay.post(MSG.ERROR, { code: "token_refresh_failed" }));
163
+ break;
164
+ case MSG.STATUS: {
165
+ const value = payload["whatsappConnected"];
166
+ if (typeof value === "boolean") setStatus(value ? "connected" : "not_connected");
167
+ break;
168
+ }
169
+ case MSG.BROADCAST_DONE:
170
+ handlers.onDone?.({
171
+ broadcastId: String(payload["broadcastId"] ?? ""),
172
+ accepted: Number(payload["accepted"] ?? 0)
173
+ });
174
+ break;
175
+ case MSG.CONNECTED:
176
+ setStatus("connected");
177
+ handlers.onConnected?.({
178
+ displayPhoneNumber: typeof payload["displayPhoneNumber"] === "string" ? payload["displayPhoneNumber"] : null
179
+ });
180
+ break;
181
+ case MSG.CLOSE:
182
+ overlay.close();
183
+ break;
184
+ case MSG.ERROR:
185
+ if (!settled) {
186
+ settled = true;
187
+ overlay.close();
188
+ reject(new Error(`@auphere/embed: ${String(payload["code"] ?? "embed_error")}`));
189
+ }
190
+ break;
191
+ }
192
+ }
193
+ });
194
+ activeOverlay = overlay;
195
+ });
196
+ }
197
+ return {
198
+ getStatus: () => status,
199
+ async refreshStatus() {
200
+ await mintSession();
201
+ return status;
202
+ },
203
+ onStatusChange(listener) {
204
+ listeners.add(listener);
205
+ return () => listeners.delete(listener);
206
+ },
207
+ openBroadcast(options) {
208
+ return open("broadcast", options);
209
+ },
210
+ connectWhatsApp(options = {}) {
211
+ return open("signup", options);
212
+ },
213
+ destroy() {
214
+ activeOverlay?.close();
215
+ activeOverlay = null;
216
+ listeners.clear();
217
+ }
218
+ };
219
+ }
220
+
221
+ exports.createAuphere = createAuphere;
222
+ //# sourceMappingURL=index.cjs.map
223
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/iframe-manager.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** Production embed origin. Overridable per-instance via `config.embedOrigin` (dev/staging). */\nexport const DEFAULT_EMBED_ORIGIN = \"https://embed.auphere.com\";\n\n/** postMessage envelope namespace — every message is `{ type: \"auph:…\" }`. */\nexport const MSG = {\n // iframe → loader\n READY: \"auph:ready\",\n RESIZE: \"auph:resize\",\n CLOSE: \"auph:close\",\n STATUS: \"auph:status\",\n BROADCAST_DONE: \"auph:broadcast:done\",\n TOKEN_EXPIRING: \"auph:token:expiring\",\n CONNECTED: \"auph:connected\",\n ERROR: \"auph:error\",\n // loader → iframe\n INIT: \"auph:init\",\n TOKEN: \"auph:token\",\n} as const;\n","/**\n * Fullscreen iframe overlay lifecycle (ADR-028).\n *\n * The iframe cannot escape its own box, so the LOADER styles it to\n * cover the viewport (`position: fixed; inset: 0`) while it's open —\n * the \"breakout\" pattern used by Stripe Checkout and Plaid Link.\n * Scroll-lock + focus restore are handled here; the focus trap itself\n * lives inside the iframe document (it owns the focusable elements).\n *\n * Security invariants enforced in this file:\n * - messages are only accepted from the configured embed origin AND\n * from this exact iframe's contentWindow;\n * - outgoing messages always use an explicit `targetOrigin` (never `*`);\n * - the session token travels ONLY via postMessage — never in the URL.\n */\n\nimport { MSG } from \"./constants\";\n\ntype MessageHandler = (type: string, payload: Record<string, unknown>) => void;\n\nexport interface OverlayHandle {\n post(type: string, payload?: Record<string, unknown>): void;\n close(): void;\n}\n\nconst OVERLAY_Z = 2147483646; // one below max — leaves room for the host's own emergency layers\n\nexport function openOverlay(options: {\n url: string;\n embedOrigin: string;\n title: string;\n onMessage: MessageHandler;\n onClose: () => void;\n}): OverlayHandle {\n const { url, embedOrigin, title, onMessage, onClose } = options;\n\n const previousFocus =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n const previousOverflow = document.body.style.overflow;\n\n const backdrop = document.createElement(\"div\");\n backdrop.setAttribute(\"data-auphere-embed\", \"\");\n backdrop.style.cssText = [\n \"position:fixed\",\n \"inset:0\",\n `z-index:${OVERLAY_Z}`,\n \"background:rgba(0,0,0,0.45)\",\n \"opacity:0\",\n \"transition:opacity 200ms ease\",\n ].join(\";\");\n\n const iframe = document.createElement(\"iframe\");\n iframe.src = url;\n iframe.title = title;\n iframe.setAttribute(\"role\", \"dialog\");\n iframe.setAttribute(\"aria-modal\", \"true\");\n iframe.allow = \"clipboard-write\";\n iframe.style.cssText = [\n \"position:fixed\",\n \"inset:0\",\n \"width:100%\",\n \"height:100%\",\n \"border:0\",\n `z-index:${OVERLAY_Z + 1}`,\n \"background:transparent\",\n \"color-scheme:normal\",\n ].join(\";\");\n\n let closed = false;\n\n function handleMessage(event: MessageEvent): void {\n // Origin AND source must match — a message from another tab or a\n // nested frame on the same origin is not our modal.\n if (event.origin !== embedOrigin) return;\n if (event.source !== iframe.contentWindow) return;\n const data = event.data as { type?: unknown } | null;\n if (!data || typeof data.type !== \"string\" || !data.type.startsWith(\"auph:\")) return;\n onMessage(data.type, data as Record<string, unknown>);\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") close();\n }\n\n function close(): void {\n if (closed) return;\n closed = true;\n window.removeEventListener(\"message\", handleMessage);\n document.removeEventListener(\"keydown\", handleKeydown);\n document.body.style.overflow = previousOverflow;\n backdrop.remove();\n iframe.remove();\n previousFocus?.focus?.();\n onClose();\n }\n\n window.addEventListener(\"message\", handleMessage);\n document.addEventListener(\"keydown\", handleKeydown);\n document.body.style.overflow = \"hidden\"; // scroll-lock while open\n document.body.append(backdrop, iframe);\n requestAnimationFrame(() => {\n backdrop.style.opacity = \"1\";\n });\n iframe.focus();\n\n return {\n post(type, payload = {}) {\n // Explicit targetOrigin — if the frame navigated elsewhere the\n // browser drops the message instead of leaking the token.\n iframe.contentWindow?.postMessage({ type, ...payload }, embedOrigin);\n },\n close,\n };\n}\n\nexport { MSG };\n","/**\n * `@auphere/embed` — public entrypoint (ADR-028).\n *\n * ```ts\n * import { createAuphere } from \"@auphere/embed\";\n *\n * const auphere = createAuphere({\n * partnerSlug: \"acme\",\n * fetchSession: async () => (await fetch(\"/api/auphere/session\", { method: \"POST\" })).json(),\n * });\n *\n * await auphere.openBroadcast({ recipients: [{ phone: \"+56912345678\", variables: { cliente: \"Ana\" } }] });\n * ```\n */\n\nimport { DEFAULT_EMBED_ORIGIN, MSG } from \"./constants\";\nimport { openOverlay } from \"./iframe-manager\";\nimport type {\n Auphere,\n AuphereConfig,\n ConnectionStatus,\n ConnectWhatsAppOptions,\n OpenBroadcastOptions,\n WidgetSession,\n WidgetSessionWire,\n} from \"./types\";\n\nexport type {\n Appearance,\n Auphere,\n AuphereConfig,\n BroadcastRecipient,\n ConnectionStatus,\n ConnectWhatsAppOptions,\n FetchSession,\n OpenBroadcastOptions,\n WidgetSession,\n} from \"./types\";\n\nfunction normalizeSession(raw: WidgetSession | WidgetSessionWire): WidgetSession {\n if (\"sessionToken\" in raw && typeof raw.sessionToken === \"string\") return raw;\n const wire = raw as WidgetSessionWire;\n return {\n sessionToken: wire.session_token,\n expiresIn: wire.expires_in,\n whatsapp: wire.whatsapp\n ? {\n status: wire.whatsapp.status,\n displayPhoneNumber: wire.whatsapp.display_phone_number ?? null,\n }\n : undefined,\n };\n}\n\nexport function createAuphere(config: AuphereConfig): Auphere {\n if (typeof config?.fetchSession !== \"function\") {\n throw new Error(\"@auphere/embed: `fetchSession` is required\");\n }\n if (!config.partnerSlug) {\n throw new Error(\"@auphere/embed: `partnerSlug` is required\");\n }\n const embedOrigin = (config.embedOrigin ?? DEFAULT_EMBED_ORIGIN).replace(/\\/+$/, \"\");\n\n let status: ConnectionStatus = \"unknown\";\n const listeners = new Set<(status: ConnectionStatus) => void>();\n let activeOverlay: { close(): void } | null = null;\n\n function setStatus(next: ConnectionStatus): void {\n if (next === status) return;\n status = next;\n listeners.forEach((listener) => listener(status));\n }\n\n async function mintSession(): Promise<WidgetSession> {\n const session = normalizeSession(await config.fetchSession());\n if (!session.sessionToken) {\n throw new Error(\"@auphere/embed: fetchSession returned no session token\");\n }\n if (session.whatsapp?.status) setStatus(session.whatsapp.status);\n return session;\n }\n\n function iframeUrl(route: \"broadcast\" | \"signup\"): string {\n return `${embedOrigin}/${route}?p=${encodeURIComponent(config.partnerSlug)}`;\n }\n\n async function open(\n route: \"broadcast\" | \"signup\",\n handlers: {\n recipients?: OpenBroadcastOptions[\"recipients\"];\n onDone?: OpenBroadcastOptions[\"onDone\"];\n onConnected?: ConnectWhatsAppOptions[\"onConnected\"];\n onExit?: () => void;\n },\n ): Promise<void> {\n if (activeOverlay) activeOverlay.close();\n const session = await mintSession(); // mint BEFORE mounting: fail fast, no empty modal\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const overlay = openOverlay({\n url: iframeUrl(route),\n embedOrigin,\n title: route === \"broadcast\" ? \"Auphere — Campañas WhatsApp\" : \"Auphere — Conectar WhatsApp\",\n onClose: () => {\n activeOverlay = null;\n handlers.onExit?.();\n if (!settled) {\n settled = true;\n resolve();\n }\n },\n onMessage: (type, payload) => {\n switch (type) {\n case MSG.READY:\n overlay.post(MSG.INIT, {\n mode: route,\n token: session.sessionToken,\n appearance: config.appearance ?? {},\n locale: config.locale ?? document.documentElement.lang ?? \"es\",\n recipients: handlers.recipients ?? [],\n });\n if (!settled) {\n settled = true;\n resolve();\n }\n break;\n case MSG.TOKEN_EXPIRING:\n void mintSession()\n .then((fresh) => overlay.post(MSG.TOKEN, { token: fresh.sessionToken }))\n .catch(() => overlay.post(MSG.ERROR, { code: \"token_refresh_failed\" }));\n break;\n case MSG.STATUS: {\n const value = payload[\"whatsappConnected\"];\n if (typeof value === \"boolean\") setStatus(value ? \"connected\" : \"not_connected\");\n break;\n }\n case MSG.BROADCAST_DONE:\n handlers.onDone?.({\n broadcastId: String(payload[\"broadcastId\"] ?? \"\"),\n accepted: Number(payload[\"accepted\"] ?? 0),\n });\n break;\n case MSG.CONNECTED:\n setStatus(\"connected\");\n handlers.onConnected?.({\n displayPhoneNumber:\n typeof payload[\"displayPhoneNumber\"] === \"string\"\n ? payload[\"displayPhoneNumber\"]\n : null,\n });\n break;\n case MSG.CLOSE:\n overlay.close();\n break;\n case MSG.ERROR:\n if (!settled) {\n settled = true;\n overlay.close();\n reject(new Error(`@auphere/embed: ${String(payload[\"code\"] ?? \"embed_error\")}`));\n }\n break;\n }\n },\n });\n activeOverlay = overlay;\n });\n }\n\n return {\n getStatus: () => status,\n async refreshStatus() {\n await mintSession();\n return status;\n },\n onStatusChange(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n openBroadcast(options: OpenBroadcastOptions) {\n return open(\"broadcast\", options);\n },\n connectWhatsApp(options: ConnectWhatsAppOptions = {}) {\n return open(\"signup\", options);\n },\n destroy() {\n activeOverlay?.close();\n activeOverlay = null;\n listeners.clear();\n },\n };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { A as AuphereConfig, a as Auphere } from './types-BomPnFf3.cjs';
2
+ export { b as Appearance, B as BroadcastRecipient, C as ConnectWhatsAppOptions, c as ConnectionStatus, F as FetchSession, O as OpenBroadcastOptions, W as WidgetSession } from './types-BomPnFf3.cjs';
3
+
4
+ /**
5
+ * `@auphere/embed` — public entrypoint (ADR-028).
6
+ *
7
+ * ```ts
8
+ * import { createAuphere } from "@auphere/embed";
9
+ *
10
+ * const auphere = createAuphere({
11
+ * partnerSlug: "acme",
12
+ * fetchSession: async () => (await fetch("/api/auphere/session", { method: "POST" })).json(),
13
+ * });
14
+ *
15
+ * await auphere.openBroadcast({ recipients: [{ phone: "+56912345678", variables: { cliente: "Ana" } }] });
16
+ * ```
17
+ */
18
+
19
+ declare function createAuphere(config: AuphereConfig): Auphere;
20
+
21
+ export { Auphere, AuphereConfig, createAuphere };
@@ -0,0 +1,21 @@
1
+ import { A as AuphereConfig, a as Auphere } from './types-BomPnFf3.js';
2
+ export { b as Appearance, B as BroadcastRecipient, C as ConnectWhatsAppOptions, c as ConnectionStatus, F as FetchSession, O as OpenBroadcastOptions, W as WidgetSession } from './types-BomPnFf3.js';
3
+
4
+ /**
5
+ * `@auphere/embed` — public entrypoint (ADR-028).
6
+ *
7
+ * ```ts
8
+ * import { createAuphere } from "@auphere/embed";
9
+ *
10
+ * const auphere = createAuphere({
11
+ * partnerSlug: "acme",
12
+ * fetchSession: async () => (await fetch("/api/auphere/session", { method: "POST" })).json(),
13
+ * });
14
+ *
15
+ * await auphere.openBroadcast({ recipients: [{ phone: "+56912345678", variables: { cliente: "Ana" } }] });
16
+ * ```
17
+ */
18
+
19
+ declare function createAuphere(config: AuphereConfig): Auphere;
20
+
21
+ export { Auphere, AuphereConfig, createAuphere };
package/dist/index.js ADDED
@@ -0,0 +1,221 @@
1
+ // src/constants.ts
2
+ var DEFAULT_EMBED_ORIGIN = "https://embed.auphere.com";
3
+ var MSG = {
4
+ // iframe → loader
5
+ READY: "auph:ready",
6
+ CLOSE: "auph:close",
7
+ STATUS: "auph:status",
8
+ BROADCAST_DONE: "auph:broadcast:done",
9
+ TOKEN_EXPIRING: "auph:token:expiring",
10
+ CONNECTED: "auph:connected",
11
+ ERROR: "auph:error",
12
+ // loader → iframe
13
+ INIT: "auph:init",
14
+ TOKEN: "auph:token"
15
+ };
16
+
17
+ // src/iframe-manager.ts
18
+ var OVERLAY_Z = 2147483646;
19
+ function openOverlay(options) {
20
+ const { url, embedOrigin, title, onMessage, onClose } = options;
21
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
22
+ const previousOverflow = document.body.style.overflow;
23
+ const backdrop = document.createElement("div");
24
+ backdrop.setAttribute("data-auphere-embed", "");
25
+ backdrop.style.cssText = [
26
+ "position:fixed",
27
+ "inset:0",
28
+ `z-index:${OVERLAY_Z}`,
29
+ "background:rgba(0,0,0,0.45)",
30
+ "opacity:0",
31
+ "transition:opacity 200ms ease"
32
+ ].join(";");
33
+ const iframe = document.createElement("iframe");
34
+ iframe.src = url;
35
+ iframe.title = title;
36
+ iframe.setAttribute("role", "dialog");
37
+ iframe.setAttribute("aria-modal", "true");
38
+ iframe.allow = "clipboard-write";
39
+ iframe.style.cssText = [
40
+ "position:fixed",
41
+ "inset:0",
42
+ "width:100%",
43
+ "height:100%",
44
+ "border:0",
45
+ `z-index:${OVERLAY_Z + 1}`,
46
+ "background:transparent",
47
+ "color-scheme:normal"
48
+ ].join(";");
49
+ let closed = false;
50
+ function handleMessage(event) {
51
+ if (event.origin !== embedOrigin) return;
52
+ if (event.source !== iframe.contentWindow) return;
53
+ const data = event.data;
54
+ if (!data || typeof data.type !== "string" || !data.type.startsWith("auph:")) return;
55
+ onMessage(data.type, data);
56
+ }
57
+ function handleKeydown(event) {
58
+ if (event.key === "Escape") close();
59
+ }
60
+ function close() {
61
+ if (closed) return;
62
+ closed = true;
63
+ window.removeEventListener("message", handleMessage);
64
+ document.removeEventListener("keydown", handleKeydown);
65
+ document.body.style.overflow = previousOverflow;
66
+ backdrop.remove();
67
+ iframe.remove();
68
+ previousFocus?.focus?.();
69
+ onClose();
70
+ }
71
+ window.addEventListener("message", handleMessage);
72
+ document.addEventListener("keydown", handleKeydown);
73
+ document.body.style.overflow = "hidden";
74
+ document.body.append(backdrop, iframe);
75
+ requestAnimationFrame(() => {
76
+ backdrop.style.opacity = "1";
77
+ });
78
+ iframe.focus();
79
+ return {
80
+ post(type, payload = {}) {
81
+ iframe.contentWindow?.postMessage({ type, ...payload }, embedOrigin);
82
+ },
83
+ close
84
+ };
85
+ }
86
+
87
+ // src/index.ts
88
+ function normalizeSession(raw) {
89
+ if ("sessionToken" in raw && typeof raw.sessionToken === "string") return raw;
90
+ const wire = raw;
91
+ return {
92
+ sessionToken: wire.session_token,
93
+ expiresIn: wire.expires_in,
94
+ whatsapp: wire.whatsapp ? {
95
+ status: wire.whatsapp.status,
96
+ displayPhoneNumber: wire.whatsapp.display_phone_number ?? null
97
+ } : void 0
98
+ };
99
+ }
100
+ function createAuphere(config) {
101
+ if (typeof config?.fetchSession !== "function") {
102
+ throw new Error("@auphere/embed: `fetchSession` is required");
103
+ }
104
+ if (!config.partnerSlug) {
105
+ throw new Error("@auphere/embed: `partnerSlug` is required");
106
+ }
107
+ const embedOrigin = (config.embedOrigin ?? DEFAULT_EMBED_ORIGIN).replace(/\/+$/, "");
108
+ let status = "unknown";
109
+ const listeners = /* @__PURE__ */ new Set();
110
+ let activeOverlay = null;
111
+ function setStatus(next) {
112
+ if (next === status) return;
113
+ status = next;
114
+ listeners.forEach((listener) => listener(status));
115
+ }
116
+ async function mintSession() {
117
+ const session = normalizeSession(await config.fetchSession());
118
+ if (!session.sessionToken) {
119
+ throw new Error("@auphere/embed: fetchSession returned no session token");
120
+ }
121
+ if (session.whatsapp?.status) setStatus(session.whatsapp.status);
122
+ return session;
123
+ }
124
+ function iframeUrl(route) {
125
+ return `${embedOrigin}/${route}?p=${encodeURIComponent(config.partnerSlug)}`;
126
+ }
127
+ async function open(route, handlers) {
128
+ if (activeOverlay) activeOverlay.close();
129
+ const session = await mintSession();
130
+ await new Promise((resolve, reject) => {
131
+ let settled = false;
132
+ const overlay = openOverlay({
133
+ url: iframeUrl(route),
134
+ embedOrigin,
135
+ title: route === "broadcast" ? "Auphere \u2014 Campa\xF1as WhatsApp" : "Auphere \u2014 Conectar WhatsApp",
136
+ onClose: () => {
137
+ activeOverlay = null;
138
+ handlers.onExit?.();
139
+ if (!settled) {
140
+ settled = true;
141
+ resolve();
142
+ }
143
+ },
144
+ onMessage: (type, payload) => {
145
+ switch (type) {
146
+ case MSG.READY:
147
+ overlay.post(MSG.INIT, {
148
+ mode: route,
149
+ token: session.sessionToken,
150
+ appearance: config.appearance ?? {},
151
+ locale: config.locale ?? document.documentElement.lang ?? "es",
152
+ recipients: handlers.recipients ?? []
153
+ });
154
+ if (!settled) {
155
+ settled = true;
156
+ resolve();
157
+ }
158
+ break;
159
+ case MSG.TOKEN_EXPIRING:
160
+ void mintSession().then((fresh) => overlay.post(MSG.TOKEN, { token: fresh.sessionToken })).catch(() => overlay.post(MSG.ERROR, { code: "token_refresh_failed" }));
161
+ break;
162
+ case MSG.STATUS: {
163
+ const value = payload["whatsappConnected"];
164
+ if (typeof value === "boolean") setStatus(value ? "connected" : "not_connected");
165
+ break;
166
+ }
167
+ case MSG.BROADCAST_DONE:
168
+ handlers.onDone?.({
169
+ broadcastId: String(payload["broadcastId"] ?? ""),
170
+ accepted: Number(payload["accepted"] ?? 0)
171
+ });
172
+ break;
173
+ case MSG.CONNECTED:
174
+ setStatus("connected");
175
+ handlers.onConnected?.({
176
+ displayPhoneNumber: typeof payload["displayPhoneNumber"] === "string" ? payload["displayPhoneNumber"] : null
177
+ });
178
+ break;
179
+ case MSG.CLOSE:
180
+ overlay.close();
181
+ break;
182
+ case MSG.ERROR:
183
+ if (!settled) {
184
+ settled = true;
185
+ overlay.close();
186
+ reject(new Error(`@auphere/embed: ${String(payload["code"] ?? "embed_error")}`));
187
+ }
188
+ break;
189
+ }
190
+ }
191
+ });
192
+ activeOverlay = overlay;
193
+ });
194
+ }
195
+ return {
196
+ getStatus: () => status,
197
+ async refreshStatus() {
198
+ await mintSession();
199
+ return status;
200
+ },
201
+ onStatusChange(listener) {
202
+ listeners.add(listener);
203
+ return () => listeners.delete(listener);
204
+ },
205
+ openBroadcast(options) {
206
+ return open("broadcast", options);
207
+ },
208
+ connectWhatsApp(options = {}) {
209
+ return open("signup", options);
210
+ },
211
+ destroy() {
212
+ activeOverlay?.close();
213
+ activeOverlay = null;
214
+ listeners.clear();
215
+ }
216
+ };
217
+ }
218
+
219
+ export { createAuphere };
220
+ //# sourceMappingURL=index.js.map
221
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/iframe-manager.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** Production embed origin. Overridable per-instance via `config.embedOrigin` (dev/staging). */\nexport const DEFAULT_EMBED_ORIGIN = \"https://embed.auphere.com\";\n\n/** postMessage envelope namespace — every message is `{ type: \"auph:…\" }`. */\nexport const MSG = {\n // iframe → loader\n READY: \"auph:ready\",\n RESIZE: \"auph:resize\",\n CLOSE: \"auph:close\",\n STATUS: \"auph:status\",\n BROADCAST_DONE: \"auph:broadcast:done\",\n TOKEN_EXPIRING: \"auph:token:expiring\",\n CONNECTED: \"auph:connected\",\n ERROR: \"auph:error\",\n // loader → iframe\n INIT: \"auph:init\",\n TOKEN: \"auph:token\",\n} as const;\n","/**\n * Fullscreen iframe overlay lifecycle (ADR-028).\n *\n * The iframe cannot escape its own box, so the LOADER styles it to\n * cover the viewport (`position: fixed; inset: 0`) while it's open —\n * the \"breakout\" pattern used by Stripe Checkout and Plaid Link.\n * Scroll-lock + focus restore are handled here; the focus trap itself\n * lives inside the iframe document (it owns the focusable elements).\n *\n * Security invariants enforced in this file:\n * - messages are only accepted from the configured embed origin AND\n * from this exact iframe's contentWindow;\n * - outgoing messages always use an explicit `targetOrigin` (never `*`);\n * - the session token travels ONLY via postMessage — never in the URL.\n */\n\nimport { MSG } from \"./constants\";\n\ntype MessageHandler = (type: string, payload: Record<string, unknown>) => void;\n\nexport interface OverlayHandle {\n post(type: string, payload?: Record<string, unknown>): void;\n close(): void;\n}\n\nconst OVERLAY_Z = 2147483646; // one below max — leaves room for the host's own emergency layers\n\nexport function openOverlay(options: {\n url: string;\n embedOrigin: string;\n title: string;\n onMessage: MessageHandler;\n onClose: () => void;\n}): OverlayHandle {\n const { url, embedOrigin, title, onMessage, onClose } = options;\n\n const previousFocus =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n const previousOverflow = document.body.style.overflow;\n\n const backdrop = document.createElement(\"div\");\n backdrop.setAttribute(\"data-auphere-embed\", \"\");\n backdrop.style.cssText = [\n \"position:fixed\",\n \"inset:0\",\n `z-index:${OVERLAY_Z}`,\n \"background:rgba(0,0,0,0.45)\",\n \"opacity:0\",\n \"transition:opacity 200ms ease\",\n ].join(\";\");\n\n const iframe = document.createElement(\"iframe\");\n iframe.src = url;\n iframe.title = title;\n iframe.setAttribute(\"role\", \"dialog\");\n iframe.setAttribute(\"aria-modal\", \"true\");\n iframe.allow = \"clipboard-write\";\n iframe.style.cssText = [\n \"position:fixed\",\n \"inset:0\",\n \"width:100%\",\n \"height:100%\",\n \"border:0\",\n `z-index:${OVERLAY_Z + 1}`,\n \"background:transparent\",\n \"color-scheme:normal\",\n ].join(\";\");\n\n let closed = false;\n\n function handleMessage(event: MessageEvent): void {\n // Origin AND source must match — a message from another tab or a\n // nested frame on the same origin is not our modal.\n if (event.origin !== embedOrigin) return;\n if (event.source !== iframe.contentWindow) return;\n const data = event.data as { type?: unknown } | null;\n if (!data || typeof data.type !== \"string\" || !data.type.startsWith(\"auph:\")) return;\n onMessage(data.type, data as Record<string, unknown>);\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") close();\n }\n\n function close(): void {\n if (closed) return;\n closed = true;\n window.removeEventListener(\"message\", handleMessage);\n document.removeEventListener(\"keydown\", handleKeydown);\n document.body.style.overflow = previousOverflow;\n backdrop.remove();\n iframe.remove();\n previousFocus?.focus?.();\n onClose();\n }\n\n window.addEventListener(\"message\", handleMessage);\n document.addEventListener(\"keydown\", handleKeydown);\n document.body.style.overflow = \"hidden\"; // scroll-lock while open\n document.body.append(backdrop, iframe);\n requestAnimationFrame(() => {\n backdrop.style.opacity = \"1\";\n });\n iframe.focus();\n\n return {\n post(type, payload = {}) {\n // Explicit targetOrigin — if the frame navigated elsewhere the\n // browser drops the message instead of leaking the token.\n iframe.contentWindow?.postMessage({ type, ...payload }, embedOrigin);\n },\n close,\n };\n}\n\nexport { MSG };\n","/**\n * `@auphere/embed` — public entrypoint (ADR-028).\n *\n * ```ts\n * import { createAuphere } from \"@auphere/embed\";\n *\n * const auphere = createAuphere({\n * partnerSlug: \"acme\",\n * fetchSession: async () => (await fetch(\"/api/auphere/session\", { method: \"POST\" })).json(),\n * });\n *\n * await auphere.openBroadcast({ recipients: [{ phone: \"+56912345678\", variables: { cliente: \"Ana\" } }] });\n * ```\n */\n\nimport { DEFAULT_EMBED_ORIGIN, MSG } from \"./constants\";\nimport { openOverlay } from \"./iframe-manager\";\nimport type {\n Auphere,\n AuphereConfig,\n ConnectionStatus,\n ConnectWhatsAppOptions,\n OpenBroadcastOptions,\n WidgetSession,\n WidgetSessionWire,\n} from \"./types\";\n\nexport type {\n Appearance,\n Auphere,\n AuphereConfig,\n BroadcastRecipient,\n ConnectionStatus,\n ConnectWhatsAppOptions,\n FetchSession,\n OpenBroadcastOptions,\n WidgetSession,\n} from \"./types\";\n\nfunction normalizeSession(raw: WidgetSession | WidgetSessionWire): WidgetSession {\n if (\"sessionToken\" in raw && typeof raw.sessionToken === \"string\") return raw;\n const wire = raw as WidgetSessionWire;\n return {\n sessionToken: wire.session_token,\n expiresIn: wire.expires_in,\n whatsapp: wire.whatsapp\n ? {\n status: wire.whatsapp.status,\n displayPhoneNumber: wire.whatsapp.display_phone_number ?? null,\n }\n : undefined,\n };\n}\n\nexport function createAuphere(config: AuphereConfig): Auphere {\n if (typeof config?.fetchSession !== \"function\") {\n throw new Error(\"@auphere/embed: `fetchSession` is required\");\n }\n if (!config.partnerSlug) {\n throw new Error(\"@auphere/embed: `partnerSlug` is required\");\n }\n const embedOrigin = (config.embedOrigin ?? DEFAULT_EMBED_ORIGIN).replace(/\\/+$/, \"\");\n\n let status: ConnectionStatus = \"unknown\";\n const listeners = new Set<(status: ConnectionStatus) => void>();\n let activeOverlay: { close(): void } | null = null;\n\n function setStatus(next: ConnectionStatus): void {\n if (next === status) return;\n status = next;\n listeners.forEach((listener) => listener(status));\n }\n\n async function mintSession(): Promise<WidgetSession> {\n const session = normalizeSession(await config.fetchSession());\n if (!session.sessionToken) {\n throw new Error(\"@auphere/embed: fetchSession returned no session token\");\n }\n if (session.whatsapp?.status) setStatus(session.whatsapp.status);\n return session;\n }\n\n function iframeUrl(route: \"broadcast\" | \"signup\"): string {\n return `${embedOrigin}/${route}?p=${encodeURIComponent(config.partnerSlug)}`;\n }\n\n async function open(\n route: \"broadcast\" | \"signup\",\n handlers: {\n recipients?: OpenBroadcastOptions[\"recipients\"];\n onDone?: OpenBroadcastOptions[\"onDone\"];\n onConnected?: ConnectWhatsAppOptions[\"onConnected\"];\n onExit?: () => void;\n },\n ): Promise<void> {\n if (activeOverlay) activeOverlay.close();\n const session = await mintSession(); // mint BEFORE mounting: fail fast, no empty modal\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const overlay = openOverlay({\n url: iframeUrl(route),\n embedOrigin,\n title: route === \"broadcast\" ? \"Auphere — Campañas WhatsApp\" : \"Auphere — Conectar WhatsApp\",\n onClose: () => {\n activeOverlay = null;\n handlers.onExit?.();\n if (!settled) {\n settled = true;\n resolve();\n }\n },\n onMessage: (type, payload) => {\n switch (type) {\n case MSG.READY:\n overlay.post(MSG.INIT, {\n mode: route,\n token: session.sessionToken,\n appearance: config.appearance ?? {},\n locale: config.locale ?? document.documentElement.lang ?? \"es\",\n recipients: handlers.recipients ?? [],\n });\n if (!settled) {\n settled = true;\n resolve();\n }\n break;\n case MSG.TOKEN_EXPIRING:\n void mintSession()\n .then((fresh) => overlay.post(MSG.TOKEN, { token: fresh.sessionToken }))\n .catch(() => overlay.post(MSG.ERROR, { code: \"token_refresh_failed\" }));\n break;\n case MSG.STATUS: {\n const value = payload[\"whatsappConnected\"];\n if (typeof value === \"boolean\") setStatus(value ? \"connected\" : \"not_connected\");\n break;\n }\n case MSG.BROADCAST_DONE:\n handlers.onDone?.({\n broadcastId: String(payload[\"broadcastId\"] ?? \"\"),\n accepted: Number(payload[\"accepted\"] ?? 0),\n });\n break;\n case MSG.CONNECTED:\n setStatus(\"connected\");\n handlers.onConnected?.({\n displayPhoneNumber:\n typeof payload[\"displayPhoneNumber\"] === \"string\"\n ? payload[\"displayPhoneNumber\"]\n : null,\n });\n break;\n case MSG.CLOSE:\n overlay.close();\n break;\n case MSG.ERROR:\n if (!settled) {\n settled = true;\n overlay.close();\n reject(new Error(`@auphere/embed: ${String(payload[\"code\"] ?? \"embed_error\")}`));\n }\n break;\n }\n },\n });\n activeOverlay = overlay;\n });\n }\n\n return {\n getStatus: () => status,\n async refreshStatus() {\n await mintSession();\n return status;\n },\n onStatusChange(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n openBroadcast(options: OpenBroadcastOptions) {\n return open(\"broadcast\", options);\n },\n connectWhatsApp(options: ConnectWhatsAppOptions = {}) {\n return open(\"signup\", options);\n },\n destroy() {\n activeOverlay?.close();\n activeOverlay = null;\n listeners.clear();\n },\n };\n}\n"]}
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/react/index.tsx
7
+ function useAuphereStatus(auphere) {
8
+ const [status, setStatus] = react.useState(() => auphere.getStatus());
9
+ react.useEffect(() => {
10
+ setStatus(auphere.getStatus());
11
+ const unsubscribe = auphere.onStatusChange(setStatus);
12
+ if (auphere.getStatus() === "unknown") {
13
+ void auphere.refreshStatus().catch(() => {
14
+ });
15
+ }
16
+ return unsubscribe;
17
+ }, [auphere]);
18
+ return status;
19
+ }
20
+ function AuphereBroadcastButton({
21
+ auphere,
22
+ recipients,
23
+ onDone,
24
+ onExit,
25
+ children,
26
+ ...buttonProps
27
+ }) {
28
+ const status = useAuphereStatus(auphere);
29
+ const [opening, setOpening] = react.useState(false);
30
+ if (status !== "connected") return null;
31
+ return /* @__PURE__ */ jsxRuntime.jsx(
32
+ "button",
33
+ {
34
+ type: "button",
35
+ ...buttonProps,
36
+ disabled: opening || buttonProps.disabled,
37
+ onClick: () => {
38
+ setOpening(true);
39
+ void auphere.openBroadcast({ recipients, onDone, onExit }).finally(() => setOpening(false));
40
+ },
41
+ children: children ?? "Campa\xF1as WhatsApp"
42
+ }
43
+ );
44
+ }
45
+
46
+ exports.AuphereBroadcastButton = AuphereBroadcastButton;
47
+ exports.useAuphereStatus = useAuphereStatus;
48
+ //# sourceMappingURL=index.cjs.map
49
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.tsx"],"names":["useState","useEffect","jsx"],"mappings":";;;;;;AAiBO,SAAS,iBAAiB,OAAA,EAAoC;AACnE,EAAA,MAAM,CAAC,QAAQ,SAAS,CAAA,GAAIA,eAA2B,MAAM,OAAA,CAAQ,WAAW,CAAA;AAChF,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,SAAA,CAAU,OAAA,CAAQ,WAAW,CAAA;AAC7B,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,cAAA,CAAe,SAAS,CAAA;AACpD,IAAA,IAAI,OAAA,CAAQ,SAAA,EAAU,KAAM,SAAA,EAAW;AACrC,MAAA,KAAK,OAAA,CAAQ,aAAA,EAAc,CAAE,KAAA,CAAM,MAAM;AAAA,MAEzC,CAAC,CAAA;AAAA,IACH;AACA,IAAA,OAAO,WAAA;AAAA,EACT,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AACZ,EAAA,OAAO,MAAA;AACT;AAiBO,SAAS,sBAAA,CAAuB;AAAA,EACrC,OAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAG;AACL,CAAA,EAAgC;AAC9B,EAAA,MAAM,MAAA,GAAS,iBAAiB,OAAO,CAAA;AACvC,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAID,eAAS,KAAK,CAAA;AAE5C,EAAA,IAAI,MAAA,KAAW,aAAa,OAAO,IAAA;AAEnC,EAAA,uBACEE,cAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,QAAA;AAAA,MACJ,GAAG,WAAA;AAAA,MACJ,QAAA,EAAU,WAAW,WAAA,CAAY,QAAA;AAAA,MACjC,SAAS,MAAM;AACb,QAAA,UAAA,CAAW,IAAI,CAAA;AACf,QAAA,KAAK,OAAA,CACF,aAAA,CAAc,EAAE,UAAA,EAAY,MAAA,EAAQ,MAAA,EAAQ,CAAA,CAC5C,OAAA,CAAQ,MAAM,UAAA,CAAW,KAAK,CAAC,CAAA;AAAA,MACpC,CAAA;AAAA,MAEC,QAAA,EAAA,QAAA,IAAY;AAAA;AAAA,GACf;AAEJ","file":"index.cjs","sourcesContent":["/**\n * React bindings for `@auphere/embed` (optional peer dependency).\n *\n * ```tsx\n * import { AuphereBroadcastButton } from \"@auphere/embed/react\";\n *\n * <AuphereBroadcastButton auphere={auphere} recipients={recipients}>\n * Campañas WhatsApp\n * </AuphereBroadcastButton>\n * ```\n */\n\nimport { useEffect, useState, type ButtonHTMLAttributes, type ReactNode } from \"react\";\n\nimport type { Auphere, BroadcastRecipient, ConnectionStatus, OpenBroadcastOptions } from \"../types\";\n\n/** Live connection status for gating your own UI. */\nexport function useAuphereStatus(auphere: Auphere): ConnectionStatus {\n const [status, setStatus] = useState<ConnectionStatus>(() => auphere.getStatus());\n useEffect(() => {\n setStatus(auphere.getStatus());\n const unsubscribe = auphere.onStatusChange(setStatus);\n if (auphere.getStatus() === \"unknown\") {\n void auphere.refreshStatus().catch(() => {\n /* stays \"unknown\" — button stays hidden */\n });\n }\n return unsubscribe;\n }, [auphere]);\n return status;\n}\n\nexport interface AuphereBroadcastButtonProps\n extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\"> {\n auphere: Auphere;\n /** The audience, from YOUR data (CRM, table selection…). */\n recipients: BroadcastRecipient[];\n onDone?: OpenBroadcastOptions[\"onDone\"];\n onExit?: OpenBroadcastOptions[\"onExit\"];\n children?: ReactNode;\n}\n\n/**\n * Renders its children as a button ONLY while the client's WhatsApp is\n * connected — the gating decision of ADR-028. Style it like any button\n * (className/style pass through).\n */\nexport function AuphereBroadcastButton({\n auphere,\n recipients,\n onDone,\n onExit,\n children,\n ...buttonProps\n}: AuphereBroadcastButtonProps) {\n const status = useAuphereStatus(auphere);\n const [opening, setOpening] = useState(false);\n\n if (status !== \"connected\") return null;\n\n return (\n <button\n type=\"button\"\n {...buttonProps}\n disabled={opening || buttonProps.disabled}\n onClick={() => {\n setOpening(true);\n void auphere\n .openBroadcast({ recipients, onDone, onExit })\n .finally(() => setOpening(false));\n }}\n >\n {children ?? \"Campañas WhatsApp\"}\n </button>\n );\n}\n\nexport type { Auphere, BroadcastRecipient, ConnectionStatus };\n"]}
@@ -0,0 +1,22 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode } from 'react';
3
+ import { a as Auphere, B as BroadcastRecipient, O as OpenBroadcastOptions, c as ConnectionStatus } from '../types-BomPnFf3.cjs';
4
+
5
+ /** Live connection status for gating your own UI. */
6
+ declare function useAuphereStatus(auphere: Auphere): ConnectionStatus;
7
+ interface AuphereBroadcastButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
8
+ auphere: Auphere;
9
+ /** The audience, from YOUR data (CRM, table selection…). */
10
+ recipients: BroadcastRecipient[];
11
+ onDone?: OpenBroadcastOptions["onDone"];
12
+ onExit?: OpenBroadcastOptions["onExit"];
13
+ children?: ReactNode;
14
+ }
15
+ /**
16
+ * Renders its children as a button ONLY while the client's WhatsApp is
17
+ * connected — the gating decision of ADR-028. Style it like any button
18
+ * (className/style pass through).
19
+ */
20
+ declare function AuphereBroadcastButton({ auphere, recipients, onDone, onExit, children, ...buttonProps }: AuphereBroadcastButtonProps): react.JSX.Element | null;
21
+
22
+ export { Auphere, AuphereBroadcastButton, type AuphereBroadcastButtonProps, BroadcastRecipient, ConnectionStatus, useAuphereStatus };
@@ -0,0 +1,22 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode } from 'react';
3
+ import { a as Auphere, B as BroadcastRecipient, O as OpenBroadcastOptions, c as ConnectionStatus } from '../types-BomPnFf3.js';
4
+
5
+ /** Live connection status for gating your own UI. */
6
+ declare function useAuphereStatus(auphere: Auphere): ConnectionStatus;
7
+ interface AuphereBroadcastButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
8
+ auphere: Auphere;
9
+ /** The audience, from YOUR data (CRM, table selection…). */
10
+ recipients: BroadcastRecipient[];
11
+ onDone?: OpenBroadcastOptions["onDone"];
12
+ onExit?: OpenBroadcastOptions["onExit"];
13
+ children?: ReactNode;
14
+ }
15
+ /**
16
+ * Renders its children as a button ONLY while the client's WhatsApp is
17
+ * connected — the gating decision of ADR-028. Style it like any button
18
+ * (className/style pass through).
19
+ */
20
+ declare function AuphereBroadcastButton({ auphere, recipients, onDone, onExit, children, ...buttonProps }: AuphereBroadcastButtonProps): react.JSX.Element | null;
21
+
22
+ export { Auphere, AuphereBroadcastButton, type AuphereBroadcastButtonProps, BroadcastRecipient, ConnectionStatus, useAuphereStatus };
@@ -0,0 +1,46 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/react/index.tsx
5
+ function useAuphereStatus(auphere) {
6
+ const [status, setStatus] = useState(() => auphere.getStatus());
7
+ useEffect(() => {
8
+ setStatus(auphere.getStatus());
9
+ const unsubscribe = auphere.onStatusChange(setStatus);
10
+ if (auphere.getStatus() === "unknown") {
11
+ void auphere.refreshStatus().catch(() => {
12
+ });
13
+ }
14
+ return unsubscribe;
15
+ }, [auphere]);
16
+ return status;
17
+ }
18
+ function AuphereBroadcastButton({
19
+ auphere,
20
+ recipients,
21
+ onDone,
22
+ onExit,
23
+ children,
24
+ ...buttonProps
25
+ }) {
26
+ const status = useAuphereStatus(auphere);
27
+ const [opening, setOpening] = useState(false);
28
+ if (status !== "connected") return null;
29
+ return /* @__PURE__ */ jsx(
30
+ "button",
31
+ {
32
+ type: "button",
33
+ ...buttonProps,
34
+ disabled: opening || buttonProps.disabled,
35
+ onClick: () => {
36
+ setOpening(true);
37
+ void auphere.openBroadcast({ recipients, onDone, onExit }).finally(() => setOpening(false));
38
+ },
39
+ children: children ?? "Campa\xF1as WhatsApp"
40
+ }
41
+ );
42
+ }
43
+
44
+ export { AuphereBroadcastButton, useAuphereStatus };
45
+ //# sourceMappingURL=index.js.map
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.tsx"],"names":[],"mappings":";;;;AAiBO,SAAS,iBAAiB,OAAA,EAAoC;AACnE,EAAA,MAAM,CAAC,QAAQ,SAAS,CAAA,GAAI,SAA2B,MAAM,OAAA,CAAQ,WAAW,CAAA;AAChF,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,SAAA,CAAU,OAAA,CAAQ,WAAW,CAAA;AAC7B,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,cAAA,CAAe,SAAS,CAAA;AACpD,IAAA,IAAI,OAAA,CAAQ,SAAA,EAAU,KAAM,SAAA,EAAW;AACrC,MAAA,KAAK,OAAA,CAAQ,aAAA,EAAc,CAAE,KAAA,CAAM,MAAM;AAAA,MAEzC,CAAC,CAAA;AAAA,IACH;AACA,IAAA,OAAO,WAAA;AAAA,EACT,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AACZ,EAAA,OAAO,MAAA;AACT;AAiBO,SAAS,sBAAA,CAAuB;AAAA,EACrC,OAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAG;AACL,CAAA,EAAgC;AAC9B,EAAA,MAAM,MAAA,GAAS,iBAAiB,OAAO,CAAA;AACvC,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAE5C,EAAA,IAAI,MAAA,KAAW,aAAa,OAAO,IAAA;AAEnC,EAAA,uBACE,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,QAAA;AAAA,MACJ,GAAG,WAAA;AAAA,MACJ,QAAA,EAAU,WAAW,WAAA,CAAY,QAAA;AAAA,MACjC,SAAS,MAAM;AACb,QAAA,UAAA,CAAW,IAAI,CAAA;AACf,QAAA,KAAK,OAAA,CACF,aAAA,CAAc,EAAE,UAAA,EAAY,MAAA,EAAQ,MAAA,EAAQ,CAAA,CAC5C,OAAA,CAAQ,MAAM,UAAA,CAAW,KAAK,CAAC,CAAA;AAAA,MACpC,CAAA;AAAA,MAEC,QAAA,EAAA,QAAA,IAAY;AAAA;AAAA,GACf;AAEJ","file":"index.js","sourcesContent":["/**\n * React bindings for `@auphere/embed` (optional peer dependency).\n *\n * ```tsx\n * import { AuphereBroadcastButton } from \"@auphere/embed/react\";\n *\n * <AuphereBroadcastButton auphere={auphere} recipients={recipients}>\n * Campañas WhatsApp\n * </AuphereBroadcastButton>\n * ```\n */\n\nimport { useEffect, useState, type ButtonHTMLAttributes, type ReactNode } from \"react\";\n\nimport type { Auphere, BroadcastRecipient, ConnectionStatus, OpenBroadcastOptions } from \"../types\";\n\n/** Live connection status for gating your own UI. */\nexport function useAuphereStatus(auphere: Auphere): ConnectionStatus {\n const [status, setStatus] = useState<ConnectionStatus>(() => auphere.getStatus());\n useEffect(() => {\n setStatus(auphere.getStatus());\n const unsubscribe = auphere.onStatusChange(setStatus);\n if (auphere.getStatus() === \"unknown\") {\n void auphere.refreshStatus().catch(() => {\n /* stays \"unknown\" — button stays hidden */\n });\n }\n return unsubscribe;\n }, [auphere]);\n return status;\n}\n\nexport interface AuphereBroadcastButtonProps\n extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\"> {\n auphere: Auphere;\n /** The audience, from YOUR data (CRM, table selection…). */\n recipients: BroadcastRecipient[];\n onDone?: OpenBroadcastOptions[\"onDone\"];\n onExit?: OpenBroadcastOptions[\"onExit\"];\n children?: ReactNode;\n}\n\n/**\n * Renders its children as a button ONLY while the client's WhatsApp is\n * connected — the gating decision of ADR-028. Style it like any button\n * (className/style pass through).\n */\nexport function AuphereBroadcastButton({\n auphere,\n recipients,\n onDone,\n onExit,\n children,\n ...buttonProps\n}: AuphereBroadcastButtonProps) {\n const status = useAuphereStatus(auphere);\n const [opening, setOpening] = useState(false);\n\n if (status !== \"connected\") return null;\n\n return (\n <button\n type=\"button\"\n {...buttonProps}\n disabled={opening || buttonProps.disabled}\n onClick={() => {\n setOpening(true);\n void auphere\n .openBroadcast({ recipients, onDone, onExit })\n .finally(() => setOpening(false));\n }}\n >\n {children ?? \"Campañas WhatsApp\"}\n </button>\n );\n}\n\nexport type { Auphere, BroadcastRecipient, ConnectionStatus };\n"]}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Public types for `@auphere/embed` (ADR-028).
3
+ *
4
+ * The SDK is a thin loader: it renders nothing itself beyond an iframe
5
+ * overlay, and it never sees WhatsApp or Auphere credentials. All the
6
+ * heavy UI lives on the Auphere-controlled embed origin and is updated
7
+ * without you redeploying.
8
+ */
9
+ /** One WhatsApp recipient of a broadcast, with the named template variables. */
10
+ interface BroadcastRecipient {
11
+ /** E.164 phone, e.g. `+56912345678`. Spaces/dashes are tolerated. */
12
+ phone: string;
13
+ /**
14
+ * Named template variables, keys matching the template's named
15
+ * parameters (e.g. `{ cliente: "Ana", saldo_pendiente: "$12.000" }`).
16
+ */
17
+ variables?: Record<string, string>;
18
+ }
19
+ /** What your backend returns after minting `POST /v1/widget-sessions`. */
20
+ interface WidgetSession {
21
+ /** The short-lived session JWT (`session_token` from the mint response). */
22
+ sessionToken: string;
23
+ /** Seconds until expiry (`expires_in`). */
24
+ expiresIn?: number;
25
+ /** Connection state included in the mint response. */
26
+ whatsapp?: {
27
+ status: "connected" | "not_connected";
28
+ displayPhoneNumber?: string | null;
29
+ };
30
+ }
31
+ /** Raw mint response shape (snake_case) is also accepted. */
32
+ interface WidgetSessionWire {
33
+ session_token: string;
34
+ expires_in?: number;
35
+ whatsapp?: {
36
+ status: "connected" | "not_connected";
37
+ display_phone_number?: string | null;
38
+ };
39
+ }
40
+ type FetchSession = () => Promise<WidgetSession | WidgetSessionWire>;
41
+ interface Appearance {
42
+ /** Primary accent color (CSS color). */
43
+ colorPrimary?: string;
44
+ /** Border radius, e.g. `"8px"`. */
45
+ radius?: string;
46
+ /** `"light" | "dark"` — defaults to the embedding page's scheme. */
47
+ theme?: "light" | "dark";
48
+ }
49
+ interface AuphereConfig {
50
+ /**
51
+ * Called whenever the SDK needs a session token: on open and again
52
+ * when a token is about to expire. MUST be re-invocable without user
53
+ * interaction — typically a `fetch("/api/auphere/session")` to your
54
+ * backend, which mints with your secret key.
55
+ */
56
+ fetchSession: FetchSession;
57
+ /**
58
+ * Your partner slug (shown in the Auphere dashboard). Used to resolve
59
+ * the iframe's security policy before any token exists.
60
+ */
61
+ partnerSlug: string;
62
+ appearance?: Appearance;
63
+ /** BCP-47, e.g. `"es"`. Defaults to the page language. */
64
+ locale?: string;
65
+ /** Override the embed origin — staging/dev only. */
66
+ embedOrigin?: string;
67
+ }
68
+ type ConnectionStatus = "connected" | "not_connected" | "unknown";
69
+ interface OpenBroadcastOptions {
70
+ /** The audience, built from YOUR data. The modal previews and sends — it never invents recipients. */
71
+ recipients: BroadcastRecipient[];
72
+ /** Fired after the broadcast is accepted by Auphere. */
73
+ onDone?: (result: {
74
+ broadcastId: string;
75
+ accepted: number;
76
+ }) => void;
77
+ /** Fired when the modal closes (sent or not). */
78
+ onExit?: () => void;
79
+ }
80
+ interface ConnectWhatsAppOptions {
81
+ onConnected?: (info: {
82
+ displayPhoneNumber: string | null;
83
+ }) => void;
84
+ onExit?: () => void;
85
+ }
86
+ interface Auphere {
87
+ /** Last known WhatsApp connection state (from the latest session mint/refresh). */
88
+ getStatus(): ConnectionStatus;
89
+ /** Re-mint a session and refresh the status. */
90
+ refreshStatus(): Promise<ConnectionStatus>;
91
+ /** Subscribe to status flips. Returns an unsubscribe function. */
92
+ onStatusChange(listener: (status: ConnectionStatus) => void): () => void;
93
+ /** Open the broadcast modal (fullscreen iframe overlay). */
94
+ openBroadcast(options: OpenBroadcastOptions): Promise<void>;
95
+ /** Open the WhatsApp Embedded Signup flow (fullscreen iframe overlay). */
96
+ connectWhatsApp(options?: ConnectWhatsAppOptions): Promise<void>;
97
+ /** Remove any mounted iframe and listeners. */
98
+ destroy(): void;
99
+ }
100
+
101
+ export type { AuphereConfig as A, BroadcastRecipient as B, ConnectWhatsAppOptions as C, FetchSession as F, OpenBroadcastOptions as O, WidgetSession as W, Auphere as a, Appearance as b, ConnectionStatus as c };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Public types for `@auphere/embed` (ADR-028).
3
+ *
4
+ * The SDK is a thin loader: it renders nothing itself beyond an iframe
5
+ * overlay, and it never sees WhatsApp or Auphere credentials. All the
6
+ * heavy UI lives on the Auphere-controlled embed origin and is updated
7
+ * without you redeploying.
8
+ */
9
+ /** One WhatsApp recipient of a broadcast, with the named template variables. */
10
+ interface BroadcastRecipient {
11
+ /** E.164 phone, e.g. `+56912345678`. Spaces/dashes are tolerated. */
12
+ phone: string;
13
+ /**
14
+ * Named template variables, keys matching the template's named
15
+ * parameters (e.g. `{ cliente: "Ana", saldo_pendiente: "$12.000" }`).
16
+ */
17
+ variables?: Record<string, string>;
18
+ }
19
+ /** What your backend returns after minting `POST /v1/widget-sessions`. */
20
+ interface WidgetSession {
21
+ /** The short-lived session JWT (`session_token` from the mint response). */
22
+ sessionToken: string;
23
+ /** Seconds until expiry (`expires_in`). */
24
+ expiresIn?: number;
25
+ /** Connection state included in the mint response. */
26
+ whatsapp?: {
27
+ status: "connected" | "not_connected";
28
+ displayPhoneNumber?: string | null;
29
+ };
30
+ }
31
+ /** Raw mint response shape (snake_case) is also accepted. */
32
+ interface WidgetSessionWire {
33
+ session_token: string;
34
+ expires_in?: number;
35
+ whatsapp?: {
36
+ status: "connected" | "not_connected";
37
+ display_phone_number?: string | null;
38
+ };
39
+ }
40
+ type FetchSession = () => Promise<WidgetSession | WidgetSessionWire>;
41
+ interface Appearance {
42
+ /** Primary accent color (CSS color). */
43
+ colorPrimary?: string;
44
+ /** Border radius, e.g. `"8px"`. */
45
+ radius?: string;
46
+ /** `"light" | "dark"` — defaults to the embedding page's scheme. */
47
+ theme?: "light" | "dark";
48
+ }
49
+ interface AuphereConfig {
50
+ /**
51
+ * Called whenever the SDK needs a session token: on open and again
52
+ * when a token is about to expire. MUST be re-invocable without user
53
+ * interaction — typically a `fetch("/api/auphere/session")` to your
54
+ * backend, which mints with your secret key.
55
+ */
56
+ fetchSession: FetchSession;
57
+ /**
58
+ * Your partner slug (shown in the Auphere dashboard). Used to resolve
59
+ * the iframe's security policy before any token exists.
60
+ */
61
+ partnerSlug: string;
62
+ appearance?: Appearance;
63
+ /** BCP-47, e.g. `"es"`. Defaults to the page language. */
64
+ locale?: string;
65
+ /** Override the embed origin — staging/dev only. */
66
+ embedOrigin?: string;
67
+ }
68
+ type ConnectionStatus = "connected" | "not_connected" | "unknown";
69
+ interface OpenBroadcastOptions {
70
+ /** The audience, built from YOUR data. The modal previews and sends — it never invents recipients. */
71
+ recipients: BroadcastRecipient[];
72
+ /** Fired after the broadcast is accepted by Auphere. */
73
+ onDone?: (result: {
74
+ broadcastId: string;
75
+ accepted: number;
76
+ }) => void;
77
+ /** Fired when the modal closes (sent or not). */
78
+ onExit?: () => void;
79
+ }
80
+ interface ConnectWhatsAppOptions {
81
+ onConnected?: (info: {
82
+ displayPhoneNumber: string | null;
83
+ }) => void;
84
+ onExit?: () => void;
85
+ }
86
+ interface Auphere {
87
+ /** Last known WhatsApp connection state (from the latest session mint/refresh). */
88
+ getStatus(): ConnectionStatus;
89
+ /** Re-mint a session and refresh the status. */
90
+ refreshStatus(): Promise<ConnectionStatus>;
91
+ /** Subscribe to status flips. Returns an unsubscribe function. */
92
+ onStatusChange(listener: (status: ConnectionStatus) => void): () => void;
93
+ /** Open the broadcast modal (fullscreen iframe overlay). */
94
+ openBroadcast(options: OpenBroadcastOptions): Promise<void>;
95
+ /** Open the WhatsApp Embedded Signup flow (fullscreen iframe overlay). */
96
+ connectWhatsApp(options?: ConnectWhatsAppOptions): Promise<void>;
97
+ /** Remove any mounted iframe and listeners. */
98
+ destroy(): void;
99
+ }
100
+
101
+ export type { AuphereConfig as A, BroadcastRecipient as B, ConnectWhatsAppOptions as C, FetchSession as F, OpenBroadcastOptions as O, WidgetSession as W, Auphere as a, Appearance as b, ConnectionStatus as c };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@auphere/embed",
3
+ "version": "0.1.0",
4
+ "description": "Auphere embed SDK — WhatsApp template broadcasts inside your own product. Thin loader: mounts the Auphere iframe, handshakes a short-lived session token, never touches credentials.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/ladrian-dev/auphere-admin/tree/main/packages/embed-sdk#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ladrian-dev/auphere-admin.git",
10
+ "directory": "packages/embed-sdk"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/ladrian-dev/auphere-admin/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs"
24
+ },
25
+ "./react": {
26
+ "types": "./dist/react/index.d.ts",
27
+ "import": "./dist/react/index.js",
28
+ "require": "./dist/react/index.cjs"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
35
+ "sideEffects": false,
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run",
40
+ "prepublishOnly": "npm run build"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=18"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "react": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "devDependencies": {
51
+ "@types/react": "^19",
52
+ "jsdom": "^29.1.1",
53
+ "react": "19.2.4",
54
+ "tsup": "^8.3.5",
55
+ "typescript": "^5.7.0",
56
+ "vitest": "^4.1.5"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "keywords": [
62
+ "auphere",
63
+ "whatsapp",
64
+ "embed",
65
+ "widget",
66
+ "broadcast"
67
+ ]
68
+ }