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