@zeroclickai/offers-sdk 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 +322 -0
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +564 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +564 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +12 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# @zeroclickai/offers-sdk
|
|
2
|
+
|
|
3
|
+
Official SDK for ZeroClick Offers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @zeroclickai/offers-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
- **`getOffers()`** - Server-side only (called from your backend, requires client IP forwarding)
|
|
14
|
+
- **`trackOfferImpressions()`** - Client-side (called directly from browser/client)
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Client/Browser ──────────────────────────────────┐
|
|
18
|
+
│ │
|
|
19
|
+
│ (1) Request offers │ (3) Track impressions
|
|
20
|
+
↓ ↓
|
|
21
|
+
Your Backend ──(2)──> ZeroClick API ZeroClick API
|
|
22
|
+
(SDK.getOffers) /api/v2/offers /api/v2/impressions
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### Server-Side: Fetching Offers
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// backend/api/offers.ts (Express, Next.js API route, etc.)
|
|
31
|
+
import { ZeroClick } from '@zeroclickai/offers-sdk';
|
|
32
|
+
|
|
33
|
+
// Initialize once on server startup
|
|
34
|
+
ZeroClick.initialize({
|
|
35
|
+
apiKey: 'your-api-key',
|
|
36
|
+
identity: {
|
|
37
|
+
userId: 'user-123',
|
|
38
|
+
userLocale: 'en-US',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// In your API route handler
|
|
43
|
+
app.post('/api/offers', async (req, res) => {
|
|
44
|
+
const offers = await ZeroClick.getOffers({
|
|
45
|
+
ipAddress: req.ip || req.headers['x-forwarded-for'], // Required
|
|
46
|
+
userAgent: req.headers['user-agent'], // Optional
|
|
47
|
+
query: req.body.query,
|
|
48
|
+
limit: 3,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
res.json(offers);
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Client-Side: Tracking Impressions
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// frontend/client.ts
|
|
59
|
+
import { ZeroClick } from '@zeroclickai/offers-sdk';
|
|
60
|
+
|
|
61
|
+
// Initialize (no API key needed for tracking)
|
|
62
|
+
ZeroClick.initialize();
|
|
63
|
+
|
|
64
|
+
// Track when offers are displayed to the user
|
|
65
|
+
await ZeroClick.trackOfferImpressions(['offer-123', 'offer-456']);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## API
|
|
69
|
+
|
|
70
|
+
### `ZeroClick.initialize(config?)`
|
|
71
|
+
|
|
72
|
+
Initialize the SDK. Must be called before other methods.
|
|
73
|
+
|
|
74
|
+
**Server-side initialization:**
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
ZeroClick.initialize({
|
|
78
|
+
apiKey: 'your-api-key', // Required for getOffers
|
|
79
|
+
identity?: {
|
|
80
|
+
userId?: string; // Your user's ID
|
|
81
|
+
userEmailSha256?: string; // SHA-256 hash of email (lowercase, trimmed)
|
|
82
|
+
userPhoneNumberSha256?: string; // SHA-256 hash of phone (E.164 format)
|
|
83
|
+
userLocale?: string; // e.g., 'en-US'
|
|
84
|
+
userSessionId?: string; // Session identifier
|
|
85
|
+
groupingId?: string; // Grouping ID for analytics segmentation
|
|
86
|
+
};
|
|
87
|
+
baseUrl?: string; // Custom API base URL (default: https://zeroclick.dev)
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Client-side initialization (tracking only):**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
ZeroClick.initialize(); // No API key needed
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `ZeroClick.getOffers(options)` 🔒 Server-side only
|
|
98
|
+
|
|
99
|
+
Fetch ranked offers based on intent signals. **Must be called from your backend server** (no CORS access from browsers). Requires `apiKey` to be set during initialization.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const offers = await ZeroClick.getOffers({
|
|
103
|
+
ipAddress: string; // Required - Client IP from incoming request
|
|
104
|
+
query?: string; // Search query for offers
|
|
105
|
+
limit?: number; // Max offers to return (default: 1)
|
|
106
|
+
userAgent?: string; // Optional - Client's user agent
|
|
107
|
+
origin?: string; // Optional - Client's origin/referer
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Example:**
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
app.post('/api/offers', async (req, res) => {
|
|
115
|
+
const offers = await ZeroClick.getOffers({
|
|
116
|
+
ipAddress: req.ip || req.headers['x-forwarded-for'],
|
|
117
|
+
userAgent: req.headers['user-agent'],
|
|
118
|
+
query: req.body.query,
|
|
119
|
+
limit: 3,
|
|
120
|
+
});
|
|
121
|
+
res.json(offers);
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Returns `Promise<Offer[]>` — an array of offers ranked by relevance.
|
|
126
|
+
|
|
127
|
+
### `ZeroClick.trackOfferImpressions(ids)` ✅ Client-side
|
|
128
|
+
|
|
129
|
+
Track offer impressions for analytics. Does not require an API key. **Should be called from the client/browser** when offers are displayed to the user.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
await ZeroClick.trackOfferImpressions(['offer-123', 'offer-456']);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `ZeroClick.renderOffer(offer, container, options?)` 🖥️ Browser-only
|
|
136
|
+
|
|
137
|
+
Render an offer as an iframe inside a DOM container. Creates an iframe, sends offer data via `postMessage` over a dedicated `MessageChannel`, and returns a handle for ongoing control.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const handle = await ZeroClick.renderOffer(offer, '#ad-container', {
|
|
141
|
+
width: '100%',
|
|
142
|
+
height: '120px',
|
|
143
|
+
mode: 'dark',
|
|
144
|
+
style: {
|
|
145
|
+
background: '#1a1a2e',
|
|
146
|
+
textColor: '#ffffff',
|
|
147
|
+
buttonBackground: '#4a9eff',
|
|
148
|
+
},
|
|
149
|
+
onCtaClick: ({ url }) => window.open(url, '_blank'),
|
|
150
|
+
autoHeight: true,
|
|
151
|
+
minHeight: 50,
|
|
152
|
+
maxHeight: 300,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Update styles later (e.g., on theme change)
|
|
156
|
+
handle.updateStyle({ background: '#fff', textColor: '#000' });
|
|
157
|
+
|
|
158
|
+
// Remove the iframe
|
|
159
|
+
handle.destroy();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Options:**
|
|
163
|
+
|
|
164
|
+
| Option | Type | Default | Description |
|
|
165
|
+
| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
166
|
+
| `mode` | `'light' \| 'dark'` | — | Theme mode passed to iframe URL |
|
|
167
|
+
| `style` | `IframeStyleConfig` | — | Custom colors to match your surface theme |
|
|
168
|
+
| `width` | `string` | `'300px'` | CSS width |
|
|
169
|
+
| `height` | `string` | `'250px'` | CSS height |
|
|
170
|
+
| `sandbox` | `string` | `'allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox'` | Iframe sandbox attributes |
|
|
171
|
+
| `onCtaClick` | `(event: CtaClickEvent) => void` | — | Callback when user clicks the CTA (use in environments where popups are blocked) |
|
|
172
|
+
| `autoHeight` | `boolean` | `false` | Enable dynamic iframe height based on content |
|
|
173
|
+
| `minHeight` | `number` | `50` | Minimum height in px when `autoHeight` is enabled |
|
|
174
|
+
| `maxHeight` | `number` | — | Maximum height in px when `autoHeight` is enabled |
|
|
175
|
+
|
|
176
|
+
**Style configuration (`IframeStyleConfig`):**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
{
|
|
180
|
+
background?: string; // e.g., '#1a1a2e'
|
|
181
|
+
backgroundHover?: string; // e.g., '#252540'
|
|
182
|
+
borderColor?: string; // e.g., '#333333'
|
|
183
|
+
borderRadius?: string; // e.g., '12px'
|
|
184
|
+
buttonBackground?: string; // e.g., '#4a9eff'
|
|
185
|
+
buttonBackgroundHover?: string;
|
|
186
|
+
buttonTextColor?: string; // e.g., '#ffffff'
|
|
187
|
+
textColor?: string; // e.g., '#ffffff'
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `ZeroClick.getIframeUrl(offer)`
|
|
192
|
+
|
|
193
|
+
Returns the iframe source URL from the offer's UI configuration, or `null` if the offer doesn't have iframe rendering configured.
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const url = ZeroClick.getIframeUrl(offer);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### `ZeroClick.getIframeTag(offer, options?)`
|
|
200
|
+
|
|
201
|
+
Generate an HTML `<iframe>` tag string for server-side rendering. Returns `null` if the offer has no iframe URL.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const html = ZeroClick.getIframeTag(offer, { width: '100%', height: '120px' });
|
|
205
|
+
if (html) {
|
|
206
|
+
container.innerHTML = html;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Offer Schema
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
interface Offer {
|
|
214
|
+
id: string;
|
|
215
|
+
title: string | null;
|
|
216
|
+
subtitle: string | null;
|
|
217
|
+
content: string | null;
|
|
218
|
+
cta: string | null;
|
|
219
|
+
clickUrl: string;
|
|
220
|
+
rawUrlEncoded: string | null;
|
|
221
|
+
imageUrl: string | null;
|
|
222
|
+
metadata: Record<string, unknown> | null;
|
|
223
|
+
context: string | null;
|
|
224
|
+
brand: { name; description; url; iconUrl } | null;
|
|
225
|
+
product: { productId; sku; title; description; category; subcategory; image; availability; metadata } | null;
|
|
226
|
+
price: { amount; currency; originalPrice; discount; interval } | null;
|
|
227
|
+
location: { text; address; city; state; zip; distance; distanceUnit; coordinates; hours } | null;
|
|
228
|
+
media: { title; url; description; contentType } | null;
|
|
229
|
+
rating: { value; scale; count } | null;
|
|
230
|
+
ui?: { type: string; url: string } | null;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## TypeScript
|
|
235
|
+
|
|
236
|
+
All types are exported for TypeScript users:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import type {
|
|
240
|
+
Offer,
|
|
241
|
+
ZeroClickConfig,
|
|
242
|
+
GetOffersOptions,
|
|
243
|
+
Identity,
|
|
244
|
+
IframeRenderOptions,
|
|
245
|
+
IframeStyleConfig,
|
|
246
|
+
CtaClickEvent,
|
|
247
|
+
RenderHandle,
|
|
248
|
+
} from '@zeroclickai/offers-sdk';
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Common Integration Patterns
|
|
252
|
+
|
|
253
|
+
### Next.js App Router
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// app/api/offers/route.ts
|
|
257
|
+
import { ZeroClick } from '@zeroclickai/offers-sdk';
|
|
258
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
259
|
+
|
|
260
|
+
ZeroClick.initialize({ apiKey: process.env.ZEROCLICK_API_KEY });
|
|
261
|
+
|
|
262
|
+
export async function POST(req: NextRequest) {
|
|
263
|
+
const { query } = await req.json();
|
|
264
|
+
const ip = req.headers.get('x-forwarded-for') || req.ip || '127.0.0.1';
|
|
265
|
+
|
|
266
|
+
const offers = await ZeroClick.getOffers({
|
|
267
|
+
ipAddress: ip,
|
|
268
|
+
userAgent: req.headers.get('user-agent') || undefined,
|
|
269
|
+
query,
|
|
270
|
+
limit: 3,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return NextResponse.json(offers);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### VS Code Extension (Webview)
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// In your webview panel setup
|
|
281
|
+
const handle = await ZeroClick.renderOffer(offer, '#ad-slot', {
|
|
282
|
+
width: '100%',
|
|
283
|
+
height: '120px',
|
|
284
|
+
mode: isDarkTheme ? 'dark' : 'light',
|
|
285
|
+
style: {
|
|
286
|
+
background: 'var(--vscode-sideBar-background)',
|
|
287
|
+
textColor: 'var(--vscode-foreground)',
|
|
288
|
+
buttonBackground: 'var(--vscode-button-background)',
|
|
289
|
+
buttonTextColor: 'var(--vscode-button-foreground)',
|
|
290
|
+
},
|
|
291
|
+
autoHeight: true,
|
|
292
|
+
// VS Code blocks window.open in webviews — handle clicks manually
|
|
293
|
+
onCtaClick: ({ url }) => vscode.env.openExternal(vscode.Uri.parse(url)),
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Express
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { ZeroClick } from '@zeroclickai/offers-sdk';
|
|
301
|
+
import express from 'express';
|
|
302
|
+
|
|
303
|
+
const app = express();
|
|
304
|
+
app.use(express.json());
|
|
305
|
+
|
|
306
|
+
ZeroClick.initialize({ apiKey: process.env.ZEROCLICK_API_KEY });
|
|
307
|
+
|
|
308
|
+
app.post('/api/offers', async (req, res) => {
|
|
309
|
+
const offers = await ZeroClick.getOffers({
|
|
310
|
+
ipAddress: req.ip || (req.headers['x-forwarded-for'] as string),
|
|
311
|
+
userAgent: req.headers['user-agent'],
|
|
312
|
+
query: req.body.query,
|
|
313
|
+
limit: 3,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
res.json(offers);
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function e(){if(typeof navigator<`u`&&navigator.userAgent)return navigator.userAgent}function t(t){let n={"Content-Type":`application/json`,"X-ZeroClick-SDK-Version":`0.1.0`};t&&(n[`x-zc-api-key`]=t);let r=e();return r&&(n[`X-ZeroClick-User-Agent`]=r),n}var n=class extends Error{constructor(e){super(e.message),this.name=`ZeroClickApiError`,this.status=e.status}};async function r(e){try{let t=await e.json();return{message:t.error?.message??t.message??`An unknown error occurred`,status:e.status}}catch{return{message:e.statusText||`An unknown error occurred`,status:e.status}}}function i(e){let{apiKey:i,baseUrl:a}=e;async function o(e,o,s){let c=`${a}${e}`,l=t(s?.authenticate??!0?i:void 0),u=await fetch(c,{method:`POST`,headers:l,body:JSON.stringify(o)});if(!u.ok)throw new n(await r(u));if(u.status!==204)return u.json()}return{post:o}}const a=`zeroclick:offer-data`,o=`zeroclick:style-update`,s=`zeroclick:cta-click`,c=`zeroclick:resize`;var l=class e{static initialize(t={}){e.config=t,e.apiClient=i({apiKey:t.apiKey,baseUrl:t.baseUrl??`https://zeroclick.dev`})}static isInitialized(){return e.config!==null&&e.apiClient!==null}static reset(){e.config=null,e.apiClient=null}static async getOffers(t){if(e.ensureInitialized(),!e.config.apiKey)throw Error(`ZeroClick: apiKey is required for getOffers`);let{ipAddress:n,limit:r=1,query:i,userAgent:a,origin:o}=t,{identity:s}=e.config,c={method:`server`,ipAddress:n,limit:r,query:i};return a&&(c.userAgent=a),o&&(c.origin=o),s&&(s.userId&&(c.userId=s.userId),s.userEmailSha256&&(c.userEmailSha256=s.userEmailSha256),s.userPhoneNumberSha256&&(c.userPhoneNumberSha256=s.userPhoneNumberSha256),s.userSessionId&&(c.userSessionId=s.userSessionId),s.userLocale&&(c.userLocale=s.userLocale),s.groupingId&&(c.groupingId=s.groupingId)),await e.apiClient.post(`/api/v2/offers`,c)??[]}static async trackOfferImpressions(t){if(e.ensureInitialized(),!t||t.length===0)throw Error(`ZeroClick: ids array cannot be empty`);await e.apiClient.post(`/api/v2/impressions`,{ids:t},{authenticate:!1})}static getIframeUrl(e){return e.ui?.type===`iframe`&&e.ui.url?e.ui.url:null}static getIframeTag(t,n){let r=e.getIframeUrl(t);if(!r)return null;let{width:i=`300px`,height:a=`250px`,sandbox:o=`allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox`,style:s}=n??{},c=s?.borderColor??`#e0e0e0`,l=s?.borderRadius??`12px`;return`<iframe
|
|
2
|
+
src="${r}"
|
|
3
|
+
width="${i}"
|
|
4
|
+
height="${a}"
|
|
5
|
+
sandbox="${o}"
|
|
6
|
+
frameborder="0"
|
|
7
|
+
scrolling="no"
|
|
8
|
+
loading="lazy"
|
|
9
|
+
data-zeroclick-offer="${t.id}"
|
|
10
|
+
style="background: transparent; border: 1px solid ${c}; border-radius: ${l}; display: block;"
|
|
11
|
+
></iframe>`}static async renderOffer(t,n,r){if(e.ensureInitialized(),typeof window>`u`)throw Error(`ZeroClick: renderOffer() can only be called in browser environments`);let i=typeof n==`string`?document.querySelector(n):n;if(!i)throw Error(`ZeroClick: Container not found: ${n}`);let l={updateStyle(){},destroy(){}},u=e.getIframeUrl(t);if(!u)return l;let d=null;try{let n=e.getIframeTag(t,r);if(!n)return l;if(i.innerHTML=n,d=i.querySelector(`iframe`),!d)throw Error(`Failed to create iframe element`);await new Promise((e,t)=>{let n=setTimeout(()=>t(Error(`Iframe load timeout`)),5e3);d.addEventListener(`load`,()=>{clearTimeout(n),e()},{once:!0}),d.addEventListener(`error`,()=>{clearTimeout(n),t(Error(`Iframe load error`))},{once:!0})});let f=new URL(u).origin,p=new MessageChannel,m=r?.onCtaClick,h=r?.autoHeight??!1,g=r?.minHeight??50,_=r?.maxHeight,v=d;p.port1.onmessage=e=>{let t=e.data;if(t?.type===s&&t?.url){m&&m({url:t.url,offerId:t.offerId});return}if(h&&t?.type===c&&t?.height){let e=Math.max(g,Math.min(t.height,_??1/0));v.style.height=`${e}px`}};let y={type:a,version:`1.0`,offer:t,style:r?.style,autoHeight:h,timestamp:Date.now()};return d.contentWindow?.postMessage(y,f,[p.port2]),{updateStyle(e){let t={type:o,version:`1.0`,style:e,timestamp:Date.now()};p.port1.postMessage(t)},destroy(){p.port1.close(),v.remove()}}}catch(e){return console.error(`ZeroClick: Failed to load iframe:`,e),d&&d.remove(),i.innerHTML=``,l}}static ensureInitialized(){if(!e.isInitialized())throw Error(`ZeroClick: SDK not initialized. Call ZeroClick.initialize() first.`)}};l.config=null,l.apiClient=null,exports.CTA_CLICK_MESSAGE_TYPE=s,exports.OFFER_DATA_MESSAGE_TYPE=a,exports.RESIZE_MESSAGE_TYPE=c,exports.STYLE_UPDATE_MESSAGE_TYPE=o,exports.ZeroClick=l,exports.ZeroClickApiError=n;
|
|
12
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/utils.ts","../src/context.ts","../src/api.ts","../src/types.ts","../src/client.ts"],"sourcesContent":["// Ambient declarations for environment detection\ndeclare const browser: unknown;\ndeclare const chrome: { runtime?: { id?: string } };\ndeclare const process: { versions?: { node?: string } } | undefined;\n\n/**\n * Storage key for anonymous ID\n */\nconst ANONYMOUS_ID_KEY = 'zeroclick_anonymous_id';\n\n/**\n * In-memory session ID (not persisted across sessions)\n */\nlet sessionId: string | null = null;\n\n/**\n * In-memory storage fallback for non-browser environments\n */\nconst memoryStorage: Record<string, string> = {};\n\n/**\n * Check if localStorage is available\n */\nfunction isLocalStorageAvailable(): boolean {\n try {\n if (typeof window === 'undefined' || !window.localStorage) {\n return false;\n }\n const testKey = '__zeroclick_test__';\n window.localStorage.setItem(testKey, 'test');\n window.localStorage.removeItem(testKey);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get a value from storage (localStorage or memory fallback)\n */\nexport function getStoredValue(key: string): string | null {\n if (isLocalStorageAvailable()) {\n return window.localStorage.getItem(key);\n }\n return memoryStorage[key] ?? null;\n}\n\n/**\n * Set a value in storage (localStorage or memory fallback)\n */\nexport function setStoredValue(key: string, value: string): void {\n if (isLocalStorageAvailable()) {\n window.localStorage.setItem(key, value);\n } else {\n memoryStorage[key] = value;\n }\n}\n\n/**\n * Generate a UUID v4\n */\nexport function generateUuid(): string {\n // Use crypto.randomUUID if available (modern browsers and Node 19+)\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Fallback to manual generation\n // eslint-disable-next-line no-bitwise\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0; // eslint-disable-line no-bitwise\n const v = c === 'x' ? r : (r & 0x3) | 0x8; // eslint-disable-line no-bitwise\n return v.toString(16);\n });\n}\n\n/**\n * Get or generate the anonymous ID for this client (persisted across sessions)\n */\nexport function getAnonymousId(): string {\n let id = getStoredValue(ANONYMOUS_ID_KEY);\n if (!id) {\n id = generateUuid();\n setStoredValue(ANONYMOUS_ID_KEY, id);\n }\n return id;\n}\n\n/**\n * Get or generate the session ID for this session (not persisted)\n */\nexport function getSessionId(): string {\n if (!sessionId) {\n sessionId = generateUuid();\n }\n return sessionId;\n}\n\n/**\n * Detect the current runtime environment\n */\nexport type RuntimeEnvironment = 'browser' | 'node' | 'extension' | 'unknown';\n\nexport function detectEnvironment(): RuntimeEnvironment {\n // Check for browser\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Check for extension environment (Chrome/Firefox)\n if ((typeof chrome !== 'undefined' && chrome.runtime?.id) || typeof browser !== 'undefined') {\n return 'extension';\n }\n return 'browser';\n }\n\n // Check for Node.js\n if (typeof process !== 'undefined' && process.versions?.node) {\n return 'node';\n }\n\n return 'unknown';\n}\n\n/**\n * Get the user agent string if available\n */\nexport function getUserAgent(): string | undefined {\n if (typeof navigator !== 'undefined' && navigator.userAgent) {\n return navigator.userAgent;\n }\n return undefined;\n}\n","import { getUserAgent } from './utils.js';\n\n/**\n * SDK version - will be replaced by build process or read from package\n */\nexport const SDK_VERSION = '0.1.0';\n\n/**\n * Build headers for API requests\n */\nexport function buildRequestHeaders(apiKey?: string): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-ZeroClick-SDK-Version': SDK_VERSION,\n };\n\n if (apiKey) {\n headers['x-zc-api-key'] = apiKey;\n }\n\n const userAgent = getUserAgent();\n if (userAgent) {\n headers['X-ZeroClick-User-Agent'] = userAgent;\n }\n\n return headers;\n}\n","import { buildRequestHeaders } from './context.js';\nimport type { ApiError } from './types.js';\n\n/**\n * Default API base URL\n */\nexport const DEFAULT_BASE_URL = 'https://zeroclick.dev';\n\n/**\n * Custom error class for API errors\n */\nexport class ZeroClickApiError extends Error {\n public readonly status: number;\n\n constructor(error: ApiError) {\n super(error.message);\n this.name = 'ZeroClickApiError';\n this.status = error.status;\n }\n}\n\n/**\n * Parse error response from API\n *\n * Spec format: { error: { message: string } }\n */\nasync function parseErrorResponse(response: Response): Promise<ApiError> {\n try {\n const data = await response.json();\n return {\n message: data.error?.message ?? data.message ?? 'An unknown error occurred',\n status: response.status,\n };\n } catch {\n return {\n message: response.statusText || 'An unknown error occurred',\n status: response.status,\n };\n }\n}\n\n/**\n * API client configuration\n */\ninterface ApiClientConfig {\n apiKey?: string;\n baseUrl: string;\n}\n\n/**\n * Create an API client instance\n */\nexport function createApiClient(config: ApiClientConfig) {\n const { apiKey, baseUrl } = config;\n\n /**\n * Make a POST request to the API\n *\n * Returns parsed JSON for responses with content, or undefined for 204 No Content.\n */\n async function post<T = void>(endpoint: string, body: Record<string, unknown>, options?: { authenticate?: boolean }): Promise<T> {\n const url = `${baseUrl}${endpoint}`;\n const authenticate = options?.authenticate ?? true;\n const headers = buildRequestHeaders(authenticate ? apiKey : undefined);\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorData = await parseErrorResponse(response);\n throw new ZeroClickApiError(errorData);\n }\n\n // Handle 204 No Content (e.g., impression tracking)\n if (response.status === 204) {\n return undefined as T;\n }\n\n return response.json() as Promise<T>;\n }\n\n return { post };\n}\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\n","/**\n * User identity context provided during initialization\n */\nexport interface Identity {\n /** Tenant client user ID */\n userId?: string;\n /** SHA-256 hash of user email address (lowercase, trimmed before hashing) */\n userEmailSha256?: string;\n /** SHA-256 hash of user phone number (E.164 format before hashing) */\n userPhoneNumberSha256?: string;\n /** User locale/language code (e.g., 'en-US') */\n userLocale?: string;\n /** Session identifier for session-level tracking */\n userSessionId?: string;\n /** Tenant-defined grouping ID for analytics segmentation */\n groupingId?: string;\n}\n\n/**\n * Configuration for initializing the ZeroClick SDK\n */\nexport interface ZeroClickConfig {\n /** ZeroClick Tenant Client API key (required for getOffers) */\n apiKey?: string;\n /** User identity context */\n identity?: Identity;\n /** Base URL for the API (defaults to production) */\n baseUrl?: string;\n}\n\n/**\n * Options for fetching offers (server-side only)\n */\nexport interface GetOffersOptions {\n /** Client IP address (required for server-to-server requests) */\n ipAddress: string;\n /** Search query for offers */\n query?: string;\n /** Maximum number of offers to return (default: 1) */\n limit?: number;\n /** Client user agent string */\n userAgent?: string;\n /** Client origin/referer URL */\n origin?: string;\n}\n\n/**\n * Product availability status\n */\nexport type ProductAvailability = 'in_stock' | 'limited' | 'out_of_stock';\n\n/**\n * Distance unit for location\n */\nexport type DistanceUnit = 'km' | 'mi';\n\n/**\n * Brand information for an offer\n */\nexport interface Brand {\n /** Name of the brand */\n name: string;\n /** Description of the brand */\n description: string | null;\n /** URL of the brand's website */\n url: string | null;\n /** URL of the brand's icon or logo */\n iconUrl: string | null;\n}\n\n/**\n * Product information for an offer\n */\nexport interface Product {\n /** Product ID */\n productId: string | null;\n /** Stock keeping unit */\n sku: string | null;\n /** Product title */\n title: string;\n /** Product description */\n description: string | null;\n /** Product category */\n category: string | null;\n /** Product subcategory */\n subcategory: string | null;\n /** Product image URL */\n image: string | null;\n /** Product availability status */\n availability: ProductAvailability | null;\n /** Additional product metadata */\n metadata: Record<string, unknown> | null;\n}\n\n/**\n * Pricing information for an offer\n */\nexport interface Price {\n /** Current price amount */\n amount: string | null;\n /** Currency code (e.g., 'USD') */\n currency: string | null;\n /** Original price before discounts */\n originalPrice: string | null;\n /** Discount amount or percentage */\n discount: string | null;\n /** Billing interval for subscriptions (e.g., 'monthly', 'yearly') */\n interval: string | null;\n}\n\n/**\n * Geographical coordinates\n */\nexport interface Coordinates {\n /** Latitude */\n lat: number;\n /** Longitude */\n lng: number;\n}\n\n/**\n * Location information for an offer\n */\nexport interface Location {\n /** Full address as plain text */\n text: string;\n /** Street address */\n address: string | null;\n /** City */\n city: string | null;\n /** State or region */\n state: string | null;\n /** ZIP or postal code */\n zip: number | null;\n /** Distance from user */\n distance: number | null;\n /** Unit of distance measurement */\n distanceUnit: DistanceUnit | null;\n /** Geographical coordinates */\n coordinates: Coordinates | null;\n /** Operating hours */\n hours: string | null;\n}\n\n/**\n * Media content information for an offer\n */\nexport interface Media {\n /** Content title */\n title: string | null;\n /** Content URL */\n url: string | null;\n /** Content description */\n description: string | null;\n /** Type of content (article, video, book, etc.) */\n contentType: string;\n}\n\n/**\n * Rating information for an offer\n */\nexport interface Rating {\n /** Rating value */\n value: number;\n /** Rating scale (e.g., 5 for 5-star, 100 for percentage) */\n scale: number;\n /** Number of ratings */\n count: number | null;\n}\n\n/**\n * UI rendering configuration returned by the API\n */\nexport interface OfferUI {\n /** Render type discriminator (e.g., 'iframe') */\n type: string;\n /** URL for the UI content (e.g., iframe src) */\n url: string;\n}\n\n/**\n * An offer returned from the ZeroClick API\n */\nexport interface Offer {\n /** Unique offer ID */\n id: string;\n /** Offer title */\n title: string | null;\n /** Offer subtitle */\n subtitle: string | null;\n /** Detailed content/description */\n content: string | null;\n /** Call to action text */\n cta: string | null;\n /** Click-through URL */\n clickUrl: string;\n /** Raw URL encoded (not for LLM use) */\n rawUrlEncoded: string | null;\n /** Offer image URL */\n imageUrl: string | null;\n /** Additional offer metadata */\n metadata: Record<string, unknown> | null;\n /** Additional context about the offer */\n context: string | null;\n /** Brand information */\n brand: Brand | null;\n /** Product information */\n product: Product | null;\n /** Pricing information */\n price: Price | null;\n /** Location information */\n location: Location | null;\n /** Media content information */\n media: Media | null;\n /** Rating information */\n rating: Rating | null;\n /** UI configuration for iframe rendering (optional) */\n ui?: OfferUI | null;\n}\n\n/**\n * Context collected and sent with each API request\n */\nexport interface RequestContext {\n /** Anonymous ID for user tracking (persisted across sessions) */\n anonymousId: string;\n /** Session ID for this specific session (not persisted) */\n sessionId: string;\n /** SDK version */\n sdkVersion: string;\n /** User agent string */\n userAgent?: string;\n /** Request timestamp (ISO 8601) */\n timestamp: string;\n}\n\n/**\n * API error response\n */\nexport interface ApiError {\n /** Error message */\n message: string;\n /** HTTP status code */\n status: number;\n}\n\n/**\n * SDK initialization state\n */\nexport interface SdkState {\n /** Whether the SDK has been initialized */\n initialized: boolean;\n /** Current configuration */\n config: ZeroClickConfig | null;\n /** Anonymous ID for this client */\n anonymousId: string | null;\n}\n\n/**\n * Iframe theme/mode\n */\nexport type IframeMode = 'light' | 'dark';\n\n/**\n * Style configuration for iframe ad rendering.\n * Allows surfaces to match their host theme (e.g., VS Code color palette).\n * If not provided, the iframe renders a default black/white theme based on mode.\n */\nexport interface IframeStyleConfig {\n /** Background color (e.g., '#1a1a2e') */\n background?: string;\n /** Background hover color (e.g., '#252540') */\n backgroundHover?: string;\n /** Border color (e.g., '#333333') */\n borderColor?: string;\n /** Border radius (e.g., '12px') */\n borderRadius?: string;\n /** Button/CTA background color (e.g., '#4a9eff') */\n buttonBackground?: string;\n /** Button/CTA hover background color (e.g., '#3a8eef') */\n buttonBackgroundHover?: string;\n /** Button/CTA text color (e.g., '#ffffff') */\n buttonTextColor?: string;\n /** Primary text color (e.g., '#ffffff') */\n textColor?: string;\n}\n\n/**\n * Event passed to the onCtaClick callback when a user clicks the CTA\n */\nexport interface CtaClickEvent {\n /** The click-through URL */\n url: string;\n /** The offer ID that was clicked */\n offerId: string;\n}\n\n/**\n * Options for rendering offers as iframes (browser only)\n */\nexport interface IframeRenderOptions {\n /** Theme/mode (URL param: m) */\n mode?: IframeMode;\n /** Style configuration to match host surface theme */\n style?: IframeStyleConfig;\n /** CSS width value (e.g., '300px', '100%') */\n width?: string;\n /** CSS height value (e.g., '250px') */\n height?: string;\n /** Sandbox attributes for iframe */\n sandbox?: string;\n /** Base URL for iframe pages (default: 'https://ui.zero.click') */\n baseUrl?: string;\n /**\n * Callback invoked when the user clicks the CTA in the iframe.\n * If not provided, the click message is ignored (native navigation handles it).\n *\n * Use this in environments where popups are blocked (e.g., VS Code webviews):\n * ```typescript\n * onCtaClick: ({ url }) => vscode.env.openExternal(vscode.Uri.parse(url))\n * ```\n */\n onCtaClick?: (event: CtaClickEvent) => void;\n /** Enable auto-height: iframe reports its content height and the SDK adjusts the iframe element */\n autoHeight?: boolean;\n /** Minimum height in pixels when autoHeight is enabled (default: 50) */\n minHeight?: number;\n /** Maximum height in pixels when autoHeight is enabled */\n maxHeight?: number;\n}\n\n/**\n * Message type constant for offer data postMessage\n */\nexport const OFFER_DATA_MESSAGE_TYPE = 'zeroclick:offer-data' as const;\n\n/**\n * Message type constant for style update postMessage\n */\nexport const STYLE_UPDATE_MESSAGE_TYPE = 'zeroclick:style-update' as const;\n\n/**\n * Message type constant for CTA click postMessage (iframe → parent)\n */\nexport const CTA_CLICK_MESSAGE_TYPE = 'zeroclick:cta-click' as const;\n\n/**\n * Message type constant for resize postMessage (iframe → parent)\n */\nexport const RESIZE_MESSAGE_TYPE = 'zeroclick:resize' as const;\n\n/**\n * PostMessage payload sent from SDK to iframe on initial render\n */\nexport interface OfferDataMessage {\n /** Message type identifier */\n type: typeof OFFER_DATA_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** Full offer data */\n offer: Offer;\n /** Style configuration to match host surface theme */\n style?: IframeStyleConfig;\n /** Whether the iframe should report its content height for auto-sizing */\n autoHeight?: boolean;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * PostMessage payload sent from SDK to iframe on style update\n */\nexport interface StyleUpdateMessage {\n /** Message type identifier */\n type: typeof STYLE_UPDATE_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** Updated style configuration */\n style: IframeStyleConfig;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * PostMessage payload sent from iframe to parent when CTA is clicked\n */\nexport interface CtaClickMessage {\n /** Message type identifier */\n type: typeof CTA_CLICK_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** The click-through URL */\n url: string;\n /** The offer ID associated with this click */\n offerId: string;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * PostMessage payload sent from iframe to parent when content height changes\n */\nexport interface ResizeMessage {\n /** Message type identifier */\n type: typeof RESIZE_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** Content height in pixels */\n height: number;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * Handle returned by renderOffer for controlling a rendered iframe\n */\nexport interface RenderHandle {\n /** Send updated style configuration to the iframe via postMessage */\n updateStyle(style: IframeStyleConfig): void;\n /** Remove the iframe from the DOM */\n destroy(): void;\n}\n","import { type ApiClient, createApiClient, DEFAULT_BASE_URL } from './api.js';\nimport type {\n GetOffersOptions,\n IframeRenderOptions,\n IframeStyleConfig,\n Offer,\n OfferDataMessage,\n RenderHandle,\n StyleUpdateMessage,\n ZeroClickConfig,\n} from './types.js';\nimport { CTA_CLICK_MESSAGE_TYPE, OFFER_DATA_MESSAGE_TYPE, RESIZE_MESSAGE_TYPE, STYLE_UPDATE_MESSAGE_TYPE } from './types.js';\n\n/**\n * ZeroClick SDK client\n *\n * Provides methods for fetching offers (server-side) and tracking impressions (client-side).\n * Must be initialized with `ZeroClick.initialize()` before use.\n *\n * @example\n * ```typescript\n * import { ZeroClick } from '@zeroclickai/offers-sdk';\n *\n * // Initialize once on server startup\n * ZeroClick.initialize({\n * apiKey: 'your-api-key',\n * identity: {\n * userId: 'user-123',\n * userLocale: 'en-US',\n * },\n * });\n *\n * // In your API route handler (server-side only)\n * app.post('/api/offers', async (req, res) => {\n * const offers = await ZeroClick.getOffers({\n * ipAddress: req.ip,\n * userAgent: req.headers['user-agent'],\n * query: 'running shoes',\n * limit: 3\n * });\n * res.json(offers);\n * });\n *\n * // Track impressions (can be called client-side)\n * await ZeroClick.trackOfferImpressions(['offer-123']);\n * ```\n */\nexport class ZeroClick {\n private static config: ZeroClickConfig | null = null;\n\n private static apiClient: ApiClient | null = null;\n\n /**\n * Initialize the ZeroClick SDK\n *\n * @param config - Configuration object containing optional API key and identity\n */\n static initialize(config: ZeroClickConfig = {}): void {\n ZeroClick.config = config;\n ZeroClick.apiClient = createApiClient({\n apiKey: config.apiKey,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n });\n }\n\n /**\n * Check if the SDK has been initialized\n */\n static isInitialized(): boolean {\n return ZeroClick.config !== null && ZeroClick.apiClient !== null;\n }\n\n /**\n * Reset the SDK state (useful for testing)\n */\n static reset(): void {\n ZeroClick.config = null;\n ZeroClick.apiClient = null;\n }\n\n /**\n * Get offers based on intent signals (server-side only)\n *\n * This method must be called from your backend server, not from the browser.\n * Pass the client's IP address from the incoming request.\n *\n * @param options - Options for fetching offers (ipAddress is required)\n * @returns Promise resolving to an array of Offer objects\n * @throws Error if SDK is not initialized\n * @throws Error if apiKey was not provided during initialization\n *\n * @example\n * ```typescript\n * // In your backend (Express, Next.js API route, etc.)\n * app.post('/api/offers', async (req, res) => {\n * const clientIp = req.ip || req.headers['x-forwarded-for'];\n *\n * const offers = await ZeroClick.getOffers({\n * ipAddress: clientIp,\n * userAgent: req.headers['user-agent'],\n * query: req.body.query,\n * limit: 3,\n * });\n *\n * res.json(offers);\n * });\n * ```\n */\n static async getOffers(options: GetOffersOptions): Promise<Offer[]> {\n ZeroClick.ensureInitialized();\n\n if (!ZeroClick.config!.apiKey) {\n throw new Error('ZeroClick: apiKey is required for getOffers');\n }\n\n const { ipAddress, limit = 1, query, userAgent, origin } = options;\n const { identity } = ZeroClick.config!;\n\n const body: Record<string, unknown> = {\n method: 'server',\n ipAddress,\n limit,\n query,\n };\n\n // Add optional client context\n if (userAgent) body.userAgent = userAgent;\n if (origin) body.origin = origin;\n\n // Flatten identity fields into the request body\n if (identity) {\n if (identity.userId) body.userId = identity.userId;\n if (identity.userEmailSha256) body.userEmailSha256 = identity.userEmailSha256;\n if (identity.userPhoneNumberSha256) body.userPhoneNumberSha256 = identity.userPhoneNumberSha256;\n if (identity.userSessionId) body.userSessionId = identity.userSessionId;\n if (identity.userLocale) body.userLocale = identity.userLocale;\n if (identity.groupingId) body.groupingId = identity.groupingId;\n }\n\n const response = await ZeroClick.apiClient!.post<Offer[]>('/api/v2/offers', body);\n\n return response ?? [];\n }\n\n /**\n * Track offer impressions\n *\n * @param ids - Array of offer IDs that were displayed to the user\n * @throws Error if SDK is not initialized\n * @throws Error if ids is empty\n *\n * @example\n * ```typescript\n * await ZeroClick.trackOfferImpressions(['offer-123', 'offer-456']);\n * ```\n */\n static async trackOfferImpressions(ids: string[]): Promise<void> {\n ZeroClick.ensureInitialized();\n\n if (!ids || ids.length === 0) {\n throw new Error('ZeroClick: ids array cannot be empty');\n }\n\n await ZeroClick.apiClient!.post('/api/v2/impressions', { ids }, { authenticate: false });\n }\n\n /**\n * Get iframe URL for an offer\n *\n * Returns the iframe source URL from the offer's UI configuration.\n * Returns null if the offer doesn't have iframe rendering configured.\n *\n * @param offer - The offer to get the iframe URL for\n * @returns The iframe URL string, or null if not available\n *\n * @example\n * ```typescript\n * const url = ZeroClick.getIframeUrl(offer);\n * if (url) {\n * // Use the iframe URL\n * }\n * ```\n */\n static getIframeUrl(offer: Offer): string | null {\n if (offer.ui?.type === 'iframe' && offer.ui.url) {\n return offer.ui.url;\n }\n return null;\n }\n\n /**\n * Generate iframe HTML tag for an offer\n *\n * Returns null if the offer doesn't have iframe rendering configured.\n *\n * @param offer - The offer to generate an iframe tag for\n * @param options - Optional rendering options (width, height, sandbox, etc.)\n * @returns HTML string for the iframe element, or null if iframe URL not available\n *\n * @example\n * ```typescript\n * const html = ZeroClick.getIframeTag(offer, { width: '300px', height: '250px' });\n * if (html) {\n * container.innerHTML = html;\n * }\n * ```\n */\n static getIframeTag(offer: Offer, options?: IframeRenderOptions): string | null {\n const url = ZeroClick.getIframeUrl(offer);\n if (!url) {\n return null;\n }\n\n const {\n width = '300px',\n height = '250px',\n sandbox = 'allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox',\n style,\n } = options ?? {};\n\n const borderColor = style?.borderColor ?? '#e0e0e0';\n const borderRadius = style?.borderRadius ?? '12px';\n\n return `<iframe\n src=\"${url}\"\n width=\"${width}\"\n height=\"${height}\"\n sandbox=\"${sandbox}\"\n frameborder=\"0\"\n scrolling=\"no\"\n loading=\"lazy\"\n data-zeroclick-offer=\"${offer.id}\"\n style=\"background: transparent; border: 1px solid ${borderColor}; border-radius: ${borderRadius}; display: block;\"\n ></iframe>`;\n }\n\n /**\n * Render an offer into a container element (browser only)\n *\n * Creates an iframe, inserts it into the container, waits for it to load,\n * then sends the offer data via postMessage. Returns a no-op cleanup function\n * if the offer doesn't have iframe rendering configured.\n *\n * @param offer - The offer to render\n * @param container - DOM element or CSS selector string for the container\n * @param options - Optional rendering options (width, height, mode, tenant, etc.)\n * @returns Promise resolving to a cleanup function that removes the iframe\n * @throws Error if not in browser environment\n * @throws Error if container element not found\n *\n * @example\n * ```typescript\n * // Render into a div with id=\"ad-container\"\n * const handle = await ZeroClick.renderOffer(offer, '#ad-container', {\n * width: '300px',\n * height: '250px',\n * mode: 'dark',\n * style: { background: '#1a1a2e', textColor: '#fff', buttonBackground: '#4a9eff' }\n * });\n *\n * // Update style (e.g., on theme change)\n * handle.updateStyle({ background: '#fff', textColor: '#000' });\n *\n * // Later, remove the iframe\n * handle.destroy();\n * ```\n */\n static async renderOffer(offer: Offer, container: HTMLElement | string, options?: IframeRenderOptions): Promise<RenderHandle> {\n ZeroClick.ensureInitialized();\n\n if (typeof window === 'undefined') {\n throw new Error('ZeroClick: renderOffer() can only be called in browser environments');\n }\n\n // Resolve container\n const containerEl = typeof container === 'string' ? document.querySelector<HTMLElement>(container) : container;\n\n if (!containerEl) {\n throw new Error(`ZeroClick: Container not found: ${container}`);\n }\n\n const noopHandle: RenderHandle = {\n updateStyle() {},\n destroy() {},\n };\n\n // Check if offer has iframe URL configured\n const url = ZeroClick.getIframeUrl(offer);\n if (!url) {\n return noopHandle;\n }\n\n let iframe: HTMLIFrameElement | null = null;\n\n try {\n // Create iframe\n const iframeHtml = ZeroClick.getIframeTag(offer, options);\n if (!iframeHtml) {\n return noopHandle;\n }\n\n containerEl.innerHTML = iframeHtml;\n iframe = containerEl.querySelector('iframe');\n\n if (!iframe) {\n throw new Error('Failed to create iframe element');\n }\n\n // Wait for iframe load with timeout\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => reject(new Error('Iframe load timeout')), 5000);\n\n const handleLoad = () => {\n clearTimeout(timeout);\n resolve();\n };\n\n const handleError = () => {\n clearTimeout(timeout);\n reject(new Error('Iframe load error'));\n };\n\n iframe!.addEventListener('load', handleLoad, { once: true });\n iframe!.addEventListener('error', handleError, { once: true });\n });\n\n // Create a dedicated MessageChannel for this render instance\n const iframeOrigin = new URL(url).origin;\n const channel = new MessageChannel();\n\n // Listen for messages from the iframe on our end of the channel\n const onCtaClick = options?.onCtaClick;\n const autoHeight = options?.autoHeight ?? false;\n const minHeight = options?.minHeight ?? 50;\n const maxHeight = options?.maxHeight;\n const iframeRef = iframe;\n\n channel.port1.onmessage = (event: MessageEvent) => {\n const msg = event.data;\n\n // Handle CTA clicks\n if (msg?.type === CTA_CLICK_MESSAGE_TYPE && msg?.url) {\n if (onCtaClick) {\n onCtaClick({ url: msg.url, offerId: msg.offerId });\n }\n return;\n }\n\n // Handle resize (auto-height)\n if (autoHeight && msg?.type === RESIZE_MESSAGE_TYPE && msg?.height) {\n const h = Math.max(minHeight, Math.min(msg.height, maxHeight ?? Infinity));\n iframeRef.style.height = `${h}px`;\n }\n };\n\n // Send offer data and transfer port2 to the iframe\n const message: OfferDataMessage = {\n type: OFFER_DATA_MESSAGE_TYPE,\n version: '1.0',\n offer,\n style: options?.style,\n autoHeight,\n timestamp: Date.now(),\n };\n\n iframe.contentWindow?.postMessage(message, iframeOrigin, [channel.port2]);\n\n // Return handle for ongoing control\n return {\n updateStyle(style: IframeStyleConfig) {\n const msg: StyleUpdateMessage = {\n type: STYLE_UPDATE_MESSAGE_TYPE,\n version: '1.0',\n style,\n timestamp: Date.now(),\n };\n channel.port1.postMessage(msg);\n },\n destroy() {\n channel.port1.close();\n iframeRef.remove();\n },\n };\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('ZeroClick: Failed to load iframe:', error);\n\n // TODO: Track render failure (potential ad blocking or network issues)\n // when /api/v2/events endpoint is implemented\n\n // Clean up failed iframe\n if (iframe) {\n iframe.remove();\n }\n containerEl.innerHTML = ''; // Clear container (show nothing)\n\n return noopHandle;\n }\n }\n\n /**\n * Ensure the SDK has been initialized\n * @throws Error if not initialized\n */\n private static ensureInitialized(): void {\n if (!ZeroClick.isInitialized()) {\n throw new Error('ZeroClick: SDK not initialized. Call ZeroClick.initialize() first.');\n }\n }\n}\n"],"mappings":"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"}
|