From 6e7c95b5bec4e948bb44799bab8490d7e0ac5359 Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins <matiss@mja.lv> Date: Mon, 13 Mar 2023 18:27:45 +0000 Subject: [PATCH] :bug: (nordigen) check server status before linking accs (#742) Related to: https://github.com/actualbudget/actual/issues/724#issuecomment-1455160250 Depends on https://github.com/actualbudget/docs/pull/126 to be merged first. Two changes here: 1. show "link account" only if actual-server is used (user is "online"); 2. allow linking accounts only if Nordigen is configured (using new API to get the status for it); Also ported the `CreateAccount` modal to a functional component. --- .../desktop-client/src/components/Modals.js | 10 +- .../src/components/accounts/Account.js | 12 +- .../src/components/modals/CreateAccount.js | 152 +++++++++--------- .../src/hooks/useSyncServerStatus.js | 14 ++ packages/loot-core/src/server/main.js | 16 ++ .../components/modals/NordigenExternalMsg.js | 46 +++++- 6 files changed, 172 insertions(+), 78 deletions(-) create mode 100644 packages/desktop-client/src/hooks/useSyncServerStatus.js diff --git a/packages/desktop-client/src/components/Modals.js b/packages/desktop-client/src/components/Modals.js index c5804e625..9c4f4bfe9 100644 --- a/packages/desktop-client/src/components/Modals.js +++ b/packages/desktop-client/src/components/Modals.js @@ -19,6 +19,8 @@ import NordigenExternalMsg from 'loot-design/src/components/modals/NordigenExter import PlaidExternalMsg from 'loot-design/src/components/modals/PlaidExternalMsg'; import SelectLinkedAccounts from 'loot-design/src/components/modals/SelectLinkedAccounts'; +import useSyncServerStatus from '../hooks/useSyncServerStatus'; + import ConfirmCategoryDelete from './modals/ConfirmCategoryDelete'; import CreateAccount from './modals/CreateAccount'; import CreateEncryptionKey from './modals/CreateEncryptionKey'; @@ -39,6 +41,8 @@ function Modals({ budgetId, actions, }) { + const syncServerStatus = useSyncServerStatus(); + return modalStack.map(({ name, options = {} }, idx) => { const modalProps = { onClose: actions.popModal, @@ -57,7 +61,11 @@ function Modals({ </Route> <Route path="/add-account"> - <CreateAccount modalProps={modalProps} actions={actions} /> + <CreateAccount + modalProps={modalProps} + actions={actions} + syncServerStatus={syncServerStatus} + /> </Route> <Route path="/add-local-account"> diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js index ee12dd255..e3db75657 100644 --- a/packages/desktop-client/src/components/accounts/Account.js +++ b/packages/desktop-client/src/components/accounts/Account.js @@ -60,6 +60,7 @@ import Pencil1 from 'loot-design/src/svg/v2/Pencil1'; import SvgRemove from 'loot-design/src/svg/v2/Remove'; import SearchAlternate from 'loot-design/src/svg/v2/SearchAlternate'; +import useSyncServerStatus from '../../hooks/useSyncServerStatus'; import { authorizeBank } from '../../nordigen'; import { useActiveLocation } from '../ActiveLocation'; import AnimatedRefresh from '../AnimatedRefresh'; @@ -259,6 +260,7 @@ function AccountMenu({ onMenuSelect, }) { let [tooltip, setTooltip] = useState('default'); + const syncServerStatus = useSyncServerStatus(); return tooltip === 'reconcile' ? ( <ReconcileTooltip @@ -291,8 +293,14 @@ function AccountMenu({ account && !account.closed && (canSync - ? { name: 'unlink', text: 'Unlink Account' } - : { name: 'link', text: 'Link Account' }), + ? { + name: 'unlink', + text: 'Unlink Account', + } + : syncServerStatus === 'online' && { + name: 'link', + text: 'Link Account', + }), account.closed ? { name: 'reopen', text: 'Reopen Account' } : { name: 'close', text: 'Close Account' }, diff --git a/packages/desktop-client/src/components/modals/CreateAccount.js b/packages/desktop-client/src/components/modals/CreateAccount.js index 435062059..fdc391dcc 100644 --- a/packages/desktop-client/src/components/modals/CreateAccount.js +++ b/packages/desktop-client/src/components/modals/CreateAccount.js @@ -1,87 +1,95 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { useDispatch } from 'react-redux'; -import { bindActionCreators } from 'redux'; - -import * as actions from 'loot-core/src/client/actions'; -import { View, Text, Modal, Button } from 'loot-design/src/components/common'; +import { pushModal } from 'loot-core/src/client/actions/modals'; +import { + View, + Text, + Modal, + P, + Button, + ButtonWithLoading, +} from 'loot-design/src/components/common'; import { colors } from 'loot-design/src/style'; import { authorizeBank } from '../../nordigen'; -class CreateAccount extends React.Component { - onConnect = async () => { - authorizeBank(this.props.pushModal); - }; +export default function CreateAccount({ modalProps, syncServerStatus }) { + const dispatch = useDispatch(); - onCreateLocalAccount = () => { - const { pushModal } = this.props; - pushModal('add-local-account'); + const onConnect = () => { + authorizeBank((modal, params) => dispatch(pushModal(modal, params))); }; - render() { - const { modalProps } = this.props; + const onCreateLocalAccount = () => { + dispatch(pushModal('add-local-account')); + }; - return ( - <Modal title="Add Account" {...modalProps}> - {() => ( - <View style={{ maxWidth: 500 }}> - <Text - style={{ marginBottom: 10, lineHeight: '1.4em', fontSize: 15 }} - > - <strong>Link your bank accounts</strong> to automatically download - transactions. We offer hundreds of banks to sync with, and our - service will provide reliable, up-to-date information. - </Text> + return ( + <Modal title="Add Account" {...modalProps}> + {() => ( + <View style={{ maxWidth: 500 }}> + <Text style={{ marginBottom: 10, lineHeight: '1.4em', fontSize: 15 }}> + <strong>Link your bank accounts</strong> to automatically download + transactions. We offer hundreds of banks to sync with, and our + service will provide reliable, up-to-date information. + </Text> - <Button - primary - style={{ - padding: '10px 0', - fontSize: 15, - fontWeight: 600, - marginTop: 10, - }} - onClick={this.onConnect} - > - Link bank account - </Button> + <ButtonWithLoading + primary + disabled={syncServerStatus !== 'online'} + style={{ + padding: '10px 0', + fontSize: 15, + fontWeight: 600, + marginTop: 10, + }} + onClick={onConnect} + > + Link bank account + </ButtonWithLoading> - <View - style={{ - marginTop: 30, - marginBottom: 10, - lineHeight: '1.4em', - fontSize: 15, - }} - > - You can also create a local account if you want to track - transactions manually. You can add transactions manually or import - QIF/OFX/QFX files. - </View> + {syncServerStatus !== 'online' && ( + <P style={{ color: colors.r5, marginTop: 5 }}> + Nordigen integration is only available for budgets using + actual-server.{' '} + <a + href="https://actualbudget.github.io/docs/Installing/overview" + target="_blank" + rel="noopener noreferrer" + > + Learn more. + </a> + </P> + )} - <Button - style={{ - padding: '10px 0', - fontSize: 15, - fontWeight: 600, - marginTop: 10, - color: colors.n3, - }} - onClick={this.onCreateLocalAccount} - > - Create local account - </Button> + <View + style={{ + marginTop: 30, + marginBottom: 10, + lineHeight: '1.4em', + fontSize: 15, + }} + > + You can also create a local account if you want to track + transactions manually. You can add transactions manually or import + QIF/OFX/QFX files. </View> - )} - </Modal> - ); - } -} -export default connect( - state => ({ - currentModal: state.modals.currentModal, - }), - dispatch => bindActionCreators(actions, dispatch), -)(CreateAccount); + <Button + style={{ + padding: '10px 0', + fontSize: 15, + fontWeight: 600, + marginTop: 10, + color: colors.n3, + }} + onClick={onCreateLocalAccount} + > + Create local account + </Button> + </View> + )} + </Modal> + ); +} diff --git a/packages/desktop-client/src/hooks/useSyncServerStatus.js b/packages/desktop-client/src/hooks/useSyncServerStatus.js new file mode 100644 index 000000000..54d989ce8 --- /dev/null +++ b/packages/desktop-client/src/hooks/useSyncServerStatus.js @@ -0,0 +1,14 @@ +import { useSelector } from 'react-redux'; + +import { useServerURL } from '../components/ServerContext'; + +export default function useSyncServerStatus() { + const serverUrl = useServerURL(); + const userData = useSelector(state => state.user.data); + + if (!serverUrl) { + return 'no-server'; + } + + return !userData || userData.offline ? 'offline' : 'online'; +} diff --git a/packages/loot-core/src/server/main.js b/packages/loot-core/src/server/main.js index 92237e705..752b75e97 100644 --- a/packages/loot-core/src/server/main.js +++ b/packages/loot-core/src/server/main.js @@ -1240,6 +1240,22 @@ handlers['nordigen-poll-web-token'] = async function ({ return null; }; +handlers['nordigen-status'] = async function () { + const userToken = await asyncStorage.getItem('user-token'); + + if (!userToken) { + return Promise.reject({ error: 'unauthorized' }); + } + + return post( + getServer().NORDIGEN_SERVER + '/status', + {}, + { + 'X-ACTUAL-TOKEN': userToken, + }, + ); +}; + handlers['nordigen-get-banks'] = async function (country) { const userToken = await asyncStorage.getItem('user-token'); diff --git a/packages/loot-design/src/components/modals/NordigenExternalMsg.js b/packages/loot-design/src/components/modals/NordigenExternalMsg.js index 35de67027..ae659b9bf 100644 --- a/packages/loot-design/src/components/modals/NordigenExternalMsg.js +++ b/packages/loot-design/src/components/modals/NordigenExternalMsg.js @@ -40,6 +40,29 @@ function useAvailableBanks(country) { }; } +function useNordigenStatus() { + const [configured, setConfigured] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + async function fetch() { + setIsLoading(true); + + const results = await send('nordigen-status'); + + setConfigured(results.configured || false); + setIsLoading(false); + } + + fetch(); + }, [setConfigured, setIsLoading]); + + return { + configured, + isLoading, + }; +} + function renderError(error) { return ( <Error style={{ alignSelf: 'center' }}> @@ -65,6 +88,8 @@ export default function NordigenExternalMsg({ const { data: bankOptions, isLoading: isBankOptionsLoading } = useAvailableBanks(country); + const { configured: isConfigured, isLoading: isConfigurationLoading } = + useNordigenStatus(); async function onJump() { setError(null); @@ -100,6 +125,7 @@ export default function NordigenExternalMsg({ <FormLabel title="Choose your country:" htmlFor="country-field" /> <Autocomplete strict + disabled={isConfigurationLoading} suggestions={COUNTRY_OPTIONS} onSelect={setCountry} value={country} @@ -178,14 +204,16 @@ export default function NordigenExternalMsg({ {error && renderError(error)} - {waiting ? ( + {waiting || isConfigurationLoading ? ( <View style={{ alignItems: 'center', marginTop: 15 }}> <AnimatedLoading color={colors.n1} style={{ width: 20, height: 20 }} /> <View style={{ marginTop: 10, color: colors.n4 }}> - {waiting === 'browser' + {isConfigurationLoading + ? 'Checking Nordigen configuration..' + : waiting === 'browser' ? 'Waiting on Nordigen...' : waiting === 'accounts' ? 'Loading accounts...' @@ -207,8 +235,20 @@ export default function NordigenExternalMsg({ > Success! Click to continue → </Button> - ) : ( + ) : isConfigured ? ( renderLinkButton() + ) : ( + <P style={{ color: colors.r5 }}> + Nordigen integration has not been configured so linking accounts + is not available.{' '} + <a + href="https://actualbudget.github.io/docs/Accounts/connecting-your-bank/" + target="_blank" + rel="noopener noreferrer" + > + Learn more. + </a> + </P> )} </View> )} -- GitLab