@nodii/onboarding 0.1.0-dev.0 → 0.2.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.
Files changed (68) hide show
  1. package/README.md +124 -0
  2. package/dist/{chunk-4SWOVHZ2.js → chunk-A3DE2IAH.js} +155 -18
  3. package/dist/chunk-A3DE2IAH.js.map +1 -0
  4. package/dist/{chunk-AFCJLUPK.js → chunk-DWFZA5T6.js} +19 -8
  5. package/dist/chunk-DWFZA5T6.js.map +1 -0
  6. package/dist/chunk-FIRSA7EH.js +3 -0
  7. package/dist/{chunk-ZDY2I6KY.js.map → chunk-FIRSA7EH.js.map} +1 -1
  8. package/dist/chunk-JFDQZ3AC.js +3 -0
  9. package/dist/chunk-JFDQZ3AC.js.map +1 -0
  10. package/dist/{chunk-34BQX2KR.js → chunk-KFHVZGX6.js} +61 -6
  11. package/dist/chunk-KFHVZGX6.js.map +1 -0
  12. package/dist/chunk-NDSBIB3N.js +99 -0
  13. package/dist/chunk-NDSBIB3N.js.map +1 -0
  14. package/dist/chunk-OLWR5COK.js +10 -0
  15. package/dist/chunk-OLWR5COK.js.map +1 -0
  16. package/dist/{chunk-SWMIIUXR.js → chunk-OVULPWNZ.js} +917 -158
  17. package/dist/chunk-OVULPWNZ.js.map +1 -0
  18. package/dist/chunk-SEKNVHD3.js +59 -0
  19. package/dist/chunk-SEKNVHD3.js.map +1 -0
  20. package/dist/chunk-T7EWURTF.js +321 -0
  21. package/dist/chunk-T7EWURTF.js.map +1 -0
  22. package/dist/{chunk-BQCTDU5V.js → chunk-UHOFGWI7.js} +90 -15
  23. package/dist/chunk-UHOFGWI7.js.map +1 -0
  24. package/dist/chunk-WKN4U7QF.js +35 -0
  25. package/dist/chunk-WKN4U7QF.js.map +1 -0
  26. package/dist/{chunk-AW7W3HAC.js → chunk-XJTJPBJB.js} +40 -3
  27. package/dist/chunk-XJTJPBJB.js.map +1 -0
  28. package/dist/chunk-ZR6X3GSM.js +186 -0
  29. package/dist/chunk-ZR6X3GSM.js.map +1 -0
  30. package/dist/client-NvqPpfQP.d.ts +86 -0
  31. package/dist/consent/index.d.ts +59 -0
  32. package/dist/consent/index.js +7 -0
  33. package/dist/consent/index.js.map +1 -0
  34. package/dist/controller-BLH3wnxl.d.ts +177 -0
  35. package/dist/dynamic/index.d.ts +65 -6
  36. package/dist/dynamic/index.js +5 -2
  37. package/dist/errors-vJkye4a0.d.ts +110 -0
  38. package/dist/form/index.d.ts +12 -4
  39. package/dist/form/index.js +2 -2
  40. package/dist/index-BMSbQGWP.d.ts +103 -0
  41. package/dist/index.d.ts +13 -7
  42. package/dist/index.js +13 -8
  43. package/dist/model-BI_I-D_V.d.ts +267 -0
  44. package/dist/{model-IEyDI54w.d.ts → model-CEwvHlWA.d.ts} +212 -46
  45. package/dist/render-EWQXF73I.js +5 -0
  46. package/dist/render-EWQXF73I.js.map +1 -0
  47. package/dist/schema-BrtjpXfL.d.ts +257 -0
  48. package/dist/{signup-client-Bl6eUrxR.d.ts → signup-client-CS95nkVO.d.ts} +152 -19
  49. package/dist/spec/index.d.ts +10 -81
  50. package/dist/spec/index.js +3 -2
  51. package/dist/submit/index.d.ts +7 -71
  52. package/dist/submit/index.js +4 -3
  53. package/dist/viewer/index.d.ts +4 -0
  54. package/dist/viewer/index.js +5 -0
  55. package/dist/viewer/index.js.map +1 -0
  56. package/dist/widget/index.d.ts +102 -20
  57. package/dist/widget/index.js +10 -7
  58. package/package.json +21 -2
  59. package/dist/chunk-34BQX2KR.js.map +0 -1
  60. package/dist/chunk-4SWOVHZ2.js.map +0 -1
  61. package/dist/chunk-AFCJLUPK.js.map +0 -1
  62. package/dist/chunk-AW7W3HAC.js.map +0 -1
  63. package/dist/chunk-BIFANV7M.js +0 -22
  64. package/dist/chunk-BIFANV7M.js.map +0 -1
  65. package/dist/chunk-BQCTDU5V.js.map +0 -1
  66. package/dist/chunk-SWMIIUXR.js.map +0 -1
  67. package/dist/chunk-ZDY2I6KY.js +0 -3
  68. package/dist/schema-TfyBU6JW.d.ts +0 -107
@@ -1,8 +1,10 @@
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';
1
+ import { createContactFormState } from './chunk-DWFZA5T6.js';
2
+ import { fetchOnboardingSpec, OnboardingSpecCache, defaultSpecCache, diffSpecFields } from './chunk-KFHVZGX6.js';
3
+ import { createSignupStepper, optionValue } from './chunk-A3DE2IAH.js';
4
+ import { isNavigableUrl } from './chunk-NDSBIB3N.js';
5
+ import { computePayloadFingerprint, generateIdempotencyKey, submitLead, computeSignupFingerprint, submitSignup } from './chunk-UHOFGWI7.js';
6
+ import { contactFormSchema } from './chunk-WKN4U7QF.js';
7
+ import { OnboardingBotChallengeError, OnboardingRateLimitError, OnboardingSpecStaleError, OnboardingConsentStaleError, OnboardingConsentIncompleteError, OnboardingSubdomainTakenError, OnboardingValidationError } from './chunk-XJTJPBJB.js';
6
8
  import { loadRecaptcha, executeRecaptcha } from './chunk-7NS7ENJV.js';
7
9
 
8
10
  // src/widget/dom-utils.ts
@@ -219,6 +221,35 @@ var DEFAULT_WIDGET_CSS = `
219
221
  margin-bottom: var(--_spacing);
220
222
  }
221
223
 
224
+ fieldset.nodii-onboarding-field {
225
+ border: none;
226
+ padding: 0;
227
+ margin-inline: 0;
228
+ }
229
+
230
+ .nodii-onboarding-choices {
231
+ display: flex;
232
+ flex-wrap: wrap;
233
+ gap: calc(var(--_spacing) * 0.5);
234
+ }
235
+
236
+ .nodii-onboarding-choice {
237
+ display: inline-flex;
238
+ align-items: center;
239
+ gap: calc(var(--_spacing) * 0.375);
240
+ padding: calc(var(--_spacing) * 0.375) calc(var(--_spacing) * 0.75);
241
+ border: 1px solid var(--_border);
242
+ border-radius: 999px;
243
+ background: var(--_input-bg);
244
+ font-size: 0.875rem;
245
+ color: var(--_label);
246
+ cursor: pointer;
247
+ }
248
+
249
+ fieldset.nodii-onboarding-field.has-error .nodii-onboarding-choice {
250
+ border-color: var(--_error);
251
+ }
252
+
222
253
  .nodii-onboarding-consent-row {
223
254
  display: flex;
224
255
  align-items: flex-start;
@@ -243,6 +274,193 @@ var DEFAULT_WIDGET_CSS = `
243
274
  text-align: center;
244
275
  color: var(--_label);
245
276
  }
277
+
278
+ /* \u2500\u2500 D543 ConsentPack step (full-width, generous spacing) \u2500\u2500 */
279
+
280
+ .nodii-onboarding-consent-stage {
281
+ /* Full-bleed within the widget host \u2014 consent cards may span the page. */
282
+ width: 100%;
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: calc(var(--_spacing) * 2);
286
+ margin: calc(var(--_spacing) * 1.5) 0;
287
+ }
288
+
289
+ .nodii-onboarding-consent-intro {
290
+ font-size: 0.9375rem;
291
+ color: var(--_label);
292
+ margin: 0 0 calc(var(--_spacing) * 0.5) 0;
293
+ line-height: 1.6;
294
+ }
295
+
296
+ .nodii-onboarding-consent-card {
297
+ width: 100%;
298
+ border: 1px solid var(--_border);
299
+ border-radius: var(--_radius);
300
+ background: var(--_input-bg);
301
+ padding: calc(var(--_spacing) * 1.5);
302
+ display: flex;
303
+ flex-direction: column;
304
+ gap: var(--_spacing);
305
+ }
306
+
307
+ .nodii-onboarding-consent-card.is-stale {
308
+ border-color: #fcd34d;
309
+ background: #fffbeb;
310
+ }
311
+
312
+ .nodii-onboarding-consent-card.has-error {
313
+ border-color: var(--_error);
314
+ }
315
+
316
+ .nodii-onboarding-consent-title {
317
+ font-size: 1.125rem;
318
+ font-weight: 600;
319
+ color: var(--_label);
320
+ margin: 0;
321
+ }
322
+
323
+ .nodii-onboarding-consent-stale-tag {
324
+ font-size: 0.8125rem;
325
+ font-weight: 500;
326
+ color: #92400e;
327
+ }
328
+
329
+ .nodii-onboarding-consent-gate {
330
+ width: 100%;
331
+ }
332
+
333
+ .nodii-onboarding-consent-scrollbox {
334
+ width: 100%;
335
+ height: 360px;
336
+ overflow-y: auto;
337
+ border: 1px solid var(--_border);
338
+ border-radius: var(--_radius);
339
+ background: #fff;
340
+ position: relative;
341
+ }
342
+
343
+ .nodii-onboarding-consent-scrollbox:focus {
344
+ outline: none;
345
+ border-color: var(--_primary);
346
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--_primary) 20%, transparent);
347
+ }
348
+
349
+ /* Inline, same-origin document content (no iframe). Rendered by the default
350
+ viewer: sanitized HTML/markdown, escaped plain text, or PDF.js canvases. */
351
+ .nodii-onboarding-consent-doc-body {
352
+ padding: var(--_spacing);
353
+ font-size: 0.9375rem;
354
+ line-height: 1.7;
355
+ color: var(--_label);
356
+ word-break: break-word;
357
+ }
358
+
359
+ .nodii-onboarding-consent-doc-body:not(.nodii-consent-pdf) {
360
+ white-space: pre-wrap;
361
+ }
362
+
363
+ .nodii-onboarding-consent-doc-loading {
364
+ padding: var(--_spacing);
365
+ color: #6b7280;
366
+ font-size: 0.875rem;
367
+ }
368
+
369
+ .nodii-consent-pdf {
370
+ display: flex;
371
+ flex-direction: column;
372
+ gap: var(--_spacing);
373
+ white-space: normal;
374
+ }
375
+
376
+ .nodii-consent-pdf-page {
377
+ max-width: 100%;
378
+ height: auto;
379
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
380
+ }
381
+
382
+ .nodii-onboarding-consent-note {
383
+ font-size: 0.8125rem;
384
+ color: #6b7280;
385
+ margin: calc(var(--_spacing) * 0.5) 0 0 0;
386
+ }
387
+
388
+ /* A gated doc that could not be rendered inline: the gate stays unsatisfiable
389
+ (accept disabled); we surface a clear error + Retry. NO click-to-satisfy. */
390
+ .nodii-onboarding-consent-unrenderable {
391
+ display: flex;
392
+ flex-direction: column;
393
+ gap: calc(var(--_spacing) * 0.5);
394
+ padding: var(--_spacing);
395
+ border: 1px solid color-mix(in srgb, var(--_error) 30%, transparent);
396
+ border-radius: var(--_radius);
397
+ background: color-mix(in srgb, var(--_error) 6%, transparent);
398
+ }
399
+
400
+ .nodii-onboarding-consent-retry {
401
+ align-self: flex-start;
402
+ padding: calc(var(--_spacing) * 0.5) var(--_spacing);
403
+ background: transparent;
404
+ color: var(--_primary);
405
+ border: 1px solid var(--_primary);
406
+ border-radius: var(--_radius);
407
+ font-family: var(--_font);
408
+ font-size: 0.875rem;
409
+ cursor: pointer;
410
+ }
411
+
412
+ .nodii-onboarding-consent-open-row,
413
+ .nodii-onboarding-consent-preview-row {
414
+ display: flex;
415
+ }
416
+
417
+ .nodii-onboarding-consent-open {
418
+ display: inline-flex;
419
+ align-items: center;
420
+ padding: calc(var(--_spacing) * 0.625) var(--_spacing);
421
+ border: 1px solid var(--_primary);
422
+ border-radius: var(--_radius);
423
+ color: var(--_primary);
424
+ text-decoration: none;
425
+ font-size: 0.9375rem;
426
+ font-weight: 500;
427
+ }
428
+
429
+ .nodii-onboarding-consent-open:hover {
430
+ background: color-mix(in srgb, var(--_primary) 8%, transparent);
431
+ }
432
+
433
+ .nodii-onboarding-consent-preview {
434
+ font-size: 0.875rem;
435
+ color: var(--_primary);
436
+ }
437
+
438
+ .nodii-onboarding-consent-accept-row {
439
+ display: flex;
440
+ align-items: flex-start;
441
+ gap: calc(var(--_spacing) * 0.5);
442
+ }
443
+
444
+ .nodii-onboarding-consent-accept {
445
+ margin-top: 3px;
446
+ width: 1.125rem;
447
+ height: 1.125rem;
448
+ }
449
+
450
+ .nodii-onboarding-consent-accept:disabled {
451
+ cursor: not-allowed;
452
+ opacity: 0.5;
453
+ }
454
+
455
+ .nodii-onboarding-consent-accept-label {
456
+ font-size: 0.9375rem;
457
+ color: var(--_label);
458
+ line-height: 1.5;
459
+ }
460
+
461
+ .nodii-onboarding-consent-required {
462
+ color: var(--_error);
463
+ }
246
464
  `;
247
465
  function injectWidgetStyles() {
248
466
  if (typeof document === "undefined") return;
@@ -269,59 +487,130 @@ function el(tag, attrs = {}, children = []) {
269
487
  }
270
488
  return element;
271
489
  }
490
+ function errorSlot(id) {
491
+ return el("div", {
492
+ class: "nodii-onboarding-field-error",
493
+ id: `${id}-error`,
494
+ role: "alert",
495
+ "aria-live": "polite"
496
+ });
497
+ }
272
498
  function createFieldEl(id, label, input, required) {
273
499
  const labelEl = el(
274
500
  "label",
275
501
  { for: id, class: `nodii-onboarding-label${required ? " required" : ""}` },
276
502
  [label]
277
503
  );
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" }, [
504
+ return el("div", { class: "nodii-onboarding-field" }, [
285
505
  labelEl,
286
506
  input,
287
- errorEl
507
+ errorSlot(id)
288
508
  ]);
289
- return field;
290
509
  }
291
510
  function setFieldError(container, fieldId, message) {
292
511
  const errorEl = container.querySelector(idSelector(`${fieldId}-error`));
293
512
  if (errorEl) errorEl.textContent = message;
294
513
  const inputEl = container.querySelector(idSelector(fieldId));
295
514
  if (inputEl) inputEl.classList.add("has-error");
515
+ return errorEl !== null;
296
516
  }
297
517
  function clearFieldErrors(container) {
298
- for (const el3 of container.querySelectorAll(
518
+ for (const el4 of container.querySelectorAll(
299
519
  ".nodii-onboarding-field-error"
300
520
  )) {
301
- el3.textContent = "";
521
+ el4.textContent = "";
302
522
  }
303
- for (const el3 of container.querySelectorAll(".has-error")) {
304
- el3.classList.remove("has-error");
523
+ for (const el4 of container.querySelectorAll(".has-error")) {
524
+ el4.classList.remove("has-error");
305
525
  }
306
526
  }
307
527
  function applyErrors(container, errors) {
308
528
  clearFieldErrors(container);
529
+ let unplaced = 0;
309
530
  for (const err of errors) {
310
- setFieldError(container, `nodii-${err.path}`, err.message);
531
+ const field = err.path.split(".")[0] ?? err.path;
532
+ if (!setFieldError(container, `nodii-${field}`, err.message)) unplaced++;
311
533
  }
534
+ return unplaced;
535
+ }
536
+ function assertHostValues(options) {
537
+ if (options.privacyUrl !== void 0 && !isNavigableUrl(options.privacyUrl)) {
538
+ throw new OnboardingValidationError(
539
+ "mountContactForm: invalid privacyUrl: must be an http(s) URL, absolute or relative"
540
+ );
541
+ }
542
+ const shape = contactFormSchema.shape;
543
+ for (const [name, schema, value] of [
544
+ ["intakeKind", shape.intakeKind, options.intakeKind],
545
+ ["attribution", shape.attribution, options.attribution]
546
+ ]) {
547
+ if (value === void 0) continue;
548
+ const parsed = schema.safeParse(value);
549
+ if (!parsed.success) {
550
+ const issue = parsed.error.issues[0];
551
+ throw new OnboardingValidationError(
552
+ `mountContactForm: invalid ${[name, ...issue?.path ?? []].join(".")}: ${issue?.message ?? "invalid"}`
553
+ );
554
+ }
555
+ }
556
+ }
557
+ var DEFAULT_SPEC_REFRESH_MS = 5 * 60 * 1e3;
558
+ function privacyDocumentUrl(spec) {
559
+ const url = spec?.consentPack?.documents.find(
560
+ (d) => d.docType === "privacy"
561
+ )?.downloadUrl;
562
+ return url && isNavigableUrl(url) ? url : void 0;
563
+ }
564
+ function choicesOf(spec, key) {
565
+ const field = spec?.forms.contact.fields.find((f) => f.key === key);
566
+ return (field?.options ?? []).map((option) => {
567
+ const value = optionValue(option);
568
+ const label = option !== null && typeof option === "object" && typeof option.label === "string" ? option.label : value;
569
+ return { value, label };
570
+ });
571
+ }
572
+ function isRequired(spec, key) {
573
+ return spec?.forms.contact.fields.find((f) => f.key === key)?.required === true;
574
+ }
575
+ function labelOf(spec, key, fallback) {
576
+ const label = spec?.forms.contact.fields.find((f) => f.key === key)?.label;
577
+ return label?.trim() ? label : fallback;
312
578
  }
313
579
  function mountContactForm(host, options) {
580
+ assertHostValues(options);
314
581
  injectWidgetStyles();
315
- const formControl = createContactFormState();
316
- loadRecaptcha(options.siteKey).catch(() => {
582
+ const spec = options.spec;
583
+ const interests = choicesOf(spec, "interestModules");
584
+ const sizes = choicesOf(spec, "sizeBand");
585
+ const interestRequired = interests.length > 0 && isRequired(spec, "interestModules");
586
+ const privacyUrl = options.privacyUrl ?? privacyDocumentUrl(spec);
587
+ const siteKey = options.siteKey ?? spec?.botChallenge?.siteKey;
588
+ const formControl = createContactFormState({
589
+ requireInterest: interestRequired
317
590
  });
591
+ formControl.setValue("intakeKind", options.intakeKind ?? "enquiry");
592
+ if (options.attribution) {
593
+ formControl.setValue("attribution", options.attribution);
594
+ }
595
+ if (siteKey) {
596
+ loadRecaptcha(siteKey).catch(() => {
597
+ });
598
+ }
318
599
  const wrapper = el("div", { class: "nodii-onboarding-widget" });
319
- const nameInput = el("input", {
600
+ const companyInput = el("input", {
320
601
  type: "text",
321
- id: "nodii-name",
322
- name: "name",
602
+ id: "nodii-companyName",
603
+ name: "companyName",
604
+ class: "nodii-onboarding-input",
605
+ autocomplete: "organization",
606
+ maxlength: "200",
607
+ required: "required"
608
+ });
609
+ const contactInput = el("input", {
610
+ type: "text",
611
+ id: "nodii-contactName",
612
+ name: "contactName",
323
613
  class: "nodii-onboarding-input",
324
- placeholder: "Your name",
325
614
  autocomplete: "name",
326
615
  maxlength: "200",
327
616
  required: "required"
@@ -340,8 +629,8 @@ function mountContactForm(host, options) {
340
629
  id: "nodii-phone",
341
630
  name: "phone",
342
631
  class: "nodii-onboarding-input",
343
- placeholder: "+1 555 000 0000",
344
- autocomplete: "tel"
632
+ autocomplete: "tel",
633
+ maxlength: "40"
345
634
  });
346
635
  const messageInput = el("textarea", {
347
636
  id: "nodii-message",
@@ -351,6 +640,34 @@ function mountContactForm(host, options) {
351
640
  maxlength: "4000",
352
641
  rows: "4"
353
642
  });
643
+ const interestBoxes = interests.map(
644
+ (choice) => el("input", {
645
+ type: "checkbox",
646
+ name: "interestModules",
647
+ value: choice.value
648
+ })
649
+ );
650
+ const sizeSelect = el(
651
+ "select",
652
+ { id: "nodii-sizeBand", name: "sizeBand", class: "nodii-onboarding-input" },
653
+ [
654
+ el("option", { value: "" }, ["Choose\u2026"]),
655
+ ...sizes.map(
656
+ (choice) => el("option", { value: choice.value }, [choice.label])
657
+ )
658
+ ]
659
+ );
660
+ const privacyBox = el("input", {
661
+ type: "checkbox",
662
+ id: "nodii-privacyNoticeAcknowledged",
663
+ name: "privacyNoticeAcknowledged",
664
+ required: "required"
665
+ });
666
+ const marketingBox = el("input", {
667
+ type: "checkbox",
668
+ id: "nodii-marketingOptIn",
669
+ name: "marketingOptIn"
670
+ });
354
671
  const serverErrorEl = el("div", {
355
672
  class: "nodii-onboarding-server-error",
356
673
  role: "alert",
@@ -374,60 +691,165 @@ function mountContactForm(host, options) {
374
691
  },
375
692
  [options.submitLabel ?? "Get in touch"]
376
693
  );
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
694
  const form = el("form", {
402
695
  novalidate: "true",
403
696
  "aria-label": "Contact us"
404
697
  });
405
- form.appendChild(createFieldEl("nodii-name", "Name", nameInput, true));
406
- form.appendChild(createFieldEl("nodii-email", "Email", emailInput, true));
407
698
  form.appendChild(
408
- createFieldEl("nodii-phone", "Phone (optional)", phoneInput, false)
699
+ createFieldEl(
700
+ "nodii-companyName",
701
+ labelOf(spec, "companyName", "Company name"),
702
+ companyInput,
703
+ true
704
+ )
409
705
  );
410
706
  form.appendChild(
411
- createFieldEl("nodii-message", "Message (optional)", messageInput, false)
707
+ createFieldEl(
708
+ "nodii-contactName",
709
+ labelOf(spec, "contactName", "Your name"),
710
+ contactInput,
711
+ true
712
+ )
713
+ );
714
+ form.appendChild(
715
+ createFieldEl(
716
+ "nodii-email",
717
+ labelOf(spec, "email", "Email"),
718
+ emailInput,
719
+ true
720
+ )
721
+ );
722
+ form.appendChild(
723
+ createFieldEl(
724
+ "nodii-phone",
725
+ `${labelOf(spec, "phone", "Phone")} (optional)`,
726
+ phoneInput,
727
+ false
728
+ )
729
+ );
730
+ if (interests.length > 0) {
731
+ form.appendChild(
732
+ el(
733
+ "fieldset",
734
+ { id: "nodii-interestModules", class: "nodii-onboarding-field" },
735
+ [
736
+ el(
737
+ "legend",
738
+ {
739
+ class: `nodii-onboarding-label${interestRequired ? " required" : ""}`
740
+ },
741
+ [labelOf(spec, "interestModules", "What are you interested in?")]
742
+ ),
743
+ el(
744
+ "div",
745
+ { class: "nodii-onboarding-choices" },
746
+ interests.map(
747
+ (choice, i) => el("label", { class: "nodii-onboarding-choice" }, [
748
+ interestBoxes[i],
749
+ choice.label
750
+ ])
751
+ )
752
+ ),
753
+ errorSlot("nodii-interestModules")
754
+ ]
755
+ )
756
+ );
757
+ }
758
+ if (sizes.length > 0) {
759
+ form.appendChild(
760
+ createFieldEl(
761
+ "nodii-sizeBand",
762
+ `${labelOf(spec, "sizeBand", "Team size")} (optional)`,
763
+ sizeSelect,
764
+ false
765
+ )
766
+ );
767
+ }
768
+ form.appendChild(
769
+ createFieldEl(
770
+ "nodii-message",
771
+ `${labelOf(spec, "message", "Message")} (optional)`,
772
+ messageInput,
773
+ false
774
+ )
775
+ );
776
+ const privacyLabel = ["I have read the "];
777
+ let privacyLink = null;
778
+ if (privacyUrl) {
779
+ privacyLink = Object.assign(
780
+ el("a", {
781
+ href: privacyUrl,
782
+ target: "_blank",
783
+ rel: "noopener noreferrer"
784
+ }),
785
+ { textContent: "privacy notice" }
786
+ );
787
+ privacyLabel.push(privacyLink);
788
+ } else {
789
+ privacyLabel.push("privacy notice");
790
+ }
791
+ form.appendChild(
792
+ el("div", { class: "nodii-onboarding-field" }, [
793
+ el("label", { class: "nodii-onboarding-consent-row" }, [
794
+ privacyBox,
795
+ el("span", {}, privacyLabel)
796
+ ]),
797
+ errorSlot("nodii-privacyNoticeAcknowledged")
798
+ ])
799
+ );
800
+ form.appendChild(
801
+ el("label", { class: "nodii-onboarding-consent-row" }, [
802
+ marketingBox,
803
+ el("span", {}, [
804
+ labelOf(spec, "marketingOptIn", "Send me occasional product updates")
805
+ ])
806
+ ])
412
807
  );
413
808
  form.appendChild(serverErrorEl);
414
809
  form.appendChild(submitBtn);
415
- form.appendChild(recaptchaNotice);
810
+ if (siteKey) {
811
+ form.appendChild(
812
+ el("p", { class: "nodii-onboarding-recaptcha-notice" }, [
813
+ "Protected by reCAPTCHA. ",
814
+ Object.assign(
815
+ el("a", {
816
+ href: "https://policies.google.com/privacy",
817
+ target: "_blank",
818
+ rel: "noopener noreferrer"
819
+ }),
820
+ { textContent: "Privacy" }
821
+ ),
822
+ " & ",
823
+ Object.assign(
824
+ el("a", {
825
+ href: "https://policies.google.com/terms",
826
+ target: "_blank",
827
+ rel: "noopener noreferrer"
828
+ }),
829
+ { textContent: "Terms" }
830
+ )
831
+ ])
832
+ );
833
+ }
416
834
  wrapper.appendChild(successEl);
417
835
  wrapper.appendChild(form);
418
836
  host.appendChild(wrapper);
419
- nameInput.addEventListener("input", () => {
420
- formControl.setValue("name", nameInput.value);
421
- });
422
- emailInput.addEventListener("input", () => {
837
+ function syncValues() {
838
+ formControl.setValue("companyName", companyInput.value);
839
+ formControl.setValue("contactName", contactInput.value);
423
840
  formControl.setValue("email", emailInput.value);
424
- });
425
- phoneInput.addEventListener("input", () => {
426
841
  formControl.setValue("phone", phoneInput.value || void 0);
427
- });
428
- messageInput.addEventListener("input", () => {
429
842
  formControl.setValue("message", messageInput.value || void 0);
430
- });
843
+ formControl.setValue(
844
+ "interestModules",
845
+ interestBoxes.filter((box) => box.checked).map((box) => box.value)
846
+ );
847
+ formControl.setValue("sizeBand", sizeSelect.value || void 0);
848
+ formControl.setValue("privacyNoticeAcknowledged", privacyBox.checked);
849
+ formControl.setValue("marketingOptIn", marketingBox.checked);
850
+ }
851
+ form.addEventListener("input", syncValues);
852
+ form.addEventListener("change", syncValues);
431
853
  const submitLabel = options.submitLabel ?? "Get in touch";
432
854
  function renderSubmitButton(submitting) {
433
855
  while (submitBtn.firstChild) {
@@ -445,9 +867,10 @@ function mountContactForm(host, options) {
445
867
  const unsubscribe = formControl.subscribe((state) => {
446
868
  submitBtn.disabled = state.status === "submitting";
447
869
  renderSubmitButton(state.status === "submitting");
448
- applyErrors(wrapper, state.errors);
449
- if (state.serverError) {
450
- serverErrorEl.textContent = state.serverError;
870
+ const unplaced = applyErrors(wrapper, state.errors);
871
+ const formError = state.serverError ?? (unplaced > 0 ? "This form can\u2019t be sent right now." : null);
872
+ if (formError) {
873
+ serverErrorEl.textContent = formError;
451
874
  serverErrorEl.style.display = "";
452
875
  } else {
453
876
  serverErrorEl.style.display = "none";
@@ -461,10 +884,7 @@ function mountContactForm(host, options) {
461
884
  let lastFingerprint = null;
462
885
  form.addEventListener("submit", async (event) => {
463
886
  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);
887
+ syncValues();
468
888
  const valid = formControl.validate();
469
889
  if (!valid) return;
470
890
  formControl.setSubmitting();
@@ -475,14 +895,18 @@ function mountContactForm(host, options) {
475
895
  lastFingerprint = fingerprint;
476
896
  }
477
897
  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;
898
+ if (siteKey) {
899
+ try {
900
+ firstToken = await executeRecaptcha(siteKey, "contact_submit", {
901
+ timeoutMs: options.timeoutMs
902
+ });
903
+ } catch (recaptchaErr) {
904
+ formControl.setError(
905
+ "Could not load bot protection. Please try again."
906
+ );
907
+ options.onError?.(recaptchaErr);
908
+ return;
909
+ }
486
910
  }
487
911
  let usedFirst = false;
488
912
  const tokenProvider = async () => {
@@ -490,14 +914,14 @@ function mountContactForm(host, options) {
490
914
  usedFirst = true;
491
915
  return firstToken;
492
916
  }
493
- return executeRecaptcha(options.siteKey, "contact_submit", {
917
+ return executeRecaptcha(siteKey, "contact_submit", {
494
918
  timeoutMs: options.timeoutMs
495
919
  });
496
920
  };
497
921
  try {
498
922
  await submitLead(values, {
499
923
  baseUrl: options.baseUrl,
500
- botToken: tokenProvider,
924
+ ...siteKey ? { botToken: tokenProvider } : {},
501
925
  idempotencyKey,
502
926
  fetch: options.fetch,
503
927
  timeoutMs: options.timeoutMs
@@ -524,32 +948,286 @@ function mountContactForm(host, options) {
524
948
  }
525
949
  }
526
950
  });
951
+ const refreshMs = options.specRefreshMs ?? DEFAULT_SPEC_REFRESH_MS;
952
+ let refreshing = false;
953
+ const specRefresh = privacyLink && !options.privacyUrl && refreshMs > 0 ? setInterval(() => {
954
+ if (refreshing || !wrapper.isConnected) return;
955
+ refreshing = true;
956
+ fetchOnboardingSpec({
957
+ baseUrl: options.baseUrl,
958
+ onboarder: options.onboarder,
959
+ fetch: options.fetch,
960
+ timeoutMs: options.timeoutMs,
961
+ cache: new OnboardingSpecCache()
962
+ }).then(
963
+ (fresh) => {
964
+ const url = privacyDocumentUrl(fresh);
965
+ if (url && privacyLink) privacyLink.href = url;
966
+ },
967
+ () => {
968
+ }
969
+ ).finally(() => {
970
+ refreshing = false;
971
+ });
972
+ }, refreshMs) : void 0;
527
973
  return {
528
974
  unmount() {
975
+ clearInterval(specRefresh);
529
976
  unsubscribe();
530
977
  host.removeChild(wrapper);
531
978
  },
532
979
  reset() {
533
980
  idempotencyKey = null;
534
981
  lastFingerprint = null;
982
+ form.reset();
535
983
  formControl.reset();
984
+ formControl.setValue("intakeKind", options.intakeKind ?? "enquiry");
985
+ if (options.attribution) {
986
+ formControl.setValue("attribution", options.attribution);
987
+ }
536
988
  form.style.display = "";
537
989
  successEl.style.display = "none";
538
- nameInput.value = "";
539
- emailInput.value = "";
540
- phoneInput.value = "";
541
- messageInput.value = "";
542
990
  }
543
991
  };
544
992
  }
545
993
 
994
+ // src/widget/consent-step.ts
995
+ var CONSENT_ID_PREFIX = "nodii-consent-doc-";
996
+ function el2(tag, attrs = {}, children = []) {
997
+ const element = document.createElement(tag);
998
+ for (const [key, value] of Object.entries(attrs)) {
999
+ element.setAttribute(key, value);
1000
+ }
1001
+ for (const child of children) {
1002
+ if (typeof child === "string") {
1003
+ element.appendChild(document.createTextNode(child));
1004
+ } else {
1005
+ element.appendChild(child);
1006
+ }
1007
+ }
1008
+ return element;
1009
+ }
1010
+ function consentDocElementId(key) {
1011
+ return `${CONSENT_ID_PREFIX}${key.replace(/[^A-Za-z0-9_-]/g, "-")}`;
1012
+ }
1013
+ function buildAcceptRow(control, doc) {
1014
+ const id = consentDocElementId(doc.key);
1015
+ const checkbox = el2("input", {
1016
+ type: "checkbox",
1017
+ id,
1018
+ class: "nodii-onboarding-consent-accept"
1019
+ });
1020
+ checkbox.checked = doc.accepted;
1021
+ checkbox.disabled = !doc.gateSatisfied;
1022
+ checkbox.addEventListener("change", () => {
1023
+ control.setAccepted(doc.key, checkbox.checked);
1024
+ checkbox.checked = control.getState().documents.find((d) => d.key === doc.key)?.accepted ?? false;
1025
+ });
1026
+ const labelText = doc.acceptanceLevel === "checkbox" ? `I accept the ${doc.doc.title}` : `I have reviewed and accept the ${doc.doc.title}`;
1027
+ const label = el2(
1028
+ "label",
1029
+ { for: id, class: "nodii-onboarding-consent-accept-label" },
1030
+ [labelText]
1031
+ );
1032
+ if (doc.required) {
1033
+ label.appendChild(
1034
+ el2("span", { class: "nodii-onboarding-consent-required" }, [" *"])
1035
+ );
1036
+ }
1037
+ const row = el2("div", { class: "nodii-onboarding-consent-accept-row" }, [
1038
+ checkbox,
1039
+ label
1040
+ ]);
1041
+ const errorEl = el2("div", {
1042
+ class: "nodii-onboarding-field-error nodii-onboarding-consent-error",
1043
+ id: `${id}-error`,
1044
+ role: "alert",
1045
+ "aria-live": "polite"
1046
+ });
1047
+ return { row, errorEl };
1048
+ }
1049
+ function buildOpenAffordance(control, doc) {
1050
+ const id = consentDocElementId(doc.key);
1051
+ const openLink = el2(
1052
+ "a",
1053
+ {
1054
+ class: "nodii-onboarding-consent-open",
1055
+ id: `${id}-open`,
1056
+ href: doc.doc.downloadUrl,
1057
+ target: "_blank",
1058
+ rel: "noopener noreferrer"
1059
+ },
1060
+ [`Open ${doc.doc.title}`]
1061
+ );
1062
+ const markOpened = () => control.markOpened(doc.key);
1063
+ openLink.addEventListener("click", markOpened);
1064
+ openLink.addEventListener("keydown", (event) => {
1065
+ if (event.key === "Enter") markOpened();
1066
+ });
1067
+ return el2("div", { class: "nodii-onboarding-consent-open-row" }, [openLink]);
1068
+ }
1069
+ function buildGateAffordance(control, doc, opts) {
1070
+ const id = consentDocElementId(doc.key);
1071
+ if (doc.acceptanceLevel === "open_then_accept") {
1072
+ return buildOpenAffordance(control, doc);
1073
+ }
1074
+ if (doc.acceptanceLevel === "checkbox") {
1075
+ return el2("div", { class: "nodii-onboarding-consent-preview-row" }, [
1076
+ el2(
1077
+ "a",
1078
+ {
1079
+ class: "nodii-onboarding-consent-preview",
1080
+ id: `${id}-preview`,
1081
+ href: doc.doc.downloadUrl,
1082
+ target: "_blank",
1083
+ rel: "noopener noreferrer"
1084
+ },
1085
+ [`View ${doc.doc.title}`]
1086
+ )
1087
+ ]);
1088
+ }
1089
+ const container = el2("div", {
1090
+ class: "nodii-onboarding-consent-gate",
1091
+ id: `${id}-gate`
1092
+ });
1093
+ container.appendChild(
1094
+ el2("div", { class: "nodii-onboarding-consent-doc-loading" }, [
1095
+ "Loading document\u2026"
1096
+ ])
1097
+ );
1098
+ const importViewer = opts.importViewer ?? (() => import('./render-EWQXF73I.js'));
1099
+ const loadViewer = () => {
1100
+ void (async () => {
1101
+ try {
1102
+ const { renderConsentDocument } = await importViewer();
1103
+ await renderConsentDocument(container, doc.doc, control, {
1104
+ consentKey: doc.key,
1105
+ fetch: opts.fetch,
1106
+ timeoutMs: opts.timeoutMs
1107
+ });
1108
+ } catch {
1109
+ while (container.firstChild) {
1110
+ container.removeChild(container.firstChild);
1111
+ }
1112
+ const wrap = el2("div", {
1113
+ class: "nodii-onboarding-consent-unrenderable",
1114
+ role: "alert"
1115
+ });
1116
+ wrap.appendChild(
1117
+ el2(
1118
+ "p",
1119
+ {
1120
+ // DISTINCT id (NOT `${id}-error`): buildAcceptRow already owns
1121
+ // `${id}-error`, and updateConsentStep's querySelector would match
1122
+ // THIS paragraph first and blank it on the next state change,
1123
+ // silently wiping the compliance message. A unique id keeps this
1124
+ // load-failure error persistent (P1b).
1125
+ class: "nodii-onboarding-field-error nodii-onboarding-consent-error",
1126
+ id: `${id}-load-error`
1127
+ },
1128
+ [
1129
+ `\u201C${doc.doc.title}\u201D could not be loaded, so it cannot be accepted yet. Please retry.`
1130
+ ]
1131
+ )
1132
+ );
1133
+ const retry = el2(
1134
+ "button",
1135
+ { type: "button", class: "nodii-onboarding-consent-retry" },
1136
+ ["Retry loading document"]
1137
+ );
1138
+ retry.addEventListener("click", loadViewer);
1139
+ wrap.appendChild(retry);
1140
+ wrap.appendChild(
1141
+ el2(
1142
+ "a",
1143
+ {
1144
+ class: "nodii-onboarding-consent-open",
1145
+ href: doc.doc.downloadUrl,
1146
+ target: "_blank",
1147
+ rel: "noopener noreferrer"
1148
+ },
1149
+ [`Open ${doc.doc.title} in a new tab`]
1150
+ )
1151
+ );
1152
+ container.appendChild(wrap);
1153
+ }
1154
+ })();
1155
+ };
1156
+ loadViewer();
1157
+ return container;
1158
+ }
1159
+ function buildConsentCard(control, doc, stale, opts) {
1160
+ const card = el2("div", {
1161
+ class: `nodii-onboarding-consent-card${stale ? " is-stale" : ""}`,
1162
+ "data-consent-key": doc.key
1163
+ });
1164
+ card.appendChild(
1165
+ el2("h4", { class: "nodii-onboarding-consent-title" }, [doc.doc.title])
1166
+ );
1167
+ if (stale) {
1168
+ card.appendChild(
1169
+ el2(
1170
+ "div",
1171
+ { class: "nodii-onboarding-consent-stale-tag", role: "status" },
1172
+ ["Updated \u2014 please review and re-accept."]
1173
+ )
1174
+ );
1175
+ }
1176
+ card.appendChild(buildGateAffordance(control, doc, opts));
1177
+ const { row, errorEl } = buildAcceptRow(control, doc);
1178
+ card.appendChild(row);
1179
+ card.appendChild(errorEl);
1180
+ return card;
1181
+ }
1182
+ function renderConsentStep(host, control, state, staleKeys = [], opts = {}) {
1183
+ while (host.firstChild) host.removeChild(host.firstChild);
1184
+ const staleSet = new Set(staleKeys);
1185
+ host.appendChild(
1186
+ el2("h3", { class: "nodii-onboarding-stage-title" }, [
1187
+ "Review & accept documents"
1188
+ ])
1189
+ );
1190
+ host.appendChild(
1191
+ el2("p", { class: "nodii-onboarding-consent-intro" }, [
1192
+ "Please review each document below and accept to continue."
1193
+ ])
1194
+ );
1195
+ for (const doc of state.documents) {
1196
+ host.appendChild(
1197
+ buildConsentCard(control, doc, staleSet.has(doc.key), opts)
1198
+ );
1199
+ }
1200
+ }
1201
+ function updateConsentStep(host, state, consentErrors) {
1202
+ const errorByKey = new Map(consentErrors.map((e) => [e.path, e.message]));
1203
+ for (const doc of state.documents) {
1204
+ const id = consentDocElementId(doc.key);
1205
+ const checkbox = host.querySelector(
1206
+ idSelector(id)
1207
+ );
1208
+ if (checkbox) {
1209
+ checkbox.disabled = !doc.gateSatisfied;
1210
+ checkbox.checked = doc.accepted;
1211
+ }
1212
+ const errorNode = host.querySelector(idSelector(`${id}-error`));
1213
+ if (errorNode) errorNode.textContent = errorByKey.get(doc.key) ?? "";
1214
+ const card = host.querySelector(
1215
+ `[data-consent-key="${cssEscape(doc.key)}"]`
1216
+ );
1217
+ if (card) {
1218
+ if (errorByKey.has(doc.key)) card.classList.add("has-error");
1219
+ else card.classList.remove("has-error");
1220
+ }
1221
+ }
1222
+ }
1223
+
546
1224
  // src/widget/signup-form.ts
547
1225
  var STAGE_TITLES = {
548
1226
  lead: "About you",
549
1227
  prospect: "Your business",
550
1228
  activation: "Choose your services"
551
1229
  };
552
- function el2(tag, attrs = {}, children = []) {
1230
+ function el3(tag, attrs = {}, children = []) {
553
1231
  const element = document.createElement(tag);
554
1232
  for (const [key, value] of Object.entries(attrs)) {
555
1233
  element.setAttribute(key, value);
@@ -574,7 +1252,7 @@ function buildFieldInput(field, currentValue, onChange) {
574
1252
  const id = `nodii-f-${field.key}`;
575
1253
  switch (field.type) {
576
1254
  case "textarea": {
577
- const input = el2("textarea", {
1255
+ const input = el3("textarea", {
578
1256
  id,
579
1257
  class: "nodii-onboarding-textarea",
580
1258
  rows: "4"
@@ -585,7 +1263,7 @@ function buildFieldInput(field, currentValue, onChange) {
585
1263
  }
586
1264
  case "checkbox":
587
1265
  case "boolean": {
588
- const input = el2("input", {
1266
+ const input = el3("input", {
589
1267
  type: "checkbox",
590
1268
  id
591
1269
  });
@@ -595,14 +1273,14 @@ function buildFieldInput(field, currentValue, onChange) {
595
1273
  }
596
1274
  case "select":
597
1275
  case "radio": {
598
- const select = el2("select", {
1276
+ const select = el3("select", {
599
1277
  id,
600
1278
  class: "nodii-onboarding-input"
601
1279
  });
602
- select.appendChild(el2("option", { value: "" }, ["\u2014 select \u2014"]));
1280
+ select.appendChild(el3("option", { value: "" }, ["\u2014 select \u2014"]));
603
1281
  for (const opt of field.options ?? []) {
604
1282
  const value = optionValue(opt);
605
- const optionEl = el2("option", { value }, [optionLabel(opt)]);
1283
+ const optionEl = el3("option", { value }, [optionLabel(opt)]);
606
1284
  select.appendChild(optionEl);
607
1285
  }
608
1286
  if (typeof currentValue === "string") select.value = currentValue;
@@ -610,7 +1288,7 @@ function buildFieldInput(field, currentValue, onChange) {
610
1288
  return select;
611
1289
  }
612
1290
  case "multiselect": {
613
- const select = el2("select", {
1291
+ const select = el3("select", {
614
1292
  id,
615
1293
  class: "nodii-onboarding-input",
616
1294
  multiple: "multiple"
@@ -618,7 +1296,7 @@ function buildFieldInput(field, currentValue, onChange) {
618
1296
  const current = Array.isArray(currentValue) ? currentValue.map(String) : [];
619
1297
  for (const opt of field.options ?? []) {
620
1298
  const value = optionValue(opt);
621
- const optionEl = el2("option", { value }, [
1299
+ const optionEl = el3("option", { value }, [
622
1300
  optionLabel(opt)
623
1301
  ]);
624
1302
  if (current.includes(value)) optionEl.selected = true;
@@ -635,7 +1313,7 @@ function buildFieldInput(field, currentValue, onChange) {
635
1313
  }
636
1314
  default: {
637
1315
  const typeAttr = field.type === "email" || field.type === "tel" || field.type === "number" || field.type === "date" ? field.type : "text";
638
- const input = el2("input", {
1316
+ const input = el3("input", {
639
1317
  type: typeAttr,
640
1318
  id,
641
1319
  class: "nodii-onboarding-input"
@@ -649,7 +1327,7 @@ function buildFieldInput(field, currentValue, onChange) {
649
1327
  }
650
1328
  function fieldRow(field, input, idOverride) {
651
1329
  const id = idOverride ?? `nodii-f-${field.key}`;
652
- const labelEl = el2(
1330
+ const labelEl = el3(
653
1331
  "label",
654
1332
  {
655
1333
  for: id,
@@ -657,13 +1335,13 @@ function fieldRow(field, input, idOverride) {
657
1335
  },
658
1336
  [field.label]
659
1337
  );
660
- const errorEl = el2("div", {
1338
+ const errorEl = el3("div", {
661
1339
  class: "nodii-onboarding-field-error",
662
1340
  id: `${id}-error`,
663
1341
  role: "alert",
664
1342
  "aria-live": "polite"
665
1343
  });
666
- return el2("div", { class: "nodii-onboarding-field" }, [
1344
+ return el3("div", { class: "nodii-onboarding-field" }, [
667
1345
  labelEl,
668
1346
  input,
669
1347
  errorEl
@@ -671,8 +1349,8 @@ function fieldRow(field, input, idOverride) {
671
1349
  }
672
1350
  function mountSignupForm(host, options) {
673
1351
  injectWidgetStyles();
674
- const wrapper = el2("div", { class: "nodii-onboarding-widget" });
675
- const loadingEl = el2("div", { class: "nodii-onboarding-loading" }, [
1352
+ const wrapper = el3("div", { class: "nodii-onboarding-widget" });
1353
+ const loadingEl = el3("div", { class: "nodii-onboarding-loading" }, [
676
1354
  "Loading signup form\u2026"
677
1355
  ]);
678
1356
  wrapper.appendChild(loadingEl);
@@ -686,27 +1364,28 @@ function mountSignupForm(host, options) {
686
1364
  let lastFingerprint = null;
687
1365
  let renderedStepIndex = -1;
688
1366
  let renderedStale = false;
1367
+ let renderedConsentStale = false;
689
1368
  let currentSpecVersion = null;
690
1369
  let staleDiffSummary = "";
691
- const stepArea = el2("div", {});
692
- const staleBanner = el2("div", {
1370
+ const stepArea = el3("div", {});
1371
+ const staleBanner = el3("div", {
693
1372
  class: "nodii-onboarding-stale-banner",
694
1373
  role: "alert",
695
1374
  style: "display:none"
696
1375
  });
697
- const serverErrorEl = el2("div", {
1376
+ const serverErrorEl = el3("div", {
698
1377
  class: "nodii-onboarding-server-error",
699
1378
  role: "alert",
700
1379
  "aria-live": "assertive",
701
1380
  style: "display:none"
702
1381
  });
703
- const successEl = el2("div", {
1382
+ const successEl = el3("div", {
704
1383
  class: "nodii-onboarding-success",
705
1384
  role: "status",
706
1385
  style: "display:none"
707
1386
  });
708
- const stepsIndicator = el2("div", { class: "nodii-onboarding-steps" });
709
- const form = el2("form", { novalidate: "true", "aria-label": "Sign up" });
1387
+ const stepsIndicator = el3("div", { class: "nodii-onboarding-steps" });
1388
+ const form = el3("form", { novalidate: "true", "aria-label": "Sign up" });
710
1389
  function setFieldErrorsInPlace(container, errors, idPrefix) {
711
1390
  for (const errEl of container.querySelectorAll(
712
1391
  ".nodii-onboarding-field-error"
@@ -726,7 +1405,7 @@ function mountSignupForm(host, options) {
726
1405
  }
727
1406
  function accountIdOf(path) {
728
1407
  if (path === "email") return "nodii-account-email";
729
- if (path === "password") return "nodii-account-password";
1408
+ if (path === "displayName") return "nodii-account-display-name";
730
1409
  if (path === "tosAccepted") return "nodii-consent-tos";
731
1410
  if (path === "privacyAccepted") return "nodii-consent-privacy";
732
1411
  return `nodii-account-${path}`;
@@ -737,15 +1416,15 @@ function mountSignupForm(host, options) {
737
1416
  }
738
1417
  for (let i = 0; i < state.stepCount; i++) {
739
1418
  stepsIndicator.appendChild(
740
- el2("div", {
1419
+ el3("div", {
741
1420
  class: `nodii-onboarding-step-dot${i <= state.stepIndex ? " active" : ""}`
742
1421
  })
743
1422
  );
744
1423
  }
745
1424
  }
746
1425
  function renderAccountSection(control, state) {
747
- const section = el2("div", { class: "nodii-onboarding-account-section" });
748
- const emailInput = el2("input", {
1426
+ const section = el3("div", { class: "nodii-onboarding-account-section" });
1427
+ const emailInput = el3("input", {
749
1428
  type: "email",
750
1429
  id: "nodii-account-email",
751
1430
  class: "nodii-onboarding-input",
@@ -768,32 +1447,54 @@ function mountSignupForm(host, options) {
768
1447
  // label for + error id match the real input id
769
1448
  )
770
1449
  );
771
- const passwordInput = el2("input", {
772
- type: "password",
773
- id: "nodii-account-password",
1450
+ const displayNameInput = el3("input", {
1451
+ type: "text",
1452
+ id: "nodii-account-display-name",
774
1453
  class: "nodii-onboarding-input",
775
- autocomplete: "new-password",
776
- minlength: "12",
777
- maxlength: "256"
1454
+ autocomplete: "name",
1455
+ maxlength: "200"
778
1456
  });
779
- passwordInput.value = state.credentials.password;
780
- passwordInput.addEventListener(
1457
+ displayNameInput.value = state.credentials.displayName;
1458
+ displayNameInput.addEventListener(
781
1459
  "input",
782
- () => control.setCredentials({ password: passwordInput.value })
1460
+ () => control.setCredentials({ displayName: displayNameInput.value })
783
1461
  );
784
1462
  section.appendChild(
785
1463
  fieldRow(
786
1464
  {
787
- key: "account-password",
788
- label: "Password (12+ characters)",
1465
+ key: "account-display-name",
1466
+ label: "Your name",
789
1467
  required: true
790
1468
  },
791
- passwordInput,
792
- "nodii-account-password"
793
- // label for + error id match the real input id
1469
+ displayNameInput,
1470
+ "nodii-account-display-name"
794
1471
  )
795
1472
  );
796
- const tosCheckbox = el2("input", {
1473
+ for (const [key, label, autocomplete] of [
1474
+ ["organisationName", "Organisation name", "organization"],
1475
+ ["subdomain", "Workspace subdomain", "off"]
1476
+ ]) {
1477
+ const input = el3("input", {
1478
+ type: "text",
1479
+ id: `nodii-account-${key}`,
1480
+ class: "nodii-onboarding-input",
1481
+ autocomplete,
1482
+ maxlength: key === "subdomain" ? "63" : "200"
1483
+ });
1484
+ input.value = state.credentials[key];
1485
+ input.addEventListener(
1486
+ "input",
1487
+ () => control.setCredentials({ [key]: input.value })
1488
+ );
1489
+ section.appendChild(
1490
+ fieldRow(
1491
+ { key: `account-${key}`, label, required: true },
1492
+ input,
1493
+ `nodii-account-${key}`
1494
+ )
1495
+ );
1496
+ }
1497
+ const tosCheckbox = el3("input", {
797
1498
  type: "checkbox",
798
1499
  id: "nodii-consent-tos"
799
1500
  });
@@ -805,7 +1506,7 @@ function mountSignupForm(host, options) {
805
1506
  const tosLabelChildren = ["I accept the "];
806
1507
  if (options.tosUrl) {
807
1508
  tosLabelChildren.push(
808
- el2(
1509
+ el3(
809
1510
  "a",
810
1511
  {
811
1512
  href: options.tosUrl,
@@ -819,19 +1520,19 @@ function mountSignupForm(host, options) {
819
1520
  tosLabelChildren.push("Terms of Service");
820
1521
  }
821
1522
  section.appendChild(
822
- el2("div", { class: "nodii-onboarding-consent-row" }, [
1523
+ el3("div", { class: "nodii-onboarding-consent-row" }, [
823
1524
  tosCheckbox,
824
- el2("label", { for: "nodii-consent-tos" }, tosLabelChildren)
1525
+ el3("label", { for: "nodii-consent-tos" }, tosLabelChildren)
825
1526
  ])
826
1527
  );
827
1528
  section.appendChild(
828
- el2("div", {
1529
+ el3("div", {
829
1530
  class: "nodii-onboarding-field-error",
830
1531
  id: "nodii-consent-tos-error",
831
1532
  role: "alert"
832
1533
  })
833
1534
  );
834
- const privacyCheckbox = el2("input", {
1535
+ const privacyCheckbox = el3("input", {
835
1536
  type: "checkbox",
836
1537
  id: "nodii-consent-privacy"
837
1538
  });
@@ -843,7 +1544,7 @@ function mountSignupForm(host, options) {
843
1544
  const privacyLabelChildren = ["I accept the "];
844
1545
  if (options.privacyUrl) {
845
1546
  privacyLabelChildren.push(
846
- el2(
1547
+ el3(
847
1548
  "a",
848
1549
  {
849
1550
  href: options.privacyUrl,
@@ -857,13 +1558,13 @@ function mountSignupForm(host, options) {
857
1558
  privacyLabelChildren.push("Privacy Policy");
858
1559
  }
859
1560
  section.appendChild(
860
- el2("div", { class: "nodii-onboarding-consent-row" }, [
1561
+ el3("div", { class: "nodii-onboarding-consent-row" }, [
861
1562
  privacyCheckbox,
862
- el2("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
1563
+ el3("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
863
1564
  ])
864
1565
  );
865
1566
  section.appendChild(
866
- el2("div", {
1567
+ el3("div", {
867
1568
  class: "nodii-onboarding-field-error",
868
1569
  id: "nodii-consent-privacy-error",
869
1570
  role: "alert"
@@ -876,7 +1577,7 @@ function mountSignupForm(host, options) {
876
1577
  const step = state.currentStep;
877
1578
  if (step) {
878
1579
  stepArea.appendChild(
879
- el2("h3", { class: "nodii-onboarding-stage-title" }, [
1580
+ el3("h3", { class: "nodii-onboarding-stage-title" }, [
880
1581
  STAGE_TITLES[step.stage] ?? step.stage
881
1582
  ])
882
1583
  );
@@ -891,11 +1592,24 @@ function mountSignupForm(host, options) {
891
1592
  }
892
1593
  const isLast = control.isLastStep();
893
1594
  if (isLast) {
1595
+ if (!control.consent.isEmpty()) {
1596
+ const consentStage = el3("div", {
1597
+ class: "nodii-onboarding-consent-stage"
1598
+ });
1599
+ renderConsentStep(
1600
+ consentStage,
1601
+ control.consent,
1602
+ state.consentState,
1603
+ state.staleConsentKeys,
1604
+ { fetch: options.fetch, timeoutMs: options.timeoutMs }
1605
+ );
1606
+ stepArea.appendChild(consentStage);
1607
+ }
894
1608
  stepArea.appendChild(renderAccountSection(control, state));
895
1609
  }
896
- const nav = el2("div", { class: "nodii-onboarding-nav" });
1610
+ const nav = el3("div", { class: "nodii-onboarding-nav" });
897
1611
  if (!control.isFirstStep()) {
898
- const backBtn = el2(
1612
+ const backBtn = el3(
899
1613
  "button",
900
1614
  { type: "button", class: "nodii-onboarding-back-btn" },
901
1615
  ["Back"]
@@ -903,7 +1617,7 @@ function mountSignupForm(host, options) {
903
1617
  backBtn.addEventListener("click", () => control.back());
904
1618
  nav.appendChild(backBtn);
905
1619
  }
906
- const forwardBtn = el2(
1620
+ const forwardBtn = el3(
907
1621
  "button",
908
1622
  {
909
1623
  type: isLast ? "submit" : "button",
@@ -940,12 +1654,27 @@ function mountSignupForm(host, options) {
940
1654
  } else {
941
1655
  staleDiffSummary = "";
942
1656
  }
943
- siteKey = options.siteKey ?? fresh.botChallenge.siteKey;
1657
+ siteKey = options.siteKey ?? fresh.botChallenge?.siteKey ?? "";
944
1658
  currentSpecVersion = fresh.specVersion;
945
1659
  control.applyNewSpec(fresh);
946
1660
  idempotencyKey = null;
947
1661
  lastFingerprint = null;
948
1662
  }
1663
+ async function handleConsentStale(control) {
1664
+ const fresh = await fetchOnboardingSpec({
1665
+ baseUrl: options.baseUrl,
1666
+ onboarder: options.onboarder,
1667
+ fetch: options.fetch,
1668
+ timeoutMs: options.timeoutMs,
1669
+ cache
1670
+ });
1671
+ if (unmounted) return;
1672
+ siteKey = options.siteKey ?? fresh.botChallenge?.siteKey ?? "";
1673
+ currentSpecVersion = fresh.specVersion;
1674
+ control.applyNewConsentPack(fresh);
1675
+ idempotencyKey = null;
1676
+ lastFingerprint = null;
1677
+ }
949
1678
  async function handleSubmit(control) {
950
1679
  if (!control.validateAll()) return;
951
1680
  control.acknowledgeSpecStale();
@@ -957,14 +1686,16 @@ function mountSignupForm(host, options) {
957
1686
  lastFingerprint = fingerprint;
958
1687
  }
959
1688
  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;
1689
+ if (siteKey) {
1690
+ try {
1691
+ firstToken = await executeRecaptcha(siteKey, "signup_submit", {
1692
+ timeoutMs: options.timeoutMs
1693
+ });
1694
+ } catch (recaptchaErr) {
1695
+ control.setError("Could not load bot protection. Please try again.");
1696
+ options.onError?.(recaptchaErr);
1697
+ return;
1698
+ }
968
1699
  }
969
1700
  let usedFirst = false;
970
1701
  const tokenProvider = async () => {
@@ -979,7 +1710,7 @@ function mountSignupForm(host, options) {
979
1710
  try {
980
1711
  const result = await submitSignup(body, {
981
1712
  baseUrl: options.baseUrl,
982
- botToken: tokenProvider,
1713
+ ...siteKey ? { botToken: tokenProvider } : {},
983
1714
  idempotencyKey,
984
1715
  fetch: options.fetch,
985
1716
  timeoutMs: options.timeoutMs
@@ -1000,6 +1731,27 @@ function mountSignupForm(host, options) {
1000
1731
  return;
1001
1732
  }
1002
1733
  options.onError?.(err);
1734
+ } else if (err instanceof OnboardingConsentStaleError) {
1735
+ try {
1736
+ await handleConsentStale(control);
1737
+ } catch (refetchErr) {
1738
+ control.setError(
1739
+ "The legal documents changed and could not be reloaded. Please refresh the page."
1740
+ );
1741
+ options.onError?.(refetchErr);
1742
+ return;
1743
+ }
1744
+ options.onError?.(err);
1745
+ } else if (err instanceof OnboardingConsentIncompleteError) {
1746
+ control.validateAll();
1747
+ control.setError("Please accept all required documents to continue.");
1748
+ options.onError?.(err);
1749
+ } else if (err instanceof OnboardingSubdomainTakenError) {
1750
+ control.setAccountError(
1751
+ "subdomain",
1752
+ "This workspace address is already taken. Choose another one."
1753
+ );
1754
+ options.onError?.(err);
1003
1755
  } else if (err instanceof OnboardingBotChallengeError) {
1004
1756
  control.setError(
1005
1757
  "Bot protection check failed. Please refresh the page and try again."
@@ -1017,14 +1769,15 @@ function mountSignupForm(host, options) {
1017
1769
  }
1018
1770
  }
1019
1771
  function onStateChange(control, state) {
1020
- if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale) {
1772
+ if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale || state.consentStale !== renderedConsentStale) {
1021
1773
  renderedStepIndex = state.stepIndex;
1022
1774
  renderedStale = state.specStale;
1775
+ renderedConsentStale = state.consentStale;
1023
1776
  renderStep(control, state);
1024
1777
  renderStepsIndicator(state);
1025
1778
  }
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.` : "";
1779
+ staleBanner.style.display = state.specStale || state.consentStale ? "" : "none";
1780
+ 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." : "";
1028
1781
  setFieldErrorsInPlace(
1029
1782
  stepArea,
1030
1783
  state.errors,
@@ -1037,6 +1790,12 @@ function mountSignupForm(host, options) {
1037
1790
  const inputNode = stepArea.querySelector(idSelector(id));
1038
1791
  if (inputNode) inputNode.classList.add("has-error");
1039
1792
  }
1793
+ const consentStage = stepArea.querySelector(
1794
+ ".nodii-onboarding-consent-stage"
1795
+ );
1796
+ if (consentStage) {
1797
+ updateConsentStep(consentStage, state.consentState, state.consentErrors);
1798
+ }
1040
1799
  if (state.serverError) {
1041
1800
  serverErrorEl.textContent = state.serverError;
1042
1801
  serverErrorEl.style.display = "";
@@ -1080,7 +1839,7 @@ function mountSignupForm(host, options) {
1080
1839
  return;
1081
1840
  }
1082
1841
  if (unmounted) return;
1083
- siteKey = options.siteKey ?? spec.botChallenge.siteKey;
1842
+ siteKey = options.siteKey ?? spec.botChallenge?.siteKey ?? "";
1084
1843
  currentSpecVersion = spec.specVersion;
1085
1844
  const control = createSignupStepper(spec);
1086
1845
  stepper = control;
@@ -1144,12 +1903,12 @@ var OnboardingWidget = {
1144
1903
  * });
1145
1904
  * ```
1146
1905
  */
1147
- mount(el3, options) {
1906
+ mount(el4, options) {
1148
1907
  if (options.form === "contact") {
1149
- return mountContactForm(el3, options);
1908
+ return mountContactForm(el4, options);
1150
1909
  }
1151
1910
  if (options.form === "signup") {
1152
- return mountSignupForm(el3, options);
1911
+ return mountSignupForm(el4, options);
1153
1912
  }
1154
1913
  throw new Error(
1155
1914
  `OnboardingWidget.mount: unknown form "${options.form}". Supported: "contact", "signup".`
@@ -1157,6 +1916,6 @@ var OnboardingWidget = {
1157
1916
  }
1158
1917
  };
1159
1918
 
1160
- export { DEFAULT_WIDGET_CSS, OnboardingWidget, cssEscape, idSelector, injectWidgetStyles, mountContactForm, mountSignupForm };
1161
- //# sourceMappingURL=chunk-SWMIIUXR.js.map
1162
- //# sourceMappingURL=chunk-SWMIIUXR.js.map
1919
+ export { DEFAULT_WIDGET_CSS, OnboardingWidget, consentDocElementId, cssEscape, idSelector, injectWidgetStyles, mountContactForm, mountSignupForm, renderConsentStep, updateConsentStep };
1920
+ //# sourceMappingURL=chunk-OVULPWNZ.js.map
1921
+ //# sourceMappingURL=chunk-OVULPWNZ.js.map