Skip to content

Commit

Permalink
SimpleFin (actualbudget#2188)
Browse files Browse the repository at this point in the history
* Some initial UI work for adding SimpleFin.

* SimpleFin proof of concept working.

* Adds linking & unlinking to existing accounts through the account menu UI.

* Added loading and lint fixes.

* Lint changes.

* Added release notes.

* Typecheck cleanup.

* Import, lint, typecheck cleanups.

* More typecheck cleanup.

* Refactored language for consistency.

* Added default institution name.

* Lint cleanup.

* Addressed change requests.

* Added a default to migration, made variables consistent, added feature flag.

* Added account_sync_source to server schema.

* Adds account_sync_source to test.

* Fix for typecheck.

* Attempt to make typecheck happy.

* Added strict ignore.

* Moved account_sync_source to the right model (face palm).

* Hotfix for institution format.

* Lint cleanup.

* Removed unnecessary promise.all.

* Lint cleanup.
  • Loading branch information
zachwhelchel authored Jan 20, 2024
1 parent 96f7b43 commit e2ff6d0
Show file tree
Hide file tree
Showing 25 changed files with 538 additions and 72 deletions.
11 changes: 11 additions & 0 deletions packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { PlaidExternalMsg } from './modals/PlaidExternalMsg';
import { ReportBudgetSummary } from './modals/ReportBudgetSummary';
import { RolloverBudgetSummary } from './modals/RolloverBudgetSummary';
import { SelectLinkedAccounts } from './modals/SelectLinkedAccounts';
import { SimpleFinInitialise } from './modals/SimpleFinInitialise';
import { SingleInput } from './modals/SingleInput';
import { SwitchBudgetType } from './modals/SwitchBudgetType';
import { DiscoverSchedules } from './schedules/DiscoverSchedules';
Expand Down Expand Up @@ -80,6 +81,7 @@ export function Modals() {
<CreateAccount
modalProps={modalProps}
syncServerStatus={syncServerStatus}
upgradingAccountId={options?.upgradingAccountId}
/>
);

Expand Down Expand Up @@ -109,6 +111,7 @@ export function Modals() {
requisitionId={options.requisitionId}
localAccounts={accounts.filter(acct => acct.closed === 0)}
actions={actions}
syncSource={options.syncSource}
/>
);

Expand Down Expand Up @@ -196,6 +199,14 @@ export function Modals() {
/>
);

case 'simplefin-init':
return (
<SimpleFinInitialise
modalProps={modalProps}
onSuccess={options.onSuccess}
/>
);

case 'gocardless-external-msg':
return (
<GoCardlessExternalMsg
Expand Down
5 changes: 3 additions & 2 deletions packages/desktop-client/src/components/accounts/Account.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
} from 'loot-core/src/shared/transactions';
import { applyChanges, groupById } from 'loot-core/src/shared/util';

import { authorizeBank } from '../../gocardless';
import { useCategories } from '../../hooks/useCategories';
import { SelectedProviderWithItems } from '../../hooks/useSelected';
import { styles, theme } from '../../style';
Expand Down Expand Up @@ -589,7 +588,9 @@ class AccountInternal extends PureComponent {

switch (item) {
case 'link':
authorizeBank(this.props.pushModal, { upgradingAccountId: accountId });
this.props.pushModal('add-account', {
upgradingAccountId: accountId,
});
break;
case 'unlink':
this.props.unlinkAccount(accountId);
Expand Down
182 changes: 145 additions & 37 deletions packages/desktop-client/src/components/modals/CreateAccount.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// @ts-strict-ignore
import React, { useEffect, useState } from 'react';

import { send } from 'loot-core/src/platform/client/fetch';

import { authorizeBank } from '../../gocardless';
import { useActions } from '../../hooks/useActions';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import { useGoCardlessStatus } from '../../hooks/useGoCardlessStatus';
import { useSimpleFinStatus } from '../../hooks/useSimpleFinStatus';
import { type SyncServerStatus } from '../../hooks/useSyncServerStatus';
import { theme } from '../../style';
import { type CommonModalProps } from '../../types/modals';
Expand All @@ -17,23 +21,75 @@ import { View } from '../common/View';
type CreateAccountProps = {
modalProps: CommonModalProps;
syncServerStatus: SyncServerStatus;
upgradingAccountId?: string;
};

export function CreateAccount({
modalProps,
syncServerStatus,
upgradingAccountId,
}: CreateAccountProps) {
const actions = useActions();
const [isGoCardlessSetupComplete, setIsGoCardlessSetupComplete] =
useState(null);
const [isSimpleFinSetupComplete, setIsSimpleFinSetupComplete] =
useState(null);

const onConnect = () => {
const onConnectGoCardless = () => {
if (!isGoCardlessSetupComplete) {
onGoCardlessInit();
return;
}

authorizeBank(actions.pushModal);
if (upgradingAccountId == null) {
authorizeBank(actions.pushModal);
} else {
authorizeBank(actions.pushModal, {
upgradingAccountId,
});
}
};

const onConnectSimpleFin = async () => {
if (!isSimpleFinSetupComplete) {
onSimpleFinInit();
return;
}

if (loadingSimpleFinAccounts) {
return;
}

setLoadingSimpleFinAccounts(true);

const results = await send('simplefin-accounts');

const newAccounts = [];

type NormalizedAccount = {
account_id: string;
name: string;
institution: string;
orgDomain: string;
};

for (const oldAccount of results.accounts) {
const newAccount: NormalizedAccount = {
account_id: oldAccount.id,
name: oldAccount.name,
institution: oldAccount.org.name,
orgDomain: oldAccount.org.domain,
};

newAccounts.push(newAccount);
}

actions.pushModal('select-linked-accounts', {
accounts: newAccounts,
syncSource: 'simpleFin',
});

setLoadingSimpleFinAccounts(false);
};

const onGoCardlessInit = () => {
Expand All @@ -42,45 +98,68 @@ export function CreateAccount({
});
};

const onSimpleFinInit = () => {
actions.pushModal('simplefin-init', {
onSuccess: () => setIsSimpleFinSetupComplete(true),
});
};

const onCreateLocalAccount = () => {
actions.pushModal('add-local-account');
};

const { configured } = useGoCardlessStatus();
const { configuredGoCardless } = useGoCardlessStatus();
useEffect(() => {
setIsGoCardlessSetupComplete(configured);
}, [configured]);
setIsGoCardlessSetupComplete(configuredGoCardless);
}, [configuredGoCardless]);

const { configuredSimpleFin } = useSimpleFinStatus();
useEffect(() => {
setIsSimpleFinSetupComplete(configuredSimpleFin);
}, [configuredSimpleFin]);

let title = 'Add Account';
const [loadingSimpleFinAccounts, setLoadingSimpleFinAccounts] =
useState(false);

if (upgradingAccountId != null) {
title = 'Link Account';
}

const simpleFinSyncFeatureFlag = useFeatureFlag('simpleFinSync');

return (
<Modal title="Add Account" {...modalProps}>
<Modal title={title} {...modalProps}>
{() => (
<View style={{ maxWidth: 500, gap: 30, color: theme.pageText }}>
<View style={{ gap: 10 }}>
<Button
type="primary"
style={{
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
}}
onClick={onCreateLocalAccount}
>
Create local account
</Button>
<View style={{ lineHeight: '1.4em', fontSize: 15 }}>
<Text>
<strong>Create a local account</strong> if you want to add
transactions manually. You can also{' '}
<ExternalLink
to="https://actualbudget.org/docs/transactions/importing"
linkColor="muted"
>
import QIF/OFX/QFX files into a local account
</ExternalLink>
.
</Text>
{upgradingAccountId == null && (
<View style={{ gap: 10 }}>
<Button
type="primary"
style={{
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
}}
onClick={onCreateLocalAccount}
>
Create local account
</Button>
<View style={{ lineHeight: '1.4em', fontSize: 15 }}>
<Text>
<strong>Create a local account</strong> if you want to add
transactions manually. You can also{' '}
<ExternalLink
to="https://actualbudget.org/docs/transactions/importing"
linkColor="muted"
>
import QIF/OFX/QFX files into a local account
</ExternalLink>
.
</Text>
</View>
</View>
</View>
)}
<View style={{ gap: 10 }}>
{syncServerStatus === 'online' ? (
<>
Expand All @@ -92,17 +171,46 @@ export function CreateAccount({
fontWeight: 600,
flex: 1,
}}
onClick={onConnect}
onClick={onConnectGoCardless}
>
{isGoCardlessSetupComplete
? 'Link bank account with GoCardless'
: 'Set up GoCardless for bank sync'}
</ButtonWithLoading>
<Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
<strong>Link a bank account</strong> to automatically download
transactions. GoCardless provides reliable, up-to-date
information from hundreds of banks.
<strong>
Link a <u>European</u> bank account
</strong>{' '}
to automatically download transactions. GoCardless provides
reliable, up-to-date information from hundreds of banks.
</Text>
{simpleFinSyncFeatureFlag === true && (
<>
<ButtonWithLoading
disabled={syncServerStatus !== 'online'}
loading={loadingSimpleFinAccounts}
style={{
marginTop: '18px',
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
flex: 1,
}}
onClick={onConnectSimpleFin}
>
{isSimpleFinSetupComplete
? 'Link bank account with SimpleFIN'
: 'Set up SimpleFIN for bank sync'}
</ButtonWithLoading>
<Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
<strong>
Link a <u>North American</u> bank account
</strong>{' '}
to automatically download transactions. SimpleFIN provides
reliable, up-to-date information from hundreds of banks.
</Text>
</>
)}
</>
) : (
<>
Expand All @@ -114,15 +222,15 @@ export function CreateAccount({
fontWeight: 600,
}}
>
Set up GoCardless for bank sync
Set up bank sync
</Button>
<Paragraph style={{ fontSize: 15 }}>
Connect to an Actual server to set up{' '}
<ExternalLink
to="https://actualbudget.org/docs/advanced/bank-sync"
linkColor="muted"
>
automatic syncing with GoCardless
automatic syncing.
</ExternalLink>
.
</Paragraph>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ export function GoCardlessExternalMsg({
isLoading: isBankOptionsLoading,
isError: isBankOptionError,
} = useAvailableBanks(country);
const { configured: isConfigured, isLoading: isConfigurationLoading } =
useGoCardlessStatus();
const {
configuredGoCardless: isConfigured,
isLoading: isConfigurationLoading,
} = useGoCardlessStatus();

async function onJump() {
setError(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function SelectLinkedAccounts({
externalAccounts,
localAccounts,
actions,
syncSource,
}) {
const [chosenAccounts, setChosenAccounts] = useState(() => {
return Object.fromEntries(
Expand Down Expand Up @@ -49,13 +50,22 @@ export function SelectLinkedAccounts({
}

// Finally link the matched account
actions.linkAccount(
requisitionId,
externalAccount,
chosenLocalAccountId !== addAccountOption.id
? chosenLocalAccountId
: undefined,
);
if (syncSource === 'simpleFin') {
actions.linkAccountSimpleFin(
externalAccount,
chosenLocalAccountId !== addAccountOption.id
? chosenLocalAccountId
: undefined,
);
} else {
actions.linkAccount(
requisitionId,
externalAccount,
chosenLocalAccountId !== addAccountOption.id
? chosenLocalAccountId
: undefined,
);
}
},
);

Expand Down
Loading

0 comments on commit e2ff6d0

Please sign in to comment.