bison-web-components 1.7.0 → 3.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/.vscode/settings.json +3 -0
- package/api.js +269 -0
- package/bison-operator-payments.js +868 -38
- package/demo-payments.html +91 -64
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* onUnlinkError(error) – called when unlinking bank account(s) fails
|
|
26
26
|
*
|
|
27
27
|
* @author @kfajardo
|
|
28
|
-
* @version
|
|
28
|
+
* @version 2.0.0
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
const BOP_BISON_LOGO =
|
|
@@ -95,7 +95,7 @@ class BisonJibPayAPI {
|
|
|
95
95
|
if (!embeddableKey || typeof embeddableKey !== 'string' || !embeddableKey.trim()) {
|
|
96
96
|
throw new Error("Missing required 'x-embeddable-key' for BisonJibPayAPI");
|
|
97
97
|
}
|
|
98
|
-
this.baseURL = baseURL || "https://bison-
|
|
98
|
+
this.baseURL = baseURL || "https://bison-backend-development-hhgrdbhcbwhahdfk.southeastasia-01.azurewebsites.net";
|
|
99
99
|
this.embeddableKey = embeddableKey;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -288,10 +288,8 @@ class BisonJibPayAPI {
|
|
|
288
288
|
* const api = new BisonJibPayAPI(baseURL, embeddableKey);
|
|
289
289
|
* const tokenData = await api.generateMoovToken('operator@example.com');
|
|
290
290
|
* console.log(tokenData.access_token);
|
|
291
|
-
|
|
291
|
+
*/
|
|
292
292
|
async generateMoovToken(operatorEmail, moovAccountId = null, operatorId = null, clientId = null) {
|
|
293
|
-
console.log("CALLED GENERATE MOOV TOKEN");
|
|
294
|
-
|
|
295
293
|
// Use provided moovAccountId or fetch it if not provided
|
|
296
294
|
let accountId = moovAccountId;
|
|
297
295
|
if (!accountId) {
|
|
@@ -315,7 +313,6 @@ class BisonJibPayAPI {
|
|
|
315
313
|
};
|
|
316
314
|
}
|
|
317
315
|
}
|
|
318
|
-
console.log("MOOV ACCOUNT ID", accountId);
|
|
319
316
|
let accountScopes = [
|
|
320
317
|
"/accounts/{ACCOUNT_ID}/bank-accounts.read",
|
|
321
318
|
"/accounts/{ACCOUNT_ID}/bank-accounts.write",
|
|
@@ -381,7 +378,7 @@ class BisonJibPayAPI {
|
|
|
381
378
|
return this.request("/api/embeddable/plaid/link-token", {
|
|
382
379
|
method: "POST",
|
|
383
380
|
body: JSON.stringify({
|
|
384
|
-
clientName:
|
|
381
|
+
clientName: "BisonJibPay",
|
|
385
382
|
countryCodes: ["US"],
|
|
386
383
|
user: {
|
|
387
384
|
clientUserId: "wio-email",
|
|
@@ -393,6 +390,187 @@ class BisonJibPayAPI {
|
|
|
393
390
|
});
|
|
394
391
|
}
|
|
395
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Create Plaid Link token using the new Plaid flow.
|
|
395
|
+
*
|
|
396
|
+
* @param {Object} payload
|
|
397
|
+
* @returns {Promise<any>}
|
|
398
|
+
*/
|
|
399
|
+
async createPlaidLinkToken(payload) {
|
|
400
|
+
if (!payload?.user?.clientUserId) {
|
|
401
|
+
throw {
|
|
402
|
+
status: 400,
|
|
403
|
+
data: {
|
|
404
|
+
success: false,
|
|
405
|
+
message: "user.clientUserId is required",
|
|
406
|
+
errors: ["payload.user.clientUserId parameter is missing"],
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return this.request("/api/plaid/create-token", {
|
|
412
|
+
method: "POST",
|
|
413
|
+
body: JSON.stringify(payload),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Generate Plaid embeddable Link token
|
|
419
|
+
*
|
|
420
|
+
* Calls POST /api/plaid/embeddable/create-token with entityId as query param.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} entityId - Entity UUID (required)
|
|
423
|
+
* @param {Object} payload - Plaid create-token request payload
|
|
424
|
+
* @returns {Promise<any>}
|
|
425
|
+
*/
|
|
426
|
+
async generatePlaidLinkToken(entityId, payload = {}) {
|
|
427
|
+
if (!entityId || typeof entityId !== "string" || !entityId.trim()) {
|
|
428
|
+
throw {
|
|
429
|
+
status: 400,
|
|
430
|
+
data: {
|
|
431
|
+
success: false,
|
|
432
|
+
message: "entityId is required",
|
|
433
|
+
errors: ["entityId parameter is missing"],
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const isUuid =
|
|
439
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
440
|
+
entityId
|
|
441
|
+
);
|
|
442
|
+
if (!isUuid) {
|
|
443
|
+
throw {
|
|
444
|
+
status: 400,
|
|
445
|
+
data: {
|
|
446
|
+
success: false,
|
|
447
|
+
message: "entityId must be a valid UUID",
|
|
448
|
+
errors: ["entityId parameter must be a UUID"],
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const params = new URLSearchParams();
|
|
454
|
+
params.append("entityId", entityId);
|
|
455
|
+
|
|
456
|
+
return this.request(`/api/plaid/embeddable/create-token?${params.toString()}`, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
body: JSON.stringify(payload),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Register a Plaid-linked bank account using public token and selected account.
|
|
464
|
+
*
|
|
465
|
+
* @param {Object} payload
|
|
466
|
+
* @returns {Promise<any>}
|
|
467
|
+
*/
|
|
468
|
+
async registerPlaidBankAccount(payload) {
|
|
469
|
+
if (!payload?.publicToken) {
|
|
470
|
+
throw {
|
|
471
|
+
status: 400,
|
|
472
|
+
data: {
|
|
473
|
+
success: false,
|
|
474
|
+
message: "publicToken is required",
|
|
475
|
+
errors: ["payload.publicToken parameter is missing"],
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!payload?.accountId) {
|
|
481
|
+
throw {
|
|
482
|
+
status: 400,
|
|
483
|
+
data: {
|
|
484
|
+
success: false,
|
|
485
|
+
message: "accountId is required",
|
|
486
|
+
errors: ["payload.accountId parameter is missing"],
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (payload?.entityType !== 0 && payload?.entityType !== 1) {
|
|
492
|
+
throw {
|
|
493
|
+
status: 400,
|
|
494
|
+
data: {
|
|
495
|
+
success: false,
|
|
496
|
+
message: "entityType must be 0 (WIO) or 1 (Operator)",
|
|
497
|
+
errors: ["payload.entityType must be 0 or 1"],
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!payload?.entityId) {
|
|
503
|
+
throw {
|
|
504
|
+
status: 400,
|
|
505
|
+
data: {
|
|
506
|
+
success: false,
|
|
507
|
+
message: "entityId is required",
|
|
508
|
+
errors: ["payload.entityId parameter is missing"],
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return this.request("/api/plaid/embeddable/register-bank-account", {
|
|
514
|
+
method: "POST",
|
|
515
|
+
body: JSON.stringify(payload),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Retry Plaid bank account registration (embeddable)
|
|
521
|
+
*
|
|
522
|
+
* Calls POST /api/plaid/embeddable/retry-registration.
|
|
523
|
+
*
|
|
524
|
+
* @param {Object} payload - Retry registration payload
|
|
525
|
+
* @param {number} payload.entityType - 0 for WIO, 1 for Operator
|
|
526
|
+
* @param {string} payload.entityId - Entity ID
|
|
527
|
+
* @param {string[]} payload.providers - Provider names to retry against
|
|
528
|
+
* @returns {Promise<any>}
|
|
529
|
+
*/
|
|
530
|
+
async retryEmbeddablePlaidRegistration(payload = {}) {
|
|
531
|
+
if (payload?.entityType !== 0 && payload?.entityType !== 1) {
|
|
532
|
+
throw {
|
|
533
|
+
status: 400,
|
|
534
|
+
data: {
|
|
535
|
+
success: false,
|
|
536
|
+
message: "entityType must be 0 (WIO) or 1 (Operator)",
|
|
537
|
+
errors: ["payload.entityType must be 0 or 1"],
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!payload?.entityId || typeof payload.entityId !== "string" || !payload.entityId.trim()) {
|
|
543
|
+
throw {
|
|
544
|
+
status: 400,
|
|
545
|
+
data: {
|
|
546
|
+
success: false,
|
|
547
|
+
message: "entityId is required",
|
|
548
|
+
errors: ["payload.entityId parameter is missing"],
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
!Array.isArray(payload?.providers) ||
|
|
555
|
+
payload.providers.length === 0 ||
|
|
556
|
+
payload.providers.some((provider) => typeof provider !== "string" || !provider.trim())
|
|
557
|
+
) {
|
|
558
|
+
throw {
|
|
559
|
+
status: 400,
|
|
560
|
+
data: {
|
|
561
|
+
success: false,
|
|
562
|
+
message: "providers is required",
|
|
563
|
+
errors: ["payload.providers must be a non-empty string array"],
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return this.request("/api/plaid/embeddable/retry-registration", {
|
|
569
|
+
method: "POST",
|
|
570
|
+
body: JSON.stringify(payload),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
396
574
|
/**
|
|
397
575
|
* Create Plaid processor token
|
|
398
576
|
*
|
|
@@ -761,6 +939,14 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
761
939
|
this._linkModalDirty = false;
|
|
762
940
|
this._linkModalBeforeUnload = null;
|
|
763
941
|
this._linkModalPopState = null;
|
|
942
|
+
this._isPlaidLinkInProgress = false;
|
|
943
|
+
this._isPlaidLinkFinalizing = false;
|
|
944
|
+
this._isPlaidRetryInProgress = false;
|
|
945
|
+
this._plaidLinkErrorState = null;
|
|
946
|
+
this._plaidRetryPayload = null;
|
|
947
|
+
this._plaidLinkToken = null;
|
|
948
|
+
this._plaidLinkHandler = null;
|
|
949
|
+
this._plaidScriptPromise = null;
|
|
764
950
|
|
|
765
951
|
// Embeddable key gate
|
|
766
952
|
this._embeddableKey = null;
|
|
@@ -1183,12 +1369,8 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1183
1369
|
|
|
1184
1370
|
/** Internal dev-friendly logger, prefixed for easy identification. */
|
|
1185
1371
|
_log(msg, data) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
console.log(prefix, msg, data);
|
|
1189
|
-
} else {
|
|
1190
|
-
console.log(prefix, msg);
|
|
1191
|
-
}
|
|
1372
|
+
void msg;
|
|
1373
|
+
void data;
|
|
1192
1374
|
}
|
|
1193
1375
|
|
|
1194
1376
|
_resetState() {
|
|
@@ -1203,6 +1385,13 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1203
1385
|
this._unlinkResult = null;
|
|
1204
1386
|
this._isFetchingAccounts = false;
|
|
1205
1387
|
this._pendingLinkedAccount = null;
|
|
1388
|
+
this._isPlaidLinkInProgress = false;
|
|
1389
|
+
this._isPlaidLinkFinalizing = false;
|
|
1390
|
+
this._isPlaidRetryInProgress = false;
|
|
1391
|
+
this._plaidLinkErrorState = null;
|
|
1392
|
+
this._plaidRetryPayload = null;
|
|
1393
|
+
this._plaidLinkToken = null;
|
|
1394
|
+
this._plaidLinkHandler = null;
|
|
1206
1395
|
this._resetLinkModal();
|
|
1207
1396
|
}
|
|
1208
1397
|
|
|
@@ -1269,7 +1458,19 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1269
1458
|
}
|
|
1270
1459
|
}
|
|
1271
1460
|
|
|
1272
|
-
_handleClose() {
|
|
1461
|
+
_handleClose(showBlockedAlert = true) {
|
|
1462
|
+
if (this._isPlaidLinkInProgress) {
|
|
1463
|
+
if (!showBlockedAlert) return;
|
|
1464
|
+
let message = "Please finish or exit the Plaid flow before closing this window.";
|
|
1465
|
+
if (this._isPlaidRetryInProgress) {
|
|
1466
|
+
message = "Please wait — we're retrying your bank link.";
|
|
1467
|
+
} else if (this._isPlaidLinkFinalizing) {
|
|
1468
|
+
message = "Please wait — we're finishing your bank link.";
|
|
1469
|
+
}
|
|
1470
|
+
window.alert(message);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1273
1474
|
if (this._linkModalOpen && this._linkModalSubmitting) {
|
|
1274
1475
|
// Submitting — cannot close (keep native alert for this blocking case)
|
|
1275
1476
|
window.alert(
|
|
@@ -1332,6 +1533,7 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1332
1533
|
}
|
|
1333
1534
|
|
|
1334
1535
|
_handleAccountToggle(id) {
|
|
1536
|
+
if (this._isPlaidLinkInProgress) return;
|
|
1335
1537
|
if (this._selectedAccounts.has(id)) this._selectedAccounts.delete(id);
|
|
1336
1538
|
else this._selectedAccounts.add(id);
|
|
1337
1539
|
// Update selection state in-place without re-rendering
|
|
@@ -1504,8 +1706,528 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1504
1706
|
this._navigateStep("select-accounts", -1);
|
|
1505
1707
|
}
|
|
1506
1708
|
|
|
1507
|
-
_handleLinkAccountClick() {
|
|
1508
|
-
this.
|
|
1709
|
+
async _handleLinkAccountClick() {
|
|
1710
|
+
if (this._isPlaidLinkInProgress) return;
|
|
1711
|
+
|
|
1712
|
+
const api = await this._getApi();
|
|
1713
|
+
if (
|
|
1714
|
+
!api ||
|
|
1715
|
+
typeof api.generatePlaidLinkToken !== "function" ||
|
|
1716
|
+
typeof api.registerPlaidBankAccount !== "function"
|
|
1717
|
+
) {
|
|
1718
|
+
const err = { message: "Plaid API handlers are not available on the API instance." };
|
|
1719
|
+
this._log("Error linking account via Plaid:", err);
|
|
1720
|
+
if (typeof this.onLinkError === "function") this.onLinkError(err);
|
|
1721
|
+
window.alert(err.message);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
this._isPlaidLinkInProgress = true;
|
|
1726
|
+
this._isPlaidLinkFinalizing = false;
|
|
1727
|
+
this._isPlaidRetryInProgress = false;
|
|
1728
|
+
this._clearPlaidLinkErrorState();
|
|
1729
|
+
this._plaidRetryPayload = null;
|
|
1730
|
+
this._syncSelectAccountsInteractionState();
|
|
1731
|
+
|
|
1732
|
+
let entityContext = null;
|
|
1733
|
+
try {
|
|
1734
|
+
entityContext = this._resolvePlaidEntityContext();
|
|
1735
|
+
|
|
1736
|
+
const createTokenPayload = {
|
|
1737
|
+
clientName: "BisonJibPay",
|
|
1738
|
+
language: "en",
|
|
1739
|
+
products: ["auth"],
|
|
1740
|
+
countryCodes: ["US"],
|
|
1741
|
+
user: {
|
|
1742
|
+
clientUserId: entityContext.clientUserId,
|
|
1743
|
+
},
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
const tokenResponse = await api.generatePlaidLinkToken(
|
|
1747
|
+
entityContext.entityId,
|
|
1748
|
+
createTokenPayload,
|
|
1749
|
+
);
|
|
1750
|
+
const tokenData = tokenResponse?.data || tokenResponse || {};
|
|
1751
|
+
this._plaidLinkToken = tokenData.linkToken || tokenData.link_token || null;
|
|
1752
|
+
|
|
1753
|
+
if (!this._plaidLinkToken) {
|
|
1754
|
+
throw new Error("Failed to create Plaid Link token.");
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const linkResult = await this._openPlaidLinkAndRegister(
|
|
1758
|
+
api,
|
|
1759
|
+
this._plaidLinkToken,
|
|
1760
|
+
entityContext,
|
|
1761
|
+
);
|
|
1762
|
+
|
|
1763
|
+
// User exited Plaid without completing a link.
|
|
1764
|
+
if (!linkResult) return;
|
|
1765
|
+
|
|
1766
|
+
const registerResponse = linkResult.registerResponse || {};
|
|
1767
|
+
if (this._isPlaidRegistrationFailure(registerResponse)) {
|
|
1768
|
+
this._setPlaidLinkErrorState(
|
|
1769
|
+
registerResponse,
|
|
1770
|
+
"We couldn’t finish linking your account. Please retry.",
|
|
1771
|
+
entityContext,
|
|
1772
|
+
);
|
|
1773
|
+
if (typeof this.onLinkError === "function") this.onLinkError(registerResponse);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (this._operatorId) {
|
|
1778
|
+
await this._fetchBankAccounts(api);
|
|
1779
|
+
} else if (this._step === "select-accounts") {
|
|
1780
|
+
this._renderAccountCards();
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (typeof this.onLinkSuccess === "function") {
|
|
1784
|
+
this.onLinkSuccess(registerResponse?.data || registerResponse);
|
|
1785
|
+
}
|
|
1786
|
+
} catch (err) {
|
|
1787
|
+
const errData = err?.data || err;
|
|
1788
|
+
this._log("Error linking account via Plaid:", errData);
|
|
1789
|
+
if (typeof this.onLinkError === "function") this.onLinkError(errData);
|
|
1790
|
+
const message =
|
|
1791
|
+
errData?.message || err?.message || "Unable to link bank account.";
|
|
1792
|
+
|
|
1793
|
+
// If we already have retry context, show in-modal retry screen.
|
|
1794
|
+
if (this._plaidRetryPayload && this._step === "select-accounts") {
|
|
1795
|
+
this._setPlaidLinkErrorState(errData, message, entityContext);
|
|
1796
|
+
} else {
|
|
1797
|
+
window.alert(message);
|
|
1798
|
+
}
|
|
1799
|
+
} finally {
|
|
1800
|
+
this._isPlaidLinkFinalizing = false;
|
|
1801
|
+
this._isPlaidRetryInProgress = false;
|
|
1802
|
+
this._isPlaidLinkInProgress = false;
|
|
1803
|
+
if (this._step === "select-accounts") {
|
|
1804
|
+
this._renderHeader();
|
|
1805
|
+
this._renderContent();
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async _handlePlaidRetryRegistration() {
|
|
1811
|
+
if (this._isPlaidRetryInProgress) return;
|
|
1812
|
+
if (!this._plaidRetryPayload) return;
|
|
1813
|
+
|
|
1814
|
+
const api = await this._getApi();
|
|
1815
|
+
if (!api || typeof api.retryEmbeddablePlaidRegistration !== "function") {
|
|
1816
|
+
const err = { message: "Plaid retry API handler is not available on the API instance." };
|
|
1817
|
+
this._log("Error retrying Plaid registration:", err);
|
|
1818
|
+
if (typeof this.onLinkError === "function") this.onLinkError(err);
|
|
1819
|
+
this._setPlaidLinkErrorState(err, err.message);
|
|
1820
|
+
this._renderHeader();
|
|
1821
|
+
this._renderContent();
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
this._isPlaidLinkInProgress = true;
|
|
1826
|
+
this._isPlaidRetryInProgress = true;
|
|
1827
|
+
this._isPlaidLinkFinalizing = false;
|
|
1828
|
+
this._renderHeader();
|
|
1829
|
+
this._renderContent();
|
|
1830
|
+
|
|
1831
|
+
try {
|
|
1832
|
+
const retryResponse = await api.retryEmbeddablePlaidRegistration(
|
|
1833
|
+
this._plaidRetryPayload,
|
|
1834
|
+
);
|
|
1835
|
+
|
|
1836
|
+
if (this._isPlaidRegistrationFailure(retryResponse)) {
|
|
1837
|
+
this._setPlaidLinkErrorState(
|
|
1838
|
+
retryResponse,
|
|
1839
|
+
"Retry failed. Please try again.",
|
|
1840
|
+
);
|
|
1841
|
+
if (typeof this.onLinkError === "function") this.onLinkError(retryResponse);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
this._clearPlaidLinkErrorState();
|
|
1846
|
+
this._isPlaidLinkFinalizing = true;
|
|
1847
|
+
this._renderHeader();
|
|
1848
|
+
this._renderContent();
|
|
1849
|
+
|
|
1850
|
+
if (this._operatorId) {
|
|
1851
|
+
await this._fetchBankAccounts(api);
|
|
1852
|
+
} else if (this._step === "select-accounts") {
|
|
1853
|
+
this._renderAccountCards();
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (typeof this.onLinkSuccess === "function") {
|
|
1857
|
+
this.onLinkSuccess(retryResponse?.data || retryResponse);
|
|
1858
|
+
}
|
|
1859
|
+
} catch (err) {
|
|
1860
|
+
const errData = err?.data || err;
|
|
1861
|
+
this._log("Error retrying Plaid registration:", errData);
|
|
1862
|
+
if (typeof this.onLinkError === "function") this.onLinkError(errData);
|
|
1863
|
+
const message =
|
|
1864
|
+
errData?.message || err?.message || "Unable to retry linking bank account.";
|
|
1865
|
+
this._setPlaidLinkErrorState(errData, message);
|
|
1866
|
+
} finally {
|
|
1867
|
+
this._isPlaidLinkFinalizing = false;
|
|
1868
|
+
this._isPlaidRetryInProgress = false;
|
|
1869
|
+
this._isPlaidLinkInProgress = false;
|
|
1870
|
+
if (this._step === "select-accounts") {
|
|
1871
|
+
this._renderHeader();
|
|
1872
|
+
this._renderContent();
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
_dismissPlaidLinkError() {
|
|
1878
|
+
this._clearPlaidLinkErrorState();
|
|
1879
|
+
this._renderHeader();
|
|
1880
|
+
this._renderContent();
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
_getLinkAccountButtonLabel() {
|
|
1884
|
+
return this._isPlaidLinkInProgress
|
|
1885
|
+
? `<span class="bop-spinner">${BOP_ICONS.loader}</span> Linking...`
|
|
1886
|
+
: `${BOP_ICONS.plus} Link Account`;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
_syncSelectAccountsInteractionState() {
|
|
1890
|
+
if (this._step !== "select-accounts") return;
|
|
1891
|
+
|
|
1892
|
+
const closeBtn = this._headerEl?.querySelector(".bop-close-btn");
|
|
1893
|
+
if (closeBtn) closeBtn.disabled = this._isPlaidLinkInProgress;
|
|
1894
|
+
|
|
1895
|
+
const linkAccountBtn = this._contentEl?.querySelector(".bop-link-account-btn");
|
|
1896
|
+
if (linkAccountBtn) {
|
|
1897
|
+
linkAccountBtn.disabled = this._isPlaidLinkInProgress;
|
|
1898
|
+
linkAccountBtn.innerHTML = this._getLinkAccountButtonLabel();
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
this._contentEl
|
|
1902
|
+
?.querySelectorAll(".bop-account-card")
|
|
1903
|
+
.forEach((card) => {
|
|
1904
|
+
card.disabled = this._isPlaidLinkInProgress;
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
const unlinkBtn = this._contentEl?.querySelector(".bop-btn-unlink");
|
|
1908
|
+
if (unlinkBtn) unlinkBtn.disabled = this._isPlaidLinkInProgress;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
_resolvePlaidEntityContext() {
|
|
1912
|
+
const lookupData = this._operatorData?.data || this._operatorData || {};
|
|
1913
|
+
const operatorId = lookupData.operatorId || this._operatorId || null;
|
|
1914
|
+
|
|
1915
|
+
if (operatorId) {
|
|
1916
|
+
return {
|
|
1917
|
+
entityType: 1,
|
|
1918
|
+
entityId: String(operatorId),
|
|
1919
|
+
clientUserId: String(operatorId),
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const wioEntityId = lookupData.wioId || lookupData.entityId || null;
|
|
1924
|
+
if (!wioEntityId) {
|
|
1925
|
+
throw new Error(
|
|
1926
|
+
"Missing entityId for WIO flow. Lookup data must include operatorId, wioId, or entityId.",
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
return {
|
|
1931
|
+
entityType: 0,
|
|
1932
|
+
entityId: String(wioEntityId),
|
|
1933
|
+
clientUserId: String(wioEntityId),
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
_getPlaidAccountType(metadataAccount) {
|
|
1938
|
+
const subtype = (metadataAccount?.subtype || "").toLowerCase();
|
|
1939
|
+
if (subtype.includes("savings")) return "Savings";
|
|
1940
|
+
return "Checking";
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
_buildPlaidRegisterPayload(publicToken, plaidAccountId, entityContext, metadata) {
|
|
1944
|
+
const lookupData = this._operatorData?.data || this._operatorData || {};
|
|
1945
|
+
const metadataAccount = metadata?.accounts?.[0] || null;
|
|
1946
|
+
const payload = {
|
|
1947
|
+
publicToken,
|
|
1948
|
+
accountId: plaidAccountId,
|
|
1949
|
+
entityType: entityContext.entityType,
|
|
1950
|
+
entityId: entityContext.entityId,
|
|
1951
|
+
accountType: this._getPlaidAccountType(metadataAccount),
|
|
1952
|
+
description: "Linked via Plaid Link",
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
const accountHolderName = (lookupData.companyName || "").trim();
|
|
1956
|
+
if (accountHolderName) payload.accountHolderName = accountHolderName;
|
|
1957
|
+
|
|
1958
|
+
return payload;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
_extractPlaidRetryDetails(source) {
|
|
1962
|
+
if (!source || typeof source !== "object") return {};
|
|
1963
|
+
if (
|
|
1964
|
+
source.data &&
|
|
1965
|
+
typeof source.data === "object" &&
|
|
1966
|
+
("success" in source || "message" in source || "errors" in source || "status" in source)
|
|
1967
|
+
) {
|
|
1968
|
+
return source.data;
|
|
1969
|
+
}
|
|
1970
|
+
return source;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
_collectPlaidRetryProviders(details) {
|
|
1974
|
+
const providers = [];
|
|
1975
|
+
|
|
1976
|
+
if (Array.isArray(details?.providers)) {
|
|
1977
|
+
for (const provider of details.providers) {
|
|
1978
|
+
if (typeof provider === "string" && provider.trim()) {
|
|
1979
|
+
providers.push(provider.trim());
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
if (Array.isArray(details?.registrations)) {
|
|
1985
|
+
for (const registration of details.registrations) {
|
|
1986
|
+
const provider = registration?.provider;
|
|
1987
|
+
if (typeof provider === "string" && provider.trim()) {
|
|
1988
|
+
providers.push(provider.trim());
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const unique = Array.from(new Set(providers));
|
|
1994
|
+
return unique.length > 0 ? unique : ["Moov", "Column"];
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
_updatePlaidRetryPayload(entityContext, details = {}) {
|
|
1998
|
+
if (!entityContext?.entityId) return null;
|
|
1999
|
+
|
|
2000
|
+
const existing = this._plaidRetryPayload || {};
|
|
2001
|
+
const next = {
|
|
2002
|
+
entityType: entityContext.entityType,
|
|
2003
|
+
entityId: entityContext.entityId,
|
|
2004
|
+
providers: this._collectPlaidRetryProviders({
|
|
2005
|
+
...details,
|
|
2006
|
+
providers: details.providers || existing.providers,
|
|
2007
|
+
registrations: details.registrations || [],
|
|
2008
|
+
}),
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
const accountId = details.accountId || existing.accountId || null;
|
|
2012
|
+
if (accountId) next.accountId = accountId;
|
|
2013
|
+
|
|
2014
|
+
const plaidItemId = details.plaidItemId || existing.plaidItemId || null;
|
|
2015
|
+
if (plaidItemId) next.plaidItemId = plaidItemId;
|
|
2016
|
+
|
|
2017
|
+
this._plaidRetryPayload = next;
|
|
2018
|
+
return next;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
_isPlaidRegistrationFailure(response) {
|
|
2022
|
+
if (!response || typeof response !== "object") return true;
|
|
2023
|
+
if (response.success === false) return true;
|
|
2024
|
+
|
|
2025
|
+
const data = this._extractPlaidRetryDetails(response);
|
|
2026
|
+
if (data?.allSucceeded === false) return true;
|
|
2027
|
+
|
|
2028
|
+
if (
|
|
2029
|
+
Array.isArray(data?.registrations) &&
|
|
2030
|
+
data.registrations.some((registration) => registration?.success === false)
|
|
2031
|
+
) {
|
|
2032
|
+
return true;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
return false;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
_setPlaidLinkErrorState(source, fallbackMessage, entityContext = null) {
|
|
2039
|
+
const details = this._extractPlaidRetryDetails(source);
|
|
2040
|
+
const retryContext =
|
|
2041
|
+
entityContext ||
|
|
2042
|
+
(this._plaidRetryPayload
|
|
2043
|
+
? {
|
|
2044
|
+
entityType: this._plaidRetryPayload.entityType,
|
|
2045
|
+
entityId: this._plaidRetryPayload.entityId,
|
|
2046
|
+
}
|
|
2047
|
+
: null);
|
|
2048
|
+
if (retryContext) this._updatePlaidRetryPayload(retryContext, details);
|
|
2049
|
+
|
|
2050
|
+
const message =
|
|
2051
|
+
source?.message ||
|
|
2052
|
+
details?.message ||
|
|
2053
|
+
fallbackMessage ||
|
|
2054
|
+
"We couldn’t finish linking your bank account.";
|
|
2055
|
+
|
|
2056
|
+
this._isPlaidLinkFinalizing = false;
|
|
2057
|
+
this._plaidLinkErrorState = {
|
|
2058
|
+
message,
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
_clearPlaidLinkErrorState() {
|
|
2063
|
+
this._plaidLinkErrorState = null;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
async _ensurePlaidLinkLoaded() {
|
|
2067
|
+
if (typeof window !== "undefined" && window.Plaid && typeof window.Plaid.create === "function") {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (!this._plaidScriptPromise) {
|
|
2072
|
+
this._plaidScriptPromise = new Promise((resolve, reject) => {
|
|
2073
|
+
const scriptSrc = "https://cdn.plaid.com/link/v2/stable/link-initialize.js";
|
|
2074
|
+
const existingScript = document.querySelector(`script[src="${scriptSrc}"]`);
|
|
2075
|
+
let timeoutId = null;
|
|
2076
|
+
|
|
2077
|
+
const cleanup = () => {
|
|
2078
|
+
if (timeoutId) {
|
|
2079
|
+
clearTimeout(timeoutId);
|
|
2080
|
+
timeoutId = null;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
const handleLoad = () => {
|
|
2085
|
+
cleanup();
|
|
2086
|
+
resolve();
|
|
2087
|
+
};
|
|
2088
|
+
const handleError = () => {
|
|
2089
|
+
cleanup();
|
|
2090
|
+
reject(new Error("Failed to load Plaid Link script."));
|
|
2091
|
+
};
|
|
2092
|
+
timeoutId = setTimeout(() => {
|
|
2093
|
+
reject(new Error("Timed out while loading Plaid Link script."));
|
|
2094
|
+
}, 10000);
|
|
2095
|
+
|
|
2096
|
+
if (existingScript) {
|
|
2097
|
+
existingScript.addEventListener("load", handleLoad, { once: true });
|
|
2098
|
+
existingScript.addEventListener("error", handleError, { once: true });
|
|
2099
|
+
if (window.Plaid && typeof window.Plaid.create === "function") {
|
|
2100
|
+
cleanup();
|
|
2101
|
+
resolve();
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (existingScript.readyState === "complete" || existingScript.readyState === "loaded") {
|
|
2105
|
+
cleanup();
|
|
2106
|
+
reject(new Error("Plaid Link script is present but Plaid is unavailable."));
|
|
2107
|
+
}
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
const script = document.createElement("script");
|
|
2112
|
+
script.src = scriptSrc;
|
|
2113
|
+
script.async = true;
|
|
2114
|
+
script.onload = handleLoad;
|
|
2115
|
+
script.onerror = handleError;
|
|
2116
|
+
document.head.appendChild(script);
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
try {
|
|
2121
|
+
await this._plaidScriptPromise;
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
this._plaidScriptPromise = null;
|
|
2124
|
+
throw error;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
if (!window.Plaid || typeof window.Plaid.create !== "function") {
|
|
2128
|
+
throw new Error("Plaid Link is not available after script load.");
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
_setPlaidOverlayPriority(isPlaidOpen) {
|
|
2133
|
+
const overlay = this.shadowRoot?.querySelector(".bop-overlay");
|
|
2134
|
+
if (!overlay) return;
|
|
2135
|
+
overlay.classList.toggle("bop-overlay--behind-plaid", Boolean(isPlaidOpen));
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
_ensurePlaidGlobalLayerFix() {
|
|
2139
|
+
if (typeof document === "undefined") return;
|
|
2140
|
+
const styleId = "bop-plaid-link-layer-fix";
|
|
2141
|
+
if (document.getElementById(styleId)) return;
|
|
2142
|
+
|
|
2143
|
+
const style = document.createElement("style");
|
|
2144
|
+
style.id = styleId;
|
|
2145
|
+
style.textContent = `
|
|
2146
|
+
iframe[id^="plaid-link-iframe"],
|
|
2147
|
+
iframe[name^="plaid-link-iframe"],
|
|
2148
|
+
.plaid-link-iframe,
|
|
2149
|
+
.plaid-link-container,
|
|
2150
|
+
.plaid-link-iframe-wrapper {
|
|
2151
|
+
z-index: 2147483647 !important;
|
|
2152
|
+
}
|
|
2153
|
+
`;
|
|
2154
|
+
document.head.appendChild(style);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
async _openPlaidLinkAndRegister(api, linkToken, entityContext) {
|
|
2158
|
+
await this._ensurePlaidLinkLoaded();
|
|
2159
|
+
this._setPlaidOverlayPriority(true);
|
|
2160
|
+
this._ensurePlaidGlobalLayerFix();
|
|
2161
|
+
|
|
2162
|
+
return new Promise((resolve, reject) => {
|
|
2163
|
+
let settled = false;
|
|
2164
|
+
let successStarted = false;
|
|
2165
|
+
const resolveOnce = (value) => {
|
|
2166
|
+
if (settled) return;
|
|
2167
|
+
settled = true;
|
|
2168
|
+
this._setPlaidOverlayPriority(false);
|
|
2169
|
+
resolve(value);
|
|
2170
|
+
};
|
|
2171
|
+
const rejectOnce = (error) => {
|
|
2172
|
+
if (settled) return;
|
|
2173
|
+
settled = true;
|
|
2174
|
+
this._setPlaidOverlayPriority(false);
|
|
2175
|
+
reject(error);
|
|
2176
|
+
};
|
|
2177
|
+
|
|
2178
|
+
try {
|
|
2179
|
+
this._plaidLinkHandler = window.Plaid.create({
|
|
2180
|
+
token: linkToken,
|
|
2181
|
+
onSuccess: async (publicToken, metadata) => {
|
|
2182
|
+
successStarted = true;
|
|
2183
|
+
this._isPlaidLinkFinalizing = true;
|
|
2184
|
+
this._setPlaidOverlayPriority(false);
|
|
2185
|
+
if (this._step === "select-accounts") {
|
|
2186
|
+
this._renderHeader();
|
|
2187
|
+
this._renderContent();
|
|
2188
|
+
}
|
|
2189
|
+
try {
|
|
2190
|
+
const plaidAccountId = metadata?.accounts?.[0]?.id;
|
|
2191
|
+
if (!plaidAccountId) {
|
|
2192
|
+
throw new Error("Plaid did not return an accountId in onSuccess metadata.");
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
this._updatePlaidRetryPayload(entityContext, {
|
|
2196
|
+
accountId: plaidAccountId,
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
const registerPayload = this._buildPlaidRegisterPayload(
|
|
2200
|
+
publicToken,
|
|
2201
|
+
plaidAccountId,
|
|
2202
|
+
entityContext,
|
|
2203
|
+
metadata,
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
const registerResponse = await api.registerPlaidBankAccount(registerPayload);
|
|
2207
|
+
this._updatePlaidRetryPayload(
|
|
2208
|
+
entityContext,
|
|
2209
|
+
this._extractPlaidRetryDetails(registerResponse),
|
|
2210
|
+
);
|
|
2211
|
+
resolveOnce({ registerResponse, metadata });
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
rejectOnce(error);
|
|
2214
|
+
}
|
|
2215
|
+
},
|
|
2216
|
+
onExit: (error) => {
|
|
2217
|
+
if (successStarted) return;
|
|
2218
|
+
if (error) {
|
|
2219
|
+
rejectOnce(error);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
resolveOnce(null);
|
|
2223
|
+
},
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
this._plaidLinkHandler.open();
|
|
2227
|
+
} catch (error) {
|
|
2228
|
+
rejectOnce(error);
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
1509
2231
|
}
|
|
1510
2232
|
|
|
1511
2233
|
_getHeaderTitle() {
|
|
@@ -1513,6 +2235,8 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1513
2235
|
case "loading":
|
|
1514
2236
|
return this._selectedBank?.name || "Connecting";
|
|
1515
2237
|
case "select-accounts":
|
|
2238
|
+
if (this._isPlaidLinkFinalizing) return "Linking Account";
|
|
2239
|
+
if (this._plaidLinkErrorState) return "Link Failed";
|
|
1516
2240
|
return "Manage Accounts";
|
|
1517
2241
|
case "confirm-unlink":
|
|
1518
2242
|
return "Unlink Account";
|
|
@@ -1973,12 +2697,13 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1973
2697
|
@keyframes bopSlideFromLeft{from{opacity:0;transform:translateX(-10px)}to{opacity:1;transform:translateX(0)}}
|
|
1974
2698
|
@keyframes bopFadeIn{from{opacity:0}to{opacity:1}}
|
|
1975
2699
|
@keyframes bopFadeOut{from{opacity:1}to{opacity:0}}
|
|
1976
|
-
@keyframes bopExpandIn{from{opacity:0;max-height:0;padding-top:0;padding-bottom:0}to{opacity:1;max-height:60px;padding-top:.75rem;padding-bottom:.75rem}}
|
|
2700
|
+
@keyframes bopExpandIn{from{opacity:0;max-height:0;padding-top:0;padding-bottom:0}to{opacity:1;max-height:60px;padding-top:.75rem;padding-bottom:.75rem}}
|
|
1977
2701
|
|
|
1978
|
-
.bop-overlay{position:fixed;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:1rem;border:none;background:transparent;max-width:none;max-height:none;width:100%;height:100%;margin:0}
|
|
1979
|
-
.bop-overlay
|
|
1980
|
-
.bop-overlay
|
|
1981
|
-
.bop-overlay[data-state="
|
|
2702
|
+
.bop-overlay{position:fixed;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:1rem;border:none;background:transparent;max-width:none;max-height:none;width:100%;height:100%;margin:0}
|
|
2703
|
+
.bop-overlay.bop-overlay--behind-plaid{z-index:-1!important;opacity:0!important;pointer-events:none!important}
|
|
2704
|
+
.bop-overlay::backdrop{background:transparent}
|
|
2705
|
+
.bop-overlay[data-state="open"]{pointer-events:auto}
|
|
2706
|
+
.bop-overlay[data-state="closed"]{pointer-events:none}
|
|
1982
2707
|
.bop-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.4);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);animation:bopBackdropIn .3s var(--bop-ease) forwards}
|
|
1983
2708
|
.bop-overlay[data-state="closing"] .bop-backdrop{animation:bopBackdropOut .3s var(--bop-ease) forwards}
|
|
1984
2709
|
.bop-modal{position:relative;width:100%;max-width:448px;height:520px;background:#fff;border:1px solid var(--bop-border);box-shadow:var(--bop-shadow-2xl);border-radius:var(--bop-radius-xl);overflow:hidden;display:flex;flex-direction:column;max-height:90vh;animation:bopModalIn .3s var(--bop-ease-spring) forwards;transition:max-width .4s var(--bop-ease-spring),height .4s var(--bop-ease-spring)}
|
|
@@ -1994,6 +2719,8 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
1994
2719
|
.bop-header-logo{width:22px;height:22px;object-fit:contain;flex-shrink:0}
|
|
1995
2720
|
.bop-close-btn{padding:.375rem;margin-right:-.375rem;color:var(--bop-secondary);background:transparent;border:none;border-radius:var(--bop-radius-md);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:color var(--bop-dur-norm) var(--bop-ease),background var(--bop-dur-norm) var(--bop-ease)}
|
|
1996
2721
|
.bop-close-btn:hover{color:var(--bop-headline);background:var(--bop-sidebar)}
|
|
2722
|
+
.bop-close-btn:disabled{opacity:.45;cursor:not-allowed}
|
|
2723
|
+
.bop-close-btn:disabled:hover{color:var(--bop-secondary);background:transparent}
|
|
1997
2724
|
|
|
1998
2725
|
.bop-content{position:relative;flex:1;overflow:hidden;min-height:0;background:rgba(248,250,252,.3);transition:opacity .2s var(--bop-ease)}
|
|
1999
2726
|
.bop-content.bop-content-fading{opacity:0}
|
|
@@ -2064,6 +2791,8 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2064
2791
|
.bop-accounts-fetch-text{font-size:var(--bop-xs);font-weight:500;color:var(--bop-primary)}
|
|
2065
2792
|
.bop-account-card{width:100%;display:flex;align-items:center;padding:1rem;border-radius:var(--bop-radius-xl);border:2px solid transparent;background:#fff;box-shadow:var(--bop-shadow-sm);cursor:pointer;transition:all var(--bop-dur-norm) var(--bop-ease);font-family:var(--bop-font);text-align:left;animation:bopItemFade .3s var(--bop-ease) forwards;opacity:0}
|
|
2066
2793
|
.bop-account-card:hover{border-color:rgba(76,123,99,.2);box-shadow:var(--bop-shadow-md)}
|
|
2794
|
+
.bop-account-card:disabled{opacity:.7;cursor:not-allowed;pointer-events:none}
|
|
2795
|
+
.bop-account-card:disabled:hover{border-color:transparent;box-shadow:var(--bop-shadow-sm)}
|
|
2067
2796
|
.bop-account-card[data-selected="true"]{border-color:var(--bop-primary);background:rgba(76,123,99,.05)}
|
|
2068
2797
|
.bop-card-inner{display:flex;align-items:center;gap:1rem;flex:1}
|
|
2069
2798
|
.bop-check-circle{width:1.5rem;height:1.5rem;border-radius:var(--bop-radius-full);border:2px solid var(--bop-border);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all var(--bop-dur-norm) var(--bop-ease)}
|
|
@@ -2150,15 +2879,17 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2150
2879
|
.bop-btn-done:active{transform:scale(.98)}
|
|
2151
2880
|
.bop-btn-label{animation:bopFadeIn .2s var(--bop-ease) forwards}
|
|
2152
2881
|
.bop-btn-loading{display:flex;align-items:center;justify-content:center;gap:.5rem;position:absolute;inset:0;animation:bopFadeIn .2s var(--bop-ease) forwards}
|
|
2153
|
-
.bop-spinner{animation:bopSpin 1s linear infinite}
|
|
2882
|
+
.bop-spinner{display:inline-flex;animation:bopSpin 1s linear infinite}
|
|
2154
2883
|
.bop-hidden{display:none!important}
|
|
2155
2884
|
.bop-loading-view{padding:2rem;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:#fff;gap:1.5rem}
|
|
2156
2885
|
.bop-loading-logo{width:4.5rem;height:4.5rem;border-radius:1.25rem;display:flex;align-items:center;justify-content:center;color:#fff;box-shadow:var(--bop-shadow-md);outline:1px solid rgba(0,0,0,.05);overflow:hidden;animation:bopFadeInUp .5s var(--bop-ease-spring) forwards,bopBreath 2s ease-in-out .5s infinite}
|
|
2157
2886
|
.bop-loading-body{display:flex;flex-direction:column;align-items:center;gap:.375rem;opacity:0;animation:bopFadeInUp .4s var(--bop-ease) .15s forwards}
|
|
2158
2887
|
.bop-loading-title{font-size:var(--bop-base);font-weight:600;color:var(--bop-headline)}
|
|
2159
2888
|
.bop-loading-text{font-size:var(--bop-sm);color:var(--bop-secondary);line-height:1.5}
|
|
2160
|
-
.bop-loading-
|
|
2161
|
-
.bop-loading-
|
|
2889
|
+
.bop-loading-indicator{display:inline-flex;align-items:center;gap:.5rem;opacity:0;animation:bopFadeIn .3s var(--bop-ease) .3s forwards}
|
|
2890
|
+
.bop-loading-spinner{display:inline-flex;color:var(--bop-primary);animation:bopSpin 1s linear infinite}
|
|
2891
|
+
.bop-loading-spinner svg{width:1rem;height:1rem}
|
|
2892
|
+
.bop-loading-indicator-text{font-size:var(--bop-xs);color:var(--bop-secondary)}
|
|
2162
2893
|
.bop-loading-secure{display:flex;align-items:center;gap:.375rem;font-size:var(--bop-xs);color:var(--bop-secondary);opacity:0;animation:bopFadeIn .3s var(--bop-ease) .45s forwards}
|
|
2163
2894
|
.bop-loading-secure svg{color:#10b981}
|
|
2164
2895
|
@keyframes bopBreath{0%,100%{transform:scale(1)}50%{transform:scale(1.04)}}
|
|
@@ -2273,12 +3004,12 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2273
3004
|
// Intercept native ESC key behavior to use smooth animation
|
|
2274
3005
|
overlay.addEventListener("cancel", (e) => {
|
|
2275
3006
|
e.preventDefault();
|
|
2276
|
-
this._handleClose();
|
|
3007
|
+
this._handleClose(false);
|
|
2277
3008
|
});
|
|
2278
3009
|
|
|
2279
3010
|
const backdrop = document.createElement("div");
|
|
2280
3011
|
backdrop.className = "bop-backdrop";
|
|
2281
|
-
backdrop.addEventListener("click", () => this._handleClose());
|
|
3012
|
+
backdrop.addEventListener("click", () => this._handleClose(false));
|
|
2282
3013
|
overlay.appendChild(backdrop);
|
|
2283
3014
|
|
|
2284
3015
|
const modal = document.createElement("div");
|
|
@@ -2296,11 +3027,9 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2296
3027
|
this._renderContent();
|
|
2297
3028
|
|
|
2298
3029
|
this.shadowRoot.appendChild(overlay);
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
overlay.setAttribute("open", "");
|
|
2303
|
-
}
|
|
3030
|
+
// Use non-modal dialog mode so external overlays (e.g., Plaid Link iframe)
|
|
3031
|
+
// can stack above this component via normal z-index rules.
|
|
3032
|
+
overlay.setAttribute("open", "");
|
|
2304
3033
|
}
|
|
2305
3034
|
|
|
2306
3035
|
_renderHeader() {
|
|
@@ -2329,7 +3058,8 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2329
3058
|
const close = document.createElement("button");
|
|
2330
3059
|
close.className = "bop-close-btn";
|
|
2331
3060
|
close.innerHTML = BOP_ICONS.x;
|
|
2332
|
-
close.
|
|
3061
|
+
close.disabled = this._isPlaidLinkInProgress;
|
|
3062
|
+
close.addEventListener("click", () => this._handleClose(false));
|
|
2333
3063
|
this._headerEl.appendChild(close);
|
|
2334
3064
|
}
|
|
2335
3065
|
|
|
@@ -2488,7 +3218,10 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2488
3218
|
<p class="bop-loading-title">${bankName}</p>
|
|
2489
3219
|
<p class="bop-loading-text">Securely retrieving your accounts</p>
|
|
2490
3220
|
</div>
|
|
2491
|
-
<div class="bop-loading-
|
|
3221
|
+
<div class="bop-loading-indicator">
|
|
3222
|
+
<span class="bop-loading-spinner">${BOP_ICONS.loader}</span>
|
|
3223
|
+
<span class="bop-loading-indicator-text">Establishing secure connection...</span>
|
|
3224
|
+
</div>
|
|
2492
3225
|
<div class="bop-loading-secure">${BOP_ICONS.shield} 256-bit encrypted connection</div>
|
|
2493
3226
|
`;
|
|
2494
3227
|
this._contentEl.appendChild(step);
|
|
@@ -2524,6 +3257,16 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2524
3257
|
}
|
|
2525
3258
|
|
|
2526
3259
|
_renderSelectAccounts() {
|
|
3260
|
+
if (this._isPlaidLinkFinalizing) {
|
|
3261
|
+
this._renderPlaidFinalizingView();
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
if (this._plaidLinkErrorState) {
|
|
3266
|
+
this._renderPlaidLinkErrorView();
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
2527
3270
|
const step = document.createElement("div");
|
|
2528
3271
|
step.className = "bop-step bop-accounts";
|
|
2529
3272
|
step.setAttribute(
|
|
@@ -2553,10 +3296,9 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2553
3296
|
|
|
2554
3297
|
const linkAccountBtn = document.createElement("button");
|
|
2555
3298
|
linkAccountBtn.className = "bop-link-account-btn";
|
|
2556
|
-
linkAccountBtn.
|
|
2557
|
-
linkAccountBtn.
|
|
2558
|
-
|
|
2559
|
-
);
|
|
3299
|
+
linkAccountBtn.disabled = this._isPlaidLinkInProgress;
|
|
3300
|
+
linkAccountBtn.innerHTML = this._getLinkAccountButtonLabel();
|
|
3301
|
+
linkAccountBtn.addEventListener("click", () => this._handleLinkAccountClick());
|
|
2560
3302
|
hd.appendChild(linkAccountBtn);
|
|
2561
3303
|
inner.appendChild(hd);
|
|
2562
3304
|
|
|
@@ -2576,9 +3318,89 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2576
3318
|
this._contentEl.appendChild(step);
|
|
2577
3319
|
}
|
|
2578
3320
|
|
|
3321
|
+
_renderPlaidFinalizingView() {
|
|
3322
|
+
this._accountListEl = null;
|
|
3323
|
+
this._accountsFooterEl = null;
|
|
3324
|
+
|
|
3325
|
+
const step = document.createElement("div");
|
|
3326
|
+
step.className = "bop-step bop-loading-view";
|
|
3327
|
+
step.setAttribute(
|
|
3328
|
+
"data-direction",
|
|
3329
|
+
this._direction > 0 ? "forward" : "backward",
|
|
3330
|
+
);
|
|
3331
|
+
|
|
3332
|
+
const bankBg = this._selectedBank?.bg || "#2563eb";
|
|
3333
|
+
const bankName = this._selectedBank?.name || "your bank";
|
|
3334
|
+
const bankLogo = this._selectedBank?.logo
|
|
3335
|
+
? `<img class="bop-logo-img" src="${this._selectedBank.logo}" alt="${bankName}">`
|
|
3336
|
+
: BOP_ICONS.buildingLg;
|
|
3337
|
+
|
|
3338
|
+
step.innerHTML = `
|
|
3339
|
+
<div class="bop-loading-logo" style="background:${bankBg}">${bankLogo}</div>
|
|
3340
|
+
<div class="bop-loading-body">
|
|
3341
|
+
<p class="bop-loading-title">Finalizing account link</p>
|
|
3342
|
+
<p class="bop-loading-text">Please wait while we securely connect your bank account.</p>
|
|
3343
|
+
</div>
|
|
3344
|
+
<div class="bop-loading-indicator">
|
|
3345
|
+
<span class="bop-loading-spinner">${BOP_ICONS.loader}</span>
|
|
3346
|
+
<span class="bop-loading-indicator-text">Finalizing with payment providers...</span>
|
|
3347
|
+
</div>
|
|
3348
|
+
<div class="bop-loading-secure">${BOP_ICONS.shield} Verifying and syncing account details</div>
|
|
3349
|
+
`;
|
|
3350
|
+
|
|
3351
|
+
this._contentEl.appendChild(step);
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
_renderPlaidLinkErrorView() {
|
|
3355
|
+
this._accountListEl = null;
|
|
3356
|
+
this._accountsFooterEl = null;
|
|
3357
|
+
|
|
3358
|
+
const step = document.createElement("div");
|
|
3359
|
+
step.className = "bop-step bop-confirm-unlink";
|
|
3360
|
+
step.setAttribute(
|
|
3361
|
+
"data-direction",
|
|
3362
|
+
this._direction > 0 ? "forward" : "backward",
|
|
3363
|
+
);
|
|
3364
|
+
|
|
3365
|
+
const alertIcon =
|
|
3366
|
+
'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>';
|
|
3367
|
+
|
|
3368
|
+
const message =
|
|
3369
|
+
this._plaidLinkErrorState?.message ||
|
|
3370
|
+
"We couldn’t finish linking your bank account. Please retry.";
|
|
3371
|
+
|
|
3372
|
+
step.innerHTML = `
|
|
3373
|
+
<div class="bop-unlink-icon">${alertIcon}</div>
|
|
3374
|
+
<h3 class="bop-unlink-title">Unable to Link Account</h3>
|
|
3375
|
+
<p class="bop-unlink-desc">${message}</p>
|
|
3376
|
+
<div class="bop-unlink-actions"></div>
|
|
3377
|
+
`;
|
|
3378
|
+
|
|
3379
|
+
const actions = step.querySelector(".bop-unlink-actions");
|
|
3380
|
+
|
|
3381
|
+
const retryBtn = document.createElement("button");
|
|
3382
|
+
retryBtn.className = "bop-btn";
|
|
3383
|
+
retryBtn.disabled = this._isPlaidRetryInProgress;
|
|
3384
|
+
retryBtn.innerHTML = this._isPlaidRetryInProgress
|
|
3385
|
+
? `<span class="bop-btn-loading"><span class="bop-spinner">${BOP_ICONS.loader}</span><span>Retrying...</span></span>`
|
|
3386
|
+
: '<span class="bop-btn-label">Retry Link</span>';
|
|
3387
|
+
retryBtn.addEventListener("click", () => this._handlePlaidRetryRegistration());
|
|
3388
|
+
actions.appendChild(retryBtn);
|
|
3389
|
+
|
|
3390
|
+
const backBtn = document.createElement("button");
|
|
3391
|
+
backBtn.className = "bop-btn bop-btn-ghost";
|
|
3392
|
+
backBtn.disabled = this._isPlaidRetryInProgress;
|
|
3393
|
+
backBtn.innerHTML = '<span class="bop-btn-label">Back to Accounts</span>';
|
|
3394
|
+
backBtn.addEventListener("click", () => this._dismissPlaidLinkError());
|
|
3395
|
+
actions.appendChild(backBtn);
|
|
3396
|
+
|
|
3397
|
+
this._contentEl.appendChild(step);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
2579
3400
|
_renderAccountCards(skipAnimationIds = null) {
|
|
2580
3401
|
if (!this._accountListEl) return;
|
|
2581
3402
|
this._accountListEl.innerHTML = "";
|
|
3403
|
+
const isInteractionLocked = this._isPlaidLinkInProgress;
|
|
2582
3404
|
|
|
2583
3405
|
// Show loader while refetching accounts after a successful link
|
|
2584
3406
|
if (this._isFetchingAccounts) {
|
|
@@ -2610,6 +3432,7 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2610
3432
|
card.className = "bop-account-card";
|
|
2611
3433
|
card.dataset.accountId = acct.id;
|
|
2612
3434
|
card.setAttribute("data-selected", String(isSelected));
|
|
3435
|
+
card.disabled = isInteractionLocked;
|
|
2613
3436
|
|
|
2614
3437
|
// Skip fade-in for accounts already visible before this render pass
|
|
2615
3438
|
if (skipAnimationIds && skipAnimationIds.has(acct.id)) {
|
|
@@ -2642,6 +3465,7 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2642
3465
|
_renderAccountsButton() {
|
|
2643
3466
|
if (!this._accountsFooterEl) return;
|
|
2644
3467
|
const c = this._selectedAccounts.size;
|
|
3468
|
+
const isInteractionLocked = this._isPlaidLinkInProgress;
|
|
2645
3469
|
const isOpen = this._accountsFooterEl.classList.contains(
|
|
2646
3470
|
"bop-accounts-footer--open",
|
|
2647
3471
|
);
|
|
@@ -2667,6 +3491,7 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2667
3491
|
if (existing) {
|
|
2668
3492
|
// Already open — just update the label, no rebuild
|
|
2669
3493
|
existing.innerHTML = label;
|
|
3494
|
+
existing.disabled = isInteractionLocked;
|
|
2670
3495
|
return;
|
|
2671
3496
|
}
|
|
2672
3497
|
|
|
@@ -2675,7 +3500,9 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2675
3500
|
btn.className = "bop-btn bop-btn-danger bop-btn-unlink";
|
|
2676
3501
|
btn.style.animation = "bopFadeIn .15s var(--bop-ease) forwards";
|
|
2677
3502
|
btn.innerHTML = label;
|
|
3503
|
+
btn.disabled = isInteractionLocked;
|
|
2678
3504
|
btn.addEventListener("click", () => {
|
|
3505
|
+
if (this._isPlaidLinkInProgress) return;
|
|
2679
3506
|
const accounts = this._accounts.filter((a) =>
|
|
2680
3507
|
this._selectedAccounts.has(a.id),
|
|
2681
3508
|
);
|
|
@@ -2718,7 +3545,10 @@ class BisonOperatorPayments extends HTMLElement {
|
|
|
2718
3545
|
<p class="bop-loading-title">Unlinking Account${plural ? "s" : ""}</p>
|
|
2719
3546
|
<p class="bop-loading-text">Removing ${count} account${plural ? "s" : ""} from your linked accounts</p>
|
|
2720
3547
|
</div>
|
|
2721
|
-
<div class="bop-loading-
|
|
3548
|
+
<div class="bop-loading-indicator">
|
|
3549
|
+
<span class="bop-loading-spinner">${BOP_ICONS.loader}</span>
|
|
3550
|
+
<span class="bop-loading-indicator-text">Removing selected account${plural ? "s" : ""}...</span>
|
|
3551
|
+
</div>
|
|
2722
3552
|
`;
|
|
2723
3553
|
this._contentEl.appendChild(step);
|
|
2724
3554
|
return;
|