Lots of refactoring to get a working list of checkboxes
This commit is contained in:
parent
32dfd7d7b1
commit
52583c4e32
|
@ -21,10 +21,12 @@
|
|||
"maxParallelRequests": 20
|
||||
},
|
||||
"dependencies": {
|
||||
"cash-dom": "^8.1.4",
|
||||
"date-fns": "^2.29.3",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^5.2.0",
|
||||
"jsx-dom": "^8.0.5",
|
||||
"level": "^8.0.0"
|
||||
"level": "^8.0.0",
|
||||
"nanoevents": "^7.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +1,67 @@
|
|||
import { PartialStateUpdate } from "../state";
|
||||
import { isStatePopulated, isStateClear, PartialStateUpdate, ExporterState, isStateAbleToRequest } from "../state";
|
||||
import { Status } from '../state';
|
||||
import { State } from '../state/level';
|
||||
import * as landsbankinn from '../landsbankinn';
|
||||
import { fetchAccounts } from "../landsbankinn";
|
||||
import { Actions, EnsureStateMessage, EnsureStateReply, GetAccountsReply } from './messages';
|
||||
|
||||
export enum Actions {
|
||||
EnsureState = 'ENSURE_STATE',
|
||||
GetAccounts = 'GET_ACCOUNTS',
|
||||
DownloadTransactions = 'DOWNLOAD_TRANSACTIONS'
|
||||
}
|
||||
|
||||
type MessageSender = browser.runtime.MessageSender;
|
||||
|
||||
async function getAccounts(sender: MessageSender): Promise<any> {
|
||||
//TODO somehow ensure state exists.
|
||||
const message = {
|
||||
accounts: (await State.current).accounts
|
||||
};
|
||||
|
||||
browser.tabs.sendMessage(sender.tab?.id!, message);
|
||||
}
|
||||
|
||||
async function ensureState(message: any, sender: MessageSender) {
|
||||
const clientState: PartialStateUpdate = message.clientState;
|
||||
const state = await State.current;
|
||||
|
||||
if (!state.ready) {
|
||||
await State.update({ ready: true });
|
||||
const session = await landsbankinn.authSession();
|
||||
const newState = await State.update({
|
||||
authToken: session.accessToken, ...clientState
|
||||
})
|
||||
|
||||
if (!newState.accounts) {
|
||||
const accounts = await fetchAccounts(newState);
|
||||
await State.update({ accounts });
|
||||
console.info('Acquired account data');
|
||||
}
|
||||
|
||||
console.info('Updated state from client and auth session');
|
||||
console.log(await State.current);
|
||||
async function ensureDataPopulated(state: ExporterState) {
|
||||
if (isStateAbleToRequest(state) && !isStatePopulated(state)) {
|
||||
const accounts = await fetchAccounts(state);
|
||||
await State.update({ status: Status.Populated, accounts });
|
||||
console.info('Populated account data');
|
||||
}
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((message, sender) => {
|
||||
console.log('received message', message);
|
||||
async function ensureState(message: EnsureStateMessage): Promise<EnsureStateReply> {
|
||||
console.info('Ensuring state');
|
||||
const clientState: PartialStateUpdate = message.clientState;;
|
||||
let state = await State.current;
|
||||
|
||||
if (isStateClear(state)) {
|
||||
await State.update({ status: Status.Updating });
|
||||
const session = await landsbankinn.authSession();
|
||||
await State.update({
|
||||
status: Status.AbleToRequest, authToken: session.accessToken, ...clientState
|
||||
});
|
||||
|
||||
console.info('Updated auth state from client and auth session');
|
||||
}
|
||||
|
||||
state = await State.current;
|
||||
await ensureDataPopulated(state);
|
||||
return { state: 'Populated' };
|
||||
}
|
||||
|
||||
async function getAccounts(): Promise<GetAccountsReply> {
|
||||
const state = await State.current;
|
||||
let message: any;
|
||||
|
||||
await ensureDataPopulated(state);
|
||||
|
||||
// Should always be populated, but we can be careful anyway.
|
||||
if (isStatePopulated(state)) {
|
||||
message = {
|
||||
accounts: (await State.current).accounts,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
else {
|
||||
message = {
|
||||
accounts: undefined,
|
||||
error: new Error('state not yet populated')
|
||||
};
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(async (message) => {
|
||||
switch (message.action) {
|
||||
case Actions.EnsureState:
|
||||
ensureState(message, sender);
|
||||
return await ensureState(message);
|
||||
case Actions.GetAccounts:
|
||||
getAccounts(sender);
|
||||
return await getAccounts();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Request and replies for messages passed between the background page
|
||||
// and the content script.
|
||||
|
||||
import { PartialStateUpdate } from "~state";
|
||||
import { Account } from "../landsbankinn/models";
|
||||
|
||||
export enum Actions {
|
||||
EnsureState = 'ENSURE_STATE',
|
||||
GetAccounts = 'GET_ACCOUNTS',
|
||||
DownloadTransactions = 'DOWNLOAD_TRANSACTIONS'
|
||||
}
|
||||
|
||||
export interface GetAccountsMessage {
|
||||
action: Actions.GetAccounts;
|
||||
}
|
||||
|
||||
export interface GetAccountsReply {
|
||||
accounts: Array<Account>,
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export interface EnsureStateMessage {
|
||||
action: Actions.EnsureState;
|
||||
clientState: PartialStateUpdate;
|
||||
}
|
||||
|
||||
export interface EnsureStateReply {
|
||||
state: 'Populated' | 'Error';
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Functions for communicating with the background page.
|
||||
|
||||
import { PartialStateUpdate } from '../state';
|
||||
import { Actions, EnsureStateMessage, EnsureStateReply, GetAccountsMessage, GetAccountsReply } from '../background/messages';
|
||||
|
||||
export async function getAccounts(): Promise<GetAccountsReply> {
|
||||
const message: GetAccountsMessage = { action: Actions.GetAccounts };
|
||||
return await browser.runtime.sendMessage(message);
|
||||
}
|
||||
|
||||
export async function ensureState(clientState: PartialStateUpdate): Promise<EnsureStateReply> {
|
||||
const message: EnsureStateMessage = { action: Actions.EnsureState, clientState };
|
||||
return await browser.runtime.sendMessage(message);
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
// Communicates data about the GraphQL URL and API key to the
|
||||
// extension.
|
||||
|
||||
import { PartialStateUpdate } from "../state"; // TODO do not run state on content scripts
|
||||
import { Actions } from '../background/message-handler';
|
||||
import { Event, ClientEvents } from './events';
|
||||
import * as background from './background';
|
||||
|
||||
// The actual information we care about
|
||||
interface ApiInformation {
|
||||
|
@ -38,6 +42,7 @@ function isApiInformation(liData: ApiInformation) {
|
|||
liData.apikey !== undefined &&
|
||||
liData.locale !== undefined;
|
||||
}
|
||||
|
||||
function isRegularWindow(win: AnyWindow): win is LIWindowRegular {
|
||||
const liWindow = win as LIWindowRegular;
|
||||
return typeof liWindow.wrappedJSObject.add_deviceprint === 'function' &&
|
||||
|
@ -59,6 +64,10 @@ function isLandsbankinnWindow(win: AnyWindow): win is LIWindow {
|
|||
return isRegularWindow(win) || isMegaHeaderWindow(win);
|
||||
}
|
||||
|
||||
// Extract data from a page where the LIPageData field is prseent.
|
||||
// Supports extraction from both the normal data structure found on
|
||||
// most pages, and the "megaHeader" structure foud on the statements
|
||||
// page.
|
||||
function extractStateUpdate(win: LIWindow): PartialStateUpdate | undefined {
|
||||
if (isRegularWindow(win)) {
|
||||
return {
|
||||
|
@ -75,17 +84,16 @@ function extractStateUpdate(win: LIWindow): PartialStateUpdate | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
// Communicates data about the GraphQL URL and API key to the
|
||||
// extension.
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log(window);
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
if (isLandsbankinnWindow(window)) {
|
||||
console.info('Ensuring extension state');
|
||||
const clientState = extractStateUpdate(window);
|
||||
await browser.runtime.sendMessage({
|
||||
action: Actions.EnsureState, clientState
|
||||
});
|
||||
|
||||
if (clientState) {
|
||||
await background.ensureState(clientState);
|
||||
ClientEvents.emit(Event.StatePopulated);
|
||||
} else {
|
||||
console.error('Could not find auth info on page!');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { }
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { createNanoEvents } from 'nanoevents';
|
||||
|
||||
export enum Event {
|
||||
StatePopulated = 'StatePopulated'
|
||||
}
|
||||
|
||||
export const ClientEvents = createNanoEvents();
|
|
@ -1,27 +1,65 @@
|
|||
import React from 'jsx-dom';
|
||||
import React from 'jsx-dom'; // necessary for react to work
|
||||
import { Actions, GetAccountsReply } from '../background/messages';
|
||||
import { Event, ClientEvents } from './events';
|
||||
import * as background from './background';
|
||||
import { Account } from '../landsbankinn/models';
|
||||
import $ from 'cash-dom';
|
||||
|
||||
async function populateAccounts() {
|
||||
const resp = await browser.runtime.sendMessage({
|
||||
action: 'GET_ACCOUNTS',
|
||||
});
|
||||
interface AccountListProps {
|
||||
accounts: Array<Account>;
|
||||
}
|
||||
|
||||
//Add an element to the statement page
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const contentDiv = document.querySelector('div[class="table-data content-box fill shadow"]');
|
||||
contentDiv?.prepend(
|
||||
<div class="content-box fill shadow ui-form">
|
||||
<h2>Download Transaction Statements</h2>
|
||||
<p>
|
||||
Here, you can export statements from one or more of your accounts
|
||||
over a specified date range.
|
||||
</p>
|
||||
</div>
|
||||
function AccountList(props: AccountListProps) {
|
||||
const selectAllHandler = () => {
|
||||
const checked = $('#check-all').prop('checked');
|
||||
$('#account-export-list input[type="checkbox"]').prop('checked', checked);
|
||||
};
|
||||
|
||||
const accountList = props.accounts.map(acct =>
|
||||
<li>
|
||||
<input id="export-{acct.acountNumber}" type="checkbox" />
|
||||
|
||||
<label htmlFor="export-{acct.acountNumber}">
|
||||
{acct.accountNumber} - {acct.name}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
|
||||
await populateAccounts();
|
||||
});
|
||||
return (
|
||||
<div id="account-export-list">
|
||||
<ul>
|
||||
<li>
|
||||
<input id="check-all" type="checkbox" onClick={selectAllHandler} />
|
||||
|
||||
<label htmlFor="check-all">Select All</label>
|
||||
</li>
|
||||
{accountList}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((message, sender) => {
|
||||
console.log('message received!', message);
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
ClientEvents.on(Event.StatePopulated, async () => {
|
||||
const response = await background.getAccounts();
|
||||
|
||||
if (response.error == null) {
|
||||
const exportDiv = (
|
||||
<div id="account-export" class="content-box fill shadow ui-form">
|
||||
<h2>Download Transaction Statements</h2>
|
||||
<p>
|
||||
Here, you can export statements from one or more of your accounts
|
||||
over a specified date range.
|
||||
</p>
|
||||
|
||||
<AccountList accounts={response.accounts} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const contentDiv = document.querySelector(
|
||||
'div[class="table-data content-box fill shadow"]'
|
||||
);
|
||||
contentDiv?.prepend(exportDiv);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
|
||||
export interface Account {
|
||||
accountNumber: string;
|
||||
name: String;
|
||||
name: string;
|
||||
currency: string;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import { Account } from '../landsbankinn/models';
|
||||
|
||||
export enum Status {
|
||||
NotReady, // completely clear
|
||||
Updating, // in process of gathering auth data
|
||||
AbleToRequest, // able to make requests
|
||||
Populated // has all necessary information
|
||||
}
|
||||
|
||||
export interface ExporterState {
|
||||
apiKey: string;
|
||||
authToken: string;
|
||||
fingerprintValue: string;
|
||||
accounts: Array<Account>;
|
||||
ready: boolean;
|
||||
status: Status
|
||||
}
|
||||
|
||||
export interface PartialStateUpdate {
|
||||
|
@ -13,7 +20,7 @@ export interface PartialStateUpdate {
|
|||
authToken?: string;
|
||||
fingerprintValue?: string;
|
||||
accounts?: Array<Account>;
|
||||
ready?: boolean;
|
||||
status?: Status
|
||||
}
|
||||
|
||||
// TODO export it and replace direct references to level with it
|
||||
|
@ -23,3 +30,12 @@ interface StateStore {
|
|||
clear(): Promise<any>;
|
||||
|
||||
}
|
||||
|
||||
export const isStateClear = (state: ExporterState) =>
|
||||
state.status === undefined || state.status == Status.NotReady;
|
||||
|
||||
export const isStatePopulated = (state: ExporterState) =>
|
||||
state.status == Status.Populated;
|
||||
|
||||
export const isStateAbleToRequest = (state: ExporterState) =>
|
||||
state.status == Status.AbleToRequest || state.status == Status.Populated;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Level } from 'level';
|
||||
import { ExporterState, PartialStateUpdate } from './index';
|
||||
import { ExporterState, PartialStateUpdate, Status } from './index';
|
||||
|
||||
const FIVE_MINUTES = 0.25 * 60 * 1000;
|
||||
|
||||
|
@ -33,7 +33,7 @@ class LevelState {
|
|||
|
||||
async clear() {
|
||||
console.info('Cleared state');
|
||||
return await this._db.put('state', {} as any);
|
||||
return await this._db.put('state', { status: Status.NotReady } as any);
|
||||
}
|
||||
|
||||
private async clearTTL() {
|
||||
|
|
9
todo.org
9
todo.org
|
@ -1,11 +1,14 @@
|
|||
How to get it working:
|
||||
- [X] Landsbankinn API client for arbitrary transaction lists
|
||||
- [ ] Transform raw transactions into friendlier ones (i.e. date objects)
|
||||
- [ ] Content script that can run all the time and puts a button somewhere (statements tab, and extension icon)
|
||||
- [X] Content script that can run all the time and puts a button somewhere (statements tab, and extension icon)
|
||||
- [ ] Use config.ts to set TTL for state
|
||||
- [ ] Background page that downloads all the shit and combines into CSV, then 'downloads' file.
|
||||
- [ ] Build up communication message passing to background page for downloads
|
||||
- [ ] Investigate persistent = false + setTimeout
|
||||
- [X] Build up communication message passing to background page for downloads
|
||||
- [ ] Don't run level on client.
|
||||
- [-] Investigate persistent = false + setTimeout
|
||||
- [ ] Maybe can force clear state when page reloads itself?
|
||||
- [X] Change ready true/false to lock state, of not updated, updating, ready
|
||||
- [X] Graphql API client to get information about user, like listing accounts.
|
||||
- [X] Get working client
|
||||
- [X] Set API key
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -938,6 +938,11 @@ caniuse-lite@^1.0.30001449:
|
|||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001468.tgz#0101837c6a4e38e6331104c33dcfb3bdf367a4b7"
|
||||
integrity sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A==
|
||||
|
||||
cash-dom@^8.1.4:
|
||||
version "8.1.4"
|
||||
resolved "https://registry.yarnpkg.com/cash-dom/-/cash-dom-8.1.4.tgz#445c2a509cffa8b1c99094634418b4b439d57718"
|
||||
integrity sha512-bFLMk+r3lv+sDwxlAFfRlMxpRls7zMnSQePVpNouwnpm9G4MbLYZZtIUG2urUgfmIaKlc/hqG8o7yZg3+nFKRA==
|
||||
|
||||
catering@^2.1.0, catering@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510"
|
||||
|
@ -1480,6 +1485,11 @@ msgpackr@^1.5.4:
|
|||
optionalDependencies:
|
||||
msgpackr-extract "^3.0.1"
|
||||
|
||||
nanoevents@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoevents/-/nanoevents-7.0.1.tgz#181580b47787688d8cac775b977b1cf24e26e570"
|
||||
integrity sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q==
|
||||
|
||||
napi-macros@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
|
||||
|
|
Loading…
Reference in New Issue