@nodii/onboarding 0.1.0-dev.0 → 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.
Files changed (66) hide show
  1. package/README.md +78 -0
  2. package/dist/chunk-AZE3NPSY.js +186 -0
  3. package/dist/chunk-AZE3NPSY.js.map +1 -0
  4. package/dist/{chunk-BQCTDU5V.js → chunk-BMQINH6O.js} +89 -15
  5. package/dist/chunk-BMQINH6O.js.map +1 -0
  6. package/dist/{chunk-34BQX2KR.js → chunk-F75ZJERM.js} +41 -6
  7. package/dist/chunk-F75ZJERM.js.map +1 -0
  8. package/dist/chunk-FIRSA7EH.js +3 -0
  9. package/dist/{chunk-ZDY2I6KY.js.map → chunk-FIRSA7EH.js.map} +1 -1
  10. package/dist/{chunk-SWMIIUXR.js → chunk-IPAKPLIF.js} +590 -92
  11. package/dist/chunk-IPAKPLIF.js.map +1 -0
  12. package/dist/chunk-JFDQZ3AC.js +3 -0
  13. package/dist/chunk-JFDQZ3AC.js.map +1 -0
  14. package/dist/{chunk-4SWOVHZ2.js → chunk-KQTS47FC.js} +129 -17
  15. package/dist/chunk-KQTS47FC.js.map +1 -0
  16. package/dist/chunk-LVJM4CTG.js +306 -0
  17. package/dist/chunk-LVJM4CTG.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-SIMLMQPT.js +37 -0
  21. package/dist/chunk-SIMLMQPT.js.map +1 -0
  22. package/dist/chunk-U56GQITG.js +79 -0
  23. package/dist/chunk-U56GQITG.js.map +1 -0
  24. package/dist/{chunk-AFCJLUPK.js → chunk-U6AXKGYQ.js} +14 -5
  25. package/dist/chunk-U6AXKGYQ.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/client-CqE2dF9A.d.ts +86 -0
  29. package/dist/consent/index.d.ts +59 -0
  30. package/dist/consent/index.js +7 -0
  31. package/dist/consent/index.js.map +1 -0
  32. package/dist/controller-BQb62D0N.d.ts +177 -0
  33. package/dist/dynamic/index.d.ts +57 -6
  34. package/dist/dynamic/index.js +4 -2
  35. package/dist/errors-vJkye4a0.d.ts +110 -0
  36. package/dist/form/index.d.ts +2 -2
  37. package/dist/form/index.js +2 -2
  38. package/dist/index-Dqfa890U.d.ts +83 -0
  39. package/dist/index.d.ts +12 -6
  40. package/dist/index.js +12 -8
  41. package/dist/{model-IEyDI54w.d.ts → model-ESLSEbi0.d.ts} +199 -46
  42. package/dist/model-mnjAEtKe.d.ts +253 -0
  43. package/dist/render-DC7P2BGQ.js +5 -0
  44. package/dist/render-DC7P2BGQ.js.map +1 -0
  45. package/dist/schema-BrXb9gnh.d.ts +267 -0
  46. package/dist/{signup-client-Bl6eUrxR.d.ts → signup-client-BL5DfarJ.d.ts} +152 -19
  47. package/dist/spec/index.d.ts +10 -81
  48. package/dist/spec/index.js +3 -2
  49. package/dist/submit/index.d.ts +7 -71
  50. package/dist/submit/index.js +3 -3
  51. package/dist/viewer/index.d.ts +4 -0
  52. package/dist/viewer/index.js +5 -0
  53. package/dist/viewer/index.js.map +1 -0
  54. package/dist/widget/index.d.ts +50 -7
  55. package/dist/widget/index.js +9 -7
  56. package/package.json +21 -2
  57. package/dist/chunk-34BQX2KR.js.map +0 -1
  58. package/dist/chunk-4SWOVHZ2.js.map +0 -1
  59. package/dist/chunk-AFCJLUPK.js.map +0 -1
  60. package/dist/chunk-AW7W3HAC.js.map +0 -1
  61. package/dist/chunk-BIFANV7M.js +0 -22
  62. package/dist/chunk-BIFANV7M.js.map +0 -1
  63. package/dist/chunk-BQCTDU5V.js.map +0 -1
  64. package/dist/chunk-SWMIIUXR.js.map +0 -1
  65. package/dist/chunk-ZDY2I6KY.js +0 -3
  66. package/dist/schema-TfyBU6JW.d.ts +0 -107
@@ -1,8 +1,8 @@
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-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
6
  import { loadRecaptcha, executeRecaptcha } from './chunk-7NS7ENJV.js';
7
7
 
8
8
  // src/widget/dom-utils.ts
@@ -243,6 +243,193 @@ var DEFAULT_WIDGET_CSS = `
243
243
  text-align: center;
244
244
  color: var(--_label);
245
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
+ }
246
433
  `;
247
434
  function injectWidgetStyles() {
248
435
  if (typeof document === "undefined") return;
@@ -295,13 +482,13 @@ function setFieldError(container, fieldId, message) {
295
482
  if (inputEl) inputEl.classList.add("has-error");
296
483
  }
297
484
  function clearFieldErrors(container) {
298
- for (const el3 of container.querySelectorAll(
485
+ for (const el4 of container.querySelectorAll(
299
486
  ".nodii-onboarding-field-error"
300
487
  )) {
301
- el3.textContent = "";
488
+ el4.textContent = "";
302
489
  }
303
- for (const el3 of container.querySelectorAll(".has-error")) {
304
- el3.classList.remove("has-error");
490
+ for (const el4 of container.querySelectorAll(".has-error")) {
491
+ el4.classList.remove("has-error");
305
492
  }
306
493
  }
307
494
  function applyErrors(container, errors) {
@@ -313,8 +500,10 @@ function applyErrors(container, errors) {
313
500
  function mountContactForm(host, options) {
314
501
  injectWidgetStyles();
315
502
  const formControl = createContactFormState();
316
- loadRecaptcha(options.siteKey).catch(() => {
317
- });
503
+ if (options.siteKey) {
504
+ loadRecaptcha(options.siteKey).catch(() => {
505
+ });
506
+ }
318
507
  const wrapper = el("div", { class: "nodii-onboarding-widget" });
319
508
  const nameInput = el("input", {
320
509
  type: "text",
@@ -475,14 +664,18 @@ function mountContactForm(host, options) {
475
664
  lastFingerprint = fingerprint;
476
665
  }
477
666
  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;
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
+ }
486
679
  }
487
680
  let usedFirst = false;
488
681
  const tokenProvider = async () => {
@@ -497,7 +690,7 @@ function mountContactForm(host, options) {
497
690
  try {
498
691
  await submitLead(values, {
499
692
  baseUrl: options.baseUrl,
500
- botToken: tokenProvider,
693
+ ...options.siteKey ? { botToken: tokenProvider } : {},
501
694
  idempotencyKey,
502
695
  fetch: options.fetch,
503
696
  timeoutMs: options.timeoutMs
@@ -543,13 +736,243 @@ function mountContactForm(host, options) {
543
736
  };
544
737
  }
545
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
+
546
969
  // src/widget/signup-form.ts
547
970
  var STAGE_TITLES = {
548
971
  lead: "About you",
549
972
  prospect: "Your business",
550
973
  activation: "Choose your services"
551
974
  };
552
- function el2(tag, attrs = {}, children = []) {
975
+ function el3(tag, attrs = {}, children = []) {
553
976
  const element = document.createElement(tag);
554
977
  for (const [key, value] of Object.entries(attrs)) {
555
978
  element.setAttribute(key, value);
@@ -574,7 +997,7 @@ function buildFieldInput(field, currentValue, onChange) {
574
997
  const id = `nodii-f-${field.key}`;
575
998
  switch (field.type) {
576
999
  case "textarea": {
577
- const input = el2("textarea", {
1000
+ const input = el3("textarea", {
578
1001
  id,
579
1002
  class: "nodii-onboarding-textarea",
580
1003
  rows: "4"
@@ -585,7 +1008,7 @@ function buildFieldInput(field, currentValue, onChange) {
585
1008
  }
586
1009
  case "checkbox":
587
1010
  case "boolean": {
588
- const input = el2("input", {
1011
+ const input = el3("input", {
589
1012
  type: "checkbox",
590
1013
  id
591
1014
  });
@@ -595,14 +1018,14 @@ function buildFieldInput(field, currentValue, onChange) {
595
1018
  }
596
1019
  case "select":
597
1020
  case "radio": {
598
- const select = el2("select", {
1021
+ const select = el3("select", {
599
1022
  id,
600
1023
  class: "nodii-onboarding-input"
601
1024
  });
602
- select.appendChild(el2("option", { value: "" }, ["\u2014 select \u2014"]));
1025
+ select.appendChild(el3("option", { value: "" }, ["\u2014 select \u2014"]));
603
1026
  for (const opt of field.options ?? []) {
604
1027
  const value = optionValue(opt);
605
- const optionEl = el2("option", { value }, [optionLabel(opt)]);
1028
+ const optionEl = el3("option", { value }, [optionLabel(opt)]);
606
1029
  select.appendChild(optionEl);
607
1030
  }
608
1031
  if (typeof currentValue === "string") select.value = currentValue;
@@ -610,7 +1033,7 @@ function buildFieldInput(field, currentValue, onChange) {
610
1033
  return select;
611
1034
  }
612
1035
  case "multiselect": {
613
- const select = el2("select", {
1036
+ const select = el3("select", {
614
1037
  id,
615
1038
  class: "nodii-onboarding-input",
616
1039
  multiple: "multiple"
@@ -618,7 +1041,7 @@ function buildFieldInput(field, currentValue, onChange) {
618
1041
  const current = Array.isArray(currentValue) ? currentValue.map(String) : [];
619
1042
  for (const opt of field.options ?? []) {
620
1043
  const value = optionValue(opt);
621
- const optionEl = el2("option", { value }, [
1044
+ const optionEl = el3("option", { value }, [
622
1045
  optionLabel(opt)
623
1046
  ]);
624
1047
  if (current.includes(value)) optionEl.selected = true;
@@ -635,7 +1058,7 @@ function buildFieldInput(field, currentValue, onChange) {
635
1058
  }
636
1059
  default: {
637
1060
  const typeAttr = field.type === "email" || field.type === "tel" || field.type === "number" || field.type === "date" ? field.type : "text";
638
- const input = el2("input", {
1061
+ const input = el3("input", {
639
1062
  type: typeAttr,
640
1063
  id,
641
1064
  class: "nodii-onboarding-input"
@@ -649,7 +1072,7 @@ function buildFieldInput(field, currentValue, onChange) {
649
1072
  }
650
1073
  function fieldRow(field, input, idOverride) {
651
1074
  const id = idOverride ?? `nodii-f-${field.key}`;
652
- const labelEl = el2(
1075
+ const labelEl = el3(
653
1076
  "label",
654
1077
  {
655
1078
  for: id,
@@ -657,13 +1080,13 @@ function fieldRow(field, input, idOverride) {
657
1080
  },
658
1081
  [field.label]
659
1082
  );
660
- const errorEl = el2("div", {
1083
+ const errorEl = el3("div", {
661
1084
  class: "nodii-onboarding-field-error",
662
1085
  id: `${id}-error`,
663
1086
  role: "alert",
664
1087
  "aria-live": "polite"
665
1088
  });
666
- return el2("div", { class: "nodii-onboarding-field" }, [
1089
+ return el3("div", { class: "nodii-onboarding-field" }, [
667
1090
  labelEl,
668
1091
  input,
669
1092
  errorEl
@@ -671,8 +1094,8 @@ function fieldRow(field, input, idOverride) {
671
1094
  }
672
1095
  function mountSignupForm(host, options) {
673
1096
  injectWidgetStyles();
674
- const wrapper = el2("div", { class: "nodii-onboarding-widget" });
675
- const loadingEl = el2("div", { class: "nodii-onboarding-loading" }, [
1097
+ const wrapper = el3("div", { class: "nodii-onboarding-widget" });
1098
+ const loadingEl = el3("div", { class: "nodii-onboarding-loading" }, [
676
1099
  "Loading signup form\u2026"
677
1100
  ]);
678
1101
  wrapper.appendChild(loadingEl);
@@ -686,27 +1109,28 @@ function mountSignupForm(host, options) {
686
1109
  let lastFingerprint = null;
687
1110
  let renderedStepIndex = -1;
688
1111
  let renderedStale = false;
1112
+ let renderedConsentStale = false;
689
1113
  let currentSpecVersion = null;
690
1114
  let staleDiffSummary = "";
691
- const stepArea = el2("div", {});
692
- const staleBanner = el2("div", {
1115
+ const stepArea = el3("div", {});
1116
+ const staleBanner = el3("div", {
693
1117
  class: "nodii-onboarding-stale-banner",
694
1118
  role: "alert",
695
1119
  style: "display:none"
696
1120
  });
697
- const serverErrorEl = el2("div", {
1121
+ const serverErrorEl = el3("div", {
698
1122
  class: "nodii-onboarding-server-error",
699
1123
  role: "alert",
700
1124
  "aria-live": "assertive",
701
1125
  style: "display:none"
702
1126
  });
703
- const successEl = el2("div", {
1127
+ const successEl = el3("div", {
704
1128
  class: "nodii-onboarding-success",
705
1129
  role: "status",
706
1130
  style: "display:none"
707
1131
  });
708
- const stepsIndicator = el2("div", { class: "nodii-onboarding-steps" });
709
- const form = el2("form", { novalidate: "true", "aria-label": "Sign up" });
1132
+ const stepsIndicator = el3("div", { class: "nodii-onboarding-steps" });
1133
+ const form = el3("form", { novalidate: "true", "aria-label": "Sign up" });
710
1134
  function setFieldErrorsInPlace(container, errors, idPrefix) {
711
1135
  for (const errEl of container.querySelectorAll(
712
1136
  ".nodii-onboarding-field-error"
@@ -726,7 +1150,7 @@ function mountSignupForm(host, options) {
726
1150
  }
727
1151
  function accountIdOf(path) {
728
1152
  if (path === "email") return "nodii-account-email";
729
- if (path === "password") return "nodii-account-password";
1153
+ if (path === "displayName") return "nodii-account-display-name";
730
1154
  if (path === "tosAccepted") return "nodii-consent-tos";
731
1155
  if (path === "privacyAccepted") return "nodii-consent-privacy";
732
1156
  return `nodii-account-${path}`;
@@ -737,15 +1161,15 @@ function mountSignupForm(host, options) {
737
1161
  }
738
1162
  for (let i = 0; i < state.stepCount; i++) {
739
1163
  stepsIndicator.appendChild(
740
- el2("div", {
1164
+ el3("div", {
741
1165
  class: `nodii-onboarding-step-dot${i <= state.stepIndex ? " active" : ""}`
742
1166
  })
743
1167
  );
744
1168
  }
745
1169
  }
746
1170
  function renderAccountSection(control, state) {
747
- const section = el2("div", { class: "nodii-onboarding-account-section" });
748
- const emailInput = el2("input", {
1171
+ const section = el3("div", { class: "nodii-onboarding-account-section" });
1172
+ const emailInput = el3("input", {
749
1173
  type: "email",
750
1174
  id: "nodii-account-email",
751
1175
  class: "nodii-onboarding-input",
@@ -768,32 +1192,54 @@ function mountSignupForm(host, options) {
768
1192
  // label for + error id match the real input id
769
1193
  )
770
1194
  );
771
- const passwordInput = el2("input", {
772
- type: "password",
773
- id: "nodii-account-password",
1195
+ const displayNameInput = el3("input", {
1196
+ type: "text",
1197
+ id: "nodii-account-display-name",
774
1198
  class: "nodii-onboarding-input",
775
- autocomplete: "new-password",
776
- minlength: "12",
777
- maxlength: "256"
1199
+ autocomplete: "name",
1200
+ maxlength: "200"
778
1201
  });
779
- passwordInput.value = state.credentials.password;
780
- passwordInput.addEventListener(
1202
+ displayNameInput.value = state.credentials.displayName;
1203
+ displayNameInput.addEventListener(
781
1204
  "input",
782
- () => control.setCredentials({ password: passwordInput.value })
1205
+ () => control.setCredentials({ displayName: displayNameInput.value })
783
1206
  );
784
1207
  section.appendChild(
785
1208
  fieldRow(
786
1209
  {
787
- key: "account-password",
788
- label: "Password (12+ characters)",
1210
+ key: "account-display-name",
1211
+ label: "Your name",
789
1212
  required: true
790
1213
  },
791
- passwordInput,
792
- "nodii-account-password"
793
- // label for + error id match the real input id
1214
+ displayNameInput,
1215
+ "nodii-account-display-name"
794
1216
  )
795
1217
  );
796
- const tosCheckbox = el2("input", {
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", {
797
1243
  type: "checkbox",
798
1244
  id: "nodii-consent-tos"
799
1245
  });
@@ -805,7 +1251,7 @@ function mountSignupForm(host, options) {
805
1251
  const tosLabelChildren = ["I accept the "];
806
1252
  if (options.tosUrl) {
807
1253
  tosLabelChildren.push(
808
- el2(
1254
+ el3(
809
1255
  "a",
810
1256
  {
811
1257
  href: options.tosUrl,
@@ -819,19 +1265,19 @@ function mountSignupForm(host, options) {
819
1265
  tosLabelChildren.push("Terms of Service");
820
1266
  }
821
1267
  section.appendChild(
822
- el2("div", { class: "nodii-onboarding-consent-row" }, [
1268
+ el3("div", { class: "nodii-onboarding-consent-row" }, [
823
1269
  tosCheckbox,
824
- el2("label", { for: "nodii-consent-tos" }, tosLabelChildren)
1270
+ el3("label", { for: "nodii-consent-tos" }, tosLabelChildren)
825
1271
  ])
826
1272
  );
827
1273
  section.appendChild(
828
- el2("div", {
1274
+ el3("div", {
829
1275
  class: "nodii-onboarding-field-error",
830
1276
  id: "nodii-consent-tos-error",
831
1277
  role: "alert"
832
1278
  })
833
1279
  );
834
- const privacyCheckbox = el2("input", {
1280
+ const privacyCheckbox = el3("input", {
835
1281
  type: "checkbox",
836
1282
  id: "nodii-consent-privacy"
837
1283
  });
@@ -843,7 +1289,7 @@ function mountSignupForm(host, options) {
843
1289
  const privacyLabelChildren = ["I accept the "];
844
1290
  if (options.privacyUrl) {
845
1291
  privacyLabelChildren.push(
846
- el2(
1292
+ el3(
847
1293
  "a",
848
1294
  {
849
1295
  href: options.privacyUrl,
@@ -857,13 +1303,13 @@ function mountSignupForm(host, options) {
857
1303
  privacyLabelChildren.push("Privacy Policy");
858
1304
  }
859
1305
  section.appendChild(
860
- el2("div", { class: "nodii-onboarding-consent-row" }, [
1306
+ el3("div", { class: "nodii-onboarding-consent-row" }, [
861
1307
  privacyCheckbox,
862
- el2("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
1308
+ el3("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
863
1309
  ])
864
1310
  );
865
1311
  section.appendChild(
866
- el2("div", {
1312
+ el3("div", {
867
1313
  class: "nodii-onboarding-field-error",
868
1314
  id: "nodii-consent-privacy-error",
869
1315
  role: "alert"
@@ -876,7 +1322,7 @@ function mountSignupForm(host, options) {
876
1322
  const step = state.currentStep;
877
1323
  if (step) {
878
1324
  stepArea.appendChild(
879
- el2("h3", { class: "nodii-onboarding-stage-title" }, [
1325
+ el3("h3", { class: "nodii-onboarding-stage-title" }, [
880
1326
  STAGE_TITLES[step.stage] ?? step.stage
881
1327
  ])
882
1328
  );
@@ -891,11 +1337,24 @@ function mountSignupForm(host, options) {
891
1337
  }
892
1338
  const isLast = control.isLastStep();
893
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
+ }
894
1353
  stepArea.appendChild(renderAccountSection(control, state));
895
1354
  }
896
- const nav = el2("div", { class: "nodii-onboarding-nav" });
1355
+ const nav = el3("div", { class: "nodii-onboarding-nav" });
897
1356
  if (!control.isFirstStep()) {
898
- const backBtn = el2(
1357
+ const backBtn = el3(
899
1358
  "button",
900
1359
  { type: "button", class: "nodii-onboarding-back-btn" },
901
1360
  ["Back"]
@@ -903,7 +1362,7 @@ function mountSignupForm(host, options) {
903
1362
  backBtn.addEventListener("click", () => control.back());
904
1363
  nav.appendChild(backBtn);
905
1364
  }
906
- const forwardBtn = el2(
1365
+ const forwardBtn = el3(
907
1366
  "button",
908
1367
  {
909
1368
  type: isLast ? "submit" : "button",
@@ -940,12 +1399,27 @@ function mountSignupForm(host, options) {
940
1399
  } else {
941
1400
  staleDiffSummary = "";
942
1401
  }
943
- siteKey = options.siteKey ?? fresh.botChallenge.siteKey;
1402
+ siteKey = options.siteKey ?? fresh.botChallenge?.siteKey ?? "";
944
1403
  currentSpecVersion = fresh.specVersion;
945
1404
  control.applyNewSpec(fresh);
946
1405
  idempotencyKey = null;
947
1406
  lastFingerprint = null;
948
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
+ }
949
1423
  async function handleSubmit(control) {
950
1424
  if (!control.validateAll()) return;
951
1425
  control.acknowledgeSpecStale();
@@ -957,14 +1431,16 @@ function mountSignupForm(host, options) {
957
1431
  lastFingerprint = fingerprint;
958
1432
  }
959
1433
  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;
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
+ }
968
1444
  }
969
1445
  let usedFirst = false;
970
1446
  const tokenProvider = async () => {
@@ -979,7 +1455,7 @@ function mountSignupForm(host, options) {
979
1455
  try {
980
1456
  const result = await submitSignup(body, {
981
1457
  baseUrl: options.baseUrl,
982
- botToken: tokenProvider,
1458
+ ...siteKey ? { botToken: tokenProvider } : {},
983
1459
  idempotencyKey,
984
1460
  fetch: options.fetch,
985
1461
  timeoutMs: options.timeoutMs
@@ -1000,6 +1476,21 @@ function mountSignupForm(host, options) {
1000
1476
  return;
1001
1477
  }
1002
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);
1003
1494
  } else if (err instanceof OnboardingBotChallengeError) {
1004
1495
  control.setError(
1005
1496
  "Bot protection check failed. Please refresh the page and try again."
@@ -1017,14 +1508,15 @@ function mountSignupForm(host, options) {
1017
1508
  }
1018
1509
  }
1019
1510
  function onStateChange(control, state) {
1020
- if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale) {
1511
+ if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale || state.consentStale !== renderedConsentStale) {
1021
1512
  renderedStepIndex = state.stepIndex;
1022
1513
  renderedStale = state.specStale;
1514
+ renderedConsentStale = state.consentStale;
1023
1515
  renderStep(control, state);
1024
1516
  renderStepsIndicator(state);
1025
1517
  }
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.` : "";
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." : "";
1028
1520
  setFieldErrorsInPlace(
1029
1521
  stepArea,
1030
1522
  state.errors,
@@ -1037,6 +1529,12 @@ function mountSignupForm(host, options) {
1037
1529
  const inputNode = stepArea.querySelector(idSelector(id));
1038
1530
  if (inputNode) inputNode.classList.add("has-error");
1039
1531
  }
1532
+ const consentStage = stepArea.querySelector(
1533
+ ".nodii-onboarding-consent-stage"
1534
+ );
1535
+ if (consentStage) {
1536
+ updateConsentStep(consentStage, state.consentState, state.consentErrors);
1537
+ }
1040
1538
  if (state.serverError) {
1041
1539
  serverErrorEl.textContent = state.serverError;
1042
1540
  serverErrorEl.style.display = "";
@@ -1080,7 +1578,7 @@ function mountSignupForm(host, options) {
1080
1578
  return;
1081
1579
  }
1082
1580
  if (unmounted) return;
1083
- siteKey = options.siteKey ?? spec.botChallenge.siteKey;
1581
+ siteKey = options.siteKey ?? spec.botChallenge?.siteKey ?? "";
1084
1582
  currentSpecVersion = spec.specVersion;
1085
1583
  const control = createSignupStepper(spec);
1086
1584
  stepper = control;
@@ -1144,12 +1642,12 @@ var OnboardingWidget = {
1144
1642
  * });
1145
1643
  * ```
1146
1644
  */
1147
- mount(el3, options) {
1645
+ mount(el4, options) {
1148
1646
  if (options.form === "contact") {
1149
- return mountContactForm(el3, options);
1647
+ return mountContactForm(el4, options);
1150
1648
  }
1151
1649
  if (options.form === "signup") {
1152
- return mountSignupForm(el3, options);
1650
+ return mountSignupForm(el4, options);
1153
1651
  }
1154
1652
  throw new Error(
1155
1653
  `OnboardingWidget.mount: unknown form "${options.form}". Supported: "contact", "signup".`
@@ -1157,6 +1655,6 @@ var OnboardingWidget = {
1157
1655
  }
1158
1656
  };
1159
1657
 
1160
- export { DEFAULT_WIDGET_CSS, OnboardingWidget, cssEscape, idSelector, injectWidgetStyles, mountContactForm, mountSignupForm };
1161
- //# sourceMappingURL=chunk-SWMIIUXR.js.map
1162
- //# sourceMappingURL=chunk-SWMIIUXR.js.map
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