@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/flows-ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/flows-ui.ts"],"sourcesContent":["/**\n * The web renderer of the cancel flow, onboarding and in-app messages (flows.ts): plain DOM, no React, so the Next.js\n * provider and the script tag share it. Both load it only when something is about to show (a dynamic import, and\n * `sdk-flows.js` for the script tag), so pages that show nothing never download it.\n *\n * Themeable three ways: `theme` (\"light\", \"dark\", \"auto\") and `accent` from the SDK's options, and CSS variables on\n * `.ckf-root` in the site's own stylesheet (`--ckf-accent`, `--ckf-bg`, `--ckf-fg`, `--ckf-muted`, `--ckf-line`,\n * `--ckf-radius`, `--ckf-font`). Every text is set with textContent; nothing from the server is parsed as HTML.\n */\nimport { type FlowView, type FlowsController, discounted, money, offerButton } from \"./flows\";\n\nexport interface FlowsTheme {\n theme?: \"light\" | \"dark\" | \"auto\";\n /** The main color of buttons and highlights, like \"#3f76ff\". */\n accent?: string;\n /** An extra class on the root, for the site's own CSS. */\n className?: string;\n}\n\nconst STYLE_ID = \"ckf-styles\";\n\nexport const FLOWS_CSS = `\n.ckf-root{--ckf-accent:#3f76ff;--ckf-accent-ink:#fff;--ckf-bg:#fff;--ckf-fg:#1c1d20;--ckf-muted:#5d6068;--ckf-soft:#f4f5f7;--ckf-line:#e4e5e8;--ckf-danger:#d92d20;--ckf-radius:14px;--ckf-font:inherit;--ckf-shadow:0 16px 40px -12px rgba(16,18,24,.28),0 0 0 1px rgba(16,18,24,.07)}\n.ckf-root[data-theme=dark]{--ckf-bg:#17181b;--ckf-fg:#f1f2f4;--ckf-muted:#a9acb3;--ckf-soft:#232428;--ckf-line:#2e2f35;--ckf-shadow:0 16px 40px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.08)}\n@media (prefers-color-scheme:dark){.ckf-root[data-theme=auto]{--ckf-bg:#17181b;--ckf-fg:#f1f2f4;--ckf-muted:#a9acb3;--ckf-soft:#232428;--ckf-line:#2e2f35;--ckf-shadow:0 16px 40px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.08)}}\n.ckf-root,.ckf-root *{box-sizing:border-box}\n.ckf-root{font-family:var(--ckf-font);font-size:14px;line-height:1.45;color:var(--ckf-fg);-webkit-font-smoothing:antialiased}\n.ckf-root :where(button,textarea){font:inherit;color:inherit;margin:0}\n.ckf-root :focus-visible{outline:2px solid var(--ckf-accent);outline-offset:2px}\n.ckf-scrim{position:fixed;inset:0;z-index:2147483002;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;padding:16px;animation:ckf-fade .15s ease-out}\n.ckf-card{position:relative;width:100%;max-width:400px;max-height:calc(100dvh - 32px);overflow-y:auto;background:var(--ckf-bg);color:var(--ckf-fg);border-radius:var(--ckf-radius);box-shadow:var(--ckf-shadow);padding:18px 20px 20px;display:flex;flex-direction:column;gap:12px;animation:ckf-in .18s ease-out}\n.ckf-float{position:fixed;z-index:2147483001;right:20px;bottom:20px;width:340px;max-width:calc(100vw - 24px)}\n.ckf-top{display:flex;align-items:center;gap:8px;min-height:20px}\n.ckf-dots{display:flex;gap:4px}\n.ckf-dot{width:16px;height:4px;border-radius:4px;background:var(--ckf-line)}\n.ckf-dot[data-on]{background:var(--ckf-accent)}\n.ckf-x{margin-left:auto;width:28px;height:28px;border:0;border-radius:8px;background:none;color:var(--ckf-muted);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:18px;line-height:1}\n.ckf-x:hover{background:var(--ckf-soft);color:var(--ckf-fg)}\n.ckf-h{margin:0;font-size:17px;font-weight:650;letter-spacing:-.01em;text-wrap:balance}\n.ckf-p{margin:0;color:var(--ckf-muted);text-wrap:pretty;white-space:pre-wrap}\n.ckf-small{margin:0;font-size:12px;color:var(--ckf-muted)}\n.ckf-opts{display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none}\n.ckf-opt{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;padding:8px 12px;border:1px solid var(--ckf-line);border-radius:10px;background:var(--ckf-bg);text-align:left;cursor:pointer;font-weight:500}\n.ckf-opt:hover{background:var(--ckf-soft)}\n.ckf-opt[aria-checked=true]{border-color:var(--ckf-accent);background:color-mix(in srgb,var(--ckf-accent) 8%,var(--ckf-bg))}\n.ckf-radio{width:16px;height:16px;flex:none;border-radius:50%;border:1.5px solid var(--ckf-line);display:inline-flex;align-items:center;justify-content:center}\n.ckf-opt[aria-checked=true] .ckf-radio{border-color:var(--ckf-accent)}\n.ckf-opt[aria-checked=true] .ckf-radio::after{content:\"\";width:8px;height:8px;border-radius:50%;background:var(--ckf-accent)}\n.ckf-box{border-radius:10px;background:var(--ckf-soft);padding:12px;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}\n.ckf-big{font-size:22px;font-weight:650;font-variant-numeric:tabular-nums}\n.ckf-strike{color:var(--ckf-muted);text-decoration:line-through;font-variant-numeric:tabular-nums}\n.ckf-text{width:100%;min-height:64px;resize:vertical;border:1px solid var(--ckf-line);border-radius:10px;background:var(--ckf-bg);padding:8px 10px}\n.ckf-text::placeholder{color:var(--ckf-muted)}\n.ckf-btns{display:flex;flex-direction:column;gap:6px}\n.ckf-btn{height:38px;padding:0 14px;border-radius:10px;border:1px solid var(--ckf-line);background:var(--ckf-bg);font-weight:600;cursor:pointer;transition:filter .1s linear,background-color .1s linear}\n.ckf-btn:hover{background:var(--ckf-soft)}\n.ckf-btn[disabled]{opacity:.55;cursor:not-allowed}\n.ckf-primary{background:var(--ckf-accent);border-color:var(--ckf-accent);color:var(--ckf-accent-ink)}\n.ckf-primary:hover{background:var(--ckf-accent);filter:brightness(1.07)}\n.ckf-danger{background:var(--ckf-danger);border-color:var(--ckf-danger);color:#fff}\n.ckf-danger:hover{background:var(--ckf-danger);filter:brightness(1.07)}\n.ckf-quiet{border-color:transparent;background:none;color:var(--ckf-muted);height:30px;font-weight:500}\n.ckf-quiet:hover{background:none;color:var(--ckf-fg)}\n.ckf-err{margin:0;color:var(--ckf-danger);font-size:13px;font-weight:500}\n.ckf-row{display:flex;justify-content:flex-end;gap:8px;align-items:center}\n.ckf-row .ckf-small{margin-right:auto}\n.ckf-ring{position:fixed;z-index:2147483000;pointer-events:none;border-radius:10px;box-shadow:0 0 0 2px var(--ckf-accent),0 0 0 6px color-mix(in srgb,var(--ckf-accent) 25%,transparent);transition:all .15s ease-out}\n.ckf-check{display:flex;align-items:flex-start;gap:10px;width:100%;padding:8px 6px;border:0;border-radius:8px;background:none;text-align:left}\n.ckf-check:where(button){cursor:pointer}\n.ckf-check:where(button):hover{background:var(--ckf-soft)}\n.ckf-tick{width:18px;height:18px;flex:none;margin-top:1px;border-radius:50%;border:1.5px solid var(--ckf-line);display:inline-flex;align-items:center;justify-content:center;color:var(--ckf-accent-ink);font-size:11px;font-weight:700}\n.ckf-check[data-done] .ckf-tick{background:var(--ckf-accent);border-color:var(--ckf-accent)}\n.ckf-check[data-done] b{color:var(--ckf-muted);text-decoration:line-through}\n.ckf-check span{display:flex;flex-direction:column;gap:2px;min-width:0}\n.ckf-bar{height:4px;border-radius:4px;background:var(--ckf-soft);overflow:hidden}\n.ckf-bar i{display:block;height:100%;background:var(--ckf-accent)}\n.ckf-banner{position:fixed;z-index:2147483001;left:50%;top:12px;transform:translateX(-50%);width:min(640px,calc(100vw - 24px));background:var(--ckf-bg);border-radius:12px;box-shadow:var(--ckf-shadow);padding:10px 10px 10px 16px;display:flex;align-items:center;gap:12px;animation:ckf-in .18s ease-out}\n.ckf-banner>span{flex:1;min-width:0;display:flex;flex-direction:column}\n.ckf-banner .ckf-btn{height:32px}\n@keyframes ckf-fade{from{opacity:0}}\n@keyframes ckf-in{from{opacity:0;transform:translateY(6px)}}\n.ckf-banner{animation-name:ckf-fade}\n@media (max-width:520px){.ckf-scrim{align-items:flex-end;padding:0}.ckf-scrim .ckf-card{max-width:none;border-radius:var(--ckf-radius) var(--ckf-radius) 0 0;max-height:92dvh}.ckf-float{right:12px;bottom:12px}}\n@media (prefers-reduced-motion:reduce){.ckf-root *{animation:none!important;transition:none!important}}\n`;\n\ntype Attrs = Record<string, string | boolean | undefined | ((e: Event) => void)>;\n\n/** A small element builder: attributes, `on*` handlers and children (strings become text, never HTML). */\nconst h = (tag: string, attrs: Attrs = {}, ...children: (Node | string | false | null | undefined)[]) => {\n const el = document.createElement(tag);\n for (const [k, v] of Object.entries(attrs)) {\n if (v === undefined || v === false) continue;\n if (typeof v === \"function\") el.addEventListener(k.slice(2).toLowerCase(), v);\n else if (k === \"className\") el.className = String(v);\n else el.setAttribute(k, v === true ? \"\" : v);\n }\n for (const c of children) if (c || c === \"\") el.append(typeof c === \"string\" ? document.createTextNode(c) : c);\n return el;\n};\n\nconst button = (label: string, kind: \"primary\" | \"danger\" | \"secondary\" | \"quiet\", onClick: () => void, opts: { disabled?: boolean; id?: string } = {}) =>\n h(\n \"button\",\n {\n type: \"button\",\n className: `ckf-btn${kind === \"secondary\" ? \"\" : ` ckf-${kind}`}`,\n disabled: !!opts.disabled,\n \"data-ckf-id\": opts.id,\n onClick: () => onClick(),\n },\n label,\n );\n\nconst closeButton = (label: string, onClick: () => void) =>\n h(\"button\", { type: \"button\", className: \"ckf-x\", \"aria-label\": label, \"data-ckf-id\": \"x\", onClick: () => onClick() }, \"×\");\n\nconst dots = (step: number, steps: number) =>\n steps > 1\n ? h(\n \"span\",\n { className: \"ckf-dots\", role: \"img\", \"aria-label\": `Step ${step} of ${steps}` },\n ...Array.from({ length: steps }, (_, i) => h(\"span\", { className: \"ckf-dot\", \"data-on\": i < step })),\n )\n : h(\"span\");\n\n/** A key that changes when the screen (not just its state) changes: then focus moves to the new screen. */\nconst screenKey = (v: FlowView) =>\n v.type === \"cancel\" ? `c:${v.screen}` : v.type === \"step\" ? `s:${v.step.id}` : v.type === \"checklist\" ? `l:${v.flow.id}` : `m:${v.message.id}`;\n\n/**\n * Draws the controller's view into the page until the returned function is called. One root element under <body>;\n * nothing is drawn while there is nothing to show.\n */\nexport const mountFlows = (ctl: FlowsController, theme: FlowsTheme = {}): (() => void) => {\n if (typeof document === \"undefined\") return () => {};\n if (!document.getElementById(STYLE_ID)) {\n const style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = FLOWS_CSS;\n document.head.appendChild(style);\n }\n const root = h(\"div\", { className: `ckf-root${theme.className ? ` ${theme.className}` : \"\"}`, \"data-theme\": theme.theme ?? \"auto\" });\n if (theme.accent) root.style.setProperty(\"--ckf-accent\", theme.accent);\n document.body.appendChild(root);\n\n let lastKey = \"\";\n let returnFocus: HTMLElement | null = null;\n let comment = \"\";\n let placed: { float: HTMLElement; ring: HTMLElement; target: HTMLElement } | null = null;\n\n // ---------- Cancel flow ----------\n\n const cancelCard = (v: Extract<FlowView, { type: \"cancel\" }>) => {\n const titleId = \"ckf-title\";\n const busy = v.busy;\n const body: (Node | false)[] = [];\n if (v.screen === \"survey\") {\n body.push(\n h(\"h2\", { className: \"ckf-h\", id: titleId }, v.flow.question || \"Why are you leaving?\"),\n h(\n \"div\",\n { className: \"ckf-opts\", role: \"radiogroup\", \"aria-labelledby\": titleId },\n ...v.flow.reasons.map((r) =>\n h(\n \"button\",\n {\n type: \"button\",\n role: \"radio\",\n className: \"ckf-opt\",\n \"aria-checked\": String(v.reason === r.id),\n \"data-ckf-id\": `r:${r.id}`,\n onClick: () => ctl.cancel.pick(r.id),\n },\n h(\"span\", { className: \"ckf-radio\" }),\n r.label,\n ),\n ),\n ),\n h(\n \"div\",\n { className: \"ckf-btns\" },\n button(\"Continue\", \"primary\", ctl.cancel.next, { disabled: !v.reason, id: \"go\" }),\n button(\"Never mind, keep my plan\", \"quiet\", ctl.cancel.close),\n ),\n );\n } else if (v.screen === \"offer\" && v.offer) {\n const o = v.offer;\n const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;\n let detail: Node | false = false;\n if (o.kind === \"discount\" && o.percent)\n detail = v.price\n ? h(\n \"div\",\n { className: \"ckf-box\" },\n h(\"span\", { className: \"ckf-big\" }, money(discounted(v.price.amount, o.percent), v.price.currency)),\n h(\"span\", { className: \"ckf-strike\" }, money(v.price.amount, v.price.currency)),\n h(\n \"span\",\n { className: \"ckf-small\" },\n `/${v.price.interval ?? \"month\"}${o.months ? ` for ${o.months} ${o.months === 1 ? \"month\" : \"months\"}` : \"\"}`,\n ),\n )\n : h(\n \"div\",\n { className: \"ckf-box\" },\n h(\"span\", { className: \"ckf-big\" }, `${o.percent}% off`),\n o.months ? h(\"span\", { className: \"ckf-small\" }, `for ${o.months} ${o.months === 1 ? \"month\" : \"months\"}`) : false,\n );\n if (o.kind === \"pause\" && o.months)\n detail = h(\n \"div\",\n { className: \"ckf-box\" },\n h(\"span\", {}, `Pause for up to ${o.months} ${o.months === 1 ? \"month\" : \"months\"}. Your data stays.`),\n );\n if (o.kind === \"downgrade\" && o.planName) detail = h(\"div\", { className: \"ckf-box\" }, h(\"b\", {}, o.planName));\n const text =\n o.kind === \"contact\"\n ? (h(\"textarea\", {\n className: \"ckf-text\",\n placeholder: \"What would make you stay?\",\n \"aria-label\": \"Your message\",\n maxlength: \"500\",\n \"data-ckf-id\": \"comment\",\n onInput: (e) => (comment = (e.target as HTMLTextAreaElement).value),\n }) as HTMLTextAreaElement)\n : null;\n if (text) text.value = comment;\n body.push(\n label ? h(\"p\", { className: \"ckf-small\" }, `You said: ${label}`) : false,\n h(\"h2\", { className: \"ckf-h\", id: titleId }, o.headline),\n detail,\n text ?? false,\n h(\n \"div\",\n { className: \"ckf-btns\" },\n button(busy ? \"One moment…\" : offerButton(o), \"primary\", () => ctl.cancel.accept(comment), { disabled: busy, id: \"go\" }),\n button(\"No thanks, continue cancelling\", \"quiet\", ctl.cancel.decline, { disabled: busy }),\n ),\n );\n } else {\n const plain = v.steps === 1;\n const text = plain\n ? null\n : (h(\"textarea\", {\n className: \"ckf-text\",\n placeholder: \"Anything else we should know? (optional)\",\n \"aria-label\": \"Anything else we should know? (optional)\",\n maxlength: \"500\",\n \"data-ckf-id\": \"comment\",\n onInput: (e) => (comment = (e.target as HTMLTextAreaElement).value),\n }) as HTMLTextAreaElement);\n if (text) text.value = comment;\n body.push(\n h(\"h2\", { className: \"ckf-h\", id: titleId }, v.flow.confirm.title),\n v.flow.confirm.body ? h(\"p\", { className: \"ckf-p\" }, v.flow.confirm.body) : false,\n text ?? false,\n h(\n \"div\",\n { className: \"ckf-btns\" },\n button(busy ? \"One moment…\" : v.flow.confirm.button, \"danger\", () => ctl.cancel.confirm(comment), { disabled: busy, id: \"go\" }),\n button(\"Keep my plan\", \"secondary\", ctl.cancel.close, { disabled: busy }),\n ),\n );\n }\n return h(\n \"div\",\n { className: \"ckf-scrim\", onClick: (e) => e.target === e.currentTarget && !busy && ctl.cancel.close() },\n h(\n \"div\",\n { className: \"ckf-card\", role: \"dialog\", \"aria-modal\": \"true\", \"aria-labelledby\": titleId },\n h(\"div\", { className: \"ckf-top\" }, dots(v.step, v.steps), closeButton(\"Close\", ctl.cancel.close)),\n ...body,\n v.error ? h(\"p\", { className: \"ckf-err\", role: \"alert\" }, v.error) : false,\n ),\n );\n };\n\n // ---------- Onboarding ----------\n\n const stepCard = (v: Extract<FlowView, { type: \"step\" }>) => {\n const s = v.step;\n const card = h(\n \"div\",\n { className: \"ckf-card\", role: \"dialog\", \"aria-modal\": s.kind === \"modal\" ? \"true\" : undefined, \"aria-labelledby\": \"ckf-title\" },\n h(\n \"div\",\n { className: \"ckf-top\" },\n v.total > 1 ? h(\"p\", { className: \"ckf-small\" }, `Step ${v.index + 1} of ${v.total}`) : h(\"span\"),\n closeButton(\"Close the tour\", ctl.onboarding.dismiss),\n ),\n h(\"h2\", { className: \"ckf-h\", id: \"ckf-title\" }, s.title),\n s.body ? h(\"p\", { className: \"ckf-p\" }, s.body) : false,\n h(\"div\", { className: \"ckf-row\" }, button(s.button || (v.last ? \"Done\" : \"Next\"), \"primary\", ctl.onboarding.next, { id: \"go\" })),\n );\n if (s.kind === \"modal\") return h(\"div\", { className: \"ckf-scrim\" }, card);\n const target = s.target ? document.querySelector<HTMLElement>(`[data-ck-target=\"${CSS.escape(s.target)}\"]`) : null;\n const float = h(\"div\", { className: \"ckf-float\" }, card);\n if (target) {\n const ring = h(\"div\", { className: \"ckf-ring\", \"aria-hidden\": \"true\" });\n placed = { float, ring, target };\n place(float, ring, target);\n return h(\"div\", {}, ring, float);\n }\n return float;\n };\n\n /** Puts a tour card next to the element it points at (below it, or above when there is no room). */\n const place = (float: HTMLElement, ring: HTMLElement, target: HTMLElement) => {\n const r = target.getBoundingClientRect();\n Object.assign(ring.style, { left: `${r.left - 4}px`, top: `${r.top - 4}px`, width: `${r.width + 8}px`, height: `${r.height + 8}px` });\n const w = Math.min(340, window.innerWidth - 24);\n const left = Math.max(12, Math.min(r.left, window.innerWidth - w - 12));\n const below = r.bottom + 12;\n Object.assign(float.style, { left: `${left}px`, right: \"auto\", width: `${w}px` });\n if (below + 200 < window.innerHeight) Object.assign(float.style, { top: `${below}px`, bottom: \"auto\" });\n else Object.assign(float.style, { top: \"auto\", bottom: `${Math.max(12, window.innerHeight - r.top + 12)}px` });\n };\n\n const checklist = (v: Extract<FlowView, { type: \"checklist\" }>) => {\n const done = v.items.filter((i) => i.done).length;\n return h(\n \"div\",\n { className: \"ckf-float\" },\n h(\n \"div\",\n { className: \"ckf-card\", role: \"region\", \"aria-labelledby\": \"ckf-title\" },\n h(\n \"div\",\n { className: \"ckf-top\" },\n h(\"h2\", { className: \"ckf-h\", id: \"ckf-title\" }, v.flow.name),\n closeButton(\"Hide the checklist\", ctl.onboarding.dismiss),\n ),\n h(\"p\", { className: \"ckf-small\" }, `${done} of ${v.items.length} done`),\n h(\n \"div\",\n { className: \"ckf-bar\", role: \"progressbar\", \"aria-valuemin\": \"0\", \"aria-valuemax\": String(v.items.length), \"aria-valuenow\": String(done) },\n h(\"i\", { style: `width:${(done / v.items.length) * 100}%` } as Attrs),\n ),\n h(\n \"div\",\n { className: \"ckf-opts\" },\n ...v.items.map(({ step, done: d }) => {\n const inner = [\n h(\"span\", { className: \"ckf-tick\", \"aria-hidden\": \"true\" }, d ? \"✓\" : \"\"),\n h(\"span\", {}, h(\"b\", {}, step.title), step.body ? h(\"span\", { className: \"ckf-small\" }, step.body) : false),\n ];\n // An item with an event ticks itself when the app records it; the others are ticked by hand.\n return d || step.event\n ? h(\"div\", { className: \"ckf-check\", \"data-done\": d, role: \"listitem\" }, ...inner)\n : h(\n \"button\",\n {\n type: \"button\",\n className: \"ckf-check\",\n \"aria-label\": `Mark “${step.title}” done`,\n \"data-ckf-id\": `i:${step.id}`,\n onClick: () => ctl.onboarding.check(step.id),\n },\n ...inner,\n );\n }),\n ),\n ),\n );\n };\n\n // ---------- Messages ----------\n\n const message = (v: Extract<FlowView, { type: \"message\" }>) => {\n const m = v.message;\n const action = m.button ? button(m.button, \"primary\", ctl.message.click, { id: \"go\" }) : false;\n if (m.style === \"banner\")\n return h(\n \"div\",\n { className: \"ckf-banner\", role: \"status\" },\n h(\"span\", {}, h(\"b\", {}, m.title), m.body ? h(\"span\", { className: \"ckf-small\" }, m.body) : false),\n action,\n closeButton(\"Dismiss\", ctl.message.dismiss),\n );\n return h(\n \"div\",\n { className: \"ckf-scrim\", onClick: (e) => e.target === e.currentTarget && ctl.message.dismiss() },\n h(\n \"div\",\n { className: \"ckf-card\", role: \"dialog\", \"aria-modal\": \"true\", \"aria-labelledby\": \"ckf-title\" },\n h(\"div\", { className: \"ckf-top\" }, h(\"span\"), closeButton(\"Dismiss\", ctl.message.dismiss)),\n h(\"h2\", { className: \"ckf-h\", id: \"ckf-title\" }, m.title),\n m.body ? h(\"p\", { className: \"ckf-p\" }, m.body) : false,\n action ? h(\"div\", { className: \"ckf-row\" }, action) : false,\n ),\n );\n };\n\n // ---------- Drawing ----------\n\n const draw = () => {\n const v = ctl.view();\n const key = v ? screenKey(v) : \"\";\n const focused = (document.activeElement as HTMLElement | null)?.closest?.(\".ckf-root\") ? document.activeElement?.getAttribute(\"data-ckf-id\") : null;\n if (key && !lastKey && document.activeElement instanceof HTMLElement && !document.activeElement.closest(\".ckf-root\"))\n returnFocus = document.activeElement;\n if (key !== lastKey && !key.startsWith(\"c:\")) comment = \"\";\n if (!key) comment = \"\";\n placed = null;\n root.replaceChildren(\n ...(v ? [v.type === \"cancel\" ? cancelCard(v) : v.type === \"step\" ? stepCard(v) : v.type === \"checklist\" ? checklist(v) : message(v)] : []),\n );\n if (key !== lastKey) {\n // A new screen: focus its first control (a dialog's), so keyboard and screen reader users land in it.\n const modal = root.querySelector<HTMLElement>(\"[aria-modal=true]\");\n const first = modal?.querySelector<HTMLElement>(\"[role=radio], [data-ckf-id=go], textarea\") ?? null;\n first?.focus();\n if (!key && returnFocus) {\n returnFocus.focus();\n returnFocus = null;\n }\n } else if (focused) root.querySelector<HTMLElement>(`[data-ckf-id=\"${CSS.escape(focused)}\"]`)?.focus();\n lastKey = key;\n };\n\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n const v = ctl.view();\n if (v?.type === \"cancel\") ctl.cancel.close();\n else if (v?.type === \"message\") ctl.message.dismiss();\n else if (v?.type === \"step\" && v.step.kind === \"modal\") ctl.onboarding.dismiss();\n };\n // A tour card follows its element when the page scrolls or resizes.\n const follow = () => {\n if (placed) place(placed.float, placed.ring, placed.target);\n };\n document.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"resize\", follow);\n window.addEventListener(\"scroll\", follow, { passive: true, capture: true });\n const off = ctl.subscribe(draw);\n draw();\n return () => {\n off();\n document.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"resize\", follow);\n window.removeEventListener(\"scroll\", follow, { capture: true });\n root.remove();\n };\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/flows-ui.ts"],"sourcesContent":["/**\n * The web renderer of the cancel flow, onboarding and in-app messages (flows.ts): plain DOM, no React, so the Next.js\n * provider and the script tag share it. Both load it only when something is about to show (a dynamic import, and\n * `sdk-flows.js` for the script tag), so pages that show nothing never download it.\n *\n * Themeable three ways: `theme` (\"light\", \"dark\", \"auto\") and `accent` from the SDK's options, and CSS variables on\n * `.ckf-root` in the site's own stylesheet (`--ckf-accent`, `--ckf-bg`, `--ckf-fg`, `--ckf-muted`, `--ckf-line`,\n * `--ckf-radius`, `--ckf-font`). Every text is set with textContent; nothing from the server is parsed as HTML.\n */\nimport { type FlowView, type FlowsController, discounted, money, offerButton } from \"./flows\";\n\nexport interface FlowsTheme {\n theme?: \"light\" | \"dark\" | \"auto\";\n /** The main color of buttons and highlights, like \"#3f76ff\". */\n accent?: string;\n /** An extra class on the root, for the site's own CSS. */\n className?: string;\n}\n\nconst STYLE_ID = \"ckf-styles\";\n\nexport const FLOWS_CSS = `\n.ckf-root{--ckf-accent:#3f76ff;--ckf-accent-ink:#fff;--ckf-bg:#fff;--ckf-fg:#1c1d20;--ckf-muted:#5d6068;--ckf-soft:#f4f5f7;--ckf-line:#e4e5e8;--ckf-danger:#d92d20;--ckf-radius:14px;--ckf-font:inherit;--ckf-shadow:0 16px 40px -12px rgba(16,18,24,.28),0 0 0 1px rgba(16,18,24,.07)}\n.ckf-root[data-theme=dark]{--ckf-bg:#17181b;--ckf-fg:#f1f2f4;--ckf-muted:#a9acb3;--ckf-soft:#232428;--ckf-line:#2e2f35;--ckf-shadow:0 16px 40px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.08)}\n@media (prefers-color-scheme:dark){.ckf-root[data-theme=auto]{--ckf-bg:#17181b;--ckf-fg:#f1f2f4;--ckf-muted:#a9acb3;--ckf-soft:#232428;--ckf-line:#2e2f35;--ckf-shadow:0 16px 40px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.08)}}\n.ckf-root,.ckf-root *{box-sizing:border-box}\n.ckf-root{font-family:var(--ckf-font);font-size:14px;line-height:1.45;color:var(--ckf-fg);-webkit-font-smoothing:antialiased}\n.ckf-root :where(button,textarea){font:inherit;color:inherit;margin:0}\n.ckf-root :focus-visible{outline:2px solid var(--ckf-accent);outline-offset:2px}\n.ckf-scrim{position:fixed;inset:0;z-index:2147483002;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;padding:16px;animation:ckf-fade .15s ease-out}\n.ckf-card{position:relative;width:100%;max-width:400px;max-height:calc(100dvh - 32px);overflow-y:auto;background:var(--ckf-bg);color:var(--ckf-fg);border-radius:var(--ckf-radius);box-shadow:var(--ckf-shadow);padding:18px 20px 20px;display:flex;flex-direction:column;gap:12px;animation:ckf-in .18s ease-out}\n.ckf-float{position:fixed;z-index:2147483001;right:20px;bottom:20px;width:340px;max-width:calc(100vw - 24px)}\n.ckf-top{display:flex;align-items:center;gap:8px;min-height:20px}\n.ckf-dots{display:flex;gap:4px}\n.ckf-dot{width:16px;height:4px;border-radius:4px;background:var(--ckf-line)}\n.ckf-dot[data-on]{background:var(--ckf-accent)}\n.ckf-x{margin-left:auto;width:28px;height:28px;border:0;border-radius:8px;background:none;color:var(--ckf-muted);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:18px;line-height:1}\n.ckf-x:hover{background:var(--ckf-soft);color:var(--ckf-fg)}\n.ckf-h{margin:0;font-size:17px;font-weight:650;letter-spacing:-.01em;text-wrap:balance}\n.ckf-p{margin:0;color:var(--ckf-muted);text-wrap:pretty;white-space:pre-wrap}\n.ckf-small{margin:0;font-size:12px;color:var(--ckf-muted)}\n.ckf-opts{display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none}\n.ckf-opt{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;padding:8px 12px;border:1px solid var(--ckf-line);border-radius:10px;background:var(--ckf-bg);text-align:left;cursor:pointer;font-weight:500}\n.ckf-opt:hover{background:var(--ckf-soft)}\n.ckf-opt[aria-checked=true]{border-color:var(--ckf-accent);background:color-mix(in srgb,var(--ckf-accent) 8%,var(--ckf-bg))}\n.ckf-radio{width:16px;height:16px;flex:none;border-radius:50%;border:1.5px solid var(--ckf-line);display:inline-flex;align-items:center;justify-content:center}\n.ckf-opt[aria-checked=true] .ckf-radio{border-color:var(--ckf-accent)}\n.ckf-opt[aria-checked=true] .ckf-radio::after{content:\"\";width:8px;height:8px;border-radius:50%;background:var(--ckf-accent)}\n.ckf-box{border-radius:10px;background:var(--ckf-soft);padding:12px;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}\n.ckf-big{font-size:22px;font-weight:650;font-variant-numeric:tabular-nums}\n.ckf-strike{color:var(--ckf-muted);text-decoration:line-through;font-variant-numeric:tabular-nums}\n.ckf-text{width:100%;min-height:64px;resize:vertical;border:1px solid var(--ckf-line);border-radius:10px;background:var(--ckf-bg);padding:8px 10px}\n.ckf-text::placeholder{color:var(--ckf-muted)}\n.ckf-btns{display:flex;flex-direction:column;gap:6px}\n.ckf-btn{height:38px;padding:0 14px;border-radius:10px;border:1px solid var(--ckf-line);background:var(--ckf-bg);font-weight:600;cursor:pointer;transition:filter .1s linear,background-color .1s linear}\n.ckf-btn:hover{background:var(--ckf-soft)}\n.ckf-btn[disabled]{opacity:.55;cursor:not-allowed}\n.ckf-primary{background:var(--ckf-accent);border-color:var(--ckf-accent);color:var(--ckf-accent-ink)}\n.ckf-primary:hover{background:var(--ckf-accent);filter:brightness(1.07)}\n.ckf-danger{background:var(--ckf-danger);border-color:var(--ckf-danger);color:#fff}\n.ckf-danger:hover{background:var(--ckf-danger);filter:brightness(1.07)}\n.ckf-quiet{border-color:transparent;background:none;color:var(--ckf-muted);height:30px;font-weight:500}\n.ckf-quiet:hover{background:none;color:var(--ckf-fg)}\n.ckf-err{margin:0;color:var(--ckf-danger);font-size:13px;font-weight:500}\n.ckf-row{display:flex;justify-content:flex-end;gap:8px;align-items:center}\n.ckf-row .ckf-small{margin-right:auto}\n.ckf-ring{position:fixed;z-index:2147483000;pointer-events:none;border-radius:10px;box-shadow:0 0 0 2px var(--ckf-accent),0 0 0 6px color-mix(in srgb,var(--ckf-accent) 25%,transparent);transition:all .15s ease-out}\n.ckf-check{display:flex;align-items:flex-start;gap:10px;width:100%;padding:8px 6px;border:0;border-radius:8px;background:none;text-align:left}\n.ckf-check:where(button){cursor:pointer}\n.ckf-check:where(button):hover{background:var(--ckf-soft)}\n.ckf-tick{width:18px;height:18px;flex:none;margin-top:1px;border-radius:50%;border:1.5px solid var(--ckf-line);display:inline-flex;align-items:center;justify-content:center;color:var(--ckf-accent-ink);font-size:11px;font-weight:700}\n.ckf-check[data-done] .ckf-tick{background:var(--ckf-accent);border-color:var(--ckf-accent)}\n.ckf-check[data-done] b{color:var(--ckf-muted);text-decoration:line-through}\n.ckf-check span{display:flex;flex-direction:column;gap:2px;min-width:0}\n.ckf-bar{height:4px;border-radius:4px;background:var(--ckf-soft);overflow:hidden}\n.ckf-bar i{display:block;height:100%;background:var(--ckf-accent)}\n.ckf-banner{position:fixed;z-index:2147483001;left:50%;top:12px;transform:translateX(-50%);width:min(640px,calc(100vw - 24px));background:var(--ckf-bg);border-radius:12px;box-shadow:var(--ckf-shadow);padding:10px 10px 10px 16px;display:flex;align-items:center;gap:12px;animation:ckf-in .18s ease-out}\n.ckf-banner>span{flex:1;min-width:0;display:flex;flex-direction:column}\n.ckf-banner .ckf-btn{height:32px}\n@keyframes ckf-fade{from{opacity:0}}\n@keyframes ckf-in{from{opacity:0;transform:translateY(6px)}}\n.ckf-banner{animation-name:ckf-fade}\n@media (max-width:520px){.ckf-scrim{align-items:flex-end;padding:0}.ckf-scrim .ckf-card{max-width:none;border-radius:var(--ckf-radius) var(--ckf-radius) 0 0;max-height:92dvh}.ckf-float{right:12px;bottom:12px}}\n@media (prefers-reduced-motion:reduce){.ckf-root *{animation:none!important;transition:none!important}}\n`;\n\ntype Attrs = Record<string, string | boolean | undefined | ((e: Event) => void)>;\n\n/** A small element builder: attributes, `on*` handlers and children (strings become text, never HTML). */\nconst h = (tag: string, attrs: Attrs = {}, ...children: (Node | string | false | null | undefined)[]) => {\n const el = document.createElement(tag);\n for (const [k, v] of Object.entries(attrs)) {\n if (v === undefined || v === false) continue;\n if (typeof v === \"function\") el.addEventListener(k.slice(2).toLowerCase(), v);\n else if (k === \"className\") el.className = String(v);\n else el.setAttribute(k, v === true ? \"\" : v);\n }\n for (const c of children) if (c || c === \"\") el.append(typeof c === \"string\" ? document.createTextNode(c) : c);\n return el;\n};\n\nconst button = (label: string, kind: \"primary\" | \"danger\" | \"secondary\" | \"quiet\", onClick: () => void, opts: { disabled?: boolean; id?: string } = {}) =>\n h(\n \"button\",\n {\n type: \"button\",\n className: `ckf-btn${kind === \"secondary\" ? \"\" : ` ckf-${kind}`}`,\n disabled: !!opts.disabled,\n \"data-ckf-id\": opts.id,\n onClick: () => onClick(),\n },\n label,\n );\n\nconst closeButton = (label: string, onClick: () => void) =>\n h(\"button\", { type: \"button\", className: \"ckf-x\", \"aria-label\": label, \"data-ckf-id\": \"x\", onClick: () => onClick() }, \"×\");\n\nconst dots = (step: number, steps: number) =>\n steps > 1\n ? h(\n \"span\",\n { className: \"ckf-dots\", role: \"img\", \"aria-label\": `Step ${step} of ${steps}` },\n ...Array.from({ length: steps }, (_, i) => h(\"span\", { className: \"ckf-dot\", \"data-on\": i < step })),\n )\n : h(\"span\");\n\n/** A key that changes when the screen (not just its state) changes: then focus moves to the new screen. */\nconst screenKey = (v: FlowView) =>\n v.type === \"cancel\" ? `c:${v.screen}` : v.type === \"step\" ? `s:${v.step.id}` : v.type === \"checklist\" ? `l:${v.flow.id}` : `m:${v.message.id}`;\n\n/**\n * Draws the controller's view into the page until the returned function is called. One root element under <body>;\n * nothing is drawn while there is nothing to show.\n */\nexport const mountFlows = (ctl: FlowsController, theme: FlowsTheme = {}): (() => void) => {\n if (typeof document === \"undefined\") return () => {};\n if (!document.getElementById(STYLE_ID)) {\n const style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = FLOWS_CSS;\n document.head.appendChild(style);\n }\n const root = h(\"div\", { className: `ckf-root${theme.className ? ` ${theme.className}` : \"\"}`, \"data-theme\": theme.theme ?? \"auto\" });\n if (theme.accent) root.style.setProperty(\"--ckf-accent\", theme.accent);\n document.body.appendChild(root);\n\n let lastKey = \"\";\n let returnFocus: HTMLElement | null = null;\n let comment = \"\";\n let placed: { float: HTMLElement; ring: HTMLElement; target: HTMLElement } | null = null;\n\n // ---------- Cancel flow ----------\n\n const cancelCard = (v: Extract<FlowView, { type: \"cancel\" }>) => {\n const titleId = \"ckf-title\";\n const busy = v.busy;\n const body: (Node | false)[] = [];\n if (v.screen === \"survey\") {\n body.push(\n h(\"h2\", { className: \"ckf-h\", id: titleId }, v.flow.question || \"Why are you leaving?\"),\n h(\n \"div\",\n { className: \"ckf-opts\", role: \"radiogroup\", \"aria-labelledby\": titleId },\n ...v.flow.reasons.map((r) =>\n h(\n \"button\",\n {\n type: \"button\",\n role: \"radio\",\n className: \"ckf-opt\",\n \"aria-checked\": String(v.reason === r.id),\n \"data-ckf-id\": `r:${r.id}`,\n onClick: () => ctl.cancel.pick(r.id),\n },\n h(\"span\", { className: \"ckf-radio\" }),\n r.label,\n ),\n ),\n ),\n h(\n \"div\",\n { className: \"ckf-btns\" },\n button(\"Continue\", \"primary\", ctl.cancel.next, { disabled: !v.reason, id: \"go\" }),\n button(\"Never mind, keep my plan\", \"quiet\", ctl.cancel.close),\n ),\n );\n } else if (v.screen === \"offer\" && v.offer) {\n const o = v.offer;\n const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;\n let detail: Node | false = false;\n if (o.kind === \"discount\" && o.percent)\n detail = v.price\n ? h(\n \"div\",\n { className: \"ckf-box\" },\n h(\"span\", { className: \"ckf-big\" }, money(discounted(v.price.amount, o.percent), v.price.currency)),\n h(\"span\", { className: \"ckf-strike\" }, money(v.price.amount, v.price.currency)),\n h(\n \"span\",\n { className: \"ckf-small\" },\n `/${v.price.interval ?? \"month\"}${o.months ? ` for ${o.months} ${o.months === 1 ? \"month\" : \"months\"}` : \"\"}`,\n ),\n )\n : h(\n \"div\",\n { className: \"ckf-box\" },\n h(\"span\", { className: \"ckf-big\" }, `${o.percent}% off`),\n o.months ? h(\"span\", { className: \"ckf-small\" }, `for ${o.months} ${o.months === 1 ? \"month\" : \"months\"}`) : false,\n );\n if (o.kind === \"pause\" && o.months)\n detail = h(\n \"div\",\n { className: \"ckf-box\" },\n h(\"span\", {}, `Pause for up to ${o.months} ${o.months === 1 ? \"month\" : \"months\"}. Your data stays.`),\n );\n if (o.kind === \"downgrade\" && o.planName) detail = h(\"div\", { className: \"ckf-box\" }, h(\"b\", {}, o.planName));\n const text =\n o.kind === \"contact\"\n ? (h(\"textarea\", {\n className: \"ckf-text\",\n placeholder: \"What would make you stay?\",\n \"aria-label\": \"Your message\",\n maxlength: \"500\",\n \"data-ckf-id\": \"comment\",\n onInput: (e) => (comment = (e.target as HTMLTextAreaElement).value),\n }) as HTMLTextAreaElement)\n : null;\n if (text) text.value = comment;\n body.push(\n label ? h(\"p\", { className: \"ckf-small\" }, `You said: ${label}`) : false,\n h(\"h2\", { className: \"ckf-h\", id: titleId }, o.headline),\n detail,\n text ?? false,\n h(\n \"div\",\n { className: \"ckf-btns\" },\n button(busy ? \"One moment…\" : offerButton(o), \"primary\", () => ctl.cancel.accept(comment), { disabled: busy, id: \"go\" }),\n button(\"No thanks, continue cancelling\", \"quiet\", ctl.cancel.decline, { disabled: busy }),\n ),\n );\n } else {\n const plain = v.steps === 1;\n const text = plain\n ? null\n : (h(\"textarea\", {\n className: \"ckf-text\",\n placeholder: \"Anything else we should know? (optional)\",\n \"aria-label\": \"Anything else we should know? (optional)\",\n maxlength: \"500\",\n \"data-ckf-id\": \"comment\",\n onInput: (e) => (comment = (e.target as HTMLTextAreaElement).value),\n }) as HTMLTextAreaElement);\n if (text) text.value = comment;\n body.push(\n h(\"h2\", { className: \"ckf-h\", id: titleId }, v.flow.confirm.title),\n v.flow.confirm.body ? h(\"p\", { className: \"ckf-p\" }, v.flow.confirm.body) : false,\n text ?? false,\n h(\n \"div\",\n { className: \"ckf-btns\" },\n button(busy ? \"One moment…\" : v.flow.confirm.button, \"danger\", () => ctl.cancel.confirm(comment), { disabled: busy, id: \"go\" }),\n button(\"Keep my plan\", \"secondary\", ctl.cancel.close, { disabled: busy }),\n ),\n );\n }\n return h(\n \"div\",\n { className: \"ckf-scrim\", onClick: (e) => e.target === e.currentTarget && !busy && ctl.cancel.close() },\n h(\n \"div\",\n { className: \"ckf-card\", role: \"dialog\", \"aria-modal\": \"true\", \"aria-labelledby\": titleId },\n h(\"div\", { className: \"ckf-top\" }, dots(v.step, v.steps), closeButton(\"Close\", ctl.cancel.close)),\n ...body,\n v.error ? h(\"p\", { className: \"ckf-err\", role: \"alert\" }, v.error) : false,\n ),\n );\n };\n\n // ---------- Onboarding ----------\n\n const stepCard = (v: Extract<FlowView, { type: \"step\" }>) => {\n const s = v.step;\n const card = h(\n \"div\",\n { className: \"ckf-card\", role: \"dialog\", \"aria-modal\": s.kind === \"modal\" ? \"true\" : undefined, \"aria-labelledby\": \"ckf-title\" },\n h(\n \"div\",\n { className: \"ckf-top\" },\n v.total > 1 ? h(\"p\", { className: \"ckf-small\" }, `Step ${v.index + 1} of ${v.total}`) : h(\"span\"),\n closeButton(\"Close the tour\", ctl.onboarding.dismiss),\n ),\n h(\"h2\", { className: \"ckf-h\", id: \"ckf-title\" }, s.title),\n s.body ? h(\"p\", { className: \"ckf-p\" }, s.body) : false,\n h(\"div\", { className: \"ckf-row\" }, button(s.button || (v.last ? \"Done\" : \"Next\"), \"primary\", ctl.onboarding.next, { id: \"go\" })),\n );\n if (s.kind === \"modal\") return h(\"div\", { className: \"ckf-scrim\" }, card);\n const target = s.target ? findTarget(s.target) : null;\n const float = h(\"div\", { className: \"ckf-float\" }, card);\n if (target) {\n const ring = h(\"div\", { className: \"ckf-ring\", \"aria-hidden\": \"true\" });\n placed = { float, ring, target };\n place(float, ring, target);\n return h(\"div\", {}, ring, float);\n }\n return float;\n };\n\n /**\n * The element a tour step points at: a CSS selector (`#new-project`, `.toolbar button`, picked on the site in\n * MRRocket), or a plain name for `data-ck-target=\"…\"`. A selector that does not parse falls back to the name.\n */\n const findTarget = (t: string): HTMLElement | null => {\n const named = () => document.querySelector<HTMLElement>(`[data-ck-target=\"${CSS.escape(t)}\"]`);\n if (!/^[#.[]|[\\s>:=]/.test(t)) return named();\n return safeQuery(t) ?? named();\n };\n const safeQuery = (sel: string) => {\n try {\n return document.querySelector<HTMLElement>(sel);\n } catch {\n return null;\n }\n };\n\n /** Puts a tour card next to the element it points at (below it, or above when there is no room). */\n const place = (float: HTMLElement, ring: HTMLElement, target: HTMLElement) => {\n const r = target.getBoundingClientRect();\n Object.assign(ring.style, { left: `${r.left - 4}px`, top: `${r.top - 4}px`, width: `${r.width + 8}px`, height: `${r.height + 8}px` });\n const w = Math.min(340, window.innerWidth - 24);\n const left = Math.max(12, Math.min(r.left, window.innerWidth - w - 12));\n const below = r.bottom + 12;\n Object.assign(float.style, { left: `${left}px`, right: \"auto\", width: `${w}px` });\n if (below + 200 < window.innerHeight) Object.assign(float.style, { top: `${below}px`, bottom: \"auto\" });\n else Object.assign(float.style, { top: \"auto\", bottom: `${Math.max(12, window.innerHeight - r.top + 12)}px` });\n };\n\n const checklist = (v: Extract<FlowView, { type: \"checklist\" }>) => {\n const done = v.items.filter((i) => i.done).length;\n return h(\n \"div\",\n { className: \"ckf-float\" },\n h(\n \"div\",\n { className: \"ckf-card\", role: \"region\", \"aria-labelledby\": \"ckf-title\" },\n h(\n \"div\",\n { className: \"ckf-top\" },\n h(\"h2\", { className: \"ckf-h\", id: \"ckf-title\" }, v.flow.name),\n closeButton(\"Hide the checklist\", ctl.onboarding.dismiss),\n ),\n h(\"p\", { className: \"ckf-small\" }, `${done} of ${v.items.length} done`),\n h(\n \"div\",\n { className: \"ckf-bar\", role: \"progressbar\", \"aria-valuemin\": \"0\", \"aria-valuemax\": String(v.items.length), \"aria-valuenow\": String(done) },\n h(\"i\", { style: `width:${(done / v.items.length) * 100}%` } as Attrs),\n ),\n h(\n \"div\",\n { className: \"ckf-opts\" },\n ...v.items.map(({ step, done: d }) => {\n const inner = [\n h(\"span\", { className: \"ckf-tick\", \"aria-hidden\": \"true\" }, d ? \"✓\" : \"\"),\n h(\"span\", {}, h(\"b\", {}, step.title), step.body ? h(\"span\", { className: \"ckf-small\" }, step.body) : false),\n ];\n // An item with an event ticks itself when the app records it; the others are ticked by hand.\n return d || step.event\n ? h(\"div\", { className: \"ckf-check\", \"data-done\": d, role: \"listitem\" }, ...inner)\n : h(\n \"button\",\n {\n type: \"button\",\n className: \"ckf-check\",\n \"aria-label\": `Mark “${step.title}” done`,\n \"data-ckf-id\": `i:${step.id}`,\n onClick: () => ctl.onboarding.check(step.id),\n },\n ...inner,\n );\n }),\n ),\n ),\n );\n };\n\n // ---------- Messages ----------\n\n const message = (v: Extract<FlowView, { type: \"message\" }>) => {\n const m = v.message;\n const action = m.button ? button(m.button, \"primary\", ctl.message.click, { id: \"go\" }) : false;\n if (m.style === \"banner\")\n return h(\n \"div\",\n { className: \"ckf-banner\", role: \"status\" },\n h(\"span\", {}, h(\"b\", {}, m.title), m.body ? h(\"span\", { className: \"ckf-small\" }, m.body) : false),\n action,\n closeButton(\"Dismiss\", ctl.message.dismiss),\n );\n return h(\n \"div\",\n { className: \"ckf-scrim\", onClick: (e) => e.target === e.currentTarget && ctl.message.dismiss() },\n h(\n \"div\",\n { className: \"ckf-card\", role: \"dialog\", \"aria-modal\": \"true\", \"aria-labelledby\": \"ckf-title\" },\n h(\"div\", { className: \"ckf-top\" }, h(\"span\"), closeButton(\"Dismiss\", ctl.message.dismiss)),\n h(\"h2\", { className: \"ckf-h\", id: \"ckf-title\" }, m.title),\n m.body ? h(\"p\", { className: \"ckf-p\" }, m.body) : false,\n action ? h(\"div\", { className: \"ckf-row\" }, action) : false,\n ),\n );\n };\n\n // ---------- Drawing ----------\n\n const draw = () => {\n const v = ctl.view();\n const key = v ? screenKey(v) : \"\";\n const focused = (document.activeElement as HTMLElement | null)?.closest?.(\".ckf-root\") ? document.activeElement?.getAttribute(\"data-ckf-id\") : null;\n if (key && !lastKey && document.activeElement instanceof HTMLElement && !document.activeElement.closest(\".ckf-root\"))\n returnFocus = document.activeElement;\n if (key !== lastKey && !key.startsWith(\"c:\")) comment = \"\";\n if (!key) comment = \"\";\n placed = null;\n root.replaceChildren(\n ...(v ? [v.type === \"cancel\" ? cancelCard(v) : v.type === \"step\" ? stepCard(v) : v.type === \"checklist\" ? checklist(v) : message(v)] : []),\n );\n if (key !== lastKey) {\n // A new screen: focus its first control (a dialog's), so keyboard and screen reader users land in it.\n const modal = root.querySelector<HTMLElement>(\"[aria-modal=true]\");\n const first = modal?.querySelector<HTMLElement>(\"[role=radio], [data-ckf-id=go], textarea\") ?? null;\n first?.focus();\n if (!key && returnFocus) {\n returnFocus.focus();\n returnFocus = null;\n }\n } else if (focused) root.querySelector<HTMLElement>(`[data-ckf-id=\"${CSS.escape(focused)}\"]`)?.focus();\n lastKey = key;\n };\n\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n const v = ctl.view();\n if (v?.type === \"cancel\") ctl.cancel.close();\n else if (v?.type === \"message\") ctl.message.dismiss();\n else if (v?.type === \"step\" && v.step.kind === \"modal\") ctl.onboarding.dismiss();\n };\n // A tour card follows its element when the page scrolls or resizes.\n const follow = () => {\n if (placed) place(placed.float, placed.ring, placed.target);\n };\n document.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"resize\", follow);\n window.addEventListener(\"scroll\", follow, { passive: true, capture: true });\n const off = ctl.subscribe(draw);\n draw();\n return () => {\n off();\n document.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"resize\", follow);\n window.removeEventListener(\"scroll\", follow, { capture: true });\n root.remove();\n };\n};\n"],"mappings":"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","names":[]}
|
package/dist/flows-web.js
CHANGED
|
@@ -29,6 +29,7 @@ var webFlowsEnv = (o) => ({
|
|
|
29
29
|
auto: o.auto,
|
|
30
30
|
storage: () => o.canStore() ? local : null,
|
|
31
31
|
openUrl: (url) => window.location.assign(url),
|
|
32
|
+
path: () => window.location.pathname,
|
|
32
33
|
fetchInApp: async (id) => {
|
|
33
34
|
if (!o.apiKey) return null;
|
|
34
35
|
const r = await fetch(`${o.host}/api/sdk/inapp?key=${encodeURIComponent(o.apiKey)}&distinctId=${encodeURIComponent(id)}`);
|
package/dist/flows-web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/flows-web.ts"],"sourcesContent":["/**\n * What the flows controller (flows.ts) needs in a browser, shared by the Next.js provider and the script tag: the\n * person's messages and onboarding progress from `GET /api/sdk/inapp`, progress kept in localStorage only while the\n * consent is \"cookies\", and links opened in the same tab. Plain functions; only called in the browser.\n */\nimport type { FlowsEnv, InAppState } from \"./flows\";\n\nexport interface WebFlowsOptions {\n host: string;\n apiKey: string;\n track: FlowsEnv[\"track\"];\n /** The identify() id while tracking is allowed; undefined otherwise (no messages, no stored progress then). */\n personId: () => string | undefined;\n /** True while the consent is \"cookies\": progress may be kept in localStorage. */\n canStore: () => boolean;\n auto?: boolean;\n}\n\nconst local = {\n get: (key: string) => {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n set: (key: string, value: string) => {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Private mode or full: progress stays in memory.\n }\n },\n};\n\n/** Removes what the flows kept in localStorage (consent withdrawn). */\nexport const forgetFlowsStorage = () => {\n try {\n for (let i = window.localStorage.length - 1; i >= 0; i--) {\n const key = window.localStorage.key(i);\n if (key?.startsWith(\"mrrocket:onboarding\")) window.localStorage.removeItem(key);\n }\n } catch {\n // No storage: nothing to delete.\n }\n};\n\nexport const webFlowsEnv = (o: WebFlowsOptions): FlowsEnv => ({\n track: o.track,\n personId: o.personId,\n auto: o.auto,\n storage: () => (o.canStore() ? local : null),\n openUrl: (url) => window.location.assign(url),\n fetchInApp: async (id) => {\n if (!o.apiKey) return null;\n const r = await fetch(`${o.host}/api/sdk/inapp?key=${encodeURIComponent(o.apiKey)}&distinctId=${encodeURIComponent(id)}`);\n return r.ok ? ((await r.json()) as InAppState) : null;\n },\n});\n"],"mappings":";AAkBA,IAAM,QAAQ;AAAA,EACV,KAAK,CAAC,QAAgB;AAClB,QAAI;AACA,aAAO,OAAO,aAAa,QAAQ,GAAG;AAAA,IAC1C,QAAQ;AACJ,aAAO;AAAA,IACX;AAAA,EACJ;AAAA,EACA,KAAK,CAAC,KAAa,UAAkB;AACjC,QAAI;AACA,aAAO,aAAa,QAAQ,KAAK,KAAK;AAAA,IAC1C,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;AAGO,IAAM,qBAAqB,MAAM;AACpC,MAAI;AACA,aAAS,IAAI,OAAO,aAAa,SAAS,GAAG,KAAK,GAAG,KAAK;AACtD,YAAM,MAAM,OAAO,aAAa,IAAI,CAAC;AACrC,UAAI,KAAK,WAAW,qBAAqB,EAAG,QAAO,aAAa,WAAW,GAAG;AAAA,IAClF;AAAA,EACJ,QAAQ;AAAA,EAER;AACJ;AAEO,IAAM,cAAc,CAAC,OAAkC;AAAA,EAC1D,OAAO,EAAE;AAAA,EACT,UAAU,EAAE;AAAA,EACZ,MAAM,EAAE;AAAA,EACR,SAAS,MAAO,EAAE,SAAS,IAAI,QAAQ;AAAA,EACvC,SAAS,CAAC,QAAQ,OAAO,SAAS,OAAO,GAAG;AAAA,EAC5C,YAAY,OAAO,OAAO;AACtB,QAAI,CAAC,EAAE,OAAQ,QAAO;AACtB,UAAM,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,sBAAsB,mBAAmB,EAAE,MAAM,CAAC,eAAe,mBAAmB,EAAE,CAAC,EAAE;AACxH,WAAO,EAAE,KAAO,MAAM,EAAE,KAAK,IAAoB;AAAA,EACrD;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/flows-web.ts"],"sourcesContent":["/**\n * What the flows controller (flows.ts) needs in a browser, shared by the Next.js provider and the script tag: the\n * person's messages and onboarding progress from `GET /api/sdk/inapp`, progress kept in localStorage only while the\n * consent is \"cookies\", and links opened in the same tab. Plain functions; only called in the browser.\n */\nimport type { FlowsEnv, InAppState } from \"./flows\";\n\nexport interface WebFlowsOptions {\n host: string;\n apiKey: string;\n track: FlowsEnv[\"track\"];\n /** The identify() id while tracking is allowed; undefined otherwise (no messages, no stored progress then). */\n personId: () => string | undefined;\n /** True while the consent is \"cookies\": progress may be kept in localStorage. */\n canStore: () => boolean;\n auto?: boolean;\n}\n\nconst local = {\n get: (key: string) => {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n set: (key: string, value: string) => {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Private mode or full: progress stays in memory.\n }\n },\n};\n\n/** Removes what the flows kept in localStorage (consent withdrawn). */\nexport const forgetFlowsStorage = () => {\n try {\n for (let i = window.localStorage.length - 1; i >= 0; i--) {\n const key = window.localStorage.key(i);\n if (key?.startsWith(\"mrrocket:onboarding\")) window.localStorage.removeItem(key);\n }\n } catch {\n // No storage: nothing to delete.\n }\n};\n\nexport const webFlowsEnv = (o: WebFlowsOptions): FlowsEnv => ({\n track: o.track,\n personId: o.personId,\n auto: o.auto,\n storage: () => (o.canStore() ? local : null),\n openUrl: (url) => window.location.assign(url),\n path: () => window.location.pathname,\n fetchInApp: async (id) => {\n if (!o.apiKey) return null;\n const r = await fetch(`${o.host}/api/sdk/inapp?key=${encodeURIComponent(o.apiKey)}&distinctId=${encodeURIComponent(id)}`);\n return r.ok ? ((await r.json()) as InAppState) : null;\n },\n});\n"],"mappings":";AAkBA,IAAM,QAAQ;AAAA,EACV,KAAK,CAAC,QAAgB;AAClB,QAAI;AACA,aAAO,OAAO,aAAa,QAAQ,GAAG;AAAA,IAC1C,QAAQ;AACJ,aAAO;AAAA,IACX;AAAA,EACJ;AAAA,EACA,KAAK,CAAC,KAAa,UAAkB;AACjC,QAAI;AACA,aAAO,aAAa,QAAQ,KAAK,KAAK;AAAA,IAC1C,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;AAGO,IAAM,qBAAqB,MAAM;AACpC,MAAI;AACA,aAAS,IAAI,OAAO,aAAa,SAAS,GAAG,KAAK,GAAG,KAAK;AACtD,YAAM,MAAM,OAAO,aAAa,IAAI,CAAC;AACrC,UAAI,KAAK,WAAW,qBAAqB,EAAG,QAAO,aAAa,WAAW,GAAG;AAAA,IAClF;AAAA,EACJ,QAAQ;AAAA,EAER;AACJ;AAEO,IAAM,cAAc,CAAC,OAAkC;AAAA,EAC1D,OAAO,EAAE;AAAA,EACT,UAAU,EAAE;AAAA,EACZ,MAAM,EAAE;AAAA,EACR,SAAS,MAAO,EAAE,SAAS,IAAI,QAAQ;AAAA,EACvC,SAAS,CAAC,QAAQ,OAAO,SAAS,OAAO,GAAG;AAAA,EAC5C,MAAM,MAAM,OAAO,SAAS;AAAA,EAC5B,YAAY,OAAO,OAAO;AACtB,QAAI,CAAC,EAAE,OAAQ,QAAO;AACtB,UAAM,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,sBAAsB,mBAAmB,EAAE,MAAM,CAAC,eAAe,mBAAmB,EAAE,CAAC,EAAE;AACxH,WAAO,EAAE,KAAO,MAAM,EAAE,KAAK,IAAoB;AAAA,EACrD;AACJ;","names":[]}
|
package/dist/flows.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* `finished` with `outcome: "saved"` into a save and with `outcome: "cancelled"` into a cancellation on the Churn page.
|
|
10
10
|
* MRRocket never changes a subscription: an accepted offer goes to the app's `onSave`, a confirmed cancel to its
|
|
11
11
|
* `onCancel`, and only after they succeed is the outcome recorded.
|
|
12
|
-
* - `$onboarding` `{flow, step?, action: "started" | "shown" | "completed" | "dismissed"}
|
|
12
|
+
* - `$onboarding` `{flow, step?, action: "started" | "shown" | "completed" | "dismissed"}`; since 0.11.0 a wait records
|
|
13
|
+
* "shown" when the person reaches it and "completed" when it is over, a condition `{action: "answered", answer: "yes"
|
|
14
|
+
* | "no"}` once it is decided.
|
|
13
15
|
* - `$inapp` `{message, journey, action: "shown" | "clicked" | "dismissed"}`; the journey moves the person on.
|
|
14
16
|
*/
|
|
15
17
|
import type { CockpitConfig } from "./core.js";
|
|
@@ -64,13 +66,37 @@ export interface OnboardingStep {
|
|
|
64
66
|
/** Checklist items: the event that ticks them (`track("<event>")`). Without it the person ticks it themselves. */
|
|
65
67
|
event?: string;
|
|
66
68
|
}
|
|
69
|
+
/** A pause in a flow (0.11.0): the next step shows this long after the one before was done. */
|
|
70
|
+
export interface OnboardingWait {
|
|
71
|
+
id: string;
|
|
72
|
+
kind: "wait";
|
|
73
|
+
amount: number;
|
|
74
|
+
unit: "hours" | "days";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A fork in a flow (0.11.0): "Did <event> happen?" (the person's events, here and on MRRocket) or "Is on <page>?" (the
|
|
78
|
+
* page they are on when they reach it; `*` at the end for everything below). Decided once, when the person reaches it;
|
|
79
|
+
* like a journey's branch it ends its line, the rest of the flow lives in its two paths.
|
|
80
|
+
*/
|
|
81
|
+
export interface OnboardingCondition {
|
|
82
|
+
id: string;
|
|
83
|
+
kind: "condition";
|
|
84
|
+
event?: string;
|
|
85
|
+
page?: string;
|
|
86
|
+
yes: OnboardingNode[];
|
|
87
|
+
no: OnboardingNode[];
|
|
88
|
+
}
|
|
89
|
+
export type OnboardingNode = OnboardingStep | OnboardingWait | OnboardingCondition;
|
|
67
90
|
/** A live onboarding flow's in-app steps (MRRocket › Conversion › Onboarding); its emails go out from MRRocket. */
|
|
68
91
|
export interface OnboardingFlow {
|
|
69
92
|
id: string;
|
|
70
93
|
name: string;
|
|
71
94
|
/** When it starts: an event's words ("Signed up", "First export"), or "First launch" / "First visit". */
|
|
72
95
|
trigger: string;
|
|
96
|
+
/** What every version shows: with `nodes`, only the in-app steps before the first wait or condition. */
|
|
73
97
|
steps: OnboardingStep[];
|
|
98
|
+
/** 0.11.0+: the whole flow with its waits and conditions. Read instead of `steps` when it is there. */
|
|
99
|
+
nodes?: OnboardingNode[];
|
|
74
100
|
}
|
|
75
101
|
/** An in-app message a Lifecycle journey has for this person. */
|
|
76
102
|
export interface InAppMessage {
|
|
@@ -83,14 +109,23 @@ export interface InAppMessage {
|
|
|
83
109
|
url?: string;
|
|
84
110
|
style: "banner" | "modal";
|
|
85
111
|
}
|
|
112
|
+
/** One flow's progress: the steps done, whether the person closed it, and (0.11.0) its decided conditions and times. */
|
|
113
|
+
export interface FlowProgress {
|
|
114
|
+
done: string[];
|
|
115
|
+
dismissed?: boolean;
|
|
116
|
+
/** Each decided condition's path. */
|
|
117
|
+
answers?: Record<string, "yes" | "no">;
|
|
118
|
+
/** When each step was done and each wait was reached (ms since 1970). */
|
|
119
|
+
at?: Record<string, number>;
|
|
120
|
+
reached?: Record<string, number>;
|
|
121
|
+
}
|
|
86
122
|
/** What `GET /api/sdk/inapp` answers for one signed-in person. */
|
|
87
123
|
export interface InAppState {
|
|
88
124
|
/** Progress per onboarding flow: the steps done and whether the person closed it. */
|
|
89
|
-
onboarding: Record<string,
|
|
90
|
-
done: string[];
|
|
91
|
-
dismissed?: boolean;
|
|
92
|
-
}>;
|
|
125
|
+
onboarding: Record<string, FlowProgress>;
|
|
93
126
|
messages: InAppMessage[];
|
|
127
|
+
/** 0.11.0: the events the flows' conditions ask about that this person did (as `eventSlug`s). */
|
|
128
|
+
events?: string[];
|
|
94
129
|
}
|
|
95
130
|
export interface CancelFlowResult {
|
|
96
131
|
/** "cancelled": confirmed; "saved": took the offer; "kept": closed the dialog without cancelling. */
|
|
@@ -170,6 +205,10 @@ export interface FlowsEnv {
|
|
|
170
205
|
openUrl?: (url: string) => void;
|
|
171
206
|
/** Start onboarding flows by their trigger on its own (default true). */
|
|
172
207
|
auto?: boolean;
|
|
208
|
+
/** The page the person is on (a path), for "Is on <page>?" conditions; without it they answer no. */
|
|
209
|
+
path?: () => string | undefined;
|
|
210
|
+
/** The clock, for tests. */
|
|
211
|
+
now?: () => number;
|
|
173
212
|
}
|
|
174
213
|
export interface FlowsController {
|
|
175
214
|
setConfig(config: CockpitConfig | null): void;
|
|
@@ -219,4 +258,10 @@ export declare const money: (amount: number, currency: string) => string;
|
|
|
219
258
|
export declare const offerButton: (o: CancelOffer) => "Send to the team" | "Pause instead" | "Switch plan" | "Accept offer";
|
|
220
259
|
/** A link a message may open: http(s), or a path of the app. Nothing else (no `javascript:`). */
|
|
221
260
|
export declare const safeUrl: (url: string | undefined) => string | undefined;
|
|
261
|
+
/** The flow's nodes: the whole tree (0.11.0 config), or the plain steps of an older one. */
|
|
262
|
+
export declare const nodesOf: (f: OnboardingFlow) => OnboardingNode[];
|
|
263
|
+
/** How long a wait lasts. */
|
|
264
|
+
export declare const waitMs: (w: Pick<OnboardingWait, "amount" | "unit">) => number;
|
|
265
|
+
/** Whether a path matches a condition's page: the same path, or below it for a pattern ending in `*`. */
|
|
266
|
+
export declare const pageMatches: (pattern: string, path: string) => boolean;
|
|
222
267
|
export declare const createFlows: (env: FlowsEnv) => FlowsController;
|
package/dist/flows.js
CHANGED
|
@@ -35,6 +35,19 @@ var safeUrl = (url) => {
|
|
|
35
35
|
const u = (url ?? "").trim();
|
|
36
36
|
return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
|
|
37
37
|
};
|
|
38
|
+
var nodesOf = (f) => f.nodes ?? f.steps;
|
|
39
|
+
var waitMs = (w) => Math.max(0, w.amount) * (w.unit === "hours" ? 36e5 : 864e5);
|
|
40
|
+
var pageMatches = (pattern, path) => {
|
|
41
|
+
const p = pattern.trim();
|
|
42
|
+
const at = (path.split(/[?#]/)[0] || "/").replace(/\/+$/, "") || "/";
|
|
43
|
+
if (!p) return false;
|
|
44
|
+
if (p.endsWith("*")) {
|
|
45
|
+
const base = p.slice(0, -1).replace(/\/+$/, "");
|
|
46
|
+
return !base || at === base || at.startsWith(`${base}/`);
|
|
47
|
+
}
|
|
48
|
+
return at === (p.replace(/\/+$/, "") || "/");
|
|
49
|
+
};
|
|
50
|
+
var stepsIn = (nodes) => nodes.flatMap((n) => n.kind === "condition" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === "wait" ? [] : [n]);
|
|
38
51
|
var createFlows = (env) => {
|
|
39
52
|
let config = null;
|
|
40
53
|
let cancel = null;
|
|
@@ -49,25 +62,99 @@ var createFlows = (env) => {
|
|
|
49
62
|
const shown = /* @__PURE__ */ new Set();
|
|
50
63
|
const listeners = /* @__PURE__ */ new Set();
|
|
51
64
|
const auto = env.auto !== false;
|
|
65
|
+
const seenEvents = /* @__PURE__ */ new Set();
|
|
66
|
+
const now = () => env.now?.() ?? Date.now();
|
|
67
|
+
let timer = null;
|
|
52
68
|
const flows = () => config?.onboarding ?? [];
|
|
53
69
|
const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
|
|
54
70
|
const prog = (id) => {
|
|
55
71
|
let p = progress.get(id);
|
|
56
|
-
if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false });
|
|
72
|
+
if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false, answers: /* @__PURE__ */ new Map(), at: /* @__PURE__ */ new Map(), reached: /* @__PURE__ */ new Map() });
|
|
57
73
|
return p;
|
|
58
74
|
};
|
|
75
|
+
let dirty = false;
|
|
76
|
+
const record = (props) => {
|
|
77
|
+
env.track(ONBOARDING_EVENT, props);
|
|
78
|
+
dirty = true;
|
|
79
|
+
};
|
|
80
|
+
const markDone = (p, id) => {
|
|
81
|
+
p.done.add(id);
|
|
82
|
+
p.at.set(id, now());
|
|
83
|
+
};
|
|
84
|
+
const decide = (c) => {
|
|
85
|
+
if (c.page?.trim()) {
|
|
86
|
+
const path = env.path?.();
|
|
87
|
+
return path === void 0 ? false : pageMatches(c.page, path);
|
|
88
|
+
}
|
|
89
|
+
if (c.event?.trim()) {
|
|
90
|
+
if (!known) return void 0;
|
|
91
|
+
for (const e of seenEvents) if (matchesTrigger(c.event, e)) return true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
96
|
+
const walkLine = (flow, line, p, live, before = 0, last) => {
|
|
97
|
+
for (let i = 0; i < line.length; i++) {
|
|
98
|
+
const n = line[i];
|
|
99
|
+
if (n.kind === "wait") {
|
|
100
|
+
if (p.done.has(n.id)) {
|
|
101
|
+
last = p.at.get(n.id) ?? last;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let since = p.reached.get(n.id);
|
|
105
|
+
if (since === void 0) {
|
|
106
|
+
if (!live) return { at: "pending" };
|
|
107
|
+
since = last ?? now();
|
|
108
|
+
p.reached.set(n.id, since);
|
|
109
|
+
record({ flow: flow.id, step: n.id, kind: "wait", action: "shown" });
|
|
110
|
+
}
|
|
111
|
+
const until = since + waitMs(n);
|
|
112
|
+
if (now() < until) return { at: "wait", until };
|
|
113
|
+
if (!live) return { at: "pending" };
|
|
114
|
+
markDone(p, n.id);
|
|
115
|
+
record({ flow: flow.id, step: n.id, kind: "wait", action: "completed" });
|
|
116
|
+
last = until;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (n.kind === "condition") {
|
|
120
|
+
let answer = p.answers.get(n.id);
|
|
121
|
+
if (answer === void 0) {
|
|
122
|
+
if (!live) return { at: "pending" };
|
|
123
|
+
answer = decide(n);
|
|
124
|
+
if (answer === void 0) return { at: "pending" };
|
|
125
|
+
p.answers.set(n.id, answer);
|
|
126
|
+
record({ flow: flow.id, step: n.id, kind: "condition", action: "answered", answer: answer ? "yes" : "no" });
|
|
127
|
+
}
|
|
128
|
+
return walkLine(flow, answer ? n.yes : n.no, p, live, before, last);
|
|
129
|
+
}
|
|
130
|
+
if (!p.done.has(n.id)) return { at: "step", step: n, line, index: i, before };
|
|
131
|
+
last = p.at.get(n.id) ?? last;
|
|
132
|
+
before++;
|
|
133
|
+
}
|
|
134
|
+
return { at: "end" };
|
|
135
|
+
};
|
|
59
136
|
const finished = (f) => {
|
|
60
137
|
const p = progress.get(f.id);
|
|
61
|
-
return !!p && (p.dismissed || f
|
|
138
|
+
return !!p && (p.dismissed || walkLine(f, nodesOf(f), p, false).at === "end");
|
|
62
139
|
};
|
|
63
140
|
const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
|
|
141
|
+
const eventsKeyOf = (pid) => `${PROGRESS_KEY}:events:${pid ?? "visitor"}`;
|
|
64
142
|
const save = () => {
|
|
143
|
+
dirty = false;
|
|
65
144
|
const store = env.storage?.();
|
|
66
145
|
if (!store) return;
|
|
67
146
|
const out = {};
|
|
68
|
-
for (const [id, p] of progress)
|
|
147
|
+
for (const [id, p] of progress)
|
|
148
|
+
out[id] = {
|
|
149
|
+
done: [...p.done],
|
|
150
|
+
...p.dismissed ? { dismissed: true } : {},
|
|
151
|
+
...p.answers.size ? { answers: Object.fromEntries([...p.answers].map(([k, v]) => [k, v ? "yes" : "no"])) } : {},
|
|
152
|
+
...p.at.size ? { at: Object.fromEntries(p.at) } : {},
|
|
153
|
+
...p.reached.size ? { reached: Object.fromEntries(p.reached) } : {}
|
|
154
|
+
};
|
|
69
155
|
try {
|
|
70
156
|
store.set(keyOf(env.personId()), JSON.stringify(out));
|
|
157
|
+
if (seenEvents.size) store.set(eventsKeyOf(env.personId()), JSON.stringify([...seenEvents].slice(-200)));
|
|
71
158
|
} catch {
|
|
72
159
|
}
|
|
73
160
|
};
|
|
@@ -76,23 +163,55 @@ var createFlows = (env) => {
|
|
|
76
163
|
const p = prog(id);
|
|
77
164
|
for (const d of s.done ?? []) p.done.add(d);
|
|
78
165
|
if (s.dismissed) p.dismissed = true;
|
|
166
|
+
for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
|
|
167
|
+
for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
|
|
168
|
+
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);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const wakeAt = (until) => {
|
|
172
|
+
if (timer) clearTimeout(timer);
|
|
173
|
+
timer = null;
|
|
174
|
+
const ms = until - now();
|
|
175
|
+
if (ms > 0 && ms <= 864e5) {
|
|
176
|
+
timer = setTimeout(() => (timer = null, emit()), ms + 50);
|
|
177
|
+
timer.unref?.();
|
|
79
178
|
}
|
|
80
179
|
};
|
|
180
|
+
let holding = false;
|
|
81
181
|
const onboardingView = () => {
|
|
182
|
+
holding = false;
|
|
82
183
|
const flow = flowById(active);
|
|
83
184
|
if (!flow) return null;
|
|
84
185
|
const p = prog(flow.id);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
186
|
+
if (p.dismissed) return null;
|
|
187
|
+
const w = walkLine(flow, nodesOf(flow), p, true);
|
|
188
|
+
if (dirty) save();
|
|
189
|
+
if (w.at === "wait") wakeAt(w.until);
|
|
190
|
+
holding = w.at === "wait" || w.at === "pending";
|
|
191
|
+
if (w.at !== "step") return null;
|
|
192
|
+
const { step, line, index } = w;
|
|
193
|
+
const isStep = (n) => !!n && n.kind !== "wait" && n.kind !== "condition";
|
|
88
194
|
if (step.kind === "checklist") {
|
|
89
|
-
let from =
|
|
90
|
-
while (from > 0 &&
|
|
91
|
-
let to =
|
|
92
|
-
while (to + 1 <
|
|
93
|
-
|
|
195
|
+
let from = index;
|
|
196
|
+
while (from > 0 && isStep(line[from - 1]) && line[from - 1].kind === "checklist") from--;
|
|
197
|
+
let to = index;
|
|
198
|
+
while (to + 1 < line.length && isStep(line[to + 1]) && line[to + 1].kind === "checklist") to++;
|
|
199
|
+
const items = line.slice(from, to + 1).filter(isStep);
|
|
200
|
+
for (const i of items)
|
|
201
|
+
if (!p.done.has(i.id) && !shown.has(`s:${i.id}`)) {
|
|
202
|
+
shown.add(`s:${i.id}`);
|
|
203
|
+
env.track(ONBOARDING_EVENT, { flow: flow.id, step: i.id, kind: "checklist", action: "shown" });
|
|
204
|
+
}
|
|
205
|
+
return { type: "checklist", flow, items: items.map((s) => ({ step: s, done: p.done.has(s.id) })) };
|
|
206
|
+
}
|
|
207
|
+
if (!shown.has(`s:${step.id}`)) {
|
|
208
|
+
shown.add(`s:${step.id}`);
|
|
209
|
+
env.track(ONBOARDING_EVENT, { flow: flow.id, step: step.id, kind: step.kind, action: "shown" });
|
|
94
210
|
}
|
|
95
|
-
|
|
211
|
+
const rest = line.slice(index + 1);
|
|
212
|
+
const fork = rest.findIndex((n) => n.kind === "condition");
|
|
213
|
+
const ahead = (fork < 0 ? rest : rest.slice(0, fork)).filter(isStep).length;
|
|
214
|
+
return { type: "step", flow, step, index: w.before, total: w.before + 1 + ahead, last: !ahead && fork < 0 };
|
|
96
215
|
};
|
|
97
216
|
const view = () => {
|
|
98
217
|
if (cancel) {
|
|
@@ -112,22 +231,13 @@ var createFlows = (env) => {
|
|
|
112
231
|
}
|
|
113
232
|
const ob = onboardingView();
|
|
114
233
|
if (ob) return ob;
|
|
115
|
-
if (active) active = null;
|
|
234
|
+
if (active && !holding) active = null;
|
|
116
235
|
const m = messages.find((x) => !handled.has(x.id));
|
|
117
236
|
return m ? { type: "message", message: m } : null;
|
|
118
237
|
};
|
|
119
238
|
const emit = () => {
|
|
120
239
|
const v = view();
|
|
121
|
-
if (v?.type === "
|
|
122
|
-
shown.add(`s:${v.step.id}`);
|
|
123
|
-
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "shown" });
|
|
124
|
-
} else if (v?.type === "checklist") {
|
|
125
|
-
for (const i of v.items)
|
|
126
|
-
if (!i.done && !shown.has(`s:${i.step.id}`)) {
|
|
127
|
-
shown.add(`s:${i.step.id}`);
|
|
128
|
-
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: i.step.id, kind: "checklist", action: "shown" });
|
|
129
|
-
}
|
|
130
|
-
} else if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
|
|
240
|
+
if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
|
|
131
241
|
shown.add(`m:${v.message.id}`);
|
|
132
242
|
env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "shown" });
|
|
133
243
|
}
|
|
@@ -149,13 +259,14 @@ var createFlows = (env) => {
|
|
|
149
259
|
const autoStart = () => {
|
|
150
260
|
if (!auto || active || !known) return;
|
|
151
261
|
if (!env.personId() && !env.storage?.()) return;
|
|
152
|
-
const next = flows().find((f) => startsOnLoad(f.trigger) && f.
|
|
262
|
+
const next = flows().find((f) => startsOnLoad(f.trigger) && nodesOf(f).length && !finished(f));
|
|
153
263
|
if (next) start(next);
|
|
154
264
|
};
|
|
155
265
|
const refresh = () => {
|
|
156
266
|
const pid = env.personId();
|
|
157
267
|
if (progressFor !== null && progressFor !== pid) {
|
|
158
268
|
progress.clear();
|
|
269
|
+
seenEvents.clear();
|
|
159
270
|
handled.clear();
|
|
160
271
|
active = null;
|
|
161
272
|
loadedFor = void 0;
|
|
@@ -167,6 +278,10 @@ var createFlows = (env) => {
|
|
|
167
278
|
try {
|
|
168
279
|
const raw = await store.get(keyOf(pid));
|
|
169
280
|
if (raw) merge(JSON.parse(raw));
|
|
281
|
+
const seen = await store.get(eventsKeyOf(pid));
|
|
282
|
+
if (seen) {
|
|
283
|
+
for (const e of JSON.parse(seen)) if (typeof e === "string") seenEvents.add(e);
|
|
284
|
+
}
|
|
170
285
|
} catch {
|
|
171
286
|
}
|
|
172
287
|
}
|
|
@@ -176,6 +291,7 @@ var createFlows = (env) => {
|
|
|
176
291
|
const state = await env.fetchInApp(pid).catch(() => null);
|
|
177
292
|
if (state && env.personId() === pid) {
|
|
178
293
|
merge(state.onboarding);
|
|
294
|
+
for (const e of state.events ?? []) if (typeof e === "string") seenEvents.add(eventSlug(e));
|
|
179
295
|
messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
|
|
180
296
|
}
|
|
181
297
|
}
|
|
@@ -252,29 +368,32 @@ var createFlows = (env) => {
|
|
|
252
368
|
refresh,
|
|
253
369
|
onEvent: (name) => {
|
|
254
370
|
if (!name || name.startsWith("$")) return;
|
|
371
|
+
const slug = eventSlug(name);
|
|
372
|
+
const fresh = !!slug && !seenEvents.has(slug);
|
|
373
|
+
if (fresh) seenEvents.add(slug);
|
|
255
374
|
const flow = flowById(active);
|
|
256
375
|
let ticked = false;
|
|
257
376
|
if (flow) {
|
|
258
377
|
const p = prog(flow.id);
|
|
259
|
-
for (const s of flow
|
|
378
|
+
for (const s of stepsIn(nodesOf(flow)))
|
|
260
379
|
if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
|
|
261
|
-
p
|
|
380
|
+
markDone(p, s.id);
|
|
262
381
|
ticked = true;
|
|
263
382
|
env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
|
|
264
383
|
}
|
|
265
384
|
}
|
|
266
|
-
if (ticked) {
|
|
385
|
+
if (ticked || fresh && flow) {
|
|
267
386
|
save();
|
|
268
387
|
emit();
|
|
269
|
-
}
|
|
388
|
+
} else if (fresh) save();
|
|
270
389
|
if (!auto || !env.personId() && !env.storage?.()) return;
|
|
271
|
-
const next = flows().find((f) => f.
|
|
390
|
+
const next = flows().find((f) => nodesOf(f).length && matchesTrigger(f.trigger, name) && !finished(f));
|
|
272
391
|
if (next) start(next);
|
|
273
392
|
},
|
|
274
393
|
openCancelFlow,
|
|
275
394
|
startOnboarding: (id) => {
|
|
276
|
-
const f = id ? flowById(id) : flows().find((x) => x.
|
|
277
|
-
if (!f || !f.
|
|
395
|
+
const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !finished(x));
|
|
396
|
+
if (!f || !nodesOf(f).length) return false;
|
|
278
397
|
const p = prog(f.id);
|
|
279
398
|
p.dismissed = false;
|
|
280
399
|
return start(f);
|
|
@@ -345,7 +464,7 @@ var createFlows = (env) => {
|
|
|
345
464
|
next: () => {
|
|
346
465
|
const v = onboardingView();
|
|
347
466
|
if (v?.type !== "step") return;
|
|
348
|
-
prog(v.flow.id)
|
|
467
|
+
markDone(prog(v.flow.id), v.step.id);
|
|
349
468
|
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
|
|
350
469
|
save();
|
|
351
470
|
emit();
|
|
@@ -355,7 +474,7 @@ var createFlows = (env) => {
|
|
|
355
474
|
if (v?.type !== "checklist") return;
|
|
356
475
|
const item = v.items.find((i) => i.step.id === stepId);
|
|
357
476
|
if (!item || item.done) return;
|
|
358
|
-
prog(v.flow.id)
|
|
477
|
+
markDone(prog(v.flow.id), stepId);
|
|
359
478
|
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
|
|
360
479
|
save();
|
|
361
480
|
emit();
|
|
@@ -401,9 +520,12 @@ export {
|
|
|
401
520
|
eventSlug,
|
|
402
521
|
matchesTrigger,
|
|
403
522
|
money,
|
|
523
|
+
nodesOf,
|
|
404
524
|
offerButton,
|
|
525
|
+
pageMatches,
|
|
405
526
|
runId,
|
|
406
527
|
safeUrl,
|
|
407
|
-
startsOnLoad
|
|
528
|
+
startsOnLoad,
|
|
529
|
+
waitMs
|
|
408
530
|
};
|
|
409
531
|
//# sourceMappingURL=flows.js.map
|