From 6366b3cb42b4303f4c165c7a516b51cdeffde33f Mon Sep 17 00:00:00 2001 From: "George W. Walker" Date: Sun, 17 Jan 2021 17:08:17 -0500 Subject: [PATCH] Fix issues from Typescript Refactor (#26) Bug Fixes: - Fixes an issue where, during initial setup, the extension could cause the sync to happen before the properties have been set up in Mint. - Fixes #22: The extension would sometimes fail to open the tabs used for syncing. - Fixes #25: The inputs for the values received from Mint could not be found sometimes New Features: - "Debug Mode" setting that enables logging. This can be used to diagnose issues in the future. This will also prevent the tabs the extension uses for syncing from closing when they are finished. Maintenance: - Include prettier configuration - Update release script - Improve `waitForElement` utility --- package-lock.json | 2 +- package.json | 5 +- public/manifest.json | 2 +- src/.prettierrc | 5 ++ src/background/main.ts | 103 +++++++++++++++----------- src/content/mint/properties/check.ts | 3 + src/content/mint/properties/update.ts | 54 ++++++-------- src/content/robinhood/main.ts | 36 +++------ src/utilities/debug.ts | 8 +- src/utilities/waitForElement.ts | 18 +++-- 10 files changed, 125 insertions(+), 111 deletions(-) create mode 100644 src/.prettierrc diff --git a/package-lock.json b/package-lock.json index dea2eaf..bff894f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "robinhood-mint-sync-chrome", - "version": "3.0.0", + "version": "3.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b7dc43c..16e6592 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "robinhood-mint-sync-chrome", - "version": "3.1.0", + "version": "3.2.0", "repository": { "type": "git", "url": "git+https://github.com/pkmnct/robinhood-mint-sync-chrome.git" @@ -38,6 +38,7 @@ "clean": "rimraf dist", "prerelease": "npm run clean", "release": "npm run build", - "postrelease": "cd dist && cross-var bestzip ../$npm_package_name@$npm_package_version.zip * && cd .." + "zip": "cd dist && cross-var bestzip ../$npm_package_name@$npm_package_version.zip * && cd ..", + "postrelease": "rimraf *.zip && npm run zip" } } diff --git a/public/manifest.json b/public/manifest.json index c1ea669..184d51e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,7 +3,7 @@ "name": "Robinhood Mint Sync for Chrome", "description": "Automatically add your Robinhood portfolio to Mint", - "version": "3.1.0", + "version": "3.2.0", "author": "George Walker", "icons": { diff --git a/src/.prettierrc b/src/.prettierrc new file mode 100644 index 0000000..99ef3b7 --- /dev/null +++ b/src/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 160 +} diff --git a/src/background/main.ts b/src/background/main.ts index 9fd06d5..a501297 100644 --- a/src/background/main.ts +++ b/src/background/main.ts @@ -4,7 +4,9 @@ import { Debug } from "../utilities/debug"; const debug = new Debug("background", "Main"); // Need to be able to access this regardless of the message. -let mintTab; +let mintTab: undefined | number; +let newProperties = 0; +let newPropertiesComplete = 0; interface eventHandler { message: any; @@ -15,7 +17,7 @@ interface eventHandler { const eventHandlers = { // This event is emitted by the main Robinhood content script. "robinhood-login-needed": ({ sender }: eventHandler) => { - debug.log("robinhood-login-needed event"); + debug.log("robinhood-login-needed event"); chrome.tabs.sendMessage(mintTab, { status: "You need to log in to Robinhood", link: urls.robinhood.login, @@ -27,7 +29,7 @@ const eventHandlers = { }, // This event is emitted by the login Robinhood content script. "robinhood-login-success": ({ sender }: eventHandler) => { - debug.log("robinhood-login-success event"); + debug.log("robinhood-login-success event"); // Close the Robinhood tab logged in from if (!debug.isEnabled()) chrome.tabs.remove(sender.tab.id); @@ -36,7 +38,6 @@ const eventHandlers = { chrome.tabs.create({ url: urls.robinhood.scrape, active: false, - openerTabId: mintTab, }); // Switch focus back to Mint @@ -46,29 +47,31 @@ const eventHandlers = { }, // This event is emitted by the main Robinhood content script. "robinhood-portfolio-scraped": ({ sender, message }: eventHandler) => { - debug.log("robinhood-portfolio-scraped event"); + debug.log("robinhood-portfolio-scraped event"); // Trigger the Mint portfolio update content script chrome.tabs.create( { url: urls.mint.properties.update, active: false, - openerTabId: sender.tab.id, }, (tab) => { - debug.log("waiting for Mint tab to load") + debug.log("waiting for Mint tab to load"); const checkIfLoaded = () => { - chrome.tabs.get(tab.id, (tab) => { - if (!tab) { - clearInterval(sendMessageInterval); - debug.log("tab was not found. Clearing interval to prevent endless loop."); - } - if (tab.status === "complete") { - // Once the tab is loaded, pass the message to it - chrome.tabs.sendMessage(tab.id, message); - clearInterval(sendMessageInterval); - debug.log("Mint tab loaded"); - } - }); + if (!tab) { + clearInterval(sendMessageInterval); + debug.log( + "Unexpected: Tab was not found. Clearing interval to prevent endless loop. Did the tab get closed?" + ); + } else { + chrome.tabs.get(tab.id, (tab) => { + if (tab.status === "complete") { + // Once the tab is loaded, pass the message to it + chrome.tabs.sendMessage(tab.id, message); + clearInterval(sendMessageInterval); + debug.log("Mint tab loaded"); + } + }); + } }; const sendMessageInterval = setInterval(checkIfLoaded, 200); } @@ -77,49 +80,67 @@ const eventHandlers = { }, // This event is emitted by the Mint main content script. "mint-force-sync": () => { - debug.log("mint-force-sync event"); + debug.log("mint-force-sync event"); // Trigger the main Robinhood sync script chrome.tabs.create({ url: urls.robinhood.scrape, active: false, - openerTabId: mintTab, }); }, // This event is emitted by the Mint property create content script. "mint-property-added": ({ sender }: eventHandler) => { - debug.log("mint-property-added event"); - if (!debug.isEnabled()) chrome.tabs.remove(sender.tab.id); - }, - // This event is emitted by the Mint property check content script. - "mint-property-setup-complete": ({ sender }: eventHandler) => { - debug.log("mint-property-setup-complete event"); - - chrome.storage.sync.set({ - propertiesSetup: true, - needsOldPropertyRemoved: false, - }); - + debug.log("mint-property-added event"); + newPropertiesComplete++; if (!debug.isEnabled()) chrome.tabs.remove(sender.tab.id); + + debug.log(`Setup ${newPropertiesComplete} of ${newProperties} properties.`); + if (newPropertiesComplete === newProperties) { + eventHandlers["setup-complete"](); + } + }, + // This event is emitted by the mint-property-added and/or the mint-property-setup-complete event handlers + "setup-complete": () => { + debug.log("setup-complete event"); + debug.log("setup-complete sending notification"); chrome.tabs.sendMessage(mintTab, { status: "Setup complete! Initiating Sync.", persistent: true, }); + debug.log("setup-complete opening Robinhood to scrape"); // Trigger the Robinood sync content script chrome.tabs.create({ url: urls.robinhood.scrape, active: false, - openerTabId: mintTab, }); + debug.log("setup-complete switching focus back to Mint"); // Switch focus back to Mint chrome.tabs.update(mintTab, { selected: true, }); }, + // This event is emitted by the Mint property check content script. + "mint-property-setup-complete": ({ sender, message }: eventHandler) => { + debug.log("mint-property-setup-complete event"); + + debug.log("mint-property-setup-complete setting storage"); + chrome.storage.sync.set({ + propertiesSetup: true, + needsOldPropertyRemoved: false, + }); + + if (!debug.isEnabled()) chrome.tabs.remove(sender.tab.id); + + newProperties = parseInt(message.newProperties); + debug.log(`Setting up ${newProperties} properties.`); + if (newProperties === 0) { + eventHandlers["setup-complete"](); + } + }, // This event is emitted by the Mint property update content script. "mint-sync-complete": () => { - debug.log("mint-sync-complete event"); + debug.log("mint-sync-complete event"); chrome.storage.sync.set({ syncTime: new Date().toString() }); chrome.tabs.sendMessage(mintTab, { status: "Sync Complete! Reload to see the change.", @@ -130,7 +151,7 @@ const eventHandlers = { }, // This event is emitted by the main Mint content script "mint-opened": ({ sender }: eventHandler) => { - debug.log("mint-opened event"); + debug.log("mint-opened event"); // Store a reference to the mint tab to be able to show the notifications mintTab = sender.tab.id; @@ -152,7 +173,6 @@ const eventHandlers = { chrome.tabs.create({ url: urls.mint.properties.check, active: false, - openerTabId: mintTab, }); } else if (!propertiesSetup || !syncTime) { // Sync has not been set up @@ -182,7 +202,6 @@ const eventHandlers = { chrome.tabs.create({ url: urls.robinhood.scrape, active: false, - openerTabId: mintTab, }); } else { chrome.tabs.sendMessage(mintTab, { @@ -197,16 +216,16 @@ const eventHandlers = { ); }, // This event is emitted by the Mint property check content script. - "mint-create": ({ message, sender }: eventHandler) => { - debug.log("mint-create event"); + "mint-create": ({ message }: eventHandler) => { + debug.log("mint-create event"); chrome.tabs.create({ url: urls.mint.properties.create + "&property=" + message.property, active: false, }); }, // This event is emitted by the Mint property check content script. - "mint-property-remove": ({ message, sender }: eventHandler) => { - debug.log("mint-property-remove event"); + "mint-property-remove": ({ sender }: eventHandler) => { + debug.log("mint-property-remove event"); chrome.tabs.sendMessage(mintTab, { status: "Your account was set up prior to version 3 of this extension. Version 3 introduced separation of asset types when syncing. Please remove the old 'Robinhood Account' property from Mint to prevent duplication of your portfolio balance. Reload the overview to sync after removing the property.", diff --git a/src/content/mint/properties/check.ts b/src/content/mint/properties/check.ts index b30b50e..50e95e0 100644 --- a/src/content/mint/properties/check.ts +++ b/src/content/mint/properties/check.ts @@ -32,6 +32,7 @@ const checkIfPropertyExists = (property) => { window.addEventListener("load", () => { waitForElement(".OtherPropertyView", null, () => { + let newProperties = 0; robinhoodProperties.forEach((property) => { if (!checkIfPropertyExists(property)) { // Trigger setup of property @@ -39,6 +40,7 @@ window.addEventListener("load", () => { event: "mint-create", property, }); + newProperties++; } }); if (checkIfPropertyExists("Account")) { @@ -48,6 +50,7 @@ window.addEventListener("load", () => { } else { chrome.runtime.sendMessage({ event: "mint-property-setup-complete", + newProperties, }); } }); diff --git a/src/content/mint/properties/update.ts b/src/content/mint/properties/update.ts index e556327..f576153 100644 --- a/src/content/mint/properties/update.ts +++ b/src/content/mint/properties/update.ts @@ -7,16 +7,13 @@ import { waitForElement } from "../../../utilities/waitForElement"; const debug = new Debug("content", "Mint - Properties - Update"); -new Overlay( - "Syncing Mint and Robinhood...", - "This window will automatically close when the sync is complete" -); +new Overlay("Updating Mint Properties...", "This window will automatically close when the sync is complete"); chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { if (request.event === "robinhood-portfolio-scraped") { debug.log("Waiting for Property Tab View to load"); - waitForElement(".PropertyTabView", null, () => { - debug.log("Property Tab View loaded."); + waitForElement(".PropertyTabView", null, (propertyViewElement) => { + debug.log("Property Tab View loaded.", propertyViewElement); let crypto = 0; let stocks = 0; @@ -38,7 +35,7 @@ chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { debug.log(`Fields updated. Attempting to save.`); const saveButtons = document.querySelectorAll(".saveButton"); saveButtons.forEach((button) => { - debug.log(`Clicking save`); + debug.log(`Clicking save`, button); button.removeAttribute("disabled"); (button as HTMLInputElement).click(); }); @@ -48,30 +45,27 @@ chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { function setRobinhoodAmount(label, amount) { debug.log(`Attempting to set ${label} to ${amount}`); - // Find the property that contains the label - const otherProperties = document.querySelectorAll(".OtherPropertyView"); - let property; - otherProperties.forEach((thisProperty) => { - if ((thisProperty as HTMLElement).innerText.includes(label)) { - property = thisProperty; - return; - } - }); - - if (property) { - property.querySelector("span").click(); - - const robinhoodInputs = property.querySelectorAll("input"); + waitForElement(".OtherPropertyView", `Robinhood ${label}`, (foundElement) => { + debug.log(`Expanding property ${label}`, foundElement); + foundElement.querySelector("span").click(); - if (robinhoodInputs[0].value === `Robinhood ${label}`) { - debug.log(`Found ${label} input, setting amount`); - robinhoodInputs[1].value = amount; - syncedLabels.push(label); - callback(); - } - } else { - setTimeout(() => setRobinhoodAmount(label, amount), 50); - } + waitForElement( + "input", + null, + () => { + const inputs = foundElement.querySelectorAll("input"); + inputs.forEach((foundInput) => { + if (foundInput.getAttribute("name") === "value") { + debug.log(`Found ${label} input, setting amount`, foundInput); + foundInput.value = amount; + syncedLabels.push(label); + callback(); + } + }); + }, + foundElement + ); + }); } if (request.uninvested_cash) { diff --git a/src/content/robinhood/main.ts b/src/content/robinhood/main.ts index eea9c49..5963949 100644 --- a/src/content/robinhood/main.ts +++ b/src/content/robinhood/main.ts @@ -10,18 +10,13 @@ const getBearerToken = () => new Promise((resolve, reject) => { const database = window.indexedDB.open("localforage"); database.onsuccess = () => { - const transaction = database.result.transaction( - "keyvaluepairs", - "readwrite" - ); + const transaction = database.result.transaction("keyvaluepairs", "readwrite"); const objectStore = transaction.objectStore("keyvaluepairs"); const auth = objectStore.get("reduxPersist:auth"); auth.onsuccess = () => { try { - const access_token = JSON.parse(auth.result) - .split(`access_token","`)[1] - .split(`"`)[0]; + const access_token = JSON.parse(auth.result).split(`access_token","`)[1].split(`"`)[0]; resolve(access_token); } catch (error) { reject(error); @@ -50,6 +45,7 @@ export interface Message { * Function to scrape the portfolio and cash values */ const scrapeData = async () => { + debug.log("Scraping data using Robinhood API"); const returnValue = { event: "robinhood-portfolio-scraped", } as Message; @@ -73,21 +69,11 @@ const scrapeData = async () => { returnValue.uninvested_cash = json.uninvested_cash.amount; } - if ( - json && - json.crypto && - json.crypto.market_value && - json.crypto.market_value.amount - ) { + if (json && json.crypto && json.crypto.market_value && json.crypto.market_value.amount) { returnValue.crypto = json.crypto.market_value.amount; } - if ( - json && - json.equities && - json.equities.market_value && - json.equities.market_value.amount - ) { + if (json && json.equities && json.equities.market_value && json.equities.market_value.amount) { returnValue.equities = json.equities.market_value.amount; } @@ -107,20 +93,20 @@ const scrapeData = async () => { */ const init = () => { window.addEventListener("load", () => { - new Overlay( - "Syncing Mint and Robinhood...", - "This window will automatically close when the sync is complete" - ); + new Overlay("Getting data from Robinhood...", "This window will automatically close when the sync is complete"); const checkIfLoggedIn = async () => { + debug.log("Waiting for page to load"); if (document.location.pathname.includes("/account")) { + clearInterval(checkIfLoggedInInterval); + debug.log("Page loaded. Appears to be logged in."); const data = await scrapeData(); chrome.runtime.sendMessage(data); - clearInterval(checkIfLoggedInInterval); } else if (document.location.pathname.includes("/login")) { + clearInterval(checkIfLoggedInInterval); + debug.log("Page loaded. Appears to be logged out."); chrome.runtime.sendMessage({ event: "robinhood-login-needed", }); - clearInterval(checkIfLoggedInInterval); } }; const checkIfLoggedInInterval = setInterval(checkIfLoggedIn, 500); diff --git a/src/utilities/debug.ts b/src/utilities/debug.ts index 9fc49e4..2a18ab3 100644 --- a/src/utilities/debug.ts +++ b/src/utilities/debug.ts @@ -17,7 +17,7 @@ export class Debug { this.prefix = `${ type === "content" ? extensionPrefix + " - " : "" - }${typePrefix}${namePrefix} `; + }${typePrefix}${namePrefix}`; chrome.storage.sync.get("debugMode", ({ debugMode }) => { if (debugMode) { @@ -27,8 +27,10 @@ export class Debug { }); } - public log = (message: string): void => { - if (this.isDebug) console.log(`${this.prefix}${message}`); + public log = (...params: any[]): void => { + const newParams = [...params]; + newParams.unshift(this.prefix); + if (this.isDebug) console.log(...newParams); }; public isEnabled = (): boolean => this.isDebug; diff --git a/src/utilities/waitForElement.ts b/src/utilities/waitForElement.ts index da06454..7e381af 100644 --- a/src/utilities/waitForElement.ts +++ b/src/utilities/waitForElement.ts @@ -1,25 +1,29 @@ export const waitForElement = ( selector: string, withText: string | null, - callback: () => void + callback: (foundElement?: HTMLElement) => void, + initialContainer?: Element ): void => { - const elements = document.querySelectorAll(selector); + const queryContainer = initialContainer ? initialContainer : document; + const elements = queryContainer.querySelectorAll(selector); if (!elements.length) { - setTimeout(() => waitForElement(selector, withText, callback), 500); + setTimeout(() => waitForElement(selector, withText, callback, initialContainer), 500); } else if (withText) { let foundText = false; + let foundElement; elements.forEach((element) => { - if ((element as HTMLElement).innerText.toLowerCase().includes(withText)) { + if ((element as HTMLElement).innerText.toLowerCase().includes(withText.toLowerCase())) { foundText = true; + foundElement = element; return; } }); if (foundText) { - callback(); + callback(foundElement); } else { - setTimeout(() => waitForElement(selector, withText, callback), 500); + setTimeout(() => waitForElement(selector, withText, callback, initialContainer), 500); } } else { - callback(); + callback(elements[0] as HTMLElement); } };