@returningai/widget-sdk 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -81,33 +81,93 @@ function setTokens(config, state, data, onRefreshScheduled) {
81
81
  async function authenticateServerless(config, state, onRefreshScheduled) {
82
82
  const nonce = crypto.randomUUID();
83
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}`);
84
+ const maxRetries = config.maxRetries ?? 3;
85
+ const retryDelay = config.retryDelay ?? 500;
86
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
87
+ if (attempt > 0) {
88
+ await new Promise(
89
+ (resolve) => setTimeout(resolve, retryDelay * (1 << attempt - 1))
90
+ );
99
91
  }
100
- const data = await response.json();
101
- if (!data.accessToken || !data.refreshToken) {
102
- throw new Error("Invalid authentication response");
92
+ let is4xx = false;
93
+ try {
94
+ const response = await fetch(`${config.apiUrl}/${config.widgetId}/auth/serverless`, {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify({
98
+ nonce,
99
+ timestamp,
100
+ widgetType: config.widgetType,
101
+ userIdentifiers: config.userIdentifiers
102
+ }),
103
+ credentials: "include"
104
+ });
105
+ if (!response.ok) {
106
+ is4xx = response.status >= 400 && response.status < 500;
107
+ const error = await response.json().catch(() => ({ error: "Authentication failed" }));
108
+ throw new Error(error.error || `HTTP ${response.status}`);
109
+ }
110
+ const data = await response.json();
111
+ if (!data.accessToken || !data.refreshToken) {
112
+ throw new Error("Invalid authentication response");
113
+ }
114
+ setTokens(config, state, data, onRefreshScheduled);
115
+ state.isAuthenticated = true;
116
+ return true;
117
+ } catch {
118
+ if (is4xx || attempt === maxRetries) {
119
+ state.isAuthenticated = false;
120
+ return false;
121
+ }
103
122
  }
104
- setTokens(config, state, data, onRefreshScheduled);
105
- state.isAuthenticated = true;
106
- return true;
107
- } catch {
108
- state.isAuthenticated = false;
109
- return false;
110
123
  }
124
+ state.isAuthenticated = false;
125
+ return false;
126
+ }
127
+ async function authenticateViaProxy(config, state, onRefreshScheduled) {
128
+ const maxRetries = config.maxRetries ?? 3;
129
+ const retryDelay = config.retryDelay ?? 500;
130
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
131
+ if (attempt > 0) {
132
+ await new Promise(
133
+ (resolve) => setTimeout(resolve, retryDelay * (1 << attempt - 1))
134
+ );
135
+ }
136
+ let is4xx = false;
137
+ try {
138
+ const response = await fetch(config.authUrl, {
139
+ method: "POST",
140
+ credentials: "include",
141
+ headers: { "Content-Type": "application/json" }
142
+ });
143
+ if (!response.ok) {
144
+ is4xx = response.status >= 400 && response.status < 500;
145
+ throw new Error(`HTTP ${response.status}`);
146
+ }
147
+ const data = await response.json();
148
+ if (!data.token) {
149
+ throw new Error("Invalid proxy auth response");
150
+ }
151
+ const ttl = data.expiresIn ?? 300;
152
+ state.accessToken = data.token;
153
+ state.refreshToken = null;
154
+ state.tokenFamily = null;
155
+ state.accessTokenExpiry = Date.now() + ttl * 1e3;
156
+ state.refreshTokenExpiry = null;
157
+ state.isAuthenticated = true;
158
+ if (config.autoRefresh && onRefreshScheduled) {
159
+ onRefreshScheduled();
160
+ }
161
+ return true;
162
+ } catch {
163
+ if (is4xx || attempt === maxRetries) {
164
+ state.isAuthenticated = false;
165
+ return false;
166
+ }
167
+ }
168
+ }
169
+ state.isAuthenticated = false;
170
+ return false;
111
171
  }
112
172
  async function refreshAccessToken(config, state, onRefreshScheduled, sendToken) {
113
173
  if (state.isRefreshing) {
@@ -158,6 +218,11 @@ async function getValidToken(config, state, onRefreshScheduled) {
158
218
  if (state.accessToken && !isTokenExpired(state.accessTokenExpiry)) {
159
219
  return state.accessToken;
160
220
  }
221
+ if (config.authUrl) {
222
+ const authed2 = await authenticateViaProxy(config, state, onRefreshScheduled);
223
+ if (authed2 && state.accessToken) return state.accessToken;
224
+ throw new Error("Unable to obtain valid token");
225
+ }
161
226
  const refreshed = await refreshAccessToken(config, state, onRefreshScheduled);
162
227
  if (refreshed && state.accessToken) return state.accessToken;
163
228
  const authed = await authenticateServerless(config, state, onRefreshScheduled);
@@ -181,6 +246,18 @@ async function logout(config, state) {
181
246
  clearState(state);
182
247
  clearStorage(config);
183
248
  }
249
+ async function validateEmbedToken(config) {
250
+ try {
251
+ const res = await fetch(`${config.apiUrl}/v2/api/widget-access-keys/validate`, {
252
+ method: "POST",
253
+ headers: { "Content-Type": "application/json" },
254
+ body: JSON.stringify({ embedToken: config.embedToken })
255
+ });
256
+ return res.ok;
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
184
261
  async function fetchErrorSettings(config, state) {
185
262
  const cached = loadErrorSettingsFromStorage(config);
186
263
  if (cached) {
@@ -231,6 +308,24 @@ function clearState(state) {
231
308
  state.syncTimer = null;
232
309
  }
233
310
  }
311
+ function debounce(fn, wait) {
312
+ let timer = null;
313
+ return {
314
+ call(h) {
315
+ if (timer) clearTimeout(timer);
316
+ timer = setTimeout(() => {
317
+ fn(h);
318
+ timer = null;
319
+ }, wait);
320
+ },
321
+ cancel() {
322
+ if (timer) {
323
+ clearTimeout(timer);
324
+ timer = null;
325
+ }
326
+ }
327
+ };
328
+ }
234
329
  function sendTokenToWidget(config, state, iframe) {
235
330
  var _a;
236
331
  if (!state.accessToken) return;
@@ -240,7 +335,8 @@ function sendTokenToWidget(config, state, iframe) {
240
335
  type: "RETURNINGAI_WIDGET_TOKEN",
241
336
  value: {
242
337
  widgetId: config.widgetId,
243
- token: state.accessToken
338
+ token: state.accessToken,
339
+ ...config.customData !== void 0 && { customData: config.customData }
244
340
  }
245
341
  },
246
342
  config.widgetDomain
@@ -248,7 +344,11 @@ function sendTokenToWidget(config, state, iframe) {
248
344
  } catch {
249
345
  }
250
346
  }
251
- function setupMessageListener(config, state, _shadow, iframe, onRefreshScheduled, onLogout, hideLoader) {
347
+ function setupMessageListener(config, state, _shadow, iframe, onRefreshScheduled, onLogout, hideLoader, emit) {
348
+ const heightSetter = debounce((h) => {
349
+ iframe.style.height = `${h}px`;
350
+ emit == null ? void 0 : emit("rai-height-change", { height: h });
351
+ }, config.heightDebounce ?? 100);
252
352
  const handler = async (event) => {
253
353
  var _a;
254
354
  if (event.origin !== config.widgetDomain) return;
@@ -261,7 +361,11 @@ function setupMessageListener(config, state, _shadow, iframe, onRefreshScheduled
261
361
  (_a = iframe.contentWindow) == null ? void 0 : _a.postMessage(
262
362
  {
263
363
  type: "RETURNINGAI_WIDGET_TOKEN",
264
- value: { token, widgetId: config.widgetId }
364
+ value: {
365
+ token,
366
+ widgetId: config.widgetId,
367
+ ...config.customData !== void 0 && { customData: config.customData }
368
+ }
265
369
  },
266
370
  config.widgetDomain
267
371
  );
@@ -272,7 +376,7 @@ function setupMessageListener(config, state, _shadow, iframe, onRefreshScheduled
272
376
  case "WIDGET_HEIGHT_UPDATE": {
273
377
  const h = Number(payload == null ? void 0 : payload.height);
274
378
  if (Number.isFinite(h) && h > 0) {
275
- iframe.style.height = `${h}px`;
379
+ heightSetter.call(h);
276
380
  }
277
381
  break;
278
382
  }
@@ -281,6 +385,7 @@ function setupMessageListener(config, state, _shadow, iframe, onRefreshScheduled
281
385
  if (containerId !== void 0 && containerId !== config.container) break;
282
386
  iframe.classList.add("loaded");
283
387
  if (hideLoader) hideLoader();
388
+ emit == null ? void 0 : emit("rai-ready");
284
389
  break;
285
390
  }
286
391
  case "WIDGET_ERROR": {
@@ -294,11 +399,17 @@ function setupMessageListener(config, state, _shadow, iframe, onRefreshScheduled
294
399
  }
295
400
  };
296
401
  window.addEventListener("message", handler);
297
- return () => window.removeEventListener("message", handler);
402
+ return () => {
403
+ window.removeEventListener("message", handler);
404
+ heightSetter.cancel();
405
+ };
298
406
  }
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)}}";
407
+ 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}.rai-sdk-loader{position:relative;width:75px;height:100px}.rai-sdk-loader__bar{position:absolute;bottom:0;width:10px;height:50%;background:var(--rai-accent, #000000);transform-origin:center bottom;box-shadow:1px 1px #0003}.rai-sdk-loader__bar:nth-child(1){left:0;transform:scaleY(.2);animation:rai-sdk-barUp1 4s infinite}.rai-sdk-loader__bar:nth-child(2){left:15px;transform:scaleY(.4);animation:rai-sdk-barUp2 4s infinite}.rai-sdk-loader__bar:nth-child(3){left:30px;transform:scaleY(.6);animation:rai-sdk-barUp3 4s infinite}.rai-sdk-loader__bar:nth-child(4){left:45px;transform:scaleY(.8);animation:rai-sdk-barUp4 4s infinite}.rai-sdk-loader__bar:nth-child(5){left:60px;transform:scale(1);animation:rai-sdk-barUp5 4s infinite}.rai-sdk-loader__ball{position:absolute;bottom:10px;left:0;width:10px;height:10px;background:var(--rai-accent, #000000);border-radius:50%;animation:rai-sdk-ball 4s infinite}@keyframes rai-sdk-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 rai-sdk-barUp1{0%{transform:scaleY(.2)}40%{transform:scaleY(.2)}50%{transform:scale(1)}90%{transform:scale(1)}to{transform:scaleY(.2)}}@keyframes rai-sdk-barUp2{0%{transform:scaleY(.4)}40%{transform:scaleY(.4)}50%{transform:scaleY(.8)}90%{transform:scaleY(.8)}to{transform:scaleY(.4)}}@keyframes rai-sdk-barUp3{0%{transform:scaleY(.6)}to{transform:scaleY(.6)}}@keyframes rai-sdk-barUp4{0%{transform:scaleY(.8)}40%{transform:scaleY(.8)}50%{transform:scaleY(.4)}90%{transform:scaleY(.4)}to{transform:scaleY(.8)}}@keyframes rai-sdk-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)}}.rai-retry-btn{padding:8px 20px;border:none;border-radius:6px;background:var(--rai-accent, #000000);color:var(--rai-error-bg, #ffffff);font-size:14px;font-family:system-ui,-apple-system,sans-serif;cursor:pointer}.rai-retry-btn:hover{opacity:.85}";
300
408
  const DEFINED_ATTRS = /* @__PURE__ */ new Set([
409
+ "community-id",
410
+ "channel-id",
301
411
  "widget-id",
412
+ // deprecated — 'community-id' is the canonical name
302
413
  "widget-type",
303
414
  "theme",
304
415
  "container",
@@ -307,14 +418,76 @@ const DEFINED_ATTRS = /* @__PURE__ */ new Set([
307
418
  "api-url",
308
419
  "widget-url",
309
420
  "auto-refresh",
310
- "debug"
421
+ "debug",
422
+ // New attrs — excluded from userIdentifiers
423
+ "storage-prefix",
424
+ "max-retries",
425
+ "retry-delay",
426
+ "height-debounce",
427
+ "locale",
428
+ "custom-data",
429
+ "retry-label",
430
+ "auth-url",
431
+ "bundle-url",
432
+ "embed-token"
311
433
  ]);
434
+ const WIDGET_GLOBALS = {
435
+ store: "RaiStoreWidget",
436
+ channel: "RaiChannelWidget",
437
+ social: "RaiSocialWidget",
438
+ milestone: "RaiMilestoneWidget",
439
+ "currency-view": "RaiCurrencyWidget"
440
+ };
441
+ const TAG_TO_TYPE = {
442
+ "RAI-STORE-WIDGET": "store",
443
+ "RAI-CHANNEL-WIDGET": "channel",
444
+ "RAI-MILESTONE-WIDGET": "milestone",
445
+ "RAI-SOCIAL-WIDGET": "social",
446
+ "RAI-CURRENCY-WIDGET": "currency-view",
447
+ "RAI-WIDGET": "store"
448
+ // deprecated alias
449
+ };
450
+ function loadScript(url) {
451
+ return new Promise((resolve, reject) => {
452
+ const s = document.createElement("script");
453
+ s.src = url;
454
+ s.onload = () => resolve();
455
+ s.onerror = () => reject(new Error(`Failed to load ${url}`));
456
+ document.head.appendChild(s);
457
+ });
458
+ }
312
459
  function readConfig(el, existingId) {
313
460
  const get = (name) => el.getAttribute(name) ?? el.getAttribute(`data-${name}`) ?? "";
461
+ const num = (name, def) => {
462
+ const v = parseInt(get(name), 10);
463
+ return Number.isFinite(v) && v >= 0 ? v : def;
464
+ };
465
+ const usesExplicitIds = !!get("community-id");
466
+ const communityId = get("community-id") || get("widget-id") || existingId || "";
467
+ if (communityId && !/^[a-zA-Z0-9_\-=]{8,}$/.test(communityId)) {
468
+ throw new Error(`[rai-widget] Invalid community-id: "${communityId}"`);
469
+ }
470
+ const channelId = get("channel-id") || void 0;
471
+ if (channelId && !/^[a-zA-Z0-9_\-=]{8,}$/.test(channelId)) {
472
+ throw new Error(`[rai-widget] Invalid channel-id: "${channelId}"`);
473
+ }
474
+ const widgetType = get("widget-type") || TAG_TO_TYPE[el.tagName] || "store";
475
+ let widgetId;
476
+ if (usesExplicitIds) {
477
+ if (widgetType === "store") {
478
+ widgetId = communityId;
479
+ } else if (widgetType === "channel") {
480
+ widgetId = channelId ? btoa(channelId) : communityId;
481
+ } else {
482
+ widgetId = communityId ? btoa(communityId) : communityId;
483
+ }
484
+ } else {
485
+ widgetId = communityId;
486
+ }
314
487
  const rawWidgetUrl = get("widget-url") || "https://widget.returningai.com";
315
488
  let widgetUrl = rawWidgetUrl;
316
489
  if (widgetUrl.endsWith("store-widget")) {
317
- widgetUrl = `${widgetUrl}/${get("widget-id")}/open-widget`;
490
+ widgetUrl = `${widgetUrl}/${communityId}/open-widget`;
318
491
  }
319
492
  let widgetDomain;
320
493
  try {
@@ -327,11 +500,7 @@ function readConfig(el, existingId) {
327
500
  u.searchParams.set("color", get("theme") || "light");
328
501
  widgetUrl = u.toString();
329
502
  }
330
- const widgetId = get("widget-id") || existingId || "";
331
- if (widgetId && !/^[a-zA-Z0-9_\-=]{8,}$/.test(widgetId)) {
332
- throw new Error(`[rai-widget] Invalid widget-id format: "${widgetId}"`);
333
- }
334
- const container = get("container") || get("data-container") || `returning-ai-widget-${widgetId}`;
503
+ const container = get("container") || get("data-container") || `returning-ai-widget-${communityId}`;
335
504
  const userIdentifiers = {};
336
505
  Array.from(el.attributes).forEach((attr) => {
337
506
  const name = attr.name.toLowerCase();
@@ -341,9 +510,21 @@ function readConfig(el, existingId) {
341
510
  userIdentifiers[name] = attr.value;
342
511
  }
343
512
  });
513
+ const customData = (() => {
514
+ const raw = get("custom-data");
515
+ if (!raw) return void 0;
516
+ try {
517
+ return JSON.parse(raw);
518
+ } catch {
519
+ return void 0;
520
+ }
521
+ })();
344
522
  return {
523
+ communityId,
524
+ channelId,
345
525
  widgetId,
346
- widgetType: get("widget-type") || "store",
526
+ // auth token — base64-encoded per widget type for auth URLs + iframe URL building
527
+ widgetType,
347
528
  theme: get("theme") || "light",
348
529
  container,
349
530
  width: get("width") || "100%",
@@ -353,8 +534,25 @@ function readConfig(el, existingId) {
353
534
  widgetDomain,
354
535
  autoRefresh: get("auto-refresh") !== "false",
355
536
  debug: get("debug") === "true",
356
- storagePrefix: "returning-ai-widget",
357
- userIdentifiers
537
+ storagePrefix: get("storage-prefix") || "returning-ai-widget",
538
+ // #7
539
+ userIdentifiers,
540
+ maxRetries: num("max-retries", 3),
541
+ // #2
542
+ retryDelay: num("retry-delay", 500),
543
+ // #2
544
+ heightDebounce: num("height-debounce", 100),
545
+ // #4
546
+ locale: get("locale") || void 0,
547
+ // #5
548
+ customData,
549
+ // #8
550
+ authUrl: get("auth-url") || void 0,
551
+ // authenticated embed
552
+ bundleUrl: get("bundle-url") || void 0,
553
+ // bundle mode
554
+ embedToken: get("embed-token") || void 0
555
+ // required — issued by customer's server
358
556
  };
359
557
  }
360
558
  function createInitialState() {
@@ -381,7 +579,11 @@ class BaseWidget extends HTMLElement {
381
579
  __publicField(this, "state", createInitialState());
382
580
  __publicField(this, "loaderEl", null);
383
581
  __publicField(this, "errorEl", null);
582
+ __publicField(this, "msgEl", null);
384
583
  __publicField(this, "cleanupListener");
584
+ __publicField(this, "intersectionObserver");
585
+ // #3
586
+ __publicField(this, "themeObserver");
385
587
  this.shadow = this.attachShadow({ mode: "closed" });
386
588
  }
387
589
  connectedCallback() {
@@ -393,17 +595,43 @@ class BaseWidget extends HTMLElement {
393
595
  const accentColor = theme === "dark" ? "#ffffff" : "#000000";
394
596
  const text4Color = theme === "dark" ? "#9ca3af" : "#6b7280";
395
597
  const bgColor = theme === "dark" ? "#1a1a1a" : "#ffffff";
598
+ this.style.width = this.config.width;
599
+ this.style.height = this.config.height;
396
600
  this.style.setProperty("--rai-accent", accentColor);
397
601
  this.style.setProperty("--rai-text4", text4Color);
398
602
  this.style.setProperty("--rai-loader-bg", bgColor);
399
603
  this.style.setProperty("--rai-error-bg", bgColor);
400
604
  this.renderShell();
401
- this.init();
605
+ if (this.hasAttribute("eager")) {
606
+ this.init();
607
+ } else {
608
+ this.intersectionObserver = new IntersectionObserver((entries) => {
609
+ if (entries[0].isIntersecting) {
610
+ this.intersectionObserver.disconnect();
611
+ this.intersectionObserver = void 0;
612
+ this.init();
613
+ }
614
+ });
615
+ this.intersectionObserver.observe(this);
616
+ }
402
617
  }
403
618
  disconnectedCallback() {
619
+ var _a, _b;
620
+ (_a = this.intersectionObserver) == null ? void 0 : _a.disconnect();
621
+ (_b = this.themeObserver) == null ? void 0 : _b.disconnect();
404
622
  if (this.cleanupListener) this.cleanupListener();
405
623
  clearState(this.state);
406
624
  }
625
+ // ── DOM event helper (#1) ─────────────────────────────────────────────
626
+ emit(type, detail) {
627
+ this.dispatchEvent(
628
+ new CustomEvent(type, {
629
+ bubbles: true,
630
+ composed: true,
631
+ detail: detail ?? {}
632
+ })
633
+ );
634
+ }
407
635
  // ── Rendering ─────────────────────────────────────────────────────────
408
636
  renderShell() {
409
637
  this.loaderEl = document.createElement("div");
@@ -412,19 +640,28 @@ class BaseWidget extends HTMLElement {
412
640
  this.shadow.appendChild(this.loaderEl);
413
641
  this.errorEl = document.createElement("div");
414
642
  this.errorEl.className = "rai-error";
415
- this.errorEl.textContent = "Authentication failed. Please try again later.";
643
+ this.msgEl = document.createElement("span");
644
+ this.msgEl.className = "rai-error-msg";
645
+ this.msgEl.textContent = "Authentication failed. Please try again later.";
646
+ this.errorEl.appendChild(this.msgEl);
647
+ const retryLabel = this.getAttribute("retry-label") || this.getAttribute("data-retry-label") || "Retry";
648
+ const btn = document.createElement("button");
649
+ btn.className = "rai-retry-btn";
650
+ btn.textContent = retryLabel;
651
+ btn.addEventListener("click", () => this.reload());
652
+ this.errorEl.appendChild(btn);
416
653
  this.shadow.appendChild(this.errorEl);
417
654
  }
418
655
  createDefaultLoader() {
419
656
  const loader = document.createElement("div");
420
- loader.className = "loader";
657
+ loader.className = "rai-sdk-loader";
421
658
  for (let i = 0; i < 5; i++) {
422
659
  const bar = document.createElement("div");
423
- bar.className = "loader__bar";
660
+ bar.className = "rai-sdk-loader__bar";
424
661
  loader.appendChild(bar);
425
662
  }
426
663
  const ball = document.createElement("div");
427
- ball.className = "loader__ball";
664
+ ball.className = "rai-sdk-loader__ball";
428
665
  loader.appendChild(ball);
429
666
  return loader;
430
667
  }
@@ -438,22 +675,51 @@ class BaseWidget extends HTMLElement {
438
675
  this.loaderEl = null;
439
676
  }
440
677
  showError() {
441
- var _a;
678
+ var _a, _b;
442
679
  this.hideLoader();
443
680
  if (this.errorEl) {
444
- if ((_a = this.state.errorSettings) == null ? void 0 : _a.errorMessage) {
445
- this.errorEl.textContent = this.state.errorSettings.errorMessage;
681
+ if (((_a = this.state.errorSettings) == null ? void 0 : _a.errorMessage) && this.msgEl) {
682
+ this.msgEl.textContent = this.state.errorSettings.errorMessage;
446
683
  }
447
684
  this.errorEl.classList.add("visible");
448
685
  }
686
+ this.emit("rai-error", { message: ((_b = this.msgEl) == null ? void 0 : _b.textContent) ?? "Authentication failed" });
449
687
  }
450
688
  // ── Initialization ─────────────────────────────────────────────────────
451
689
  async init() {
452
- if (!this.config.widgetId) {
690
+ if (!this.config.communityId) {
453
691
  this.showError();
454
692
  return;
455
693
  }
456
694
  await fetchErrorSettings(this.config, this.state);
695
+ if (this.config.embedToken) {
696
+ const valid = await validateEmbedToken(this.config);
697
+ if (!valid) {
698
+ this.showError();
699
+ return;
700
+ }
701
+ }
702
+ const launchWidget = () => {
703
+ if (this.config.bundleUrl) {
704
+ this.mountWidget();
705
+ } else {
706
+ this.createIframe();
707
+ }
708
+ };
709
+ if (this.config.authUrl) {
710
+ const authed2 = await authenticateViaProxy(
711
+ this.config,
712
+ this.state,
713
+ () => this.scheduleRefresh()
714
+ );
715
+ if (authed2) {
716
+ this.emit("rai-authenticated");
717
+ launchWidget();
718
+ } else {
719
+ this.showError();
720
+ }
721
+ return;
722
+ }
457
723
  const hasStored = loadFromStorage(this.config, this.state);
458
724
  if (hasStored) {
459
725
  const refreshed = await refreshAccessToken(
@@ -466,7 +732,8 @@ class BaseWidget extends HTMLElement {
466
732
  return;
467
733
  }
468
734
  this.state.isAuthenticated = true;
469
- this.createIframe();
735
+ this.emit("rai-authenticated");
736
+ launchWidget();
470
737
  return;
471
738
  }
472
739
  const authed = await authenticateServerless(
@@ -475,7 +742,8 @@ class BaseWidget extends HTMLElement {
475
742
  () => this.scheduleRefresh()
476
743
  );
477
744
  if (authed) {
478
- this.createIframe();
745
+ this.emit("rai-authenticated");
746
+ launchWidget();
479
747
  } else {
480
748
  this.showError();
481
749
  }
@@ -507,34 +775,83 @@ class BaseWidget extends HTMLElement {
507
775
  iframe,
508
776
  () => this.scheduleRefresh(),
509
777
  () => this.logoutAndClear(),
510
- () => this.hideLoader()
778
+ () => this.hideLoader(),
779
+ (type, detail) => this.emit(type, detail)
780
+ // #1
511
781
  );
512
782
  }
783
+ // ── Bundle mode (no-iframe) ──────────────────────────────────────────
784
+ async mountWidget() {
785
+ try {
786
+ const w = window;
787
+ const endpointVars = [
788
+ "api_url",
789
+ "base_url",
790
+ "auth_api",
791
+ "socket_path_v2",
792
+ "socket_path",
793
+ "channel_api"
794
+ ];
795
+ for (const v of endpointVars) {
796
+ if (!w[v]) w[v] = this.config.apiUrl;
797
+ }
798
+ await loadScript(this.config.bundleUrl);
799
+ const globalName = WIDGET_GLOBALS[this.config.widgetType];
800
+ const widgetGlobal = window[globalName];
801
+ if (!(widgetGlobal == null ? void 0 : widgetGlobal.mount)) {
802
+ throw new Error(`window.${globalName}.mount not found after loading bundle`);
803
+ }
804
+ const container = document.createElement("div");
805
+ container.style.cssText = "position: relative; width: 100%; height: 100%;";
806
+ this.appendChild(container);
807
+ if (!this.shadow.querySelector("slot")) {
808
+ const slot = document.createElement("slot");
809
+ this.shadow.appendChild(slot);
810
+ }
811
+ if (this.loaderEl) {
812
+ this.loaderEl.remove();
813
+ this.loaderEl = null;
814
+ }
815
+ widgetGlobal.mount(container, {
816
+ widgetType: this.config.widgetType,
817
+ communityId: this.config.communityId,
818
+ channelId: this.config.channelId,
819
+ // channel widget only; others receive undefined and ignore it
820
+ widgetId: this.config.communityId,
821
+ // deprecated alias kept for widget main.tsx compatibility
822
+ apiUrl: this.config.apiUrl,
823
+ token: this.state.accessToken,
824
+ basePath: "/",
825
+ isPreview: false,
826
+ isOpenWidget: true,
827
+ defaultColorSchema: this.config.theme === "dark" ? "dark" : "light"
828
+ });
829
+ this.emit("rai-mounted");
830
+ } catch (err) {
831
+ console.error("[rai-widget] Bundle mount failed:", err);
832
+ this.showError();
833
+ }
834
+ }
513
835
  // ── Token scheduling ──────────────────────────────────────────────────
514
836
  scheduleRefresh() {
515
837
  if (this.state.refreshTimer) clearTimeout(this.state.refreshTimer);
516
838
  if (!this.state.accessTokenExpiry) return;
517
839
  const delay = this.state.accessTokenExpiry - Date.now() - 6e4;
840
+ const pushToken = () => {
841
+ if (this.state.iframe) sendTokenToWidget(this.config, this.state, this.state.iframe);
842
+ };
843
+ const doRefresh = async () => {
844
+ if (this.config.authUrl) {
845
+ const ok = await authenticateViaProxy(this.config, this.state, () => this.scheduleRefresh());
846
+ if (ok) pushToken();
847
+ } else {
848
+ await refreshAccessToken(this.config, this.state, () => this.scheduleRefresh(), pushToken);
849
+ }
850
+ };
518
851
  if (delay > 0) {
519
- this.state.refreshTimer = setTimeout(async () => {
520
- await refreshAccessToken(
521
- this.config,
522
- this.state,
523
- () => this.scheduleRefresh(),
524
- () => {
525
- if (this.state.iframe) sendTokenToWidget(this.config, this.state, this.state.iframe);
526
- }
527
- );
528
- }, delay);
852
+ this.state.refreshTimer = setTimeout(doRefresh, delay);
529
853
  } else {
530
- refreshAccessToken(
531
- this.config,
532
- this.state,
533
- () => this.scheduleRefresh(),
534
- () => {
535
- if (this.state.iframe) sendTokenToWidget(this.config, this.state, this.state.iframe);
536
- }
537
- );
854
+ doRefresh();
538
855
  }
539
856
  }
540
857
  schedulePeriodicSync(iframe) {
@@ -547,6 +864,7 @@ class BaseWidget extends HTMLElement {
547
864
  async logoutAndClear() {
548
865
  await logout(this.config, this.state);
549
866
  this.shadow.querySelectorAll("iframe").forEach((el) => el.remove());
867
+ this.emit("rai-logout");
550
868
  }
551
869
  // ── Public API ────────────────────────────────────────────────────────
552
870
  async reload() {
@@ -578,48 +896,73 @@ class StoreWidget extends BaseWidget {
578
896
  url.searchParams.set("containerId", config.container);
579
897
  url.searchParams.set("connectType", "simple");
580
898
  url.searchParams.set("mode", "private");
899
+ if (config.locale) url.searchParams.set("locale", config.locale);
581
900
  return url.toString();
582
901
  }
583
902
  }
584
903
  class ChannelWidget extends BaseWidget {
585
904
  buildWidgetUrl(config) {
586
- if (config.widgetUrl.endsWith("channel-widget")) {
587
- return `${config.widgetUrl}/${config.widgetId}`;
905
+ const base = config.widgetUrl.endsWith("channel-widget") ? `${config.widgetUrl}/${config.widgetId}` : config.widgetUrl;
906
+ if (!config.locale) return base;
907
+ try {
908
+ const u = new URL(base);
909
+ u.searchParams.set("locale", config.locale);
910
+ return u.toString();
911
+ } catch {
912
+ return `${base}?locale=${encodeURIComponent(config.locale)}`;
588
913
  }
589
- return config.widgetUrl;
590
914
  }
591
915
  }
592
916
  class MilestoneWidget extends BaseWidget {
593
917
  buildWidgetUrl(config) {
594
- if (config.widgetUrl.endsWith("milestone-widget")) {
595
- return `${config.widgetUrl}/${config.widgetId}`;
918
+ const base = config.widgetUrl.endsWith("milestone-widget") ? `${config.widgetUrl}/${config.widgetId}` : config.widgetUrl;
919
+ if (!config.locale) return base;
920
+ try {
921
+ const u = new URL(base);
922
+ u.searchParams.set("locale", config.locale);
923
+ return u.toString();
924
+ } catch {
925
+ return `${base}?locale=${encodeURIComponent(config.locale)}`;
596
926
  }
597
- return config.widgetUrl;
598
927
  }
599
928
  }
600
929
  class SocialWidget extends BaseWidget {
601
930
  buildWidgetUrl(config) {
602
- if (config.widgetUrl.endsWith("social-widget")) {
603
- return `${config.widgetUrl}/${config.widgetId}`;
931
+ const base = config.widgetUrl.endsWith("social-widget") ? `${config.widgetUrl}/${config.widgetId}` : config.widgetUrl;
932
+ if (!config.locale) return base;
933
+ try {
934
+ const u = new URL(base);
935
+ u.searchParams.set("locale", config.locale);
936
+ return u.toString();
937
+ } catch {
938
+ return `${base}?locale=${encodeURIComponent(config.locale)}`;
604
939
  }
605
- return config.widgetUrl;
606
940
  }
607
941
  }
608
942
  class CurrencyWidget extends BaseWidget {
609
943
  buildWidgetUrl(config) {
610
- if (config.widgetUrl.endsWith("currency-overview-widget")) {
611
- return `${config.widgetUrl}/${config.widgetId}`;
944
+ const base = config.widgetUrl.endsWith("currency-overview-widget") ? `${config.widgetUrl}/${config.widgetId}` : config.widgetUrl;
945
+ if (!config.locale) return base;
946
+ try {
947
+ const u = new URL(base);
948
+ u.searchParams.set("locale", config.locale);
949
+ return u.toString();
950
+ } catch {
951
+ return `${base}?locale=${encodeURIComponent(config.locale)}`;
612
952
  }
613
- return config.widgetUrl;
614
953
  }
615
954
  }
616
- console.log(`[rai-widget] v${"1.0.2"}`);
955
+ console.log(`[rai-widget] v${"1.1.0"}`);
956
+ class StoreWidgetCompat extends StoreWidget {
957
+ }
617
958
  const WIDGET_REGISTRY = [
618
- ["rai-widget", StoreWidget],
959
+ ["rai-store-widget", StoreWidget],
619
960
  ["rai-channel-widget", ChannelWidget],
620
961
  ["rai-milestone-widget", MilestoneWidget],
621
962
  ["rai-social-widget", SocialWidget],
622
- ["rai-currency-widget", CurrencyWidget]
963
+ ["rai-currency-widget", CurrencyWidget],
964
+ ["rai-widget", StoreWidgetCompat]
965
+ // deprecated alias — use <rai-store-widget> instead
623
966
  ];
624
967
  for (const [tag, cls] of WIDGET_REGISTRY) {
625
968
  if (!customElements.get(tag)) {
@@ -635,10 +978,10 @@ const WIDGET_CLASS_MAP = {
635
978
  };
636
979
  const ALL_WIDGET_SELECTOR = WIDGET_REGISTRY.map(([tag]) => tag).join(", ");
637
980
  function bootstrapFromScriptTag() {
638
- var _a;
639
- const script = (((_a = document.currentScript) == null ? void 0 : _a.hasAttribute("data-widget-id")) ? document.currentScript : null) || document.querySelector("script[data-widget-id]");
981
+ var _a, _b;
982
+ const script = (((_a = document.currentScript) == null ? void 0 : _a.hasAttribute("data-widget-id")) || ((_b = document.currentScript) == null ? void 0 : _b.hasAttribute("data-community-id")) ? document.currentScript : null) || document.querySelector("script[data-widget-id], script[data-community-id]");
640
983
  if (!script) return;
641
- const widgetId = script.getAttribute("data-widget-id");
984
+ const widgetId = script.getAttribute("data-community-id") || script.getAttribute("data-widget-id");
642
985
  if (!widgetId) return;
643
986
  const containerId = script.getAttribute("data-container") || `returning-ai-widget-${widgetId}`;
644
987
  const container = document.getElementById(containerId);
@@ -661,15 +1004,15 @@ if (document.readyState === "loading") {
661
1004
  }
662
1005
  function exposePublicApi() {
663
1006
  const container = (() => {
664
- var _a;
665
- const script = (((_a = document.currentScript) == null ? void 0 : _a.hasAttribute("data-widget-id")) ? document.currentScript : null) || document.querySelector("script[data-widget-id]");
1007
+ var _a, _b;
1008
+ const script = (((_a = document.currentScript) == null ? void 0 : _a.hasAttribute("data-widget-id")) || ((_b = document.currentScript) == null ? void 0 : _b.hasAttribute("data-community-id")) ? document.currentScript : null) || document.querySelector("script[data-widget-id], script[data-community-id]");
666
1009
  if (!script) return null;
667
- const id = script.getAttribute("data-container") || `returning-ai-widget-${script.getAttribute("data-widget-id")}`;
1010
+ const id = script.getAttribute("data-container") || `returning-ai-widget-${script.getAttribute("data-community-id") || script.getAttribute("data-widget-id")}`;
668
1011
  return document.getElementById(id);
669
1012
  })();
670
1013
  const widget = container == null ? void 0 : container.querySelector(ALL_WIDGET_SELECTOR);
671
1014
  window.ReturningAIWidget = {
672
- version: "1.0.2",
1015
+ version: "1.1.0",
673
1016
  reload: () => (widget == null ? void 0 : widget.reload()) ?? Promise.resolve(),
674
1017
  logout: () => (widget == null ? void 0 : widget.logoutPublic()) ?? Promise.resolve(),
675
1018
  isAuthenticated: () => (widget == null ? void 0 : widget.isAuthenticated()) ?? false,