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,2404 @@
1
+ /**
2
+ * OperatorPayment Web Component
3
+ *
4
+ * A web component for operator payment management with a consistent UI.
5
+ * Provides a button to open a modal with payment methods management,
6
+ * using Moov for the underlying payment infrastructure.
7
+ *
8
+ * @author @kfajardo
9
+ * @version 2.0.0
10
+ *
11
+ * @requires BisonJibPayAPI - Must be loaded before this component (from component.js)
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <script src="component.js"></script>
16
+ * <script src="operator-payment.js"></script>
17
+ *
18
+ * <operator-payment
19
+ * id="payment"
20
+ * operator-email="operator@example.com"
21
+ * operator-id="OP123456"
22
+ * ></operator-payment>
23
+ * <script>
24
+ * const payment = document.getElementById('payment');
25
+ * payment.addEventListener('payment-linking-success', (e) => {
26
+ * console.log('Account data:', e.detail);
27
+ * });
28
+ * payment.addEventListener('payment-linking-error', (e) => {
29
+ * console.error('Error:', e.detail);
30
+ * });
31
+ * </script>
32
+ * ```
33
+ */
34
+
35
+ class OperatorPayment extends HTMLElement {
36
+ constructor() {
37
+ super();
38
+ this.attachShadow({ mode: "open" });
39
+
40
+ // API Configuration
41
+ this.apiBaseURL =
42
+ this.getAttribute("api-base-url") ||
43
+ "https://bison-jib-development.azurewebsites.net";
44
+ this.embeddableKey =
45
+ this.getAttribute("embeddable-key") ||
46
+ "R80WMkbNN8457RofiMYx03DL65P06IaVT30Q2emYJUBQwYCzRC";
47
+
48
+ // Check if BisonJibPayAPI is available
49
+ if (typeof BisonJibPayAPI === "undefined") {
50
+ console.error(
51
+ "OperatorPayment: BisonJibPayAPI is not available. Please ensure component.js is loaded before operator-payment.js"
52
+ );
53
+ this.api = null;
54
+ } else {
55
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
56
+ }
57
+
58
+ // Internal state
59
+ this._state = {
60
+ operatorEmail: null,
61
+ operatorId: null,
62
+ isOpen: false,
63
+ isLoading: false,
64
+ accountData: null,
65
+ moovAccountId: null,
66
+ moovToken: null,
67
+ error: null,
68
+ initializationError: false,
69
+ // Payment methods from API
70
+ bankAccounts: [],
71
+ isLoadingPaymentMethods: false,
72
+ paymentMethodsError: null,
73
+ // Delete confirmation modal
74
+ deleteConfirmation: {
75
+ isOpen: false,
76
+ accountId: null,
77
+ account: null,
78
+ isDeleting: false,
79
+ },
80
+ };
81
+
82
+ // Moov drop reference
83
+ this._moovRef = null;
84
+
85
+ // Render the component
86
+ this.render();
87
+ }
88
+
89
+ // ==================== STATIC PROPERTIES ====================
90
+
91
+ static get observedAttributes() {
92
+ return ["operator-email", "operator-id", "api-base-url", "embeddable-key"];
93
+ }
94
+
95
+ // ==================== PROPERTY GETTERS/SETTERS ====================
96
+
97
+ /**
98
+ * Get the operator email
99
+ * @returns {string|null}
100
+ */
101
+ get operatorEmail() {
102
+ return this._state.operatorEmail;
103
+ }
104
+
105
+ /**
106
+ * Get the moovAccountId
107
+ * @returns {string|null}
108
+ */
109
+ get moovAccountId() {
110
+ return this._state.moovAccountId;
111
+ }
112
+
113
+ /**
114
+ * Set the operator email
115
+ * @param {string} value - Operator email address
116
+ */
117
+ set operatorEmail(value) {
118
+ console.log("OperatorPayment: Setting operator email to:", value);
119
+
120
+ const oldEmail = this._state.operatorEmail;
121
+
122
+ // Update internal state
123
+ this._state.operatorEmail = value;
124
+
125
+ // Update attribute only if different to prevent circular updates
126
+ const currentAttr = this.getAttribute("operator-email");
127
+ if (currentAttr !== value) {
128
+ if (value) {
129
+ this.setAttribute("operator-email", value);
130
+ } else {
131
+ this.removeAttribute("operator-email");
132
+ }
133
+ }
134
+
135
+ console.log(
136
+ "OperatorPayment: Email state after set:",
137
+ this._state.operatorEmail
138
+ );
139
+
140
+ // Trigger initialization if required fields are set and component is connected
141
+ const missingMessage = this.getMissingOperatorInfoMessage();
142
+ if (!missingMessage && value && value !== oldEmail && this.isConnected) {
143
+ this.initializeAccount();
144
+ } else if (missingMessage && this.isConnected) {
145
+ this._state.isLoading = false;
146
+ this._state.initializationError = true;
147
+ this._state.error = missingMessage;
148
+ this.updateMainButtonState();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Get the operator ID
154
+ * @returns {string|null}
155
+ */
156
+ get operatorId() {
157
+ return this._state.operatorId;
158
+ }
159
+
160
+ /**
161
+ * Set the operator ID
162
+ * @param {string} value - Operator ID
163
+ */
164
+ set operatorId(value) {
165
+ console.log("OperatorPayment: Setting operator ID to:", value);
166
+
167
+ const oldOperatorId = this._state.operatorId;
168
+
169
+ // Update internal state
170
+ this._state.operatorId = value;
171
+
172
+ // Update attribute only if different to prevent circular updates
173
+ const currentAttr = this.getAttribute("operator-id");
174
+ if (currentAttr !== value) {
175
+ if (value) {
176
+ this.setAttribute("operator-id", value);
177
+ } else {
178
+ this.removeAttribute("operator-id");
179
+ }
180
+ }
181
+
182
+ // Trigger initialization if required fields are set and component is connected
183
+ const missingMessage = this.getMissingOperatorInfoMessage();
184
+ if (
185
+ !missingMessage &&
186
+ value &&
187
+ value !== oldOperatorId &&
188
+ this.isConnected
189
+ ) {
190
+ this.initializeAccount();
191
+ } else if (missingMessage && this.isConnected) {
192
+ this._state.isLoading = false;
193
+ this._state.initializationError = true;
194
+ this._state.error = missingMessage;
195
+ this.updateMainButtonState();
196
+ }
197
+ }
198
+
199
+ getMissingOperatorInfoMessage() {
200
+ if (!this._state.operatorEmail && !this._state.operatorId) {
201
+ return "Operator email and operator ID are required";
202
+ }
203
+ if (!this._state.operatorEmail) {
204
+ return "Operator email is required";
205
+ }
206
+ if (!this._state.operatorId) {
207
+ return "Operator ID is required";
208
+ }
209
+ return null;
210
+ }
211
+
212
+ /**
213
+ * Get the open state
214
+ * @returns {boolean}
215
+ */
216
+ get isOpen() {
217
+ return this._state.isOpen;
218
+ }
219
+
220
+ /**
221
+ * Refresh the component by re-checking operator status
222
+ * Useful after onboarding is complete to enable the payment button
223
+ * @returns {Promise<void>}
224
+ */
225
+ async refresh() {
226
+ console.log("OperatorPayment: Refreshing component...");
227
+
228
+ // Reset error state
229
+ this._state.error = null;
230
+ this._state.initializationError = false;
231
+
232
+ // Re-initialize account if required fields are set
233
+ const missingMessage = this.getMissingOperatorInfoMessage();
234
+ if (!missingMessage) {
235
+ await this.initializeAccount();
236
+ } else {
237
+ this._state.isLoading = false;
238
+ this._state.initializationError = true;
239
+ this._state.error = missingMessage;
240
+ this.updateMainButtonState();
241
+ }
242
+ }
243
+
244
+ // ==================== LIFECYCLE METHODS ====================
245
+
246
+ connectedCallback() {
247
+ // Initialize email from attribute if present
248
+ const emailAttr = this.getAttribute("operator-email");
249
+ if (emailAttr && !this._state.operatorEmail) {
250
+ this._state.operatorEmail = emailAttr;
251
+ }
252
+
253
+ // Initialize operator ID from attribute if present
254
+ const operatorIdAttr = this.getAttribute("operator-id");
255
+ if (operatorIdAttr && !this._state.operatorId) {
256
+ this._state.operatorId = operatorIdAttr;
257
+ }
258
+
259
+ // Load Moov SDK (preload for faster access later)
260
+ this.ensureMoovSDK();
261
+
262
+ this.setupEventListeners();
263
+
264
+ // Auto-initialize if required fields are already set
265
+ const missingMessage = this.getMissingOperatorInfoMessage();
266
+ if (!missingMessage) {
267
+ this.initializeAccount();
268
+ } else {
269
+ this._state.isLoading = false;
270
+ this._state.initializationError = true;
271
+ this._state.error = missingMessage;
272
+ this.updateMainButtonState();
273
+ }
274
+ }
275
+
276
+ disconnectedCallback() {
277
+ this.removeEventListeners();
278
+ }
279
+
280
+ attributeChangedCallback(name, oldValue, newValue) {
281
+ if (oldValue === newValue) return;
282
+
283
+ switch (name) {
284
+ case "operator-email":
285
+ console.log(
286
+ "OperatorPayment: attributeChangedCallback - operator-email:",
287
+ newValue
288
+ );
289
+ this._state.operatorEmail = newValue;
290
+ // Clear any existing token when email changes (new email = new account)
291
+ this._state.moovToken = null;
292
+ this._state.moovAccountId = null;
293
+ break;
294
+
295
+ case "operator-id":
296
+ console.log(
297
+ "OperatorPayment: attributeChangedCallback - operator-id:",
298
+ newValue
299
+ );
300
+ this._state.operatorId = newValue;
301
+ // Clear any existing token when operator ID changes
302
+ this._state.moovToken = null;
303
+ this._state.moovAccountId = null;
304
+ break;
305
+
306
+ case "api-base-url":
307
+ this.apiBaseURL = newValue;
308
+ if (this.api) {
309
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
310
+ }
311
+ break;
312
+
313
+ case "embeddable-key":
314
+ this.embeddableKey = newValue;
315
+ if (this.api) {
316
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
317
+ }
318
+ break;
319
+ }
320
+ }
321
+
322
+ // ==================== MOOV SDK LOADING ====================
323
+
324
+ /**
325
+ * Inject Moov Bison theme styles into document body (sibling to moov-payment-methods)
326
+ * This ensures Moov Drops components use the Bison theme
327
+ */
328
+ injectMoovThemeStyles() {
329
+ if (document.getElementById("moov-bison-theme")) {
330
+ return; // Already injected
331
+ }
332
+
333
+ const styleTag = document.createElement("style");
334
+ styleTag.id = "moov-bison-theme";
335
+ styleTag.textContent = `
336
+ :root {
337
+ --moov-color-background: var(--color-white, #fff);
338
+ --moov-color-background-secondary: var(--color-sidebar, #fafafa);
339
+ --moov-color-background-tertiary: var(--color-gray-100, #f3f4f6);
340
+ --moov-color-primary: var(--color-primary, #4c7b63);
341
+ --moov-color-secondary: var(--color-primary-hover, #436c57);
342
+ --moov-color-tertiary: var(--color-border, #e8e8e8);
343
+ --moov-color-info: var(--color-primary, #4c7b63);
344
+ --moov-color-warn: var(--color-warning, #f59e0b);
345
+ --moov-color-danger: var(--color-error, #dd524b);
346
+ --moov-color-success: var(--color-success, #22c55e);
347
+ --moov-color-low-contrast: var(--color-gray-400, #9ca3af);
348
+ --moov-color-medium-contrast: var(--color-gray-600, #4b5563);
349
+ --moov-color-high-contrast: var(--color-headline, #0f2a39);
350
+ --moov-color-graphic-1: var(--color-primary, #4c7b63);
351
+ --moov-color-graphic-2: var(--color-gray-500, #6b7280);
352
+ --moov-color-graphic-3: var(--color-warning, #f59e0b);
353
+ --moov-radius-small: var(--radius-lg, 0.5rem);
354
+ --moov-radius-large: var(--radius-xl, 0.75rem);
355
+ }
356
+ `;
357
+ // Append to body as sibling to moov-payment-methods
358
+ document.body.appendChild(styleTag);
359
+ console.log("OperatorPayment: Bison theme styles injected");
360
+ }
361
+
362
+ /**
363
+ * Ensure Moov SDK is loaded
364
+ *
365
+ * This method dynamically loads the Moov SDK from the CDN if not already present.
366
+ * This eliminates the need for consumers to manually include the script tag.
367
+ *
368
+ * @returns {Promise<void>} Resolves when SDK is ready
369
+ */
370
+ async ensureMoovSDK() {
371
+ // Check if Moov is already loaded
372
+ if (window.Moov) {
373
+ console.log("OperatorPayment: Moov SDK already loaded");
374
+ return Promise.resolve();
375
+ }
376
+
377
+ // Check if script is already being loaded
378
+ const existingScript = document.querySelector('script[src*="moov.js"]');
379
+ if (existingScript) {
380
+ console.log(
381
+ "OperatorPayment: Moov SDK script found, waiting for load..."
382
+ );
383
+ return new Promise((resolve, reject) => {
384
+ existingScript.addEventListener("load", () => {
385
+ console.log("OperatorPayment: Moov SDK loaded from existing script");
386
+ resolve();
387
+ });
388
+ existingScript.addEventListener("error", () =>
389
+ reject(new Error("Failed to load Moov SDK"))
390
+ );
391
+ });
392
+ }
393
+
394
+ // Load the SDK
395
+ console.log("OperatorPayment: Loading Moov SDK from CDN...");
396
+ return new Promise((resolve, reject) => {
397
+ const script = document.createElement("script");
398
+ script.src = "https://js.moov.io/v1";
399
+ // script.crossOrigin = "anonymous";
400
+ script.async = true;
401
+
402
+ script.onload = () => {
403
+ console.log("OperatorPayment: Moov SDK loaded successfully");
404
+ resolve();
405
+ };
406
+
407
+ script.onerror = () => {
408
+ const error = new Error("Failed to load Moov SDK from CDN");
409
+ console.error("OperatorPayment:", error);
410
+ this._state.error = error.message;
411
+ this.dispatchEvent(
412
+ new CustomEvent("payment-linking-error", {
413
+ detail: {
414
+ error: error.message,
415
+ type: "sdk",
416
+ },
417
+ bubbles: true,
418
+ composed: true,
419
+ })
420
+ );
421
+ reject(error);
422
+ };
423
+
424
+ // Append to document head
425
+ document.head.appendChild(script);
426
+ });
427
+ }
428
+
429
+ // ==================== EVENT HANDLING ====================
430
+
431
+ setupEventListeners() {
432
+ const button = this.shadowRoot.querySelector(".link-payment-btn");
433
+ const closeBtn = this.shadowRoot.querySelector(".close-btn");
434
+ const overlay = this.shadowRoot.querySelector(".modal-overlay");
435
+ const addBankBtn = this.shadowRoot.querySelector(".add-bank-btn");
436
+
437
+ if (button) {
438
+ button.addEventListener("click", this.handleButtonClick.bind(this));
439
+ }
440
+
441
+ if (closeBtn) {
442
+ closeBtn.addEventListener("click", this.closeModal.bind(this));
443
+ }
444
+
445
+ if (overlay) {
446
+ overlay.addEventListener("click", this.closeModal.bind(this));
447
+ }
448
+
449
+ if (addBankBtn) {
450
+ addBankBtn.addEventListener("click", this.openMoovDrop.bind(this));
451
+ }
452
+
453
+ // Setup delete button event listeners
454
+ this.setupMenuListeners();
455
+
456
+ // ESC key to close modal
457
+ this._escHandler = (e) => {
458
+ if (e.key === "Escape" && this._state.isOpen) {
459
+ this.closeModal();
460
+ }
461
+ };
462
+ document.addEventListener("keydown", this._escHandler);
463
+ }
464
+
465
+ /**
466
+ * Setup event listeners for delete buttons
467
+ */
468
+ setupMenuListeners() {
469
+ const deleteBtns = this.shadowRoot.querySelectorAll(".delete-btn");
470
+
471
+ deleteBtns.forEach((btn) => {
472
+ btn.addEventListener("click", (e) => {
473
+ e.stopPropagation();
474
+ const accountId = btn.dataset.accountId;
475
+ this.handleDeleteAccount(accountId);
476
+ });
477
+ });
478
+ }
479
+
480
+ /**
481
+ * Handle delete account action - shows confirmation modal
482
+ * @param {string} accountId - Account ID to delete
483
+ */
484
+ handleDeleteAccount(accountId) {
485
+ console.log("OperatorPayment: Delete account requested for:", accountId);
486
+
487
+ // Find the account to get details for the confirmation modal
488
+ const account = this._state.bankAccounts.find((a) => a.id === accountId);
489
+
490
+ // Show confirmation modal
491
+ this._state.deleteConfirmation = {
492
+ isOpen: true,
493
+ accountId,
494
+ account,
495
+ isDeleting: false,
496
+ };
497
+ this.updateDeleteConfirmationModal();
498
+ }
499
+
500
+ /**
501
+ * Cancel delete operation
502
+ */
503
+ cancelDelete() {
504
+ this._state.deleteConfirmation = {
505
+ isOpen: false,
506
+ accountId: null,
507
+ account: null,
508
+ isDeleting: false,
509
+ };
510
+ this.updateDeleteConfirmationModal();
511
+ }
512
+
513
+ /**
514
+ * Confirm and execute delete operation
515
+ */
516
+ async confirmDelete() {
517
+ const { accountId, account } = this._state.deleteConfirmation;
518
+
519
+ if (!accountId) {
520
+ console.warn("OperatorPayment: No account ID for deletion");
521
+ return;
522
+ }
523
+
524
+ // Set deleting state
525
+ this._state.deleteConfirmation.isDeleting = true;
526
+ this.updateDeleteConfirmationModal();
527
+
528
+ // Dispatch delete event for consumer to handle
529
+ this.dispatchEvent(
530
+ new CustomEvent("payment-method-delete", {
531
+ detail: {
532
+ accountId,
533
+ account,
534
+ },
535
+ bubbles: true,
536
+ composed: true,
537
+ })
538
+ );
539
+
540
+ // Call the API to delete the payment method
541
+ // Use cached moovAccountId to avoid extra API call
542
+ if (this.api && this._state.moovAccountId) {
543
+ try {
544
+ console.log("OperatorPayment: Deleting payment method via API...");
545
+ await this.api.deletePaymentMethodByAccountId(
546
+ this._state.moovAccountId,
547
+ accountId
548
+ );
549
+ console.log("OperatorPayment: Payment method deleted successfully");
550
+
551
+ // Remove from local state after successful API call
552
+ this._state.bankAccounts = this._state.bankAccounts.filter(
553
+ (a) => a.id !== accountId
554
+ );
555
+
556
+ // Close confirmation modal
557
+ this._state.deleteConfirmation = {
558
+ isOpen: false,
559
+ accountId: null,
560
+ account: null,
561
+ isDeleting: false,
562
+ };
563
+
564
+ this.updateBankAccountsList();
565
+ this.updateDeleteConfirmationModal();
566
+
567
+ // Dispatch success event
568
+ this.dispatchEvent(
569
+ new CustomEvent("payment-method-deleted", {
570
+ detail: {
571
+ accountId,
572
+ account,
573
+ },
574
+ bubbles: true,
575
+ composed: true,
576
+ })
577
+ );
578
+ } catch (error) {
579
+ console.error(
580
+ "OperatorPayment: Failed to delete payment method",
581
+ error
582
+ );
583
+
584
+ // Reset deleting state but keep modal open
585
+ this._state.deleteConfirmation.isDeleting = false;
586
+ this.updateDeleteConfirmationModal();
587
+
588
+ // Dispatch error event
589
+ this.dispatchEvent(
590
+ new CustomEvent("payment-method-delete-error", {
591
+ detail: {
592
+ accountId,
593
+ account,
594
+ error:
595
+ error.data?.message ||
596
+ error.message ||
597
+ "Failed to delete payment method",
598
+ },
599
+ bubbles: true,
600
+ composed: true,
601
+ })
602
+ );
603
+ }
604
+ } else if (this.api && this._state.operatorEmail) {
605
+ // Fallback to email-based method if moovAccountId is not cached
606
+ try {
607
+ console.log(
608
+ "OperatorPayment: Deleting payment method via API (using email)..."
609
+ );
610
+ await this.api.deletePaymentMethodById(
611
+ this._state.operatorEmail,
612
+ accountId
613
+ );
614
+ console.log("OperatorPayment: Payment method deleted successfully");
615
+
616
+ // Remove from local state after successful API call
617
+ this._state.bankAccounts = this._state.bankAccounts.filter(
618
+ (a) => a.id !== accountId
619
+ );
620
+
621
+ // Close confirmation modal
622
+ this._state.deleteConfirmation = {
623
+ isOpen: false,
624
+ accountId: null,
625
+ account: null,
626
+ isDeleting: false,
627
+ };
628
+
629
+ this.updateBankAccountsList();
630
+ this.updateDeleteConfirmationModal();
631
+
632
+ // Dispatch success event
633
+ this.dispatchEvent(
634
+ new CustomEvent("payment-method-deleted", {
635
+ detail: {
636
+ accountId,
637
+ account,
638
+ },
639
+ bubbles: true,
640
+ composed: true,
641
+ })
642
+ );
643
+ } catch (error) {
644
+ console.error(
645
+ "OperatorPayment: Failed to delete payment method",
646
+ error
647
+ );
648
+
649
+ // Reset deleting state but keep modal open
650
+ this._state.deleteConfirmation.isDeleting = false;
651
+ this.updateDeleteConfirmationModal();
652
+
653
+ // Dispatch error event
654
+ this.dispatchEvent(
655
+ new CustomEvent("payment-method-delete-error", {
656
+ detail: {
657
+ accountId,
658
+ account,
659
+ error:
660
+ error.data?.message ||
661
+ error.message ||
662
+ "Failed to delete payment method",
663
+ },
664
+ bubbles: true,
665
+ composed: true,
666
+ })
667
+ );
668
+ }
669
+ } else {
670
+ // Fallback: remove from local state if no API
671
+ this._state.bankAccounts = this._state.bankAccounts.filter(
672
+ (a) => a.id !== accountId
673
+ );
674
+
675
+ // Close confirmation modal
676
+ this._state.deleteConfirmation = {
677
+ isOpen: false,
678
+ accountId: null,
679
+ account: null,
680
+ isDeleting: false,
681
+ };
682
+
683
+ this.updateBankAccountsList();
684
+ this.updateDeleteConfirmationModal();
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Update the delete confirmation modal in the DOM
690
+ */
691
+ updateDeleteConfirmationModal() {
692
+ const container = this.shadowRoot.querySelector("#deleteConfirmationModal");
693
+ if (container) {
694
+ container.innerHTML = this.renderDeleteConfirmationModal();
695
+ this.setupDeleteConfirmationListeners();
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Setup event listeners for delete confirmation modal
701
+ */
702
+ setupDeleteConfirmationListeners() {
703
+ const cancelBtn = this.shadowRoot.querySelector(".delete-cancel-btn");
704
+ const confirmBtn = this.shadowRoot.querySelector(".delete-confirm-btn");
705
+ const overlay = this.shadowRoot.querySelector(
706
+ ".delete-confirmation-overlay"
707
+ );
708
+
709
+ if (cancelBtn) {
710
+ cancelBtn.addEventListener("click", () => this.cancelDelete());
711
+ }
712
+
713
+ if (confirmBtn) {
714
+ confirmBtn.addEventListener("click", () => this.confirmDelete());
715
+ }
716
+
717
+ if (overlay) {
718
+ overlay.addEventListener("click", () => this.cancelDelete());
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Render the delete confirmation modal
724
+ * @returns {string} HTML string for the modal
725
+ */
726
+ renderDeleteConfirmationModal() {
727
+ const { isOpen, account, isDeleting } = this._state.deleteConfirmation;
728
+
729
+ if (!isOpen) {
730
+ return "";
731
+ }
732
+
733
+ const bankName = account?.bankName || "this payment method";
734
+ const lastFour = account?.lastFourAccountNumber || "****";
735
+
736
+ return `
737
+ <div class="delete-confirmation-overlay"></div>
738
+ <div class="delete-confirmation-dialog">
739
+ <div class="delete-confirmation-icon">
740
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
741
+ <circle cx="12" cy="12" r="10"></circle>
742
+ <line x1="12" y1="8" x2="12" y2="12"></line>
743
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
744
+ </svg>
745
+ </div>
746
+ <h3 class="delete-confirmation-title">Delete Payment Method?</h3>
747
+ <p class="delete-confirmation-message">
748
+ Are you sure you want to delete <strong>${bankName}</strong> ending in <strong>••••${lastFour}</strong>? This action cannot be undone.
749
+ </p>
750
+ <div class="delete-confirmation-actions">
751
+ <button class="delete-cancel-btn" ${isDeleting ? "disabled" : ""
752
+ }>Cancel</button>
753
+ <button class="delete-confirm-btn" ${isDeleting ? "disabled" : ""}>
754
+ ${isDeleting
755
+ ? '<span class="delete-spinner"></span> Deleting...'
756
+ : "Delete"
757
+ }
758
+ </button>
759
+ </div>
760
+ </div>
761
+ `;
762
+ }
763
+
764
+ /**
765
+ * Update the bank accounts list in the DOM
766
+ */
767
+ updateBankAccountsList() {
768
+ const container = this.shadowRoot.querySelector("#bankAccountsList");
769
+ if (container) {
770
+ container.innerHTML = this.renderBankAccounts();
771
+ this.setupMenuListeners();
772
+ }
773
+ }
774
+
775
+ removeEventListeners() {
776
+ if (this._escHandler) {
777
+ document.removeEventListener("keydown", this._escHandler);
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Handle button click - just open modal
783
+ * Token generation is deferred to when "Add Bank Account" is clicked
784
+ */
785
+ handleButtonClick() {
786
+ console.log(
787
+ "OperatorPayment: Button clicked, current email state:",
788
+ this._state.operatorEmail
789
+ );
790
+
791
+ // Validate required fields are set before opening modal
792
+ const missingMessage = this.getMissingOperatorInfoMessage();
793
+ if (missingMessage) {
794
+ console.warn(
795
+ "OperatorPayment: Cannot open modal - required fields are missing"
796
+ );
797
+ this.dispatchEvent(
798
+ new CustomEvent("payment-linking-error", {
799
+ detail: { error: missingMessage, type: "validation" },
800
+ bubbles: true,
801
+ composed: true,
802
+ })
803
+ );
804
+ return;
805
+ }
806
+
807
+ // Validate API is available
808
+ if (!this.api) {
809
+ console.warn("OperatorPayment: Cannot open modal - API is not available");
810
+ this.dispatchEvent(
811
+ new CustomEvent("payment-linking-error", {
812
+ detail: {
813
+ error: "BisonJibPayAPI is not available",
814
+ type: "initialization",
815
+ },
816
+ bubbles: true,
817
+ composed: true,
818
+ })
819
+ );
820
+ return;
821
+ }
822
+
823
+ // Open modal
824
+ this.openModal();
825
+ }
826
+
827
+ /**
828
+ * Open the modal
829
+ */
830
+ openModal() {
831
+ this._state.isOpen = true;
832
+ const modal = this.shadowRoot.querySelector(".modal");
833
+ if (modal) {
834
+ // Show modal and start animation
835
+ modal.classList.add("show", "animating-in");
836
+
837
+ // Remove animating-in class after animation completes
838
+ setTimeout(() => {
839
+ modal.classList.remove("animating-in");
840
+ }, 200);
841
+ }
842
+
843
+ // Prevent background scrolling when modal is open
844
+ document.body.style.overflow = "hidden";
845
+
846
+ // Fetch payment methods when modal opens
847
+ this.fetchPaymentMethods();
848
+ }
849
+
850
+ /**
851
+ * Fetch payment methods from API
852
+ * Uses cached moovAccountId when available to avoid extra API calls
853
+ */
854
+ async fetchPaymentMethods() {
855
+ const missingMessage = this.getMissingOperatorInfoMessage();
856
+ if (missingMessage) {
857
+ console.warn(
858
+ "OperatorPayment: Required fields missing to fetch payment methods"
859
+ );
860
+ this._state.isLoadingPaymentMethods = false;
861
+ this._state.paymentMethodsError = missingMessage;
862
+ this.updateBankAccountsList();
863
+ return;
864
+ }
865
+
866
+ if (!this.api) {
867
+ console.error("OperatorPayment: API not available");
868
+ this._state.paymentMethodsError = "API not available";
869
+ this.updateBankAccountsList();
870
+ return;
871
+ }
872
+
873
+ try {
874
+ this._state.isLoadingPaymentMethods = true;
875
+ this._state.paymentMethodsError = null;
876
+ this.updateBankAccountsList();
877
+
878
+ console.log(
879
+ "OperatorPayment: Fetching payment methods for",
880
+ this._state.operatorEmail
881
+ );
882
+
883
+ // Use the cached moovAccountId (should be set from initializeAccount)
884
+ if (!this._state.moovAccountId) {
885
+ console.warn(
886
+ "OperatorPayment: moovAccountId not cached, account may not be initialized"
887
+ );
888
+ // Fallback: fetch it now if not available
889
+ try {
890
+ const accountResult = await this.api.getAccountByEmail(
891
+ this._state.operatorEmail
892
+ );
893
+ this._state.moovAccountId =
894
+ accountResult.data?.moovAccountId || accountResult.moovAccountId;
895
+ console.log(
896
+ "OperatorPayment: Fetched and cached moovAccountId:",
897
+ this._state.moovAccountId
898
+ );
899
+ } catch (error) {
900
+ console.error(
901
+ "OperatorPayment: Failed to fetch moovAccountId",
902
+ error
903
+ );
904
+ throw error;
905
+ }
906
+ }
907
+
908
+ // Use the cached moovAccountId directly
909
+ console.log(
910
+ "OperatorPayment: Using cached moovAccountId:",
911
+ this._state.moovAccountId
912
+ );
913
+ const response = await this.api.getPaymentMethodsByAccountId(
914
+ this._state.moovAccountId
915
+ );
916
+
917
+ if (response.success && response.data) {
918
+ // Transform API response to match the expected format
919
+ const allMethods = response.data.map((method) => ({
920
+ // Use the correct ID for deletion based on payment method type
921
+ id: this.getPaymentMethodId(method),
922
+ paymentMethodType: method.paymentMethodType,
923
+ bankName: this.getBankName(method),
924
+ holderName: this.getHolderName(method),
925
+ bankAccountType: method.bankAccount?.bankAccountType || "checking",
926
+ lastFourAccountNumber: this.getLastFour(method),
927
+ status: this.getPaymentMethodStatus(method),
928
+ // Keep original data for reference
929
+ _original: method,
930
+ }));
931
+
932
+ // Deduplicate by ID, prioritizing ach-credit-same-day, then ach-credit-standard
933
+ this._state.bankAccounts = this.deduplicatePaymentMethods(allMethods);
934
+
935
+ console.log(
936
+ "OperatorPayment: Payment methods fetched successfully",
937
+ this._state.bankAccounts
938
+ );
939
+ } else {
940
+ this._state.bankAccounts = [];
941
+ }
942
+
943
+ this._state.isLoadingPaymentMethods = false;
944
+ this.updateBankAccountsList();
945
+ } catch (error) {
946
+ console.error("OperatorPayment: Failed to fetch payment methods", error);
947
+ this._state.isLoadingPaymentMethods = false;
948
+ this._state.paymentMethodsError =
949
+ error.data?.message ||
950
+ error.message ||
951
+ "Failed to fetch payment methods";
952
+ this.updateBankAccountsList();
953
+ }
954
+ }
955
+
956
+ /**
957
+ * Deduplicate payment methods by ID, prioritizing certain payment method types
958
+ * Priority: ach-credit-same-day > ach-credit-standard > others
959
+ * @param {Array} methods - Array of payment method objects
960
+ * @returns {Array} Deduplicated array with highest priority method for each ID
961
+ */
962
+ deduplicatePaymentMethods(methods) {
963
+ const priorityOrder = {
964
+ "ach-credit-same-day": 1,
965
+ "ach-credit-standard": 2,
966
+ };
967
+
968
+ const methodMap = new Map();
969
+
970
+ methods.forEach((method) => {
971
+ const existingMethod = methodMap.get(method.id);
972
+
973
+ if (!existingMethod) {
974
+ // First occurrence of this ID
975
+ methodMap.set(method.id, method);
976
+ } else {
977
+ // Compare priorities - lower number = higher priority
978
+ const existingPriority =
979
+ priorityOrder[existingMethod.paymentMethodType] || 999;
980
+ const newPriority = priorityOrder[method.paymentMethodType] || 999;
981
+
982
+ if (newPriority < existingPriority) {
983
+ // New method has higher priority, replace
984
+ methodMap.set(method.id, method);
985
+ }
986
+ }
987
+ });
988
+
989
+ return Array.from(methodMap.values());
990
+ }
991
+
992
+ /**
993
+ * Get bank name from payment method
994
+ * @param {Object} method - Payment method data
995
+ * @returns {string} Bank name
996
+ */
997
+ getBankName(method) {
998
+ if (method.bankAccount) {
999
+ return method.bankAccount.bankName || "Bank Account";
1000
+ }
1001
+ if (method.card) {
1002
+ return method.card.brand || method.card.cardType || "Card";
1003
+ }
1004
+ if (method.wallet || method.paymentMethodType === "moovWallet") {
1005
+ return "Moov Wallet";
1006
+ }
1007
+ if (method.applePay) {
1008
+ return "Apple Pay";
1009
+ }
1010
+ return "Payment Method";
1011
+ }
1012
+
1013
+ /**
1014
+ * Get holder name from payment method
1015
+ * @param {Object} method - Payment method data
1016
+ * @returns {string} Holder name
1017
+ */
1018
+ getHolderName(method) {
1019
+ if (method.bankAccount) {
1020
+ return method.bankAccount.holderName || "Account Holder";
1021
+ }
1022
+ if (method.card) {
1023
+ return method.card.holderName || "Card Holder";
1024
+ }
1025
+ return "Account Holder";
1026
+ }
1027
+
1028
+ /**
1029
+ * Get last four digits from payment method
1030
+ * @param {Object} method - Payment method data
1031
+ * @returns {string} Last four digits
1032
+ */
1033
+ getLastFour(method) {
1034
+ if (method.bankAccount) {
1035
+ return method.bankAccount.lastFourAccountNumber || "****";
1036
+ }
1037
+ if (method.card) {
1038
+ return method.card.lastFourCardNumber || "****";
1039
+ }
1040
+ return "****";
1041
+ }
1042
+
1043
+ /**
1044
+ * Get payment method status
1045
+ * @param {Object} method - Payment method data
1046
+ * @returns {string} Status
1047
+ */
1048
+ getPaymentMethodStatus(method) {
1049
+ if (method.bankAccount) {
1050
+ return method.bankAccount.status || "verified";
1051
+ }
1052
+ return "verified";
1053
+ }
1054
+
1055
+ /**
1056
+ * Get the correct ID for a payment method based on its type
1057
+ * Used for deletion - bank accounts use bankAccountID, wallets use walletID
1058
+ * @param {Object} method - Payment method data
1059
+ * @returns {string} The ID to use for deletion
1060
+ */
1061
+ getPaymentMethodId(method) {
1062
+ if (method.bankAccount) {
1063
+ return method.bankAccount.bankAccountID;
1064
+ }
1065
+ if (method.wallet) {
1066
+ return method.wallet.walletID;
1067
+ }
1068
+ if (method.card) {
1069
+ return method.card.cardID;
1070
+ }
1071
+ // Fallback to paymentMethodID if no specific ID is found
1072
+ return method.paymentMethodID;
1073
+ }
1074
+
1075
+ /**
1076
+ * Close the modal
1077
+ */
1078
+ closeModal() {
1079
+ this._state.isOpen = false;
1080
+ this._state.isLoading = false;
1081
+ const modal = this.shadowRoot.querySelector(".modal");
1082
+ if (modal) {
1083
+ // Start close animation
1084
+ modal.classList.add("animating-out");
1085
+
1086
+ // Hide modal after animation completes
1087
+ setTimeout(() => {
1088
+ modal.classList.remove("show", "animating-out");
1089
+ }, 150);
1090
+ }
1091
+
1092
+ // Restore background scrolling when modal is closed
1093
+ document.body.style.overflow = "";
1094
+
1095
+ // Dispatch close event
1096
+ this.dispatchEvent(
1097
+ new CustomEvent("payment-linking-close", {
1098
+ detail: {
1099
+ moovAccountId: this._state.moovAccountId,
1100
+ accountData: this._state.accountData,
1101
+ },
1102
+ bubbles: true,
1103
+ composed: true,
1104
+ })
1105
+ );
1106
+ }
1107
+
1108
+ // ==================== INITIALIZATION METHODS ====================
1109
+
1110
+ /**
1111
+ * Initialize account - fetches account data and caches moovAccountId
1112
+ * Token generation is deferred to when "Add Bank Account" is clicked
1113
+ */
1114
+ async initializeAccount() {
1115
+ // Validate required fields
1116
+ const missingMessage = this.getMissingOperatorInfoMessage();
1117
+ if (missingMessage) {
1118
+ console.warn("OperatorPayment: Required fields missing for initialization");
1119
+ this._state.isLoading = false;
1120
+ this._state.initializationError = true;
1121
+ this._state.error = missingMessage;
1122
+ this.updateMainButtonState();
1123
+ return;
1124
+ }
1125
+
1126
+ // Validate API availability
1127
+ if (!this.api) {
1128
+ console.error(
1129
+ "OperatorPayment: BisonJibPayAPI is not available. Please ensure component.js is loaded first."
1130
+ );
1131
+ this._state.initializationError = true;
1132
+ this.updateMainButtonState();
1133
+ return;
1134
+ }
1135
+
1136
+ try {
1137
+ this._state.isLoading = true;
1138
+ this._state.error = null;
1139
+ this._state.initializationError = false;
1140
+ this.updateMainButtonState();
1141
+
1142
+ console.log(
1143
+ "OperatorPayment: Initializing account for",
1144
+ this._state.operatorEmail
1145
+ );
1146
+
1147
+ // First, verify the operator is registered/onboarded
1148
+ console.log("OperatorPayment: Verifying operator...");
1149
+ const verifyResult = await this.api.verifyOperator(
1150
+ this._state.operatorEmail,
1151
+ this._state.operatorId
1152
+ );
1153
+
1154
+ if (!verifyResult.success) {
1155
+ throw new Error(
1156
+ verifyResult.message || "Operator is not registered in the system"
1157
+ );
1158
+ }
1159
+
1160
+ console.log(
1161
+ "OperatorPayment: Operator verified successfully:",
1162
+ verifyResult.message
1163
+ );
1164
+
1165
+ // Fetch account by email to get and cache moovAccountId
1166
+ const result = await this.api.getAccountByEmail(
1167
+ this._state.operatorEmail
1168
+ );
1169
+ this._state.accountData = result.data;
1170
+ this._state.moovAccountId = result.data?.moovAccountId || null;
1171
+
1172
+ console.log(
1173
+ "OperatorPayment: Account fetched successfully, moovAccountId:",
1174
+ this._state.moovAccountId
1175
+ );
1176
+
1177
+ // Pre-fetch Moov token if we have a moovAccountId (optimization for Add Bank Account)
1178
+ if (this._state.moovAccountId) {
1179
+ console.log("OperatorPayment: Pre-fetching Moov token...");
1180
+ await this.initializeMoovToken();
1181
+ console.log(
1182
+ "OperatorPayment: Token generated and cached during initialization"
1183
+ );
1184
+ }
1185
+
1186
+ this._state.isLoading = false;
1187
+ this.updateMainButtonState();
1188
+
1189
+ // Dispatch success event
1190
+ this.dispatchEvent(
1191
+ new CustomEvent("payment-linking-success", {
1192
+ detail: {
1193
+ ...result.data,
1194
+ moovAccountId: this._state.moovAccountId,
1195
+ },
1196
+ bubbles: true,
1197
+ composed: true,
1198
+ })
1199
+ );
1200
+
1201
+ // Dispatch ready event
1202
+ this.dispatchEvent(
1203
+ new CustomEvent("payment-linking-ready", {
1204
+ detail: {
1205
+ operatorEmail: this._state.operatorEmail,
1206
+ moovAccountId: this._state.moovAccountId,
1207
+ },
1208
+ bubbles: true,
1209
+ composed: true,
1210
+ })
1211
+ );
1212
+ } catch (error) {
1213
+ this._state.isLoading = false;
1214
+ this._state.error = error.message || "Failed to fetch account data";
1215
+ this._state.initializationError = true;
1216
+
1217
+ console.error("OperatorPayment: Account initialization failed", error);
1218
+ this.updateMainButtonState();
1219
+
1220
+ // Dispatch error event
1221
+ this.dispatchEvent(
1222
+ new CustomEvent("payment-linking-error", {
1223
+ detail: {
1224
+ error: this._state.error,
1225
+ type: "initialization",
1226
+ originalError: error,
1227
+ },
1228
+ bubbles: true,
1229
+ composed: true,
1230
+ })
1231
+ );
1232
+ }
1233
+ }
1234
+
1235
+ /**
1236
+ * Initialize Moov token - generates access token for Moov API
1237
+ * Called lazily when "Add Bank Account" is clicked
1238
+ * @returns {Promise<boolean>} True if token was generated successfully
1239
+ */
1240
+ async initializeMoovToken() {
1241
+ // If we already have a token, return true
1242
+ if (this._state.moovToken) {
1243
+ console.log("OperatorPayment: Using existing Moov token");
1244
+ return true;
1245
+ }
1246
+
1247
+ try {
1248
+ console.log("OperatorPayment: Generating Moov token...");
1249
+ // Pass the cached moovAccountId to avoid redundant getAccountByEmail call
1250
+ const tokenResult = await this.api.generateMoovToken(
1251
+ this._state.operatorEmail,
1252
+ this._state.moovAccountId
1253
+ );
1254
+
1255
+ if (!tokenResult || !tokenResult.data?.accessToken) {
1256
+ throw new Error("Failed to generate Moov token");
1257
+ }
1258
+
1259
+ this._state.moovToken = tokenResult.data.accessToken;
1260
+ // Only update moovAccountId if returned, otherwise keep existing cached value
1261
+ if (tokenResult.data?.accountID) {
1262
+ this._state.moovAccountId = tokenResult.data.accountID;
1263
+ }
1264
+ console.log(
1265
+ "OperatorPayment: Moov token generated and cached successfully"
1266
+ );
1267
+
1268
+ // Dispatch success event
1269
+ this.dispatchEvent(
1270
+ new CustomEvent("payment-linking-success", {
1271
+ detail: {
1272
+ moovAccountId: this._state.moovAccountId,
1273
+ hasToken: true,
1274
+ },
1275
+ bubbles: true,
1276
+ composed: true,
1277
+ })
1278
+ );
1279
+
1280
+ return true;
1281
+ } catch (error) {
1282
+ this._state.error = error.message || "Failed to generate Moov token";
1283
+
1284
+ console.error("OperatorPayment: Moov token generation failed", error);
1285
+
1286
+ // Dispatch error event
1287
+ this.dispatchEvent(
1288
+ new CustomEvent("payment-linking-error", {
1289
+ detail: {
1290
+ error: this._state.error,
1291
+ type: "token",
1292
+ originalError: error,
1293
+ },
1294
+ bubbles: true,
1295
+ composed: true,
1296
+ })
1297
+ );
1298
+
1299
+ return false;
1300
+ }
1301
+ }
1302
+
1303
+ /**
1304
+ * Open Moov Drop - triggered by Add Bank Account button click
1305
+ */
1306
+ async openMoovDrop() {
1307
+ console.log("OperatorPayment: Add Bank Account button clicked");
1308
+ console.log(
1309
+ "OperatorPayment: Current moovToken:",
1310
+ this._state.moovToken ? "exists (cached)" : "null (will generate)"
1311
+ );
1312
+
1313
+ // Ensure Moov SDK is loaded
1314
+ if (!window.Moov) {
1315
+ console.log("OperatorPayment: Moov SDK not loaded yet, waiting...");
1316
+ try {
1317
+ await this.ensureMoovSDK();
1318
+ } catch (error) {
1319
+ console.error("OperatorPayment: Failed to load Moov SDK:", error);
1320
+ return;
1321
+ }
1322
+ }
1323
+
1324
+ // Lazy initialization: Generate token if not already available
1325
+ if (!this._state.moovToken) {
1326
+ console.log("OperatorPayment: Generating Moov token on demand...");
1327
+ const success = await this.initializeMoovToken();
1328
+ if (!success) {
1329
+ console.error("OperatorPayment: Failed to generate Moov token");
1330
+ return;
1331
+ }
1332
+ }
1333
+
1334
+ console.log("OperatorPayment: Opening Moov payment methods drop...");
1335
+
1336
+ // Inject Bison theme styles as sibling to moov-payment-methods
1337
+ this.injectMoovThemeStyles();
1338
+
1339
+ // Remove any existing moov-payment-methods element to ensure fresh state
1340
+ const existingMoovDrop = document.getElementById("operator-payment-moov-drop");
1341
+ if (existingMoovDrop) {
1342
+ existingMoovDrop.remove();
1343
+ console.log("OperatorPayment: Removed existing Moov drop element for fresh state");
1344
+ }
1345
+
1346
+ // Create a fresh moov-payment-methods element in the document body (light DOM)
1347
+ // Moov components need to be in light DOM to properly render their modals
1348
+ const moovDrop = document.createElement("moov-payment-methods");
1349
+ moovDrop.id = "operator-payment-moov-drop";
1350
+ document.body.appendChild(moovDrop);
1351
+
1352
+ // Configure the Moov drop
1353
+ moovDrop.token = this._state.moovToken;
1354
+ moovDrop.accountID = this._state.moovAccountId;
1355
+ moovDrop.microDeposits = false;
1356
+
1357
+ // Set up callbacks
1358
+ moovDrop.onResourceCreated = async (result) => {
1359
+ console.log("OperatorPayment: Payment method successfully added", result);
1360
+
1361
+ // Optimistic update: add to local bank accounts list immediately
1362
+ const newAccount = {
1363
+ id: result.paymentMethodID || Date.now().toString(),
1364
+ bankName: result.bankName || "Bank Account",
1365
+ holderName: result.holderName || "Account Holder",
1366
+ bankAccountType: result.bankAccountType || "checking",
1367
+ lastFourAccountNumber: result.lastFourAccountNumber || "****",
1368
+ status: result.status || "pending",
1369
+ };
1370
+ this._state.bankAccounts.push(newAccount);
1371
+ this.updateBankAccountsList();
1372
+
1373
+ // Refresh the Moov token for subsequent operations
1374
+ try {
1375
+ const tokenResult = await this.api.generateMoovToken(
1376
+ this._state.operatorEmail,
1377
+ this._state.moovAccountId
1378
+ );
1379
+ if (tokenResult?.data?.accessToken) {
1380
+ this._state.moovToken = tokenResult.data.accessToken;
1381
+ moovDrop.token = this._state.moovToken;
1382
+ console.log("OperatorPayment: Moov token refreshed");
1383
+ }
1384
+ } catch (error) {
1385
+ console.error("OperatorPayment: Failed to refresh Moov token", error);
1386
+ }
1387
+
1388
+ // Refetch in background to sync with server
1389
+ this.fetchPaymentMethods();
1390
+
1391
+ // Dispatch success event
1392
+ this.dispatchEvent(
1393
+ new CustomEvent("moov-link-success", {
1394
+ detail: result,
1395
+ bubbles: true,
1396
+ composed: true,
1397
+ })
1398
+ );
1399
+ };
1400
+
1401
+ moovDrop.onError = ({ errorType, error }) => {
1402
+ console.error("OperatorPayment: Moov error", errorType, error);
1403
+ this.dispatchEvent(
1404
+ new CustomEvent("moov-link-error", {
1405
+ detail: { errorType, error },
1406
+ bubbles: true,
1407
+ composed: true,
1408
+ })
1409
+ );
1410
+ };
1411
+
1412
+ // Add close handler for when user closes the Moov UI
1413
+ moovDrop.onClose = () => {
1414
+ console.log("OperatorPayment: Moov UI closed by user");
1415
+ moovDrop.open = false;
1416
+
1417
+ // Dispatch close event
1418
+ this.dispatchEvent(
1419
+ new CustomEvent("moov-link-close", {
1420
+ bubbles: true,
1421
+ composed: true,
1422
+ })
1423
+ );
1424
+ };
1425
+
1426
+ // Add cancel handler for when user clicks the X button
1427
+ moovDrop.onCancel = () => {
1428
+ console.log("OperatorPayment: Moov UI cancelled by user (X button)");
1429
+ moovDrop.open = false;
1430
+
1431
+ // Dispatch close event
1432
+ this.dispatchEvent(
1433
+ new CustomEvent("moov-link-close", {
1434
+ bubbles: true,
1435
+ composed: true,
1436
+ })
1437
+ );
1438
+ };
1439
+
1440
+ moovDrop.paymentMethodTypes = ["bankAccount"];
1441
+
1442
+ // Add click event delegation for modalClose button (inside Moov's shadow DOM)
1443
+ // Use document-level listener since element is in shadow DOM
1444
+ const handleModalClose = (e) => {
1445
+ const target = e.target;
1446
+
1447
+ // Check if the clicked element or any parent has data-testid="modalClose"
1448
+ const modalCloseElement =
1449
+ target.closest && target.closest('[data-testid="modalClose"]');
1450
+
1451
+ // Only proceed if the modalClose element was clicked
1452
+ if (modalCloseElement) {
1453
+ console.log(
1454
+ 'OperatorPayment: [data-testid="modalClose"] element clicked inside Moov UI'
1455
+ );
1456
+ console.log("Element:", modalCloseElement);
1457
+ console.log("Event target:", target);
1458
+
1459
+ // Close the Moov UI by setting moovDrop.open to false
1460
+ this.closeMoovDrop();
1461
+
1462
+ console.log("OperatorPayment: Moov UI closed successfully");
1463
+ }
1464
+ };
1465
+
1466
+ // Add listener to document to catch events from shadow DOM
1467
+ document.addEventListener("click", handleModalClose, true);
1468
+
1469
+ // Store handler reference for cleanup
1470
+ if (!this._modalCloseHandlers) {
1471
+ this._modalCloseHandlers = [];
1472
+ }
1473
+ this._modalCloseHandlers.push(handleModalClose);
1474
+
1475
+ // Open the Moov drop
1476
+ console.log("OperatorPayment: Setting moovDrop.open = true");
1477
+ moovDrop.open = true;
1478
+
1479
+ // Store reference to moovDrop for external access
1480
+ this._moovRef = moovDrop;
1481
+ }
1482
+
1483
+ /**
1484
+ * Close the Moov UI
1485
+ * Can be called externally to close the Moov drop
1486
+ */
1487
+ closeMoovDrop() {
1488
+ if (this._moovRef && this._moovRef.open) {
1489
+ console.log("OperatorPayment: Closing Moov UI");
1490
+ this._moovRef.open = false;
1491
+ this._moovRef = null;
1492
+ }
1493
+ }
1494
+
1495
+ /**
1496
+ * Update main button state based on initialization status
1497
+ */
1498
+ updateMainButtonState() {
1499
+ const button = this.shadowRoot.querySelector(".link-payment-btn");
1500
+ const wrapper = this.shadowRoot.querySelector(".btn-wrapper");
1501
+ const tooltip = this.shadowRoot.querySelector(".tooltip");
1502
+ if (!button) return;
1503
+
1504
+ // Handle loading state
1505
+ if (this._state.isLoading) {
1506
+ button.classList.add("loading");
1507
+ button.classList.remove("error");
1508
+ button.disabled = true;
1509
+ if (wrapper) wrapper.classList.remove("has-error");
1510
+ }
1511
+ // Handle error state
1512
+ else if (this._state.initializationError) {
1513
+ button.classList.remove("loading");
1514
+ button.classList.add("error");
1515
+ button.disabled = true;
1516
+ if (wrapper) wrapper.classList.add("has-error");
1517
+ if (tooltip && this._state.error) {
1518
+ tooltip.textContent = this._state.error;
1519
+ }
1520
+ }
1521
+ // Handle normal state
1522
+ else {
1523
+ button.classList.remove("loading");
1524
+ button.classList.remove("error");
1525
+ button.disabled = false;
1526
+ if (wrapper) wrapper.classList.remove("has-error");
1527
+ }
1528
+ }
1529
+
1530
+ // ==================== RENDERING ====================
1531
+
1532
+ /**
1533
+ * Get account type label
1534
+ * @param {string} type - Account type (checking/savings)
1535
+ * @returns {string} Formatted label
1536
+ */
1537
+ getAccountTypeLabel(type) {
1538
+ const labels = {
1539
+ checking: "Checking",
1540
+ savings: "Savings",
1541
+ };
1542
+ return labels[type] || type;
1543
+ }
1544
+
1545
+ /**
1546
+ * Mask account number
1547
+ * @param {string} lastFour - Last 4 digits of account number
1548
+ * @returns {string} Masked account number
1549
+ */
1550
+ maskAccountNumber(lastFour) {
1551
+ return `••••${lastFour}`;
1552
+ }
1553
+
1554
+ /**
1555
+ * Render bank account cards
1556
+ * @returns {string} HTML string for bank accounts
1557
+ */
1558
+ renderBankAccounts() {
1559
+ // Show loading state
1560
+ if (this._state.isLoadingPaymentMethods) {
1561
+ return `
1562
+ <div class="loading-state">
1563
+ <div class="loading-spinner-large"></div>
1564
+ <p>Loading payment methods...</p>
1565
+ </div>
1566
+ `;
1567
+ }
1568
+
1569
+ // Show error state
1570
+ if (this._state.paymentMethodsError) {
1571
+ return `
1572
+ <div class="error-state">
1573
+ <svg class="error-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1574
+ <circle cx="12" cy="12" r="10"></circle>
1575
+ <line x1="12" y1="8" x2="12" y2="12"></line>
1576
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
1577
+ </svg>
1578
+ <p>${this._state.paymentMethodsError}</p>
1579
+ <button class="retry-btn" onclick="this.getRootNode().host.fetchPaymentMethods()">Retry</button>
1580
+ </div>
1581
+ `;
1582
+ }
1583
+
1584
+ const accounts = this._state.bankAccounts || [];
1585
+
1586
+ if (accounts.length === 0) {
1587
+ return `
1588
+ <div class="empty-state">
1589
+ <svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1590
+ <rect x="3" y="10" width="18" height="11" rx="2" ry="2"></rect>
1591
+ <path d="M12 3L2 10h20L12 3z"></path>
1592
+ </svg>
1593
+ <p>No bank accounts linked yet</p>
1594
+ </div>
1595
+ `;
1596
+ }
1597
+
1598
+ return accounts
1599
+ .map(
1600
+ (account, index) => `
1601
+ <div class="bank-account-card" data-account-id="${account.id}">
1602
+ <div class="bank-account-info">
1603
+ <div class="bank-icon-wrapper">
1604
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1605
+ <rect x="3" y="10" width="18" height="11" rx="2" ry="2"></rect>
1606
+ <path d="M12 3L2 10h20L12 3z"></path>
1607
+ <line x1="12" y1="14" x2="12" y2="17"></line>
1608
+ <line x1="7" y1="14" x2="7" y2="17"></line>
1609
+ <line x1="17" y1="14" x2="17" y2="17"></line>
1610
+ </svg>
1611
+ </div>
1612
+ <div class="bank-account-details">
1613
+ <div class="bank-name-row">
1614
+ <span class="bank-name">${account.bankName}</span>
1615
+ ${account.status === "verified"
1616
+ ? `
1617
+ <svg class="verified-icon" viewBox="0 0 24 24" fill="currentColor" stroke="none">
1618
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
1619
+ </svg>
1620
+ `
1621
+ : ""
1622
+ }
1623
+ </div>
1624
+ <span class="holder-name">${account.holderName}</span>
1625
+ <span class="account-meta">${this.getAccountTypeLabel(
1626
+ account.bankAccountType
1627
+ )} • ${this.maskAccountNumber(account.lastFourAccountNumber)}</span>
1628
+ </div>
1629
+ </div>
1630
+ <div class="card-actions">
1631
+ <span class="status-badge ${account.status}">${account.status}</span>
1632
+ <button class="delete-btn" data-account-id="${account.id
1633
+ }" aria-label="Delete payment method">
1634
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1635
+ <polyline points="3 6 5 6 21 6"></polyline>
1636
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
1637
+ <line x1="10" y1="11" x2="10" y2="17"></line>
1638
+ <line x1="14" y1="11" x2="14" y2="17"></line>
1639
+ </svg>
1640
+ </button>
1641
+ </div>
1642
+ </div>
1643
+ `
1644
+ )
1645
+ .join("");
1646
+ }
1647
+
1648
+ /**
1649
+ * Render the component (Shadow DOM)
1650
+ */
1651
+ render() {
1652
+ this.shadowRoot.innerHTML = `
1653
+ <style>
1654
+ :host {
1655
+ display: inline-block;
1656
+ font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
1657
+ color: var(--color-secondary, #5f6e78);
1658
+ }
1659
+
1660
+ .link-payment-btn {
1661
+ padding: 12px 24px;
1662
+ background: var(--color-primary, #4c7b63);
1663
+ color: var(--color-white, #fff);
1664
+ border: none;
1665
+ border-radius: var(--radius-xl, 0.75rem);
1666
+ font-size: var(--text-sm, 0.875rem);
1667
+ font-weight: var(--font-weight-medium, 500);
1668
+ cursor: pointer;
1669
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1670
+ display: inline-flex;
1671
+ align-items: center;
1672
+ gap: 8px;
1673
+ height: 40px;
1674
+ box-sizing: border-box;
1675
+ }
1676
+
1677
+ .link-payment-btn:hover:not(.error):not(.loading) {
1678
+ background: var(--color-primary-hover, #436c57);
1679
+ }
1680
+
1681
+ .link-payment-btn:active:not(.error):not(.loading) {
1682
+ background: var(--color-primary-active, #3d624f);
1683
+ transform: translateY(0);
1684
+ }
1685
+
1686
+ .link-payment-btn.error {
1687
+ background: var(--color-gray-400, #9ca3af);
1688
+ cursor: not-allowed;
1689
+ }
1690
+
1691
+ .link-payment-btn.loading {
1692
+ background: var(--color-primary-soft, #678f7a);
1693
+ cursor: wait;
1694
+ }
1695
+
1696
+ .link-payment-btn .broken-link-icon {
1697
+ display: none;
1698
+ }
1699
+
1700
+ .link-payment-btn.error .broken-link-icon {
1701
+ display: inline-block;
1702
+ }
1703
+
1704
+ .link-payment-btn .loading-spinner {
1705
+ display: none;
1706
+ width: 16px;
1707
+ height: 16px;
1708
+ border: 2px solid rgba(255, 255, 255, 0.3);
1709
+ border-top-color: var(--color-white, #fff);
1710
+ border-radius: 50%;
1711
+ animation: spin 0.8s linear infinite;
1712
+ }
1713
+
1714
+ .link-payment-btn.loading .loading-spinner {
1715
+ display: inline-block;
1716
+ }
1717
+
1718
+ @keyframes spin {
1719
+ to {
1720
+ transform: rotate(360deg);
1721
+ }
1722
+ }
1723
+
1724
+ .btn-wrapper {
1725
+ position: relative;
1726
+ display: inline-block;
1727
+ }
1728
+
1729
+ .tooltip {
1730
+ visibility: hidden;
1731
+ opacity: 0;
1732
+ position: absolute;
1733
+ bottom: 100%;
1734
+ left: 50%;
1735
+ transform: translateX(-50%);
1736
+ background: var(--color-gray-700, #374151);
1737
+ color: var(--color-white, #fff);
1738
+ padding: 8px 12px;
1739
+ border-radius: var(--radius-lg, 0.5rem);
1740
+ font-size: 13px;
1741
+ white-space: nowrap;
1742
+ margin-bottom: 8px;
1743
+ transition: opacity var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1)),
1744
+ visibility var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1745
+ z-index: 10002;
1746
+ }
1747
+
1748
+ .tooltip::after {
1749
+ content: '';
1750
+ position: absolute;
1751
+ top: 100%;
1752
+ left: 50%;
1753
+ transform: translateX(-50%);
1754
+ border: 6px solid transparent;
1755
+ border-top-color: var(--color-gray-700, #374151);
1756
+ }
1757
+
1758
+ .btn-wrapper:hover .tooltip {
1759
+ visibility: visible;
1760
+ opacity: 1;
1761
+ }
1762
+
1763
+ .btn-wrapper:not(.has-error) .tooltip {
1764
+ display: none;
1765
+ }
1766
+
1767
+ .modal {
1768
+ display: none;
1769
+ position: fixed;
1770
+ top: 0;
1771
+ left: 0;
1772
+ right: 0;
1773
+ bottom: 0;
1774
+ z-index: 10000;
1775
+ align-items: center;
1776
+ justify-content: center;
1777
+ }
1778
+
1779
+ .modal.show {
1780
+ display: flex;
1781
+ }
1782
+
1783
+ .modal.animating-in .modal-overlay {
1784
+ animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1785
+ }
1786
+
1787
+ .modal.animating-in .modal-content {
1788
+ animation: slideInScale 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1789
+ }
1790
+
1791
+ .modal.animating-out .modal-overlay {
1792
+ animation: fadeOut 0.15s cubic-bezier(0.4, 0, 1, 1);
1793
+ }
1794
+
1795
+ .modal.animating-out .modal-content {
1796
+ animation: slideOutScale 0.15s cubic-bezier(0.4, 0, 1, 1);
1797
+ }
1798
+
1799
+ @keyframes fadeIn {
1800
+ from {
1801
+ opacity: 0;
1802
+ }
1803
+ to {
1804
+ opacity: 1;
1805
+ }
1806
+ }
1807
+
1808
+ @keyframes fadeOut {
1809
+ from {
1810
+ opacity: 1;
1811
+ }
1812
+ to {
1813
+ opacity: 0;
1814
+ }
1815
+ }
1816
+
1817
+ @keyframes slideInScale {
1818
+ from {
1819
+ opacity: 0;
1820
+ transform: scale(0.95) translateY(-10px);
1821
+ }
1822
+ to {
1823
+ opacity: 1;
1824
+ transform: scale(1) translateY(0);
1825
+ }
1826
+ }
1827
+
1828
+ @keyframes slideOutScale {
1829
+ from {
1830
+ opacity: 1;
1831
+ transform: scale(1) translateY(0);
1832
+ }
1833
+ to {
1834
+ opacity: 0;
1835
+ transform: scale(0.98) translateY(-8px);
1836
+ }
1837
+ }
1838
+
1839
+ .modal-overlay {
1840
+ position: absolute;
1841
+ top: 0;
1842
+ left: 0;
1843
+ right: 0;
1844
+ bottom: 0;
1845
+ background: rgba(0, 0, 0, 0.5);
1846
+ }
1847
+
1848
+ .modal-content {
1849
+ position: relative;
1850
+ background: var(--color-white, #fff);
1851
+ border-radius: var(--radius-xl, 0.75rem);
1852
+ width: 90%;
1853
+ max-width: 600px;
1854
+ min-height: 400px;
1855
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1856
+ z-index: 10001;
1857
+ display: flex;
1858
+ flex-direction: column;
1859
+ align-items: center;
1860
+ justify-content: center;
1861
+ padding: 40px;
1862
+ }
1863
+
1864
+ .close-btn {
1865
+ position: absolute;
1866
+ top: 16px;
1867
+ right: 16px;
1868
+ background: transparent;
1869
+ border: none;
1870
+ font-size: 28px;
1871
+ color: var(--color-gray-400, #9ca3af);
1872
+ cursor: pointer;
1873
+ width: 32px;
1874
+ height: 32px;
1875
+ display: flex;
1876
+ align-items: center;
1877
+ justify-content: center;
1878
+ border-radius: 4px;
1879
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1880
+ }
1881
+
1882
+ .close-btn:hover {
1883
+ background: var(--color-gray-100, #f3f4f6);
1884
+ color: var(--color-headline, #0f2a39);
1885
+ }
1886
+
1887
+ .add-bank-btn {
1888
+ padding: 12px 24px;
1889
+ background: var(--color-primary, #4c7b63);
1890
+ color: var(--color-white, #fff);
1891
+ border: none;
1892
+ border-radius: var(--radius-xl, 0.75rem);
1893
+ font-size: var(--text-sm, 0.875rem);
1894
+ font-weight: var(--font-weight-medium, 500);
1895
+ cursor: pointer;
1896
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1897
+ display: inline-flex;
1898
+ align-items: center;
1899
+ gap: 10px;
1900
+ }
1901
+
1902
+ .add-bank-btn:hover {
1903
+ background: var(--color-primary-hover, #436c57);
1904
+ }
1905
+
1906
+ .add-bank-btn:active {
1907
+ background: var(--color-primary-active, #3d624f);
1908
+ }
1909
+
1910
+ .add-bank-btn .bank-icon {
1911
+ width: 20px;
1912
+ height: 20px;
1913
+ }
1914
+
1915
+ .modal-header {
1916
+ width: 100%;
1917
+ text-align: center;
1918
+ margin-bottom: var(--spacing-lg, 24px);
1919
+ padding-bottom: var(--spacing-md, 16px);
1920
+ border-bottom: 1px solid var(--color-border, #e8e8e8);
1921
+ }
1922
+
1923
+ .modal-header h2 {
1924
+ font-size: 20px;
1925
+ font-weight: var(--font-weight-semibold, 600);
1926
+ color: var(--color-headline, #0f2a39);
1927
+ margin: 0;
1928
+ }
1929
+
1930
+ .modal-header p {
1931
+ font-size: 14px;
1932
+ color: var(--color-gray-500, #6b7280);
1933
+ margin-top: 4px;
1934
+ }
1935
+
1936
+ .bank-accounts-list {
1937
+ width: 100%;
1938
+ display: flex;
1939
+ flex-direction: column;
1940
+ gap: 12px;
1941
+ margin-bottom: 24px;
1942
+ max-height: 300px;
1943
+ overflow-y: auto;
1944
+ }
1945
+
1946
+ .bank-account-card {
1947
+ display: flex;
1948
+ align-items: flex-start;
1949
+ justify-content: space-between;
1950
+ padding: 16px;
1951
+ background: var(--color-gray-50, #f9fafb);
1952
+ border: 1px solid var(--color-border, #e8e8e8);
1953
+ border-radius: var(--radius-xl, 0.75rem);
1954
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1955
+ }
1956
+
1957
+ .bank-account-card:hover {
1958
+ border-color: var(--color-primary, #4c7b63);
1959
+ background: var(--color-gray-100, #f3f4f6);
1960
+ }
1961
+
1962
+ .bank-account-info {
1963
+ display: flex;
1964
+ align-items: flex-start;
1965
+ gap: 12px;
1966
+ }
1967
+
1968
+ .bank-icon-wrapper {
1969
+ padding: 10px;
1970
+ background: var(--color-primary-light, #e8f0eb);
1971
+ border-radius: var(--radius-lg, 0.5rem);
1972
+ display: flex;
1973
+ align-items: center;
1974
+ justify-content: center;
1975
+ }
1976
+
1977
+ .bank-icon-wrapper svg {
1978
+ width: 20px;
1979
+ height: 20px;
1980
+ color: var(--color-primary, #4c7b63);
1981
+ stroke: var(--color-primary, #4c7b63);
1982
+ }
1983
+
1984
+ .bank-account-details {
1985
+ display: flex;
1986
+ flex-direction: column;
1987
+ gap: 2px;
1988
+ }
1989
+
1990
+ .bank-name-row {
1991
+ display: flex;
1992
+ align-items: center;
1993
+ gap: 8px;
1994
+ }
1995
+
1996
+ .bank-name {
1997
+ font-weight: 500;
1998
+ font-size: 15px;
1999
+ color: var(--color-headline, #0f2a39);
2000
+ }
2001
+
2002
+ .verified-icon {
2003
+ width: 16px;
2004
+ height: 16px;
2005
+ color: var(--color-success, #22c55e);
2006
+ }
2007
+
2008
+ .holder-name {
2009
+ font-size: 14px;
2010
+ color: var(--color-gray-500, #6b7280);
2011
+ text-align: left;
2012
+ }
2013
+
2014
+ .account-meta {
2015
+ font-size: 12px;
2016
+ color: var(--color-gray-400, #9ca3af);
2017
+ margin-top: 2px;
2018
+ text-align: left;
2019
+ }
2020
+
2021
+ .status-badge {
2022
+ padding: 4px 10px;
2023
+ border-radius: 6px;
2024
+ font-size: 12px;
2025
+ font-weight: 500;
2026
+ text-transform: capitalize;
2027
+ }
2028
+
2029
+ .status-badge.verified {
2030
+ background: var(--color-success-light, #d3f3df);
2031
+ color: var(--color-success-dark, #136c34);
2032
+ }
2033
+
2034
+ .status-badge.pending {
2035
+ background: var(--color-orange-100, #fdecce);
2036
+ color: var(--color-orange-600, #875706);
2037
+ }
2038
+
2039
+ .card-actions {
2040
+ display: flex;
2041
+ align-items: center;
2042
+ gap: 8px;
2043
+ }
2044
+
2045
+ .delete-btn {
2046
+ background: transparent;
2047
+ border: none;
2048
+ padding: 6px;
2049
+ cursor: pointer;
2050
+ border-radius: 6px;
2051
+ display: flex;
2052
+ align-items: center;
2053
+ justify-content: center;
2054
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
2055
+ color: var(--color-gray-400, #9ca3af);
2056
+ }
2057
+
2058
+ .delete-btn:hover {
2059
+ background: var(--color-error-light, #fae5e4);
2060
+ color: var(--color-error, #dd524b);
2061
+ }
2062
+
2063
+ .delete-btn svg {
2064
+ width: 18px;
2065
+ height: 18px;
2066
+ }
2067
+
2068
+ .empty-state {
2069
+ text-align: center;
2070
+ padding: 32px 16px;
2071
+ color: var(--color-gray-500, #6b7280);
2072
+ }
2073
+
2074
+ .empty-state-icon {
2075
+ width: 48px;
2076
+ height: 48px;
2077
+ margin: 0 auto 12px;
2078
+ color: var(--color-gray-200, #d1d5db);
2079
+ }
2080
+
2081
+ .empty-state p {
2082
+ font-size: 14px;
2083
+ margin: 0;
2084
+ }
2085
+
2086
+ .loading-state {
2087
+ text-align: center;
2088
+ padding: 32px 16px;
2089
+ color: var(--color-gray-500, #6b7280);
2090
+ }
2091
+
2092
+ .loading-spinner-large {
2093
+ width: 32px;
2094
+ height: 32px;
2095
+ border: 3px solid var(--color-border, #e8e8e8);
2096
+ border-top-color: var(--color-primary, #4c7b63);
2097
+ border-radius: 50%;
2098
+ animation: spin 0.8s linear infinite;
2099
+ margin: 0 auto 12px;
2100
+ }
2101
+
2102
+ .loading-state p {
2103
+ font-size: 14px;
2104
+ margin: 0;
2105
+ }
2106
+
2107
+ .error-state {
2108
+ text-align: center;
2109
+ padding: 32px 16px;
2110
+ color: var(--color-error, #dd524b);
2111
+ }
2112
+
2113
+ .error-state-icon {
2114
+ width: 48px;
2115
+ height: 48px;
2116
+ margin: 0 auto 12px;
2117
+ color: var(--color-error, #dd524b);
2118
+ }
2119
+
2120
+ .error-state p {
2121
+ font-size: 14px;
2122
+ margin: 0 0 16px 0;
2123
+ color: var(--color-gray-500, #6b7280);
2124
+ }
2125
+
2126
+ .retry-btn {
2127
+ padding: 8px 16px;
2128
+ background: var(--color-primary, #4c7b63);
2129
+ color: var(--color-white, #fff);
2130
+ border: none;
2131
+ border-radius: var(--radius-lg, 0.5rem);
2132
+ font-size: 13px;
2133
+ font-weight: var(--font-weight-medium, 500);
2134
+ cursor: pointer;
2135
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
2136
+ }
2137
+
2138
+ .retry-btn:hover {
2139
+ background: var(--color-primary-hover, #436c57);
2140
+ }
2141
+
2142
+ .divider {
2143
+ width: 100%;
2144
+ height: 1px;
2145
+ background: var(--color-border, #e8e8e8);
2146
+ margin: 16px 0;
2147
+ }
2148
+
2149
+ .add-bank-section {
2150
+ width: 100%;
2151
+ display: flex;
2152
+ flex-direction: column;
2153
+ align-items: center;
2154
+ gap: 8px;
2155
+ }
2156
+
2157
+ .add-bank-section p {
2158
+ font-size: 13px;
2159
+ color: var(--color-gray-500, #6b7280);
2160
+ margin: 0;
2161
+ }
2162
+
2163
+ .powered-by {
2164
+ display: flex;
2165
+ align-items: center;
2166
+ justify-content: center;
2167
+ gap: 6px;
2168
+ margin-top: 24px;
2169
+ padding-top: 16px;
2170
+ font-size: 11px;
2171
+ color: var(--color-gray-400, #9ca3af);
2172
+ }
2173
+
2174
+ .powered-by svg {
2175
+ width: 16px;
2176
+ height: 16px;
2177
+ }
2178
+
2179
+ .powered-by span {
2180
+ font-weight: 500;
2181
+ color: var(--color-gray-500, #6b7280);
2182
+ }
2183
+
2184
+ /* Delete Confirmation Modal */
2185
+ .delete-confirmation-container {
2186
+ display: contents;
2187
+ }
2188
+
2189
+ .delete-confirmation-overlay {
2190
+ position: fixed;
2191
+ top: 0;
2192
+ left: 0;
2193
+ right: 0;
2194
+ bottom: 0;
2195
+ background: rgba(0, 0, 0, 0.6);
2196
+ z-index: 10200;
2197
+ }
2198
+
2199
+ .delete-confirmation-dialog {
2200
+ position: fixed;
2201
+ top: 50%;
2202
+ left: 50%;
2203
+ transform: translate(-50%, -50%);
2204
+ background: var(--color-white, #fff);
2205
+ border-radius: var(--radius-2xl, 1rem);
2206
+ padding: 32px;
2207
+ width: 90%;
2208
+ max-width: 400px;
2209
+ z-index: 10201;
2210
+ text-align: center;
2211
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
2212
+ animation: deleteModalIn 0.2s ease-out;
2213
+ }
2214
+
2215
+ @keyframes deleteModalIn {
2216
+ from {
2217
+ opacity: 0;
2218
+ transform: translate(-50%, -50%) scale(0.95);
2219
+ }
2220
+ to {
2221
+ opacity: 1;
2222
+ transform: translate(-50%, -50%) scale(1);
2223
+ }
2224
+ }
2225
+
2226
+ .delete-confirmation-icon {
2227
+ width: 56px;
2228
+ height: 56px;
2229
+ margin: 0 auto 16px;
2230
+ background: var(--color-error-light, #fae5e4);
2231
+ border-radius: 50%;
2232
+ display: flex;
2233
+ align-items: center;
2234
+ justify-content: center;
2235
+ }
2236
+
2237
+ .delete-confirmation-icon svg {
2238
+ width: 28px;
2239
+ height: 28px;
2240
+ color: var(--color-error, #dd524b);
2241
+ }
2242
+
2243
+ .delete-confirmation-title {
2244
+ font-size: 18px;
2245
+ font-weight: var(--font-weight-semibold, 600);
2246
+ color: var(--color-headline, #0f2a39);
2247
+ margin: 0 0 8px 0;
2248
+ }
2249
+
2250
+ .delete-confirmation-message {
2251
+ font-size: 14px;
2252
+ color: var(--color-gray-500, #6b7280);
2253
+ margin: 0 0 24px 0;
2254
+ line-height: 1.5;
2255
+ }
2256
+
2257
+ .delete-confirmation-message strong {
2258
+ color: var(--color-gray-700, #374151);
2259
+ }
2260
+
2261
+ .delete-confirmation-actions {
2262
+ display: flex;
2263
+ gap: 12px;
2264
+ justify-content: center;
2265
+ }
2266
+
2267
+ .delete-cancel-btn {
2268
+ padding: 10px 20px;
2269
+ background: var(--color-white, #fff);
2270
+ color: var(--color-gray-700, #374151);
2271
+ border: 1px solid var(--color-gray-200, #d1d5db);
2272
+ border-radius: var(--radius-lg, 0.5rem);
2273
+ font-size: var(--text-sm, 0.875rem);
2274
+ font-weight: var(--font-weight-medium, 500);
2275
+ cursor: pointer;
2276
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
2277
+ height: 40px;
2278
+ box-sizing: border-box;
2279
+ }
2280
+
2281
+ .delete-cancel-btn:hover:not(:disabled) {
2282
+ background: var(--color-gray-100, #f3f4f6);
2283
+ border-color: var(--color-gray-400, #9ca3af);
2284
+ }
2285
+
2286
+ .delete-cancel-btn:disabled {
2287
+ opacity: 0.5;
2288
+ cursor: not-allowed;
2289
+ }
2290
+
2291
+ .delete-confirm-btn {
2292
+ padding: 10px 20px;
2293
+ background: var(--color-error, #dd524b);
2294
+ color: var(--color-white, #fff);
2295
+ border: none;
2296
+ border-radius: var(--radius-lg, 0.5rem);
2297
+ font-size: var(--text-sm, 0.875rem);
2298
+ font-weight: var(--font-weight-medium, 500);
2299
+ cursor: pointer;
2300
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
2301
+ display: inline-flex;
2302
+ align-items: center;
2303
+ justify-content: center;
2304
+ gap: 8px;
2305
+ height: 40px;
2306
+ box-sizing: border-box;
2307
+ min-width: 100px;
2308
+ }
2309
+
2310
+ .delete-confirm-btn:hover:not(:disabled) {
2311
+ background: var(--color-error-dark, #903531);
2312
+ }
2313
+
2314
+ .delete-confirm-btn:disabled {
2315
+ background: var(--color-error-muted, #eea9a5);
2316
+ cursor: not-allowed;
2317
+ }
2318
+
2319
+ .delete-spinner {
2320
+ width: 14px;
2321
+ height: 14px;
2322
+ border: 2px solid rgba(255, 255, 255, 0.3);
2323
+ border-top-color: var(--color-white, #fff);
2324
+ border-radius: 50%;
2325
+ animation: spin 0.8s linear infinite;
2326
+ box-sizing: border-box;
2327
+ }
2328
+ </style>
2329
+
2330
+ <!-- Main Button -->
2331
+ <div class="btn-wrapper">
2332
+ <span class="tooltip">Operator email and operator ID are required</span>
2333
+ <button class="link-payment-btn">
2334
+ <span class="loading-spinner"></span>
2335
+ <svg class="broken-link-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2336
+ <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
2337
+ <line x1="1" y1="1" x2="23" y2="23"></line>
2338
+ </svg>
2339
+ Manage Payments
2340
+ </button>
2341
+ </div>
2342
+
2343
+ <!-- Modal -->
2344
+ <div class="modal">
2345
+ <div class="modal-overlay"></div>
2346
+ <div class="modal-content">
2347
+ <button class="close-btn">×</button>
2348
+
2349
+ <!-- Modal Header -->
2350
+ <div class="modal-header">
2351
+ <h2>Payment Methods</h2>
2352
+ <p>Manage your operator payment accounts</p>
2353
+ </div>
2354
+
2355
+ <!-- Bank Accounts List -->
2356
+ <div class="bank-accounts-list" id="bankAccountsList">
2357
+ ${this.renderBankAccounts()}
2358
+ </div>
2359
+
2360
+ <!-- Divider -->
2361
+ <div class="divider"></div>
2362
+
2363
+ <!-- Add Bank Section -->
2364
+ <div class="add-bank-section">
2365
+ <button class="add-bank-btn">
2366
+ <svg class="bank-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2367
+ <line x1="12" y1="5" x2="12" y2="19"></line>
2368
+ <line x1="5" y1="12" x2="19" y2="12"></line>
2369
+ </svg>
2370
+ Add Bank Account
2371
+ </button>
2372
+ <p>Connect a new bank account via Moov</p>
2373
+ </div>
2374
+
2375
+ <!-- Powered by Bison -->
2376
+ <div class="powered-by">
2377
+ Powered by
2378
+ <img src="./bison_logo.png" alt="Bison" style="height: 16px; margin-left: 4px;" onerror="this.onerror=null; this.src='https://bisonpaywell.com/lovable-uploads/28831244-e8b3-4e7b-8dbb-c016f9f9d54f.png';">
2379
+ </div>
2380
+ </div>
2381
+ </div>
2382
+
2383
+ <!-- Delete Confirmation Modal -->
2384
+ <div class="delete-confirmation-container" id="deleteConfirmationModal">
2385
+ ${this.renderDeleteConfirmationModal()}
2386
+ </div>
2387
+ `;
2388
+ }
2389
+ }
2390
+
2391
+ // Register the custom element only if it hasn't been registered yet
2392
+ if (!customElements.get("operator-payment")) {
2393
+ customElements.define("operator-payment", OperatorPayment);
2394
+ }
2395
+
2396
+ // Export for module usage
2397
+ if (typeof module !== "undefined" && module.exports) {
2398
+ module.exports = { OperatorPayment };
2399
+ }
2400
+
2401
+ // Make available globally for script tag usage
2402
+ if (typeof window !== "undefined") {
2403
+ window.OperatorPayment = OperatorPayment;
2404
+ }