Last active
June 9, 2023 16:45
-
-
Save ThatGuySam/711ca473ddeeef861f335fefe180c4c1 to your computer and use it in GitHub Desktop.
Chrome Source Script to export ONE transactions to a CSV for the 2023 Tax Season
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 🚨 Running random code on your bank website | |
// is a popular way to commit fraud. 🚨 | |
// | |
// | |
// Although I wrote this with the intent | |
// to help people get their transactions | |
// out of their One accounts as CSV | |
// this script is given with no warranty | |
// and you should ask a developer that | |
// you trust to double check it to make | |
// sure it's safe to run on your bank account. | |
// Designed to work on "All" Transactions page, typically ending in "/transactions" | |
// If it's not working you make need to click the "All" link in your Pocket | |
// How to run scripts in Chrome sources panel | |
// https://developer.chrome.com/docs/devtools/javascript/snippets/ | |
function sleep (ms) { | |
return new Promise(resolve => setTimeout(resolve, ms)); | |
} | |
// Get App Container with function incase it get rerendered | |
function getAppContainer () { | |
return document.querySelector( '.AppContainer-centerContent' ) | |
} | |
function getPocketHeading () { | |
return getAppContainer().querySelector( '.MuiTypography-titleXL' ).textContent | |
} | |
function getRecentTransactionCells () { | |
const recentSelector = '.CellList-root .MuiPaper-elevation.CellList-cells' | |
// Pending and recent are selected together | |
const pendingAndRecentLists = getAppContainer().querySelectorAll( recentSelector ) | |
// Cince pending may or may not be present we get the last element found | |
// which should be recent | |
const recent = Array.from( pendingAndRecentLists ).at(-1) | |
// Now we get the cells from recent | |
const recentCells = recent.querySelectorAll( 'button.Cell-root' ) | |
// console.log( recentCells ) | |
return Array.from( recentCells ) | |
} | |
const monthNumbers = { | |
'Jan': 1, | |
'Feb': 2, | |
'Mar': 3, | |
'Apr': 4, | |
'May': 5, | |
'Jun': 6, | |
'Jul': 7, | |
'Aug': 8, | |
'Sep': 9, | |
'Oct': 10, | |
'Nov': 11, | |
'Dec': 12, | |
} | |
function groupTransactionsByMonth () { | |
const recentCells = getRecentTransactionCells() | |
const transactionsByMonth = {} | |
let year = Number( new Date().getFullYear() ) | |
let groupKey = '' | |
for ( const transactionCell of recentCells ) { | |
// Check if description contains hasDecember | |
const decriptionElement = transactionCell.querySelector('[data-cy="description"]') | |
const [ month ] = decriptionElement.innerText.split(' ') | |
const [ , previousMonth = '' ] = groupKey.split( '-' ) | |
const isNewGroup = !groupKey.includes( month ) | |
const isLaterMonth = ( | |
!!monthNumbers[ previousMonth ] | |
&& !!monthNumbers[ month ] | |
&& monthNumbers[ month ] > monthNumbers[ previousMonth ] | |
) | |
// If month is first found december entry we subtract from the year | |
if ( isNewGroup && isLaterMonth ) { | |
year -= 1 | |
} | |
groupKey = `${ year }-${ month }` | |
const hasGroup = !!transactionsByMonth[ groupKey ] | |
transactionsByMonth[ groupKey ] = | |
!!hasGroup | |
? [ | |
...transactionsByMonth[ groupKey ], | |
transactionCell | |
] | |
: [ transactionCell ] | |
} | |
return transactionsByMonth | |
} | |
function hasDecember () { | |
const recentCells = getRecentTransactionCells() | |
for ( const transactionCell of recentCells.reverse() ) { | |
// Check if description contains hasDecember | |
const decriptionElement = transactionCell.querySelector('[data-cy="description"]') | |
if ( decriptionElement.innerText.includes('Dec') ) { | |
return true | |
} | |
} | |
return false | |
} | |
function hasDecemberBeforeLast () { | |
// If we have 2 December months then we're good | |
// Get all December groups | |
const decembers = Object.keys( groupTransactionsByMonth() ) | |
.filter( groupKey => groupKey.includes('Dec') ) | |
// console.log( { decembers }, Object.keys( groupTransactionsByMonth() ) ) | |
return decembers.length > 1 | |
} | |
async function scrollCells () { | |
console.log( 'Starting scroll' ) | |
let timeoutId | |
let hasTimedOut = false | |
let loadedCellsCount = getRecentTransactionCells().length | |
const loadingTimeout = 3_000 | |
const resetTimeout = () => { | |
clearTimeout( timeoutId ) | |
timeoutId = setTimeout(() => { | |
console.log( '⏰ Timeout' ) | |
hasTimedOut = true | |
}, loadingTimeout ) | |
} | |
// Start the initial timeout | |
resetTimeout() | |
while ( hasDecemberBeforeLast() === false ) { | |
if ( hasTimedOut ) { | |
break | |
} | |
console.log( `Found ${ loadedCellsCount } scrolling to bottom` ) | |
getAppContainer().scrollTop = getAppContainer().scrollHeight | |
await sleep( 750 ) | |
const newCellsCount = getRecentTransactionCells().length | |
// If the amount of cells changed then resetTimeout | |
if ( loadedCellsCount !== newCellsCount ) { | |
console.log( '+ Extending timeout' ) | |
resetTimeout() | |
} | |
// Update count | |
loadedCellsCount = newCellsCount | |
} | |
clearTimeout( timeoutId ) | |
} | |
function cleanAmount ( rawText ) { | |
const cleaned = rawText | |
.replace( '$', '' ) | |
.trim() | |
const isPositive = cleaned.startsWith( '+' ) | |
if ( isPositive ) { | |
return cleaned | |
} | |
return `-${ cleaned }` | |
} | |
function showCells () { | |
const transactionsContainer = document.createElement('div') | |
const close = () => transactionsContainer.remove() | |
transactionsContainer.onclick = event => { | |
if ( transactionsContainer.contains( event.target ) ){ | |
return | |
} | |
close() | |
} | |
transactionsContainer.style.position = 'fixed' | |
transactionsContainer.style.inset = '5vw' | |
transactionsContainer.style.overflow = 'scroll' | |
transactionsContainer.style.fontSize = '0.7rem' | |
const transactionsTable = document.createElement('table') | |
transactionsContainer.append( transactionsTable ) | |
transactionsTable.style.background = 'black' | |
transactionsTable.style.position = 'relative' | |
transactionsTable.style.inset = '0' | |
transactionsTable.style.width = '100%' | |
const menuRow = transactionsTable.insertRow() | |
menuRow.classList.add( 'menu-row' ) | |
const countCell = menuRow.insertCell() | |
const recentCells = getRecentTransactionCells() | |
countCell.innerHTML = `${ recentCells.length } total transactions` | |
const downloadCell = menuRow.insertCell() | |
downloadCell.append( 'Download: ' ) | |
const download2022Button = document.createElement('button') | |
download2022Button.innerText = '⬇️ 2022' | |
download2022Button.style.color = 'inherit' | |
download2022Button.onclick = () => { | |
const csvContent = generateCSVContentFromTable( transactionsContainer, 'tr.year-2022' ) | |
downloadAsCsv( csvContent ) | |
} | |
downloadCell.append( download2022Button ) | |
const downloadAllButton = document.createElement('button') | |
downloadAllButton.innerText = '⬇️ All' | |
downloadAllButton.style.color = 'inherit' | |
downloadAllButton.onclick = () => { | |
const csvContent = generateCSVContentFromTable( transactionsContainer ) | |
downloadAsCsv( csvContent ) | |
} | |
downloadCell.append( downloadAllButton ) | |
const closeCell = menuRow.insertCell() | |
closeCell.onclick = () => close() | |
closeCell.innerText = '❌ Close' | |
const headerRow = transactionsTable.insertRow() | |
headerRow.classList.add( 'header-row' ) | |
const dateHeader = headerRow.insertCell() | |
dateHeader.innerText = 'Date' | |
const descriptionHeader = headerRow.insertCell() | |
descriptionHeader.innerText = 'Description' | |
const amountHeader = headerRow.insertCell() | |
amountHeader.innerText = 'Amount' | |
const cellsByMonth = groupTransactionsByMonth() | |
for ( const [ key, monthCells ] of Object.entries( cellsByMonth ) ) { | |
const [ year ] = key.split( '-' ) | |
for ( const transactionCell of monthCells ) { | |
const newRow = transactionsTable.insertRow() | |
newRow.classList.add(`year-${ year }`) | |
const date = newRow.insertCell() | |
const monthDay = transactionCell.querySelector('[data-cy="description"]').innerText | |
date.innerText = `${ monthDay }, ${ year }` | |
const description = newRow.insertCell() | |
description.innerText = transactionCell.querySelector('.Cell-label').innerText | |
const amount = newRow.insertCell() | |
amount.innerText = cleanAmount( transactionCell.querySelector('.MuiTypography-root').innerText ) | |
} | |
} | |
// console.log({ transactionsTable: transactionsTable.innerHTML }) | |
document.body.append( transactionsContainer ) | |
return transactionsContainer | |
} | |
function generateCSVContentFromTable ( transactionsContainer, rowSelector = 'tr' ) { | |
const rows = transactionsContainer.querySelectorAll( `${ rowSelector }:not(.menu-row), tr.header-row` ) | |
return Array.from( rows ).map( row => { | |
const cells = row.querySelectorAll( 'td' ) | |
return Array.from( cells ) | |
.map( ( cell, cellIndex ) => { | |
return cell.textContent | |
.replace( ',', '' ) | |
} ) | |
.join( ', ' ) | |
} ) | |
.join( ' \r\n' ) | |
} | |
/** Download contents as a file | |
* Source: https://stackoverflow.com/questions/14964035/how-to-export-javascript-array-info-to-csv-on-client-side | |
*/ | |
function downloadAsCsv ( | |
content, | |
filename = `${ getPocketHeading() }-ONE_Transactions.csv`, | |
contentType = 'text/csv;charset=utf-8;' | |
) { | |
// Create a blob | |
var blob = new Blob([content], { type: contentType }); | |
var url = URL.createObjectURL(blob); | |
// Create a link to download it | |
var pom = document.createElement('a'); | |
pom.href = url; | |
pom.setAttribute('download', filename); | |
pom.click(); | |
} | |
async function start () { | |
// console.log( 'Cells', getRecentTransactionCells() ) | |
scrollCells() | |
.finally(() => { | |
// console.log( 'By month', groupTransactionsByMonth() ) | |
showCells() | |
console.log('Done') | |
}) | |
} | |
start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment