@flamingo-stack/openframe-frontend-core 0.0.671 → 0.0.672-snapshot.20260928034232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-protocol/card-marker.d.ts +12 -0
- package/dist/chat-protocol/card-marker.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +17 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.d.ts +1 -0
- package/dist/chat-protocol/index.d.ts.map +1 -1
- package/dist/chat-protocol/index.js +15 -1
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/{chunk-DAGKBFZN.cjs → chunk-26NSIJMT.cjs} +33 -33
- package/dist/{chunk-DAGKBFZN.cjs.map → chunk-26NSIJMT.cjs.map} +1 -1
- package/dist/{chunk-FUXUJSPP.cjs → chunk-3UDBK2YG.cjs} +1 -1
- package/dist/chunk-3UDBK2YG.cjs.map +1 -0
- package/dist/{chunk-XGXHHSDU.cjs → chunk-6QUKYN4K.cjs} +22 -22
- package/dist/{chunk-XGXHHSDU.cjs.map → chunk-6QUKYN4K.cjs.map} +1 -1
- package/dist/{chunk-SNNE34OP.js → chunk-BF7GZ6BS.js} +5 -5
- package/dist/{chunk-SJZPKIW6.cjs → chunk-DRURJCEY.cjs} +12 -12
- package/dist/{chunk-SJZPKIW6.cjs.map → chunk-DRURJCEY.cjs.map} +1 -1
- package/dist/{chunk-YIDNSRPL.cjs → chunk-G262S3Q5.cjs} +31 -31
- package/dist/{chunk-YIDNSRPL.cjs.map → chunk-G262S3Q5.cjs.map} +1 -1
- package/dist/{chunk-OG2BNJ2Q.cjs → chunk-HJ532KQR.cjs} +390 -375
- package/dist/chunk-HJ532KQR.cjs.map +1 -0
- package/dist/{chunk-K3O4D7LW.js → chunk-HRM6242Y.js} +4 -4
- package/dist/{chunk-BP542DGQ.cjs → chunk-IFQMIYFO.cjs} +55 -28
- package/dist/chunk-IFQMIYFO.cjs.map +1 -0
- package/dist/{chunk-IJIIL6WU.js → chunk-IG7M3XKX.js} +6 -6
- package/dist/{chunk-PQ7YC5Y6.js → chunk-II6ABVLK.js} +3 -3
- package/dist/{chunk-TLLTL6LB.js → chunk-JD4U7QMG.js} +1 -1
- package/dist/chunk-JD4U7QMG.js.map +1 -0
- package/dist/{chunk-ZRIVCLPR.js → chunk-JD5TKRCU.js} +1 -1
- package/dist/chunk-JD5TKRCU.js.map +1 -0
- package/dist/{chunk-XH4MRTZM.cjs → chunk-K365MY6G.cjs} +5 -5
- package/dist/{chunk-XH4MRTZM.cjs.map → chunk-K365MY6G.cjs.map} +1 -1
- package/dist/{chunk-EIAVE6SE.cjs → chunk-KV7W5T6U.cjs} +10 -10
- package/dist/{chunk-EIAVE6SE.cjs.map → chunk-KV7W5T6U.cjs.map} +1 -1
- package/dist/{chunk-II66XWU5.cjs → chunk-KYF5EONP.cjs} +3 -3
- package/dist/{chunk-II66XWU5.cjs.map → chunk-KYF5EONP.cjs.map} +1 -1
- package/dist/{chunk-D55IENZJ.cjs → chunk-LCMT2GZK.cjs} +1 -1
- package/dist/chunk-LCMT2GZK.cjs.map +1 -0
- package/dist/{chunk-STLBB6PF.js → chunk-LMUZZUYC.js} +35 -8
- package/dist/chunk-LMUZZUYC.js.map +1 -0
- package/dist/{chunk-FAJUQKWQ.cjs → chunk-N6TC6IPV.cjs} +76 -75
- package/dist/chunk-N6TC6IPV.cjs.map +1 -0
- package/dist/{chunk-RKUCK6MN.js → chunk-OGOMAK7B.js} +2 -2
- package/dist/{chunk-G64BNLDO.js → chunk-P4GUIOLP.js} +2 -2
- package/dist/{chunk-DUZEFACP.js → chunk-RRU46J3N.js} +3 -3
- package/dist/{chunk-DAWYS25M.cjs → chunk-THSIFQGS.cjs} +17 -17
- package/dist/{chunk-DAWYS25M.cjs.map → chunk-THSIFQGS.cjs.map} +1 -1
- package/dist/{chunk-6C6UUOH4.js → chunk-TTHBFT7Q.js} +8 -7
- package/dist/{chunk-6C6UUOH4.js.map → chunk-TTHBFT7Q.js.map} +1 -1
- package/dist/{chunk-YNGAJGYB.cjs → chunk-TZP2OXHG.cjs} +346 -6
- package/dist/chunk-TZP2OXHG.cjs.map +1 -0
- package/dist/{chunk-T3ACDOVD.cjs → chunk-U2H3C4NT.cjs} +7 -7
- package/dist/{chunk-T3ACDOVD.cjs.map → chunk-U2H3C4NT.cjs.map} +1 -1
- package/dist/{chunk-OYMOG544.js → chunk-UFMXQKVT.js} +5 -5
- package/dist/{chunk-NIYIK32F.js → chunk-UMTXGEPU.js} +19 -4
- package/dist/{chunk-NIYIK32F.js.map → chunk-UMTXGEPU.js.map} +1 -1
- package/dist/{chunk-3PVOF3IB.js → chunk-UTHZ7DLN.js} +343 -3
- package/dist/{chunk-3PVOF3IB.js.map → chunk-UTHZ7DLN.js.map} +1 -1
- package/dist/{chunk-YJWR7D6O.cjs → chunk-UXT5EK27.cjs} +3 -3
- package/dist/{chunk-YJWR7D6O.cjs.map → chunk-UXT5EK27.cjs.map} +1 -1
- package/dist/{chunk-UH3BL2OT.js → chunk-VK5737GE.js} +2 -2
- package/dist/{chunk-R22IA7MP.js → chunk-VKLM5UKG.js} +4 -4
- package/dist/{chunk-FHTNCWIP.js → chunk-X2DR3XNN.js} +5 -5
- package/dist/{chunk-GF5TSQ6C.cjs → chunk-YCYOZAB5.cjs} +10 -10
- package/dist/{chunk-GF5TSQ6C.cjs.map → chunk-YCYOZAB5.cjs.map} +1 -1
- package/dist/{chunk-4PVY4JJN.js → chunk-YWQVL5VJ.js} +3 -3
- package/dist/{chunk-PLG6LP7E.cjs → chunk-Z5K4IQZU.cjs} +49 -49
- package/dist/{chunk-PLG6LP7E.cjs.map → chunk-Z5K4IQZU.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +18 -17
- package/dist/components/case-studies/index.cjs.map +1 -1
- package/dist/components/case-studies/index.js +7 -6
- package/dist/components/case-studies/index.js.map +1 -1
- package/dist/components/case-studies/share-experience-section.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/contact-form.d.ts +6 -1
- package/dist/components/contact/contact-form.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +6 -6
- package/dist/components/contact/index.js +5 -5
- package/dist/components/docs/index.cjs +9 -9
- package/dist/components/docs/index.js +8 -8
- package/dist/components/embeds/index.cjs +6 -6
- package/dist/components/embeds/index.js +5 -5
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +5 -5
- package/dist/components/features/index.js +4 -4
- package/dist/components/features/waitlist-form.d.ts +7 -2
- package/dist/components/features/waitlist-form.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +77 -76
- package/dist/components/help-center-pages/index.cjs.map +1 -1
- package/dist/components/help-center-pages/index.js +17 -16
- package/dist/components/help-center-pages/index.js.map +1 -1
- package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
- package/dist/components/index.cjs +43 -43
- package/dist/components/index.js +15 -15
- package/dist/components/meeting-scheduler/booking-form.d.ts +4 -1
- package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +75 -40
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.d.ts +5 -1
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +46 -11
- package/dist/components/meeting-scheduler/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +8 -8
- package/dist/components/navigation/index.js +7 -7
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/tickets/help-center-create-form.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +9 -9
- package/dist/components/tickets/index.js +8 -8
- package/dist/components/ui/index.cjs +5 -5
- package/dist/components/ui/index.js +4 -4
- package/dist/contexts/endpoints-runtime-context.d.ts +5 -0
- package/dist/contexts/endpoints-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +5 -3
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +4 -2
- package/dist/hooks/use-form-rescue.d.ts +37 -0
- package/dist/hooks/use-form-rescue.d.ts.map +1 -0
- package/dist/index.cjs +53 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +52 -8
- package/dist/index.js.map +1 -1
- package/dist/utils/form-rescue.cjs +123 -0
- package/dist/utils/form-rescue.cjs.map +1 -0
- package/dist/utils/form-rescue.d.ts +105 -0
- package/dist/utils/form-rescue.d.ts.map +1 -0
- package/dist/utils/form-rescue.js +101 -0
- package/dist/utils/form-rescue.js.map +1 -0
- package/dist/utils/index.cjs +119 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +99 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +7 -1
- package/src/chat-protocol/__tests__/card-marker.test.ts +27 -1
- package/src/chat-protocol/card-marker.ts +24 -0
- package/src/chat-protocol/index.ts +3 -0
- package/src/components/case-studies/share-experience-section.tsx +1 -0
- package/src/components/contact/contact-form.tsx +38 -2
- package/src/components/features/waitlist-form.tsx +27 -2
- package/src/components/help-center-pages/trust-center-sections.tsx +1 -0
- package/src/components/meeting-scheduler/booking-form.tsx +14 -0
- package/src/components/meeting-scheduler/index.tsx +38 -3
- package/src/components/tickets/help-center-create-form.tsx +2 -0
- package/src/contexts/endpoints-runtime-context.tsx +5 -0
- package/src/hooks/__tests__/use-form-rescue.test.tsx +189 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-form-rescue.ts +315 -0
- package/src/utils/__tests__/form-rescue.test.ts +106 -0
- package/src/utils/form-rescue.ts +198 -0
- package/src/utils/index.ts +5 -0
- package/dist/chunk-BP542DGQ.cjs.map +0 -1
- package/dist/chunk-D55IENZJ.cjs.map +0 -1
- package/dist/chunk-FAJUQKWQ.cjs.map +0 -1
- package/dist/chunk-FUXUJSPP.cjs.map +0 -1
- package/dist/chunk-OG2BNJ2Q.cjs.map +0 -1
- package/dist/chunk-STLBB6PF.js.map +0 -1
- package/dist/chunk-TLLTL6LB.js.map +0 -1
- package/dist/chunk-YNGAJGYB.cjs.map +0 -1
- package/dist/chunk-ZRIVCLPR.js.map +0 -1
- /package/dist/{chunk-SNNE34OP.js.map → chunk-BF7GZ6BS.js.map} +0 -0
- /package/dist/{chunk-K3O4D7LW.js.map → chunk-HRM6242Y.js.map} +0 -0
- /package/dist/{chunk-IJIIL6WU.js.map → chunk-IG7M3XKX.js.map} +0 -0
- /package/dist/{chunk-PQ7YC5Y6.js.map → chunk-II6ABVLK.js.map} +0 -0
- /package/dist/{chunk-RKUCK6MN.js.map → chunk-OGOMAK7B.js.map} +0 -0
- /package/dist/{chunk-G64BNLDO.js.map → chunk-P4GUIOLP.js.map} +0 -0
- /package/dist/{chunk-DUZEFACP.js.map → chunk-RRU46J3N.js.map} +0 -0
- /package/dist/{chunk-OYMOG544.js.map → chunk-UFMXQKVT.js.map} +0 -0
- /package/dist/{chunk-UH3BL2OT.js.map → chunk-VK5737GE.js.map} +0 -0
- /package/dist/{chunk-R22IA7MP.js.map → chunk-VKLM5UKG.js.map} +0 -0
- /package/dist/{chunk-FHTNCWIP.js.map → chunk-X2DR3XNN.js.map} +0 -0
- /package/dist/{chunk-4PVY4JJN.js.map → chunk-YWQVL5VJ.js.map} +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/utils/form-rescue.ts
|
|
4
|
+
var FORM_RESCUE_FORM_IDS = [
|
|
5
|
+
"contact",
|
|
6
|
+
"case_study_pitch",
|
|
7
|
+
"data_room_request",
|
|
8
|
+
"trust_center_request",
|
|
9
|
+
"tmcg_join",
|
|
10
|
+
"meeting_booking",
|
|
11
|
+
"waitlist"
|
|
12
|
+
];
|
|
13
|
+
function isFormRescueFormId(value) {
|
|
14
|
+
return typeof value === "string" && FORM_RESCUE_FORM_IDS.includes(value);
|
|
15
|
+
}
|
|
16
|
+
var FORM_RESCUE_VALUE_FIELDS = [
|
|
17
|
+
"email",
|
|
18
|
+
"name",
|
|
19
|
+
"firstName",
|
|
20
|
+
"lastName",
|
|
21
|
+
"company",
|
|
22
|
+
"jobtitle",
|
|
23
|
+
"companySize",
|
|
24
|
+
"linkedin_url"
|
|
25
|
+
];
|
|
26
|
+
var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
|
|
27
|
+
function isExcludedRescueField(name) {
|
|
28
|
+
return EXCLUDED_FIELD_PATTERN.test(name);
|
|
29
|
+
}
|
|
30
|
+
var FORM_RESCUE_MAX_VALUE_CHARS = 200;
|
|
31
|
+
var FORM_RESCUE_DEBOUNCE_MS = 1500;
|
|
32
|
+
var FORM_RESCUE_MAX_FIELDS = 40;
|
|
33
|
+
var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
|
|
34
|
+
var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
|
|
35
|
+
var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
|
|
36
|
+
var FORM_RESCUE_RESUME_PARAM = "resume";
|
|
37
|
+
var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
|
|
38
|
+
var FORM_RESCUE_SKIP_REASONS = {
|
|
39
|
+
finishedElsewhere: "finished_elsewhere",
|
|
40
|
+
alreadyRescued: "already_rescued",
|
|
41
|
+
internal: "internal_email"
|
|
42
|
+
};
|
|
43
|
+
var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
|
44
|
+
function isRescueEmail(value) {
|
|
45
|
+
return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
|
|
46
|
+
}
|
|
47
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
48
|
+
function isFormAttemptId(value) {
|
|
49
|
+
return typeof value === "string" && UUID_PATTERN.test(value);
|
|
50
|
+
}
|
|
51
|
+
function isFilled(value) {
|
|
52
|
+
if (value == null) return false;
|
|
53
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
54
|
+
if (typeof value === "boolean") return value;
|
|
55
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
56
|
+
if (typeof value === "object") return Object.values(value).some(isFilled);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function sanitizeRescueValues(values) {
|
|
60
|
+
const out = {};
|
|
61
|
+
if (!values || typeof values !== "object") return out;
|
|
62
|
+
for (const key of FORM_RESCUE_VALUE_FIELDS) {
|
|
63
|
+
if (isExcludedRescueField(key)) continue;
|
|
64
|
+
const raw = values[key];
|
|
65
|
+
if (typeof raw !== "string") continue;
|
|
66
|
+
const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
|
|
67
|
+
if (trimmed) out[key] = trimmed;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function filledRescueFields(values, fieldNames) {
|
|
72
|
+
return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
|
|
73
|
+
}
|
|
74
|
+
function rescueCompletionPct(filledCount, totalCount) {
|
|
75
|
+
if (totalCount <= 0) return 0;
|
|
76
|
+
return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
|
|
77
|
+
}
|
|
78
|
+
function sanitizeRescueFieldName(value) {
|
|
79
|
+
if (typeof value !== "string") return null;
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
|
|
82
|
+
}
|
|
83
|
+
var FORM_RESCUE_EVENTS = {
|
|
84
|
+
started: "form_started",
|
|
85
|
+
progress: "form_progress",
|
|
86
|
+
leadCaptured: "form_lead_captured",
|
|
87
|
+
abandoned: "form_abandoned",
|
|
88
|
+
submitted: "form_submitted",
|
|
89
|
+
rescued: "form_rescued",
|
|
90
|
+
resumed: "form_resumed"
|
|
91
|
+
};
|
|
92
|
+
function captureFormRescueEvent(event, properties) {
|
|
93
|
+
try {
|
|
94
|
+
if (typeof window === "undefined") return;
|
|
95
|
+
const ph = window.posthog;
|
|
96
|
+
if (typeof ph?.capture === "function") ph.capture(event, properties);
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
exports.FORM_DRAFT_STATUSES = FORM_DRAFT_STATUSES;
|
|
102
|
+
exports.FORM_RESCUE_ATTEMPT_FIELD = FORM_RESCUE_ATTEMPT_FIELD;
|
|
103
|
+
exports.FORM_RESCUE_DEBOUNCE_MS = FORM_RESCUE_DEBOUNCE_MS;
|
|
104
|
+
exports.FORM_RESCUE_EVENTS = FORM_RESCUE_EVENTS;
|
|
105
|
+
exports.FORM_RESCUE_FORM_IDS = FORM_RESCUE_FORM_IDS;
|
|
106
|
+
exports.FORM_RESCUE_MAX_FIELDS = FORM_RESCUE_MAX_FIELDS;
|
|
107
|
+
exports.FORM_RESCUE_MAX_VALUE_CHARS = FORM_RESCUE_MAX_VALUE_CHARS;
|
|
108
|
+
exports.FORM_RESCUE_RESUME_FIELD = FORM_RESCUE_RESUME_FIELD;
|
|
109
|
+
exports.FORM_RESCUE_RESUME_PARAM = FORM_RESCUE_RESUME_PARAM;
|
|
110
|
+
exports.FORM_RESCUE_SKIP_REASONS = FORM_RESCUE_SKIP_REASONS;
|
|
111
|
+
exports.FORM_RESCUE_SUBMIT_KEYS = FORM_RESCUE_SUBMIT_KEYS;
|
|
112
|
+
exports.FORM_RESCUE_VALUE_FIELDS = FORM_RESCUE_VALUE_FIELDS;
|
|
113
|
+
exports.captureFormRescueEvent = captureFormRescueEvent;
|
|
114
|
+
exports.filledRescueFields = filledRescueFields;
|
|
115
|
+
exports.isExcludedRescueField = isExcludedRescueField;
|
|
116
|
+
exports.isFormAttemptId = isFormAttemptId;
|
|
117
|
+
exports.isFormRescueFormId = isFormRescueFormId;
|
|
118
|
+
exports.isRescueEmail = isRescueEmail;
|
|
119
|
+
exports.rescueCompletionPct = rescueCompletionPct;
|
|
120
|
+
exports.sanitizeRescueFieldName = sanitizeRescueFieldName;
|
|
121
|
+
exports.sanitizeRescueValues = sanitizeRescueValues;
|
|
122
|
+
//# sourceMappingURL=form-rescue.cjs.map
|
|
123
|
+
//# sourceMappingURL=form-rescue.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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","file":"form-rescue.cjs","sourcesContent":["/**\n * Form rescue — the shared rules for saving a half-filled public form so the\n * team can follow up with a visitor who left before submitting.\n *\n * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the\n * host's draft endpoint import the SAME allowlist, exclusions, caps and\n * completion math, so the server re-applies exactly the filter the client ran\n * and a rename here reaches both sides at once. Also exported through the\n * granular subpath `./utils/form-rescue` for server-only consumers.\n *\n * What a draft may carry:\n * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),\n * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;\n * - for every other field, only whether it is filled (never its value);\n * - nothing whose name looks like a credential or payment field\n * (`isExcludedRescueField`), whatever the allowlist says.\n *\n * Field values go to the host only, never to analytics: the PostHog events\n * (`captureFormRescueEvent`) carry ids, field names and percentages.\n */\n\n/** Every public form that can be rescued. The host validates `form_id` against this list. */\nexport const FORM_RESCUE_FORM_IDS = [\n 'contact',\n 'case_study_pitch',\n 'data_room_request',\n 'trust_center_request',\n 'tmcg_join',\n 'meeting_booking',\n 'waitlist',\n] as const;\n\nexport type FormRescueFormId = (typeof FORM_RESCUE_FORM_IDS)[number];\n\nexport function isFormRescueFormId(value: unknown): value is FormRescueFormId {\n return typeof value === 'string' && (FORM_RESCUE_FORM_IDS as readonly string[]).includes(value);\n}\n\n/**\n * Fields whose VALUES a draft keeps: who the visitor is and where they work.\n * Covers both naming schemes in use (the contact form's `name` / `companySize`,\n * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).\n * Free text (`message`) and phone numbers are reported as filled only.\n */\nexport const FORM_RESCUE_VALUE_FIELDS = [\n 'email',\n 'name',\n 'firstName',\n 'lastName',\n 'company',\n 'jobtitle',\n 'companySize',\n 'linkedin_url',\n] as const;\n\n/** Never send or store these, whatever the allowlist says. */\nconst EXCLUDED_FIELD_PATTERN =\n /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;\n\nexport function isExcludedRescueField(name: string): boolean {\n return EXCLUDED_FIELD_PATTERN.test(name);\n}\n\n/** Longest value a draft keeps per field. */\nexport const FORM_RESCUE_MAX_VALUE_CHARS = 200;\n/** Save this long after the visitor stops typing. */\nexport const FORM_RESCUE_DEBOUNCE_MS = 1500;\n/** Most fields a draft reports (filled names), a bound on the payload. */\nexport const FORM_RESCUE_MAX_FIELDS = 40;\n\n/** Body keys a form's SUBMIT carries so the host can close the matching draft. */\nexport const FORM_RESCUE_ATTEMPT_FIELD = 'form_attempt_id';\nexport const FORM_RESCUE_RESUME_FIELD = 'form_resume_token';\n/** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */\nexport const FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD] as const;\n\n/** Query parameter a resume link carries. */\nexport const FORM_RESCUE_RESUME_PARAM = 'resume';\n\n/** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */\nexport const FORM_DRAFT_STATUSES = ['started', 'rescuable', 'rescued', 'submitted'] as const;\nexport type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];\n\n/** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */\nexport const FORM_RESCUE_SKIP_REASONS = {\n finishedElsewhere: 'finished_elsewhere',\n alreadyRescued: 'already_rescued',\n internal: 'internal_email',\n} as const;\nexport type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];\n\n/** Same shape the host's `contact_submissions.email` check accepts. */\nconst EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/;\n\nexport function isRescueEmail(value: unknown): value is string {\n return typeof value === 'string' && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isFormAttemptId(value: unknown): value is string {\n return typeof value === 'string' && UUID_PATTERN.test(value);\n}\n\nfunction isFilled(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.trim().length > 0;\n if (typeof value === 'boolean') return value;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.values(value as Record<string, unknown>).some(isFilled);\n return true;\n}\n\n/**\n * Keep only allowlisted, non-excluded string values, trimmed and capped.\n * The ONE filter, run by the browser before sending and by the host before storing.\n */\nexport function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!values || typeof values !== 'object') return out;\n for (const key of FORM_RESCUE_VALUE_FIELDS) {\n if (isExcludedRescueField(key)) continue;\n const raw = values[key];\n if (typeof raw !== 'string') continue;\n const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);\n if (trimmed) out[key] = trimmed;\n }\n return out;\n}\n\n/** Names of the given fields that hold something, credentials never included. */\nexport function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[] {\n return fieldNames\n .filter(name => !isExcludedRescueField(name) && isFilled(values[name]))\n .slice(0, FORM_RESCUE_MAX_FIELDS);\n}\n\nexport function rescueCompletionPct(filledCount: number, totalCount: number): number {\n if (totalCount <= 0) return 0;\n return Math.max(0, Math.min(100, Math.round((filledCount / totalCount) * 100)));\n}\n\n/** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */\nexport function sanitizeRescueFieldName(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;\n}\n\n/** A form's progress at one moment: allowlisted values, filled field names, position. */\nexport interface FormDraftProgress {\n values: Record<string, string>;\n fields_filled: string[];\n last_field: string | null;\n completion_pct: number;\n}\n\n/** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */\nexport interface FormDraftSaveRequest extends FormDraftProgress {\n /** The attempt's idempotency key: every save of one attempt updates one draft. */\n attempt_id: string;\n form_id: FormRescueFormId;\n source_path: string;\n utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;\n}\n\n/** What the host answers a resume link with: the allowlisted values, nothing internal. */\nexport interface FormDraftResumeResponse {\n form_id: FormRescueFormId;\n values: Record<string, string>;\n}\n\n/** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */\nexport const FORM_RESCUE_EVENTS = {\n started: 'form_started',\n progress: 'form_progress',\n leadCaptured: 'form_lead_captured',\n abandoned: 'form_abandoned',\n submitted: 'form_submitted',\n rescued: 'form_rescued',\n resumed: 'form_resumed',\n} as const;\n\ntype AnalyticsValue = string | number | boolean | string[] | null;\n\n/**\n * Send one form event to PostHog when the page loaded it (GTM exposes\n * `window.posthog`); a no-op otherwise. Never throws.\n */\nexport function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void {\n try {\n if (typeof window === 'undefined') return;\n const ph = (window as unknown as { posthog?: { capture?: (e: string, p: object) => void } }).posthog;\n if (typeof ph?.capture === 'function') ph.capture(event, properties);\n } catch {\n // Analytics never breaks a form.\n }\n}\n"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form rescue — the shared rules for saving a half-filled public form so the
|
|
3
|
+
* team can follow up with a visitor who left before submitting.
|
|
4
|
+
*
|
|
5
|
+
* Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the
|
|
6
|
+
* host's draft endpoint import the SAME allowlist, exclusions, caps and
|
|
7
|
+
* completion math, so the server re-applies exactly the filter the client ran
|
|
8
|
+
* and a rename here reaches both sides at once. Also exported through the
|
|
9
|
+
* granular subpath `./utils/form-rescue` for server-only consumers.
|
|
10
|
+
*
|
|
11
|
+
* What a draft may carry:
|
|
12
|
+
* - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),
|
|
13
|
+
* trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;
|
|
14
|
+
* - for every other field, only whether it is filled (never its value);
|
|
15
|
+
* - nothing whose name looks like a credential or payment field
|
|
16
|
+
* (`isExcludedRescueField`), whatever the allowlist says.
|
|
17
|
+
*
|
|
18
|
+
* Field values go to the host only, never to analytics: the PostHog events
|
|
19
|
+
* (`captureFormRescueEvent`) carry ids, field names and percentages.
|
|
20
|
+
*/
|
|
21
|
+
/** Every public form that can be rescued. The host validates `form_id` against this list. */
|
|
22
|
+
export declare const FORM_RESCUE_FORM_IDS: readonly ["contact", "case_study_pitch", "data_room_request", "trust_center_request", "tmcg_join", "meeting_booking", "waitlist"];
|
|
23
|
+
export type FormRescueFormId = (typeof FORM_RESCUE_FORM_IDS)[number];
|
|
24
|
+
export declare function isFormRescueFormId(value: unknown): value is FormRescueFormId;
|
|
25
|
+
/**
|
|
26
|
+
* Fields whose VALUES a draft keeps: who the visitor is and where they work.
|
|
27
|
+
* Covers both naming schemes in use (the contact form's `name` / `companySize`,
|
|
28
|
+
* the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).
|
|
29
|
+
* Free text (`message`) and phone numbers are reported as filled only.
|
|
30
|
+
*/
|
|
31
|
+
export declare const FORM_RESCUE_VALUE_FIELDS: readonly ["email", "name", "firstName", "lastName", "company", "jobtitle", "companySize", "linkedin_url"];
|
|
32
|
+
export declare function isExcludedRescueField(name: string): boolean;
|
|
33
|
+
/** Longest value a draft keeps per field. */
|
|
34
|
+
export declare const FORM_RESCUE_MAX_VALUE_CHARS = 200;
|
|
35
|
+
/** Save this long after the visitor stops typing. */
|
|
36
|
+
export declare const FORM_RESCUE_DEBOUNCE_MS = 1500;
|
|
37
|
+
/** Most fields a draft reports (filled names), a bound on the payload. */
|
|
38
|
+
export declare const FORM_RESCUE_MAX_FIELDS = 40;
|
|
39
|
+
/** Body keys a form's SUBMIT carries so the host can close the matching draft. */
|
|
40
|
+
export declare const FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
|
|
41
|
+
export declare const FORM_RESCUE_RESUME_FIELD = "form_resume_token";
|
|
42
|
+
/** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */
|
|
43
|
+
export declare const FORM_RESCUE_SUBMIT_KEYS: readonly ["form_attempt_id", "form_resume_token"];
|
|
44
|
+
/** Query parameter a resume link carries. */
|
|
45
|
+
export declare const FORM_RESCUE_RESUME_PARAM = "resume";
|
|
46
|
+
/** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */
|
|
47
|
+
export declare const FORM_DRAFT_STATUSES: readonly ["started", "rescuable", "rescued", "submitted"];
|
|
48
|
+
export type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];
|
|
49
|
+
/** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */
|
|
50
|
+
export declare const FORM_RESCUE_SKIP_REASONS: {
|
|
51
|
+
readonly finishedElsewhere: "finished_elsewhere";
|
|
52
|
+
readonly alreadyRescued: "already_rescued";
|
|
53
|
+
readonly internal: "internal_email";
|
|
54
|
+
};
|
|
55
|
+
export type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];
|
|
56
|
+
export declare function isRescueEmail(value: unknown): value is string;
|
|
57
|
+
export declare function isFormAttemptId(value: unknown): value is string;
|
|
58
|
+
/**
|
|
59
|
+
* Keep only allowlisted, non-excluded string values, trimmed and capped.
|
|
60
|
+
* The ONE filter, run by the browser before sending and by the host before storing.
|
|
61
|
+
*/
|
|
62
|
+
export declare function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string>;
|
|
63
|
+
/** Names of the given fields that hold something, credentials never included. */
|
|
64
|
+
export declare function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[];
|
|
65
|
+
export declare function rescueCompletionPct(filledCount: number, totalCount: number): number;
|
|
66
|
+
/** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */
|
|
67
|
+
export declare function sanitizeRescueFieldName(value: unknown): string | null;
|
|
68
|
+
/** A form's progress at one moment: allowlisted values, filled field names, position. */
|
|
69
|
+
export interface FormDraftProgress {
|
|
70
|
+
values: Record<string, string>;
|
|
71
|
+
fields_filled: string[];
|
|
72
|
+
last_field: string | null;
|
|
73
|
+
completion_pct: number;
|
|
74
|
+
}
|
|
75
|
+
/** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */
|
|
76
|
+
export interface FormDraftSaveRequest extends FormDraftProgress {
|
|
77
|
+
/** The attempt's idempotency key: every save of one attempt updates one draft. */
|
|
78
|
+
attempt_id: string;
|
|
79
|
+
form_id: FormRescueFormId;
|
|
80
|
+
source_path: string;
|
|
81
|
+
utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;
|
|
82
|
+
}
|
|
83
|
+
/** What the host answers a resume link with: the allowlisted values, nothing internal. */
|
|
84
|
+
export interface FormDraftResumeResponse {
|
|
85
|
+
form_id: FormRescueFormId;
|
|
86
|
+
values: Record<string, string>;
|
|
87
|
+
}
|
|
88
|
+
/** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */
|
|
89
|
+
export declare const FORM_RESCUE_EVENTS: {
|
|
90
|
+
readonly started: "form_started";
|
|
91
|
+
readonly progress: "form_progress";
|
|
92
|
+
readonly leadCaptured: "form_lead_captured";
|
|
93
|
+
readonly abandoned: "form_abandoned";
|
|
94
|
+
readonly submitted: "form_submitted";
|
|
95
|
+
readonly rescued: "form_rescued";
|
|
96
|
+
readonly resumed: "form_resumed";
|
|
97
|
+
};
|
|
98
|
+
type AnalyticsValue = string | number | boolean | string[] | null;
|
|
99
|
+
/**
|
|
100
|
+
* Send one form event to PostHog when the page loaded it (GTM exposes
|
|
101
|
+
* `window.posthog`); a no-op otherwise. Never throws.
|
|
102
|
+
*/
|
|
103
|
+
export declare function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void;
|
|
104
|
+
export {};
|
|
105
|
+
//# sourceMappingURL=form-rescue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-rescue.d.ts","sourceRoot":"","sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/utils/form-rescue.ts
|
|
2
|
+
var FORM_RESCUE_FORM_IDS = [
|
|
3
|
+
"contact",
|
|
4
|
+
"case_study_pitch",
|
|
5
|
+
"data_room_request",
|
|
6
|
+
"trust_center_request",
|
|
7
|
+
"tmcg_join",
|
|
8
|
+
"meeting_booking",
|
|
9
|
+
"waitlist"
|
|
10
|
+
];
|
|
11
|
+
function isFormRescueFormId(value) {
|
|
12
|
+
return typeof value === "string" && FORM_RESCUE_FORM_IDS.includes(value);
|
|
13
|
+
}
|
|
14
|
+
var FORM_RESCUE_VALUE_FIELDS = [
|
|
15
|
+
"email",
|
|
16
|
+
"name",
|
|
17
|
+
"firstName",
|
|
18
|
+
"lastName",
|
|
19
|
+
"company",
|
|
20
|
+
"jobtitle",
|
|
21
|
+
"companySize",
|
|
22
|
+
"linkedin_url"
|
|
23
|
+
];
|
|
24
|
+
var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
|
|
25
|
+
function isExcludedRescueField(name) {
|
|
26
|
+
return EXCLUDED_FIELD_PATTERN.test(name);
|
|
27
|
+
}
|
|
28
|
+
var FORM_RESCUE_MAX_VALUE_CHARS = 200;
|
|
29
|
+
var FORM_RESCUE_DEBOUNCE_MS = 1500;
|
|
30
|
+
var FORM_RESCUE_MAX_FIELDS = 40;
|
|
31
|
+
var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
|
|
32
|
+
var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
|
|
33
|
+
var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
|
|
34
|
+
var FORM_RESCUE_RESUME_PARAM = "resume";
|
|
35
|
+
var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
|
|
36
|
+
var FORM_RESCUE_SKIP_REASONS = {
|
|
37
|
+
finishedElsewhere: "finished_elsewhere",
|
|
38
|
+
alreadyRescued: "already_rescued",
|
|
39
|
+
internal: "internal_email"
|
|
40
|
+
};
|
|
41
|
+
var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
|
42
|
+
function isRescueEmail(value) {
|
|
43
|
+
return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
|
|
44
|
+
}
|
|
45
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
46
|
+
function isFormAttemptId(value) {
|
|
47
|
+
return typeof value === "string" && UUID_PATTERN.test(value);
|
|
48
|
+
}
|
|
49
|
+
function isFilled(value) {
|
|
50
|
+
if (value == null) return false;
|
|
51
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
52
|
+
if (typeof value === "boolean") return value;
|
|
53
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
54
|
+
if (typeof value === "object") return Object.values(value).some(isFilled);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function sanitizeRescueValues(values) {
|
|
58
|
+
const out = {};
|
|
59
|
+
if (!values || typeof values !== "object") return out;
|
|
60
|
+
for (const key of FORM_RESCUE_VALUE_FIELDS) {
|
|
61
|
+
if (isExcludedRescueField(key)) continue;
|
|
62
|
+
const raw = values[key];
|
|
63
|
+
if (typeof raw !== "string") continue;
|
|
64
|
+
const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
|
|
65
|
+
if (trimmed) out[key] = trimmed;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function filledRescueFields(values, fieldNames) {
|
|
70
|
+
return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
|
|
71
|
+
}
|
|
72
|
+
function rescueCompletionPct(filledCount, totalCount) {
|
|
73
|
+
if (totalCount <= 0) return 0;
|
|
74
|
+
return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
|
|
75
|
+
}
|
|
76
|
+
function sanitizeRescueFieldName(value) {
|
|
77
|
+
if (typeof value !== "string") return null;
|
|
78
|
+
const trimmed = value.trim();
|
|
79
|
+
return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
|
|
80
|
+
}
|
|
81
|
+
var FORM_RESCUE_EVENTS = {
|
|
82
|
+
started: "form_started",
|
|
83
|
+
progress: "form_progress",
|
|
84
|
+
leadCaptured: "form_lead_captured",
|
|
85
|
+
abandoned: "form_abandoned",
|
|
86
|
+
submitted: "form_submitted",
|
|
87
|
+
rescued: "form_rescued",
|
|
88
|
+
resumed: "form_resumed"
|
|
89
|
+
};
|
|
90
|
+
function captureFormRescueEvent(event, properties) {
|
|
91
|
+
try {
|
|
92
|
+
if (typeof window === "undefined") return;
|
|
93
|
+
const ph = window.posthog;
|
|
94
|
+
if (typeof ph?.capture === "function") ph.capture(event, properties);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { FORM_DRAFT_STATUSES, FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_DEBOUNCE_MS, FORM_RESCUE_EVENTS, FORM_RESCUE_FORM_IDS, FORM_RESCUE_MAX_FIELDS, FORM_RESCUE_MAX_VALUE_CHARS, FORM_RESCUE_RESUME_FIELD, FORM_RESCUE_RESUME_PARAM, FORM_RESCUE_SKIP_REASONS, FORM_RESCUE_SUBMIT_KEYS, FORM_RESCUE_VALUE_FIELDS, captureFormRescueEvent, filledRescueFields, isExcludedRescueField, isFormAttemptId, isFormRescueFormId, isRescueEmail, rescueCompletionPct, sanitizeRescueFieldName, sanitizeRescueValues };
|
|
100
|
+
//# sourceMappingURL=form-rescue.js.map
|
|
101
|
+
//# sourceMappingURL=form-rescue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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","file":"form-rescue.js","sourcesContent":["/**\n * Form rescue — the shared rules for saving a half-filled public form so the\n * team can follow up with a visitor who left before submitting.\n *\n * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the\n * host's draft endpoint import the SAME allowlist, exclusions, caps and\n * completion math, so the server re-applies exactly the filter the client ran\n * and a rename here reaches both sides at once. Also exported through the\n * granular subpath `./utils/form-rescue` for server-only consumers.\n *\n * What a draft may carry:\n * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),\n * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;\n * - for every other field, only whether it is filled (never its value);\n * - nothing whose name looks like a credential or payment field\n * (`isExcludedRescueField`), whatever the allowlist says.\n *\n * Field values go to the host only, never to analytics: the PostHog events\n * (`captureFormRescueEvent`) carry ids, field names and percentages.\n */\n\n/** Every public form that can be rescued. The host validates `form_id` against this list. */\nexport const FORM_RESCUE_FORM_IDS = [\n 'contact',\n 'case_study_pitch',\n 'data_room_request',\n 'trust_center_request',\n 'tmcg_join',\n 'meeting_booking',\n 'waitlist',\n] as const;\n\nexport type FormRescueFormId = (typeof FORM_RESCUE_FORM_IDS)[number];\n\nexport function isFormRescueFormId(value: unknown): value is FormRescueFormId {\n return typeof value === 'string' && (FORM_RESCUE_FORM_IDS as readonly string[]).includes(value);\n}\n\n/**\n * Fields whose VALUES a draft keeps: who the visitor is and where they work.\n * Covers both naming schemes in use (the contact form's `name` / `companySize`,\n * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).\n * Free text (`message`) and phone numbers are reported as filled only.\n */\nexport const FORM_RESCUE_VALUE_FIELDS = [\n 'email',\n 'name',\n 'firstName',\n 'lastName',\n 'company',\n 'jobtitle',\n 'companySize',\n 'linkedin_url',\n] as const;\n\n/** Never send or store these, whatever the allowlist says. */\nconst EXCLUDED_FIELD_PATTERN =\n /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;\n\nexport function isExcludedRescueField(name: string): boolean {\n return EXCLUDED_FIELD_PATTERN.test(name);\n}\n\n/** Longest value a draft keeps per field. */\nexport const FORM_RESCUE_MAX_VALUE_CHARS = 200;\n/** Save this long after the visitor stops typing. */\nexport const FORM_RESCUE_DEBOUNCE_MS = 1500;\n/** Most fields a draft reports (filled names), a bound on the payload. */\nexport const FORM_RESCUE_MAX_FIELDS = 40;\n\n/** Body keys a form's SUBMIT carries so the host can close the matching draft. */\nexport const FORM_RESCUE_ATTEMPT_FIELD = 'form_attempt_id';\nexport const FORM_RESCUE_RESUME_FIELD = 'form_resume_token';\n/** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */\nexport const FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD] as const;\n\n/** Query parameter a resume link carries. */\nexport const FORM_RESCUE_RESUME_PARAM = 'resume';\n\n/** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */\nexport const FORM_DRAFT_STATUSES = ['started', 'rescuable', 'rescued', 'submitted'] as const;\nexport type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];\n\n/** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */\nexport const FORM_RESCUE_SKIP_REASONS = {\n finishedElsewhere: 'finished_elsewhere',\n alreadyRescued: 'already_rescued',\n internal: 'internal_email',\n} as const;\nexport type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];\n\n/** Same shape the host's `contact_submissions.email` check accepts. */\nconst EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/;\n\nexport function isRescueEmail(value: unknown): value is string {\n return typeof value === 'string' && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isFormAttemptId(value: unknown): value is string {\n return typeof value === 'string' && UUID_PATTERN.test(value);\n}\n\nfunction isFilled(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.trim().length > 0;\n if (typeof value === 'boolean') return value;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.values(value as Record<string, unknown>).some(isFilled);\n return true;\n}\n\n/**\n * Keep only allowlisted, non-excluded string values, trimmed and capped.\n * The ONE filter, run by the browser before sending and by the host before storing.\n */\nexport function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!values || typeof values !== 'object') return out;\n for (const key of FORM_RESCUE_VALUE_FIELDS) {\n if (isExcludedRescueField(key)) continue;\n const raw = values[key];\n if (typeof raw !== 'string') continue;\n const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);\n if (trimmed) out[key] = trimmed;\n }\n return out;\n}\n\n/** Names of the given fields that hold something, credentials never included. */\nexport function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[] {\n return fieldNames\n .filter(name => !isExcludedRescueField(name) && isFilled(values[name]))\n .slice(0, FORM_RESCUE_MAX_FIELDS);\n}\n\nexport function rescueCompletionPct(filledCount: number, totalCount: number): number {\n if (totalCount <= 0) return 0;\n return Math.max(0, Math.min(100, Math.round((filledCount / totalCount) * 100)));\n}\n\n/** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */\nexport function sanitizeRescueFieldName(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;\n}\n\n/** A form's progress at one moment: allowlisted values, filled field names, position. */\nexport interface FormDraftProgress {\n values: Record<string, string>;\n fields_filled: string[];\n last_field: string | null;\n completion_pct: number;\n}\n\n/** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */\nexport interface FormDraftSaveRequest extends FormDraftProgress {\n /** The attempt's idempotency key: every save of one attempt updates one draft. */\n attempt_id: string;\n form_id: FormRescueFormId;\n source_path: string;\n utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;\n}\n\n/** What the host answers a resume link with: the allowlisted values, nothing internal. */\nexport interface FormDraftResumeResponse {\n form_id: FormRescueFormId;\n values: Record<string, string>;\n}\n\n/** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */\nexport const FORM_RESCUE_EVENTS = {\n started: 'form_started',\n progress: 'form_progress',\n leadCaptured: 'form_lead_captured',\n abandoned: 'form_abandoned',\n submitted: 'form_submitted',\n rescued: 'form_rescued',\n resumed: 'form_resumed',\n} as const;\n\ntype AnalyticsValue = string | number | boolean | string[] | null;\n\n/**\n * Send one form event to PostHog when the page loaded it (GTM exposes\n * `window.posthog`); a no-op otherwise. Never throws.\n */\nexport function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void {\n try {\n if (typeof window === 'undefined') return;\n const ph = (window as unknown as { posthog?: { capture?: (e: string, p: object) => void } }).posthog;\n if (typeof ph?.capture === 'function') ph.capture(event, properties);\n } catch {\n // Analytics never breaks a form.\n }\n}\n"]}
|
package/dist/utils/index.cjs
CHANGED
|
@@ -5300,6 +5300,104 @@ function evaluateHumanitySignals(body, opts) {
|
|
|
5300
5300
|
}
|
|
5301
5301
|
var splitCsvEnv = (s) => s?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
|
|
5302
5302
|
|
|
5303
|
+
// src/utils/form-rescue.ts
|
|
5304
|
+
var FORM_RESCUE_FORM_IDS = [
|
|
5305
|
+
"contact",
|
|
5306
|
+
"case_study_pitch",
|
|
5307
|
+
"data_room_request",
|
|
5308
|
+
"trust_center_request",
|
|
5309
|
+
"tmcg_join",
|
|
5310
|
+
"meeting_booking",
|
|
5311
|
+
"waitlist"
|
|
5312
|
+
];
|
|
5313
|
+
function isFormRescueFormId(value) {
|
|
5314
|
+
return typeof value === "string" && FORM_RESCUE_FORM_IDS.includes(value);
|
|
5315
|
+
}
|
|
5316
|
+
var FORM_RESCUE_VALUE_FIELDS = [
|
|
5317
|
+
"email",
|
|
5318
|
+
"name",
|
|
5319
|
+
"firstName",
|
|
5320
|
+
"lastName",
|
|
5321
|
+
"company",
|
|
5322
|
+
"jobtitle",
|
|
5323
|
+
"companySize",
|
|
5324
|
+
"linkedin_url"
|
|
5325
|
+
];
|
|
5326
|
+
var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
|
|
5327
|
+
function isExcludedRescueField(name) {
|
|
5328
|
+
return EXCLUDED_FIELD_PATTERN.test(name);
|
|
5329
|
+
}
|
|
5330
|
+
var FORM_RESCUE_MAX_VALUE_CHARS = 200;
|
|
5331
|
+
var FORM_RESCUE_DEBOUNCE_MS = 1500;
|
|
5332
|
+
var FORM_RESCUE_MAX_FIELDS = 40;
|
|
5333
|
+
var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
|
|
5334
|
+
var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
|
|
5335
|
+
var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
|
|
5336
|
+
var FORM_RESCUE_RESUME_PARAM = "resume";
|
|
5337
|
+
var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
|
|
5338
|
+
var FORM_RESCUE_SKIP_REASONS = {
|
|
5339
|
+
finishedElsewhere: "finished_elsewhere",
|
|
5340
|
+
alreadyRescued: "already_rescued",
|
|
5341
|
+
internal: "internal_email"
|
|
5342
|
+
};
|
|
5343
|
+
var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
|
5344
|
+
function isRescueEmail(value) {
|
|
5345
|
+
return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
|
|
5346
|
+
}
|
|
5347
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5348
|
+
function isFormAttemptId(value) {
|
|
5349
|
+
return typeof value === "string" && UUID_PATTERN.test(value);
|
|
5350
|
+
}
|
|
5351
|
+
function isFilled(value) {
|
|
5352
|
+
if (value == null) return false;
|
|
5353
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
5354
|
+
if (typeof value === "boolean") return value;
|
|
5355
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
5356
|
+
if (typeof value === "object") return Object.values(value).some(isFilled);
|
|
5357
|
+
return true;
|
|
5358
|
+
}
|
|
5359
|
+
function sanitizeRescueValues(values) {
|
|
5360
|
+
const out = {};
|
|
5361
|
+
if (!values || typeof values !== "object") return out;
|
|
5362
|
+
for (const key of FORM_RESCUE_VALUE_FIELDS) {
|
|
5363
|
+
if (isExcludedRescueField(key)) continue;
|
|
5364
|
+
const raw = values[key];
|
|
5365
|
+
if (typeof raw !== "string") continue;
|
|
5366
|
+
const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
|
|
5367
|
+
if (trimmed) out[key] = trimmed;
|
|
5368
|
+
}
|
|
5369
|
+
return out;
|
|
5370
|
+
}
|
|
5371
|
+
function filledRescueFields(values, fieldNames) {
|
|
5372
|
+
return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
|
|
5373
|
+
}
|
|
5374
|
+
function rescueCompletionPct(filledCount, totalCount) {
|
|
5375
|
+
if (totalCount <= 0) return 0;
|
|
5376
|
+
return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
|
|
5377
|
+
}
|
|
5378
|
+
function sanitizeRescueFieldName(value) {
|
|
5379
|
+
if (typeof value !== "string") return null;
|
|
5380
|
+
const trimmed = value.trim();
|
|
5381
|
+
return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
|
|
5382
|
+
}
|
|
5383
|
+
var FORM_RESCUE_EVENTS = {
|
|
5384
|
+
started: "form_started",
|
|
5385
|
+
progress: "form_progress",
|
|
5386
|
+
leadCaptured: "form_lead_captured",
|
|
5387
|
+
abandoned: "form_abandoned",
|
|
5388
|
+
submitted: "form_submitted",
|
|
5389
|
+
rescued: "form_rescued",
|
|
5390
|
+
resumed: "form_resumed"
|
|
5391
|
+
};
|
|
5392
|
+
function captureFormRescueEvent(event, properties) {
|
|
5393
|
+
try {
|
|
5394
|
+
if (typeof window === "undefined") return;
|
|
5395
|
+
const ph = window.posthog;
|
|
5396
|
+
if (typeof ph?.capture === "function") ph.capture(event, properties);
|
|
5397
|
+
} catch {
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
|
|
5303
5401
|
// src/utils/doc-tree-nav.ts
|
|
5304
5402
|
var DEFAULT_FOLDER_INDEX_FILE = "README.md";
|
|
5305
5403
|
var DEFAULT_FOLDER_INDEX = DEFAULT_FOLDER_INDEX_FILE;
|
|
@@ -6032,6 +6130,18 @@ exports.DEV_SECTION_PARAM_KEYS = DEV_SECTION_PARAM_KEYS;
|
|
|
6032
6130
|
exports.DOC_TABLE_TYPES = DOC_TABLE_TYPES;
|
|
6033
6131
|
exports.ELAPSED_MS_FIELD = ELAPSED_MS_FIELD;
|
|
6034
6132
|
exports.FIRST_TOUCH_ATTRIBUTION_KEY = FIRST_TOUCH_ATTRIBUTION_KEY;
|
|
6133
|
+
exports.FORM_DRAFT_STATUSES = FORM_DRAFT_STATUSES;
|
|
6134
|
+
exports.FORM_RESCUE_ATTEMPT_FIELD = FORM_RESCUE_ATTEMPT_FIELD;
|
|
6135
|
+
exports.FORM_RESCUE_DEBOUNCE_MS = FORM_RESCUE_DEBOUNCE_MS;
|
|
6136
|
+
exports.FORM_RESCUE_EVENTS = FORM_RESCUE_EVENTS;
|
|
6137
|
+
exports.FORM_RESCUE_FORM_IDS = FORM_RESCUE_FORM_IDS;
|
|
6138
|
+
exports.FORM_RESCUE_MAX_FIELDS = FORM_RESCUE_MAX_FIELDS;
|
|
6139
|
+
exports.FORM_RESCUE_MAX_VALUE_CHARS = FORM_RESCUE_MAX_VALUE_CHARS;
|
|
6140
|
+
exports.FORM_RESCUE_RESUME_FIELD = FORM_RESCUE_RESUME_FIELD;
|
|
6141
|
+
exports.FORM_RESCUE_RESUME_PARAM = FORM_RESCUE_RESUME_PARAM;
|
|
6142
|
+
exports.FORM_RESCUE_SKIP_REASONS = FORM_RESCUE_SKIP_REASONS;
|
|
6143
|
+
exports.FORM_RESCUE_SUBMIT_KEYS = FORM_RESCUE_SUBMIT_KEYS;
|
|
6144
|
+
exports.FORM_RESCUE_VALUE_FIELDS = FORM_RESCUE_VALUE_FIELDS;
|
|
6035
6145
|
exports.FullscreenSwitchController = FullscreenSwitchController;
|
|
6036
6146
|
exports.GENERIC_EMAIL_DOMAINS = GENERIC_EMAIL_DOMAINS;
|
|
6037
6147
|
exports.HEADING_EMOJI_RE = HEADING_EMOJI_RE;
|
|
@@ -6091,6 +6201,7 @@ exports.buildSuggestionUrl = buildSuggestionUrl;
|
|
|
6091
6201
|
exports.buildTicketOpenHref = buildTicketOpenHref;
|
|
6092
6202
|
exports.canonicalContentRefType = canonicalContentRefType;
|
|
6093
6203
|
exports.captureFirstTouchAttribution = captureFirstTouchAttribution;
|
|
6204
|
+
exports.captureFormRescueEvent = captureFormRescueEvent;
|
|
6094
6205
|
exports.clamp = clamp;
|
|
6095
6206
|
exports.clampPageToTotal = clampPageToTotal;
|
|
6096
6207
|
exports.classifySocialHost = classifySocialHost;
|
|
@@ -6138,6 +6249,7 @@ exports.extractSections = extractSections;
|
|
|
6138
6249
|
exports.faqItemAnchor = faqItemAnchor;
|
|
6139
6250
|
exports.faqSectionSlug = faqSectionSlug;
|
|
6140
6251
|
exports.fetchPriorityProp = fetchPriorityProp;
|
|
6252
|
+
exports.filledRescueFields = filledRescueFields;
|
|
6141
6253
|
exports.findAnchorElementByNormalizedId = findAnchorElementByNormalizedId;
|
|
6142
6254
|
exports.findDocNodeByPath = findDocNodeByPath;
|
|
6143
6255
|
exports.findHoneypotCopySource = findHoneypotCopySource;
|
|
@@ -6268,7 +6380,10 @@ exports.isBadgePaletteKey = isBadgePaletteKey;
|
|
|
6268
6380
|
exports.isBlankLine = isBlankLine;
|
|
6269
6381
|
exports.isCrossOriginUrl = isCrossOriginUrl;
|
|
6270
6382
|
exports.isDismissedCookieValue = isDismissedCookieValue;
|
|
6383
|
+
exports.isExcludedRescueField = isExcludedRescueField;
|
|
6271
6384
|
exports.isFigmaSlidesUrl = isFigmaSlidesUrl;
|
|
6385
|
+
exports.isFormAttemptId = isFormAttemptId;
|
|
6386
|
+
exports.isFormRescueFormId = isFormRescueFormId;
|
|
6272
6387
|
exports.isGenericDomain = isGenericDomain;
|
|
6273
6388
|
exports.isGenericWebsiteDomain = isGenericWebsiteDomain;
|
|
6274
6389
|
exports.isHumanitySignalKey = isHumanitySignalKey;
|
|
@@ -6278,6 +6393,7 @@ exports.isLocalUrl = isLocalUrl;
|
|
|
6278
6393
|
exports.isOSPlatform = isOSPlatform;
|
|
6279
6394
|
exports.isOrderedRange = isOrderedRange;
|
|
6280
6395
|
exports.isPublishableMediaType = isPublishableMediaType;
|
|
6396
|
+
exports.isRescueEmail = isRescueEmail;
|
|
6281
6397
|
exports.isToday = isToday;
|
|
6282
6398
|
exports.isValidEmailDomain = isValidEmailDomain;
|
|
6283
6399
|
exports.isValidSchedulingSlug = isValidSchedulingSlug;
|
|
@@ -6337,11 +6453,14 @@ exports.readDismissCookie = readDismissCookie;
|
|
|
6337
6453
|
exports.readLeadingDecisionFrame = readLeadingDecisionFrame;
|
|
6338
6454
|
exports.recordCountLabel = recordCountLabel;
|
|
6339
6455
|
exports.releaseTypeToBadgeColor = releaseTypeToBadgeColor;
|
|
6456
|
+
exports.rescueCompletionPct = rescueCompletionPct;
|
|
6340
6457
|
exports.resolveContentFetchPath = resolveContentFetchPath;
|
|
6341
6458
|
exports.resolveRedirectTarget = resolveRedirectTarget;
|
|
6342
6459
|
exports.resolveReleaseCover = resolveReleaseCover;
|
|
6343
6460
|
exports.rgbToHex = rgbToHex;
|
|
6344
6461
|
exports.sanitizeLandingUrl = sanitizeLandingUrl;
|
|
6462
|
+
exports.sanitizeRescueFieldName = sanitizeRescueFieldName;
|
|
6463
|
+
exports.sanitizeRescueValues = sanitizeRescueValues;
|
|
6345
6464
|
exports.sanitizeTitleForChat = sanitizeTitleForChat;
|
|
6346
6465
|
exports.scanHeadings = scanHeadings;
|
|
6347
6466
|
exports.schedulingAudienceKey = schedulingAudienceKey;
|