Instantly share code, notes, and snippets.
Created
August 24, 2025 11:06
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save kacper-serewis/bcfb4775c5ee4fb8547830d8dd867f36 to your computer and use it in GitHub Desktop.
Extract icscards.nl card statements to JSON using TamperMonkey
This file contains hidden or 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
| // ==UserScript== | |
| // @name ICS Dashboard Button | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2025-08-24 | |
| // @description Export as JSON | |
| // @author You | |
| // @match https://www.icscards.nl/web/consumer/dashboard | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=icscards.nl | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| let btnEl = null; | |
| // --- shared helpers ------------------------------------------------------- | |
| async function getXsrfToken() { | |
| if (window.cookieStore && cookieStore.get) { | |
| const c = await cookieStore.get('XSRF-TOKEN'); | |
| return c?.value ?? ''; | |
| } | |
| const m = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/); | |
| return m ? decodeURIComponent(m[1]) : ''; | |
| } | |
| function setStatus(text, { busy = false } = {}) { | |
| if (!btnEl) return; | |
| btnEl.textContent = text; | |
| btnEl.disabled = !!busy; | |
| btnEl.setAttribute('aria-busy', busy ? 'true' : 'false'); | |
| } | |
| function downloadJSON(data, filename) { | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(url); | |
| } | |
| /** | |
| * Unified request helper. | |
| */ | |
| async function makeRequest(url, opts = {}) { | |
| const { | |
| method = 'GET', | |
| headers = {}, | |
| body = undefined, | |
| json = true, | |
| raw = false, | |
| credentials = 'include', | |
| mode = 'cors', | |
| referrer = location.href, | |
| } = opts; | |
| const xsrf = await getXsrfToken(); | |
| const defaultHeaders = { | |
| Accept: 'application/json, text/plain, */*', | |
| 'Accept-Language': 'en-US,en;q=0.5', | |
| 'X-XSRF-TOKEN': xsrf, | |
| }; | |
| const finalHeaders = { ...defaultHeaders, ...headers }; | |
| let finalBody = body; | |
| const isPlainObject = | |
| body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof Blob); | |
| if (isPlainObject && json) { | |
| finalHeaders['Content-Type'] = 'application/json;charset=UTF-8'; | |
| finalBody = JSON.stringify(body); | |
| } | |
| const resp = await fetch(url, { | |
| method, | |
| headers: finalHeaders, | |
| body: finalBody, | |
| credentials, | |
| mode, | |
| referrer, | |
| }); | |
| if (!resp.ok) { | |
| const text = await resp.text().catch(() => ''); | |
| throw new Error(`Request failed ${resp.status} ${resp.statusText}: ${text.slice(0, 500)}`); | |
| } | |
| if (raw) return resp; | |
| if (json) return resp.json(); | |
| return resp.text(); | |
| } | |
| // --- API wrappers --------------------------------------------------------- | |
| async function getAccounts() { | |
| setStatus('Loading accounts…', { busy: true }); | |
| const accounts = await makeRequest( | |
| 'https://www.icscards.nl/api/nl/sec/frontendservices/allaccountsv2' | |
| ); | |
| return accounts; // return full list so we can use accountNumber later | |
| } | |
| async function getAvailableStatements(accountNumber) { | |
| setStatus('Loading periods…', { busy: true }); | |
| const json = await makeRequest( | |
| `https://www.icscards.nl/api/nl/sec/frontendservices/periods?accountNumber=${encodeURIComponent( | |
| accountNumber | |
| )}` | |
| ); | |
| const first = json[0]; | |
| const last = json[json.length - 1]; | |
| return [first.period, last.period]; // [start, end] | |
| } | |
| async function fetchAllStatements() { | |
| try { | |
| const accounts = await getAccounts(); | |
| const accountNumber = accounts[0].accountNumber; | |
| const [start, end] = await getAvailableStatements(accountNumber); | |
| setStatus('Fetching transactions…', { busy: true }); | |
| const data = await makeRequest( | |
| `https://www.icscards.nl/api/nl/sec/frontendservices/transactionsv3?accountNumber=${encodeURIComponent( | |
| accountNumber | |
| )}&flushCache=true&fromPeriod=${encodeURIComponent(end)}&untilPeriod=${encodeURIComponent( | |
| start | |
| )}` | |
| ); | |
| // Build safe filename: {accountNumber}-{start}-{end}.json | |
| const safeAccount = String(accountNumber).replace(/[^\w-]+/g, ''); | |
| const safeStart = String(start).replace(/[^\w-]+/g, ''); | |
| const safeEnd = String(end).replace(/[^\w-]+/g, ''); | |
| const filename = `${safeAccount}-${safeStart}-${safeEnd}.json`; | |
| downloadJSON(data, filename); | |
| // Optional: show count if structure has .transactions | |
| const count = Array.isArray(data?.transactions) ? data.transactions.length : undefined; | |
| setStatus( | |
| count != null ? `Downloaded (${count} items)` : 'Downloaded ✓', | |
| { busy: false } | |
| ); | |
| } catch (err) { | |
| console.error(err); | |
| setStatus('Failed — see console', { busy: false }); | |
| } finally { | |
| // Re-enable button but keep status text | |
| btnEl && (btnEl.disabled = false); | |
| } | |
| } | |
| // --- UI button ------------------------------------------------------------ | |
| function addButton() { | |
| const container = document.querySelector('.add-withdraw'); | |
| if (!container) return; // not yet available | |
| if (container.querySelector('.tm-custom-button')) return; // avoid duplicates | |
| btnEl = document.createElement('button'); | |
| btnEl.textContent = 'Extract statements'; | |
| btnEl.className = 'tm-custom-button'; | |
| btnEl.type = 'button'; | |
| btnEl.addEventListener('click', () => { | |
| setStatus('Starting…', { busy: true }); | |
| fetchAllStatements().catch(() => {}); | |
| }); | |
| container.appendChild(btnEl); | |
| } | |
| // Try immediately | |
| addButton(); | |
| // In case the page loads dynamically | |
| const observer = new MutationObserver(() => addButton()); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment