bison-web-components 1.0.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.
@@ -0,0 +1,3981 @@
1
+ /**
2
+ * WIO Onboarding Web Component
3
+ *
4
+ * A web component that captures WIO information via inline stepper form
5
+ * with necessary field validations. This displays the form directly without
6
+ * requiring a button to open a modal.
7
+ *
8
+ * @requires BisonJibPayAPI - Must be loaded before this component (from api.js)
9
+ *
10
+ * @author @kfajardo
11
+ * @version 1.0.0
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <script src="api.js"></script>
16
+ * <script src="wio-onboarding.js"></script>
17
+ *
18
+ * <wio-onboarding id="onboarding"></wio-onboarding>
19
+ * <script>
20
+ * const onboarding = document.getElementById('onboarding');
21
+ * onboarding.onSuccess = (data) => console.log('Success!', data);
22
+ * onboarding.onError = (error) => console.error('Error:', error);
23
+ * </script>
24
+ * ```
25
+ */
26
+
27
+ class WioOnboarding extends HTMLElement {
28
+ constructor() {
29
+ super();
30
+ this.attachShadow({ mode: "open" });
31
+
32
+ // API Configuration
33
+ this.apiBaseURL =
34
+ this.getAttribute("api-base-url") ||
35
+ "https://bison-jib-development.azurewebsites.net";
36
+ this.embeddableKey =
37
+ this.getAttribute("embeddable-key") ||
38
+ "R80WMkbNN8457RofiMYx03DL65P06IaVT30Q2emYJUBQwYCzRC";
39
+
40
+ // Check if BisonJibPayAPI is available
41
+ const BisonJibPayAPIClass =
42
+ typeof BisonJibPayAPI !== "undefined"
43
+ ? BisonJibPayAPI
44
+ : typeof window !== "undefined" && window.BisonJibPayAPI;
45
+
46
+ if (!BisonJibPayAPIClass) {
47
+ console.error(
48
+ "WioOnboarding: BisonJibPayAPI is not available. Please ensure api.js is loaded before wio-onboarding.js"
49
+ );
50
+ console.error(
51
+ "Available global objects:",
52
+ Object.keys(window).filter((k) => k.includes("Bison"))
53
+ );
54
+ this.api = null;
55
+ } else {
56
+ console.log(
57
+ "✅ WioOnboarding: BisonJibPayAPI found, initializing API client"
58
+ );
59
+ this.api = new BisonJibPayAPIClass(this.apiBaseURL, this.embeddableKey);
60
+ console.log("✅ WioOnboarding: API client initialized successfully");
61
+ }
62
+
63
+ // Initialize state (no isModalOpen needed for inline component)
64
+ this.state = {
65
+ currentStep: 0,
66
+ totalSteps: 3, // Business, Representatives, Business Verification
67
+ isSubmitted: false,
68
+ isFailed: false,
69
+ isSubmissionFailed: false,
70
+ formData: {
71
+ businessDetails: {
72
+ businessName: "",
73
+ doingBusinessAs: "",
74
+ ein: "",
75
+ businessWebsite: "",
76
+ businessPhoneNumber: "",
77
+ businessEmail: "",
78
+ BusinessAddress1: "",
79
+ businessAddress2: "",
80
+ businessCity: "",
81
+ businessState: "",
82
+ businessPostalCode: "",
83
+ },
84
+ representatives: [],
85
+ businessVerification: {
86
+ verificationDocuments: [],
87
+ },
88
+
89
+ },
90
+ validationState: {
91
+ step0: { isValid: false, errors: {} }, // Business Details
92
+ step1: { isValid: false, errors: {} }, // Representatives
93
+ step2: { isValid: false, errors: {} }, // Business Verification (required)
94
+ },
95
+ completedSteps: new Set(),
96
+ uiState: {
97
+ isLoading: false,
98
+ showErrors: false,
99
+ errorMessage: null,
100
+ },
101
+ };
102
+
103
+ // Step configuration
104
+ this.STEPS = [
105
+ {
106
+ id: "business-details",
107
+ title: "Business",
108
+ description: "Provide your business details",
109
+ canSkip: false,
110
+ },
111
+
112
+ {
113
+ id: "representative-details",
114
+ title: "Representative",
115
+ description: "Add representative information (required)",
116
+ canSkip: false,
117
+ },
118
+ {
119
+ id: "business-verification",
120
+ title: "Business Verification",
121
+ description: "Upload required documents for verification",
122
+ canSkip: false,
123
+ },
124
+ ];
125
+
126
+ // US States for dropdown
127
+ this.US_STATES = [
128
+ "AL",
129
+ "AK",
130
+ "AZ",
131
+ "AR",
132
+ "CA",
133
+ "CO",
134
+ "CT",
135
+ "DE",
136
+ "FL",
137
+ "GA",
138
+ "HI",
139
+ "ID",
140
+ "IL",
141
+ "IN",
142
+ "IA",
143
+ "KS",
144
+ "KY",
145
+ "LA",
146
+ "ME",
147
+ "MD",
148
+ "MA",
149
+ "MI",
150
+ "MN",
151
+ "MS",
152
+ "MO",
153
+ "MT",
154
+ "NE",
155
+ "NV",
156
+ "NH",
157
+ "NJ",
158
+ "NM",
159
+ "NY",
160
+ "NC",
161
+ "ND",
162
+ "OH",
163
+ "OK",
164
+ "OR",
165
+ "PA",
166
+ "RI",
167
+ "SC",
168
+ "SD",
169
+ "TN",
170
+ "TX",
171
+ "UT",
172
+ "VT",
173
+ "VA",
174
+ "WA",
175
+ "WV",
176
+ "WI",
177
+ "WY",
178
+ ];
179
+
180
+ // Internal callback storage
181
+ this._onSuccessCallback = null;
182
+ this._onErrorCallback = null;
183
+ this._onSubmitCallback = null;
184
+ this._onConfirmCallback = null;
185
+ this._onDoneCallback = null;
186
+ this._initialData = null;
187
+ this._doneButtonText = "Done";
188
+
189
+ this.render();
190
+ }
191
+
192
+ // ==================== GETTERS/SETTERS ====================
193
+
194
+ get onSuccess() {
195
+ return this._onSuccessCallback;
196
+ }
197
+
198
+ set onSuccess(callback) {
199
+ if (typeof callback === "function" || callback === null) {
200
+ this._onSuccessCallback = callback;
201
+ }
202
+ }
203
+
204
+ get onError() {
205
+ return this._onErrorCallback;
206
+ }
207
+
208
+ set onError(callback) {
209
+ if (typeof callback === "function" || callback === null) {
210
+ this._onErrorCallback = callback;
211
+ }
212
+ }
213
+
214
+ get onSubmit() {
215
+ return this._onSubmitCallback;
216
+ }
217
+
218
+ set onSubmit(callback) {
219
+ if (typeof callback === "function" || callback === null) {
220
+ this._onSubmitCallback = callback;
221
+ }
222
+ }
223
+
224
+ get onConfirm() {
225
+ return this._onConfirmCallback;
226
+ }
227
+
228
+ set onConfirm(callback) {
229
+ if (typeof callback === "function" || callback === null) {
230
+ this._onConfirmCallback = callback;
231
+ }
232
+ }
233
+
234
+ get onLoad() {
235
+ return this._initialData;
236
+ }
237
+
238
+ set onLoad(data) {
239
+ if (data && typeof data === "object") {
240
+ this._initialData = data;
241
+ this.loadInitialData(data);
242
+ }
243
+ }
244
+
245
+ get onDone() {
246
+ return this._onDoneCallback;
247
+ }
248
+
249
+ set onDone(callback) {
250
+ if (typeof callback === "function" || callback === null) {
251
+ this._onDoneCallback = callback;
252
+ }
253
+ }
254
+
255
+ get doneButtonText() {
256
+ return this._doneButtonText;
257
+ }
258
+
259
+ set doneButtonText(value) {
260
+ this._doneButtonText = value;
261
+ this.render();
262
+ }
263
+
264
+ static get observedAttributes() {
265
+ return [
266
+ "on-success",
267
+ "on-error",
268
+ "on-submit",
269
+ "on-load",
270
+ "on-done",
271
+ "done-button-text",
272
+ "api-base-url",
273
+ "embeddable-key",
274
+ "initial-step",
275
+ ];
276
+ }
277
+
278
+ attributeChangedCallback(name, oldValue, newValue) {
279
+ if (oldValue === newValue) return;
280
+
281
+ switch (name) {
282
+ case "done-button-text":
283
+ this.doneButtonText = newValue;
284
+ break;
285
+
286
+ case "api-base-url": {
287
+ const fallbackURL = "https://bison-jib-development.azurewebsites.net";
288
+ this.apiBaseURL = newValue || fallbackURL;
289
+ const BisonJibPayAPIClass =
290
+ typeof BisonJibPayAPI !== "undefined"
291
+ ? BisonJibPayAPI
292
+ : typeof window !== "undefined" && window.BisonJibPayAPI;
293
+ if (BisonJibPayAPIClass) {
294
+ this.api = new BisonJibPayAPIClass(
295
+ this.apiBaseURL,
296
+ this.embeddableKey
297
+ );
298
+ }
299
+ break;
300
+ }
301
+
302
+ case "embeddable-key": {
303
+ const fallbackKey =
304
+ "R80WMkbNN8457RofiMYx03DL65P06IaVT30Q2emYJUBQwYCzRC";
305
+ this.embeddableKey = newValue || fallbackKey;
306
+ const BisonJibPayAPIClass =
307
+ typeof BisonJibPayAPI !== "undefined"
308
+ ? BisonJibPayAPI
309
+ : typeof window !== "undefined" && window.BisonJibPayAPI;
310
+ if (BisonJibPayAPIClass) {
311
+ this.api = new BisonJibPayAPIClass(
312
+ this.apiBaseURL,
313
+ this.embeddableKey
314
+ );
315
+ }
316
+ break;
317
+ }
318
+
319
+ case "initial-step": {
320
+ const step = parseInt(newValue, 10);
321
+ if (!isNaN(step) && step >= 0 && step < this.STEPS.length) {
322
+ this.setState({ currentStep: step });
323
+ }
324
+ break;
325
+ }
326
+ }
327
+ }
328
+
329
+ // ==================== VALIDATORS ====================
330
+
331
+ validators = {
332
+ required: (value, fieldName) => ({
333
+ isValid: value && value.trim().length > 0,
334
+ error: `${fieldName} is required`,
335
+ }),
336
+
337
+ email: (value) => ({
338
+ isValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
339
+ error: "Please enter a valid email address",
340
+ }),
341
+
342
+ usPhone: (value) => {
343
+ const cleaned = value.replace(/\D/g, "");
344
+ return {
345
+ isValid: cleaned.length === 10,
346
+ error: "Please enter a valid 10-digit U.S. phone number",
347
+ };
348
+ },
349
+
350
+
351
+
352
+ ein: (value) => {
353
+ const cleaned = value.replace(/\D/g, "");
354
+ return {
355
+ isValid: cleaned.length === 9,
356
+ error: "EIN must be 9 digits",
357
+ };
358
+ },
359
+
360
+ url: (value) => {
361
+ if (!value) return { isValid: true, error: "" };
362
+
363
+ const trimmed = value.trim();
364
+ if (!trimmed) return { isValid: true, error: "" };
365
+
366
+ const domainPattern = /^(?:[a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$/;
367
+
368
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
369
+ try {
370
+ new URL(trimmed);
371
+ return { isValid: true, error: "", normalizedValue: trimmed };
372
+ } catch {
373
+ return { isValid: false, error: "Please enter a valid URL" };
374
+ }
375
+ }
376
+
377
+ if (domainPattern.test(trimmed)) {
378
+ const normalized = `https://${trimmed}`;
379
+ try {
380
+ new URL(normalized);
381
+ return { isValid: true, error: "", normalizedValue: normalized };
382
+ } catch {
383
+ return { isValid: false, error: "Please enter a valid URL" };
384
+ }
385
+ }
386
+
387
+ return {
388
+ isValid: false,
389
+ error:
390
+ "Please enter a valid URL (e.g., example.com, www.example.com, or https://example.com)",
391
+ };
392
+ },
393
+
394
+ postalCode: (value) => {
395
+ const cleaned = value.replace(/\D/g, "");
396
+ return {
397
+ isValid: cleaned.length === 5,
398
+ error: "Please enter a valid 5-digit ZIP code",
399
+ };
400
+ },
401
+ };
402
+
403
+ // ==================== STATE MANAGEMENT ====================
404
+
405
+ setState(newState) {
406
+ this.state = {
407
+ ...this.state,
408
+ ...newState,
409
+ formData: {
410
+ ...this.state.formData,
411
+ ...(newState.formData || {}),
412
+ },
413
+ validationState: {
414
+ ...this.state.validationState,
415
+ ...(newState.validationState || {}),
416
+ },
417
+ uiState: {
418
+ ...this.state.uiState,
419
+ ...(newState.uiState || {}),
420
+ },
421
+ };
422
+
423
+ this.render();
424
+ }
425
+
426
+ // ==================== VALIDATION ====================
427
+
428
+ validateField(value, validators, fieldName) {
429
+ for (const validatorName of validators) {
430
+ const validator = this.validators[validatorName];
431
+ if (validator) {
432
+ const result = validator(value, fieldName);
433
+ if (!result.isValid) {
434
+ return result.error;
435
+ }
436
+ }
437
+ }
438
+ return "";
439
+ }
440
+
441
+ validateCurrentStep() {
442
+ const stepId = this.STEPS[this.state.currentStep].id;
443
+ let isValid = true;
444
+ const errors = {};
445
+
446
+ if (stepId === "business-details") {
447
+ const data = this.state.formData.businessDetails;
448
+ const fields = [
449
+ {
450
+ name: "businessName",
451
+ validators: ["required"],
452
+ label: "Business Name",
453
+ },
454
+ {
455
+ name: "doingBusinessAs",
456
+ validators: ["required"],
457
+ label: "Doing Business As (DBA)",
458
+ },
459
+ { name: "ein", validators: ["required", "ein"], label: "EIN" },
460
+ {
461
+ name: "businessWebsite",
462
+ validators: ["url"],
463
+ label: "Business Website",
464
+ },
465
+ {
466
+ name: "businessPhoneNumber",
467
+ validators: ["required", "usPhone"],
468
+ label: "Business Phone",
469
+ },
470
+ {
471
+ name: "businessEmail",
472
+ validators: ["required", "email"],
473
+ label: "Business Email",
474
+ },
475
+ {
476
+ name: "BusinessAddress1",
477
+ validators: ["required"],
478
+ label: "Street Address",
479
+ },
480
+ { name: "businessCity", validators: ["required"], label: "City" },
481
+ { name: "businessState", validators: ["required"], label: "State" },
482
+ {
483
+ name: "businessPostalCode",
484
+ validators: ["required", "postalCode"],
485
+ label: "ZIP Code",
486
+ },
487
+ ];
488
+
489
+ fields.forEach((field) => {
490
+ const error = this.validateField(
491
+ data[field.name],
492
+ field.validators,
493
+ field.label
494
+ );
495
+ if (error) {
496
+ errors[field.name] = error;
497
+ isValid = false;
498
+ }
499
+ });
500
+ } else if (stepId === "business-verification") {
501
+ const data = this.state.formData.businessVerification;
502
+ if (
503
+ !data.verificationDocuments ||
504
+ data.verificationDocuments.length === 0
505
+ ) {
506
+ errors.verificationDocuments = "At least one document is required";
507
+ isValid = false;
508
+ }
509
+ } else if (stepId === "representative-details") {
510
+ // Validate each representative if any field is filled
511
+ this.state.formData.representatives.forEach((rep, index) => {
512
+ const hasAnyValue = Object.values(rep).some(
513
+ (v) =>
514
+ (typeof v === "string" && v.trim()) ||
515
+ (typeof v === "object" &&
516
+ Object.values(v).some((av) => av && av.trim()))
517
+ );
518
+
519
+ if (hasAnyValue) {
520
+ const requiredFields = [
521
+ {
522
+ name: "representativeFirstName",
523
+ validators: ["required"],
524
+ label: "First Name",
525
+ },
526
+ {
527
+ name: "representativeLastName",
528
+ validators: ["required"],
529
+ label: "Last Name",
530
+ },
531
+ {
532
+ name: "representativeJobTitle",
533
+ validators: ["required"],
534
+ label: "Job Title",
535
+ },
536
+ {
537
+ name: "representativePhone",
538
+ validators: ["required", "usPhone"],
539
+ label: "Phone",
540
+ },
541
+ {
542
+ name: "representativeEmail",
543
+ validators: ["required", "email"],
544
+ label: "Email",
545
+ },
546
+ {
547
+ name: "representativeDateOfBirth",
548
+ validators: ["required"],
549
+ label: "Date of Birth",
550
+ },
551
+ {
552
+ name: "representativeAddress",
553
+ validators: ["required"],
554
+ label: "Address",
555
+ },
556
+ {
557
+ name: "representativeCity",
558
+ validators: ["required"],
559
+ label: "City",
560
+ },
561
+ {
562
+ name: "representativeState",
563
+ validators: ["required"],
564
+ label: "State",
565
+ },
566
+ {
567
+ name: "representativeZip",
568
+ validators: ["required", "postalCode"],
569
+ label: "ZIP Code",
570
+ },
571
+ ];
572
+
573
+ requiredFields.forEach((field) => {
574
+ const error = this.validateField(
575
+ rep[field.name],
576
+ field.validators,
577
+ field.label
578
+ );
579
+ if (error) {
580
+ if (!errors[`rep${index}`]) errors[`rep${index}`] = {};
581
+ errors[`rep${index}`][field.name] = error;
582
+ isValid = false;
583
+ }
584
+ });
585
+ }
586
+ });
587
+ }
588
+
589
+ this.setState({
590
+ validationState: {
591
+ [`step${this.state.currentStep}`]: { isValid, errors },
592
+ },
593
+ uiState: { showErrors: !isValid },
594
+ });
595
+
596
+ return isValid;
597
+ }
598
+
599
+ // ==================== NAVIGATION ====================
600
+
601
+ async goToNextStep() {
602
+ const isValid = this.validateCurrentStep();
603
+
604
+ if (!isValid) {
605
+ console.warn("❌ Validation failed - cannot proceed to next step");
606
+ return;
607
+ }
608
+
609
+ const completedSteps = new Set(this.state.completedSteps);
610
+ completedSteps.add(this.state.currentStep);
611
+
612
+ if (this.state.currentStep < this.state.totalSteps - 1) {
613
+ this.setState({
614
+ currentStep: this.state.currentStep + 1,
615
+ completedSteps,
616
+ uiState: { showErrors: false },
617
+ });
618
+ } else {
619
+ this.handleFormCompletion();
620
+ }
621
+ }
622
+
623
+ goToPreviousStep() {
624
+ if (this.state.currentStep > 0) {
625
+ this.setState({
626
+ currentStep: this.state.currentStep - 1,
627
+ uiState: { showErrors: false },
628
+ });
629
+ }
630
+ }
631
+
632
+ goToStep(stepIndex) {
633
+ if (
634
+ this.state.completedSteps.has(stepIndex) ||
635
+ stepIndex < this.state.currentStep
636
+ ) {
637
+ this.setState({
638
+ currentStep: stepIndex,
639
+ uiState: { showErrors: false },
640
+ });
641
+ }
642
+ }
643
+
644
+ skipStep() {
645
+ if (this.STEPS[this.state.currentStep].canSkip) {
646
+ const completedSteps = new Set(this.state.completedSteps);
647
+ completedSteps.add(this.state.currentStep);
648
+
649
+ this.setState({
650
+ currentStep: this.state.currentStep + 1,
651
+ completedSteps,
652
+ uiState: { showErrors: false },
653
+ });
654
+ }
655
+ }
656
+
657
+ // ==================== REPRESENTATIVES CRUD ====================
658
+
659
+ addRepresentative() {
660
+ const newRep = {
661
+ id: crypto.randomUUID(),
662
+ representativeFirstName: "",
663
+ representativeLastName: "",
664
+ representativeJobTitle: "",
665
+ representativePhone: "",
666
+ representativeEmail: "",
667
+ representativeDateOfBirth: "",
668
+ representativeAddress: "",
669
+ representativeCity: "",
670
+ representativeState: "",
671
+ representativeZip: "",
672
+ isStaged: false,
673
+ stagedData: null,
674
+ isExpanded: true,
675
+ isValidating: false,
676
+ };
677
+
678
+ this.setState({
679
+ formData: {
680
+ representatives: [...this.state.formData.representatives, newRep],
681
+ },
682
+ });
683
+ }
684
+
685
+ removeRepresentative(index) {
686
+ const representatives = this.state.formData.representatives.filter(
687
+ (_, i) => i !== index
688
+ );
689
+ this.setState({
690
+ formData: { representatives },
691
+ });
692
+ }
693
+
694
+ updateRepresentative(index, field, value) {
695
+ const representatives = [...this.state.formData.representatives];
696
+ const currentRep = representatives[index];
697
+
698
+ representatives[index] = {
699
+ ...currentRep,
700
+ [field]: value,
701
+ // Don't reset isStaged - we track modifications via stagedData comparison
702
+ };
703
+
704
+ this.setState({
705
+ formData: { representatives },
706
+ });
707
+ }
708
+
709
+ /**
710
+ * Validate a single representative's fields
711
+ * @param {number} index - The index of the representative to validate
712
+ * @returns {{isValid: boolean, errors: Object}} Validation result
713
+ */
714
+ validateSingleRepresentative(index) {
715
+ const rep = this.state.formData.representatives[index];
716
+ if (!rep) return { isValid: false, errors: {} };
717
+
718
+ const errors = {};
719
+ let isValid = true;
720
+
721
+ const requiredFields = [
722
+ {
723
+ name: "representativeFirstName",
724
+ validators: ["required"],
725
+ label: "First Name",
726
+ },
727
+ {
728
+ name: "representativeLastName",
729
+ validators: ["required"],
730
+ label: "Last Name",
731
+ },
732
+ {
733
+ name: "representativeJobTitle",
734
+ validators: ["required"],
735
+ label: "Job Title",
736
+ },
737
+ {
738
+ name: "representativePhone",
739
+ validators: ["required", "usPhone"],
740
+ label: "Phone",
741
+ },
742
+ {
743
+ name: "representativeEmail",
744
+ validators: ["required", "email"],
745
+ label: "Email",
746
+ },
747
+ {
748
+ name: "representativeDateOfBirth",
749
+ validators: ["required"],
750
+ label: "Date of Birth",
751
+ },
752
+ {
753
+ name: "representativeAddress",
754
+ validators: ["required"],
755
+ label: "Address",
756
+ },
757
+ {
758
+ name: "representativeCity",
759
+ validators: ["required"],
760
+ label: "City",
761
+ },
762
+ {
763
+ name: "representativeState",
764
+ validators: ["required"],
765
+ label: "State",
766
+ },
767
+ {
768
+ name: "representativeZip",
769
+ validators: ["required", "postalCode"],
770
+ label: "ZIP Code",
771
+ },
772
+ ];
773
+
774
+ requiredFields.forEach((field) => {
775
+ const error = this.validateField(
776
+ rep[field.name],
777
+ field.validators,
778
+ field.label
779
+ );
780
+ if (error) {
781
+ errors[field.name] = error;
782
+ isValid = false;
783
+ }
784
+ });
785
+
786
+ return { isValid, errors };
787
+ }
788
+
789
+ /**
790
+ * Stage a representative after validation and email check
791
+ * @param {number} index - The index of the representative to stage
792
+ */
793
+ async stageRepresentative(index) {
794
+ const rep = this.state.formData.representatives[index];
795
+ if (!rep) return;
796
+
797
+ // Step 1: Validate all fields for this representative
798
+ const { isValid, errors } = this.validateSingleRepresentative(index);
799
+
800
+ if (!isValid) {
801
+ // Show validation errors for this representative
802
+ const currentErrors = this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
803
+ this.setState({
804
+ validationState: {
805
+ [`step${this.state.currentStep}`]: {
806
+ isValid: false,
807
+ errors: {
808
+ ...currentErrors,
809
+ [`rep${index}`]: errors,
810
+ },
811
+ },
812
+ },
813
+ uiState: { showErrors: true },
814
+ });
815
+ return;
816
+ }
817
+
818
+ // Step 2: Call validateUserEmail API
819
+ if (!this.api) {
820
+ console.error("API not initialized");
821
+ return;
822
+ }
823
+
824
+ // Set validating state to disable fields
825
+ const repsBeforeValidation = [...this.state.formData.representatives];
826
+ repsBeforeValidation[index] = {
827
+ ...repsBeforeValidation[index],
828
+ isValidating: true,
829
+ };
830
+ this.setState({ formData: { representatives: repsBeforeValidation } });
831
+
832
+ try {
833
+ const response = await this.api.validateUserEmail(rep.representativeEmail);
834
+
835
+ // Step 3: Check if email exists
836
+ if (response.data?.exists === true) {
837
+ // Add error to the representative email field
838
+ const currentErrors = this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
839
+ const repsAfterError = [...this.state.formData.representatives];
840
+ repsAfterError[index] = { ...repsAfterError[index], isValidating: false };
841
+ this.setState({
842
+ formData: { representatives: repsAfterError },
843
+ validationState: {
844
+ [`step${this.state.currentStep}`]: {
845
+ isValid: false,
846
+ errors: {
847
+ ...currentErrors,
848
+ [`rep${index}`]: {
849
+ representativeEmail: "A representative with this email already exists",
850
+ },
851
+ },
852
+ },
853
+ },
854
+ uiState: { showErrors: true },
855
+ });
856
+ return;
857
+ }
858
+
859
+ // Email doesn't exist - mark representative as staged and store snapshot
860
+ const representatives = [...this.state.formData.representatives];
861
+ const currentRep = representatives[index];
862
+
863
+ // Store current data as stagedData for undo functionality
864
+ const stagedData = {
865
+ representativeFirstName: currentRep.representativeFirstName,
866
+ representativeLastName: currentRep.representativeLastName,
867
+ representativeJobTitle: currentRep.representativeJobTitle,
868
+ representativePhone: currentRep.representativePhone,
869
+ representativeEmail: currentRep.representativeEmail,
870
+ representativeDateOfBirth: currentRep.representativeDateOfBirth,
871
+ representativeAddress: currentRep.representativeAddress,
872
+ representativeCity: currentRep.representativeCity,
873
+ representativeState: currentRep.representativeState,
874
+ representativeZip: currentRep.representativeZip,
875
+ };
876
+
877
+ representatives[index] = {
878
+ ...currentRep,
879
+ isStaged: true,
880
+ stagedData: stagedData,
881
+ isExpanded: false, // Collapse the card after staging
882
+ isValidating: false,
883
+ };
884
+
885
+ // Clear any previous errors for this representative
886
+ const currentErrors = this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
887
+ const updatedErrors = { ...currentErrors };
888
+ delete updatedErrors[`rep${index}`];
889
+
890
+ this.setState({
891
+ formData: { representatives },
892
+ validationState: {
893
+ [`step${this.state.currentStep}`]: {
894
+ isValid: Object.keys(updatedErrors).length === 0,
895
+ errors: updatedErrors,
896
+ },
897
+ },
898
+ uiState: { showErrors: Object.keys(updatedErrors).length > 0 },
899
+ });
900
+
901
+ console.log(`✅ Representative ${index + 1} confirmed successfully`);
902
+ } catch (error) {
903
+ console.error("Error validating user email:", error);
904
+ // Show API error and reset validating state
905
+ const currentErrors = this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
906
+ const repsAfterCatch = [...this.state.formData.representatives];
907
+ repsAfterCatch[index] = { ...repsAfterCatch[index], isValidating: false };
908
+ this.setState({
909
+ formData: { representatives: repsAfterCatch },
910
+ validationState: {
911
+ [`step${this.state.currentStep}`]: {
912
+ isValid: false,
913
+ errors: {
914
+ ...currentErrors,
915
+ [`rep${index}`]: {
916
+ representativeEmail: error.data?.message || "Failed to validate email. Please try again.",
917
+ },
918
+ },
919
+ },
920
+ },
921
+ uiState: { showErrors: true },
922
+ });
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Check if there's an active (non-staged OR modified staged) representative form
928
+ * An "active" form means the user has added a representative but hasn't confirmed/staged it yet,
929
+ * OR a staged representative has been modified and needs re-confirmation
930
+ * @returns {boolean} True if there's at least one non-staged or modified staged representative
931
+ */
932
+ hasActiveRepresentativeForm() {
933
+ return this.state.formData.representatives.some((rep, index) => {
934
+ if (rep.isStaged !== true) return true;
935
+ // Staged rep is "active" if it has been modified
936
+ return this.isRepresentativeModified(index);
937
+ });
938
+ }
939
+
940
+ /**
941
+ * Check if there's at least one staged (confirmed) representative that is NOT modified
942
+ * @returns {boolean} True if at least one representative is staged and unmodified
943
+ */
944
+ hasStagedRepresentative() {
945
+ return this.state.formData.representatives.some((rep, index) => {
946
+ return rep.isStaged === true && !this.isRepresentativeModified(index);
947
+ });
948
+ }
949
+
950
+ /**
951
+ * Check if a staged representative has been modified from its staged state
952
+ * @param {number} index - The index of the representative
953
+ * @returns {boolean} True if the representative has been modified
954
+ */
955
+ isRepresentativeModified(index) {
956
+ const rep = this.state.formData.representatives[index];
957
+ if (!rep || !rep.isStaged || !rep.stagedData) return false;
958
+
959
+ const fieldsToCompare = [
960
+ 'representativeFirstName',
961
+ 'representativeLastName',
962
+ 'representativeJobTitle',
963
+ 'representativePhone',
964
+ 'representativeEmail',
965
+ 'representativeDateOfBirth',
966
+ 'representativeAddress',
967
+ 'representativeCity',
968
+ 'representativeState',
969
+ 'representativeZip',
970
+ ];
971
+
972
+ return fieldsToCompare.some((field) => rep[field] !== rep.stagedData[field]);
973
+ }
974
+
975
+ /**
976
+ * Undo changes to a staged representative, restoring it to its original staged state
977
+ * @param {number} index - The index of the representative
978
+ */
979
+ undoRepresentativeChanges(index) {
980
+ const rep = this.state.formData.representatives[index];
981
+ if (!rep || !rep.isStaged || !rep.stagedData) return;
982
+
983
+ const representatives = [...this.state.formData.representatives];
984
+ representatives[index] = {
985
+ ...rep,
986
+ representativeFirstName: rep.stagedData.representativeFirstName,
987
+ representativeLastName: rep.stagedData.representativeLastName,
988
+ representativeJobTitle: rep.stagedData.representativeJobTitle,
989
+ representativePhone: rep.stagedData.representativePhone,
990
+ representativeEmail: rep.stagedData.representativeEmail,
991
+ representativeDateOfBirth: rep.stagedData.representativeDateOfBirth,
992
+ representativeAddress: rep.stagedData.representativeAddress,
993
+ representativeCity: rep.stagedData.representativeCity,
994
+ representativeState: rep.stagedData.representativeState,
995
+ representativeZip: rep.stagedData.representativeZip,
996
+ isExpanded: false,
997
+ };
998
+
999
+ // Clear any validation errors for this representative
1000
+ const currentErrors = this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
1001
+ const updatedErrors = { ...currentErrors };
1002
+ delete updatedErrors[`rep${index}`];
1003
+
1004
+ this.setState({
1005
+ formData: { representatives },
1006
+ validationState: {
1007
+ [`step${this.state.currentStep}`]: {
1008
+ isValid: Object.keys(updatedErrors).length === 0,
1009
+ errors: updatedErrors,
1010
+ },
1011
+ },
1012
+ uiState: { showErrors: Object.keys(updatedErrors).length > 0 },
1013
+ });
1014
+
1015
+ console.log(`✅ Representative ${index + 1} changes undone`);
1016
+ }
1017
+
1018
+ /**
1019
+ * Toggle the expanded/collapsed state of a representative card
1020
+ * @param {number} index - The index of the representative
1021
+ */
1022
+ toggleRepresentativeExpand(index) {
1023
+ const representatives = [...this.state.formData.representatives];
1024
+ const rep = representatives[index];
1025
+ if (!rep) return;
1026
+
1027
+ representatives[index] = {
1028
+ ...rep,
1029
+ isExpanded: !rep.isExpanded,
1030
+ };
1031
+
1032
+ this.setState({
1033
+ formData: { representatives },
1034
+ });
1035
+ }
1036
+
1037
+ /**
1038
+ * Discard an active (non-staged) representative form
1039
+ * @param {number} index - The index of the representative to discard
1040
+ */
1041
+ discardRepresentative(index) {
1042
+ const rep = this.state.formData.representatives[index];
1043
+ if (!rep || rep.isStaged) return; // Can only discard non-staged representatives
1044
+
1045
+ const representatives = this.state.formData.representatives.filter(
1046
+ (_, i) => i !== index
1047
+ );
1048
+
1049
+ // Clear any previous errors for this representative
1050
+ const currentErrors = this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
1051
+ const updatedErrors = { ...currentErrors };
1052
+ delete updatedErrors[`rep${index}`];
1053
+
1054
+ this.setState({
1055
+ formData: { representatives },
1056
+ validationState: {
1057
+ [`step${this.state.currentStep}`]: {
1058
+ isValid: Object.keys(updatedErrors).length === 0,
1059
+ errors: updatedErrors,
1060
+ },
1061
+ },
1062
+ uiState: { showErrors: Object.keys(updatedErrors).length > 0 },
1063
+ });
1064
+
1065
+ console.log(`✅ Representative ${index + 1} discarded`);
1066
+ }
1067
+
1068
+ // ==================== INITIAL DATA LOADING ====================
1069
+
1070
+ loadInitialData(data) {
1071
+ const newFormData = { ...this.state.formData };
1072
+
1073
+ if (data.businessDetails) {
1074
+ newFormData.businessDetails = {
1075
+ ...newFormData.businessDetails,
1076
+ ...data.businessDetails,
1077
+ };
1078
+ }
1079
+
1080
+ if (data.businessVerification) {
1081
+ newFormData.businessVerification = {
1082
+ ...newFormData.businessVerification,
1083
+ ...data.businessVerification,
1084
+ };
1085
+ }
1086
+
1087
+
1088
+
1089
+ if (data.representatives && Array.isArray(data.representatives)) {
1090
+ newFormData.representatives = data.representatives.map((rep) => ({
1091
+ id: rep.id || crypto.randomUUID(),
1092
+ representativeFirstName: rep.representativeFirstName || "",
1093
+ representativeLastName: rep.representativeLastName || "",
1094
+ representativeJobTitle: rep.representativeJobTitle || "",
1095
+ representativePhone: rep.representativePhone || "",
1096
+ representativeEmail: rep.representativeEmail || "",
1097
+ representativeDateOfBirth: rep.representativeDateOfBirth || "",
1098
+ representativeAddress: rep.representativeAddress || "",
1099
+ representativeCity: rep.representativeCity || "",
1100
+ representativeState: rep.representativeState || "",
1101
+ representativeZip: rep.representativeZip || "",
1102
+ isStaged: rep.isStaged || false,
1103
+ }));
1104
+ }
1105
+
1106
+ const newState = {
1107
+ formData: newFormData,
1108
+ };
1109
+
1110
+ if (
1111
+ typeof data.initialStep === "number" &&
1112
+ data.initialStep >= 0 &&
1113
+ data.initialStep < this.state.totalSteps
1114
+ ) {
1115
+ newState.currentStep = data.initialStep;
1116
+ }
1117
+
1118
+ this.setState(newState);
1119
+ }
1120
+
1121
+ resetForm() {
1122
+ const defaultFormData = {
1123
+ businessDetails: {
1124
+ businessName: "",
1125
+ doingBusinessAs: "",
1126
+ ein: "",
1127
+ businessWebsite: "",
1128
+ businessPhoneNumber: "",
1129
+ businessEmail: "",
1130
+ BusinessAddress1: "",
1131
+ businessAddress2: "",
1132
+ businessCity: "",
1133
+ businessState: "",
1134
+ businessPostalCode: "",
1135
+ },
1136
+ representatives: [],
1137
+ businessVerification: {
1138
+ verificationDocuments: [],
1139
+ },
1140
+
1141
+ };
1142
+
1143
+ const defaultValidationState = {
1144
+ step0: { isValid: false, errors: {} },
1145
+ step1: { isValid: false, errors: {} },
1146
+ step2: { isValid: false, errors: {} },
1147
+ };
1148
+
1149
+ this.state = {
1150
+ ...this.state,
1151
+ currentStep: 0,
1152
+ isSubmitted: false,
1153
+ isFailed: false,
1154
+ isSubmissionFailed: false,
1155
+ formData: defaultFormData,
1156
+ validationState: defaultValidationState,
1157
+ completedSteps: new Set(),
1158
+ uiState: {
1159
+ isLoading: false,
1160
+ showErrors: false,
1161
+ errorMessage: null,
1162
+ },
1163
+ };
1164
+
1165
+ if (this._initialData) {
1166
+ this.loadInitialData(this._initialData);
1167
+ }
1168
+ }
1169
+
1170
+ // ==================== UTILITIES ====================
1171
+
1172
+ formatPhoneNumber(value) {
1173
+ const cleaned = value.replace(/\D/g, "");
1174
+ const limited = cleaned.slice(0, 10);
1175
+
1176
+ if (limited.length === 0) {
1177
+ return "";
1178
+ } else if (limited.length <= 3) {
1179
+ return limited;
1180
+ } else if (limited.length <= 6) {
1181
+ return `(${limited.slice(0, 3)}) ${limited.slice(3)}`;
1182
+ } else {
1183
+ return `(${limited.slice(0, 3)}) ${limited.slice(3, 6)}-${limited.slice(6)}`;
1184
+ }
1185
+ }
1186
+
1187
+ formatEIN(value) {
1188
+ const cleaned = value.replace(/\D/g, "");
1189
+ const limited = cleaned.slice(0, 9);
1190
+
1191
+ if (limited.length <= 2) {
1192
+ return limited;
1193
+ } else {
1194
+ return `${limited.slice(0, 2)}-${limited.slice(2)}`;
1195
+ }
1196
+ }
1197
+
1198
+ getFieldError(fieldName, repIndex = null) {
1199
+ if (!this.state.uiState.showErrors) return "";
1200
+
1201
+ const errors =
1202
+ this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
1203
+
1204
+ if (repIndex !== null) {
1205
+ return errors[`rep${repIndex}`]?.[fieldName] || "";
1206
+ }
1207
+
1208
+ return errors[fieldName] || "";
1209
+ }
1210
+
1211
+ // ==================== FORM COMPLETION ====================
1212
+
1213
+ async handleFormCompletion(shouldFail = false) {
1214
+ console.log("WioOnboarding: handleFormCompletion STARTED");
1215
+
1216
+ const completedSteps = new Set(this.state.completedSteps);
1217
+ completedSteps.add(this.state.currentStep);
1218
+
1219
+ const formData = {
1220
+ businessDetails: this.state.formData.businessDetails,
1221
+ representatives: this.state.formData.representatives,
1222
+ businessVerification: this.state.formData.businessVerification,
1223
+ };
1224
+
1225
+ let processedData = formData;
1226
+ if (this.onSubmit && typeof this.onSubmit === "function") {
1227
+ try {
1228
+ const result = await this.onSubmit(formData);
1229
+
1230
+ if (result === false) {
1231
+ console.log("Form submission cancelled by onSubmit callback");
1232
+ return;
1233
+ }
1234
+
1235
+ if (result && typeof result === "object") {
1236
+ processedData = result;
1237
+ }
1238
+ } catch (error) {
1239
+ console.error("Error in onSubmit callback:", error);
1240
+ this.handleSubmissionFailure(formData);
1241
+ return;
1242
+ }
1243
+ }
1244
+
1245
+ this.setState({
1246
+ completedSteps,
1247
+ uiState: { isLoading: true },
1248
+ });
1249
+
1250
+ // Ensure API is available (with fallback initialization)
1251
+ if (!this.api) {
1252
+ console.warn(
1253
+ "WioOnboarding: API was null, attempting to reinitialize..."
1254
+ );
1255
+ const BisonJibPayAPIClass =
1256
+ typeof BisonJibPayAPI !== "undefined"
1257
+ ? BisonJibPayAPI
1258
+ : typeof window !== "undefined" && window.BisonJibPayAPI;
1259
+
1260
+ if (BisonJibPayAPIClass) {
1261
+ console.log("✅ Found BisonJibPayAPI, reinitializing...");
1262
+ this.api = new BisonJibPayAPIClass(this.apiBaseURL, this.embeddableKey);
1263
+ } else {
1264
+ console.error("❌ WioOnboarding: BisonJibPayAPI class not found");
1265
+ console.error(
1266
+ "Available globals:",
1267
+ typeof window !== "undefined"
1268
+ ? Object.keys(window).filter((k) =>
1269
+ k.toLowerCase().includes("bison")
1270
+ )
1271
+ : "N/A"
1272
+ );
1273
+ this.handleSubmissionFailure(processedData);
1274
+ return;
1275
+ }
1276
+ }
1277
+
1278
+ if (!this.api) {
1279
+ console.error("❌ WioOnboarding: API initialization failed completely");
1280
+ this.handleSubmissionFailure(processedData);
1281
+ return;
1282
+ }
1283
+
1284
+ try {
1285
+ const businessDetails = processedData.businessDetails;
1286
+ const representativeDetails = processedData.representativeDetails;
1287
+ const businessVerification = processedData.businessVerification;
1288
+
1289
+ // Debug: Log the extracted data objects
1290
+ console.log("=== DATA EXTRACTION DEBUG ===");
1291
+ console.log("businessDetails:", JSON.stringify(businessDetails, null, 2));
1292
+ console.log(
1293
+ "representativeDetails:",
1294
+ JSON.stringify(representativeDetails, null, 2)
1295
+ );
1296
+ console.log(
1297
+ "businessVerification files count:",
1298
+ businessVerification?.verificationDocuments?.length || 0
1299
+ );
1300
+ console.log("=== END DATA EXTRACTION DEBUG ===");
1301
+
1302
+ // Build FormData for API submission
1303
+ const payload = new FormData();
1304
+
1305
+ // Add business details
1306
+ payload.append("businessName", businessDetails.businessName || "");
1307
+ payload.append("doingBusinessAs", businessDetails.doingBusinessAs || "");
1308
+ payload.append("ein", businessDetails.ein || "");
1309
+ payload.append("businessWebsite", businessDetails.businessWebsite || "");
1310
+ payload.append(
1311
+ "businessPhoneNumber",
1312
+ businessDetails.businessPhoneNumber || ""
1313
+ );
1314
+ payload.append("businessEmail", businessDetails.businessEmail || "");
1315
+ payload.append(
1316
+ "businessAddress1",
1317
+ businessDetails.BusinessAddress1 || ""
1318
+ );
1319
+ payload.append(
1320
+ "businessAddress2",
1321
+ businessDetails.businessAddress2 || ""
1322
+ );
1323
+ payload.append("businessCity", businessDetails.businessCity || "");
1324
+ payload.append("businessState", businessDetails.businessState || "");
1325
+ payload.append(
1326
+ "businessPostalCode",
1327
+ businessDetails.businessPostalCode || ""
1328
+ );
1329
+
1330
+ // Add representatives as JSON string
1331
+ if (
1332
+ processedData.representatives &&
1333
+ processedData.representatives.length > 0
1334
+ ) {
1335
+ payload.append(
1336
+ "representatives",
1337
+ JSON.stringify(processedData.representatives)
1338
+ );
1339
+ }
1340
+
1341
+ // Add business verification documents (files)
1342
+ const verificationDocs = businessVerification.verificationDocuments || [];
1343
+ verificationDocs.forEach((file) => {
1344
+ if (file instanceof File) {
1345
+ payload.append("businessVerificationDocuments", file);
1346
+ }
1347
+ });
1348
+
1349
+ // Enhanced debugging - Log payload
1350
+ console.log("=== WIO REGISTRATION DEBUG START ===");
1351
+ console.log("FormData entries:");
1352
+ for (const [key, value] of payload.entries()) {
1353
+ console.log(
1354
+ ` ${key}:`,
1355
+ value instanceof File ? `File(${value.name})` : value
1356
+ );
1357
+ }
1358
+
1359
+ console.log("\nCalling API: registerWIO");
1360
+ const response = await this.api.registerWIO(payload);
1361
+
1362
+ console.log("\n=== API RESPONSE DEBUG ===");
1363
+ console.log("Full response object:", JSON.stringify(response, null, 2));
1364
+ console.log("Response type:", typeof response);
1365
+ console.log("Response.success:", response?.success);
1366
+ console.log("Response.data:", response?.data);
1367
+ console.log("Response.message:", response?.message);
1368
+ console.log("Response.errors:", response?.errors);
1369
+ console.log("=== WIO REGISTRATION DEBUG END ===\n");
1370
+
1371
+ // Check if response indicates success
1372
+ const isSuccess =
1373
+ response && (response.success === true || response.success === "true");
1374
+
1375
+ if (shouldFail || !isSuccess) {
1376
+ console.error("❌ WioOnboarding: Submission failed");
1377
+ console.error(
1378
+ "Reason: shouldFail =",
1379
+ shouldFail,
1380
+ "| isSuccess =",
1381
+ isSuccess
1382
+ );
1383
+ console.error("Full response:", response);
1384
+
1385
+ // Determine status code from response
1386
+ let statusCode = 500; // Default to server error
1387
+ if (response?.errors?.length > 0) {
1388
+ // Check for authentication errors
1389
+ const hasAuthError = response.errors.some(
1390
+ (err) =>
1391
+ err.toLowerCase().includes("unauthorized") ||
1392
+ err.toLowerCase().includes("authentication")
1393
+ );
1394
+ if (hasAuthError) {
1395
+ statusCode = 401;
1396
+ } else {
1397
+ // Assume validation/bad request error
1398
+ statusCode = 400;
1399
+ }
1400
+ }
1401
+
1402
+ this.handleSubmissionFailure(processedData, {
1403
+ statusCode,
1404
+ success: response?.success ?? false,
1405
+ message: response?.message || null,
1406
+ errors: response?.errors || [],
1407
+ timestamp: response?.timestamp || new Date().toISOString(),
1408
+ traceId: response?.traceId || null,
1409
+ });
1410
+ return;
1411
+ }
1412
+
1413
+ console.log("✅ WioOnboarding: Submission successful!");
1414
+
1415
+ // Extract data from 201 success response matching Swagger schema
1416
+ const responseData = response?.data || {};
1417
+ const successData = {
1418
+ // Form data
1419
+ formData: processedData,
1420
+ // API response metadata
1421
+ success: response.success,
1422
+ message: response.message || null,
1423
+ timestamp: response.timestamp,
1424
+ traceId: response.traceId || null,
1425
+ // WIO registration data from response.data
1426
+ userId: responseData.userId || null,
1427
+ wioId: responseData.wioId || null,
1428
+ tenantId: responseData.tenantId || null,
1429
+ moovAccountId: responseData.moovAccountId || null,
1430
+ moovBankAccountId: responseData.moovBankAccountId || null,
1431
+ uploadedDocumentsCount: responseData.uploadedDocumentsCount || 0,
1432
+ uploadErrors: responseData.uploadErrors || [],
1433
+ requiresOnboarding: responseData.requiresOnboarding ?? false,
1434
+ // Legacy fields for backward compatibility
1435
+ email: processedData.businessDetails.businessEmail,
1436
+ apiResponse: response,
1437
+ };
1438
+
1439
+ console.log("Success data:", successData);
1440
+
1441
+ this.setState({
1442
+ isSubmitted: true,
1443
+ submissionEmail: successData.email,
1444
+ submissionMoovAccountId: successData.moovAccountId,
1445
+ submissionData: successData,
1446
+ uiState: { isLoading: false },
1447
+ });
1448
+
1449
+ this.dispatchEvent(
1450
+ new CustomEvent("formComplete", {
1451
+ detail: successData,
1452
+ bubbles: true,
1453
+ composed: true,
1454
+ })
1455
+ );
1456
+
1457
+ if (this.onSuccess && typeof this.onSuccess === "function") {
1458
+ await this.onSuccess(successData);
1459
+ }
1460
+ } catch (error) {
1461
+ console.error("WioOnboarding: registerWIO API error", error);
1462
+
1463
+ // Handle network or unexpected errors
1464
+ this.handleSubmissionFailure(processedData, {
1465
+ statusCode: 500,
1466
+ success: false,
1467
+ message: error.message || "An unexpected error occurred",
1468
+ errors: [error.message || "Network error or server unavailable"],
1469
+ timestamp: new Date().toISOString(),
1470
+ traceId: null,
1471
+ });
1472
+ }
1473
+ }
1474
+
1475
+ async handleSubmissionFailure(formData, apiError = null) {
1476
+ // Build structured error data matching Swagger schema
1477
+ const errorData = {
1478
+ formData,
1479
+ // API error response fields
1480
+ statusCode: apiError?.statusCode || 500,
1481
+ success: apiError?.success ?? false,
1482
+ message: apiError?.message || null,
1483
+ errors: apiError?.errors || [],
1484
+ timestamp: apiError?.timestamp || new Date().toISOString(),
1485
+ traceId: apiError?.traceId || null,
1486
+ };
1487
+
1488
+ // Build user-friendly error message
1489
+ let displayMessage = "Form submission failed. Please try again.";
1490
+
1491
+ if (errorData.statusCode === 400) {
1492
+ displayMessage = "Validation failed. Please check your information and try again.";
1493
+ } else if (errorData.statusCode === 401) {
1494
+ displayMessage = "Authentication failed. Please check your credentials.";
1495
+ } else if (errorData.statusCode === 500) {
1496
+ displayMessage = "Server error. Please try again later.";
1497
+ }
1498
+
1499
+ // Use API message if available, otherwise use status-based message
1500
+ if (errorData.message) {
1501
+ displayMessage = errorData.message;
1502
+ }
1503
+
1504
+ // Append specific errors if available
1505
+ const errorDetails =
1506
+ errorData.errors.length > 0 ? errorData.errors.join("; ") : null;
1507
+
1508
+ console.error("Submission Failed:", errorData);
1509
+
1510
+ this.setState({
1511
+ isSubmissionFailed: true,
1512
+ submissionError: errorData,
1513
+ uiState: {
1514
+ ...this.state.uiState,
1515
+ isLoading: false,
1516
+ errorMessage: displayMessage,
1517
+ errorDetails: errorDetails,
1518
+ statusCode: errorData.statusCode,
1519
+ showErrors: false,
1520
+ },
1521
+ });
1522
+
1523
+ this.dispatchEvent(
1524
+ new CustomEvent("submissionFailed", {
1525
+ detail: errorData,
1526
+ bubbles: true,
1527
+ composed: true,
1528
+ })
1529
+ );
1530
+
1531
+ if (this.onError && typeof this.onError === "function") {
1532
+ await this.onError(errorData);
1533
+ }
1534
+ }
1535
+
1536
+ handleSuccessConfirm() {
1537
+ const confirmData = {
1538
+ formData: this.state.formData,
1539
+ timestamp: new Date().toISOString(),
1540
+ };
1541
+
1542
+ this.dispatchEvent(
1543
+ new CustomEvent("onboardingConfirmed", {
1544
+ detail: confirmData,
1545
+ bubbles: true,
1546
+ composed: true,
1547
+ })
1548
+ );
1549
+
1550
+ if (this.onConfirm && typeof this.onConfirm === "function") {
1551
+ this.onConfirm(confirmData);
1552
+ }
1553
+
1554
+ // Reset form after confirmation
1555
+ if (this.onDone && typeof this.onDone === "function") {
1556
+ this.onDone(confirmData);
1557
+ }
1558
+ this.resetForm();
1559
+ }
1560
+
1561
+ // ==================== FILE HANDLING ====================
1562
+
1563
+ handleFileUpload(files) {
1564
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
1565
+ const MAX_FILES = 10;
1566
+ const ALLOWED_TYPES = [
1567
+ "application/pdf",
1568
+ "image/jpeg",
1569
+ "image/jpg",
1570
+ "image/png",
1571
+ "application/msword",
1572
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1573
+ ];
1574
+
1575
+ const currentFiles =
1576
+ this.state.formData.businessVerification.verificationDocuments || [];
1577
+ const validFiles = [];
1578
+ const errors = [];
1579
+
1580
+ files.forEach((file) => {
1581
+ if (currentFiles.length + validFiles.length >= MAX_FILES) {
1582
+ errors.push(`Maximum ${MAX_FILES} files allowed`);
1583
+ return;
1584
+ }
1585
+
1586
+ if (!ALLOWED_TYPES.includes(file.type)) {
1587
+ errors.push(`${file.name}: Invalid file type`);
1588
+ return;
1589
+ }
1590
+
1591
+ if (file.size > MAX_FILE_SIZE) {
1592
+ errors.push(`${file.name}: File size exceeds 10MB`);
1593
+ return;
1594
+ }
1595
+
1596
+ validFiles.push(file);
1597
+ });
1598
+
1599
+ if (errors.length > 0) {
1600
+ alert(errors.join("\n"));
1601
+ }
1602
+
1603
+ if (validFiles.length > 0) {
1604
+ this.setState({
1605
+ formData: {
1606
+ businessVerification: {
1607
+ verificationDocuments: [...currentFiles, ...validFiles],
1608
+ },
1609
+ },
1610
+ });
1611
+ }
1612
+ }
1613
+
1614
+ removeFile(index) {
1615
+ const files = [
1616
+ ...this.state.formData.businessVerification.verificationDocuments,
1617
+ ];
1618
+ files.splice(index, 1);
1619
+
1620
+ this.setState({
1621
+ formData: {
1622
+ businessVerification: {
1623
+ verificationDocuments: files,
1624
+ },
1625
+ },
1626
+ });
1627
+ }
1628
+
1629
+ // ==================== FIELD HANDLERS ====================
1630
+
1631
+ handleFieldInput(e) {
1632
+ const input = e.target;
1633
+ const name = input.name;
1634
+ let value = input.value;
1635
+
1636
+ if (input.dataset.format === "ein") {
1637
+ const cursorPosition = input.selectionStart;
1638
+ const oldValue = value;
1639
+ value = this.formatEIN(value);
1640
+ input.value = value;
1641
+
1642
+ if (oldValue.length < value.length) {
1643
+ input.setSelectionRange(cursorPosition + 1, cursorPosition + 1);
1644
+ } else {
1645
+ input.setSelectionRange(cursorPosition, cursorPosition);
1646
+ }
1647
+ }
1648
+
1649
+ if (input.dataset.format === "phone") {
1650
+ const cursorPosition = input.selectionStart;
1651
+ const oldValue = value;
1652
+ const oldDigits = oldValue.replace(/\D/g, "");
1653
+
1654
+ value = this.formatPhoneNumber(value);
1655
+ input.value = value;
1656
+
1657
+ const newDigits = value.replace(/\D/g, "");
1658
+
1659
+ // Calculate new cursor position based on digit count before cursor
1660
+ const digitsBeforeCursor = oldValue
1661
+ .substring(0, cursorPosition)
1662
+ .replace(/\D/g, "").length;
1663
+
1664
+ let newCursorPos = 0;
1665
+ let digitCount = 0;
1666
+
1667
+ for (
1668
+ let i = 0;
1669
+ i < value.length && digitCount < digitsBeforeCursor;
1670
+ i++
1671
+ ) {
1672
+ if (/\d/.test(value[i])) {
1673
+ digitCount++;
1674
+ }
1675
+ newCursorPos = i + 1;
1676
+ }
1677
+
1678
+ // If we added digits, move cursor to end of new content
1679
+ if (newDigits.length > oldDigits.length) {
1680
+ newCursorPos = value.length;
1681
+ }
1682
+
1683
+ input.setSelectionRange(newCursorPos, newCursorPos);
1684
+ }
1685
+
1686
+ const stepId = this.STEPS[this.state.currentStep].id;
1687
+
1688
+ if (stepId === "business-details") {
1689
+ this.state.formData.businessDetails[name] = input.value;
1690
+ } else if (stepId === "representative-details") {
1691
+ const repIndex = input.dataset.repIndex;
1692
+ if (repIndex !== undefined) {
1693
+ const idx = parseInt(repIndex);
1694
+ if (this.state.formData.representatives[idx]) {
1695
+ this.state.formData.representatives[idx][name] = input.value;
1696
+ }
1697
+ }
1698
+ }
1699
+ }
1700
+
1701
+ updatePasswordStrengthIndicator(password) {
1702
+ const shadow = this.shadowRoot;
1703
+ const passwordField = shadow.querySelector("#password");
1704
+
1705
+ if (!passwordField) return;
1706
+
1707
+ // Find the password field container
1708
+ const passwordContainer = passwordField.closest(".form-field");
1709
+ if (!passwordContainer) return;
1710
+
1711
+ // Find existing strength indicator or requirements
1712
+ const existingStrength =
1713
+ passwordContainer.querySelector(".password-strength");
1714
+ const existingRequirements = passwordContainer.querySelector(
1715
+ ".password-requirements"
1716
+ );
1717
+ const errorMessage = passwordContainer.querySelector(".error-message");
1718
+
1719
+ // If password is empty, show requirements
1720
+ if (!password) {
1721
+ if (existingStrength) {
1722
+ existingStrength.remove();
1723
+ }
1724
+ if (!existingRequirements) {
1725
+ const requirementsHTML = `<div class="password-requirements">
1726
+ <p style="font-size: 12px; color: var(--gray-medium); margin-top: 8px;">
1727
+ Password must contain:
1728
+ </p>
1729
+ <ul style="font-size: 12px; color: var(--gray-medium); margin: 4px 0 0 20px; padding: 0;">
1730
+ <li>At least 8 characters</li>
1731
+ <li>One uppercase letter (A-Z)</li>
1732
+ <li>One lowercase letter (a-z)</li>
1733
+ <li>One number (0-9)</li>
1734
+ <li>One special character (!@#$%^&*)</li>
1735
+ </ul>
1736
+ </div>`;
1737
+
1738
+ const tempDiv = document.createElement("div");
1739
+ tempDiv.innerHTML = requirementsHTML;
1740
+ const newRequirements = tempDiv.firstElementChild;
1741
+
1742
+ if (errorMessage) {
1743
+ passwordContainer.insertBefore(newRequirements, errorMessage);
1744
+ } else {
1745
+ passwordContainer.appendChild(newRequirements);
1746
+ }
1747
+ }
1748
+ return;
1749
+ }
1750
+
1751
+ // Remove requirements if showing
1752
+ if (existingRequirements) {
1753
+ existingRequirements.remove();
1754
+ }
1755
+
1756
+ // Calculate new strength values
1757
+ const { strength, label, color, checks } =
1758
+ this.calculatePasswordStrength(password);
1759
+
1760
+ // If strength indicator already exists, just update its values smoothly
1761
+ if (existingStrength) {
1762
+ const fillBar = existingStrength.querySelector(".password-strength-fill");
1763
+ const strengthLabel = existingStrength.querySelector(
1764
+ ".password-strength-label span:first-child"
1765
+ );
1766
+ const strengthPercentage = existingStrength.querySelector(
1767
+ ".password-strength-label span:last-child"
1768
+ );
1769
+ const checkItems = existingStrength.querySelectorAll(
1770
+ ".password-check-item"
1771
+ );
1772
+
1773
+ // Update fill bar with smooth transition
1774
+ if (fillBar) {
1775
+ fillBar.style.width = `${strength}%`;
1776
+ fillBar.style.backgroundColor = color;
1777
+ }
1778
+
1779
+ // Update label and percentage
1780
+ if (strengthLabel) {
1781
+ strengthLabel.textContent = label;
1782
+ strengthLabel.style.color = color;
1783
+ }
1784
+ if (strengthPercentage) {
1785
+ strengthPercentage.textContent = `${strength.toFixed(0)}%`;
1786
+ }
1787
+
1788
+ // Update check items
1789
+ const checkKeys = [
1790
+ "minLength",
1791
+ "hasUppercase",
1792
+ "hasLowercase",
1793
+ "hasNumber",
1794
+ "hasSpecial",
1795
+ ];
1796
+ checkItems.forEach((item, index) => {
1797
+ const checkKey = checkKeys[index];
1798
+ const isValid = checks[checkKey];
1799
+ const icon = item.querySelector(".password-check-icon");
1800
+
1801
+ if (isValid) {
1802
+ item.classList.add("valid");
1803
+ if (icon) icon.textContent = "✓";
1804
+ } else {
1805
+ item.classList.remove("valid");
1806
+ if (icon) icon.textContent = "○";
1807
+ }
1808
+ });
1809
+ } else {
1810
+ // Create new indicator HTML if it doesn't exist
1811
+ const strengthHTML = this.renderPasswordStrength(password);
1812
+ const tempDiv = document.createElement("div");
1813
+ tempDiv.innerHTML = strengthHTML;
1814
+ const newIndicator = tempDiv.firstElementChild;
1815
+
1816
+ // Insert before error message if it exists, otherwise append to container
1817
+ if (errorMessage) {
1818
+ passwordContainer.insertBefore(newIndicator, errorMessage);
1819
+ } else {
1820
+ passwordContainer.appendChild(newIndicator);
1821
+ }
1822
+ }
1823
+ }
1824
+
1825
+ handleFieldBlur(e) {
1826
+ const input = e.target;
1827
+ const name = input.name;
1828
+ const value = input.value;
1829
+
1830
+ if (input.dataset.format === "phone") {
1831
+ input.value = this.formatPhoneNumber(value);
1832
+ }
1833
+
1834
+ if (input.dataset.format === "ein") {
1835
+ input.value = this.formatEIN(value);
1836
+ }
1837
+
1838
+ if (input.type === "url" && value) {
1839
+ const validationResult = this.validators.url(value);
1840
+ if (validationResult.isValid && validationResult.normalizedValue) {
1841
+ input.value = validationResult.normalizedValue;
1842
+ }
1843
+ }
1844
+
1845
+ const stepId = this.STEPS[this.state.currentStep].id;
1846
+
1847
+ if (stepId === "business-details") {
1848
+ this.state.formData.businessDetails[name] = input.value;
1849
+ } else if (stepId === "representative-details") {
1850
+ const repIndex = input.dataset.repIndex;
1851
+ if (repIndex !== undefined) {
1852
+ const idx = parseInt(repIndex);
1853
+ if (this.state.formData.representatives[idx]) {
1854
+ this.state.formData.representatives[idx][name] = input.value;
1855
+ }
1856
+ }
1857
+ }
1858
+ }
1859
+
1860
+ attachSubmissionFailureListeners() {
1861
+ const shadow = this.shadowRoot;
1862
+
1863
+ const resubmitBtn = shadow.querySelector(".btn-resubmit");
1864
+ if (resubmitBtn) {
1865
+ resubmitBtn.addEventListener("click", () => {
1866
+ this.setState({
1867
+ isSubmissionFailed: false,
1868
+ uiState: {
1869
+ isLoading: false,
1870
+ showErrors: false,
1871
+ errorMessage: null,
1872
+ },
1873
+ });
1874
+ });
1875
+ }
1876
+ }
1877
+
1878
+ // ==================== RENDERING ====================
1879
+
1880
+ render() {
1881
+ let content;
1882
+
1883
+ if (this.state.isSubmissionFailed) {
1884
+ content = this.renderSubmissionFailurePage();
1885
+ } else if (this.state.isSubmitted) {
1886
+ content = this.renderSuccessPage();
1887
+ } else if (this.state.uiState.isLoading) {
1888
+ content = `
1889
+ <div class="loading-content">
1890
+ <h2>Submitting Your Application...</h2>
1891
+ <p style="color: var(--gray-medium); margin-bottom: var(--spacing-lg);">
1892
+ Please wait while we process your information.
1893
+ </p>
1894
+ <div class="loading-spinner"></div>
1895
+ </div>
1896
+ `;
1897
+ } else {
1898
+ content = `
1899
+ <div class="form-header">
1900
+ <div class="form-logo">
1901
+ <img src="https://bisonpaywell.com/lovable-uploads/28831244-e8b3-4e7b-8dbb-c016f9f9d54f.png" alt="Logo" />
1902
+ </div>
1903
+ ${this.renderStepperHeader()}
1904
+ </div>
1905
+ <div class="form-body">
1906
+ ${this.renderFormContent()}
1907
+ </div>
1908
+ <div class="form-footer">
1909
+ ${this.renderNavigationFooter()}
1910
+ </div>
1911
+ `;
1912
+ }
1913
+
1914
+ this.shadowRoot.innerHTML = `
1915
+ ${this.renderStyles()}
1916
+ <div class="form-container">
1917
+ ${content}
1918
+ </div>
1919
+ `;
1920
+
1921
+ this.attachEventListeners();
1922
+ }
1923
+
1924
+ renderStepperHeader() {
1925
+ return `
1926
+ <div class="stepper-header">
1927
+ ${this.STEPS.map((step, index) =>
1928
+ this.renderStepIndicator(step, index)
1929
+ ).join("")}
1930
+ </div>
1931
+ `;
1932
+ }
1933
+
1934
+ renderStepIndicator(step, index) {
1935
+ const isComplete = this.state.completedSteps.has(index);
1936
+ const isCurrent = this.state.currentStep === index;
1937
+ const isClickable = isComplete || index < this.state.currentStep;
1938
+
1939
+ return `
1940
+ <div class="step-indicator ${isCurrent ? "active" : ""} ${isComplete ? "complete" : ""
1941
+ } ${isClickable ? "clickable" : ""}"
1942
+ ${isClickable ? `data-step="${index}"` : ""}>
1943
+ <div class="step-circle">
1944
+ ${isComplete ? "✓" : index + 1}
1945
+ </div>
1946
+ <div class="step-label">${step.title}</div>
1947
+ </div>
1948
+ `;
1949
+ }
1950
+
1951
+ renderFormContent() {
1952
+ const stepId = this.STEPS[this.state.currentStep].id;
1953
+
1954
+ switch (stepId) {
1955
+ case "business-details":
1956
+ return this.renderBusinessDetailsForm();
1957
+ case "representative-details":
1958
+ return this.renderRepresentativeDetailsForm();
1959
+ case "business-verification":
1960
+ return this.renderBusinessVerificationForm();
1961
+ default:
1962
+ return "";
1963
+ }
1964
+ }
1965
+
1966
+ renderPasswordStrength(password) {
1967
+ const { strength, label, color, checks } =
1968
+ this.calculatePasswordStrength(password);
1969
+
1970
+ return `
1971
+ <div class="password-strength">
1972
+ <div class="password-strength-bar">
1973
+ <div class="password-strength-fill" style="width: ${strength}%; background-color: ${color};"></div>
1974
+ </div>
1975
+ <div class="password-strength-label">
1976
+ <span style="color: ${color};">${label}</span>
1977
+ <span>${strength.toFixed(0)}%</span>
1978
+ </div>
1979
+ <div class="password-check">
1980
+ <div class="password-check-item ${checks.minLength ? "valid" : ""}">
1981
+ <span class="password-check-icon">${checks.minLength ? "✓" : "○"}</span>
1982
+ <span>8+ characters</span>
1983
+ </div>
1984
+ <div class="password-check-item ${checks.hasUppercase ? "valid" : ""}">
1985
+ <span class="password-check-icon">${checks.hasUppercase ? "✓" : "○"}</span>
1986
+ <span>Uppercase (A-Z)</span>
1987
+ </div>
1988
+ <div class="password-check-item ${checks.hasLowercase ? "valid" : ""}">
1989
+ <span class="password-check-icon">${checks.hasLowercase ? "✓" : "○"}</span>
1990
+ <span>Lowercase (a-z)</span>
1991
+ </div>
1992
+ <div class="password-check-item ${checks.hasNumber ? "valid" : ""}">
1993
+ <span class="password-check-icon">${checks.hasNumber ? "✓" : "○"}</span>
1994
+ <span>Number (0-9)</span>
1995
+ </div>
1996
+ <div class="password-check-item ${checks.hasSpecial ? "valid" : ""}">
1997
+ <span class="password-check-icon">${checks.hasSpecial ? "✓" : "○"}</span>
1998
+ <span>Special (!@#$%...)</span>
1999
+ </div>
2000
+ </div>
2001
+ </div>
2002
+ `;
2003
+ }
2004
+
2005
+ renderPersonalDetailsForm() {
2006
+ const data = this.state.formData.personalDetails;
2007
+
2008
+ return `
2009
+ <div class="form-section">
2010
+ <h2>Personal Information</h2>
2011
+ <p>Provide your personal details</p>
2012
+
2013
+ <div class="form-grid">
2014
+ ${this.renderField({
2015
+ name: "firstName",
2016
+ label: "First Name *",
2017
+ value: data.firstName,
2018
+ error: this.getFieldError("firstName"),
2019
+ })}
2020
+
2021
+ ${this.renderField({
2022
+ name: "lastName",
2023
+ label: "Last Name *",
2024
+ value: data.lastName,
2025
+ error: this.getFieldError("lastName"),
2026
+ })}
2027
+
2028
+ <div class="form-field full-width ${this.getFieldError("password") ? "has-error" : ""
2029
+ }">
2030
+ <label for="password">Password <span class="required-asterisk">*</span></label>
2031
+ <input
2032
+ type="password"
2033
+ id="password"
2034
+ name="password"
2035
+ value="${data.password}"
2036
+ autocomplete="new-password"
2037
+ />
2038
+ ${data.password
2039
+ ? this.renderPasswordStrength(data.password)
2040
+ : `<div class="password-requirements">
2041
+ <p style="font-size: 12px; color: var(--gray-medium); margin-top: 8px;">
2042
+ Password must contain:
2043
+ </p>
2044
+ <ul style="font-size: 12px; color: var(--gray-medium); margin: 4px 0 0 20px; padding: 0;">
2045
+ <li>At least 8 characters</li>
2046
+ <li>One uppercase letter (A-Z)</li>
2047
+ <li>One lowercase letter (a-z)</li>
2048
+ <li>One number (0-9)</li>
2049
+ <li>One special character (!@#$%^&*)</li>
2050
+ </ul>
2051
+ </div>`
2052
+ }
2053
+ ${this.getFieldError("password")
2054
+ ? `<span class="error-message">${this.getFieldError("password")}</span>`
2055
+ : ""
2056
+ }
2057
+ </div>
2058
+
2059
+ ${this.renderField({
2060
+ name: "confirmPassword",
2061
+ label: "Confirm Password *",
2062
+ type: "password",
2063
+ value: data.confirmPassword,
2064
+ error: this.getFieldError("confirmPassword"),
2065
+ className: "full-width",
2066
+ })}
2067
+ </div>
2068
+ </div>
2069
+ `;
2070
+ }
2071
+
2072
+ renderField({
2073
+ name,
2074
+ label,
2075
+ type = "text",
2076
+ value = "",
2077
+ error = "",
2078
+ readOnly = false,
2079
+ disabled = false,
2080
+ placeholder = "",
2081
+ className = "",
2082
+ maxLength = null,
2083
+ dataRepIndex = null,
2084
+ dataFormat = null,
2085
+ }) {
2086
+ const fieldClass = `form-field ${error ? "has-error" : ""} ${className}`;
2087
+ const fieldId = dataRepIndex !== null ? `${name}-${dataRepIndex}` : name;
2088
+
2089
+ return `
2090
+ <div class="${fieldClass}">
2091
+ <label for="${fieldId}">${label.replace(
2092
+ " *",
2093
+ ' <span class="required-asterisk">*</span>'
2094
+ )}</label>
2095
+ <input
2096
+ type="${type}"
2097
+ id="${fieldId}"
2098
+ name="${name}"
2099
+ value="${value}"
2100
+ ${readOnly ? "readonly" : ""}
2101
+ ${disabled ? "disabled" : ""}
2102
+ ${placeholder ? `placeholder="${placeholder}"` : ""}
2103
+ ${maxLength ? `maxlength="${maxLength}"` : ""}
2104
+ ${dataRepIndex !== null ? `data-rep-index="${dataRepIndex}"` : ""}
2105
+ ${dataFormat ? `data-format="${dataFormat}"` : ""}
2106
+ />
2107
+ ${error ? `<span class="error-message">${error}</span>` : ""}
2108
+ </div>
2109
+ `;
2110
+ }
2111
+
2112
+ renderBusinessDetailsForm() {
2113
+ const data = this.state.formData.businessDetails;
2114
+
2115
+ return `
2116
+ <div class="form-section">
2117
+ <h2>Business Information</h2>
2118
+ <p>Provide your business details</p>
2119
+
2120
+ <div class="form-grid">
2121
+ ${this.renderField({
2122
+ name: "businessName",
2123
+ label: "Business Name *",
2124
+ value: data.businessName,
2125
+ error: this.getFieldError("businessName"),
2126
+ })}
2127
+
2128
+ ${this.renderField({
2129
+ name: "doingBusinessAs",
2130
+ label: "Doing Business As (DBA) *",
2131
+ value: data.doingBusinessAs,
2132
+ error: this.getFieldError("doingBusinessAs"),
2133
+ })}
2134
+
2135
+ ${this._initialData?.businessDetails?.businessEmail ? `
2136
+ ${this.renderField({
2137
+ name: "businessPhoneNumber",
2138
+ label: "Business Phone *",
2139
+ type: "tel",
2140
+ value: data.businessPhoneNumber,
2141
+ error: this.getFieldError("businessPhoneNumber"),
2142
+ placeholder: "(555) 123-4567",
2143
+ dataFormat: "phone",
2144
+ })}
2145
+
2146
+ ${this.renderField({
2147
+ name: "ein",
2148
+ label: "EIN *",
2149
+ value: data.ein,
2150
+ error: this.getFieldError("ein"),
2151
+ placeholder: "12-3456789",
2152
+ maxLength: 10,
2153
+ dataFormat: "ein",
2154
+ })}
2155
+ ` : `
2156
+ ${this.renderField({
2157
+ name: "ein",
2158
+ label: "EIN *",
2159
+ value: data.ein,
2160
+ error: this.getFieldError("ein"),
2161
+ placeholder: "12-3456789",
2162
+ maxLength: 10,
2163
+ dataFormat: "ein",
2164
+ className: "full-width",
2165
+ })}
2166
+ `}
2167
+
2168
+ ${this.renderField({
2169
+ name: "businessWebsite",
2170
+ label: "Business Website",
2171
+ type: "url",
2172
+ value: data.businessWebsite,
2173
+ error: this.getFieldError("businessWebsite"),
2174
+ placeholder: "https://example.com",
2175
+ className: "full-width",
2176
+ })}
2177
+
2178
+ ${!this._initialData?.businessDetails?.businessEmail ? `
2179
+ ${this.renderField({
2180
+ name: "businessPhoneNumber",
2181
+ label: "Business Phone *",
2182
+ type: "tel",
2183
+ value: data.businessPhoneNumber,
2184
+ error: this.getFieldError("businessPhoneNumber"),
2185
+ placeholder: "(555) 123-4567",
2186
+ dataFormat: "phone",
2187
+ })}
2188
+
2189
+ ${this.renderField({
2190
+ name: "businessEmail",
2191
+ label: "Business Email *",
2192
+ type: "email",
2193
+ value: data.businessEmail,
2194
+ error: this.getFieldError("businessEmail"),
2195
+ readOnly: false,
2196
+ })}
2197
+ ` : ``}
2198
+
2199
+ ${this.renderField({
2200
+ name: "BusinessAddress1",
2201
+ label: "Street Address *",
2202
+ value: data.BusinessAddress1,
2203
+ error: this.getFieldError("BusinessAddress1"),
2204
+ className: "full-width",
2205
+ })}
2206
+
2207
+ ${this.renderField({
2208
+ name: "businessAddress2",
2209
+ label: "Street Address 2 (Optional)",
2210
+ value: data.businessAddress2,
2211
+ error: this.getFieldError("businessAddress2"),
2212
+ className: "full-width",
2213
+ })}
2214
+
2215
+ ${this.renderField({
2216
+ name: "businessCity",
2217
+ label: "City *",
2218
+ value: data.businessCity,
2219
+ error: this.getFieldError("businessCity"),
2220
+ })}
2221
+
2222
+ <div class="form-field ${this.getFieldError("businessState") ? "has-error" : ""
2223
+ }">
2224
+ <label for="businessState">State <span class="required-asterisk">*</span></label>
2225
+ <select id="businessState" name="businessState">
2226
+ <option value="">Select State</option>
2227
+ ${this.US_STATES.map(
2228
+ (state) => `
2229
+ <option value="${state}" ${data.businessState === state ? "selected" : ""
2230
+ }>${state}</option>
2231
+ `
2232
+ ).join("")}
2233
+ </select>
2234
+ ${this.getFieldError("businessState")
2235
+ ? `<span class="error-message">${this.getFieldError(
2236
+ "businessState"
2237
+ )}</span>`
2238
+ : ""
2239
+ }
2240
+ </div>
2241
+
2242
+ ${this.renderField({
2243
+ name: "businessPostalCode",
2244
+ label: "ZIP Code *",
2245
+ value: data.businessPostalCode,
2246
+ error: this.getFieldError("businessPostalCode"),
2247
+ placeholder: "12345",
2248
+ maxLength: 5,
2249
+ })}
2250
+ </div>
2251
+ </div>
2252
+ `;
2253
+ }
2254
+
2255
+
2256
+
2257
+ renderRepresentativeDetailsForm() {
2258
+ const representatives = this.state.formData.representatives;
2259
+
2260
+ return `
2261
+ <div class="form-section">
2262
+ <h2>Business Representatives</h2>
2263
+ <p>Add business representatives (required)</p>
2264
+
2265
+ <div class="representatives-list">
2266
+ ${representatives.length === 0
2267
+ ? `
2268
+ <div class="empty-state">
2269
+ <p>No representatives added yet. Click below to add one.</p>
2270
+ </div>
2271
+ `
2272
+ : ""
2273
+ }
2274
+ ${representatives
2275
+ .map((rep, index) => this.renderRepresentativeCard(rep, index))
2276
+ .join("")}
2277
+ </div>
2278
+
2279
+ <button type="button" class="add-representative-btn">
2280
+ + Add Representative
2281
+ </button>
2282
+ </div>
2283
+ `;
2284
+ }
2285
+
2286
+ renderRepresentativeCard(representative, index) {
2287
+ const isStaged = representative.isStaged === true;
2288
+ const isExpanded = representative.isExpanded !== false; // Default to expanded for new reps
2289
+ const isModified = this.isRepresentativeModified(index);
2290
+ const isValidating = representative.isValidating === true;
2291
+
2292
+ // Collapsed view for staged representatives (not expanded)
2293
+ if (isStaged && !isExpanded) {
2294
+ return `
2295
+ <div class="representative-card staged collapsed" data-index="${index}">
2296
+ <div class="card-header collapsed-header">
2297
+ <div class="collapsed-info">
2298
+ <span class="confirmed-badge">✓</span>
2299
+ <div class="collapsed-details">
2300
+ <h3>Representative ${index + 1}</h3>
2301
+ <p class="collapsed-summary">${representative.representativeFirstName} ${representative.representativeLastName} · ${representative.representativeEmail}</p>
2302
+ </div>
2303
+ </div>
2304
+ <div class="collapsed-actions">
2305
+ <button type="button" class="btn-expand-representative" data-index="${index}">Edit</button>
2306
+ <button type="button" class="remove-btn" data-index="${index}">Remove</button>
2307
+ </div>
2308
+ </div>
2309
+ </div>
2310
+ `;
2311
+ }
2312
+
2313
+ // Expanded view (for new reps, or when editing a staged rep)
2314
+ return `
2315
+ <div class="representative-card ${isStaged ? 'staged' : ''} ${isModified ? 'modified' : ''}" data-index="${index}">
2316
+ <div class="card-header">
2317
+ <h3>Representative ${index + 1}${isStaged && !isModified ? ' <span class="confirmed-badge-inline">✓ Confirmed</span>' : ''}${isModified ? ' <span class="modified-badge">Modified</span>' : ''}</h3>
2318
+ <button type="button" class="remove-btn" data-index="${index}">Remove</button>
2319
+ </div>
2320
+ <div class="card-body ${isValidating ? 'validating' : ''}">
2321
+ <div class="form-grid">
2322
+ ${this.renderField({
2323
+ name: "representativeFirstName",
2324
+ label: "First Name *",
2325
+ value: representative.representativeFirstName,
2326
+ error: this.getFieldError("representativeFirstName", index),
2327
+ dataRepIndex: index,
2328
+ disabled: isValidating,
2329
+ })}
2330
+
2331
+ ${this.renderField({
2332
+ name: "representativeLastName",
2333
+ label: "Last Name *",
2334
+ value: representative.representativeLastName,
2335
+ error: this.getFieldError("representativeLastName", index),
2336
+ dataRepIndex: index,
2337
+ disabled: isValidating,
2338
+ })}
2339
+
2340
+ ${this.renderField({
2341
+ name: "representativeJobTitle",
2342
+ label: "Job Title *",
2343
+ value: representative.representativeJobTitle,
2344
+ error: this.getFieldError("representativeJobTitle", index),
2345
+ dataRepIndex: index,
2346
+ className: "full-width",
2347
+ disabled: isValidating,
2348
+ })}
2349
+
2350
+ ${this.renderField({
2351
+ name: "representativePhone",
2352
+ label: "Phone *",
2353
+ type: "tel",
2354
+ value: representative.representativePhone,
2355
+ error: this.getFieldError("representativePhone", index),
2356
+ placeholder: "(555) 123-4567",
2357
+ dataRepIndex: index,
2358
+ dataFormat: "phone",
2359
+ disabled: isValidating,
2360
+ })}
2361
+
2362
+ ${this.renderField({
2363
+ name: "representativeEmail",
2364
+ label: "Email *",
2365
+ type: "email",
2366
+ value: representative.representativeEmail,
2367
+ error: this.getFieldError("representativeEmail", index),
2368
+ dataRepIndex: index,
2369
+ disabled: isValidating,
2370
+ })}
2371
+
2372
+ ${this.renderField({
2373
+ name: "representativeDateOfBirth",
2374
+ label: "Date of Birth *",
2375
+ type: "date",
2376
+ value: representative.representativeDateOfBirth,
2377
+ error: this.getFieldError("representativeDateOfBirth", index),
2378
+ dataRepIndex: index,
2379
+ className: "full-width",
2380
+ disabled: isValidating,
2381
+ })}
2382
+
2383
+ ${this.renderField({
2384
+ name: "representativeAddress",
2385
+ label: "Address *",
2386
+ value: representative.representativeAddress,
2387
+ error: this.getFieldError("representativeAddress", index),
2388
+ dataRepIndex: index,
2389
+ className: "full-width",
2390
+ disabled: isValidating,
2391
+ })}
2392
+
2393
+ ${this.renderField({
2394
+ name: "representativeCity",
2395
+ label: "City *",
2396
+ value: representative.representativeCity,
2397
+ error: this.getFieldError("representativeCity", index),
2398
+ dataRepIndex: index,
2399
+ disabled: isValidating,
2400
+ })}
2401
+
2402
+ <div class="form-field ${this.getFieldError("representativeState", index)
2403
+ ? "has-error"
2404
+ : ""
2405
+ }">
2406
+ <label for="representativeState-${index}">State <span class="required-asterisk">*</span></label>
2407
+ <select id="representativeState-${index}" name="representativeState" data-rep-index="${index}" ${isValidating ? 'disabled' : ''}>
2408
+ <option value="">Select State</option>
2409
+ ${this.US_STATES.map(
2410
+ (state) => `
2411
+ <option value="${state}" ${representative.representativeState === state
2412
+ ? "selected"
2413
+ : ""
2414
+ }>${state}</option>
2415
+ `
2416
+ ).join("")}
2417
+ </select>
2418
+ ${this.getFieldError("representativeState", index)
2419
+ ? `<span class="error-message">${this.getFieldError(
2420
+ "representativeState",
2421
+ index
2422
+ )}</span>`
2423
+ : ""
2424
+ }
2425
+ </div>
2426
+
2427
+ ${this.renderField({
2428
+ name: "representativeZip",
2429
+ label: "ZIP Code *",
2430
+ value: representative.representativeZip,
2431
+ error: this.getFieldError("representativeZip", index),
2432
+ placeholder: "12345",
2433
+ maxLength: 5,
2434
+ dataRepIndex: index,
2435
+ disabled: isValidating,
2436
+ })}
2437
+ </div>
2438
+ </div>
2439
+ <div class="card-footer">
2440
+ ${isValidating
2441
+ ? `<div class="card-footer-actions">
2442
+ <span class="validating-indicator">Validating...</span>
2443
+ </div>`
2444
+ : isStaged && isModified
2445
+ ? `<div class="card-footer-actions">
2446
+ <button type="button" class="btn-undo-representative" data-index="${index}">
2447
+ Undo Changes
2448
+ </button>
2449
+ <button type="button" class="btn-stage-representative" data-index="${index}">
2450
+ Confirm Changes
2451
+ </button>
2452
+ </div>`
2453
+ : isStaged && !isModified
2454
+ ? `<div class="card-footer-actions">
2455
+ <button type="button" class="btn-collapse-representative" data-index="${index}">
2456
+ Done
2457
+ </button>
2458
+ </div>`
2459
+ : `<div class="card-footer-actions">
2460
+ <button type="button" class="btn-discard-representative" data-index="${index}">
2461
+ Discard
2462
+ </button>
2463
+ <button type="button" class="btn-stage-representative" data-index="${index}">
2464
+ Confirm Representative
2465
+ </button>
2466
+ </div>`
2467
+ }
2468
+ </div>
2469
+ </div>
2470
+ `;
2471
+ }
2472
+
2473
+ renderBusinessVerificationForm() {
2474
+ const data = this.state.formData.businessVerification;
2475
+ const verificationDocuments = data.verificationDocuments || [];
2476
+ const error = this.getFieldError("verificationDocuments");
2477
+ const showErrors = this.state.uiState.showErrors;
2478
+
2479
+ return `
2480
+ <div class="form-section">
2481
+ <h2>Business Verification</h2>
2482
+ <p>Upload supporting documents for verification (required, max 10 files, 10MB each)</p>
2483
+
2484
+ <div class="form-grid">
2485
+ <div class="form-field full-width ${showErrors && error ? "has-error" : ""
2486
+ }">
2487
+ <label for="verificationDocs">
2488
+ Upload Verification Documents <span class="required-asterisk">*</span>
2489
+ <span style="font-size: 12px; color: var(--gray-medium); font-weight: normal;">
2490
+ (PDF, JPG, PNG, DOC, DOCX - Max 10MB each)
2491
+ </span>
2492
+ </label>
2493
+
2494
+ <div class="drag-drop-area" id="dragDropArea">
2495
+ <div class="drag-drop-content">
2496
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2497
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
2498
+ <polyline points="17 8 12 3 7 8"></polyline>
2499
+ <line x1="12" y1="3" x2="12" y2="15"></line>
2500
+ </svg>
2501
+ <p style="margin-bottom: var(--spacing-sm, 0.5rem); color: var(--color-headline, #0f2a39); font-weight: 500; font-size: 16px;">
2502
+ Drag and drop files here
2503
+ </p>
2504
+ <p style="font-size: 14px; color: var(--gray-medium); margin-bottom: var(--spacing-md, 1rem);">
2505
+ or
2506
+ </p>
2507
+ <button type="button" class="btn-browse" style="
2508
+ padding: 10px 20px;
2509
+ background: var(--primary-color);
2510
+ color: var(--color-white, #fff);
2511
+ border: none;
2512
+ border-radius: var(--border-radius-sm, 0.5rem);
2513
+ cursor: pointer;
2514
+ font-size: 14px;
2515
+ font-weight: 500;
2516
+ ">Browse Files</button>
2517
+ <input
2518
+ type="file"
2519
+ id="verificationDocs"
2520
+ name="verificationDocs"
2521
+ multiple
2522
+ accept=".pdf,.jpg,.jpeg,.png,.doc,.docx"
2523
+ style="display: none;"
2524
+ />
2525
+ </div>
2526
+ </div>
2527
+
2528
+ <div id="fileList" style="margin-top: var(--spacing-md);">
2529
+ ${verificationDocuments.length > 0
2530
+ ? this.renderFileList(verificationDocuments)
2531
+ : ""
2532
+ }
2533
+ </div>
2534
+
2535
+ ${showErrors && error
2536
+ ? `<span class="error-message">${error}</span>`
2537
+ : ""
2538
+ }
2539
+ </div>
2540
+ </div>
2541
+ </div>
2542
+ `;
2543
+ }
2544
+
2545
+ renderFileList(files) {
2546
+ return `
2547
+ <div class="uploaded-files">
2548
+ <p style="font-size: 14px; font-weight: 500; margin-bottom: var(--spacing-sm); color: var(--color-headline, #0f2a39);">
2549
+ ${files.length} file(s) uploaded:
2550
+ </p>
2551
+ ${files
2552
+ .map(
2553
+ (file, index) => `
2554
+ <div class="file-item" data-index="${index}" style="
2555
+ display: flex;
2556
+ align-items: center;
2557
+ justify-content: space-between;
2558
+ padding: var(--spacing-sm);
2559
+ background: var(--gray-light);
2560
+ border-radius: var(--border-radius-sm);
2561
+ margin-bottom: var(--spacing-sm);
2562
+ ">
2563
+ <div style="display: flex; align-items: center; gap: var(--spacing-sm); flex: 1; min-width: 0;">
2564
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
2565
+ <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
2566
+ <polyline points="13 2 13 9 20 9"></polyline>
2567
+ </svg>
2568
+ <span style="font-size: 14px; color: var(--color-headline, #0f2a39); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
2569
+ ${file.name}
2570
+ </span>
2571
+ <span style="font-size: 12px; color: var(--gray-medium); white-space: nowrap;">
2572
+ (${(file.size / 1024).toFixed(1)} KB)
2573
+ </span>
2574
+ </div>
2575
+ <button type="button" class="btn-remove-file" data-index="${index}" style="
2576
+ background: none;
2577
+ border: none;
2578
+ color: var(--error-color);
2579
+ cursor: pointer;
2580
+ padding: var(--spacing-sm);
2581
+ font-size: 14px;
2582
+ flex-shrink: 0;
2583
+ ">✕</button>
2584
+ </div>
2585
+ `
2586
+ )
2587
+ .join("")}
2588
+ </div>
2589
+ `;
2590
+ }
2591
+
2592
+ renderNavigationFooter() {
2593
+ const isFirstStep = this.state.currentStep === 0;
2594
+ const isLastStep = this.state.currentStep === this.state.totalSteps - 1;
2595
+ const canSkip = this.STEPS[this.state.currentStep].canSkip;
2596
+ const stepId = this.STEPS[this.state.currentStep].id;
2597
+
2598
+ const showBack = !isFirstStep;
2599
+
2600
+ // Disable Next button on representative step if:
2601
+ // 1. There are active (non-staged) forms, OR
2602
+ // 2. No representative has been staged yet (step is required)
2603
+ const isRepStep = stepId === 'representative-details';
2604
+ const hasActiveRepForm = isRepStep && this.hasActiveRepresentativeForm();
2605
+ const noStagedRep = isRepStep && !this.hasStagedRepresentative();
2606
+ const isNextDisabled = hasActiveRepForm || noStagedRep;
2607
+
2608
+ // Determine hint message
2609
+ let hintMessage = '';
2610
+ if (isRepStep && isNextDisabled) {
2611
+ if (hasActiveRepForm) {
2612
+ hintMessage = 'Please confirm or discard all representatives before proceeding';
2613
+ } else if (noStagedRep) {
2614
+ hintMessage = 'At least one representative is required to proceed';
2615
+ }
2616
+ }
2617
+
2618
+ return `
2619
+ <div class="navigation-footer">
2620
+ ${showBack ? '<button type="button" class="btn-back">Back</button>' : ""
2621
+ }
2622
+ ${canSkip ? '<button type="button" class="btn-skip">Skip</button>' : ""}
2623
+ <button type="button" class="btn-next" ${isNextDisabled ? 'disabled' : ''}>
2624
+ ${isLastStep ? "Submit" : "Next"}
2625
+ </button>
2626
+ ${hintMessage ? `<p class="next-disabled-hint">${hintMessage}</p>` : ''}
2627
+ </div>
2628
+ `;
2629
+ }
2630
+
2631
+ renderSuccessPage() {
2632
+ const { businessDetails } = this.state.formData;
2633
+
2634
+ return `
2635
+ <div class="success-container">
2636
+ <div class="success-icon">
2637
+ <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
2638
+ <circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none"/>
2639
+ <path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
2640
+ </svg>
2641
+ </div>
2642
+
2643
+ <h2>Bison Account Created! 🎉</h2>
2644
+ <p style="color: var(--color-headline, #0f2a39); font-size: 16px; margin-bottom: var(--spacing-lg);">
2645
+ Your WIO onboarding has been successfully completed.
2646
+ </p>
2647
+
2648
+ <div class="success-actions">
2649
+ <button class="btn-confirm-success" type="button">
2650
+ ${this.doneButtonText || "Done"}
2651
+ </button>
2652
+ </div>
2653
+ </div>
2654
+ `;
2655
+ }
2656
+
2657
+ renderSubmissionFailurePage() {
2658
+ const { errorMessage, errorDetails, statusCode } = this.state.uiState;
2659
+ const submissionError = this.state.submissionError || {};
2660
+
2661
+ // Determine error title based on status code
2662
+ let errorTitle = "Submission Failed";
2663
+ let errorSubtitle = "Your onboarding submission could not be processed.";
2664
+
2665
+ if (statusCode === 400) {
2666
+ errorTitle = "Validation Error";
2667
+ errorSubtitle = "Some of your information needs to be corrected.";
2668
+ } else if (statusCode === 401) {
2669
+ errorTitle = "Authentication Error";
2670
+ errorSubtitle = "Your session may have expired or credentials are invalid.";
2671
+ } else if (statusCode === 500) {
2672
+ errorTitle = "Server Error";
2673
+ errorSubtitle = "We encountered an issue processing your request.";
2674
+ }
2675
+
2676
+ // Build error list HTML if there are specific errors
2677
+ const errorsListHtml =
2678
+ submissionError.errors && submissionError.errors.length > 0
2679
+ ? `
2680
+ <ul style="
2681
+ text-align: left;
2682
+ margin: var(--spacing-md) 0;
2683
+ padding-left: var(--spacing-lg);
2684
+ color: var(--color-error-dark, #991b1b);
2685
+ font-size: 14px;
2686
+ line-height: 1.6;
2687
+ ">
2688
+ ${submissionError.errors.map((err) => `<li>${err}</li>`).join("")}
2689
+ </ul>
2690
+ `
2691
+ : "";
2692
+
2693
+ // Show trace ID if available (for support reference)
2694
+ const traceIdHtml = submissionError.traceId
2695
+ ? `<p style="
2696
+ margin-top: var(--spacing-sm);
2697
+ font-size: 12px;
2698
+ color: var(--gray-medium);
2699
+ ">Reference ID: <code style="background: var(--gray-light, #f3f4f6); padding: 2px 6px; border-radius: 4px;">${submissionError.traceId}</code></p>`
2700
+ : "";
2701
+
2702
+ return `
2703
+ <div class="error-container">
2704
+ <div class="error-icon">
2705
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2706
+ <circle cx="12" cy="12" r="10" stroke-opacity="0.2"></circle>
2707
+ <path d="M12 8v4m0 4h.01" stroke-linecap="round" stroke-linejoin="round"></path>
2708
+ </svg>
2709
+ </div>
2710
+
2711
+ <h2>${errorTitle}</h2>
2712
+ <p>${errorSubtitle}</p>
2713
+
2714
+ <div class="error-details">
2715
+ <h3>Error Details</h3>
2716
+ <p><strong>Issue:</strong> ${errorMessage || "The submission failed due to a server error."}</p>
2717
+ ${errorsListHtml}
2718
+ ${traceIdHtml}
2719
+ <p style="margin-top: var(--spacing-md); color: var(--color-error-dark, #991b1b);">
2720
+ Please try submitting again. If the problem persists, contact support.
2721
+ </p>
2722
+ </div>
2723
+
2724
+ <div style="margin-top: var(--spacing-lg); display: flex; gap: var(--spacing-sm); justify-content: center; width: 100%;">
2725
+ <button type="button" class="btn-resubmit" style="
2726
+ padding: 12px 24px;
2727
+ background: var(--primary-color);
2728
+ color: var(--color-white, #fff);
2729
+ border: none;
2730
+ border-radius: var(--border-radius-sm);
2731
+ font-size: 15px;
2732
+ font-weight: 600;
2733
+ cursor: pointer;
2734
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
2735
+ transition: all 0.2s ease;
2736
+ ">Try Again</button>
2737
+ </div>
2738
+ </div>
2739
+ `;
2740
+ }
2741
+
2742
+ // ==================== EVENT LISTENERS ====================
2743
+
2744
+ attachEventListeners() {
2745
+ const shadow = this.shadowRoot;
2746
+
2747
+ // Success confirmation button
2748
+ const confirmBtn = shadow.querySelector(".btn-confirm-success");
2749
+ if (confirmBtn) {
2750
+ confirmBtn.addEventListener("click", () => this.handleSuccessConfirm());
2751
+ }
2752
+
2753
+
2754
+
2755
+ // Submission failure resubmit button
2756
+ if (this.state.isSubmissionFailed) {
2757
+ this.attachSubmissionFailureListeners();
2758
+ }
2759
+
2760
+ // Form inputs - blur validation
2761
+ shadow.querySelectorAll("input, select").forEach((input) => {
2762
+ input.addEventListener("blur", (e) => this.handleFieldBlur(e));
2763
+ input.addEventListener("input", (e) => this.handleFieldInput(e));
2764
+ });
2765
+
2766
+ // Navigation buttons - use mousedown to prevent blur interference
2767
+ const nextBtn = shadow.querySelector(".btn-next");
2768
+ if (nextBtn) {
2769
+ nextBtn.addEventListener("mousedown", (e) => {
2770
+ e.preventDefault();
2771
+ this.goToNextStep();
2772
+ });
2773
+ }
2774
+
2775
+ const backBtn = shadow.querySelector(".btn-back");
2776
+ if (backBtn) {
2777
+ backBtn.addEventListener("mousedown", (e) => {
2778
+ e.preventDefault();
2779
+ this.goToPreviousStep();
2780
+ });
2781
+ }
2782
+
2783
+ const skipBtn = shadow.querySelector(".btn-skip");
2784
+ if (skipBtn) {
2785
+ skipBtn.addEventListener("mousedown", (e) => {
2786
+ e.preventDefault();
2787
+ this.skipStep();
2788
+ });
2789
+ }
2790
+
2791
+ // Step indicators (for navigation)
2792
+ shadow.querySelectorAll("[data-step]").forEach((indicator) => {
2793
+ indicator.addEventListener("click", (e) => {
2794
+ const stepIndex = parseInt(e.currentTarget.dataset.step);
2795
+ this.goToStep(stepIndex);
2796
+ });
2797
+ });
2798
+
2799
+ // File upload handlers for business verification documents
2800
+ const fileInput = shadow.querySelector("#verificationDocs");
2801
+ const dragDropArea = shadow.querySelector("#dragDropArea");
2802
+ const browseBtn = shadow.querySelector(".btn-browse");
2803
+
2804
+ if (fileInput && dragDropArea) {
2805
+ // Browse button click
2806
+ if (browseBtn) {
2807
+ browseBtn.addEventListener("click", (e) => {
2808
+ e.stopPropagation();
2809
+ fileInput.click();
2810
+ });
2811
+ }
2812
+
2813
+ // Click on drag area
2814
+ dragDropArea.addEventListener("click", () => {
2815
+ fileInput.click();
2816
+ });
2817
+
2818
+ // Drag and drop events
2819
+ dragDropArea.addEventListener("dragenter", (e) => {
2820
+ e.preventDefault();
2821
+ e.stopPropagation();
2822
+ dragDropArea.classList.add("drag-over");
2823
+ });
2824
+
2825
+ dragDropArea.addEventListener("dragover", (e) => {
2826
+ e.preventDefault();
2827
+ e.stopPropagation();
2828
+ dragDropArea.classList.add("drag-over");
2829
+ });
2830
+
2831
+ dragDropArea.addEventListener("dragleave", (e) => {
2832
+ e.preventDefault();
2833
+ e.stopPropagation();
2834
+ if (e.target === dragDropArea) {
2835
+ dragDropArea.classList.remove("drag-over");
2836
+ }
2837
+ });
2838
+
2839
+ dragDropArea.addEventListener("drop", (e) => {
2840
+ e.preventDefault();
2841
+ e.stopPropagation();
2842
+ dragDropArea.classList.remove("drag-over");
2843
+
2844
+ const files = Array.from(e.dataTransfer.files);
2845
+ this.handleFileUpload(files);
2846
+ });
2847
+
2848
+ // File input change
2849
+ fileInput.addEventListener("change", (e) => {
2850
+ const files = Array.from(e.target.files);
2851
+ this.handleFileUpload(files);
2852
+ });
2853
+ }
2854
+
2855
+ // Remove file buttons
2856
+ shadow.querySelectorAll(".btn-remove-file").forEach((btn) => {
2857
+ btn.addEventListener("click", (e) => {
2858
+ e.stopPropagation();
2859
+ const index = parseInt(btn.dataset.index);
2860
+ this.removeFile(index);
2861
+ });
2862
+ });
2863
+
2864
+ // Representative CRUD - use mousedown to prevent blur interference
2865
+ const addBtn = shadow.querySelector(".add-representative-btn");
2866
+ if (addBtn) {
2867
+ addBtn.addEventListener("mousedown", (e) => {
2868
+ e.preventDefault();
2869
+ this.addRepresentative();
2870
+ });
2871
+ }
2872
+
2873
+ shadow.querySelectorAll(".remove-btn").forEach((btn) => {
2874
+ btn.addEventListener("mousedown", (e) => {
2875
+ e.preventDefault();
2876
+ const index = parseInt(e.target.dataset.index);
2877
+ this.removeRepresentative(index);
2878
+ });
2879
+ });
2880
+
2881
+ // Stage representative buttons
2882
+ shadow.querySelectorAll(".btn-stage-representative").forEach((btn) => {
2883
+ btn.addEventListener("mousedown", (e) => {
2884
+ e.preventDefault();
2885
+ const index = parseInt(e.target.dataset.index);
2886
+ this.stageRepresentative(index);
2887
+ });
2888
+ });
2889
+
2890
+ // Discard representative buttons
2891
+ shadow.querySelectorAll(".btn-discard-representative").forEach((btn) => {
2892
+ btn.addEventListener("mousedown", (e) => {
2893
+ e.preventDefault();
2894
+ const index = parseInt(e.target.dataset.index);
2895
+ this.discardRepresentative(index);
2896
+ });
2897
+ });
2898
+
2899
+ // Expand representative buttons (Edit on collapsed card)
2900
+ shadow.querySelectorAll(".btn-expand-representative").forEach((btn) => {
2901
+ btn.addEventListener("mousedown", (e) => {
2902
+ e.preventDefault();
2903
+ const index = parseInt(e.target.dataset.index);
2904
+ this.toggleRepresentativeExpand(index);
2905
+ });
2906
+ });
2907
+
2908
+ // Collapse representative buttons (Done after editing)
2909
+ shadow.querySelectorAll(".btn-collapse-representative").forEach((btn) => {
2910
+ btn.addEventListener("mousedown", (e) => {
2911
+ e.preventDefault();
2912
+ const index = parseInt(e.target.dataset.index);
2913
+ this.toggleRepresentativeExpand(index);
2914
+ });
2915
+ });
2916
+
2917
+ // Undo representative changes buttons
2918
+ shadow.querySelectorAll(".btn-undo-representative").forEach((btn) => {
2919
+ btn.addEventListener("mousedown", (e) => {
2920
+ e.preventDefault();
2921
+ const index = parseInt(e.target.dataset.index);
2922
+ this.undoRepresentativeChanges(index);
2923
+ });
2924
+ });
2925
+ }
2926
+
2927
+ renderStyles() {
2928
+ return `
2929
+ <style>
2930
+ * {
2931
+ box-sizing: border-box;
2932
+ margin: 0;
2933
+ padding: 0;
2934
+ }
2935
+
2936
+ :host {
2937
+ --primary-color: var(--color-primary, #4c7b63);
2938
+ --primary-hover: var(--color-primary-dark, #3a614e);
2939
+ --success-color: var(--color-success, #22c55e);
2940
+ --error-color: var(--color-error, #dd524b);
2941
+ --border-color: var(--color-border, #e5e7eb);
2942
+ --gray-light: var(--color-gray-50, #f9fafb);
2943
+ --gray-medium: var(--color-gray-500, #6b7280);
2944
+ --border-radius: var(--radius-xl, 0.75rem);
2945
+ --border-radius-sm: var(--radius-lg, 0.5rem);
2946
+ --border-radius-lg: var(--radius-2xl, 1rem);
2947
+ --spacing-sm: 0.75rem;
2948
+ --spacing-md: 1.25rem;
2949
+ --spacing-lg: 2rem;
2950
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
2951
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
2952
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
2953
+ --focus-ring: 0 0 0 4px rgba(76, 123, 99, 0.2);
2954
+ font-family: var(--font-sans, 'Inter', system-ui, -apple-system, sans-serif);
2955
+ color: var(--color-secondary, #374151);
2956
+ display: block;
2957
+ }
2958
+
2959
+ /* Form Container (Inline Display) */
2960
+ .form-container {
2961
+ background: var(--color-white, #fff);
2962
+ border-radius: var(--border-radius-lg);
2963
+ max-width: 900px;
2964
+ margin: 0 auto;
2965
+ box-shadow: var(--shadow-lg);
2966
+ overflow: hidden;
2967
+ display: flex;
2968
+ flex-direction: column;
2969
+ min-height: 600px;
2970
+ }
2971
+
2972
+ .form-header {
2973
+ padding: var(--spacing-lg);
2974
+ border-bottom: 1px solid var(--border-color);
2975
+ background: var(--color-white, #fff);
2976
+ }
2977
+
2978
+ .form-body {
2979
+ padding: var(--spacing-lg);
2980
+ flex: 1; /* Allow body to take available space */
2981
+ overflow-y: auto;
2982
+ }
2983
+
2984
+ .form-footer {
2985
+ padding: var(--spacing-lg);
2986
+ border-top: 1px solid var(--border-color);
2987
+ background: var(--gray-light); /* Slight contrast for footer */
2988
+ }
2989
+
2990
+ /* Loading State - Centered */
2991
+ .loading-content {
2992
+ display: flex;
2993
+ flex-direction: column;
2994
+ align-items: center;
2995
+ justify-content: center;
2996
+ min-height: 400px;
2997
+ text-align: center;
2998
+ padding: var(--spacing-lg);
2999
+ max-width: 600px;
3000
+ margin: 40px auto;
3001
+ }
3002
+
3003
+ /* ... existing styles ... */
3004
+
3005
+ /* Success Page - Centered & Polished */
3006
+ .success-container {
3007
+ display: flex;
3008
+ flex-direction: column;
3009
+ align-items: center;
3010
+ justify-content: center;
3011
+ text-align: center;
3012
+ padding: var(--spacing-lg);
3013
+ max-width: 600px;
3014
+ margin: 0 auto;
3015
+ min-height: 500px;
3016
+ }
3017
+
3018
+ /* ... existing styles ... */
3019
+
3020
+ /* Error Page */
3021
+ .error-container {
3022
+ display: flex;
3023
+ flex-direction: column;
3024
+ align-items: center;
3025
+ justify-content: center;
3026
+ padding: var(--spacing-lg);
3027
+ max-width: 600px;
3028
+ margin: 0 auto;
3029
+ min-height: 500px;
3030
+ text-align: center;
3031
+ }
3032
+
3033
+ .error-icon {
3034
+ width: 96px;
3035
+ height: 96px;
3036
+ border-radius: 50%;
3037
+ background: #fee2e2; /* Red 100 */
3038
+ display: flex;
3039
+ align-items: center;
3040
+ justify-content: center;
3041
+ margin-bottom: var(--spacing-lg);
3042
+ }
3043
+
3044
+ .error-icon svg {
3045
+ width: 48px;
3046
+ height: 48px;
3047
+ stroke: var(--error-color);
3048
+ }
3049
+
3050
+ .error-container h2 {
3051
+ font-size: 32px;
3052
+ color: var(--color-headline, #0f2a39);
3053
+ margin-bottom: 0.75rem;
3054
+ }
3055
+
3056
+ .error-container > p {
3057
+ color: var(--gray-medium);
3058
+ margin-bottom: var(--spacing-lg);
3059
+ }
3060
+
3061
+ .error-details {
3062
+ background: #fef2f2; /* Red 50 */
3063
+ border: 1px solid #fee2e2;
3064
+ border-radius: var(--border-radius);
3065
+ padding: var(--spacing-lg);
3066
+ margin: var(--spacing-lg) 0;
3067
+ text-align: left;
3068
+ width: 100%;
3069
+ }
3070
+
3071
+ .error-details h3 {
3072
+ font-size: 16px;
3073
+ color: var(--color-error-dark, #991b1b);
3074
+ margin-bottom: var(--spacing-md);
3075
+ }
3076
+
3077
+ .error-details p {
3078
+ color: var(--color-headline, #0f2a39);
3079
+ line-height: 1.5;
3080
+ }
3081
+ .loading-content h2 {
3082
+ margin-bottom: var(--spacing-md);
3083
+ color: var(--color-headline, #111827);
3084
+ font-size: 24px;
3085
+ }
3086
+
3087
+ .loading-spinner {
3088
+ width: 50px;
3089
+ height: 50px;
3090
+ border: 4px solid var(--gray-light);
3091
+ border-top-color: var(--primary-color);
3092
+ border-radius: 50%;
3093
+ animation: spin 1s linear infinite;
3094
+ margin: var(--spacing-lg) auto;
3095
+ }
3096
+
3097
+ @keyframes spin {
3098
+ to { transform: rotate(360deg); }
3099
+ }
3100
+
3101
+ /* Form Logo */
3102
+ .form-logo {
3103
+ text-align: center;
3104
+ margin-bottom: var(--spacing-lg);
3105
+ }
3106
+
3107
+ .form-logo img {
3108
+ height: 48px; /* Slightly larger */
3109
+ width: auto;
3110
+ }
3111
+
3112
+ /* Stepper Header */
3113
+ .stepper-header {
3114
+ display: flex;
3115
+ justify-content: space-between;
3116
+ align-items: flex-start; /* Align to top */
3117
+ margin-bottom: var(--spacing-md);
3118
+ position: relative; /* For line positioning context if needed */
3119
+ }
3120
+
3121
+ .step-indicator {
3122
+ flex: 1;
3123
+ display: flex;
3124
+ flex-direction: column;
3125
+ align-items: center;
3126
+ gap: var(--spacing-sm);
3127
+ position: relative;
3128
+ z-index: 1;
3129
+ }
3130
+
3131
+ .step-indicator::after {
3132
+ content: '';
3133
+ position: absolute;
3134
+ top: 20px;
3135
+ left: 50%;
3136
+ width: 100%;
3137
+ height: 2px;
3138
+ background: var(--border-color);
3139
+ z-index: -1;
3140
+ }
3141
+
3142
+ .step-indicator:last-child::after {
3143
+ display: none;
3144
+ }
3145
+
3146
+ .step-circle {
3147
+ width: 40px;
3148
+ height: 40px;
3149
+ border-radius: 50%;
3150
+ background: var(--color-white, #fff); /* White background for clean look */
3151
+ color: var(--gray-medium);
3152
+ display: flex;
3153
+ align-items: center;
3154
+ justify-content: center;
3155
+ font-weight: 600;
3156
+ font-size: 14px;
3157
+ border: 2px solid var(--border-color);
3158
+ transition: all 0.3s ease;
3159
+ box-shadow: var(--shadow-sm);
3160
+ }
3161
+
3162
+ .step-indicator.active .step-circle {
3163
+ background: var(--primary-color);
3164
+ color: var(--color-white, #fff);
3165
+ border-color: var(--primary-color);
3166
+ box-shadow: 0 0 0 4px rgba(76, 123, 99, 0.2); /* Focus ring effect */
3167
+ }
3168
+
3169
+ .step-indicator.complete .step-circle {
3170
+ background: var(--success-color);
3171
+ color: var(--color-white, #fff);
3172
+ border-color: var(--success-color);
3173
+ }
3174
+
3175
+ .step-indicator.clickable {
3176
+ cursor: pointer;
3177
+ }
3178
+
3179
+ .step-indicator.clickable:hover .step-circle {
3180
+ transform: translateY(-2px);
3181
+ border-color: var(--primary-color);
3182
+ }
3183
+
3184
+ .step-label {
3185
+ font-size: 13px;
3186
+ color: var(--gray-medium);
3187
+ text-align: center;
3188
+ font-weight: 500;
3189
+ transition: color 0.3s ease;
3190
+ }
3191
+
3192
+ .step-indicator.active .step-label {
3193
+ color: var(--primary-color);
3194
+ font-weight: 700;
3195
+ }
3196
+
3197
+ /* Form Sections */
3198
+ .form-section {
3199
+ margin-bottom: var(--spacing-lg);
3200
+ padding-bottom: var(--spacing-md);
3201
+ }
3202
+
3203
+ .form-section h2 {
3204
+ font-size: 24px;
3205
+ font-weight: 700;
3206
+ color: var(--color-headline, #111827);
3207
+ margin-bottom: 0.5rem;
3208
+ letter-spacing: -0.025em;
3209
+ }
3210
+
3211
+ .form-section > p {
3212
+ color: var(--gray-medium);
3213
+ font-size: 15px;
3214
+ line-height: 1.6;
3215
+ margin-bottom: var(--spacing-lg);
3216
+ }
3217
+
3218
+ .form-grid {
3219
+ display: grid;
3220
+ grid-template-columns: repeat(2, 1fr);
3221
+ gap: var(--spacing-lg); /* Increased gap */
3222
+ }
3223
+
3224
+ .form-field {
3225
+ display: flex;
3226
+ flex-direction: column;
3227
+ gap: 0.5rem;
3228
+ }
3229
+
3230
+ .form-field.full-width {
3231
+ grid-column: 1 / -1;
3232
+ }
3233
+
3234
+ .form-field label {
3235
+ font-size: 14px;
3236
+ font-weight: 600;
3237
+ color: var(--color-headline, #374151);
3238
+ }
3239
+
3240
+ .required-asterisk {
3241
+ color: var(--error-color);
3242
+ margin-left: 2px;
3243
+ }
3244
+
3245
+ .form-field input,
3246
+ .form-field select {
3247
+ padding: 12px 16px;
3248
+ border: 1px solid var(--border-color);
3249
+ border-radius: var(--border-radius-sm);
3250
+ font-size: 15px;
3251
+ font-family: inherit;
3252
+ line-height: 1.5;
3253
+ transition: all 0.2s ease;
3254
+ height: 48px; /* Taller inputs */
3255
+ box-shadow: var(--shadow-sm);
3256
+ width: 100%;
3257
+ background-color: var(--color-white, #fff);
3258
+ }
3259
+
3260
+ .form-field input:focus,
3261
+ .form-field select:focus {
3262
+ outline: none;
3263
+ border-color: var(--primary-color);
3264
+ box-shadow: var(--focus-ring);
3265
+ }
3266
+
3267
+ .form-field.has-error input,
3268
+ .form-field.has-error select {
3269
+ border-color: var(--error-color);
3270
+ }
3271
+
3272
+ .form-field input:disabled,
3273
+ .form-field input[readonly] {
3274
+ background-color: var(--gray-light);
3275
+ color: var(--gray-medium);
3276
+ cursor: not-allowed;
3277
+ }
3278
+
3279
+ .error-message {
3280
+ font-size: 13px;
3281
+ line-height: 1.4;
3282
+ color: var(--error-color);
3283
+ margin-top: 4px;
3284
+ }
3285
+
3286
+ /* Password Strength Indicator */
3287
+ .password-strength {
3288
+ margin-top: 12px;
3289
+ background: var(--gray-light);
3290
+ padding: 12px;
3291
+ border-radius: var(--border-radius-sm);
3292
+ }
3293
+
3294
+ /* ... (Password strength styles mostly same, just slight spacing tweaks if needed) ... */
3295
+ .password-strength-bar {
3296
+ height: 6px;
3297
+ background: #e5e7eb;
3298
+ border-radius: 3px;
3299
+ overflow: hidden;
3300
+ margin-bottom: 8px;
3301
+ }
3302
+
3303
+ .password-strength-fill {
3304
+ height: 100%;
3305
+ transition: width 0.3s ease, background-color 0.3s ease;
3306
+ border-radius: 3px;
3307
+ }
3308
+
3309
+ .password-strength-label a { /* Target text directly if needed or keep structure */ }
3310
+
3311
+ .password-check {
3312
+ margin-top: 8px;
3313
+ gap: 8px;
3314
+ }
3315
+
3316
+
3317
+ /* Radio Group */
3318
+ .radio-group {
3319
+ display: flex;
3320
+ gap: var(--spacing-lg);
3321
+ padding: 8px 0;
3322
+ }
3323
+
3324
+ .radio-option {
3325
+ display: flex;
3326
+ align-items: center;
3327
+ gap: 0.75rem;
3328
+ cursor: pointer;
3329
+ }
3330
+
3331
+ .radio-option input[type="radio"] {
3332
+ width: 20px;
3333
+ height: 20px;
3334
+ cursor: pointer;
3335
+ accent-color: var(--primary-color);
3336
+ margin: 0;
3337
+ box-shadow: none; /* Reset specialized input shadow */
3338
+ }
3339
+
3340
+ .radio-option label {
3341
+ cursor: pointer;
3342
+ font-weight: 500;
3343
+ }
3344
+
3345
+ /* Representatives */
3346
+ .representatives-list {
3347
+ display: flex;
3348
+ flex-direction: column;
3349
+ gap: var(--spacing-md);
3350
+ margin-bottom: var(--spacing-lg);
3351
+ }
3352
+
3353
+ .empty-state {
3354
+ padding: 3rem;
3355
+ text-align: center;
3356
+ background: var(--gray-light);
3357
+ border: 2px dashed var(--border-color);
3358
+ border-radius: var(--border-radius);
3359
+ color: var(--gray-medium);
3360
+ }
3361
+
3362
+ .representative-card {
3363
+ background: var(--color-white, #fff);
3364
+ border: 1px solid var(--border-color);
3365
+ border-radius: var(--border-radius);
3366
+ padding: var(--spacing-lg);
3367
+ box-shadow: var(--shadow-sm);
3368
+ transition: box-shadow 0.2s ease;
3369
+ }
3370
+
3371
+ .representative-card:hover {
3372
+ box-shadow: var(--shadow-md);
3373
+ }
3374
+
3375
+ .card-header {
3376
+ display: flex;
3377
+ justify-content: space-between;
3378
+ align-items: center;
3379
+ margin-bottom: var(--spacing-lg);
3380
+ padding-bottom: var(--spacing-sm);
3381
+ border-bottom: 1px solid var(--border-color);
3382
+ }
3383
+
3384
+ .card-header h3 {
3385
+ font-size: 18px;
3386
+ font-weight: 600;
3387
+ color: var(--color-headline, #0f2a39);
3388
+ }
3389
+
3390
+ .remove-btn {
3391
+ background: transparent;
3392
+ color: var(--error-color);
3393
+ border: 1px solid var(--error-color);
3394
+ padding: 6px 16px;
3395
+ border-radius: var(--border-radius-sm);
3396
+ cursor: pointer;
3397
+ font-size: 13px;
3398
+ font-weight: 600;
3399
+ transition: all 0.2s ease;
3400
+ }
3401
+
3402
+ .remove-btn:hover {
3403
+ background: var(--error-color);
3404
+ color: white;
3405
+ }
3406
+
3407
+ .add-representative-btn {
3408
+ background: transparent;
3409
+ color: var(--primary-color);
3410
+ border: 2px dashed var(--primary-color);
3411
+ padding: 16px;
3412
+ border-radius: var(--border-radius);
3413
+ cursor: pointer;
3414
+ font-size: 15px;
3415
+ font-weight: 600;
3416
+ width: 100%;
3417
+ transition: all 0.2s ease;
3418
+ display: flex;
3419
+ align-items: center;
3420
+ justify-content: center;
3421
+ gap: 0.5rem;
3422
+ }
3423
+
3424
+ .add-representative-btn:hover {
3425
+ background: var(--color-primary-light, #f0fdf4);
3426
+ border-style: solid;
3427
+ }
3428
+
3429
+ /* Staged Representative Styles */
3430
+ .representative-card.staged {
3431
+ border-color: var(--success-color);
3432
+ background: linear-gradient(to bottom, rgba(34, 197, 94, 0.05), var(--color-white, #fff));
3433
+ }
3434
+
3435
+ .staged-badge {
3436
+ display: inline-block;
3437
+ font-size: 12px;
3438
+ font-weight: 600;
3439
+ color: var(--success-color);
3440
+ background: rgba(34, 197, 94, 0.1);
3441
+ padding: 2px 8px;
3442
+ border-radius: 12px;
3443
+ margin-left: 8px;
3444
+ vertical-align: middle;
3445
+ }
3446
+
3447
+ .card-footer {
3448
+ padding-top: var(--spacing-md);
3449
+ margin-top: var(--spacing-md);
3450
+ border-top: 1px solid var(--border-color);
3451
+ display: flex;
3452
+ justify-content: flex-end;
3453
+ }
3454
+
3455
+ .btn-stage-representative {
3456
+ background: var(--primary-color);
3457
+ color: var(--color-white, #fff);
3458
+ border: none;
3459
+ padding: 10px 20px;
3460
+ border-radius: var(--border-radius-sm);
3461
+ cursor: pointer;
3462
+ font-size: 14px;
3463
+ font-weight: 600;
3464
+ transition: all 0.2s ease;
3465
+ }
3466
+
3467
+ .btn-stage-representative:hover {
3468
+ background: var(--primary-hover);
3469
+ transform: translateY(-1px);
3470
+ box-shadow: var(--shadow-sm);
3471
+ }
3472
+
3473
+ .card-footer-actions {
3474
+ display: flex;
3475
+ gap: 12px;
3476
+ align-items: center;
3477
+ }
3478
+
3479
+ .btn-discard-representative {
3480
+ background: transparent;
3481
+ color: var(--gray-medium);
3482
+ border: 1px solid var(--border-color);
3483
+ padding: 10px 20px;
3484
+ border-radius: var(--border-radius-sm);
3485
+ cursor: pointer;
3486
+ font-size: 14px;
3487
+ font-weight: 500;
3488
+ transition: all 0.2s ease;
3489
+ }
3490
+
3491
+ .btn-discard-representative:hover {
3492
+ background: var(--gray-light);
3493
+ border-color: var(--gray-medium);
3494
+ color: var(--error-color);
3495
+ }
3496
+
3497
+ .next-disabled-hint {
3498
+ font-size: 13px;
3499
+ color: var(--gray-medium);
3500
+ margin-top: 8px;
3501
+ text-align: center;
3502
+ width: 100%;
3503
+ }
3504
+
3505
+ .staged-status {
3506
+ display: flex;
3507
+ align-items: center;
3508
+ gap: 8px;
3509
+ color: var(--success-color);
3510
+ font-size: 14px;
3511
+ font-weight: 500;
3512
+ }
3513
+
3514
+ /* Collapsed Representative Card */
3515
+ .representative-card.collapsed {
3516
+ background: var(--gray-light);
3517
+ border: 1px solid var(--success-color);
3518
+ }
3519
+
3520
+ .representative-card.collapsed .collapsed-header {
3521
+ display: flex;
3522
+ align-items: center;
3523
+ justify-content: space-between;
3524
+ padding: 0;
3525
+ }
3526
+
3527
+ .collapsed-info {
3528
+ display: flex;
3529
+ align-items: center;
3530
+ gap: 12px;
3531
+ }
3532
+
3533
+ .confirmed-badge {
3534
+ display: inline-flex;
3535
+ align-items: center;
3536
+ justify-content: center;
3537
+ width: 28px;
3538
+ height: 28px;
3539
+ background: var(--success-color);
3540
+ color: white;
3541
+ border-radius: 50%;
3542
+ font-size: 14px;
3543
+ font-weight: bold;
3544
+ flex-shrink: 0;
3545
+ }
3546
+
3547
+ .collapsed-details h3 {
3548
+ font-size: 15px;
3549
+ font-weight: 600;
3550
+ color: var(--color-headline, #111827);
3551
+ margin: 0;
3552
+ }
3553
+
3554
+ .collapsed-summary {
3555
+ font-size: 13px;
3556
+ color: var(--gray-medium);
3557
+ margin: 4px 0 0 0;
3558
+ }
3559
+
3560
+ .collapsed-actions {
3561
+ display: flex;
3562
+ gap: 8px;
3563
+ }
3564
+
3565
+ .btn-expand-representative {
3566
+ background: var(--color-white, #fff);
3567
+ color: var(--primary-color);
3568
+ border: 1px solid var(--primary-color);
3569
+ padding: 8px 16px;
3570
+ border-radius: var(--border-radius-sm);
3571
+ cursor: pointer;
3572
+ font-size: 13px;
3573
+ font-weight: 500;
3574
+ transition: all 0.2s ease;
3575
+ }
3576
+
3577
+ .btn-expand-representative:hover {
3578
+ background: var(--primary-color);
3579
+ color: var(--color-white, #fff);
3580
+ }
3581
+
3582
+ /* Confirmed badge inline in expanded view */
3583
+ .confirmed-badge-inline {
3584
+ background: var(--success-color);
3585
+ color: white;
3586
+ padding: 2px 8px;
3587
+ border-radius: 12px;
3588
+ font-size: 12px;
3589
+ font-weight: 500;
3590
+ margin-left: 8px;
3591
+ }
3592
+
3593
+ /* Modified badge */
3594
+ .modified-badge {
3595
+ background: #fef3c7;
3596
+ color: #92400e;
3597
+ padding: 2px 8px;
3598
+ border-radius: 12px;
3599
+ font-size: 12px;
3600
+ font-weight: 500;
3601
+ margin-left: 8px;
3602
+ }
3603
+
3604
+ .representative-card.modified {
3605
+ border-color: #f59e0b;
3606
+ }
3607
+
3608
+ /* Undo button */
3609
+ .btn-undo-representative {
3610
+ background: transparent;
3611
+ color: var(--gray-medium);
3612
+ border: 1px solid var(--border-color);
3613
+ padding: 10px 20px;
3614
+ border-radius: var(--border-radius-sm);
3615
+ cursor: pointer;
3616
+ font-size: 14px;
3617
+ font-weight: 500;
3618
+ transition: all 0.2s ease;
3619
+ }
3620
+
3621
+ .btn-undo-representative:hover {
3622
+ background: var(--gray-light);
3623
+ border-color: var(--gray-medium);
3624
+ }
3625
+
3626
+ /* Collapse/Done button */
3627
+ .btn-collapse-representative {
3628
+ background: var(--primary-color);
3629
+ color: var(--color-white, #fff);
3630
+ border: none;
3631
+ padding: 10px 20px;
3632
+ border-radius: var(--border-radius-sm);
3633
+ cursor: pointer;
3634
+ font-size: 14px;
3635
+ font-weight: 600;
3636
+ transition: all 0.2s ease;
3637
+ }
3638
+
3639
+ .btn-collapse-representative:hover {
3640
+ background: var(--primary-hover);
3641
+ transform: translateY(-1px);
3642
+ box-shadow: var(--shadow-sm);
3643
+ }
3644
+
3645
+ /* Validating state */
3646
+ .validating-indicator {
3647
+ display: inline-flex;
3648
+ align-items: center;
3649
+ gap: 8px;
3650
+ color: var(--primary-color);
3651
+ font-size: 14px;
3652
+ font-weight: 500;
3653
+ }
3654
+
3655
+ .validating-indicator::before {
3656
+ content: '';
3657
+ width: 16px;
3658
+ height: 16px;
3659
+ border: 2px solid var(--primary-color);
3660
+ border-top-color: transparent;
3661
+ border-radius: 50%;
3662
+ animation: spin 0.8s linear infinite;
3663
+ }
3664
+
3665
+ @keyframes spin {
3666
+ to { transform: rotate(360deg); }
3667
+ }
3668
+
3669
+ .card-body.validating {
3670
+ opacity: 0.6;
3671
+ pointer-events: none;
3672
+ }
3673
+
3674
+ input:disabled,
3675
+ select:disabled {
3676
+ background: var(--gray-light);
3677
+ cursor: not-allowed;
3678
+ opacity: 0.7;
3679
+ }
3680
+
3681
+ .staged-icon {
3682
+ display: inline-flex;
3683
+ align-items: center;
3684
+ justify-content: center;
3685
+ width: 20px;
3686
+ height: 20px;
3687
+ background: var(--success-color);
3688
+ color: white;
3689
+ border-radius: 50%;
3690
+ font-size: 12px;
3691
+ }
3692
+
3693
+ /* File Upload */
3694
+ .drag-drop-area {
3695
+ border: 2px dashed var(--border-color);
3696
+ border-radius: var(--border-radius);
3697
+ padding: 3rem 2rem;
3698
+ text-align: center;
3699
+ cursor: pointer;
3700
+ transition: all 0.2s ease;
3701
+ min-height: 240px;
3702
+ display: flex;
3703
+ align-items: center;
3704
+ justify-content: center;
3705
+ background: var(--gray-light);
3706
+ }
3707
+
3708
+ .drag-drop-area:hover,
3709
+ .drag-drop-area.drag-over {
3710
+ border-color: var(--primary-color);
3711
+ background: #f0fdf4; /* Light green tint */
3712
+ }
3713
+
3714
+ .btn-browse {
3715
+ margin-top: 1rem;
3716
+ padding: 10px 24px !important;
3717
+ background: var(--color-white, #fff) !important;
3718
+ color: var(--primary-color) !important;
3719
+ border: 1px solid var(--border-color) !important;
3720
+ box-shadow: var(--shadow-sm);
3721
+ transition: all 0.2s ease;
3722
+ }
3723
+
3724
+ .btn-browse:hover {
3725
+ border-color: var(--primary-color) !important;
3726
+ box-shadow: var(--shadow-md);
3727
+ }
3728
+
3729
+ .uploaded-files {
3730
+ margin-top: var(--spacing-md);
3731
+ display: flex;
3732
+ flex-direction: column;
3733
+ gap: 0.5rem;
3734
+ }
3735
+
3736
+ /* Navigation Footer */
3737
+ .navigation-footer {
3738
+ display: flex;
3739
+ justify-content: flex-end;
3740
+ gap: 1.5rem; /* Explicit larger gap */
3741
+ align-items: center;
3742
+ flex-wrap: wrap;
3743
+ }
3744
+
3745
+ .btn-back,
3746
+ .btn-skip {
3747
+ padding: 12px 24px;
3748
+ background: var(--color-white, #fff);
3749
+ color: var(--gray-medium);
3750
+ border: 1px solid var(--border-color);
3751
+ border-radius: var(--border-radius-sm);
3752
+ cursor: pointer;
3753
+ font-size: 15px;
3754
+ font-weight: 500;
3755
+ transition: all 0.2s ease;
3756
+ min-width: 100px;
3757
+ }
3758
+
3759
+ .btn-back:hover,
3760
+ .btn-skip:hover {
3761
+ background: var(--gray-light);
3762
+ border-color: var(--gray-medium);
3763
+ color: var(--color-headline, #111827);
3764
+ }
3765
+
3766
+ .btn-next {
3767
+ padding: 12px 32px;
3768
+ background: var(--primary-color);
3769
+ color: var(--color-white, #fff);
3770
+ border: none;
3771
+ border-radius: var(--border-radius-sm);
3772
+ cursor: pointer;
3773
+ font-size: 15px;
3774
+ font-weight: 600;
3775
+ transition: all 0.2s ease;
3776
+ min-width: 120px;
3777
+ box-shadow: var(--shadow-sm);
3778
+ }
3779
+
3780
+ .btn-next:hover:not(:disabled) {
3781
+ background-color: var(--primary-hover);
3782
+ transform: translateY(-1px);
3783
+ box-shadow: var(--shadow-md);
3784
+ }
3785
+
3786
+ .btn-next:disabled {
3787
+ background: var(--gray-medium);
3788
+ cursor: not-allowed;
3789
+ opacity: 0.6;
3790
+ transform: none;
3791
+ box-shadow: none;
3792
+ }
3793
+
3794
+ /* Success Page - Centered & Polished */
3795
+ .success-container {
3796
+ display: flex;
3797
+ flex-direction: column;
3798
+ align-items: center;
3799
+ justify-content: center;
3800
+ text-align: center;
3801
+ padding: var(--spacing-lg);
3802
+ max-width: 600px;
3803
+ margin: 0 auto;
3804
+ min-height: 500px;
3805
+ }
3806
+
3807
+ .success-icon {
3808
+ width: 80px;
3809
+ height: 80px;
3810
+ margin-bottom: var(--spacing-lg);
3811
+ border-radius: 50%;
3812
+ display: flex;
3813
+ align-items: center;
3814
+ justify-content: center;
3815
+ }
3816
+
3817
+ .checkmark {
3818
+ width: 80px;
3819
+ height: 80px;
3820
+ border-radius: 50%;
3821
+ display: block;
3822
+ stroke-width: 2;
3823
+ stroke: #fff;
3824
+ stroke-miterlimit: 10;
3825
+ box-shadow: inset 0px 0px 0px var(--success-color);
3826
+ animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
3827
+ }
3828
+
3829
+ .checkmark__circle {
3830
+ stroke-dasharray: 166;
3831
+ stroke-dashoffset: 166;
3832
+ stroke-width: 2;
3833
+ stroke-miterlimit: 10;
3834
+ stroke: var(--success-color);
3835
+ fill: none;
3836
+ animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
3837
+ }
3838
+
3839
+ .checkmark__check {
3840
+ transform-origin: 50% 50%;
3841
+ stroke-dasharray: 48;
3842
+ stroke-dashoffset: 48;
3843
+ animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
3844
+ }
3845
+
3846
+ @keyframes stroke {
3847
+ 100% {
3848
+ stroke-dashoffset: 0;
3849
+ }
3850
+ }
3851
+
3852
+ @keyframes scale {
3853
+ 0%, 100% {
3854
+ transform: none;
3855
+ }
3856
+ 50% {
3857
+ transform: scale3d(1.1, 1.1, 1);
3858
+ }
3859
+ }
3860
+
3861
+ @keyframes fill {
3862
+ 100% {
3863
+ box-shadow: inset 0px 0px 0px 50px var(--success-color);
3864
+ }
3865
+ }
3866
+
3867
+ .success-container h2 {
3868
+ font-size: 32px;
3869
+ margin-bottom: 0.75rem;
3870
+ }
3871
+
3872
+ .verification-notice {
3873
+ background: #ecfdf5; /* Green 50 */
3874
+ border: 1px solid #d1fae5;
3875
+ padding: var(--spacing-lg);
3876
+ width: 100%;
3877
+ border-radius: var(--border-radius);
3878
+ margin: var(--spacing-lg) 0;
3879
+ }
3880
+
3881
+ .success-details {
3882
+ width: 100%;
3883
+ background: var(--gray-light);
3884
+ border-radius: var(--border-radius);
3885
+ padding: var(--spacing-lg);
3886
+ border: 1px solid var(--border-color);
3887
+ }
3888
+
3889
+ .success-actions {
3890
+ display: flex;
3891
+ gap: 1rem;
3892
+ margin-top: 2rem;
3893
+ width: 100%;
3894
+ justify-content: center;
3895
+ }
3896
+
3897
+ .btn-confirm-success {
3898
+ padding: 12px 32px;
3899
+ background: var(--primary-color);
3900
+ color: var(--color-white, #fff);
3901
+ border: none;
3902
+ border-radius: var(--border-radius-sm);
3903
+ cursor: pointer;
3904
+ font-size: 15px;
3905
+ font-weight: 600;
3906
+ transition: all 0.2s ease;
3907
+ min-width: 120px;
3908
+ box-shadow: var(--shadow-sm);
3909
+ }
3910
+
3911
+ .btn-confirm-success:hover {
3912
+ background-color: var(--primary-hover);
3913
+ transform: translateY(-1px);
3914
+ box-shadow: var(--shadow-md);
3915
+ }
3916
+
3917
+ /* Error Page */
3918
+ .error-container {
3919
+ display: flex;
3920
+ flex-direction: column;
3921
+ align-items: center;
3922
+ justify-content: center;
3923
+ padding: var(--spacing-lg);
3924
+ max-width: 600px;
3925
+ margin: 0 auto;
3926
+ min-height: 500px;
3927
+ text-align: center;
3928
+ }
3929
+
3930
+ .error-details {
3931
+ width: 100%;
3932
+ text-align: left;
3933
+ }
3934
+
3935
+ /* Responsive Design */
3936
+ @media (max-width: 768px) {
3937
+ .form-grid {
3938
+ grid-template-columns: 1fr;
3939
+ gap: var(--spacing-md);
3940
+ }
3941
+
3942
+ .step-label {
3943
+ display: none; /* Hide labels on mobile for cleaner look */
3944
+ }
3945
+
3946
+ .navigation-footer {
3947
+ justify-content: space-between;
3948
+ }
3949
+
3950
+ .btn-back, .btn-next {
3951
+ flex: 1;
3952
+ }
3953
+
3954
+ .success-actions {
3955
+ flex-direction: column;
3956
+ }
3957
+
3958
+ .btn-confirm-success {
3959
+ max-width: 100%;
3960
+ }
3961
+ }
3962
+ </style>
3963
+ `;
3964
+
3965
+ }
3966
+ }
3967
+
3968
+ // Register the custom element only if it hasn't been registered yet
3969
+ if (!customElements.get("wio-onboarding")) {
3970
+ customElements.define("wio-onboarding", WioOnboarding);
3971
+ }
3972
+
3973
+ // Export for module usage
3974
+ if (typeof module !== "undefined" && module.exports) {
3975
+ module.exports = { WioOnboarding };
3976
+ }
3977
+
3978
+ // Make available globally for script tag usage
3979
+ if (typeof window !== "undefined") {
3980
+ window.WioOnboarding = WioOnboarding;
3981
+ }