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
|
"maxParallelRequests": 20
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cash-dom": "^8.1.4",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { State } from '../state/level';
|
||||||
import * as landsbankinn from '../landsbankinn';
|
import * as landsbankinn from '../landsbankinn';
|
||||||
import { fetchAccounts } from "../landsbankinn";
|
import { fetchAccounts } from "../landsbankinn";
|
||||||
|
import { Actions, EnsureStateMessage, EnsureStateReply, GetAccountsReply } from './messages';
|
||||||
|
|
||||||
export enum Actions {
|
async function ensureDataPopulated(state: ExporterState) {
|
||||||
EnsureState = 'ENSURE_STATE',
|
if (isStateAbleToRequest(state) && !isStatePopulated(state)) {
|
||||||
GetAccounts = 'GET_ACCOUNTS',
|
const accounts = await fetchAccounts(state);
|
||||||
DownloadTransactions = 'DOWNLOAD_TRANSACTIONS'
|
await State.update({ status: Status.Populated, accounts });
|
||||||
}
|
console.info('Populated account data');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message, sender) => {
|
async function ensureState(message: EnsureStateMessage): Promise<EnsureStateReply> {
|
||||||
console.log('received message', message);
|
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) {
|
switch (message.action) {
|
||||||
case Actions.EnsureState:
|
case Actions.EnsureState:
|
||||||
ensureState(message, sender);
|
return await ensureState(message);
|
||||||
case Actions.GetAccounts:
|
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 { 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
|
// The actual information we care about
|
||||||
interface ApiInformation {
|
interface ApiInformation {
|
||||||
|
@ -38,6 +42,7 @@ function isApiInformation(liData: ApiInformation) {
|
||||||
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' &&
|
||||||
|
@ -59,6 +64,10 @@ function isLandsbankinnWindow(win: AnyWindow): win is LIWindow {
|
||||||
return isRegularWindow(win) || isMegaHeaderWindow(win);
|
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 {
|
function extractStateUpdate(win: LIWindow): PartialStateUpdate | undefined {
|
||||||
if (isRegularWindow(win)) {
|
if (isRegularWindow(win)) {
|
||||||
return {
|
return {
|
||||||
|
@ -75,17 +84,16 @@ function extractStateUpdate(win: LIWindow): PartialStateUpdate | undefined {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Communicates data about the GraphQL URL and API key to the
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
// extension.
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
console.log(window);
|
|
||||||
if (isLandsbankinnWindow(window)) {
|
if (isLandsbankinnWindow(window)) {
|
||||||
console.info('Ensuring extension state');
|
console.info('Ensuring extension state');
|
||||||
const clientState = extractStateUpdate(window);
|
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() {
|
interface AccountListProps {
|
||||||
const resp = await browser.runtime.sendMessage({
|
accounts: Array<Account>;
|
||||||
action: 'GET_ACCOUNTS',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add an element to the statement page
|
function AccountList(props: AccountListProps) {
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
const selectAllHandler = () => {
|
||||||
const contentDiv = document.querySelector('div[class="table-data content-box fill shadow"]');
|
const checked = $('#check-all').prop('checked');
|
||||||
contentDiv?.prepend(
|
$('#account-export-list input[type="checkbox"]').prop('checked', checked);
|
||||||
<div class="content-box fill shadow ui-form">
|
};
|
||||||
<h2>Download Transaction Statements</h2>
|
|
||||||
<p>
|
const accountList = props.accounts.map(acct =>
|
||||||
Here, you can export statements from one or more of your accounts
|
<li>
|
||||||
over a specified date range.
|
<input id="export-{acct.acountNumber}" type="checkbox" />
|
||||||
</p>
|
|
||||||
</div>
|
<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) => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
console.log('message received!', message);
|
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 {
|
export interface Account {
|
||||||
accountNumber: string;
|
accountNumber: string;
|
||||||
name: String;
|
name: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import { Account } from '../landsbankinn/models';
|
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 {
|
export interface ExporterState {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
authToken: string;
|
authToken: string;
|
||||||
fingerprintValue: string;
|
fingerprintValue: string;
|
||||||
accounts: Array<Account>;
|
accounts: Array<Account>;
|
||||||
ready: boolean;
|
status: Status
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PartialStateUpdate {
|
export interface PartialStateUpdate {
|
||||||
|
@ -13,7 +20,7 @@ export interface PartialStateUpdate {
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
fingerprintValue?: string;
|
fingerprintValue?: string;
|
||||||
accounts?: Array<Account>;
|
accounts?: Array<Account>;
|
||||||
ready?: boolean;
|
status?: Status
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO export it and replace direct references to level with it
|
// TODO export it and replace direct references to level with it
|
||||||
|
@ -23,3 +30,12 @@ interface StateStore {
|
||||||
clear(): Promise<any>;
|
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 { Level } from 'level';
|
||||||
import { ExporterState, PartialStateUpdate } from './index';
|
import { ExporterState, PartialStateUpdate, Status } from './index';
|
||||||
|
|
||||||
const FIVE_MINUTES = 0.25 * 60 * 1000;
|
const FIVE_MINUTES = 0.25 * 60 * 1000;
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class LevelState {
|
||||||
|
|
||||||
async clear() {
|
async clear() {
|
||||||
console.info('Cleared state');
|
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() {
|
private async clearTTL() {
|
||||||
|
|
9
todo.org
9
todo.org
|
@ -1,11 +1,14 @@
|
||||||
How to get it working:
|
How to get it working:
|
||||||
- [X] Landsbankinn API client for arbitrary transaction lists
|
- [X] Landsbankinn API client for arbitrary transaction lists
|
||||||
- [ ] Transform raw transactions into friendlier ones (i.e. date objects)
|
- [ ] 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.
|
- [ ] Background page that downloads all the shit and combines into CSV, then 'downloads' file.
|
||||||
- [ ] Build up communication message passing to background page for downloads
|
- [X] Build up communication message passing to background page for downloads
|
||||||
- [ ] Investigate persistent = false + setTimeout
|
- [ ] Don't run level on client.
|
||||||
|
- [-] Investigate persistent = false + setTimeout
|
||||||
- [ ] Maybe can force clear state when page reloads itself?
|
- [ ] 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] Graphql API client to get information about user, like listing accounts.
|
||||||
- [X] Get working client
|
- [X] Get working client
|
||||||
- [X] Set API key
|
- [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"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001468.tgz#0101837c6a4e38e6331104c33dcfb3bdf367a4b7"
|
||||||
integrity sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A==
|
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:
|
catering@^2.1.0, catering@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510"
|
resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510"
|
||||||
|
@ -1480,6 +1485,11 @@ msgpackr@^1.5.4:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
msgpackr-extract "^3.0.1"
|
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:
|
napi-macros@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
|
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
|
||||||
|
|
Loading…
Reference in New Issue