@mrrocket/sdk 0.14.0 → 0.16.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +45 -4
  3. package/dist/api-client.d.ts +67 -5
  4. package/dist/api-client.js +39 -8
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/api.d.ts +4 -1
  7. package/dist/api.js +2 -1
  8. package/dist/api.js.map +1 -1
  9. package/dist/batch.d.ts +6 -4
  10. package/dist/batch.js +3 -3
  11. package/dist/batch.js.map +1 -1
  12. package/dist/browser/mrrocket-flows.js +2 -2
  13. package/dist/browser/mrrocket-forms.js +1 -1
  14. package/dist/browser/mrrocket-messenger.js +202 -46
  15. package/dist/browser/mrrocket-push.js +1 -1
  16. package/dist/browser/mrrocket-replay.js +3 -3
  17. package/dist/browser/mrrocket.js +1 -1
  18. package/dist/client.d.ts +10 -2
  19. package/dist/client.js +36 -15
  20. package/dist/client.js.map +1 -1
  21. package/dist/context.d.ts +5 -1
  22. package/dist/context.js.map +1 -1
  23. package/dist/core.d.ts +1 -1
  24. package/dist/core.js +1 -1
  25. package/dist/core.js.map +1 -1
  26. package/dist/errors.d.ts +5 -1
  27. package/dist/errors.js +8 -2
  28. package/dist/errors.js.map +1 -1
  29. package/dist/flows-ui.js +48 -1
  30. package/dist/flows-ui.js.map +1 -1
  31. package/dist/flows-web.d.ts +2 -0
  32. package/dist/flows-web.js +4 -1
  33. package/dist/flows-web.js.map +1 -1
  34. package/dist/flows.d.ts +61 -1
  35. package/dist/flows.js +64 -21
  36. package/dist/flows.js.map +1 -1
  37. package/dist/forms.d.ts +4 -0
  38. package/dist/forms.js +4 -1
  39. package/dist/forms.js.map +1 -1
  40. package/dist/identity.d.ts +42 -0
  41. package/dist/identity.js +24 -0
  42. package/dist/identity.js.map +1 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/messenger-ui.d.ts +21 -6
  46. package/dist/messenger-ui.js +334 -80
  47. package/dist/messenger-ui.js.map +1 -1
  48. package/dist/messenger.d.ts +11 -3
  49. package/dist/messenger.js +9 -5
  50. package/dist/messenger.js.map +1 -1
  51. package/dist/push-web.d.ts +2 -13
  52. package/dist/push-web.js +4 -2
  53. package/dist/push-web.js.map +1 -1
  54. package/dist/react-native/index.d.cts +162 -14
  55. package/dist/react-native/index.d.ts +162 -14
  56. package/dist/react-native/index.js +552 -207
  57. package/dist/react-native/index.js.map +1 -1
  58. package/dist/replay.d.ts +4 -1
  59. package/dist/replay.js +8 -2
  60. package/dist/replay.js.map +1 -1
  61. package/dist/server.d.ts +2 -0
  62. package/dist/server.js +2 -0
  63. package/dist/server.js.map +1 -1
  64. package/dist/styles-css.d.ts +1 -1
  65. package/dist/styles-css.js +200 -44
  66. package/dist/styles-css.js.map +1 -1
  67. package/dist/user-hash.d.ts +27 -0
  68. package/dist/user-hash.js +24 -0
  69. package/dist/user-hash.js.map +1 -0
  70. package/package.json +9 -1
package/dist/flows.js CHANGED
@@ -2,6 +2,7 @@
2
2
  var CANCEL_EVENT = "$cancel_flow";
3
3
  var ONBOARDING_EVENT = "$onboarding";
4
4
  var INAPP_EVENT = "$inapp";
5
+ var REVIEW_EVENT = "$review_prompt";
5
6
  var PLAIN_CANCEL = {
6
7
  version: 0,
7
8
  question: "",
@@ -48,6 +49,9 @@ var pageMatches = (pattern, path) => {
48
49
  return at === (p.replace(/\/+$/, "") || "/");
49
50
  };
50
51
  var stepsIn = (nodes) => nodes.flatMap((n) => n.kind === "condition" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === "wait" ? [] : [n]);
52
+ var monthsSince = (since, now = Date.now()) => since == null || since === "" ? NaN : Math.floor((now - new Date(since).getTime()) / 2630016e3);
53
+ var low = (x) => x?.trim().toLowerCase();
54
+ var ruleMatches = ({ when: w = {} }, c) => (w.answer == null || low(w.answer) === low(c.answer)) && (w.plan == null || [w.plan].flat().some((x) => low(x) === low(c.plan))) && !(c.months >= w.tenureUnder) && !(c.months < w.tenureOver) && (w.tenureUnder == null || c.months >= 0) && (w.tenureOver == null || c.months >= 0);
51
55
  var createFlows = (env) => {
52
56
  let config = null;
53
57
  let cancel = null;
@@ -215,18 +219,23 @@ var createFlows = (env) => {
215
219
  };
216
220
  const view = () => {
217
221
  if (cancel) {
218
- const offer = cancel.reason ? cancel.flow.offers[cancel.reason] : void 0;
222
+ const c = cancel;
223
+ const followUp = c.flow.followUps?.[c.reason];
224
+ const f = followUp ? 1 : 0;
225
+ const steps = c.plain ? 1 : 3 + f + (c.flow.second?.[c.reason] ? 1 : 0);
219
226
  return {
220
227
  type: "cancel",
221
- screen: cancel.screen,
222
- flow: cancel.flow,
223
- reason: cancel.reason,
224
- offer: cancel.screen === "offer" ? offer : void 0,
225
- price: cancel.options.price,
226
- busy: cancel.busy,
227
- error: cancel.error,
228
- step: cancel.plain ? 1 : cancel.screen === "survey" ? 1 : cancel.screen === "offer" ? 2 : 3,
229
- steps: cancel.plain ? 1 : 3
228
+ screen: c.screen,
229
+ flow: c.flow,
230
+ reason: c.reason,
231
+ ...c.screen === "followup" && followUp ? { followUp, answer: c.answer } : {},
232
+ offer: c.screen === "offer" ? c.offer : void 0,
233
+ ...c.screen === "offer" ? { round: c.round } : {},
234
+ price: c.options.price,
235
+ busy: c.busy,
236
+ error: c.error,
237
+ step: c.screen === "survey" ? 1 : c.screen === "followup" ? 2 : c.screen === "offer" ? 1 + f + c.round : steps,
238
+ steps
230
239
  };
231
240
  }
232
241
  const ob = onboardingView();
@@ -305,12 +314,22 @@ var createFlows = (env) => {
305
314
  });
306
315
  return loading;
307
316
  };
317
+ const offerAt = (c) => c.round > 1 ? { round: 2 } : c.rule >= 0 ? { rule: c.rule } : {};
318
+ const toOffer = (c) => {
319
+ const who = { answer: c.answer, plan: c.options.plan, months: monthsSince(c.options.customerSince, env.now?.()) };
320
+ const rules = c.flow.rules?.[c.reason] ?? [];
321
+ c.rule = rules.findIndex((r) => ruleMatches(r, who));
322
+ c.offer = c.rule < 0 ? c.flow.offers[c.reason] : rules[c.rule].offer ?? void 0;
323
+ c.round = 1;
324
+ c.screen = c.offer ? "offer" : "confirm";
325
+ };
308
326
  const result = (c, outcome, comment) => {
309
- const offer = c.reason ? c.flow.offers[c.reason] : void 0;
327
+ const offer = c.screen === "offer" ? c.offer : void 0;
310
328
  return {
311
329
  outcome,
312
330
  run: c.run,
313
331
  ...c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {},
332
+ ...c.answer ? { answer: c.answer } : {},
314
333
  ...outcome === "saved" && offer ? { offer } : {},
315
334
  ...comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}
316
335
  };
@@ -323,7 +342,8 @@ var createFlows = (env) => {
323
342
  outcome: r.outcome,
324
343
  customer: c.options.customer ?? "consumer",
325
344
  ...r.reason ? { reason: r.reason } : {},
326
- ...r.offer ? { offer: r.offer.kind } : {},
345
+ ...r.answer ? { answer: r.answer } : {},
346
+ ...r.offer ? { offer: r.offer.kind, ...offerAt(c) } : {},
327
347
  ...r.comment ? { comment: r.comment } : {}
328
348
  });
329
349
  if (cancel === c) cancel = null;
@@ -353,7 +373,7 @@ var createFlows = (env) => {
353
373
  let resolve = () => {
354
374
  };
355
375
  const promise = new Promise((r) => resolve = r);
356
- const c = { flow, options, plain, screen: plain ? "confirm" : "survey", run: runId(), busy: false, resolve, promise };
376
+ const c = { flow, options, plain, screen: plain ? "confirm" : "survey", round: 1, run: runId(), busy: false, resolve, promise };
357
377
  cancel = c;
358
378
  env.track(CANCEL_EVENT, { action: "shown", run: c.run, version: flow.version, customer: options.customer ?? "consumer", plain });
359
379
  emit();
@@ -405,29 +425,43 @@ var createFlows = (env) => {
405
425
  },
406
426
  cancel: {
407
427
  pick: (reason) => {
408
- if (!cancel || cancel.busy || !cancel.flow.reasons.some((r) => r.id === reason)) return;
428
+ if (!cancel || cancel.busy || cancel.screen !== "survey" || !cancel.flow.reasons.some((r) => r.id === reason)) return;
429
+ if (cancel.reason !== reason) cancel.answer = void 0;
409
430
  cancel.reason = reason;
410
431
  cancel.error = void 0;
411
432
  emit();
412
433
  },
434
+ answer: (text) => {
435
+ const c = cancel;
436
+ if (!c || c.busy || c.screen !== "followup") return;
437
+ const t = text.slice(0, 500);
438
+ c.answer = t.trim() ? t : void 0;
439
+ emit();
440
+ },
413
441
  next: () => {
414
442
  const c = cancel;
415
- if (!c || c.screen !== "survey" || !c.reason) return;
416
- env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
417
- c.screen = c.flow.offers[c.reason] ? "offer" : "confirm";
443
+ if (!c || !c.reason) return;
444
+ if (c.screen === "survey") {
445
+ env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
446
+ if (c.flow.followUps?.[c.reason]) c.screen = "followup";
447
+ else toOffer(c);
448
+ } else if (c.screen === "followup") {
449
+ c.answer = c.answer?.trim();
450
+ toOffer(c);
451
+ } else return;
418
452
  emit();
419
453
  },
420
454
  accept: (comment) => {
421
455
  const c = cancel;
422
456
  if (!c || c.screen !== "offer" || !c.reason) return;
423
- const offer = c.flow.offers[c.reason];
424
457
  if (!c.busy)
425
458
  env.track(CANCEL_EVENT, {
426
459
  action: "offer",
427
460
  run: c.run,
428
461
  version: c.flow.version,
429
462
  reason: c.reason,
430
- offer: offer?.kind,
463
+ offer: c.offer?.kind,
464
+ ...offerAt(c),
431
465
  outcome: "accepted"
432
466
  });
433
467
  settle(c, "saved", comment);
@@ -440,10 +474,16 @@ var createFlows = (env) => {
440
474
  run: c.run,
441
475
  version: c.flow.version,
442
476
  reason: c.reason,
443
- offer: c.reason ? c.flow.offers[c.reason]?.kind : void 0,
477
+ offer: c.offer?.kind,
478
+ ...offerAt(c),
444
479
  outcome: "declined"
445
480
  });
446
- c.screen = "confirm";
481
+ const second = c.round === 1 && c.reason ? c.flow.second?.[c.reason] : void 0;
482
+ if (second) {
483
+ c.offer = second;
484
+ c.rule = void 0;
485
+ c.round = 2;
486
+ } else c.screen = "confirm";
447
487
  c.error = void 0;
448
488
  emit();
449
489
  },
@@ -515,14 +555,17 @@ export {
515
555
  INAPP_EVENT,
516
556
  ONBOARDING_EVENT,
517
557
  PLAIN_CANCEL,
558
+ REVIEW_EVENT,
518
559
  createFlows,
519
560
  discounted,
520
561
  eventSlug,
521
562
  matchesTrigger,
522
563
  money,
564
+ monthsSince,
523
565
  nodesOf,
524
566
  offerButton,
525
567
  pageMatches,
568
+ ruleMatches,
526
569
  runId,
527
570
  safeUrl,
528
571
  startsOnLoad,
package/dist/flows.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/flows.ts"],"sourcesContent":["/**\n * The cancel flow, onboarding and in-app messages (SDK 0.7.0): what MRRocket's config and `GET /api/sdk/inapp` serve,\n * and one controller that decides what shows when and records what the person did. No DOM and no React: the web\n * renderer (flows-ui.ts, loaded only when something shows) and the React Native one (native/flows-view.ts) draw\n * `view()` and call the actions.\n *\n * What it records, as normal events under the SDK's analytics consent (nothing with consent \"off\"):\n * - `$cancel_flow` `{action: \"shown\" | \"answered\" | \"offer\" | \"finished\", run, version, …}`. MRRocket turns a\n * `finished` with `outcome: \"saved\"` into a save and with `outcome: \"cancelled\"` into a cancellation on the Churn page.\n * MRRocket never changes a subscription: an accepted offer goes to the app's `onSave`, a confirmed cancel to its\n * `onCancel`, and only after they succeed is the outcome recorded.\n * - `$onboarding` `{flow, step?, action: \"started\" | \"shown\" | \"completed\" | \"dismissed\"}`; since 0.11.0 a wait records\n * \"shown\" when the person reaches it and \"completed\" when it is over, a condition `{action: \"answered\", answer: \"yes\"\n * | \"no\"}` once it is decided.\n * - `$inapp` `{message, journey, action: \"shown\" | \"clicked\" | \"dismissed\"}`; the journey moves the person on.\n */\nimport type { CockpitConfig } from \"./core\";\n\nexport const CANCEL_EVENT = \"$cancel_flow\";\nexport const ONBOARDING_EVENT = \"$onboarding\";\nexport const INAPP_EVENT = \"$inapp\";\n\nexport type CancelOfferKind = \"discount\" | \"pause\" | \"downgrade\" | \"contact\";\n\n/** What one answer of the survey offers before the confirmation. An answer without one goes straight to it. */\nexport interface CancelOffer {\n kind: CancelOfferKind;\n headline: string;\n /** Discount: percent off. */\n percent?: number;\n /** Discount: for how many months; pause: up to how many months. */\n months?: number;\n /** Downgrade: the plan to switch to, as MRRocket knows it from the payment provider. */\n plan?: string;\n planName?: string;\n}\n\n/** The product's live cancel flow (MRRocket › Churn › Cancel flow). */\nexport interface CancelFlowConfig {\n /** Grows with every publish. */\n version: number;\n question: string;\n /** The survey's answers, in order. */\n reasons: { id: string; label: string }[];\n /** The offer per answer id. */\n offers: Record<string, CancelOffer>;\n confirm: { title: string; body: string; button: string };\n /**\n * Who gets the survey and the offers: business customers only. Anyone else (a consumer, or a customer the app does\n * not say) gets the plain confirmation, with nothing in the way of cancelling (§ 312k BGB).\n */\n offersFor: \"business\";\n}\n\nexport type OnboardingStepKind = \"tour\" | \"checklist\" | \"modal\";\n\nexport interface OnboardingStep {\n id: string;\n kind: OnboardingStepKind;\n title: string;\n body?: string;\n button?: string;\n /** Tour steps: the element they point at, marked `data-ck-target=\"<target>\"` in the app. Without it: a card. */\n target?: string;\n /** Checklist items: the event that ticks them (`track(\"<event>\")`). Without it the person ticks it themselves. */\n event?: string;\n}\n\n/** A pause in a flow (0.11.0): the next step shows this long after the one before was done. */\nexport interface OnboardingWait {\n id: string;\n kind: \"wait\";\n amount: number;\n unit: \"hours\" | \"days\";\n}\n\n/**\n * A fork in a flow (0.11.0): \"Did <event> happen?\" (the person's events, here and on MRRocket) or \"Is on <page>?\" (the\n * page they are on when they reach it; `*` at the end for everything below). Decided once, when the person reaches it;\n * like a journey's branch it ends its line, the rest of the flow lives in its two paths.\n */\nexport interface OnboardingCondition {\n id: string;\n kind: \"condition\";\n event?: string;\n page?: string;\n yes: OnboardingNode[];\n no: OnboardingNode[];\n}\n\nexport type OnboardingNode = OnboardingStep | OnboardingWait | OnboardingCondition;\n\n/** A live onboarding flow's in-app steps (MRRocket › Conversion › Onboarding); its emails go out from MRRocket. */\nexport interface OnboardingFlow {\n id: string;\n name: string;\n /** When it starts: an event's words (\"Signed up\", \"First export\"), or \"First launch\" / \"First visit\". */\n trigger: string;\n /** What every version shows: with `nodes`, only the in-app steps before the first wait or condition. */\n steps: OnboardingStep[];\n /** 0.11.0+: the whole flow with its waits and conditions. Read instead of `steps` when it is there. */\n nodes?: OnboardingNode[];\n}\n\n/** An in-app message a Lifecycle journey has for this person. */\nexport interface InAppMessage {\n id: string;\n journey?: string;\n title: string;\n body?: string;\n button?: string;\n /** Where the button goes (http(s) or a path of the app). */\n url?: string;\n style: \"banner\" | \"modal\";\n}\n\n/** One flow's progress: the steps done, whether the person closed it, and (0.11.0) its decided conditions and times. */\nexport interface FlowProgress {\n done: string[];\n dismissed?: boolean;\n /** Each decided condition's path. */\n answers?: Record<string, \"yes\" | \"no\">;\n /** When each step was done and each wait was reached (ms since 1970). */\n at?: Record<string, number>;\n reached?: Record<string, number>;\n}\n\n/** What `GET /api/sdk/inapp` answers for one signed-in person. */\nexport interface InAppState {\n /** Progress per onboarding flow: the steps done and whether the person closed it. */\n onboarding: Record<string, FlowProgress>;\n messages: InAppMessage[];\n /** 0.11.0: the events the flows' conditions ask about that this person did (as `eventSlug`s). */\n events?: string[];\n}\n\nexport interface CancelFlowResult {\n /** \"cancelled\": confirmed; \"saved\": took the offer; \"kept\": closed the dialog without cancelling. */\n outcome: \"cancelled\" | \"saved\" | \"kept\";\n reason?: string;\n reasonLabel?: string;\n offer?: CancelOffer;\n /** What the customer wrote on the confirmation, if anything. */\n comment?: string;\n /** This run's id, the same as in the events. */\n run: string;\n}\n\nexport interface CancelFlowOptions {\n /**\n * \"business\" shows the survey and the offers; anything else (the default) only the plain confirmation, so a\n * consumer can always cancel in one step (§ 312k BGB).\n */\n customer?: \"business\" | \"consumer\";\n /** The customer's current price, to show what a discount costs (`{ amount: 29, currency: \"EUR\" }`). */\n price?: { amount: number; currency: string; interval?: \"month\" | \"year\" };\n /** The customer confirmed: cancel the subscription in your code. Throwing keeps the dialog open with an error. */\n onCancel?: (result: CancelFlowResult) => unknown;\n /** The customer took an offer: apply it in your code (coupon, pause, plan change). Throwing keeps the dialog open. */\n onSave?: (result: CancelFlowResult) => unknown;\n /** Closed without cancelling or taking an offer. */\n onClose?: (result: CancelFlowResult) => void;\n}\n\n/** What a renderer draws. */\nexport type FlowView =\n | {\n type: \"cancel\";\n screen: \"survey\" | \"offer\" | \"confirm\";\n flow: CancelFlowConfig;\n /** The answer picked on the survey. */\n reason?: string;\n offer?: CancelOffer;\n price?: CancelFlowOptions[\"price\"];\n busy: boolean;\n error?: string;\n /** The step for the dots, 1 to `steps`. */\n step: number;\n steps: number;\n }\n | { type: \"step\"; flow: OnboardingFlow; step: OnboardingStep; index: number; total: number; last: boolean }\n | { type: \"checklist\"; flow: OnboardingFlow; items: { step: OnboardingStep; done: boolean }[] }\n | { type: \"message\"; message: InAppMessage };\n\nexport interface FlowsEnv {\n /** Records an event under the SDK's consent (the normal queue). */\n track(event: string, props?: Record<string, unknown>): void;\n /** Whose messages and progress these are: the identify() id; undefined while nobody is signed in. */\n personId(): string | undefined;\n /** `GET /api/sdk/inapp` for that person; null when it fails. */\n fetchInApp(personId: string): Promise<InAppState | null>;\n /** Keeps onboarding progress on the device; only while the consent lets the SDK store something. */\n storage?: () => { get(key: string): string | null | Promise<string | null>; set(key: string, value: string): void } | null;\n /** Opens a message's link. */\n openUrl?: (url: string) => void;\n /** Start onboarding flows by their trigger on its own (default true). */\n auto?: boolean;\n /** The page the person is on (a path), for \"Is on <page>?\" conditions; without it they answer no. */\n path?: () => string | undefined;\n /** The clock, for tests. */\n now?: () => number;\n}\n\nexport interface FlowsController {\n setConfig(config: CockpitConfig | null): void;\n /** The person changed (identify, reset, consent): loads their messages and onboarding progress. */\n refresh(): Promise<void>;\n /** An event the app recorded: starts flows whose trigger it is and ticks checklist items. */\n onEvent(name: string): void;\n openCancelFlow(options?: CancelFlowOptions): Promise<CancelFlowResult>;\n /** Starts an onboarding flow now (the first live one without an id). False when there is none to start. */\n startOnboarding(id?: string): boolean;\n view(): FlowView | null;\n subscribe(listener: () => void): () => void;\n /** `accept(comment)`: a \"Talk to us\" offer sends what the customer wrote with it. */\n cancel: { pick(reason: string): void; next(): void; accept(comment?: string): void; decline(): void; confirm(comment?: string): void; close(): void };\n onboarding: { next(): void; check(stepId: string): void; dismiss(): void };\n message: { click(): void; dismiss(): void };\n}\n\n/** The plain confirmation, when the product has no live cancel flow (or MRRocket was not reachable). */\nexport const PLAIN_CANCEL: CancelFlowConfig = {\n version: 0,\n question: \"\",\n reasons: [],\n offers: {},\n confirm: { title: \"Cancel your subscription?\", body: \"You keep access until the end of the billing period.\", button: \"Cancel subscription\" },\n offersFor: \"business\",\n};\n\nconst FAILED = \"Something went wrong. Please try again.\";\nconst PROGRESS_KEY = \"mrrocket:onboarding\";\n\n/** `\"Signed up\"` → `signed_up`, the way MRRocket names events from words. */\nexport const eventSlug = (s: string) =>\n s\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n\nconst SIGNUP = new Set([\"signed_up\", \"signup\", \"sign_up\", \"registered\", \"account_created\", \"user_signed_up\"]);\n\n/** Whether recording `event` is the trigger's moment (\"Signed up\" is any of the usual signup event names). */\nexport const matchesTrigger = (trigger: string, event: string) => {\n const t = eventSlug(trigger);\n const e = eventSlug(event);\n if (!t || !e) return false;\n return t === e || (SIGNUP.has(t) && SIGNUP.has(e));\n};\n\n/** Triggers that mean \"the first time the person opens the app\": the flow starts on load until it is done. */\nexport const startsOnLoad = (trigger: string) =>\n /^(first (launch|visit|open|session|login|sign ?in|app open)|app (opened|launched)|on load|every visit|always)?$/i.test(trigger.trim());\n\n/** A random id for one cancel flow run. */\nexport const runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;\n\n/** The money a discount leaves: `29 EUR` at 30 % → `20.30`. */\nexport const discounted = (amount: number, percent: number) => Math.round(amount * (1 - percent / 100) * 100) / 100;\n\n/** \"€20.30\" in the customer's own currency, without a library. */\nexport const money = (amount: number, currency: string) => {\n try {\n return new Intl.NumberFormat(undefined, { style: \"currency\", currency }).format(amount);\n } catch {\n return `${amount.toFixed(2)} ${currency}`;\n }\n};\n\n/** The button of an offer. */\nexport const offerButton = (o: CancelOffer) =>\n o.kind === \"contact\" ? \"Send to the team\" : o.kind === \"pause\" ? \"Pause instead\" : o.kind === \"downgrade\" ? \"Switch plan\" : \"Accept offer\";\n\n/** A link a message may open: http(s), or a path of the app. Nothing else (no `javascript:`). */\nexport const safeUrl = (url: string | undefined) => {\n const u = (url ?? \"\").trim();\n return /^https?:\\/\\//i.test(u) || /^\\/(?!\\/)/.test(u) ? u : undefined;\n};\n\ninterface Progress {\n done: Set<string>;\n dismissed: boolean;\n answers: Map<string, boolean>;\n at: Map<string, number>;\n reached: Map<string, number>;\n}\n\n/** The flow's nodes: the whole tree (0.11.0 config), or the plain steps of an older one. */\nexport const nodesOf = (f: OnboardingFlow): OnboardingNode[] => f.nodes ?? f.steps;\n\n/** How long a wait lasts. */\nexport const waitMs = (w: Pick<OnboardingWait, \"amount\" | \"unit\">) => Math.max(0, w.amount) * (w.unit === \"hours\" ? 3_600_000 : 86_400_000);\n\n/** Whether a path matches a condition's page: the same path, or below it for a pattern ending in `*`. */\nexport const pageMatches = (pattern: string, path: string) => {\n const p = pattern.trim();\n const at = (path.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n if (!p) return false;\n if (p.endsWith(\"*\")) {\n const base = p.slice(0, -1).replace(/\\/+$/, \"\");\n return !base || at === base || at.startsWith(`${base}/`);\n }\n return at === (p.replace(/\\/+$/, \"\") || \"/\");\n};\n\n/** Every in-app step of a tree, in reading order (for ticking checklist items by their event). */\nconst stepsIn = (nodes: OnboardingNode[]): OnboardingStep[] =>\n nodes.flatMap((n) => (n.kind === \"condition\" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === \"wait\" ? [] : [n]));\n\ntype Walked =\n | { at: \"step\"; step: OnboardingStep; line: OnboardingNode[]; index: number; before: number }\n | { at: \"wait\"; until: number }\n | { at: \"pending\" }\n | { at: \"end\" };\n\ninterface CancelRun {\n flow: CancelFlowConfig;\n options: CancelFlowOptions;\n plain: boolean;\n screen: \"survey\" | \"offer\" | \"confirm\";\n reason?: string;\n run: string;\n busy: boolean;\n error?: string;\n resolve: (r: CancelFlowResult) => void;\n promise: Promise<CancelFlowResult>;\n}\n\nexport const createFlows = (env: FlowsEnv): FlowsController => {\n let config: CockpitConfig | null = null;\n let cancel: CancelRun | null = null;\n let active: string | null = null;\n let messages: InAppMessage[] = [];\n let known = false;\n let loading: Promise<void> | null = null;\n let loadedFor: string | undefined;\n let progressFor: string | undefined | null = null;\n const progress = new Map<string, Progress>();\n const handled = new Set<string>();\n const shown = new Set<string>();\n const listeners = new Set<() => void>();\n const auto = env.auto !== false;\n /** Events the person did (as slugs), for \"Did <event> happen?\": from MRRocket and from this device. */\n const seenEvents = new Set<string>();\n const now = () => env.now?.() ?? Date.now();\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const flows = () => config?.onboarding ?? [];\n const flowById = (id: string | null) => (id ? flows().find((f) => f.id === id) : undefined);\n const prog = (id: string) => {\n let p = progress.get(id);\n if (!p) progress.set(id, (p = { done: new Set(), dismissed: false, answers: new Map(), at: new Map(), reached: new Map() }));\n return p;\n };\n\n let dirty = false;\n const record = (props: Record<string, unknown>) => {\n env.track(ONBOARDING_EVENT, props);\n dirty = true;\n };\n const markDone = (p: Progress, id: string) => {\n p.done.add(id);\n p.at.set(id, now());\n };\n\n /** A condition's answer for this person, or undefined while what it asks is not known yet. */\n const decide = (c: OnboardingCondition): boolean | undefined => {\n if (c.page?.trim()) {\n const path = env.path?.();\n return path === undefined ? false : pageMatches(c.page, path);\n }\n if (c.event?.trim()) {\n // The person's events from MRRocket arrive with their progress: before that, wait.\n if (!known) return undefined;\n for (const e of seenEvents) if (matchesTrigger(c.event, e)) return true;\n return false;\n }\n return false;\n };\n\n /**\n * Where the person is in a line of the flow. `live`: it is their flow now, so reaching a wait starts it, a decided\n * condition is kept and both are recorded; otherwise nothing changes (to ask whether a flow is finished).\n */\n const walkLine = (flow: OnboardingFlow, line: OnboardingNode[], p: Progress, live: boolean, before = 0, last?: number): Walked => {\n for (let i = 0; i < line.length; i++) {\n const n = line[i];\n if (n.kind === \"wait\") {\n if (p.done.has(n.id)) {\n last = p.at.get(n.id) ?? last;\n continue;\n }\n let since = p.reached.get(n.id);\n if (since === undefined) {\n if (!live) return { at: \"pending\" };\n // Counted from when the step before was done; a flow that opens with a wait counts from now.\n since = last ?? now();\n p.reached.set(n.id, since);\n record({ flow: flow.id, step: n.id, kind: \"wait\", action: \"shown\" });\n }\n const until = since + waitMs(n);\n if (now() < until) return { at: \"wait\", until };\n if (!live) return { at: \"pending\" };\n markDone(p, n.id);\n record({ flow: flow.id, step: n.id, kind: \"wait\", action: \"completed\" });\n last = until;\n continue;\n }\n if (n.kind === \"condition\") {\n let answer = p.answers.get(n.id);\n if (answer === undefined) {\n if (!live) return { at: \"pending\" };\n answer = decide(n);\n if (answer === undefined) return { at: \"pending\" };\n p.answers.set(n.id, answer);\n record({ flow: flow.id, step: n.id, kind: \"condition\", action: \"answered\", answer: answer ? \"yes\" : \"no\" });\n }\n return walkLine(flow, answer ? n.yes : n.no, p, live, before, last);\n }\n if (!p.done.has(n.id)) return { at: \"step\", step: n, line, index: i, before };\n last = p.at.get(n.id) ?? last;\n before++;\n }\n return { at: \"end\" };\n };\n\n const finished = (f: OnboardingFlow) => {\n const p = progress.get(f.id);\n return !!p && (p.dismissed || walkLine(f, nodesOf(f), p, false).at === \"end\");\n };\n\n const keyOf = (pid: string | undefined) => `${PROGRESS_KEY}:${pid ?? \"visitor\"}`;\n const eventsKeyOf = (pid: string | undefined) => `${PROGRESS_KEY}:events:${pid ?? \"visitor\"}`;\n\n const save = () => {\n dirty = false;\n const store = env.storage?.();\n if (!store) return;\n const out: Record<string, FlowProgress> = {};\n for (const [id, p] of progress)\n out[id] = {\n done: [...p.done],\n ...(p.dismissed ? { dismissed: true } : {}),\n ...(p.answers.size ? { answers: Object.fromEntries([...p.answers].map(([k, v]) => [k, v ? \"yes\" : \"no\"])) } : {}),\n ...(p.at.size ? { at: Object.fromEntries(p.at) } : {}),\n ...(p.reached.size ? { reached: Object.fromEntries(p.reached) } : {}),\n };\n try {\n store.set(keyOf(env.personId()), JSON.stringify(out));\n if (seenEvents.size) store.set(eventsKeyOf(env.personId()), JSON.stringify([...seenEvents].slice(-200)));\n } catch {\n // Storage full or gone: progress stays in memory.\n }\n };\n\n const merge = (state: InAppState[\"onboarding\"] | undefined) => {\n for (const [id, s] of Object.entries(state ?? {})) {\n const p = prog(id);\n for (const d of s.done ?? []) p.done.add(d);\n if (s.dismissed) p.dismissed = true;\n // A condition decided elsewhere keeps its path; the earliest time wins for waits.\n for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === \"yes\");\n for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === \"number\" && !p.at.has(k)) p.at.set(k, v);\n for (const [k, v] of Object.entries(s.reached ?? {})) if (typeof v === \"number\" && (!p.reached.has(k) || v < p.reached.get(k)!)) p.reached.set(k, v);\n }\n };\n\n // ---------- What shows ----------\n\n /** Comes back when a short wait is over, so the next step shows without a reload (long waits wait for the next visit). */\n const wakeAt = (until: number) => {\n if (timer) clearTimeout(timer);\n timer = null;\n const ms = until - now();\n if (ms > 0 && ms <= 86_400_000) {\n timer = setTimeout(() => ((timer = null), emit()), ms + 50);\n // Never what keeps a process alive (tests, server rendering).\n (timer as { unref?: () => void }).unref?.();\n }\n };\n\n /** The active flow waits (a wait step, or a condition whose answer is not known yet): it stays the active one. */\n let holding = false;\n\n const onboardingView = (): FlowView | null => {\n holding = false;\n const flow = flowById(active);\n if (!flow) return null;\n const p = prog(flow.id);\n if (p.dismissed) return null;\n const w = walkLine(flow, nodesOf(flow), p, true);\n if (dirty) save();\n if (w.at === \"wait\") wakeAt(w.until);\n holding = w.at === \"wait\" || w.at === \"pending\";\n if (w.at !== \"step\") return null;\n const { step, line, index } = w;\n const isStep = (n: OnboardingNode | undefined): n is OnboardingStep => !!n && n.kind !== \"wait\" && n.kind !== \"condition\";\n if (step.kind === \"checklist\") {\n let from = index;\n while (from > 0 && isStep(line[from - 1]) && line[from - 1].kind === \"checklist\") from--;\n let to = index;\n while (to + 1 < line.length && isStep(line[to + 1]) && line[to + 1].kind === \"checklist\") to++;\n const items = line.slice(from, to + 1).filter(isStep);\n for (const i of items)\n if (!p.done.has(i.id) && !shown.has(`s:${i.id}`)) {\n shown.add(`s:${i.id}`);\n env.track(ONBOARDING_EVENT, { flow: flow.id, step: i.id, kind: \"checklist\", action: \"shown\" });\n }\n return { type: \"checklist\", flow, items: items.map((s) => ({ step: s, done: p.done.has(s.id) })) };\n }\n if (!shown.has(`s:${step.id}`)) {\n shown.add(`s:${step.id}`);\n env.track(ONBOARDING_EVENT, { flow: flow.id, step: step.id, kind: step.kind, action: \"shown\" });\n }\n // The dots: the steps behind on the person's path, this one, and the ones left on its line before any fork.\n const rest = line.slice(index + 1);\n const fork = rest.findIndex((n) => n.kind === \"condition\");\n const ahead = (fork < 0 ? rest : rest.slice(0, fork)).filter(isStep).length;\n return { type: \"step\", flow, step, index: w.before, total: w.before + 1 + ahead, last: !ahead && fork < 0 };\n };\n\n const view = (): FlowView | null => {\n if (cancel) {\n const offer = cancel.reason ? cancel.flow.offers[cancel.reason] : undefined;\n return {\n type: \"cancel\",\n screen: cancel.screen,\n flow: cancel.flow,\n reason: cancel.reason,\n offer: cancel.screen === \"offer\" ? offer : undefined,\n price: cancel.options.price,\n busy: cancel.busy,\n error: cancel.error,\n step: cancel.plain ? 1 : cancel.screen === \"survey\" ? 1 : cancel.screen === \"offer\" ? 2 : 3,\n steps: cancel.plain ? 1 : 3,\n };\n }\n const ob = onboardingView();\n if (ob) return ob;\n // A flow that just ended lets the next thing show; one that waits keeps its place.\n if (active && !holding) active = null;\n const m = messages.find((x) => !handled.has(x.id));\n return m ? { type: \"message\", message: m } : null;\n };\n\n /** Tells the renderers, and records what became visible (once per step or message per page load). */\n const emit = () => {\n // Onboarding steps record their \"shown\" where they are worked out (onboardingView), so a step a wait let through\n // counts also when nothing else changed.\n const v = view();\n if (v?.type === \"message\" && !shown.has(`m:${v.message.id}`)) {\n shown.add(`m:${v.message.id}`);\n env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: \"shown\" });\n }\n for (const l of [...listeners]) {\n try {\n l();\n } catch {\n // One renderer must not stop another.\n }\n }\n };\n\n const start = (flow: OnboardingFlow) => {\n if (active === flow.id) return true;\n if (active && flowById(active) && !finished(flowById(active)!)) return false;\n active = flow.id;\n env.track(ONBOARDING_EVENT, { flow: flow.id, action: \"started\" });\n emit();\n return true;\n };\n\n /** Flows whose trigger is \"the first launch\" start once their progress is known (never twice for a done flow). */\n const autoStart = () => {\n if (!auto || active || !known) return;\n // Without an id or storage a \"first launch\" could not be told from any other: no automatic start then.\n if (!env.personId() && !env.storage?.()) return;\n const next = flows().find((f) => startsOnLoad(f.trigger) && nodesOf(f).length && !finished(f));\n if (next) start(next);\n };\n\n const refresh = () => {\n const pid = env.personId();\n // Another person on this device: nothing of the one before carries over.\n if (progressFor !== null && progressFor !== pid) {\n progress.clear();\n seenEvents.clear();\n handled.clear();\n active = null;\n loadedFor = undefined;\n }\n progressFor = pid;\n const run = (async () => {\n const store = env.storage?.();\n if (store) {\n try {\n const raw = await store.get(keyOf(pid));\n if (raw) merge(JSON.parse(raw) as InAppState[\"onboarding\"]);\n const seen = await store.get(eventsKeyOf(pid));\n if (seen) for (const e of JSON.parse(seen) as string[]) if (typeof e === \"string\") seenEvents.add(e);\n } catch {\n // A damaged entry is ignored.\n }\n }\n const wants = flows().length > 0 || !!config?.messages;\n if (pid && wants && loadedFor !== pid) {\n loadedFor = pid;\n const state = await env.fetchInApp(pid).catch(() => null);\n if (state && env.personId() === pid) {\n merge(state.onboarding);\n for (const e of state.events ?? []) if (typeof e === \"string\") seenEvents.add(eventSlug(e));\n messages = (state.messages ?? []).filter((m) => m && m.id && m.title);\n }\n }\n if (!pid) messages = [];\n known = true;\n autoStart();\n emit();\n })();\n loading = run.finally(() => {\n if (loading === run) loading = null;\n });\n return loading;\n };\n\n // ---------- Cancel flow ----------\n\n const result = (c: CancelRun, outcome: CancelFlowResult[\"outcome\"], comment?: string): CancelFlowResult => {\n const offer = c.reason ? c.flow.offers[c.reason] : undefined;\n return {\n outcome,\n run: c.run,\n ...(c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {}),\n ...(outcome === \"saved\" && offer ? { offer } : {}),\n ...(comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}),\n };\n };\n\n const finish = (c: CancelRun, r: CancelFlowResult) => {\n env.track(CANCEL_EVENT, {\n action: \"finished\",\n run: c.run,\n version: c.flow.version,\n outcome: r.outcome,\n customer: c.options.customer ?? \"consumer\",\n ...(r.reason ? { reason: r.reason } : {}),\n ...(r.offer ? { offer: r.offer.kind } : {}),\n ...(r.comment ? { comment: r.comment } : {}),\n });\n if (cancel === c) cancel = null;\n c.resolve(r);\n emit();\n };\n\n /** Runs the app's callback; the outcome is recorded only once it went through. */\n const settle = (c: CancelRun, outcome: \"saved\" | \"cancelled\", comment?: string) => {\n if (c.busy) return;\n const r = result(c, outcome, comment);\n const fn = outcome === \"saved\" ? c.options.onSave : c.options.onCancel;\n c.busy = true;\n c.error = undefined;\n emit();\n Promise.resolve()\n .then(() => fn?.(r))\n .then(\n () => finish(c, r),\n () => {\n c.busy = false;\n c.error = FAILED;\n emit();\n },\n );\n };\n\n const openCancelFlow = (options: CancelFlowOptions = {}) => {\n if (cancel) return cancel.promise;\n const flow = config?.cancelFlow ?? PLAIN_CANCEL;\n const plain = options.customer !== \"business\" || flow.reasons.length < 2;\n let resolve: (r: CancelFlowResult) => void = () => {};\n const promise = new Promise<CancelFlowResult>((r) => (resolve = r));\n const c: CancelRun = { flow, options, plain, screen: plain ? \"confirm\" : \"survey\", run: runId(), busy: false, resolve, promise };\n cancel = c;\n env.track(CANCEL_EVENT, { action: \"shown\", run: c.run, version: flow.version, customer: options.customer ?? \"consumer\", plain });\n emit();\n return promise;\n };\n\n const controller: FlowsController = {\n setConfig: (c) => {\n config = c;\n if (active && !flowById(active)) active = null;\n emit();\n },\n refresh,\n onEvent: (name) => {\n if (!name || name.startsWith(\"$\")) return;\n // Remembered for \"Did <event> happen?\" conditions the person reaches later.\n const slug = eventSlug(name);\n const fresh = !!slug && !seenEvents.has(slug);\n if (fresh) seenEvents.add(slug);\n const flow = flowById(active);\n let ticked = false;\n if (flow) {\n const p = prog(flow.id);\n for (const s of stepsIn(nodesOf(flow)))\n if (s.kind === \"checklist\" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {\n markDone(p, s.id);\n ticked = true;\n env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: \"completed\" });\n }\n }\n if (ticked || (fresh && flow)) {\n save();\n emit();\n } else if (fresh) save();\n // Like the first-launch start: only for someone whose progress is kept, so nobody gets a flow twice.\n if (!auto || (!env.personId() && !env.storage?.())) return;\n const next = flows().find((f) => nodesOf(f).length && matchesTrigger(f.trigger, name) && !finished(f));\n if (next) start(next);\n },\n openCancelFlow,\n startOnboarding: (id) => {\n const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !finished(x));\n if (!f || !nodesOf(f).length) return false;\n const p = prog(f.id);\n // Started by hand: a flow the person closed before shows again.\n p.dismissed = false;\n return start(f);\n },\n view,\n subscribe: (l) => {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n cancel: {\n pick: (reason) => {\n if (!cancel || cancel.busy || !cancel.flow.reasons.some((r) => r.id === reason)) return;\n cancel.reason = reason;\n cancel.error = undefined;\n emit();\n },\n next: () => {\n const c = cancel;\n if (!c || c.screen !== \"survey\" || !c.reason) return;\n env.track(CANCEL_EVENT, { action: \"answered\", run: c.run, version: c.flow.version, reason: c.reason });\n c.screen = c.flow.offers[c.reason] ? \"offer\" : \"confirm\";\n emit();\n },\n accept: (comment) => {\n const c = cancel;\n if (!c || c.screen !== \"offer\" || !c.reason) return;\n const offer = c.flow.offers[c.reason];\n if (!c.busy)\n env.track(CANCEL_EVENT, {\n action: \"offer\",\n run: c.run,\n version: c.flow.version,\n reason: c.reason,\n offer: offer?.kind,\n outcome: \"accepted\",\n });\n settle(c, \"saved\", comment);\n },\n decline: () => {\n const c = cancel;\n if (!c || c.screen !== \"offer\" || c.busy) return;\n env.track(CANCEL_EVENT, {\n action: \"offer\",\n run: c.run,\n version: c.flow.version,\n reason: c.reason,\n offer: c.reason ? c.flow.offers[c.reason]?.kind : undefined,\n outcome: \"declined\",\n });\n c.screen = \"confirm\";\n c.error = undefined;\n emit();\n },\n confirm: (comment) => {\n const c = cancel;\n if (!c || c.screen !== \"confirm\") return;\n settle(c, \"cancelled\", comment);\n },\n close: () => {\n const c = cancel;\n if (!c || c.busy) return;\n const r = result(c, \"kept\");\n c.options.onClose?.(r);\n finish(c, r);\n },\n },\n onboarding: {\n next: () => {\n const v = onboardingView();\n if (v?.type !== \"step\") return;\n markDone(prog(v.flow.id), v.step.id);\n env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: \"completed\" });\n save();\n emit();\n },\n check: (stepId) => {\n const v = onboardingView();\n if (v?.type !== \"checklist\") return;\n const item = v.items.find((i) => i.step.id === stepId);\n if (!item || item.done) return;\n markDone(prog(v.flow.id), stepId);\n env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: \"checklist\", action: \"completed\" });\n save();\n emit();\n },\n dismiss: () => {\n const v = onboardingView();\n if (!v || (v.type !== \"step\" && v.type !== \"checklist\")) return;\n prog(v.flow.id).dismissed = true;\n env.track(ONBOARDING_EVENT, { flow: v.flow.id, ...(v.type === \"step\" ? { step: v.step.id } : {}), action: \"dismissed\" });\n active = null;\n save();\n emit();\n },\n },\n message: {\n click: () => {\n const v = view();\n if (v?.type !== \"message\") return;\n handled.add(v.message.id);\n env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: \"clicked\" });\n const url = safeUrl(v.message.url);\n emit();\n if (url) env.openUrl?.(url);\n },\n dismiss: () => {\n const v = view();\n if (v?.type !== \"message\") return;\n handled.add(v.message.id);\n env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: \"dismissed\" });\n emit();\n },\n },\n };\n return controller;\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/flows.ts"],"sourcesContent":["/**\n * The cancel flow, onboarding and in-app messages (SDK 0.7.0): what MRRocket's config and `GET /api/sdk/inapp` serve,\n * and one controller that decides what shows when and records what the person did. No DOM and no React: the web\n * renderer (flows-ui.ts, loaded only when something shows) and the React Native one (native/flows-view.ts) draw\n * `view()` and call the actions.\n *\n * What it records, as normal events under the SDK's analytics consent (nothing with consent \"off\"):\n * - `$cancel_flow` `{action: \"shown\" | \"answered\" | \"offer\" | \"finished\", run, version, …}`. MRRocket turns a\n * `finished` with `outcome: \"saved\"` into a save and with `outcome: \"cancelled\"` into a cancellation on the Churn page.\n * MRRocket never changes a subscription: an accepted offer goes to the app's `onSave`, a confirmed cancel to its\n * `onCancel`, and only after they succeed is the outcome recorded.\n * - `$onboarding` `{flow, step?, action: \"started\" | \"shown\" | \"completed\" | \"dismissed\"}`; since 0.11.0 a wait records\n * \"shown\" when the person reaches it and \"completed\" when it is over, a condition `{action: \"answered\", answer: \"yes\"\n * | \"no\"}` once it is decided.\n * - `$inapp` `{message, journey, action: \"shown\" | \"clicked\" | \"dismissed\"}`; the journey moves the person on.\n */\nimport type { CockpitConfig } from \"./core\";\n\nexport const CANCEL_EVENT = \"$cancel_flow\";\nexport const ONBOARDING_EVENT = \"$onboarding\";\nexport const INAPP_EVENT = \"$inapp\";\n/** 0.16.0: the app asked the store's rating dialog for a review request (or found none on the device). */\nexport const REVIEW_EVENT = \"$review_prompt\";\n\nexport type CancelOfferKind = \"discount\" | \"pause\" | \"downgrade\" | \"contact\";\n\n/** What one answer of the survey offers before the confirmation. An answer without one goes straight to it. */\nexport interface CancelOffer {\n kind: CancelOfferKind;\n headline: string;\n /** Discount: percent off. */\n percent?: number;\n /** Discount: for how many months; pause: up to how many months. */\n months?: number;\n /** Downgrade: the plan to switch to, as MRRocket knows it from the payment provider. */\n plan?: string;\n planName?: string;\n}\n\n/** 0.15.0: a second question after an answer (\"What would be a fair price?\"); no answers means a free text. */\nexport interface CancelFollowUp {\n question: string;\n /** The choices, in order; empty for a free text. */\n answers: string[];\n}\n\n/**\n * 0.15.0: a different offer for some customers of one answer. Every condition that is set must hold; the first rule\n * that matches wins, else the answer's own offer. `offer: null` means no offer (straight to the confirmation).\n */\nexport interface CancelRule {\n when: {\n /** The follow-up's answer, as written. */\n answer?: string;\n /** The customer's plan: its id or name as MRRocket knows it from the payment provider (any of a list). */\n plan?: string | string[];\n /** A customer for fewer than this many months. */\n tenureUnder?: number;\n /** A customer for at least this many months. */\n tenureOver?: number;\n };\n offer: CancelOffer | null;\n}\n\n/** The product's live cancel flow (MRRocket › Churn › Cancel flow). */\nexport interface CancelFlowConfig {\n /** Grows with every publish. */\n version: number;\n question: string;\n /** The survey's answers, in order. */\n reasons: { id: string; label: string }[];\n /** The offer per answer id. */\n offers: Record<string, CancelOffer>;\n /** 0.15.0: the follow-up question per answer id. */\n followUps?: Record<string, CancelFollowUp>;\n /** 0.15.0: the rules per answer id, checked in order before its offer. */\n rules?: Record<string, CancelRule[]>;\n /** 0.15.0: what one answer offers after its first offer was declined. */\n second?: Record<string, CancelOffer>;\n confirm: { title: string; body: string; button: string };\n /**\n * Who gets the survey and the offers: business customers only. Anyone else (a consumer, or a customer the app does\n * not say) gets the plain confirmation, with nothing in the way of cancelling (§ 312k BGB).\n */\n offersFor: \"business\";\n}\n\nexport type OnboardingStepKind = \"tour\" | \"checklist\" | \"modal\";\n\nexport interface OnboardingStep {\n id: string;\n kind: OnboardingStepKind;\n title: string;\n body?: string;\n button?: string;\n /** Tour steps: the element they point at, marked `data-ck-target=\"<target>\"` in the app. Without it: a card. */\n target?: string;\n /** Checklist items: the event that ticks them (`track(\"<event>\")`). Without it the person ticks it themselves. */\n event?: string;\n}\n\n/** A pause in a flow (0.11.0): the next step shows this long after the one before was done. */\nexport interface OnboardingWait {\n id: string;\n kind: \"wait\";\n amount: number;\n unit: \"hours\" | \"days\";\n}\n\n/**\n * A fork in a flow (0.11.0): \"Did <event> happen?\" (the person's events, here and on MRRocket) or \"Is on <page>?\" (the\n * page they are on when they reach it; `*` at the end for everything below). Decided once, when the person reaches it;\n * like a journey's branch it ends its line, the rest of the flow lives in its two paths.\n */\nexport interface OnboardingCondition {\n id: string;\n kind: \"condition\";\n event?: string;\n page?: string;\n yes: OnboardingNode[];\n no: OnboardingNode[];\n}\n\nexport type OnboardingNode = OnboardingStep | OnboardingWait | OnboardingCondition;\n\n/** A live onboarding flow's in-app steps (MRRocket › Conversion › Onboarding); its emails go out from MRRocket. */\nexport interface OnboardingFlow {\n id: string;\n name: string;\n /** When it starts: an event's words (\"Signed up\", \"First export\"), or \"First launch\" / \"First visit\". */\n trigger: string;\n /** What every version shows: with `nodes`, only the in-app steps before the first wait or condition. */\n steps: OnboardingStep[];\n /** 0.11.0+: the whole flow with its waits and conditions. Read instead of `steps` when it is there. */\n nodes?: OnboardingNode[];\n}\n\n/** An in-app message a Lifecycle journey has for this person. */\nexport interface InAppMessage {\n id: string;\n journey?: string;\n title: string;\n body?: string;\n button?: string;\n /** Where the button goes (http(s) or a path of the app). */\n url?: string;\n style: \"banner\" | \"modal\";\n}\n\n/** One flow's progress: the steps done, whether the person closed it, and (0.11.0) its decided conditions and times. */\nexport interface FlowProgress {\n done: string[];\n dismissed?: boolean;\n /** Each decided condition's path. */\n answers?: Record<string, \"yes\" | \"no\">;\n /** When each step was done and each wait was reached (ms since 1970). */\n at?: Record<string, number>;\n reached?: Record<string, number>;\n}\n\n/** What `GET /api/sdk/inapp` answers for one signed-in person. */\nexport interface InAppState {\n /** Progress per onboarding flow: the steps done and whether the person closed it. */\n onboarding: Record<string, FlowProgress>;\n messages: InAppMessage[];\n /** 0.11.0: the events the flows' conditions ask about that this person did (as `eventSlug`s). */\n events?: string[];\n /**\n * 0.16.0: a live review request may ask this person now. The React Native client's `requestReview()` opens the\n * store's own rating dialog (expo-store-review) and reports `$review_prompt`; websites have no store dialog.\n */\n review?: { campaign: string; store: string };\n}\n\nexport interface CancelFlowResult {\n /** \"cancelled\": confirmed; \"saved\": took the offer; \"kept\": closed the dialog without cancelling. */\n outcome: \"cancelled\" | \"saved\" | \"kept\";\n reason?: string;\n reasonLabel?: string;\n /** 0.15.0: the answer to the follow-up question. */\n answer?: string;\n offer?: CancelOffer;\n /** What the customer wrote on the confirmation, if anything. */\n comment?: string;\n /** This run's id, the same as in the events. */\n run: string;\n}\n\nexport interface CancelFlowOptions {\n /**\n * \"business\" shows the survey and the offers; anything else (the default) only the plain confirmation, so a\n * consumer can always cancel in one step (§ 312k BGB).\n */\n customer?: \"business\" | \"consumer\";\n /** The customer's current price, to show what a discount costs (`{ amount: 29, currency: \"EUR\" }`). */\n price?: { amount: number; currency: string; interval?: \"month\" | \"year\" };\n /** 0.15.0: the customer's plan (its id or name in your billing), for offers that depend on it. */\n plan?: string;\n /** 0.15.0: since when they pay (a date, ISO string or ms), for offers that depend on how long. */\n customerSince?: Date | string | number;\n /** The customer confirmed: cancel the subscription in your code. Throwing keeps the dialog open with an error. */\n onCancel?: (result: CancelFlowResult) => unknown;\n /** The customer took an offer: apply it in your code (coupon, pause, plan change). Throwing keeps the dialog open. */\n onSave?: (result: CancelFlowResult) => unknown;\n /** Closed without cancelling or taking an offer. */\n onClose?: (result: CancelFlowResult) => void;\n}\n\n/** What a renderer draws. */\nexport type FlowView =\n | {\n type: \"cancel\";\n screen: \"survey\" | \"followup\" | \"offer\" | \"confirm\";\n flow: CancelFlowConfig;\n /** The answer picked on the survey. */\n reason?: string;\n /** 0.15.0: the follow-up question on its screen, and its answer so far. */\n followUp?: CancelFollowUp;\n answer?: string;\n offer?: CancelOffer;\n /** 0.15.0: 2 for the offer after a declined one. */\n round?: 1 | 2;\n price?: CancelFlowOptions[\"price\"];\n busy: boolean;\n error?: string;\n /** The step for the dots, 1 to `steps`. */\n step: number;\n steps: number;\n }\n | { type: \"step\"; flow: OnboardingFlow; step: OnboardingStep; index: number; total: number; last: boolean }\n | { type: \"checklist\"; flow: OnboardingFlow; items: { step: OnboardingStep; done: boolean }[] }\n | { type: \"message\"; message: InAppMessage };\n\nexport interface FlowsEnv {\n /** Records an event under the SDK's consent (the normal queue). */\n track(event: string, props?: Record<string, unknown>): void;\n /** Whose messages and progress these are: the identify() id; undefined while nobody is signed in. */\n personId(): string | undefined;\n /** `GET /api/sdk/inapp` for that person; null when it fails. */\n fetchInApp(personId: string): Promise<InAppState | null>;\n /** Keeps onboarding progress on the device; only while the consent lets the SDK store something. */\n storage?: () => { get(key: string): string | null | Promise<string | null>; set(key: string, value: string): void } | null;\n /** Opens a message's link. */\n openUrl?: (url: string) => void;\n /** Start onboarding flows by their trigger on its own (default true). */\n auto?: boolean;\n /** The page the person is on (a path), for \"Is on <page>?\" conditions; without it they answer no. */\n path?: () => string | undefined;\n /** The clock, for tests. */\n now?: () => number;\n}\n\nexport interface FlowsController {\n setConfig(config: CockpitConfig | null): void;\n /** The person changed (identify, reset, consent): loads their messages and onboarding progress. */\n refresh(): Promise<void>;\n /** An event the app recorded: starts flows whose trigger it is and ticks checklist items. */\n onEvent(name: string): void;\n openCancelFlow(options?: CancelFlowOptions): Promise<CancelFlowResult>;\n /** Starts an onboarding flow now (the first live one without an id). False when there is none to start. */\n startOnboarding(id?: string): boolean;\n view(): FlowView | null;\n subscribe(listener: () => void): () => void;\n /** `accept(comment)`: a \"Talk to us\" offer sends what the customer wrote with it. */\n /** `answer(text)`: the follow-up's choice or free text (0.15.0); `next()` moves on from the survey and the follow-up. */\n cancel: {\n pick(reason: string): void;\n answer(text: string): void;\n next(): void;\n accept(comment?: string): void;\n decline(): void;\n confirm(comment?: string): void;\n close(): void;\n };\n onboarding: { next(): void; check(stepId: string): void; dismiss(): void };\n message: { click(): void; dismiss(): void };\n}\n\n/** The plain confirmation, when the product has no live cancel flow (or MRRocket was not reachable). */\nexport const PLAIN_CANCEL: CancelFlowConfig = {\n version: 0,\n question: \"\",\n reasons: [],\n offers: {},\n confirm: { title: \"Cancel your subscription?\", body: \"You keep access until the end of the billing period.\", button: \"Cancel subscription\" },\n offersFor: \"business\",\n};\n\nconst FAILED = \"Something went wrong. Please try again.\";\nconst PROGRESS_KEY = \"mrrocket:onboarding\";\n\n/** `\"Signed up\"` → `signed_up`, the way MRRocket names events from words. */\nexport const eventSlug = (s: string) =>\n s\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n\nconst SIGNUP = new Set([\"signed_up\", \"signup\", \"sign_up\", \"registered\", \"account_created\", \"user_signed_up\"]);\n\n/** Whether recording `event` is the trigger's moment (\"Signed up\" is any of the usual signup event names). */\nexport const matchesTrigger = (trigger: string, event: string) => {\n const t = eventSlug(trigger);\n const e = eventSlug(event);\n if (!t || !e) return false;\n return t === e || (SIGNUP.has(t) && SIGNUP.has(e));\n};\n\n/** Triggers that mean \"the first time the person opens the app\": the flow starts on load until it is done. */\nexport const startsOnLoad = (trigger: string) =>\n /^(first (launch|visit|open|session|login|sign ?in|app open)|app (opened|launched)|on load|every visit|always)?$/i.test(trigger.trim());\n\n/** A random id for one cancel flow run. */\nexport const runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;\n\n/** The money a discount leaves: `29 EUR` at 30 % → `20.30`. */\nexport const discounted = (amount: number, percent: number) => Math.round(amount * (1 - percent / 100) * 100) / 100;\n\n/** \"€20.30\" in the customer's own currency, without a library. */\nexport const money = (amount: number, currency: string) => {\n try {\n return new Intl.NumberFormat(undefined, { style: \"currency\", currency }).format(amount);\n } catch {\n return `${amount.toFixed(2)} ${currency}`;\n }\n};\n\n/** The button of an offer. */\nexport const offerButton = (o: CancelOffer) =>\n o.kind === \"contact\" ? \"Send to the team\" : o.kind === \"pause\" ? \"Pause instead\" : o.kind === \"downgrade\" ? \"Switch plan\" : \"Accept offer\";\n\n/** A link a message may open: http(s), or a path of the app. Nothing else (no `javascript:`). */\nexport const safeUrl = (url: string | undefined) => {\n const u = (url ?? \"\").trim();\n return /^https?:\\/\\//i.test(u) || /^\\/(?!\\/)/.test(u) ? u : undefined;\n};\n\ninterface Progress {\n done: Set<string>;\n dismissed: boolean;\n answers: Map<string, boolean>;\n at: Map<string, number>;\n reached: Map<string, number>;\n}\n\n/** The flow's nodes: the whole tree (0.11.0 config), or the plain steps of an older one. */\nexport const nodesOf = (f: OnboardingFlow): OnboardingNode[] => f.nodes ?? f.steps;\n\n/** How long a wait lasts. */\nexport const waitMs = (w: Pick<OnboardingWait, \"amount\" | \"unit\">) => Math.max(0, w.amount) * (w.unit === \"hours\" ? 3_600_000 : 86_400_000);\n\n/** Whether a path matches a condition's page: the same path, or below it for a pattern ending in `*`. */\nexport const pageMatches = (pattern: string, path: string) => {\n const p = pattern.trim();\n const at = (path.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n if (!p) return false;\n if (p.endsWith(\"*\")) {\n const base = p.slice(0, -1).replace(/\\/+$/, \"\");\n return !base || at === base || at.startsWith(`${base}/`);\n }\n return at === (p.replace(/\\/+$/, \"\") || \"/\");\n};\n\n/** Every in-app step of a tree, in reading order (for ticking checklist items by their event). */\nconst stepsIn = (nodes: OnboardingNode[]): OnboardingStep[] =>\n nodes.flatMap((n) => (n.kind === \"condition\" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === \"wait\" ? [] : [n]));\n\ntype Walked =\n | { at: \"step\"; step: OnboardingStep; line: OnboardingNode[]; index: number; before: number }\n | { at: \"wait\"; until: number }\n | { at: \"pending\" }\n | { at: \"end\" };\n\n/** Whole months since a date the app passed (NaN without one, which no tenure condition holds for). */\nexport const monthsSince = (since: CancelFlowOptions[\"customerSince\"], now = Date.now()) =>\n since == null || since === \"\" ? NaN : Math.floor((now - new Date(since).getTime()) / 2_630_016_000);\n\nconst low = (x?: string) => x?.trim().toLowerCase();\n\n/** Whether a rule holds for this customer; a condition about something the app did not pass does not hold. */\nexport const ruleMatches = ({ when: w = {} }: CancelRule, c: { answer?: string; plan?: string; months: number }) =>\n (w.answer == null || low(w.answer) === low(c.answer)) &&\n (w.plan == null || [w.plan].flat().some((x) => low(x) === low(c.plan))) &&\n !(c.months >= w.tenureUnder!) &&\n !(c.months < w.tenureOver!) &&\n (w.tenureUnder == null || c.months >= 0) &&\n (w.tenureOver == null || c.months >= 0);\n\ninterface CancelRun {\n flow: CancelFlowConfig;\n options: CancelFlowOptions;\n plain: boolean;\n screen: \"survey\" | \"followup\" | \"offer\" | \"confirm\";\n reason?: string;\n answer?: string;\n /** The offer on the offer screen, which rule picked it, and which round it is. */\n offer?: CancelOffer;\n rule?: number;\n round: 1 | 2;\n run: string;\n busy: boolean;\n error?: string;\n resolve: (r: CancelFlowResult) => void;\n promise: Promise<CancelFlowResult>;\n}\n\nexport const createFlows = (env: FlowsEnv): FlowsController => {\n let config: CockpitConfig | null = null;\n let cancel: CancelRun | null = null;\n let active: string | null = null;\n let messages: InAppMessage[] = [];\n let known = false;\n let loading: Promise<void> | null = null;\n let loadedFor: string | undefined;\n let progressFor: string | undefined | null = null;\n const progress = new Map<string, Progress>();\n const handled = new Set<string>();\n const shown = new Set<string>();\n const listeners = new Set<() => void>();\n const auto = env.auto !== false;\n /** Events the person did (as slugs), for \"Did <event> happen?\": from MRRocket and from this device. */\n const seenEvents = new Set<string>();\n const now = () => env.now?.() ?? Date.now();\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const flows = () => config?.onboarding ?? [];\n const flowById = (id: string | null) => (id ? flows().find((f) => f.id === id) : undefined);\n const prog = (id: string) => {\n let p = progress.get(id);\n if (!p) progress.set(id, (p = { done: new Set(), dismissed: false, answers: new Map(), at: new Map(), reached: new Map() }));\n return p;\n };\n\n let dirty = false;\n const record = (props: Record<string, unknown>) => {\n env.track(ONBOARDING_EVENT, props);\n dirty = true;\n };\n const markDone = (p: Progress, id: string) => {\n p.done.add(id);\n p.at.set(id, now());\n };\n\n /** A condition's answer for this person, or undefined while what it asks is not known yet. */\n const decide = (c: OnboardingCondition): boolean | undefined => {\n if (c.page?.trim()) {\n const path = env.path?.();\n return path === undefined ? false : pageMatches(c.page, path);\n }\n if (c.event?.trim()) {\n // The person's events from MRRocket arrive with their progress: before that, wait.\n if (!known) return undefined;\n for (const e of seenEvents) if (matchesTrigger(c.event, e)) return true;\n return false;\n }\n return false;\n };\n\n /**\n * Where the person is in a line of the flow. `live`: it is their flow now, so reaching a wait starts it, a decided\n * condition is kept and both are recorded; otherwise nothing changes (to ask whether a flow is finished).\n */\n const walkLine = (flow: OnboardingFlow, line: OnboardingNode[], p: Progress, live: boolean, before = 0, last?: number): Walked => {\n for (let i = 0; i < line.length; i++) {\n const n = line[i];\n if (n.kind === \"wait\") {\n if (p.done.has(n.id)) {\n last = p.at.get(n.id) ?? last;\n continue;\n }\n let since = p.reached.get(n.id);\n if (since === undefined) {\n if (!live) return { at: \"pending\" };\n // Counted from when the step before was done; a flow that opens with a wait counts from now.\n since = last ?? now();\n p.reached.set(n.id, since);\n record({ flow: flow.id, step: n.id, kind: \"wait\", action: \"shown\" });\n }\n const until = since + waitMs(n);\n if (now() < until) return { at: \"wait\", until };\n if (!live) return { at: \"pending\" };\n markDone(p, n.id);\n record({ flow: flow.id, step: n.id, kind: \"wait\", action: \"completed\" });\n last = until;\n continue;\n }\n if (n.kind === \"condition\") {\n let answer = p.answers.get(n.id);\n if (answer === undefined) {\n if (!live) return { at: \"pending\" };\n answer = decide(n);\n if (answer === undefined) return { at: \"pending\" };\n p.answers.set(n.id, answer);\n record({ flow: flow.id, step: n.id, kind: \"condition\", action: \"answered\", answer: answer ? \"yes\" : \"no\" });\n }\n return walkLine(flow, answer ? n.yes : n.no, p, live, before, last);\n }\n if (!p.done.has(n.id)) return { at: \"step\", step: n, line, index: i, before };\n last = p.at.get(n.id) ?? last;\n before++;\n }\n return { at: \"end\" };\n };\n\n const finished = (f: OnboardingFlow) => {\n const p = progress.get(f.id);\n return !!p && (p.dismissed || walkLine(f, nodesOf(f), p, false).at === \"end\");\n };\n\n const keyOf = (pid: string | undefined) => `${PROGRESS_KEY}:${pid ?? \"visitor\"}`;\n const eventsKeyOf = (pid: string | undefined) => `${PROGRESS_KEY}:events:${pid ?? \"visitor\"}`;\n\n const save = () => {\n dirty = false;\n const store = env.storage?.();\n if (!store) return;\n const out: Record<string, FlowProgress> = {};\n for (const [id, p] of progress)\n out[id] = {\n done: [...p.done],\n ...(p.dismissed ? { dismissed: true } : {}),\n ...(p.answers.size ? { answers: Object.fromEntries([...p.answers].map(([k, v]) => [k, v ? \"yes\" : \"no\"])) } : {}),\n ...(p.at.size ? { at: Object.fromEntries(p.at) } : {}),\n ...(p.reached.size ? { reached: Object.fromEntries(p.reached) } : {}),\n };\n try {\n store.set(keyOf(env.personId()), JSON.stringify(out));\n if (seenEvents.size) store.set(eventsKeyOf(env.personId()), JSON.stringify([...seenEvents].slice(-200)));\n } catch {\n // Storage full or gone: progress stays in memory.\n }\n };\n\n const merge = (state: InAppState[\"onboarding\"] | undefined) => {\n for (const [id, s] of Object.entries(state ?? {})) {\n const p = prog(id);\n for (const d of s.done ?? []) p.done.add(d);\n if (s.dismissed) p.dismissed = true;\n // A condition decided elsewhere keeps its path; the earliest time wins for waits.\n for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === \"yes\");\n for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === \"number\" && !p.at.has(k)) p.at.set(k, v);\n for (const [k, v] of Object.entries(s.reached ?? {})) if (typeof v === \"number\" && (!p.reached.has(k) || v < p.reached.get(k)!)) p.reached.set(k, v);\n }\n };\n\n // ---------- What shows ----------\n\n /** Comes back when a short wait is over, so the next step shows without a reload (long waits wait for the next visit). */\n const wakeAt = (until: number) => {\n if (timer) clearTimeout(timer);\n timer = null;\n const ms = until - now();\n if (ms > 0 && ms <= 86_400_000) {\n timer = setTimeout(() => ((timer = null), emit()), ms + 50);\n // Never what keeps a process alive (tests, server rendering).\n (timer as { unref?: () => void }).unref?.();\n }\n };\n\n /** The active flow waits (a wait step, or a condition whose answer is not known yet): it stays the active one. */\n let holding = false;\n\n const onboardingView = (): FlowView | null => {\n holding = false;\n const flow = flowById(active);\n if (!flow) return null;\n const p = prog(flow.id);\n if (p.dismissed) return null;\n const w = walkLine(flow, nodesOf(flow), p, true);\n if (dirty) save();\n if (w.at === \"wait\") wakeAt(w.until);\n holding = w.at === \"wait\" || w.at === \"pending\";\n if (w.at !== \"step\") return null;\n const { step, line, index } = w;\n const isStep = (n: OnboardingNode | undefined): n is OnboardingStep => !!n && n.kind !== \"wait\" && n.kind !== \"condition\";\n if (step.kind === \"checklist\") {\n let from = index;\n while (from > 0 && isStep(line[from - 1]) && line[from - 1].kind === \"checklist\") from--;\n let to = index;\n while (to + 1 < line.length && isStep(line[to + 1]) && line[to + 1].kind === \"checklist\") to++;\n const items = line.slice(from, to + 1).filter(isStep);\n for (const i of items)\n if (!p.done.has(i.id) && !shown.has(`s:${i.id}`)) {\n shown.add(`s:${i.id}`);\n env.track(ONBOARDING_EVENT, { flow: flow.id, step: i.id, kind: \"checklist\", action: \"shown\" });\n }\n return { type: \"checklist\", flow, items: items.map((s) => ({ step: s, done: p.done.has(s.id) })) };\n }\n if (!shown.has(`s:${step.id}`)) {\n shown.add(`s:${step.id}`);\n env.track(ONBOARDING_EVENT, { flow: flow.id, step: step.id, kind: step.kind, action: \"shown\" });\n }\n // The dots: the steps behind on the person's path, this one, and the ones left on its line before any fork.\n const rest = line.slice(index + 1);\n const fork = rest.findIndex((n) => n.kind === \"condition\");\n const ahead = (fork < 0 ? rest : rest.slice(0, fork)).filter(isStep).length;\n return { type: \"step\", flow, step, index: w.before, total: w.before + 1 + ahead, last: !ahead && fork < 0 };\n };\n\n const view = (): FlowView | null => {\n if (cancel) {\n const c = cancel;\n const followUp = c.flow.followUps?.[c.reason!];\n // The dots: the survey, a follow-up, one or two offers, the confirmation.\n const f = followUp ? 1 : 0;\n const steps = c.plain ? 1 : 3 + f + (c.flow.second?.[c.reason!] ? 1 : 0);\n return {\n type: \"cancel\",\n screen: c.screen,\n flow: c.flow,\n reason: c.reason,\n ...(c.screen === \"followup\" && followUp ? { followUp, answer: c.answer } : {}),\n offer: c.screen === \"offer\" ? c.offer : undefined,\n ...(c.screen === \"offer\" ? { round: c.round } : {}),\n price: c.options.price,\n busy: c.busy,\n error: c.error,\n step: c.screen === \"survey\" ? 1 : c.screen === \"followup\" ? 2 : c.screen === \"offer\" ? 1 + f + c.round : steps,\n steps,\n };\n }\n const ob = onboardingView();\n if (ob) return ob;\n // A flow that just ended lets the next thing show; one that waits keeps its place.\n if (active && !holding) active = null;\n const m = messages.find((x) => !handled.has(x.id));\n return m ? { type: \"message\", message: m } : null;\n };\n\n /** Tells the renderers, and records what became visible (once per step or message per page load). */\n const emit = () => {\n // Onboarding steps record their \"shown\" where they are worked out (onboardingView), so a step a wait let through\n // counts also when nothing else changed.\n const v = view();\n if (v?.type === \"message\" && !shown.has(`m:${v.message.id}`)) {\n shown.add(`m:${v.message.id}`);\n env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: \"shown\" });\n }\n for (const l of [...listeners]) {\n try {\n l();\n } catch {\n // One renderer must not stop another.\n }\n }\n };\n\n const start = (flow: OnboardingFlow) => {\n if (active === flow.id) return true;\n if (active && flowById(active) && !finished(flowById(active)!)) return false;\n active = flow.id;\n env.track(ONBOARDING_EVENT, { flow: flow.id, action: \"started\" });\n emit();\n return true;\n };\n\n /** Flows whose trigger is \"the first launch\" start once their progress is known (never twice for a done flow). */\n const autoStart = () => {\n if (!auto || active || !known) return;\n // Without an id or storage a \"first launch\" could not be told from any other: no automatic start then.\n if (!env.personId() && !env.storage?.()) return;\n const next = flows().find((f) => startsOnLoad(f.trigger) && nodesOf(f).length && !finished(f));\n if (next) start(next);\n };\n\n const refresh = () => {\n const pid = env.personId();\n // Another person on this device: nothing of the one before carries over.\n if (progressFor !== null && progressFor !== pid) {\n progress.clear();\n seenEvents.clear();\n handled.clear();\n active = null;\n loadedFor = undefined;\n }\n progressFor = pid;\n const run = (async () => {\n const store = env.storage?.();\n if (store) {\n try {\n const raw = await store.get(keyOf(pid));\n if (raw) merge(JSON.parse(raw) as InAppState[\"onboarding\"]);\n const seen = await store.get(eventsKeyOf(pid));\n if (seen) for (const e of JSON.parse(seen) as string[]) if (typeof e === \"string\") seenEvents.add(e);\n } catch {\n // A damaged entry is ignored.\n }\n }\n const wants = flows().length > 0 || !!config?.messages;\n if (pid && wants && loadedFor !== pid) {\n loadedFor = pid;\n const state = await env.fetchInApp(pid).catch(() => null);\n if (state && env.personId() === pid) {\n merge(state.onboarding);\n for (const e of state.events ?? []) if (typeof e === \"string\") seenEvents.add(eventSlug(e));\n messages = (state.messages ?? []).filter((m) => m && m.id && m.title);\n }\n }\n if (!pid) messages = [];\n known = true;\n autoStart();\n emit();\n })();\n loading = run.finally(() => {\n if (loading === run) loading = null;\n });\n return loading;\n };\n\n // ---------- Cancel flow ----------\n\n /** Which offer of the answer was on screen, for MRRocket to know what was saved: a rule's or the second. */\n const offerAt = (c: CancelRun) => (c.round > 1 ? { round: 2 } : c.rule! >= 0 ? { rule: c.rule } : {});\n\n /** Into the offer screen with the answer's first offer (the first rule that holds, else its own), or the confirmation. */\n const toOffer = (c: CancelRun) => {\n const who = { answer: c.answer, plan: c.options.plan, months: monthsSince(c.options.customerSince, env.now?.()) };\n const rules = c.flow.rules?.[c.reason!] ?? [];\n c.rule = rules.findIndex((r) => ruleMatches(r, who));\n c.offer = c.rule < 0 ? c.flow.offers[c.reason!] : (rules[c.rule].offer ?? undefined);\n c.round = 1;\n c.screen = c.offer ? \"offer\" : \"confirm\";\n };\n\n const result = (c: CancelRun, outcome: CancelFlowResult[\"outcome\"], comment?: string): CancelFlowResult => {\n const offer = c.screen === \"offer\" ? c.offer : undefined;\n return {\n outcome,\n run: c.run,\n ...(c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {}),\n ...(c.answer ? { answer: c.answer } : {}),\n ...(outcome === \"saved\" && offer ? { offer } : {}),\n ...(comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}),\n };\n };\n\n const finish = (c: CancelRun, r: CancelFlowResult) => {\n env.track(CANCEL_EVENT, {\n action: \"finished\",\n run: c.run,\n version: c.flow.version,\n outcome: r.outcome,\n customer: c.options.customer ?? \"consumer\",\n ...(r.reason ? { reason: r.reason } : {}),\n ...(r.answer ? { answer: r.answer } : {}),\n ...(r.offer ? { offer: r.offer.kind, ...offerAt(c) } : {}),\n ...(r.comment ? { comment: r.comment } : {}),\n });\n if (cancel === c) cancel = null;\n c.resolve(r);\n emit();\n };\n\n /** Runs the app's callback; the outcome is recorded only once it went through. */\n const settle = (c: CancelRun, outcome: \"saved\" | \"cancelled\", comment?: string) => {\n if (c.busy) return;\n const r = result(c, outcome, comment);\n const fn = outcome === \"saved\" ? c.options.onSave : c.options.onCancel;\n c.busy = true;\n c.error = undefined;\n emit();\n Promise.resolve()\n .then(() => fn?.(r))\n .then(\n () => finish(c, r),\n () => {\n c.busy = false;\n c.error = FAILED;\n emit();\n },\n );\n };\n\n const openCancelFlow = (options: CancelFlowOptions = {}) => {\n if (cancel) return cancel.promise;\n const flow = config?.cancelFlow ?? PLAIN_CANCEL;\n const plain = options.customer !== \"business\" || flow.reasons.length < 2;\n let resolve: (r: CancelFlowResult) => void = () => {};\n const promise = new Promise<CancelFlowResult>((r) => (resolve = r));\n const c: CancelRun = { flow, options, plain, screen: plain ? \"confirm\" : \"survey\", round: 1, run: runId(), busy: false, resolve, promise };\n cancel = c;\n env.track(CANCEL_EVENT, { action: \"shown\", run: c.run, version: flow.version, customer: options.customer ?? \"consumer\", plain });\n emit();\n return promise;\n };\n\n const controller: FlowsController = {\n setConfig: (c) => {\n config = c;\n if (active && !flowById(active)) active = null;\n emit();\n },\n refresh,\n onEvent: (name) => {\n if (!name || name.startsWith(\"$\")) return;\n // Remembered for \"Did <event> happen?\" conditions the person reaches later.\n const slug = eventSlug(name);\n const fresh = !!slug && !seenEvents.has(slug);\n if (fresh) seenEvents.add(slug);\n const flow = flowById(active);\n let ticked = false;\n if (flow) {\n const p = prog(flow.id);\n for (const s of stepsIn(nodesOf(flow)))\n if (s.kind === \"checklist\" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {\n markDone(p, s.id);\n ticked = true;\n env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: \"completed\" });\n }\n }\n if (ticked || (fresh && flow)) {\n save();\n emit();\n } else if (fresh) save();\n // Like the first-launch start: only for someone whose progress is kept, so nobody gets a flow twice.\n if (!auto || (!env.personId() && !env.storage?.())) return;\n const next = flows().find((f) => nodesOf(f).length && matchesTrigger(f.trigger, name) && !finished(f));\n if (next) start(next);\n },\n openCancelFlow,\n startOnboarding: (id) => {\n const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !finished(x));\n if (!f || !nodesOf(f).length) return false;\n const p = prog(f.id);\n // Started by hand: a flow the person closed before shows again.\n p.dismissed = false;\n return start(f);\n },\n view,\n subscribe: (l) => {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n cancel: {\n pick: (reason) => {\n if (!cancel || cancel.busy || cancel.screen !== \"survey\" || !cancel.flow.reasons.some((r) => r.id === reason)) return;\n if (cancel.reason !== reason) cancel.answer = undefined;\n cancel.reason = reason;\n cancel.error = undefined;\n emit();\n },\n answer: (text) => {\n const c = cancel;\n if (!c || c.busy || c.screen !== \"followup\") return;\n const t = text.slice(0, 500);\n c.answer = t.trim() ? t : undefined;\n emit();\n },\n next: () => {\n const c = cancel;\n if (!c || !c.reason) return;\n // The follow-up's answer rides along on the finished event.\n if (c.screen === \"survey\") {\n env.track(CANCEL_EVENT, { action: \"answered\", run: c.run, version: c.flow.version, reason: c.reason });\n if (c.flow.followUps?.[c.reason]) c.screen = \"followup\";\n else toOffer(c);\n } else if (c.screen === \"followup\") {\n c.answer = c.answer?.trim();\n toOffer(c);\n } else return;\n emit();\n },\n accept: (comment) => {\n const c = cancel;\n if (!c || c.screen !== \"offer\" || !c.reason) return;\n if (!c.busy)\n env.track(CANCEL_EVENT, {\n action: \"offer\",\n run: c.run,\n version: c.flow.version,\n reason: c.reason,\n offer: c.offer?.kind,\n ...offerAt(c),\n outcome: \"accepted\",\n });\n settle(c, \"saved\", comment);\n },\n decline: () => {\n const c = cancel;\n if (!c || c.screen !== \"offer\" || c.busy) return;\n env.track(CANCEL_EVENT, {\n action: \"offer\",\n run: c.run,\n version: c.flow.version,\n reason: c.reason,\n offer: c.offer?.kind,\n ...offerAt(c),\n outcome: \"declined\",\n });\n // A second offer once, then the confirmation.\n const second = c.round === 1 && c.reason ? c.flow.second?.[c.reason] : undefined;\n if (second) {\n c.offer = second;\n c.rule = undefined;\n c.round = 2;\n } else c.screen = \"confirm\";\n c.error = undefined;\n emit();\n },\n confirm: (comment) => {\n const c = cancel;\n if (!c || c.screen !== \"confirm\") return;\n settle(c, \"cancelled\", comment);\n },\n close: () => {\n const c = cancel;\n if (!c || c.busy) return;\n const r = result(c, \"kept\");\n c.options.onClose?.(r);\n finish(c, r);\n },\n },\n onboarding: {\n next: () => {\n const v = onboardingView();\n if (v?.type !== \"step\") return;\n markDone(prog(v.flow.id), v.step.id);\n env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: \"completed\" });\n save();\n emit();\n },\n check: (stepId) => {\n const v = onboardingView();\n if (v?.type !== \"checklist\") return;\n const item = v.items.find((i) => i.step.id === stepId);\n if (!item || item.done) return;\n markDone(prog(v.flow.id), stepId);\n env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: \"checklist\", action: \"completed\" });\n save();\n emit();\n },\n dismiss: () => {\n const v = onboardingView();\n if (!v || (v.type !== \"step\" && v.type !== \"checklist\")) return;\n prog(v.flow.id).dismissed = true;\n env.track(ONBOARDING_EVENT, { flow: v.flow.id, ...(v.type === \"step\" ? { step: v.step.id } : {}), action: \"dismissed\" });\n active = null;\n save();\n emit();\n },\n },\n message: {\n click: () => {\n const v = view();\n if (v?.type !== \"message\") return;\n handled.add(v.message.id);\n env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: \"clicked\" });\n const url = safeUrl(v.message.url);\n emit();\n if (url) env.openUrl?.(url);\n },\n dismiss: () => {\n const v = view();\n if (v?.type !== \"message\") return;\n handled.add(v.message.id);\n env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: \"dismissed\" });\n emit();\n },\n },\n };\n return controller;\n};\n"],"mappings":"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","names":[]}
package/dist/forms.d.ts CHANGED
@@ -56,6 +56,8 @@ export interface FormLead extends FormFields {
56
56
  referrer?: string;
57
57
  utm?: Record<string, string>;
58
58
  distinctId?: string;
59
+ /** The identity hash of `distinctId` (0.16.0), when the site signed the person. */
60
+ userHash?: string;
59
61
  }
60
62
  /** The form's name: the value of `data-mrrocket-form`, else its name, id or label, else "Form on /path". */
61
63
  export declare const formName: (form: HTMLFormElement) => string;
@@ -70,6 +72,8 @@ export interface WatchFormsOptions {
70
72
  auto?: boolean;
71
73
  /** The visitor id to link the lead to, while analytics consent allows one. */
72
74
  distinctId: () => string | undefined;
75
+ /** The identity hash of that id (0.16.0), when the site gave one to identify. */
76
+ userHash?: (id: string) => string | undefined;
73
77
  /** Whether the site already called identify(). */
74
78
  identified: () => boolean;
75
79
  /** Links this visitor's history to the person (the SDK's own identify). */
package/dist/forms.js CHANGED
@@ -85,6 +85,8 @@ var watchForms = (o) => {
85
85
  const read = fieldsOf(inputsOf(form));
86
86
  if (!read.email) return;
87
87
  const utm = Object.fromEntries(Object.entries(o.utm?.() ?? {}).filter((x) => typeof x[1] === "string" && !!x[1]));
88
+ const distinctId = o.distinctId();
89
+ const userHash = distinctId ? o.userHash?.(distinctId) : void 0;
88
90
  const lead = {
89
91
  key: o.apiKey,
90
92
  form: formName(form),
@@ -92,7 +94,8 @@ var watchForms = (o) => {
92
94
  title: document.title.slice(0, 200) || void 0,
93
95
  referrer: o.referrer?.() || document.referrer || void 0,
94
96
  ...Object.keys(utm).length ? { utm } : {},
95
- distinctId: o.distinctId(),
97
+ distinctId,
98
+ ...userHash ? { userHash } : {},
96
99
  ...read
97
100
  };
98
101
  const body = JSON.stringify(lead);
package/dist/forms.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/forms.ts"],"sourcesContent":["/**\n * Form leads (0.12.0): a form on the customer's site becomes a contact in MRRocket when it is submitted. Forms marked\n * `data-mrrocket-form` (its value names the form, e.g. `data-mrrocket-form=\"Demo request\"`) are captured; with\n * auto-detection on (`data-forms=\"auto\"` on the script tag, `forms=\"auto\"` on the provider) also any form with an email\n * field, except forms with a password field (sign-in and sign-up) and forms marked `data-mrrocket-ignore`.\n *\n * What is sent: the email (required), the name, and the other fields a person typed, never passwords, card fields,\n * hidden fields, files or fields whose name looks sensitive (`SENSITIVE`); a field or form part marked\n * `data-mrrocket-ignore` stays out too. Plus the form's name, the page, the referrer, UTM, and the state of a checkbox\n * marked `data-mrrocket-consent` (the visitor's own yes to hear from the site). MRRocket records consent as the contact's\n * legal basis only when that box was ticked.\n *\n * No React and no globals beyond the DOM, so the script tag (`sdk-forms.js`, loaded only on pages with a form to\n * capture) and the Next.js provider (a dynamic import) share it. `fieldsOf` is pure for the tests.\n */\n\n/** Field names that never leave the page, whatever the form says. */\nexport const SENSITIVE =\n /pass(word|wd|code)?|pwd|secret|token|csrf|xsrf|nonce|captcha|honeypot|card|^cc([-_ ]|num|exp|$)|cvv|cvc|csc|expir|iban|(^|[^a-z])bic([^a-z]|$)|swift|routing|account[-_ ]?(no|num)|sort[-_ ]?code|(^|[^a-z])ssn([^a-z]|$)|social[-_ ]?sec|tax[-_ ]?id|vat[-_ ]?id|steuer|passport|(^|[^a-z])(pin|otp|pw)([^a-z]|$)|one[-_ ]?time|auth(?!or)|api[-_ ]?key|session|signature/i;\n\n/** Input types whose value is never sent. */\nconst SKIP_TYPES = new Set([\"password\", \"hidden\", \"file\", \"submit\", \"button\", \"reset\", \"image\"]);\n\nexport const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\n\n/** Limits: what a lead may carry, also checked again by the server. */\nexport const MAX_FIELDS = 30;\nexport const MAX_VALUE = 1000;\n\n/** One control of a form as the filter sees it. */\nexport interface FieldInput {\n name: string;\n type: string;\n value: string;\n autocomplete?: string;\n /** A checkbox or radio that is not ticked. */\n unchecked?: boolean;\n /** Marked `data-mrrocket-ignore` (itself or a part of the form around it). */\n ignored?: boolean;\n /** Marked `data-mrrocket-consent`. */\n consent?: boolean;\n}\n\nexport interface FormFields {\n email: string | null;\n name: string | null;\n /** The other fields, by name; checkbox groups joined with \", \". */\n fields: Record<string, string>;\n /** True or false when the form has a checkbox marked `data-mrrocket-consent`; undefined without one. */\n consent?: boolean;\n}\n\nconst norm = (s: string) => s.toLowerCase().replace(/[\\s_-]+/g, \"\");\n\n/** Whether a field's value may be sent: not a password, card, hidden or sensitive-looking field. */\nexport const sendable = (f: Pick<FieldInput, \"name\" | \"type\" | \"autocomplete\" | \"ignored\">) => {\n if (f.ignored || SKIP_TYPES.has(f.type)) return false;\n const ac = (f.autocomplete ?? \"\").toLowerCase();\n if (/^cc-|password|one-time-code/.test(ac)) return false;\n return !SENSITIVE.test(f.name);\n};\n\nconst isEmailField = (f: FieldInput) => f.type === \"email\" || /e-?mail/i.test(f.name) || f.autocomplete === \"email\";\n\n/**\n * The lead in a form's controls: the email (first valid one), the name (a full-name field, or first and last), the\n * consent box's state, and the other sendable fields. Pure.\n */\nexport const fieldsOf = (inputs: FieldInput[]): FormFields => {\n let email: string | null = null;\n let full: string | null = null;\n let first: string | null = null;\n let last: string | null = null;\n let consent: boolean | undefined;\n const fields: Record<string, string> = {};\n for (const f of inputs) {\n if (f.consent) {\n consent = (consent ?? false) || !f.unchecked;\n continue;\n }\n if (!f.name || !sendable(f) || f.unchecked) continue;\n const value = f.value.trim().slice(0, MAX_VALUE);\n if (!value) continue;\n const n = norm(f.name);\n const ac = (f.autocomplete ?? \"\").toLowerCase();\n if (!email && isEmailField(f) && EMAIL_RE.test(value)) {\n email = value.toLowerCase();\n continue;\n }\n if (!full && (ac === \"name\" || [\"name\", \"fullname\", \"yourname\", \"contactname\"].includes(n))) {\n full = value.slice(0, 100);\n continue;\n }\n if (!first && (ac === \"given-name\" || [\"firstname\", \"first\", \"vorname\", \"givenname\", \"fname\"].includes(n))) {\n first = value.slice(0, 60);\n continue;\n }\n if (!last && (ac === \"family-name\" || [\"lastname\", \"last\", \"nachname\", \"surname\", \"familyname\", \"lname\"].includes(n))) {\n last = value.slice(0, 60);\n continue;\n }\n const key = f.name.trim().slice(0, 60);\n if (key in fields) fields[key] = `${fields[key]}, ${value}`.slice(0, MAX_VALUE);\n else if (Object.keys(fields).length < MAX_FIELDS) fields[key] = value;\n }\n const name = full ?? ([first, last].filter(Boolean).join(\" \") || null);\n return { email, name, fields, ...(consent === undefined ? {} : { consent }) };\n};\n\n/** What the page sends to `POST /api/sdk/forms`. */\nexport interface FormLead extends FormFields {\n key: string;\n form: string;\n url: string;\n title?: string;\n referrer?: string;\n utm?: Record<string, string>;\n distinctId?: string;\n}\n\n/** The form's name: the value of `data-mrrocket-form`, else its name, id or label, else \"Form on /path\". */\nexport const formName = (form: HTMLFormElement) =>\n (form.getAttribute(\"data-mrrocket-form\") || form.getAttribute(\"name\") || form.id || form.getAttribute(\"aria-label\") || `Form on ${window.location.pathname}`)\n .trim()\n .slice(0, 100);\n\n/** Whether a form is captured: marked, or (auto) one with an email field and no password field. */\nexport const captures = (form: HTMLFormElement, auto: boolean) => {\n if (form.closest(\"[data-mrrocket-ignore]\")) return false;\n if (form.hasAttribute(\"data-mrrocket-form\")) return true;\n return auto && !!form.querySelector('input[type=\"email\"], input[name*=\"email\" i]') && !form.querySelector('input[type=\"password\"]');\n};\n\n/** The controls of a form, read for `fieldsOf`. */\nexport const inputsOf = (form: HTMLFormElement): FieldInput[] =>\n Array.from(form.elements).flatMap((el) => {\n if (!(el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement)) return [];\n if (el.disabled) return [];\n const type = el instanceof HTMLInputElement ? el.type.toLowerCase() : el instanceof HTMLSelectElement ? \"select\" : \"textarea\";\n const check = type === \"checkbox\" || type === \"radio\";\n const value = el instanceof HTMLSelectElement && el.multiple ? Array.from(el.selectedOptions, (o) => o.value).join(\", \") : check && el.value === \"on\" ? \"yes\" : el.value;\n return [\n {\n name: el.name || el.id || \"\",\n type,\n value,\n autocomplete: el.getAttribute(\"autocomplete\") ?? undefined,\n unchecked: check && !(el as HTMLInputElement).checked,\n ignored: !!el.closest(\"[data-mrrocket-ignore]\"),\n consent: el.hasAttribute(\"data-mrrocket-consent\"),\n },\n ];\n });\n\nexport interface WatchFormsOptions {\n host: string;\n apiKey: string;\n /** Also forms with an email field that are not marked (default false). */\n auto?: boolean;\n /** The visitor id to link the lead to, while analytics consent allows one. */\n distinctId: () => string | undefined;\n /** Whether the site already called identify(). */\n identified: () => boolean;\n /** Links this visitor's history to the person (the SDK's own identify). */\n identify: (id: string, props: Record<string, unknown>) => void;\n /** Whether analytics consent lets the SDK identify the visitor (not \"off\"). */\n tracking: () => boolean;\n referrer?: () => string | undefined;\n utm?: () => Record<string, string | undefined> | undefined;\n}\n\n/**\n * Captures submits from now on (one listener on the document, capture phase, so it sees forms that render later and\n * forms whose own handler stops the event). The lead goes out with `sendBeacon`, which survives the page leaving. Returns\n * a function that stops it. Calling it twice keeps one listener.\n */\nexport const watchForms = (o: WatchFormsOptions): (() => void) => {\n const w = window as Window & { __mrrocketFormsOff?: () => void };\n w.__mrrocketFormsOff?.();\n const onSubmit = (e: Event) => {\n const form = e.target;\n if (!(form instanceof HTMLFormElement) || !captures(form, !!o.auto)) return;\n const read = fieldsOf(inputsOf(form));\n if (!read.email) return;\n const utm = Object.fromEntries(Object.entries(o.utm?.() ?? {}).filter((x): x is [string, string] => typeof x[1] === \"string\" && !!x[1]));\n const lead: FormLead = {\n key: o.apiKey,\n form: formName(form),\n url: window.location.href.slice(0, 500),\n title: document.title.slice(0, 200) || undefined,\n referrer: o.referrer?.() || document.referrer || undefined,\n ...(Object.keys(utm).length ? { utm } : {}),\n distinctId: o.distinctId(),\n ...read,\n };\n const body = JSON.stringify(lead);\n const url = `${o.host}/api/sdk/forms`;\n // text/plain keeps it a simple request (no CORS preflight), which sendBeacon needs anyway.\n const sent = \"sendBeacon\" in navigator && navigator.sendBeacon(url, new Blob([body], { type: \"text/plain\" }));\n if (!sent) void fetch(url, { method: \"POST\", headers: { \"content-type\": \"text/plain\" }, body, keepalive: true }).catch(() => undefined);\n // Their visits before the form and after it belong to the same person from now on.\n if (o.tracking() && !o.identified()) o.identify(read.email, { email: read.email, ...(read.name ? { name: read.name } : {}) });\n };\n document.addEventListener(\"submit\", onSubmit, true);\n const off = () => {\n document.removeEventListener(\"submit\", onSubmit, true);\n if (w.__mrrocketFormsOff === off) delete w.__mrrocketFormsOff;\n };\n w.__mrrocketFormsOff = off;\n return off;\n};\n"],"mappings":";AAiBO,IAAM,YACT;AAGJ,IAAM,aAAa,oBAAI,IAAI,CAAC,YAAY,UAAU,QAAQ,UAAU,UAAU,SAAS,OAAO,CAAC;AAExF,IAAM,WAAW;AAGjB,IAAM,aAAa;AACnB,IAAM,YAAY;AAyBzB,IAAM,OAAO,CAAC,MAAc,EAAE,YAAY,EAAE,QAAQ,YAAY,EAAE;AAG3D,IAAM,WAAW,CAAC,MAAsE;AAC3F,MAAI,EAAE,WAAW,WAAW,IAAI,EAAE,IAAI,EAAG,QAAO;AAChD,QAAM,MAAM,EAAE,gBAAgB,IAAI,YAAY;AAC9C,MAAI,8BAA8B,KAAK,EAAE,EAAG,QAAO;AACnD,SAAO,CAAC,UAAU,KAAK,EAAE,IAAI;AACjC;AAEA,IAAM,eAAe,CAAC,MAAkB,EAAE,SAAS,WAAW,WAAW,KAAK,EAAE,IAAI,KAAK,EAAE,iBAAiB;AAMrG,IAAM,WAAW,CAAC,WAAqC;AAC1D,MAAI,QAAuB;AAC3B,MAAI,OAAsB;AAC1B,MAAI,QAAuB;AAC3B,MAAI,OAAsB;AAC1B,MAAI;AACJ,QAAM,SAAiC,CAAC;AACxC,aAAW,KAAK,QAAQ;AACpB,QAAI,EAAE,SAAS;AACX,iBAAW,WAAW,UAAU,CAAC,EAAE;AACnC;AAAA,IACJ;AACA,QAAI,CAAC,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,UAAW;AAC5C,UAAM,QAAQ,EAAE,MAAM,KAAK,EAAE,MAAM,GAAG,SAAS;AAC/C,QAAI,CAAC,MAAO;AACZ,UAAM,IAAI,KAAK,EAAE,IAAI;AACrB,UAAM,MAAM,EAAE,gBAAgB,IAAI,YAAY;AAC9C,QAAI,CAAC,SAAS,aAAa,CAAC,KAAK,SAAS,KAAK,KAAK,GAAG;AACnD,cAAQ,MAAM,YAAY;AAC1B;AAAA,IACJ;AACA,QAAI,CAAC,SAAS,OAAO,UAAU,CAAC,QAAQ,YAAY,YAAY,aAAa,EAAE,SAAS,CAAC,IAAI;AACzF,aAAO,MAAM,MAAM,GAAG,GAAG;AACzB;AAAA,IACJ;AACA,QAAI,CAAC,UAAU,OAAO,gBAAgB,CAAC,aAAa,SAAS,WAAW,aAAa,OAAO,EAAE,SAAS,CAAC,IAAI;AACxG,cAAQ,MAAM,MAAM,GAAG,EAAE;AACzB;AAAA,IACJ;AACA,QAAI,CAAC,SAAS,OAAO,iBAAiB,CAAC,YAAY,QAAQ,YAAY,WAAW,cAAc,OAAO,EAAE,SAAS,CAAC,IAAI;AACnH,aAAO,MAAM,MAAM,GAAG,EAAE;AACxB;AAAA,IACJ;AACA,UAAM,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,GAAG,EAAE;AACrC,QAAI,OAAO,OAAQ,QAAO,GAAG,IAAI,GAAG,OAAO,GAAG,CAAC,KAAK,KAAK,GAAG,MAAM,GAAG,SAAS;AAAA,aACrE,OAAO,KAAK,MAAM,EAAE,SAAS,WAAY,QAAO,GAAG,IAAI;AAAA,EACpE;AACA,QAAM,OAAO,SAAS,CAAC,OAAO,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AACjE,SAAO,EAAE,OAAO,MAAM,QAAQ,GAAI,YAAY,SAAY,CAAC,IAAI,EAAE,QAAQ,EAAG;AAChF;AAcO,IAAM,WAAW,CAAC,UACpB,KAAK,aAAa,oBAAoB,KAAK,KAAK,aAAa,MAAM,KAAK,KAAK,MAAM,KAAK,aAAa,YAAY,KAAK,WAAW,OAAO,SAAS,QAAQ,IACrJ,KAAK,EACL,MAAM,GAAG,GAAG;AAGd,IAAM,WAAW,CAAC,MAAuB,SAAkB;AAC9D,MAAI,KAAK,QAAQ,wBAAwB,EAAG,QAAO;AACnD,MAAI,KAAK,aAAa,oBAAoB,EAAG,QAAO;AACpD,SAAO,QAAQ,CAAC,CAAC,KAAK,cAAc,6CAA6C,KAAK,CAAC,KAAK,cAAc,wBAAwB;AACtI;AAGO,IAAM,WAAW,CAAC,SACrB,MAAM,KAAK,KAAK,QAAQ,EAAE,QAAQ,CAAC,OAAO;AACtC,MAAI,EAAE,cAAc,oBAAoB,cAAc,qBAAqB,cAAc,qBAAsB,QAAO,CAAC;AACvH,MAAI,GAAG,SAAU,QAAO,CAAC;AACzB,QAAM,OAAO,cAAc,mBAAmB,GAAG,KAAK,YAAY,IAAI,cAAc,oBAAoB,WAAW;AACnH,QAAM,QAAQ,SAAS,cAAc,SAAS;AAC9C,QAAM,QAAQ,cAAc,qBAAqB,GAAG,WAAW,MAAM,KAAK,GAAG,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,IAAI,SAAS,GAAG,UAAU,OAAO,QAAQ,GAAG;AACnK,SAAO;AAAA,IACH;AAAA,MACI,MAAM,GAAG,QAAQ,GAAG,MAAM;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,cAAc,GAAG,aAAa,cAAc,KAAK;AAAA,MACjD,WAAW,SAAS,CAAE,GAAwB;AAAA,MAC9C,SAAS,CAAC,CAAC,GAAG,QAAQ,wBAAwB;AAAA,MAC9C,SAAS,GAAG,aAAa,uBAAuB;AAAA,IACpD;AAAA,EACJ;AACJ,CAAC;AAwBE,IAAM,aAAa,CAAC,MAAuC;AAC9D,QAAM,IAAI;AACV,IAAE,qBAAqB;AACvB,QAAM,WAAW,CAAC,MAAa;AAC3B,UAAM,OAAO,EAAE;AACf,QAAI,EAAE,gBAAgB,oBAAoB,CAAC,SAAS,MAAM,CAAC,CAAC,EAAE,IAAI,EAAG;AACrE,UAAM,OAAO,SAAS,SAAS,IAAI,CAAC;AACpC,QAAI,CAAC,KAAK,MAAO;AACjB,UAAM,MAAM,OAAO,YAAY,OAAO,QAAQ,EAAE,MAAM,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,MAA6B,OAAO,EAAE,CAAC,MAAM,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACvI,UAAM,OAAiB;AAAA,MACnB,KAAK,EAAE;AAAA,MACP,MAAM,SAAS,IAAI;AAAA,MACnB,KAAK,OAAO,SAAS,KAAK,MAAM,GAAG,GAAG;AAAA,MACtC,OAAO,SAAS,MAAM,MAAM,GAAG,GAAG,KAAK;AAAA,MACvC,UAAU,EAAE,WAAW,KAAK,SAAS,YAAY;AAAA,MACjD,GAAI,OAAO,KAAK,GAAG,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC;AAAA,MACzC,YAAY,EAAE,WAAW;AAAA,MACzB,GAAG;AAAA,IACP;AACA,UAAM,OAAO,KAAK,UAAU,IAAI;AAChC,UAAM,MAAM,GAAG,EAAE,IAAI;AAErB,UAAM,OAAO,gBAAgB,aAAa,UAAU,WAAW,KAAK,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,aAAa,CAAC,CAAC;AAC5G,QAAI,CAAC,KAAM,MAAK,MAAM,KAAK,EAAE,QAAQ,QAAQ,SAAS,EAAE,gBAAgB,aAAa,GAAG,MAAM,WAAW,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAEtI,QAAI,EAAE,SAAS,KAAK,CAAC,EAAE,WAAW,EAAG,GAAE,SAAS,KAAK,OAAO,EAAE,OAAO,KAAK,OAAO,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC,EAAG,CAAC;AAAA,EAChI;AACA,WAAS,iBAAiB,UAAU,UAAU,IAAI;AAClD,QAAM,MAAM,MAAM;AACd,aAAS,oBAAoB,UAAU,UAAU,IAAI;AACrD,QAAI,EAAE,uBAAuB,IAAK,QAAO,EAAE;AAAA,EAC/C;AACA,IAAE,qBAAqB;AACvB,SAAO;AACX;","names":[]}
1
+ {"version":3,"sources":["../src/forms.ts"],"sourcesContent":["/**\n * Form leads (0.12.0): a form on the customer's site becomes a contact in MRRocket when it is submitted. Forms marked\n * `data-mrrocket-form` (its value names the form, e.g. `data-mrrocket-form=\"Demo request\"`) are captured; with\n * auto-detection on (`data-forms=\"auto\"` on the script tag, `forms=\"auto\"` on the provider) also any form with an email\n * field, except forms with a password field (sign-in and sign-up) and forms marked `data-mrrocket-ignore`.\n *\n * What is sent: the email (required), the name, and the other fields a person typed, never passwords, card fields,\n * hidden fields, files or fields whose name looks sensitive (`SENSITIVE`); a field or form part marked\n * `data-mrrocket-ignore` stays out too. Plus the form's name, the page, the referrer, UTM, and the state of a checkbox\n * marked `data-mrrocket-consent` (the visitor's own yes to hear from the site). MRRocket records consent as the contact's\n * legal basis only when that box was ticked.\n *\n * No React and no globals beyond the DOM, so the script tag (`sdk-forms.js`, loaded only on pages with a form to\n * capture) and the Next.js provider (a dynamic import) share it. `fieldsOf` is pure for the tests.\n */\n\n/** Field names that never leave the page, whatever the form says. */\nexport const SENSITIVE =\n /pass(word|wd|code)?|pwd|secret|token|csrf|xsrf|nonce|captcha|honeypot|card|^cc([-_ ]|num|exp|$)|cvv|cvc|csc|expir|iban|(^|[^a-z])bic([^a-z]|$)|swift|routing|account[-_ ]?(no|num)|sort[-_ ]?code|(^|[^a-z])ssn([^a-z]|$)|social[-_ ]?sec|tax[-_ ]?id|vat[-_ ]?id|steuer|passport|(^|[^a-z])(pin|otp|pw)([^a-z]|$)|one[-_ ]?time|auth(?!or)|api[-_ ]?key|session|signature/i;\n\n/** Input types whose value is never sent. */\nconst SKIP_TYPES = new Set([\"password\", \"hidden\", \"file\", \"submit\", \"button\", \"reset\", \"image\"]);\n\nexport const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\n\n/** Limits: what a lead may carry, also checked again by the server. */\nexport const MAX_FIELDS = 30;\nexport const MAX_VALUE = 1000;\n\n/** One control of a form as the filter sees it. */\nexport interface FieldInput {\n name: string;\n type: string;\n value: string;\n autocomplete?: string;\n /** A checkbox or radio that is not ticked. */\n unchecked?: boolean;\n /** Marked `data-mrrocket-ignore` (itself or a part of the form around it). */\n ignored?: boolean;\n /** Marked `data-mrrocket-consent`. */\n consent?: boolean;\n}\n\nexport interface FormFields {\n email: string | null;\n name: string | null;\n /** The other fields, by name; checkbox groups joined with \", \". */\n fields: Record<string, string>;\n /** True or false when the form has a checkbox marked `data-mrrocket-consent`; undefined without one. */\n consent?: boolean;\n}\n\nconst norm = (s: string) => s.toLowerCase().replace(/[\\s_-]+/g, \"\");\n\n/** Whether a field's value may be sent: not a password, card, hidden or sensitive-looking field. */\nexport const sendable = (f: Pick<FieldInput, \"name\" | \"type\" | \"autocomplete\" | \"ignored\">) => {\n if (f.ignored || SKIP_TYPES.has(f.type)) return false;\n const ac = (f.autocomplete ?? \"\").toLowerCase();\n if (/^cc-|password|one-time-code/.test(ac)) return false;\n return !SENSITIVE.test(f.name);\n};\n\nconst isEmailField = (f: FieldInput) => f.type === \"email\" || /e-?mail/i.test(f.name) || f.autocomplete === \"email\";\n\n/**\n * The lead in a form's controls: the email (first valid one), the name (a full-name field, or first and last), the\n * consent box's state, and the other sendable fields. Pure.\n */\nexport const fieldsOf = (inputs: FieldInput[]): FormFields => {\n let email: string | null = null;\n let full: string | null = null;\n let first: string | null = null;\n let last: string | null = null;\n let consent: boolean | undefined;\n const fields: Record<string, string> = {};\n for (const f of inputs) {\n if (f.consent) {\n consent = (consent ?? false) || !f.unchecked;\n continue;\n }\n if (!f.name || !sendable(f) || f.unchecked) continue;\n const value = f.value.trim().slice(0, MAX_VALUE);\n if (!value) continue;\n const n = norm(f.name);\n const ac = (f.autocomplete ?? \"\").toLowerCase();\n if (!email && isEmailField(f) && EMAIL_RE.test(value)) {\n email = value.toLowerCase();\n continue;\n }\n if (!full && (ac === \"name\" || [\"name\", \"fullname\", \"yourname\", \"contactname\"].includes(n))) {\n full = value.slice(0, 100);\n continue;\n }\n if (!first && (ac === \"given-name\" || [\"firstname\", \"first\", \"vorname\", \"givenname\", \"fname\"].includes(n))) {\n first = value.slice(0, 60);\n continue;\n }\n if (!last && (ac === \"family-name\" || [\"lastname\", \"last\", \"nachname\", \"surname\", \"familyname\", \"lname\"].includes(n))) {\n last = value.slice(0, 60);\n continue;\n }\n const key = f.name.trim().slice(0, 60);\n if (key in fields) fields[key] = `${fields[key]}, ${value}`.slice(0, MAX_VALUE);\n else if (Object.keys(fields).length < MAX_FIELDS) fields[key] = value;\n }\n const name = full ?? ([first, last].filter(Boolean).join(\" \") || null);\n return { email, name, fields, ...(consent === undefined ? {} : { consent }) };\n};\n\n/** What the page sends to `POST /api/sdk/forms`. */\nexport interface FormLead extends FormFields {\n key: string;\n form: string;\n url: string;\n title?: string;\n referrer?: string;\n utm?: Record<string, string>;\n distinctId?: string;\n /** The identity hash of `distinctId` (0.16.0), when the site signed the person. */\n userHash?: string;\n}\n\n/** The form's name: the value of `data-mrrocket-form`, else its name, id or label, else \"Form on /path\". */\nexport const formName = (form: HTMLFormElement) =>\n (form.getAttribute(\"data-mrrocket-form\") || form.getAttribute(\"name\") || form.id || form.getAttribute(\"aria-label\") || `Form on ${window.location.pathname}`)\n .trim()\n .slice(0, 100);\n\n/** Whether a form is captured: marked, or (auto) one with an email field and no password field. */\nexport const captures = (form: HTMLFormElement, auto: boolean) => {\n if (form.closest(\"[data-mrrocket-ignore]\")) return false;\n if (form.hasAttribute(\"data-mrrocket-form\")) return true;\n return auto && !!form.querySelector('input[type=\"email\"], input[name*=\"email\" i]') && !form.querySelector('input[type=\"password\"]');\n};\n\n/** The controls of a form, read for `fieldsOf`. */\nexport const inputsOf = (form: HTMLFormElement): FieldInput[] =>\n Array.from(form.elements).flatMap((el) => {\n if (!(el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement)) return [];\n if (el.disabled) return [];\n const type = el instanceof HTMLInputElement ? el.type.toLowerCase() : el instanceof HTMLSelectElement ? \"select\" : \"textarea\";\n const check = type === \"checkbox\" || type === \"radio\";\n const value = el instanceof HTMLSelectElement && el.multiple ? Array.from(el.selectedOptions, (o) => o.value).join(\", \") : check && el.value === \"on\" ? \"yes\" : el.value;\n return [\n {\n name: el.name || el.id || \"\",\n type,\n value,\n autocomplete: el.getAttribute(\"autocomplete\") ?? undefined,\n unchecked: check && !(el as HTMLInputElement).checked,\n ignored: !!el.closest(\"[data-mrrocket-ignore]\"),\n consent: el.hasAttribute(\"data-mrrocket-consent\"),\n },\n ];\n });\n\nexport interface WatchFormsOptions {\n host: string;\n apiKey: string;\n /** Also forms with an email field that are not marked (default false). */\n auto?: boolean;\n /** The visitor id to link the lead to, while analytics consent allows one. */\n distinctId: () => string | undefined;\n /** The identity hash of that id (0.16.0), when the site gave one to identify. */\n userHash?: (id: string) => string | undefined;\n /** Whether the site already called identify(). */\n identified: () => boolean;\n /** Links this visitor's history to the person (the SDK's own identify). */\n identify: (id: string, props: Record<string, unknown>) => void;\n /** Whether analytics consent lets the SDK identify the visitor (not \"off\"). */\n tracking: () => boolean;\n referrer?: () => string | undefined;\n utm?: () => Record<string, string | undefined> | undefined;\n}\n\n/**\n * Captures submits from now on (one listener on the document, capture phase, so it sees forms that render later and\n * forms whose own handler stops the event). The lead goes out with `sendBeacon`, which survives the page leaving. Returns\n * a function that stops it. Calling it twice keeps one listener.\n */\nexport const watchForms = (o: WatchFormsOptions): (() => void) => {\n const w = window as Window & { __mrrocketFormsOff?: () => void };\n w.__mrrocketFormsOff?.();\n const onSubmit = (e: Event) => {\n const form = e.target;\n if (!(form instanceof HTMLFormElement) || !captures(form, !!o.auto)) return;\n const read = fieldsOf(inputsOf(form));\n if (!read.email) return;\n const utm = Object.fromEntries(Object.entries(o.utm?.() ?? {}).filter((x): x is [string, string] => typeof x[1] === \"string\" && !!x[1]));\n const distinctId = o.distinctId();\n const userHash = distinctId ? o.userHash?.(distinctId) : undefined;\n const lead: FormLead = {\n key: o.apiKey,\n form: formName(form),\n url: window.location.href.slice(0, 500),\n title: document.title.slice(0, 200) || undefined,\n referrer: o.referrer?.() || document.referrer || undefined,\n ...(Object.keys(utm).length ? { utm } : {}),\n distinctId,\n ...(userHash ? { userHash } : {}),\n ...read,\n };\n const body = JSON.stringify(lead);\n const url = `${o.host}/api/sdk/forms`;\n // text/plain keeps it a simple request (no CORS preflight), which sendBeacon needs anyway.\n const sent = \"sendBeacon\" in navigator && navigator.sendBeacon(url, new Blob([body], { type: \"text/plain\" }));\n if (!sent) void fetch(url, { method: \"POST\", headers: { \"content-type\": \"text/plain\" }, body, keepalive: true }).catch(() => undefined);\n // Their visits before the form and after it belong to the same person from now on.\n if (o.tracking() && !o.identified()) o.identify(read.email, { email: read.email, ...(read.name ? { name: read.name } : {}) });\n };\n document.addEventListener(\"submit\", onSubmit, true);\n const off = () => {\n document.removeEventListener(\"submit\", onSubmit, true);\n if (w.__mrrocketFormsOff === off) delete w.__mrrocketFormsOff;\n };\n w.__mrrocketFormsOff = off;\n return off;\n};\n"],"mappings":"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","names":[]}