Repository: joinmarket-webui/jam (ts)

Issue:
/ char should not be allowed in wallet name
**Expected behavior**

entering a `/` char in the wallet name should be disallowed by the javascript.

**Actual behavior**

Wallet creation in bitcoin core will fail because / cannot be in a directory name

**Steps to reproduce the problem**
  1. Create a new wallet
  2. Put a / in the name
  3. Click create

**Specifications**
  - Version: 016
  - Platform: Linux
  - Browser: Firefox

**Additional context**

![image](https://github.com/joinmarket-webui/jam/assets/106188942/6babd4bd-eac7-43a5-a20e-8cf32f4d6c20)


Pull request description:
chore: validate wallet name
Resolves #683.

Before this commit, the wallet name has been validated only on the backend.
After this commit is applied, it will also be validated by the frontend.

The validation is a bit more strict than "necessary", only numbers, letters and `_` (underscore) will be allowed from now on.

<img src="https://github.com/joinmarket-webui/jam/assets/3358649/3cc76bb8-85f5-412a-9308-b7e5b671bc3b" width=350 />



Patch:
diff --git a/src/components/App.tsx b/src/components/App.tsx
index f3d3993..a71aba1 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from 'react'
 import * as rb from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import {
@@ -27,7 +27,7 @@ import { isDebugFeatureEnabled } from '../constants/debugFeatures'
 import CreateWallet from './CreateWallet'
 import ImportWallet from './ImportWallet'
 import Earn from './Earn'
-import ErrorPage, { ErrorThrowingComponent } from './ErrorPage'
+import ErrorPage from './ErrorPage'
 import Footer from './Footer'
 import Jam from './Jam'
 import Layout from './Layout'
@@ -39,6 +39,7 @@ import Send from './Send'
 import RescanChain from './RescanChain'
 import Settings from './Settings'
 import Wallets from './Wallets'
+const DevSetupPage = lazy(() => import('./DevSetupPage'))
 
 export default function App() {
   const { t } = useTranslation()
@@ -168,6 +169,17 @@ export default function App() {
               {isDebugFeatureEnabled('errorExamplePage') && (
                 <Route id="error-example" path={routes.__errorExample} element={<ErrorThrowingComponent />} />
               )}
+              {isDebugFeatureEnabled('devSetupPage') && (
+                <Route
+                  id="dev-env"
+                  path={routes.__devSetup}
+                  element={
+                    <Suspense fallback={<Loading />}>
+                      <DevSetupPage />
+                    </Suspense>
+                  }
+                />
+              )}
               <Route id="404" path="*" element={<Navigate to={routes.home} replace={true} />} />
             </>
           )}
@@ -208,6 +220,23 @@ export default function App() {
   )
 }
 
+const Loading = () => {
+  const { t } = useTranslation()
+  return (
+    <div className="text-center">
+      <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
+      {t('global.loading')}
+    </div>
+  )
+}
+
+const ErrorThrowingComponent = () => {
+  useEffect(() => {
+    throw new Error('This error is thrown on purpose. Only to be used for testing.')
+  }, [])
+  return <></>
+}
+
 const RELOAD_WALLET_INFO_DELAY: {
   AFTER_RESCAN: Milliseconds
   AFTER_UNLOCK: Milliseconds
diff --git a/src/components/DevSetupPage.tsx b/src/components/DevSetupPage.tsx
new file mode 100644
index 0000000..2894de4
--- /dev/null
+++ b/src/components/DevSetupPage.tsx
@@ -0,0 +1,111 @@
+import { Link } from 'react-router-dom'
+import Sprite from './Sprite'
+import PageTitle from './PageTitle'
+import { routes } from '../constants/routes'
+
+const DEFAULT_BASIC_AUTH = {
+  user: 'joinmarket',
+  password: 'joinmarket',
+}
+
+const LINK_JM_REGTEST_JOINMARKET2 = 'http://localhost:29080'
+const LINK_JM_REGTEST_JOINMARKET2_AUTH = DEFAULT_BASIC_AUTH
+const LINK_JM_REGTEST_JOINMARKET3 = 'http://localhost:30080'
+const LINK_JM_REGTEST_EXPLORER = 'http://localhost:3002'
+const LINK_JM_REGTEST_EXPLORER_AUTH = DEFAULT_BASIC_AUTH
+const LINK_JM_REGTEST_RPC_TERMINAL = `${LINK_JM_REGTEST_EXPLORER}/rpc-terminal`
+
+export default function DevSetupPage() {
+  return (
+    <div>
+      <PageTitle title="Development setup" subtitle="" />
+      <div className="d-flex flex-column gap-3">
+        <div className="mb-4">
+          <h5>Test Wallet</h5>
+          <div className="ms-3 my-2">
+            Name: <span className="font-monospace">Satoshi</span>
+            <br />
+            Password: <span className="font-monospace">test</span>
+          </div>
+        </div>
+      </div>
+
+      <div className="d-flex flex-column gap-3">
+        <div className="mb-4">
+          <h5>Links</h5>
+          <div className="my-2">
+            <Link className="link-dark" to={routes.__errorExample}>
+              Error Example Page
+            </Link>
+          </div>
+        </div>
+      </div>
+
+      <div className="mb-4">
+        <h5>Jam Instances</h5>
+        <div>
+          <div className="d-flex align-items-center">
+            <Sprite symbol="logo" width="24" height="24" className="me-2" />
+            <a href={LINK_JM_REGTEST_JOINMARKET2} target="_blank" rel="noopener noreferrer" className="link-dark">
+              jm_regtest_joinmarket2 ({LINK_JM_REGTEST_JOINMARKET2})
+            </a>
+            <span className="badge rounded-pill bg-primary ms-2">secondary</span>
+          </div>
+
+          <div className="ms-5 my-2">
+            Basic Authentication
+            <br />
+            <small>
+              User: <span className="font-monospace">{LINK_JM_REGTEST_JOINMARKET2_AUTH.user}</span>
+              <br />
+              Password: <span className="font-monospace">{LINK_JM_REGTEST_JOINMARKET2_AUTH.password}</span>
+            </small>
+          </div>
+          <div className="d-flex align-items-center">
+            <Sprite symbol="logo" width="24" height="24" className="me-2" />
+            <a href={LINK_JM_REGTEST_JOINMARKET3} target="_blank" rel="noopener noreferrer" className="link-dark">
+              jm_regtest_joinmarket3 ({LINK_JM_REGTEST_JOINMARKET3})
+            </a>
+            <span className="badge rounded-pill bg-success ms-2">tertiary</span>
+          </div>
+        </div>
+      </div>
+
+      <div className="mb-4">
+        <h5>Block Explorer</h5>
+        <div>
+          {' '}
+          <div className="d-flex align-items-center">
+            <Sprite symbol="block" width="24" height="24" className="me-2" />
+
+            <a href={LINK_JM_REGTEST_EXPLORER} target="_blank" rel="noopener noreferrer" className="link-dark">
+              jm_regtest_explorer ({LINK_JM_REGTEST_EXPLORER})
+            </a>
+          </div>
+          <div className="ms-5 my-2">
+            Basic Authentication
+            <br />
+            <small>
+              User: <span className="font-monospace">{LINK_JM_REGTEST_EXPLORER_AUTH.user}</span>
+              <br />
+              Password: <span className="font-monospace">{LINK_JM_REGTEST_EXPLORER_AUTH.password}</span>
+            </small>
+          </div>
+        </div>
+        <div>
+          <div className="d-flex align-items-center">
+            <Sprite symbol="console" width="24" height="24" className="me-2" />
+
+            <a href={LINK_JM_REGTEST_RPC_TERMINAL} target="_blank" rel="noopener noreferrer" className="link-dark">
+              Regtest RPC Terminal ({LINK_JM_REGTEST_RPC_TERMINAL})
+            </a>
+          </div>
+          <div className="ms-5 my-2">
+            Mine a block, e.g.:
+            <pre>generatetoaddress 1 bcrt1qrnz0thqslhxu86th069r9j6y7ldkgs2tzgf5wx</pre>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/src/components/ErrorPage.tsx b/src/components/ErrorPage.tsx
index 4e09c1d..5ad9159 100644
--- a/src/components/ErrorPage.tsx
+++ b/src/components/ErrorPage.tsx
@@ -3,14 +3,6 @@ import * as rb from 'react-bootstrap'
 import { useRouteError } from 'react-router-dom'
 import PageTitle from './PageTitle'
 import { t } from 'i18next'
-import { useEffect } from 'react'
-
-export function ErrorThrowingComponent() {
-  useEffect(() => {
-    throw new Error('This error is thrown on purpose. Only to be used for testing.')
-  }, [])
-  return <></>
-}
 
 interface ErrorViewProps {
   title: string
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index db77fcb..f54261c 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -8,8 +8,10 @@ import { useCurrentWallet } from '../context/WalletContext'
 import Sprite from './Sprite'
 import Cheatsheet from './Cheatsheet'
 import packageInfo from '../../package.json'
-import { isDevMode } from '../constants/debugFeatures'
+import { isDebugFeatureEnabled, isDevMode } from '../constants/debugFeatures'
 import { toSemVer } from '../utils'
+import { Link } from 'react-router-dom'
+import { routes } from '../constants/routes'
 
 const APP_DISPLAY_VERSION = (() => {
   const version = toSemVer(packageInfo.version)
@@ -52,7 +54,7 @@ export default function Footer() {
               <rb.Card.Title className="text-center mb-3">{t('footer.warning_alert_title')}</rb.Card.Title>
               <p>{t('footer.warning_alert_text')}</p>
               <p className="text-secondary">
-                JoinMarket: v{serviceInfo?.server?.version?.raw || 'unknown'}
+                JoinMarket: v{serviceInfo?.server?.version?.raw || '_unknown'}
                 <br />
                 Jam: v{APP_DISPLAY_VERSION}
               </p>
@@ -68,13 +70,13 @@ export default function Footer() {
 
       <rb.Nav as="footer" className="border-top py-2">
         <rb.Container fluid="xl" className="d-flex justify-content-center py-2 px-4">
-          <div className="d-none d-md-flex flex-1 order-0 justify-content-start align-items-center">
-            <div className="warning-hint text-start text-secondary">
+          <div className="flex-1 order-0 justify-content-start align-items-center">
+            <div className="text-small text-start text-secondary">
               <Trans i18nKey="footer.warning">
                 This is pre-alpha software.
                 <rb.Button
                   variant="link"
-                  className="warning-hint text-start border-0 p-0 text-secondary"
+                  className="text-small text-start border-0 p-0 text-secondary"
                   onClick={() => setShowBetaWarning(true)}
                 >
                   Read this before using.
@@ -102,7 +104,14 @@ export default function Footer() {
             )}
           </div>
           <div className="d-flex flex-1 order-2 justify-content-end align-items-center gap-1">
-            <div className="warning-hint text-start text-secondary d-none d-md-block pe-1">
+            {isDebugFeatureEnabled('devSetupPage') && (
+              <div className="d-none d-md-block text-small text-start mx-1">
+                <Link className="text-warning" to={routes.__devSetup}>
+                  Dev Setup
+                </Link>
+              </div>
+            )}
+            <div className="text-small text-start text-secondary mx-1">
               <a
                 href="https://github.com/joinmarket-webui/jam/tags"
                 target="_blank"
@@ -112,7 +121,7 @@ export default function Footer() {
                 v{APP_DISPLAY_VERSION}
               </a>
             </div>
-            <div className="d-flex gap-2 pe-2">
+            <div className="d-flex gap-2 me-2">
               <a
                 href="https://github.com/joinmarket-webui/jam"
                 target="_blank"
@@ -130,7 +139,7 @@ export default function Footer() {
                 <Sprite symbol="telegram" width="18px" height="18px" />
               </a>
             </div>
-            <div className="d-flex text-secondary">|</div>
+            <div className="text-secondary">|</div>
             <div className="d-flex">
               <rb.OverlayTrigger
                 delay={{ hide: 300, show: 200 }}
diff --git a/src/components/LogOverlay.module.css b/src/components/LogOverlay.module.css
index c3b7843..99e66bd 100644
--- a/src/components/LogOverlay.module.css
+++ b/src/components/LogOverlay.module.css
@@ -1,5 +1,5 @@
 .logContentPlaceholder {
-  height: 1rem;
+  height: 2.625rem;
   margin: 1px 0;
 }
 
diff --git a/src/components/LogOverlay.tsx b/src/components/LogOverlay.tsx
index 29e30ef..7921d0b 100644
--- a/src/components/LogOverlay.tsx
+++ b/src/components/LogOverlay.tsx
@@ -7,6 +7,7 @@ import { useSettings } from '../context/SettingsContext'
 import { CurrentWallet } from '../context/WalletContext'
 import Sprite from './Sprite'
 import styles from './LogOverlay.module.css'
+import { isDevMode } from '../constants/debugFeatures'
 
 const JMWALLETD_LOG_FILE_NAME = 'jmwalletd_stdout.log'
 
@@ -74,7 +75,7 @@ export function LogOverlay({ currentWallet, show, onHide }: LogOverlayProps) {
   const [alert, setAlert] = useState<SimpleAlert>()
   const [isInitialized, setIsInitialized] = useState(false)
   const [isLoading, setIsLoading] = useState(true)
-  const [content, setContent] = useState<string | null>(null)
+  const [content, setContent] = useState<string>()
 
   const refresh = useCallback(
     (signal: AbortSignal) => {
@@ -85,9 +86,14 @@ export function LogOverlay({ currentWallet, show, onHide }: LogOverlayProps) {
           setAlert(undefined)
           setContent(data)
         })
-        .catch((err) => {
+        .catch((e) => {
           if (signal.aborted) return
-          setAlert({ variant: 'danger', message: t('logs.error_loading_logs_failed') })
+          setAlert({
+            variant: 'danger',
+            message: t('logs.error_loading_logs_failed', {
+              reason: e.message || t('global.errors.reason_unknown'),
+            }),
+          })
         })
         .finally(() => {
           if (signal.aborted) return
@@ -138,7 +144,7 @@ export function LogOverlay({ currentWallet, show, onHide }: LogOverlayProps) {
       <rb.Offcanvas.Body>
         <rb.Container fluid="lg" className="py-3">
           {!isInitialized && isLoading ? (
-            Array(12)
+            Array(5)
               .fill('')
               .map((_, index) => {
                 return (
@@ -149,6 +155,13 @@ export function LogOverlay({ currentWallet, show, onHide }: LogOverlayProps) {
               })
           ) : (
             <>
+              {alert && !content && isDevMode() && (
+                <div className="my-4">
+                  <span className="badge rounded-pill bg-warning me-2">dev</span>
+                  In order to test the log file feature, start the application with
+                  <code className="mx-2">npm run dev:start:secondary</code>.
+                </div>
+              )}
               {alert && <rb.Alert variant={alert.variant}>{alert.message}</rb.Alert>}
               {content && (
                 <rb.Row>
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 5df9312..2fb27e4 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -299,6 +299,11 @@ export default function Navbar() {
                     <span>{t('navbar.menu')}</span>
                   </rb.Navbar.Toggle>
                 </div>
+                {isDebugFeatureEnabled('fastThemeToggle') && (
+                  <rb.Nav.Item className="d-none d-md-flex align-items-center pe-2">
+                    <FastThemeToggle />
+                  </rb.Nav.Item>
+                )}
                 <rb.Navbar.Offcanvas className={`navbar-offcanvas navbar-${settings.theme}`} placement="end">
                   <rb.Offcanvas.Header>
                     <rb.Offcanvas.Title>{t('navbar.title')}</rb.Offcanvas.Title>
@@ -314,6 +319,15 @@ export default function Navbar() {
                           {t('navbar.button_create_wallet')}
                         </Link>
                       </rb.Nav.Item>
+                      <rb.Nav.Item>
+                        <Link
+                          to={routes.importWallet}
+                          onClick={() => isExpanded && setIsExpanded(false)}
+                          className="nav-link"
+                        >
+                          {t('navbar.button_import_wallet')}
+                        </Link>
+                      </rb.Nav.Item>
                     </rb.Nav>
                   </rb.Offcanvas.Body>
                 </rb.Navbar.Offcanvas>
diff --git a/src/components/Orderbook.module.css b/src/components/Orderbook.module.css
index 0e98de8..eecfd95 100644
--- a/src/components/Orderbook.module.css
+++ b/src/components/Orderbook.module.css
@@ -1,4 +1,4 @@
-.orderbook-line-placeholder {
+.orderbookContentPlaceholder {
   height: 2.625rem;
   margin: 1px 0;
 }
diff --git a/src/components/Orderbook.tsx b/src/components/Orderbook.tsx
index 5e192d1..6b837f3 100644
--- a/src/components/Orderbook.tsx
+++ b/src/components/Orderbook.tsx
@@ -461,7 +461,7 @@ export function OrderbookOverlay({ nickname, show, onHide }: OrderbookOverlayPro
               .map((_, index) => {
                 return (
                   <rb.Placeholder key={index} as="div" animation="wave">
-                    <rb.Placeholder xs={12} className={styles['orderbook-line-placeholder']} />
+                    <rb.Placeholder xs={12} className={styles.orderbookContentPlaceholder} />
                   </rb.Placeholder>
                 )
               })
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
index 0577b54..d9801e6 100644
--- a/src/components/Settings.tsx
+++ b/src/components/Settings.tsx
@@ -16,7 +16,7 @@ import languages from '../i18n/languages'
 import styles from './Settings.module.css'
 import SeedModal from './settings/SeedModal'
 import FeeConfigModal from './settings/FeeConfigModal'
-import { isDebugFeatureEnabled } from '../constants/debugFeatures'
+import { isDebugFeatureEnabled, isDevMode } from '../constants/debugFeatures'
 import { isFeatureEnabled } from '../constants/features'
 import { CurrentWallet } from '../context/WalletContext'
 
@@ -86,11 +86,14 @@ export default function Settings({ wallet, stopWallet }: SettingsProps) {
       .then((data) => data && data.features)
       .then((features) => {
         if (abortCtrl.signal.aborted) return
-        setShowLogsEnabled(features && features.logs === true)
+        const hasLogsFeatureOld = features && features.logs === true
+        const hasLogsFeature =
+          features && Array.isArray(features) && features.some((it) => it.name === 'logs' && it.enabled === true)
+        setShowLogsEnabled(hasLogsFeatureOld || hasLogsFeature)
       })
       .catch((_) => {
         if (abortCtrl.signal.aborted) return
-        setShowLogsEnabled(false)
+        setShowLogsEnabled(isDevMode())
       })
 
     return () => {
@@ -179,15 +182,6 @@ export default function Settings({ wallet, stopWallet }: SettingsProps) {
         </rb.Button>
         {showingFeeConfig && <FeeConfigModal show={showingFeeConfig} onHide={() => setShowingFeeConfig(false)} />}
 
-        {showLogsEnabled && (
-          <>
-            <rb.Button variant="outline-dark" className={styles['settings-btn']} onClick={() => setShowingLogs(true)}>
-              <Sprite symbol="console" width="24" height="24" />
-              {t('settings.show_logs')}
-            </rb.Button>
-            <LogOverlay currentWallet={wallet} show={showingLogs} onHide={() => setShowingLogs(false)} />
-          </>
-        )}
         <div className={styles['section-title']}>{t('settings.section_title_wallet')}</div>
         <div className={styles['settings-group-container']}>
           <rb.Button variant="outline-dark" className={styles['settings-btn']} onClick={(e) => setShowingSeed(true)}>
@@ -230,6 +224,16 @@ export default function Settings({ wallet, stopWallet }: SettingsProps) {
               </span>
             </Link>
           )}
+
+          {showLogsEnabled && (
+            <>
+              <rb.Button variant="outline-dark" className={styles['settings-btn']} onClick={() => setShowingLogs(true)}>
+                <Sprite symbol="console" width="24" height="24" />
+                {t('settings.show_logs')}
+              </rb.Button>
+              <LogOverlay currentWallet={wallet} show={showingLogs} onHide={() => setShowingLogs(false)} />
+            </>
+          )}
         </div>
 
         <div className={styles['section-title']}>{t('settings.section_title_community')}</div>
diff --git a/src/components/WalletCreationForm.tsx b/src/components/WalletCreationForm.tsx
index baf3441..db7ba6e 100644
--- a/src/components/WalletCreationForm.tsx
+++ b/src/components/WalletCreationForm.tsx
@@ -3,7 +3,7 @@ import * as rb from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { Formik, FormikErrors } from 'formik'
 import Sprite from './Sprite'
-import { sanitizeWalletName } from '../utils'
+import { JM_WALLET_FILE_EXTENSION, sanitizeWalletName } from '../utils'
 import styles from './WalletCreationForm.module.css'
 
 export interface CreateWalletFormValues {
@@ -20,6 +20,9 @@ const initialCreateWalletFormValues: CreateWalletFormValues = {
 
 export type WalletNameAndPassword = { name: string; password: string }
 
+const MAX_WALLET_NAME_LENGTH = 240 - JM_WALLET_FILE_EXTENSION.length
+const validateWalletName = (input: string) => input.length <= MAX_WALLET_NAME_LENGTH && /^[\w-]+$/.test(input)
+
 interface WalletCreationFormProps {
   initialValues?: CreateWalletFormValues
   submitButtonText: (isSubmitting: boolean) => React.ReactNode | string
@@ -38,7 +41,7 @@ const WalletCreationForm = ({
   const validate = useCallback(
     (values: CreateWalletFormValues) => {
       const errors = {} as FormikErrors<CreateWalletFormValues>
-      if (!values.walletName) {
+      if (!values.walletName || !validateWalletName(values.walletName)) {
         errors.walletName = t('create_wallet.feedback_invalid_wallet_name')
       }
       if (!values.password) {
@@ -75,6 +78,7 @@ const WalletCreationForm = ({
               isValid={touched.walletName && !errors.walletName}
               isInvalid={touched.walletName && !!errors.walletName}
               className={styles.input}
+              maxLength={MAX_WALLET_NAME_LENGTH}
             />
             <rb.Form.Control.Feedback>{t('create_wallet.feedback_valid')}</rb.Form.Control.Feedback>
             <rb.Form.Control.Feedback type="invalid">{errors.walletName}</rb.Form.Control.Feedback>
diff --git a/src/components/jar_details/DisplayBranch.tsx b/src/components/jar_details/DisplayBranch.tsx
index 65da72b..e84b4f9 100644
--- a/src/components/jar_details/DisplayBranch.tsx
+++ b/src/components/jar_details/DisplayBranch.tsx
@@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next'
 import Balance from '../Balance'
 import { useSettings } from '../../context/SettingsContext'
 import { Branch, BranchEntry } from '../../context/WalletContext'
-import styles from './DisplayBranch.module.css'
 import { CopyButton } from '../CopyButton'
 import Sprite from '../Sprite'
+import styles from './DisplayBranch.module.css'
 
 const toHdPathIndex = (hdPath: string) => {
   const indexOfLastSeparator = hdPath.lastIndexOf('/')
@@ -63,42 +63,19 @@ export function DisplayBranchHeader({ branch }: DisplayBranchProps) {
 }
 
 export function DisplayBranchBody({ branch }: DisplayBranchProps) {
-  const { branch: detailsString, entries } = branch
-  const xpub: string | undefined = detailsString.split('\t')[2]
   return (
     <rb.Container className="mb-2" fluid>
-      <rb.Row>
-        <rb.Col>
-          {xpub && (
-            <div className="p-3 mb-1">
-              <div>Extended public key</div>
-              <code className="text-secondary text-break">{xpub}</code>
-            </div>
-          )}
-        </rb.Col>
-      </rb.Row>
-      {entries.map((entry, index) => (
+      {branch.entries.map((entry) => (
         <DisplayBranchEntry
           key={entry.address}
           entry={entry}
-          className={`bg-transparent rounded-0 border-start-0 border-end-0 ${
-            index === 0 ? 'border-top-1 mt-2' : 'border-top-0'
-          }`}
+          className="bg-transparent rounded-0 border-start-0 border-end-0 border-bottom-0"
         />
       ))}
     </rb.Container>
   )
 }
 
-export default function DisplayBranch({ branch }: DisplayBranchProps) {
-  return (
-    <article>
-      <DisplayBranchHeader branch={branch} />
-      <DisplayBranchBody branch={branch} />
-    </article>
-  )
-}
-
 interface DisplayBranchEntryProps extends rb.CardProps {
   entry: BranchEntry
 }
diff --git a/src/components/jar_details/JarDetailsOverlay.tsx b/src/components/jar_details/JarDetailsOverlay.tsx
index b4d74ea..5f122e2 100644
--- a/src/components/jar_details/JarDetailsOverlay.tsx
+++ b/src/components/jar_details/JarDetailsOverlay.tsx
@@ -414,16 +414,18 @@ const JarDetailsOverlay = (props: JarDetailsOverlayProps) => {
                   </>
                 ) : (
                   <rb.Accordion flush className="p-3 p-lg-0">
-                    {jar.branches.map((branch, index) => (
-                      <rb.Accordion.Item className={styles.jarItem} key={branch.branch} eventKey={`${index}`}>
-                        <rb.Accordion.Header>
-                          <DisplayBranchHeader branch={branch} />
-                        </rb.Accordion.Header>
-                        <rb.Accordion.Body>
-                          <DisplayBranchBody branch={branch} />
-                        </rb.Accordion.Body>
-                      </rb.Accordion.Item>
-                    ))}
+                    {jar.branches
+                      .filter((it) => it.entries.length > 0)
+                      .map((branch, index) => (
+                        <rb.Accordion.Item className={styles.jarItem} key={branch.branch} eventKey={`${index}`}>
+                          <rb.Accordion.Header>
+                            <DisplayBranchHeader branch={branch} />
+                          </rb.Accordion.Header>
+                          <rb.Accordion.Body>
+                            <DisplayBranchBody branch={branch} />
+                          </rb.Accordion.Body>
+                        </rb.Accordion.Item>
+                      ))}
                   </rb.Accordion>
                 )}
               </div>
diff --git a/src/constants/debugFeatures.ts b/src/constants/debugFeatures.ts
index 97e3ab7..43f9cfd 100644
--- a/src/constants/debugFeatures.ts
+++ b/src/constants/debugFeatures.ts
@@ -3,6 +3,7 @@ interface DebugFeatures {
   allowCreatingExpiredFidelityBond: boolean
   skipWalletBackupConfirmation: boolean
   errorExamplePage: boolean
+  devSetupPage: boolean
   importDummyMnemonicPhrase: boolean
   rescanChainPage: boolean
   fastThemeToggle: boolean
@@ -15,6 +16,7 @@ const debugFeatures: DebugFeatures = {
   insecureScheduleTesting: devMode,
   skipWalletBackupConfirmation: devMode,
   errorExamplePage: devMode,
+  devSetupPage: devMode,
   importDummyMnemonicPhrase: devMode,
   rescanChainPage: devMode,
   fastThemeToggle: devMode,
diff --git a/src/constants/routes.ts b/src/constants/routes.ts
index 68a2020..62b78a9 100644
--- a/src/constants/routes.ts
+++ b/src/constants/routes.ts
@@ -11,6 +11,7 @@ export const routes = {
   importWallet: '/import-wallet',
   rescanChain: '/rescan',
   __errorExample: '/error-example',
+  __devSetup: '/dev-setup',
 }
 
 export type Route = keyof typeof routes
diff --git a/src/context/ServiceInfoContext.tsx b/src/context/ServiceInfoContext.tsx
index 219fbf7..87a97d3 100644
--- a/src/context/ServiceInfoContext.tsx
+++ b/src/context/ServiceInfoContext.tsx
@@ -208,7 +208,7 @@ const ServiceInfoProvider = ({ children }: PropsWithChildren<{}>) => {
         })
         .catch((err) => {
           if (!signal.aborted) {
-            const isUnauthorized = err instanceof Api.JmApiError && err.response.status === 401
+            const isUnauthorized = err instanceof Api.JmApiError && err.response?.status === 401
             if (isUnauthorized) {
               resetWalletAndClearSession()
             } else {
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json
index 4f50da2..b01bc72 100644
--- a/src/i18n/locales/en/translation.json
+++ b/src/i18n/locales/en/translation.json
@@ -37,6 +37,7 @@
     "title": "Jam",
     "text_rescan_in_progress": "Rescanning...",
     "button_create_wallet": "Create Wallet",
+    "button_import_wallet": "Import Wallet",
     "tab_send": "Send",
     "tab_receive": "Receive",
     "tab_earn": "Earn",
@@ -125,7 +126,7 @@
     "feedback_valid": "Looks good!",
     "label_wallet_name": "Wallet name",
     "placeholder_wallet_name": "Your Wallet...",
-    "feedback_invalid_wallet_name": "Please set a wallet name.",
+    "feedback_invalid_wallet_name": "Please choose a valid wallet name: Use only letters, numbers, underscores or hyphens.",
     "label_password": "Password to unlock the wallet",
     "placeholder_password": "Choose a secure password...",
     "feedback_invalid_password": "Please set a password.",
diff --git a/src/index.css b/src/index.css
index 6316f9a..ef2b624 100644
--- a/src/index.css
+++ b/src/index.css
@@ -573,10 +573,6 @@ h2 {
 }
 /* Alpha Warning */
 
-.warning-hint {
-  font-size: 0.8rem;
-}
-
 .warning-card-wrapper {
   position: fixed;
   height: 100%;
@@ -726,6 +722,7 @@ h2 {
 
 :root[data-theme='dark'] .link-dark {
   color: var(--bs-white) !important;
+  text-decoration-color: RGBA(var(--bs-white), var(--bs-link-underline-opacity, 1)) !important;
 }
 
 :root[data-theme='dark'] .toast {
diff --git a/src/utils.ts b/src/utils.ts
index 9c68b98..30590cb 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -13,7 +13,7 @@ const SATS_FORMATTER = new Intl.NumberFormat('en-US', {
 export const BTC: Unit = 'BTC'
 export const SATS: Unit = 'sats'
 
-const JM_WALLET_FILE_EXTENSION = '.jmdat'
+export const JM_WALLET_FILE_EXTENSION = '.jmdat'
 
 export const DUMMY_MNEMONIC_PHRASE: MnemonicPhrase =
   'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'.split(' ')


Test patch:
diff --git a/docker/regtest/docker-compose.yml b/docker/regtest/docker-compose.yml
index 3af8e43..e6c634e 100644
--- a/docker/regtest/docker-compose.yml
+++ b/docker/regtest/docker-compose.yml
@@ -175,6 +175,10 @@ services:
       BTCEXP_BITCOIND_PORT: 43782
       BTCEXP_BITCOIND_USER: regtest
       BTCEXP_BITCOIND_PASS: regtest
+      BTCEXP_BASIC_AUTH_PASSWORD: joinmarket
+      BTCEXP_PRIVACY_MODE: 'true'
+      BTCEXP_NO_RATES: 'true'
+      BTCEXP_RPC_ALLOWALL: 'true'
     ports:
       - "3002:3002"
     depends_on:
diff --git a/src/components/CreateWallet.test.tsx b/src/components/CreateWallet.test.tsx
index 6330dee..49c0eaf 100644
--- a/src/components/CreateWallet.test.tsx
+++ b/src/components/CreateWallet.test.tsx
@@ -19,7 +19,8 @@ jest.mock('../libs/JmWalletApi', () => ({
 const NOOP = () => {}
 
 describe('<CreateWallet />', () => {
-  const testWalletName = 'wallet'
+  const testWalletName = 'test_wallet21'
+  const invalidTestWalletName = 'invalid_wallet_name!'
   const testWalletPassword = 'correct horse battery staple'
 
   const setup = ({
@@ -56,12 +57,14 @@ describe('<CreateWallet />', () => {
   it('should show validation messages to user if form is invalid', async () => {
     act(() => setup({}))
 
+    expect(await screen.queryByText('create_wallet.feedback_invalid_wallet_name')).not.toBeInTheDocument()
+    expect(await screen.queryByText('create_wallet.feedback_invalid_password')).not.toBeInTheDocument()
+    expect(await screen.queryByText('create_wallet.feedback_invalid_password_confirm')).not.toBeInTheDocument()
     expect(await screen.findByText('create_wallet.button_create')).toBeVisible()
 
     act(() => {
       // click on the "create" button without filling the form
-      const createWalletButton = screen.getByText('create_wallet.button_create')
-      user.click(createWalletButton)
+      user.click(screen.getByText('create_wallet.button_create'))
     })
 
     expect(await screen.findByText('create_wallet.feedback_invalid_wallet_name')).toBeVisible()
@@ -69,11 +72,32 @@ describe('<CreateWallet />', () => {
     expect(await screen.findByText('create_wallet.feedback_invalid_password_confirm')).toBeVisible()
   })
 
+  it('should not submit form if wallet name contains invalid characters', async () => {
+    act(() => setup({}))
+
+    expect(await screen.queryByText('create_wallet.feedback_invalid_wallet_name')).not.toBeInTheDocument()
+    expect(await screen.queryByText('create_wallet.feedback_invalid_password_confirm')).not.toBeInTheDocument()
+
+    act(() => {
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name'), invalidTestWalletName)
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_password'), testWalletPassword)
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_password_confirm'), testWalletPassword)
+    })
+
+    act(() => user.click(screen.getByText('create_wallet.button_create')))
+
+    expect(await screen.findByText('create_wallet.button_create')).toBeVisible()
+    expect(await screen.findByText('create_wallet.feedback_invalid_wallet_name')).toBeVisible()
+    expect(await screen.queryByText('create_wallet.feedback_invalid_password_confirm')).not.toBeInTheDocument()
+  })
+
   it('should not submit form if passwords do not match', async () => {
     act(() => setup({}))
 
     expect(await screen.findByPlaceholderText('create_wallet.placeholder_password')).toBeVisible()
     expect(await screen.findByPlaceholderText('create_wallet.placeholder_password_confirm')).toBeVisible()
+    expect(await screen.queryByText('create_wallet.feedback_invalid_wallet_name')).not.toBeInTheDocument()
+    expect(await screen.queryByText('create_wallet.feedback_invalid_password_confirm')).not.toBeInTheDocument()
     expect(await screen.findByText('create_wallet.button_create')).toBeVisible()
 
     act(() => {
@@ -82,12 +106,10 @@ describe('<CreateWallet />', () => {
       user.type(screen.getByPlaceholderText('create_wallet.placeholder_password_confirm'), 'a_mismatching_input')
     })
 
-    act(() => {
-      const createWalletButton = screen.getByText('create_wallet.button_create')
-      user.click(createWalletButton)
-    })
+    act(() => user.click(screen.getByText('create_wallet.button_create')))
 
     expect(await screen.findByText('create_wallet.button_create')).toBeVisible()
+    expect(await screen.queryByText('create_wallet.feedback_invalid_wallet_name')).not.toBeInTheDocument()
     expect(await screen.findByText('create_wallet.feedback_invalid_password_confirm')).toBeVisible()
   })
 
@@ -114,8 +136,7 @@ describe('<CreateWallet />', () => {
     })
 
     await act(async () => {
-      const createWalletButton = screen.getByText('create_wallet.button_create')
-      user.click(createWalletButton)
+      user.click(screen.getByText('create_wallet.button_create'))
 
       await waitFor(() => screen.findByText(/create_wallet.button_creating/))
     })