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,1193 @@
1
+ /**
2
+ * OperatorBankAccount Web Component
3
+ *
4
+ * A simple web component that provides a button to add a bank account via Moov.
5
+ * Opens Moov's drop payment method directly when clicked.
6
+ *
7
+ * @author @kfajardo
8
+ * @version 1.0.0
9
+ *
10
+ * @example
11
+ * ```html
12
+ * <script src="component.js"></script>
13
+ * <script src="operator-bank-account.js"></script>
14
+ *
15
+ * <operator-bank-account
16
+ * id="addBank"
17
+ * email="operator@example.com"
18
+ * operator-id="OP123456"
19
+ * client-id="CLIENT789"
20
+ * api-url="https://your-api.com"
21
+ * ></operator-bank-account>
22
+ *
23
+ * Provide at least one of: email, operator-id, or client-id.
24
+ *
25
+ * <script>
26
+ * const addBank = document.getElementById('addBank');
27
+ *
28
+ * // Success callback
29
+ * addBank.onSuccess = (result) => {
30
+ * console.log('Bank account added:', result);
31
+ * };
32
+ *
33
+ * // Fail callback
34
+ * addBank.onFail = (error) => {
35
+ * console.error('Failed to add bank account:', error);
36
+ * };
37
+ * </script>
38
+ * ```
39
+ */
40
+
41
+ class OperatorBankAccount extends HTMLElement {
42
+ constructor() {
43
+ super();
44
+ this.attachShadow({ mode: "open" });
45
+
46
+ // API Configuration
47
+ this.apiBaseURL =
48
+ this.getAttribute("api-url") ||
49
+ "https://bison-jib-development.azurewebsites.net";
50
+ this.embeddableKey =
51
+ this.getAttribute("embeddable-key") ||
52
+ "R80WMkbNN8457RofiMYx03DL65P06IaVT30Q2emYJUBQwYCzRC";
53
+
54
+ // API will be initialized lazily when needed
55
+ this.api = null;
56
+
57
+ // Internal state
58
+ this._state = {
59
+ email: null,
60
+ operatorId: null,
61
+ clientId: null,
62
+ isLoading: true, // Loading by default for verification
63
+ moovAccountId: null,
64
+ moovToken: null,
65
+ error: null,
66
+ isVerified: false,
67
+ initializationError: false,
68
+ };
69
+
70
+ // Callback functions
71
+ this._onSuccess = null;
72
+ this._onFail = null;
73
+
74
+ // Moov drop reference
75
+ this._moovRef = null;
76
+
77
+ // Render the component
78
+ this.render();
79
+ }
80
+
81
+ // ==================== STATIC PROPERTIES ====================
82
+
83
+ static get observedAttributes() {
84
+ return ["email", "operator-id", "client-id", "api-url", "embeddable-key"];
85
+ }
86
+
87
+ // ==================== PROPERTY GETTERS/SETTERS ====================
88
+
89
+ /**
90
+ * Get the email
91
+ * @returns {string|null}
92
+ */
93
+ get email() {
94
+ return this._state.email;
95
+ }
96
+
97
+ /**
98
+ * Set the email
99
+ * @param {string} value - Operator email address
100
+ */
101
+ set email(value) {
102
+ console.log("OperatorBankAccount: Setting email to:", value);
103
+
104
+ const oldEmail = this._state.email;
105
+
106
+ // Update internal state
107
+ this._state.email = value;
108
+
109
+ // Update attribute only if different to prevent circular updates
110
+ const currentAttr = this.getAttribute("email");
111
+ if (currentAttr !== value) {
112
+ if (value) {
113
+ this.setAttribute("email", value);
114
+ } else {
115
+ this.removeAttribute("email");
116
+ }
117
+ }
118
+
119
+ // Trigger verification if required fields are set and component is connected
120
+ const missingMessage = this.getMissingOperatorInfoMessage();
121
+ if (!missingMessage && value && value !== oldEmail && this.isConnected) {
122
+ this.verifyAndInitialize();
123
+ } else if (missingMessage && this.isConnected) {
124
+ // Missing required fields, show error state
125
+ this._state.isLoading = false;
126
+ this._state.initializationError = true;
127
+ this._state.error = missingMessage;
128
+ this.updateButtonState();
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get the operatorId
134
+ * @returns {string|null}
135
+ */
136
+ get operatorId() {
137
+ return this._state.operatorId;
138
+ }
139
+
140
+ /**
141
+ * Set the operatorId
142
+ * @param {string} value - Operator ID
143
+ */
144
+ set operatorId(value) {
145
+ console.log("OperatorBankAccount: Setting operatorId to:", value);
146
+
147
+ const oldOperatorId = this._state.operatorId;
148
+
149
+ // Update internal state
150
+ this._state.operatorId = value;
151
+
152
+ // Update attribute only if different to prevent circular updates
153
+ const currentAttr = this.getAttribute("operator-id");
154
+ if (currentAttr !== value) {
155
+ if (value) {
156
+ this.setAttribute("operator-id", value);
157
+ } else {
158
+ this.removeAttribute("operator-id");
159
+ }
160
+ }
161
+
162
+ // Trigger verification if required fields are set and component is connected
163
+ const missingMessage = this.getMissingOperatorInfoMessage();
164
+ if (
165
+ !missingMessage &&
166
+ value &&
167
+ value !== oldOperatorId &&
168
+ this.isConnected
169
+ ) {
170
+ this.verifyAndInitialize();
171
+ } else if (missingMessage && this.isConnected) {
172
+ // Missing required fields, show error state
173
+ this._state.isLoading = false;
174
+ this._state.initializationError = true;
175
+ this._state.error = missingMessage;
176
+ this.updateButtonState();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Get the clientId
182
+ * @returns {string|null}
183
+ */
184
+ get clientId() {
185
+ return this._state.clientId;
186
+ }
187
+
188
+ /**
189
+ * Set the clientId
190
+ * @param {string} value - Client ID
191
+ */
192
+ set clientId(value) {
193
+ console.log("OperatorBankAccount: Setting clientId to:", value);
194
+
195
+ const oldClientId = this._state.clientId;
196
+
197
+ // Update internal state
198
+ this._state.clientId = value;
199
+
200
+ // Update attribute only if different to prevent circular updates
201
+ const currentAttr = this.getAttribute("client-id");
202
+ if (currentAttr !== value) {
203
+ if (value) {
204
+ this.setAttribute("client-id", value);
205
+ } else {
206
+ this.removeAttribute("client-id");
207
+ }
208
+ }
209
+
210
+ // Trigger verification if required fields are set and component is connected
211
+ const missingMessage = this.getMissingOperatorInfoMessage();
212
+ if (
213
+ !missingMessage &&
214
+ value &&
215
+ value !== oldClientId &&
216
+ this.isConnected
217
+ ) {
218
+ this.verifyAndInitialize();
219
+ } else if (missingMessage && this.isConnected) {
220
+ // Missing required fields, show error state
221
+ this._state.isLoading = false;
222
+ this._state.initializationError = true;
223
+ this._state.error = missingMessage;
224
+ this.updateButtonState();
225
+ }
226
+ }
227
+
228
+ getMissingOperatorInfoMessage() {
229
+ if (!this._state.email && !this._state.operatorId && !this._state.clientId) {
230
+ return "Email, operator ID, or client ID is required";
231
+ }
232
+ return null;
233
+ }
234
+
235
+ /**
236
+ * Get the moovAccountId
237
+ * @returns {string|null}
238
+ */
239
+ get moovAccountId() {
240
+ return this._state.moovAccountId;
241
+ }
242
+
243
+ /**
244
+ * Get/Set onSuccess callback
245
+ */
246
+ get onSuccess() {
247
+ return this._onSuccess;
248
+ }
249
+
250
+ set onSuccess(callback) {
251
+ if (typeof callback === "function" || callback === null) {
252
+ this._onSuccess = callback;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Get/Set onFail callback
258
+ */
259
+ get onFail() {
260
+ return this._onFail;
261
+ }
262
+
263
+ set onFail(callback) {
264
+ if (typeof callback === "function" || callback === null) {
265
+ this._onFail = callback;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Get/Set API URL
271
+ */
272
+ get apiUrl() {
273
+ return this.apiBaseURL;
274
+ }
275
+
276
+ set apiUrl(value) {
277
+ this.apiBaseURL = value;
278
+ if (this.api) {
279
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
280
+ }
281
+ // Update attribute
282
+ if (value) {
283
+ this.setAttribute("api-url", value);
284
+ }
285
+ }
286
+
287
+ // ==================== LIFECYCLE METHODS ====================
288
+
289
+ connectedCallback() {
290
+ // Initialize email from attribute if present
291
+ const emailAttr = this.getAttribute("email");
292
+ if (emailAttr && !this._state.email) {
293
+ this._state.email = emailAttr;
294
+ }
295
+
296
+ // Initialize operatorId from attribute if present
297
+ const operatorIdAttr = this.getAttribute("operator-id");
298
+ if (operatorIdAttr && !this._state.operatorId) {
299
+ this._state.operatorId = operatorIdAttr;
300
+ }
301
+
302
+ // Initialize clientId from attribute if present
303
+ const clientIdAttr = this.getAttribute("client-id");
304
+ if (clientIdAttr && !this._state.clientId) {
305
+ this._state.clientId = clientIdAttr;
306
+ }
307
+
308
+ // Load Moov SDK (preload for faster access later)
309
+ this.ensureMoovSDK();
310
+
311
+ this.setupEventListeners();
312
+
313
+ // Auto-verify if required fields are already set
314
+ const missingMessage = this.getMissingOperatorInfoMessage();
315
+ if (!missingMessage) {
316
+ this.verifyAndInitialize();
317
+ } else {
318
+ // Missing required fields, show error state with tooltip
319
+ this._state.isLoading = false;
320
+ this._state.initializationError = true;
321
+ this._state.error = missingMessage;
322
+ this.updateButtonState();
323
+ }
324
+ }
325
+
326
+ disconnectedCallback() {
327
+ this.removeEventListeners();
328
+ }
329
+
330
+ attributeChangedCallback(name, oldValue, newValue) {
331
+ if (oldValue === newValue) return;
332
+
333
+ switch (name) {
334
+ case "email":
335
+ console.log(
336
+ "OperatorBankAccount: attributeChangedCallback - email:",
337
+ newValue
338
+ );
339
+ this._state.email = newValue;
340
+ // Reset state when email changes
341
+ this._state.moovToken = null;
342
+ this._state.moovAccountId = null;
343
+ this._state.isVerified = false;
344
+ this._state.isLoading = true;
345
+ this._state.initializationError = false;
346
+ this.updateButtonState();
347
+ // Trigger verification if required fields are set
348
+ if (this.isConnected) {
349
+ const missingMessage = this.getMissingOperatorInfoMessage();
350
+ if (!missingMessage) {
351
+ this.verifyAndInitialize();
352
+ } else {
353
+ this._state.isLoading = false;
354
+ this._state.initializationError = true;
355
+ this._state.error = missingMessage;
356
+ this.updateButtonState();
357
+ }
358
+ }
359
+ break;
360
+
361
+ case "operator-id":
362
+ console.log(
363
+ "OperatorBankAccount: attributeChangedCallback - operator-id:",
364
+ newValue
365
+ );
366
+ this._state.operatorId = newValue;
367
+ // Reset state when operator ID changes
368
+ this._state.moovToken = null;
369
+ this._state.moovAccountId = null;
370
+ this._state.isVerified = false;
371
+ this._state.isLoading = true;
372
+ this._state.initializationError = false;
373
+ this.updateButtonState();
374
+ // Trigger verification if required fields are set
375
+ if (this.isConnected) {
376
+ const missingMessage = this.getMissingOperatorInfoMessage();
377
+ if (!missingMessage) {
378
+ this.verifyAndInitialize();
379
+ } else {
380
+ this._state.isLoading = false;
381
+ this._state.initializationError = true;
382
+ this._state.error = missingMessage;
383
+ this.updateButtonState();
384
+ }
385
+ }
386
+ break;
387
+
388
+ case "client-id":
389
+ console.log(
390
+ "OperatorBankAccount: attributeChangedCallback - client-id:",
391
+ newValue
392
+ );
393
+ this._state.clientId = newValue;
394
+ // Reset state when client ID changes
395
+ this._state.moovToken = null;
396
+ this._state.moovAccountId = null;
397
+ this._state.isVerified = false;
398
+ this._state.isLoading = true;
399
+ this._state.initializationError = false;
400
+ this.updateButtonState();
401
+ // Trigger verification if required fields are set
402
+ if (this.isConnected) {
403
+ const missingMessage = this.getMissingOperatorInfoMessage();
404
+ if (!missingMessage) {
405
+ this.verifyAndInitialize();
406
+ } else {
407
+ this._state.isLoading = false;
408
+ this._state.initializationError = true;
409
+ this._state.error = missingMessage;
410
+ this.updateButtonState();
411
+ }
412
+ }
413
+ break;
414
+
415
+ case "api-url":
416
+ this.apiBaseURL = newValue;
417
+ if (this.api) {
418
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
419
+ }
420
+ break;
421
+
422
+ case "embeddable-key":
423
+ this.embeddableKey = newValue;
424
+ if (this.api) {
425
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
426
+ }
427
+ break;
428
+ }
429
+ }
430
+
431
+ // ==================== MOOV SDK LOADING ====================
432
+
433
+ /**
434
+ * Inject Moov Bison theme styles into document body
435
+ */
436
+ injectMoovThemeStyles() {
437
+ if (document.getElementById("moov-bison-theme")) {
438
+ return;
439
+ }
440
+
441
+ const styleTag = document.createElement("style");
442
+ styleTag.id = "moov-bison-theme";
443
+ styleTag.textContent = `
444
+ :root {
445
+ --moov-color-background: var(--color-white, #fff);
446
+ --moov-color-background-secondary: var(--color-sidebar, #fafafa);
447
+ --moov-color-background-tertiary: var(--color-gray-100, #f3f4f6);
448
+ --moov-color-primary: var(--color-primary, #4c7b63);
449
+ --moov-color-secondary: var(--color-primary-hover, #436c57);
450
+ --moov-color-tertiary: var(--color-border, #e8e8e8);
451
+ --moov-color-info: var(--color-primary, #4c7b63);
452
+ --moov-color-warn: var(--color-warning, #f59e0b);
453
+ --moov-color-danger: var(--color-error, #dd524b);
454
+ --moov-color-success: var(--color-success, #22c55e);
455
+ --moov-color-low-contrast: var(--color-gray-400, #9ca3af);
456
+ --moov-color-medium-contrast: var(--color-gray-600, #4b5563);
457
+ --moov-color-high-contrast: var(--color-headline, #0f2a39);
458
+ --moov-color-graphic-1: var(--color-primary, #4c7b63);
459
+ --moov-color-graphic-2: var(--color-gray-500, #6b7280);
460
+ --moov-color-graphic-3: var(--color-warning, #f59e0b);
461
+ --moov-radius-small: var(--radius-lg, 0.5rem);
462
+ --moov-radius-large: var(--radius-xl, 0.75rem);
463
+ }
464
+ `;
465
+ document.body.appendChild(styleTag);
466
+ console.log("OperatorBankAccount: Bison theme styles injected");
467
+ }
468
+
469
+ /**
470
+ * Ensure Moov SDK is loaded
471
+ * @returns {Promise<void>}
472
+ */
473
+ async ensureMoovSDK() {
474
+ if (window.Moov) {
475
+ console.log("OperatorBankAccount: Moov SDK already loaded");
476
+ return Promise.resolve();
477
+ }
478
+
479
+ const existingScript = document.querySelector('script[src*="moov.js"]');
480
+ if (existingScript) {
481
+ console.log(
482
+ "OperatorBankAccount: Moov SDK script found, waiting for load..."
483
+ );
484
+ return new Promise((resolve, reject) => {
485
+ existingScript.addEventListener("load", () => {
486
+ console.log(
487
+ "OperatorBankAccount: Moov SDK loaded from existing script"
488
+ );
489
+ resolve();
490
+ });
491
+ existingScript.addEventListener("error", () =>
492
+ reject(new Error("Failed to load Moov SDK"))
493
+ );
494
+ });
495
+ }
496
+
497
+ console.log("OperatorBankAccount: Loading Moov SDK from CDN...");
498
+ return new Promise((resolve, reject) => {
499
+ const script = document.createElement("script");
500
+ script.src = "https://js.moov.io/v1";
501
+ script.async = true;
502
+
503
+ script.onload = () => {
504
+ console.log("OperatorBankAccount: Moov SDK loaded successfully");
505
+ resolve();
506
+ };
507
+
508
+ script.onerror = () => {
509
+ const error = new Error("Failed to load Moov SDK from CDN");
510
+ console.error("OperatorBankAccount:", error);
511
+ this._state.error = error.message;
512
+ this.triggerFail({ errorType: "sdk", error: error.message });
513
+ reject(error);
514
+ };
515
+
516
+ document.head.appendChild(script);
517
+ });
518
+ }
519
+
520
+ // ==================== EVENT HANDLING ====================
521
+
522
+ setupEventListeners() {
523
+ const button = this.shadowRoot.querySelector(".add-bank-btn");
524
+
525
+ if (button) {
526
+ button.addEventListener("click", this.handleButtonClick.bind(this));
527
+ }
528
+ }
529
+
530
+ removeEventListeners() {
531
+ // Clean up modal close handlers
532
+ if (this._modalCloseHandlers) {
533
+ this._modalCloseHandlers.forEach((handler) => {
534
+ document.removeEventListener("click", handler, true);
535
+ });
536
+ this._modalCloseHandlers = [];
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Handle button click
542
+ */
543
+ handleButtonClick() {
544
+ console.log("OperatorBankAccount: Button clicked");
545
+
546
+ // Validate required fields are set
547
+ const missingMessage = this.getMissingOperatorInfoMessage();
548
+ if (missingMessage) {
549
+ console.warn(
550
+ "OperatorBankAccount: Cannot open - required fields missing"
551
+ );
552
+ this.triggerFail({
553
+ errorType: "validation",
554
+ error: missingMessage,
555
+ });
556
+ return;
557
+ }
558
+
559
+ // Validate operator is verified
560
+ if (!this._state.isVerified) {
561
+ console.warn("OperatorBankAccount: Cannot open - operator not verified");
562
+ this.triggerFail({
563
+ errorType: "verification",
564
+ error: "Operator is not verified",
565
+ });
566
+ return;
567
+ }
568
+
569
+ // Validate API is available (lazy initialization)
570
+ if (!this.ensureAPI()) {
571
+ console.warn("OperatorBankAccount: Cannot open - API is not available");
572
+ this.triggerFail({
573
+ errorType: "initialization",
574
+ error: "BisonJibPayAPI is not available",
575
+ });
576
+ return;
577
+ }
578
+
579
+ // Open Moov drop
580
+ this.openMoovDrop();
581
+ }
582
+
583
+ // ==================== VERIFICATION & INITIALIZATION ====================
584
+
585
+ /**
586
+ * Ensure API is initialized (lazy initialization)
587
+ * @returns {boolean} True if API is available
588
+ */
589
+ ensureAPI() {
590
+ if (this.api) {
591
+ return true;
592
+ }
593
+
594
+ // Try to create API if BisonJibPayAPI is now available
595
+ if (typeof BisonJibPayAPI !== "undefined") {
596
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
597
+ console.log("OperatorBankAccount: API initialized lazily");
598
+ return true;
599
+ }
600
+
601
+ console.error(
602
+ "OperatorBankAccount: BisonJibPayAPI is not available. Please ensure component.js is loaded."
603
+ );
604
+ return false;
605
+ }
606
+
607
+ /**
608
+ * Verify operator and initialize Moov token
609
+ */
610
+ async verifyAndInitialize() {
611
+ console.log(
612
+ "🔍 OperatorBankAccount: verifyAndInitialize() called with email:",
613
+ this._state.email
614
+ );
615
+
616
+ const missingMessage = this.getMissingOperatorInfoMessage();
617
+ if (missingMessage) {
618
+ console.warn(
619
+ "❌ OperatorBankAccount: Required fields are missing for verification"
620
+ );
621
+ this._state.isLoading = false;
622
+ this._state.initializationError = true;
623
+ this._state.error = missingMessage;
624
+ this.updateButtonState();
625
+ return;
626
+ }
627
+
628
+ console.log("🔧 OperatorBankAccount: Checking if API is available...");
629
+ // Ensure API is available (lazy initialization)
630
+ if (!this.ensureAPI()) {
631
+ console.error(
632
+ "❌ OperatorBankAccount: ensureAPI() returned false - BisonJibPayAPI is not available"
633
+ );
634
+ console.error("❌ This is why verifyOperator is NOT being called!");
635
+ console.error(
636
+ "💡 Solution: Load component.js with type='module' or use api.js directly"
637
+ );
638
+ this._state.isLoading = false;
639
+ this._state.initializationError = true;
640
+ this.updateButtonState();
641
+ return;
642
+ }
643
+
644
+ console.log(
645
+ "✅ OperatorBankAccount: API is available, proceeding with verification..."
646
+ );
647
+
648
+ try {
649
+ this._state.isLoading = true;
650
+ this._state.error = null;
651
+ this._state.initializationError = false;
652
+ this.updateButtonState();
653
+
654
+ console.log(
655
+ "OperatorBankAccount: Verifying operator:",
656
+ this._state.email
657
+ );
658
+
659
+ // Step 1: Verify operator exists
660
+ const verifyResult = await this.api.verifyOperator(
661
+ this._state.email || undefined,
662
+ this._state.operatorId || undefined,
663
+ this._state.clientId || undefined
664
+ );
665
+
666
+ if (!verifyResult.success) {
667
+ throw new Error(verifyResult.message || "Operator verification failed");
668
+ }
669
+
670
+ console.log("OperatorBankAccount: Operator verified successfully");
671
+
672
+ // Step 2: Get account to retrieve moovAccountId
673
+ // Priority: operatorId > clientId > email
674
+ let accountResult;
675
+ if (this._state.operatorId) {
676
+ accountResult = await this.api.getAccountByOperatorId(this._state.operatorId);
677
+ } else if (this._state.clientId) {
678
+ accountResult = await this.api.getAccountByClientId(this._state.clientId);
679
+ } else {
680
+ accountResult = await this.api.getAccountByEmail(this._state.email);
681
+ }
682
+
683
+ if (!accountResult.data?.moovAccountId) {
684
+ throw new Error("Operator does not have a Moov account");
685
+ }
686
+
687
+ this._state.moovAccountId = accountResult.data.moovAccountId;
688
+ console.log(
689
+ "OperatorBankAccount: moovAccountId:",
690
+ this._state.moovAccountId
691
+ );
692
+
693
+ // Step 3: Generate Moov token
694
+ await this.generateMoovToken();
695
+
696
+ // Mark as verified
697
+ this._state.isVerified = true;
698
+ this._state.isLoading = false;
699
+ this.updateButtonState();
700
+
701
+ // Dispatch ready event
702
+ this.dispatchEvent(
703
+ new CustomEvent("operator-bank-account-ready", {
704
+ detail: {
705
+ email: this._state.email,
706
+ operatorId: this._state.operatorId,
707
+ clientId: this._state.clientId,
708
+ moovAccountId: this._state.moovAccountId,
709
+ },
710
+ bubbles: true,
711
+ composed: true,
712
+ })
713
+ );
714
+ } catch (error) {
715
+ console.error("OperatorBankAccount: Verification failed:", error);
716
+
717
+ this._state.isLoading = false;
718
+ this._state.isVerified = false;
719
+ this._state.initializationError = true;
720
+ this._state.error =
721
+ error.data?.message || error.message || "Verification failed";
722
+ this.updateButtonState();
723
+
724
+ // Dispatch error event
725
+ this.dispatchEvent(
726
+ new CustomEvent("operator-bank-account-error", {
727
+ detail: {
728
+ error: this._state.error,
729
+ type: "verification",
730
+ originalError: error,
731
+ },
732
+ bubbles: true,
733
+ composed: true,
734
+ })
735
+ );
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Generate Moov access token
741
+ * @returns {Promise<boolean>}
742
+ */
743
+ async generateMoovToken() {
744
+ try {
745
+ console.log("OperatorBankAccount: Generating Moov token...");
746
+
747
+ const tokenResult = await this.api.generateMoovToken(
748
+ this._state.email,
749
+ this._state.moovAccountId,
750
+ this._state.operatorId,
751
+ this._state.clientId
752
+ );
753
+
754
+ if (!tokenResult || !tokenResult.data?.accessToken) {
755
+ throw new Error("Failed to generate Moov token");
756
+ }
757
+
758
+ this._state.moovToken = tokenResult.data.accessToken;
759
+
760
+ if (tokenResult.data?.accountID) {
761
+ this._state.moovAccountId = tokenResult.data.accountID;
762
+ }
763
+
764
+ console.log("OperatorBankAccount: Moov token generated successfully");
765
+ return true;
766
+ } catch (error) {
767
+ console.error("OperatorBankAccount: Token generation failed:", error);
768
+ this._state.error = error.message || "Failed to generate Moov token";
769
+ throw error;
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Reset and regenerate token (called on Moov drop close)
775
+ */
776
+ async resetAndRefreshToken() {
777
+ console.log("OperatorBankAccount: Resetting token and accountID...");
778
+
779
+ // Clear current token
780
+ this._state.moovToken = null;
781
+
782
+ // Regenerate token
783
+ try {
784
+ await this.generateMoovToken();
785
+ console.log("OperatorBankAccount: Token refreshed successfully");
786
+ } catch (error) {
787
+ console.error("OperatorBankAccount: Failed to refresh token:", error);
788
+ }
789
+ }
790
+
791
+ // ==================== MOOV DROP ====================
792
+
793
+ /**
794
+ * Open Moov Drop
795
+ */
796
+ async openMoovDrop() {
797
+ console.log("OperatorBankAccount: Opening Moov drop...");
798
+
799
+ // Ensure Moov SDK is loaded
800
+ if (!window.Moov) {
801
+ console.log("OperatorBankAccount: Moov SDK not loaded yet, waiting...");
802
+ try {
803
+ await this.ensureMoovSDK();
804
+ } catch (error) {
805
+ console.error("OperatorBankAccount: Failed to load Moov SDK:", error);
806
+ this.triggerFail({ errorType: "sdk", error: error.message });
807
+ return;
808
+ }
809
+ }
810
+
811
+ // Generate token if not available
812
+ if (!this._state.moovToken) {
813
+ console.log("OperatorBankAccount: Generating Moov token on demand...");
814
+ try {
815
+ await this.generateMoovToken();
816
+ } catch (error) {
817
+ console.error("OperatorBankAccount: Failed to generate token:", error);
818
+ this.triggerFail({ errorType: "token", error: error.message });
819
+ return;
820
+ }
821
+ }
822
+
823
+ // Inject Bison theme styles
824
+ this.injectMoovThemeStyles();
825
+
826
+ // Remove any existing moov-payment-methods element
827
+ const existingMoovDrop = document.getElementById(
828
+ "operator-bank-account-moov-drop"
829
+ );
830
+ if (existingMoovDrop) {
831
+ existingMoovDrop.remove();
832
+ console.log("OperatorBankAccount: Removed existing Moov drop element");
833
+ }
834
+
835
+ // Create fresh moov-payment-methods element
836
+ const moovDrop = document.createElement("moov-payment-methods");
837
+ moovDrop.id = "operator-bank-account-moov-drop";
838
+ document.body.appendChild(moovDrop);
839
+
840
+ // Configure the Moov drop
841
+ moovDrop.token = this._state.moovToken;
842
+ moovDrop.accountID = this._state.moovAccountId;
843
+ moovDrop.microDeposits = false;
844
+ moovDrop.paymentMethodTypes = ["bankAccount"];
845
+
846
+ // Set up callbacks
847
+ moovDrop.onResourceCreated = async (result) => {
848
+ console.log("OperatorBankAccount: Payment method created:", result);
849
+
850
+ // Trigger success callback
851
+ this.triggerSuccess(result);
852
+
853
+ // Dispatch success event
854
+ this.dispatchEvent(
855
+ new CustomEvent("bank-account-added", {
856
+ detail: result,
857
+ bubbles: true,
858
+ composed: true,
859
+ })
860
+ );
861
+ };
862
+
863
+ moovDrop.onError = ({ errorType, error }) => {
864
+ console.error("OperatorBankAccount: Moov error:", errorType, error);
865
+
866
+ // Trigger fail callback
867
+ this.triggerFail({ errorType, error });
868
+
869
+ // Dispatch error event
870
+ this.dispatchEvent(
871
+ new CustomEvent("bank-account-error", {
872
+ detail: { errorType, error },
873
+ bubbles: true,
874
+ composed: true,
875
+ })
876
+ );
877
+ };
878
+
879
+ // Close handler - reset token and accountID
880
+ moovDrop.onClose = async () => {
881
+ console.log("OperatorBankAccount: Moov UI closed");
882
+ moovDrop.open = false;
883
+
884
+ // Reset and refresh token on every close
885
+ await this.resetAndRefreshToken();
886
+
887
+ // Update moov drop with new token if it exists
888
+ if (this._state.moovToken && moovDrop) {
889
+ moovDrop.token = this._state.moovToken;
890
+ moovDrop.accountID = this._state.moovAccountId;
891
+ }
892
+
893
+ // Dispatch close event
894
+ this.dispatchEvent(
895
+ new CustomEvent("moov-drop-close", {
896
+ bubbles: true,
897
+ composed: true,
898
+ })
899
+ );
900
+ };
901
+
902
+ // Cancel handler
903
+ moovDrop.onCancel = async () => {
904
+ console.log("OperatorBankAccount: Moov UI cancelled");
905
+ moovDrop.open = false;
906
+
907
+ // Reset and refresh token on every close
908
+ await this.resetAndRefreshToken();
909
+
910
+ // Update moov drop with new token if it exists
911
+ if (this._state.moovToken && moovDrop) {
912
+ moovDrop.token = this._state.moovToken;
913
+ moovDrop.accountID = this._state.moovAccountId;
914
+ }
915
+
916
+ // Dispatch close event
917
+ this.dispatchEvent(
918
+ new CustomEvent("moov-drop-close", {
919
+ bubbles: true,
920
+ composed: true,
921
+ })
922
+ );
923
+ };
924
+
925
+ // Handle modal close button clicks
926
+ const handleModalClose = async (e) => {
927
+ const target = e.target;
928
+ const modalCloseElement =
929
+ target.closest && target.closest('[data-testid="modalClose"]');
930
+
931
+ if (modalCloseElement) {
932
+ console.log("OperatorBankAccount: Modal close button clicked");
933
+ this.closeMoovDrop();
934
+ }
935
+ };
936
+
937
+ document.addEventListener("click", handleModalClose, true);
938
+
939
+ if (!this._modalCloseHandlers) {
940
+ this._modalCloseHandlers = [];
941
+ }
942
+ this._modalCloseHandlers.push(handleModalClose);
943
+
944
+ // Open the Moov drop
945
+ console.log("OperatorBankAccount: Setting moovDrop.open = true");
946
+ moovDrop.open = true;
947
+
948
+ // Store reference
949
+ this._moovRef = moovDrop;
950
+ }
951
+
952
+ /**
953
+ * Close the Moov UI
954
+ */
955
+ async closeMoovDrop() {
956
+ if (this._moovRef && this._moovRef.open) {
957
+ console.log("OperatorBankAccount: Closing Moov UI");
958
+ this._moovRef.open = false;
959
+
960
+ // Reset and refresh token
961
+ await this.resetAndRefreshToken();
962
+
963
+ this._moovRef = null;
964
+ }
965
+ }
966
+
967
+ // ==================== CALLBACKS ====================
968
+
969
+ /**
970
+ * Trigger success callback
971
+ */
972
+ triggerSuccess(result) {
973
+ if (typeof this._onSuccess === "function") {
974
+ this._onSuccess(result);
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Trigger fail callback
980
+ */
981
+ triggerFail(error) {
982
+ if (typeof this._onFail === "function") {
983
+ this._onFail(error);
984
+ }
985
+ }
986
+
987
+ // ==================== UI UPDATES ====================
988
+
989
+ /**
990
+ * Update button state based on verification status
991
+ */
992
+ updateButtonState() {
993
+ const button = this.shadowRoot.querySelector(".add-bank-btn");
994
+ const wrapper = this.shadowRoot.querySelector(".btn-wrapper");
995
+ const tooltip = this.shadowRoot.querySelector(".tooltip");
996
+
997
+ if (!button) return;
998
+
999
+ // Handle loading state
1000
+ if (this._state.isLoading) {
1001
+ button.classList.add("loading");
1002
+ button.classList.remove("error");
1003
+ button.disabled = true;
1004
+ if (wrapper) wrapper.classList.remove("has-error");
1005
+ }
1006
+ // Handle error state (not verified)
1007
+ else if (this._state.initializationError || !this._state.isVerified) {
1008
+ button.classList.remove("loading");
1009
+ button.classList.add("error");
1010
+ button.disabled = true;
1011
+ if (wrapper) wrapper.classList.add("has-error");
1012
+
1013
+ // Update tooltip text based on error
1014
+ if (tooltip && this._state.error) {
1015
+ tooltip.textContent = this._state.error;
1016
+ }
1017
+ }
1018
+ // Handle normal state (verified)
1019
+ else {
1020
+ button.classList.remove("loading", "error");
1021
+ button.disabled = false;
1022
+ if (wrapper) wrapper.classList.remove("has-error");
1023
+ }
1024
+ }
1025
+
1026
+ // ==================== RENDERING ====================
1027
+
1028
+ render() {
1029
+ this.shadowRoot.innerHTML = `
1030
+ <style>
1031
+ :host {
1032
+ display: inline-block;
1033
+ font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
1034
+ color: var(--color-secondary, #5f6e78);
1035
+ }
1036
+
1037
+ .btn-wrapper {
1038
+ position: relative;
1039
+ display: inline-block;
1040
+ }
1041
+
1042
+ .add-bank-btn {
1043
+ padding: 12px 24px;
1044
+ background: var(--color-primary, #4c7b63);
1045
+ color: var(--color-white, #fff);
1046
+ border: none;
1047
+ border-radius: var(--radius-xl, 0.75rem);
1048
+ font-size: var(--text-sm, 0.875rem);
1049
+ font-weight: var(--font-weight-medium, 500);
1050
+ cursor: pointer;
1051
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1052
+ display: inline-flex;
1053
+ align-items: center;
1054
+ gap: 8px;
1055
+ height: 40px;
1056
+ box-sizing: border-box;
1057
+ }
1058
+
1059
+ .add-bank-btn:hover:not(.error):not(.loading) {
1060
+ background: var(--color-primary-hover, #436c57);
1061
+ }
1062
+
1063
+ .add-bank-btn:active:not(.error):not(.loading) {
1064
+ background: var(--color-primary-active, #3d624f);
1065
+ transform: translateY(0);
1066
+ }
1067
+
1068
+ .add-bank-btn.error {
1069
+ background: var(--color-gray-400, #9ca3af);
1070
+ cursor: not-allowed;
1071
+ }
1072
+
1073
+ .add-bank-btn.loading {
1074
+ background: var(--color-primary-soft, #678f7a);
1075
+ cursor: wait;
1076
+ }
1077
+
1078
+ .add-bank-btn .bank-icon {
1079
+ width: 18px;
1080
+ height: 18px;
1081
+ }
1082
+
1083
+ .add-bank-btn .loading-spinner {
1084
+ display: none;
1085
+ width: 16px;
1086
+ height: 16px;
1087
+ border: 2px solid rgba(255, 255, 255, 0.3);
1088
+ border-top-color: var(--color-white, #fff);
1089
+ border-radius: 50%;
1090
+ animation: spin 0.8s linear infinite;
1091
+ }
1092
+
1093
+ .add-bank-btn.loading .loading-spinner {
1094
+ display: inline-block;
1095
+ }
1096
+
1097
+ .add-bank-btn.loading .bank-icon {
1098
+ display: none;
1099
+ }
1100
+
1101
+ .add-bank-btn .error-icon {
1102
+ display: none;
1103
+ width: 16px;
1104
+ height: 16px;
1105
+ }
1106
+
1107
+ .add-bank-btn.error .error-icon {
1108
+ display: inline-block;
1109
+ }
1110
+
1111
+ .add-bank-btn.error .bank-icon {
1112
+ display: none;
1113
+ }
1114
+
1115
+ @keyframes spin {
1116
+ to {
1117
+ transform: rotate(360deg);
1118
+ }
1119
+ }
1120
+
1121
+ .tooltip {
1122
+ visibility: hidden;
1123
+ opacity: 0;
1124
+ position: absolute;
1125
+ bottom: 100%;
1126
+ left: 50%;
1127
+ transform: translateX(-50%);
1128
+ background: var(--color-gray-700, #374151);
1129
+ color: var(--color-white, #fff);
1130
+ padding: 8px 12px;
1131
+ border-radius: var(--radius-lg, 0.5rem);
1132
+ font-size: 13px;
1133
+ white-space: nowrap;
1134
+ margin-bottom: 8px;
1135
+ transition: opacity var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1)),
1136
+ visibility var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1137
+ z-index: 10002;
1138
+ }
1139
+
1140
+ .tooltip::after {
1141
+ content: '';
1142
+ position: absolute;
1143
+ top: 100%;
1144
+ left: 50%;
1145
+ transform: translateX(-50%);
1146
+ border: 6px solid transparent;
1147
+ border-top-color: var(--color-gray-700, #374151);
1148
+ }
1149
+
1150
+ .btn-wrapper:hover .tooltip {
1151
+ visibility: visible;
1152
+ opacity: 1;
1153
+ }
1154
+
1155
+ .btn-wrapper:not(.has-error) .tooltip {
1156
+ display: none;
1157
+ }
1158
+ </style>
1159
+
1160
+ <div class="btn-wrapper">
1161
+ <span class="tooltip">Email, operator ID, or client ID is required</span>
1162
+ <button class="add-bank-btn error">
1163
+ <span class="loading-spinner"></span>
1164
+ <svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1165
+ <circle cx="12" cy="12" r="10"></circle>
1166
+ <line x1="12" y1="8" x2="12" y2="12"></line>
1167
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
1168
+ </svg>
1169
+ <svg class="bank-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1170
+ <rect x="3" y="10" width="18" height="11" rx="2" ry="2"></rect>
1171
+ <path d="M12 3L2 10h20L12 3z"></path>
1172
+ </svg>
1173
+ Add Bank Account
1174
+ </button>
1175
+ </div>
1176
+ `;
1177
+ }
1178
+ }
1179
+
1180
+ // Register the custom element only if it hasn't been registered yet
1181
+ if (!customElements.get("operator-bank-account")) {
1182
+ customElements.define("operator-bank-account", OperatorBankAccount);
1183
+ }
1184
+
1185
+ // Export for module usage
1186
+ if (typeof module !== "undefined" && module.exports) {
1187
+ module.exports = { OperatorBankAccount };
1188
+ }
1189
+
1190
+ // Make available globally for script tag usage
1191
+ if (typeof window !== "undefined") {
1192
+ window.OperatorBankAccount = OperatorBankAccount;
1193
+ }