@nodii/onboarding 0.0.1 → 0.1.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-34BQX2KR.js +164 -0
- package/dist/chunk-34BQX2KR.js.map +1 -0
- package/dist/chunk-4SWOVHZ2.js +434 -0
- package/dist/chunk-4SWOVHZ2.js.map +1 -0
- package/dist/chunk-7NS7ENJV.js +232 -0
- package/dist/chunk-7NS7ENJV.js.map +1 -0
- package/dist/chunk-AFCJLUPK.js +92 -0
- package/dist/chunk-AFCJLUPK.js.map +1 -0
- package/dist/chunk-AW7W3HAC.js +154 -0
- package/dist/chunk-AW7W3HAC.js.map +1 -0
- package/dist/chunk-BIFANV7M.js +22 -0
- package/dist/chunk-BIFANV7M.js.map +1 -0
- package/dist/chunk-BQCTDU5V.js +327 -0
- package/dist/chunk-BQCTDU5V.js.map +1 -0
- package/dist/chunk-JFIONONR.js +3 -0
- package/dist/chunk-JFIONONR.js.map +1 -0
- package/dist/chunk-SWMIIUXR.js +1162 -0
- package/dist/chunk-SWMIIUXR.js.map +1 -0
- package/dist/chunk-VH6FWDWI.js +3 -0
- package/dist/chunk-VH6FWDWI.js.map +1 -0
- package/dist/chunk-Z4PA6C2Z.js +3 -0
- package/dist/chunk-Z4PA6C2Z.js.map +1 -0
- package/dist/chunk-ZDY2I6KY.js +3 -0
- package/dist/chunk-ZDY2I6KY.js.map +1 -0
- package/dist/dynamic/index.d.ts +184 -0
- package/dist/dynamic/index.js +4 -0
- package/dist/dynamic/index.js.map +1 -0
- package/dist/form/index.d.ts +64 -0
- package/dist/form/index.js +5 -0
- package/dist/form/index.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -0
- package/dist/model-IEyDI54w.d.ts +487 -0
- package/dist/recaptcha/index.d.ts +102 -0
- package/dist/recaptcha/index.js +4 -0
- package/dist/recaptcha/index.js.map +1 -0
- package/dist/schema-TfyBU6JW.d.ts +107 -0
- package/dist/signup-client-Bl6eUrxR.d.ts +259 -0
- package/dist/spec/index.d.ts +88 -0
- package/dist/spec/index.js +4 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/submit/index.d.ts +106 -0
- package/dist/submit/index.js +6 -0
- package/dist/submit/index.js.map +1 -0
- package/dist/widget/index.d.ts +219 -0
- package/dist/widget/index.js +10 -0
- package/dist/widget/index.js.map +1 -0
- package/package.json +77 -6
- package/README.md +0 -5
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
import { createContactFormState } from './chunk-AFCJLUPK.js';
|
|
2
|
+
import { defaultSpecCache, fetchOnboardingSpec, diffSpecFields } from './chunk-34BQX2KR.js';
|
|
3
|
+
import { createSignupStepper, optionValue } from './chunk-4SWOVHZ2.js';
|
|
4
|
+
import { computePayloadFingerprint, generateIdempotencyKey, submitLead, computeSignupFingerprint, submitSignup } from './chunk-BQCTDU5V.js';
|
|
5
|
+
import { OnboardingBotChallengeError, OnboardingRateLimitError, OnboardingSpecStaleError } from './chunk-AW7W3HAC.js';
|
|
6
|
+
import { loadRecaptcha, executeRecaptcha } from './chunk-7NS7ENJV.js';
|
|
7
|
+
|
|
8
|
+
// src/widget/dom-utils.ts
|
|
9
|
+
function cssEscape(value) {
|
|
10
|
+
const g = globalThis;
|
|
11
|
+
if (g.CSS && typeof g.CSS.escape === "function") {
|
|
12
|
+
return g.CSS.escape(value);
|
|
13
|
+
}
|
|
14
|
+
return value.replace(/[^A-Za-z0-9_-]/g, (ch) => `\\${ch}`);
|
|
15
|
+
}
|
|
16
|
+
function idSelector(id) {
|
|
17
|
+
return `#${cssEscape(id)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/widget/styles.ts
|
|
21
|
+
var DEFAULT_WIDGET_CSS = `
|
|
22
|
+
.nodii-onboarding-widget {
|
|
23
|
+
--_font: var(--nodii-onboarding-font-family, system-ui, -apple-system, sans-serif);
|
|
24
|
+
--_primary: var(--nodii-onboarding-primary-color, #2563eb);
|
|
25
|
+
--_primary-hover: var(--nodii-onboarding-primary-hover, #1d4ed8);
|
|
26
|
+
--_error: var(--nodii-onboarding-error-color, #dc2626);
|
|
27
|
+
--_border: var(--nodii-onboarding-border-color, #d1d5db);
|
|
28
|
+
--_input-bg: var(--nodii-onboarding-input-bg, #fff);
|
|
29
|
+
--_label: var(--nodii-onboarding-label-color, #374151);
|
|
30
|
+
--_radius: var(--nodii-onboarding-radius, 6px);
|
|
31
|
+
--_spacing: var(--nodii-onboarding-spacing, 16px);
|
|
32
|
+
|
|
33
|
+
font-family: var(--_font);
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.nodii-onboarding-widget *,
|
|
38
|
+
.nodii-onboarding-widget *::before,
|
|
39
|
+
.nodii-onboarding-widget *::after {
|
|
40
|
+
box-sizing: inherit;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.nodii-onboarding-field {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: calc(var(--_spacing) * 0.25);
|
|
47
|
+
margin-bottom: var(--_spacing);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.nodii-onboarding-label {
|
|
51
|
+
font-size: 0.875rem;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
color: var(--_label);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.nodii-onboarding-label.required::after {
|
|
57
|
+
content: " *";
|
|
58
|
+
color: var(--_error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.nodii-onboarding-input,
|
|
62
|
+
.nodii-onboarding-textarea {
|
|
63
|
+
width: 100%;
|
|
64
|
+
padding: calc(var(--_spacing) * 0.5) calc(var(--_spacing) * 0.75);
|
|
65
|
+
border: 1px solid var(--_border);
|
|
66
|
+
border-radius: var(--_radius);
|
|
67
|
+
background: var(--_input-bg);
|
|
68
|
+
font-family: var(--_font);
|
|
69
|
+
font-size: 1rem;
|
|
70
|
+
line-height: 1.5;
|
|
71
|
+
outline: none;
|
|
72
|
+
transition: border-color 0.15s ease;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.nodii-onboarding-input:focus,
|
|
76
|
+
.nodii-onboarding-textarea:focus {
|
|
77
|
+
border-color: var(--_primary);
|
|
78
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--_primary) 20%, transparent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.nodii-onboarding-input.has-error,
|
|
82
|
+
.nodii-onboarding-textarea.has-error {
|
|
83
|
+
border-color: var(--_error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.nodii-onboarding-textarea {
|
|
87
|
+
min-height: 100px;
|
|
88
|
+
resize: vertical;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.nodii-onboarding-field-error {
|
|
92
|
+
font-size: 0.75rem;
|
|
93
|
+
color: var(--_error);
|
|
94
|
+
margin-top: 2px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.nodii-onboarding-server-error {
|
|
98
|
+
padding: calc(var(--_spacing) * 0.75);
|
|
99
|
+
border-radius: var(--_radius);
|
|
100
|
+
background: color-mix(in srgb, var(--_error) 8%, transparent);
|
|
101
|
+
border: 1px solid color-mix(in srgb, var(--_error) 30%, transparent);
|
|
102
|
+
color: var(--_error);
|
|
103
|
+
font-size: 0.875rem;
|
|
104
|
+
margin-bottom: var(--_spacing);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.nodii-onboarding-success {
|
|
108
|
+
padding: var(--_spacing);
|
|
109
|
+
border-radius: var(--_radius);
|
|
110
|
+
background: #f0fdf4;
|
|
111
|
+
border: 1px solid #bbf7d0;
|
|
112
|
+
color: #15803d;
|
|
113
|
+
font-size: 0.9375rem;
|
|
114
|
+
text-align: center;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.nodii-onboarding-submit-btn {
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
width: 100%;
|
|
122
|
+
padding: calc(var(--_spacing) * 0.625) var(--_spacing);
|
|
123
|
+
background: var(--_primary);
|
|
124
|
+
color: #fff;
|
|
125
|
+
border: none;
|
|
126
|
+
border-radius: var(--_radius);
|
|
127
|
+
font-family: var(--_font);
|
|
128
|
+
font-size: 1rem;
|
|
129
|
+
font-weight: 500;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
transition: background 0.15s ease;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.nodii-onboarding-submit-btn:hover:not(:disabled) {
|
|
135
|
+
background: var(--_primary-hover);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.nodii-onboarding-submit-btn:disabled {
|
|
139
|
+
opacity: 0.6;
|
|
140
|
+
cursor: not-allowed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.nodii-onboarding-spinner {
|
|
144
|
+
display: inline-block;
|
|
145
|
+
width: 1em;
|
|
146
|
+
height: 1em;
|
|
147
|
+
border: 2px solid rgba(255,255,255,0.4);
|
|
148
|
+
border-top-color: #fff;
|
|
149
|
+
border-radius: 50%;
|
|
150
|
+
animation: nodii-spin 0.6s linear infinite;
|
|
151
|
+
margin-right: 0.5em;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@keyframes nodii-spin {
|
|
155
|
+
to { transform: rotate(360deg); }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.nodii-onboarding-recaptcha-notice {
|
|
159
|
+
font-size: 0.7rem;
|
|
160
|
+
color: #6b7280;
|
|
161
|
+
margin-top: calc(var(--_spacing) * 0.5);
|
|
162
|
+
text-align: center;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.nodii-onboarding-recaptcha-notice a {
|
|
166
|
+
color: inherit;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* \u2500\u2500 dynamic stepper (signup form) \u2500\u2500 */
|
|
170
|
+
|
|
171
|
+
.nodii-onboarding-steps {
|
|
172
|
+
display: flex;
|
|
173
|
+
gap: calc(var(--_spacing) * 0.5);
|
|
174
|
+
margin-bottom: var(--_spacing);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.nodii-onboarding-step-dot {
|
|
178
|
+
flex: 1;
|
|
179
|
+
height: 4px;
|
|
180
|
+
border-radius: 2px;
|
|
181
|
+
background: var(--_border);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.nodii-onboarding-step-dot.active {
|
|
185
|
+
background: var(--_primary);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.nodii-onboarding-stage-title {
|
|
189
|
+
font-size: 1.0625rem;
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
color: var(--_label);
|
|
192
|
+
margin: 0 0 var(--_spacing) 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.nodii-onboarding-nav {
|
|
196
|
+
display: flex;
|
|
197
|
+
gap: calc(var(--_spacing) * 0.5);
|
|
198
|
+
margin-top: var(--_spacing);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.nodii-onboarding-back-btn {
|
|
202
|
+
padding: calc(var(--_spacing) * 0.625) var(--_spacing);
|
|
203
|
+
background: transparent;
|
|
204
|
+
color: var(--_label);
|
|
205
|
+
border: 1px solid var(--_border);
|
|
206
|
+
border-radius: var(--_radius);
|
|
207
|
+
font-family: var(--_font);
|
|
208
|
+
font-size: 1rem;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.nodii-onboarding-stale-banner {
|
|
213
|
+
padding: calc(var(--_spacing) * 0.75);
|
|
214
|
+
border-radius: var(--_radius);
|
|
215
|
+
background: #fffbeb;
|
|
216
|
+
border: 1px solid #fcd34d;
|
|
217
|
+
color: #92400e;
|
|
218
|
+
font-size: 0.875rem;
|
|
219
|
+
margin-bottom: var(--_spacing);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.nodii-onboarding-consent-row {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: flex-start;
|
|
225
|
+
gap: calc(var(--_spacing) * 0.5);
|
|
226
|
+
margin-bottom: calc(var(--_spacing) * 0.5);
|
|
227
|
+
font-size: 0.875rem;
|
|
228
|
+
color: var(--_label);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.nodii-onboarding-consent-row input[type="checkbox"] {
|
|
232
|
+
margin-top: 3px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.nodii-onboarding-account-section {
|
|
236
|
+
border-top: 1px solid var(--_border);
|
|
237
|
+
margin-top: var(--_spacing);
|
|
238
|
+
padding-top: var(--_spacing);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.nodii-onboarding-loading {
|
|
242
|
+
padding: var(--_spacing);
|
|
243
|
+
text-align: center;
|
|
244
|
+
color: var(--_label);
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
function injectWidgetStyles() {
|
|
248
|
+
if (typeof document === "undefined") return;
|
|
249
|
+
const styleId = "__nodii_onboarding_styles__";
|
|
250
|
+
if (document.getElementById(styleId)) return;
|
|
251
|
+
const style = document.createElement("style");
|
|
252
|
+
style.id = styleId;
|
|
253
|
+
style.textContent = DEFAULT_WIDGET_CSS;
|
|
254
|
+
document.head.appendChild(style);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/widget/contact-form.ts
|
|
258
|
+
function el(tag, attrs = {}, children = []) {
|
|
259
|
+
const element = document.createElement(tag);
|
|
260
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
261
|
+
element.setAttribute(key, value);
|
|
262
|
+
}
|
|
263
|
+
for (const child of children) {
|
|
264
|
+
if (typeof child === "string") {
|
|
265
|
+
element.appendChild(document.createTextNode(child));
|
|
266
|
+
} else {
|
|
267
|
+
element.appendChild(child);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return element;
|
|
271
|
+
}
|
|
272
|
+
function createFieldEl(id, label, input, required) {
|
|
273
|
+
const labelEl = el(
|
|
274
|
+
"label",
|
|
275
|
+
{ for: id, class: `nodii-onboarding-label${required ? " required" : ""}` },
|
|
276
|
+
[label]
|
|
277
|
+
);
|
|
278
|
+
const errorEl = el("div", {
|
|
279
|
+
class: "nodii-onboarding-field-error",
|
|
280
|
+
id: `${id}-error`,
|
|
281
|
+
role: "alert",
|
|
282
|
+
"aria-live": "polite"
|
|
283
|
+
});
|
|
284
|
+
const field = el("div", { class: "nodii-onboarding-field" }, [
|
|
285
|
+
labelEl,
|
|
286
|
+
input,
|
|
287
|
+
errorEl
|
|
288
|
+
]);
|
|
289
|
+
return field;
|
|
290
|
+
}
|
|
291
|
+
function setFieldError(container, fieldId, message) {
|
|
292
|
+
const errorEl = container.querySelector(idSelector(`${fieldId}-error`));
|
|
293
|
+
if (errorEl) errorEl.textContent = message;
|
|
294
|
+
const inputEl = container.querySelector(idSelector(fieldId));
|
|
295
|
+
if (inputEl) inputEl.classList.add("has-error");
|
|
296
|
+
}
|
|
297
|
+
function clearFieldErrors(container) {
|
|
298
|
+
for (const el3 of container.querySelectorAll(
|
|
299
|
+
".nodii-onboarding-field-error"
|
|
300
|
+
)) {
|
|
301
|
+
el3.textContent = "";
|
|
302
|
+
}
|
|
303
|
+
for (const el3 of container.querySelectorAll(".has-error")) {
|
|
304
|
+
el3.classList.remove("has-error");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function applyErrors(container, errors) {
|
|
308
|
+
clearFieldErrors(container);
|
|
309
|
+
for (const err of errors) {
|
|
310
|
+
setFieldError(container, `nodii-${err.path}`, err.message);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function mountContactForm(host, options) {
|
|
314
|
+
injectWidgetStyles();
|
|
315
|
+
const formControl = createContactFormState();
|
|
316
|
+
loadRecaptcha(options.siteKey).catch(() => {
|
|
317
|
+
});
|
|
318
|
+
const wrapper = el("div", { class: "nodii-onboarding-widget" });
|
|
319
|
+
const nameInput = el("input", {
|
|
320
|
+
type: "text",
|
|
321
|
+
id: "nodii-name",
|
|
322
|
+
name: "name",
|
|
323
|
+
class: "nodii-onboarding-input",
|
|
324
|
+
placeholder: "Your name",
|
|
325
|
+
autocomplete: "name",
|
|
326
|
+
maxlength: "200",
|
|
327
|
+
required: "required"
|
|
328
|
+
});
|
|
329
|
+
const emailInput = el("input", {
|
|
330
|
+
type: "email",
|
|
331
|
+
id: "nodii-email",
|
|
332
|
+
name: "email",
|
|
333
|
+
class: "nodii-onboarding-input",
|
|
334
|
+
placeholder: "you@company.com",
|
|
335
|
+
autocomplete: "email",
|
|
336
|
+
required: "required"
|
|
337
|
+
});
|
|
338
|
+
const phoneInput = el("input", {
|
|
339
|
+
type: "tel",
|
|
340
|
+
id: "nodii-phone",
|
|
341
|
+
name: "phone",
|
|
342
|
+
class: "nodii-onboarding-input",
|
|
343
|
+
placeholder: "+1 555 000 0000",
|
|
344
|
+
autocomplete: "tel"
|
|
345
|
+
});
|
|
346
|
+
const messageInput = el("textarea", {
|
|
347
|
+
id: "nodii-message",
|
|
348
|
+
name: "message",
|
|
349
|
+
class: "nodii-onboarding-textarea",
|
|
350
|
+
placeholder: "How can we help?",
|
|
351
|
+
maxlength: "4000",
|
|
352
|
+
rows: "4"
|
|
353
|
+
});
|
|
354
|
+
const serverErrorEl = el("div", {
|
|
355
|
+
class: "nodii-onboarding-server-error",
|
|
356
|
+
role: "alert",
|
|
357
|
+
"aria-live": "assertive",
|
|
358
|
+
style: "display:none"
|
|
359
|
+
});
|
|
360
|
+
const successEl = el(
|
|
361
|
+
"div",
|
|
362
|
+
{
|
|
363
|
+
class: "nodii-onboarding-success",
|
|
364
|
+
role: "status",
|
|
365
|
+
style: "display:none"
|
|
366
|
+
},
|
|
367
|
+
[options.successMessage ?? "Thank you! We'll be in touch shortly."]
|
|
368
|
+
);
|
|
369
|
+
const submitBtn = el(
|
|
370
|
+
"button",
|
|
371
|
+
{
|
|
372
|
+
type: "submit",
|
|
373
|
+
class: "nodii-onboarding-submit-btn"
|
|
374
|
+
},
|
|
375
|
+
[options.submitLabel ?? "Get in touch"]
|
|
376
|
+
);
|
|
377
|
+
const recaptchaNotice = el(
|
|
378
|
+
"p",
|
|
379
|
+
{ class: "nodii-onboarding-recaptcha-notice" },
|
|
380
|
+
[
|
|
381
|
+
"Protected by reCAPTCHA. ",
|
|
382
|
+
Object.assign(
|
|
383
|
+
el("a", {
|
|
384
|
+
href: "https://policies.google.com/privacy",
|
|
385
|
+
target: "_blank",
|
|
386
|
+
rel: "noopener noreferrer"
|
|
387
|
+
}),
|
|
388
|
+
{ textContent: "Privacy" }
|
|
389
|
+
),
|
|
390
|
+
" & ",
|
|
391
|
+
Object.assign(
|
|
392
|
+
el("a", {
|
|
393
|
+
href: "https://policies.google.com/terms",
|
|
394
|
+
target: "_blank",
|
|
395
|
+
rel: "noopener noreferrer"
|
|
396
|
+
}),
|
|
397
|
+
{ textContent: "Terms" }
|
|
398
|
+
)
|
|
399
|
+
]
|
|
400
|
+
);
|
|
401
|
+
const form = el("form", {
|
|
402
|
+
novalidate: "true",
|
|
403
|
+
"aria-label": "Contact us"
|
|
404
|
+
});
|
|
405
|
+
form.appendChild(createFieldEl("nodii-name", "Name", nameInput, true));
|
|
406
|
+
form.appendChild(createFieldEl("nodii-email", "Email", emailInput, true));
|
|
407
|
+
form.appendChild(
|
|
408
|
+
createFieldEl("nodii-phone", "Phone (optional)", phoneInput, false)
|
|
409
|
+
);
|
|
410
|
+
form.appendChild(
|
|
411
|
+
createFieldEl("nodii-message", "Message (optional)", messageInput, false)
|
|
412
|
+
);
|
|
413
|
+
form.appendChild(serverErrorEl);
|
|
414
|
+
form.appendChild(submitBtn);
|
|
415
|
+
form.appendChild(recaptchaNotice);
|
|
416
|
+
wrapper.appendChild(successEl);
|
|
417
|
+
wrapper.appendChild(form);
|
|
418
|
+
host.appendChild(wrapper);
|
|
419
|
+
nameInput.addEventListener("input", () => {
|
|
420
|
+
formControl.setValue("name", nameInput.value);
|
|
421
|
+
});
|
|
422
|
+
emailInput.addEventListener("input", () => {
|
|
423
|
+
formControl.setValue("email", emailInput.value);
|
|
424
|
+
});
|
|
425
|
+
phoneInput.addEventListener("input", () => {
|
|
426
|
+
formControl.setValue("phone", phoneInput.value || void 0);
|
|
427
|
+
});
|
|
428
|
+
messageInput.addEventListener("input", () => {
|
|
429
|
+
formControl.setValue("message", messageInput.value || void 0);
|
|
430
|
+
});
|
|
431
|
+
const submitLabel = options.submitLabel ?? "Get in touch";
|
|
432
|
+
function renderSubmitButton(submitting) {
|
|
433
|
+
while (submitBtn.firstChild) {
|
|
434
|
+
submitBtn.removeChild(submitBtn.firstChild);
|
|
435
|
+
}
|
|
436
|
+
if (submitting) {
|
|
437
|
+
const spinner = document.createElement("span");
|
|
438
|
+
spinner.className = "nodii-onboarding-spinner";
|
|
439
|
+
submitBtn.appendChild(spinner);
|
|
440
|
+
submitBtn.appendChild(document.createTextNode("Sending\u2026"));
|
|
441
|
+
} else {
|
|
442
|
+
submitBtn.appendChild(document.createTextNode(submitLabel));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const unsubscribe = formControl.subscribe((state) => {
|
|
446
|
+
submitBtn.disabled = state.status === "submitting";
|
|
447
|
+
renderSubmitButton(state.status === "submitting");
|
|
448
|
+
applyErrors(wrapper, state.errors);
|
|
449
|
+
if (state.serverError) {
|
|
450
|
+
serverErrorEl.textContent = state.serverError;
|
|
451
|
+
serverErrorEl.style.display = "";
|
|
452
|
+
} else {
|
|
453
|
+
serverErrorEl.style.display = "none";
|
|
454
|
+
}
|
|
455
|
+
if (state.status === "success") {
|
|
456
|
+
successEl.style.display = "";
|
|
457
|
+
form.style.display = "none";
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
let idempotencyKey = null;
|
|
461
|
+
let lastFingerprint = null;
|
|
462
|
+
form.addEventListener("submit", async (event) => {
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
formControl.setValue("name", nameInput.value);
|
|
465
|
+
formControl.setValue("email", emailInput.value);
|
|
466
|
+
formControl.setValue("phone", phoneInput.value || void 0);
|
|
467
|
+
formControl.setValue("message", messageInput.value || void 0);
|
|
468
|
+
const valid = formControl.validate();
|
|
469
|
+
if (!valid) return;
|
|
470
|
+
formControl.setSubmitting();
|
|
471
|
+
const values = formControl.getState().values;
|
|
472
|
+
const fingerprint = computePayloadFingerprint(values);
|
|
473
|
+
if (idempotencyKey === null || fingerprint !== lastFingerprint) {
|
|
474
|
+
idempotencyKey = generateIdempotencyKey();
|
|
475
|
+
lastFingerprint = fingerprint;
|
|
476
|
+
}
|
|
477
|
+
let firstToken;
|
|
478
|
+
try {
|
|
479
|
+
firstToken = await executeRecaptcha(options.siteKey, "contact_submit", {
|
|
480
|
+
timeoutMs: options.timeoutMs
|
|
481
|
+
});
|
|
482
|
+
} catch (recaptchaErr) {
|
|
483
|
+
formControl.setError("Could not load bot protection. Please try again.");
|
|
484
|
+
options.onError?.(recaptchaErr);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
let usedFirst = false;
|
|
488
|
+
const tokenProvider = async () => {
|
|
489
|
+
if (!usedFirst) {
|
|
490
|
+
usedFirst = true;
|
|
491
|
+
return firstToken;
|
|
492
|
+
}
|
|
493
|
+
return executeRecaptcha(options.siteKey, "contact_submit", {
|
|
494
|
+
timeoutMs: options.timeoutMs
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
try {
|
|
498
|
+
await submitLead(values, {
|
|
499
|
+
baseUrl: options.baseUrl,
|
|
500
|
+
botToken: tokenProvider,
|
|
501
|
+
idempotencyKey,
|
|
502
|
+
fetch: options.fetch,
|
|
503
|
+
timeoutMs: options.timeoutMs
|
|
504
|
+
});
|
|
505
|
+
idempotencyKey = null;
|
|
506
|
+
lastFingerprint = null;
|
|
507
|
+
formControl.setSuccess();
|
|
508
|
+
options.onSuccess?.();
|
|
509
|
+
} catch (err) {
|
|
510
|
+
if (err instanceof OnboardingBotChallengeError) {
|
|
511
|
+
formControl.setError(
|
|
512
|
+
"Bot protection check failed. Please refresh the page and try again."
|
|
513
|
+
);
|
|
514
|
+
options.onError?.(err);
|
|
515
|
+
} else if (err instanceof OnboardingRateLimitError) {
|
|
516
|
+
const msg = err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment.";
|
|
517
|
+
formControl.setError(msg);
|
|
518
|
+
options.onError?.(err);
|
|
519
|
+
} else {
|
|
520
|
+
formControl.setError(
|
|
521
|
+
"Something went wrong. Please try again in a moment."
|
|
522
|
+
);
|
|
523
|
+
options.onError?.(err);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
unmount() {
|
|
529
|
+
unsubscribe();
|
|
530
|
+
host.removeChild(wrapper);
|
|
531
|
+
},
|
|
532
|
+
reset() {
|
|
533
|
+
idempotencyKey = null;
|
|
534
|
+
lastFingerprint = null;
|
|
535
|
+
formControl.reset();
|
|
536
|
+
form.style.display = "";
|
|
537
|
+
successEl.style.display = "none";
|
|
538
|
+
nameInput.value = "";
|
|
539
|
+
emailInput.value = "";
|
|
540
|
+
phoneInput.value = "";
|
|
541
|
+
messageInput.value = "";
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/widget/signup-form.ts
|
|
547
|
+
var STAGE_TITLES = {
|
|
548
|
+
lead: "About you",
|
|
549
|
+
prospect: "Your business",
|
|
550
|
+
activation: "Choose your services"
|
|
551
|
+
};
|
|
552
|
+
function el2(tag, attrs = {}, children = []) {
|
|
553
|
+
const element = document.createElement(tag);
|
|
554
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
555
|
+
element.setAttribute(key, value);
|
|
556
|
+
}
|
|
557
|
+
for (const child of children) {
|
|
558
|
+
if (typeof child === "string") {
|
|
559
|
+
element.appendChild(document.createTextNode(child));
|
|
560
|
+
} else {
|
|
561
|
+
element.appendChild(child);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return element;
|
|
565
|
+
}
|
|
566
|
+
function optionLabel(option) {
|
|
567
|
+
if (option !== null && typeof option === "object") {
|
|
568
|
+
const o = option;
|
|
569
|
+
if (typeof o["label"] === "string") return o["label"];
|
|
570
|
+
}
|
|
571
|
+
return optionValue(option);
|
|
572
|
+
}
|
|
573
|
+
function buildFieldInput(field, currentValue, onChange) {
|
|
574
|
+
const id = `nodii-f-${field.key}`;
|
|
575
|
+
switch (field.type) {
|
|
576
|
+
case "textarea": {
|
|
577
|
+
const input = el2("textarea", {
|
|
578
|
+
id,
|
|
579
|
+
class: "nodii-onboarding-textarea",
|
|
580
|
+
rows: "4"
|
|
581
|
+
});
|
|
582
|
+
if (typeof currentValue === "string") input.value = currentValue;
|
|
583
|
+
input.addEventListener("input", () => onChange(input.value));
|
|
584
|
+
return input;
|
|
585
|
+
}
|
|
586
|
+
case "checkbox":
|
|
587
|
+
case "boolean": {
|
|
588
|
+
const input = el2("input", {
|
|
589
|
+
type: "checkbox",
|
|
590
|
+
id
|
|
591
|
+
});
|
|
592
|
+
input.checked = currentValue === true;
|
|
593
|
+
input.addEventListener("change", () => onChange(input.checked));
|
|
594
|
+
return input;
|
|
595
|
+
}
|
|
596
|
+
case "select":
|
|
597
|
+
case "radio": {
|
|
598
|
+
const select = el2("select", {
|
|
599
|
+
id,
|
|
600
|
+
class: "nodii-onboarding-input"
|
|
601
|
+
});
|
|
602
|
+
select.appendChild(el2("option", { value: "" }, ["\u2014 select \u2014"]));
|
|
603
|
+
for (const opt of field.options ?? []) {
|
|
604
|
+
const value = optionValue(opt);
|
|
605
|
+
const optionEl = el2("option", { value }, [optionLabel(opt)]);
|
|
606
|
+
select.appendChild(optionEl);
|
|
607
|
+
}
|
|
608
|
+
if (typeof currentValue === "string") select.value = currentValue;
|
|
609
|
+
select.addEventListener("change", () => onChange(select.value));
|
|
610
|
+
return select;
|
|
611
|
+
}
|
|
612
|
+
case "multiselect": {
|
|
613
|
+
const select = el2("select", {
|
|
614
|
+
id,
|
|
615
|
+
class: "nodii-onboarding-input",
|
|
616
|
+
multiple: "multiple"
|
|
617
|
+
});
|
|
618
|
+
const current = Array.isArray(currentValue) ? currentValue.map(String) : [];
|
|
619
|
+
for (const opt of field.options ?? []) {
|
|
620
|
+
const value = optionValue(opt);
|
|
621
|
+
const optionEl = el2("option", { value }, [
|
|
622
|
+
optionLabel(opt)
|
|
623
|
+
]);
|
|
624
|
+
if (current.includes(value)) optionEl.selected = true;
|
|
625
|
+
select.appendChild(optionEl);
|
|
626
|
+
}
|
|
627
|
+
select.addEventListener("change", () => {
|
|
628
|
+
const values = [];
|
|
629
|
+
for (const o of Array.from(select.options)) {
|
|
630
|
+
if (o.selected && o.value !== "") values.push(o.value);
|
|
631
|
+
}
|
|
632
|
+
onChange(values);
|
|
633
|
+
});
|
|
634
|
+
return select;
|
|
635
|
+
}
|
|
636
|
+
default: {
|
|
637
|
+
const typeAttr = field.type === "email" || field.type === "tel" || field.type === "number" || field.type === "date" ? field.type : "text";
|
|
638
|
+
const input = el2("input", {
|
|
639
|
+
type: typeAttr,
|
|
640
|
+
id,
|
|
641
|
+
class: "nodii-onboarding-input"
|
|
642
|
+
});
|
|
643
|
+
if (typeof currentValue === "string") input.value = currentValue;
|
|
644
|
+
if (typeof currentValue === "number") input.value = String(currentValue);
|
|
645
|
+
input.addEventListener("input", () => onChange(input.value));
|
|
646
|
+
return input;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function fieldRow(field, input, idOverride) {
|
|
651
|
+
const id = idOverride ?? `nodii-f-${field.key}`;
|
|
652
|
+
const labelEl = el2(
|
|
653
|
+
"label",
|
|
654
|
+
{
|
|
655
|
+
for: id,
|
|
656
|
+
class: `nodii-onboarding-label${field.required ? " required" : ""}`
|
|
657
|
+
},
|
|
658
|
+
[field.label]
|
|
659
|
+
);
|
|
660
|
+
const errorEl = el2("div", {
|
|
661
|
+
class: "nodii-onboarding-field-error",
|
|
662
|
+
id: `${id}-error`,
|
|
663
|
+
role: "alert",
|
|
664
|
+
"aria-live": "polite"
|
|
665
|
+
});
|
|
666
|
+
return el2("div", { class: "nodii-onboarding-field" }, [
|
|
667
|
+
labelEl,
|
|
668
|
+
input,
|
|
669
|
+
errorEl
|
|
670
|
+
]);
|
|
671
|
+
}
|
|
672
|
+
function mountSignupForm(host, options) {
|
|
673
|
+
injectWidgetStyles();
|
|
674
|
+
const wrapper = el2("div", { class: "nodii-onboarding-widget" });
|
|
675
|
+
const loadingEl = el2("div", { class: "nodii-onboarding-loading" }, [
|
|
676
|
+
"Loading signup form\u2026"
|
|
677
|
+
]);
|
|
678
|
+
wrapper.appendChild(loadingEl);
|
|
679
|
+
host.appendChild(wrapper);
|
|
680
|
+
const cache = options.cache ?? defaultSpecCache;
|
|
681
|
+
let stepper = null;
|
|
682
|
+
let unsubscribe = null;
|
|
683
|
+
let unmounted = false;
|
|
684
|
+
let siteKey = options.siteKey ?? "";
|
|
685
|
+
let idempotencyKey = null;
|
|
686
|
+
let lastFingerprint = null;
|
|
687
|
+
let renderedStepIndex = -1;
|
|
688
|
+
let renderedStale = false;
|
|
689
|
+
let currentSpecVersion = null;
|
|
690
|
+
let staleDiffSummary = "";
|
|
691
|
+
const stepArea = el2("div", {});
|
|
692
|
+
const staleBanner = el2("div", {
|
|
693
|
+
class: "nodii-onboarding-stale-banner",
|
|
694
|
+
role: "alert",
|
|
695
|
+
style: "display:none"
|
|
696
|
+
});
|
|
697
|
+
const serverErrorEl = el2("div", {
|
|
698
|
+
class: "nodii-onboarding-server-error",
|
|
699
|
+
role: "alert",
|
|
700
|
+
"aria-live": "assertive",
|
|
701
|
+
style: "display:none"
|
|
702
|
+
});
|
|
703
|
+
const successEl = el2("div", {
|
|
704
|
+
class: "nodii-onboarding-success",
|
|
705
|
+
role: "status",
|
|
706
|
+
style: "display:none"
|
|
707
|
+
});
|
|
708
|
+
const stepsIndicator = el2("div", { class: "nodii-onboarding-steps" });
|
|
709
|
+
const form = el2("form", { novalidate: "true", "aria-label": "Sign up" });
|
|
710
|
+
function setFieldErrorsInPlace(container, errors, idPrefix) {
|
|
711
|
+
for (const errEl of container.querySelectorAll(
|
|
712
|
+
".nodii-onboarding-field-error"
|
|
713
|
+
)) {
|
|
714
|
+
errEl.textContent = "";
|
|
715
|
+
}
|
|
716
|
+
for (const marked of container.querySelectorAll(".has-error")) {
|
|
717
|
+
marked.classList.remove("has-error");
|
|
718
|
+
}
|
|
719
|
+
for (const err of errors) {
|
|
720
|
+
const id = idPrefix(err.path);
|
|
721
|
+
const errorNode = container.querySelector(idSelector(`${id}-error`));
|
|
722
|
+
if (errorNode) errorNode.textContent = err.message;
|
|
723
|
+
const inputNode = container.querySelector(idSelector(id));
|
|
724
|
+
if (inputNode) inputNode.classList.add("has-error");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function accountIdOf(path) {
|
|
728
|
+
if (path === "email") return "nodii-account-email";
|
|
729
|
+
if (path === "password") return "nodii-account-password";
|
|
730
|
+
if (path === "tosAccepted") return "nodii-consent-tos";
|
|
731
|
+
if (path === "privacyAccepted") return "nodii-consent-privacy";
|
|
732
|
+
return `nodii-account-${path}`;
|
|
733
|
+
}
|
|
734
|
+
function renderStepsIndicator(state) {
|
|
735
|
+
while (stepsIndicator.firstChild) {
|
|
736
|
+
stepsIndicator.removeChild(stepsIndicator.firstChild);
|
|
737
|
+
}
|
|
738
|
+
for (let i = 0; i < state.stepCount; i++) {
|
|
739
|
+
stepsIndicator.appendChild(
|
|
740
|
+
el2("div", {
|
|
741
|
+
class: `nodii-onboarding-step-dot${i <= state.stepIndex ? " active" : ""}`
|
|
742
|
+
})
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function renderAccountSection(control, state) {
|
|
747
|
+
const section = el2("div", { class: "nodii-onboarding-account-section" });
|
|
748
|
+
const emailInput = el2("input", {
|
|
749
|
+
type: "email",
|
|
750
|
+
id: "nodii-account-email",
|
|
751
|
+
class: "nodii-onboarding-input",
|
|
752
|
+
autocomplete: "email"
|
|
753
|
+
});
|
|
754
|
+
emailInput.value = state.credentials.email;
|
|
755
|
+
emailInput.addEventListener(
|
|
756
|
+
"input",
|
|
757
|
+
() => control.setCredentials({ email: emailInput.value })
|
|
758
|
+
);
|
|
759
|
+
section.appendChild(
|
|
760
|
+
fieldRow(
|
|
761
|
+
{
|
|
762
|
+
key: "account-email",
|
|
763
|
+
label: "Account email",
|
|
764
|
+
required: true
|
|
765
|
+
},
|
|
766
|
+
emailInput,
|
|
767
|
+
"nodii-account-email"
|
|
768
|
+
// label for + error id match the real input id
|
|
769
|
+
)
|
|
770
|
+
);
|
|
771
|
+
const passwordInput = el2("input", {
|
|
772
|
+
type: "password",
|
|
773
|
+
id: "nodii-account-password",
|
|
774
|
+
class: "nodii-onboarding-input",
|
|
775
|
+
autocomplete: "new-password",
|
|
776
|
+
minlength: "12",
|
|
777
|
+
maxlength: "256"
|
|
778
|
+
});
|
|
779
|
+
passwordInput.value = state.credentials.password;
|
|
780
|
+
passwordInput.addEventListener(
|
|
781
|
+
"input",
|
|
782
|
+
() => control.setCredentials({ password: passwordInput.value })
|
|
783
|
+
);
|
|
784
|
+
section.appendChild(
|
|
785
|
+
fieldRow(
|
|
786
|
+
{
|
|
787
|
+
key: "account-password",
|
|
788
|
+
label: "Password (12+ characters)",
|
|
789
|
+
required: true
|
|
790
|
+
},
|
|
791
|
+
passwordInput,
|
|
792
|
+
"nodii-account-password"
|
|
793
|
+
// label for + error id match the real input id
|
|
794
|
+
)
|
|
795
|
+
);
|
|
796
|
+
const tosCheckbox = el2("input", {
|
|
797
|
+
type: "checkbox",
|
|
798
|
+
id: "nodii-consent-tos"
|
|
799
|
+
});
|
|
800
|
+
tosCheckbox.checked = state.consent.tosAccepted;
|
|
801
|
+
tosCheckbox.addEventListener(
|
|
802
|
+
"change",
|
|
803
|
+
() => control.setConsent({ tosAccepted: tosCheckbox.checked })
|
|
804
|
+
);
|
|
805
|
+
const tosLabelChildren = ["I accept the "];
|
|
806
|
+
if (options.tosUrl) {
|
|
807
|
+
tosLabelChildren.push(
|
|
808
|
+
el2(
|
|
809
|
+
"a",
|
|
810
|
+
{
|
|
811
|
+
href: options.tosUrl,
|
|
812
|
+
target: "_blank",
|
|
813
|
+
rel: "noopener noreferrer"
|
|
814
|
+
},
|
|
815
|
+
["Terms of Service"]
|
|
816
|
+
)
|
|
817
|
+
);
|
|
818
|
+
} else {
|
|
819
|
+
tosLabelChildren.push("Terms of Service");
|
|
820
|
+
}
|
|
821
|
+
section.appendChild(
|
|
822
|
+
el2("div", { class: "nodii-onboarding-consent-row" }, [
|
|
823
|
+
tosCheckbox,
|
|
824
|
+
el2("label", { for: "nodii-consent-tos" }, tosLabelChildren)
|
|
825
|
+
])
|
|
826
|
+
);
|
|
827
|
+
section.appendChild(
|
|
828
|
+
el2("div", {
|
|
829
|
+
class: "nodii-onboarding-field-error",
|
|
830
|
+
id: "nodii-consent-tos-error",
|
|
831
|
+
role: "alert"
|
|
832
|
+
})
|
|
833
|
+
);
|
|
834
|
+
const privacyCheckbox = el2("input", {
|
|
835
|
+
type: "checkbox",
|
|
836
|
+
id: "nodii-consent-privacy"
|
|
837
|
+
});
|
|
838
|
+
privacyCheckbox.checked = state.consent.privacyAccepted;
|
|
839
|
+
privacyCheckbox.addEventListener(
|
|
840
|
+
"change",
|
|
841
|
+
() => control.setConsent({ privacyAccepted: privacyCheckbox.checked })
|
|
842
|
+
);
|
|
843
|
+
const privacyLabelChildren = ["I accept the "];
|
|
844
|
+
if (options.privacyUrl) {
|
|
845
|
+
privacyLabelChildren.push(
|
|
846
|
+
el2(
|
|
847
|
+
"a",
|
|
848
|
+
{
|
|
849
|
+
href: options.privacyUrl,
|
|
850
|
+
target: "_blank",
|
|
851
|
+
rel: "noopener noreferrer"
|
|
852
|
+
},
|
|
853
|
+
["Privacy Policy"]
|
|
854
|
+
)
|
|
855
|
+
);
|
|
856
|
+
} else {
|
|
857
|
+
privacyLabelChildren.push("Privacy Policy");
|
|
858
|
+
}
|
|
859
|
+
section.appendChild(
|
|
860
|
+
el2("div", { class: "nodii-onboarding-consent-row" }, [
|
|
861
|
+
privacyCheckbox,
|
|
862
|
+
el2("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
|
|
863
|
+
])
|
|
864
|
+
);
|
|
865
|
+
section.appendChild(
|
|
866
|
+
el2("div", {
|
|
867
|
+
class: "nodii-onboarding-field-error",
|
|
868
|
+
id: "nodii-consent-privacy-error",
|
|
869
|
+
role: "alert"
|
|
870
|
+
})
|
|
871
|
+
);
|
|
872
|
+
return section;
|
|
873
|
+
}
|
|
874
|
+
function renderStep(control, state) {
|
|
875
|
+
while (stepArea.firstChild) stepArea.removeChild(stepArea.firstChild);
|
|
876
|
+
const step = state.currentStep;
|
|
877
|
+
if (step) {
|
|
878
|
+
stepArea.appendChild(
|
|
879
|
+
el2("h3", { class: "nodii-onboarding-stage-title" }, [
|
|
880
|
+
STAGE_TITLES[step.stage] ?? step.stage
|
|
881
|
+
])
|
|
882
|
+
);
|
|
883
|
+
for (const field of step.fields) {
|
|
884
|
+
const input = buildFieldInput(
|
|
885
|
+
field,
|
|
886
|
+
state.answers[field.key],
|
|
887
|
+
(value) => control.setAnswer(field.key, value)
|
|
888
|
+
);
|
|
889
|
+
stepArea.appendChild(fieldRow(field, input));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const isLast = control.isLastStep();
|
|
893
|
+
if (isLast) {
|
|
894
|
+
stepArea.appendChild(renderAccountSection(control, state));
|
|
895
|
+
}
|
|
896
|
+
const nav = el2("div", { class: "nodii-onboarding-nav" });
|
|
897
|
+
if (!control.isFirstStep()) {
|
|
898
|
+
const backBtn = el2(
|
|
899
|
+
"button",
|
|
900
|
+
{ type: "button", class: "nodii-onboarding-back-btn" },
|
|
901
|
+
["Back"]
|
|
902
|
+
);
|
|
903
|
+
backBtn.addEventListener("click", () => control.back());
|
|
904
|
+
nav.appendChild(backBtn);
|
|
905
|
+
}
|
|
906
|
+
const forwardBtn = el2(
|
|
907
|
+
"button",
|
|
908
|
+
{
|
|
909
|
+
type: isLast ? "submit" : "button",
|
|
910
|
+
class: "nodii-onboarding-submit-btn",
|
|
911
|
+
id: "nodii-signup-forward"
|
|
912
|
+
},
|
|
913
|
+
[isLast ? options.submitLabel ?? "Create account" : "Next"]
|
|
914
|
+
);
|
|
915
|
+
if (!isLast) {
|
|
916
|
+
forwardBtn.addEventListener("click", () => control.next());
|
|
917
|
+
}
|
|
918
|
+
nav.appendChild(forwardBtn);
|
|
919
|
+
stepArea.appendChild(nav);
|
|
920
|
+
}
|
|
921
|
+
async function handleSpecStale(control) {
|
|
922
|
+
const prior = currentSpecVersion !== null ? cache.get(options.onboarder, currentSpecVersion) : void 0;
|
|
923
|
+
const fresh = await fetchOnboardingSpec({
|
|
924
|
+
baseUrl: options.baseUrl,
|
|
925
|
+
onboarder: options.onboarder,
|
|
926
|
+
fetch: options.fetch,
|
|
927
|
+
timeoutMs: options.timeoutMs,
|
|
928
|
+
cache
|
|
929
|
+
});
|
|
930
|
+
if (unmounted) return;
|
|
931
|
+
if (prior) {
|
|
932
|
+
const diff = diffSpecFields(prior, fresh);
|
|
933
|
+
const parts = [];
|
|
934
|
+
if (diff.added.length) parts.push(`added: ${diff.added.join(", ")}`);
|
|
935
|
+
if (diff.removed.length)
|
|
936
|
+
parts.push(`removed: ${diff.removed.join(", ")}`);
|
|
937
|
+
if (diff.changed.length)
|
|
938
|
+
parts.push(`changed: ${diff.changed.join(", ")}`);
|
|
939
|
+
staleDiffSummary = parts.length ? ` (${parts.join("; ")})` : "";
|
|
940
|
+
} else {
|
|
941
|
+
staleDiffSummary = "";
|
|
942
|
+
}
|
|
943
|
+
siteKey = options.siteKey ?? fresh.botChallenge.siteKey;
|
|
944
|
+
currentSpecVersion = fresh.specVersion;
|
|
945
|
+
control.applyNewSpec(fresh);
|
|
946
|
+
idempotencyKey = null;
|
|
947
|
+
lastFingerprint = null;
|
|
948
|
+
}
|
|
949
|
+
async function handleSubmit(control) {
|
|
950
|
+
if (!control.validateAll()) return;
|
|
951
|
+
control.acknowledgeSpecStale();
|
|
952
|
+
control.setSubmitting();
|
|
953
|
+
const body = control.buildSubmitPayload();
|
|
954
|
+
const fingerprint = computeSignupFingerprint(body);
|
|
955
|
+
if (idempotencyKey === null || fingerprint !== lastFingerprint) {
|
|
956
|
+
idempotencyKey = generateIdempotencyKey();
|
|
957
|
+
lastFingerprint = fingerprint;
|
|
958
|
+
}
|
|
959
|
+
let firstToken;
|
|
960
|
+
try {
|
|
961
|
+
firstToken = await executeRecaptcha(siteKey, "signup_submit", {
|
|
962
|
+
timeoutMs: options.timeoutMs
|
|
963
|
+
});
|
|
964
|
+
} catch (recaptchaErr) {
|
|
965
|
+
control.setError("Could not load bot protection. Please try again.");
|
|
966
|
+
options.onError?.(recaptchaErr);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
let usedFirst = false;
|
|
970
|
+
const tokenProvider = async () => {
|
|
971
|
+
if (!usedFirst) {
|
|
972
|
+
usedFirst = true;
|
|
973
|
+
return firstToken;
|
|
974
|
+
}
|
|
975
|
+
return executeRecaptcha(siteKey, "signup_submit", {
|
|
976
|
+
timeoutMs: options.timeoutMs
|
|
977
|
+
});
|
|
978
|
+
};
|
|
979
|
+
try {
|
|
980
|
+
const result = await submitSignup(body, {
|
|
981
|
+
baseUrl: options.baseUrl,
|
|
982
|
+
botToken: tokenProvider,
|
|
983
|
+
idempotencyKey,
|
|
984
|
+
fetch: options.fetch,
|
|
985
|
+
timeoutMs: options.timeoutMs
|
|
986
|
+
});
|
|
987
|
+
idempotencyKey = null;
|
|
988
|
+
lastFingerprint = null;
|
|
989
|
+
control.setSuccess(result.terminalStage);
|
|
990
|
+
options.onSuccess?.(result);
|
|
991
|
+
} catch (err) {
|
|
992
|
+
if (err instanceof OnboardingSpecStaleError) {
|
|
993
|
+
try {
|
|
994
|
+
await handleSpecStale(control);
|
|
995
|
+
} catch (refetchErr) {
|
|
996
|
+
control.setError(
|
|
997
|
+
"The signup form changed and could not be reloaded. Please refresh the page."
|
|
998
|
+
);
|
|
999
|
+
options.onError?.(refetchErr);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
options.onError?.(err);
|
|
1003
|
+
} else if (err instanceof OnboardingBotChallengeError) {
|
|
1004
|
+
control.setError(
|
|
1005
|
+
"Bot protection check failed. Please refresh the page and try again."
|
|
1006
|
+
);
|
|
1007
|
+
options.onError?.(err);
|
|
1008
|
+
} else if (err instanceof OnboardingRateLimitError) {
|
|
1009
|
+
control.setError(
|
|
1010
|
+
err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment."
|
|
1011
|
+
);
|
|
1012
|
+
options.onError?.(err);
|
|
1013
|
+
} else {
|
|
1014
|
+
control.setError("Something went wrong. Please try again in a moment.");
|
|
1015
|
+
options.onError?.(err);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function onStateChange(control, state) {
|
|
1020
|
+
if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale) {
|
|
1021
|
+
renderedStepIndex = state.stepIndex;
|
|
1022
|
+
renderedStale = state.specStale;
|
|
1023
|
+
renderStep(control, state);
|
|
1024
|
+
renderStepsIndicator(state);
|
|
1025
|
+
}
|
|
1026
|
+
staleBanner.style.display = state.specStale ? "" : "none";
|
|
1027
|
+
staleBanner.textContent = state.specStale ? `This form was just updated by the provider${staleDiffSummary}. Please review your answers below and submit again.` : "";
|
|
1028
|
+
setFieldErrorsInPlace(
|
|
1029
|
+
stepArea,
|
|
1030
|
+
state.errors,
|
|
1031
|
+
(path) => `nodii-f-${path.split(".")[0] ?? path}`
|
|
1032
|
+
);
|
|
1033
|
+
for (const err of state.accountErrors) {
|
|
1034
|
+
const id = accountIdOf(err.path.split(".")[0] ?? err.path);
|
|
1035
|
+
const errorNode = stepArea.querySelector(idSelector(`${id}-error`));
|
|
1036
|
+
if (errorNode) errorNode.textContent = err.message;
|
|
1037
|
+
const inputNode = stepArea.querySelector(idSelector(id));
|
|
1038
|
+
if (inputNode) inputNode.classList.add("has-error");
|
|
1039
|
+
}
|
|
1040
|
+
if (state.serverError) {
|
|
1041
|
+
serverErrorEl.textContent = state.serverError;
|
|
1042
|
+
serverErrorEl.style.display = "";
|
|
1043
|
+
} else {
|
|
1044
|
+
serverErrorEl.style.display = "none";
|
|
1045
|
+
}
|
|
1046
|
+
const forwardBtn = stepArea.querySelector(
|
|
1047
|
+
"#nodii-signup-forward"
|
|
1048
|
+
);
|
|
1049
|
+
if (forwardBtn) {
|
|
1050
|
+
forwardBtn.disabled = state.status === "submitting";
|
|
1051
|
+
}
|
|
1052
|
+
if (state.status === "success") {
|
|
1053
|
+
const message = state.terminalStage === "active" ? "Your account is ready \u2014 you're all set!" : state.terminalStage === "inactive" ? "Your account was created. Complete verification to activate it." : "Thanks for signing up! Check your email to verify your account.";
|
|
1054
|
+
successEl.textContent = message;
|
|
1055
|
+
successEl.style.display = "";
|
|
1056
|
+
form.style.display = "none";
|
|
1057
|
+
} else {
|
|
1058
|
+
successEl.style.display = "none";
|
|
1059
|
+
form.style.display = "";
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
void (async () => {
|
|
1063
|
+
let spec;
|
|
1064
|
+
try {
|
|
1065
|
+
spec = await fetchOnboardingSpec({
|
|
1066
|
+
baseUrl: options.baseUrl,
|
|
1067
|
+
onboarder: options.onboarder,
|
|
1068
|
+
fetch: options.fetch,
|
|
1069
|
+
timeoutMs: options.timeoutMs,
|
|
1070
|
+
cache,
|
|
1071
|
+
// Reuse a spec already loaded for this onboarder this session (a
|
|
1072
|
+
// remount into the same page shouldn't re-hit the network). A 409 on
|
|
1073
|
+
// submit is the authoritative staleness signal that triggers refetch.
|
|
1074
|
+
preferCached: true
|
|
1075
|
+
});
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
if (unmounted) return;
|
|
1078
|
+
loadingEl.textContent = "The signup form could not be loaded. Please refresh the page.";
|
|
1079
|
+
options.onError?.(err);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (unmounted) return;
|
|
1083
|
+
siteKey = options.siteKey ?? spec.botChallenge.siteKey;
|
|
1084
|
+
currentSpecVersion = spec.specVersion;
|
|
1085
|
+
const control = createSignupStepper(spec);
|
|
1086
|
+
stepper = control;
|
|
1087
|
+
form.addEventListener("submit", (event) => {
|
|
1088
|
+
event.preventDefault();
|
|
1089
|
+
void handleSubmit(control);
|
|
1090
|
+
});
|
|
1091
|
+
form.appendChild(stepsIndicator);
|
|
1092
|
+
form.appendChild(staleBanner);
|
|
1093
|
+
form.appendChild(stepArea);
|
|
1094
|
+
form.appendChild(serverErrorEl);
|
|
1095
|
+
wrapper.removeChild(loadingEl);
|
|
1096
|
+
wrapper.appendChild(successEl);
|
|
1097
|
+
wrapper.appendChild(form);
|
|
1098
|
+
unsubscribe = control.subscribe((state) => onStateChange(control, state));
|
|
1099
|
+
onStateChange(control, control.getState());
|
|
1100
|
+
options.onReady?.(spec);
|
|
1101
|
+
})();
|
|
1102
|
+
return {
|
|
1103
|
+
unmount() {
|
|
1104
|
+
unmounted = true;
|
|
1105
|
+
unsubscribe?.();
|
|
1106
|
+
if (wrapper.parentNode === host) host.removeChild(wrapper);
|
|
1107
|
+
},
|
|
1108
|
+
reset() {
|
|
1109
|
+
idempotencyKey = null;
|
|
1110
|
+
lastFingerprint = null;
|
|
1111
|
+
renderedStepIndex = -1;
|
|
1112
|
+
renderedStale = false;
|
|
1113
|
+
if (stepper) {
|
|
1114
|
+
stepper.reset();
|
|
1115
|
+
} else {
|
|
1116
|
+
form.style.display = "";
|
|
1117
|
+
successEl.style.display = "none";
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/widget/index.ts
|
|
1124
|
+
var OnboardingWidget = {
|
|
1125
|
+
/**
|
|
1126
|
+
* Mount the onboarding widget into a host element.
|
|
1127
|
+
*
|
|
1128
|
+
* @param el - The DOM element to render into.
|
|
1129
|
+
* @param options - Widget configuration. `form: "contact"` renders the
|
|
1130
|
+
* fixed contact form (Phase 1); `form: "signup"` fetches the D540 spec
|
|
1131
|
+
* and renders the dynamic stepper (Phase 2 — siteKey comes from the
|
|
1132
|
+
* spec's botChallenge unless overridden).
|
|
1133
|
+
* @returns A mounted widget control (unmount / reset).
|
|
1134
|
+
*
|
|
1135
|
+
* @example
|
|
1136
|
+
* ```ts
|
|
1137
|
+
* import { OnboardingWidget } from "@nodii/onboarding/widget";
|
|
1138
|
+
*
|
|
1139
|
+
* OnboardingWidget.mount(document.getElementById("signup-form")!, {
|
|
1140
|
+
* onboarder: "acme-corp",
|
|
1141
|
+
* form: "signup",
|
|
1142
|
+
* baseUrl: "https://api.acme.nucleus-cloud.in",
|
|
1143
|
+
* onSuccess: (r) => console.log("landed at", r.terminalStage),
|
|
1144
|
+
* });
|
|
1145
|
+
* ```
|
|
1146
|
+
*/
|
|
1147
|
+
mount(el3, options) {
|
|
1148
|
+
if (options.form === "contact") {
|
|
1149
|
+
return mountContactForm(el3, options);
|
|
1150
|
+
}
|
|
1151
|
+
if (options.form === "signup") {
|
|
1152
|
+
return mountSignupForm(el3, options);
|
|
1153
|
+
}
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`OnboardingWidget.mount: unknown form "${options.form}". Supported: "contact", "signup".`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
export { DEFAULT_WIDGET_CSS, OnboardingWidget, cssEscape, idSelector, injectWidgetStyles, mountContactForm, mountSignupForm };
|
|
1161
|
+
//# sourceMappingURL=chunk-SWMIIUXR.js.map
|
|
1162
|
+
//# sourceMappingURL=chunk-SWMIIUXR.js.map
|