From fc308ececc1b2423b8f9f32d940ec520a784b9d1 Mon Sep 17 00:00:00 2001
From: Jed Fox <git@jedfox.com>
Date: Tue, 21 Feb 2023 13:36:11 -0500
Subject: [PATCH] Allow the server to auto-configure the server URL for the
 client (#649)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Allow the server to auto-configure the server URL for the client

* Extract server URL/version logic out to ensure consistent updates

* ()

* Be more explicit about when the server version is re-fetched

* Use a single layer of context provider

* Move the bootstrap route to /account/needs-bootstrap

* No more `isActual`

* Refactor to call subscribe-needs-bootstrap instead of fetch()

* Dedupe calls to subscribe-needs-bootstrap

* Don’t revalidate the server when we just validated it

* simplify

* Fix setServerURL
---
 .../src/components/LoggedInUser.js            |  2 +-
 .../src/components/ServerContext.js           | 68 +++++++++++++++++++
 .../desktop-client/src/components/Titlebar.js |  3 +-
 .../src/components/manager/ConfigServer.js    | 12 ++--
 .../src/components/manager/ManagementApp.js   |  2 +-
 .../src/components/manager/ServerURL.js       |  2 +-
 .../components/manager/subscribe/common.js    | 22 +++++-
 .../src/components/settings/Encryption.js     |  2 +-
 .../src/components/settings/index.js          |  2 +-
 .../desktop-client/src/hooks/useServerURL.js  | 18 -----
 .../src/hooks/useServerVersion.js             | 23 -------
 packages/desktop-client/src/index.js          |  5 +-
 packages/loot-core/src/server/main.js         | 20 +++---
 13 files changed, 116 insertions(+), 65 deletions(-)
 create mode 100644 packages/desktop-client/src/components/ServerContext.js
 delete mode 100644 packages/desktop-client/src/hooks/useServerURL.js
 delete mode 100644 packages/desktop-client/src/hooks/useServerVersion.js

diff --git a/packages/desktop-client/src/components/LoggedInUser.js b/packages/desktop-client/src/components/LoggedInUser.js
index 41fec806e..63306f797 100644
--- a/packages/desktop-client/src/components/LoggedInUser.js
+++ b/packages/desktop-client/src/components/LoggedInUser.js
@@ -12,7 +12,7 @@ import {
 } from 'loot-design/src/components/common';
 import { colors } from 'loot-design/src/style';
 
-import { useServerURL } from '../hooks/useServerURL';
+import { useServerURL } from './ServerContext';
 
 function LoggedInUser({
   files,
diff --git a/packages/desktop-client/src/components/ServerContext.js b/packages/desktop-client/src/components/ServerContext.js
new file mode 100644
index 000000000..270503318
--- /dev/null
+++ b/packages/desktop-client/src/components/ServerContext.js
@@ -0,0 +1,68 @@
+import React, {
+  createContext,
+  useState,
+  useCallback,
+  useEffect,
+  useContext,
+} from 'react';
+
+import { send } from 'loot-core/src/platform/client/fetch';
+
+const ServerContext = createContext({});
+
+export const useServerURL = () => useContext(ServerContext).url;
+export const useServerVersion = () => useContext(ServerContext).version;
+export const useSetServerURL = () => useContext(ServerContext).setURL;
+
+async function getServerUrl() {
+  let url = (await send('get-server-url')) || '';
+  if (url === 'https://not-configured/') {
+    url = '';
+  }
+  return url;
+}
+
+async function getServerVersion() {
+  let { error, version } = await send('get-server-version');
+  if (error) {
+    return '';
+  }
+  return version;
+}
+
+export function ServerProvider({ children }) {
+  let [serverURL, setServerURL] = useState('');
+  let [version, setVersion] = useState('');
+
+  useEffect(() => {
+    async function run() {
+      setServerURL(await getServerUrl());
+      setVersion(await getServerVersion());
+    }
+    run();
+  }, []);
+
+  let setURL = useCallback(
+    async (url, opts = {}) => {
+      let { error } = await send('set-server-url', { ...opts, url });
+      if (!error) {
+        setServerURL(await getServerUrl());
+        setVersion(await getServerVersion());
+      }
+      return { error };
+    },
+    [setServerURL],
+  );
+
+  return (
+    <ServerContext.Provider
+      value={{
+        url: serverURL,
+        setURL,
+        version: version ? `v${version}` : 'N/A',
+      }}
+    >
+      {children}
+    </ServerContext.Provider>
+  );
+}
diff --git a/packages/desktop-client/src/components/Titlebar.js b/packages/desktop-client/src/components/Titlebar.js
index 8585f0cec..585af85a8 100644
--- a/packages/desktop-client/src/components/Titlebar.js
+++ b/packages/desktop-client/src/components/Titlebar.js
@@ -25,13 +25,12 @@ import ArrowButtonRight1 from 'loot-design/src/svg/v2/ArrowButtonRight1';
 import NavigationMenu from 'loot-design/src/svg/v2/NavigationMenu';
 import tokens from 'loot-design/src/tokens';
 
-import { useServerURL } from '../hooks/useServerURL';
-
 import AccountSyncCheck from './accounts/AccountSyncCheck';
 import AnimatedRefresh from './AnimatedRefresh';
 import { MonthCountSelector } from './budget/MonthCountSelector';
 import { useSidebar } from './FloatableSidebar';
 import LoggedInUser from './LoggedInUser';
+import { useServerURL } from './ServerContext';
 
 export let TitlebarContext = React.createContext();
 
diff --git a/packages/desktop-client/src/components/manager/ConfigServer.js b/packages/desktop-client/src/components/manager/ConfigServer.js
index a9385ddef..d00f0e563 100644
--- a/packages/desktop-client/src/components/manager/ConfigServer.js
+++ b/packages/desktop-client/src/components/manager/ConfigServer.js
@@ -4,7 +4,6 @@ import { useHistory } from 'react-router-dom';
 
 import { createBudget } from 'loot-core/src/client/actions/budgets';
 import { signOut, loggedIn } from 'loot-core/src/client/actions/user';
-import { send } from 'loot-core/src/platform/client/fetch';
 import {
   View,
   Text,
@@ -18,7 +17,7 @@ import {
   isPreviewEnvironment,
 } from 'loot-design/src/util/environment';
 
-import { useServerURL } from '../../hooks/useServerURL';
+import { useServerURL, useSetServerURL } from '../ServerContext';
 
 import { Title, Input } from './subscribe/common';
 
@@ -28,6 +27,7 @@ export default function ConfigServer() {
   let history = useHistory();
   let [url, setUrl] = useState('');
   let currentUrl = useServerURL();
+  let setServerUrl = useSetServerURL();
   useEffect(() => {
     setUrl(currentUrl);
   }, [currentUrl]);
@@ -50,14 +50,14 @@ export default function ConfigServer() {
 
     setError(null);
     setLoading(true);
-    let { error } = await send('set-server-url', { url });
+    let { error } = await setServerUrl(url);
 
     if (
       error === 'network-failure' &&
       !url.startsWith('http://') &&
       !url.startsWith('https://')
     ) {
-      let { error } = await send('set-server-url', { url: 'https://' + url });
+      let { error } = await setServerUrl('https://' + url);
       if (error) {
         setUrl('https://' + url);
         setError(error);
@@ -81,13 +81,13 @@ export default function ConfigServer() {
   }
 
   async function onSkip() {
-    await send('set-server-url', { url: null });
+    await setServerUrl(null);
     await dispatch(loggedIn());
     history.push('/');
   }
 
   async function onCreateTestFile() {
-    await send('set-server-url', { url: null });
+    await setServerUrl(null);
     await dispatch(createBudget({ testMode: true }));
     window.__history.push('/');
   }
diff --git a/packages/desktop-client/src/components/manager/ManagementApp.js b/packages/desktop-client/src/components/manager/ManagementApp.js
index 96b0e0925..bc29ceade 100644
--- a/packages/desktop-client/src/components/manager/ManagementApp.js
+++ b/packages/desktop-client/src/components/manager/ManagementApp.js
@@ -9,9 +9,9 @@ import { View, Text } from 'loot-design/src/components/common';
 import { colors } from 'loot-design/src/style';
 import tokens from 'loot-design/src/tokens';
 
-import useServerVersion from '../../hooks/useServerVersion';
 import LoggedInUser from '../LoggedInUser';
 import Notifications from '../Notifications';
+import { useServerVersion } from '../ServerContext';
 
 import ConfigServer from './ConfigServer';
 import Modals from './Modals';
diff --git a/packages/desktop-client/src/components/manager/ServerURL.js b/packages/desktop-client/src/components/manager/ServerURL.js
index 48ce36c3a..6d3538135 100644
--- a/packages/desktop-client/src/components/manager/ServerURL.js
+++ b/packages/desktop-client/src/components/manager/ServerURL.js
@@ -2,7 +2,7 @@ import React from 'react';
 
 import { View, Text, AnchorLink } from 'loot-design/src/components/common';
 
-import { useServerURL } from '../../hooks/useServerURL';
+import { useServerURL } from '../ServerContext';
 
 export default function ServerURL() {
   const url = useServerURL();
diff --git a/packages/desktop-client/src/components/manager/subscribe/common.js b/packages/desktop-client/src/components/manager/subscribe/common.js
index 26ad67eda..16516e36d 100644
--- a/packages/desktop-client/src/components/manager/subscribe/common.js
+++ b/packages/desktop-client/src/components/manager/subscribe/common.js
@@ -9,6 +9,8 @@ import {
 } from 'loot-design/src/components/common';
 import { colors, styles } from 'loot-design/src/style';
 
+import { useSetServerURL } from '../../ServerContext';
+
 // There are two URLs that dance with each other: `/login` and
 // `/bootstrap`. Both of these URLs check the state of the the server
 // and make sure the user is looking at the right page. For example,
@@ -22,6 +24,7 @@ export function useBootstrapped() {
   let [checked, setChecked] = useState(false);
   let history = useHistory();
   let location = useLocation();
+  let setServerURL = useSetServerURL();
 
   useEffect(() => {
     async function run() {
@@ -36,7 +39,24 @@ export function useBootstrapped() {
       let url = await send('get-server-url');
       if (url == null) {
         // A server hasn't been specified yet
-        history.push('/config-server');
+        let serverURL = window.location.origin;
+        let { error, hasServer, bootstrapped } = await send(
+          'subscribe-needs-bootstrap',
+          { url: serverURL },
+        );
+        if (error || !hasServer) {
+          console.log(error);
+          history.push('/config-server');
+          return;
+        }
+
+        await setServerURL(serverURL, { validate: false });
+
+        if (bootstrapped) {
+          ensure('/login');
+        } else {
+          ensure('/bootstrap');
+        }
       } else {
         let { error, bootstrapped } = await send('subscribe-needs-bootstrap');
         if (error) {
diff --git a/packages/desktop-client/src/components/settings/Encryption.js b/packages/desktop-client/src/components/settings/Encryption.js
index 5b3176aea..b20ec99c5 100644
--- a/packages/desktop-client/src/components/settings/Encryption.js
+++ b/packages/desktop-client/src/components/settings/Encryption.js
@@ -3,7 +3,7 @@ import React from 'react';
 import { Text, Button } from 'loot-design/src/components/common';
 import { colors } from 'loot-design/src/style';
 
-import { useServerURL } from '../../hooks/useServerURL';
+import { useServerURL } from '../ServerContext';
 
 import { Setting } from './UI';
 
diff --git a/packages/desktop-client/src/components/settings/index.js b/packages/desktop-client/src/components/settings/index.js
index c5a39923d..bb7b2f589 100644
--- a/packages/desktop-client/src/components/settings/index.js
+++ b/packages/desktop-client/src/components/settings/index.js
@@ -12,9 +12,9 @@ import { colors } from 'loot-design/src/style';
 import tokens from 'loot-design/src/tokens';
 import { withThemeColor } from 'loot-design/src/util/withThemeColor';
 
-import useServerVersion from '../../hooks/useServerVersion';
 import { isMobile } from '../../util';
 import { Page } from '../Page';
+import { useServerVersion } from '../ServerContext';
 
 import EncryptionSettings from './Encryption';
 import ExperimentalFeatures from './Experimental';
diff --git a/packages/desktop-client/src/hooks/useServerURL.js b/packages/desktop-client/src/hooks/useServerURL.js
deleted file mode 100644
index 8adfb05dd..000000000
--- a/packages/desktop-client/src/hooks/useServerURL.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useState, useEffect } from 'react';
-
-import { send } from 'loot-core/src/platform/client/fetch';
-
-export function useServerURL() {
-  let [serverUrl, setServerUrl] = useState('');
-  useEffect(() => {
-    async function run() {
-      let url = (await send('get-server-url')) || '';
-      if (url === 'https://not-configured/') {
-        url = '';
-      }
-      setServerUrl(url);
-    }
-    run();
-  }, []);
-  return serverUrl;
-}
diff --git a/packages/desktop-client/src/hooks/useServerVersion.js b/packages/desktop-client/src/hooks/useServerVersion.js
deleted file mode 100644
index 07fd31de8..000000000
--- a/packages/desktop-client/src/hooks/useServerVersion.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useState, useEffect } from 'react';
-
-import { send } from 'loot-core/src/platform/client/fetch';
-
-function useServerVersion() {
-  let [version, setVersion] = useState('');
-
-  useEffect(() => {
-    (async () => {
-      const { error, version } = await send('get-server-version');
-
-      if (error) {
-        setVersion('');
-      } else {
-        setVersion(version);
-      }
-    })();
-  }, []);
-
-  return version ? `v${version}` : 'N/A';
-}
-
-export default useServerVersion;
diff --git a/packages/desktop-client/src/index.js b/packages/desktop-client/src/index.js
index 1cd3eabad..5658e3b87 100644
--- a/packages/desktop-client/src/index.js
+++ b/packages/desktop-client/src/index.js
@@ -27,6 +27,7 @@ import { initialState as initialAppState } from 'loot-core/src/client/reducers/a
 import { send } from 'loot-core/src/platform/client/fetch';
 
 import App from './components/App';
+import { ServerProvider } from './components/ServerContext';
 import { handleGlobalEvents } from './global-events';
 
 // See https://github.com/WICG/focus-visible. Only makes the blue
@@ -71,7 +72,9 @@ window.$q = q;
 
 ReactDOM.render(
   <Provider store={store}>
-    <App />
+    <ServerProvider>
+      <App />
+    </ServerProvider>
   </Provider>,
   document.getElementById('root'),
 );
diff --git a/packages/loot-core/src/server/main.js b/packages/loot-core/src/server/main.js
index 62bcaeb94..52a5a4c5c 100644
--- a/packages/loot-core/src/server/main.js
+++ b/packages/loot-core/src/server/main.js
@@ -1370,7 +1370,7 @@ handlers['key-test'] = async function ({ fileId, password }) {
 
 handlers['subscribe-needs-bootstrap'] = async function ({ url } = {}) {
   if (getServer(url).BASE_SERVER === UNCONFIGURED_SERVER) {
-    return { bootstrapped: true };
+    return { bootstrapped: true, hasServer: false };
   }
 
   let res;
@@ -1390,7 +1390,7 @@ handlers['subscribe-needs-bootstrap'] = async function ({ url } = {}) {
     return { error: res.reason };
   }
 
-  return { bootstrapped: res.data.bootstrapped };
+  return { bootstrapped: res.data.bootstrapped, hasServer: true };
 };
 
 handlers['subscribe-bootstrap'] = async function ({ password }) {
@@ -1505,14 +1505,16 @@ handlers['get-server-url'] = async function () {
   return getServer() && getServer().BASE_SERVER;
 };
 
-handlers['set-server-url'] = async function ({ url }) {
+handlers['set-server-url'] = async function ({ url, validate = true }) {
   if (url != null) {
-    // Validate the server is running
-    let { error } = await runHandler(handlers['subscribe-needs-bootstrap'], {
-      url,
-    });
-    if (error) {
-      return { error };
+    if (validate) {
+      // Validate the server is running
+      let { error } = await runHandler(handlers['subscribe-needs-bootstrap'], {
+        url,
+      });
+      if (error) {
+        return { error };
+      }
     }
   } else {
     // When the server isn't configured, we just use a placeholder
-- 
GitLab