@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/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('https://api-v2.returning.ai/v2/api/widget-access-keys/token', {
28
- method: 'POST',
29
- headers: { 'Content-Type': 'application/json' },
30
- body: JSON.stringify({
31
- accessId: process.env.RAI_ACCESS_ID,
32
- accessKey: process.env.RAI_ACCESS_KEY,
33
- // userIdentifiers keys MUST be `data-*` prefixed — they match the
34
- // `dataAttribute` value of each field configured in your widget's
35
- // User Identifier Fields (e.g. the dashboard stores "data-email",
36
- // "data-user-id", not bare "email"/"userId").
37
- userIdentifiers: {
38
- 'data-user-id': [YourUserObject].id,
39
- 'data-email': [YourUserObject].email,
40
- // ...any other fields configured for this widget
41
- },
42
- }),
43
- })
44
- const { data } = await response.json()
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 id="returning-ai-widget-YOUR_WIDGET_ID" style="width:100%;height:600px"></div>
52
- <script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
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` | Description |
88
- |---------------------|-------------|
89
- | `store` | Store / rewards widget |
90
- | `channel` | Channel widget |
91
- | `milestone` | Milestone widget |
92
- | `currency-view` | Currency widget |
93
- | `referral-conditions` | Referral widget |
94
- | `custom` | Custom widget |
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 id="returning-ai-widget-YOUR_WIDGET_ID" style="width:100%;height:600px"></div>
102
- <script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
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 id="returning-ai-widget-YOUR_WIDGET_ID" style="width:100%;height:600px"></div>
115
- <script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
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 id="returning-ai-widget-YOUR_WIDGET_ID" style="width:100%;height:600px"></div>
130
- <script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
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 id="returning-ai-widget-YOUR_WIDGET_ID" style="width:100%;height:600px"></div>
143
- <script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
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 | Required | Default | Description |
168
- |-----------|----------|---------|-------------|
169
- | `data-widget-id` | **Yes** | — | Your widget ID (from dashboard) |
170
- | `data-widget-type` | **Yes** | `store` | Widget type (see table above) |
171
- | `api-url` | No | ReturningAI widget auth server | Optional widget auth server override |
172
- | `widget-url` | **Yes**\* | — | Widget URL (from dashboard). \*Not required when using `data-bundle-url` |
173
- | `data-bundle-url` | No | — | URL to the widget IIFE bundle — triggers [bundle mode](#bundle-mode) (non-iframe) |
174
- | `data-theme` | No | `light` | `light` or `dark` |
175
- | `data-email` | Public Embed only | — | User's email for identification. **Access Key Embed:** omit — identity is signed into `data-embed-token` |
176
- | `data-embed-token` | Access Key Embed only | — | Short-lived JWT from your server. Carries signed user identifiers — no `data-*` identifier attributes needed |
177
- | `data-locale` | No | — | BCP 47 locale tag (e.g. `fr-FR`) |
178
- | `data-eager` | No | — | Load immediately instead of waiting until visible (presence-based boolean) |
179
- | `data-auto-refresh` | No | `true` | Auto-refresh access token before expiry |
180
- | `data-debug` | No | `false` | Verbose console logging |
181
- | `data-custom-data` | No | — | JSON object forwarded to the widget |
182
- | `data-retry-label` | No | `Retry` | Label text for the retry button on the error screen |
183
- | `data-v2-api-url` | No | ReturningAI V2 API | Optional V2 API override for bundle mode |
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 | Required | Default | Description |
188
- |-----------|----------|---------|-------------|
189
- | `community-id` | **Yes**\* | — | Your community ID (raw ObjectId from dashboard). \*Custom widgets use `widget-id` instead |
190
- | `channel-id` | **Channel only** | — | Channel ID (raw ObjectId) — required for `<rai-channel-widget>` |
191
- | `api-url` | No | ReturningAI widget auth server | Optional widget auth server override |
192
- | `widget-url` | **Yes**\* | — | Widget URL (from dashboard). \*Not required when using `bundle-url` |
193
- | `bundle-url` | No | — | URL to the widget IIFE bundle — triggers [bundle mode](#bundle-mode) (non-iframe) |
194
- | `theme` | No | `light` | `light` or `dark` |
195
- | `data-email` | Public Embed only | — | User's email for identification. **Access Key Embed:** omit — identity is signed into `embed-token` |
196
- | `embed-token` | Access Key Embed only | — | Short-lived JWT from your server. Carries signed user identifiers — no `data-*` identifier attributes needed |
197
- | `width` | No | `100%` | CSS width |
198
- | `height` | No | `600px` | CSS height |
199
- | `locale` | No | — | BCP 47 locale tag (e.g. `fr-FR`) |
200
- | `eager` | No | — | Load immediately instead of waiting until visible (presence-based boolean) |
201
- | `auto-refresh` | No | `true` | Auto-refresh access token before expiry |
202
- | `debug` | No | `false` | Verbose console logging |
203
- | `custom-data` | No | — | JSON object forwarded to the widget |
204
- | `retry-label` | No | `Retry` | Label text for the retry button on the error screen |
205
- | `v2-api-url` | No | ReturningAI V2 API | Optional V2 API override for bundle mode |
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 id="returning-ai-widget-YOUR_WIDGET_ID" style="width:100%;height:600px"></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 src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
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 id="returning-ai-widget-YOUR_WIDGET_ID" style="width:100%;height:100vh"></div>
267
- <script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
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 | Bundle global | Custom element tag |
291
- |-------------|--------------|-------------------|
292
- | `store` | `RaiStoreWidget` | `<rai-store-widget>` |
293
- | `channel` | `RaiChannelWidget` | `<rai-channel-widget>` |
294
- | `milestone` | `RaiMilestoneWidget` | `<rai-milestone-widget>` |
295
- | `currency-view` | `RaiCurrencyWidget` | `<rai-currency-widget>` |
296
- | `referral-conditions` | `RaiReferralWidget` | `<rai-referral-widget>` |
297
- | `custom` | `RaiCustomWidget` | `<rai-custom-widget>` |
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 '@returningai/widget-sdk'
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 '@returningai/widget-sdk'
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 | Required attributes | Widget type |
423
- |-----|-------------------|-------------|
424
- | `<rai-store-widget>` | `community-id` | Store |
425
- | `<rai-channel-widget>` | `community-id` + `channel-id` | Channel |
426
- | `<rai-milestone-widget>` | `community-id` | Milestone |
427
- | `<rai-currency-widget>` | `community-id` | Currency |
428
- | `<rai-referral-widget>` | `community-id` | Referral |
429
- | `<rai-custom-widget>` | `widget-id` (not `community-id`) | Custom |
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 | `detail` | Fired when |
438
- |-------|----------|-----------|
439
- | `rai-authenticated` | `{}` | Auth succeeded, before widget mounts |
440
- | `rai-ready` | `{}` | `WIDGET_READY` received, loader hidden (iframe mode) |
441
- | `rai-mounted` | `{}` | Widget bundle mounted successfully (bundle mode) |
442
- | `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. |
443
- | `rai-logout` | `{}` | Widget logged out |
444
- | `rai-height-change` | `{ height }` | iframe resized (after debounce) |
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('[data-widget-id]')
789
+ const widget = document.querySelector("[data-widget-id]");
449
790
 
450
791
  // Custom element embed
451
- const widget = document.querySelector('rai-store-widget')
452
-
453
- widget.addEventListener('rai-authenticated', () => { /* auth succeeded */ })
454
- widget.addEventListener('rai-ready', () => { /* widget loaded and visible (iframe mode) */ })
455
- widget.addEventListener('rai-mounted', () => { /* widget mounted (bundle mode) */ })
456
- widget.addEventListener('rai-error', (e) => console.error(e.detail.status, e.detail.message, e.detail.hint))
457
- widget.addEventListener('rai-logout', () => { /* user logged out */ })
458
- widget.addEventListener('rai-height-change', (e) => console.log(e.detail.height))
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 // current SDK version
469
- await window.ReturningAIWidget.reload() // re-runs auth flow and reloads widget
470
- await window.ReturningAIWidget.logout() // clears tokens and removes widget
471
- window.ReturningAIWidget.isAuthenticated() // boolean
472
- window.ReturningAIWidget.getTokenInfo() // token metadata (no token values exposed)
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 | 67 |
484
- | Firefox | 63 |
485
- | Safari | 13 |
835
+ | ------- | --------------- |
836
+ | Chrome | 67 |
837
+ | Firefox | 63 |
838
+ | Safari | 13 |