Download data and create CSVs
This commit is contained in:
parent
3f884f0923
commit
a8cfd41892
|
@ -2,3 +2,4 @@ node_modules/
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
.parcel-cache/
|
.parcel-cache/
|
||||||
dist/
|
dist/
|
||||||
|
src/**/**.js
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 90,
|
||||||
|
"semi": true
|
||||||
|
}
|
14
package.json
14
package.json
|
@ -6,9 +6,15 @@
|
||||||
"@parcel/transformer-inline-string": "2.8.3",
|
"@parcel/transformer-inline-string": "2.8.3",
|
||||||
"@types/firefox-webext-browser": "^111.0.0",
|
"@types/firefox-webext-browser": "^111.0.0",
|
||||||
"@types/level-ttl": "^3.1.2",
|
"@types/level-ttl": "^3.1.2",
|
||||||
|
"@types/papaparse": "^5.3.15",
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
"events": "^3.1.0",
|
"events": "^3.1.0",
|
||||||
"parcel": "^2.8.3"
|
"parcel": "^2.12.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"string_decoder": "^1.3.0",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"util": "^0.12.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "parcel watch src/manifest.json --host localhost --config @parcel/config-webextension",
|
"start": "parcel watch src/manifest.json --host localhost --config @parcel/config-webextension",
|
||||||
|
@ -27,6 +33,8 @@
|
||||||
"graphql-request": "^5.2.0",
|
"graphql-request": "^5.2.0",
|
||||||
"jsx-dom": "^8.0.5",
|
"jsx-dom": "^8.0.5",
|
||||||
"level": "^8.0.0",
|
"level": "^8.0.0",
|
||||||
"nanoevents": "^7.0.1"
|
"nanoevents": "^7.0.1",
|
||||||
}
|
"papaparse": "^5.4.1"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
const matchGraphQL = { urls: ["https://graphql.landsbankinn.is/v2"] };
|
|
||||||
|
|
||||||
import { fetchAccounts } from '../landsbankinn';
|
|
||||||
import { PartialStateUpdate } from '../state';
|
|
||||||
import { State } from '../state/level';
|
|
||||||
|
|
||||||
type HttpHeaders = browser.webRequest.HttpHeaders;
|
|
||||||
|
|
||||||
function extractAuthStateFromHeaders(headers: HttpHeaders): PartialStateUpdate {
|
|
||||||
let apiKey = '';
|
|
||||||
let authToken = '';
|
|
||||||
let fingerprintValue = '';
|
|
||||||
|
|
||||||
for (const header of headers) {
|
|
||||||
switch (header.name.toLowerCase()) {
|
|
||||||
case "authorization":
|
|
||||||
authToken = header.value || '';
|
|
||||||
break;
|
|
||||||
case "apikey":
|
|
||||||
apiKey = header.value || '';
|
|
||||||
break;
|
|
||||||
case "fingerprintvalue":
|
|
||||||
fingerprintValue = header.value || '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { apiKey, authToken, fingerprintValue };
|
|
||||||
}
|
|
||||||
|
|
||||||
browser.webRequest.onBeforeSendHeaders.addListener(e => {
|
|
||||||
// TODO if request errors, then clear state and try again.
|
|
||||||
(async function() {
|
|
||||||
let state = await State.current;
|
|
||||||
if (!state.ready && e.requestHeaders) {
|
|
||||||
console.info('Will update auth info from headers');
|
|
||||||
|
|
||||||
// Lock ready to true so we don't have multiple attempts
|
|
||||||
// at getting account info.
|
|
||||||
await State.update({ ready: true });
|
|
||||||
let newState = extractAuthStateFromHeaders(e.requestHeaders);
|
|
||||||
state = await State.update(newState);
|
|
||||||
|
|
||||||
if (!newState.accounts) {
|
|
||||||
const accounts = await fetchAccounts(state);
|
|
||||||
await State.update({ accounts });
|
|
||||||
console.info('Acquired account data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, matchGraphQL, ["requestHeaders"]);
|
|
||||||
|
|
||||||
export { }
|
|
||||||
|
|
||||||
// Here we intercept and record the necessary auth info. But what if
|
|
||||||
// we cannot do that, if user logs directly into a page where there
|
|
||||||
// are no graphql calls?
|
|
||||||
|
|
||||||
// We can get device fingerprint via content_script, and call session.
|
|
||||||
|
|
||||||
// Have a message called ENSURE_STATE.
|
|
||||||
// Sent to background page w/ device_fingerprint.
|
|
||||||
// If state not ready, call session endpoint, and parse output.
|
|
||||||
// Record state.
|
|
||||||
// Content script for ensure state runs everywhere.
|
|
||||||
// There is a value on the page that has everything we need!
|
|
|
@ -1,9 +1,33 @@
|
||||||
import { isStatePopulated, isStateClear, PartialStateUpdate, ExporterState, isStateAbleToRequest } from "../state";
|
import Papa from 'papaparse';
|
||||||
|
import {
|
||||||
|
isStatePopulated,
|
||||||
|
isStateClear,
|
||||||
|
PartialStateUpdate,
|
||||||
|
ExporterState,
|
||||||
|
isStateAbleToRequest
|
||||||
|
} from '../state';
|
||||||
import { Status } from '../state';
|
import { Status } from '../state';
|
||||||
import { LevelState } from '../state/level';
|
import { LevelState } from '../state/level';
|
||||||
import * as landsbankinn from '../landsbankinn';
|
import * as landsbankinn from '../landsbankinn';
|
||||||
import { fetchAccounts } from "../landsbankinn";
|
import {
|
||||||
import { Actions, EnsureStateMessage, EnsureStateReply, GetAccountsReply } from './messages';
|
fetchAccounts,
|
||||||
|
downloadTransactions,
|
||||||
|
OutputFormat,
|
||||||
|
RawTransaction
|
||||||
|
} from '../landsbankinn';
|
||||||
|
import {
|
||||||
|
Actions,
|
||||||
|
DownloadTransactionReply,
|
||||||
|
DownloadTransactionsMessage,
|
||||||
|
EnsureStateMessage,
|
||||||
|
EnsureStateReply,
|
||||||
|
GetAccountsReply
|
||||||
|
} from './messages';
|
||||||
|
|
||||||
|
type GroupedRawTransaction = RawTransaction & { txnAccountNumber: string };
|
||||||
|
type EnsureStateAction = Required<Pick<EnsureStateMessage, 'clientState'>>;
|
||||||
|
type DownloadTransactionsAction = Omit<DownloadTransactionsMessage, 'action'>;
|
||||||
|
type TransactionsByAccount = Record<string, RawTransaction[] | undefined>;
|
||||||
|
|
||||||
// State singleton (move to its own file if we eventually get more
|
// State singleton (move to its own file if we eventually get more
|
||||||
// than 1 background script)
|
// than 1 background script)
|
||||||
|
@ -17,22 +41,28 @@ async function ensureDataPopulated(state: ExporterState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureState(message: EnsureStateMessage): Promise<EnsureStateReply> {
|
async function ensureAuthState(message: EnsureStateAction) {
|
||||||
console.info('Ensuring state');
|
const clientState: PartialStateUpdate = message.clientState!;
|
||||||
const clientState: PartialStateUpdate = message.clientState;;
|
|
||||||
let state = await State.current;
|
let state = await State.current;
|
||||||
|
|
||||||
if (isStateClear(state)) {
|
if (isStateClear(state)) {
|
||||||
await State.update({ status: Status.Updating });
|
await State.update({ status: Status.Updating });
|
||||||
const session = await landsbankinn.authSession();
|
const session = await landsbankinn.authSession();
|
||||||
await State.update({
|
await State.update({
|
||||||
status: Status.AbleToRequest, authToken: session.accessToken, ...clientState
|
status: Status.AbleToRequest,
|
||||||
|
authToken: session.accessToken,
|
||||||
|
...clientState
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info('Updated auth state from client and auth session');
|
console.info('Updated auth state from client and auth session');
|
||||||
}
|
}
|
||||||
|
|
||||||
state = await State.current;
|
return await State.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureState(message: EnsureStateAction): Promise<EnsureStateReply> {
|
||||||
|
console.info('Ensuring state');
|
||||||
|
const state = await ensureAuthState(message);
|
||||||
await ensureDataPopulated(state);
|
await ensureDataPopulated(state);
|
||||||
return { state: 'Populated' };
|
return { state: 'Populated' };
|
||||||
}
|
}
|
||||||
|
@ -49,8 +79,7 @@ async function getAccounts(): Promise<GetAccountsReply> {
|
||||||
accounts: (await State.current).accounts,
|
accounts: (await State.current).accounts,
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
message = {
|
message = {
|
||||||
accounts: undefined,
|
accounts: undefined,
|
||||||
error: new Error('state not yet populated')
|
error: new Error('state not yet populated')
|
||||||
|
@ -60,13 +89,58 @@ async function getAccounts(): Promise<GetAccountsReply> {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DownloadResult {
|
||||||
|
accountNumber: string;
|
||||||
|
transactions: landsbankinn.RawTransaction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllTransactions = async (message: DownloadTransactionsAction) =>
|
||||||
|
await Promise.all(
|
||||||
|
message.accounts.map(async (accountNumber) => {
|
||||||
|
const transactions = (
|
||||||
|
await downloadTransactions({
|
||||||
|
accountNumber: accountNumber,
|
||||||
|
dateFrom: message.dateFrom,
|
||||||
|
dateTo: message.dateTo,
|
||||||
|
outputFormat: OutputFormat.CSV
|
||||||
|
})
|
||||||
|
).map((txn) => ({ txnAccountNumber: accountNumber, ...txn }));
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapToCsv = (transactions: GroupedRawTransaction[]) => Papa.unparse(transactions);
|
||||||
|
|
||||||
|
async function downloadStatements(
|
||||||
|
message: DownloadTransactionsMessage
|
||||||
|
): Promise<DownloadTransactionReply> {
|
||||||
|
console.info('Initiating download for', message.accounts);
|
||||||
|
const state = await ensureAuthState({ clientState: await State.current });
|
||||||
|
await ensureDataPopulated(state);
|
||||||
|
|
||||||
|
// Should always be populated, but we can be careful anyway.
|
||||||
|
if (isStatePopulated(state)) {
|
||||||
|
const transactions = (await getAllTransactions(message)).flat();
|
||||||
|
return { csv: mapToCsv(transactions), error: null };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
csv: undefined,
|
||||||
|
error: new Error('state not yet populated')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener(async (message) => {
|
browser.runtime.onMessage.addListener(async (message) => {
|
||||||
|
console.log('Received message:', message);
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case Actions.EnsureState:
|
case Actions.EnsureState:
|
||||||
return await ensureState(message);
|
return await ensureState(message as EnsureStateMessage);
|
||||||
case Actions.GetAccounts:
|
case Actions.GetAccounts:
|
||||||
return await getAccounts();
|
return await getAccounts();
|
||||||
|
case Actions.DownloadTransactions:
|
||||||
|
return await downloadStatements(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export { }
|
export {};
|
||||||
|
|
|
@ -27,3 +27,15 @@ export interface EnsureStateMessage {
|
||||||
export interface EnsureStateReply {
|
export interface EnsureStateReply {
|
||||||
state: 'Populated' | 'Error';
|
state: 'Populated' | 'Error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DownloadTransactionsMessage {
|
||||||
|
action: Actions.DownloadTransactions;
|
||||||
|
accounts: string[];
|
||||||
|
dateFrom: Date;
|
||||||
|
dateTo: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadTransactionReply {
|
||||||
|
csv: string | undefined;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,38 @@
|
||||||
// Functions for communicating with the background page.
|
// Functions for communicating with the background page.
|
||||||
|
|
||||||
import { PartialStateUpdate } from '../state';
|
import { PartialStateUpdate } from '../state';
|
||||||
import { Actions, EnsureStateMessage, EnsureStateReply, GetAccountsMessage, GetAccountsReply } from '../background/messages';
|
import {
|
||||||
|
Actions,
|
||||||
|
DownloadTransactionReply,
|
||||||
|
DownloadTransactionsMessage,
|
||||||
|
EnsureStateMessage,
|
||||||
|
EnsureStateReply,
|
||||||
|
GetAccountsMessage,
|
||||||
|
GetAccountsReply
|
||||||
|
} from '../background/messages';
|
||||||
|
|
||||||
export async function getAccounts(): Promise<GetAccountsReply> {
|
export async function getAccounts(): Promise<GetAccountsReply> {
|
||||||
const message: GetAccountsMessage = { action: Actions.GetAccounts };
|
const message: GetAccountsMessage = { action: Actions.GetAccounts };
|
||||||
return await browser.runtime.sendMessage(message);
|
return await browser.runtime.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureState(clientState: PartialStateUpdate): Promise<EnsureStateReply> {
|
export async function ensureState(
|
||||||
|
clientState: PartialStateUpdate
|
||||||
|
): Promise<EnsureStateReply> {
|
||||||
const message: EnsureStateMessage = { action: Actions.EnsureState, clientState };
|
const message: EnsureStateMessage = { action: Actions.EnsureState, clientState };
|
||||||
return await browser.runtime.sendMessage(message);
|
return await browser.runtime.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadTransactions(
|
||||||
|
accounts: string[],
|
||||||
|
dateFrom: Date,
|
||||||
|
dateTo: Date
|
||||||
|
): Promise<DownloadTransactionReply> {
|
||||||
|
const message: DownloadTransactionsMessage = {
|
||||||
|
accounts,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
action: Actions.DownloadTransactions
|
||||||
|
};
|
||||||
|
return await browser.runtime.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Communicates data about the GraphQL URL and API key to the
|
// Communicates data about the GraphQL URL and API key to the
|
||||||
// extension.
|
// extension.
|
||||||
|
|
||||||
import { PartialStateUpdate } from "../state"; // TODO do not run state on content scripts
|
import { PartialStateUpdate } from '../state'; // TODO do not run state on content scripts
|
||||||
import { Event, ClientEvents } from './events';
|
import { Event, ClientEvents } from './events';
|
||||||
import * as background from './background';
|
import * as background from './background';
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ interface ApiInformation {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegularWindowProperties {
|
interface RegularWindowProperties {
|
||||||
add_deviceprint(): string
|
add_deviceprint(): string;
|
||||||
LIPageData: ApiInformation;
|
LIPageData: ApiInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,26 +35,32 @@ interface LIWindowMega extends Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LIWindow = LIWindowRegular | LIWindowMega;
|
type LIWindow = LIWindowRegular | LIWindowMega;
|
||||||
type AnyWindow = Window | LIWindowRegular | LIWindowMega
|
type AnyWindow = Window | LIWindowRegular | LIWindowMega;
|
||||||
|
|
||||||
function isApiInformation(liData: ApiInformation) {
|
function isApiInformation(liData: ApiInformation) {
|
||||||
return liData.graphQLUrl !== undefined &&
|
return (
|
||||||
|
liData.graphQLUrl !== undefined &&
|
||||||
liData.apikey !== undefined &&
|
liData.apikey !== undefined &&
|
||||||
liData.locale !== undefined;
|
liData.locale !== undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRegularWindow(win: AnyWindow): win is LIWindowRegular {
|
function isRegularWindow(win: AnyWindow): win is LIWindowRegular {
|
||||||
const liWindow = win as LIWindowRegular;
|
const liWindow = win as LIWindowRegular;
|
||||||
return typeof liWindow.wrappedJSObject.add_deviceprint === 'function' &&
|
return (
|
||||||
|
typeof liWindow.wrappedJSObject.add_deviceprint === 'function' &&
|
||||||
liWindow.wrappedJSObject.LIPageData !== undefined &&
|
liWindow.wrappedJSObject.LIPageData !== undefined &&
|
||||||
isApiInformation(liWindow.wrappedJSObject.LIPageData);
|
isApiInformation(liWindow.wrappedJSObject.LIPageData)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMegaHeaderWindow(win: AnyWindow): win is LIWindowRegular {
|
function isMegaHeaderWindow(win: AnyWindow): win is LIWindowRegular {
|
||||||
const liWindow = win as LIWindowMega;
|
const liWindow = win as LIWindowMega;
|
||||||
return typeof liWindow.wrappedJSObject.add_deviceprint === 'function' &&
|
return (
|
||||||
|
typeof liWindow.wrappedJSObject.add_deviceprint === 'function' &&
|
||||||
liWindow.wrappedJSObject.LIPageData !== undefined &&
|
liWindow.wrappedJSObject.LIPageData !== undefined &&
|
||||||
isApiInformation(liWindow.wrappedJSObject.LIPageData.megaHeader);
|
isApiInformation(liWindow.wrappedJSObject.LIPageData.megaHeader)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type guard that coerces the global window object to the specialized
|
// Type guard that coerces the global window object to the specialized
|
||||||
|
@ -66,7 +72,7 @@ function isLandsbankinnWindow(win: AnyWindow): win is LIWindow {
|
||||||
|
|
||||||
// Extract data from a page where the LIPageData field is prseent.
|
// Extract data from a page where the LIPageData field is prseent.
|
||||||
// Supports extraction from both the normal data structure found on
|
// Supports extraction from both the normal data structure found on
|
||||||
// most pages, and the "megaHeader" structure foud on the statements
|
// most pages, and the "megaHeader" structure found on the statements
|
||||||
// page.
|
// page.
|
||||||
function extractStateUpdate(win: LIWindow): PartialStateUpdate | undefined {
|
function extractStateUpdate(win: LIWindow): PartialStateUpdate | undefined {
|
||||||
if (isRegularWindow(win)) {
|
if (isRegularWindow(win)) {
|
||||||
|
@ -90,6 +96,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||||
const clientState = extractStateUpdate(window);
|
const clientState = extractStateUpdate(window);
|
||||||
|
|
||||||
if (clientState) {
|
if (clientState) {
|
||||||
|
console.info('Acquired client state');
|
||||||
await background.ensureState(clientState);
|
await background.ensureState(clientState);
|
||||||
ClientEvents.emit(Event.StatePopulated);
|
ClientEvents.emit(Event.StatePopulated);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { FormEventHandler } from 'jsx-dom'; // necessary for react to work
|
import * as React from 'jsx-dom'; // necessary for React to work?
|
||||||
import { Account } from '../../landsbankinn/models';
|
import { Account } from '../../landsbankinn/models';
|
||||||
import $ from 'cash-dom';
|
import $ from 'cash-dom';
|
||||||
|
|
||||||
interface AccountListProps {
|
interface AccountListProps {
|
||||||
accounts: Array<Account>;
|
accounts: Array<Account>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatAccountNumber = (accountNumber: string) =>
|
const formatAccountNumber = (accountNumber: string) =>
|
||||||
|
@ -129,14 +130,19 @@ interface ExportDivProps {
|
||||||
fromLabel: string;
|
fromLabel: string;
|
||||||
toLabel: string;
|
toLabel: string;
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
onDownload: (accountNumbers: string[]) => void;
|
onDownload: (accountNumbers: string[], dateFrom: Date, dateTo: Date) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExportDiv = (props: ExportDivProps) => {
|
export const ExportDiv = (props: ExportDivProps) => {
|
||||||
const submitHandler = (e: Event) => {
|
const submitHandler = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const accountNumbers = getValues($('#account-export').get(0)!);
|
const accountNumbers = getValues($('#account-export').get(0)!);
|
||||||
props.onDownload(accountNumbers);
|
const dateFrom = $(`#${datePickerID(props.fromLabel)}`).val() as string;
|
||||||
|
const dateTo = $(`#${datePickerID(props.toLabel)}`).val() as string;
|
||||||
|
|
||||||
|
console.log(dateFrom);
|
||||||
|
props.onDownload(accountNumbers, new Date(dateFrom), new Date(dateTo));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,14 +1,31 @@
|
||||||
import React from 'jsx-dom'; // necessary for react to work
|
import * as React from 'jsx-dom'; // necessary for react to work
|
||||||
import { Event, ClientEvents } from '../events';
|
import { Event, ClientEvents } from '../events';
|
||||||
import * as background from '../background';
|
import * as background from '../background';
|
||||||
import { ExportDiv } from './components';
|
import { ExportDiv } from './components';
|
||||||
|
|
||||||
|
const logDownload = (accountNumbers: string[], dateFrom: Date, dateTo: Date) =>
|
||||||
|
console.info(
|
||||||
|
`Downloading transactions from ${dateFrom} to ${dateTo} ` +
|
||||||
|
`for: ${accountNumbers.join(', ')}`
|
||||||
|
);
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
ClientEvents.on(Event.StatePopulated, async () => {
|
ClientEvents.on(Event.StatePopulated, async () => {
|
||||||
const response = await background.getAccounts();
|
const response = await background.getAccounts();
|
||||||
|
|
||||||
const downloadHandler = (accountNumbers: string[]) => {
|
const downloadHandler = async (
|
||||||
console.log('downloading txns for', accountNumbers);
|
accountNumbers: string[], dateFrom: Date, dateTo: Date
|
||||||
|
) => {
|
||||||
|
logDownload(accountNumbers, dateFrom, dateTo);
|
||||||
|
const reply = await background.downloadTransactions(
|
||||||
|
accountNumbers, dateFrom, dateTo
|
||||||
|
);
|
||||||
|
|
||||||
|
if (reply.error == null) {
|
||||||
|
console.log(reply.csv);
|
||||||
|
} else {
|
||||||
|
console.error(reply.error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response.error == null) {
|
if (response.error == null) {
|
||||||
|
|
|
@ -43,15 +43,20 @@ export class LevelState {
|
||||||
|
|
||||||
private async prepTTL() {
|
private async prepTTL() {
|
||||||
console.info(`Will clear state in ${Config.StateTTL} milliseconds`);
|
console.info(`Will clear state in ${Config.StateTTL} milliseconds`);
|
||||||
this.currentTTLHandle = window.setTimeout(async () => await this.clear(), Config.StateTTL);
|
this.currentTTLHandle = window.setTimeout(
|
||||||
|
async () => await this.clear(),
|
||||||
|
Config.StateTTL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(stateUpdate: ExporterState | PartialStateUpdate): Promise<ExporterState> {
|
async update(
|
||||||
|
stateUpdate: ExporterState | PartialStateUpdate
|
||||||
|
): Promise<ExporterState> {
|
||||||
this.clearTTL();
|
this.clearTTL();
|
||||||
const currentState: ExporterState = await this.current;
|
const currentState: ExporterState = await this.current;
|
||||||
let newState = { ...currentState, ...stateUpdate };
|
let newState = { ...currentState, ...stateUpdate };
|
||||||
await this._db.put('state', newState as any);
|
await this._db.put('state', newState as any);
|
||||||
this.prepTTL();
|
this.prepTTL();
|
||||||
return this.current;
|
return await this.current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"target": "es6",
|
"target": "es2022",
|
||||||
"module": "es6",
|
"module": "es2022",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"lib": [ "dom", "es6" ],
|
"lib": [ "dom", "es2022", "esnext" ],
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "jsx-dom",
|
"jsxImportSource": "jsx-dom",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
Loading…
Reference in New Issue