@usesophi/sophi-web-sdk 0.1.0-beta.1
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/LICENSE +22 -0
- package/README.md +487 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +472 -0
- package/dist/index.d.ts +472 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sophi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# Sophi Web SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/sophi-web-sdk)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A framework-agnostic, TypeScript-first SDK for embedding the Sophi AI widget into any web application. Works seamlessly with vanilla JavaScript, React, Vue, Svelte, Angular, Next.js, and more.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🚀 **Framework Agnostic** - Works with any JavaScript framework or vanilla JS
|
|
11
|
+
- 📦 **Zero Dependencies** - Lightweight with no runtime dependencies
|
|
12
|
+
- 🔒 **Secure** - Built-in authentication and origin validation
|
|
13
|
+
- 🔑 **Automatic Session Management** - Handles authentication automatically
|
|
14
|
+
- 💪 **TypeScript First** - Full type definitions included
|
|
15
|
+
- 🎯 **Event-Driven** - Clean event-based API for loose coupling
|
|
16
|
+
- 📱 **Modern** - ESM and CommonJS support with tree-shaking
|
|
17
|
+
- 🎨 **Flexible** - Client controls all UX (toasts, errors, styling)
|
|
18
|
+
- 📁 **Well-Structured** - Modular architecture for easy maintenance
|
|
19
|
+
|
|
20
|
+
## Security Note
|
|
21
|
+
|
|
22
|
+
**Q: Will users see my API URLs?**
|
|
23
|
+
**A: Yes, and that's okay!**
|
|
24
|
+
|
|
25
|
+
API URLs in client-side code are always visible to users (they can see them in browser DevTools network tab). This is **normal and safe** because:
|
|
26
|
+
|
|
27
|
+
- ✅ API URLs themselves aren't secrets - they're just endpoints
|
|
28
|
+
- ✅ **Real security comes from your `apiKey` and `clientId` validation on the server**
|
|
29
|
+
- ✅ Your API should authenticate and authorize every request
|
|
30
|
+
- ✅ Never put actual secrets (API keys, tokens) in client code - they come from your backend
|
|
31
|
+
|
|
32
|
+
Think of API URLs like a street address - it's public information. The security is the lock on the door (your API authentication).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install sophi-web-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or with yarn:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
yarn add sophi-web-sdk
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or with pnpm:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm add sophi-web-sdk
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
### Vanilla JavaScript
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
import { SophiWidget } from "sophi-web-sdk";
|
|
58
|
+
|
|
59
|
+
const widget = new SophiWidget();
|
|
60
|
+
|
|
61
|
+
// Initialize with authentication (async)
|
|
62
|
+
await widget.init({
|
|
63
|
+
apiKey: "your-api-key", // From parent app
|
|
64
|
+
clientId: "your-client-id", // From parent app
|
|
65
|
+
userId: "user-12345", // Current user ID
|
|
66
|
+
iframeUrl: "https://widget.sophi.ai",
|
|
67
|
+
container: "#sophi-widget",
|
|
68
|
+
environment: "production", // API URL determined automatically
|
|
69
|
+
metadata: {
|
|
70
|
+
// Optional metadata
|
|
71
|
+
theme: "dark",
|
|
72
|
+
plan: "premium",
|
|
73
|
+
},
|
|
74
|
+
onReady: () => console.log("Widget ready!"),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Listen for events
|
|
78
|
+
widget.on("add_to_cart", (data) => {
|
|
79
|
+
console.log("Products to add:", data.products);
|
|
80
|
+
// Handle cart logic here
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> **Note**: The `init()` method is now async and automatically handles authentication with the Sophi API before loading the widget. The API URL is determined automatically based on the `environment` setting.
|
|
85
|
+
|
|
86
|
+
### React
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { useEffect, useRef } from "react";
|
|
90
|
+
import { SophiWidget } from "sophi-web-sdk";
|
|
91
|
+
|
|
92
|
+
function MyComponent() {
|
|
93
|
+
const widgetRef = useRef<SophiWidget | null>(null);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const widget = new SophiWidget();
|
|
97
|
+
widgetRef.current = widget;
|
|
98
|
+
|
|
99
|
+
// Async initialization
|
|
100
|
+
const initWidget = async () => {
|
|
101
|
+
try {
|
|
102
|
+
await widget.init({
|
|
103
|
+
apiKey: "your-api-key",
|
|
104
|
+
clientId: "your-client-id",
|
|
105
|
+
userId: "user-12345",
|
|
106
|
+
container: "#sophi-widget",
|
|
107
|
+
iframeUrl: "https://widget.sophi.ai",
|
|
108
|
+
environment: "production",
|
|
109
|
+
metadata: { theme: "dark" },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
widget.on("add_to_cart", (data) => {
|
|
113
|
+
// Handle add to cart
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Failed to initialize widget:", error);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
initWidget();
|
|
121
|
+
|
|
122
|
+
return () => widget.destroy();
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
return <div id="sophi-widget" />;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
See `examples/react-example/` for a complete React example with custom hooks.
|
|
130
|
+
|
|
131
|
+
### Vue 3
|
|
132
|
+
|
|
133
|
+
```vue
|
|
134
|
+
<template>
|
|
135
|
+
<div id="sophi-widget"></div>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<script setup>
|
|
139
|
+
import { onMounted, onUnmounted } from "vue";
|
|
140
|
+
import { SophiWidget } from "sophi-web-sdk";
|
|
141
|
+
|
|
142
|
+
let widget = null;
|
|
143
|
+
|
|
144
|
+
onMounted(() => {
|
|
145
|
+
widget = new SophiWidget();
|
|
146
|
+
|
|
147
|
+
widget.init({
|
|
148
|
+
apiKey: "your-api-key",
|
|
149
|
+
container: "#sophi-widget",
|
|
150
|
+
iframeUrl: "https://widget.sophi.ai",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
widget.on("add_to_cart", (data) => {
|
|
154
|
+
// Handle add to cart
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
onUnmounted(() => {
|
|
159
|
+
if (widget) {
|
|
160
|
+
widget.destroy();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
</script>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Svelte
|
|
167
|
+
|
|
168
|
+
```svelte
|
|
169
|
+
<script>
|
|
170
|
+
import { onMount, onDestroy } from 'svelte';
|
|
171
|
+
import { SophiWidget } from 'sophi-web-sdk';
|
|
172
|
+
|
|
173
|
+
let widget;
|
|
174
|
+
|
|
175
|
+
onMount(() => {
|
|
176
|
+
widget = new SophiWidget();
|
|
177
|
+
|
|
178
|
+
widget.init({
|
|
179
|
+
apiKey: 'your-api-key',
|
|
180
|
+
container: '#sophi-widget',
|
|
181
|
+
iframeUrl: 'https://widget.sophi.ai',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
widget.on('add_to_cart', (data) => {
|
|
185
|
+
// Handle add to cart
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
onDestroy(() => {
|
|
190
|
+
if (widget) {
|
|
191
|
+
widget.destroy();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<div id="sophi-widget"></div>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Angular
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { Component, OnInit, OnDestroy } from "@angular/core";
|
|
203
|
+
import { SophiWidget } from "sophi-web-sdk";
|
|
204
|
+
|
|
205
|
+
@Component({
|
|
206
|
+
selector: "app-widget",
|
|
207
|
+
template: '<div id="sophi-widget"></div>',
|
|
208
|
+
})
|
|
209
|
+
export class WidgetComponent implements OnInit, OnDestroy {
|
|
210
|
+
private widget: SophiWidget | null = null;
|
|
211
|
+
|
|
212
|
+
ngOnInit() {
|
|
213
|
+
this.widget = new SophiWidget();
|
|
214
|
+
|
|
215
|
+
this.widget.init({
|
|
216
|
+
apiKey: "your-api-key",
|
|
217
|
+
container: "#sophi-widget",
|
|
218
|
+
iframeUrl: "https://widget.sophi.ai",
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
this.widget.on("add_to_cart", (data) => {
|
|
222
|
+
// Handle add to cart
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ngOnDestroy() {
|
|
227
|
+
if (this.widget) {
|
|
228
|
+
this.widget.destroy();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## API Reference
|
|
235
|
+
|
|
236
|
+
### `SophiWidget`
|
|
237
|
+
|
|
238
|
+
The main class for interacting with the Sophi widget.
|
|
239
|
+
|
|
240
|
+
#### `init(config: SophiConfig): Promise<void>`
|
|
241
|
+
|
|
242
|
+
Initialize the widget with configuration. This method is now **async** and handles authentication automatically.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
await widget.init({
|
|
246
|
+
// Required fields
|
|
247
|
+
apiKey: "your-api-key", // Your API key (x-api-key header)
|
|
248
|
+
clientId: "your-client-id", // Client ID (x-sophi-client-id header)
|
|
249
|
+
userId: "user-12345", // External user ID from your app
|
|
250
|
+
container: "#sophi-widget", // CSS selector or HTMLElement
|
|
251
|
+
iframeUrl: "https://widget.sophi.ai", // Widget iframe URL
|
|
252
|
+
|
|
253
|
+
// Optional fields
|
|
254
|
+
environment: "production", // 'test' | 'staging' | 'production' (determines API URL)
|
|
255
|
+
metadata: {
|
|
256
|
+
// Custom metadata for the session
|
|
257
|
+
theme: "dark",
|
|
258
|
+
plan: "premium",
|
|
259
|
+
},
|
|
260
|
+
width: "100%", // Widget width (default: '100%')
|
|
261
|
+
height: "600px", // Widget height (default: '600px')
|
|
262
|
+
onReady: () => {}, // Callback when widget is ready
|
|
263
|
+
onError: (error) => {}, // Callback for errors
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Authentication Flow:**
|
|
268
|
+
|
|
269
|
+
1. SDK validates configuration
|
|
270
|
+
2. SDK determines API URL based on `environment` setting
|
|
271
|
+
3. SDK calls authentication API with `apiKey`, `clientId`, and `userId`
|
|
272
|
+
4. API returns session data (accessToken, sessionId, etc.)
|
|
273
|
+
5. SDK creates iframe and sends session data via postMessage
|
|
274
|
+
6. Widget is ready to use
|
|
275
|
+
|
|
276
|
+
**Environment to API URL Mapping:**
|
|
277
|
+
|
|
278
|
+
- `test`: Test API URL (for development/testing)
|
|
279
|
+
- `staging`: Staging API URL
|
|
280
|
+
- `production`: Production API URL (default)
|
|
281
|
+
|
|
282
|
+
See [AUTHENTICATION.md](./AUTHENTICATION.md) for detailed authentication documentation.
|
|
283
|
+
|
|
284
|
+
#### `sendUserData(data: UserData): void`
|
|
285
|
+
|
|
286
|
+
Send user data to the widget via postMessage.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
widget.sendUserData({
|
|
290
|
+
userId: "user-123",
|
|
291
|
+
email: "user@example.com",
|
|
292
|
+
name: "John Doe",
|
|
293
|
+
// Add any custom fields
|
|
294
|
+
customField: "value",
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### `on<K extends EventName>(event: K, handler: EventHandler<SophiEventMap[K]>): void`
|
|
299
|
+
|
|
300
|
+
Register an event listener.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Listen for add_to_cart events
|
|
304
|
+
widget.on("add_to_cart", (data) => {
|
|
305
|
+
console.log("Products:", data.products);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Listen for ready events
|
|
309
|
+
widget.on("ready", () => {
|
|
310
|
+
console.log("Widget is ready");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Listen for error events
|
|
314
|
+
widget.on("error", (error) => {
|
|
315
|
+
console.error("Widget error:", error);
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### `off<K extends EventName>(event: K, handler: EventHandler<SophiEventMap[K]>): void`
|
|
320
|
+
|
|
321
|
+
Unregister an event listener.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const handler = (data) => console.log(data);
|
|
325
|
+
widget.on("add_to_cart", handler);
|
|
326
|
+
widget.off("add_to_cart", handler);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### `show(): void`
|
|
330
|
+
|
|
331
|
+
Show the widget (sets display to 'block').
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
widget.show();
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### `hide(): void`
|
|
338
|
+
|
|
339
|
+
Hide the widget (sets display to 'none').
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
widget.hide();
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### `destroy(): void`
|
|
346
|
+
|
|
347
|
+
Destroy the widget and cleanup all resources.
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
widget.destroy();
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### `isReady(): boolean`
|
|
354
|
+
|
|
355
|
+
Check if the widget is initialized and ready.
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
if (widget.isReady()) {
|
|
359
|
+
widget.sendUserData({ userId: "123" });
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## TypeScript Support
|
|
364
|
+
|
|
365
|
+
The SDK is written in TypeScript and includes full type definitions.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { SophiWidget, SophiConfig, AddToCartEvent, UserData } from "sophi-web-sdk";
|
|
369
|
+
|
|
370
|
+
const config: SophiConfig = {
|
|
371
|
+
apiKey: "your-api-key",
|
|
372
|
+
container: "#widget",
|
|
373
|
+
iframeUrl: "https://widget.sophi.ai",
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const widget = new SophiWidget();
|
|
377
|
+
widget.init(config);
|
|
378
|
+
|
|
379
|
+
widget.on("add_to_cart", (data: AddToCartEvent) => {
|
|
380
|
+
data.products.forEach((product) => {
|
|
381
|
+
console.log(product.productId, product.variantId);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Events
|
|
387
|
+
|
|
388
|
+
### `add_to_cart`
|
|
389
|
+
|
|
390
|
+
Fired when the widget requests to add products to the cart.
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
interface AddToCartEvent {
|
|
394
|
+
products: Array<{
|
|
395
|
+
productId: string;
|
|
396
|
+
variantId?: string;
|
|
397
|
+
}>;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
widget.on("add_to_cart", (data: AddToCartEvent) => {
|
|
401
|
+
// Handle cart logic
|
|
402
|
+
// Show toast notifications
|
|
403
|
+
// Update cart UI
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### `ready`
|
|
408
|
+
|
|
409
|
+
Fired when the widget iframe is ready.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
widget.on("ready", () => {
|
|
413
|
+
console.log("Widget is ready");
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### `error`
|
|
418
|
+
|
|
419
|
+
Fired when an error occurs.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
widget.on("error", (error: Error) => {
|
|
423
|
+
console.error("Error:", error.message);
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Security
|
|
428
|
+
|
|
429
|
+
The SDK includes comprehensive security features:
|
|
430
|
+
|
|
431
|
+
- **Authentication**: Automatic session creation with JWT tokens
|
|
432
|
+
- **Origin Validation**: All postMessage events are validated against the iframe origin
|
|
433
|
+
- **Message Validation**: Incoming messages are validated for correct structure
|
|
434
|
+
- **Type Safety**: TypeScript ensures type-safe message handling
|
|
435
|
+
- **Secure Headers**: API key and client ID sent via secure headers
|
|
436
|
+
- **Token Expiration**: Session tokens include expiration timestamps
|
|
437
|
+
|
|
438
|
+
See [AUTHENTICATION.md](./AUTHENTICATION.md) for security best practices.
|
|
439
|
+
|
|
440
|
+
## Documentation
|
|
441
|
+
|
|
442
|
+
- **[AUTHENTICATION.md](./AUTHENTICATION.md)** - Detailed authentication flow and API documentation
|
|
443
|
+
- **[FILE_STRUCTURE.md](./FILE_STRUCTURE.md)** - Project structure and architecture
|
|
444
|
+
- **[GETTING_STARTED.md](./GETTING_STARTED.md)** - Complete getting started guide
|
|
445
|
+
- **[QUICK_START_TESTING.md](./QUICK_START_TESTING.md)** - Quick testing guide
|
|
446
|
+
- **[TESTING.md](./TESTING.md)** - Comprehensive testing documentation
|
|
447
|
+
|
|
448
|
+
## Examples
|
|
449
|
+
|
|
450
|
+
Check out the `examples/` directory for complete working examples:
|
|
451
|
+
|
|
452
|
+
- **`examples/vanilla-html/`** - Pure HTML/JS example with no build step
|
|
453
|
+
- **`examples/react-example/`** - React example with custom hooks
|
|
454
|
+
|
|
455
|
+
To run the examples:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
# Vanilla HTML example
|
|
459
|
+
cd examples/vanilla-html
|
|
460
|
+
# Open index.html in your browser
|
|
461
|
+
|
|
462
|
+
# React example
|
|
463
|
+
cd examples/react-example
|
|
464
|
+
npm install
|
|
465
|
+
npm run dev
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Both examples include the new authentication flow with all required fields.
|
|
469
|
+
|
|
470
|
+
## Browser Support
|
|
471
|
+
|
|
472
|
+
- Chrome (latest)
|
|
473
|
+
- Firefox (latest)
|
|
474
|
+
- Safari (latest)
|
|
475
|
+
- Edge (latest)
|
|
476
|
+
|
|
477
|
+
## Contributing
|
|
478
|
+
|
|
479
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
480
|
+
|
|
481
|
+
## License
|
|
482
|
+
|
|
483
|
+
MIT © Sophi
|
|
484
|
+
|
|
485
|
+
## Support
|
|
486
|
+
|
|
487
|
+
For issues and questions, please open an issue on GitHub or contact support@sophi.ai.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
'use strict';var p={ADD_TO_CART:"add_to_cart",READY:"ready",ERROR:"error",SEND_TO_CHECKOUT:"send_to_checkout"},a={USER_DATA:"user_data",CONFIG:"config",UPDATE_USER_CART:"update_user_cart",DESTROY:"destroy",SESSION_DATA:"session_data",CLIENT_ID_UPDATED:"client_id_updated"};var o=class{constructor(e,r,t){this.baseUrl=e,this.apiKey=r,this.clientId=t;}async createSession(e){let r=`${this.baseUrl}/api/v1/auth/client/session`;try{let t=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json","x-api-key":this.apiKey,"x-sophi-client-id":this.clientId},body:JSON.stringify(e)});if(!t.ok){let s=await t.text();throw new Error(`Failed to create session: ${t.status} ${t.statusText}. ${s}`)}let i=await t.json();if(!i.success)throw new Error(`Session creation failed: ${i.message||"Unknown error"}`);if(!i.data)throw new Error("Session data is missing from response");return i.data}catch(t){throw t instanceof Error?t:new Error(`Unexpected error during session creation: ${String(t)}`)}}validateSessionData(e){return !!(e.accessToken&&e.sessionId&&e.clientId&&e.companyCode&&e.expiresAt)}async mergeUser(e){let r=`${this.baseUrl}/api/v1/auth/client/info`;try{let t=await fetch(r,{method:"PATCH",headers:{"Content-Type":"application/json",Accept:"application/json","x-api-key":this.apiKey,"x-sophi-client-id":e.guestId},body:JSON.stringify({externalUserId:e.userId})});if(!t.ok){let s=await t.text();throw new Error(`Failed to merge user: ${t.status} ${t.statusText}. ${s}`)}let i=await t.json();if(!i.success)throw new Error(`User merge failed: ${i.message||"Unknown error"}`);if(!i.data)throw new Error("Merge data is missing from response");return i.data}catch(t){throw t instanceof Error?t:new Error(`Unexpected error during user merge: ${String(t)}`)}}};var h={WIDTH:"100%",HEIGHT:"600px"},d="http://10.0.2.127:80",u={CREATE_SESSION:"/api/v1/auth/client/session"},n={MISSING_API_KEY:"API key is required and must be a string",MISSING_CLIENT_ID:"Client ID is required and must be a string",MISSING_USER_ID:"User ID is required and must be a string",MISSING_CONTAINER:"Container is required",MISSING_IFRAME_URL:"Iframe URL is required and must be a string",INVALID_IFRAME_URL:"Invalid iframe URL",INVALID_ENVIRONMENT:"Invalid environment. Must be 'dev', 'test', 'staging', or 'production'",CONTAINER_NOT_FOUND:"Container element not found",NOT_INITIALIZED:"Widget not initialized. Call init() first.",ALREADY_INITIALIZED:"Sophi Widget is already initialized. Call destroy() first to reinitialize.",SESSION_CREATION_FAILED:"Failed to create authentication session",INVALID_SESSION_DATA:"Invalid session data received from API"};var l=class{constructor(){this.config=null;this.iframe=null;this.container=null;this.eventListeners=new Map;this.messageHandler=null;this.isInitialized=false;this.iframeOrigin=null;this.sessionData=null;this.apiService=null;}async init(e){if(this.isInitialized){console.warn(n.ALREADY_INITIALIZED);return}try{this.validateConfig(e),this.config=e;let r=d;this.apiService=new o(r,e.apiKey,e.clientId),await this.createAuthSession(),this.isInitialized=!0;try{let t=new URL(e.iframeUrl);this.iframeOrigin=t.origin;}catch{let i=new Error(`${n.INVALID_IFRAME_URL}: ${e.iframeUrl}`);throw this.emitError(i),i}if(this.container=this.resolveContainer(e.container),!this.container){let t=new Error(n.CONTAINER_NOT_FOUND);throw this.emitError(t),t}this.createIframe(),this.setupMessageListener(),this.iframe?.addEventListener("load",()=>{console.log("Iframe loaded, sending session data..."),setTimeout(()=>{this.sendSessionData();},100);}),e.onReady&&e.onReady();}catch(r){this.isInitialized=false,this.config=null,this.apiService=null,this.sessionData=null;let t=r instanceof Error?r:new Error(String(r));throw this.emitError(t),t}}sendUserData(e){if(!this.isInitialized||!this.iframe){console.warn("Widget not initialized. Call init() first.");return}let r={type:a.USER_DATA,data:e};this.postMessage(r);}updateUserCart(e){if(!this.isInitialized||!this.iframe){console.warn("Widget not initialized. Call init() first.");return}let r={type:a.UPDATE_USER_CART,data:e};this.postMessage(r);}async mergeUser(e,r,t){try{let i;if(this.apiService)i=this.apiService;else {if(!t)throw new Error("API key is required when widget is not initialized");let c=d;i=new o(c,t,e);}let s=await i.mergeUser({guestId:e,userId:r});if(this.isInitialized&&this.iframe){let c={type:a.CLIENT_ID_UPDATED,data:{clientId:s.clientId,externalUserId:s.externalUserId}};this.postMessage(c);}return s}catch(i){let s=i instanceof Error?i:new Error(String(i));throw this.isInitialized&&this.emitError(s),s}}static async mergeUser(e,r,t){let i=d;return await new o(i,t,e).mergeUser({guestId:e,userId:r})}onDestroy(){if(!this.isInitialized||!this.iframe){console.warn("Widget not initialized. Call init() first.");return}let e={type:a.DESTROY};this.postMessage(e);}on(e,r){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(r);}off(e,r){let t=this.eventListeners.get(e);t&&t.delete(r);}show(){this.iframe&&(this.iframe.style.display="block");}hide(){this.iframe&&(this.iframe.style.display="none");}async destroy(){this.onDestroy(),await new Promise(e=>setTimeout(e,300)),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.iframe&&this.iframe.parentNode&&(this.iframe.parentNode.removeChild(this.iframe),this.iframe=null),this.eventListeners.clear(),this.config=null,this.container=null,this.isInitialized=false,this.iframeOrigin=null,this.sessionData=null,this.apiService=null;}isReady(){return this.isInitialized&&this.iframe!==null}validateConfig(e){if(!e.apiKey||typeof e.apiKey!="string")throw new Error(n.MISSING_API_KEY);if(!e.clientId||typeof e.clientId!="string")throw new Error(n.MISSING_CLIENT_ID);if(!e.clientId||typeof e.clientId!="string")throw new Error(n.MISSING_USER_ID);if(!e.container)throw new Error(n.MISSING_CONTAINER);if(!e.iframeUrl||typeof e.iframeUrl!="string")throw new Error(n.MISSING_IFRAME_URL)}async createAuthSession(){if(!this.config||!this.apiService)throw new Error(n.NOT_INITIALIZED);try{if(this.sessionData=await this.apiService.createSession({externalUserId:this.config.clientId,metadata:this.config.metadata}),!this.apiService.validateSessionData(this.sessionData))throw new Error(n.INVALID_SESSION_DATA)}catch(e){throw e instanceof Error?e:new Error(n.SESSION_CREATION_FAILED)}}sendSessionData(){if(!this.sessionData){console.warn("Session data not available");return}let e={type:a.SESSION_DATA,data:this.sessionData};this.postMessage(e);}resolveContainer(e){return typeof e=="string"?document.querySelector(e):e}createIframe(){if(!this.container||!this.config)return;let e=document.createElement("iframe");e.src=this.buildIframeUrl(),e.style.width=this.config.width||"100%",e.style.height=this.config.height||"600px",e.style.border="none",e.title="Sophi AI Widget",e.allow="microphone; camera",this.container.appendChild(e),this.iframe=e;}buildIframeUrl(){return this.config?new URL(this.config.iframeUrl).toString():""}setupMessageListener(){this.messageHandler=e=>{if(this.iframeOrigin&&e.origin!==this.iframeOrigin){console.warn(`Received message from untrusted origin: ${e.origin}`);return}try{let r=typeof e.data=="string"?JSON.parse(e.data):e.data;this.handleIncomingMessage(r);}catch(r){console.error("Error processing message from iframe:",r);}},window.addEventListener("message",this.messageHandler);}handleIncomingMessage(e){switch(e.type){case "add_to_cart":this.isValidAddToCartMessage(e.data)?this.emit("add_to_cart",e.data):(console.error("Invalid add_to_cart message structure:",e.data),this.emitError(new Error("Invalid add_to_cart message format")));break;case "send_to_checkout":this.emit("send_to_checkout",void 0);break;case "ready":this.emit("ready",void 0);break;case "error":let r=e.data instanceof Error?e.data:new Error(String(e.data));this.emitError(r);break;default:console.warn("Unknown message type:",e);}}isValidAddToCartMessage(e){if(!e||typeof e!="object")return false;let r=e;return Array.isArray(r.products)?r.products.every(t=>{if(!t||typeof t!="object")return false;let i=t;return !(typeof i.productId!="string"||i.variantId!==void 0&&typeof i.variantId!="string")}):false}postMessage(e){if(!this.iframe||!this.iframe.contentWindow||!this.iframeOrigin){console.warn("Cannot send message: iframe not ready");return}try{console.log("Posting message to iframe:",e.type,"to origin:",this.iframeOrigin),this.iframe.contentWindow.postMessage(e,this.iframeOrigin),console.log("Message posted successfully");}catch(r){console.error("Error sending message to iframe:",r),this.emitError(r instanceof Error?r:new Error(String(r)));}}emit(e,r){let t=this.eventListeners.get(e);t&&t.forEach(i=>{try{i(r);}catch(s){console.error(`Error in ${e} event handler:`,s);}});}emitError(e){this.emit("error",e),this.config?.onError&&this.config.onError(e);}};
|
|
2
|
+
exports.API_BASE_URLS=d;exports.API_ENDPOINTS=u;exports.ApiService=o;exports.DEFAULT_CONFIG=h;exports.ERROR_MESSAGES=n;exports.IncomingMessageTypes=p;exports.OutgoingMessageTypes=a;exports.SophiWidget=l;//# sourceMappingURL=index.cjs.map
|
|
3
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/services/api.service.ts","../src/constants/config.ts","../src/SophiWidget.ts"],"names":["IncomingMessageTypes","OutgoingMessageTypes","ApiService","baseUrl","apiKey","clientId","request","url","response","errorText","data","error","sessionData","DEFAULT_CONFIG","API_BASE_URLS","API_ENDPOINTS","ERROR_MESSAGES","SophiWidget","config","apiUrl","err","message","cart","guestId","userId","apiService","mergeData","event","handler","handlers","resolve","container","iframe","product","prod"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Configuration for initializing the Sophi Widget\n */\nexport interface SophiConfig {\n /**\n * API key for authentication with Sophi services (x-api-key header)\n * This comes from the parent application\n */\n apiKey: string;\n\n /**\n * Client ID for authentication (x-sophi-client-id header)\n * This comes from the parent application\n */\n clientId: string;\n\n /**\n * Container element selector (e.g., '#widget') or HTMLElement where the widget will be mounted\n */\n container: string | HTMLElement;\n\n /**\n * URL of the iframe to embed (e.g., 'https://widget.sophi.ai' or 'http://localhost:5173')\n */\n iframeUrl: string;\n\n /**\n * Optional metadata to send with the session request\n */\n metadata?: Record<string, unknown>;\n\n /**\n * Width of the widget iframe\n * @default '100%'\n */\n width?: string;\n\n /**\n * Height of the widget iframe\n * @default '600px'\n */\n height?: string;\n\n /**\n * Callback fired when the widget is ready\n */\n onReady?: () => void;\n\n /**\n * Callback fired when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Product information for add to cart events\n */\nexport interface Product {\n /**\n * Unique product identifier\n */\n productId: string;\n\n /**\n * Optional variant identifier\n */\n variantId?: string;\n}\n\n/**\n * Add to cart event data\n */\nexport interface AddToCartEvent {\n /**\n * List of products to add to cart\n */\n products: Product[];\n}\n\n/**\n * User data that can be sent to the widget\n */\nexport interface UserData {\n /**\n * User ID\n */\n userId?: string;\n\n /**\n * User email\n */\n email?: string;\n\n /**\n * User name\n */\n name?: string;\n\n /**\n * Additional custom data\n */\n [key: string]: unknown;\n}\n\nexport interface IUserCartItem {\n productId: string;\n variantId: string;\n quantity?: number;\n}\n\nexport interface IUserCart {\n cardId: string;\n items: IUserCartItem[];\n}\n\n/**\n * Message types that can be received from the iframe\n */\nexport type IncomingMessageType = \"add_to_cart\" | \"send_to_checkout\" | \"ready\" | \"error\";\n\n/**\n * Incoming message type constants for easy access\n * @example\n * widget.on(IncomingMessageTypes.ADD_TO_CART, (data) => {...})\n */\nexport const IncomingMessageTypes = {\n ADD_TO_CART: \"add_to_cart\",\n READY: \"ready\",\n ERROR: \"error\",\n SEND_TO_CHECKOUT: \"send_to_checkout\",\n} as const;\n\n/**\n * Message types that can be sent to the iframe\n */\nexport type OutgoingMessageType = \"user_data\" | \"config\" | \"update_user_cart\" | \"destroy\" | \"session_data\" | \"client_id_updated\";\n\n/**\n * Outgoing message type constants for easy access\n * @example\n * widget.sendMessage({ type: OutgoingMessageTypes.USER_DATA, data: {...} })\n */\nexport const OutgoingMessageTypes = {\n USER_DATA: \"user_data\",\n CONFIG: \"config\",\n UPDATE_USER_CART: \"update_user_cart\",\n DESTROY: \"destroy\",\n SESSION_DATA: \"session_data\",\n CLIENT_ID_UPDATED: \"client_id_updated\",\n} as const;\n\n/**\n * Incoming message structure from iframe\n */\nexport interface IncomingMessage {\n type: IncomingMessageType;\n data?: unknown;\n}\n\n/**\n * Outgoing message structure to iframe\n */\nexport interface OutgoingMessage {\n type: OutgoingMessageType;\n data?: unknown;\n}\n\n/**\n * Event map for type-safe event handling\n */\nexport interface SophiEventMap {\n /**\n * Fired when products should be added to cart\n */\n add_to_cart: AddToCartEvent;\n\n /**\n * Fired when the widget is ready\n */\n ready: void;\n\n /**\n * Fired when an error occurs\n */\n error: Error;\n\n /**\n * Fired when the user should be sent to checkout\n */\n send_to_checkout: void;\n}\n\n/**\n * Event handler type\n */\nexport type EventHandler<T> = (data: T) => void;\n\n/**\n * Event name type\n */\nexport type EventName = keyof SophiEventMap;\n\n/**\n * Authentication session request payload\n */\nexport interface AuthSessionRequest {\n /**\n * External user ID from the parent application\n */\n externalUserId: string;\n\n /**\n * Optional metadata to include with the session\n */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Session data returned from the authentication API\n */\nexport interface SessionData {\n /**\n * JWT access token for authenticating requests\n */\n accessToken: string;\n\n /**\n * Unique session identifier\n */\n sessionId: string;\n\n /**\n * Client ID\n */\n clientId: string;\n\n /**\n * Company code\n */\n companyCode: string;\n\n /**\n * Session expiration timestamp\n */\n expiresAt: string;\n}\n\n/**\n * Authentication session API response\n */\nexport interface AuthSessionResponse {\n /**\n * Whether the request was successful\n */\n success: boolean;\n\n /**\n * Response message\n */\n message: string;\n\n /**\n * Session data\n */\n data?: SessionData;\n}\n\n/**\n * Merge user request payload\n */\nexport interface MergeUserRequest {\n /**\n * The guest client ID to merge from\n */\n guestId: string;\n\n /**\n * The logged-in user ID to merge to\n */\n userId: string;\n}\n\n/**\n * Merge user data returned from the API\n */\nexport interface MergeUserData {\n /**\n * The resulting client ID after merge\n */\n clientId: string;\n\n /**\n * The external user ID\n */\n externalUserId: string;\n}\n\n/**\n * Merge user API response\n */\nexport interface MergeUserResponse {\n /**\n * Whether the request was successful\n */\n success: boolean;\n\n /**\n * Response message\n */\n message: string;\n\n /**\n * Merge result data\n */\n data?: MergeUserData;\n}\n","import type { AuthSessionRequest, AuthSessionResponse, SessionData, MergeUserRequest, MergeUserResponse, MergeUserData } from \"../types\";\n\n/**\n * API Service for Sophi authentication and session management\n */\nexport class ApiService {\n private baseUrl: string;\n private apiKey: string;\n private clientId: string;\n\n constructor(baseUrl: string, apiKey: string, clientId: string) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n this.clientId = clientId;\n }\n\n /**\n * Create a client session by calling the authentication endpoint\n * @param request - Session request with externalUserId and optional metadata\n * @returns Session data including accessToken, sessionId, etc.\n */\n async createSession(request: AuthSessionRequest): Promise<SessionData> {\n const url = `${this.baseUrl}/api/v1/auth/client/session`;\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\",\n \"x-api-key\": this.apiKey,\n \"x-sophi-client-id\": this.clientId,\n },\n body: JSON.stringify(request),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to create session: ${response.status} ${response.statusText}. ${errorText}`\n );\n }\n\n const data: AuthSessionResponse = await response.json();\n\n if (!data.success) {\n throw new Error(`Session creation failed: ${data.message || \"Unknown error\"}`);\n }\n\n if (!data.data) {\n throw new Error(\"Session data is missing from response\");\n }\n\n return data.data;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n }\n throw new Error(`Unexpected error during session creation: ${String(error)}`);\n }\n }\n\n /**\n * Validate session data\n */\n validateSessionData(sessionData: SessionData): boolean {\n return !!(\n sessionData.accessToken &&\n sessionData.sessionId &&\n sessionData.clientId &&\n sessionData.companyCode &&\n sessionData.expiresAt\n );\n }\n\n /**\n * Merge guest user with logged-in user\n * @param request - Merge request with guestId and userId\n * @returns Merge result data including the resulting clientId\n */\n async mergeUser(request: MergeUserRequest): Promise<MergeUserData> {\n const url = `${this.baseUrl}/api/v1/auth/client/info`;\n\n try {\n const response = await fetch(url, {\n method: \"PATCH\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\",\n \"x-api-key\": this.apiKey,\n \"x-sophi-client-id\": request.guestId,\n },\n body: JSON.stringify({\n externalUserId: request.userId,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to merge user: ${response.status} ${response.statusText}. ${errorText}`\n );\n }\n\n const data: MergeUserResponse = await response.json();\n\n if (!data.success) {\n throw new Error(`User merge failed: ${data.message || \"Unknown error\"}`);\n }\n\n if (!data.data) {\n throw new Error(\"Merge data is missing from response\");\n }\n\n return data.data;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n }\n throw new Error(`Unexpected error during user merge: ${String(error)}`);\n }\n }\n}\n","/**\n * Default configuration constants for Sophi Widget\n */\nexport const DEFAULT_CONFIG = {\n /**\n * Default width of the widget iframe\n */\n WIDTH: \"100%\",\n\n /**\n * Default height of the widget iframe\n */\n HEIGHT: \"600px\",\n} as const;\n\n/**\n * API base URLs for different environments\n *\n * Note: These URLs will be visible in the client-side bundle.\n * This is normal and safe - the real security is in your API key validation.\n *\n * To use different URLs, modify these values before building the SDK.\n *\n * Production API URL - update before building for production\n */\nexport const API_BASE_URLS = \"http://10.0.2.127:80\" as const;\n\n/**\n * API endpoints configuration\n */\nexport const API_ENDPOINTS = {\n /**\n * Session creation endpoint path\n */\n CREATE_SESSION: \"/api/v1/auth/client/session\",\n} as const;\n\n/**\n * Error messages\n */\nexport const ERROR_MESSAGES = {\n MISSING_API_KEY: \"API key is required and must be a string\",\n MISSING_CLIENT_ID: \"Client ID is required and must be a string\",\n MISSING_USER_ID: \"User ID is required and must be a string\",\n MISSING_CONTAINER: \"Container is required\",\n MISSING_IFRAME_URL: \"Iframe URL is required and must be a string\",\n INVALID_IFRAME_URL: \"Invalid iframe URL\",\n INVALID_ENVIRONMENT: \"Invalid environment. Must be 'dev', 'test', 'staging', or 'production'\",\n CONTAINER_NOT_FOUND: \"Container element not found\",\n NOT_INITIALIZED: \"Widget not initialized. Call init() first.\",\n ALREADY_INITIALIZED: \"Sophi Widget is already initialized. Call destroy() first to reinitialize.\",\n SESSION_CREATION_FAILED: \"Failed to create authentication session\",\n INVALID_SESSION_DATA: \"Invalid session data received from API\",\n} as const;\n","import { type SophiConfig, type UserData, type EventHandler, type EventName, type SophiEventMap, type IncomingMessage, type OutgoingMessage, type AddToCartEvent, type IUserCart, type SessionData, type MergeUserRequest, type MergeUserData, OutgoingMessageTypes } from \"./types\";\nimport { ApiService } from \"./services/api.service\";\nimport { DEFAULT_CONFIG, ERROR_MESSAGES, API_BASE_URLS } from \"./constants/config\";\n\n/**\n * Main Sophi Widget SDK class\n * Provides a framework-agnostic way to embed and interact with the Sophi AI widget\n */\nexport class SophiWidget {\n private config: SophiConfig | null = null;\n private iframe: HTMLIFrameElement | null = null;\n private container: HTMLElement | null = null;\n private eventListeners: Map<EventName, Set<EventHandler<any>>> = new Map();\n private messageHandler: ((event: MessageEvent) => void) | null = null;\n private isInitialized = false;\n private iframeOrigin: string | null = null;\n private sessionData: SessionData | null = null;\n private apiService: ApiService | null = null;\n\n /**\n * Initialize the widget with configuration\n * @param config - Widget configuration\n */\n public async init(config: SophiConfig): Promise<void> {\n if (this.isInitialized) {\n console.warn(ERROR_MESSAGES.ALREADY_INITIALIZED);\n return;\n }\n\n try {\n // Validate configuration\n this.validateConfig(config);\n\n this.config = config;\n\n // Determine API URL based on environment\n const apiUrl = API_BASE_URLS;\n\n // Initialize API service\n this.apiService = new ApiService(apiUrl, config.apiKey, config.clientId);\n\n // Create authentication session\n await this.createAuthSession();\n\n this.isInitialized = true;\n\n // Extract origin from iframe URL for security\n try {\n const url = new URL(config.iframeUrl);\n this.iframeOrigin = url.origin;\n } catch (error) {\n const err = new Error(`${ERROR_MESSAGES.INVALID_IFRAME_URL}: ${config.iframeUrl}`);\n this.emitError(err);\n throw err;\n }\n\n // Resolve container element\n this.container = this.resolveContainer(config.container);\n\n if (!this.container) {\n const err = new Error(ERROR_MESSAGES.CONTAINER_NOT_FOUND);\n this.emitError(err);\n throw err;\n }\n\n // Create and mount iframe\n this.createIframe();\n\n // Setup postMessage listener\n this.setupMessageListener();\n\n // Send session data to iframe after it's loaded\n this.iframe?.addEventListener(\"load\", () => {\n console.log(\"Iframe loaded, sending session data...\");\n // Small delay to ensure iframe is fully ready\n setTimeout(() => {\n this.sendSessionData();\n }, 100);\n });\n\n // Call onReady callback if provided\n if (config.onReady) {\n config.onReady();\n }\n } catch (error) {\n // Reset state on error\n this.isInitialized = false;\n this.config = null;\n this.apiService = null;\n this.sessionData = null;\n\n const err = error instanceof Error ? error : new Error(String(error));\n this.emitError(err);\n throw err;\n }\n }\n\n /**\n * Send user data to the widget\n * @param data - User data object\n */\n public sendUserData(data: UserData): void {\n if (!this.isInitialized || !this.iframe) {\n console.warn(\"Widget not initialized. Call init() first.\");\n return;\n }\n\n const message: OutgoingMessage = {\n type: OutgoingMessageTypes.USER_DATA,\n data,\n };\n\n this.postMessage(message);\n }\n\n public updateUserCart(cart: IUserCart): void {\n if (!this.isInitialized || !this.iframe) {\n console.warn(\"Widget not initialized. Call init() first.\");\n return;\n }\n\n const message: OutgoingMessage = {\n type: OutgoingMessageTypes.UPDATE_USER_CART,\n data: cart,\n };\n\n this.postMessage(message);\n }\n\n /**\n * Merge guest user with logged-in user\n * This can be called anytime, even before widget initialization\n * @param guestId - The guest client ID (previous clientId)\n * @param userId - The logged-in user ID (new externalUserId)\n * @param apiKey - API key for authentication (required if widget not initialized)\n * @returns Promise with merge result data\n */\n public async mergeUser(guestId: string, userId: string, apiKey?: string): Promise<MergeUserData> {\n try {\n let apiService: ApiService;\n\n // Use existing API service if widget is initialized, otherwise create a temporary one\n if (this.apiService) {\n apiService = this.apiService;\n } else {\n // Widget not initialized - create temporary API service\n if (!apiKey) {\n throw new Error(\"API key is required when widget is not initialized\");\n }\n const apiUrl = API_BASE_URLS;\n apiService = new ApiService(apiUrl, apiKey, guestId);\n }\n\n const mergeData = await apiService.mergeUser({ guestId, userId });\n\n // If iframe exists, send the updated clientId to it\n // This allows the iframe to update its own stores automatically\n if (this.isInitialized && this.iframe) {\n const message: OutgoingMessage = {\n type: OutgoingMessageTypes.CLIENT_ID_UPDATED,\n data: {\n clientId: mergeData.clientId,\n externalUserId: mergeData.externalUserId,\n },\n };\n this.postMessage(message);\n }\n\n return mergeData;\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n // Only emit error if widget is initialized\n if (this.isInitialized) {\n this.emitError(err);\n }\n throw err;\n }\n }\n\n /**\n * Static method to merge guest user with logged-in user\n * Use this when you don't have a widget instance or want to merge before initialization\n * @param guestId - The guest client ID (previous clientId)\n * @param userId - The logged-in user ID (new externalUserId)\n * @param apiKey - API key for authentication\n * @returns Promise with merge result data\n */\n public static async mergeUser(guestId: string, userId: string, apiKey: string): Promise<MergeUserData> {\n const apiUrl = API_BASE_URLS;\n const apiService = new ApiService(apiUrl, apiKey, guestId);\n return await apiService.mergeUser({ guestId, userId });\n }\n\n private onDestroy(): void {\n if (!this.isInitialized || !this.iframe) {\n console.warn(\"Widget not initialized. Call init() first.\");\n return;\n }\n\n const message: OutgoingMessage = {\n type: OutgoingMessageTypes.DESTROY,\n };\n\n this.postMessage(message);\n }\n\n /**\n * Register an event listener\n * @param event - Event name\n * @param handler - Event handler function\n */\n public on<K extends EventName>(event: K, handler: EventHandler<SophiEventMap[K]>): void {\n if (!this.eventListeners.has(event)) {\n this.eventListeners.set(event, new Set());\n }\n this.eventListeners.get(event)!.add(handler);\n }\n\n /**\n * Unregister an event listener\n * @param event - Event name\n * @param handler - Event handler function to remove\n */\n public off<K extends EventName>(event: K, handler: EventHandler<SophiEventMap[K]>): void {\n const handlers = this.eventListeners.get(event);\n if (handlers) {\n handlers.delete(handler);\n }\n }\n\n /**\n * Show the widget\n */\n public show(): void {\n if (this.iframe) {\n this.iframe.style.display = \"block\";\n }\n }\n\n /**\n * Hide the widget\n */\n public hide(): void {\n if (this.iframe) {\n this.iframe.style.display = \"none\";\n }\n }\n\n /**\n * Destroy the widget and cleanup resources\n * Waits 300ms after sending destroy message to give iframe time to cleanup\n */\n public async destroy(): Promise<void> {\n this.onDestroy();\n\n // Wait 300ms to give iframe time to process the destroy event and cleanup\n await new Promise((resolve) => setTimeout(resolve, 300));\n\n // Remove message listener\n if (this.messageHandler) {\n window.removeEventListener(\"message\", this.messageHandler);\n this.messageHandler = null;\n }\n\n // Remove iframe\n if (this.iframe && this.iframe.parentNode) {\n this.iframe.parentNode.removeChild(this.iframe);\n this.iframe = null;\n }\n\n // Clear event listeners\n this.eventListeners.clear();\n\n // Reset state\n this.config = null;\n this.container = null;\n this.isInitialized = false;\n this.iframeOrigin = null;\n this.sessionData = null;\n this.apiService = null;\n }\n\n /**\n * Check if widget is initialized\n */\n public isReady(): boolean {\n return this.isInitialized && this.iframe !== null;\n }\n\n /**\n * Validate configuration\n */\n private validateConfig(config: SophiConfig): void {\n if (!config.apiKey || typeof config.apiKey !== \"string\") {\n throw new Error(ERROR_MESSAGES.MISSING_API_KEY);\n }\n\n if (!config.clientId || typeof config.clientId !== \"string\") {\n throw new Error(ERROR_MESSAGES.MISSING_CLIENT_ID);\n }\n\n if (!config.clientId || typeof config.clientId !== \"string\") {\n throw new Error(ERROR_MESSAGES.MISSING_USER_ID);\n }\n\n if (!config.container) {\n throw new Error(ERROR_MESSAGES.MISSING_CONTAINER);\n }\n\n if (!config.iframeUrl || typeof config.iframeUrl !== \"string\") {\n throw new Error(ERROR_MESSAGES.MISSING_IFRAME_URL);\n }\n }\n\n /**\n * Create authentication session with the API\n */\n private async createAuthSession(): Promise<void> {\n if (!this.config || !this.apiService) {\n throw new Error(ERROR_MESSAGES.NOT_INITIALIZED);\n }\n\n try {\n this.sessionData = await this.apiService.createSession({\n externalUserId: this.config.clientId,\n metadata: this.config.metadata,\n });\n\n // Validate session data\n if (!this.apiService.validateSessionData(this.sessionData)) {\n throw new Error(ERROR_MESSAGES.INVALID_SESSION_DATA);\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(ERROR_MESSAGES.SESSION_CREATION_FAILED);\n throw err;\n }\n }\n\n /**\n * Send session data to iframe\n */\n private sendSessionData(): void {\n if (!this.sessionData) {\n console.warn(\"Session data not available\");\n return;\n }\n\n const message: OutgoingMessage = {\n type: OutgoingMessageTypes.SESSION_DATA,\n data: this.sessionData,\n };\n\n this.postMessage(message);\n }\n\n /**\n * Resolve container element from selector or element\n */\n private resolveContainer(container: string | HTMLElement): HTMLElement | null {\n if (typeof container === \"string\") {\n return document.querySelector(container);\n }\n return container;\n }\n\n /**\n * Create and mount the iframe\n */\n private createIframe(): void {\n if (!this.container || !this.config) {\n return;\n }\n\n const iframe = document.createElement(\"iframe\");\n\n // Set iframe attributes\n iframe.src = this.buildIframeUrl();\n iframe.style.width = this.config.width || \"100%\";\n iframe.style.height = this.config.height || \"600px\";\n iframe.style.border = \"none\";\n iframe.title = \"Sophi AI Widget\";\n iframe.allow = \"microphone; camera\";\n\n // Append to container\n this.container.appendChild(iframe);\n this.iframe = iframe;\n }\n\n /**\n * Build iframe URL with query parameters\n */\n private buildIframeUrl(): string {\n if (!this.config) {\n return \"\";\n }\n\n const url = new URL(this.config.iframeUrl);\n\n return url.toString();\n }\n\n /**\n * Setup postMessage listener for iframe communication\n */\n private setupMessageListener(): void {\n this.messageHandler = (event: MessageEvent) => {\n // Validate origin for security\n if (this.iframeOrigin && event.origin !== this.iframeOrigin) {\n console.warn(`Received message from untrusted origin: ${event.origin}`);\n return;\n }\n\n try {\n // Parse message\n const message: IncomingMessage = typeof event.data === \"string\" ? JSON.parse(event.data) : event.data;\n\n // Handle different message types\n this.handleIncomingMessage(message);\n } catch (error) {\n console.error(\"Error processing message from iframe:\", error);\n }\n };\n\n window.addEventListener(\"message\", this.messageHandler);\n }\n\n /**\n * Handle incoming messages from iframe\n */\n private handleIncomingMessage(message: IncomingMessage): void {\n switch (message.type) {\n case \"add_to_cart\":\n if (this.isValidAddToCartMessage(message.data)) {\n this.emit(\"add_to_cart\", message.data);\n } else {\n console.error(\"Invalid add_to_cart message structure:\", message.data);\n this.emitError(new Error(\"Invalid add_to_cart message format\"));\n }\n break;\n\n case \"send_to_checkout\":\n this.emit(\"send_to_checkout\", undefined);\n break;\n case \"ready\":\n this.emit(\"ready\", undefined);\n break;\n\n case \"error\":\n const error = message.data instanceof Error ? message.data : new Error(String(message.data));\n this.emitError(error);\n break;\n\n default:\n console.warn(\"Unknown message type:\", message);\n }\n }\n\n /**\n * Validate add_to_cart message structure\n */\n private isValidAddToCartMessage(data: unknown): data is AddToCartEvent {\n if (!data || typeof data !== \"object\") {\n return false;\n }\n\n const event = data as Record<string, unknown>;\n\n // Check products array exists and is an array\n if (!Array.isArray(event.products)) {\n return false;\n }\n\n // Validate each product in the array\n return event.products.every((product) => {\n if (!product || typeof product !== \"object\") {\n return false;\n }\n const prod = product as Record<string, unknown>;\n\n // productId is required and must be a string\n if (typeof prod.productId !== \"string\") {\n return false;\n }\n\n // variantId is optional, but if present must be a string\n if (prod.variantId !== undefined && typeof prod.variantId !== \"string\") {\n return false;\n }\n\n return true;\n });\n }\n\n /**\n * Post message to iframe\n */\n private postMessage(message: OutgoingMessage): void {\n if (!this.iframe || !this.iframe.contentWindow || !this.iframeOrigin) {\n console.warn(\"Cannot send message: iframe not ready\");\n return;\n }\n\n try {\n console.log(\"Posting message to iframe:\", message.type, \"to origin:\", this.iframeOrigin);\n this.iframe.contentWindow.postMessage(message, this.iframeOrigin);\n console.log(\"Message posted successfully\");\n } catch (error) {\n console.error(\"Error sending message to iframe:\", error);\n this.emitError(error instanceof Error ? error : new Error(String(error)));\n }\n }\n\n /**\n * Emit event to registered listeners\n */\n private emit<K extends EventName>(event: K, data: SophiEventMap[K]): void {\n const handlers = this.eventListeners.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(data);\n } catch (error) {\n console.error(`Error in ${event} event handler:`, error);\n }\n });\n }\n }\n\n /**\n * Emit error event\n */\n private emitError(error: Error): void {\n this.emit(\"error\", error);\n\n // Also call onError callback if provided\n if (this.config?.onError) {\n this.config.onError(error);\n }\n }\n}\n"]}
|