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 &rarr;
             </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