Repository: joinmarket-webui/jam
Pull request #647 (2023-08-23 09:55:18)

feat: backend version based feature toggles
Resolves https://github.com/joinmarket-webui/jam/issues/637.

Using the new `/getinfo` endpoint to provide the ability to use backend version based feature toggles.
e.g. as "import wallet" feature will be available with JM v0.9.10, this enables hiding/disabling the button.

### How to test

Change the port from the dev JM backend (port `28183`) to the secondary JM backend (port `29183`), which runs an older version. Restart the ui (`npm run dev:start`) and verify that the "Import Wallet" button is not displayed anymore.

```diff
diff --git a/src/setupProxy.js b/src/setupProxy.js
index 360592a..6cdcfa3 100644
--- a/src/setupProxy.js
+++ b/src/setupProxy.js
@@ -7,7 +7,7 @@ const SUPPORTED_BACKENDS = [BACKEND_NATIVE, BACKEND_STANDALONE]
 const {
   PUBLIC_URL = '',
   JAM_BACKEND = BACKEND_NATIVE,
-  JMWALLETD_API_PORT = '28183',
+  JMWALLETD_API_PORT = '29183',
   JMWALLETD_WEBSOCKET_PORT = '28283',
   JMOBWATCH_PORT = '62601',
   JAM_API_PORT = undefined,
```


Patch:
diff --git a/.gitignore b/.gitignore
index f7d330a..ec94a96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@
 .env.production.local
 
 npm-debug.log*
+
+.idea/
diff --git a/src/components/App.tsx b/src/components/App.tsx
index cf5c76b..89eecfb 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -179,8 +179,8 @@ export default function App() {
 
   if (settings.showOnboarding === true) {
     return (
-      <rb.Container className="onboarding my-5">
-        <rb.Row className="justify-content-center mt-md-5">
+      <rb.Container className="onboarding pt-3 pt-md-5">
+        <rb.Row className="justify-content-center">
           <rb.Col xs={10} sm={10} md={8} lg={6} xl={4}>
             <Onboarding />
           </rb.Col>
diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx
index e8dd311..8d7bab2 100644
--- a/src/components/CopyButton.tsx
+++ b/src/components/CopyButton.tsx
@@ -1,5 +1,4 @@
-import React, { PropsWithChildren, useState, useEffect, useRef } from 'react'
-import Sprite from './Sprite'
+import { ReactNode, PropsWithChildren, useState, useEffect, useRef } from 'react'
 
 const copyToClipboard = (
   text: string,
@@ -69,10 +68,9 @@ function Copyable({ value, onSuccess, onError, className, children, ...props }:
 }
 
 interface CopyButtonProps extends CopyableProps {
-  text: React.ReactNode | string
-  successText?: React.ReactNode | string
+  text: ReactNode
+  successText?: ReactNode
   successTextTimeout?: number
-  showSprites?: boolean
 }
 
 export function CopyButton({
@@ -83,7 +81,6 @@ export function CopyButton({
   successText = text,
   successTextTimeout = 1_500,
   className,
-  showSprites = true,
   ...props
 }: CopyButtonProps) {
   const [showValueCopiedConfirmation, setShowValueCopiedConfirmation] = useState(false)
@@ -112,16 +109,7 @@ export function CopyButton({
       }}
     >
       <div className="d-flex align-items-center justify-content-center">
-        {showSprites && (
-          <>
-            {showValueCopiedConfirmation ? (
-              <Sprite color="green" symbol="checkmark" className="me-1" width="20" height="20" />
-            ) : (
-              <Sprite symbol="copy" className="me-1" width="20" height="20" />
-            )}
-          </>
-        )}
-        <>{showValueCopiedConfirmation ? successText : text}</>
+        {showValueCopiedConfirmation ? successText : text}
       </div>
     </Copyable>
   )
diff --git a/src/components/CreateWallet.jsx b/src/components/CreateWallet.jsx
index b4e2072..0eacb09 100644
--- a/src/components/CreateWallet.jsx
+++ b/src/components/CreateWallet.jsx
@@ -13,7 +13,6 @@ import { useServiceInfo } from '../context/ServiceInfoContext'
 import * as Api from '../libs/JmWalletApi'
 import { routes } from '../constants/routes'
 import { isDebugFeatureEnabled } from '../constants/debugFeatures'
-import styles from './CreateWallet.module.css'
 
 const BackupConfirmation = ({ wallet, onSuccess, onCancel }) => {
   const { t } = useTranslation()
@@ -45,17 +44,18 @@ const BackupConfirmation = ({ wallet, onSuccess, onCancel }) => {
         <div className="mb-4 text-center text-success">{t('create_wallet.feedback_seed_confirmed')}</div>
       )}
 
-      <rb.Button variant="dark" className={styles.button} onClick={() => onSuccess()} disabled={!isSeedBackupConfirmed}>
+      <rb.Button
+        className="w-100 mb-4"
+        variant="dark"
+        size="lg"
+        disabled={!isSeedBackupConfirmed}
+        onClick={() => onSuccess()}
+      >
         {t('create_wallet.confirmation_button_fund_wallet')}
       </rb.Button>
 
-      <div className="d-flex mt-4 mb-4 gap-4">
-        <rb.Button
-          variant="outline-dark"
-          disabled={isSeedBackupConfirmed}
-          className={styles.button}
-          onClick={() => onCancel()}
-        >
+      <div className="d-flex justify-content-between mb-4 gap-4">
+        <rb.Button variant="none" disabled={isSeedBackupConfirmed} onClick={() => onCancel()}>
           <div className="d-flex justify-content-center align-items-center">
             <Sprite symbol="arrow-left" width="20" height="20" className="me-2" />
             {t('create_wallet.back_button')}
@@ -64,10 +64,10 @@ const BackupConfirmation = ({ wallet, onSuccess, onCancel }) => {
 
         {showSkipButton && (
           <rb.Button
+            className="position-relative"
             variant="outline-dark"
-            className={`${styles.button} position-relative`}
-            onClick={() => onSuccess()}
             disabled={isSeedBackupConfirmed}
+            onClick={() => onSuccess()}
           >
             <div className="d-flex justify-content-center align-items-center">
               {t('create_wallet.skip_button')}
diff --git a/src/components/Earn.jsx b/src/components/Earn.jsx
index a9a5a38..251b151 100644
--- a/src/components/Earn.jsx
+++ b/src/components/Earn.jsx
@@ -368,7 +368,7 @@ export default function Earn({ wallet }) {
     <div className={styles['earn']}>
       <PageTitle title={t('earn.title')} subtitle={t('earn.subtitle')} />
 
-      <rb.Row>
+      <rb.Row className="mb-2">
         <rb.Col>
           <rb.Fade in={serviceInfo?.coinjoinInProgress} mountOnEnter={true} unmountOnExit={true}>
             <rb.Alert variant="info" className="mb-4">
@@ -610,21 +610,22 @@ export default function Earn({ wallet }) {
                         </>
                       </Accordion>
                     )}
-                    <div className="mb-4">
-                      <rb.Button
-                        variant="dark"
-                        type="submit"
-                        className={styles['earn-btn']}
-                        disabled={
-                          isLoading ||
-                          isSubmitting ||
-                          serviceInfo?.rescanning === true ||
-                          isWaitingMakerStart ||
-                          isWaitingMakerStop
-                        }
-                      >
-                        <div className="d-flex justify-content-center align-items-center">
-                          {(isWaitingMakerStart || isWaitingMakerStop) && (
+                    <rb.Button
+                      className="w-100 mb-4"
+                      variant="dark"
+                      size="lg"
+                      type="submit"
+                      disabled={
+                        isLoading ||
+                        isSubmitting ||
+                        serviceInfo?.rescanning === true ||
+                        isWaitingMakerStart ||
+                        isWaitingMakerStop
+                      }
+                    >
+                      <div className="d-flex justify-content-center align-items-center">
+                        {isWaitingMakerStart || isWaitingMakerStop ? (
+                          <>
                             <rb.Spinner
                               as="span"
                               animation="border"
@@ -633,18 +634,14 @@ export default function Earn({ wallet }) {
                               aria-hidden="true"
                               className="me-2"
                             />
-                          )}
-                          {isWaitingMakerStart || isWaitingMakerStop ? (
-                            <>
-                              {isWaitingMakerStart && t('earn.text_starting')}
-                              {isWaitingMakerStop && t('earn.text_stopping')}
-                            </>
-                          ) : (
-                            <>{serviceInfo?.makerRunning === true ? t('earn.button_stop') : t('earn.button_start')}</>
-                          )}
-                        </div>
-                      </rb.Button>
-                    </div>
+                            {isWaitingMakerStart && t('earn.text_starting')}
+                            {isWaitingMakerStop && t('earn.text_stopping')}
+                          </>
+                        ) : (
+                          <>{serviceInfo?.makerRunning === true ? t('earn.button_stop') : t('earn.button_start')}</>
+                        )}
+                      </div>
+                    </rb.Button>
                   </rb.Form>
                 </>
               )}
@@ -652,7 +649,7 @@ export default function Earn({ wallet }) {
           )}
         </rb.Col>
       </rb.Row>
-      <rb.Row className="my-3">
+      <rb.Row className="mb-4">
         <rb.Col className="d-flex justify-content-center">
           <OrderbookOverlay
             show={isShowOrderbook}
@@ -661,7 +658,7 @@ export default function Earn({ wallet }) {
           />
           <rb.Button
             variant="outline-dark"
-            className="border-0 mb-2 d-inline-flex align-items-center"
+            className="border-0 d-inline-flex align-items-center"
             onClick={() => setIsShowOrderbook(true)}
           >
             <Sprite symbol="globe" width="24" height="24" className="me-2" />
@@ -673,7 +670,7 @@ export default function Earn({ wallet }) {
 
           <rb.Button
             variant="outline-dark"
-            className="border-0 mb-2 d-inline-flex align-items-center"
+            className="border-0 d-inline-flex align-items-center"
             onClick={() => setIsShowReport(true)}
           >
             <Sprite symbol="show" width="24" height="24" className="me-2" />
diff --git a/src/components/Earn.module.css b/src/components/Earn.module.css
index f43f912..f420767 100644
--- a/src/components/Earn.module.css
+++ b/src/components/Earn.module.css
@@ -12,11 +12,6 @@
   height: 3.5rem;
 }
 
-.earn .earn-btn {
-  height: 3rem;
-  width: 100%;
-}
-
 .inputGroupText {
   width: 5ch;
   display: inline-flex;
diff --git a/src/components/ImportWallet.tsx b/src/components/ImportWallet.tsx
index 916a070..36a2271 100644
--- a/src/components/ImportWallet.tsx
+++ b/src/components/ImportWallet.tsx
@@ -209,8 +209,10 @@ const ImportWalletDetailsForm = ({
           </rb.Button>
           <div className="d-flex mb-4 gap-4">
             <rb.Button variant="none" hidden={isSubmitting} disabled={isSubmitting} onClick={() => onCancel()}>
-              <Sprite symbol="arrow-left" width="20" height="20" className="me-2" />
-              {t('global.back')}
+              <div className="d-flex justify-content-center align-items-center">
+                <Sprite symbol="arrow-left" width="20" height="20" className="me-2" />
+                {t('global.back')}
+              </div>
             </rb.Button>
           </div>
         </rb.Form>
diff --git a/src/components/Jam.module.css b/src/components/Jam.module.css
index 6f25000..fc6c44f 100644
--- a/src/components/Jam.module.css
+++ b/src/components/Jam.module.css
@@ -1,8 +1,3 @@
-.submit {
-  height: 3rem;
-  width: 100%;
-}
-
 .input {
   height: 3.5rem;
   width: 100%;
diff --git a/src/components/Jam.tsx b/src/components/Jam.tsx
index f197a09..0815895 100644
--- a/src/components/Jam.tsx
+++ b/src/components/Jam.tsx
@@ -345,8 +345,9 @@ export default function Jam({ wallet }: JamProps) {
                   </div>
 
                   <rb.Button
-                    className={styles.submit}
+                    className="w-100 mb-4"
                     variant="dark"
+                    size="lg"
                     disabled={isLoading}
                     onClick={async () => {
                       await stopSchedule()
@@ -506,8 +507,9 @@ export default function Jam({ wallet }: JamProps) {
                           <p className="text-secondary mb-4">{t('scheduler.description_fees')}</p>
 
                           <rb.Button
-                            className={styles.submit}
+                            className="w-100 mb-4"
                             variant="dark"
+                            size="lg"
                             type="submit"
                             disabled={isSubmitting || !isValid || serviceInfo?.rescanning === true}
                           >
@@ -520,7 +522,7 @@ export default function Jam({ wallet }: JamProps) {
                     )}
                   </Formik>
 
-                  <rb.Row className="mt-5 mb-3">
+                  <rb.Row className="mt-2 mb-4">
                     <rb.Col className="d-flex justify-content-center">
                       <rb.Button
                         variant="outline-dark"
diff --git a/src/components/MainWalletView.module.css b/src/components/MainWalletView.module.css
index 4ee6a05..f882ae2 100644
--- a/src/components/MainWalletView.module.css
+++ b/src/components/MainWalletView.module.css
@@ -62,7 +62,10 @@
 }
 
 .sendReceiveButton {
-  padding: 0.25rem;
   font-weight: 500;
-  border-color: rgba(222, 222, 222, 1) !important;
+  border-color: var(--bs-border-color) !important;
+}
+
+.sendReceiveButton:hover {
+  border-color: transparent !important;
 }
diff --git a/src/components/CreateWallet.module.css b/src/components/MnemonicWordInput.module.css
similarity index 71%
rename from src/components/CreateWallet.module.css
rename to src/components/MnemonicWordInput.module.css
index 36257fc..f4576c8 100644
--- a/src/components/CreateWallet.module.css
+++ b/src/components/MnemonicWordInput.module.css
@@ -3,11 +3,6 @@
   width: 100%;
 }
 
-.button {
-  height: 3rem;
-  width: 100%;
-}
-
 .seedwordIndexBackup {
   width: 5ch;
   justify-content: right;
diff --git a/src/components/MnemonicWordInput.tsx b/src/components/MnemonicWordInput.tsx
index 8d63b6b..fcbf93a 100644
--- a/src/components/MnemonicWordInput.tsx
+++ b/src/components/MnemonicWordInput.tsx
@@ -1,6 +1,6 @@
 import * as rb from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
-import styles from './CreateWallet.module.css'
+import styles from './MnemonicWordInput.module.css'
 
 interface MnemonicWordInputProps {
   index: number
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 14118e0..7d59c3b 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
 import { Link, NavLink, To } from 'react-router-dom'
 import * as rb from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
@@ -6,7 +6,7 @@ import classNames from 'classnames'
 import Sprite from './Sprite'
 import Balance from './Balance'
 import { TabActivityIndicator, JoiningIndicator } from './ActivityIndicators'
-import { useSettings } from '../context/SettingsContext'
+import { useSettings, useSettingsDispatch } from '../context/SettingsContext'
 import { CurrentWallet, useCurrentWallet, useCurrentWalletInfo } from '../context/WalletContext'
 import { useServiceInfo, useSessionConnectionError } from '../context/ServiceInfoContext'
 import { walletDisplayName } from '../utils'
@@ -14,6 +14,7 @@ import { routes } from '../constants/routes'
 import { AmountSats } from '../libs/JmWalletApi'
 
 import styles from './Navbar.module.css'
+import { isDebugFeatureEnabled } from '../constants/debugFeatures'
 
 const BalanceLoadingIndicator = () => {
   return (
@@ -187,6 +188,11 @@ const TrailingNav = ({ joiningRoute, onClick }: TrailingNavProps) => {
           </div>
         </rb.Nav.Item>
       )}
+      {isDebugFeatureEnabled('fastThemeToggle') && (
+        <rb.Nav.Item className="d-none d-md-flex align-items-center pe-2">
+          <FastThemeToggle />
+        </rb.Nav.Item>
+      )}
       <rb.Nav.Item className="d-flex align-items-stretch">
         <NavLink
           to={routes.settings}
@@ -203,6 +209,32 @@ const TrailingNav = ({ joiningRoute, onClick }: TrailingNavProps) => {
   )
 }
 
+function FastThemeToggle() {
+  const settings = useSettings()
+  const settingsDispatch = useSettingsDispatch()
+  const isLightTheme = useMemo(() => settings.theme === window.JM.THEMES[0], [settings])
+
+  const setTheme = useCallback(
+    (theme) => {
+      if (window.JM.THEMES.includes(theme)) {
+        document.documentElement.setAttribute(window.JM.THEME_ROOT_ATTR, theme)
+        settingsDispatch({ theme })
+      }
+    },
+    [settingsDispatch]
+  )
+
+  return (
+    <Sprite
+      className="cursor-pointer"
+      onClick={() => setTheme(isLightTheme ? window.JM.THEMES[1] : window.JM.THEMES[0])}
+      symbol={isLightTheme ? window.JM.THEMES[0] : window.JM.THEMES[1]}
+      width="30"
+      height="30"
+    />
+  )
+}
+
 export default function Navbar() {
   const { t } = useTranslation()
   const settings = useSettings()
diff --git a/src/components/Onboarding.jsx b/src/components/Onboarding.jsx
index dc151e6..4207ae8 100644
--- a/src/components/Onboarding.jsx
+++ b/src/components/Onboarding.jsx
@@ -1,76 +1,83 @@
+import { useCallback, useState, useMemo } from 'react'
 import * as rb from 'react-bootstrap'
-import React, { useState } from 'react'
 import { Trans, useTranslation } from 'react-i18next'
 import Sprite from './Sprite'
 import { useSettingsDispatch } from '../context/SettingsContext'
+import styles from './Onboarding.module.css'
 
 export default function Onboarding() {
   const { t } = useTranslation()
   const [step, setStep] = useState(0)
   const settingsDispatch = useSettingsDispatch()
 
-  const next = () => {
+  const steps = useMemo(
+    () => [
+      {
+        title: t('onboarding.screen_1_title'),
+        description: t('onboarding.screen_1_description'),
+        icon: <Sprite symbol="welcome" width="11rem" height="11rem" />,
+      },
+      {
+        title: t('onboarding.screen_2_title'),
+        description: t('onboarding.screen_2_description'),
+        icon: <Sprite symbol="collab" width="10rem" height="10rem" />,
+      },
+      {
+        title: t('onboarding.screen_3_title'),
+        description: t('onboarding.screen_3_description'),
+        icon: <Sprite symbol="key" width="11rem" height="11rem" />,
+      },
+      {
+        title: t('onboarding.screen_4_title'),
+        description: t('onboarding.screen_4_description'),
+        icon: <Sprite symbol="handshake" width="11rem" height="11rem" />,
+      },
+      {
+        title: t('onboarding.screen_5_title'),
+        description: t('onboarding.screen_5_description'),
+        icon: <Sprite symbol="shield-outline" width="11rem" height="11rem" />,
+      },
+    ],
+    [t]
+  )
+
+  const next = useCallback(() => {
     if (step < steps.length) {
       setStep(step + 1)
     } else {
       settingsDispatch({ showOnboarding: false })
     }
-  }
+  }, [step, steps.length, settingsDispatch])
 
-  const steps = [
-    {
-      title: t('onboarding.screen_1_title'),
-      description: t('onboarding.screen_1_description'),
-      icon: 'welcome',
-    },
-    {
-      title: t('onboarding.screen_2_title'),
-      description: t('onboarding.screen_2_description'),
-      icon: 'collab',
-    },
-    {
-      title: t('onboarding.screen_3_title'),
-      description: t('onboarding.screen_3_description'),
-      icon: 'key',
-    },
-    {
-      title: t('onboarding.screen_4_title'),
-      description: t('onboarding.screen_4_description'),
-      icon: 'handshake',
-    },
-    {
-      title: t('onboarding.screen_5_title'),
-      description: t('onboarding.screen_5_description'),
-      icon: 'shield-outline',
-    },
-  ]
+  const back = () => setStep((current) => Math.max(0, current - 1))
 
   if (step === 0) {
     return (
-      <div>
-        <div className="d-flex justify-content-center">
+      <>
+        <div className="text-center mt-3 mb-4">
           <Sprite symbol="logo" width="128px" height="128px" className="mb-4" />
-        </div>
-        <h1 className="text-center">{t('onboarding.splashscreen_title')}</h1>
-        <h2 className="text-center fw-normal text-secondary mb-5">{t('onboarding.splashscreen_subtitle')}</h2>
-        <div className="d-flex justify-content-center mb-2">
-          <rb.Button size="lg" variant="dark" onClick={next}>
+          <h1>{t('onboarding.splashscreen_title')}</h1>
+          <h2 className="fw-normal text-secondary mb-5">{t('onboarding.splashscreen_subtitle')}</h2>
+          <rb.Button className="w-100 mb-2" size="lg" variant="dark" onClick={next}>
             {t('onboarding.splashscreen_button_get_started')}
           </rb.Button>
-        </div>
-        <div className="d-flex justify-content-center">
-          <rb.Button size="lg" variant="outline-dark" onClick={() => settingsDispatch({ showOnboarding: false })}>
+          <rb.Button
+            className="w-100 mb-2"
+            size="lg"
+            variant="outline-dark"
+            onClick={() => settingsDispatch({ showOnboarding: false })}
+          >
             {t('onboarding.splashscreen_button_skip_intro')}
           </rb.Button>
         </div>
-        <p className="mt-4 text-center text-secondary">
-          {t('onboarding.splashscreen_description_line1')}
-          <br />
-          {t('onboarding.splashscreen_description_line2')}
-        </p>
-        <div className="mt-4 text-secondary text-center">
-          <div className="fw-bolder">{t('onboarding.splashscreen_warning_title')}</div>
-          <div>
+        <div className="text-secondary mb-2">
+          <p className="text-center mb-4">
+            {t('onboarding.splashscreen_description_line1')}
+            <br />
+            {t('onboarding.splashscreen_description_line2')}
+          </p>
+          <div className="text-center fw-bolder">{t('onboarding.splashscreen_warning_title')}</div>
+          <p className="text-justify">
             <Trans i18nKey="onboarding.splashscreen_warning_text">
               While JoinMarket is tried and tested, Jam is not. It is in a beta stage, so use with caution.
               <a
@@ -85,26 +92,29 @@ export default function Onboarding() {
                 read the documentation
               </a>
             </Trans>
-          </div>
+          </p>
         </div>
-      </div>
+      </>
     )
   } else {
     const content = steps[step - 1]
 
     return (
       <>
-        <div className="flex-1">
-          <div className="d-flex justify-content-center">
-            <Sprite symbol={content.icon} width="5rem" height="5rem" className="mb-4" />
+        <div className="text-center mt-3">
+          <div className={`${styles.icon} d-flex justify-content-center align-items-center mb-4`}>{content.icon}</div>
+          <h2 className={`${styles.title} d-flex justify-content-center align-items-center mb-2`}>{content.title}</h2>
+          <div className={`${styles.description} d-flex justify-content-center align-items-center text-secondary mb-4`}>
+            {content.description}
           </div>
-          <h2 className="text-center mb-3">{content.title}</h2>
-          <p className="description text-center text-secondary mb-5">{content.description}</p>
         </div>
-        <div className="button d-flex justify-content-center">
-          <rb.Button size="lg" variant="dark" onClick={next}>
+        <div className="d-flex flex-column align-items-center gap-2">
+          <rb.Button className="w-50" variant="dark" size="lg" onClick={next}>
             {step === steps.length ? t('onboarding.button_complete') : t('onboarding.button_next')}
           </rb.Button>
+          <rb.Button className="w-50" variant="none" size="sm" onClick={back}>
+            {t('global.back')}
+          </rb.Button>
         </div>
       </>
     )
diff --git a/src/components/Onboarding.module.css b/src/components/Onboarding.module.css
new file mode 100644
index 0000000..80b18a4
--- /dev/null
+++ b/src/components/Onboarding.module.css
@@ -0,0 +1,9 @@
+.icon {
+  min-height: 19ch;
+}
+.title {
+  min-height: 4ch;
+}
+.description {
+  min-height: 17ch;
+}
diff --git a/src/components/Receive.jsx b/src/components/Receive.jsx
index 433b2f4..d51ecd5 100644
--- a/src/components/Receive.jsx
+++ b/src/components/Receive.jsx
@@ -104,8 +104,18 @@ export default function Receive({ wallet }) {
               <CopyButton
                 className="btn btn-outline-dark flex-1"
                 value={address}
-                text={t('receive.button_copy_address')}
-                successText={t('receive.text_copy_address_confirmed')}
+                text={
+                  <>
+                    <Sprite symbol="copy" className="me-1" width="24" height="24" />
+                    {t('receive.button_copy_address')}
+                  </>
+                }
+                successText={
+                  <>
+                    <Sprite color="green" symbol="checkmark" className="me-1" width="24" height="24" />
+                    {t('receive.text_copy_address_confirmed')}
+                  </>
+                }
                 disabled={!address || isLoading}
               />
               {checkIsWebShareAPISupported() && <ShareButton value={address} className="flex-1" />}
diff --git a/src/components/Receive.module.css b/src/components/Receive.module.css
index 844a123..31adaff 100644
--- a/src/components/Receive.module.css
+++ b/src/components/Receive.module.css
@@ -9,7 +9,6 @@
 }
 
 .receive button {
-  padding: 0.25rem 1rem;
   font-weight: 500;
   border-color: rgba(222, 222, 222, 1);
 }
diff --git a/src/components/Send/Send.module.css b/src/components/Send/Send.module.css
index 052d3ad..e4539b2 100644
--- a/src/components/Send/Send.module.css
+++ b/src/components/Send/Send.module.css
@@ -103,24 +103,13 @@ input[type='number'] {
   color: var(--bs-white) !important;
 }
 
-.button {
-  height: 3rem;
-  width: 100%;
-}
-
 .inputLoader {
   height: 3.5rem;
   border-radius: 0.25rem;
 }
 
-.serviceRunning .sendForm {
-  filter: blur(2px);
-}
-
-.serviceRunning .collaboratorsSelector {
-  filter: blur(2px);
-}
-
+.serviceRunning .sendForm,
+.serviceRunning .collaboratorsSelector,
 .serviceRunning .sendButton {
   filter: blur(2px);
 }
diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx
index a41c14d..a824111 100644
--- a/src/components/Send/index.tsx
+++ b/src/components/Send/index.tsx
@@ -920,10 +920,11 @@ export default function Send({ wallet }: SendProps) {
         </rb.Form>
         <rb.Button
           ref={submitButtonRef}
+          className={classNames('w-100', 'mb-4', styles.sendButton)}
           variant={submitButtonOptions.variant}
+          size="lg"
           type="submit"
           disabled={isOperationDisabled || isLoading || isSending || !formIsValid}
-          className={classNames(styles.button, styles.sendButton, 'mb-4')}
           form="send-form"
         >
           {isSending ? (
diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx
index 1ea0243..56204f2 100644
--- a/src/components/Settings.jsx
+++ b/src/components/Settings.jsx
@@ -17,6 +17,7 @@ import styles from './Settings.module.css'
 import SeedModal from './settings/SeedModal'
 import FeeConfigModal from './settings/FeeConfigModal'
 import { isDebugFeatureEnabled } from '../constants/debugFeatures'
+import { isFeatureEnabled } from '../constants/features'
 
 export default function Settings({ wallet, stopWallet }) {
   const { t, i18n } = useTranslation()
@@ -226,7 +227,7 @@ export default function Settings({ wallet, stopWallet }) {
             )}
           </rb.Button>
 
-          {isDebugFeatureEnabled('rescanChainPage') && (
+          {serviceInfo && isFeatureEnabled('rescanChain', serviceInfo) && isDebugFeatureEnabled('rescanChainPage') && (
             <Link
               to={routes.rescanChain}
               className={`btn btn-outline-dark ${styles['settings-btn']} position-relative`}
diff --git a/src/components/WalletCreationConfirmation.tsx b/src/components/WalletCreationConfirmation.tsx
index 5525006..41b84d1 100644
--- a/src/components/WalletCreationConfirmation.tsx
+++ b/src/components/WalletCreationConfirmation.tsx
@@ -5,8 +5,6 @@ import { Formik } from 'formik'
 import Seedphrase from './Seedphrase'
 import ToggleSwitch from './ToggleSwitch'
 import { walletDisplayName } from '../utils'
-// TODO: currently reusing CreateWallet styles - move to own module.css?
-import styles from './CreateWallet.module.css'
 
 export type WalletInfo = {
   walletFileName: string
@@ -82,8 +80,9 @@ const WalletCreationConfirmation = ({ wallet, submitButtonText, onSubmit }: Wall
             />
           </div>
           <rb.Button
+            className="w-100 mb-4"
             variant="dark"
-            className={styles.button}
+            size="lg"
             type="submit"
             disabled={!sensitiveInfoWasRevealed || !userConfirmed || isSubmitting}
           >
diff --git a/src/components/WalletCreationForm.module.css b/src/components/WalletCreationForm.module.css
index 7221cc0..821c7c6 100644
--- a/src/components/WalletCreationForm.module.css
+++ b/src/components/WalletCreationForm.module.css
@@ -2,8 +2,3 @@
   height: 3.5rem;
   width: 100%;
 }
-
-.button {
-  height: 3rem;
-  width: 100%;
-}
diff --git a/src/components/WalletCreationForm.tsx b/src/components/WalletCreationForm.tsx
index 14dde36..fd4c6db 100644
--- a/src/components/WalletCreationForm.tsx
+++ b/src/components/WalletCreationForm.tsx
@@ -116,14 +116,12 @@ const WalletCreationForm = ({
             <rb.Form.Control.Feedback type="invalid">{errors.passwordConfirm}</rb.Form.Control.Feedback>
           </rb.Form.Group>
           <rb.Button className="w-100 mb-4" variant="dark" size="lg" type="submit" disabled={isSubmitting}>
-            {isSubmitting ? (
-              <div className="d-flex justify-content-center align-items-center">
+            <div className="d-flex justify-content-center align-items-center">
+              {isSubmitting && (
                 <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
-                {submitButtonText(isSubmitting)}
-              </div>
-            ) : (
-              submitButtonText(isSubmitting)
-            )}
+              )}
+              {submitButtonText(isSubmitting)}
+            </div>
           </rb.Button>
           {isSubmitting && (
             <div className="text-center text-muted small mb-4">
@@ -137,8 +135,10 @@ const WalletCreationForm = ({
             disabled={isSubmitting}
             onClick={() => onCancel()}
           >
-            <Sprite symbol="cross" width="20" height="20" className="me-2" />
-            {t('global.cancel')}
+            <div className="d-flex justify-content-center align-items-center">
+              <Sprite symbol="cross" width="20" height="20" className="me-2" />
+              {t('global.cancel')}
+            </div>
           </rb.Button>
         </rb.Form>
       )}
diff --git a/src/components/Wallets.jsx b/src/components/Wallets.jsx
index fead18e..9141879 100644
--- a/src/components/Wallets.jsx
+++ b/src/components/Wallets.jsx
@@ -12,6 +12,7 @@ import { walletDisplayName } from '../utils'
 import * as Api from '../libs/JmWalletApi'
 import { routes } from '../constants/routes'
 import { ConfirmModal } from './Modal'
+import { isFeatureEnabled } from '../constants/features'
 
 function arrayEquals(a, b) {
   return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index])
@@ -232,6 +233,7 @@ export default function Wallets({ currentWallet, startWallet, stopWallet }) {
             )
           })
         )}
+
         <div
           className={classNames('d-flex', 'justify-content-center', 'gap-2', 'mt-4', {
             'flex-column': walletList?.length === 0,
@@ -243,7 +245,7 @@ export default function Wallets({ currentWallet, startWallet, stopWallet }) {
               'btn-lg': walletList?.length === 0,
               'btn-dark': walletList?.length === 0,
               'btn-outline-dark': !walletList || walletList.length > 0,
-              disabled: isUnlocking,
+              disabled: isLoading || isUnlocking,
             })}
             data-testid="new-wallet-btn"
           >
@@ -252,19 +254,21 @@ export default function Wallets({ currentWallet, startWallet, stopWallet }) {
               <span>{t('wallets.button_new_wallet')}</span>
             </div>
           </Link>
-          <Link
-            to={routes.importWallet}
-            className={classNames('btn', 'btn-outline-dark', {
-              'btn-lg': walletList?.length === 0,
-              disabled: isUnlocking,
-            })}
-            data-testid="import-wallet-btn"
-          >
-            <div className="d-flex justify-content-center align-items-center">
-              <Sprite symbol="arrow-right" width="20" height="20" className="me-2" />
-              <span>{t('wallets.button_import_wallet')}</span>
-            </div>
-          </Link>
+          {serviceInfo && isFeatureEnabled('importWallet', serviceInfo) && (
+            <Link
+              to={routes.importWallet}
+              className={classNames('btn', 'btn-outline-dark', {
+                'btn-lg': walletList?.length === 0,
+                disabled: isLoading || isUnlocking,
+              })}
+              data-testid="import-wallet-btn"
+            >
+              <div className="d-flex justify-content-center align-items-center">
+                <Sprite symbol="arrow-right" width="20" height="20" className="me-2" />
+                <span>{t('wallets.button_import_wallet')}</span>
+              </div>
+            </Link>
+          )}
         </div>
       </div>
 
diff --git a/src/components/fb/CreateFidelityBond.module.css b/src/components/fb/CreateFidelityBond.module.css
index 8bb6222..2b4545d 100644
--- a/src/components/fb/CreateFidelityBond.module.css
+++ b/src/components/fb/CreateFidelityBond.module.css
@@ -43,21 +43,6 @@
   display: none;
 }
 
-.buttons {
-  display: flex;
-  flex-direction: column;
-  gap: 0.5rem;
-  margin-top: 1rem;
-}
-
-.buttons > button {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 2.2rem;
-  font-size: 1rem;
-}
-
 .successCheckmark {
   width: '2rem';
   height: '2rem';
diff --git a/src/components/fb/CreateFidelityBond.tsx b/src/components/fb/CreateFidelityBond.tsx
index b4a497a..45909cd 100644
--- a/src/components/fb/CreateFidelityBond.tsx
+++ b/src/components/fb/CreateFidelityBond.tsx
@@ -452,7 +452,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon
     }
 
     if (currentStep === steps.selectJar) {
-      if (selectedJar !== null) {
+      if (selectedJar !== undefined) {
         return steps.selectUtxos
       }
     }
@@ -625,9 +625,10 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon
         <div>
           <hr />
           <div className="mb-5">{stepComponent(step)}</div>
-          <div className={styles.buttons}>
+          <div className="d-flex flex-column gap-2">
             {!isLoading && primaryButtonText(step) !== null && (
               <rb.Button
+                className="w-100"
                 variant={
                   nextStep(step) === steps.createFidelityBond && !onlyCjOutOrFbUtxosSelected() ? 'danger' : 'dark'
                 }
@@ -639,7 +640,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon
               </rb.Button>
             )}
             {!isLoading && secondaryButtonText(step) !== null && (
-              <rb.Button variant="white" type="submit" onClick={onSecondaryButtonClicked}>
+              <rb.Button className="w-100" variant="none" onClick={onSecondaryButtonClicked}>
                 {secondaryButtonText(step)}
               </rb.Button>
             )}
diff --git a/src/components/fb/ExistingFidelityBond.tsx b/src/components/fb/ExistingFidelityBond.tsx
index 9ccb8fa..7a9dfda 100644
--- a/src/components/fb/ExistingFidelityBond.tsx
+++ b/src/components/fb/ExistingFidelityBond.tsx
@@ -77,7 +77,6 @@ const ExistingFidelityBond = ({ fidelityBond, children }: PropsWithChildren<Exis
           </div>
           <div className="d-flex align-items-center gap-2">
             <CopyButton
-              showSprites={false}
               text={<Sprite symbol="copy" width="18" height="18" />}
               successText={<Sprite symbol="checkmark" width="18" height="18" />}
               value={fidelityBond.address}
diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx
index 8168a32..1dfd317 100644
--- a/src/components/fb/FidelityBondSteps.tsx
+++ b/src/components/fb/FidelityBondSteps.tsx
@@ -316,7 +316,6 @@ const ReviewInputs = ({ lockDate, jar, utxos, selectedUtxos, timelockedAddress }
     {
       icon: (
         <CopyButton
-          showSprites={false}
           text={<Sprite symbol="copy" width="18" height="18" />}
           successText={<Sprite symbol="checkmark" width="18" height="18" />}
           value={timelockedAddress}
@@ -391,7 +390,6 @@ const CreatedFidelityBond = ({ fbUtxo, frozenUtxos }: CreatedFidelityBondProps)
             </div>
             <div className="d-flex align-items-center gap-2">
               <CopyButton
-                showSprites={false}
                 text={<Sprite symbol="copy" width="18" height="18" />}
                 successText={<Sprite symbol="checkmark" width="18" height="18" />}
                 value={fbUtxo.address}
diff --git a/src/components/jar_details/DisplayBranch.tsx b/src/components/jar_details/DisplayBranch.tsx
index ad1bd30..99c6a42 100644
--- a/src/components/jar_details/DisplayBranch.tsx
+++ b/src/components/jar_details/DisplayBranch.tsx
@@ -126,8 +126,7 @@ const DisplayBranchEntry = ({ entry, ...props }: DisplayBranchEntryProps) => {
           </rb.Col>
           <rb.Col xs={{ span: 12, order: 3 }} sm={{ span: 10 }} lg={{ span: true, order: 2 }}>
             <CopyButton
-              className={`${styles['address-copy-button']}`}
-              showSprites={false}
+              className={styles['address-copy-button']}
               text={
                 <>
                   <code className="text-break">{address}</code>
diff --git a/src/constants/debugFeatures.ts b/src/constants/debugFeatures.ts
index eb3dcc6..97e3ab7 100644
--- a/src/constants/debugFeatures.ts
+++ b/src/constants/debugFeatures.ts
@@ -5,6 +5,7 @@ interface DebugFeatures {
   errorExamplePage: boolean
   importDummyMnemonicPhrase: boolean
   rescanChainPage: boolean
+  fastThemeToggle: boolean
 }
 
 const devMode = process.env.NODE_ENV === 'development' && process.env.REACT_APP_JAM_DEV_MODE === 'true'
@@ -16,6 +17,7 @@ const debugFeatures: DebugFeatures = {
   errorExamplePage: devMode,
   importDummyMnemonicPhrase: devMode,
   rescanChainPage: devMode,
+  fastThemeToggle: devMode,
 }
 
 type DebugFeature = keyof DebugFeatures
diff --git a/src/constants/features.ts b/src/constants/features.ts
new file mode 100644
index 0000000..d0ea911
--- /dev/null
+++ b/src/constants/features.ts
@@ -0,0 +1,26 @@
+import { ServiceInfo } from '../context/ServiceInfoContext'
+
+interface Features {
+  importWallet: SemVer
+  rescanChain: SemVer
+}
+
+const features: Features = {
+  importWallet: { major: 0, minor: 9, patch: 10 }, // added in https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1461
+  rescanChain: { major: 0, minor: 9, patch: 10 }, // added in https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1461
+}
+
+type Feature = keyof Features
+
+const __isFeatureEnabled = (name: Feature, version: SemVer): boolean => {
+  const target = features[name]
+  return (
+    version.major > target.major ||
+    (version.major === target.major && version.minor > target.minor) ||
+    (version.major === target.major && version.minor === target.minor && version.patch >= target.patch)
+  )
+}
+
+export const isFeatureEnabled = (name: Feature, serviceInfo: ServiceInfo): boolean => {
+  return !!serviceInfo.server?.version && __isFeatureEnabled(name, serviceInfo.server.version)
+}
diff --git a/src/context/ServiceInfoContext.tsx b/src/context/ServiceInfoContext.tsx
index 1659e8f..ba82779 100644
--- a/src/context/ServiceInfoContext.tsx
+++ b/src/context/ServiceInfoContext.tsx
@@ -53,21 +53,56 @@ interface JmSessionData {
   rescanning: boolean
 }
 
+interface JmGetInfoData {
+  version: string
+}
+
+const UNKNOWN_VERSION: SemVer = { major: 0, minor: 0, patch: 0, raw: 'unknown' }
+
 type SessionFlag = { sessionActive: boolean }
 type MakerRunningFlag = { makerRunning: boolean }
 type CoinjoinInProgressFlag = { coinjoinInProgress: boolean }
 type RescanBlockchainInProgressFlag = { rescanning: boolean }
 
+type SessionInfo = {
+  walletName: Api.WalletName | null
+  schedule: Schedule | null
+  offers: Offer[] | null
+  nickname: string | null
+}
+type ServerInfo = {
+  server?: {
+    version?: SemVer
+  }
+}
+
 type ServiceInfo = SessionFlag &
   MakerRunningFlag &
   CoinjoinInProgressFlag &
-  RescanBlockchainInProgressFlag & {
-    walletName: Api.WalletName | null
-    schedule: Schedule | null
-    offers: Offer[] | null
-    nickname: string | null
+  RescanBlockchainInProgressFlag &
+  SessionInfo &
+  ServerInfo
+type ServiceInfoUpdate =
+  | ServiceInfo
+  | MakerRunningFlag
+  | CoinjoinInProgressFlag
+  | RescanBlockchainInProgressFlag
+  | ServerInfo
+
+const versionRegex = new RegExp(/^(\d+)\.(\d+)\.(\d+).*$/)
+const toSemVer = (data: JmGetInfoData): SemVer => {
+  const arr = versionRegex.exec(data.version)
+  if (!arr || arr.length < 4) {
+    return UNKNOWN_VERSION
   }
-type ServiceInfoUpdate = ServiceInfo | MakerRunningFlag | CoinjoinInProgressFlag | RescanBlockchainInProgressFlag
+
+  return {
+    major: parseInt(arr[1], 10),
+    minor: parseInt(arr[2], 10),
+    patch: parseInt(arr[3], 10),
+    raw: data.version,
+  }
+}
 
 interface ServiceInfoContextEntry {
   serviceInfo: ServiceInfo | null
@@ -91,6 +126,34 @@ const ServiceInfoProvider = ({ children }: React.PropsWithChildren<{}>) => {
   )
   const [connectionError, setConnectionError] = useState<Error>()
 
+  useEffect(() => {
+    const abortCtrl = new AbortController()
+
+    Api.getGetinfo({ signal: abortCtrl.signal })
+      .then((res) => (res.ok ? res.json() : Api.Helper.throwError(res)))
+      .then((data: JmGetInfoData) => {
+        dispatchServiceInfo({
+          server: {
+            version: toSemVer(data),
+          },
+        })
+      })
+      .catch((err) => {
+        const notFound = err.response.status === 404
+        if (notFound) {
+          dispatchServiceInfo({
+            server: {
+              version: UNKNOWN_VERSION,
+            },
+          })
+        }
+      })
+
+    return () => {
+      abortCtrl.abort()
+    }
+  }, [connectionError])
+
   useEffect(() => {
     if (connectionError) {
       // Just reset the wallet info, not the session storage (token),
@@ -262,6 +325,7 @@ export {
   useReloadServiceInfo,
   useDispatchServiceInfo,
   useSessionConnectionError,
+  ServiceInfo,
   Schedule,
   StateFlag,
 }
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 476ca5f..30c01b0 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -8,3 +8,5 @@ type Seconds = number
 declare type MnemonicPhrase = string[]
 
 declare type SimpleAlert = import('react-bootstrap').AlertProps & { message: string | import('react').ReactNode }
+
+declare type SemVer = { major: number; minor: number; patch: number; raw?: string }
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json
index 649ece1..6f5a996 100644
--- a/src/i18n/locales/en/translation.json
+++ b/src/i18n/locales/en/translation.json
@@ -133,7 +133,7 @@
     "placeholder_password_confirm": "Repeat the password...",
     "feedback_invalid_password_confirm": "Given passwords do not match.",
     "button_create": "Create",
-    "button_creating": "Creating",
+    "button_creating": "Creating...",
     "error_creating_failed": "Error while creating the wallet. Reason: {{ reason }}",
     "alert_confirmation_failed": "Wallet confirmation failed.",
     "confirmation_label_wallet_name": "Wallet Name",
diff --git a/src/index.css b/src/index.css
index fa4002b..62c5ce8 100644
--- a/src/index.css
+++ b/src/index.css
@@ -292,6 +292,16 @@ main {
 .text-small {
   font-size: 0.875rem;
 }
+.text-justify {
+  text-align: justify;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+.cursor-wait {
+  cursor: wait;
+}
 
 /* Fullscreen Overlays */
 .offcanvas-fullscreen {
@@ -506,11 +516,6 @@ h2 {
   text-decoration: underline;
 }
 
-.wallets .btn {
-  padding: 0.25rem 1rem;
-  font-weight: 500;
-}
-
 .wallets .btn.btn-outline-dark:not(:hover) {
   border-color: rgba(222, 222, 222, 1);
 }
@@ -551,21 +556,10 @@ h2 {
   }
 }
 
-/* Onboarding */
-
-.onboarding button {
-  height: 3rem;
-  width: 80%;
-}
-
 .btn:disabled:hover {
   cursor: not-allowed;
 }
 
-.cursor-wait {
-  cursor: wait;
-}
-
 .accordion {
   --bs-accordion-color: var(--bs-black);
   --bs-accordion-active-color: var(--bs-black);
diff --git a/src/libs/JmWalletApi.ts b/src/libs/JmWalletApi.ts
index aa8305b..916be1d 100644
--- a/src/libs/JmWalletApi.ts
+++ b/src/libs/JmWalletApi.ts
@@ -210,6 +210,12 @@ const Helper = (() => {
   }
 })()
 
+const getGetinfo = async ({ signal }: ApiRequestContext) => {
+  return await fetch(`${basePath()}/v1/getinfo`, {
+    signal,
+  })
+}
+
 const getSession = async ({ token, signal }: ApiRequestContext & { token?: ApiToken }) => {
   return await fetch(`${basePath()}/v1/session`, {
     headers: token ? { ...Helper.buildAuthHeader(token) } : undefined,
@@ -465,6 +471,7 @@ export class JmApiError extends Error {
 }
 
 export {
+  getGetinfo,
   postMakerStart,
   getMakerStop,
   getSession,


Test patch:
diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx
index c3f047e..ff6c22a 100644
--- a/src/components/App.test.tsx
+++ b/src/components/App.test.tsx
@@ -7,12 +7,14 @@ import App from './App'
 
 jest.mock('../libs/JmWalletApi', () => ({
   ...jest.requireActual('../libs/JmWalletApi'),
+  getGetinfo: jest.fn(),
   getSession: jest.fn(),
 }))
 
 describe('<App />', () => {
   beforeEach(() => {
     const neverResolvingPromise = new Promise(() => {})
+    ;(apiMock.getGetinfo as jest.Mock).mockResolvedValue(neverResolvingPromise)
     ;(apiMock.getSession as jest.Mock).mockResolvedValue(neverResolvingPromise)
   })
 
diff --git a/src/components/CreateWallet.test.jsx b/src/components/CreateWallet.test.jsx
index cd1ef52..6d0c9f3 100644
--- a/src/components/CreateWallet.test.jsx
+++ b/src/components/CreateWallet.test.jsx
@@ -11,6 +11,7 @@ import CreateWallet from './CreateWallet'
 
 jest.mock('../libs/JmWalletApi', () => ({
   ...jest.requireActual('../libs/JmWalletApi'),
+  getGetinfo: jest.fn(),
   getSession: jest.fn(),
   postWalletCreate: jest.fn(),
 }))
@@ -32,7 +33,8 @@ describe('<CreateWallet />', () => {
 
   beforeEach(() => {
     const neverResolvingPromise = new Promise(() => {})
-    apiMock.getSession.mockReturnValue(neverResolvingPromise)
+    apiMock.getGetinfo.mockResolvedValue(neverResolvingPromise)
+    apiMock.getSession.mockResolvedValue(neverResolvingPromise)
   })
 
   it('should render without errors', () => {
diff --git a/src/components/Wallet.test.tsx b/src/components/Wallet.test.tsx
index 8e9cd87..7bddc31 100644
--- a/src/components/Wallet.test.tsx
+++ b/src/components/Wallet.test.tsx
@@ -9,6 +9,7 @@ import Wallet, { WalletProps } from './Wallet'
 
 jest.mock('../libs/JmWalletApi', () => ({
   ...jest.requireActual('../libs/JmWalletApi'),
+  getGetinfo: jest.fn(),
   getSession: jest.fn(),
 }))
 
@@ -43,7 +44,8 @@ describe('<Wallet />', () => {
 
   beforeEach(() => {
     const neverResolvingPromise = new Promise<Response>(() => {})
-    jest.mocked(apiMock.getSession).mockReturnValue(neverResolvingPromise)
+    ;(apiMock.getGetinfo as jest.Mock).mockResolvedValue(neverResolvingPromise)
+    ;(apiMock.getSession as jest.Mock).mockResolvedValue(neverResolvingPromise)
   })
 
   it('should render inactive wallet without errors', () => {
diff --git a/src/components/Wallets.test.jsx b/src/components/Wallets.test.jsx
index 9616a01..802c789 100644
--- a/src/components/Wallets.test.jsx
+++ b/src/components/Wallets.test.jsx
@@ -9,6 +9,7 @@ import Wallets from './Wallets'
 
 jest.mock('../libs/JmWalletApi', () => ({
   ...jest.requireActual('../libs/JmWalletApi'),
+  getGetinfo: jest.fn(),
   getSession: jest.fn(),
   getWalletAll: jest.fn(),
   postWalletUnlock: jest.fn(),
@@ -38,6 +39,7 @@ describe('<Wallets />', () => {
   beforeEach(() => {
     const neverResolvingPromise = new Promise(() => {})
     apiMock.getSession.mockResolvedValue(neverResolvingPromise)
+    apiMock.getGetinfo.mockResolvedValue(neverResolvingPromise)
   })
 
   it('should render without errors', () => {
@@ -86,6 +88,10 @@ describe('<Wallets />', () => {
       ok: true,
       json: () => Promise.resolve({ wallets: [] }),
     })
+    apiMock.getGetinfo.mockResolvedValueOnce({
+      ok: true,
+      json: () => Promise.resolve({ version: '0.9.10dev' }),
+    })
 
     act(() => setup({}))
 
@@ -102,7 +108,7 @@ describe('<Wallets />', () => {
     const newWalletButtonBeforeAfter = screen.getByTestId('new-wallet-btn')
     expect(newWalletButtonBeforeAfter.classList.contains('btn-lg')).toBe(true)
 
-    const importWalletButton = screen.getByTestId('import-wallet-btn')
+    const importWalletButton = await screen.findByTestId('import-wallet-btn')
     expect(importWalletButton.classList.contains('btn-lg')).toBe(true)
   })
 
@@ -121,6 +127,10 @@ describe('<Wallets />', () => {
       ok: true,
       json: () => Promise.resolve({ wallets: ['wallet0.jmdat', 'wallet1.jmdat'] }),
     })
+    apiMock.getGetinfo.mockResolvedValueOnce({
+      ok: true,
+      json: () => Promise.resolve({ version: '0.9.10dev' }),
+    })
 
     act(() => setup({}))
 
@@ -136,11 +146,40 @@ describe('<Wallets />', () => {
     expect(newWalletButton.classList.contains('btn')).toBe(true)
     expect(newWalletButton.classList.contains('btn-lg')).toBe(false)
 
-    const importWalletButton = screen.getByTestId('import-wallet-btn')
+    const importWalletButton = await screen.findByTestId('import-wallet-btn')
     expect(importWalletButton.classList.contains('btn')).toBe(true)
     expect(importWalletButton.classList.contains('btn-lg')).toBe(false)
   })
 
+  it('should hide "Import Wallet"-button on unsupported backend version', async () => {
+    apiMock.getSession.mockResolvedValueOnce({
+      ok: true,
+      json: () =>
+        Promise.resolve({
+          session: false,
+          maker_running: false,
+          coinjoin_in_process: false,
+          wallet_name: 'None',
+        }),
+    })
+    apiMock.getWalletAll.mockResolvedValueOnce({
+      ok: true,
+      json: () => Promise.resolve({ wallets: [] }),
+    })
+    apiMock.getGetinfo.mockResolvedValueOnce({
+      ok: true,
+      json: () => Promise.resolve({ version: '0.9.9' }),
+    })
+
+    act(() => setup({}))
+
+    expect(screen.getByText('wallets.text_loading')).toBeInTheDocument()
+    await waitForElementToBeRemoved(screen.getByText('wallets.text_loading'))
+
+    expect(screen.queryByTestId('import-wallet-btn')).not.toBeInTheDocument()
+    expect(screen.getByTestId('new-wallet-btn')).toBeInTheDocument()
+  })
+
   describe('<Wallets /> lock/unlock flow', () => {
     const dummyWalletName = 'dummy.jmdat'
     const dummyToken = 'dummyToken'