@returningai/widget-sdk 1.4.3 → 1.5.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/CHANGELOG.md +26 -0
- package/README.md +476 -123
- package/dist/rai-widget.iife.js +1 -1
- package/dist/rai-widget.js +123 -6
- package/dist/types/BaseWidget.d.ts +9 -1
- package/dist/types/core/callback-registry.d.ts +15 -0
- package/dist/types/core/postmessage.d.ts +2 -2
- package/dist/types/index.d.ts +11 -7
- package/dist/types/jsx.d.ts +55 -0
- package/dist/types/types.d.ts +56 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -24,32 +24,39 @@ Store both values securely on your server.
|
|
|
24
24
|
|
|
25
25
|
```js
|
|
26
26
|
// Runs on YOUR SERVER — never in the browser
|
|
27
|
-
const response = await fetch(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
27
|
+
const response = await fetch(
|
|
28
|
+
"https://api-v2.returning.ai/v2/api/widget-access-keys/token",
|
|
29
|
+
{
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
accessId: process.env.RAI_ACCESS_ID,
|
|
34
|
+
accessKey: process.env.RAI_ACCESS_KEY,
|
|
35
|
+
// userIdentifiers keys MUST be `data-*` prefixed — they match the
|
|
36
|
+
// `dataAttribute` value of each field configured in your widget's
|
|
37
|
+
// User Identifier Fields (e.g. the dashboard stores "data-email",
|
|
38
|
+
// "data-user-id", not bare "email"/"userId").
|
|
39
|
+
userIdentifiers: {
|
|
40
|
+
"data-user-id": [YourUserObject].id,
|
|
41
|
+
"data-email": [YourUserObject].email,
|
|
42
|
+
// ...any other fields configured for this widget
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
const { data } = await response.json();
|
|
45
48
|
// data.embedToken expires in 15 minutes and carries signed identity claims
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
**Step 3 — Embed** (only the short-lived token appears in HTML — no identity attributes):
|
|
49
52
|
|
|
50
53
|
```html
|
|
51
|
-
<div
|
|
52
|
-
|
|
54
|
+
<div
|
|
55
|
+
id="returning-ai-widget-YOUR_WIDGET_ID"
|
|
56
|
+
style="width:100%;height:600px"
|
|
57
|
+
></div>
|
|
58
|
+
<script
|
|
59
|
+
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
|
|
53
60
|
data-widget-id="YOUR_WIDGET_ID"
|
|
54
61
|
data-widget-type="store"
|
|
55
62
|
data-theme="dark"
|
|
@@ -84,22 +91,26 @@ npm install @returningai/widget-sdk
|
|
|
84
91
|
|
|
85
92
|
Set the `data-widget-type` attribute to choose which widget to display. The Widget ID and Widget URL for each type are available in your dashboard.
|
|
86
93
|
|
|
87
|
-
| `data-widget-type`
|
|
88
|
-
|
|
89
|
-
| `store`
|
|
90
|
-
| `channel`
|
|
91
|
-
| `milestone`
|
|
92
|
-
| `currency-view`
|
|
93
|
-
| `referral-conditions` | Referral widget
|
|
94
|
-
| `custom`
|
|
94
|
+
| `data-widget-type` | Description |
|
|
95
|
+
| --------------------- | ---------------------- |
|
|
96
|
+
| `store` | Store / rewards widget |
|
|
97
|
+
| `channel` | Channel widget |
|
|
98
|
+
| `milestone` | Milestone widget |
|
|
99
|
+
| `currency-view` | Currency widget |
|
|
100
|
+
| `referral-conditions` | Referral widget |
|
|
101
|
+
| `custom` | Custom widget |
|
|
95
102
|
|
|
96
103
|
All examples below use **Access Key Embed** (recommended) — identity is signed into `data-embed-token` server-side per the [Quick Start](#quick-start). Swap `data-embed-token="eyJ..."` for `data-*` identifier attributes if you want [Public Embed](#user-identifier-attributes) instead.
|
|
97
104
|
|
|
98
105
|
### Store widget
|
|
99
106
|
|
|
100
107
|
```html
|
|
101
|
-
<div
|
|
102
|
-
|
|
108
|
+
<div
|
|
109
|
+
id="returning-ai-widget-YOUR_WIDGET_ID"
|
|
110
|
+
style="width:100%;height:600px"
|
|
111
|
+
></div>
|
|
112
|
+
<script
|
|
113
|
+
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
|
|
103
114
|
data-widget-id="YOUR_WIDGET_ID"
|
|
104
115
|
data-widget-type="store"
|
|
105
116
|
data-theme="dark"
|
|
@@ -108,11 +119,154 @@ All examples below use **Access Key Embed** (recommended) — identity is signed
|
|
|
108
119
|
></script>
|
|
109
120
|
```
|
|
110
121
|
|
|
122
|
+
#### Store callback field options
|
|
123
|
+
|
|
124
|
+
Store and custom-store widgets can ask the host page to populate dynamic purchase fields. In the store product setup, this applies to `single-select` custom instruction fields where **Field Options** is set to `Callback Field`. The callback returns signed field options; it is not a purchase eligibility check.
|
|
125
|
+
|
|
126
|
+
ReturningAI calls `callbackFieldOptions` with this payload:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
{
|
|
130
|
+
scope: 'store.callback_field_options',
|
|
131
|
+
callbackName: 'callbackFieldOptions',
|
|
132
|
+
context: {
|
|
133
|
+
productId: 'PRODUCT_ID',
|
|
134
|
+
productName: 'Product name',
|
|
135
|
+
fields: [
|
|
136
|
+
{ id: 'FIELD_ID', name: 'Field label', type: 'single-select' },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Register the callback on the widget element. For custom-store widgets, register it on the `<rai-custom-widget>` element instead.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
async function registerStoreCallbacks() {
|
|
146
|
+
const widget = document.querySelector("rai-store-widget");
|
|
147
|
+
|
|
148
|
+
if (!widget) {
|
|
149
|
+
throw new Error("ReturningAI store widget was not found");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await customElements.whenDefined(widget.localName);
|
|
153
|
+
|
|
154
|
+
widget.registerCallback("callbackFieldOptions", async (payload) => {
|
|
155
|
+
// Call your backend. Never call ReturningAI's callback-signature endpoint
|
|
156
|
+
// directly from browser code because your SDK access key must stay private.
|
|
157
|
+
const response = await fetch("/api/returning-ai/store-field-options", {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { "Content-Type": "application/json" },
|
|
160
|
+
body: JSON.stringify(payload),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error("Unable to load store field options");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return response.json();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Prevent later scripts from replacing this widget's registered callback.
|
|
171
|
+
widget.lockCallbacks();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
registerStoreCallbacks().catch((error) => {
|
|
175
|
+
console.error("ReturningAI store callback setup failed", error);
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Your backend should generate options for each requested field, request a callback signature from ReturningAI, then return:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
return {
|
|
183
|
+
fieldOptions: {
|
|
184
|
+
FIELD_ID: ["Option A", "Option B"],
|
|
185
|
+
},
|
|
186
|
+
signature: "SIGNATURE_FROM_CALLBACK_SIGNATURE_ENDPOINT",
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Node.js / Express example:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
// Add this route to your backend. The browser callback above calls:
|
|
194
|
+
// POST /api/returning-ai/store-field-options
|
|
195
|
+
//
|
|
196
|
+
// This route receives ReturningAI's callback payload, builds options for each
|
|
197
|
+
// requested field, asks ReturningAI to sign the result, then returns
|
|
198
|
+
// { fieldOptions, signature } to the browser callback.
|
|
199
|
+
|
|
200
|
+
app.post("/api/returning-ai/store-field-options", async (req, res) => {
|
|
201
|
+
// The browser callback forwards ReturningAI's callback payload to this endpoint.
|
|
202
|
+
const payload = req.body;
|
|
203
|
+
|
|
204
|
+
// Build one option array for each requested dynamic field.
|
|
205
|
+
// Each key must be the field.id from payload.context.fields.
|
|
206
|
+
const fieldOptions = {};
|
|
207
|
+
|
|
208
|
+
for (const field of payload.context.fields) {
|
|
209
|
+
// Replace this with your own lookup using payload.context.productId and field.id.
|
|
210
|
+
fieldOptions[field.id] = ["Option 1", "Option 2"];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Ask ReturningAI to sign the exact context and fieldOptions result.
|
|
214
|
+
// Keep SDK_ACCESS_ID and SDK_ACCESS_KEY on this backend only.
|
|
215
|
+
const signatureResponse = await fetch(
|
|
216
|
+
"https://api-v2.returning.ai/v2/api/widget-access-keys/callback-signature",
|
|
217
|
+
{
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: { "Content-Type": "application/json" },
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
accessId: process.env.SDK_ACCESS_ID,
|
|
222
|
+
accessKey: process.env.SDK_ACCESS_KEY,
|
|
223
|
+
scope: payload.scope,
|
|
224
|
+
callbackName: payload.callbackName,
|
|
225
|
+
context: payload.context,
|
|
226
|
+
result: { fieldOptions },
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (!signatureResponse.ok) {
|
|
232
|
+
const errorText = await signatureResponse.text();
|
|
233
|
+
console.error("ReturningAI callback signature failed", errorText);
|
|
234
|
+
return res
|
|
235
|
+
.status(502)
|
|
236
|
+
.json({ error: "Unable to sign ReturningAI callback result" });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Read the callback signature returned by ReturningAI.
|
|
240
|
+
const signaturePayload = await signatureResponse.json();
|
|
241
|
+
const signature = signaturePayload.data?.signature;
|
|
242
|
+
|
|
243
|
+
if (!signature) {
|
|
244
|
+
console.error(
|
|
245
|
+
"ReturningAI callback signature response did not include a signature",
|
|
246
|
+
signaturePayload,
|
|
247
|
+
);
|
|
248
|
+
return res
|
|
249
|
+
.status(502)
|
|
250
|
+
.json({ error: "Unable to read ReturningAI callback signature" });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Return the signed options to the browser callback.
|
|
254
|
+
return res.json({
|
|
255
|
+
fieldOptions,
|
|
256
|
+
signature,
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
111
261
|
### Channel widget
|
|
112
262
|
|
|
113
263
|
```html
|
|
114
|
-
<div
|
|
115
|
-
|
|
264
|
+
<div
|
|
265
|
+
id="returning-ai-widget-YOUR_WIDGET_ID"
|
|
266
|
+
style="width:100%;height:600px"
|
|
267
|
+
></div>
|
|
268
|
+
<script
|
|
269
|
+
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
|
|
116
270
|
data-widget-id="YOUR_WIDGET_ID"
|
|
117
271
|
data-widget-type="channel"
|
|
118
272
|
data-theme="dark"
|
|
@@ -126,8 +280,12 @@ All examples below use **Access Key Embed** (recommended) — identity is signed
|
|
|
126
280
|
### Milestone widget
|
|
127
281
|
|
|
128
282
|
```html
|
|
129
|
-
<div
|
|
130
|
-
|
|
283
|
+
<div
|
|
284
|
+
id="returning-ai-widget-YOUR_WIDGET_ID"
|
|
285
|
+
style="width:100%;height:600px"
|
|
286
|
+
></div>
|
|
287
|
+
<script
|
|
288
|
+
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
|
|
131
289
|
data-widget-id="YOUR_WIDGET_ID"
|
|
132
290
|
data-widget-type="milestone"
|
|
133
291
|
data-theme="dark"
|
|
@@ -139,8 +297,12 @@ All examples below use **Access Key Embed** (recommended) — identity is signed
|
|
|
139
297
|
### Custom widget
|
|
140
298
|
|
|
141
299
|
```html
|
|
142
|
-
<div
|
|
143
|
-
|
|
300
|
+
<div
|
|
301
|
+
id="returning-ai-widget-YOUR_WIDGET_ID"
|
|
302
|
+
style="width:100%;height:600px"
|
|
303
|
+
></div>
|
|
304
|
+
<script
|
|
305
|
+
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
|
|
144
306
|
data-widget-id="YOUR_WIDGET_ID"
|
|
145
307
|
data-widget-type="custom"
|
|
146
308
|
data-theme="dark"
|
|
@@ -164,45 +326,45 @@ All attributes can be provided with or without the `data-` prefix — both work.
|
|
|
164
326
|
|
|
165
327
|
### Script-tag attributes
|
|
166
328
|
|
|
167
|
-
| Attribute
|
|
168
|
-
|
|
169
|
-
| `data-widget-id`
|
|
170
|
-
| `data-widget-type`
|
|
171
|
-
| `api-url`
|
|
172
|
-
| `widget-url`
|
|
173
|
-
| `data-bundle-url`
|
|
174
|
-
| `data-theme`
|
|
175
|
-
| `data-email`
|
|
176
|
-
| `data-embed-token`
|
|
177
|
-
| `data-locale`
|
|
178
|
-
| `data-eager`
|
|
179
|
-
| `data-auto-refresh` | No
|
|
180
|
-
| `data-debug`
|
|
181
|
-
| `data-custom-data`
|
|
182
|
-
| `data-retry-label`
|
|
183
|
-
| `data-v2-api-url`
|
|
329
|
+
| Attribute | Required | Default | Description |
|
|
330
|
+
| ------------------- | --------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
|
331
|
+
| `data-widget-id` | **Yes** | — | Your widget ID (from dashboard) |
|
|
332
|
+
| `data-widget-type` | **Yes** | `store` | Widget type (see table above) |
|
|
333
|
+
| `api-url` | No | ReturningAI widget auth server | Optional widget auth server override |
|
|
334
|
+
| `widget-url` | **Yes**\* | — | Widget URL (from dashboard). \*Not required when using `data-bundle-url` |
|
|
335
|
+
| `data-bundle-url` | No | — | URL to the widget IIFE bundle — triggers [bundle mode](#bundle-mode) (non-iframe) |
|
|
336
|
+
| `data-theme` | No | `light` | `light` or `dark` |
|
|
337
|
+
| `data-email` | Public Embed only | — | User's email for identification. **Access Key Embed:** omit — identity is signed into `data-embed-token` |
|
|
338
|
+
| `data-embed-token` | Access Key Embed only | — | Short-lived JWT from your server. Carries signed user identifiers — no `data-*` identifier attributes needed |
|
|
339
|
+
| `data-locale` | No | — | BCP 47 locale tag (e.g. `fr-FR`) |
|
|
340
|
+
| `data-eager` | No | — | Load immediately instead of waiting until visible (presence-based boolean) |
|
|
341
|
+
| `data-auto-refresh` | No | `true` | Auto-refresh access token before expiry |
|
|
342
|
+
| `data-debug` | No | `false` | Verbose console logging |
|
|
343
|
+
| `data-custom-data` | No | — | JSON object forwarded to the widget |
|
|
344
|
+
| `data-retry-label` | No | `Retry` | Label text for the retry button on the error screen |
|
|
345
|
+
| `data-v2-api-url` | No | ReturningAI V2 API | Optional V2 API override for bundle mode |
|
|
184
346
|
|
|
185
347
|
### Custom element attributes
|
|
186
348
|
|
|
187
|
-
| Attribute
|
|
188
|
-
|
|
189
|
-
| `community-id` | **Yes**\*
|
|
190
|
-
| `channel-id`
|
|
191
|
-
| `api-url`
|
|
192
|
-
| `widget-url`
|
|
193
|
-
| `bundle-url`
|
|
194
|
-
| `theme`
|
|
195
|
-
| `data-email`
|
|
196
|
-
| `embed-token`
|
|
197
|
-
| `width`
|
|
198
|
-
| `height`
|
|
199
|
-
| `locale`
|
|
200
|
-
| `eager`
|
|
201
|
-
| `auto-refresh` | No
|
|
202
|
-
| `debug`
|
|
203
|
-
| `custom-data`
|
|
204
|
-
| `retry-label`
|
|
205
|
-
| `v2-api-url`
|
|
349
|
+
| Attribute | Required | Default | Description |
|
|
350
|
+
| -------------- | --------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
|
351
|
+
| `community-id` | **Yes**\* | — | Your community ID (raw ObjectId from dashboard). \*Custom widgets use `widget-id` instead |
|
|
352
|
+
| `channel-id` | **Channel only** | — | Channel ID (raw ObjectId) — required for `<rai-channel-widget>` |
|
|
353
|
+
| `api-url` | No | ReturningAI widget auth server | Optional widget auth server override |
|
|
354
|
+
| `widget-url` | **Yes**\* | — | Widget URL (from dashboard). \*Not required when using `bundle-url` |
|
|
355
|
+
| `bundle-url` | No | — | URL to the widget IIFE bundle — triggers [bundle mode](#bundle-mode) (non-iframe) |
|
|
356
|
+
| `theme` | No | `light` | `light` or `dark` |
|
|
357
|
+
| `data-email` | Public Embed only | — | User's email for identification. **Access Key Embed:** omit — identity is signed into `embed-token` |
|
|
358
|
+
| `embed-token` | Access Key Embed only | — | Short-lived JWT from your server. Carries signed user identifiers — no `data-*` identifier attributes needed |
|
|
359
|
+
| `width` | No | `100%` | CSS width |
|
|
360
|
+
| `height` | No | `600px` | CSS height |
|
|
361
|
+
| `locale` | No | — | BCP 47 locale tag (e.g. `fr-FR`) |
|
|
362
|
+
| `eager` | No | — | Load immediately instead of waiting until visible (presence-based boolean) |
|
|
363
|
+
| `auto-refresh` | No | `true` | Auto-refresh access token before expiry |
|
|
364
|
+
| `debug` | No | `false` | Verbose console logging |
|
|
365
|
+
| `custom-data` | No | — | JSON object forwarded to the widget |
|
|
366
|
+
| `retry-label` | No | `Retry` | Label text for the retry button on the error screen |
|
|
367
|
+
| `v2-api-url` | No | ReturningAI V2 API | Optional V2 API override for bundle mode |
|
|
206
368
|
|
|
207
369
|
> **`widget-id` vs `community-id`:** The script-tag method uses `data-widget-id` which is a pre-encoded identifier (base64). The custom element method uses raw ObjectIds — `community-id` for most widget types, plus `channel-id` for channel widgets. The SDK handles the encoding internally.
|
|
208
370
|
|
|
@@ -211,7 +373,10 @@ All attributes can be provided with or without the `data-` prefix — both work.
|
|
|
211
373
|
The SDK looks for a `<div>` with `id="returning-ai-widget-{YOUR_WIDGET_ID}"`. Size the div to control the widget dimensions:
|
|
212
374
|
|
|
213
375
|
```html
|
|
214
|
-
<div
|
|
376
|
+
<div
|
|
377
|
+
id="returning-ai-widget-YOUR_WIDGET_ID"
|
|
378
|
+
style="width:100%;height:600px"
|
|
379
|
+
></div>
|
|
215
380
|
```
|
|
216
381
|
|
|
217
382
|
Custom elements don't need a container div — they size themselves using `width` and `height` attributes (defaults: `100%` and `600px`).
|
|
@@ -224,7 +389,8 @@ Custom elements don't need a container div — they size themselves using `width
|
|
|
224
389
|
|
|
225
390
|
```html
|
|
226
391
|
<!-- Public Embed — identifiers in HTML -->
|
|
227
|
-
<script
|
|
392
|
+
<script
|
|
393
|
+
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
|
|
228
394
|
data-widget-id="YOUR_WIDGET_ID"
|
|
229
395
|
data-widget-type="store"
|
|
230
396
|
data-email="user@example.com"
|
|
@@ -263,8 +429,12 @@ Both examples below use Access Key Embed and the default endpoints.
|
|
|
263
429
|
|
|
264
430
|
```html
|
|
265
431
|
<!-- Script-tag approach -->
|
|
266
|
-
<div
|
|
267
|
-
|
|
432
|
+
<div
|
|
433
|
+
id="returning-ai-widget-YOUR_WIDGET_ID"
|
|
434
|
+
style="width:100%;height:100vh"
|
|
435
|
+
></div>
|
|
436
|
+
<script
|
|
437
|
+
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
|
|
268
438
|
data-widget-id="YOUR_WIDGET_ID"
|
|
269
439
|
data-widget-type="store"
|
|
270
440
|
data-theme="dark"
|
|
@@ -287,14 +457,14 @@ Both examples below use Access Key Embed and the default endpoints.
|
|
|
287
457
|
|
|
288
458
|
Each widget type has its own IIFE bundle and global name:
|
|
289
459
|
|
|
290
|
-
| Widget type
|
|
291
|
-
|
|
292
|
-
| `store`
|
|
293
|
-
| `channel`
|
|
294
|
-
| `milestone`
|
|
295
|
-
| `currency-view`
|
|
296
|
-
| `referral-conditions` | `RaiReferralWidget`
|
|
297
|
-
| `custom`
|
|
460
|
+
| Widget type | Bundle global | Custom element tag |
|
|
461
|
+
| --------------------- | -------------------- | ------------------------ |
|
|
462
|
+
| `store` | `RaiStoreWidget` | `<rai-store-widget>` |
|
|
463
|
+
| `channel` | `RaiChannelWidget` | `<rai-channel-widget>` |
|
|
464
|
+
| `milestone` | `RaiMilestoneWidget` | `<rai-milestone-widget>` |
|
|
465
|
+
| `currency-view` | `RaiCurrencyWidget` | `<rai-currency-widget>` |
|
|
466
|
+
| `referral-conditions` | `RaiReferralWidget` | `<rai-referral-widget>` |
|
|
467
|
+
| `custom` | `RaiCustomWidget` | `<rai-custom-widget>` |
|
|
298
468
|
|
|
299
469
|
The bundle must export a `mount(container, config)` function on its global (e.g. `window.RaiStoreWidget.mount`).
|
|
300
470
|
|
|
@@ -334,7 +504,7 @@ All examples below use **Access Key Embed** — pass the short-lived token your
|
|
|
334
504
|
Types are included — no manual JSX declarations needed.
|
|
335
505
|
|
|
336
506
|
```tsx
|
|
337
|
-
import
|
|
507
|
+
import "@returningai/widget-sdk";
|
|
338
508
|
|
|
339
509
|
export function StoreEmbed({ embedToken }: { embedToken: string }) {
|
|
340
510
|
return (
|
|
@@ -344,7 +514,7 @@ export function StoreEmbed({ embedToken }: { embedToken: string }) {
|
|
|
344
514
|
embed-token={embedToken}
|
|
345
515
|
widget-url="YOUR_WIDGET_URL"
|
|
346
516
|
/>
|
|
347
|
-
)
|
|
517
|
+
);
|
|
348
518
|
}
|
|
349
519
|
|
|
350
520
|
export function ChannelEmbed({ embedToken }: { embedToken: string }) {
|
|
@@ -356,7 +526,67 @@ export function ChannelEmbed({ embedToken }: { embedToken: string }) {
|
|
|
356
526
|
embed-token={embedToken}
|
|
357
527
|
widget-url="YOUR_WIDGET_URL"
|
|
358
528
|
/>
|
|
359
|
-
)
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
For callback-backed store fields, register the callback through a ref:
|
|
534
|
+
|
|
535
|
+
```tsx
|
|
536
|
+
import { useEffect, useRef } from "react";
|
|
537
|
+
import "@returningai/widget-sdk";
|
|
538
|
+
import type {
|
|
539
|
+
ReturningAIStoreCallbackFieldOptionsCallback,
|
|
540
|
+
ReturningAIWidgetCallbackRegistration,
|
|
541
|
+
} from "@returningai/widget-sdk";
|
|
542
|
+
|
|
543
|
+
type ReturningAIStoreElement = HTMLElement &
|
|
544
|
+
ReturningAIWidgetCallbackRegistration;
|
|
545
|
+
|
|
546
|
+
const fieldOptionsCallback: ReturningAIStoreCallbackFieldOptionsCallback =
|
|
547
|
+
async (payload) => {
|
|
548
|
+
const response = await fetch("/api/returning-ai/store-field-options", {
|
|
549
|
+
method: "POST",
|
|
550
|
+
headers: { "Content-Type": "application/json" },
|
|
551
|
+
body: JSON.stringify(payload),
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
if (!response.ok) {
|
|
555
|
+
throw new Error("Unable to load ReturningAI store field options");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return response.json();
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
export function StoreEmbedWithCallbacks({
|
|
562
|
+
embedToken,
|
|
563
|
+
}: {
|
|
564
|
+
embedToken: string;
|
|
565
|
+
}) {
|
|
566
|
+
const storeWidgetRef = useRef<HTMLElement | null>(null);
|
|
567
|
+
|
|
568
|
+
useEffect(() => {
|
|
569
|
+
const storeWidget =
|
|
570
|
+
storeWidgetRef.current as ReturningAIStoreElement | null;
|
|
571
|
+
|
|
572
|
+
if (!storeWidget) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
storeWidget.registerCallback("callbackFieldOptions", fieldOptionsCallback);
|
|
577
|
+
// Prevent later scripts from replacing this widget's registered callback.
|
|
578
|
+
storeWidget.lockCallbacks();
|
|
579
|
+
}, []);
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<rai-store-widget
|
|
583
|
+
ref={storeWidgetRef}
|
|
584
|
+
community-id="YOUR_COMMUNITY_ID"
|
|
585
|
+
theme="dark"
|
|
586
|
+
embed-token={embedToken}
|
|
587
|
+
widget-url="YOUR_WIDGET_URL"
|
|
588
|
+
/>
|
|
589
|
+
);
|
|
360
590
|
}
|
|
361
591
|
```
|
|
362
592
|
|
|
@@ -364,8 +594,8 @@ export function ChannelEmbed({ embedToken }: { embedToken: string }) {
|
|
|
364
594
|
|
|
365
595
|
```vue
|
|
366
596
|
<script setup>
|
|
367
|
-
import
|
|
368
|
-
defineProps({ embedToken: String })
|
|
597
|
+
import "@returningai/widget-sdk";
|
|
598
|
+
defineProps({ embedToken: String });
|
|
369
599
|
</script>
|
|
370
600
|
|
|
371
601
|
<template>
|
|
@@ -388,6 +618,60 @@ defineProps({ embedToken: String })
|
|
|
388
618
|
</template>
|
|
389
619
|
```
|
|
390
620
|
|
|
621
|
+
For callback-backed store fields, register the callback through a template ref:
|
|
622
|
+
|
|
623
|
+
```vue
|
|
624
|
+
<script setup lang="ts">
|
|
625
|
+
import { onMounted, ref } from "vue";
|
|
626
|
+
import "@returningai/widget-sdk";
|
|
627
|
+
import type {
|
|
628
|
+
ReturningAIStoreCallbackFieldOptionsCallback,
|
|
629
|
+
ReturningAIWidgetCallbackRegistration,
|
|
630
|
+
} from "@returningai/widget-sdk";
|
|
631
|
+
|
|
632
|
+
const props = defineProps<{ embedToken: string }>();
|
|
633
|
+
const storeWidget = ref<
|
|
634
|
+
(HTMLElement & ReturningAIWidgetCallbackRegistration) | null
|
|
635
|
+
>(null);
|
|
636
|
+
|
|
637
|
+
const fieldOptionsCallback: ReturningAIStoreCallbackFieldOptionsCallback =
|
|
638
|
+
async (payload) => {
|
|
639
|
+
const response = await fetch("/api/returning-ai/store-field-options", {
|
|
640
|
+
method: "POST",
|
|
641
|
+
headers: { "Content-Type": "application/json" },
|
|
642
|
+
body: JSON.stringify(payload),
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
throw new Error("Unable to load ReturningAI store field options");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return response.json();
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
onMounted(() => {
|
|
653
|
+
if (storeWidget.value) {
|
|
654
|
+
storeWidget.value.registerCallback(
|
|
655
|
+
"callbackFieldOptions",
|
|
656
|
+
fieldOptionsCallback,
|
|
657
|
+
);
|
|
658
|
+
// Prevent later scripts from replacing this widget's registered callback.
|
|
659
|
+
storeWidget.value.lockCallbacks();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
</script>
|
|
663
|
+
|
|
664
|
+
<template>
|
|
665
|
+
<rai-store-widget
|
|
666
|
+
ref="storeWidget"
|
|
667
|
+
community-id="YOUR_COMMUNITY_ID"
|
|
668
|
+
theme="dark"
|
|
669
|
+
:embed-token="props.embedToken"
|
|
670
|
+
widget-url="YOUR_WIDGET_URL"
|
|
671
|
+
/>
|
|
672
|
+
</template>
|
|
673
|
+
```
|
|
674
|
+
|
|
391
675
|
### Angular
|
|
392
676
|
|
|
393
677
|
```ts
|
|
@@ -417,16 +701,73 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|
|
417
701
|
></rai-channel-widget>
|
|
418
702
|
```
|
|
419
703
|
|
|
704
|
+
For callback-backed store fields, add a template ref and register the callback in the component:
|
|
705
|
+
|
|
706
|
+
```ts
|
|
707
|
+
import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core";
|
|
708
|
+
import type {
|
|
709
|
+
ReturningAIStoreCallbackFieldOptionsCallback,
|
|
710
|
+
ReturningAIWidgetCallbackRegistration,
|
|
711
|
+
} from "@returningai/widget-sdk";
|
|
712
|
+
|
|
713
|
+
type ReturningAIStoreElement = HTMLElement &
|
|
714
|
+
ReturningAIWidgetCallbackRegistration;
|
|
715
|
+
|
|
716
|
+
@Component({
|
|
717
|
+
selector: "app-store-widget",
|
|
718
|
+
templateUrl: "./store-widget.component.html",
|
|
719
|
+
})
|
|
720
|
+
export class StoreWidgetComponent implements AfterViewInit {
|
|
721
|
+
@ViewChild("storeWidget") storeWidget!: ElementRef<ReturningAIStoreElement>;
|
|
722
|
+
|
|
723
|
+
embedToken = "TOKEN_FROM_YOUR_SERVER";
|
|
724
|
+
|
|
725
|
+
ngAfterViewInit() {
|
|
726
|
+
const fieldOptionsCallback: ReturningAIStoreCallbackFieldOptionsCallback =
|
|
727
|
+
async (payload) => {
|
|
728
|
+
const response = await fetch("/api/returning-ai/store-field-options", {
|
|
729
|
+
method: "POST",
|
|
730
|
+
headers: { "Content-Type": "application/json" },
|
|
731
|
+
body: JSON.stringify(payload),
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
if (!response.ok) {
|
|
735
|
+
throw new Error("Unable to load ReturningAI store field options");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return response.json();
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
this.storeWidget.nativeElement.registerCallback(
|
|
742
|
+
"callbackFieldOptions",
|
|
743
|
+
fieldOptionsCallback,
|
|
744
|
+
);
|
|
745
|
+
// Prevent later scripts from replacing this widget's registered callback.
|
|
746
|
+
this.storeWidget.nativeElement.lockCallbacks();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
```html
|
|
752
|
+
<rai-store-widget
|
|
753
|
+
#storeWidget
|
|
754
|
+
community-id="YOUR_COMMUNITY_ID"
|
|
755
|
+
theme="dark"
|
|
756
|
+
[attr.embed-token]="embedToken"
|
|
757
|
+
widget-url="YOUR_WIDGET_URL"
|
|
758
|
+
></rai-store-widget>
|
|
759
|
+
```
|
|
760
|
+
|
|
420
761
|
### Available custom element tags
|
|
421
762
|
|
|
422
|
-
| Tag
|
|
423
|
-
|
|
424
|
-
| `<rai-store-widget>`
|
|
425
|
-
| `<rai-channel-widget>`
|
|
426
|
-
| `<rai-milestone-widget>` | `community-id`
|
|
427
|
-
| `<rai-currency-widget>`
|
|
428
|
-
| `<rai-referral-widget>`
|
|
429
|
-
| `<rai-custom-widget>`
|
|
763
|
+
| Tag | Required attributes | Widget type |
|
|
764
|
+
| ------------------------ | -------------------------------- | ----------- |
|
|
765
|
+
| `<rai-store-widget>` | `community-id` | Store |
|
|
766
|
+
| `<rai-channel-widget>` | `community-id` + `channel-id` | Channel |
|
|
767
|
+
| `<rai-milestone-widget>` | `community-id` | Milestone |
|
|
768
|
+
| `<rai-currency-widget>` | `community-id` | Currency |
|
|
769
|
+
| `<rai-referral-widget>` | `community-id` | Referral |
|
|
770
|
+
| `<rai-custom-widget>` | `widget-id` (not `community-id`) | Custom |
|
|
430
771
|
|
|
431
772
|
---
|
|
432
773
|
|
|
@@ -434,28 +775,40 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|
|
434
775
|
|
|
435
776
|
Listen for widget lifecycle events:
|
|
436
777
|
|
|
437
|
-
| Event
|
|
438
|
-
|
|
439
|
-
| `rai-authenticated` | `{}`
|
|
440
|
-
| `rai-ready`
|
|
441
|
-
| `rai-mounted`
|
|
442
|
-
| `rai-error`
|
|
443
|
-
| `rai-logout`
|
|
444
|
-
| `rai-height-change` | `{ height }`
|
|
778
|
+
| Event | `detail` | Fired when |
|
|
779
|
+
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
780
|
+
| `rai-authenticated` | `{}` | Auth succeeded, before widget mounts |
|
|
781
|
+
| `rai-ready` | `{}` | `WIDGET_READY` received, loader hidden (iframe mode) |
|
|
782
|
+
| `rai-mounted` | `{}` | Widget bundle mounted successfully (bundle mode) |
|
|
783
|
+
| `rai-error` | `{ message, hint?, status? }` | Auth failed after all retries. `message` is the backend error text (e.g. `"Invalid or expired embed token"`), `hint` is the optional human-readable suggestion, `status` is the HTTP status code of the failing response. |
|
|
784
|
+
| `rai-logout` | `{}` | Widget logged out |
|
|
785
|
+
| `rai-height-change` | `{ height }` | iframe resized (after debounce) |
|
|
445
786
|
|
|
446
787
|
```js
|
|
447
788
|
// Script-tag embed
|
|
448
|
-
const widget = document.querySelector(
|
|
789
|
+
const widget = document.querySelector("[data-widget-id]");
|
|
449
790
|
|
|
450
791
|
// Custom element embed
|
|
451
|
-
const widget = document.querySelector(
|
|
452
|
-
|
|
453
|
-
widget.addEventListener(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
widget.addEventListener(
|
|
457
|
-
widget
|
|
458
|
-
|
|
792
|
+
const widget = document.querySelector("rai-store-widget");
|
|
793
|
+
|
|
794
|
+
widget.addEventListener("rai-authenticated", () => {
|
|
795
|
+
/* auth succeeded */
|
|
796
|
+
});
|
|
797
|
+
widget.addEventListener("rai-ready", () => {
|
|
798
|
+
/* widget loaded and visible (iframe mode) */
|
|
799
|
+
});
|
|
800
|
+
widget.addEventListener("rai-mounted", () => {
|
|
801
|
+
/* widget mounted (bundle mode) */
|
|
802
|
+
});
|
|
803
|
+
widget.addEventListener("rai-error", (e) =>
|
|
804
|
+
console.error(e.detail.status, e.detail.message, e.detail.hint),
|
|
805
|
+
);
|
|
806
|
+
widget.addEventListener("rai-logout", () => {
|
|
807
|
+
/* user logged out */
|
|
808
|
+
});
|
|
809
|
+
widget.addEventListener("rai-height-change", (e) =>
|
|
810
|
+
console.log(e.detail.height),
|
|
811
|
+
);
|
|
459
812
|
```
|
|
460
813
|
|
|
461
814
|
---
|
|
@@ -465,11 +818,11 @@ widget.addEventListener('rai-height-change', (e) => console.log(e.detail.height)
|
|
|
465
818
|
After the SDK loads, `window.ReturningAIWidget` is available:
|
|
466
819
|
|
|
467
820
|
```js
|
|
468
|
-
window.ReturningAIWidget.version
|
|
469
|
-
await window.ReturningAIWidget.reload()
|
|
470
|
-
await window.ReturningAIWidget.logout()
|
|
471
|
-
window.ReturningAIWidget.isAuthenticated() // boolean
|
|
472
|
-
window.ReturningAIWidget.getTokenInfo()
|
|
821
|
+
window.ReturningAIWidget.version; // current SDK version
|
|
822
|
+
await window.ReturningAIWidget.reload(); // re-runs auth flow and reloads widget
|
|
823
|
+
await window.ReturningAIWidget.logout(); // clears tokens and removes widget
|
|
824
|
+
window.ReturningAIWidget.isAuthenticated(); // boolean
|
|
825
|
+
window.ReturningAIWidget.getTokenInfo(); // token metadata (no token values exposed)
|
|
473
826
|
```
|
|
474
827
|
|
|
475
828
|
---
|
|
@@ -479,7 +832,7 @@ window.ReturningAIWidget.getTokenInfo() // token metadata (no token values ex
|
|
|
479
832
|
Requires Custom Elements v1 + Shadow DOM v1.
|
|
480
833
|
|
|
481
834
|
| Browser | Minimum version |
|
|
482
|
-
|
|
483
|
-
| Chrome
|
|
484
|
-
| Firefox | 63
|
|
485
|
-
| Safari
|
|
835
|
+
| ------- | --------------- |
|
|
836
|
+
| Chrome | 67 |
|
|
837
|
+
| Firefox | 63 |
|
|
838
|
+
| Safari | 13 |
|