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