@nodii/onboarding 0.0.1 → 0.1.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/README.md +75 -2
- package/dist/chunk-7NS7ENJV.js +232 -0
- package/dist/chunk-7NS7ENJV.js.map +1 -0
- package/dist/chunk-AZE3NPSY.js +186 -0
- package/dist/chunk-AZE3NPSY.js.map +1 -0
- package/dist/chunk-BMQINH6O.js +401 -0
- package/dist/chunk-BMQINH6O.js.map +1 -0
- package/dist/chunk-F75ZJERM.js +199 -0
- package/dist/chunk-F75ZJERM.js.map +1 -0
- package/dist/chunk-FIRSA7EH.js +3 -0
- package/dist/chunk-FIRSA7EH.js.map +1 -0
- package/dist/chunk-IPAKPLIF.js +1660 -0
- package/dist/chunk-IPAKPLIF.js.map +1 -0
- package/dist/chunk-JFDQZ3AC.js +3 -0
- package/dist/chunk-JFDQZ3AC.js.map +1 -0
- package/dist/chunk-JFIONONR.js +3 -0
- package/dist/chunk-JFIONONR.js.map +1 -0
- package/dist/chunk-KQTS47FC.js +546 -0
- package/dist/chunk-KQTS47FC.js.map +1 -0
- package/dist/chunk-LVJM4CTG.js +306 -0
- package/dist/chunk-LVJM4CTG.js.map +1 -0
- package/dist/chunk-SEKNVHD3.js +59 -0
- package/dist/chunk-SEKNVHD3.js.map +1 -0
- package/dist/chunk-SIMLMQPT.js +37 -0
- package/dist/chunk-SIMLMQPT.js.map +1 -0
- package/dist/chunk-U56GQITG.js +79 -0
- package/dist/chunk-U56GQITG.js.map +1 -0
- package/dist/chunk-U6AXKGYQ.js +101 -0
- package/dist/chunk-U6AXKGYQ.js.map +1 -0
- package/dist/chunk-VH6FWDWI.js +3 -0
- package/dist/chunk-VH6FWDWI.js.map +1 -0
- package/dist/chunk-XJTJPBJB.js +191 -0
- package/dist/chunk-XJTJPBJB.js.map +1 -0
- package/dist/chunk-Z4PA6C2Z.js +3 -0
- package/dist/chunk-Z4PA6C2Z.js.map +1 -0
- package/dist/client-CqE2dF9A.d.ts +86 -0
- package/dist/consent/index.d.ts +59 -0
- package/dist/consent/index.js +7 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/controller-BQb62D0N.d.ts +177 -0
- package/dist/dynamic/index.d.ts +235 -0
- package/dist/dynamic/index.js +6 -0
- package/dist/dynamic/index.js.map +1 -0
- package/dist/errors-vJkye4a0.d.ts +110 -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-Dqfa890U.d.ts +83 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -0
- package/dist/model-ESLSEbi0.d.ts +640 -0
- package/dist/model-mnjAEtKe.d.ts +253 -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/render-DC7P2BGQ.js +5 -0
- package/dist/render-DC7P2BGQ.js.map +1 -0
- package/dist/schema-BrXb9gnh.d.ts +267 -0
- package/dist/signup-client-BL5DfarJ.d.ts +392 -0
- package/dist/spec/index.d.ts +17 -0
- package/dist/spec/index.js +5 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/submit/index.d.ts +42 -0
- package/dist/submit/index.js +6 -0
- package/dist/submit/index.js.map +1 -0
- package/dist/viewer/index.d.ts +4 -0
- package/dist/viewer/index.js +5 -0
- package/dist/viewer/index.js.map +1 -0
- package/dist/widget/index.d.ts +262 -0
- package/dist/widget/index.js +12 -0
- package/dist/widget/index.js.map +1 -0
- package/package.json +96 -6
|
@@ -0,0 +1,1660 @@
|
|
|
1
|
+
import { createContactFormState } from './chunk-U6AXKGYQ.js';
|
|
2
|
+
import { defaultSpecCache, fetchOnboardingSpec, diffSpecFields } from './chunk-F75ZJERM.js';
|
|
3
|
+
import { createSignupStepper, optionValue } from './chunk-KQTS47FC.js';
|
|
4
|
+
import { computePayloadFingerprint, generateIdempotencyKey, submitLead, computeSignupFingerprint, submitSignup } from './chunk-BMQINH6O.js';
|
|
5
|
+
import { OnboardingBotChallengeError, OnboardingRateLimitError, OnboardingSpecStaleError, OnboardingConsentStaleError, OnboardingConsentIncompleteError } from './chunk-XJTJPBJB.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
|
+
/* \u2500\u2500 D543 ConsentPack step (full-width, generous spacing) \u2500\u2500 */
|
|
248
|
+
|
|
249
|
+
.nodii-onboarding-consent-stage {
|
|
250
|
+
/* Full-bleed within the widget host \u2014 consent cards may span the page. */
|
|
251
|
+
width: 100%;
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: calc(var(--_spacing) * 2);
|
|
255
|
+
margin: calc(var(--_spacing) * 1.5) 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.nodii-onboarding-consent-intro {
|
|
259
|
+
font-size: 0.9375rem;
|
|
260
|
+
color: var(--_label);
|
|
261
|
+
margin: 0 0 calc(var(--_spacing) * 0.5) 0;
|
|
262
|
+
line-height: 1.6;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.nodii-onboarding-consent-card {
|
|
266
|
+
width: 100%;
|
|
267
|
+
border: 1px solid var(--_border);
|
|
268
|
+
border-radius: var(--_radius);
|
|
269
|
+
background: var(--_input-bg);
|
|
270
|
+
padding: calc(var(--_spacing) * 1.5);
|
|
271
|
+
display: flex;
|
|
272
|
+
flex-direction: column;
|
|
273
|
+
gap: var(--_spacing);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.nodii-onboarding-consent-card.is-stale {
|
|
277
|
+
border-color: #fcd34d;
|
|
278
|
+
background: #fffbeb;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.nodii-onboarding-consent-card.has-error {
|
|
282
|
+
border-color: var(--_error);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.nodii-onboarding-consent-title {
|
|
286
|
+
font-size: 1.125rem;
|
|
287
|
+
font-weight: 600;
|
|
288
|
+
color: var(--_label);
|
|
289
|
+
margin: 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.nodii-onboarding-consent-stale-tag {
|
|
293
|
+
font-size: 0.8125rem;
|
|
294
|
+
font-weight: 500;
|
|
295
|
+
color: #92400e;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.nodii-onboarding-consent-gate {
|
|
299
|
+
width: 100%;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.nodii-onboarding-consent-scrollbox {
|
|
303
|
+
width: 100%;
|
|
304
|
+
height: 360px;
|
|
305
|
+
overflow-y: auto;
|
|
306
|
+
border: 1px solid var(--_border);
|
|
307
|
+
border-radius: var(--_radius);
|
|
308
|
+
background: #fff;
|
|
309
|
+
position: relative;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.nodii-onboarding-consent-scrollbox:focus {
|
|
313
|
+
outline: none;
|
|
314
|
+
border-color: var(--_primary);
|
|
315
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--_primary) 20%, transparent);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Inline, same-origin document content (no iframe). Rendered by the default
|
|
319
|
+
viewer: sanitized HTML/markdown, escaped plain text, or PDF.js canvases. */
|
|
320
|
+
.nodii-onboarding-consent-doc-body {
|
|
321
|
+
padding: var(--_spacing);
|
|
322
|
+
font-size: 0.9375rem;
|
|
323
|
+
line-height: 1.7;
|
|
324
|
+
color: var(--_label);
|
|
325
|
+
word-break: break-word;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.nodii-onboarding-consent-doc-body:not(.nodii-consent-pdf) {
|
|
329
|
+
white-space: pre-wrap;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.nodii-onboarding-consent-doc-loading {
|
|
333
|
+
padding: var(--_spacing);
|
|
334
|
+
color: #6b7280;
|
|
335
|
+
font-size: 0.875rem;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.nodii-consent-pdf {
|
|
339
|
+
display: flex;
|
|
340
|
+
flex-direction: column;
|
|
341
|
+
gap: var(--_spacing);
|
|
342
|
+
white-space: normal;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.nodii-consent-pdf-page {
|
|
346
|
+
max-width: 100%;
|
|
347
|
+
height: auto;
|
|
348
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.nodii-onboarding-consent-note {
|
|
352
|
+
font-size: 0.8125rem;
|
|
353
|
+
color: #6b7280;
|
|
354
|
+
margin: calc(var(--_spacing) * 0.5) 0 0 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* A gated doc that could not be rendered inline: the gate stays unsatisfiable
|
|
358
|
+
(accept disabled); we surface a clear error + Retry. NO click-to-satisfy. */
|
|
359
|
+
.nodii-onboarding-consent-unrenderable {
|
|
360
|
+
display: flex;
|
|
361
|
+
flex-direction: column;
|
|
362
|
+
gap: calc(var(--_spacing) * 0.5);
|
|
363
|
+
padding: var(--_spacing);
|
|
364
|
+
border: 1px solid color-mix(in srgb, var(--_error) 30%, transparent);
|
|
365
|
+
border-radius: var(--_radius);
|
|
366
|
+
background: color-mix(in srgb, var(--_error) 6%, transparent);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.nodii-onboarding-consent-retry {
|
|
370
|
+
align-self: flex-start;
|
|
371
|
+
padding: calc(var(--_spacing) * 0.5) var(--_spacing);
|
|
372
|
+
background: transparent;
|
|
373
|
+
color: var(--_primary);
|
|
374
|
+
border: 1px solid var(--_primary);
|
|
375
|
+
border-radius: var(--_radius);
|
|
376
|
+
font-family: var(--_font);
|
|
377
|
+
font-size: 0.875rem;
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.nodii-onboarding-consent-open-row,
|
|
382
|
+
.nodii-onboarding-consent-preview-row {
|
|
383
|
+
display: flex;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.nodii-onboarding-consent-open {
|
|
387
|
+
display: inline-flex;
|
|
388
|
+
align-items: center;
|
|
389
|
+
padding: calc(var(--_spacing) * 0.625) var(--_spacing);
|
|
390
|
+
border: 1px solid var(--_primary);
|
|
391
|
+
border-radius: var(--_radius);
|
|
392
|
+
color: var(--_primary);
|
|
393
|
+
text-decoration: none;
|
|
394
|
+
font-size: 0.9375rem;
|
|
395
|
+
font-weight: 500;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.nodii-onboarding-consent-open:hover {
|
|
399
|
+
background: color-mix(in srgb, var(--_primary) 8%, transparent);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.nodii-onboarding-consent-preview {
|
|
403
|
+
font-size: 0.875rem;
|
|
404
|
+
color: var(--_primary);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.nodii-onboarding-consent-accept-row {
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: flex-start;
|
|
410
|
+
gap: calc(var(--_spacing) * 0.5);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.nodii-onboarding-consent-accept {
|
|
414
|
+
margin-top: 3px;
|
|
415
|
+
width: 1.125rem;
|
|
416
|
+
height: 1.125rem;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.nodii-onboarding-consent-accept:disabled {
|
|
420
|
+
cursor: not-allowed;
|
|
421
|
+
opacity: 0.5;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.nodii-onboarding-consent-accept-label {
|
|
425
|
+
font-size: 0.9375rem;
|
|
426
|
+
color: var(--_label);
|
|
427
|
+
line-height: 1.5;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.nodii-onboarding-consent-required {
|
|
431
|
+
color: var(--_error);
|
|
432
|
+
}
|
|
433
|
+
`;
|
|
434
|
+
function injectWidgetStyles() {
|
|
435
|
+
if (typeof document === "undefined") return;
|
|
436
|
+
const styleId = "__nodii_onboarding_styles__";
|
|
437
|
+
if (document.getElementById(styleId)) return;
|
|
438
|
+
const style = document.createElement("style");
|
|
439
|
+
style.id = styleId;
|
|
440
|
+
style.textContent = DEFAULT_WIDGET_CSS;
|
|
441
|
+
document.head.appendChild(style);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/widget/contact-form.ts
|
|
445
|
+
function el(tag, attrs = {}, children = []) {
|
|
446
|
+
const element = document.createElement(tag);
|
|
447
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
448
|
+
element.setAttribute(key, value);
|
|
449
|
+
}
|
|
450
|
+
for (const child of children) {
|
|
451
|
+
if (typeof child === "string") {
|
|
452
|
+
element.appendChild(document.createTextNode(child));
|
|
453
|
+
} else {
|
|
454
|
+
element.appendChild(child);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return element;
|
|
458
|
+
}
|
|
459
|
+
function createFieldEl(id, label, input, required) {
|
|
460
|
+
const labelEl = el(
|
|
461
|
+
"label",
|
|
462
|
+
{ for: id, class: `nodii-onboarding-label${required ? " required" : ""}` },
|
|
463
|
+
[label]
|
|
464
|
+
);
|
|
465
|
+
const errorEl = el("div", {
|
|
466
|
+
class: "nodii-onboarding-field-error",
|
|
467
|
+
id: `${id}-error`,
|
|
468
|
+
role: "alert",
|
|
469
|
+
"aria-live": "polite"
|
|
470
|
+
});
|
|
471
|
+
const field = el("div", { class: "nodii-onboarding-field" }, [
|
|
472
|
+
labelEl,
|
|
473
|
+
input,
|
|
474
|
+
errorEl
|
|
475
|
+
]);
|
|
476
|
+
return field;
|
|
477
|
+
}
|
|
478
|
+
function setFieldError(container, fieldId, message) {
|
|
479
|
+
const errorEl = container.querySelector(idSelector(`${fieldId}-error`));
|
|
480
|
+
if (errorEl) errorEl.textContent = message;
|
|
481
|
+
const inputEl = container.querySelector(idSelector(fieldId));
|
|
482
|
+
if (inputEl) inputEl.classList.add("has-error");
|
|
483
|
+
}
|
|
484
|
+
function clearFieldErrors(container) {
|
|
485
|
+
for (const el4 of container.querySelectorAll(
|
|
486
|
+
".nodii-onboarding-field-error"
|
|
487
|
+
)) {
|
|
488
|
+
el4.textContent = "";
|
|
489
|
+
}
|
|
490
|
+
for (const el4 of container.querySelectorAll(".has-error")) {
|
|
491
|
+
el4.classList.remove("has-error");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function applyErrors(container, errors) {
|
|
495
|
+
clearFieldErrors(container);
|
|
496
|
+
for (const err of errors) {
|
|
497
|
+
setFieldError(container, `nodii-${err.path}`, err.message);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function mountContactForm(host, options) {
|
|
501
|
+
injectWidgetStyles();
|
|
502
|
+
const formControl = createContactFormState();
|
|
503
|
+
if (options.siteKey) {
|
|
504
|
+
loadRecaptcha(options.siteKey).catch(() => {
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const wrapper = el("div", { class: "nodii-onboarding-widget" });
|
|
508
|
+
const nameInput = el("input", {
|
|
509
|
+
type: "text",
|
|
510
|
+
id: "nodii-name",
|
|
511
|
+
name: "name",
|
|
512
|
+
class: "nodii-onboarding-input",
|
|
513
|
+
placeholder: "Your name",
|
|
514
|
+
autocomplete: "name",
|
|
515
|
+
maxlength: "200",
|
|
516
|
+
required: "required"
|
|
517
|
+
});
|
|
518
|
+
const emailInput = el("input", {
|
|
519
|
+
type: "email",
|
|
520
|
+
id: "nodii-email",
|
|
521
|
+
name: "email",
|
|
522
|
+
class: "nodii-onboarding-input",
|
|
523
|
+
placeholder: "you@company.com",
|
|
524
|
+
autocomplete: "email",
|
|
525
|
+
required: "required"
|
|
526
|
+
});
|
|
527
|
+
const phoneInput = el("input", {
|
|
528
|
+
type: "tel",
|
|
529
|
+
id: "nodii-phone",
|
|
530
|
+
name: "phone",
|
|
531
|
+
class: "nodii-onboarding-input",
|
|
532
|
+
placeholder: "+1 555 000 0000",
|
|
533
|
+
autocomplete: "tel"
|
|
534
|
+
});
|
|
535
|
+
const messageInput = el("textarea", {
|
|
536
|
+
id: "nodii-message",
|
|
537
|
+
name: "message",
|
|
538
|
+
class: "nodii-onboarding-textarea",
|
|
539
|
+
placeholder: "How can we help?",
|
|
540
|
+
maxlength: "4000",
|
|
541
|
+
rows: "4"
|
|
542
|
+
});
|
|
543
|
+
const serverErrorEl = el("div", {
|
|
544
|
+
class: "nodii-onboarding-server-error",
|
|
545
|
+
role: "alert",
|
|
546
|
+
"aria-live": "assertive",
|
|
547
|
+
style: "display:none"
|
|
548
|
+
});
|
|
549
|
+
const successEl = el(
|
|
550
|
+
"div",
|
|
551
|
+
{
|
|
552
|
+
class: "nodii-onboarding-success",
|
|
553
|
+
role: "status",
|
|
554
|
+
style: "display:none"
|
|
555
|
+
},
|
|
556
|
+
[options.successMessage ?? "Thank you! We'll be in touch shortly."]
|
|
557
|
+
);
|
|
558
|
+
const submitBtn = el(
|
|
559
|
+
"button",
|
|
560
|
+
{
|
|
561
|
+
type: "submit",
|
|
562
|
+
class: "nodii-onboarding-submit-btn"
|
|
563
|
+
},
|
|
564
|
+
[options.submitLabel ?? "Get in touch"]
|
|
565
|
+
);
|
|
566
|
+
const recaptchaNotice = el(
|
|
567
|
+
"p",
|
|
568
|
+
{ class: "nodii-onboarding-recaptcha-notice" },
|
|
569
|
+
[
|
|
570
|
+
"Protected by reCAPTCHA. ",
|
|
571
|
+
Object.assign(
|
|
572
|
+
el("a", {
|
|
573
|
+
href: "https://policies.google.com/privacy",
|
|
574
|
+
target: "_blank",
|
|
575
|
+
rel: "noopener noreferrer"
|
|
576
|
+
}),
|
|
577
|
+
{ textContent: "Privacy" }
|
|
578
|
+
),
|
|
579
|
+
" & ",
|
|
580
|
+
Object.assign(
|
|
581
|
+
el("a", {
|
|
582
|
+
href: "https://policies.google.com/terms",
|
|
583
|
+
target: "_blank",
|
|
584
|
+
rel: "noopener noreferrer"
|
|
585
|
+
}),
|
|
586
|
+
{ textContent: "Terms" }
|
|
587
|
+
)
|
|
588
|
+
]
|
|
589
|
+
);
|
|
590
|
+
const form = el("form", {
|
|
591
|
+
novalidate: "true",
|
|
592
|
+
"aria-label": "Contact us"
|
|
593
|
+
});
|
|
594
|
+
form.appendChild(createFieldEl("nodii-name", "Name", nameInput, true));
|
|
595
|
+
form.appendChild(createFieldEl("nodii-email", "Email", emailInput, true));
|
|
596
|
+
form.appendChild(
|
|
597
|
+
createFieldEl("nodii-phone", "Phone (optional)", phoneInput, false)
|
|
598
|
+
);
|
|
599
|
+
form.appendChild(
|
|
600
|
+
createFieldEl("nodii-message", "Message (optional)", messageInput, false)
|
|
601
|
+
);
|
|
602
|
+
form.appendChild(serverErrorEl);
|
|
603
|
+
form.appendChild(submitBtn);
|
|
604
|
+
form.appendChild(recaptchaNotice);
|
|
605
|
+
wrapper.appendChild(successEl);
|
|
606
|
+
wrapper.appendChild(form);
|
|
607
|
+
host.appendChild(wrapper);
|
|
608
|
+
nameInput.addEventListener("input", () => {
|
|
609
|
+
formControl.setValue("name", nameInput.value);
|
|
610
|
+
});
|
|
611
|
+
emailInput.addEventListener("input", () => {
|
|
612
|
+
formControl.setValue("email", emailInput.value);
|
|
613
|
+
});
|
|
614
|
+
phoneInput.addEventListener("input", () => {
|
|
615
|
+
formControl.setValue("phone", phoneInput.value || void 0);
|
|
616
|
+
});
|
|
617
|
+
messageInput.addEventListener("input", () => {
|
|
618
|
+
formControl.setValue("message", messageInput.value || void 0);
|
|
619
|
+
});
|
|
620
|
+
const submitLabel = options.submitLabel ?? "Get in touch";
|
|
621
|
+
function renderSubmitButton(submitting) {
|
|
622
|
+
while (submitBtn.firstChild) {
|
|
623
|
+
submitBtn.removeChild(submitBtn.firstChild);
|
|
624
|
+
}
|
|
625
|
+
if (submitting) {
|
|
626
|
+
const spinner = document.createElement("span");
|
|
627
|
+
spinner.className = "nodii-onboarding-spinner";
|
|
628
|
+
submitBtn.appendChild(spinner);
|
|
629
|
+
submitBtn.appendChild(document.createTextNode("Sending\u2026"));
|
|
630
|
+
} else {
|
|
631
|
+
submitBtn.appendChild(document.createTextNode(submitLabel));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const unsubscribe = formControl.subscribe((state) => {
|
|
635
|
+
submitBtn.disabled = state.status === "submitting";
|
|
636
|
+
renderSubmitButton(state.status === "submitting");
|
|
637
|
+
applyErrors(wrapper, state.errors);
|
|
638
|
+
if (state.serverError) {
|
|
639
|
+
serverErrorEl.textContent = state.serverError;
|
|
640
|
+
serverErrorEl.style.display = "";
|
|
641
|
+
} else {
|
|
642
|
+
serverErrorEl.style.display = "none";
|
|
643
|
+
}
|
|
644
|
+
if (state.status === "success") {
|
|
645
|
+
successEl.style.display = "";
|
|
646
|
+
form.style.display = "none";
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
let idempotencyKey = null;
|
|
650
|
+
let lastFingerprint = null;
|
|
651
|
+
form.addEventListener("submit", async (event) => {
|
|
652
|
+
event.preventDefault();
|
|
653
|
+
formControl.setValue("name", nameInput.value);
|
|
654
|
+
formControl.setValue("email", emailInput.value);
|
|
655
|
+
formControl.setValue("phone", phoneInput.value || void 0);
|
|
656
|
+
formControl.setValue("message", messageInput.value || void 0);
|
|
657
|
+
const valid = formControl.validate();
|
|
658
|
+
if (!valid) return;
|
|
659
|
+
formControl.setSubmitting();
|
|
660
|
+
const values = formControl.getState().values;
|
|
661
|
+
const fingerprint = computePayloadFingerprint(values);
|
|
662
|
+
if (idempotencyKey === null || fingerprint !== lastFingerprint) {
|
|
663
|
+
idempotencyKey = generateIdempotencyKey();
|
|
664
|
+
lastFingerprint = fingerprint;
|
|
665
|
+
}
|
|
666
|
+
let firstToken;
|
|
667
|
+
if (options.siteKey) {
|
|
668
|
+
try {
|
|
669
|
+
firstToken = await executeRecaptcha(options.siteKey, "contact_submit", {
|
|
670
|
+
timeoutMs: options.timeoutMs
|
|
671
|
+
});
|
|
672
|
+
} catch (recaptchaErr) {
|
|
673
|
+
formControl.setError(
|
|
674
|
+
"Could not load bot protection. Please try again."
|
|
675
|
+
);
|
|
676
|
+
options.onError?.(recaptchaErr);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
let usedFirst = false;
|
|
681
|
+
const tokenProvider = async () => {
|
|
682
|
+
if (!usedFirst) {
|
|
683
|
+
usedFirst = true;
|
|
684
|
+
return firstToken;
|
|
685
|
+
}
|
|
686
|
+
return executeRecaptcha(options.siteKey, "contact_submit", {
|
|
687
|
+
timeoutMs: options.timeoutMs
|
|
688
|
+
});
|
|
689
|
+
};
|
|
690
|
+
try {
|
|
691
|
+
await submitLead(values, {
|
|
692
|
+
baseUrl: options.baseUrl,
|
|
693
|
+
...options.siteKey ? { botToken: tokenProvider } : {},
|
|
694
|
+
idempotencyKey,
|
|
695
|
+
fetch: options.fetch,
|
|
696
|
+
timeoutMs: options.timeoutMs
|
|
697
|
+
});
|
|
698
|
+
idempotencyKey = null;
|
|
699
|
+
lastFingerprint = null;
|
|
700
|
+
formControl.setSuccess();
|
|
701
|
+
options.onSuccess?.();
|
|
702
|
+
} catch (err) {
|
|
703
|
+
if (err instanceof OnboardingBotChallengeError) {
|
|
704
|
+
formControl.setError(
|
|
705
|
+
"Bot protection check failed. Please refresh the page and try again."
|
|
706
|
+
);
|
|
707
|
+
options.onError?.(err);
|
|
708
|
+
} else if (err instanceof OnboardingRateLimitError) {
|
|
709
|
+
const msg = err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment.";
|
|
710
|
+
formControl.setError(msg);
|
|
711
|
+
options.onError?.(err);
|
|
712
|
+
} else {
|
|
713
|
+
formControl.setError(
|
|
714
|
+
"Something went wrong. Please try again in a moment."
|
|
715
|
+
);
|
|
716
|
+
options.onError?.(err);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
return {
|
|
721
|
+
unmount() {
|
|
722
|
+
unsubscribe();
|
|
723
|
+
host.removeChild(wrapper);
|
|
724
|
+
},
|
|
725
|
+
reset() {
|
|
726
|
+
idempotencyKey = null;
|
|
727
|
+
lastFingerprint = null;
|
|
728
|
+
formControl.reset();
|
|
729
|
+
form.style.display = "";
|
|
730
|
+
successEl.style.display = "none";
|
|
731
|
+
nameInput.value = "";
|
|
732
|
+
emailInput.value = "";
|
|
733
|
+
phoneInput.value = "";
|
|
734
|
+
messageInput.value = "";
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/widget/consent-step.ts
|
|
740
|
+
var CONSENT_ID_PREFIX = "nodii-consent-doc-";
|
|
741
|
+
function el2(tag, attrs = {}, children = []) {
|
|
742
|
+
const element = document.createElement(tag);
|
|
743
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
744
|
+
element.setAttribute(key, value);
|
|
745
|
+
}
|
|
746
|
+
for (const child of children) {
|
|
747
|
+
if (typeof child === "string") {
|
|
748
|
+
element.appendChild(document.createTextNode(child));
|
|
749
|
+
} else {
|
|
750
|
+
element.appendChild(child);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return element;
|
|
754
|
+
}
|
|
755
|
+
function consentDocElementId(key) {
|
|
756
|
+
return `${CONSENT_ID_PREFIX}${key.replace(/[^A-Za-z0-9_-]/g, "-")}`;
|
|
757
|
+
}
|
|
758
|
+
function buildAcceptRow(control, doc) {
|
|
759
|
+
const id = consentDocElementId(doc.key);
|
|
760
|
+
const checkbox = el2("input", {
|
|
761
|
+
type: "checkbox",
|
|
762
|
+
id,
|
|
763
|
+
class: "nodii-onboarding-consent-accept"
|
|
764
|
+
});
|
|
765
|
+
checkbox.checked = doc.accepted;
|
|
766
|
+
checkbox.disabled = !doc.gateSatisfied;
|
|
767
|
+
checkbox.addEventListener("change", () => {
|
|
768
|
+
control.setAccepted(doc.key, checkbox.checked);
|
|
769
|
+
checkbox.checked = control.getState().documents.find((d) => d.key === doc.key)?.accepted ?? false;
|
|
770
|
+
});
|
|
771
|
+
const labelText = doc.acceptanceLevel === "checkbox" ? `I accept the ${doc.doc.title}` : `I have reviewed and accept the ${doc.doc.title}`;
|
|
772
|
+
const label = el2(
|
|
773
|
+
"label",
|
|
774
|
+
{ for: id, class: "nodii-onboarding-consent-accept-label" },
|
|
775
|
+
[labelText]
|
|
776
|
+
);
|
|
777
|
+
if (doc.required) {
|
|
778
|
+
label.appendChild(
|
|
779
|
+
el2("span", { class: "nodii-onboarding-consent-required" }, [" *"])
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
const row = el2("div", { class: "nodii-onboarding-consent-accept-row" }, [
|
|
783
|
+
checkbox,
|
|
784
|
+
label
|
|
785
|
+
]);
|
|
786
|
+
const errorEl = el2("div", {
|
|
787
|
+
class: "nodii-onboarding-field-error nodii-onboarding-consent-error",
|
|
788
|
+
id: `${id}-error`,
|
|
789
|
+
role: "alert",
|
|
790
|
+
"aria-live": "polite"
|
|
791
|
+
});
|
|
792
|
+
return { row, errorEl };
|
|
793
|
+
}
|
|
794
|
+
function buildOpenAffordance(control, doc) {
|
|
795
|
+
const id = consentDocElementId(doc.key);
|
|
796
|
+
const openLink = el2(
|
|
797
|
+
"a",
|
|
798
|
+
{
|
|
799
|
+
class: "nodii-onboarding-consent-open",
|
|
800
|
+
id: `${id}-open`,
|
|
801
|
+
href: doc.doc.downloadUrl,
|
|
802
|
+
target: "_blank",
|
|
803
|
+
rel: "noopener noreferrer"
|
|
804
|
+
},
|
|
805
|
+
[`Open ${doc.doc.title}`]
|
|
806
|
+
);
|
|
807
|
+
const markOpened = () => control.markOpened(doc.key);
|
|
808
|
+
openLink.addEventListener("click", markOpened);
|
|
809
|
+
openLink.addEventListener("keydown", (event) => {
|
|
810
|
+
if (event.key === "Enter") markOpened();
|
|
811
|
+
});
|
|
812
|
+
return el2("div", { class: "nodii-onboarding-consent-open-row" }, [openLink]);
|
|
813
|
+
}
|
|
814
|
+
function buildGateAffordance(control, doc, opts) {
|
|
815
|
+
const id = consentDocElementId(doc.key);
|
|
816
|
+
if (doc.acceptanceLevel === "open_then_accept") {
|
|
817
|
+
return buildOpenAffordance(control, doc);
|
|
818
|
+
}
|
|
819
|
+
if (doc.acceptanceLevel === "checkbox") {
|
|
820
|
+
return el2("div", { class: "nodii-onboarding-consent-preview-row" }, [
|
|
821
|
+
el2(
|
|
822
|
+
"a",
|
|
823
|
+
{
|
|
824
|
+
class: "nodii-onboarding-consent-preview",
|
|
825
|
+
id: `${id}-preview`,
|
|
826
|
+
href: doc.doc.downloadUrl,
|
|
827
|
+
target: "_blank",
|
|
828
|
+
rel: "noopener noreferrer"
|
|
829
|
+
},
|
|
830
|
+
[`View ${doc.doc.title}`]
|
|
831
|
+
)
|
|
832
|
+
]);
|
|
833
|
+
}
|
|
834
|
+
const container = el2("div", {
|
|
835
|
+
class: "nodii-onboarding-consent-gate",
|
|
836
|
+
id: `${id}-gate`
|
|
837
|
+
});
|
|
838
|
+
container.appendChild(
|
|
839
|
+
el2("div", { class: "nodii-onboarding-consent-doc-loading" }, [
|
|
840
|
+
"Loading document\u2026"
|
|
841
|
+
])
|
|
842
|
+
);
|
|
843
|
+
const importViewer = opts.importViewer ?? (() => import('./render-DC7P2BGQ.js'));
|
|
844
|
+
const loadViewer = () => {
|
|
845
|
+
void (async () => {
|
|
846
|
+
try {
|
|
847
|
+
const { renderConsentDocument } = await importViewer();
|
|
848
|
+
await renderConsentDocument(container, doc.doc, control, {
|
|
849
|
+
consentKey: doc.key,
|
|
850
|
+
fetch: opts.fetch,
|
|
851
|
+
timeoutMs: opts.timeoutMs
|
|
852
|
+
});
|
|
853
|
+
} catch {
|
|
854
|
+
while (container.firstChild) {
|
|
855
|
+
container.removeChild(container.firstChild);
|
|
856
|
+
}
|
|
857
|
+
const wrap = el2("div", {
|
|
858
|
+
class: "nodii-onboarding-consent-unrenderable",
|
|
859
|
+
role: "alert"
|
|
860
|
+
});
|
|
861
|
+
wrap.appendChild(
|
|
862
|
+
el2(
|
|
863
|
+
"p",
|
|
864
|
+
{
|
|
865
|
+
// DISTINCT id (NOT `${id}-error`): buildAcceptRow already owns
|
|
866
|
+
// `${id}-error`, and updateConsentStep's querySelector would match
|
|
867
|
+
// THIS paragraph first and blank it on the next state change,
|
|
868
|
+
// silently wiping the compliance message. A unique id keeps this
|
|
869
|
+
// load-failure error persistent (P1b).
|
|
870
|
+
class: "nodii-onboarding-field-error nodii-onboarding-consent-error",
|
|
871
|
+
id: `${id}-load-error`
|
|
872
|
+
},
|
|
873
|
+
[
|
|
874
|
+
`\u201C${doc.doc.title}\u201D could not be loaded, so it cannot be accepted yet. Please retry.`
|
|
875
|
+
]
|
|
876
|
+
)
|
|
877
|
+
);
|
|
878
|
+
const retry = el2(
|
|
879
|
+
"button",
|
|
880
|
+
{ type: "button", class: "nodii-onboarding-consent-retry" },
|
|
881
|
+
["Retry loading document"]
|
|
882
|
+
);
|
|
883
|
+
retry.addEventListener("click", loadViewer);
|
|
884
|
+
wrap.appendChild(retry);
|
|
885
|
+
wrap.appendChild(
|
|
886
|
+
el2(
|
|
887
|
+
"a",
|
|
888
|
+
{
|
|
889
|
+
class: "nodii-onboarding-consent-open",
|
|
890
|
+
href: doc.doc.downloadUrl,
|
|
891
|
+
target: "_blank",
|
|
892
|
+
rel: "noopener noreferrer"
|
|
893
|
+
},
|
|
894
|
+
[`Open ${doc.doc.title} in a new tab`]
|
|
895
|
+
)
|
|
896
|
+
);
|
|
897
|
+
container.appendChild(wrap);
|
|
898
|
+
}
|
|
899
|
+
})();
|
|
900
|
+
};
|
|
901
|
+
loadViewer();
|
|
902
|
+
return container;
|
|
903
|
+
}
|
|
904
|
+
function buildConsentCard(control, doc, stale, opts) {
|
|
905
|
+
const card = el2("div", {
|
|
906
|
+
class: `nodii-onboarding-consent-card${stale ? " is-stale" : ""}`,
|
|
907
|
+
"data-consent-key": doc.key
|
|
908
|
+
});
|
|
909
|
+
card.appendChild(
|
|
910
|
+
el2("h4", { class: "nodii-onboarding-consent-title" }, [doc.doc.title])
|
|
911
|
+
);
|
|
912
|
+
if (stale) {
|
|
913
|
+
card.appendChild(
|
|
914
|
+
el2(
|
|
915
|
+
"div",
|
|
916
|
+
{ class: "nodii-onboarding-consent-stale-tag", role: "status" },
|
|
917
|
+
["Updated \u2014 please review and re-accept."]
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
card.appendChild(buildGateAffordance(control, doc, opts));
|
|
922
|
+
const { row, errorEl } = buildAcceptRow(control, doc);
|
|
923
|
+
card.appendChild(row);
|
|
924
|
+
card.appendChild(errorEl);
|
|
925
|
+
return card;
|
|
926
|
+
}
|
|
927
|
+
function renderConsentStep(host, control, state, staleKeys = [], opts = {}) {
|
|
928
|
+
while (host.firstChild) host.removeChild(host.firstChild);
|
|
929
|
+
const staleSet = new Set(staleKeys);
|
|
930
|
+
host.appendChild(
|
|
931
|
+
el2("h3", { class: "nodii-onboarding-stage-title" }, [
|
|
932
|
+
"Review & accept documents"
|
|
933
|
+
])
|
|
934
|
+
);
|
|
935
|
+
host.appendChild(
|
|
936
|
+
el2("p", { class: "nodii-onboarding-consent-intro" }, [
|
|
937
|
+
"Please review each document below and accept to continue."
|
|
938
|
+
])
|
|
939
|
+
);
|
|
940
|
+
for (const doc of state.documents) {
|
|
941
|
+
host.appendChild(
|
|
942
|
+
buildConsentCard(control, doc, staleSet.has(doc.key), opts)
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function updateConsentStep(host, state, consentErrors) {
|
|
947
|
+
const errorByKey = new Map(consentErrors.map((e) => [e.path, e.message]));
|
|
948
|
+
for (const doc of state.documents) {
|
|
949
|
+
const id = consentDocElementId(doc.key);
|
|
950
|
+
const checkbox = host.querySelector(
|
|
951
|
+
idSelector(id)
|
|
952
|
+
);
|
|
953
|
+
if (checkbox) {
|
|
954
|
+
checkbox.disabled = !doc.gateSatisfied;
|
|
955
|
+
checkbox.checked = doc.accepted;
|
|
956
|
+
}
|
|
957
|
+
const errorNode = host.querySelector(idSelector(`${id}-error`));
|
|
958
|
+
if (errorNode) errorNode.textContent = errorByKey.get(doc.key) ?? "";
|
|
959
|
+
const card = host.querySelector(
|
|
960
|
+
`[data-consent-key="${cssEscape(doc.key)}"]`
|
|
961
|
+
);
|
|
962
|
+
if (card) {
|
|
963
|
+
if (errorByKey.has(doc.key)) card.classList.add("has-error");
|
|
964
|
+
else card.classList.remove("has-error");
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/widget/signup-form.ts
|
|
970
|
+
var STAGE_TITLES = {
|
|
971
|
+
lead: "About you",
|
|
972
|
+
prospect: "Your business",
|
|
973
|
+
activation: "Choose your services"
|
|
974
|
+
};
|
|
975
|
+
function el3(tag, attrs = {}, children = []) {
|
|
976
|
+
const element = document.createElement(tag);
|
|
977
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
978
|
+
element.setAttribute(key, value);
|
|
979
|
+
}
|
|
980
|
+
for (const child of children) {
|
|
981
|
+
if (typeof child === "string") {
|
|
982
|
+
element.appendChild(document.createTextNode(child));
|
|
983
|
+
} else {
|
|
984
|
+
element.appendChild(child);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return element;
|
|
988
|
+
}
|
|
989
|
+
function optionLabel(option) {
|
|
990
|
+
if (option !== null && typeof option === "object") {
|
|
991
|
+
const o = option;
|
|
992
|
+
if (typeof o["label"] === "string") return o["label"];
|
|
993
|
+
}
|
|
994
|
+
return optionValue(option);
|
|
995
|
+
}
|
|
996
|
+
function buildFieldInput(field, currentValue, onChange) {
|
|
997
|
+
const id = `nodii-f-${field.key}`;
|
|
998
|
+
switch (field.type) {
|
|
999
|
+
case "textarea": {
|
|
1000
|
+
const input = el3("textarea", {
|
|
1001
|
+
id,
|
|
1002
|
+
class: "nodii-onboarding-textarea",
|
|
1003
|
+
rows: "4"
|
|
1004
|
+
});
|
|
1005
|
+
if (typeof currentValue === "string") input.value = currentValue;
|
|
1006
|
+
input.addEventListener("input", () => onChange(input.value));
|
|
1007
|
+
return input;
|
|
1008
|
+
}
|
|
1009
|
+
case "checkbox":
|
|
1010
|
+
case "boolean": {
|
|
1011
|
+
const input = el3("input", {
|
|
1012
|
+
type: "checkbox",
|
|
1013
|
+
id
|
|
1014
|
+
});
|
|
1015
|
+
input.checked = currentValue === true;
|
|
1016
|
+
input.addEventListener("change", () => onChange(input.checked));
|
|
1017
|
+
return input;
|
|
1018
|
+
}
|
|
1019
|
+
case "select":
|
|
1020
|
+
case "radio": {
|
|
1021
|
+
const select = el3("select", {
|
|
1022
|
+
id,
|
|
1023
|
+
class: "nodii-onboarding-input"
|
|
1024
|
+
});
|
|
1025
|
+
select.appendChild(el3("option", { value: "" }, ["\u2014 select \u2014"]));
|
|
1026
|
+
for (const opt of field.options ?? []) {
|
|
1027
|
+
const value = optionValue(opt);
|
|
1028
|
+
const optionEl = el3("option", { value }, [optionLabel(opt)]);
|
|
1029
|
+
select.appendChild(optionEl);
|
|
1030
|
+
}
|
|
1031
|
+
if (typeof currentValue === "string") select.value = currentValue;
|
|
1032
|
+
select.addEventListener("change", () => onChange(select.value));
|
|
1033
|
+
return select;
|
|
1034
|
+
}
|
|
1035
|
+
case "multiselect": {
|
|
1036
|
+
const select = el3("select", {
|
|
1037
|
+
id,
|
|
1038
|
+
class: "nodii-onboarding-input",
|
|
1039
|
+
multiple: "multiple"
|
|
1040
|
+
});
|
|
1041
|
+
const current = Array.isArray(currentValue) ? currentValue.map(String) : [];
|
|
1042
|
+
for (const opt of field.options ?? []) {
|
|
1043
|
+
const value = optionValue(opt);
|
|
1044
|
+
const optionEl = el3("option", { value }, [
|
|
1045
|
+
optionLabel(opt)
|
|
1046
|
+
]);
|
|
1047
|
+
if (current.includes(value)) optionEl.selected = true;
|
|
1048
|
+
select.appendChild(optionEl);
|
|
1049
|
+
}
|
|
1050
|
+
select.addEventListener("change", () => {
|
|
1051
|
+
const values = [];
|
|
1052
|
+
for (const o of Array.from(select.options)) {
|
|
1053
|
+
if (o.selected && o.value !== "") values.push(o.value);
|
|
1054
|
+
}
|
|
1055
|
+
onChange(values);
|
|
1056
|
+
});
|
|
1057
|
+
return select;
|
|
1058
|
+
}
|
|
1059
|
+
default: {
|
|
1060
|
+
const typeAttr = field.type === "email" || field.type === "tel" || field.type === "number" || field.type === "date" ? field.type : "text";
|
|
1061
|
+
const input = el3("input", {
|
|
1062
|
+
type: typeAttr,
|
|
1063
|
+
id,
|
|
1064
|
+
class: "nodii-onboarding-input"
|
|
1065
|
+
});
|
|
1066
|
+
if (typeof currentValue === "string") input.value = currentValue;
|
|
1067
|
+
if (typeof currentValue === "number") input.value = String(currentValue);
|
|
1068
|
+
input.addEventListener("input", () => onChange(input.value));
|
|
1069
|
+
return input;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function fieldRow(field, input, idOverride) {
|
|
1074
|
+
const id = idOverride ?? `nodii-f-${field.key}`;
|
|
1075
|
+
const labelEl = el3(
|
|
1076
|
+
"label",
|
|
1077
|
+
{
|
|
1078
|
+
for: id,
|
|
1079
|
+
class: `nodii-onboarding-label${field.required ? " required" : ""}`
|
|
1080
|
+
},
|
|
1081
|
+
[field.label]
|
|
1082
|
+
);
|
|
1083
|
+
const errorEl = el3("div", {
|
|
1084
|
+
class: "nodii-onboarding-field-error",
|
|
1085
|
+
id: `${id}-error`,
|
|
1086
|
+
role: "alert",
|
|
1087
|
+
"aria-live": "polite"
|
|
1088
|
+
});
|
|
1089
|
+
return el3("div", { class: "nodii-onboarding-field" }, [
|
|
1090
|
+
labelEl,
|
|
1091
|
+
input,
|
|
1092
|
+
errorEl
|
|
1093
|
+
]);
|
|
1094
|
+
}
|
|
1095
|
+
function mountSignupForm(host, options) {
|
|
1096
|
+
injectWidgetStyles();
|
|
1097
|
+
const wrapper = el3("div", { class: "nodii-onboarding-widget" });
|
|
1098
|
+
const loadingEl = el3("div", { class: "nodii-onboarding-loading" }, [
|
|
1099
|
+
"Loading signup form\u2026"
|
|
1100
|
+
]);
|
|
1101
|
+
wrapper.appendChild(loadingEl);
|
|
1102
|
+
host.appendChild(wrapper);
|
|
1103
|
+
const cache = options.cache ?? defaultSpecCache;
|
|
1104
|
+
let stepper = null;
|
|
1105
|
+
let unsubscribe = null;
|
|
1106
|
+
let unmounted = false;
|
|
1107
|
+
let siteKey = options.siteKey ?? "";
|
|
1108
|
+
let idempotencyKey = null;
|
|
1109
|
+
let lastFingerprint = null;
|
|
1110
|
+
let renderedStepIndex = -1;
|
|
1111
|
+
let renderedStale = false;
|
|
1112
|
+
let renderedConsentStale = false;
|
|
1113
|
+
let currentSpecVersion = null;
|
|
1114
|
+
let staleDiffSummary = "";
|
|
1115
|
+
const stepArea = el3("div", {});
|
|
1116
|
+
const staleBanner = el3("div", {
|
|
1117
|
+
class: "nodii-onboarding-stale-banner",
|
|
1118
|
+
role: "alert",
|
|
1119
|
+
style: "display:none"
|
|
1120
|
+
});
|
|
1121
|
+
const serverErrorEl = el3("div", {
|
|
1122
|
+
class: "nodii-onboarding-server-error",
|
|
1123
|
+
role: "alert",
|
|
1124
|
+
"aria-live": "assertive",
|
|
1125
|
+
style: "display:none"
|
|
1126
|
+
});
|
|
1127
|
+
const successEl = el3("div", {
|
|
1128
|
+
class: "nodii-onboarding-success",
|
|
1129
|
+
role: "status",
|
|
1130
|
+
style: "display:none"
|
|
1131
|
+
});
|
|
1132
|
+
const stepsIndicator = el3("div", { class: "nodii-onboarding-steps" });
|
|
1133
|
+
const form = el3("form", { novalidate: "true", "aria-label": "Sign up" });
|
|
1134
|
+
function setFieldErrorsInPlace(container, errors, idPrefix) {
|
|
1135
|
+
for (const errEl of container.querySelectorAll(
|
|
1136
|
+
".nodii-onboarding-field-error"
|
|
1137
|
+
)) {
|
|
1138
|
+
errEl.textContent = "";
|
|
1139
|
+
}
|
|
1140
|
+
for (const marked of container.querySelectorAll(".has-error")) {
|
|
1141
|
+
marked.classList.remove("has-error");
|
|
1142
|
+
}
|
|
1143
|
+
for (const err of errors) {
|
|
1144
|
+
const id = idPrefix(err.path);
|
|
1145
|
+
const errorNode = container.querySelector(idSelector(`${id}-error`));
|
|
1146
|
+
if (errorNode) errorNode.textContent = err.message;
|
|
1147
|
+
const inputNode = container.querySelector(idSelector(id));
|
|
1148
|
+
if (inputNode) inputNode.classList.add("has-error");
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function accountIdOf(path) {
|
|
1152
|
+
if (path === "email") return "nodii-account-email";
|
|
1153
|
+
if (path === "displayName") return "nodii-account-display-name";
|
|
1154
|
+
if (path === "tosAccepted") return "nodii-consent-tos";
|
|
1155
|
+
if (path === "privacyAccepted") return "nodii-consent-privacy";
|
|
1156
|
+
return `nodii-account-${path}`;
|
|
1157
|
+
}
|
|
1158
|
+
function renderStepsIndicator(state) {
|
|
1159
|
+
while (stepsIndicator.firstChild) {
|
|
1160
|
+
stepsIndicator.removeChild(stepsIndicator.firstChild);
|
|
1161
|
+
}
|
|
1162
|
+
for (let i = 0; i < state.stepCount; i++) {
|
|
1163
|
+
stepsIndicator.appendChild(
|
|
1164
|
+
el3("div", {
|
|
1165
|
+
class: `nodii-onboarding-step-dot${i <= state.stepIndex ? " active" : ""}`
|
|
1166
|
+
})
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
function renderAccountSection(control, state) {
|
|
1171
|
+
const section = el3("div", { class: "nodii-onboarding-account-section" });
|
|
1172
|
+
const emailInput = el3("input", {
|
|
1173
|
+
type: "email",
|
|
1174
|
+
id: "nodii-account-email",
|
|
1175
|
+
class: "nodii-onboarding-input",
|
|
1176
|
+
autocomplete: "email"
|
|
1177
|
+
});
|
|
1178
|
+
emailInput.value = state.credentials.email;
|
|
1179
|
+
emailInput.addEventListener(
|
|
1180
|
+
"input",
|
|
1181
|
+
() => control.setCredentials({ email: emailInput.value })
|
|
1182
|
+
);
|
|
1183
|
+
section.appendChild(
|
|
1184
|
+
fieldRow(
|
|
1185
|
+
{
|
|
1186
|
+
key: "account-email",
|
|
1187
|
+
label: "Account email",
|
|
1188
|
+
required: true
|
|
1189
|
+
},
|
|
1190
|
+
emailInput,
|
|
1191
|
+
"nodii-account-email"
|
|
1192
|
+
// label for + error id match the real input id
|
|
1193
|
+
)
|
|
1194
|
+
);
|
|
1195
|
+
const displayNameInput = el3("input", {
|
|
1196
|
+
type: "text",
|
|
1197
|
+
id: "nodii-account-display-name",
|
|
1198
|
+
class: "nodii-onboarding-input",
|
|
1199
|
+
autocomplete: "name",
|
|
1200
|
+
maxlength: "200"
|
|
1201
|
+
});
|
|
1202
|
+
displayNameInput.value = state.credentials.displayName;
|
|
1203
|
+
displayNameInput.addEventListener(
|
|
1204
|
+
"input",
|
|
1205
|
+
() => control.setCredentials({ displayName: displayNameInput.value })
|
|
1206
|
+
);
|
|
1207
|
+
section.appendChild(
|
|
1208
|
+
fieldRow(
|
|
1209
|
+
{
|
|
1210
|
+
key: "account-display-name",
|
|
1211
|
+
label: "Your name",
|
|
1212
|
+
required: true
|
|
1213
|
+
},
|
|
1214
|
+
displayNameInput,
|
|
1215
|
+
"nodii-account-display-name"
|
|
1216
|
+
)
|
|
1217
|
+
);
|
|
1218
|
+
for (const [key, label, autocomplete] of [
|
|
1219
|
+
["organisationName", "Organisation name", "organization"],
|
|
1220
|
+
["subdomain", "Workspace subdomain", "off"]
|
|
1221
|
+
]) {
|
|
1222
|
+
const input = el3("input", {
|
|
1223
|
+
type: "text",
|
|
1224
|
+
id: `nodii-account-${key}`,
|
|
1225
|
+
class: "nodii-onboarding-input",
|
|
1226
|
+
autocomplete,
|
|
1227
|
+
maxlength: key === "subdomain" ? "63" : "200"
|
|
1228
|
+
});
|
|
1229
|
+
input.value = state.credentials[key];
|
|
1230
|
+
input.addEventListener(
|
|
1231
|
+
"input",
|
|
1232
|
+
() => control.setCredentials({ [key]: input.value })
|
|
1233
|
+
);
|
|
1234
|
+
section.appendChild(
|
|
1235
|
+
fieldRow(
|
|
1236
|
+
{ key: `account-${key}`, label, required: true },
|
|
1237
|
+
input,
|
|
1238
|
+
`nodii-account-${key}`
|
|
1239
|
+
)
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const tosCheckbox = el3("input", {
|
|
1243
|
+
type: "checkbox",
|
|
1244
|
+
id: "nodii-consent-tos"
|
|
1245
|
+
});
|
|
1246
|
+
tosCheckbox.checked = state.consent.tosAccepted;
|
|
1247
|
+
tosCheckbox.addEventListener(
|
|
1248
|
+
"change",
|
|
1249
|
+
() => control.setConsent({ tosAccepted: tosCheckbox.checked })
|
|
1250
|
+
);
|
|
1251
|
+
const tosLabelChildren = ["I accept the "];
|
|
1252
|
+
if (options.tosUrl) {
|
|
1253
|
+
tosLabelChildren.push(
|
|
1254
|
+
el3(
|
|
1255
|
+
"a",
|
|
1256
|
+
{
|
|
1257
|
+
href: options.tosUrl,
|
|
1258
|
+
target: "_blank",
|
|
1259
|
+
rel: "noopener noreferrer"
|
|
1260
|
+
},
|
|
1261
|
+
["Terms of Service"]
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
1264
|
+
} else {
|
|
1265
|
+
tosLabelChildren.push("Terms of Service");
|
|
1266
|
+
}
|
|
1267
|
+
section.appendChild(
|
|
1268
|
+
el3("div", { class: "nodii-onboarding-consent-row" }, [
|
|
1269
|
+
tosCheckbox,
|
|
1270
|
+
el3("label", { for: "nodii-consent-tos" }, tosLabelChildren)
|
|
1271
|
+
])
|
|
1272
|
+
);
|
|
1273
|
+
section.appendChild(
|
|
1274
|
+
el3("div", {
|
|
1275
|
+
class: "nodii-onboarding-field-error",
|
|
1276
|
+
id: "nodii-consent-tos-error",
|
|
1277
|
+
role: "alert"
|
|
1278
|
+
})
|
|
1279
|
+
);
|
|
1280
|
+
const privacyCheckbox = el3("input", {
|
|
1281
|
+
type: "checkbox",
|
|
1282
|
+
id: "nodii-consent-privacy"
|
|
1283
|
+
});
|
|
1284
|
+
privacyCheckbox.checked = state.consent.privacyAccepted;
|
|
1285
|
+
privacyCheckbox.addEventListener(
|
|
1286
|
+
"change",
|
|
1287
|
+
() => control.setConsent({ privacyAccepted: privacyCheckbox.checked })
|
|
1288
|
+
);
|
|
1289
|
+
const privacyLabelChildren = ["I accept the "];
|
|
1290
|
+
if (options.privacyUrl) {
|
|
1291
|
+
privacyLabelChildren.push(
|
|
1292
|
+
el3(
|
|
1293
|
+
"a",
|
|
1294
|
+
{
|
|
1295
|
+
href: options.privacyUrl,
|
|
1296
|
+
target: "_blank",
|
|
1297
|
+
rel: "noopener noreferrer"
|
|
1298
|
+
},
|
|
1299
|
+
["Privacy Policy"]
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
} else {
|
|
1303
|
+
privacyLabelChildren.push("Privacy Policy");
|
|
1304
|
+
}
|
|
1305
|
+
section.appendChild(
|
|
1306
|
+
el3("div", { class: "nodii-onboarding-consent-row" }, [
|
|
1307
|
+
privacyCheckbox,
|
|
1308
|
+
el3("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
|
|
1309
|
+
])
|
|
1310
|
+
);
|
|
1311
|
+
section.appendChild(
|
|
1312
|
+
el3("div", {
|
|
1313
|
+
class: "nodii-onboarding-field-error",
|
|
1314
|
+
id: "nodii-consent-privacy-error",
|
|
1315
|
+
role: "alert"
|
|
1316
|
+
})
|
|
1317
|
+
);
|
|
1318
|
+
return section;
|
|
1319
|
+
}
|
|
1320
|
+
function renderStep(control, state) {
|
|
1321
|
+
while (stepArea.firstChild) stepArea.removeChild(stepArea.firstChild);
|
|
1322
|
+
const step = state.currentStep;
|
|
1323
|
+
if (step) {
|
|
1324
|
+
stepArea.appendChild(
|
|
1325
|
+
el3("h3", { class: "nodii-onboarding-stage-title" }, [
|
|
1326
|
+
STAGE_TITLES[step.stage] ?? step.stage
|
|
1327
|
+
])
|
|
1328
|
+
);
|
|
1329
|
+
for (const field of step.fields) {
|
|
1330
|
+
const input = buildFieldInput(
|
|
1331
|
+
field,
|
|
1332
|
+
state.answers[field.key],
|
|
1333
|
+
(value) => control.setAnswer(field.key, value)
|
|
1334
|
+
);
|
|
1335
|
+
stepArea.appendChild(fieldRow(field, input));
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
const isLast = control.isLastStep();
|
|
1339
|
+
if (isLast) {
|
|
1340
|
+
if (!control.consent.isEmpty()) {
|
|
1341
|
+
const consentStage = el3("div", {
|
|
1342
|
+
class: "nodii-onboarding-consent-stage"
|
|
1343
|
+
});
|
|
1344
|
+
renderConsentStep(
|
|
1345
|
+
consentStage,
|
|
1346
|
+
control.consent,
|
|
1347
|
+
state.consentState,
|
|
1348
|
+
state.staleConsentKeys,
|
|
1349
|
+
{ fetch: options.fetch, timeoutMs: options.timeoutMs }
|
|
1350
|
+
);
|
|
1351
|
+
stepArea.appendChild(consentStage);
|
|
1352
|
+
}
|
|
1353
|
+
stepArea.appendChild(renderAccountSection(control, state));
|
|
1354
|
+
}
|
|
1355
|
+
const nav = el3("div", { class: "nodii-onboarding-nav" });
|
|
1356
|
+
if (!control.isFirstStep()) {
|
|
1357
|
+
const backBtn = el3(
|
|
1358
|
+
"button",
|
|
1359
|
+
{ type: "button", class: "nodii-onboarding-back-btn" },
|
|
1360
|
+
["Back"]
|
|
1361
|
+
);
|
|
1362
|
+
backBtn.addEventListener("click", () => control.back());
|
|
1363
|
+
nav.appendChild(backBtn);
|
|
1364
|
+
}
|
|
1365
|
+
const forwardBtn = el3(
|
|
1366
|
+
"button",
|
|
1367
|
+
{
|
|
1368
|
+
type: isLast ? "submit" : "button",
|
|
1369
|
+
class: "nodii-onboarding-submit-btn",
|
|
1370
|
+
id: "nodii-signup-forward"
|
|
1371
|
+
},
|
|
1372
|
+
[isLast ? options.submitLabel ?? "Create account" : "Next"]
|
|
1373
|
+
);
|
|
1374
|
+
if (!isLast) {
|
|
1375
|
+
forwardBtn.addEventListener("click", () => control.next());
|
|
1376
|
+
}
|
|
1377
|
+
nav.appendChild(forwardBtn);
|
|
1378
|
+
stepArea.appendChild(nav);
|
|
1379
|
+
}
|
|
1380
|
+
async function handleSpecStale(control) {
|
|
1381
|
+
const prior = currentSpecVersion !== null ? cache.get(options.onboarder, currentSpecVersion) : void 0;
|
|
1382
|
+
const fresh = await fetchOnboardingSpec({
|
|
1383
|
+
baseUrl: options.baseUrl,
|
|
1384
|
+
onboarder: options.onboarder,
|
|
1385
|
+
fetch: options.fetch,
|
|
1386
|
+
timeoutMs: options.timeoutMs,
|
|
1387
|
+
cache
|
|
1388
|
+
});
|
|
1389
|
+
if (unmounted) return;
|
|
1390
|
+
if (prior) {
|
|
1391
|
+
const diff = diffSpecFields(prior, fresh);
|
|
1392
|
+
const parts = [];
|
|
1393
|
+
if (diff.added.length) parts.push(`added: ${diff.added.join(", ")}`);
|
|
1394
|
+
if (diff.removed.length)
|
|
1395
|
+
parts.push(`removed: ${diff.removed.join(", ")}`);
|
|
1396
|
+
if (diff.changed.length)
|
|
1397
|
+
parts.push(`changed: ${diff.changed.join(", ")}`);
|
|
1398
|
+
staleDiffSummary = parts.length ? ` (${parts.join("; ")})` : "";
|
|
1399
|
+
} else {
|
|
1400
|
+
staleDiffSummary = "";
|
|
1401
|
+
}
|
|
1402
|
+
siteKey = options.siteKey ?? fresh.botChallenge?.siteKey ?? "";
|
|
1403
|
+
currentSpecVersion = fresh.specVersion;
|
|
1404
|
+
control.applyNewSpec(fresh);
|
|
1405
|
+
idempotencyKey = null;
|
|
1406
|
+
lastFingerprint = null;
|
|
1407
|
+
}
|
|
1408
|
+
async function handleConsentStale(control) {
|
|
1409
|
+
const fresh = await fetchOnboardingSpec({
|
|
1410
|
+
baseUrl: options.baseUrl,
|
|
1411
|
+
onboarder: options.onboarder,
|
|
1412
|
+
fetch: options.fetch,
|
|
1413
|
+
timeoutMs: options.timeoutMs,
|
|
1414
|
+
cache
|
|
1415
|
+
});
|
|
1416
|
+
if (unmounted) return;
|
|
1417
|
+
siteKey = options.siteKey ?? fresh.botChallenge?.siteKey ?? "";
|
|
1418
|
+
currentSpecVersion = fresh.specVersion;
|
|
1419
|
+
control.applyNewConsentPack(fresh);
|
|
1420
|
+
idempotencyKey = null;
|
|
1421
|
+
lastFingerprint = null;
|
|
1422
|
+
}
|
|
1423
|
+
async function handleSubmit(control) {
|
|
1424
|
+
if (!control.validateAll()) return;
|
|
1425
|
+
control.acknowledgeSpecStale();
|
|
1426
|
+
control.setSubmitting();
|
|
1427
|
+
const body = control.buildSubmitPayload();
|
|
1428
|
+
const fingerprint = computeSignupFingerprint(body);
|
|
1429
|
+
if (idempotencyKey === null || fingerprint !== lastFingerprint) {
|
|
1430
|
+
idempotencyKey = generateIdempotencyKey();
|
|
1431
|
+
lastFingerprint = fingerprint;
|
|
1432
|
+
}
|
|
1433
|
+
let firstToken;
|
|
1434
|
+
if (siteKey) {
|
|
1435
|
+
try {
|
|
1436
|
+
firstToken = await executeRecaptcha(siteKey, "signup_submit", {
|
|
1437
|
+
timeoutMs: options.timeoutMs
|
|
1438
|
+
});
|
|
1439
|
+
} catch (recaptchaErr) {
|
|
1440
|
+
control.setError("Could not load bot protection. Please try again.");
|
|
1441
|
+
options.onError?.(recaptchaErr);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
let usedFirst = false;
|
|
1446
|
+
const tokenProvider = async () => {
|
|
1447
|
+
if (!usedFirst) {
|
|
1448
|
+
usedFirst = true;
|
|
1449
|
+
return firstToken;
|
|
1450
|
+
}
|
|
1451
|
+
return executeRecaptcha(siteKey, "signup_submit", {
|
|
1452
|
+
timeoutMs: options.timeoutMs
|
|
1453
|
+
});
|
|
1454
|
+
};
|
|
1455
|
+
try {
|
|
1456
|
+
const result = await submitSignup(body, {
|
|
1457
|
+
baseUrl: options.baseUrl,
|
|
1458
|
+
...siteKey ? { botToken: tokenProvider } : {},
|
|
1459
|
+
idempotencyKey,
|
|
1460
|
+
fetch: options.fetch,
|
|
1461
|
+
timeoutMs: options.timeoutMs
|
|
1462
|
+
});
|
|
1463
|
+
idempotencyKey = null;
|
|
1464
|
+
lastFingerprint = null;
|
|
1465
|
+
control.setSuccess(result.terminalStage);
|
|
1466
|
+
options.onSuccess?.(result);
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
if (err instanceof OnboardingSpecStaleError) {
|
|
1469
|
+
try {
|
|
1470
|
+
await handleSpecStale(control);
|
|
1471
|
+
} catch (refetchErr) {
|
|
1472
|
+
control.setError(
|
|
1473
|
+
"The signup form changed and could not be reloaded. Please refresh the page."
|
|
1474
|
+
);
|
|
1475
|
+
options.onError?.(refetchErr);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
options.onError?.(err);
|
|
1479
|
+
} else if (err instanceof OnboardingConsentStaleError) {
|
|
1480
|
+
try {
|
|
1481
|
+
await handleConsentStale(control);
|
|
1482
|
+
} catch (refetchErr) {
|
|
1483
|
+
control.setError(
|
|
1484
|
+
"The legal documents changed and could not be reloaded. Please refresh the page."
|
|
1485
|
+
);
|
|
1486
|
+
options.onError?.(refetchErr);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
options.onError?.(err);
|
|
1490
|
+
} else if (err instanceof OnboardingConsentIncompleteError) {
|
|
1491
|
+
control.validateAll();
|
|
1492
|
+
control.setError("Please accept all required documents to continue.");
|
|
1493
|
+
options.onError?.(err);
|
|
1494
|
+
} else if (err instanceof OnboardingBotChallengeError) {
|
|
1495
|
+
control.setError(
|
|
1496
|
+
"Bot protection check failed. Please refresh the page and try again."
|
|
1497
|
+
);
|
|
1498
|
+
options.onError?.(err);
|
|
1499
|
+
} else if (err instanceof OnboardingRateLimitError) {
|
|
1500
|
+
control.setError(
|
|
1501
|
+
err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment."
|
|
1502
|
+
);
|
|
1503
|
+
options.onError?.(err);
|
|
1504
|
+
} else {
|
|
1505
|
+
control.setError("Something went wrong. Please try again in a moment.");
|
|
1506
|
+
options.onError?.(err);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function onStateChange(control, state) {
|
|
1511
|
+
if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale || state.consentStale !== renderedConsentStale) {
|
|
1512
|
+
renderedStepIndex = state.stepIndex;
|
|
1513
|
+
renderedStale = state.specStale;
|
|
1514
|
+
renderedConsentStale = state.consentStale;
|
|
1515
|
+
renderStep(control, state);
|
|
1516
|
+
renderStepsIndicator(state);
|
|
1517
|
+
}
|
|
1518
|
+
staleBanner.style.display = state.specStale || state.consentStale ? "" : "none";
|
|
1519
|
+
staleBanner.textContent = state.specStale ? `This form was just updated by the provider${staleDiffSummary}. Please review your answers below and submit again.` : state.consentStale ? "One or more legal documents were updated. Please review and re-accept the updated documents below, then submit again." : "";
|
|
1520
|
+
setFieldErrorsInPlace(
|
|
1521
|
+
stepArea,
|
|
1522
|
+
state.errors,
|
|
1523
|
+
(path) => `nodii-f-${path.split(".")[0] ?? path}`
|
|
1524
|
+
);
|
|
1525
|
+
for (const err of state.accountErrors) {
|
|
1526
|
+
const id = accountIdOf(err.path.split(".")[0] ?? err.path);
|
|
1527
|
+
const errorNode = stepArea.querySelector(idSelector(`${id}-error`));
|
|
1528
|
+
if (errorNode) errorNode.textContent = err.message;
|
|
1529
|
+
const inputNode = stepArea.querySelector(idSelector(id));
|
|
1530
|
+
if (inputNode) inputNode.classList.add("has-error");
|
|
1531
|
+
}
|
|
1532
|
+
const consentStage = stepArea.querySelector(
|
|
1533
|
+
".nodii-onboarding-consent-stage"
|
|
1534
|
+
);
|
|
1535
|
+
if (consentStage) {
|
|
1536
|
+
updateConsentStep(consentStage, state.consentState, state.consentErrors);
|
|
1537
|
+
}
|
|
1538
|
+
if (state.serverError) {
|
|
1539
|
+
serverErrorEl.textContent = state.serverError;
|
|
1540
|
+
serverErrorEl.style.display = "";
|
|
1541
|
+
} else {
|
|
1542
|
+
serverErrorEl.style.display = "none";
|
|
1543
|
+
}
|
|
1544
|
+
const forwardBtn = stepArea.querySelector(
|
|
1545
|
+
"#nodii-signup-forward"
|
|
1546
|
+
);
|
|
1547
|
+
if (forwardBtn) {
|
|
1548
|
+
forwardBtn.disabled = state.status === "submitting";
|
|
1549
|
+
}
|
|
1550
|
+
if (state.status === "success") {
|
|
1551
|
+
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.";
|
|
1552
|
+
successEl.textContent = message;
|
|
1553
|
+
successEl.style.display = "";
|
|
1554
|
+
form.style.display = "none";
|
|
1555
|
+
} else {
|
|
1556
|
+
successEl.style.display = "none";
|
|
1557
|
+
form.style.display = "";
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
void (async () => {
|
|
1561
|
+
let spec;
|
|
1562
|
+
try {
|
|
1563
|
+
spec = await fetchOnboardingSpec({
|
|
1564
|
+
baseUrl: options.baseUrl,
|
|
1565
|
+
onboarder: options.onboarder,
|
|
1566
|
+
fetch: options.fetch,
|
|
1567
|
+
timeoutMs: options.timeoutMs,
|
|
1568
|
+
cache,
|
|
1569
|
+
// Reuse a spec already loaded for this onboarder this session (a
|
|
1570
|
+
// remount into the same page shouldn't re-hit the network). A 409 on
|
|
1571
|
+
// submit is the authoritative staleness signal that triggers refetch.
|
|
1572
|
+
preferCached: true
|
|
1573
|
+
});
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
if (unmounted) return;
|
|
1576
|
+
loadingEl.textContent = "The signup form could not be loaded. Please refresh the page.";
|
|
1577
|
+
options.onError?.(err);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (unmounted) return;
|
|
1581
|
+
siteKey = options.siteKey ?? spec.botChallenge?.siteKey ?? "";
|
|
1582
|
+
currentSpecVersion = spec.specVersion;
|
|
1583
|
+
const control = createSignupStepper(spec);
|
|
1584
|
+
stepper = control;
|
|
1585
|
+
form.addEventListener("submit", (event) => {
|
|
1586
|
+
event.preventDefault();
|
|
1587
|
+
void handleSubmit(control);
|
|
1588
|
+
});
|
|
1589
|
+
form.appendChild(stepsIndicator);
|
|
1590
|
+
form.appendChild(staleBanner);
|
|
1591
|
+
form.appendChild(stepArea);
|
|
1592
|
+
form.appendChild(serverErrorEl);
|
|
1593
|
+
wrapper.removeChild(loadingEl);
|
|
1594
|
+
wrapper.appendChild(successEl);
|
|
1595
|
+
wrapper.appendChild(form);
|
|
1596
|
+
unsubscribe = control.subscribe((state) => onStateChange(control, state));
|
|
1597
|
+
onStateChange(control, control.getState());
|
|
1598
|
+
options.onReady?.(spec);
|
|
1599
|
+
})();
|
|
1600
|
+
return {
|
|
1601
|
+
unmount() {
|
|
1602
|
+
unmounted = true;
|
|
1603
|
+
unsubscribe?.();
|
|
1604
|
+
if (wrapper.parentNode === host) host.removeChild(wrapper);
|
|
1605
|
+
},
|
|
1606
|
+
reset() {
|
|
1607
|
+
idempotencyKey = null;
|
|
1608
|
+
lastFingerprint = null;
|
|
1609
|
+
renderedStepIndex = -1;
|
|
1610
|
+
renderedStale = false;
|
|
1611
|
+
if (stepper) {
|
|
1612
|
+
stepper.reset();
|
|
1613
|
+
} else {
|
|
1614
|
+
form.style.display = "";
|
|
1615
|
+
successEl.style.display = "none";
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/widget/index.ts
|
|
1622
|
+
var OnboardingWidget = {
|
|
1623
|
+
/**
|
|
1624
|
+
* Mount the onboarding widget into a host element.
|
|
1625
|
+
*
|
|
1626
|
+
* @param el - The DOM element to render into.
|
|
1627
|
+
* @param options - Widget configuration. `form: "contact"` renders the
|
|
1628
|
+
* fixed contact form (Phase 1); `form: "signup"` fetches the D540 spec
|
|
1629
|
+
* and renders the dynamic stepper (Phase 2 — siteKey comes from the
|
|
1630
|
+
* spec's botChallenge unless overridden).
|
|
1631
|
+
* @returns A mounted widget control (unmount / reset).
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```ts
|
|
1635
|
+
* import { OnboardingWidget } from "@nodii/onboarding/widget";
|
|
1636
|
+
*
|
|
1637
|
+
* OnboardingWidget.mount(document.getElementById("signup-form")!, {
|
|
1638
|
+
* onboarder: "acme-corp",
|
|
1639
|
+
* form: "signup",
|
|
1640
|
+
* baseUrl: "https://api.acme.nucleus-cloud.in",
|
|
1641
|
+
* onSuccess: (r) => console.log("landed at", r.terminalStage),
|
|
1642
|
+
* });
|
|
1643
|
+
* ```
|
|
1644
|
+
*/
|
|
1645
|
+
mount(el4, options) {
|
|
1646
|
+
if (options.form === "contact") {
|
|
1647
|
+
return mountContactForm(el4, options);
|
|
1648
|
+
}
|
|
1649
|
+
if (options.form === "signup") {
|
|
1650
|
+
return mountSignupForm(el4, options);
|
|
1651
|
+
}
|
|
1652
|
+
throw new Error(
|
|
1653
|
+
`OnboardingWidget.mount: unknown form "${options.form}". Supported: "contact", "signup".`
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
export { DEFAULT_WIDGET_CSS, OnboardingWidget, consentDocElementId, cssEscape, idSelector, injectWidgetStyles, mountContactForm, mountSignupForm, renderConsentStep, updateConsentStep };
|
|
1659
|
+
//# sourceMappingURL=chunk-IPAKPLIF.js.map
|
|
1660
|
+
//# sourceMappingURL=chunk-IPAKPLIF.js.map
|