@mrrocket/sdk 0.10.0 → 0.11.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/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.\n * No React, no browser APIs, so the server helper, the proxy helper and the client share it.\n */\nimport type { CancelFlowConfig, OnboardingFlow } from \"./flows\";\n\nexport type VariantId = string;\n\n/** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */\nexport const SDK_VERSION = \"0.10.0\";\n\n/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */\nexport interface VariantConfig {\n id: VariantId;\n /** Share of traffic, 0 to 1. The weights of an experiment add up to 1. */\n weight: number;\n changes?: Record<string, string>;\n /**\n * A redirect test: where this variant sends the visitor instead of the test's page (a path like \"/pricing-b\" or a\n * full address). Control has none and stays. SDK 0.7.0+.\n */\n url?: string;\n}\n\nexport interface ExperimentConfig {\n /** The experiment's key, the same as in Cockpit. */\n key: string;\n status: \"running\" | \"draft\" | \"stopped\";\n /** Control first. */\n variants: VariantConfig[];\n /** A winner that shipped: everyone gets it, and nobody is assigned anymore. */\n winner?: VariantId;\n /** A redirect test: the path of the page it runs on (\"/pricing\"). Visitors there are sent to their variant's `url`. */\n path?: string;\n}\n\n/** A page redirect set in MRRocket (Conversion › Landing pages › Redirect): everyone on `from` goes to `to`. SDK 0.7.0+. */\nexport interface RedirectConfig {\n /** A path of the site, like \"/old-pricing\". */\n from: string;\n /** A path or a full address. */\n to: string;\n}\n\nexport interface CockpitConfig {\n /** The product the public key belongs to. */\n project: string;\n experiments: ExperimentConfig[];\n /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */\n referral?: { cookieDays: number };\n /** Session recordings: present only while the product records (Cockpit › Sessions). See `ReplayConfig`. */\n replay?: ReplayConfig;\n /** Feature flags of the product in Production (MRRocket › Build › Feature flags), evaluated by `useFlag` and `getFlag`. */\n flags?: FlagConfig[];\n /**\n * Published copy for texts the site marks (`<Text id>`, `data-ck-text`): a landing page's headline, subheadline and\n * button (`pricing.headline` …) and a paywall's (`paywall.<placement>.headline` …) as published in MRRocket. A\n * running test's variant changes them on top. Missing when nothing is published. SDK 0.5.0+.\n */\n texts?: Record<string, string>;\n /** The product's live cancel flow (MRRocket › Churn › Cancel flow), for `cancelFlow.open()`. SDK 0.7.0+. */\n cancelFlow?: CancelFlowConfig;\n /** The in-app steps of live onboarding flows (tours, checklists, modals). SDK 0.7.0+. */\n onboarding?: OnboardingFlow[];\n /** A live Lifecycle journey has in-app messages: signed-in people ask `/api/sdk/inapp` for theirs. SDK 0.7.0+. */\n messages?: boolean;\n /** Page redirects set in MRRocket; the SDK sends visitors on before the page shows (`redirectFor`). SDK 0.7.0+. */\n redirects?: RedirectConfig[];\n}\n\n/** One condition of a flag's audience, on a property of the person (`identify(id, props)`), the id or the page. */\nexport interface FlagCondition {\n /** \"Email\", \"Plan\", \"Country\", \"id\", \"path\" … Matched against the person's properties without regard to case. */\n property: string;\n op: \"is\" | \"is not\" | \"in\" | \"not in\" | \"≥\" | \"in cohort\";\n values: string[];\n}\n\n/** Users who match every condition, of whom `rollout` percent get the flag. No conditions: everyone. */\nexport interface FlagConditionSet {\n conditions: FlagCondition[];\n /** 0 to 100. */\n rollout: number;\n}\n\nexport interface FlagConfig {\n key: string;\n /** Off: everyone gets the off side. */\n on: boolean;\n /** The first set a person matches decides; none matched: off. */\n sets: FlagConditionSet[];\n /** Multivariate flags: the variants and their split in percent (control included). Missing for an on/off flag. */\n variants?: { key: string; split: number; payload?: string }[];\n}\n\n/** What a person is for flag conditions: their properties from identify(), their id and the page they are on. */\nexport type FlagPerson = Record<string, unknown>;\n\nconst propOf = (person: FlagPerson, name: string) => {\n const want = name.trim().toLowerCase();\n for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;\n return undefined;\n};\n\n/** `\"@acme.com\"` matches every address at that domain; anything else matches the whole value, without regard to case. */\nconst same = (actual: string, value: string) => {\n const a = actual.trim().toLowerCase();\n const v = value.trim().toLowerCase();\n return v.startsWith(\"@\") ? a.endsWith(v) : a === v;\n};\n\nconst matchesCondition = (c: FlagCondition, person: FlagPerson) => {\n // Cohorts live in MRRocket; a browser cannot know them, so a cohort condition never matches here.\n if (c.op === \"in cohort\") return false;\n const raw = propOf(person, c.property);\n const actual = raw === undefined || raw === null ? \"\" : String(raw);\n if (c.op === \"≥\") {\n const a = Number(actual);\n const b = Number(c.values[0]);\n return actual !== \"\" && Number.isFinite(a) && Number.isFinite(b) && a >= b;\n }\n const hit = actual !== \"\" && c.values.some((v) => same(actual, v));\n return c.op === \"is\" || c.op === \"in\" ? hit : !hit;\n};\n\n/**\n * The flag for one person: `false` (off), `true` (on, an on/off flag) or the variant's key (a multivariate flag). The\n * bucket is a stable hash of flag and person, so a person keeps their side as the rollout grows.\n */\nexport const evaluateFlag = (flag: FlagConfig, distinctId: string, person: FlagPerson = {}): boolean | string => {\n if (!flag.on) return false;\n const subject = { id: distinctId, ...person };\n const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };\n if (!set) return false;\n if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;\n if (!flag.variants?.length) return true;\n const u = flagUnit(`${flag.key}:variant:${distinctId}`) * 100;\n let acc = 0;\n for (const v of flag.variants) {\n acc += v.split;\n if (u < acc) return v.key;\n }\n return flag.variants[flag.variants.length - 1].key;\n};\n\n/** A flag of the config for this person; an unknown flag (or no config) is off. */\nexport const flagFor = (key: string, config: CockpitConfig | null, distinctId: string, person: FlagPerson = {}) => {\n const flag = config?.flags?.find((f) => f.key === key);\n return flag ? evaluateFlag(flag, distinctId, person) : false;\n};\n\n/** One JavaScript error as it goes over the wire to `POST /api/sdk/errors`. */\nexport interface ErrorReport {\n /** \"TypeError\", \"Error\" … */\n type: string;\n message: string;\n /** The raw stack, at most 8 KB. MRRocket parses it into frames. */\n stack?: string;\n /** \"onerror\" (uncaught), \"onunhandledrejection\" (a rejected promise nobody caught) or \"manual\" (captureError). */\n mechanism: \"onerror\" | \"onunhandledrejection\" | \"manual\";\n handled: boolean;\n url: string;\n ts: number;\n /** What happened just before: pages and clicks, newest last, at most 20. */\n crumbs?: { ts: number; type: \"navigation\" | \"click\" | \"lifecycle\"; message: string }[];\n /** Your release (a version or commit), from the provider's `release` prop. */\n release?: string;\n distinctId?: string;\n sessionId?: string;\n}\n\n/**\n * Session recordings of a product, from the config. Recording also needs the visitor's own replay consent\n * (`<CockpitProvider replay>` or `setConsent({ replay: true })`); without it nothing is recorded or buffered.\n */\nexport interface ReplayConfig {\n /**\n * Share of sessions uploaded from their start, 0 to 1 (default 0.1). The others keep only the last minute in\n * memory and upload it when something goes wrong: a JS error, a rage click, a `purchase…` or `checkout…` event.\n */\n sampleRate: number;\n}\n\n/**\n * What the SDK may send and store.\n * - \"cookieless\" (default): no cookie, no localStorage. Events go without a visitor id; the server counts visitors\n * with a hash of key, date, IP and user agent that changes every day.\n * - \"cookies\": a persistent `ck_id` cookie identifies the visitor across visits, `ck_sid` holds the session.\n * - \"off\": nothing is sent and nothing is stored.\n */\nexport type ConsentMode = \"cookieless\" | \"cookies\" | \"off\";\n\n/**\n * What `setConsent` takes: a mode as before, or the parts to change. `replay` is the separate consent for session\n * recordings (default false); it applies only while analytics is not \"off\".\n *\n * setConsent(\"cookies\");\n * setConsent({ replay: true });\n * setConsent({ analytics: \"cookieless\", replay: false });\n */\nexport type ConsentChoice = ConsentMode | { analytics?: ConsentMode; replay?: boolean };\n\n/** The campaign parameters the SDK reads from the landing URL. */\nexport const UTM_KEYS = [\"utm_source\", \"utm_medium\", \"utm_campaign\", \"utm_term\", \"utm_content\"] as const;\nexport type UtmKey = (typeof UTM_KEYS)[number];\n\n/** One event as it goes over the wire to `POST /api/sdk/events`. */\nexport interface CockpitEvent {\n event: string;\n /** The visitor. Missing in \"cookieless\" mode until identify(): the server then uses its daily visitor hash. */\n distinctId?: string;\n /** Ends after 30 minutes without an event. */\n sessionId?: string;\n props?: Record<string, unknown>;\n /** Milliseconds since the epoch. */\n ts: number;\n url?: string;\n /** `document.referrer` on the first page of a page load, the previous in-app URL after client-side navigation. */\n referrer?: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n /** The screen size in CSS pixels (points on a phone). */\n screen?: { w: number; h: number };\n /** The app that sent it (React Native only): MRRocket stores it with the event and fills OS and device from it. */\n app?: AppContext;\n}\n\n/** Where a React Native event comes from. Every field is optional except the platform; nothing identifies the person. */\nexport interface AppContext {\n /** \"ios\" or \"android\" (React Native's `Platform.OS`). */\n platform: string;\n /** The app's version, like \"1.4.0\" (`expo-application`'s `nativeApplicationVersion`). */\n appVersion?: string;\n /** The build number, like \"42\". */\n build?: string;\n /** The OS version, like \"17.4\". */\n osVersion?: string;\n /** The device model, like \"iPhone 15 Pro\" (`expo-device`). */\n deviceModel?: string;\n /** \"phone\", \"tablet\", \"desktop\" or \"tv\" when `expo-device` knows it. */\n deviceType?: string;\n /** The first preferred locale, like \"de-DE\". */\n locale?: string;\n}\n\n/** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */\nexport const PREVIEW_PARAM = \"ck_preview\";\n/** Set when Cockpit embeds the page: nothing is tracked, so previews never count as visits. */\nexport const EMBED_PARAM = \"ck_embed\";\n/** The cookie that keeps a visitor in the same variant across visits. Only written in \"cookies\" mode. */\nexport const ID_COOKIE = \"ck_id\";\n/** The cookie that holds the session id in \"cookies\" mode. */\nexport const SESSION_COOKIE = \"ck_sid\";\n/** A session ends after this long without an event. */\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\n/** The UTM parameters from a parameter lookup (`URLSearchParams.get` in a browser). Empty values are left out. */\nexport const utmFrom = (get: (name: string) => string | null | undefined): Partial<Record<UtmKey, string>> => {\n const out: Partial<Record<UtmKey, string>> = {};\n for (const key of UTM_KEYS) {\n const value = get(key)?.trim();\n if (value) out[key] = value.slice(0, 150);\n }\n return out;\n};\n\n/** The UTM parameters of a query string, like `\"?utm_source=x\"`. Empty values are left out. */\nexport const readUtm = (search: string): Partial<Record<UtmKey, string>> => {\n const params = new URLSearchParams(search);\n return utmFrom((k) => params.get(k));\n};\n\n/**\n * The query parameters of a URL or query string, without `URLSearchParams` (React Native's is incomplete): `+` is a\n * space, values are percent-decoded, the first of a repeated name wins.\n */\nexport const parseQuery = (url: string): Record<string, string> => {\n const out: Record<string, string> = {};\n const q = url.split(\"#\")[0];\n const at = q.indexOf(\"?\");\n if (at < 0) return out;\n const dec = (s: string) => {\n try {\n return decodeURIComponent(s.replace(/\\+/g, \" \"));\n } catch {\n return s;\n }\n };\n for (const pair of q.slice(at + 1).split(\"&\")) {\n if (!pair) continue;\n const eq = pair.indexOf(\"=\");\n const k = dec(eq < 0 ? pair : pair.slice(0, eq));\n if (k && !(k in out)) out[k] = eq < 0 ? \"\" : dec(pair.slice(eq + 1));\n }\n return out;\n};\n\n/** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */\nexport const hashUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0) / 4294967296;\n};\n\n/**\n * FNV-1a with murmur3's finalizer, mapped to [0, 1): ids that differ only at the end (\"user-1\", \"user-2\") still spread\n * evenly, so a 30 % rollout reaches about 30 % of people. Flags only; experiments keep `hashUnit` so nobody switches.\n */\nexport const flagUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return (h >>> 0) / 4294967296;\n};\n\n/** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */\nexport const assign = (experiment: ExperimentConfig, distinctId: string): VariantId => {\n const control = experiment.variants[0]?.id ?? \"control\";\n if (experiment.winner) return experiment.winner;\n if (experiment.status !== \"running\") return control;\n const u = hashUnit(`${experiment.key}:${distinctId}`);\n let acc = 0;\n for (const v of experiment.variants) {\n acc += v.weight;\n if (u < acc) return v.id;\n }\n return experiment.variants[experiment.variants.length - 1]?.id ?? control;\n};\n\n/** `\"a:b,c:d\"` → `{ a: \"b\", c: \"d\" }`. Ignores anything malformed. */\nexport const parsePreview = (value: string | string[] | null | undefined): Record<string, VariantId> => {\n const raw = Array.isArray(value) ? value[0] : value;\n if (!raw) return {};\n const out: Record<string, VariantId> = {};\n for (const pair of raw.split(\",\")) {\n const [key, variant] = pair.split(\":\");\n if (key && variant) out[key.trim()] = variant.trim();\n }\n return out;\n};\n\n/** A link that shows one variant of a page, for example in Cockpit's preview frames. */\nexport const previewHref = (url: string, forced: Record<string, VariantId>, embed = true) => {\n const [path, query = \"\"] = url.split(\"?\");\n const params = new URLSearchParams(query);\n const value = Object.entries(forced)\n .map(([k, v]) => `${k}:${v}`)\n .join(\",\");\n if (value) params.set(PREVIEW_PARAM, value);\n if (embed) params.set(EMBED_PARAM, \"1\");\n return `${path}?${params.toString()}`;\n};\n\n/** The variant for one experiment, from a forced preview first, then the config. Unknown experiments get control. */\n/** The experiments whose variants change the text `id`: showing that text is an exposure to them. */\nexport const experimentsOfText = (config: CockpitConfig | null, id: string) =>\n (config?.experiments ?? []).filter((e) => e.variants.some((v) => v.changes && id in v.changes)).map((e) => e.key);\n\nexport const resolveVariant = (key: string, config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>): VariantId => {\n if (preview[key]) return preview[key];\n const experiment = config?.experiments.find((e) => e.key === key);\n return experiment ? assign(experiment, distinctId) : \"control\";\n};\n\n/**\n * Every text change that applies to this visitor: the published texts first, then each experiment's in config order.\n * Control keeps the published (or the page's own) words, so only a non-control variant (assigned, forced in a preview,\n * or a shipped winner) changes them further.\n */\nexport const textChanges = (config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => {\n const out: Record<string, string> = { ...(config?.texts ?? {}) };\n for (const experiment of config?.experiments ?? []) {\n const id = resolveVariant(experiment.key, config, distinctId, preview);\n if (id === (experiment.variants[0]?.id ?? \"control\")) continue;\n Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});\n }\n return out;\n};\n\n/**\n * The query parameter a redirect test adds to the variant's address (`ck_x=<test>:<variant>`): the page there records\n * the exposure and takes it out of the address bar. It also keeps that page from redirecting again.\n */\nexport const EXPOSURE_PARAM = \"ck_x\";\n\n/** What `redirectFor` decided: go to `to`, and/or the redirect test this page belongs to and the visitor's variant. */\nexport interface RedirectDecision {\n /** Where to send the visitor now. Missing for control, which stays on the page. */\n to?: string;\n /** The redirect test, while it runs: record `$exposure` for it (on this page for control, on `to` otherwise). */\n experiment?: string;\n variant?: VariantId;\n}\n\nconst pathOf = (p: string) => p.replace(/\\/+$/, \"\") || \"/\";\n\n/**\n * Where a page sends the visitor, before it shows: a page redirect set in MRRocket first, else a redirect test that runs\n * on this path (the visitor's variant, by the same stable assignment as every test). Null when nothing applies.\n *\n * - The new address keeps the page's query (UTM parameters) and its hash; a running test adds `ck_x=<test>:<variant>`,\n * so the page it lands on records the exposure (and never redirects again).\n * - `distinctId` null (no stable id, as on a server without the `ck_id` cookie): running tests are left to the browser,\n * page redirects and shipped winners still apply.\n * - A redirect to the page itself is ignored.\n */\nexport const redirectFor = (config: CockpitConfig | null, href: string, distinctId: string | null, preview: Record<string, VariantId> = {}): RedirectDecision | null => {\n if (!config) return null;\n let here: URL;\n try {\n here = new URL(href);\n } catch {\n return null;\n }\n if (here.searchParams.has(EXPOSURE_PARAM)) return null;\n const path = pathOf(here.pathname);\n const go = (to: string, mark?: string) => {\n let t: URL;\n try {\n t = new URL(to, here);\n } catch {\n return undefined;\n }\n if (!/^https?:$/.test(t.protocol) || (t.origin === here.origin && pathOf(t.pathname) === path)) return undefined;\n here.searchParams.forEach((v, k) => {\n if (!t.searchParams.has(k)) t.searchParams.set(k, v);\n });\n if (mark) t.searchParams.set(EXPOSURE_PARAM, mark);\n if (!t.hash) t.hash = here.hash;\n return t.href;\n };\n for (const r of config.redirects ?? []) {\n const to = r.from && pathOf(r.from) === path ? go(r.to) : undefined;\n if (to) return { to };\n }\n for (const e of config.experiments) {\n if (!e.path || pathOf(e.path) !== path) continue;\n const running = e.status === \"running\" && !e.winner;\n if (running && distinctId === null && !preview[e.key]) return null;\n const variant = resolveVariant(e.key, config, distinctId ?? \"\", preview);\n const url = e.variants.find((v) => v.id === variant)?.url;\n const to = url ? go(url, running ? `${e.key}:${variant}` : undefined) : undefined;\n return running ? { to, experiment: e.key, variant } : to ? { to } : null;\n }\n return null;\n};\n\n/** The test and variant a redirect test's `ck_x` names, when the config has them; null otherwise. */\nexport const exposureFrom = (config: CockpitConfig | null, value: string | null | undefined) => {\n const [key, variant] = (value ?? \"\").split(\":\");\n const e = config?.experiments.find((x) => x.key === key);\n return e && e.variants.some((v) => v.id === variant) ? { experiment: key, variant } : null;\n};\n\n/**\n * The MRRocket API host from `NEXT_PUBLIC_MRROCKET_HOST`. Written as a literal property access on purpose: Next.js\n * inlines `process.env.NEXT_PUBLIC_*` at build time, in the browser bundle and in the server bundle (also on\n * Cloudflare Workers), and only replaces the literal form. Without a bundler (or a `process`) it is undefined.\n */\nexport const envHost = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_HOST || undefined;\n } catch {\n return undefined;\n }\n};\n\n/** The public key from `NEXT_PUBLIC_MRROCKET_KEY`, the default for the `apiKey` prop. Same literal-access rule as `envHost`. */\nexport const envKey = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_KEY || undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Where the API lives: the `host` prop, else `NEXT_PUBLIC_MRROCKET_HOST`, else \"\" (the page's own origin, which is how\n * MRRocket's own demo pages call it). Without a trailing slash.\n */\nexport const resolveHost = (host?: string) => (host ?? envHost() ?? \"\").trim().replace(/\\/+$/, \"\");\n\n/** A random visitor id. */\nexport const newDistinctId = () =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.\n * No React, no browser APIs, so the server helper, the proxy helper and the client share it.\n */\nimport type { CancelFlowConfig, OnboardingFlow } from \"./flows\";\n\nexport type VariantId = string;\n\n/** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */\nexport const SDK_VERSION = \"0.11.0\";\n\n/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */\nexport interface VariantConfig {\n id: VariantId;\n /** Share of traffic, 0 to 1. The weights of an experiment add up to 1. */\n weight: number;\n changes?: Record<string, string>;\n /**\n * A redirect test: where this variant sends the visitor instead of the test's page (a path like \"/pricing-b\" or a\n * full address). Control has none and stays. SDK 0.7.0+.\n */\n url?: string;\n}\n\nexport interface ExperimentConfig {\n /** The experiment's key, the same as in Cockpit. */\n key: string;\n status: \"running\" | \"draft\" | \"stopped\";\n /** Control first. */\n variants: VariantConfig[];\n /** A winner that shipped: everyone gets it, and nobody is assigned anymore. */\n winner?: VariantId;\n /** A redirect test: the path of the page it runs on (\"/pricing\"). Visitors there are sent to their variant's `url`. */\n path?: string;\n}\n\n/** A page redirect set in MRRocket (Conversion › Landing pages › Redirect): everyone on `from` goes to `to`. SDK 0.7.0+. */\nexport interface RedirectConfig {\n /** A path of the site, like \"/old-pricing\". */\n from: string;\n /** A path or a full address. */\n to: string;\n}\n\nexport interface CockpitConfig {\n /** The product the public key belongs to. */\n project: string;\n experiments: ExperimentConfig[];\n /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */\n referral?: { cookieDays: number };\n /** Session recordings: present only while the product records (Cockpit › Sessions). See `ReplayConfig`. */\n replay?: ReplayConfig;\n /** Feature flags of the product in Production (MRRocket › Build › Feature flags), evaluated by `useFlag` and `getFlag`. */\n flags?: FlagConfig[];\n /**\n * Published copy for texts the site marks (`<Text id>`, `data-ck-text`): a landing page's headline, subheadline and\n * button (`pricing.headline` …) and a paywall's (`paywall.<placement>.headline` …) as published in MRRocket. A\n * running test's variant changes them on top. Missing when nothing is published. SDK 0.5.0+.\n */\n texts?: Record<string, string>;\n /** The product's live cancel flow (MRRocket › Churn › Cancel flow), for `cancelFlow.open()`. SDK 0.7.0+. */\n cancelFlow?: CancelFlowConfig;\n /** The in-app steps of live onboarding flows (tours, checklists, modals). SDK 0.7.0+. */\n onboarding?: OnboardingFlow[];\n /** A live Lifecycle journey has in-app messages: signed-in people ask `/api/sdk/inapp` for theirs. SDK 0.7.0+. */\n messages?: boolean;\n /** Page redirects set in MRRocket; the SDK sends visitors on before the page shows (`redirectFor`). SDK 0.7.0+. */\n redirects?: RedirectConfig[];\n}\n\n/** One condition of a flag's audience, on a property of the person (`identify(id, props)`), the id or the page. */\nexport interface FlagCondition {\n /** \"Email\", \"Plan\", \"Country\", \"id\", \"path\" … Matched against the person's properties without regard to case. */\n property: string;\n op: \"is\" | \"is not\" | \"in\" | \"not in\" | \"≥\" | \"in cohort\";\n values: string[];\n}\n\n/** Users who match every condition, of whom `rollout` percent get the flag. No conditions: everyone. */\nexport interface FlagConditionSet {\n conditions: FlagCondition[];\n /** 0 to 100. */\n rollout: number;\n}\n\nexport interface FlagConfig {\n key: string;\n /** Off: everyone gets the off side. */\n on: boolean;\n /** The first set a person matches decides; none matched: off. */\n sets: FlagConditionSet[];\n /** Multivariate flags: the variants and their split in percent (control included). Missing for an on/off flag. */\n variants?: { key: string; split: number; payload?: string }[];\n}\n\n/** What a person is for flag conditions: their properties from identify(), their id and the page they are on. */\nexport type FlagPerson = Record<string, unknown>;\n\nconst propOf = (person: FlagPerson, name: string) => {\n const want = name.trim().toLowerCase();\n for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;\n return undefined;\n};\n\n/** `\"@acme.com\"` matches every address at that domain; anything else matches the whole value, without regard to case. */\nconst same = (actual: string, value: string) => {\n const a = actual.trim().toLowerCase();\n const v = value.trim().toLowerCase();\n return v.startsWith(\"@\") ? a.endsWith(v) : a === v;\n};\n\nconst matchesCondition = (c: FlagCondition, person: FlagPerson) => {\n // Cohorts live in MRRocket; a browser cannot know them, so a cohort condition never matches here.\n if (c.op === \"in cohort\") return false;\n const raw = propOf(person, c.property);\n const actual = raw === undefined || raw === null ? \"\" : String(raw);\n if (c.op === \"≥\") {\n const a = Number(actual);\n const b = Number(c.values[0]);\n return actual !== \"\" && Number.isFinite(a) && Number.isFinite(b) && a >= b;\n }\n const hit = actual !== \"\" && c.values.some((v) => same(actual, v));\n return c.op === \"is\" || c.op === \"in\" ? hit : !hit;\n};\n\n/**\n * The flag for one person: `false` (off), `true` (on, an on/off flag) or the variant's key (a multivariate flag). The\n * bucket is a stable hash of flag and person, so a person keeps their side as the rollout grows.\n */\nexport const evaluateFlag = (flag: FlagConfig, distinctId: string, person: FlagPerson = {}): boolean | string => {\n if (!flag.on) return false;\n const subject = { id: distinctId, ...person };\n const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };\n if (!set) return false;\n if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;\n if (!flag.variants?.length) return true;\n const u = flagUnit(`${flag.key}:variant:${distinctId}`) * 100;\n let acc = 0;\n for (const v of flag.variants) {\n acc += v.split;\n if (u < acc) return v.key;\n }\n return flag.variants[flag.variants.length - 1].key;\n};\n\n/** A flag of the config for this person; an unknown flag (or no config) is off. */\nexport const flagFor = (key: string, config: CockpitConfig | null, distinctId: string, person: FlagPerson = {}) => {\n const flag = config?.flags?.find((f) => f.key === key);\n return flag ? evaluateFlag(flag, distinctId, person) : false;\n};\n\n/** One JavaScript error as it goes over the wire to `POST /api/sdk/errors`. */\nexport interface ErrorReport {\n /** \"TypeError\", \"Error\" … */\n type: string;\n message: string;\n /** The raw stack, at most 8 KB. MRRocket parses it into frames. */\n stack?: string;\n /** \"onerror\" (uncaught), \"onunhandledrejection\" (a rejected promise nobody caught) or \"manual\" (captureError). */\n mechanism: \"onerror\" | \"onunhandledrejection\" | \"manual\";\n handled: boolean;\n url: string;\n ts: number;\n /** What happened just before: pages and clicks, newest last, at most 20. */\n crumbs?: { ts: number; type: \"navigation\" | \"click\" | \"lifecycle\"; message: string }[];\n /** Your release (a version or commit), from the provider's `release` prop. */\n release?: string;\n distinctId?: string;\n sessionId?: string;\n}\n\n/**\n * Session recordings of a product, from the config. Recording also needs the visitor's own replay consent\n * (`<CockpitProvider replay>` or `setConsent({ replay: true })`); without it nothing is recorded or buffered.\n */\nexport interface ReplayConfig {\n /**\n * Share of sessions uploaded from their start, 0 to 1 (default 0.1). The others keep only the last minute in\n * memory and upload it when something goes wrong: a JS error, a rage click, a `purchase…` or `checkout…` event.\n */\n sampleRate: number;\n}\n\n/**\n * What the SDK may send and store.\n * - \"cookieless\" (default): no cookie, no localStorage. Events go without a visitor id; the server counts visitors\n * with a hash of key, date, IP and user agent that changes every day.\n * - \"cookies\": a persistent `ck_id` cookie identifies the visitor across visits, `ck_sid` holds the session.\n * - \"off\": nothing is sent and nothing is stored.\n */\nexport type ConsentMode = \"cookieless\" | \"cookies\" | \"off\";\n\n/**\n * What `setConsent` takes: a mode as before, or the parts to change. `replay` is the separate consent for session\n * recordings (default false); it applies only while analytics is not \"off\".\n *\n * setConsent(\"cookies\");\n * setConsent({ replay: true });\n * setConsent({ analytics: \"cookieless\", replay: false });\n */\nexport type ConsentChoice = ConsentMode | { analytics?: ConsentMode; replay?: boolean };\n\n/** The campaign parameters the SDK reads from the landing URL. */\nexport const UTM_KEYS = [\"utm_source\", \"utm_medium\", \"utm_campaign\", \"utm_term\", \"utm_content\"] as const;\nexport type UtmKey = (typeof UTM_KEYS)[number];\n\n/** One event as it goes over the wire to `POST /api/sdk/events`. */\nexport interface CockpitEvent {\n event: string;\n /** The visitor. Missing in \"cookieless\" mode until identify(): the server then uses its daily visitor hash. */\n distinctId?: string;\n /** Ends after 30 minutes without an event. */\n sessionId?: string;\n props?: Record<string, unknown>;\n /** Milliseconds since the epoch. */\n ts: number;\n url?: string;\n /** `document.referrer` on the first page of a page load, the previous in-app URL after client-side navigation. */\n referrer?: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n /** The screen size in CSS pixels (points on a phone). */\n screen?: { w: number; h: number };\n /** The app that sent it (React Native only): MRRocket stores it with the event and fills OS and device from it. */\n app?: AppContext;\n}\n\n/** Where a React Native event comes from. Every field is optional except the platform; nothing identifies the person. */\nexport interface AppContext {\n /** \"ios\" or \"android\" (React Native's `Platform.OS`). */\n platform: string;\n /** The app's version, like \"1.4.0\" (`expo-application`'s `nativeApplicationVersion`). */\n appVersion?: string;\n /** The build number, like \"42\". */\n build?: string;\n /** The OS version, like \"17.4\". */\n osVersion?: string;\n /** The device model, like \"iPhone 15 Pro\" (`expo-device`). */\n deviceModel?: string;\n /** \"phone\", \"tablet\", \"desktop\" or \"tv\" when `expo-device` knows it. */\n deviceType?: string;\n /** The first preferred locale, like \"de-DE\". */\n locale?: string;\n}\n\n/** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */\nexport const PREVIEW_PARAM = \"ck_preview\";\n/** Set when Cockpit embeds the page: nothing is tracked, so previews never count as visits. */\nexport const EMBED_PARAM = \"ck_embed\";\n/** The cookie that keeps a visitor in the same variant across visits. Only written in \"cookies\" mode. */\nexport const ID_COOKIE = \"ck_id\";\n/** The cookie that holds the session id in \"cookies\" mode. */\nexport const SESSION_COOKIE = \"ck_sid\";\n/** A session ends after this long without an event. */\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\n/** The UTM parameters from a parameter lookup (`URLSearchParams.get` in a browser). Empty values are left out. */\nexport const utmFrom = (get: (name: string) => string | null | undefined): Partial<Record<UtmKey, string>> => {\n const out: Partial<Record<UtmKey, string>> = {};\n for (const key of UTM_KEYS) {\n const value = get(key)?.trim();\n if (value) out[key] = value.slice(0, 150);\n }\n return out;\n};\n\n/** The UTM parameters of a query string, like `\"?utm_source=x\"`. Empty values are left out. */\nexport const readUtm = (search: string): Partial<Record<UtmKey, string>> => {\n const params = new URLSearchParams(search);\n return utmFrom((k) => params.get(k));\n};\n\n/**\n * The query parameters of a URL or query string, without `URLSearchParams` (React Native's is incomplete): `+` is a\n * space, values are percent-decoded, the first of a repeated name wins.\n */\nexport const parseQuery = (url: string): Record<string, string> => {\n const out: Record<string, string> = {};\n const q = url.split(\"#\")[0];\n const at = q.indexOf(\"?\");\n if (at < 0) return out;\n const dec = (s: string) => {\n try {\n return decodeURIComponent(s.replace(/\\+/g, \" \"));\n } catch {\n return s;\n }\n };\n for (const pair of q.slice(at + 1).split(\"&\")) {\n if (!pair) continue;\n const eq = pair.indexOf(\"=\");\n const k = dec(eq < 0 ? pair : pair.slice(0, eq));\n if (k && !(k in out)) out[k] = eq < 0 ? \"\" : dec(pair.slice(eq + 1));\n }\n return out;\n};\n\n/** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */\nexport const hashUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0) / 4294967296;\n};\n\n/**\n * FNV-1a with murmur3's finalizer, mapped to [0, 1): ids that differ only at the end (\"user-1\", \"user-2\") still spread\n * evenly, so a 30 % rollout reaches about 30 % of people. Flags only; experiments keep `hashUnit` so nobody switches.\n */\nexport const flagUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return (h >>> 0) / 4294967296;\n};\n\n/** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */\nexport const assign = (experiment: ExperimentConfig, distinctId: string): VariantId => {\n const control = experiment.variants[0]?.id ?? \"control\";\n if (experiment.winner) return experiment.winner;\n if (experiment.status !== \"running\") return control;\n const u = hashUnit(`${experiment.key}:${distinctId}`);\n let acc = 0;\n for (const v of experiment.variants) {\n acc += v.weight;\n if (u < acc) return v.id;\n }\n return experiment.variants[experiment.variants.length - 1]?.id ?? control;\n};\n\n/** `\"a:b,c:d\"` → `{ a: \"b\", c: \"d\" }`. Ignores anything malformed. */\nexport const parsePreview = (value: string | string[] | null | undefined): Record<string, VariantId> => {\n const raw = Array.isArray(value) ? value[0] : value;\n if (!raw) return {};\n const out: Record<string, VariantId> = {};\n for (const pair of raw.split(\",\")) {\n const [key, variant] = pair.split(\":\");\n if (key && variant) out[key.trim()] = variant.trim();\n }\n return out;\n};\n\n/** A link that shows one variant of a page, for example in Cockpit's preview frames. */\nexport const previewHref = (url: string, forced: Record<string, VariantId>, embed = true) => {\n const [path, query = \"\"] = url.split(\"?\");\n const params = new URLSearchParams(query);\n const value = Object.entries(forced)\n .map(([k, v]) => `${k}:${v}`)\n .join(\",\");\n if (value) params.set(PREVIEW_PARAM, value);\n if (embed) params.set(EMBED_PARAM, \"1\");\n return `${path}?${params.toString()}`;\n};\n\n/** The variant for one experiment, from a forced preview first, then the config. Unknown experiments get control. */\n/** The experiments whose variants change the text `id`: showing that text is an exposure to them. */\nexport const experimentsOfText = (config: CockpitConfig | null, id: string) =>\n (config?.experiments ?? []).filter((e) => e.variants.some((v) => v.changes && id in v.changes)).map((e) => e.key);\n\nexport const resolveVariant = (key: string, config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>): VariantId => {\n if (preview[key]) return preview[key];\n const experiment = config?.experiments.find((e) => e.key === key);\n return experiment ? assign(experiment, distinctId) : \"control\";\n};\n\n/**\n * Every text change that applies to this visitor: the published texts first, then each experiment's in config order.\n * Control keeps the published (or the page's own) words, so only a non-control variant (assigned, forced in a preview,\n * or a shipped winner) changes them further.\n */\nexport const textChanges = (config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => {\n const out: Record<string, string> = { ...(config?.texts ?? {}) };\n for (const experiment of config?.experiments ?? []) {\n const id = resolveVariant(experiment.key, config, distinctId, preview);\n if (id === (experiment.variants[0]?.id ?? \"control\")) continue;\n Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});\n }\n return out;\n};\n\n/**\n * The query parameter a redirect test adds to the variant's address (`ck_x=<test>:<variant>`): the page there records\n * the exposure and takes it out of the address bar. It also keeps that page from redirecting again.\n */\nexport const EXPOSURE_PARAM = \"ck_x\";\n\n/** What `redirectFor` decided: go to `to`, and/or the redirect test this page belongs to and the visitor's variant. */\nexport interface RedirectDecision {\n /** Where to send the visitor now. Missing for control, which stays on the page. */\n to?: string;\n /** The redirect test, while it runs: record `$exposure` for it (on this page for control, on `to` otherwise). */\n experiment?: string;\n variant?: VariantId;\n}\n\nconst pathOf = (p: string) => p.replace(/\\/+$/, \"\") || \"/\";\n\n/**\n * Where a page sends the visitor, before it shows: a page redirect set in MRRocket first, else a redirect test that runs\n * on this path (the visitor's variant, by the same stable assignment as every test). Null when nothing applies.\n *\n * - The new address keeps the page's query (UTM parameters) and its hash; a running test adds `ck_x=<test>:<variant>`,\n * so the page it lands on records the exposure (and never redirects again).\n * - `distinctId` null (no stable id, as on a server without the `ck_id` cookie): running tests are left to the browser,\n * page redirects and shipped winners still apply.\n * - A redirect to the page itself is ignored.\n */\nexport const redirectFor = (config: CockpitConfig | null, href: string, distinctId: string | null, preview: Record<string, VariantId> = {}): RedirectDecision | null => {\n if (!config) return null;\n let here: URL;\n try {\n here = new URL(href);\n } catch {\n return null;\n }\n if (here.searchParams.has(EXPOSURE_PARAM)) return null;\n const path = pathOf(here.pathname);\n const go = (to: string, mark?: string) => {\n let t: URL;\n try {\n t = new URL(to, here);\n } catch {\n return undefined;\n }\n if (!/^https?:$/.test(t.protocol) || (t.origin === here.origin && pathOf(t.pathname) === path)) return undefined;\n here.searchParams.forEach((v, k) => {\n if (!t.searchParams.has(k)) t.searchParams.set(k, v);\n });\n if (mark) t.searchParams.set(EXPOSURE_PARAM, mark);\n if (!t.hash) t.hash = here.hash;\n return t.href;\n };\n for (const r of config.redirects ?? []) {\n const to = r.from && pathOf(r.from) === path ? go(r.to) : undefined;\n if (to) return { to };\n }\n for (const e of config.experiments) {\n if (!e.path || pathOf(e.path) !== path) continue;\n const running = e.status === \"running\" && !e.winner;\n if (running && distinctId === null && !preview[e.key]) return null;\n const variant = resolveVariant(e.key, config, distinctId ?? \"\", preview);\n const url = e.variants.find((v) => v.id === variant)?.url;\n const to = url ? go(url, running ? `${e.key}:${variant}` : undefined) : undefined;\n return running ? { to, experiment: e.key, variant } : to ? { to } : null;\n }\n return null;\n};\n\n/** The test and variant a redirect test's `ck_x` names, when the config has them; null otherwise. */\nexport const exposureFrom = (config: CockpitConfig | null, value: string | null | undefined) => {\n const [key, variant] = (value ?? \"\").split(\":\");\n const e = config?.experiments.find((x) => x.key === key);\n return e && e.variants.some((v) => v.id === variant) ? { experiment: key, variant } : null;\n};\n\n/**\n * The MRRocket API host from `NEXT_PUBLIC_MRROCKET_HOST`. Written as a literal property access on purpose: Next.js\n * inlines `process.env.NEXT_PUBLIC_*` at build time, in the browser bundle and in the server bundle (also on\n * Cloudflare Workers), and only replaces the literal form. Without a bundler (or a `process`) it is undefined.\n */\nexport const envHost = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_HOST || undefined;\n } catch {\n return undefined;\n }\n};\n\n/** The public key from `NEXT_PUBLIC_MRROCKET_KEY`, the default for the `apiKey` prop. Same literal-access rule as `envHost`. */\nexport const envKey = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_KEY || undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Where the API lives: the `host` prop, else `NEXT_PUBLIC_MRROCKET_HOST`, else \"\" (the page's own origin, which is how\n * MRRocket's own demo pages call it). Without a trailing slash.\n */\nexport const resolveHost = (host?: string) => (host ?? envHost() ?? \"\").trim().replace(/\\/+$/, \"\");\n\n/** A random visitor id. */\nexport const newDistinctId = () =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n"],"mappings":"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","names":[]}
package/dist/flows-ui.js CHANGED
@@ -243,7 +243,7 @@ var mountFlows = (ctl, theme = {}) => {
243
243
  h("div", { className: "ckf-row" }, button(s.button || (v.last ? "Done" : "Next"), "primary", ctl.onboarding.next, { id: "go" }))
244
244
  );
245
245
  if (s.kind === "modal") return h("div", { className: "ckf-scrim" }, card);
246
- const target = s.target ? document.querySelector(`[data-ck-target="${CSS.escape(s.target)}"]`) : null;
246
+ const target = s.target ? findTarget(s.target) : null;
247
247
  const float = h("div", { className: "ckf-float" }, card);
248
248
  if (target) {
249
249
  const ring = h("div", { className: "ckf-ring", "aria-hidden": "true" });
@@ -253,6 +253,18 @@ var mountFlows = (ctl, theme = {}) => {
253
253
  }
254
254
  return float;
255
255
  };
256
+ const findTarget = (t) => {
257
+ const named = () => document.querySelector(`[data-ck-target="${CSS.escape(t)}"]`);
258
+ if (!/^[#.[]|[\s>:=]/.test(t)) return named();
259
+ return safeQuery(t) ?? named();
260
+ };
261
+ const safeQuery = (sel) => {
262
+ try {
263
+ return document.querySelector(sel);
264
+ } catch {
265
+ return null;
266
+ }
267
+ };
256
268
  const place = (float, ring, target) => {
257
269
  const r = target.getBoundingClientRect();
258
270
  Object.assign(ring.style, { left: `${r.left - 4}px`, top: `${r.top - 4}px`, width: `${r.width + 8}px`, height: `${r.height + 8}px` });
@@ -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)}`);
@@ -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;