@mrrocket/sdk 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +20 -0
- package/dist/browser/mrrocket-flows.js +2 -2
- package/dist/browser/mrrocket-forms.js +1 -0
- package/dist/browser/mrrocket.js +1 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +28 -0
- package/dist/client.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/flows-ui.js +13 -1
- package/dist/flows-ui.js.map +1 -1
- package/dist/flows-web.js +1 -0
- package/dist/flows-web.js.map +1 -1
- package/dist/flows.d.ts +50 -5
- package/dist/flows.js +156 -34
- package/dist/flows.js.map +1 -1
- package/dist/forms.d.ts +87 -0
- package/dist/forms.js +124 -0
- package/dist/forms.js.map +1 -0
- package/dist/pick.d.ts +16 -0
- package/dist/pick.js +117 -0
- package/dist/pick.js.map +1 -0
- package/dist/react-native/index.d.cts +28 -2
- package/dist/react-native/index.d.ts +28 -2
- package/dist/react-native/index.js +153 -34
- package/dist/react-native/index.js.map +1 -1
- package/package.json +1 -1
package/dist/pick.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pick.ts"],"sourcesContent":["/**\n * \"Pick on your site\" (0.11.0). MRRocket's onboarding editor opens the site in a frame with\n * `?ck_embed=1&ck_pick=1&ck_origin=<the editor's origin>`: nothing is tracked (ck_embed), the element under the pointer\n * gets an outline with its selector, and a click picks it instead of doing what it does on the page. The selector goes to\n * the editor by postMessage, to that origin only; Esc tells it the person stopped. Only inside a frame, only with the\n * parameter: a normal visit never sees any of this.\n *\n * Messages to the editor: `{ type: \"mrrocket:pick-ready\" }` once listening, `{ type: \"mrrocket:picked\", selector, text }`\n * on a click, `{ type: \"mrrocket:pick-cancel\" }` on Esc.\n */\nexport const PICK_PARAM = \"ck_pick\";\nexport const PICK_ORIGIN_PARAM = \"ck_origin\";\n\nconst esc = (s: string) => (typeof CSS !== \"undefined\" && CSS.escape ? CSS.escape(s) : s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\\\${c}`));\n\n/** Classes worth naming: no state classes, no generated hashes or numbers (css-modules, tailwind arbitrary values). */\nconst stableClasses = (el: Element) =>\n [...el.classList].filter((c) => c.length < 30 && !/\\d{2,}|[_-][a-z0-9]{5,}$|^(is|has)-|hover|active|focus|open|selected|:|\\[/.test(c)).slice(0, 2);\n\n/** A short CSS selector that finds this element, and only it, on the page. */\nexport const pickSelector = (el: Element): string => {\n const doc = el.ownerDocument;\n const unique = (sel: string) => {\n try {\n return doc.querySelectorAll(sel).length === 1;\n } catch {\n return false;\n }\n };\n const named = el.getAttribute(\"data-ck-target\");\n if (named) return `[data-ck-target=\"${named.replace(/\"/g, '\\\\\"')}\"]`;\n if (el.id && !/\\d{3,}/.test(el.id) && unique(`#${esc(el.id)}`)) return `#${esc(el.id)}`;\n const parts: string[] = [];\n let node: Element | null = el;\n while (node && node !== doc.documentElement && parts.length < 6) {\n if (node !== el && node.id && !/\\d{3,}/.test(node.id)) {\n parts.unshift(`#${esc(node.id)}`);\n break;\n }\n const tag = node.tagName.toLowerCase();\n const cls = stableClasses(node)\n .map((c) => `.${esc(c)}`)\n .join(\"\");\n const parent: Element | null = node.parentElement;\n const same = parent ? [...parent.children].filter((c) => c.tagName === node!.tagName) : [];\n const nth = same.length > 1 ? `:nth-of-type(${same.indexOf(node) + 1})` : \"\";\n parts.unshift(`${tag}${cls}${nth}`);\n const sel = parts.join(\" > \");\n if (unique(sel)) return sel;\n node = parent;\n }\n return parts.join(\" > \");\n};\n\n/** Starts picking when the page was opened for it; returns how to stop, or null when it was not. */\nexport const startPicker = (): (() => void) | null => {\n if (typeof window === \"undefined\" || window.top === window) return null;\n const params = new URLSearchParams(window.location.search);\n if (params.get(PICK_PARAM) !== \"1\") return null;\n let origin: string;\n try {\n origin = new URL(params.get(PICK_ORIGIN_PARAM) ?? \"\").origin;\n } catch {\n return null;\n }\n if (!/^https?:\\/\\//.test(origin)) return null;\n const send = (data: Record<string, unknown>) => window.parent.postMessage(data, origin);\n\n const box = document.createElement(\"div\");\n box.setAttribute(\"aria-hidden\", \"true\");\n Object.assign(box.style, {\n position: \"fixed\",\n zIndex: \"2147483647\",\n pointerEvents: \"none\",\n border: \"2px solid #0ea5e9\",\n background: \"rgba(14,165,233,.12)\",\n borderRadius: \"6px\",\n transition: \"all 60ms linear\",\n display: \"none\",\n });\n const tag = document.createElement(\"div\");\n Object.assign(tag.style, {\n position: \"absolute\",\n left: \"-2px\",\n bottom: \"calc(100% + 4px)\",\n font: \"600 11px/1.4 system-ui, sans-serif\",\n color: \"#fff\",\n background: \"#0c4a6e\",\n padding: \"2px 6px\",\n borderRadius: \"4px\",\n whiteSpace: \"nowrap\",\n maxWidth: \"360px\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n });\n box.appendChild(tag);\n document.body.appendChild(box);\n\n let current: Element | null = null;\n const show = (el: Element | null) => {\n current = el && el !== box && !box.contains(el) ? el : null;\n if (!current) {\n box.style.display = \"none\";\n return;\n }\n const r = current.getBoundingClientRect();\n Object.assign(box.style, { display: \"block\", left: `${r.left - 2}px`, top: `${r.top - 2}px`, width: `${r.width + 4}px`, height: `${r.height + 4}px` });\n tag.textContent = pickSelector(current);\n };\n const move = (e: MouseEvent) => show(document.elementFromPoint(e.clientX, e.clientY));\n const click = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n const el = document.elementFromPoint(e.clientX, e.clientY) ?? current;\n if (!el || el === box) return;\n send({ type: \"mrrocket:picked\", selector: pickSelector(el), text: (el.textContent ?? \"\").trim().replace(/\\s+/g, \" \").slice(0, 80) });\n };\n const key = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") send({ type: \"mrrocket:pick-cancel\" });\n };\n // Capturing, so the page's own handlers (links, buttons) never see the picking click.\n window.addEventListener(\"mousemove\", move, true);\n window.addEventListener(\"click\", click, true);\n window.addEventListener(\"keydown\", key, true);\n send({ type: \"mrrocket:pick-ready\" });\n return () => {\n window.removeEventListener(\"mousemove\", move, true);\n window.removeEventListener(\"click\", click, true);\n window.removeEventListener(\"keydown\", key, true);\n box.remove();\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -224,7 +224,9 @@ type Api = ReturnType<typeof createApi>;
|
|
|
224
224
|
* `finished` with `outcome: "saved"` into a save and with `outcome: "cancelled"` into a cancellation on the Churn page.
|
|
225
225
|
* MRRocket never changes a subscription: an accepted offer goes to the app's `onSave`, a confirmed cancel to its
|
|
226
226
|
* `onCancel`, and only after they succeed is the outcome recorded.
|
|
227
|
-
* - `$onboarding` `{flow, step?, action: "started" | "shown" | "completed" | "dismissed"}
|
|
227
|
+
* - `$onboarding` `{flow, step?, action: "started" | "shown" | "completed" | "dismissed"}`; since 0.11.0 a wait records
|
|
228
|
+
* "shown" when the person reaches it and "completed" when it is over, a condition `{action: "answered", answer: "yes"
|
|
229
|
+
* | "no"}` once it is decided.
|
|
228
230
|
* - `$inapp` `{message, journey, action: "shown" | "clicked" | "dismissed"}`; the journey moves the person on.
|
|
229
231
|
*/
|
|
230
232
|
|
|
@@ -276,13 +278,37 @@ interface OnboardingStep {
|
|
|
276
278
|
/** Checklist items: the event that ticks them (`track("<event>")`). Without it the person ticks it themselves. */
|
|
277
279
|
event?: string;
|
|
278
280
|
}
|
|
281
|
+
/** A pause in a flow (0.11.0): the next step shows this long after the one before was done. */
|
|
282
|
+
interface OnboardingWait {
|
|
283
|
+
id: string;
|
|
284
|
+
kind: "wait";
|
|
285
|
+
amount: number;
|
|
286
|
+
unit: "hours" | "days";
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A fork in a flow (0.11.0): "Did <event> happen?" (the person's events, here and on MRRocket) or "Is on <page>?" (the
|
|
290
|
+
* page they are on when they reach it; `*` at the end for everything below). Decided once, when the person reaches it;
|
|
291
|
+
* like a journey's branch it ends its line, the rest of the flow lives in its two paths.
|
|
292
|
+
*/
|
|
293
|
+
interface OnboardingCondition {
|
|
294
|
+
id: string;
|
|
295
|
+
kind: "condition";
|
|
296
|
+
event?: string;
|
|
297
|
+
page?: string;
|
|
298
|
+
yes: OnboardingNode[];
|
|
299
|
+
no: OnboardingNode[];
|
|
300
|
+
}
|
|
301
|
+
type OnboardingNode = OnboardingStep | OnboardingWait | OnboardingCondition;
|
|
279
302
|
/** A live onboarding flow's in-app steps (MRRocket › Conversion › Onboarding); its emails go out from MRRocket. */
|
|
280
303
|
interface OnboardingFlow {
|
|
281
304
|
id: string;
|
|
282
305
|
name: string;
|
|
283
306
|
/** When it starts: an event's words ("Signed up", "First export"), or "First launch" / "First visit". */
|
|
284
307
|
trigger: string;
|
|
308
|
+
/** What every version shows: with `nodes`, only the in-app steps before the first wait or condition. */
|
|
285
309
|
steps: OnboardingStep[];
|
|
310
|
+
/** 0.11.0+: the whole flow with its waits and conditions. Read instead of `steps` when it is there. */
|
|
311
|
+
nodes?: OnboardingNode[];
|
|
286
312
|
}
|
|
287
313
|
/** An in-app message a Lifecycle journey has for this person. */
|
|
288
314
|
interface InAppMessage {
|
|
@@ -395,7 +421,7 @@ interface FlowsController {
|
|
|
395
421
|
|
|
396
422
|
type VariantId = string;
|
|
397
423
|
/** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
|
|
398
|
-
declare const SDK_VERSION = "0.
|
|
424
|
+
declare const SDK_VERSION = "0.12.0";
|
|
399
425
|
/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
|
|
400
426
|
interface VariantConfig {
|
|
401
427
|
id: VariantId;
|
|
@@ -224,7 +224,9 @@ type Api = ReturnType<typeof createApi>;
|
|
|
224
224
|
* `finished` with `outcome: "saved"` into a save and with `outcome: "cancelled"` into a cancellation on the Churn page.
|
|
225
225
|
* MRRocket never changes a subscription: an accepted offer goes to the app's `onSave`, a confirmed cancel to its
|
|
226
226
|
* `onCancel`, and only after they succeed is the outcome recorded.
|
|
227
|
-
* - `$onboarding` `{flow, step?, action: "started" | "shown" | "completed" | "dismissed"}
|
|
227
|
+
* - `$onboarding` `{flow, step?, action: "started" | "shown" | "completed" | "dismissed"}`; since 0.11.0 a wait records
|
|
228
|
+
* "shown" when the person reaches it and "completed" when it is over, a condition `{action: "answered", answer: "yes"
|
|
229
|
+
* | "no"}` once it is decided.
|
|
228
230
|
* - `$inapp` `{message, journey, action: "shown" | "clicked" | "dismissed"}`; the journey moves the person on.
|
|
229
231
|
*/
|
|
230
232
|
|
|
@@ -276,13 +278,37 @@ interface OnboardingStep {
|
|
|
276
278
|
/** Checklist items: the event that ticks them (`track("<event>")`). Without it the person ticks it themselves. */
|
|
277
279
|
event?: string;
|
|
278
280
|
}
|
|
281
|
+
/** A pause in a flow (0.11.0): the next step shows this long after the one before was done. */
|
|
282
|
+
interface OnboardingWait {
|
|
283
|
+
id: string;
|
|
284
|
+
kind: "wait";
|
|
285
|
+
amount: number;
|
|
286
|
+
unit: "hours" | "days";
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A fork in a flow (0.11.0): "Did <event> happen?" (the person's events, here and on MRRocket) or "Is on <page>?" (the
|
|
290
|
+
* page they are on when they reach it; `*` at the end for everything below). Decided once, when the person reaches it;
|
|
291
|
+
* like a journey's branch it ends its line, the rest of the flow lives in its two paths.
|
|
292
|
+
*/
|
|
293
|
+
interface OnboardingCondition {
|
|
294
|
+
id: string;
|
|
295
|
+
kind: "condition";
|
|
296
|
+
event?: string;
|
|
297
|
+
page?: string;
|
|
298
|
+
yes: OnboardingNode[];
|
|
299
|
+
no: OnboardingNode[];
|
|
300
|
+
}
|
|
301
|
+
type OnboardingNode = OnboardingStep | OnboardingWait | OnboardingCondition;
|
|
279
302
|
/** A live onboarding flow's in-app steps (MRRocket › Conversion › Onboarding); its emails go out from MRRocket. */
|
|
280
303
|
interface OnboardingFlow {
|
|
281
304
|
id: string;
|
|
282
305
|
name: string;
|
|
283
306
|
/** When it starts: an event's words ("Signed up", "First export"), or "First launch" / "First visit". */
|
|
284
307
|
trigger: string;
|
|
308
|
+
/** What every version shows: with `nodes`, only the in-app steps before the first wait or condition. */
|
|
285
309
|
steps: OnboardingStep[];
|
|
310
|
+
/** 0.11.0+: the whole flow with its waits and conditions. Read instead of `steps` when it is there. */
|
|
311
|
+
nodes?: OnboardingNode[];
|
|
286
312
|
}
|
|
287
313
|
/** An in-app message a Lifecycle journey has for this person. */
|
|
288
314
|
interface InAppMessage {
|
|
@@ -395,7 +421,7 @@ interface FlowsController {
|
|
|
395
421
|
|
|
396
422
|
type VariantId = string;
|
|
397
423
|
/** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
|
|
398
|
-
declare const SDK_VERSION = "0.
|
|
424
|
+
declare const SDK_VERSION = "0.12.0";
|
|
399
425
|
/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
|
|
400
426
|
interface VariantConfig {
|
|
401
427
|
id: VariantId;
|
|
@@ -49,7 +49,7 @@ module.exports = __toCommonJS(native_exports);
|
|
|
49
49
|
var import_react3 = require("react");
|
|
50
50
|
|
|
51
51
|
// src/core.ts
|
|
52
|
-
var SDK_VERSION = "0.
|
|
52
|
+
var SDK_VERSION = "0.12.0";
|
|
53
53
|
var propOf = (person, name) => {
|
|
54
54
|
const want = name.trim().toLowerCase();
|
|
55
55
|
for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
|
|
@@ -329,6 +329,19 @@ var safeUrl = (url) => {
|
|
|
329
329
|
const u = (url ?? "").trim();
|
|
330
330
|
return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
|
|
331
331
|
};
|
|
332
|
+
var nodesOf = (f) => f.nodes ?? f.steps;
|
|
333
|
+
var waitMs = (w) => Math.max(0, w.amount) * (w.unit === "hours" ? 36e5 : 864e5);
|
|
334
|
+
var pageMatches = (pattern, path) => {
|
|
335
|
+
const p = pattern.trim();
|
|
336
|
+
const at = (path.split(/[?#]/)[0] || "/").replace(/\/+$/, "") || "/";
|
|
337
|
+
if (!p) return false;
|
|
338
|
+
if (p.endsWith("*")) {
|
|
339
|
+
const base = p.slice(0, -1).replace(/\/+$/, "");
|
|
340
|
+
return !base || at === base || at.startsWith(`${base}/`);
|
|
341
|
+
}
|
|
342
|
+
return at === (p.replace(/\/+$/, "") || "/");
|
|
343
|
+
};
|
|
344
|
+
var stepsIn = (nodes) => nodes.flatMap((n) => n.kind === "condition" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === "wait" ? [] : [n]);
|
|
332
345
|
var createFlows = (env) => {
|
|
333
346
|
let config = null;
|
|
334
347
|
let cancel = null;
|
|
@@ -343,25 +356,99 @@ var createFlows = (env) => {
|
|
|
343
356
|
const shown = /* @__PURE__ */ new Set();
|
|
344
357
|
const listeners = /* @__PURE__ */ new Set();
|
|
345
358
|
const auto = env.auto !== false;
|
|
359
|
+
const seenEvents = /* @__PURE__ */ new Set();
|
|
360
|
+
const now = () => env.now?.() ?? Date.now();
|
|
361
|
+
let timer = null;
|
|
346
362
|
const flows = () => config?.onboarding ?? [];
|
|
347
363
|
const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
|
|
348
364
|
const prog = (id) => {
|
|
349
365
|
let p = progress.get(id);
|
|
350
|
-
if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false });
|
|
366
|
+
if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false, answers: /* @__PURE__ */ new Map(), at: /* @__PURE__ */ new Map(), reached: /* @__PURE__ */ new Map() });
|
|
351
367
|
return p;
|
|
352
368
|
};
|
|
369
|
+
let dirty = false;
|
|
370
|
+
const record = (props) => {
|
|
371
|
+
env.track(ONBOARDING_EVENT, props);
|
|
372
|
+
dirty = true;
|
|
373
|
+
};
|
|
374
|
+
const markDone = (p, id) => {
|
|
375
|
+
p.done.add(id);
|
|
376
|
+
p.at.set(id, now());
|
|
377
|
+
};
|
|
378
|
+
const decide = (c) => {
|
|
379
|
+
if (c.page?.trim()) {
|
|
380
|
+
const path = env.path?.();
|
|
381
|
+
return path === void 0 ? false : pageMatches(c.page, path);
|
|
382
|
+
}
|
|
383
|
+
if (c.event?.trim()) {
|
|
384
|
+
if (!known) return void 0;
|
|
385
|
+
for (const e of seenEvents) if (matchesTrigger(c.event, e)) return true;
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
};
|
|
390
|
+
const walkLine = (flow, line, p, live, before = 0, last) => {
|
|
391
|
+
for (let i = 0; i < line.length; i++) {
|
|
392
|
+
const n = line[i];
|
|
393
|
+
if (n.kind === "wait") {
|
|
394
|
+
if (p.done.has(n.id)) {
|
|
395
|
+
last = p.at.get(n.id) ?? last;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
let since = p.reached.get(n.id);
|
|
399
|
+
if (since === void 0) {
|
|
400
|
+
if (!live) return { at: "pending" };
|
|
401
|
+
since = last ?? now();
|
|
402
|
+
p.reached.set(n.id, since);
|
|
403
|
+
record({ flow: flow.id, step: n.id, kind: "wait", action: "shown" });
|
|
404
|
+
}
|
|
405
|
+
const until = since + waitMs(n);
|
|
406
|
+
if (now() < until) return { at: "wait", until };
|
|
407
|
+
if (!live) return { at: "pending" };
|
|
408
|
+
markDone(p, n.id);
|
|
409
|
+
record({ flow: flow.id, step: n.id, kind: "wait", action: "completed" });
|
|
410
|
+
last = until;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (n.kind === "condition") {
|
|
414
|
+
let answer = p.answers.get(n.id);
|
|
415
|
+
if (answer === void 0) {
|
|
416
|
+
if (!live) return { at: "pending" };
|
|
417
|
+
answer = decide(n);
|
|
418
|
+
if (answer === void 0) return { at: "pending" };
|
|
419
|
+
p.answers.set(n.id, answer);
|
|
420
|
+
record({ flow: flow.id, step: n.id, kind: "condition", action: "answered", answer: answer ? "yes" : "no" });
|
|
421
|
+
}
|
|
422
|
+
return walkLine(flow, answer ? n.yes : n.no, p, live, before, last);
|
|
423
|
+
}
|
|
424
|
+
if (!p.done.has(n.id)) return { at: "step", step: n, line, index: i, before };
|
|
425
|
+
last = p.at.get(n.id) ?? last;
|
|
426
|
+
before++;
|
|
427
|
+
}
|
|
428
|
+
return { at: "end" };
|
|
429
|
+
};
|
|
353
430
|
const finished = (f) => {
|
|
354
431
|
const p = progress.get(f.id);
|
|
355
|
-
return !!p && (p.dismissed || f
|
|
432
|
+
return !!p && (p.dismissed || walkLine(f, nodesOf(f), p, false).at === "end");
|
|
356
433
|
};
|
|
357
434
|
const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
|
|
435
|
+
const eventsKeyOf = (pid) => `${PROGRESS_KEY}:events:${pid ?? "visitor"}`;
|
|
358
436
|
const save = () => {
|
|
437
|
+
dirty = false;
|
|
359
438
|
const store = env.storage?.();
|
|
360
439
|
if (!store) return;
|
|
361
440
|
const out = {};
|
|
362
|
-
for (const [id, p] of progress)
|
|
441
|
+
for (const [id, p] of progress)
|
|
442
|
+
out[id] = {
|
|
443
|
+
done: [...p.done],
|
|
444
|
+
...p.dismissed ? { dismissed: true } : {},
|
|
445
|
+
...p.answers.size ? { answers: Object.fromEntries([...p.answers].map(([k, v]) => [k, v ? "yes" : "no"])) } : {},
|
|
446
|
+
...p.at.size ? { at: Object.fromEntries(p.at) } : {},
|
|
447
|
+
...p.reached.size ? { reached: Object.fromEntries(p.reached) } : {}
|
|
448
|
+
};
|
|
363
449
|
try {
|
|
364
450
|
store.set(keyOf(env.personId()), JSON.stringify(out));
|
|
451
|
+
if (seenEvents.size) store.set(eventsKeyOf(env.personId()), JSON.stringify([...seenEvents].slice(-200)));
|
|
365
452
|
} catch {
|
|
366
453
|
}
|
|
367
454
|
};
|
|
@@ -370,23 +457,55 @@ var createFlows = (env) => {
|
|
|
370
457
|
const p = prog(id);
|
|
371
458
|
for (const d of s.done ?? []) p.done.add(d);
|
|
372
459
|
if (s.dismissed) p.dismissed = true;
|
|
460
|
+
for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
|
|
461
|
+
for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
|
|
462
|
+
for (const [k, v] of Object.entries(s.reached ?? {})) if (typeof v === "number" && (!p.reached.has(k) || v < p.reached.get(k))) p.reached.set(k, v);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const wakeAt = (until) => {
|
|
466
|
+
if (timer) clearTimeout(timer);
|
|
467
|
+
timer = null;
|
|
468
|
+
const ms = until - now();
|
|
469
|
+
if (ms > 0 && ms <= 864e5) {
|
|
470
|
+
timer = setTimeout(() => (timer = null, emit()), ms + 50);
|
|
471
|
+
timer.unref?.();
|
|
373
472
|
}
|
|
374
473
|
};
|
|
474
|
+
let holding = false;
|
|
375
475
|
const onboardingView = () => {
|
|
476
|
+
holding = false;
|
|
376
477
|
const flow = flowById(active);
|
|
377
478
|
if (!flow) return null;
|
|
378
479
|
const p = prog(flow.id);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
480
|
+
if (p.dismissed) return null;
|
|
481
|
+
const w = walkLine(flow, nodesOf(flow), p, true);
|
|
482
|
+
if (dirty) save();
|
|
483
|
+
if (w.at === "wait") wakeAt(w.until);
|
|
484
|
+
holding = w.at === "wait" || w.at === "pending";
|
|
485
|
+
if (w.at !== "step") return null;
|
|
486
|
+
const { step, line, index } = w;
|
|
487
|
+
const isStep = (n) => !!n && n.kind !== "wait" && n.kind !== "condition";
|
|
382
488
|
if (step.kind === "checklist") {
|
|
383
|
-
let from =
|
|
384
|
-
while (from > 0 &&
|
|
385
|
-
let to =
|
|
386
|
-
while (to + 1 <
|
|
387
|
-
|
|
489
|
+
let from = index;
|
|
490
|
+
while (from > 0 && isStep(line[from - 1]) && line[from - 1].kind === "checklist") from--;
|
|
491
|
+
let to = index;
|
|
492
|
+
while (to + 1 < line.length && isStep(line[to + 1]) && line[to + 1].kind === "checklist") to++;
|
|
493
|
+
const items = line.slice(from, to + 1).filter(isStep);
|
|
494
|
+
for (const i of items)
|
|
495
|
+
if (!p.done.has(i.id) && !shown.has(`s:${i.id}`)) {
|
|
496
|
+
shown.add(`s:${i.id}`);
|
|
497
|
+
env.track(ONBOARDING_EVENT, { flow: flow.id, step: i.id, kind: "checklist", action: "shown" });
|
|
498
|
+
}
|
|
499
|
+
return { type: "checklist", flow, items: items.map((s) => ({ step: s, done: p.done.has(s.id) })) };
|
|
500
|
+
}
|
|
501
|
+
if (!shown.has(`s:${step.id}`)) {
|
|
502
|
+
shown.add(`s:${step.id}`);
|
|
503
|
+
env.track(ONBOARDING_EVENT, { flow: flow.id, step: step.id, kind: step.kind, action: "shown" });
|
|
388
504
|
}
|
|
389
|
-
|
|
505
|
+
const rest = line.slice(index + 1);
|
|
506
|
+
const fork = rest.findIndex((n) => n.kind === "condition");
|
|
507
|
+
const ahead = (fork < 0 ? rest : rest.slice(0, fork)).filter(isStep).length;
|
|
508
|
+
return { type: "step", flow, step, index: w.before, total: w.before + 1 + ahead, last: !ahead && fork < 0 };
|
|
390
509
|
};
|
|
391
510
|
const view = () => {
|
|
392
511
|
if (cancel) {
|
|
@@ -406,22 +525,13 @@ var createFlows = (env) => {
|
|
|
406
525
|
}
|
|
407
526
|
const ob = onboardingView();
|
|
408
527
|
if (ob) return ob;
|
|
409
|
-
if (active) active = null;
|
|
528
|
+
if (active && !holding) active = null;
|
|
410
529
|
const m = messages.find((x) => !handled.has(x.id));
|
|
411
530
|
return m ? { type: "message", message: m } : null;
|
|
412
531
|
};
|
|
413
532
|
const emit = () => {
|
|
414
533
|
const v = view();
|
|
415
|
-
if (v?.type === "
|
|
416
|
-
shown.add(`s:${v.step.id}`);
|
|
417
|
-
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "shown" });
|
|
418
|
-
} else if (v?.type === "checklist") {
|
|
419
|
-
for (const i of v.items)
|
|
420
|
-
if (!i.done && !shown.has(`s:${i.step.id}`)) {
|
|
421
|
-
shown.add(`s:${i.step.id}`);
|
|
422
|
-
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: i.step.id, kind: "checklist", action: "shown" });
|
|
423
|
-
}
|
|
424
|
-
} else if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
|
|
534
|
+
if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
|
|
425
535
|
shown.add(`m:${v.message.id}`);
|
|
426
536
|
env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "shown" });
|
|
427
537
|
}
|
|
@@ -443,13 +553,14 @@ var createFlows = (env) => {
|
|
|
443
553
|
const autoStart = () => {
|
|
444
554
|
if (!auto || active || !known) return;
|
|
445
555
|
if (!env.personId() && !env.storage?.()) return;
|
|
446
|
-
const next = flows().find((f) => startsOnLoad(f.trigger) && f.
|
|
556
|
+
const next = flows().find((f) => startsOnLoad(f.trigger) && nodesOf(f).length && !finished(f));
|
|
447
557
|
if (next) start(next);
|
|
448
558
|
};
|
|
449
559
|
const refresh = () => {
|
|
450
560
|
const pid = env.personId();
|
|
451
561
|
if (progressFor !== null && progressFor !== pid) {
|
|
452
562
|
progress.clear();
|
|
563
|
+
seenEvents.clear();
|
|
453
564
|
handled.clear();
|
|
454
565
|
active = null;
|
|
455
566
|
loadedFor = void 0;
|
|
@@ -461,6 +572,10 @@ var createFlows = (env) => {
|
|
|
461
572
|
try {
|
|
462
573
|
const raw = await store.get(keyOf(pid));
|
|
463
574
|
if (raw) merge(JSON.parse(raw));
|
|
575
|
+
const seen = await store.get(eventsKeyOf(pid));
|
|
576
|
+
if (seen) {
|
|
577
|
+
for (const e of JSON.parse(seen)) if (typeof e === "string") seenEvents.add(e);
|
|
578
|
+
}
|
|
464
579
|
} catch {
|
|
465
580
|
}
|
|
466
581
|
}
|
|
@@ -470,6 +585,7 @@ var createFlows = (env) => {
|
|
|
470
585
|
const state = await env.fetchInApp(pid).catch(() => null);
|
|
471
586
|
if (state && env.personId() === pid) {
|
|
472
587
|
merge(state.onboarding);
|
|
588
|
+
for (const e of state.events ?? []) if (typeof e === "string") seenEvents.add(eventSlug(e));
|
|
473
589
|
messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
|
|
474
590
|
}
|
|
475
591
|
}
|
|
@@ -546,29 +662,32 @@ var createFlows = (env) => {
|
|
|
546
662
|
refresh,
|
|
547
663
|
onEvent: (name) => {
|
|
548
664
|
if (!name || name.startsWith("$")) return;
|
|
665
|
+
const slug = eventSlug(name);
|
|
666
|
+
const fresh = !!slug && !seenEvents.has(slug);
|
|
667
|
+
if (fresh) seenEvents.add(slug);
|
|
549
668
|
const flow = flowById(active);
|
|
550
669
|
let ticked = false;
|
|
551
670
|
if (flow) {
|
|
552
671
|
const p = prog(flow.id);
|
|
553
|
-
for (const s of flow
|
|
672
|
+
for (const s of stepsIn(nodesOf(flow)))
|
|
554
673
|
if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
|
|
555
|
-
p
|
|
674
|
+
markDone(p, s.id);
|
|
556
675
|
ticked = true;
|
|
557
676
|
env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
|
|
558
677
|
}
|
|
559
678
|
}
|
|
560
|
-
if (ticked) {
|
|
679
|
+
if (ticked || fresh && flow) {
|
|
561
680
|
save();
|
|
562
681
|
emit();
|
|
563
|
-
}
|
|
682
|
+
} else if (fresh) save();
|
|
564
683
|
if (!auto || !env.personId() && !env.storage?.()) return;
|
|
565
|
-
const next = flows().find((f) => f.
|
|
684
|
+
const next = flows().find((f) => nodesOf(f).length && matchesTrigger(f.trigger, name) && !finished(f));
|
|
566
685
|
if (next) start(next);
|
|
567
686
|
},
|
|
568
687
|
openCancelFlow,
|
|
569
688
|
startOnboarding: (id) => {
|
|
570
|
-
const f = id ? flowById(id) : flows().find((x) => x.
|
|
571
|
-
if (!f || !f.
|
|
689
|
+
const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !finished(x));
|
|
690
|
+
if (!f || !nodesOf(f).length) return false;
|
|
572
691
|
const p = prog(f.id);
|
|
573
692
|
p.dismissed = false;
|
|
574
693
|
return start(f);
|
|
@@ -639,7 +758,7 @@ var createFlows = (env) => {
|
|
|
639
758
|
next: () => {
|
|
640
759
|
const v = onboardingView();
|
|
641
760
|
if (v?.type !== "step") return;
|
|
642
|
-
prog(v.flow.id)
|
|
761
|
+
markDone(prog(v.flow.id), v.step.id);
|
|
643
762
|
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
|
|
644
763
|
save();
|
|
645
764
|
emit();
|
|
@@ -649,7 +768,7 @@ var createFlows = (env) => {
|
|
|
649
768
|
if (v?.type !== "checklist") return;
|
|
650
769
|
const item = v.items.find((i) => i.step.id === stepId);
|
|
651
770
|
if (!item || item.done) return;
|
|
652
|
-
prog(v.flow.id)
|
|
771
|
+
markDone(prog(v.flow.id), stepId);
|
|
653
772
|
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
|
|
654
773
|
save();
|
|
655
774
|
emit();
|