@appright/funnelshift 0.0.0-stage → 0.15.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/bin/funnel-kit.js +2 -0
- package/dist/anchors.d.ts +4 -0
- package/dist/anchors.js +94 -0
- package/dist/answers.d.ts +11 -0
- package/dist/answers.js +100 -0
- package/dist/attribution.d.ts +34 -0
- package/dist/attribution.js +237 -0
- package/dist/autotrack.d.ts +4 -0
- package/dist/autotrack.js +13 -0
- package/dist/backnav.d.ts +15 -0
- package/dist/backnav.js +73 -0
- package/dist/blocks/Choice.d.ts +19 -0
- package/dist/blocks/Choice.js +38 -0
- package/dist/blocks/Cta.d.ts +7 -0
- package/dist/blocks/Cta.js +8 -0
- package/dist/blocks/Hero.d.ts +10 -0
- package/dist/blocks/Hero.js +48 -0
- package/dist/blocks/MultiChoice.d.ts +13 -0
- package/dist/blocks/MultiChoice.js +47 -0
- package/dist/blocks/Section.d.ts +6 -0
- package/dist/blocks/Section.js +4 -0
- package/dist/bootstrap.d.ts +33 -0
- package/dist/bootstrap.js +19 -0
- package/dist/bucket.d.ts +13 -0
- package/dist/bucket.js +41 -0
- package/dist/cli/args.d.ts +5 -0
- package/dist/cli/args.js +18 -0
- package/dist/cli/build.d.ts +6 -0
- package/dist/cli/build.js +200 -0
- package/dist/cli/extract.d.ts +32 -0
- package/dist/cli/extract.js +253 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +38 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +67 -0
- package/dist/cli/lint.d.ts +25 -0
- package/dist/cli/lint.js +535 -0
- package/dist/cli/upload.d.ts +29 -0
- package/dist/cli/upload.js +90 -0
- package/dist/clicks.d.ts +7 -0
- package/dist/clicks.js +102 -0
- package/dist/content.d.ts +7 -0
- package/dist/content.js +60 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +52 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +90 -0
- package/dist/experiments.d.ts +4 -0
- package/dist/experiments.js +24 -0
- package/dist/exposure.d.ts +4 -0
- package/dist/exposure.js +35 -0
- package/dist/functions.d.ts +37 -0
- package/dist/functions.js +24 -0
- package/dist/funnel.d.ts +64 -0
- package/dist/funnel.js +433 -0
- package/dist/handoff.d.ts +8 -0
- package/dist/handoff.js +66 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/nav.d.ts +19 -0
- package/dist/nav.js +14 -0
- package/dist/offer.d.ts +3 -0
- package/dist/offer.js +7 -0
- package/dist/pixel.d.ts +36 -0
- package/dist/pixel.js +173 -0
- package/dist/prerender.d.ts +2 -0
- package/dist/prerender.js +16 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +88 -0
- package/dist/sha256.d.ts +2 -0
- package/dist/sha256.js +65 -0
- package/dist/signals.d.ts +18 -0
- package/dist/signals.js +300 -0
- package/dist/tracker.d.ts +80 -0
- package/dist/tracker.js +446 -0
- package/dist/uuid.d.ts +1 -0
- package/dist/uuid.js +12 -0
- package/dist/vitals.d.ts +4 -0
- package/dist/vitals.js +104 -0
- package/package.json +29 -4
- package/src/blocks/blocks.css +364 -0
- package/templates/init/CLAUDE.md +10 -0
- package/templates/init/funnels/welcome/index.tsx +129 -0
- package/templates/init/gitignore +5 -0
- package/templates/init/package.json.tmpl +20 -0
- package/templates/init/theme.css +77 -0
- package/templates/init/tsconfig.json +7 -0
- package/README.md +0 -3
package/dist/pixel.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const PIXEL = /^[0-9]{5,20}$/;
|
|
2
|
+
const STEP = /^[a-z0-9_-]{1,64}$/;
|
|
3
|
+
const FBCLID = /^[A-Za-z0-9_.\-]{1,400}$/;
|
|
4
|
+
const COOKIE_AGE = 90 * 24 * 3600;
|
|
5
|
+
const SRC = 'https://connect.facebook.net/en_US/fbevents.js';
|
|
6
|
+
// The bootstrap's meta_pixel, when it has the right shape.
|
|
7
|
+
export function readPixel(v) {
|
|
8
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
9
|
+
return null;
|
|
10
|
+
const { id, lead_step, checkout_step } = v;
|
|
11
|
+
if (typeof id !== 'string' || !PIXEL.test(id))
|
|
12
|
+
return null;
|
|
13
|
+
return { id, ...(typeof lead_step === 'string' && STEP.test(lead_step) ? { lead_step } : {}),
|
|
14
|
+
...(typeof checkout_step === 'string' && STEP.test(checkout_step) ? { checkout_step } : {}) };
|
|
15
|
+
}
|
|
16
|
+
export function readCookie(doc, name) {
|
|
17
|
+
let jar = '';
|
|
18
|
+
try {
|
|
19
|
+
jar = doc.cookie;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
const m = new RegExp(`(?:^|;\\s*)${name}=([^;]*)`).exec(jar);
|
|
25
|
+
if (!m)
|
|
26
|
+
return '';
|
|
27
|
+
try {
|
|
28
|
+
return decodeURIComponent(m[1]);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return m[1];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Sets the cookie on the highest domain of the page that takes it (as the pixel does: example.com for www.example.com), and gives the
|
|
35
|
+
// subdomain index Meta writes into the value (1 for example.com). An address or localhost: the host itself, index 0.
|
|
36
|
+
export function setTopCookie(doc, loc, name, make) {
|
|
37
|
+
const host = loc.hostname;
|
|
38
|
+
const secure = loc.protocol === 'https:' ? '; Secure' : '';
|
|
39
|
+
const parts = host.split('.');
|
|
40
|
+
const ipOrLocal = !host.includes('.') || /^[0-9.]+$/.test(host) || host.includes(':');
|
|
41
|
+
const tries = ipOrLocal ? [['', 0]] : parts.slice(0, -1).map((_, i) => [parts.slice(-(i + 2)).join('.'), i + 1]);
|
|
42
|
+
for (const [domain, index] of tries) {
|
|
43
|
+
const v = make(index);
|
|
44
|
+
try {
|
|
45
|
+
doc.cookie = `${name}=${encodeURIComponent(v)}; Max-Age=${COOKIE_AGE}; Path=/${domain ? '; Domain=.' + domain : ''}; SameSite=Lax${secure}`;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
if (readCookie(doc, name) === v)
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
const rand10 = () => String(Math.floor(1e9 + Math.random() * 9e9));
|
|
56
|
+
// _fbp: the browser id of Meta; made when missing.
|
|
57
|
+
export function ensureFbp(doc, loc, now) {
|
|
58
|
+
return readCookie(doc, '_fbp') || setTopCookie(doc, loc, '_fbp', i => `fb.${i}.${now}.${rand10()}`);
|
|
59
|
+
}
|
|
60
|
+
// _fbc: the click id of Meta from the link (fbclid); a new fbclid replaces the old one, a visit without one keeps it.
|
|
61
|
+
export function ensureFbc(doc, loc, now) {
|
|
62
|
+
let id = '';
|
|
63
|
+
try {
|
|
64
|
+
id = new URLSearchParams(loc.search).get('fbclid') ?? '';
|
|
65
|
+
}
|
|
66
|
+
catch { /* no search */ }
|
|
67
|
+
const had = readCookie(doc, '_fbc');
|
|
68
|
+
if (!id || !FBCLID.test(id) || had.endsWith('.' + id))
|
|
69
|
+
return had;
|
|
70
|
+
return setTopCookie(doc, loc, '_fbc', i => `fb.${i}.${now}.${id}`) || `fb.1.${now}.${id}`;
|
|
71
|
+
}
|
|
72
|
+
function loadFbq(win, doc) {
|
|
73
|
+
const w = win;
|
|
74
|
+
if (w.fbq)
|
|
75
|
+
return w.fbq;
|
|
76
|
+
const n = function (...args) { if (n.callMethod)
|
|
77
|
+
n.callMethod(...args);
|
|
78
|
+
else
|
|
79
|
+
n.queue.push(args); };
|
|
80
|
+
w.fbq = n;
|
|
81
|
+
if (!w._fbq)
|
|
82
|
+
w._fbq = n;
|
|
83
|
+
n.push = n;
|
|
84
|
+
n.loaded = true;
|
|
85
|
+
n.version = '2.0';
|
|
86
|
+
n.queue = [];
|
|
87
|
+
n.disablePushState = true;
|
|
88
|
+
try {
|
|
89
|
+
const t = doc.createElement('script');
|
|
90
|
+
t.async = true;
|
|
91
|
+
t.src = SRC;
|
|
92
|
+
const s = doc.getElementsByTagName('script')[0];
|
|
93
|
+
if (s?.parentNode)
|
|
94
|
+
s.parentNode.insertBefore(t, s);
|
|
95
|
+
else
|
|
96
|
+
doc.head.appendChild(t);
|
|
97
|
+
}
|
|
98
|
+
catch { /* a page without a DOM to add to: the queue waits */ }
|
|
99
|
+
return n;
|
|
100
|
+
}
|
|
101
|
+
function once(key, session) {
|
|
102
|
+
try {
|
|
103
|
+
if (sessionStorage.getItem(key) === session)
|
|
104
|
+
return false;
|
|
105
|
+
sessionStorage.setItem(key, session);
|
|
106
|
+
}
|
|
107
|
+
catch { /* storage blocked: memory */
|
|
108
|
+
if (mem.get(key) === session)
|
|
109
|
+
return false;
|
|
110
|
+
mem.set(key, session);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const mem = new Map();
|
|
115
|
+
// The Meta cookies, before the landing is read (mount): the server gets them with every event as the landing params fbp and fbc.
|
|
116
|
+
export function metaCookies(doc = document, loc = location, now = Date.now()) {
|
|
117
|
+
ensureFbp(doc, loc, now);
|
|
118
|
+
ensureFbc(doc, loc, now);
|
|
119
|
+
}
|
|
120
|
+
export function installPixel(cfg, d) {
|
|
121
|
+
const win = d.win ?? window, doc = d.doc ?? document;
|
|
122
|
+
void d.now;
|
|
123
|
+
// The router put the base pixel into the page (init and PageView done); a page without it (a router before it) loads it here.
|
|
124
|
+
const had = !!win.fbq;
|
|
125
|
+
const fbq = loadFbq(win, doc);
|
|
126
|
+
if (!had) {
|
|
127
|
+
fbq('set', 'autoConfig', false, cfg.id);
|
|
128
|
+
fbq('init', cfg.id, { external_id: d.visitorId });
|
|
129
|
+
fbq('track', 'PageView');
|
|
130
|
+
}
|
|
131
|
+
const paywall = new Set(d.steps.filter(s => s.role === 'paywall').map(s => s.id));
|
|
132
|
+
const fire = (name) => {
|
|
133
|
+
const session = d.session();
|
|
134
|
+
if (d.stopped() || !session || !once(`af_fb:${d.funnel}:${name}`, session))
|
|
135
|
+
return;
|
|
136
|
+
fbq('track', name, {}, { eventID: `${name.toLowerCase()}.${session}` });
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
after(name, stepId) {
|
|
140
|
+
const lead = cfg.lead_step ? name === '$step_complete' && stepId === cfg.lead_step : name === '$lead';
|
|
141
|
+
if (lead)
|
|
142
|
+
fire('Lead');
|
|
143
|
+
const checkout = name === '$step_view' && (cfg.checkout_step ? stepId === cfg.checkout_step : paywall.has(stepId));
|
|
144
|
+
if (checkout)
|
|
145
|
+
fire('InitiateCheckout');
|
|
146
|
+
},
|
|
147
|
+
purchased(p) {
|
|
148
|
+
const session = d.session();
|
|
149
|
+
if (d.stopped() || !session || typeof p?.value !== 'number' || !Number.isFinite(p.value) || p.value < 0 || !/^[A-Za-z]{3}$/.test(p?.currency ?? ''))
|
|
150
|
+
return;
|
|
151
|
+
const currency = p.currency.toUpperCase();
|
|
152
|
+
const names = p.trial ? ['StartTrial'] : p.subscription === false ? ['Purchase'] : ['Subscribe', 'Purchase'];
|
|
153
|
+
for (const n of names) {
|
|
154
|
+
if (!once(`af_fb:${d.funnel}:${n}`, session))
|
|
155
|
+
continue;
|
|
156
|
+
fbq('track', n, { value: p.trial ? 0 : p.value, currency }, { eventID: `${n.toLowerCase()}.${session}` });
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
let active = null;
|
|
162
|
+
export function setPixel(p) { active = p; }
|
|
163
|
+
// stopTracking(): Meta gets nothing more from this page (the router's pixel was loaded before the stop).
|
|
164
|
+
export function revokePixel(win = window) {
|
|
165
|
+
try {
|
|
166
|
+
win.fbq?.('consent', 'revoke');
|
|
167
|
+
}
|
|
168
|
+
catch { /* no pixel */ }
|
|
169
|
+
}
|
|
170
|
+
// purchased({ value, currency, trial?, subscription? }): call it once the payment went through in the browser (after Stripe's
|
|
171
|
+
// confirm). The pixel gets the conversion with the event_id the server gives the same money from the webhook, so Meta counts it
|
|
172
|
+
// once; the money itself is still counted only from the webhook. Without Meta switched on for the project it does nothing.
|
|
173
|
+
export function purchased(p) { active?.purchased(p); }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Prerender of the landing step (kit 0.11.0, `funnel-kit build` with `prerender: true` in defineFunnel): the first step draws
|
|
3
|
+
// into index.html with react-dom/server, so the first screen is there without JavaScript, and mount() hydrates it.
|
|
4
|
+
import { renderToString } from 'react-dom/server';
|
|
5
|
+
import { FunnelCtx, landingContext } from './funnel.js';
|
|
6
|
+
import { setContent } from './content.js';
|
|
7
|
+
// The HTML of the funnel's first step as an anonymous first visit sees it, with every t() text at its default (the build has no
|
|
8
|
+
// bootstrap; a text edited in the admin or an A/B text arm shows after hydration). The first step is the landing.
|
|
9
|
+
export function renderLanding(funnel) {
|
|
10
|
+
const step = funnel.steps[0];
|
|
11
|
+
if (!step)
|
|
12
|
+
throw new Error('the funnel has no steps');
|
|
13
|
+
setContent({}, {});
|
|
14
|
+
const C = step.component;
|
|
15
|
+
return renderToString(_jsx(FunnelCtx.Provider, { value: landingContext(funnel), children: _jsx(C, {}) }));
|
|
16
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const visit: {
|
|
2
|
+
step: string;
|
|
3
|
+
session: () => string;
|
|
4
|
+
off: () => boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare class ServerFnError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly body: unknown;
|
|
10
|
+
constructor(status: number, code: string, body?: unknown);
|
|
11
|
+
}
|
|
12
|
+
export declare function server<T = unknown>(name: string, payload?: unknown): Promise<T>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readBootstrap } from './bootstrap.js';
|
|
2
|
+
// The router's own check (spec §2); repeated here so a typo fails before a request instead of as a 404 from the edge.
|
|
3
|
+
const NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
4
|
+
// The shapes the router accepts for X-Step and X-Session (spec "all events" §2.3); anything else is not sent.
|
|
5
|
+
const STEP = /^[a-z0-9_-]{1,64}$/;
|
|
6
|
+
const SESSION = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
7
|
+
// The visit a call belongs to: the step on screen (the funnel sets it while it renders the step) and the tracker's session
|
|
8
|
+
// (mount() sets it). server() sends both, so the router's $fn_call and the function's ctx.track events join the person's
|
|
9
|
+
// journey. Neither is a secret. Outside a mounted funnel, and in a thumbnail, both stay empty and nothing is sent.
|
|
10
|
+
// off: stopTracking() was called; then neither is sent, so the router records no $fn_call for this visit.
|
|
11
|
+
export const visit = { step: '', session: () => '', off: () => false };
|
|
12
|
+
function visitHeaders() {
|
|
13
|
+
const out = {};
|
|
14
|
+
let off = false;
|
|
15
|
+
try {
|
|
16
|
+
off = visit.off();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
off = true;
|
|
20
|
+
}
|
|
21
|
+
if (off)
|
|
22
|
+
return out;
|
|
23
|
+
if (STEP.test(visit.step))
|
|
24
|
+
out['X-Step'] = visit.step;
|
|
25
|
+
let session = '';
|
|
26
|
+
try {
|
|
27
|
+
session = visit.session();
|
|
28
|
+
}
|
|
29
|
+
catch { /* storage blocked: the call goes without it */ }
|
|
30
|
+
if (SESSION.test(session))
|
|
31
|
+
out['X-Session'] = session;
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
// status 0 means the request never got an HTTP answer: 'bad_name' (not sent) or 'network' (fetch itself failed).
|
|
35
|
+
export class ServerFnError extends Error {
|
|
36
|
+
status;
|
|
37
|
+
code;
|
|
38
|
+
body;
|
|
39
|
+
constructor(status, code, body = null) {
|
|
40
|
+
super(`server function failed: ${code}${status ? ` (${status})` : ''}`);
|
|
41
|
+
this.name = 'ServerFnError';
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.body = body;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Calls functions/index.ts of the project through the router: same origin, so the funnel's HttpOnly cookies (Path=/<project>/, or
|
|
48
|
+
// Path=/ on an own domain, whose bootstrap gives fn_base "/_fn/") go along and no CORS is involved. The bootstrap is read per call: the router injects it before the bundle runs, and tests
|
|
49
|
+
// swap it between calls.
|
|
50
|
+
export async function server(name, payload) {
|
|
51
|
+
if (!NAME.test(name))
|
|
52
|
+
throw new ServerFnError(0, 'bad_name');
|
|
53
|
+
const boot = readBootstrap();
|
|
54
|
+
const headers = { 'Content-Type': 'application/json', 'X-Funnel': boot.funnel, ...visitHeaders() };
|
|
55
|
+
// Only a preview page has it; the router then runs the previewed build's functions instead of the published route's.
|
|
56
|
+
if (boot.fn_preview)
|
|
57
|
+
headers['X-Funnel-Preview'] = boot.fn_preview;
|
|
58
|
+
if (boot.fn_preview && boot.fn_arm)
|
|
59
|
+
headers['X-Funnel-Arm'] = boot.fn_arm;
|
|
60
|
+
let r, text;
|
|
61
|
+
try {
|
|
62
|
+
r = await fetch(`${boot.fn_base ?? `/${boot.project}/_fn/`}${name}`, { method: 'POST', credentials: 'same-origin', headers, body: JSON.stringify(payload ?? {}) });
|
|
63
|
+
text = await r.text();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new ServerFnError(0, 'network');
|
|
67
|
+
}
|
|
68
|
+
let body = null;
|
|
69
|
+
let parsed = !text; // an empty body is a null result
|
|
70
|
+
if (text) {
|
|
71
|
+
try {
|
|
72
|
+
body = JSON.parse(text);
|
|
73
|
+
parsed = true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
body = text;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (r.ok) {
|
|
80
|
+
// A 2xx is the router's JSON by contract (a function may return a plain string, which arrives JSON-quoted); a body that is
|
|
81
|
+
// not JSON at all (a proxy page, say) is not a result the caller can use.
|
|
82
|
+
if (!parsed)
|
|
83
|
+
throw new ServerFnError(r.status, 'bad_json', body);
|
|
84
|
+
return body;
|
|
85
|
+
}
|
|
86
|
+
const code = body?.error?.code;
|
|
87
|
+
throw new ServerFnError(r.status, typeof code === 'string' && code ? code : `http_${r.status}`, body);
|
|
88
|
+
}
|
package/dist/sha256.d.ts
ADDED
package/dist/sha256.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SHA-256 of a string's UTF-8 bytes, as lower-case hex. Synchronous and dependency-free on purpose: identify() hashes an email
|
|
2
|
+
// in the middle of a click handler, and crypto.subtle is async and missing outside secure contexts (a funnel opened over http in
|
|
3
|
+
// development, some in-app browsers).
|
|
4
|
+
const K = new Uint32Array([
|
|
5
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
6
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
7
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
8
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
9
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
10
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
11
|
+
]);
|
|
12
|
+
export function sha256Hex(text) {
|
|
13
|
+
const msg = new TextEncoder().encode(text);
|
|
14
|
+
const len = msg.length;
|
|
15
|
+
const total = (((len + 8) >> 6) + 1) << 6;
|
|
16
|
+
const buf = new Uint8Array(total);
|
|
17
|
+
buf.set(msg);
|
|
18
|
+
buf[len] = 0x80;
|
|
19
|
+
const view = new DataView(buf.buffer);
|
|
20
|
+
view.setUint32(total - 8, Math.floor((len * 8) / 0x100000000));
|
|
21
|
+
view.setUint32(total - 4, (len * 8) >>> 0);
|
|
22
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
23
|
+
const w = new Uint32Array(64);
|
|
24
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
25
|
+
for (let off = 0; off < total; off += 64) {
|
|
26
|
+
for (let i = 0; i < 16; i++)
|
|
27
|
+
w[i] = view.getUint32(off + i * 4);
|
|
28
|
+
for (let i = 16; i < 64; i++) {
|
|
29
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
30
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
31
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
|
|
32
|
+
}
|
|
33
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
34
|
+
for (let i = 0; i < 64; i++) {
|
|
35
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
36
|
+
const ch = (e & f) ^ (~e & g);
|
|
37
|
+
const t1 = (hh + S1 + ch + K[i] + w[i]) | 0;
|
|
38
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
39
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
40
|
+
const t2 = (S0 + maj) | 0;
|
|
41
|
+
hh = g;
|
|
42
|
+
g = f;
|
|
43
|
+
f = e;
|
|
44
|
+
e = (d + t1) | 0;
|
|
45
|
+
d = c;
|
|
46
|
+
c = b;
|
|
47
|
+
b = a;
|
|
48
|
+
a = (t1 + t2) | 0;
|
|
49
|
+
}
|
|
50
|
+
h[0] += a;
|
|
51
|
+
h[1] += b;
|
|
52
|
+
h[2] += c;
|
|
53
|
+
h[3] += d;
|
|
54
|
+
h[4] += e;
|
|
55
|
+
h[5] += f;
|
|
56
|
+
h[6] += g;
|
|
57
|
+
h[7] += hh;
|
|
58
|
+
}
|
|
59
|
+
return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
|
|
60
|
+
}
|
|
61
|
+
// The hash identify() sends instead of an email: SHA-256 of the lower-cased, trimmed address. '' for something that is no address.
|
|
62
|
+
export function emailHash(email) {
|
|
63
|
+
const e = String(email ?? '').trim().toLowerCase();
|
|
64
|
+
return /^[^@\s]+@[^@\s]+$/.test(e) ? sha256Hex(e) : '';
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type SignalOptions = {
|
|
2
|
+
track: (name: string, props: Record<string, unknown>, stepId: string) => void;
|
|
3
|
+
stepId: () => string;
|
|
4
|
+
scroll: boolean;
|
|
5
|
+
sections: boolean;
|
|
6
|
+
fieldErrors: boolean;
|
|
7
|
+
activeTime: boolean;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
};
|
|
10
|
+
export type StepSignals = {
|
|
11
|
+
newView: () => void;
|
|
12
|
+
activeMs: () => number | undefined;
|
|
13
|
+
off: () => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function sectionName(raw: string | null): string;
|
|
16
|
+
export declare function fieldKey(el: Element): string;
|
|
17
|
+
export declare function errorCode(el: Element): string;
|
|
18
|
+
export declare function installStepSignals(root: HTMLElement, o: SignalOptions): StepSignals;
|