@returningai/widget-sdk 1.0.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 +393 -0
- package/dist/rai-widget.iife.js +1 -0
- package/dist/rai-widget.js +683 -0
- package/dist/types/BaseWidget.d.ts +34 -0
- package/dist/types/ChannelWidget.d.ts +5 -0
- package/dist/types/CurrencyWidget.d.ts +5 -0
- package/dist/types/MilestoneWidget.d.ts +5 -0
- package/dist/types/SocialWidget.d.ts +5 -0
- package/dist/types/StoreWidget.d.ts +5 -0
- package/dist/types/core/auth.d.ts +8 -0
- package/dist/types/core/postmessage.d.ts +3 -0
- package/dist/types/core/storage.d.ts +8 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/types.d.ts +40 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# ReturningAI Widget SDK
|
|
2
|
+
|
|
3
|
+
A Shadow DOM-isolated Web Component SDK for embedding ReturningAI widgets on any customer website. Drop-in replacement for the legacy `widget-loader.js` — identical embed attributes, zero CSS conflicts.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Architecture](#architecture)
|
|
10
|
+
- [Shadow DOM Implementation](#shadow-dom-implementation)
|
|
11
|
+
- [Dependencies](#dependencies)
|
|
12
|
+
- [Usage](#usage)
|
|
13
|
+
- [Script Tag (Legacy Embed)](#script-tag-legacy-embed)
|
|
14
|
+
- [Web Component](#web-component)
|
|
15
|
+
- [Configuration Attributes](#configuration-attributes)
|
|
16
|
+
- [Public API](#public-api)
|
|
17
|
+
- [Build Process](#build-process)
|
|
18
|
+
- [Project Structure](#project-structure)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Customer page DOM
|
|
26
|
+
└── <div id="returning-ai-widget-{id}"> ← customer's container (unchanged)
|
|
27
|
+
└── <rai-widget> ← custom element (auto-mounted)
|
|
28
|
+
└── Shadow Root [closed]
|
|
29
|
+
├── <style> ← all CSS scoped here, never leaks
|
|
30
|
+
├── <div class="rai-loader"> ← animated loader (hidden after ready)
|
|
31
|
+
├── <div class="rai-error"> ← error state (hidden until error)
|
|
32
|
+
└── <iframe src="widget-url"> ← actual widget content
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Module Responsibilities
|
|
36
|
+
|
|
37
|
+
| Module | File | Purpose |
|
|
38
|
+
|--------|------|---------|
|
|
39
|
+
| Base Web Component | `src/BaseWidget.ts` | Abstract `HTMLElement` subclass; owns the Shadow Root, auth lifecycle, iframe creation, public API |
|
|
40
|
+
| Store Widget | `src/StoreWidget.ts` | Extends `BaseWidget`; implements `buildWidgetUrl()` for the store micro-frontend |
|
|
41
|
+
| Channel Widget | `src/ChannelWidget.ts` | Extends `BaseWidget`; appends `widgetId` to base URL or passes full URL through |
|
|
42
|
+
| Milestone Widget | `src/MilestoneWidget.ts` | Extends `BaseWidget`; same URL strategy as `ChannelWidget` |
|
|
43
|
+
| Social Widget | `src/SocialWidget.ts` | Extends `BaseWidget`; same URL strategy as `ChannelWidget` |
|
|
44
|
+
| Currency Widget | `src/CurrencyWidget.ts` | Extends `BaseWidget`; same URL strategy as `ChannelWidget` |
|
|
45
|
+
| Auth | `src/core/auth.ts` | Serverless auth, token refresh (with concurrency lock), logout, error settings fetch |
|
|
46
|
+
| Storage | `src/core/storage.ts` | `localStorage` helpers scoped to `{prefix}-{widgetId}-*`; access token never written to disk |
|
|
47
|
+
| postMessage | `src/core/postmessage.ts` | Sends token to iframe; receives `WIDGET_READY`, `WIDGET_HEIGHT_UPDATE`, `WIDGET_LOGOUT`, `RETURNINGAI_WIDGET_REQUEST_TOKEN` |
|
|
48
|
+
| Styles | `src/styles/widget.css` | Loader animation, error state, iframe fade-in — all scoped inside Shadow DOM |
|
|
49
|
+
| Entry | `src/index.ts` | Registers all 5 custom elements; bootstraps from `<script>` tag for legacy embeds; routes by `widget-type`; exposes `window.ReturningAIWidget` |
|
|
50
|
+
|
|
51
|
+
### Auth Flow
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Page load
|
|
55
|
+
│
|
|
56
|
+
├─ loadFromStorage() ── refresh token in localStorage?
|
|
57
|
+
│ Yes ──► refreshAccessToken() ──► createIframe()
|
|
58
|
+
│ No ──► authenticateServerless() ──► createIframe()
|
|
59
|
+
│
|
|
60
|
+
└─ iframe.onload
|
|
61
|
+
└─ sendTokenToWidget() ← postMessage with access token
|
|
62
|
+
schedulePeriodicSync() ← resend every 2 min
|
|
63
|
+
|
|
64
|
+
Token nearing expiry (1 min early)
|
|
65
|
+
└─ refreshAccessToken()
|
|
66
|
+
├─ 401/403 ──► re-authenticate serverless (token family rotated)
|
|
67
|
+
└─ success ──► sendTokenToWidget()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Token storage strategy**
|
|
71
|
+
|
|
72
|
+
| Token | Location | TTL |
|
|
73
|
+
|-------|----------|-----|
|
|
74
|
+
| Access token | Memory only (`WidgetState.accessToken`) | ~5 min |
|
|
75
|
+
| Refresh token | `localStorage` | 7 days |
|
|
76
|
+
|
|
77
|
+
The access token is never written to `localStorage`. On every page load the refresh token is exchanged for a fresh access token before the iframe mounts.
|
|
78
|
+
|
|
79
|
+
### postMessage Protocol
|
|
80
|
+
|
|
81
|
+
Messages the SDK sends **to** the widget iframe:
|
|
82
|
+
|
|
83
|
+
| Type | Payload | When |
|
|
84
|
+
|------|---------|------|
|
|
85
|
+
| `RETURNINGAI_WIDGET_TOKEN` | `{ widgetId, token }` | After iframe load, on refresh, every 2 min |
|
|
86
|
+
|
|
87
|
+
Messages the SDK **receives** from the widget iframe:
|
|
88
|
+
|
|
89
|
+
| Type | Action |
|
|
90
|
+
|------|--------|
|
|
91
|
+
| `WIDGET_READY` | Hides loader, fades in iframe |
|
|
92
|
+
| `WIDGET_HEIGHT_UPDATE` | Resizes iframe to `payload.height` |
|
|
93
|
+
| `WIDGET_ERROR` | Hides loader |
|
|
94
|
+
| `WIDGET_LOGOUT` | Calls logout endpoint, removes iframe |
|
|
95
|
+
| `RETURNINGAI_WIDGET_REQUEST_TOKEN` | Responds immediately with a fresh token |
|
|
96
|
+
|
|
97
|
+
All messages are origin-validated against `config.widgetDomain`.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Shadow DOM Implementation
|
|
102
|
+
|
|
103
|
+
The SDK uses a **closed** Shadow Root (`mode: 'closed'`), which means:
|
|
104
|
+
|
|
105
|
+
- Customer page CSS cannot reach any element inside the widget
|
|
106
|
+
- The widget's loader and error styles are fully encapsulated
|
|
107
|
+
- No class name collisions with customer frameworks (Tailwind, Bootstrap, etc.)
|
|
108
|
+
|
|
109
|
+
### How it is attached
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// StoreWidget.ts — constructor
|
|
113
|
+
this.shadow = this.attachShadow({ mode: 'closed' })
|
|
114
|
+
|
|
115
|
+
// connectedCallback — CSS injected as a <style> tag inside the shadow root
|
|
116
|
+
const style = document.createElement('style')
|
|
117
|
+
style.textContent = widgetCSS // inlined at build time via Vite ?inline import
|
|
118
|
+
this.shadow.appendChild(style)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
CSS custom properties (`--rai-accent`, `--rai-loader-bg`, `--rai-text4`) are set on the host element and cascade into the Shadow Root, allowing theme-aware colours without breaking encapsulation.
|
|
122
|
+
|
|
123
|
+
### Theme variables
|
|
124
|
+
|
|
125
|
+
| Variable | Light | Dark |
|
|
126
|
+
|----------|-------|------|
|
|
127
|
+
| `--rai-accent` | `#000000` | `#ffffff` |
|
|
128
|
+
| `--rai-text4` | `#6b7280` | `#9ca3af` |
|
|
129
|
+
| `--rai-loader-bg` | `#ffffff` | `#1a1a1a` |
|
|
130
|
+
| `--rai-error-bg` | `#ffffff` | `#1a1a1a` |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Dependencies
|
|
135
|
+
|
|
136
|
+
### Runtime
|
|
137
|
+
|
|
138
|
+
None. The SDK ships as a single self-contained IIFE with no external runtime dependencies.
|
|
139
|
+
|
|
140
|
+
### Dev / Build
|
|
141
|
+
|
|
142
|
+
| Package | Version | Purpose |
|
|
143
|
+
|---------|---------|---------|
|
|
144
|
+
| `vite` | ^5.1 | Bundles TypeScript + inlines CSS → single IIFE |
|
|
145
|
+
| `typescript` | ^5.3 | Type checking and compilation |
|
|
146
|
+
| `terser` | ^5.46 | Minification for production builds |
|
|
147
|
+
|
|
148
|
+
### Browser requirements
|
|
149
|
+
|
|
150
|
+
| Feature | Minimum |
|
|
151
|
+
|---------|---------|
|
|
152
|
+
| Custom Elements v1 | Chrome 67, Firefox 63, Safari 13 |
|
|
153
|
+
| Shadow DOM v1 | Chrome 53, Firefox 63, Safari 10 |
|
|
154
|
+
| `crypto.randomUUID()` | Chrome 92, Firefox 95, Safari 15.4 |
|
|
155
|
+
| `localStorage` | All modern browsers |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Usage
|
|
160
|
+
|
|
161
|
+
### Script Tag (Legacy Embed)
|
|
162
|
+
|
|
163
|
+
Zero changes required to existing embed HTML. Point `src` at the SDK and keep all `data-*` attributes as-is:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<!-- 1. Container div (unchanged from current embed) -->
|
|
167
|
+
<div
|
|
168
|
+
id="returning-ai-widget-6502c9e514a3e564c5c09c0a"
|
|
169
|
+
style="width: 100%; height: 600px;"
|
|
170
|
+
></div>
|
|
171
|
+
|
|
172
|
+
<!-- 2. SDK script tag -->
|
|
173
|
+
<script
|
|
174
|
+
src="https://cdn.returningai.com/widget-sdk/v1.0.0/rai-widget.iife.js"
|
|
175
|
+
data-widget-id="6502c9e514a3e564c5c09c0a"
|
|
176
|
+
data-widget-type="store"
|
|
177
|
+
data-container="returning-ai-widget-6502c9e514a3e564c5c09c0a"
|
|
178
|
+
data-theme="dark"
|
|
179
|
+
data-width="100%"
|
|
180
|
+
data-height="600px"
|
|
181
|
+
data-api-url="https://sgtr-eks-widgets.genesiv.org"
|
|
182
|
+
data-widget-url="https://widget.returningai.com/widgets/store/6502c9e514a3e564c5c09c0a/open-widget"
|
|
183
|
+
data-auto-refresh="true"
|
|
184
|
+
data-email="user@example.com"
|
|
185
|
+
></script>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The SDK scans for the loader `<script>` tag, reads its `data-*` attributes, creates a `<rai-widget>` element, and mounts it inside the container div.
|
|
189
|
+
|
|
190
|
+
### Web Component
|
|
191
|
+
|
|
192
|
+
For customers using a JavaScript framework (React, Vue, Angular), import the SDK once and use the custom element directly. Each widget type has its own tag:
|
|
193
|
+
|
|
194
|
+
| Tag | Widget type |
|
|
195
|
+
|-----|-------------|
|
|
196
|
+
| `<rai-widget>` | `store` |
|
|
197
|
+
| `<rai-channel-widget>` | `channel` |
|
|
198
|
+
| `<rai-milestone-widget>` | `milestone` |
|
|
199
|
+
| `<rai-social-widget>` | `social` |
|
|
200
|
+
| `<rai-currency-widget>` | `currency-view` |
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<script src="https://cdn.returningai.com/widget-sdk/v1.0.0/rai-widget.iife.js"></script>
|
|
204
|
+
|
|
205
|
+
<!-- Store widget -->
|
|
206
|
+
<rai-widget
|
|
207
|
+
widget-id="6502c9e514a3e564c5c09c0a"
|
|
208
|
+
widget-type="store"
|
|
209
|
+
theme="dark"
|
|
210
|
+
width="100%"
|
|
211
|
+
height="600px"
|
|
212
|
+
api-url="https://sgtr-eks-widgets.genesiv.org"
|
|
213
|
+
widget-url="https://widget.returningai.com/widgets/store/6502c9e514a3e564c5c09c0a/open-widget"
|
|
214
|
+
data-email="user@example.com"
|
|
215
|
+
></rai-widget>
|
|
216
|
+
|
|
217
|
+
<!-- Channel widget -->
|
|
218
|
+
<rai-channel-widget
|
|
219
|
+
widget-id="NjdmNmExNDU4NDQ5NDRmZjYwYWEzYzEz"
|
|
220
|
+
widget-type="channel"
|
|
221
|
+
theme="dark"
|
|
222
|
+
api-url="https://sgtr-eks-widgets.genesiv.org"
|
|
223
|
+
widget-url="https://sgtr-eks-widgets.genesiv.org/channel-widget"
|
|
224
|
+
data-email="user@example.com"
|
|
225
|
+
></rai-channel-widget>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
In React (TypeScript):
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
// JSX types are included in the package — no manual declarations needed
|
|
232
|
+
import '@returningai/widget-sdk'
|
|
233
|
+
|
|
234
|
+
export function WidgetEmbed() {
|
|
235
|
+
return (
|
|
236
|
+
<rai-widget
|
|
237
|
+
widget-id="6502c9e514a3e564c5c09c0a"
|
|
238
|
+
widget-type="store"
|
|
239
|
+
theme="dark"
|
|
240
|
+
height="600px"
|
|
241
|
+
data-email={currentUser.email}
|
|
242
|
+
/>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Configuration Attributes
|
|
248
|
+
|
|
249
|
+
All attributes can be provided with or without the `data-` prefix.
|
|
250
|
+
|
|
251
|
+
| Attribute | Required | Default | Description |
|
|
252
|
+
|-----------|----------|---------|-------------|
|
|
253
|
+
| `widget-id` | Yes | — | MongoDB ObjectId of the widget |
|
|
254
|
+
| `widget-type` | No | `store` | `store`, `channel`, `milestone`, `social`, `currency-view` |
|
|
255
|
+
| `theme` | No | `light` | `light` or `dark` |
|
|
256
|
+
| `container` | No | `returning-ai-widget-{id}` | ID of the container element |
|
|
257
|
+
| `width` | No | `100%` | CSS width of the iframe |
|
|
258
|
+
| `height` | No | `600px` | Initial CSS height (auto-resized by `WIDGET_HEIGHT_UPDATE`) |
|
|
259
|
+
| `api-url` | No | `https://sgtr-eks-widgets.genesiv.org` | Auth API base URL |
|
|
260
|
+
| `widget-url` | No | `https://widget.returningai.com` | URL served inside the iframe |
|
|
261
|
+
| `auto-refresh` | No | `true` | Automatically refresh access token before expiry |
|
|
262
|
+
| `debug` | No | `false` | Enable verbose console logging |
|
|
263
|
+
| `data-email` | No | — | User identifier passed to auth |
|
|
264
|
+
| `data-*` | No | — | Any additional `data-*` attributes are forwarded as `userIdentifiers` to the auth API |
|
|
265
|
+
|
|
266
|
+
### Public API
|
|
267
|
+
|
|
268
|
+
After the SDK loads, `window.ReturningAIWidget` is available:
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
// Check the loaded version
|
|
272
|
+
window.ReturningAIWidget.version // e.g. "1.0.0"
|
|
273
|
+
|
|
274
|
+
// Reload the widget (re-runs auth flow)
|
|
275
|
+
await window.ReturningAIWidget.reload()
|
|
276
|
+
|
|
277
|
+
// Log out and remove the iframe
|
|
278
|
+
await window.ReturningAIWidget.logout()
|
|
279
|
+
|
|
280
|
+
// Check authentication state
|
|
281
|
+
window.ReturningAIWidget.isAuthenticated() // boolean
|
|
282
|
+
|
|
283
|
+
// Inspect token metadata (no token values exposed)
|
|
284
|
+
window.ReturningAIWidget.getTokenInfo()
|
|
285
|
+
// {
|
|
286
|
+
// hasAccessToken: true,
|
|
287
|
+
// hasRefreshToken: true,
|
|
288
|
+
// accessTokenExpiry: Date,
|
|
289
|
+
// refreshTokenExpiry: Date,
|
|
290
|
+
// isAccessTokenValid: true,
|
|
291
|
+
// isRefreshTokenValid: true
|
|
292
|
+
// }
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Build Process
|
|
298
|
+
|
|
299
|
+
### Prerequisites
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
node >= 18
|
|
303
|
+
npm >= 9
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Install
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
cd rai-widget-sdks
|
|
310
|
+
npm install
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Development build (unminified, fast)
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm run build
|
|
317
|
+
# → dist/rai-widget.iife.js (~17 kB, IIFE for CDN/script-tag)
|
|
318
|
+
# → dist/rai-widget.js (~17 kB, ESM for bundlers/npm)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Production build (minified with Terser)
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
npm run build:min
|
|
325
|
+
# → dist/rai-widget.iife.js (~6 kB gzip, IIFE for CDN/script-tag)
|
|
326
|
+
# → dist/rai-widget.js (~6 kB gzip, ESM for bundlers/npm)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Local dev server
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npm run dev
|
|
333
|
+
# Opens Vite dev server — open test/index.html via the dev server URL
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### How the build works
|
|
337
|
+
|
|
338
|
+
1. **Entry**: `src/index.ts` — imports `StoreWidget`, registers the custom element, runs the bootstrap
|
|
339
|
+
2. **CSS inlining**: `src/styles/widget.css` is imported with Vite's `?inline` suffix, which converts it to a JavaScript string at build time — no separate CSS file is emitted
|
|
340
|
+
3. **Output formats**: `iife` (Immediately Invoked Function Expression) for CDN/script-tag embeds → `dist/rai-widget.iife.js`; `es` (ES module) for bundlers and npm consumers → `dist/rai-widget.js`
|
|
341
|
+
4. **Version injection**: `vite.config.ts` reads `version` from `package.json` and replaces the `__WIDGET_VERSION__` placeholder at build time
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
src/index.ts
|
|
345
|
+
├── src/StoreWidget.ts
|
|
346
|
+
│ └── src/BaseWidget.ts ← shared auth/iframe/Shadow DOM logic
|
|
347
|
+
│ ├── src/types.ts
|
|
348
|
+
│ ├── src/core/auth.ts
|
|
349
|
+
│ │ └── src/core/storage.ts
|
|
350
|
+
│ ├── src/core/storage.ts
|
|
351
|
+
│ ├── src/core/postmessage.ts
|
|
352
|
+
│ └── src/styles/widget.css [inlined as string]
|
|
353
|
+
├── src/ChannelWidget.ts └── src/BaseWidget.ts
|
|
354
|
+
├── src/MilestoneWidget.ts └── src/BaseWidget.ts
|
|
355
|
+
├── src/SocialWidget.ts └── src/BaseWidget.ts
|
|
356
|
+
└── src/CurrencyWidget.ts └── src/BaseWidget.ts
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
To release a new version, bump `version` in `package.json`, rebuild, and upload `dist/rai-widget.iife.js` to the CDN under the new version path. Run `npm publish` to push the ESM build to the npm registry (`prepublishOnly` handles the rebuild automatically).
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Project Structure
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
rai-widget-sdks/
|
|
367
|
+
├── src/
|
|
368
|
+
│ ├── types.ts # WidgetConfig, WidgetState, TokenData interfaces
|
|
369
|
+
│ ├── BaseWidget.ts # Abstract HTMLElement subclass — Shadow Root, auth, iframe
|
|
370
|
+
│ ├── StoreWidget.ts # Extends BaseWidget; store micro-frontend URL builder
|
|
371
|
+
│ ├── ChannelWidget.ts # Extends BaseWidget; channel SSR URL builder
|
|
372
|
+
│ ├── MilestoneWidget.ts # Extends BaseWidget; milestone SSR URL builder
|
|
373
|
+
│ ├── SocialWidget.ts # Extends BaseWidget; social SSR URL builder
|
|
374
|
+
│ ├── CurrencyWidget.ts # Extends BaseWidget; currency-view SSR URL builder
|
|
375
|
+
│ ├── index.ts # Registers all 5 custom elements + IIFE bootstrap
|
|
376
|
+
│ ├── jsx.d.ts # React JSX IntrinsicElements + Vue GlobalComponents type shims
|
|
377
|
+
│ ├── vite-env.d.ts # Type declarations for ?inline imports + __WIDGET_VERSION__
|
|
378
|
+
│ ├── core/
|
|
379
|
+
│ │ ├── auth.ts # Serverless auth, refresh, logout, error settings
|
|
380
|
+
│ │ ├── storage.ts # localStorage helpers (refresh token persistence)
|
|
381
|
+
│ │ └── postmessage.ts # iframe token delivery + message listener
|
|
382
|
+
│ └── styles/
|
|
383
|
+
│ └── widget.css # All loader/error/iframe CSS (inlined into Shadow DOM)
|
|
384
|
+
├── test/
|
|
385
|
+
│ └── index.html # Local test page with config panel and verification checklist
|
|
386
|
+
├── dist/ # Built output (gitignored)
|
|
387
|
+
│ ├── rai-widget.iife.js # IIFE bundle for CDN / <script src> embeds
|
|
388
|
+
│ ├── rai-widget.js # ESM bundle for bundlers / npm consumers
|
|
389
|
+
│ └── types/ # TypeScript declarations (generated by tsc)
|
|
390
|
+
├── package.json
|
|
391
|
+
├── tsconfig.json
|
|
392
|
+
└── vite.config.ts
|
|
393
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var RaiWidget=function(e){"use strict";var t=Object.defineProperty,r=(e,r,a)=>((e,r,a)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[r]=a)(e,"symbol"!=typeof r?r+"":r,a);function a(e){return`${e.storagePrefix}-${e.widgetId}-auth`}function i(e){return`${e.storagePrefix}-${e.widgetId}-error-settings`}function s(e){try{localStorage.removeItem(a(e))}catch{}}function n(e){return!e||Date.now()>=e-6e4}function o(e,t,r,i){t.accessToken=r.accessToken,t.refreshToken=r.refreshToken,t.tokenFamily=r.tokenFamily??null;const s=Date.now();t.accessTokenExpiry=s+1e3*r.accessTokenTTL,t.refreshTokenExpiry=s+1e3*r.refreshTokenTTL,function(e,t){try{const r={refreshToken:t.refreshToken,tokenFamily:t.tokenFamily,refreshTokenExpiry:t.refreshTokenExpiry};localStorage.setItem(a(e),JSON.stringify(r))}catch{}}(e,t),e.autoRefresh&&i&&i()}async function l(e,t,r){const a=crypto.randomUUID(),i=Date.now();try{const s=await fetch(`${e.apiUrl}/${e.widgetId}/auth/serverless`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({nonce:a,timestamp:i,widgetType:e.widgetType,userIdentifiers:e.userIdentifiers}),credentials:"include"});if(!s.ok){const e=await s.json().catch(()=>({error:"Authentication failed"}));throw new Error(e.error||`HTTP ${s.status}`)}const n=await s.json();if(!n.accessToken||!n.refreshToken)throw new Error("Invalid authentication response");return o(e,t,n,r),t.isAuthenticated=!0,!0}catch{return t.isAuthenticated=!1,!1}}async function c(e,t,r,a){return t.isRefreshing?t.refreshPromise:!!t.refreshToken&&(t.isRefreshing=!0,t.refreshPromise=(async()=>{try{const i=await fetch(`${e.apiUrl}/${e.widgetId}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t.refreshToken}`},body:JSON.stringify({widgetType:e.widgetType})});if(!i.ok){if(401===i.status||403===i.status)return h(t),s(e),l(e,t,r);const a=await i.json().catch(()=>({error:"Token refresh failed"}));throw new Error(a.error||`HTTP ${i.status}`)}const n=await i.json();if(!n.accessToken||!n.refreshToken)throw new Error("Invalid refresh response");return o(e,t,n,r),a&&a(),!0}catch{return h(t),s(e),l(e,t,r)}finally{t.isRefreshing=!1,t.refreshPromise=null}})(),t.refreshPromise)}async function d(e,t){const r=function(e){try{const t=localStorage.getItem(i(e));if(!t)return null;const r=JSON.parse(t);return r.cachedAt&&Date.now()-r.cachedAt<36e5?r:(localStorage.removeItem(i(e)),null)}catch{return null}}(e);if(r)return r.configured&&(t.errorSettings={errorMessage:r.errorMessage,modalColor:r.modalColor,backgroundImage:r.backgroundImage}),t.errorSettings;try{const r=await fetch(`${e.apiUrl}/${e.widgetId}/auth/error-settings?widgetType=${e.widgetType}`,{method:"GET",headers:{"Content-Type":"application/json"}});if(!r.ok)return null;const a=await r.json();return function(e,t){try{localStorage.setItem(i(e),JSON.stringify({...t,cachedAt:Date.now()}))}catch{}}(e,a),a.configured&&(t.errorSettings={errorMessage:a.errorMessage,modalColor:a.modalColor,backgroundImage:a.backgroundImage}),t.errorSettings}catch{return null}}function h(e){e.accessToken=null,e.refreshToken=null,e.tokenFamily=null,e.accessTokenExpiry=null,e.refreshTokenExpiry=null,e.isAuthenticated=!1,e.isRefreshing=!1,e.refreshPromise=null,e.refreshTimer&&(clearTimeout(e.refreshTimer),e.refreshTimer=null),e.syncTimer&&(clearInterval(e.syncTimer),e.syncTimer=null)}function f(e,t,r){var a;if(t.accessToken)try{null==(a=r.contentWindow)||a.postMessage({type:"RETURNINGAI_WIDGET_TOKEN",value:{widgetId:e.widgetId,token:t.accessToken}},e.widgetDomain)}catch{}}function u(e,t,r,a,i,s,o){const d=async r=>{var d;if(r.origin!==e.widgetDomain)return;if(!r.data||"string"!=typeof r.data.type)return;const{type:h,containerId:f,widgetId:u,payload:g}=r.data;switch(h){case"RETURNINGAI_WIDGET_REQUEST_TOKEN":try{const r=await async function(e,t,r){if(t.accessToken&&!n(t.accessTokenExpiry))return t.accessToken;if(await c(e,t,r)&&t.accessToken)return t.accessToken;if(await l(e,t,r)&&t.accessToken)return t.accessToken;throw new Error("Unable to obtain valid token")}(e,t,i);null==(d=a.contentWindow)||d.postMessage({type:"RETURNINGAI_WIDGET_TOKEN",value:{token:r,widgetId:e.widgetId}},e.widgetDomain)}catch{}break;case"WIDGET_HEIGHT_UPDATE":{const e=Number(null==g?void 0:g.height);Number.isFinite(e)&&e>0&&(a.style.height=`${e}px`);break}case"WIDGET_READY":if(void 0!==u&&u!==e.widgetId)break;if(void 0!==f&&f!==e.container)break;a.classList.add("loaded"),o&&o();break;case"WIDGET_ERROR":o&&o();break;case"WIDGET_LOGOUT":s&&await s()}};return window.addEventListener("message",d),()=>window.removeEventListener("message",d)}const g=new Set(["widget-id","widget-type","theme","container","width","height","api-url","widget-url","auto-refresh","debug"]);class m extends HTMLElement{constructor(){super(),r(this,"shadow"),r(this,"config"),r(this,"state",{accessToken:null,refreshToken:null,tokenFamily:null,accessTokenExpiry:null,refreshTokenExpiry:null,refreshTimer:null,syncTimer:null,iframe:null,isAuthenticated:!1,isRefreshing:!1,refreshPromise:null,errorSettings:null}),r(this,"loaderEl",null),r(this,"errorEl",null),r(this,"cleanupListener"),this.shadow=this.attachShadow({mode:"closed"})}connectedCallback(){this.config=function(e,t){const r=t=>e.getAttribute(t)??e.getAttribute(`data-${t}`)??"";let a,i=r("widget-url")||"https://widget.returningai.com";i.endsWith("store-widget")&&(i=`${i}/${r("widget-id")}/open-widget`);try{a=new URL(i).origin}catch{a=i}if(i.includes("store-widget")){const e=new URL(i);e.searchParams.set("color",r("theme")||"light"),i=e.toString()}const s=r("widget-id")||t||"",n=r("container")||r("data-container")||`returning-ai-widget-${s}`,o={};return Array.from(e.attributes).forEach(e=>{const t=e.name.toLowerCase();if(!t.startsWith("data-"))return;const r=t.slice(5);g.has(r)||(o[t]=e.value)}),{widgetId:s,widgetType:r("widget-type")||"store",theme:r("theme")||"light",container:n,width:r("width")||"100%",height:r("height")||"600px",apiUrl:r("api-url")||"https://sgtr-eks-widgets.genesiv.org",widgetUrl:i,widgetDomain:a,autoRefresh:"false"!==r("auto-refresh"),debug:"true"===r("debug"),storagePrefix:"returning-ai-widget",userIdentifiers:o}}(this,void 0);const e=document.createElement("style");e.textContent=":host{display:block;position:relative;width:100%;height:100%}.rai-loader{position:absolute;top:0;left:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--rai-loader-bg, #ffffff);border-radius:8px;z-index:10;transition:opacity .3s ease-out}.rai-loader.fade-out{opacity:0;pointer-events:none}.rai-error{display:none;position:absolute;top:0;left:0;width:100%;height:100%;align-items:center;justify-content:center;flex-direction:column;gap:12px;background:var(--rai-error-bg, #1a1a1a);border-radius:8px;padding:24px;box-sizing:border-box;text-align:center;color:var(--rai-error-text, #9ca3af);font-family:system-ui,-apple-system,sans-serif;z-index:10}.rai-error.visible{display:flex}iframe{display:block;border:none;opacity:0;transition:opacity .3s ease-in}iframe.loaded{opacity:1}.loader{position:relative;width:75px;height:100px}.loader__bar{position:absolute;bottom:0;width:10px;height:50%;background:var(--rai-accent, #000000);transform-origin:center bottom;box-shadow:1px 1px #0003}.loader__bar:nth-child(1){left:0;transform:scaleY(.2);animation:barUp1 4s infinite}.loader__bar:nth-child(2){left:15px;transform:scaleY(.4);animation:barUp2 4s infinite}.loader__bar:nth-child(3){left:30px;transform:scaleY(.6);animation:barUp3 4s infinite}.loader__bar:nth-child(4){left:45px;transform:scaleY(.8);animation:barUp4 4s infinite}.loader__bar:nth-child(5){left:60px;transform:scale(1);animation:barUp5 4s infinite}.loader__ball{position:absolute;bottom:10px;left:0;width:10px;height:10px;background:var(--rai-accent, #000000);border-radius:50%;animation:ball 4s infinite}@keyframes ball{0%{transform:translate(0)}5%{transform:translate(8px,-14px)}10%{transform:translate(15px,-10px)}17%{transform:translate(23px,-24px)}20%{transform:translate(30px,-20px)}27%{transform:translate(38px,-34px)}30%{transform:translate(45px,-30px)}37%{transform:translate(53px,-44px)}40%{transform:translate(60px,-40px)}50%{transform:translate(60px)}57%{transform:translate(53px,-14px)}60%{transform:translate(45px,-10px)}67%{transform:translate(37px,-24px)}70%{transform:translate(30px,-20px)}77%{transform:translate(22px,-34px)}80%{transform:translate(15px,-30px)}87%{transform:translate(7px,-44px)}90%{transform:translateY(-40px)}to{transform:translate(0)}}@keyframes barUp1{0%{transform:scaleY(.2)}40%{transform:scaleY(.2)}50%{transform:scale(1)}90%{transform:scale(1)}to{transform:scaleY(.2)}}@keyframes barUp2{0%{transform:scaleY(.4)}40%{transform:scaleY(.4)}50%{transform:scaleY(.8)}90%{transform:scaleY(.8)}to{transform:scaleY(.4)}}@keyframes barUp3{0%{transform:scaleY(.6)}to{transform:scaleY(.6)}}@keyframes barUp4{0%{transform:scaleY(.8)}40%{transform:scaleY(.8)}50%{transform:scaleY(.4)}90%{transform:scaleY(.4)}to{transform:scaleY(.8)}}@keyframes barUp5{0%{transform:scale(1)}40%{transform:scale(1)}50%{transform:scaleY(.2)}90%{transform:scaleY(.2)}to{transform:scale(1)}}#loading-square{width:75px;aspect-ratio:1;display:flex;color:var(--rai-accent, #000000);background:linear-gradient(currentColor 0 0) right / 51% 100%,linear-gradient(currentColor 0 0) bottom / 100% 51%;background-repeat:no-repeat;animation:l16-0 2s infinite linear .25s}#loading-square>div{width:50%;height:50%;background:currentColor;animation:l16-1 .5s infinite linear}@keyframes l16-0{0%,12.49%{transform:rotate(0)}12.5%,37.49%{transform:rotate(90deg)}37.5%,62.49%{transform:rotate(180deg)}62.5%,87.49%{transform:rotate(270deg)}87.5%,to{transform:rotate(360deg)}}@keyframes l16-1{0%{transform:perspective(80px) rotate3d(-1,-1,0,0)}80%,to{transform:perspective(80px) rotate3d(-1,-1,0,-180deg)}}#loading-circle{width:75px;aspect-ratio:1;display:grid;grid:50%/50%;color:var(--rai-accent, #000000);border-radius:50%;--_g: no-repeat linear-gradient(currentColor 0 0);background:var(--_g),var(--_g),var(--_g);background-size:50.1% 50.1%;animation:l9-0 1.5s infinite steps(1) alternate,l9-0-0 3s infinite steps(1) alternate}#loading-circle>div{background:var(--rai-text4, #6b7280);border-top-left-radius:100px;transform:perspective(150px) rotateY(0) rotateX(0);transform-origin:bottom right;animation:l9-1 1.5s infinite linear alternate}@keyframes l9-0{0%{background-position:0 100%,100% 100%,100% 0}33%{background-position:100% 100%,100% 100%,100% 0}66%{background-position:100% 0,100% 0,100% 0}}@keyframes l9-0-0{0%{transform:scaleX(1) rotate(0)}50%{transform:scaleX(-1) rotate(-90deg)}}@keyframes l9-1{16.5%{transform:perspective(150px) rotateX(-90deg) rotateY(0) rotateX(0);filter:grayscale(.8)}33%{transform:perspective(150px) rotateX(-180deg) rotateY(0) rotateX(0)}66%{transform:perspective(150px) rotateX(-180deg) rotateY(-180deg) rotateX(0)}to{transform:perspective(150px) rotateX(-180deg) rotateY(-180deg) rotateX(-180deg);filter:grayscale(.8)}}",this.shadow.appendChild(e);const t=this.config.theme,r="dark"===t?"#ffffff":"#000000",a="dark"===t?"#9ca3af":"#6b7280",i="dark"===t?"#1a1a1a":"#ffffff";this.style.setProperty("--rai-accent",r),this.style.setProperty("--rai-text4",a),this.style.setProperty("--rai-loader-bg",i),this.style.setProperty("--rai-error-bg",i),this.renderShell(),this.init()}disconnectedCallback(){this.cleanupListener&&this.cleanupListener(),h(this.state)}renderShell(){this.loaderEl=document.createElement("div"),this.loaderEl.className="rai-loader",this.loaderEl.appendChild(this.createDefaultLoader()),this.shadow.appendChild(this.loaderEl),this.errorEl=document.createElement("div"),this.errorEl.className="rai-error",this.errorEl.textContent="Authentication failed. Please try again later.",this.shadow.appendChild(this.errorEl)}createDefaultLoader(){const e=document.createElement("div");e.className="loader";for(let r=0;r<5;r++){const t=document.createElement("div");t.className="loader__bar",e.appendChild(t)}const t=document.createElement("div");return t.className="loader__ball",e.appendChild(t),e}hideLoader(){this.loaderEl&&(this.loaderEl.classList.add("fade-out"),setTimeout(()=>{var e;return null==(e=this.loaderEl)?void 0:e.remove()},300),this.loaderEl=null)}showError(){var e;this.hideLoader(),this.errorEl&&((null==(e=this.state.errorSettings)?void 0:e.errorMessage)&&(this.errorEl.textContent=this.state.errorSettings.errorMessage),this.errorEl.classList.add("visible"))}async init(){if(!this.config.widgetId)return void this.showError();await d(this.config,this.state);if(function(e,t){try{const r=localStorage.getItem(a(e));if(!r)return!1;const i=JSON.parse(r);return i.refreshToken&&!n(i.refreshTokenExpiry)?(t.refreshToken=i.refreshToken,t.tokenFamily=i.tokenFamily,t.refreshTokenExpiry=i.refreshTokenExpiry,!0):(s(e),!1)}catch{return!1}}(this.config,this.state)){return await c(this.config,this.state,()=>this.scheduleRefresh())?(this.state.isAuthenticated=!0,void this.createIframe()):void this.showError()}await l(this.config,this.state,()=>this.scheduleRefresh())?this.createIframe():this.showError()}createIframe(){const e=document.createElement("iframe");e.src=this.buildWidgetUrl(this.config),e.allow="clipboard-write",e.frameBorder="0",e.scrolling="no",e.style.cssText=`width: ${this.config.width}; height: ${this.config.height}; border: none; display: block;`,e.onload=()=>{f(this.config,this.state,e),this.schedulePeriodicSync(e)},e.onerror=()=>{this.showError()},this.state.iframe=e,this.shadow.appendChild(e),this.cleanupListener=u(this.config,this.state,this.shadow,e,()=>this.scheduleRefresh(),()=>this.logoutAndClear(),()=>this.hideLoader())}scheduleRefresh(){if(this.state.refreshTimer&&clearTimeout(this.state.refreshTimer),!this.state.accessTokenExpiry)return;const e=this.state.accessTokenExpiry-Date.now()-6e4;e>0?this.state.refreshTimer=setTimeout(async()=>{await c(this.config,this.state,()=>this.scheduleRefresh(),()=>{this.state.iframe&&f(this.config,this.state,this.state.iframe)})},e):c(this.config,this.state,()=>this.scheduleRefresh(),()=>{this.state.iframe&&f(this.config,this.state,this.state.iframe)})}schedulePeriodicSync(e){this.state.syncTimer&&clearInterval(this.state.syncTimer),this.state.syncTimer=setInterval(()=>{this.state.accessToken&&f(this.config,this.state,e)},12e4)}async logoutAndClear(){await async function(e,t){if(t.accessToken)try{await fetch(`${e.apiUrl}/${e.widgetId}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t.accessToken}`},body:JSON.stringify({refreshToken:t.refreshToken})})}catch{}h(t),s(e)}(this.config,this.state),this.shadow.querySelectorAll("iframe").forEach(e=>e.remove())}async reload(){h(this.state),this.shadow.querySelectorAll("iframe").forEach(e=>e.remove()),await this.init()}async logoutPublic(){await this.logoutAndClear()}isAuthenticated(){return this.state.isAuthenticated&&null!==this.state.accessToken}getTokenInfo(){return{hasAccessToken:!!this.state.accessToken,hasRefreshToken:!!this.state.refreshToken,accessTokenExpiry:this.state.accessTokenExpiry?new Date(this.state.accessTokenExpiry):null,refreshTokenExpiry:this.state.refreshTokenExpiry?new Date(this.state.refreshTokenExpiry):null,isAccessTokenValid:!!this.state.accessToken&&!n(this.state.accessTokenExpiry),isRefreshTokenValid:!!this.state.refreshToken&&!n(this.state.refreshTokenExpiry)}}}class p extends m{buildWidgetUrl(e){const t=new URL(e.widgetUrl);return t.searchParams.set("color",e.theme),t.searchParams.set("containerId",e.container),t.searchParams.set("connectType","simple"),t.searchParams.set("mode","private"),t.toString()}}class w extends m{buildWidgetUrl(e){return e.widgetUrl.endsWith("channel-widget")?`${e.widgetUrl}/${e.widgetId}`:e.widgetUrl}}class y extends m{buildWidgetUrl(e){return e.widgetUrl.endsWith("milestone-widget")?`${e.widgetUrl}/${e.widgetId}`:e.widgetUrl}}class T extends m{buildWidgetUrl(e){return e.widgetUrl.endsWith("social-widget")?`${e.widgetUrl}/${e.widgetId}`:e.widgetUrl}}class k extends m{buildWidgetUrl(e){return e.widgetUrl.endsWith("currency-overview-widget")?`${e.widgetUrl}/${e.widgetId}`:e.widgetUrl}}console.log("[rai-widget] v1.0.0");const b=[["rai-widget",p],["rai-channel-widget",w],["rai-milestone-widget",y],["rai-social-widget",T],["rai-currency-widget",k]];for(const[S,U]of b)customElements.get(S)||customElements.define(S,U);const x={store:p,channel:w,milestone:y,social:T,"currency-view":k},E=b.map(([e])=>e).join(", ");function v(){var e;const t=((null==(e=document.currentScript)?void 0:e.hasAttribute("data-widget-id"))?document.currentScript:null)||document.querySelector("script[data-widget-id]");if(!t)return;const r=t.getAttribute("data-widget-id");if(!r)return;const a=t.getAttribute("data-container")||`returning-ai-widget-${r}`,i=document.getElementById(a);if(!i)return;"static"===getComputedStyle(i).position&&(i.style.position="relative");const s=t.getAttribute("data-widget-type")??"store",n=new(x[s]??p);Array.from(t.attributes).forEach(e=>{n.setAttribute(e.name,e.value)}),i.appendChild(n)}function I(){const e=(()=>{var e;const t=((null==(e=document.currentScript)?void 0:e.hasAttribute("data-widget-id"))?document.currentScript:null)||document.querySelector("script[data-widget-id]");if(!t)return null;const r=t.getAttribute("data-container")||`returning-ai-widget-${t.getAttribute("data-widget-id")}`;return document.getElementById(r)})(),t=null==e?void 0:e.querySelector(E);window.ReturningAIWidget={version:"1.0.0",reload:()=>(null==t?void 0:t.reload())??Promise.resolve(),logout:()=>(null==t?void 0:t.logoutPublic())??Promise.resolve(),isAuthenticated:()=>(null==t?void 0:t.isAuthenticated())??!1,getTokenInfo:()=>(null==t?void 0:t.getTokenInfo())??{}}}return"loading"===document.readyState?document.addEventListener("DOMContentLoaded",v):v(),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",I):I(),e.ChannelWidget=w,e.CurrencyWidget=k,e.MilestoneWidget=y,e.SocialWidget=T,e.StoreWidget=p,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),e}({});
|
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
function storageKey(config) {
|
|
5
|
+
return `${config.storagePrefix}-${config.widgetId}-auth`;
|
|
6
|
+
}
|
|
7
|
+
function errorSettingsKey(config) {
|
|
8
|
+
return `${config.storagePrefix}-${config.widgetId}-error-settings`;
|
|
9
|
+
}
|
|
10
|
+
function saveToStorage(config, state) {
|
|
11
|
+
try {
|
|
12
|
+
const data = {
|
|
13
|
+
refreshToken: state.refreshToken,
|
|
14
|
+
tokenFamily: state.tokenFamily,
|
|
15
|
+
refreshTokenExpiry: state.refreshTokenExpiry
|
|
16
|
+
};
|
|
17
|
+
localStorage.setItem(storageKey(config), JSON.stringify(data));
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function loadFromStorage(config, state) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = localStorage.getItem(storageKey(config));
|
|
24
|
+
if (!raw) return false;
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
if (parsed.refreshToken && !isTokenExpired(parsed.refreshTokenExpiry)) {
|
|
27
|
+
state.refreshToken = parsed.refreshToken;
|
|
28
|
+
state.tokenFamily = parsed.tokenFamily;
|
|
29
|
+
state.refreshTokenExpiry = parsed.refreshTokenExpiry;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
clearStorage(config);
|
|
33
|
+
return false;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function clearStorage(config) {
|
|
39
|
+
try {
|
|
40
|
+
localStorage.removeItem(storageKey(config));
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function loadErrorSettingsFromStorage(config) {
|
|
45
|
+
const ERROR_SETTINGS_CACHE_TTL = 36e5;
|
|
46
|
+
try {
|
|
47
|
+
const raw = localStorage.getItem(errorSettingsKey(config));
|
|
48
|
+
if (!raw) return null;
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (parsed.cachedAt && Date.now() - parsed.cachedAt < ERROR_SETTINGS_CACHE_TTL) {
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
localStorage.removeItem(errorSettingsKey(config));
|
|
54
|
+
return null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function saveErrorSettingsToStorage(config, settings) {
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem(errorSettingsKey(config), JSON.stringify({ ...settings, cachedAt: Date.now() }));
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function isTokenExpired(expiry) {
|
|
66
|
+
if (!expiry) return true;
|
|
67
|
+
return Date.now() >= expiry - 6e4;
|
|
68
|
+
}
|
|
69
|
+
function setTokens(config, state, data, onRefreshScheduled) {
|
|
70
|
+
state.accessToken = data.accessToken;
|
|
71
|
+
state.refreshToken = data.refreshToken;
|
|
72
|
+
state.tokenFamily = data.tokenFamily ?? null;
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
state.accessTokenExpiry = now + data.accessTokenTTL * 1e3;
|
|
75
|
+
state.refreshTokenExpiry = now + data.refreshTokenTTL * 1e3;
|
|
76
|
+
saveToStorage(config, state);
|
|
77
|
+
if (config.autoRefresh && onRefreshScheduled) {
|
|
78
|
+
onRefreshScheduled();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function authenticateServerless(config, state, onRefreshScheduled) {
|
|
82
|
+
const nonce = crypto.randomUUID();
|
|
83
|
+
const timestamp = Date.now();
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(`${config.apiUrl}/${config.widgetId}/auth/serverless`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "Content-Type": "application/json" },
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
nonce,
|
|
90
|
+
timestamp,
|
|
91
|
+
widgetType: config.widgetType,
|
|
92
|
+
userIdentifiers: config.userIdentifiers
|
|
93
|
+
}),
|
|
94
|
+
credentials: "include"
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await response.json().catch(() => ({ error: "Authentication failed" }));
|
|
98
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
if (!data.accessToken || !data.refreshToken) {
|
|
102
|
+
throw new Error("Invalid authentication response");
|
|
103
|
+
}
|
|
104
|
+
setTokens(config, state, data, onRefreshScheduled);
|
|
105
|
+
state.isAuthenticated = true;
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
state.isAuthenticated = false;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function refreshAccessToken(config, state, onRefreshScheduled, sendToken) {
|
|
113
|
+
if (state.isRefreshing) {
|
|
114
|
+
return state.refreshPromise;
|
|
115
|
+
}
|
|
116
|
+
if (!state.refreshToken) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
state.isRefreshing = true;
|
|
120
|
+
state.refreshPromise = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(`${config.apiUrl}/${config.widgetId}/auth/refresh`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
Authorization: `Bearer ${state.refreshToken}`
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({ widgetType: config.widgetType })
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
if (response.status === 401 || response.status === 403) {
|
|
132
|
+
clearState(state);
|
|
133
|
+
clearStorage(config);
|
|
134
|
+
return authenticateServerless(config, state, onRefreshScheduled);
|
|
135
|
+
}
|
|
136
|
+
const error = await response.json().catch(() => ({ error: "Token refresh failed" }));
|
|
137
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
138
|
+
}
|
|
139
|
+
const data = await response.json();
|
|
140
|
+
if (!data.accessToken || !data.refreshToken) {
|
|
141
|
+
throw new Error("Invalid refresh response");
|
|
142
|
+
}
|
|
143
|
+
setTokens(config, state, data, onRefreshScheduled);
|
|
144
|
+
if (sendToken) sendToken();
|
|
145
|
+
return true;
|
|
146
|
+
} catch {
|
|
147
|
+
clearState(state);
|
|
148
|
+
clearStorage(config);
|
|
149
|
+
return authenticateServerless(config, state, onRefreshScheduled);
|
|
150
|
+
} finally {
|
|
151
|
+
state.isRefreshing = false;
|
|
152
|
+
state.refreshPromise = null;
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
155
|
+
return state.refreshPromise;
|
|
156
|
+
}
|
|
157
|
+
async function getValidToken(config, state, onRefreshScheduled) {
|
|
158
|
+
if (state.accessToken && !isTokenExpired(state.accessTokenExpiry)) {
|
|
159
|
+
return state.accessToken;
|
|
160
|
+
}
|
|
161
|
+
const refreshed = await refreshAccessToken(config, state, onRefreshScheduled);
|
|
162
|
+
if (refreshed && state.accessToken) return state.accessToken;
|
|
163
|
+
const authed = await authenticateServerless(config, state, onRefreshScheduled);
|
|
164
|
+
if (authed && state.accessToken) return state.accessToken;
|
|
165
|
+
throw new Error("Unable to obtain valid token");
|
|
166
|
+
}
|
|
167
|
+
async function logout(config, state) {
|
|
168
|
+
if (state.accessToken) {
|
|
169
|
+
try {
|
|
170
|
+
await fetch(`${config.apiUrl}/${config.widgetId}/auth/logout`, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
Authorization: `Bearer ${state.accessToken}`
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({ refreshToken: state.refreshToken })
|
|
177
|
+
});
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
clearState(state);
|
|
182
|
+
clearStorage(config);
|
|
183
|
+
}
|
|
184
|
+
async function fetchErrorSettings(config, state) {
|
|
185
|
+
const cached = loadErrorSettingsFromStorage(config);
|
|
186
|
+
if (cached) {
|
|
187
|
+
if (cached.configured) {
|
|
188
|
+
state.errorSettings = {
|
|
189
|
+
errorMessage: cached.errorMessage,
|
|
190
|
+
modalColor: cached.modalColor,
|
|
191
|
+
backgroundImage: cached.backgroundImage
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return state.errorSettings;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch(
|
|
198
|
+
`${config.apiUrl}/${config.widgetId}/auth/error-settings?widgetType=${config.widgetType}`,
|
|
199
|
+
{ method: "GET", headers: { "Content-Type": "application/json" } }
|
|
200
|
+
);
|
|
201
|
+
if (!response.ok) return null;
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
saveErrorSettingsToStorage(config, data);
|
|
204
|
+
if (data.configured) {
|
|
205
|
+
state.errorSettings = {
|
|
206
|
+
errorMessage: data.errorMessage,
|
|
207
|
+
modalColor: data.modalColor,
|
|
208
|
+
backgroundImage: data.backgroundImage
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return state.errorSettings;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function clearState(state) {
|
|
217
|
+
state.accessToken = null;
|
|
218
|
+
state.refreshToken = null;
|
|
219
|
+
state.tokenFamily = null;
|
|
220
|
+
state.accessTokenExpiry = null;
|
|
221
|
+
state.refreshTokenExpiry = null;
|
|
222
|
+
state.isAuthenticated = false;
|
|
223
|
+
state.isRefreshing = false;
|
|
224
|
+
state.refreshPromise = null;
|
|
225
|
+
if (state.refreshTimer) {
|
|
226
|
+
clearTimeout(state.refreshTimer);
|
|
227
|
+
state.refreshTimer = null;
|
|
228
|
+
}
|
|
229
|
+
if (state.syncTimer) {
|
|
230
|
+
clearInterval(state.syncTimer);
|
|
231
|
+
state.syncTimer = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function sendTokenToWidget(config, state, iframe) {
|
|
235
|
+
var _a;
|
|
236
|
+
if (!state.accessToken) return;
|
|
237
|
+
try {
|
|
238
|
+
(_a = iframe.contentWindow) == null ? void 0 : _a.postMessage(
|
|
239
|
+
{
|
|
240
|
+
type: "RETURNINGAI_WIDGET_TOKEN",
|
|
241
|
+
value: {
|
|
242
|
+
widgetId: config.widgetId,
|
|
243
|
+
token: state.accessToken
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
config.widgetDomain
|
|
247
|
+
);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function setupMessageListener(config, state, _shadow, iframe, onRefreshScheduled, onLogout, hideLoader) {
|
|
252
|
+
const handler = async (event) => {
|
|
253
|
+
var _a;
|
|
254
|
+
if (event.origin !== config.widgetDomain) return;
|
|
255
|
+
if (!event.data || typeof event.data.type !== "string") return;
|
|
256
|
+
const { type, containerId, widgetId, payload } = event.data;
|
|
257
|
+
switch (type) {
|
|
258
|
+
case "RETURNINGAI_WIDGET_REQUEST_TOKEN": {
|
|
259
|
+
try {
|
|
260
|
+
const token = await getValidToken(config, state, onRefreshScheduled);
|
|
261
|
+
(_a = iframe.contentWindow) == null ? void 0 : _a.postMessage(
|
|
262
|
+
{
|
|
263
|
+
type: "RETURNINGAI_WIDGET_TOKEN",
|
|
264
|
+
value: { token, widgetId: config.widgetId }
|
|
265
|
+
},
|
|
266
|
+
config.widgetDomain
|
|
267
|
+
);
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case "WIDGET_HEIGHT_UPDATE": {
|
|
273
|
+
const h = Number(payload == null ? void 0 : payload.height);
|
|
274
|
+
if (Number.isFinite(h) && h > 0) {
|
|
275
|
+
iframe.style.height = `${h}px`;
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "WIDGET_READY": {
|
|
280
|
+
if (widgetId !== void 0 && widgetId !== config.widgetId) break;
|
|
281
|
+
if (containerId !== void 0 && containerId !== config.container) break;
|
|
282
|
+
iframe.classList.add("loaded");
|
|
283
|
+
if (hideLoader) hideLoader();
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case "WIDGET_ERROR": {
|
|
287
|
+
if (hideLoader) hideLoader();
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "WIDGET_LOGOUT": {
|
|
291
|
+
if (onLogout) await onLogout();
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
window.addEventListener("message", handler);
|
|
297
|
+
return () => window.removeEventListener("message", handler);
|
|
298
|
+
}
|
|
299
|
+
const widgetCSS = ":host{display:block;position:relative;width:100%;height:100%}.rai-loader{position:absolute;top:0;left:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--rai-loader-bg, #ffffff);border-radius:8px;z-index:10;transition:opacity .3s ease-out}.rai-loader.fade-out{opacity:0;pointer-events:none}.rai-error{display:none;position:absolute;top:0;left:0;width:100%;height:100%;align-items:center;justify-content:center;flex-direction:column;gap:12px;background:var(--rai-error-bg, #1a1a1a);border-radius:8px;padding:24px;box-sizing:border-box;text-align:center;color:var(--rai-error-text, #9ca3af);font-family:system-ui,-apple-system,sans-serif;z-index:10}.rai-error.visible{display:flex}iframe{display:block;border:none;opacity:0;transition:opacity .3s ease-in}iframe.loaded{opacity:1}.loader{position:relative;width:75px;height:100px}.loader__bar{position:absolute;bottom:0;width:10px;height:50%;background:var(--rai-accent, #000000);transform-origin:center bottom;box-shadow:1px 1px #0003}.loader__bar:nth-child(1){left:0;transform:scaleY(.2);animation:barUp1 4s infinite}.loader__bar:nth-child(2){left:15px;transform:scaleY(.4);animation:barUp2 4s infinite}.loader__bar:nth-child(3){left:30px;transform:scaleY(.6);animation:barUp3 4s infinite}.loader__bar:nth-child(4){left:45px;transform:scaleY(.8);animation:barUp4 4s infinite}.loader__bar:nth-child(5){left:60px;transform:scale(1);animation:barUp5 4s infinite}.loader__ball{position:absolute;bottom:10px;left:0;width:10px;height:10px;background:var(--rai-accent, #000000);border-radius:50%;animation:ball 4s infinite}@keyframes ball{0%{transform:translate(0)}5%{transform:translate(8px,-14px)}10%{transform:translate(15px,-10px)}17%{transform:translate(23px,-24px)}20%{transform:translate(30px,-20px)}27%{transform:translate(38px,-34px)}30%{transform:translate(45px,-30px)}37%{transform:translate(53px,-44px)}40%{transform:translate(60px,-40px)}50%{transform:translate(60px)}57%{transform:translate(53px,-14px)}60%{transform:translate(45px,-10px)}67%{transform:translate(37px,-24px)}70%{transform:translate(30px,-20px)}77%{transform:translate(22px,-34px)}80%{transform:translate(15px,-30px)}87%{transform:translate(7px,-44px)}90%{transform:translateY(-40px)}to{transform:translate(0)}}@keyframes barUp1{0%{transform:scaleY(.2)}40%{transform:scaleY(.2)}50%{transform:scale(1)}90%{transform:scale(1)}to{transform:scaleY(.2)}}@keyframes barUp2{0%{transform:scaleY(.4)}40%{transform:scaleY(.4)}50%{transform:scaleY(.8)}90%{transform:scaleY(.8)}to{transform:scaleY(.4)}}@keyframes barUp3{0%{transform:scaleY(.6)}to{transform:scaleY(.6)}}@keyframes barUp4{0%{transform:scaleY(.8)}40%{transform:scaleY(.8)}50%{transform:scaleY(.4)}90%{transform:scaleY(.4)}to{transform:scaleY(.8)}}@keyframes barUp5{0%{transform:scale(1)}40%{transform:scale(1)}50%{transform:scaleY(.2)}90%{transform:scaleY(.2)}to{transform:scale(1)}}#loading-square{width:75px;aspect-ratio:1;display:flex;color:var(--rai-accent, #000000);background:linear-gradient(currentColor 0 0) right / 51% 100%,linear-gradient(currentColor 0 0) bottom / 100% 51%;background-repeat:no-repeat;animation:l16-0 2s infinite linear .25s}#loading-square>div{width:50%;height:50%;background:currentColor;animation:l16-1 .5s infinite linear}@keyframes l16-0{0%,12.49%{transform:rotate(0)}12.5%,37.49%{transform:rotate(90deg)}37.5%,62.49%{transform:rotate(180deg)}62.5%,87.49%{transform:rotate(270deg)}87.5%,to{transform:rotate(360deg)}}@keyframes l16-1{0%{transform:perspective(80px) rotate3d(-1,-1,0,0)}80%,to{transform:perspective(80px) rotate3d(-1,-1,0,-180deg)}}#loading-circle{width:75px;aspect-ratio:1;display:grid;grid:50%/50%;color:var(--rai-accent, #000000);border-radius:50%;--_g: no-repeat linear-gradient(currentColor 0 0);background:var(--_g),var(--_g),var(--_g);background-size:50.1% 50.1%;animation:l9-0 1.5s infinite steps(1) alternate,l9-0-0 3s infinite steps(1) alternate}#loading-circle>div{background:var(--rai-text4, #6b7280);border-top-left-radius:100px;transform:perspective(150px) rotateY(0) rotateX(0);transform-origin:bottom right;animation:l9-1 1.5s infinite linear alternate}@keyframes l9-0{0%{background-position:0 100%,100% 100%,100% 0}33%{background-position:100% 100%,100% 100%,100% 0}66%{background-position:100% 0,100% 0,100% 0}}@keyframes l9-0-0{0%{transform:scaleX(1) rotate(0)}50%{transform:scaleX(-1) rotate(-90deg)}}@keyframes l9-1{16.5%{transform:perspective(150px) rotateX(-90deg) rotateY(0) rotateX(0);filter:grayscale(.8)}33%{transform:perspective(150px) rotateX(-180deg) rotateY(0) rotateX(0)}66%{transform:perspective(150px) rotateX(-180deg) rotateY(-180deg) rotateX(0)}to{transform:perspective(150px) rotateX(-180deg) rotateY(-180deg) rotateX(-180deg);filter:grayscale(.8)}}";
|
|
300
|
+
const DEFINED_ATTRS = /* @__PURE__ */ new Set([
|
|
301
|
+
"widget-id",
|
|
302
|
+
"widget-type",
|
|
303
|
+
"theme",
|
|
304
|
+
"container",
|
|
305
|
+
"width",
|
|
306
|
+
"height",
|
|
307
|
+
"api-url",
|
|
308
|
+
"widget-url",
|
|
309
|
+
"auto-refresh",
|
|
310
|
+
"debug"
|
|
311
|
+
]);
|
|
312
|
+
function readConfig(el, existingId) {
|
|
313
|
+
const get = (name) => el.getAttribute(name) ?? el.getAttribute(`data-${name}`) ?? "";
|
|
314
|
+
const rawWidgetUrl = get("widget-url") || "https://widget.returningai.com";
|
|
315
|
+
let widgetUrl = rawWidgetUrl;
|
|
316
|
+
if (widgetUrl.endsWith("store-widget")) {
|
|
317
|
+
widgetUrl = `${widgetUrl}/${get("widget-id")}/open-widget`;
|
|
318
|
+
}
|
|
319
|
+
let widgetDomain;
|
|
320
|
+
try {
|
|
321
|
+
widgetDomain = new URL(widgetUrl).origin;
|
|
322
|
+
} catch {
|
|
323
|
+
widgetDomain = widgetUrl;
|
|
324
|
+
}
|
|
325
|
+
if (widgetUrl.includes("store-widget")) {
|
|
326
|
+
const u = new URL(widgetUrl);
|
|
327
|
+
u.searchParams.set("color", get("theme") || "light");
|
|
328
|
+
widgetUrl = u.toString();
|
|
329
|
+
}
|
|
330
|
+
const widgetId = get("widget-id") || existingId || "";
|
|
331
|
+
const container = get("container") || get("data-container") || `returning-ai-widget-${widgetId}`;
|
|
332
|
+
const userIdentifiers = {};
|
|
333
|
+
Array.from(el.attributes).forEach((attr) => {
|
|
334
|
+
const name = attr.name.toLowerCase();
|
|
335
|
+
if (!name.startsWith("data-")) return;
|
|
336
|
+
const bare = name.slice(5);
|
|
337
|
+
if (!DEFINED_ATTRS.has(bare)) {
|
|
338
|
+
userIdentifiers[name] = attr.value;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
widgetId,
|
|
343
|
+
widgetType: get("widget-type") || "store",
|
|
344
|
+
theme: get("theme") || "light",
|
|
345
|
+
container,
|
|
346
|
+
width: get("width") || "100%",
|
|
347
|
+
height: get("height") || "600px",
|
|
348
|
+
apiUrl: get("api-url") || "https://sgtr-eks-widgets.genesiv.org",
|
|
349
|
+
widgetUrl,
|
|
350
|
+
widgetDomain,
|
|
351
|
+
autoRefresh: get("auto-refresh") !== "false",
|
|
352
|
+
debug: get("debug") === "true",
|
|
353
|
+
storagePrefix: "returning-ai-widget",
|
|
354
|
+
userIdentifiers
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function createInitialState() {
|
|
358
|
+
return {
|
|
359
|
+
accessToken: null,
|
|
360
|
+
refreshToken: null,
|
|
361
|
+
tokenFamily: null,
|
|
362
|
+
accessTokenExpiry: null,
|
|
363
|
+
refreshTokenExpiry: null,
|
|
364
|
+
refreshTimer: null,
|
|
365
|
+
syncTimer: null,
|
|
366
|
+
iframe: null,
|
|
367
|
+
isAuthenticated: false,
|
|
368
|
+
isRefreshing: false,
|
|
369
|
+
refreshPromise: null,
|
|
370
|
+
errorSettings: null
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
class BaseWidget extends HTMLElement {
|
|
374
|
+
constructor() {
|
|
375
|
+
super();
|
|
376
|
+
__publicField(this, "shadow");
|
|
377
|
+
__publicField(this, "config");
|
|
378
|
+
__publicField(this, "state", createInitialState());
|
|
379
|
+
__publicField(this, "loaderEl", null);
|
|
380
|
+
__publicField(this, "errorEl", null);
|
|
381
|
+
__publicField(this, "cleanupListener");
|
|
382
|
+
this.shadow = this.attachShadow({ mode: "closed" });
|
|
383
|
+
}
|
|
384
|
+
connectedCallback() {
|
|
385
|
+
this.config = readConfig(this, void 0);
|
|
386
|
+
const style = document.createElement("style");
|
|
387
|
+
style.textContent = widgetCSS;
|
|
388
|
+
this.shadow.appendChild(style);
|
|
389
|
+
const theme = this.config.theme;
|
|
390
|
+
const accentColor = theme === "dark" ? "#ffffff" : "#000000";
|
|
391
|
+
const text4Color = theme === "dark" ? "#9ca3af" : "#6b7280";
|
|
392
|
+
const bgColor = theme === "dark" ? "#1a1a1a" : "#ffffff";
|
|
393
|
+
this.style.setProperty("--rai-accent", accentColor);
|
|
394
|
+
this.style.setProperty("--rai-text4", text4Color);
|
|
395
|
+
this.style.setProperty("--rai-loader-bg", bgColor);
|
|
396
|
+
this.style.setProperty("--rai-error-bg", bgColor);
|
|
397
|
+
this.renderShell();
|
|
398
|
+
this.init();
|
|
399
|
+
}
|
|
400
|
+
disconnectedCallback() {
|
|
401
|
+
if (this.cleanupListener) this.cleanupListener();
|
|
402
|
+
clearState(this.state);
|
|
403
|
+
}
|
|
404
|
+
// ── Rendering ─────────────────────────────────────────────────────────
|
|
405
|
+
renderShell() {
|
|
406
|
+
this.loaderEl = document.createElement("div");
|
|
407
|
+
this.loaderEl.className = "rai-loader";
|
|
408
|
+
this.loaderEl.appendChild(this.createDefaultLoader());
|
|
409
|
+
this.shadow.appendChild(this.loaderEl);
|
|
410
|
+
this.errorEl = document.createElement("div");
|
|
411
|
+
this.errorEl.className = "rai-error";
|
|
412
|
+
this.errorEl.textContent = "Authentication failed. Please try again later.";
|
|
413
|
+
this.shadow.appendChild(this.errorEl);
|
|
414
|
+
}
|
|
415
|
+
createDefaultLoader() {
|
|
416
|
+
const loader = document.createElement("div");
|
|
417
|
+
loader.className = "loader";
|
|
418
|
+
for (let i = 0; i < 5; i++) {
|
|
419
|
+
const bar = document.createElement("div");
|
|
420
|
+
bar.className = "loader__bar";
|
|
421
|
+
loader.appendChild(bar);
|
|
422
|
+
}
|
|
423
|
+
const ball = document.createElement("div");
|
|
424
|
+
ball.className = "loader__ball";
|
|
425
|
+
loader.appendChild(ball);
|
|
426
|
+
return loader;
|
|
427
|
+
}
|
|
428
|
+
hideLoader() {
|
|
429
|
+
if (!this.loaderEl) return;
|
|
430
|
+
this.loaderEl.classList.add("fade-out");
|
|
431
|
+
setTimeout(() => {
|
|
432
|
+
var _a;
|
|
433
|
+
return (_a = this.loaderEl) == null ? void 0 : _a.remove();
|
|
434
|
+
}, 300);
|
|
435
|
+
this.loaderEl = null;
|
|
436
|
+
}
|
|
437
|
+
showError() {
|
|
438
|
+
var _a;
|
|
439
|
+
this.hideLoader();
|
|
440
|
+
if (this.errorEl) {
|
|
441
|
+
if ((_a = this.state.errorSettings) == null ? void 0 : _a.errorMessage) {
|
|
442
|
+
this.errorEl.textContent = this.state.errorSettings.errorMessage;
|
|
443
|
+
}
|
|
444
|
+
this.errorEl.classList.add("visible");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// ── Initialization ─────────────────────────────────────────────────────
|
|
448
|
+
async init() {
|
|
449
|
+
if (!this.config.widgetId) {
|
|
450
|
+
this.showError();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
await fetchErrorSettings(this.config, this.state);
|
|
454
|
+
const hasStored = loadFromStorage(this.config, this.state);
|
|
455
|
+
if (hasStored) {
|
|
456
|
+
const refreshed = await refreshAccessToken(
|
|
457
|
+
this.config,
|
|
458
|
+
this.state,
|
|
459
|
+
() => this.scheduleRefresh()
|
|
460
|
+
);
|
|
461
|
+
if (!refreshed) {
|
|
462
|
+
this.showError();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.state.isAuthenticated = true;
|
|
466
|
+
this.createIframe();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const authed = await authenticateServerless(
|
|
470
|
+
this.config,
|
|
471
|
+
this.state,
|
|
472
|
+
() => this.scheduleRefresh()
|
|
473
|
+
);
|
|
474
|
+
if (authed) {
|
|
475
|
+
this.createIframe();
|
|
476
|
+
} else {
|
|
477
|
+
this.showError();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
createIframe() {
|
|
481
|
+
const iframe = document.createElement("iframe");
|
|
482
|
+
iframe.src = this.buildWidgetUrl(this.config);
|
|
483
|
+
iframe.allow = "clipboard-write";
|
|
484
|
+
iframe.frameBorder = "0";
|
|
485
|
+
iframe.scrolling = "no";
|
|
486
|
+
iframe.style.cssText = `width: ${this.config.width}; height: ${this.config.height}; border: none; display: block;`;
|
|
487
|
+
iframe.onload = () => {
|
|
488
|
+
sendTokenToWidget(this.config, this.state, iframe);
|
|
489
|
+
this.schedulePeriodicSync(iframe);
|
|
490
|
+
};
|
|
491
|
+
iframe.onerror = () => {
|
|
492
|
+
this.showError();
|
|
493
|
+
};
|
|
494
|
+
this.state.iframe = iframe;
|
|
495
|
+
this.shadow.appendChild(iframe);
|
|
496
|
+
this.cleanupListener = setupMessageListener(
|
|
497
|
+
this.config,
|
|
498
|
+
this.state,
|
|
499
|
+
this.shadow,
|
|
500
|
+
iframe,
|
|
501
|
+
() => this.scheduleRefresh(),
|
|
502
|
+
() => this.logoutAndClear(),
|
|
503
|
+
() => this.hideLoader()
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
// ── Token scheduling ──────────────────────────────────────────────────
|
|
507
|
+
scheduleRefresh() {
|
|
508
|
+
if (this.state.refreshTimer) clearTimeout(this.state.refreshTimer);
|
|
509
|
+
if (!this.state.accessTokenExpiry) return;
|
|
510
|
+
const delay = this.state.accessTokenExpiry - Date.now() - 6e4;
|
|
511
|
+
if (delay > 0) {
|
|
512
|
+
this.state.refreshTimer = setTimeout(async () => {
|
|
513
|
+
await refreshAccessToken(
|
|
514
|
+
this.config,
|
|
515
|
+
this.state,
|
|
516
|
+
() => this.scheduleRefresh(),
|
|
517
|
+
() => {
|
|
518
|
+
if (this.state.iframe) sendTokenToWidget(this.config, this.state, this.state.iframe);
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
}, delay);
|
|
522
|
+
} else {
|
|
523
|
+
refreshAccessToken(
|
|
524
|
+
this.config,
|
|
525
|
+
this.state,
|
|
526
|
+
() => this.scheduleRefresh(),
|
|
527
|
+
() => {
|
|
528
|
+
if (this.state.iframe) sendTokenToWidget(this.config, this.state, this.state.iframe);
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
schedulePeriodicSync(iframe) {
|
|
534
|
+
if (this.state.syncTimer) clearInterval(this.state.syncTimer);
|
|
535
|
+
this.state.syncTimer = setInterval(() => {
|
|
536
|
+
if (this.state.accessToken) sendTokenToWidget(this.config, this.state, iframe);
|
|
537
|
+
}, 12e4);
|
|
538
|
+
}
|
|
539
|
+
// ── Logout helper ─────────────────────────────────────────────────────
|
|
540
|
+
async logoutAndClear() {
|
|
541
|
+
await logout(this.config, this.state);
|
|
542
|
+
this.shadow.querySelectorAll("iframe").forEach((el) => el.remove());
|
|
543
|
+
}
|
|
544
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
545
|
+
async reload() {
|
|
546
|
+
clearState(this.state);
|
|
547
|
+
this.shadow.querySelectorAll("iframe").forEach((el) => el.remove());
|
|
548
|
+
await this.init();
|
|
549
|
+
}
|
|
550
|
+
async logoutPublic() {
|
|
551
|
+
await this.logoutAndClear();
|
|
552
|
+
}
|
|
553
|
+
isAuthenticated() {
|
|
554
|
+
return this.state.isAuthenticated && this.state.accessToken !== null;
|
|
555
|
+
}
|
|
556
|
+
getTokenInfo() {
|
|
557
|
+
return {
|
|
558
|
+
hasAccessToken: !!this.state.accessToken,
|
|
559
|
+
hasRefreshToken: !!this.state.refreshToken,
|
|
560
|
+
accessTokenExpiry: this.state.accessTokenExpiry ? new Date(this.state.accessTokenExpiry) : null,
|
|
561
|
+
refreshTokenExpiry: this.state.refreshTokenExpiry ? new Date(this.state.refreshTokenExpiry) : null,
|
|
562
|
+
isAccessTokenValid: !!this.state.accessToken && !isTokenExpired(this.state.accessTokenExpiry),
|
|
563
|
+
isRefreshTokenValid: !!this.state.refreshToken && !isTokenExpired(this.state.refreshTokenExpiry)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
class StoreWidget extends BaseWidget {
|
|
568
|
+
buildWidgetUrl(config) {
|
|
569
|
+
const url = new URL(config.widgetUrl);
|
|
570
|
+
url.searchParams.set("color", config.theme);
|
|
571
|
+
url.searchParams.set("containerId", config.container);
|
|
572
|
+
url.searchParams.set("connectType", "simple");
|
|
573
|
+
url.searchParams.set("mode", "private");
|
|
574
|
+
return url.toString();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
class ChannelWidget extends BaseWidget {
|
|
578
|
+
buildWidgetUrl(config) {
|
|
579
|
+
if (config.widgetUrl.endsWith("channel-widget")) {
|
|
580
|
+
return `${config.widgetUrl}/${config.widgetId}`;
|
|
581
|
+
}
|
|
582
|
+
return config.widgetUrl;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
class MilestoneWidget extends BaseWidget {
|
|
586
|
+
buildWidgetUrl(config) {
|
|
587
|
+
if (config.widgetUrl.endsWith("milestone-widget")) {
|
|
588
|
+
return `${config.widgetUrl}/${config.widgetId}`;
|
|
589
|
+
}
|
|
590
|
+
return config.widgetUrl;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
class SocialWidget extends BaseWidget {
|
|
594
|
+
buildWidgetUrl(config) {
|
|
595
|
+
if (config.widgetUrl.endsWith("social-widget")) {
|
|
596
|
+
return `${config.widgetUrl}/${config.widgetId}`;
|
|
597
|
+
}
|
|
598
|
+
return config.widgetUrl;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
class CurrencyWidget extends BaseWidget {
|
|
602
|
+
buildWidgetUrl(config) {
|
|
603
|
+
if (config.widgetUrl.endsWith("currency-overview-widget")) {
|
|
604
|
+
return `${config.widgetUrl}/${config.widgetId}`;
|
|
605
|
+
}
|
|
606
|
+
return config.widgetUrl;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
console.log(`[rai-widget] v${"1.0.0"}`);
|
|
610
|
+
const WIDGET_REGISTRY = [
|
|
611
|
+
["rai-widget", StoreWidget],
|
|
612
|
+
["rai-channel-widget", ChannelWidget],
|
|
613
|
+
["rai-milestone-widget", MilestoneWidget],
|
|
614
|
+
["rai-social-widget", SocialWidget],
|
|
615
|
+
["rai-currency-widget", CurrencyWidget]
|
|
616
|
+
];
|
|
617
|
+
for (const [tag, cls] of WIDGET_REGISTRY) {
|
|
618
|
+
if (!customElements.get(tag)) {
|
|
619
|
+
customElements.define(tag, cls);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const WIDGET_CLASS_MAP = {
|
|
623
|
+
store: StoreWidget,
|
|
624
|
+
channel: ChannelWidget,
|
|
625
|
+
milestone: MilestoneWidget,
|
|
626
|
+
social: SocialWidget,
|
|
627
|
+
"currency-view": CurrencyWidget
|
|
628
|
+
};
|
|
629
|
+
const ALL_WIDGET_SELECTOR = WIDGET_REGISTRY.map(([tag]) => tag).join(", ");
|
|
630
|
+
function bootstrapFromScriptTag() {
|
|
631
|
+
var _a;
|
|
632
|
+
const script = (((_a = document.currentScript) == null ? void 0 : _a.hasAttribute("data-widget-id")) ? document.currentScript : null) || document.querySelector("script[data-widget-id]");
|
|
633
|
+
if (!script) return;
|
|
634
|
+
const widgetId = script.getAttribute("data-widget-id");
|
|
635
|
+
if (!widgetId) return;
|
|
636
|
+
const containerId = script.getAttribute("data-container") || `returning-ai-widget-${widgetId}`;
|
|
637
|
+
const container = document.getElementById(containerId);
|
|
638
|
+
if (!container) return;
|
|
639
|
+
if (getComputedStyle(container).position === "static") {
|
|
640
|
+
container.style.position = "relative";
|
|
641
|
+
}
|
|
642
|
+
const widgetType = script.getAttribute("data-widget-type") ?? "store";
|
|
643
|
+
const WidgetClass = WIDGET_CLASS_MAP[widgetType] ?? StoreWidget;
|
|
644
|
+
const widget = new WidgetClass();
|
|
645
|
+
Array.from(script.attributes).forEach((attr) => {
|
|
646
|
+
widget.setAttribute(attr.name, attr.value);
|
|
647
|
+
});
|
|
648
|
+
container.appendChild(widget);
|
|
649
|
+
}
|
|
650
|
+
if (document.readyState === "loading") {
|
|
651
|
+
document.addEventListener("DOMContentLoaded", bootstrapFromScriptTag);
|
|
652
|
+
} else {
|
|
653
|
+
bootstrapFromScriptTag();
|
|
654
|
+
}
|
|
655
|
+
function exposePublicApi() {
|
|
656
|
+
const container = (() => {
|
|
657
|
+
var _a;
|
|
658
|
+
const script = (((_a = document.currentScript) == null ? void 0 : _a.hasAttribute("data-widget-id")) ? document.currentScript : null) || document.querySelector("script[data-widget-id]");
|
|
659
|
+
if (!script) return null;
|
|
660
|
+
const id = script.getAttribute("data-container") || `returning-ai-widget-${script.getAttribute("data-widget-id")}`;
|
|
661
|
+
return document.getElementById(id);
|
|
662
|
+
})();
|
|
663
|
+
const widget = container == null ? void 0 : container.querySelector(ALL_WIDGET_SELECTOR);
|
|
664
|
+
window.ReturningAIWidget = {
|
|
665
|
+
version: "1.0.0",
|
|
666
|
+
reload: () => (widget == null ? void 0 : widget.reload()) ?? Promise.resolve(),
|
|
667
|
+
logout: () => (widget == null ? void 0 : widget.logoutPublic()) ?? Promise.resolve(),
|
|
668
|
+
isAuthenticated: () => (widget == null ? void 0 : widget.isAuthenticated()) ?? false,
|
|
669
|
+
getTokenInfo: () => (widget == null ? void 0 : widget.getTokenInfo()) ?? {}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
if (document.readyState === "loading") {
|
|
673
|
+
document.addEventListener("DOMContentLoaded", exposePublicApi);
|
|
674
|
+
} else {
|
|
675
|
+
exposePublicApi();
|
|
676
|
+
}
|
|
677
|
+
export {
|
|
678
|
+
ChannelWidget,
|
|
679
|
+
CurrencyWidget,
|
|
680
|
+
MilestoneWidget,
|
|
681
|
+
SocialWidget,
|
|
682
|
+
StoreWidget
|
|
683
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { WidgetConfig } from './types';
|
|
2
|
+
export declare function readConfig(el: Element, existingId?: string): WidgetConfig;
|
|
3
|
+
export declare abstract class BaseWidget extends HTMLElement {
|
|
4
|
+
protected shadow: ShadowRoot;
|
|
5
|
+
protected config: WidgetConfig;
|
|
6
|
+
private state;
|
|
7
|
+
private loaderEl;
|
|
8
|
+
private errorEl;
|
|
9
|
+
private cleanupListener?;
|
|
10
|
+
constructor();
|
|
11
|
+
protected abstract buildWidgetUrl(config: WidgetConfig): string;
|
|
12
|
+
connectedCallback(): void;
|
|
13
|
+
disconnectedCallback(): void;
|
|
14
|
+
private renderShell;
|
|
15
|
+
private createDefaultLoader;
|
|
16
|
+
private hideLoader;
|
|
17
|
+
private showError;
|
|
18
|
+
private init;
|
|
19
|
+
private createIframe;
|
|
20
|
+
private scheduleRefresh;
|
|
21
|
+
private schedulePeriodicSync;
|
|
22
|
+
private logoutAndClear;
|
|
23
|
+
reload(): Promise<void>;
|
|
24
|
+
logoutPublic(): Promise<void>;
|
|
25
|
+
isAuthenticated(): boolean;
|
|
26
|
+
getTokenInfo(): {
|
|
27
|
+
hasAccessToken: boolean;
|
|
28
|
+
hasRefreshToken: boolean;
|
|
29
|
+
accessTokenExpiry: Date | null;
|
|
30
|
+
refreshTokenExpiry: Date | null;
|
|
31
|
+
isAccessTokenValid: boolean;
|
|
32
|
+
isRefreshTokenValid: boolean;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WidgetConfig, WidgetState } from '../types';
|
|
2
|
+
export declare function authenticateServerless(config: WidgetConfig, state: WidgetState, onRefreshScheduled?: () => void): Promise<boolean>;
|
|
3
|
+
export declare function refreshAccessToken(config: WidgetConfig, state: WidgetState, onRefreshScheduled?: () => void, sendToken?: () => void): Promise<boolean>;
|
|
4
|
+
export declare function getValidToken(config: WidgetConfig, state: WidgetState, onRefreshScheduled?: () => void): Promise<string>;
|
|
5
|
+
export declare function logout(config: WidgetConfig, state: WidgetState): Promise<void>;
|
|
6
|
+
export declare function fetchErrorSettings(config: WidgetConfig, state: WidgetState): Promise<typeof state.errorSettings>;
|
|
7
|
+
declare function clearState(state: WidgetState): void;
|
|
8
|
+
export { clearState };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { WidgetConfig, WidgetState } from '../types';
|
|
2
|
+
export declare function sendTokenToWidget(config: WidgetConfig, state: WidgetState, iframe: HTMLIFrameElement): void;
|
|
3
|
+
export declare function setupMessageListener(config: WidgetConfig, state: WidgetState, _shadow: ShadowRoot, iframe: HTMLIFrameElement, onRefreshScheduled?: () => void, onLogout?: () => Promise<void>, hideLoader?: () => void): () => void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WidgetConfig, WidgetState } from '../types';
|
|
2
|
+
export declare function saveToStorage(config: WidgetConfig, state: WidgetState): void;
|
|
3
|
+
export declare function loadFromStorage(config: WidgetConfig, state: WidgetState): boolean;
|
|
4
|
+
export declare function clearStorage(config: WidgetConfig): void;
|
|
5
|
+
export declare function loadErrorSettingsFromStorage(config: WidgetConfig): Record<string, unknown> | null;
|
|
6
|
+
export declare function saveErrorSettingsToStorage(config: WidgetConfig, settings: Record<string, unknown>): void;
|
|
7
|
+
declare function isTokenExpired(expiry: number | null): boolean;
|
|
8
|
+
export { isTokenExpired };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StoreWidget } from './StoreWidget';
|
|
2
|
+
import { ChannelWidget } from './ChannelWidget';
|
|
3
|
+
import { MilestoneWidget } from './MilestoneWidget';
|
|
4
|
+
import { SocialWidget } from './SocialWidget';
|
|
5
|
+
import { CurrencyWidget } from './CurrencyWidget';
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
ReturningAIWidget: {
|
|
9
|
+
version: string;
|
|
10
|
+
reload: () => Promise<void>;
|
|
11
|
+
logout: () => Promise<void>;
|
|
12
|
+
isAuthenticated: () => boolean;
|
|
13
|
+
getTokenInfo: () => object;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export { StoreWidget, ChannelWidget, MilestoneWidget, SocialWidget, CurrencyWidget };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface WidgetConfig {
|
|
2
|
+
widgetId: string;
|
|
3
|
+
widgetType: 'store' | 'channel' | 'milestone' | 'social' | 'currency-view';
|
|
4
|
+
theme: 'light' | 'dark';
|
|
5
|
+
container: string;
|
|
6
|
+
width: string;
|
|
7
|
+
height: string;
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
widgetUrl: string;
|
|
10
|
+
widgetDomain: string;
|
|
11
|
+
autoRefresh: boolean;
|
|
12
|
+
debug: boolean;
|
|
13
|
+
storagePrefix: string;
|
|
14
|
+
userIdentifiers: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
export interface WidgetState {
|
|
17
|
+
accessToken: string | null;
|
|
18
|
+
refreshToken: string | null;
|
|
19
|
+
tokenFamily: string | null;
|
|
20
|
+
accessTokenExpiry: number | null;
|
|
21
|
+
refreshTokenExpiry: number | null;
|
|
22
|
+
refreshTimer: ReturnType<typeof setTimeout> | null;
|
|
23
|
+
syncTimer: ReturnType<typeof setInterval> | null;
|
|
24
|
+
iframe: HTMLIFrameElement | null;
|
|
25
|
+
isAuthenticated: boolean;
|
|
26
|
+
isRefreshing: boolean;
|
|
27
|
+
refreshPromise: Promise<boolean> | null;
|
|
28
|
+
errorSettings: {
|
|
29
|
+
errorMessage?: string;
|
|
30
|
+
modalColor?: string;
|
|
31
|
+
backgroundImage?: string;
|
|
32
|
+
} | null;
|
|
33
|
+
}
|
|
34
|
+
export interface TokenData {
|
|
35
|
+
accessToken: string;
|
|
36
|
+
refreshToken: string;
|
|
37
|
+
tokenFamily?: string;
|
|
38
|
+
accessTokenTTL: number;
|
|
39
|
+
refreshTokenTTL: number;
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@returningai/widget-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shadow DOM isolated widget SDK for ReturningAI",
|
|
5
|
+
"main": "dist/rai-widget.iife.js",
|
|
6
|
+
"module": "dist/rai-widget.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"sideEffects": true,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/rai-widget.js",
|
|
12
|
+
"default": "./dist/rai-widget.iife.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"build:min": "vite build --mode production",
|
|
22
|
+
"types": "tsc --emitDeclarationOnly --outDir dist/types",
|
|
23
|
+
"prepublishOnly": "npm run build:min && npm run types"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"terser": "^5.46.0",
|
|
27
|
+
"typescript": "^5.3.3",
|
|
28
|
+
"vite": "^5.1.0"
|
|
29
|
+
}
|
|
30
|
+
}
|