@mrrocket/sdk 0.4.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +137 -8
- package/dist/api-client.d.ts +213 -0
- package/dist/api-client.js +67 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api.d.ts +9 -109
- package/dist/api.js +7 -62
- package/dist/api.js.map +1 -1
- package/dist/browser/mrrocket-flows.js +64 -0
- package/dist/browser/mrrocket-messenger.js +313 -0
- package/dist/browser/mrrocket-push.js +1 -0
- package/dist/browser/mrrocket-replay.js +3 -3
- package/dist/browser/mrrocket-sw.js +1 -0
- package/dist/browser/mrrocket.js +2 -1
- package/dist/browser.d.ts +3 -2
- package/dist/browser.js +6 -1
- package/dist/browser.js.map +1 -1
- package/dist/capture.d.ts +1 -13
- package/dist/capture.js +1 -22
- package/dist/capture.js.map +1 -1
- package/dist/client.d.ts +36 -1
- package/dist/client.js +161 -6
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +10 -0
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +64 -3
- package/dist/core.js +54 -2
- package/dist/core.js.map +1 -1
- package/dist/feedback.d.ts +19 -4
- package/dist/feedback.js +655 -190
- package/dist/feedback.js.map +1 -1
- package/dist/flows-ui.d.ts +23 -0
- package/dist/flows-ui.js +384 -0
- package/dist/flows-ui.js.map +1 -0
- package/dist/flows-web.d.ts +19 -0
- package/dist/flows-web.js +42 -0
- package/dist/flows-web.js.map +1 -0
- package/dist/flows.d.ts +222 -0
- package/dist/flows.js +409 -0
- package/dist/flows.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/messenger-ui.d.ts +47 -0
- package/dist/messenger-ui.js +583 -0
- package/dist/messenger-ui.js.map +1 -0
- package/dist/messenger.d.ts +37 -0
- package/dist/messenger.js +69 -0
- package/dist/messenger.js.map +1 -0
- package/dist/proxy.d.ts +34 -1
- package/dist/proxy.js +14 -1
- package/dist/proxy.js.map +1 -1
- package/dist/push-web.d.ts +43 -0
- package/dist/push-web.js +81 -0
- package/dist/push-web.js.map +1 -0
- package/dist/rage.d.ts +14 -0
- package/dist/rage.js +27 -0
- package/dist/rage.js.map +1 -0
- package/dist/react-native/index.d.cts +640 -3
- package/dist/react-native/index.d.ts +640 -3
- package/dist/react-native/index.js +2015 -34
- package/dist/react-native/index.js.map +1 -1
- package/dist/styles-css.d.ts +6 -0
- package/dist/styles-css.js +327 -0
- package/dist/styles-css.js.map +1 -0
- package/dist/styles.d.ts +1 -2
- package/dist/styles.js +3 -134
- package/dist/styles.js.map +1 -1
- package/dist/ui.d.ts +3 -0
- package/dist/ui.js +12 -0
- package/dist/ui.js.map +1 -1
- package/package.json +20 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messenger-ui.ts"],"sourcesContent":["/**\n * The web messenger, after Featurebase's: a round launcher and a panel with three tabs, Home (greeting, the latest\n * chat, help search, top articles), Messages (the chat with the team, answered in MRRocket › Support by the owner or\n * the Customer experience agent) and Help (the product's published help articles, searchable, read in the panel).\n * While the panel is closed it checks for answers now and then; a new answer plays a short sound, shows a count on\n * the launcher and a preview card above it.\n *\n * Plain DOM, no React, so `<Messenger />` (Next.js) and the script tag share it; both load it only when it is used (a\n * dynamic import, and `sdk-messenger.js` for the script tag). Every text is set with textContent; articles arrive as\n * blocks, never HTML.\n */\nimport { type ArticleBlock, type ArticleSpan, type ArticleSummary, type ChatMessage, type ChatThread, type MessengerLook, createApi } from \"./api-client\";\nimport { injectStyles } from \"./styles-css\";\n\nexport interface MessengerUiOptions {\n host: string;\n apiKey: string;\n /** The visitor id now (it changes on identify and reset). */\n distinctId: () => string;\n /** The signed-in person's id, name and email, when the site called identify. */\n user: () => { id?: string; name?: string; email?: string } | undefined;\n /** Show the round launcher (default true). Without it the panel opens only through `open()`. */\n launcher?: boolean;\n position?: \"bottom-right\" | \"bottom-left\";\n /** The panel's title. */\n title?: string;\n /** The big line on Home and the first line of an empty chat. */\n greeting?: string;\n /** Ask anonymous visitors for an email (optional for them). Default true. */\n askEmail?: boolean;\n /** Text on the launcher; empty shows the icon only. */\n label?: string;\n /** The Help tab with the product's published help articles (default true; hidden while there are none). */\n help?: boolean;\n /** A short sound when an answer arrives (default true). */\n sound?: boolean;\n /** A preview of a new answer above the closed launcher (default true). */\n preview?: boolean;\n theme?: \"light\" | \"dark\" | \"auto\";\n accent?: string;\n}\n\nexport type MessengerView = \"home\" | \"messages\" | \"help\";\n\nexport interface MessengerHandle {\n /** Opens the panel, on Home or the given tab; `{ article }` opens a help article. */\n open(view?: MessengerView | { article: string }): void;\n close(): void;\n toggle(): void;\n /** Removes the launcher and the panel from the page. */\n destroy(): void;\n}\n\nconst POLL_OPEN_MS = 4000;\n/** While the panel is closed: soon after the visitor wrote, answers are looked for often, later now and then. */\nconst POLL_WAITING_MS = 8000;\nconst POLL_IDLE_MS = 30000;\nconst WAITING_FOR_MS = 10 * 60_000;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\nconst SEEN_KEY = \"ckw_msgr_seen\";\nconst whoLabel: Record<ChatMessage[\"from\"], string> = { visitor: \"You\", team: \"Team\", ai: \"AI agent\", auto: \"Team\" };\n\nconst readSeen = () => {\n try {\n return window.localStorage.getItem(SEEN_KEY) ?? \"\";\n } catch {\n return \"\";\n }\n};\nconst writeSeen = (id: string) => {\n try {\n window.localStorage.setItem(SEEN_KEY, id);\n } catch {\n // Private mode: the count just shows again next time.\n }\n};\n\nconst timeAgo = (iso: string) => {\n const t = new Date(iso).getTime();\n if (Number.isNaN(t)) return \"\";\n const d = Math.max(0, Date.now() - t) / 1000;\n if (d < 60) return \"just now\";\n if (d < 3600) return `${Math.floor(d / 60)}m ago`;\n if (d < 86400) return `${Math.floor(d / 3600)}h ago`;\n if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;\n return new Date(iso).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\", year: \"numeric\" });\n};\n\nconst el = <K extends keyof HTMLElementTagNameMap>(tag: K, attrs: Record<string, string> = {}, ...kids: (Node | string | null | false | undefined)[]) => {\n const e = document.createElement(tag);\n for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);\n for (const k of kids) if (k !== null && k !== false && k !== undefined) e.append(k);\n return e;\n};\n\nconst SVG_NS = \"http://www.w3.org/2000/svg\";\nconst icon = (d: string, size = 16) => {\n const svg = document.createElementNS(SVG_NS, \"svg\");\n for (const [k, v] of Object.entries({ class: \"ckw-icon\", width: String(size), height: String(size), viewBox: \"0 0 16 16\", fill: \"none\", stroke: \"currentColor\", \"stroke-width\": \"1.7\", \"stroke-linecap\": \"round\", \"stroke-linejoin\": \"round\", \"aria-hidden\": \"true\" }))\n svg.setAttribute(k, v);\n const path = document.createElementNS(SVG_NS, \"path\");\n path.setAttribute(\"d\", d);\n svg.appendChild(path);\n return svg;\n};\nconst I = {\n cross: \"m4 4 8 8M12 4l-8 8\",\n chat: \"M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z\",\n send: \"M2.5 8h10M8.5 3.5 13 8l-4.5 4.5\",\n home: \"M2.5 7 8 2.5 13.5 7v6a1 1 0 0 1-1 1h-3v-4h-3v4h-3a1 1 0 0 1-1-1z\",\n help: \"M8 14.5A6.5 6.5 0 1 0 8 1.5a6.5 6.5 0 0 0 0 13zM6.2 6.2a1.9 1.9 0 1 1 2.6 1.8c-.5.2-.8.6-.8 1.1v.4M8 11.6v.1\",\n back: \"M10 3.5 5.5 8l4.5 4.5\",\n next: \"M6 3.5 10.5 8 6 12.5\",\n search: \"M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM10.5 10.5l3 3\",\n doc: \"M4 1.5h5.5l3 3V14a.5.5 0 0 1-.5.5H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5zM9.5 1.5v3h3M6 8.5h4M6 11h4\",\n};\n\n/** A soft two-note chime made with Web Audio (no file to load). Browsers allow it once the visitor has clicked. */\nconst chime = (() => {\n let ctx: AudioContext | null = null;\n const get = () => {\n const AC = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!AC) return null;\n ctx ??= new AC();\n return ctx;\n };\n return {\n /** Call from a click so later sounds may play. */\n unlock: () => {\n try {\n const c = get();\n if (c?.state === \"suspended\") void c.resume();\n } catch {\n // No audio: no sound.\n }\n },\n play: () => {\n try {\n const c = get();\n if (!c || c.state !== \"running\") return;\n const t = c.currentTime;\n [880, 1318.5].forEach((f, i) => {\n const o = c.createOscillator();\n const g = c.createGain();\n const at = t + i * 0.11;\n o.type = \"sine\";\n o.frequency.value = f;\n g.gain.setValueAtTime(0.0001, at);\n g.gain.exponentialRampToValueAtTime(0.12, at + 0.012);\n g.gain.exponentialRampToValueAtTime(0.0001, at + 0.32);\n o.connect(g);\n g.connect(c.destination);\n o.start(at);\n o.stop(at + 0.34);\n });\n } catch {\n // No audio: no sound.\n }\n },\n };\n})();\n\nconst spansOf = (spans: ArticleSpan[]) =>\n spans.map((s) => {\n let node: HTMLElement = s.href ? el(\"a\", { href: s.href, target: \"_blank\", rel: \"noopener noreferrer\" }, s.text) : el(\"span\", {}, s.text);\n if (s.code) node = el(\"code\", {}, node);\n if (s.italic) node = el(\"em\", {}, node);\n if (s.bold) node = el(\"strong\", {}, node);\n return node;\n });\n\nconst blockOf = (b: ArticleBlock): HTMLElement => {\n switch (b.type) {\n case \"h2\":\n case \"h3\":\n return el(b.type, {}, ...spansOf(b.spans));\n case \"quote\":\n return el(\"blockquote\", {}, ...spansOf(b.spans));\n case \"ul\":\n case \"ol\":\n return el(b.type, {}, ...b.items.map((it) => el(\"li\", {}, ...spansOf(it))));\n case \"code\":\n return el(\"pre\", {}, el(\"code\", {}, b.text));\n case \"hr\":\n return el(\"hr\");\n default:\n return el(\"p\", {}, ...spansOf(b.spans));\n }\n};\n\n/** How long the launcher waits for the look from MRRocket before it shows with the code's options and the defaults. */\nconst LOOK_WAIT_MS = 2500;\n\n/**\n * Mounts the messenger with its look from MRRocket (Support › Messenger) underneath what the code passes: an option the\n * code sets wins, one it leaves out comes from MRRocket, then the default. Calls made before the look arrived wait.\n */\nexport const mountMessenger = (o: MessengerUiOptions): MessengerHandle => {\n let real: MessengerHandle | null = null;\n let gone = false;\n const queued: ((h: MessengerHandle) => void)[] = [];\n const later = (fn: (h: MessengerHandle) => void) => (real ? fn(real) : queued.push(fn));\n const ctrl = new AbortController();\n const start = (look: Partial<MessengerLook> | null) => {\n if (real || gone) return;\n const defined = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as MessengerUiOptions;\n real = build({ ...(look ?? {}), ...defined, accent: o.accent ?? look?.accent, theme: o.theme ?? look?.theme });\n for (const fn of queued.splice(0)) fn(real);\n };\n const timer = setTimeout(() => start(null), LOOK_WAIT_MS);\n createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() })\n .messengerLook(ctrl.signal)\n .then((look) => start(look))\n .catch(() => start(null))\n .finally(() => clearTimeout(timer));\n return {\n open: (v) => later((h) => h.open(v)),\n close: () => real?.close(),\n toggle: () => later((h) => h.toggle()),\n destroy: () => {\n gone = true;\n ctrl.abort();\n clearTimeout(timer);\n real?.destroy();\n },\n };\n};\n\nconst build = (o: MessengerUiOptions): MessengerHandle => {\n injectStyles();\n const position = o.position ?? \"bottom-right\";\n const title = o.title || \"Chat with us\";\n const greeting = o.greeting || \"Hi! How can we help?\";\n const askEmail = o.askEmail ?? true;\n const label = o.label ?? \"\";\n const launcherOn = o.launcher ?? true;\n const helpOn = o.help ?? true;\n const soundOn = o.sound ?? true;\n const previewOn = o.preview ?? true;\n const api = () => createApi({ host: o.host, apiKey: o.apiKey, distinctId: o.distinctId(), user: o.user() });\n\n const root = el(\"div\", { class: \"ckw-root\", \"data-theme\": o.theme ?? \"auto\" });\n if (o.accent) root.style.setProperty(\"--ckw-accent\", o.accent);\n document.body.appendChild(root);\n\n let open = false;\n let view: MessengerView | { article: string } = \"home\";\n let thread: ChatThread | null = null;\n let loaded = false;\n let error = \"\";\n let pending: ChatMessage | null = null;\n /** The newest team message seen in this page load: what counts as new after it. */\n let lastKnown: string | null = null;\n let unseen: ChatMessage[] = [];\n let previewGone = false;\n let poll: ReturnType<typeof setTimeout> | undefined;\n let ctrl: AbortController | null = null;\n let articles: ArticleSummary[] | null = null;\n let query = \"\";\n let rated = new Set<string>();\n /** Whether the product has any published article: the Help tab and Home's help card hide when it has none. */\n let hasArticles: boolean | null = null;\n\n const teamMsgs = (t: ChatThread | null) => (t?.messages ?? []).filter((m) => m.from !== \"visitor\");\n const lastTeamId = (t: ChatThread | null) => teamMsgs(t).at(-1)?.id ?? \"\";\n const watching = () => open && view === \"messages\" && document.visibilityState !== \"hidden\";\n\n /** Takes a thread from the server: new answers count, chime and show in the preview. */\n const take = (t: ChatThread) => {\n const team = teamMsgs(t);\n if (lastKnown === null) {\n // First read: answers the visitor never saw in an earlier visit count, without a sound.\n const seen = readSeen();\n const i = team.findIndex((m) => m.id === seen);\n unseen = t.messages.some((m) => m.from === \"visitor\") ? team.slice(i + 1).filter((m) => m.from !== \"auto\") : [];\n } else {\n const i = team.findIndex((m) => m.id === lastKnown);\n const fresh = team.slice(i + 1).filter((m) => m.from !== \"auto\");\n if (fresh.length) {\n if (soundOn) chime.play();\n if (!watching()) {\n unseen = [...unseen, ...fresh];\n previewGone = false;\n }\n }\n }\n lastKnown = lastTeamId(t);\n thread = t;\n if (watching()) markSeen();\n };\n const markSeen = () => {\n unseen = [];\n const id = lastTeamId(thread);\n if (id) writeSeen(id);\n };\n\n // ---------- Launcher and preview ----------\n const fab = el(\"button\", { type: \"button\", class: `ckw-fab ckw-msgr-fab${label ? \"\" : \" ckw-msgr-round\"}`, \"data-pos\": position, \"aria-haspopup\": \"dialog\" });\n const prev = el(\"div\", { class: \"ckw-msgr-preview\", \"data-pos\": position, role: \"status\" });\n const drawFab = () => {\n fab.replaceChildren(icon(open ? I.cross : I.chat, label ? 16 : 20));\n if (label) fab.append(el(\"span\", {}, label));\n if (unseen.length && !open) fab.append(el(\"span\", { class: \"ckw-msgr-count\", \"aria-hidden\": \"true\" }, String(Math.min(unseen.length, 9))));\n fab.setAttribute(\"aria-expanded\", String(open));\n fab.setAttribute(\"aria-label\", open ? \"Close chat\" : unseen.length ? `Open chat, ${unseen.length} new ${unseen.length === 1 ? \"reply\" : \"replies\"}` : \"Open chat\");\n drawPreview();\n };\n const drawPreview = () => {\n const m = unseen.at(-1);\n if (!previewOn || open || previewGone || !m || !launcherOn) return prev.remove();\n const x = el(\"button\", { type: \"button\", class: \"ckw-msgr-preview-x\", \"aria-label\": \"Dismiss\" }, icon(I.cross, 12));\n x.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n previewGone = true;\n drawPreview();\n });\n const card = el(\n \"button\",\n { type: \"button\", class: \"ckw-msgr-preview-card\" },\n el(\"span\", { class: \"ckw-msgr-preview-who\" }, `${whoLabel[m.from]}${unseen.length > 1 ? ` · ${unseen.length} new` : \"\"}`),\n el(\"span\", { class: \"ckw-msgr-preview-text\" }, m.body),\n );\n card.addEventListener(\"click\", () => show(\"messages\"));\n prev.replaceChildren(card, x);\n if (!prev.isConnected) root.appendChild(prev);\n };\n fab.addEventListener(\"click\", () => {\n chime.unlock();\n if (open) close();\n else show(unseen.length ? \"messages\" : \"home\");\n });\n if (launcherOn) root.appendChild(fab);\n\n // ---------- Panel ----------\n const pop = el(\"div\", { class: \"ckw-pop ckw-msgr\", \"data-pos\": position, role: \"dialog\", \"aria-label\": title });\n const head = el(\"div\", { class: \"ckw-pop-head ckw-msgr-head\" });\n const body = el(\"div\", { class: \"ckw-pop-body ckw-msgr-body\" });\n const tabs = el(\"nav\", { class: \"ckw-msgr-tabs\", \"aria-label\": \"Messenger\" });\n pop.addEventListener(\"click\", () => chime.unlock());\n\n // The chat's compose form keeps its elements between draws, so the field never loses the focus.\n const form = el(\"form\", { class: \"ckw-msgr-compose\" });\n const err = el(\"p\", { class: \"ckw-msgr-error\", role: \"alert\" });\n const email = el(\"input\", { class: \"ckw-input\", type: \"email\", inputmode: \"email\", autocomplete: \"email\", placeholder: \"Your email (optional), so we can follow up\", \"aria-label\": \"Your email (optional)\", maxlength: \"200\" });\n const row = el(\"div\", { class: \"ckw-msgr-row\" });\n const text = el(\"textarea\", { class: \"ckw-textarea ckw-msgr-input\", rows: \"1\", placeholder: \"Write a message…\", \"aria-label\": \"Message\", maxlength: \"4000\" });\n const send = el(\"button\", { type: \"submit\", class: \"ckw-btn ckw-btn-primary ckw-msgr-send\", \"aria-label\": \"Send\" }, icon(I.send));\n row.append(text, send);\n form.append(row);\n\n const list = el(\"ol\", { class: \"ckw-msgs\", \"aria-live\": \"polite\", \"aria-label\": \"Messages\" });\n const needEmail = () => askEmail && !o.user()?.email && !thread?.hasEmail;\n const emailOk = () => !email.value.trim() || EMAIL.test(email.value.trim());\n const canSend = () => !!text.value.trim() && !pending && emailOk();\n\n const closeBtn = () => {\n const b = el(\"button\", { type: \"button\", class: \"ckw-btn ckw-btn-ghost ckw-msgr-x\", \"aria-label\": \"Close messenger\" }, icon(I.cross));\n b.addEventListener(\"click\", () => close());\n return b;\n };\n const go = (v: MessengerView | { article: string }) => {\n view = v;\n drawn = -1;\n draw();\n if (v === \"messages\") {\n markSeen();\n drawFab();\n text.focus();\n } else if (v === \"help\") helpInput.focus();\n schedule(0);\n };\n\n const bubble = (m: ChatMessage, isPending = false) => {\n const li = el(\"li\", { class: \"ckw-msg\", \"data-from\": m.from === \"visitor\" ? \"me\" : \"them\" }, el(\"div\", { class: \"ckw-bubble\" }, m.body));\n if (isPending) li.setAttribute(\"aria-busy\", \"true\");\n for (const a of m.articles ?? []) {\n const chip = el(\"button\", { type: \"button\", class: \"ckw-msgr-cite\" }, icon(I.doc, 14), el(\"span\", {}, a.title));\n chip.addEventListener(\"click\", () => go({ article: a.id }));\n li.append(chip);\n }\n li.append(el(\"span\", { class: \"ckw-msg-meta\" }, `${m.from !== \"visitor\" ? `${whoLabel[m.from]} · ` : \"\"}${isPending ? \"Sending…\" : timeAgo(m.at)}`));\n return li;\n };\n\n const articleRow = (a: ArticleSummary) => {\n const b = el(\"button\", { type: \"button\", class: \"ckw-msgr-art\" }, el(\"span\", { class: \"ckw-msgr-art-t\" }, a.title), icon(I.next, 14));\n b.addEventListener(\"click\", () => go({ article: a.id }));\n return b;\n };\n\n const searchBox = () => {\n const wrap = el(\"label\", { class: \"ckw-msgr-search\" }, icon(I.search, 14));\n const input = el(\"input\", { class: \"ckw-msgr-search-in\", type: \"search\", placeholder: \"Search for help\", \"aria-label\": \"Search for help\", maxlength: \"200\" });\n input.value = query;\n let t: ReturnType<typeof setTimeout> | undefined;\n input.addEventListener(\"input\", () => {\n query = input.value;\n clearTimeout(t);\n t = setTimeout(() => {\n if (view !== \"help\") return go(\"help\");\n void loadArticles(true);\n }, 250);\n });\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n if (view !== \"help\") go(\"help\");\n }\n });\n wrap.append(input);\n return wrap;\n };\n // Help's own field stays the same element between draws, so typing never loses the cursor.\n const helpSearch = searchBox();\n const helpInput = helpSearch.querySelector(\"input\")!;\n\n let loadingArticles = false;\n const loadArticles = async (force = false) => {\n if (!helpOn || (loadingArticles && !force)) return;\n loadingArticles = true;\n const q = query.trim();\n try {\n const r = await api().articles(q || undefined);\n if (q !== query.trim()) return;\n articles = r.articles;\n if (!q) hasArticles = r.articles.length > 0;\n } catch {\n articles = articles ?? [];\n } finally {\n loadingArticles = false;\n }\n draw();\n };\n const drawHome = () => {\n head.replaceChildren(el(\"span\", { class: \"ckw-msgr-brand\" }, title), closeBtn());\n const hero = el(\"div\", { class: \"ckw-msgr-hero\" }, el(\"p\", { class: \"ckw-msgr-hello\" }, greeting));\n const last = (thread?.messages ?? []).at(-1);\n const msgCard = el(\n \"button\",\n { type: \"button\", class: \"ckw-msgr-card\" },\n el(\n \"span\",\n { class: \"ckw-msgr-card-main\" },\n el(\"b\", {}, last ? \"Recent message\" : \"Send us a message\"),\n el(\"span\", { class: \"ckw-msgr-card-sub\" }, last ? `${last.from === \"visitor\" ? \"You\" : whoLabel[last.from]}: ${last.body}` : \"We usually answer within a few hours\"),\n ),\n unseen.length ? el(\"span\", { class: \"ckw-msgr-badge\" }, String(unseen.length)) : icon(I.send),\n );\n msgCard.addEventListener(\"click\", () => go(\"messages\"));\n const parts: HTMLElement[] = [hero, msgCard];\n if (helpOn && hasArticles !== false) {\n const helpCard = el(\"div\", { class: \"ckw-msgr-card ckw-msgr-card-col\" }, searchBox());\n const top = (articles ?? []).slice(0, 4);\n if (top.length) helpCard.append(...top.map(articleRow));\n else if (articles === null) helpCard.append(el(\"div\", { class: \"ckw-skel ckw-msgr-skel\" }));\n parts.push(helpCard);\n }\n body.replaceChildren(...parts);\n };\n\n const drawHelp = () => {\n head.replaceChildren(el(\"span\", {}, \"Help\"), closeBtn());\n if (helpInput.value !== query) helpInput.value = query;\n const parts: HTMLElement[] = [helpSearch];\n if (articles === null) parts.push(el(\"div\", { class: \"ckw-skel\" }));\n else if (!articles.length) {\n const ask = el(\"button\", { type: \"button\", class: \"ckw-btn ckw-btn-primary\" }, \"Send us a message\");\n ask.addEventListener(\"click\", () => go(\"messages\"));\n parts.push(el(\"div\", { class: \"ckw-msgr-empty\" }, el(\"p\", {}, query.trim() ? `Nothing found for “${query.trim()}”.` : \"No help articles yet.\"), ask));\n } else if (query.trim()) parts.push(el(\"div\", { class: \"ckw-msgr-card ckw-msgr-card-col\" }, ...articles.map(articleRow)));\n else {\n const groups = new Map<string, ArticleSummary[]>();\n for (const a of articles) groups.set(a.collection, [...(groups.get(a.collection) ?? []), a]);\n for (const [name, items] of groups)\n parts.push(el(\"section\", { class: \"ckw-msgr-group\" }, el(\"h3\", { class: \"ckw-msgr-group-h\" }, `${name} · ${items.length}`), el(\"div\", { class: \"ckw-msgr-card ckw-msgr-card-col\" }, ...items.map(articleRow))));\n }\n // The field stays in place (moving it would take its focus); only what is under it changes.\n if (body.firstChild !== helpSearch) body.replaceChildren(helpSearch);\n while (body.childNodes.length > 1) body.lastChild!.remove();\n body.append(...parts.slice(1));\n };\n\n let articleShown: { id: string; node: HTMLElement } | null = null;\n const drawArticle = (id: string) => {\n const back = el(\"button\", { type: \"button\", class: \"ckw-btn ckw-btn-ghost ckw-msgr-x\", \"aria-label\": \"Back to help\" }, icon(I.back));\n back.addEventListener(\"click\", () => go(\"help\"));\n head.replaceChildren(back, el(\"span\", { class: \"ckw-msgr-head-t\" }, \"Help\"), closeBtn());\n if (articleShown?.id === id) return body.replaceChildren(articleShown.node);\n const node = el(\"article\", { class: \"ckw-msgr-doc\" }, el(\"div\", { class: \"ckw-skel\" }));\n articleShown = { id, node };\n body.replaceChildren(node);\n api()\n .article(id)\n .then((a) => {\n const foot = el(\"div\", { class: \"ckw-msgr-rate\" });\n const drawRate = () => {\n if (rated.has(id)) return foot.replaceChildren(el(\"span\", {}, \"Thanks for letting us know.\"));\n const yes = el(\"button\", { type: \"button\", class: \"ckw-btn\" }, \"Yes\");\n const no = el(\"button\", { type: \"button\", class: \"ckw-btn\" }, \"No\");\n const rate = (helpful: boolean) => {\n rated = new Set(rated).add(id);\n drawRate();\n api()\n .rateArticle(id, helpful)\n .catch(() => undefined);\n if (!helpful) go(\"messages\");\n };\n yes.addEventListener(\"click\", () => rate(true));\n no.addEventListener(\"click\", () => rate(false));\n foot.replaceChildren(el(\"span\", {}, \"Did this answer your question?\"), el(\"span\", { class: \"ckw-msgr-rate-b\" }, yes, no));\n };\n drawRate();\n const ask = el(\"button\", { type: \"button\", class: \"ckw-btn ckw-msgr-ask\" }, icon(I.chat), \"Still need help? Send us a message\");\n ask.addEventListener(\"click\", () => go(\"messages\"));\n node.replaceChildren(\n el(\"p\", { class: \"ckw-msgr-doc-c\" }, a.collection),\n el(\"h2\", { class: \"ckw-msgr-doc-t\" }, a.title),\n el(\"p\", { class: \"ckw-msgr-doc-c\" }, `Updated ${timeAgo(a.updatedAt)}`),\n el(\"div\", { class: \"ckw-msgr-doc-b\" }, ...a.blocks.map(blockOf)),\n foot,\n ask,\n );\n })\n .catch((e: Error) => {\n articleShown = null;\n node.replaceChildren(el(\"p\", { class: \"ckw-msgr-error\" }, e.message));\n });\n };\n\n let drawn = -1;\n /** What the list shows, so a read that brings nothing new leaves it alone (no flicker, links stay where they are). */\n let listSig = \"\";\n const drawMessages = () => {\n head.replaceChildren(el(\"span\", {}, title), closeBtn());\n const msgs = thread?.messages ?? [];\n const parts: HTMLElement[] = [];\n if (!loaded) parts.push(el(\"div\", { class: \"ckw-skel\" }));\n else {\n const sig = `${msgs.map((m) => m.id).join(\",\")}|${pending ? pending.body : \"\"}|${Math.floor(Date.now() / 60_000)}`;\n if (sig !== listSig) {\n listSig = sig;\n const greet = el(\"li\", { class: \"ckw-msg\", \"data-from\": \"them\" }, el(\"div\", { class: \"ckw-bubble\" }, greeting));\n list.replaceChildren(greet, ...msgs.map((m) => bubble(m)), ...(pending ? [bubble(pending, true)] : []));\n }\n parts.push(list);\n }\n if (thread?.conversation?.status === \"solved\" && !pending) parts.push(el(\"p\", { class: \"ckw-fine ckw-msgr-note\" }, \"This conversation is closed. Write to start a new one.\"));\n if (body.childNodes.length !== parts.length || parts.some((x, i) => body.childNodes[i] !== x)) body.replaceChildren(...parts);\n const count = msgs.length + (pending ? 1 : 0);\n if (count !== drawn) {\n drawn = count;\n body.scrollTop = body.scrollHeight;\n }\n err.textContent = error;\n if (error && !err.isConnected) form.prepend(err);\n else if (!error) err.remove();\n if (needEmail()) {\n if (emailOk()) email.removeAttribute(\"aria-invalid\");\n else email.setAttribute(\"aria-invalid\", \"true\");\n if (!email.isConnected) form.insertBefore(email, row);\n } else email.remove();\n send.disabled = !canSend();\n };\n\n const drawTabs = () => {\n const current = typeof view === \"string\" ? view : \"help\";\n const tab = (v: MessengerView, name: string, d: string, badge = 0) => {\n const b = el(\"button\", { type: \"button\", class: \"ckw-msgr-tab\", \"aria-current\": current === v ? \"page\" : \"false\" }, icon(d, 18), el(\"span\", {}, name));\n if (badge) b.append(el(\"span\", { class: \"ckw-msgr-count ckw-msgr-count-tab\" }, String(Math.min(badge, 9))));\n b.addEventListener(\"click\", () => go(v));\n return b;\n };\n tabs.replaceChildren(tab(\"home\", \"Home\", I.home), tab(\"messages\", \"Messages\", I.chat, view === \"messages\" ? 0 : unseen.length), ...(helpOn && hasArticles !== false ? [tab(\"help\", \"Help\", I.help)] : []));\n };\n\n const draw = () => {\n if (!open) return;\n pop.dataset.view = typeof view === \"string\" ? view : \"article\";\n if (view === \"home\") drawHome();\n else if (view === \"messages\") drawMessages();\n else if (view === \"help\") drawHelp();\n else drawArticle(view.article);\n const kids: HTMLElement[] = [head, body];\n if (view === \"messages\") kids.push(form);\n kids.push(tabs);\n drawTabs();\n if (pop.childNodes.length !== kids.length || kids.some((k, i) => pop.childNodes[i] !== k)) pop.replaceChildren(...kids);\n };\n\n // ---------- Reading the chat ----------\n const tick = async () => {\n ctrl?.abort();\n ctrl = new AbortController();\n try {\n take(await api().chat(ctrl.signal));\n error = \"\";\n } catch (e) {\n if ((e as Error).name === \"AbortError\") return;\n if (open && view === \"messages\") error = (e as Error).message;\n }\n loaded = true;\n draw();\n drawFab();\n schedule();\n };\n /** The next read: often while the chat is on screen, now and then while the visitor waits for an answer. */\n function schedule(now?: number) {\n clearTimeout(poll);\n const waiting = !!thread?.messages.some((m) => m.from === \"visitor\") && thread?.conversation?.status !== \"solved\";\n const wrote = (thread?.messages ?? []).filter((m) => m.from === \"visitor\").at(-1)?.at;\n const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;\n const ms = now ?? (watching() ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : open || waiting ? POLL_IDLE_MS : 0);\n if (ms || now === 0) poll = setTimeout(() => void tick(), ms);\n }\n document.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"visible\") schedule(0);\n else clearTimeout(poll);\n });\n\n const submit = async () => {\n const msg = text.value.trim();\n if (!msg || pending || !emailOk()) return;\n chime.unlock();\n pending = { id: \"pending\", from: \"visitor\", body: msg, at: new Date().toISOString() };\n text.value = \"\";\n error = \"\";\n draw();\n try {\n take(await api().sendChat({ body: msg, email: needEmail() && email.value.trim() ? email.value.trim() : undefined, url: window.location.href.slice(0, 500) }));\n } catch (e) {\n text.value = msg;\n error = (e as Error).message;\n }\n pending = null;\n draw();\n schedule();\n };\n form.addEventListener(\"submit\", (e) => {\n e.preventDefault();\n void submit();\n });\n text.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" && !e.shiftKey && !e.isComposing) {\n e.preventDefault();\n void submit();\n }\n });\n text.addEventListener(\"input\", () => (send.disabled = !canSend()));\n email.addEventListener(\"input\", () => draw());\n\n function show(v: MessengerView | { article: string } = \"home\") {\n open = true;\n view = v === \"help\" && !helpOn ? \"home\" : v;\n drawn = -1;\n previewGone = true;\n if (!pop.isConnected) root.appendChild(pop);\n if (articles === null || (view === \"help\" && query)) void loadArticles();\n draw();\n drawFab();\n if (view === \"messages\") {\n markSeen();\n text.focus();\n }\n schedule(0);\n }\n function close() {\n if (!open) return;\n open = false;\n pop.remove();\n drawFab();\n schedule();\n if (launcherOn) fab.focus();\n }\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\" && open) close();\n };\n document.addEventListener(\"keydown\", onKey);\n\n // One read on load (the count and preview of answers not seen yet), and the article count for the Help tab.\n void tick();\n if (helpOn) void loadArticles();\n drawFab();\n\n return {\n open: (v) => show(v ?? \"home\"),\n close,\n toggle: () => (open ? close() : show()),\n destroy: () => {\n clearTimeout(poll);\n ctrl?.abort();\n document.removeEventListener(\"keydown\", onKey);\n root.remove();\n },\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MessengerView } from "./messenger-ui.js";
|
|
2
|
+
import type { Theme } from "./ui.js";
|
|
3
|
+
/** Open (on Home, a tab or an article), close or toggle the messenger from your own button (or mark any element `data-ck-messenger`). */
|
|
4
|
+
export declare const useMessenger: () => {
|
|
5
|
+
open: (view?: MessengerView | {
|
|
6
|
+
article: string;
|
|
7
|
+
}) => void;
|
|
8
|
+
close: () => void | undefined;
|
|
9
|
+
toggle: () => void;
|
|
10
|
+
};
|
|
11
|
+
export interface MessengerProps {
|
|
12
|
+
position?: "bottom-right" | "bottom-left";
|
|
13
|
+
/** The panel's title. */
|
|
14
|
+
title?: string;
|
|
15
|
+
/** The big line on Home and the first line of an empty chat. */
|
|
16
|
+
greeting?: string;
|
|
17
|
+
/** Ask anonymous visitors for an email (optional for them). Default true. */
|
|
18
|
+
askEmail?: boolean;
|
|
19
|
+
/** Text on the launcher; empty shows the icon only. */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Show the round launcher (default true). False: open it with `useMessenger()` or `data-ck-messenger`. */
|
|
22
|
+
launcher?: boolean;
|
|
23
|
+
/** The Help tab with your published help articles (default true; hidden while there are none). */
|
|
24
|
+
help?: boolean;
|
|
25
|
+
/** A short sound when an answer arrives (default true). */
|
|
26
|
+
sound?: boolean;
|
|
27
|
+
/** A preview of a new answer above the closed launcher (default true). */
|
|
28
|
+
preview?: boolean;
|
|
29
|
+
theme?: Theme;
|
|
30
|
+
accent?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A floating launcher that opens the messenger. Put it once inside `<CockpitProvider>`:
|
|
34
|
+
*
|
|
35
|
+
* <Messenger title="Acme" greeting="Hi! How can we help?" />
|
|
36
|
+
*/
|
|
37
|
+
export declare const Messenger: ({ position, title, greeting, askEmail, label, launcher, help, sound, preview, theme, accent }: MessengerProps) => null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/messenger.tsx
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { useCtx } from "./context.js";
|
|
6
|
+
var handle = null;
|
|
7
|
+
var waiting = null;
|
|
8
|
+
var withHandle = (fn) => {
|
|
9
|
+
if (handle) fn(handle);
|
|
10
|
+
else waiting = fn;
|
|
11
|
+
};
|
|
12
|
+
var useMessenger = () => ({
|
|
13
|
+
open: (view) => withHandle((h) => h.open(view)),
|
|
14
|
+
close: () => handle?.close(),
|
|
15
|
+
toggle: () => withHandle((h) => h.toggle())
|
|
16
|
+
});
|
|
17
|
+
var Messenger = ({ position, title, greeting, askEmail, label, launcher, help, sound, preview, theme, accent }) => {
|
|
18
|
+
const ctx = useCtx();
|
|
19
|
+
const who = useRef({ distinctId: ctx.distinctId, user: ctx.user });
|
|
20
|
+
who.current = { distinctId: ctx.distinctId, user: ctx.user };
|
|
21
|
+
const { apiKey, host, embedded } = ctx;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (embedded || !apiKey) return;
|
|
24
|
+
let gone = false;
|
|
25
|
+
let mine = null;
|
|
26
|
+
void import("./messenger-ui.js").then(({ mountMessenger }) => {
|
|
27
|
+
if (gone) return;
|
|
28
|
+
mine = mountMessenger({
|
|
29
|
+
host,
|
|
30
|
+
apiKey,
|
|
31
|
+
distinctId: () => who.current.distinctId,
|
|
32
|
+
user: () => who.current.user,
|
|
33
|
+
launcher,
|
|
34
|
+
position,
|
|
35
|
+
title,
|
|
36
|
+
greeting,
|
|
37
|
+
askEmail,
|
|
38
|
+
label,
|
|
39
|
+
help,
|
|
40
|
+
sound,
|
|
41
|
+
preview,
|
|
42
|
+
theme,
|
|
43
|
+
accent
|
|
44
|
+
});
|
|
45
|
+
handle = mine;
|
|
46
|
+
waiting?.(mine);
|
|
47
|
+
waiting = null;
|
|
48
|
+
});
|
|
49
|
+
const click = (e) => {
|
|
50
|
+
if (e.target?.closest("[data-ck-messenger]")) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
withHandle((h) => h.open());
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener("click", click);
|
|
56
|
+
return () => {
|
|
57
|
+
gone = true;
|
|
58
|
+
document.removeEventListener("click", click);
|
|
59
|
+
mine?.destroy();
|
|
60
|
+
if (handle === mine) handle = null;
|
|
61
|
+
};
|
|
62
|
+
}, [apiKey, host, embedded, launcher, position, title, greeting, askEmail, label, help, sound, preview, theme, accent]);
|
|
63
|
+
return null;
|
|
64
|
+
};
|
|
65
|
+
export {
|
|
66
|
+
Messenger,
|
|
67
|
+
useMessenger
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=messenger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messenger.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { useCtx } from \"./context\";\nimport type { MessengerHandle, MessengerView } from \"./messenger-ui\";\nimport type { Theme } from \"./ui\";\n\n/**\n * The messenger, after Featurebase's: a launcher and a panel with Home, Messages (a chat with the product's team) and\n * Help (the product's published help articles). A visitor's messages become a Support conversation (channel \"chat\")\n * in MRRocket's inbox, where the owner answers or the Customer experience agent does (Support › Automation › AI\n * answers). A new answer plays a short sound and shows a preview above the closed launcher.\n *\n * The panel is plain DOM (messenger-ui.ts, shared with the script tag), loaded with a dynamic import after the page\n * rendered. The visitor is the provider's id; a person the site identified carries their name and email.\n */\n\nlet handle: MessengerHandle | null = null;\n/** A call made before the panel loaded runs once it has. */\nlet waiting: ((h: MessengerHandle) => void) | null = null;\nconst withHandle = (fn: (h: MessengerHandle) => void) => {\n if (handle) fn(handle);\n else waiting = fn;\n};\n\n/** Open (on Home, a tab or an article), close or toggle the messenger from your own button (or mark any element `data-ck-messenger`). */\nexport const useMessenger = () => ({\n open: (view?: MessengerView | { article: string }) => withHandle((h) => h.open(view)),\n close: () => handle?.close(),\n toggle: () => withHandle((h) => h.toggle()),\n});\n\nexport interface MessengerProps {\n position?: \"bottom-right\" | \"bottom-left\";\n /** The panel's title. */\n title?: string;\n /** The big line on Home and the first line of an empty chat. */\n greeting?: string;\n /** Ask anonymous visitors for an email (optional for them). Default true. */\n askEmail?: boolean;\n /** Text on the launcher; empty shows the icon only. */\n label?: string;\n /** Show the round launcher (default true). False: open it with `useMessenger()` or `data-ck-messenger`. */\n launcher?: boolean;\n /** The Help tab with your published help articles (default true; hidden while there are none). */\n help?: boolean;\n /** A short sound when an answer arrives (default true). */\n sound?: boolean;\n /** A preview of a new answer above the closed launcher (default true). */\n preview?: boolean;\n theme?: Theme;\n accent?: string;\n}\n\n/**\n * A floating launcher that opens the messenger. Put it once inside `<CockpitProvider>`:\n *\n * <Messenger title=\"Acme\" greeting=\"Hi! How can we help?\" />\n */\nexport const Messenger = ({ position, title, greeting, askEmail, label, launcher, help, sound, preview, theme, accent }: MessengerProps) => {\n const ctx = useCtx();\n // The panel reads the visitor at each request, so a later identify or reset needs no remount.\n const who = useRef({ distinctId: ctx.distinctId, user: ctx.user });\n who.current = { distinctId: ctx.distinctId, user: ctx.user };\n const { apiKey, host, embedded } = ctx;\n\n useEffect(() => {\n if (embedded || !apiKey) return;\n let gone = false;\n let mine: MessengerHandle | null = null;\n void import(\"./messenger-ui\").then(({ mountMessenger }) => {\n if (gone) return;\n mine = mountMessenger({\n host,\n apiKey,\n distinctId: () => who.current.distinctId,\n user: () => who.current.user as { id?: string; name?: string; email?: string } | undefined,\n launcher,\n position,\n title,\n greeting,\n askEmail,\n label,\n help,\n sound,\n preview,\n theme,\n accent,\n });\n handle = mine;\n waiting?.(mine);\n waiting = null;\n });\n const click = (e: MouseEvent) => {\n if ((e.target as HTMLElement | null)?.closest(\"[data-ck-messenger]\")) {\n e.preventDefault();\n withHandle((h) => h.open());\n }\n };\n document.addEventListener(\"click\", click);\n return () => {\n gone = true;\n document.removeEventListener(\"click\", click);\n mine?.destroy();\n if (handle === mine) handle = null;\n };\n }, [apiKey, host, embedded, launcher, position, title, greeting, askEmail, label, help, sound, preview, theme, accent]);\n\n return null;\n};\n"],"mappings":";;;AAEA,SAAS,WAAW,cAAc;AAClC,SAAS,cAAc;AAcvB,IAAI,SAAiC;AAErC,IAAI,UAAiD;AACrD,IAAM,aAAa,CAAC,OAAqC;AACrD,MAAI,OAAQ,IAAG,MAAM;AAAA,MAChB,WAAU;AACnB;AAGO,IAAM,eAAe,OAAO;AAAA,EAC/B,MAAM,CAAC,SAA+C,WAAW,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;AAAA,EACpF,OAAO,MAAM,QAAQ,MAAM;AAAA,EAC3B,QAAQ,MAAM,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC;AAC9C;AA6BO,IAAM,YAAY,CAAC,EAAE,UAAU,OAAO,UAAU,UAAU,OAAO,UAAU,MAAM,OAAO,SAAS,OAAO,OAAO,MAAsB;AACxI,QAAM,MAAM,OAAO;AAEnB,QAAM,MAAM,OAAO,EAAE,YAAY,IAAI,YAAY,MAAM,IAAI,KAAK,CAAC;AACjE,MAAI,UAAU,EAAE,YAAY,IAAI,YAAY,MAAM,IAAI,KAAK;AAC3D,QAAM,EAAE,QAAQ,MAAM,SAAS,IAAI;AAEnC,YAAU,MAAM;AACZ,QAAI,YAAY,CAAC,OAAQ;AACzB,QAAI,OAAO;AACX,QAAI,OAA+B;AACnC,SAAK,OAAO,mBAAgB,EAAE,KAAK,CAAC,EAAE,eAAe,MAAM;AACvD,UAAI,KAAM;AACV,aAAO,eAAe;AAAA,QAClB;AAAA,QACA;AAAA,QACA,YAAY,MAAM,IAAI,QAAQ;AAAA,QAC9B,MAAM,MAAM,IAAI,QAAQ;AAAA,QACxB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACJ,CAAC;AACD,eAAS;AACT,gBAAU,IAAI;AACd,gBAAU;AAAA,IACd,CAAC;AACD,UAAM,QAAQ,CAAC,MAAkB;AAC7B,UAAK,EAAE,QAA+B,QAAQ,qBAAqB,GAAG;AAClE,UAAE,eAAe;AACjB,mBAAW,CAAC,MAAM,EAAE,KAAK,CAAC;AAAA,MAC9B;AAAA,IACJ;AACA,aAAS,iBAAiB,SAAS,KAAK;AACxC,WAAO,MAAM;AACT,aAAO;AACP,eAAS,oBAAoB,SAAS,KAAK;AAC3C,YAAM,QAAQ;AACd,UAAI,WAAW,KAAM,UAAS;AAAA,IAClC;AAAA,EACJ,GAAG,CAAC,QAAQ,MAAM,UAAU,UAAU,UAAU,OAAO,UAAU,UAAU,OAAO,MAAM,OAAO,SAAS,OAAO,MAAM,CAAC;AAEtH,SAAO;AACX;","names":[]}
|
package/dist/proxy.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type CockpitConfig } from "./core.js";
|
|
2
|
+
export { getCockpitConfig } from "./server.js";
|
|
1
3
|
/** What `request.cookies` (Next's `RequestCookies`) offers: read, and set so that the page of this request sees the id. */
|
|
2
4
|
interface IncomingCookies {
|
|
3
5
|
get(name: string): {
|
|
@@ -25,4 +27,35 @@ interface OutgoingCookies {
|
|
|
25
27
|
* }
|
|
26
28
|
*/
|
|
27
29
|
export declare const withCockpitId: (incoming: IncomingCookies, outgoing: OutgoingCookies) => void;
|
|
28
|
-
|
|
30
|
+
/**
|
|
31
|
+
* For `proxy.ts`: where to send this request, or null to render it. Page redirects set in MRRocket (Landing pages ›
|
|
32
|
+
* Redirect) and redirect tests (Experiments) answer before the page renders, so nobody sees the old page flash. The
|
|
33
|
+
* page the visitor lands on records the test's exposure (`<CockpitProvider>` reads `ck_x` there); control renders as
|
|
34
|
+
* usual and the provider records it.
|
|
35
|
+
*
|
|
36
|
+
* A running test needs a stable visitor id to keep each person in one variant: the `ck_id` cookie (consent "cookies",
|
|
37
|
+
* with `withCockpitId` before this), or your signed-in user id as `distinctId`. Without either, the test is left to the
|
|
38
|
+
* browser (the provider redirects after the first paint); page redirects and shipped winners still answer here.
|
|
39
|
+
*
|
|
40
|
+
* import { NextResponse, type NextRequest } from "next/server";
|
|
41
|
+
* import { experimentRedirect, getCockpitConfig } from "@mrrocket/sdk/next/proxy";
|
|
42
|
+
*
|
|
43
|
+
* export async function proxy(request: NextRequest) {
|
|
44
|
+
* const to = experimentRedirect(request, await getCockpitConfig({ timeoutMs: 800 }));
|
|
45
|
+
* return to ? NextResponse.redirect(to, 307) : NextResponse.next();
|
|
46
|
+
* }
|
|
47
|
+
* export const config = { matcher: ["/pricing", "/old-pricing"] };
|
|
48
|
+
*
|
|
49
|
+
* Keep the matcher to the pages with a redirect or a test: each matched request waits for the config (at most
|
|
50
|
+
* `timeoutMs`; MRRocket unreachable means no redirect).
|
|
51
|
+
*/
|
|
52
|
+
export declare const experimentRedirect: (request: {
|
|
53
|
+
url: string;
|
|
54
|
+
cookies?: {
|
|
55
|
+
get(name: string): {
|
|
56
|
+
value: string;
|
|
57
|
+
} | undefined;
|
|
58
|
+
};
|
|
59
|
+
}, config: CockpitConfig | null, options?: {
|
|
60
|
+
distinctId?: string;
|
|
61
|
+
}) => string | null;
|
package/dist/proxy.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
// src/proxy.ts
|
|
2
|
-
import { ID_COOKIE, newDistinctId } from "./core.js";
|
|
2
|
+
import { ID_COOKIE, PREVIEW_PARAM, newDistinctId, parsePreview, redirectFor } from "./core.js";
|
|
3
|
+
import { getCockpitConfig } from "./server.js";
|
|
3
4
|
var withCockpitId = (incoming, outgoing) => {
|
|
4
5
|
if (incoming.get(ID_COOKIE)) return;
|
|
5
6
|
const id = newDistinctId();
|
|
6
7
|
incoming.set(ID_COOKIE, id);
|
|
7
8
|
outgoing.set(ID_COOKIE, id, { path: "/", maxAge: 60 * 60 * 24 * 365, sameSite: "lax" });
|
|
8
9
|
};
|
|
10
|
+
var experimentRedirect = (request, config, options = {}) => {
|
|
11
|
+
const id = options.distinctId ?? request.cookies?.get(ID_COOKIE)?.value ?? null;
|
|
12
|
+
let preview = {};
|
|
13
|
+
try {
|
|
14
|
+
preview = parsePreview(new URL(request.url).searchParams.get(PREVIEW_PARAM));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return redirectFor(config, request.url, id, preview)?.to ?? null;
|
|
19
|
+
};
|
|
9
20
|
export {
|
|
21
|
+
experimentRedirect,
|
|
22
|
+
getCockpitConfig,
|
|
10
23
|
withCockpitId
|
|
11
24
|
};
|
|
12
25
|
//# sourceMappingURL=proxy.js.map
|
package/dist/proxy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/proxy.ts"],"sourcesContent":["import { ID_COOKIE, newDistinctId } from \"./core\";\n\n/** What `request.cookies` (Next's `RequestCookies`) offers: read, and set so that the page of this request sees the id. */\ninterface IncomingCookies {\n get(name: string): { value: string } | undefined;\n set(name: string, value: string): unknown;\n}\n\n/** What `response.cookies` (Next's `ResponseCookies`) offers: set with options. */\ninterface OutgoingCookies {\n set(name: string, value: string, options?: { path?: string; maxAge?: number; sameSite?: \"lax\" | \"strict\" | \"none\" }): unknown;\n}\n\n/**\n * For `proxy.ts`: gives a first-time visitor their `ck_id` before the page renders, so the server already assigns\n * the variant they will keep. It writes a cookie, so only use it with `consent=\"cookies\"` and only once the visitor\n * agreed; in the default cookieless mode leave it out.\n *\n * export function proxy(request: NextRequest) {\n * const response = NextResponse.next();\n * withCockpitId(request.cookies, response.cookies);\n * return response;\n * }\n */\nexport const withCockpitId = (incoming: IncomingCookies, outgoing: OutgoingCookies) => {\n if (incoming.get(ID_COOKIE)) return;\n const id = newDistinctId();\n incoming.set(ID_COOKIE, id);\n outgoing.set(ID_COOKIE, id, { path: \"/\", maxAge: 60 * 60 * 24 * 365, sameSite: \"lax\" });\n};\n"],"mappings":";AAAA,
|
|
1
|
+
{"version":3,"sources":["../src/proxy.ts"],"sourcesContent":["import { type CockpitConfig, ID_COOKIE, PREVIEW_PARAM, newDistinctId, parsePreview, redirectFor } from \"./core\";\n\nexport { getCockpitConfig } from \"./server\";\n\n/** What `request.cookies` (Next's `RequestCookies`) offers: read, and set so that the page of this request sees the id. */\ninterface IncomingCookies {\n get(name: string): { value: string } | undefined;\n set(name: string, value: string): unknown;\n}\n\n/** What `response.cookies` (Next's `ResponseCookies`) offers: set with options. */\ninterface OutgoingCookies {\n set(name: string, value: string, options?: { path?: string; maxAge?: number; sameSite?: \"lax\" | \"strict\" | \"none\" }): unknown;\n}\n\n/**\n * For `proxy.ts`: gives a first-time visitor their `ck_id` before the page renders, so the server already assigns\n * the variant they will keep. It writes a cookie, so only use it with `consent=\"cookies\"` and only once the visitor\n * agreed; in the default cookieless mode leave it out.\n *\n * export function proxy(request: NextRequest) {\n * const response = NextResponse.next();\n * withCockpitId(request.cookies, response.cookies);\n * return response;\n * }\n */\nexport const withCockpitId = (incoming: IncomingCookies, outgoing: OutgoingCookies) => {\n if (incoming.get(ID_COOKIE)) return;\n const id = newDistinctId();\n incoming.set(ID_COOKIE, id);\n outgoing.set(ID_COOKIE, id, { path: \"/\", maxAge: 60 * 60 * 24 * 365, sameSite: \"lax\" });\n};\n\n/**\n * For `proxy.ts`: where to send this request, or null to render it. Page redirects set in MRRocket (Landing pages ›\n * Redirect) and redirect tests (Experiments) answer before the page renders, so nobody sees the old page flash. The\n * page the visitor lands on records the test's exposure (`<CockpitProvider>` reads `ck_x` there); control renders as\n * usual and the provider records it.\n *\n * A running test needs a stable visitor id to keep each person in one variant: the `ck_id` cookie (consent \"cookies\",\n * with `withCockpitId` before this), or your signed-in user id as `distinctId`. Without either, the test is left to the\n * browser (the provider redirects after the first paint); page redirects and shipped winners still answer here.\n *\n * import { NextResponse, type NextRequest } from \"next/server\";\n * import { experimentRedirect, getCockpitConfig } from \"@mrrocket/sdk/next/proxy\";\n *\n * export async function proxy(request: NextRequest) {\n * const to = experimentRedirect(request, await getCockpitConfig({ timeoutMs: 800 }));\n * return to ? NextResponse.redirect(to, 307) : NextResponse.next();\n * }\n * export const config = { matcher: [\"/pricing\", \"/old-pricing\"] };\n *\n * Keep the matcher to the pages with a redirect or a test: each matched request waits for the config (at most\n * `timeoutMs`; MRRocket unreachable means no redirect).\n */\nexport const experimentRedirect = (\n request: { url: string; cookies?: { get(name: string): { value: string } | undefined } },\n config: CockpitConfig | null,\n options: { distinctId?: string } = {},\n): string | null => {\n const id = options.distinctId ?? request.cookies?.get(ID_COOKIE)?.value ?? null;\n let preview = {};\n try {\n preview = parsePreview(new URL(request.url).searchParams.get(PREVIEW_PARAM));\n } catch {\n return null;\n }\n return redirectFor(config, request.url, id, preview)?.to ?? null;\n};\n"],"mappings":";AAAA,SAA6B,WAAW,eAAe,eAAe,cAAc,mBAAmB;AAEvG,SAAS,wBAAwB;AAwB1B,IAAM,gBAAgB,CAAC,UAA2B,aAA8B;AACnF,MAAI,SAAS,IAAI,SAAS,EAAG;AAC7B,QAAM,KAAK,cAAc;AACzB,WAAS,IAAI,WAAW,EAAE;AAC1B,WAAS,IAAI,WAAW,IAAI,EAAE,MAAM,KAAK,QAAQ,KAAK,KAAK,KAAK,KAAK,UAAU,MAAM,CAAC;AAC1F;AAwBO,IAAM,qBAAqB,CAC9B,SACA,QACA,UAAmC,CAAC,MACpB;AAChB,QAAM,KAAK,QAAQ,cAAc,QAAQ,SAAS,IAAI,SAAS,GAAG,SAAS;AAC3E,MAAI,UAAU,CAAC;AACf,MAAI;AACA,cAAU,aAAa,IAAI,IAAI,QAAQ,GAAG,EAAE,aAAa,IAAI,aAAa,CAAC;AAAA,EAC/E,QAAQ;AACJ,WAAO;AAAA,EACX;AACA,SAAO,YAAY,QAAQ,QAAQ,KAAK,IAAI,OAAO,GAAG,MAAM;AAChE;","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web push in the browser (SDK 0.7.0): signs the signed-in person up for push notifications from the product's Lifecycle
|
|
3
|
+
* journeys. Loaded only when the site calls `push.subscribe()` (a dynamic import in Next.js, `sdk-push.js` for the script
|
|
4
|
+
* tag), so pages that never ask carry none of it.
|
|
5
|
+
*
|
|
6
|
+
* 1. The browser's permission (asked only from a click, and only when the site says so; `ask: false` only re-sends an
|
|
7
|
+
* existing subscription).
|
|
8
|
+
* 2. The service worker the site hosts (`/mrrocket-sw.js`, a copy of the file MRRocket serves), registered with the
|
|
9
|
+
* public key and MRRocket's address in its URL, so it can report opened notifications. A site with its own service
|
|
10
|
+
* worker imports ours there and passes `serviceWorker: false`.
|
|
11
|
+
* 3. The workspace's VAPID key from `GET /api/sdk/push`, the browser's subscription with it, and the subscription sent to
|
|
12
|
+
* `POST /api/sdk/push` for this person. A subscription made with another key is replaced.
|
|
13
|
+
*/
|
|
14
|
+
export type PushStatus = "subscribed" | "denied" | "not-asked" | "unsupported" | "not-signed-in" | "error";
|
|
15
|
+
export interface PushResult {
|
|
16
|
+
status: PushStatus;
|
|
17
|
+
/** What went wrong, for "error" and "unsupported". */
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface WebPushOptions {
|
|
21
|
+
/**
|
|
22
|
+
* The service worker's path on your site (default "/mrrocket-sw.js"), or false when your site already has one that
|
|
23
|
+
* imports ours (`importScripts("/mrrocket-sw.js")`): the SDK then uses the one that is active.
|
|
24
|
+
*/
|
|
25
|
+
serviceWorker?: string | false;
|
|
26
|
+
/** Ask for the permission when it was not given yet (default true). False: only re-send an existing subscription. */
|
|
27
|
+
ask?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface WebPushContext {
|
|
30
|
+
host: string;
|
|
31
|
+
apiKey: string;
|
|
32
|
+
/** The identify() id; push needs a signed-in person. */
|
|
33
|
+
distinctId: string | undefined;
|
|
34
|
+
/** The permission, asked synchronously by the caller inside the click (Safari needs that), or the current one. */
|
|
35
|
+
permission?: Promise<NotificationPermission>;
|
|
36
|
+
}
|
|
37
|
+
/** True where the browser can receive web push (Safari 16.4+ only for sites added to the Home Screen on iOS). */
|
|
38
|
+
export declare const webPushSupported: () => boolean;
|
|
39
|
+
/** The permission, asked right away when `ask` and not given yet. Call it synchronously in the click handler. */
|
|
40
|
+
export declare const askPermission: (ask?: boolean) => Promise<NotificationPermission>;
|
|
41
|
+
export declare const subscribeWebPush: (ctx: WebPushContext, options?: WebPushOptions) => Promise<PushResult>;
|
|
42
|
+
/** Ends this browser's subscription and tells MRRocket (sign-out, or the person turned push off). */
|
|
43
|
+
export declare const unsubscribeWebPush: (ctx: WebPushContext) => Promise<boolean>;
|
package/dist/push-web.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/push-web.ts
|
|
2
|
+
var b64urlBytes = (s) => {
|
|
3
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "="));
|
|
4
|
+
const out = new Uint8Array(bin.length);
|
|
5
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
6
|
+
return out;
|
|
7
|
+
};
|
|
8
|
+
var sameKey = (a, b) => {
|
|
9
|
+
if (!a) return false;
|
|
10
|
+
const x = new Uint8Array(a);
|
|
11
|
+
return x.length === b.length && x.every((v, i) => v === b[i]);
|
|
12
|
+
};
|
|
13
|
+
var webPushSupported = () => typeof window !== "undefined" && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
|
|
14
|
+
var askPermission = (ask = true) => {
|
|
15
|
+
if (!webPushSupported()) return Promise.resolve("denied");
|
|
16
|
+
if (Notification.permission !== "default" || !ask) return Promise.resolve(Notification.permission);
|
|
17
|
+
try {
|
|
18
|
+
return Promise.resolve(Notification.requestPermission());
|
|
19
|
+
} catch {
|
|
20
|
+
return Promise.resolve("denied");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var registration = async (host, apiKey, sw) => {
|
|
24
|
+
if (sw !== false) {
|
|
25
|
+
const url = `${sw}?key=${encodeURIComponent(apiKey)}&host=${encodeURIComponent(host)}`;
|
|
26
|
+
await navigator.serviceWorker.register(url);
|
|
27
|
+
}
|
|
28
|
+
return navigator.serviceWorker.ready;
|
|
29
|
+
};
|
|
30
|
+
var subscribeWebPush = async (ctx, options = {}) => {
|
|
31
|
+
if (!webPushSupported()) return { status: "unsupported", reason: "This browser has no web push." };
|
|
32
|
+
if (!ctx.distinctId) return { status: "not-signed-in" };
|
|
33
|
+
const ask = options.ask !== false;
|
|
34
|
+
const permission = await (ctx.permission ?? askPermission(ask));
|
|
35
|
+
if (permission === "denied") return { status: "denied" };
|
|
36
|
+
if (permission !== "granted") return { status: ask ? "denied" : "not-asked" };
|
|
37
|
+
try {
|
|
38
|
+
const r = await fetch(`${ctx.host}/api/sdk/push?key=${encodeURIComponent(ctx.apiKey)}`);
|
|
39
|
+
const vapid = r.ok ? (await r.json()).web?.vapidKey : void 0;
|
|
40
|
+
if (!vapid) return { status: "error", reason: `MRRocket answered ${r.status} for the push key.` };
|
|
41
|
+
const key = b64urlBytes(vapid);
|
|
42
|
+
const reg = await registration(ctx.host, ctx.apiKey, options.serviceWorker ?? "/mrrocket-sw.js");
|
|
43
|
+
let sub = await reg.pushManager.getSubscription();
|
|
44
|
+
if (sub && !sameKey(sub.options?.applicationServerKey, key)) {
|
|
45
|
+
await sub.unsubscribe().catch(() => false);
|
|
46
|
+
sub = null;
|
|
47
|
+
}
|
|
48
|
+
sub ??= await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
|
|
49
|
+
const saved = await fetch(`${ctx.host}/api/sdk/push`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: "web", subscription: sub.toJSON(), host: ctx.host })
|
|
53
|
+
});
|
|
54
|
+
return saved.ok ? { status: "subscribed" } : { status: "error", reason: `MRRocket answered ${saved.status}.` };
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return { status: "error", reason: e instanceof Error ? e.message : String(e) };
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var unsubscribeWebPush = async (ctx) => {
|
|
60
|
+
if (!webPushSupported()) return false;
|
|
61
|
+
try {
|
|
62
|
+
const reg = await navigator.serviceWorker.getRegistration();
|
|
63
|
+
const sub = await reg?.pushManager.getSubscription();
|
|
64
|
+
if (!sub) return false;
|
|
65
|
+
await fetch(`${ctx.host}/api/sdk/push`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: "web", remove: true, endpoint: sub.endpoint })
|
|
69
|
+
}).catch(() => null);
|
|
70
|
+
return await sub.unsubscribe();
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
export {
|
|
76
|
+
askPermission,
|
|
77
|
+
subscribeWebPush,
|
|
78
|
+
unsubscribeWebPush,
|
|
79
|
+
webPushSupported
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=push-web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/push-web.ts"],"sourcesContent":["/**\n * Web push in the browser (SDK 0.7.0): signs the signed-in person up for push notifications from the product's Lifecycle\n * journeys. Loaded only when the site calls `push.subscribe()` (a dynamic import in Next.js, `sdk-push.js` for the script\n * tag), so pages that never ask carry none of it.\n *\n * 1. The browser's permission (asked only from a click, and only when the site says so; `ask: false` only re-sends an\n * existing subscription).\n * 2. The service worker the site hosts (`/mrrocket-sw.js`, a copy of the file MRRocket serves), registered with the\n * public key and MRRocket's address in its URL, so it can report opened notifications. A site with its own service\n * worker imports ours there and passes `serviceWorker: false`.\n * 3. The workspace's VAPID key from `GET /api/sdk/push`, the browser's subscription with it, and the subscription sent to\n * `POST /api/sdk/push` for this person. A subscription made with another key is replaced.\n */\nexport type PushStatus = \"subscribed\" | \"denied\" | \"not-asked\" | \"unsupported\" | \"not-signed-in\" | \"error\";\n\nexport interface PushResult {\n status: PushStatus;\n /** What went wrong, for \"error\" and \"unsupported\". */\n reason?: string;\n}\n\nexport interface WebPushOptions {\n /**\n * The service worker's path on your site (default \"/mrrocket-sw.js\"), or false when your site already has one that\n * imports ours (`importScripts(\"/mrrocket-sw.js\")`): the SDK then uses the one that is active.\n */\n serviceWorker?: string | false;\n /** Ask for the permission when it was not given yet (default true). False: only re-send an existing subscription. */\n ask?: boolean;\n}\n\nexport interface WebPushContext {\n host: string;\n apiKey: string;\n /** The identify() id; push needs a signed-in person. */\n distinctId: string | undefined;\n /** The permission, asked synchronously by the caller inside the click (Safari needs that), or the current one. */\n permission?: Promise<NotificationPermission>;\n}\n\nconst b64urlBytes = (s: string) => {\n const bin = atob(s.replace(/-/g, \"+\").replace(/_/g, \"/\").padEnd(Math.ceil(s.length / 4) * 4, \"=\"));\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n};\n\nconst sameKey = (a: ArrayBuffer | null | undefined, b: Uint8Array) => {\n if (!a) return false;\n const x = new Uint8Array(a);\n return x.length === b.length && x.every((v, i) => v === b[i]);\n};\n\n/** True where the browser can receive web push (Safari 16.4+ only for sites added to the Home Screen on iOS). */\nexport const webPushSupported = () =>\n typeof window !== \"undefined\" && \"serviceWorker\" in navigator && \"PushManager\" in window && \"Notification\" in window;\n\n/** The permission, asked right away when `ask` and not given yet. Call it synchronously in the click handler. */\nexport const askPermission = (ask = true): Promise<NotificationPermission> => {\n if (!webPushSupported()) return Promise.resolve(\"denied\");\n if (Notification.permission !== \"default\" || !ask) return Promise.resolve(Notification.permission);\n try {\n return Promise.resolve(Notification.requestPermission());\n } catch {\n return Promise.resolve(\"denied\");\n }\n};\n\nconst registration = async (host: string, apiKey: string, sw: string | false) => {\n if (sw !== false) {\n const url = `${sw}?key=${encodeURIComponent(apiKey)}&host=${encodeURIComponent(host)}`;\n await navigator.serviceWorker.register(url);\n }\n return navigator.serviceWorker.ready;\n};\n\nexport const subscribeWebPush = async (ctx: WebPushContext, options: WebPushOptions = {}): Promise<PushResult> => {\n if (!webPushSupported()) return { status: \"unsupported\", reason: \"This browser has no web push.\" };\n if (!ctx.distinctId) return { status: \"not-signed-in\" };\n const ask = options.ask !== false;\n const permission = await (ctx.permission ?? askPermission(ask));\n if (permission === \"denied\") return { status: \"denied\" };\n if (permission !== \"granted\") return { status: ask ? \"denied\" : \"not-asked\" };\n try {\n const r = await fetch(`${ctx.host}/api/sdk/push?key=${encodeURIComponent(ctx.apiKey)}`);\n const vapid = r.ok ? ((await r.json()) as { web?: { vapidKey?: string } }).web?.vapidKey : undefined;\n if (!vapid) return { status: \"error\", reason: `MRRocket answered ${r.status} for the push key.` };\n const key = b64urlBytes(vapid);\n const reg = await registration(ctx.host, ctx.apiKey, options.serviceWorker ?? \"/mrrocket-sw.js\");\n let sub = await reg.pushManager.getSubscription();\n if (sub && !sameKey(sub.options?.applicationServerKey, key)) {\n await sub.unsubscribe().catch(() => false);\n sub = null;\n }\n sub ??= await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });\n const saved = await fetch(`${ctx.host}/api/sdk/push`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: \"web\", subscription: sub.toJSON(), host: ctx.host }),\n });\n return saved.ok ? { status: \"subscribed\" } : { status: \"error\", reason: `MRRocket answered ${saved.status}.` };\n } catch (e) {\n return { status: \"error\", reason: e instanceof Error ? e.message : String(e) };\n }\n};\n\n/** Ends this browser's subscription and tells MRRocket (sign-out, or the person turned push off). */\nexport const unsubscribeWebPush = async (ctx: WebPushContext): Promise<boolean> => {\n if (!webPushSupported()) return false;\n try {\n const reg = await navigator.serviceWorker.getRegistration();\n const sub = await reg?.pushManager.getSubscription();\n if (!sub) return false;\n await fetch(`${ctx.host}/api/sdk/push`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: \"web\", remove: true, endpoint: sub.endpoint }),\n }).catch(() => null);\n return await sub.unsubscribe();\n } catch {\n return false;\n }\n};\n"],"mappings":"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","names":[]}
|
package/dist/rage.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Rage clicks without the DOM, so the web SDK and the apps (taps) count them the same. */
|
|
2
|
+
/**
|
|
3
|
+
* Three or more clicks within one second on the same spot (30 pixels) are a rage click. `push` answers true once per
|
|
4
|
+
* burst, on the third click.
|
|
5
|
+
*/
|
|
6
|
+
export declare class RageDetector {
|
|
7
|
+
private readonly windowMs;
|
|
8
|
+
private readonly radius;
|
|
9
|
+
private readonly count;
|
|
10
|
+
private clicks;
|
|
11
|
+
private fired;
|
|
12
|
+
constructor(windowMs?: number, radius?: number, count?: number);
|
|
13
|
+
push(x: number, y: number, t: number): boolean;
|
|
14
|
+
}
|
package/dist/rage.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/rage.ts
|
|
2
|
+
var RageDetector = class {
|
|
3
|
+
constructor(windowMs = 1e3, radius = 30, count = 3) {
|
|
4
|
+
this.windowMs = windowMs;
|
|
5
|
+
this.radius = radius;
|
|
6
|
+
this.count = count;
|
|
7
|
+
}
|
|
8
|
+
windowMs;
|
|
9
|
+
radius;
|
|
10
|
+
count;
|
|
11
|
+
clicks = [];
|
|
12
|
+
fired = false;
|
|
13
|
+
push(x, y, t) {
|
|
14
|
+
const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
|
|
15
|
+
if (near.length !== this.clicks.length) this.fired = false;
|
|
16
|
+
this.clicks = [...near, { x, y, t }];
|
|
17
|
+
if (this.clicks.length >= this.count && !this.fired) {
|
|
18
|
+
this.fired = true;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
RageDetector
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=rage.js.map
|
package/dist/rage.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rage.ts"],"sourcesContent":["/** Rage clicks without the DOM, so the web SDK and the apps (taps) count them the same. */\n\n/**\n * Three or more clicks within one second on the same spot (30 pixels) are a rage click. `push` answers true once per\n * burst, on the third click.\n */\nexport class RageDetector {\n private clicks: { x: number; y: number; t: number }[] = [];\n private fired = false;\n constructor(\n private readonly windowMs = 1000,\n private readonly radius = 30,\n private readonly count = 3,\n ) {}\n push(x: number, y: number, t: number) {\n const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);\n if (near.length !== this.clicks.length) this.fired = false;\n this.clicks = [...near, { x, y, t }];\n if (this.clicks.length >= this.count && !this.fired) {\n this.fired = true;\n return true;\n }\n return false;\n }\n}\n"],"mappings":";AAMO,IAAM,eAAN,MAAmB;AAAA,EAGtB,YACqB,WAAW,KACX,SAAS,IACT,QAAQ,GAC3B;AAHmB;AACA;AACA;AAAA,EAClB;AAAA,EAHkB;AAAA,EACA;AAAA,EACA;AAAA,EALb,SAAgD,CAAC;AAAA,EACjD,QAAQ;AAAA,EAMhB,KAAK,GAAW,GAAW,GAAW;AAClC,UAAM,OAAO,KAAK,OAAO,OAAO,CAAC,MAAM,IAAI,EAAE,KAAK,KAAK,YAAY,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,IAAI,CAAC,KAAK,KAAK,MAAM;AAC9G,QAAI,KAAK,WAAW,KAAK,OAAO,OAAQ,MAAK,QAAQ;AACrD,SAAK,SAAS,CAAC,GAAG,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC;AACnC,QAAI,KAAK,OAAO,UAAU,KAAK,SAAS,CAAC,KAAK,OAAO;AACjD,WAAK,QAAQ;AACb,aAAO;AAAA,IACX;AACA,WAAO;AAAA,EACX;AACJ;","names":[]}
|