@xenia-martech/sdk-js 1.0.0-dev.27 → 1.0.0-dev.28
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/CSS_VARIABLES.md +7 -0
- package/README.md +50 -1
- package/dist/{actions-CkiSdvSa.d.cts → actions-CcBYV--3.d.cts} +177 -1
- package/dist/{actions-CkiSdvSa.d.ts → actions-CcBYV--3.d.ts} +177 -1
- package/dist/{chunk-UOW3GK4I.cjs → chunk-WYM4CQVG.cjs} +96 -4
- package/dist/chunk-WYM4CQVG.cjs.map +1 -0
- package/dist/{chunk-7D2HJMW6.js → chunk-XXNDHYAS.js} +96 -4
- package/dist/chunk-XXNDHYAS.js.map +1 -0
- package/dist/client/index.cjs +540 -27
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +112 -3
- package/dist/client/index.d.ts +112 -3
- package/dist/client/index.js +537 -28
- package/dist/client/index.js.map +1 -1
- package/dist/client/react/index.cjs +536 -27
- package/dist/client/react/index.cjs.map +1 -1
- package/dist/client/react/index.d.cts +2 -2
- package/dist/client/react/index.d.ts +2 -2
- package/dist/client/react/index.js +536 -27
- package/dist/client/react/index.js.map +1 -1
- package/dist/{cookie-names-C3LKDhsg.d.cts → cookie-names-B3b0XNc2.d.cts} +153 -1
- package/dist/{cookie-names-C3LKDhsg.d.ts → cookie-names-B3b0XNc2.d.ts} +153 -1
- package/dist/index.cjs +20 -20
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/server/index.cjs +15 -15
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +2 -2
- package/dist/xenia-sdk.iife.js +37 -3
- package/dist/xenia-sdk.iife.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7D2HJMW6.js.map +0 -1
- package/dist/chunk-UOW3GK4I.cjs.map +0 -1
package/dist/client/index.cjs
CHANGED
|
@@ -15,6 +15,7 @@ var MESSAGE_POSITIONS = /* @__PURE__ */ new Set([
|
|
|
15
15
|
"top",
|
|
16
16
|
"bottom"
|
|
17
17
|
]);
|
|
18
|
+
var WIDGET_TYPES = /* @__PURE__ */ new Set(["standard", "spin-wheel"]);
|
|
18
19
|
var ALL_TYPES = /* @__PURE__ */ new Set([
|
|
19
20
|
"swap",
|
|
20
21
|
"hide",
|
|
@@ -61,15 +62,67 @@ function toAction(raw) {
|
|
|
61
62
|
message: asString(raw.message),
|
|
62
63
|
imageUrl: asString(raw.imageUrl),
|
|
63
64
|
html: asString(raw.html),
|
|
64
|
-
|
|
65
|
+
// A link-only CTA (empty label, e.g. a spin-wheel's data-xenia-wheel-cta —
|
|
66
|
+
// label is meaningless there, the winHtml/lossHtml design supplies its own button text)
|
|
67
|
+
// must still parse through — gating on `cta.label` alone silently dropped it.
|
|
68
|
+
cta: cta && (cta.label || cta.url) ? cta : void 0,
|
|
65
69
|
dismissAfterMs: asNumber(raw.dismissAfterMs),
|
|
66
|
-
position: asMessagePosition(raw.position)
|
|
70
|
+
position: asMessagePosition(raw.position),
|
|
71
|
+
widgetType: asWidgetType(raw.widgetType),
|
|
72
|
+
segments: asSegments(raw.segments),
|
|
73
|
+
formSubmit: asFormSubmit(raw.formSubmit),
|
|
74
|
+
style: asWidgetStyle(raw.style),
|
|
75
|
+
spinnerHtml: asString(raw.spinnerHtml),
|
|
76
|
+
winHtml: asString(raw.winHtml),
|
|
77
|
+
lossHtml: asString(raw.lossHtml)
|
|
67
78
|
};
|
|
68
79
|
}
|
|
69
80
|
function asMessagePosition(v) {
|
|
70
81
|
const s = asString(v);
|
|
71
82
|
return s && MESSAGE_POSITIONS.has(s) ? s : void 0;
|
|
72
83
|
}
|
|
84
|
+
function asWidgetType(v) {
|
|
85
|
+
const s = asString(v);
|
|
86
|
+
return s && WIDGET_TYPES.has(s) ? s : void 0;
|
|
87
|
+
}
|
|
88
|
+
function asSegments(v) {
|
|
89
|
+
if (!Array.isArray(v)) return void 0;
|
|
90
|
+
const segments = [];
|
|
91
|
+
for (const entry of v) {
|
|
92
|
+
if (!isObject(entry)) continue;
|
|
93
|
+
const id = asString(entry.id);
|
|
94
|
+
const label = asString(entry.label);
|
|
95
|
+
if (!id || !label) continue;
|
|
96
|
+
segments.push({
|
|
97
|
+
id,
|
|
98
|
+
label,
|
|
99
|
+
colorHex: asString(entry.colorHex),
|
|
100
|
+
isLoss: entry.isLoss === true
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return segments.length ? segments : void 0;
|
|
104
|
+
}
|
|
105
|
+
function asFormSubmit(v) {
|
|
106
|
+
if (!isObject(v)) return void 0;
|
|
107
|
+
const mode = asString(v.mode);
|
|
108
|
+
if (!mode) return void 0;
|
|
109
|
+
return {
|
|
110
|
+
requiresResolve: v.requiresResolve === true,
|
|
111
|
+
mode,
|
|
112
|
+
flowRef: asString(v.flowRef),
|
|
113
|
+
bindingRef: asString(v.bindingRef),
|
|
114
|
+
successDisplayMs: asNumber(v.successDisplayMs)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function asWidgetStyle(v) {
|
|
118
|
+
if (!isObject(v)) return void 0;
|
|
119
|
+
const style = {
|
|
120
|
+
hideCloseButton: v.hideCloseButton === true,
|
|
121
|
+
hideOverlay: v.hideOverlay === true
|
|
122
|
+
};
|
|
123
|
+
const hasAny = style.hideCloseButton || style.hideOverlay;
|
|
124
|
+
return hasAny ? style : void 0;
|
|
125
|
+
}
|
|
73
126
|
function parseActions(body) {
|
|
74
127
|
let list;
|
|
75
128
|
if (Array.isArray(body)) {
|
|
@@ -252,6 +305,12 @@ var TokenProvider = class {
|
|
|
252
305
|
};
|
|
253
306
|
|
|
254
307
|
// src/core/api.ts
|
|
308
|
+
function isJsonObject(v) {
|
|
309
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
310
|
+
}
|
|
311
|
+
function asWidgetPrize(v) {
|
|
312
|
+
return v !== void 0 && isJsonObject(v) ? v : void 0;
|
|
313
|
+
}
|
|
255
314
|
function compact(obj) {
|
|
256
315
|
const out = {};
|
|
257
316
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -321,6 +380,39 @@ var XeniaApi = class {
|
|
|
321
380
|
receivedAt: Date.now()
|
|
322
381
|
};
|
|
323
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Widget form submission — `POST /widget-submissions?ref={bindingRef}`. Used whenever the
|
|
385
|
+
* action's `formSubmit.requiresResolve` is true (spin-wheel, which must have a
|
|
386
|
+
* server-resolved outcome before anything renders) or `mode === "endpoint"` — the common
|
|
387
|
+
* plain-popup case (`mode === "journey" && !requiresResolve`) bypasses this entirely and calls
|
|
388
|
+
* {@link webhook} against `flowRef` instead (see `XeniaClient.submitWidgetForm`'s endpoint
|
|
389
|
+
* selection).
|
|
390
|
+
*
|
|
391
|
+
* Not retried (`noRetry: true`), styled like {@link webhook} — a retried POST must never
|
|
392
|
+
* double-fire a journey or double-resolve a prize server-side.
|
|
393
|
+
*/
|
|
394
|
+
async submitWidget(ref, body) {
|
|
395
|
+
const { response, data: rawBody } = await this.http.request({
|
|
396
|
+
method: "POST",
|
|
397
|
+
path: "/widget-submissions",
|
|
398
|
+
query: { ref },
|
|
399
|
+
headers: await this.authHeaders(),
|
|
400
|
+
body: body ?? {},
|
|
401
|
+
noRetry: true
|
|
402
|
+
});
|
|
403
|
+
const obj = isJsonObject(rawBody) ? rawBody : {};
|
|
404
|
+
return {
|
|
405
|
+
status: response.status,
|
|
406
|
+
body: rawBody,
|
|
407
|
+
actions: parseActions(rawBody),
|
|
408
|
+
prize: asWidgetPrize(obj.prize),
|
|
409
|
+
alreadyPlayed: obj.alreadyPlayed === true,
|
|
410
|
+
dispatched: obj.dispatched === true,
|
|
411
|
+
dispatchError: typeof obj.dispatchError === "string" ? obj.dispatchError : void 0,
|
|
412
|
+
widgetType: typeof obj.widgetType === "string" ? obj.widgetType : void 0,
|
|
413
|
+
receivedAt: Date.now()
|
|
414
|
+
};
|
|
415
|
+
}
|
|
324
416
|
/**
|
|
325
417
|
* Unified event batch (max 100/request per the backend) — `POST /api/events/v1`.
|
|
326
418
|
* Records analytics for every event and matches/advances a journey for any
|
|
@@ -1088,6 +1180,316 @@ function resolveCookieConfig(opt) {
|
|
|
1088
1180
|
return { sameSite, secure, maxAgeDays: o.maxAgeDays ?? 365, domain: o.domain };
|
|
1089
1181
|
}
|
|
1090
1182
|
|
|
1183
|
+
// src/client/widgets/form.ts
|
|
1184
|
+
function collectFields(root) {
|
|
1185
|
+
const fields = {};
|
|
1186
|
+
root.querySelectorAll("[data-xenia-field]").forEach((el) => {
|
|
1187
|
+
const name = el.getAttribute("data-xenia-field");
|
|
1188
|
+
if (!name) return;
|
|
1189
|
+
fields[name] = fieldValue(el);
|
|
1190
|
+
});
|
|
1191
|
+
return fields;
|
|
1192
|
+
}
|
|
1193
|
+
function fieldValue(el) {
|
|
1194
|
+
if (el instanceof HTMLInputElement) {
|
|
1195
|
+
if (el.type === "checkbox") return el.checked ? el.value || "on" : "";
|
|
1196
|
+
if (el.type === "radio") return el.checked ? el.value : "";
|
|
1197
|
+
return el.value;
|
|
1198
|
+
}
|
|
1199
|
+
if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return el.value;
|
|
1200
|
+
return el.textContent?.trim() ?? "";
|
|
1201
|
+
}
|
|
1202
|
+
function setDisabled(el, disabled) {
|
|
1203
|
+
if (!el) return;
|
|
1204
|
+
if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLFieldSetElement) {
|
|
1205
|
+
el.disabled = disabled;
|
|
1206
|
+
} else if (disabled) {
|
|
1207
|
+
el.setAttribute("aria-disabled", "true");
|
|
1208
|
+
} else {
|
|
1209
|
+
el.removeAttribute("aria-disabled");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function showError(root, message) {
|
|
1213
|
+
const el = root.querySelector("[data-xenia-error]");
|
|
1214
|
+
if (el) el.textContent = message;
|
|
1215
|
+
}
|
|
1216
|
+
var SUBMIT_STYLE_ID = "xenia-form-submit-style";
|
|
1217
|
+
function ensureSubmitStyles(root) {
|
|
1218
|
+
if (root.querySelector(`#${SUBMIT_STYLE_ID}`)) return;
|
|
1219
|
+
const style = document.createElement("style");
|
|
1220
|
+
style.id = SUBMIT_STYLE_ID;
|
|
1221
|
+
style.textContent = "@keyframes xenia-submit-spin{to{transform:rotate(360deg)}}[data-xenia-submitting]::after{content:'';display:inline-block;width:0.85em;height:0.85em;margin-left:0.5em;vertical-align:-0.15em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:xenia-submit-spin 0.6s linear infinite}";
|
|
1222
|
+
root.prepend(style);
|
|
1223
|
+
}
|
|
1224
|
+
function setSubmitting(el, submitting) {
|
|
1225
|
+
if (!el) return;
|
|
1226
|
+
if (submitting) el.setAttribute("data-xenia-submitting", "");
|
|
1227
|
+
else el.removeAttribute("data-xenia-submitting");
|
|
1228
|
+
}
|
|
1229
|
+
function fadeOutAndDismiss(root, dismiss) {
|
|
1230
|
+
if (!dismiss) return;
|
|
1231
|
+
const host = root instanceof ShadowRoot ? root.host : root;
|
|
1232
|
+
if (host instanceof HTMLElement) {
|
|
1233
|
+
host.style.transition = "opacity 220ms ease, transform 220ms ease";
|
|
1234
|
+
host.style.opacity = "0";
|
|
1235
|
+
host.style.transform = "scale(0.96)";
|
|
1236
|
+
window.setTimeout(dismiss, 220);
|
|
1237
|
+
} else {
|
|
1238
|
+
dismiss();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
var DEFAULT_SUCCESS_DISPLAY_MS = 900;
|
|
1242
|
+
function revealSuccess(root, form) {
|
|
1243
|
+
const successEl = root.querySelector("[data-xenia-success]");
|
|
1244
|
+
if (!successEl) return false;
|
|
1245
|
+
if (successEl instanceof HTMLElement) successEl.hidden = false;
|
|
1246
|
+
else successEl.removeAttribute("hidden");
|
|
1247
|
+
if (form) {
|
|
1248
|
+
form.hidden = true;
|
|
1249
|
+
} else {
|
|
1250
|
+
root.querySelectorAll("[data-xenia-field], [data-xenia-submit]").forEach((el) => {
|
|
1251
|
+
el.hidden = true;
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
function wireFormSubmit(root, action, ctx) {
|
|
1257
|
+
const form = root.querySelector("form");
|
|
1258
|
+
let submitting = false;
|
|
1259
|
+
ensureSubmitStyles(root);
|
|
1260
|
+
const run = (control, fields) => {
|
|
1261
|
+
if (submitting) return;
|
|
1262
|
+
if (!action.formSubmit) {
|
|
1263
|
+
showError(root, "This form isn't connected yet.");
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
submitting = true;
|
|
1267
|
+
showError(root, "");
|
|
1268
|
+
setDisabled(control, true);
|
|
1269
|
+
setSubmitting(control, true);
|
|
1270
|
+
const startedAt = Date.now();
|
|
1271
|
+
const minDisplayMs = action.formSubmit.successDisplayMs ?? DEFAULT_SUCCESS_DISPLAY_MS;
|
|
1272
|
+
ctx.submitWidgetForm(fields).then(() => {
|
|
1273
|
+
setSubmitting(control, false);
|
|
1274
|
+
revealSuccess(root, form);
|
|
1275
|
+
const wait = Math.max(0, minDisplayMs - (Date.now() - startedAt));
|
|
1276
|
+
window.setTimeout(() => fadeOutAndDismiss(root, ctx.dismiss), wait);
|
|
1277
|
+
}).catch(() => {
|
|
1278
|
+
setSubmitting(control, false);
|
|
1279
|
+
setDisabled(control, false);
|
|
1280
|
+
showError(root, "Something went wrong. Please try again.");
|
|
1281
|
+
}).finally(() => {
|
|
1282
|
+
submitting = false;
|
|
1283
|
+
});
|
|
1284
|
+
};
|
|
1285
|
+
if (form) {
|
|
1286
|
+
form.addEventListener("submit", (ev) => {
|
|
1287
|
+
ev.preventDefault();
|
|
1288
|
+
const formFields = {};
|
|
1289
|
+
new FormData(form).forEach((value, key) => {
|
|
1290
|
+
if (typeof value === "string") formFields[key] = value;
|
|
1291
|
+
});
|
|
1292
|
+
const submitter = ev.submitter ?? form.querySelector('[type="submit"], button:not([type])');
|
|
1293
|
+
run(submitter, { ...collectFields(root), ...formFields });
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
root.querySelectorAll("[data-xenia-submit]").forEach((el) => {
|
|
1297
|
+
if (form?.contains(el)) return;
|
|
1298
|
+
el.addEventListener("click", () => run(el, collectFields(root)));
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/client/widgets/types.ts
|
|
1303
|
+
function escapeHtml(s) {
|
|
1304
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1305
|
+
}
|
|
1306
|
+
function escapeAttr(s) {
|
|
1307
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
1308
|
+
}
|
|
1309
|
+
function wireCloseAffordances(root, dismiss) {
|
|
1310
|
+
if (dismiss) {
|
|
1311
|
+
const closeEls = root.querySelectorAll(
|
|
1312
|
+
".xenia-close, [data-xenia-close], [class*='close' i], [id*='close' i], [class*='backdrop' i], [id*='backdrop' i]"
|
|
1313
|
+
);
|
|
1314
|
+
closeEls.forEach((el) => {
|
|
1315
|
+
el.removeAttribute("onclick");
|
|
1316
|
+
el.addEventListener("click", dismiss);
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
root.querySelectorAll("[class*='backdrop' i], [id*='backdrop' i]").forEach((el) => {
|
|
1320
|
+
if (el instanceof HTMLElement && el.children.length === 0 && !el.textContent?.trim()) {
|
|
1321
|
+
el.style.opacity = "0.5";
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/client/widgets/spin-wheel.ts
|
|
1327
|
+
var WHEEL_SIZE = 260;
|
|
1328
|
+
var RADIUS = WHEEL_SIZE / 2;
|
|
1329
|
+
var CENTER = WHEEL_SIZE / 2;
|
|
1330
|
+
var DEFAULT_COLORS = ["#7c3aed", "#2563eb", "#059669", "#d97706", "#dc2626", "#0891b2"];
|
|
1331
|
+
var SPIN_TRANSITION = "transform 3.2s cubic-bezier(0.15, 0.85, 0.25, 1)";
|
|
1332
|
+
var WHEEL_POINTER_HTML = '<div class="xenia-wheel-pointer" aria-hidden="true"></div>';
|
|
1333
|
+
var REVEAL_TRANSITION_MS = 320;
|
|
1334
|
+
var REVEAL_TRANSITION = `opacity ${REVEAL_TRANSITION_MS}ms ease, transform ${REVEAL_TRANSITION_MS}ms ease`;
|
|
1335
|
+
var REVEAL_PERSPECTIVE_PX = 900;
|
|
1336
|
+
var revealTransform = (scale, depthPx) => `perspective(${REVEAL_PERSPECTIVE_PX}px) translateZ(${depthPx}px) scale(${scale})`;
|
|
1337
|
+
function polarPoint(angleDeg, radius) {
|
|
1338
|
+
const rad = angleDeg * Math.PI / 180;
|
|
1339
|
+
return { x: CENTER + radius * Math.sin(rad), y: CENTER - radius * Math.cos(rad) };
|
|
1340
|
+
}
|
|
1341
|
+
function segmentPath(startAngle, endAngle) {
|
|
1342
|
+
const start = polarPoint(startAngle, RADIUS);
|
|
1343
|
+
const end = polarPoint(endAngle, RADIUS);
|
|
1344
|
+
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
|
1345
|
+
return `M ${CENTER},${CENTER} L ${start.x},${start.y} A ${RADIUS},${RADIUS} 0 ${largeArc} 1 ${end.x},${end.y} Z`;
|
|
1346
|
+
}
|
|
1347
|
+
function computeSpinRotation(segmentIndex, segmentCount, extraSpins = 0) {
|
|
1348
|
+
if (segmentCount <= 0) return 0;
|
|
1349
|
+
const segmentAngle = 360 / segmentCount;
|
|
1350
|
+
const centerAngle = segmentIndex * segmentAngle + segmentAngle / 2;
|
|
1351
|
+
const landingAngle = (360 - centerAngle) % 360;
|
|
1352
|
+
return extraSpins * 360 + landingAngle;
|
|
1353
|
+
}
|
|
1354
|
+
function crossFadeReveal(outgoing, incoming) {
|
|
1355
|
+
const outContent = outgoing ? Array.from(outgoing.children) : [];
|
|
1356
|
+
const inContent = incoming ? Array.from(incoming.children) : [];
|
|
1357
|
+
if (incoming) incoming.hidden = false;
|
|
1358
|
+
inContent.forEach((el) => {
|
|
1359
|
+
if (!(el instanceof HTMLElement)) return;
|
|
1360
|
+
el.style.transition = "none";
|
|
1361
|
+
el.style.opacity = "0";
|
|
1362
|
+
el.style.transform = revealTransform(0.94, -60);
|
|
1363
|
+
});
|
|
1364
|
+
nextFrame(() => {
|
|
1365
|
+
outContent.forEach((el) => {
|
|
1366
|
+
if (!(el instanceof HTMLElement)) return;
|
|
1367
|
+
el.style.transition = REVEAL_TRANSITION;
|
|
1368
|
+
el.style.opacity = "0";
|
|
1369
|
+
el.style.transform = revealTransform(0.94, -60);
|
|
1370
|
+
});
|
|
1371
|
+
inContent.forEach((el) => {
|
|
1372
|
+
if (!(el instanceof HTMLElement)) return;
|
|
1373
|
+
el.style.transition = REVEAL_TRANSITION;
|
|
1374
|
+
el.style.opacity = "1";
|
|
1375
|
+
el.style.transform = revealTransform(1, 0);
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
if (outgoing) {
|
|
1379
|
+
setTimeout(() => {
|
|
1380
|
+
outgoing.hidden = true;
|
|
1381
|
+
}, REVEAL_TRANSITION_MS);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
function nextFrame(fn) {
|
|
1385
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(fn);
|
|
1386
|
+
else setTimeout(fn, 0);
|
|
1387
|
+
}
|
|
1388
|
+
function wheelSvg(segments) {
|
|
1389
|
+
const n = segments.length || 1;
|
|
1390
|
+
const segmentAngle = 360 / n;
|
|
1391
|
+
const slices = segments.map((seg, i) => {
|
|
1392
|
+
const start = i * segmentAngle;
|
|
1393
|
+
const end = start + segmentAngle;
|
|
1394
|
+
const color = seg.colorHex || DEFAULT_COLORS[i % DEFAULT_COLORS.length];
|
|
1395
|
+
const labelAngle = start + segmentAngle / 2;
|
|
1396
|
+
const labelPoint = polarPoint(labelAngle, RADIUS * 0.62);
|
|
1397
|
+
return `<path d="${segmentPath(start, end)}" fill="${escapeAttr(color)}" stroke="#fff" stroke-width="1"></path><text x="${labelPoint.x}" y="${labelPoint.y}" fill="#fff" font-size="12" text-anchor="middle" transform="rotate(${labelAngle} ${labelPoint.x} ${labelPoint.y})">${escapeHtml(seg.label)}</text>`;
|
|
1398
|
+
}).join("");
|
|
1399
|
+
return `<svg class="xenia-wheel-svg" viewBox="0 0 ${WHEEL_SIZE} ${WHEEL_SIZE}" width="${WHEEL_SIZE}" height="${WHEEL_SIZE}"><g class="xenia-wheel-dial">${slices}</g><circle class="xenia-wheel-hub" cx="${CENTER}" cy="${CENTER}" r="6"></circle></svg>`;
|
|
1400
|
+
}
|
|
1401
|
+
var spinWheelRenderer = {
|
|
1402
|
+
html(action) {
|
|
1403
|
+
return `<div class="xenia-spin-wheel"><div data-xenia-wheel-face>${action.spinnerHtml ?? ""}</div><div class="xenia-widget-result" data-xenia-wheel-result hidden>${action.winHtml ?? ""}</div><div class="xenia-widget-result" data-xenia-wheel-loss hidden>${action.lossHtml ?? ""}</div></div>`;
|
|
1404
|
+
},
|
|
1405
|
+
wire(root, action, ctx) {
|
|
1406
|
+
wireCloseAffordances(root, ctx.dismiss);
|
|
1407
|
+
const segments = action.segments ?? [];
|
|
1408
|
+
const wheelFace = root.querySelector("[data-xenia-wheel-face]");
|
|
1409
|
+
const spinBtn = root.querySelector("[data-xenia-spin]");
|
|
1410
|
+
const winEl = root.querySelector("[data-xenia-wheel-result]");
|
|
1411
|
+
const lossEl = root.querySelector("[data-xenia-wheel-loss]");
|
|
1412
|
+
const errorEl = root.querySelector("[data-xenia-error]");
|
|
1413
|
+
const slotEl = root.querySelector("[data-xenia-wheel-slot]");
|
|
1414
|
+
if (slotEl) slotEl.insertAdjacentHTML("afterbegin", wheelSvg(segments) + WHEEL_POINTER_HTML);
|
|
1415
|
+
const dial = root.querySelector(".xenia-wheel-dial");
|
|
1416
|
+
if (!spinBtn || !action.formSubmit) return;
|
|
1417
|
+
root.querySelectorAll("[data-xenia-wheel-cta]").forEach((btn) => {
|
|
1418
|
+
btn.addEventListener("click", () => ctx.fireCta());
|
|
1419
|
+
});
|
|
1420
|
+
const revealResult = (prizeLabel, isLoss, canRetry) => {
|
|
1421
|
+
root.querySelectorAll("[data-xenia-wheel-prize]").forEach((el) => {
|
|
1422
|
+
el.textContent = prizeLabel;
|
|
1423
|
+
});
|
|
1424
|
+
const shown = isLoss ? lossEl : winEl;
|
|
1425
|
+
const other = isLoss ? winEl : lossEl;
|
|
1426
|
+
if (other) other.hidden = true;
|
|
1427
|
+
crossFadeReveal(wheelFace, shown);
|
|
1428
|
+
if (!canRetry) {
|
|
1429
|
+
playsExhausted = true;
|
|
1430
|
+
root.querySelectorAll("[data-xenia-spin-again]").forEach((btn) => {
|
|
1431
|
+
btn.hidden = true;
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
let spinning = false;
|
|
1436
|
+
let playsExhausted = false;
|
|
1437
|
+
root.querySelectorAll("[data-xenia-spin-again]").forEach((btn) => {
|
|
1438
|
+
btn.addEventListener("click", () => {
|
|
1439
|
+
if (playsExhausted) return;
|
|
1440
|
+
const shown = winEl && !winEl.hidden ? winEl : lossEl && !lossEl.hidden ? lossEl : null;
|
|
1441
|
+
if (!shown) return;
|
|
1442
|
+
spinning = false;
|
|
1443
|
+
spinBtn.disabled = false;
|
|
1444
|
+
if (errorEl) errorEl.textContent = "";
|
|
1445
|
+
crossFadeReveal(shown, wheelFace);
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
spinBtn.addEventListener("click", () => {
|
|
1449
|
+
if (spinning) return;
|
|
1450
|
+
spinning = true;
|
|
1451
|
+
spinBtn.disabled = true;
|
|
1452
|
+
if (errorEl) errorEl.textContent = "";
|
|
1453
|
+
const fields = collectFields(root);
|
|
1454
|
+
ctx.submitWidgetForm(fields).then((response) => {
|
|
1455
|
+
const index = segments.findIndex((s) => s.id === response.prize?.segmentId);
|
|
1456
|
+
const prizeLabel = response.prize?.label ?? segments[index]?.label ?? "";
|
|
1457
|
+
const isLoss = index >= 0 && !!segments[index]?.isLoss;
|
|
1458
|
+
const canRetry = response.alreadyPlayed !== true;
|
|
1459
|
+
if (index < 0 || !dial) {
|
|
1460
|
+
revealResult(prizeLabel, isLoss, canRetry);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const extraSpins = 3 + Math.floor(Math.random() * 3);
|
|
1464
|
+
const rotation = computeSpinRotation(index, segments.length || 1, extraSpins);
|
|
1465
|
+
const priorMatch = /rotate\(([-\d.]+)deg\)/.exec(dial.style.transform);
|
|
1466
|
+
const priorRotation = priorMatch ? parseFloat(priorMatch[1]) : 0;
|
|
1467
|
+
dial.style.transition = "none";
|
|
1468
|
+
dial.style.transform = `rotate(${priorRotation % 360}deg)`;
|
|
1469
|
+
void dial.getBoundingClientRect();
|
|
1470
|
+
dial.style.transition = SPIN_TRANSITION;
|
|
1471
|
+
const onTransitionEnd = () => {
|
|
1472
|
+
dial.removeEventListener("transitionend", onTransitionEnd);
|
|
1473
|
+
revealResult(prizeLabel, isLoss, canRetry);
|
|
1474
|
+
};
|
|
1475
|
+
dial.addEventListener("transitionend", onTransitionEnd);
|
|
1476
|
+
nextFrame(() => {
|
|
1477
|
+
dial.style.transform = `rotate(${rotation}deg)`;
|
|
1478
|
+
});
|
|
1479
|
+
}).catch(() => {
|
|
1480
|
+
spinning = false;
|
|
1481
|
+
spinBtn.disabled = false;
|
|
1482
|
+
if (errorEl) errorEl.textContent = "Something went wrong. Please try again.";
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
// src/client/widgets/index.ts
|
|
1489
|
+
var WIDGET_RENDERERS = {
|
|
1490
|
+
"spin-wheel": spinWheelRenderer
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1091
1493
|
// src/client/renderer.ts
|
|
1092
1494
|
function resolveTarget(target, root = document) {
|
|
1093
1495
|
if (!target) return null;
|
|
@@ -1145,6 +1547,7 @@ var ActionRenderer = class {
|
|
|
1145
1547
|
messageTemplate;
|
|
1146
1548
|
messageStyles;
|
|
1147
1549
|
messageComponent;
|
|
1550
|
+
submitWidgetFormFn;
|
|
1148
1551
|
/** Unmount callbacks for currently-mounted framework components, keyed by their container. */
|
|
1149
1552
|
mounted = /* @__PURE__ */ new Map();
|
|
1150
1553
|
constructor(onCta, opts) {
|
|
@@ -1152,6 +1555,7 @@ var ActionRenderer = class {
|
|
|
1152
1555
|
this.messageTemplate = opts?.messageTemplate;
|
|
1153
1556
|
this.messageStyles = opts?.messageStyles;
|
|
1154
1557
|
this.messageComponent = opts?.messageComponent;
|
|
1558
|
+
this.submitWidgetFormFn = opts?.submitWidgetForm;
|
|
1155
1559
|
}
|
|
1156
1560
|
/** Apply a list of actions. Returns the count successfully applied. */
|
|
1157
1561
|
apply(actions) {
|
|
@@ -1234,7 +1638,8 @@ ${this.messageStyles}` : STYLES;
|
|
|
1234
1638
|
const wrap = document.createElement("div");
|
|
1235
1639
|
const posClass = this.positionClass(action.type, action.position);
|
|
1236
1640
|
const customBgClass = action.html !== void 0 ? " xenia-custom-bg" : "";
|
|
1237
|
-
|
|
1641
|
+
const noOverlayClass = action.style?.hideOverlay ? " xenia-no-overlay" : "";
|
|
1642
|
+
wrap.className = `xenia-msg xenia-${action.type}${posClass ? ` ${posClass}` : ""}${customBgClass}${noOverlayClass}`;
|
|
1238
1643
|
shadow.appendChild(wrap);
|
|
1239
1644
|
const dismiss = () => {
|
|
1240
1645
|
this.unmount(wrap);
|
|
@@ -1247,7 +1652,10 @@ ${this.messageStyles}` : STYLES;
|
|
|
1247
1652
|
this.wireCard(wrap, action, dismiss);
|
|
1248
1653
|
}
|
|
1249
1654
|
if (action.dismissAfterMs && action.dismissAfterMs > 0) {
|
|
1250
|
-
setTimeout(dismiss, action.dismissAfterMs);
|
|
1655
|
+
const timer = setTimeout(dismiss, action.dismissAfterMs);
|
|
1656
|
+
const cancelAutoDismiss = () => clearTimeout(timer);
|
|
1657
|
+
wrap.addEventListener("click", cancelAutoDismiss, { once: true });
|
|
1658
|
+
wrap.addEventListener("focusin", cancelAutoDismiss, { once: true });
|
|
1251
1659
|
}
|
|
1252
1660
|
return true;
|
|
1253
1661
|
}
|
|
@@ -1291,38 +1699,32 @@ ${this.messageStyles}` : STYLES;
|
|
|
1291
1699
|
return "";
|
|
1292
1700
|
}
|
|
1293
1701
|
cardHtml(action, variant) {
|
|
1702
|
+
const widget = action.widgetType ? WIDGET_RENDERERS[action.widgetType] : void 0;
|
|
1703
|
+
if (widget) return widget.html(action, variant);
|
|
1294
1704
|
if (action.html !== void 0) return wrapCustomHtml(action.html, variant);
|
|
1295
1705
|
if (this.messageTemplate) {
|
|
1296
1706
|
const ctx = {
|
|
1297
1707
|
action,
|
|
1298
1708
|
variant,
|
|
1299
|
-
escapeHtml,
|
|
1300
|
-
escapeAttr
|
|
1709
|
+
escapeHtml: escapeHtml2,
|
|
1710
|
+
escapeAttr: escapeAttr2
|
|
1301
1711
|
};
|
|
1302
1712
|
return this.messageTemplate(ctx);
|
|
1303
1713
|
}
|
|
1304
|
-
const img = action.imageUrl ? `<img class="xenia-img" src="${
|
|
1305
|
-
const title = action.title ? `<div class="xenia-title">${
|
|
1306
|
-
const msg = action.message ? `<div class="xenia-body">${
|
|
1307
|
-
const cta = action.cta ? `<button class="xenia-cta" type="button">${
|
|
1714
|
+
const img = action.imageUrl ? `<img class="xenia-img" src="${escapeAttr2(action.imageUrl)}" alt="" />` : "";
|
|
1715
|
+
const title = action.title ? `<div class="xenia-title">${escapeHtml2(action.title)}</div>` : "";
|
|
1716
|
+
const msg = action.message ? `<div class="xenia-body">${escapeHtml2(action.message)}</div>` : "";
|
|
1717
|
+
const cta = action.cta ? `<button class="xenia-cta" type="button">${escapeHtml2(action.cta.label)}</button>` : "";
|
|
1308
1718
|
const close = variant === "inline" ? "" : `<button class="xenia-close" type="button" aria-label="Close">\xD7</button>`;
|
|
1309
1719
|
return `<div class="xenia-card">${close}${img}<div class="xenia-content">${title}${msg}${cta}</div></div>`;
|
|
1310
1720
|
}
|
|
1311
1721
|
wireCard(root, action, dismiss) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
closeEls.forEach((el) => {
|
|
1317
|
-
el.removeAttribute("onclick");
|
|
1318
|
-
el.addEventListener("click", dismiss);
|
|
1319
|
-
});
|
|
1722
|
+
const widget = action.widgetType ? WIDGET_RENDERERS[action.widgetType] : void 0;
|
|
1723
|
+
if (widget) {
|
|
1724
|
+
widget.wire(root, action, this.buildWidgetCtx(action, dismiss));
|
|
1725
|
+
return;
|
|
1320
1726
|
}
|
|
1321
|
-
root
|
|
1322
|
-
if (el instanceof HTMLElement && el.children.length === 0 && !el.textContent?.trim()) {
|
|
1323
|
-
el.style.opacity = "0.5";
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1727
|
+
wireCloseAffordances(root, dismiss);
|
|
1326
1728
|
const ctaBtn = root.querySelector(".xenia-cta, [data-xenia-cta]");
|
|
1327
1729
|
if (ctaBtn && action.cta) {
|
|
1328
1730
|
ctaBtn.addEventListener("click", () => {
|
|
@@ -1331,6 +1733,29 @@ ${this.messageStyles}` : STYLES;
|
|
|
1331
1733
|
dismiss?.();
|
|
1332
1734
|
});
|
|
1333
1735
|
}
|
|
1736
|
+
wireFormSubmit(root, action, this.buildWidgetCtx(action, dismiss));
|
|
1737
|
+
}
|
|
1738
|
+
/** Build the imperative hooks (`dismiss`/`fireCta`/`submitWidgetForm`) handed to a widget's `wire()`. */
|
|
1739
|
+
buildWidgetCtx(action, dismiss) {
|
|
1740
|
+
const idempotencyKey = generateUlid();
|
|
1741
|
+
return {
|
|
1742
|
+
dismiss,
|
|
1743
|
+
fireCta: () => {
|
|
1744
|
+
if (action.cta?.event) this.onCta?.(action.cta.event, action.cta);
|
|
1745
|
+
if (action.cta?.url) window.location.assign(action.cta.url);
|
|
1746
|
+
dismiss?.();
|
|
1747
|
+
},
|
|
1748
|
+
submitWidgetForm: (fields) => {
|
|
1749
|
+
if (!action.formSubmit || !this.submitWidgetFormFn) {
|
|
1750
|
+
return Promise.reject(
|
|
1751
|
+
new Error(
|
|
1752
|
+
"xenia: this action has no formSubmit config, or no submitWidgetForm handler is wired (construct the ActionRenderer via XeniaClient to get one)"
|
|
1753
|
+
)
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
return this.submitWidgetFormFn(action.formSubmit, fields, { idempotencyKey });
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1334
1759
|
}
|
|
1335
1760
|
};
|
|
1336
1761
|
function wrapCustomHtml(html, variant) {
|
|
@@ -1339,11 +1764,11 @@ function wrapCustomHtml(html, variant) {
|
|
|
1339
1764
|
}
|
|
1340
1765
|
return `<div class="xenia-custom-html"><style>.xenia-custom-html{position:relative}.xenia-custom-html>*{position:static!important;inset:auto!important;top:auto!important;right:auto!important;bottom:auto!important;left:auto!important}</style>${html}</div>`;
|
|
1341
1766
|
}
|
|
1342
|
-
function
|
|
1767
|
+
function escapeHtml2(s) {
|
|
1343
1768
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1344
1769
|
}
|
|
1345
|
-
function
|
|
1346
|
-
return
|
|
1770
|
+
function escapeAttr2(s) {
|
|
1771
|
+
return escapeHtml2(s).replace(/"/g, """);
|
|
1347
1772
|
}
|
|
1348
1773
|
var STYLES = `
|
|
1349
1774
|
:host { all: initial; }
|
|
@@ -1360,12 +1785,14 @@ var STYLES = `
|
|
|
1360
1785
|
.xenia-modal { inset: 0; display: flex; align-items: center; justify-content: center;
|
|
1361
1786
|
background: var(--xenia-modal-backdrop, rgba(0,0,0,.12)); }
|
|
1362
1787
|
.xenia-modal.xenia-custom-bg { background: transparent; }
|
|
1788
|
+
.xenia-modal.xenia-no-overlay { background: transparent; }
|
|
1363
1789
|
.xenia-card { position: relative; background: var(--xenia-card-bg, #fff); color: var(--xenia-card-color, #111);
|
|
1364
1790
|
border-radius: var(--xenia-card-radius, 12px); box-shadow: var(--xenia-card-shadow, 0 10px 40px rgba(0,0,0,.18));
|
|
1365
1791
|
padding: var(--xenia-card-padding, 16px 18px); display: flex; gap: var(--xenia-card-gap, 12px);
|
|
1366
1792
|
align-items: center; }
|
|
1367
1793
|
.xenia-banner .xenia-card { border-radius: var(--xenia-banner-card-radius, 0); justify-content: center; }
|
|
1368
1794
|
.xenia-modal .xenia-card { max-width: var(--xenia-modal-max-width, 440px); width: calc(100% - 32px); }
|
|
1795
|
+
.xenia-card.xenia-widget { flex-direction: column; align-items: center; text-align: center; }
|
|
1369
1796
|
.xenia-img { width: var(--xenia-img-size, 56px); height: var(--xenia-img-size, 56px); object-fit: cover;
|
|
1370
1797
|
border-radius: var(--xenia-img-radius, 8px); }
|
|
1371
1798
|
.xenia-title { font-weight: var(--xenia-title-weight, 600); font-size: var(--xenia-title-size, 15px);
|
|
@@ -1377,6 +1804,38 @@ var STYLES = `
|
|
|
1377
1804
|
font-size: var(--xenia-cta-size, 14px); cursor: pointer; }
|
|
1378
1805
|
.xenia-close { position: absolute; top: 6px; right: 8px; background: none; border: 0;
|
|
1379
1806
|
font-size: var(--xenia-close-size, 20px); line-height: 1; cursor: pointer; color: var(--xenia-close-color, #999); }
|
|
1807
|
+
.xenia-wheel-dial { transform-box: fill-box; transform-origin: center; }
|
|
1808
|
+
.xenia-wheel-hub { fill: var(--xenia-wheel-pointer-color, #111); }
|
|
1809
|
+
.xenia-wheel-wrap { position: relative; display: inline-block; }
|
|
1810
|
+
.xenia-wheel-pointer { position: absolute; top: -6px; left: 50%; transform: translateX(-50%);
|
|
1811
|
+
width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent;
|
|
1812
|
+
border-top: 14px solid var(--xenia-wheel-pointer-color, #111); pointer-events: none; }
|
|
1813
|
+
.xenia-wheel-spin { margin-top: 10px; background: var(--xenia-cta-bg, #111); color: var(--xenia-cta-color, #fff);
|
|
1814
|
+
border: 0; border-radius: var(--xenia-cta-radius, 8px); padding: var(--xenia-cta-padding, 8px 14px);
|
|
1815
|
+
font-size: var(--xenia-cta-size, 14px); font-family: inherit; cursor: pointer; }
|
|
1816
|
+
.xenia-wheel-spin:disabled { opacity: 0.6; cursor: default; }
|
|
1817
|
+
/* No animation/transform here \u2014 spin-wheel's win/loss screens are ALWAYS full
|
|
1818
|
+
custom HTML now (winHtml/lossHtml), never this class's own generated icon/title/prize/cta
|
|
1819
|
+
content (those sub-classes below are unreachable dead CSS, kept only for backwards-compat with
|
|
1820
|
+
any already-saved artifact that might still reference them). A transform on THIS wrapper
|
|
1821
|
+
would make it the CSS containing block for any fixed/absolute-positioned descendant the
|
|
1822
|
+
author's own HTML uses (a real, common case \u2014 a "full-screen overlay" win/loss design) \u2014 the
|
|
1823
|
+
fixed child would then size/position itself relative to this wrapper's own small, in-flow,
|
|
1824
|
+
often off-center box instead of the true browser viewport, visibly popping in from/toward
|
|
1825
|
+
whatever corner the wrapper happens to sit at instead of scaling from the real screen center.
|
|
1826
|
+
spin-wheel.ts's crossFadeReveal owns the actual reveal transition (applied to the author's own
|
|
1827
|
+
top-level element, never this wrapper) \u2014 this class stays a bare, transform-free layout box. */
|
|
1828
|
+
.xenia-widget-result { display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
1829
|
+
padding: 8px 4px 2px; text-align: center; }
|
|
1830
|
+
.xenia-widget-result[hidden] { display: none; }
|
|
1831
|
+
.xenia-widget-result-icon { font-size: 40px; line-height: 1; }
|
|
1832
|
+
.xenia-widget-result-title { font-weight: var(--xenia-title-weight, 700); font-size: var(--xenia-title-size, 17px); }
|
|
1833
|
+
.xenia-widget-result-prize { font-weight: 600; font-size: 15px; color: var(--xenia-cta-bg, #111);
|
|
1834
|
+
background: var(--xenia-result-prize-bg, rgba(0,0,0,.06)); border-radius: var(--xenia-cta-radius, 8px);
|
|
1835
|
+
padding: 6px 14px; }
|
|
1836
|
+
.xenia-widget-result-cta { margin-top: 8px; background: var(--xenia-cta-bg, #111); color: var(--xenia-cta-color, #fff);
|
|
1837
|
+
border: 0; border-radius: var(--xenia-cta-radius, 8px); padding: var(--xenia-cta-padding, 8px 14px);
|
|
1838
|
+
font-size: var(--xenia-cta-size, 14px); font-family: inherit; cursor: pointer; }
|
|
1380
1839
|
`;
|
|
1381
1840
|
|
|
1382
1841
|
// src/client/xenia-client.ts
|
|
@@ -1401,7 +1860,8 @@ var XeniaClient = class {
|
|
|
1401
1860
|
this.renderer = new ActionRenderer((event, cta) => this.onCtaEvent(event, cta), {
|
|
1402
1861
|
messageTemplate: options.messageTemplate,
|
|
1403
1862
|
messageStyles: options.messageStyles,
|
|
1404
|
-
messageComponent: options.messageComponent
|
|
1863
|
+
messageComponent: options.messageComponent,
|
|
1864
|
+
submitWidgetForm: (submit, fields, subOpts) => this.submitWidgetForm(submit, fields, subOpts)
|
|
1405
1865
|
});
|
|
1406
1866
|
this.consentState = { ...options.consent };
|
|
1407
1867
|
this.analyticsQueue = new AnalyticsQueue({
|
|
@@ -1673,6 +2133,55 @@ var XeniaClient = class {
|
|
|
1673
2133
|
getResponse(key) {
|
|
1674
2134
|
return this.responses.get(key);
|
|
1675
2135
|
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Submit a widget's captured form values (standard/custom popup submit, or a spin-wheel/
|
|
2138
|
+
* spin-wheel's resolve request) and apply any returned `xeniaActions`, exactly like
|
|
2139
|
+
* {@link request}.
|
|
2140
|
+
*
|
|
2141
|
+
* **Endpoint selection is config-driven, not SDK-decided** (per the cross-repo contract): a
|
|
2142
|
+
* `flowRef` (present only when `mode === "journey" && !requiresResolve` — the common
|
|
2143
|
+
* plain-popup case) bypasses the resolve endpoint entirely and POSTs straight to the existing
|
|
2144
|
+
* public `/flow` fast path; otherwise this POSTs to `/widget-submissions` via `bindingRef`. The
|
|
2145
|
+
* SDK is agnostic to *why* — it just follows whichever ref field the backend populated.
|
|
2146
|
+
*
|
|
2147
|
+
* The full ambient-context parity list {@link request}/`withVisitor()` already build
|
|
2148
|
+
* (visitorId, deviceId, url, referrer, path, query, `utm*`) rides on every call, plus `fields`
|
|
2149
|
+
* and `submit.mode`/the resolved ref, relayed verbatim/uninterpreted — the SDK never branches
|
|
2150
|
+
* on `mode`'s value itself, that's server-side.
|
|
2151
|
+
*
|
|
2152
|
+
* `opts.idempotencyKey` defaults to a freshly generated ULID; pass the SAME key back in on a
|
|
2153
|
+
* retried submit of the same interaction so a network retry can't double-fire a journey or
|
|
2154
|
+
* double-resolve a prize server-side.
|
|
2155
|
+
*/
|
|
2156
|
+
async submitWidgetForm(submit, fields, opts) {
|
|
2157
|
+
const ctx = this.identity.collectContext();
|
|
2158
|
+
const idempotencyKey = opts?.idempotencyKey ?? generateUlid();
|
|
2159
|
+
const ref = submit.flowRef ?? submit.bindingRef;
|
|
2160
|
+
const ambient = {
|
|
2161
|
+
visitorId: this.resolveVisitorId(),
|
|
2162
|
+
deviceId: this.identity.getDeviceId() ?? void 0,
|
|
2163
|
+
idempotencyKey,
|
|
2164
|
+
mode: submit.mode,
|
|
2165
|
+
ref,
|
|
2166
|
+
url: ctx.url,
|
|
2167
|
+
referrer: ctx.referrer,
|
|
2168
|
+
path: ctx.path,
|
|
2169
|
+
query: ctx.query,
|
|
2170
|
+
utmSource: ctx.utmSource,
|
|
2171
|
+
utmMedium: ctx.utmMedium,
|
|
2172
|
+
utmCampaign: ctx.utmCampaign,
|
|
2173
|
+
utmContent: ctx.utmContent,
|
|
2174
|
+
utmId: ctx.utmId
|
|
2175
|
+
};
|
|
2176
|
+
const response = submit.flowRef ? await this.api.webhook(submit.flowRef, compact({ ...ambient, ...fields }), "POST") : await this.api.submitWidget(
|
|
2177
|
+
submit.bindingRef ?? "",
|
|
2178
|
+
compact({ ...ambient, fields })
|
|
2179
|
+
);
|
|
2180
|
+
this.emit("response", { triggerId: ref, response });
|
|
2181
|
+
const apply = opts?.apply ?? this.options.autoApplyActions !== false;
|
|
2182
|
+
if (apply && response.actions.length) this.applyActions(response.actions);
|
|
2183
|
+
return response;
|
|
2184
|
+
}
|
|
1676
2185
|
// ── Imperative render helpers ─────────────────────────────────────────────────
|
|
1677
2186
|
swapContent(target, content) {
|
|
1678
2187
|
return swapContent(target, content);
|
|
@@ -1787,7 +2296,10 @@ exports.AnalyticsQueue = AnalyticsQueue;
|
|
|
1787
2296
|
exports.AutoTracker = AutoTracker;
|
|
1788
2297
|
exports.Identity = Identity;
|
|
1789
2298
|
exports.TokenProvider = TokenProvider;
|
|
2299
|
+
exports.WIDGET_RENDERERS = WIDGET_RENDERERS;
|
|
1790
2300
|
exports.XeniaClient = XeniaClient;
|
|
2301
|
+
exports.collectFields = collectFields;
|
|
2302
|
+
exports.computeSpinRotation = computeSpinRotation;
|
|
1791
2303
|
exports.createXeniaClient = createXeniaClient;
|
|
1792
2304
|
exports.extractContentSwaps = extractContentSwaps;
|
|
1793
2305
|
exports.hideContent = hideContent;
|
|
@@ -1798,5 +2310,6 @@ exports.resolveTarget = resolveTarget;
|
|
|
1798
2310
|
exports.showContent = showContent;
|
|
1799
2311
|
exports.spliceSlots = spliceSlots;
|
|
1800
2312
|
exports.swapContent = swapContent;
|
|
2313
|
+
exports.wireFormSubmit = wireFormSubmit;
|
|
1801
2314
|
//# sourceMappingURL=index.cjs.map
|
|
1802
2315
|
//# sourceMappingURL=index.cjs.map
|