mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Fix Plaid link script loading and first-sync account linking (#1165)
* fix: Handle conditional loading of Plaid Link script * fix: Plaid accounts not linking on first sync * fix: Handle Plaid script loading edge cases * fix: Use connection token for disconnect safety and retry failed script loads * fix: Destroy Plaid Link handler on controller disconnect * fix: Add timeout to Plaid CDN script loader to prevent deadlocks
This commit is contained in:
@@ -10,11 +10,75 @@ export default class extends Controller {
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.open();
|
||||
this._connectionToken = (this._connectionToken ?? 0) + 1;
|
||||
const connectionToken = this._connectionToken;
|
||||
this.open(connectionToken).catch((error) => {
|
||||
console.error("Failed to initialize Plaid Link", error);
|
||||
});
|
||||
}
|
||||
|
||||
open() {
|
||||
const handler = Plaid.create({
|
||||
disconnect() {
|
||||
this._handler?.destroy();
|
||||
this._handler = null;
|
||||
this._connectionToken = (this._connectionToken ?? 0) + 1;
|
||||
}
|
||||
|
||||
waitForPlaid() {
|
||||
if (typeof Plaid !== "undefined") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let plaidScript = document.querySelector(
|
||||
'script[src*="link-initialize.js"]'
|
||||
);
|
||||
|
||||
// Reject if the CDN request stalls without firing load or error
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
if (plaidScript) plaidScript.dataset.plaidState = "error";
|
||||
reject(new Error("Timed out loading Plaid script"));
|
||||
}, 10_000);
|
||||
|
||||
// Remove previously failed script so we can retry with a fresh element
|
||||
if (plaidScript?.dataset.plaidState === "error") {
|
||||
plaidScript.remove();
|
||||
plaidScript = null;
|
||||
}
|
||||
|
||||
if (!plaidScript) {
|
||||
plaidScript = document.createElement("script");
|
||||
plaidScript.src = "https://cdn.plaid.com/link/v2/stable/link-initialize.js";
|
||||
plaidScript.async = true;
|
||||
plaidScript.dataset.plaidState = "loading";
|
||||
document.head.appendChild(plaidScript);
|
||||
}
|
||||
|
||||
plaidScript.addEventListener("load", () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
plaidScript.dataset.plaidState = "loaded";
|
||||
resolve();
|
||||
}, { once: true });
|
||||
plaidScript.addEventListener("error", () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
plaidScript.dataset.plaidState = "error";
|
||||
reject(new Error("Failed to load Plaid script"));
|
||||
}, { once: true });
|
||||
|
||||
// Re-check after attaching listeners in case the script loaded between
|
||||
// the initial typeof check and listener attachment (avoids a permanently
|
||||
// pending promise on retry flows).
|
||||
if (typeof Plaid !== "undefined") {
|
||||
window.clearTimeout(timeoutId);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async open(connectionToken = this._connectionToken) {
|
||||
await this.waitForPlaid();
|
||||
if (connectionToken !== this._connectionToken) return;
|
||||
|
||||
this._handler = Plaid.create({
|
||||
token: this.linkTokenValue,
|
||||
onSuccess: this.handleSuccess,
|
||||
onLoad: this.handleLoad,
|
||||
@@ -22,7 +86,7 @@ export default class extends Controller {
|
||||
onEvent: this.handleEvent,
|
||||
});
|
||||
|
||||
handler.open();
|
||||
this._handler.open();
|
||||
}
|
||||
|
||||
handleSuccess = (public_token, metadata) => {
|
||||
|
||||
@@ -12,8 +12,16 @@ class PlaidItem::Syncer
|
||||
sync.update!(status_text: "Importing accounts from Plaid...") if sync.respond_to?(:status_text)
|
||||
plaid_item.import_latest_plaid_data
|
||||
|
||||
# Phase 2: Collect setup statistics
|
||||
# Phase 2: Process the raw Plaid data and create/update internal domain objects
|
||||
# This must happen before the linked/unlinked check because process_accounts
|
||||
# is what creates Account and AccountProvider records for new PlaidAccounts.
|
||||
sync.update!(status_text: "Processing accounts...") if sync.respond_to?(:status_text)
|
||||
mark_import_started(sync)
|
||||
plaid_item.process_accounts
|
||||
|
||||
# Phase 3: Collect setup statistics (now that accounts have been processed)
|
||||
sync.update!(status_text: "Checking account configuration...") if sync.respond_to?(:status_text)
|
||||
plaid_item.plaid_accounts.reload
|
||||
collect_setup_stats(sync, provider_accounts: plaid_item.plaid_accounts)
|
||||
|
||||
# Check for unlinked accounts and update pending_account_setup flag
|
||||
@@ -25,14 +33,9 @@ class PlaidItem::Syncer
|
||||
plaid_item.update!(pending_account_setup: false) if plaid_item.respond_to?(:pending_account_setup=)
|
||||
end
|
||||
|
||||
# Phase 3: Process the raw Plaid data and updates internal domain objects
|
||||
# Phase 4: Schedule balance calculations for linked accounts
|
||||
linked_accounts = plaid_item.plaid_accounts.select { |pa| pa.current_account.present? }
|
||||
if linked_accounts.any?
|
||||
sync.update!(status_text: "Processing transactions...") if sync.respond_to?(:status_text)
|
||||
mark_import_started(sync)
|
||||
plaid_item.process_accounts
|
||||
|
||||
# Phase 4: Schedule balance calculations
|
||||
sync.update!(status_text: "Calculating balances...") if sync.respond_to?(:status_text)
|
||||
plaid_item.schedule_account_syncs(
|
||||
parent_sync: sync,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
<%= combobox_style_tag %>
|
||||
|
||||
<%= yield :plaid_link %>
|
||||
<%= javascript_importmap_tags %>
|
||||
<%= render "layouts/dark_mode_check" %>
|
||||
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<%# locals: (link_token:, region:, item_id:, is_update: false) %>
|
||||
|
||||
<% content_for :plaid_link, flush: true do %>
|
||||
<%= javascript_include_tag "https://cdn.plaid.com/link/v2/stable/link-initialize.js" %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.div data: {
|
||||
controller: "plaid",
|
||||
plaid_link_token_value: link_token,
|
||||
|
||||
Reference in New Issue
Block a user