Skip to content

Instantly share code, notes, and snippets.

@ThatGuySam
Last active June 9, 2023 16:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThatGuySam/711ca473ddeeef861f335fefe180c4c1 to your computer and use it in GitHub Desktop.
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
// 🚨 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