🔥 stripping out plaid integration (#2616)

This commit is contained in:
Matiss Janis Aboltins
2024-04-18 18:31:28 +01:00
committed by GitHub
parent 02e7d036d5
commit 1c04aeae39
13 changed files with 12 additions and 729 deletions

View File

@@ -38,7 +38,6 @@ import { ManageRulesModal } from './modals/ManageRulesModal';
import { MergeUnusedPayees } from './modals/MergeUnusedPayees';
import { Notes } from './modals/Notes';
import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal';
import { PlaidExternalMsg } from './modals/PlaidExternalMsg';
import { ReportBalanceMenuModal } from './modals/ReportBalanceMenuModal';
import { ReportBudgetMenuModal } from './modals/ReportBudgetMenuModal';
import { ReportBudgetSummaryModal } from './modals/ReportBudgetSummaryModal';
@@ -216,20 +215,6 @@ export function Modals() {
/>
);
case 'plaid-external-msg':
return (
<PlaidExternalMsg
key={name}
modalProps={modalProps}
onMoveExternal={options.onMoveExternal}
onClose={() => {
options.onClose?.();
send('poll-web-token-stop');
}}
onSuccess={options.onSuccess}
/>
);
case 'gocardless-init':
return (
<GoCardlessInitialise

View File

@@ -1,154 +0,0 @@
// @ts-strict-ignore
import React, { useState, useRef } from 'react';
import { AnimatedLoading } from '../../icons/AnimatedLoading';
import { theme } from '../../style';
import { Error } from '../alerts';
import { Button } from '../common/Button';
import { Modal, ModalButtons } from '../common/Modal';
import { Paragraph } from '../common/Paragraph';
import { Text } from '../common/Text';
import { View } from '../common/View';
import { type CommonModalProps } from '../Modals';
function renderError(error) {
return (
<Error style={{ alignSelf: 'center' }}>
{error === 'timeout'
? 'Timed out. Please try again.'
: 'An error occurred while linking your account, sorry!'}
</Error>
);
}
type PlainExternalMsgProps = {
modalProps: CommonModalProps;
onMoveExternal: () => Promise<{ error; data }>;
onSuccess: (data: unknown) => Promise<void>;
onClose?: () => void;
};
export function PlaidExternalMsg({
modalProps,
onMoveExternal,
onSuccess,
onClose: originalOnClose,
}: PlainExternalMsgProps) {
const [waiting, setWaiting] = useState(null);
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
const data = useRef(null);
async function onJump() {
setError(null);
setWaiting('browser');
const res = await onMoveExternal();
if (res.error) {
setError(res.error);
setWaiting(null);
return;
}
data.current = res.data;
setWaiting(null);
setSuccess(true);
}
function onClose() {
originalOnClose?.();
modalProps.onClose();
}
async function onContinue() {
setWaiting('accounts');
await onSuccess(data.current);
setWaiting(null);
}
return (
<Modal
title="Link Your Bank"
{...modalProps}
onClose={onClose}
style={{ flex: 0 }}
>
{() => (
<View>
<Paragraph style={{ fontSize: 15 }}>
To link your bank account, you will be moved to your browser for
enhanced security. Click below and Actual will automatically resume
when you have given your banks credentials.
</Paragraph>
{error && renderError(error)}
{waiting ? (
<View style={{ alignItems: 'center', marginTop: 15 }}>
<AnimatedLoading
color={theme.pageTextDark}
style={{ width: 20, height: 20 }}
/>
<View style={{ marginTop: 10, color: theme.pageText }}>
{waiting === 'browser'
? 'Waiting on browser...'
: waiting === 'accounts'
? 'Loading accounts...'
: null}
</View>
</View>
) : success ? (
<Button
type="primary"
style={{
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
marginTop: 10,
}}
onClick={onContinue}
>
Success! Click to continue &rarr;
</Button>
) : (
<Button
type="primary"
style={{
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
marginTop: 10,
}}
onClick={onJump}
>
Link bank in browser &rarr;
</Button>
)}
<div style={{ marginTop: waiting ? 30 : 35 }}>
<Text style={{ color: theme.pageText, fontWeight: 600 }}>
Why not link it in the app?
</Text>
</div>
<Text
style={{
marginTop: 10,
color: theme.pageText,
fontSize: 13,
'& a, & a:visited': {
color: theme.pageText,
},
}}
>
Typing your banks username and password is one of the most
security-sensitive things you can do, and the browser is the most
secure app in the world. Why not use it to make sure your
information is safe? [TODO: Link to docs article]
</Text>
<ModalButtons style={{ marginTop: 10 }}>
<Button onClick={() => modalProps.onBack()}>Back</Button>
</ModalButtons>
</View>
)}
</Modal>
);
}

View File

@@ -1,52 +0,0 @@
/* eslint-disable import/no-unused-modules */
import { send } from 'loot-core/src/platform/client/fetch';
function _authorize(pushModal, plaidToken, { onSuccess, onClose }) {
pushModal('plaid-external-msg', {
onMoveExternal: async () => {
const token = await send('create-web-token');
let url = 'http://link.actualbudget.com/?token=' + token;
// let url = 'http://localhost:8080/?token=' + token;
if (plaidToken) {
url = url + '&plaidToken=' + plaidToken;
}
window.Actual?.openURLInBrowser(url);
const { error, data } = await send('poll-web-token', { token });
return { error, data };
},
onClose,
onSuccess,
});
}
export async function authorizeBank(pushModal, { upgradingId } = {}) {
_authorize(pushModal, null, {
onSuccess: async data => {
pushModal('select-linked-accounts', {
institution: data.metadata.institution,
publicToken: data.publicToken,
accounts: data.metadata.accounts,
upgradingId,
});
},
});
}
export async function reauthorizeBank(pushModal, bankId, onSuccess) {
const { linkToken } = await send('make-plaid-public-token', {
bankId,
});
// We don't do anything with the error right now
if (!linkToken) {
return false;
}
// When the modal is closed here, always try to re-sync the account
// by calling `onSuccess`
_authorize(pushModal, linkToken, { onSuccess, onClose: onSuccess });
return true;
}

View File

@@ -59,12 +59,6 @@ type FinanceModals = {
targetPayeeId: string;
};
'plaid-external-msg': {
onMoveExternal: () => Promise<void>;
onClose?: () => void;
onSuccess: (data: unknown) => Promise<void>;
};
'gocardless-init': {
onSuccess: () => void;
};

View File

@@ -1,59 +0,0 @@
// @ts-strict-ignore
import { v4 as uuidv4 } from 'uuid';
export function generateAccount(balance) {
return {
account_id: uuidv4(),
balances: {
available: balance,
current: balance,
limit: null,
},
mask: '0000',
name: 'Plaid Checking',
official_name: 'Plaid Interest Checking',
subtype: 'checking',
type: 'depository',
};
}
export function generateTransaction(
acctId,
amount,
name,
date,
pending = false,
) {
return {
account_id: acctId,
account_owner: null,
amount,
category: [],
category_id: '',
date,
location: {
address: null,
city: null,
lat: null,
lon: null,
state: null,
store_number: null,
zip: null,
},
name,
payment_meta: {
by_order_of: null,
payee: null,
payer: null,
payment_method: null,
payment_processor: null,
ppd_id: null,
reason: null,
reference_number: null,
},
pending,
pending_transaction_id: null,
transaction_id: uuidv4(),
transaction_type: 'special',
};
}

View File

@@ -1,46 +1,7 @@
// @ts-strict-ignore
import { v4 as uuidv4 } from 'uuid';
import * as asyncStorage from '../../platform/server/asyncStorage';
import { amountToInteger } from '../../shared/util';
import * as db from '../db';
import { runMutator } from '../mutators';
import { post } from '../post';
import { getServer } from '../server-config';
import * as bankSync from './sync';
export async function handoffPublicToken(institution, publicToken) {
const [[, userId], [, key]] = await asyncStorage.multiGet([
'user-id',
'user-key',
]);
if (institution == null || !institution.institution_id || !institution.name) {
throw new Error('Invalid institution object');
}
const id = uuidv4();
// Make sure to generate an access token first before inserting it
// into our local database in case it fails
await post(getServer().PLAID_SERVER + '/handoff_public_token', {
userId,
key,
item_id: id,
public_token: publicToken,
});
await runMutator(() =>
db.insertWithUUID('banks', {
id,
bank_id: institution.institution_id,
name: institution.name,
}),
);
return id;
}
export async function findOrCreateBank(institution, requisitionId) {
const bank = await db.first(
@@ -62,49 +23,3 @@ export async function findOrCreateBank(institution, requisitionId) {
return bankData;
}
export async function addGoCardlessAccounts(
bankId,
accountIds,
offbudgetIds = [],
) {
const [[, userId], [, userKey]] = await asyncStorage.multiGet([
'user-id',
'user-key',
]);
// Get all the available accounts
let accounts = await bankSync.getGoCardlessAccounts(userId, userKey, bankId);
// Only add the selected accounts
accounts = accounts.filter(acct => accountIds.includes(acct.account_id));
return Promise.all(
accounts.map(async acct => {
const id = await runMutator(async () => {
const id = await db.insertAccount({
account_id: acct.account_id,
name: acct.name,
official_name: acct.official_name,
balance_current: amountToInteger(acct.balances.current),
mask: acct.mask,
bank: bankId,
offbudget: offbudgetIds.includes(acct.account_id) ? 1 : 0,
});
// Create a transfer payee
await db.insertPayee({
name: '',
transfer_acct: id,
});
return id;
});
// Do an initial sync
await bankSync.syncAccount(userId, userKey, id, acct.account_id, bankId);
return id;
}),
);
}

View File

@@ -41,7 +41,7 @@ async function prepareDatabase() {
is_income: 1,
});
const { accounts } = await post(getServer().PLAID_SERVER + '/accounts', {
const { accounts } = await post(getServer().GOCARDLESS_SERVER + '/accounts', {
client_id: '',
group_id: '',
item_id: '1',

View File

@@ -20,9 +20,6 @@ import { title } from './title';
import { runRules } from './transaction-rules';
import { batchUpdateTransactions } from './transactions';
// Plaid article about API options:
// https://support.plaid.com/customer/en/portal/articles/2612155-transactions-returned-per-request
function BankSyncError(type: string, code: string) {
return { type: 'BankSyncError', category: type, code };
}
@@ -60,22 +57,6 @@ async function updateAccountBalance(id, balance) {
]);
}
export async function getAccounts(userId, userKey, id) {
const res = await post(getServer().PLAID_SERVER + '/accounts', {
userId,
key: userKey,
item_id: id,
});
const { accounts } = res;
accounts.forEach(acct => {
acct.balances.current = getAccountBalance(acct);
});
return accounts;
}
export async function getGoCardlessAccounts(userId, userKey, id) {
const userToken = await asyncStorage.getItem('user-token');
if (!userToken) return;
@@ -101,80 +82,6 @@ export async function getGoCardlessAccounts(userId, userKey, id) {
return accounts;
}
export function fromPlaid(trans) {
return {
imported_id: trans.transaction_id,
payee_name: trans.name,
imported_payee: trans.name,
amount: -amountToInteger(trans.amount),
date: trans.date,
};
}
async function downloadTransactions(
userId,
userKey,
acctId,
bankId,
since,
count?: number,
) {
let allTransactions = [];
let accountBalance = null;
const pageSize = 100;
let offset = 0;
let numDownloaded = 0;
while (1) {
const endDate = monthUtils.currentDay();
const res = await post(getServer().PLAID_SERVER + '/transactions', {
userId,
key: userKey,
item_id: '' + bankId,
account_id: acctId,
start_date: since,
end_date: endDate,
count: pageSize,
offset,
});
if (res.error_code) {
throw BankSyncError(res.error_type, res.error_code);
}
if (res.transactions.length === 0) {
break;
}
numDownloaded += res.transactions.length;
// Remove pending transactions for now - we will handle them in
// the future.
allTransactions = allTransactions.concat(
res.transactions.filter(t => !t.pending),
);
accountBalance = getAccountBalance(res.accounts[0]);
if (
numDownloaded === res.total_transactions ||
(count != null && allTransactions.length >= count)
) {
break;
}
offset += pageSize;
}
allTransactions =
count != null ? allTransactions.slice(0, count) : allTransactions;
return {
transactions: allTransactions.map(fromPlaid),
accountBalance,
};
}
async function downloadGoCardlessTransactions(
userId,
userKey,
@@ -745,29 +652,8 @@ export async function syncAccount(
startDate,
);
} else {
// Get all transactions since the latest transaction, plus any 5
// days before the latest transaction. This gives us a chance to
// resolve any transactions that were entered manually.
//
// TODO: What this really should do is query the last imported_id
// and since then
let date = monthUtils.subDays(
db.fromDateRepr(latestTransaction.date),
31,
);
// Never download transactions before the starting date. This was
// when the account was added to the system.
if (date < startingDate) {
date = startingDate;
}
download = await downloadTransactions(
userId,
userKey,
acctId,
bankId,
date,
throw new Error(
`Unrecognized bank-sync provider: ${acctRow.account_sync_source}`,
);
}

View File

@@ -567,25 +567,6 @@ handlers['query'] = async function (query) {
return aqlQuery(query);
};
handlers['bank-delete'] = async function ({ id }) {
const accts = await db.runQuery(
'SELECT * FROM accounts WHERE bank = ?',
[id],
true,
);
await db.delete_('banks', id);
await Promise.all(
accts.map(async acct => {
// TODO: This will not sync across devices because we are bypassing
// the "recorded" functions
await db.runQuery('DELETE FROM transactions WHERE acct = ?', [acct.id]);
await db.delete_('accounts', acct.id);
}),
);
return 'ok';
};
handlers['account-update'] = mutator(async function ({ id, name }) {
return withUndo(async () => {
await db.update('accounts', { id, name });
@@ -720,21 +701,6 @@ handlers['simplefin-accounts-link'] = async function ({
return 'ok';
};
handlers['gocardless-accounts-connect'] = async function ({
institution,
publicToken,
accountIds,
offbudgetIds,
}) {
const bankId = await link.handoffPublicToken(institution, publicToken);
const ids = await link.addGoCardlessAccounts(
bankId,
accountIds,
offbudgetIds,
);
return ids;
};
handlers['account-create'] = mutator(async function ({
name,
balance,
@@ -777,8 +743,8 @@ handlers['account-close'] = mutator(async function ({
categoryId,
forced,
}) {
// Unlink the account if it's linked. This makes sure to remove it
// from Plaid. (This should not be undo-able, as it mutates the
// Unlink the account if it's linked. This makes sure to remove it from
// bank-sync providers. (This should not be undo-able, as it mutates the
// remote server and the user will have to link the account again)
await handlers['account-unlink']({ id });
@@ -878,142 +844,6 @@ handlers['account-move'] = mutator(async function ({ id, targetId }) {
let stopPolling = false;
handlers['poll-web-token'] = async function ({ token }) {
const [[, userId], [, key]] = await asyncStorage.multiGet([
'user-id',
'user-key',
]);
const startTime = Date.now();
stopPolling = false;
async function getData(cb) {
if (stopPolling) {
return;
}
if (Date.now() - startTime >= 1000 * 60 * 10) {
cb('timeout');
return;
}
const data = await post(
getServer().PLAID_SERVER + '/get-web-token-contents',
{
userId,
key,
token,
},
);
if (data) {
if (data.error) {
cb('unknown');
} else {
cb(null, data);
}
} else {
setTimeout(() => getData(cb), 3000);
}
}
return new Promise(resolve => {
getData((error, data) => {
if (error) {
resolve({ error });
} else {
resolve({ data });
}
});
});
};
handlers['poll-web-token-stop'] = async function () {
stopPolling = true;
return 'ok';
};
handlers['accounts-sync'] = async function ({ id }) {
const [[, userId], [, userKey]] = await asyncStorage.multiGet([
'user-id',
'user-key',
]);
let accounts = await db.runQuery(
`SELECT a.*, b.id as bankId FROM accounts a
LEFT JOIN banks b ON a.bank = b.id
WHERE a.tombstone = 0 AND a.closed = 0`,
[],
true,
);
if (id) {
accounts = accounts.filter(acct => acct.id === id);
}
const errors = [];
let newTransactions = [];
let matchedTransactions = [];
let updatedAccounts = [];
for (let i = 0; i < accounts.length; i++) {
const acct = accounts[i];
if (acct.bankId) {
try {
const res = await bankSync.syncAccount(
userId,
userKey,
acct.id,
acct.account_id,
acct.bankId,
);
const { added, updated } = res;
newTransactions = newTransactions.concat(added);
matchedTransactions = matchedTransactions.concat(updated);
if (added.length > 0 || updated.length > 0) {
updatedAccounts = updatedAccounts.concat(acct.id);
}
} catch (err) {
if (err.type === 'BankSyncError') {
errors.push({
type: 'SyncError',
accountId: acct.id,
message: 'Failed syncing account “' + acct.name + '.”',
category: err.category,
code: err.code,
});
} else if (err instanceof PostError && err.reason !== 'internal') {
errors.push({
accountId: acct.id,
message: `Account “${acct.name}” is not linked properly. Please link it again`,
});
} else {
errors.push({
accountId: acct.id,
message:
'There was an internal error. Please get in touch https://actualbudget.org/contact for support.',
internal: err.stack,
});
err.message = 'Failed syncing account: ' + err.message;
captureException(err);
}
}
}
}
if (updatedAccounts.length > 0) {
connection.send('sync-event', {
type: 'success',
tables: ['transactions'],
});
}
return { errors, newTransactions, matchedTransactions, updatedAccounts };
};
handlers['secret-set'] = async function ({ name, value }) {
const userToken = await asyncStorage.getItem('user-token');
@@ -1370,25 +1200,6 @@ handlers['account-unlink'] = mutator(async function ({ id }) {
return 'ok';
});
handlers['make-plaid-public-token'] = async function ({ bankId }) {
const [[, userId], [, userKey]] = await asyncStorage.multiGet([
'user-id',
'user-key',
]);
const data = await post(getServer().PLAID_SERVER + '/make-public-token', {
userId,
key: userKey,
item_id: '' + bankId,
});
if (data.error_code) {
return { error: '', code: data.error_code, type: data.error_type };
}
return { linkToken: data.link_token };
};
handlers['save-global-prefs'] = async function (prefs) {
if ('maxMonths' in prefs) {
await asyncStorage.setItem('max-months', '' + prefs.maxMonths);

View File

@@ -4,7 +4,6 @@ type ServerConfig = {
BASE_SERVER: string;
SYNC_SERVER: string;
SIGNUP_SERVER: string;
PLAID_SERVER: string;
GOCARDLESS_SERVER: string;
SIMPLEFIN_SERVER: string;
};
@@ -32,7 +31,6 @@ export function getServer(url?: string): ServerConfig | null {
BASE_SERVER: url,
SYNC_SERVER: joinURL(url, '/sync'),
SIGNUP_SERVER: joinURL(url, '/account'),
PLAID_SERVER: joinURL(url, '/plaid'),
GOCARDLESS_SERVER: joinURL(url, '/gocardless'),
SIMPLEFIN_SERVER: joinURL(url, '/simplefin'),
};

View File

@@ -78,34 +78,11 @@ handlers['/sync/sync'] = async (data: Uint8Array): Promise<Uint8Array> => {
return responsePb.serializeBinary();
};
handlers['/plaid/handoff_public_token'] = () => {
// Do nothing
};
handlers['/plaid/accounts'] = () => {
handlers['/gocardless/accounts'] = () => {
// Ignore the parameters and just return the accounts.
return { accounts: currentMockData.accounts };
};
handlers['/plaid/transactions'] = ({
account_id,
start_date,
end_date,
count,
offset,
}) => {
const accounts = currentMockData.accounts;
const transactions = currentMockData.transactions[account_id].filter(
t => t.date >= start_date && t.date <= end_date,
);
return {
accounts: accounts.filter(acct => acct.account_id === account_id),
transactions: transactions.slice(offset, offset + count),
total_transactions: transactions.length,
};
};
export const filterMockData = func => {
const copied = JSON.parse(JSON.stringify(defaultMockData));
currentMockData = func(copied);

View File

@@ -139,8 +139,6 @@ export interface ServerHandlers {
query: (query) => Promise<{ data; dependencies }>;
'bank-delete': (arg: { id }) => Promise<unknown>;
'account-update': (arg: { id; name }) => Promise<unknown>;
'accounts-get': () => Promise<AccountEntity[]>;
@@ -160,13 +158,6 @@ export interface ServerHandlers {
upgradingId;
}) => Promise<'ok'>;
'gocardless-accounts-connect': (arg: {
institution;
publicToken;
accountIds;
offbudgetIds;
}) => Promise<unknown>;
'account-create': (arg: {
name: string;
balance?: number;
@@ -185,17 +176,6 @@ export interface ServerHandlers {
'account-move': (arg: { id; targetId }) => Promise<unknown>;
'poll-web-token': (arg: { token }) => Promise<unknown>;
'poll-web-token-stop': () => Promise<'ok'>;
'accounts-sync': (arg: { id? }) => Promise<{
errors: unknown;
newTransactions: unknown;
matchedTransactions: unknown;
updatedAccounts: unknown;
}>;
'secret-set': (arg: { name: string; value: string }) => Promise<null>;
'secret-check': (arg: string) => Promise<string | { error?: string }>;
@@ -247,10 +227,6 @@ export interface ServerHandlers {
'account-unlink': (arg: { id }) => Promise<'ok'>;
'make-plaid-public-token': (arg: {
bankId;
}) => Promise<{ error: ''; code; type } | { linkToken }>;
'save-global-prefs': (prefs) => Promise<'ok'>;
'load-global-prefs': () => Promise<GlobalPrefs>;

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Delete old Plaid integration that is no longer working.