Repository: joinmarket-webui/joinmarket-webui
Pull request #197 (2022-03-29 13:05:11)

dev: enable npm config `engine-strict`
Add `engine-strict` to `.npmrc` to enforce minimum npm and node version specified in section `engines` of `package.json`.
Thereby also updating the minimum npm version from `v7.0.0` to `v8.0.0` and node from `v16.0.0` to `v16.11.0`. This is to prevent annoying diffs to `package-lock.json` when installing new packages.

If you try to execute `npm install` with an unsupported version, you will now get the following message indicating you to update your setup, e.g. with `npm install npm -g` and `nvm install --lts` (in case you are using `nvm`):
```sh
[user@home joinmarket-webui]$ npm i
npm i
npm ERR! code EBADENGINE
npm ERR! engine Unsupported engine
npm ERR! engine Not compatible with your version of node/npm: jam@0.0.5
npm ERR! notsup Not compatible with your version of node/npm: jam@0.0.5
npm ERR! notsup Required: {"node":">=16.11.0","npm":">=8.0.0"}
npm ERR! notsup Actual:   {"npm":"1.2.3","node":"v4.5.6"}
```

Current settings are:
```json
  "engines": {
    "node": ">=16.11.0",
    "npm": ">=8.0.0"
  },
```

node `v16.11.0` has been specified, as it is an LTS release and the first version including npm `>=v8.0.0`. Please feel free to suggest other minimum version as you see fit. The only goal that must be achieved is preventing `package-lock.json` conflicts.

secondl1ght: I used nvm to switch to node version 15.0.0 and then ran `npm install`, thought I would post the output here so you can verify this is what you expect to see:

```
mk@pop-os:~/Desktop/CODE/joinmarket-webui$ npm install
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@eslint/eslintrc@1.0.5',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@typescript-eslint/eslint-plugin@5.7.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@typescript-eslint/experimental-utils@5.7.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@typescript-eslint/parser@5.7.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@typescript-eslint/scope-manager@5.7.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@typescript-eslint/types@5.7.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@typescript-eslint/typescript-estree@5.7.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@typescript-eslint/visitor-keys@5.7.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'cli-truncate@3.1.0',
npm WARN EBADENGINE   required: { node: '^12.20.0 || ^14.13.1 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'eslint@8.4.1',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'eslint-plugin-jest@25.3.0',
npm WARN EBADENGINE   required: { node: '^12.13.0 || ^14.15.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'eslint-plugin-testing-library@5.0.1',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0', npm: '>=6' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'eslint-scope@7.1.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'eslint-visitor-keys@3.1.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'espree@9.2.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'lint-staged@12.3.7',
npm WARN EBADENGINE   required: { node: '^12.20.0 || ^14.13.1 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'nanoid@3.2.0',
npm WARN EBADENGINE   required: { node: '^10 || ^12 || ^13.7 || ^14 || >=15.0.1' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-color-functional-notation@4.0.2',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-dir-pseudo-class@6.0.1',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-double-position-gradients@3.0.3',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-env-function@4.0.3',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-focus-visible@6.0.2',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-focus-within@5.0.2',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-gap-properties@3.0.1',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-image-set-function@4.0.3',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-lab-function@4.0.2',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-logical@5.0.1',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-nesting@10.0.3',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-overflow-shorthand@3.0.1',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-place@7.0.2',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'postcss-pseudo-class-any-link@7.0.1',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || >=16' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'jest-watch-typeahead@1.0.0',
npm WARN EBADENGINE   required: { node: '^12.22.0 || ^14.17.0 || >=16.0.0' },
npm WARN EBADENGINE   current: { node: 'v15.0.0', npm: '7.0.2' }
npm WARN EBADENGINE }

> jam@0.0.5 postinstall
> husky install

husky - Git hooks installed

added 1 package, and audited 1550 packages in 3s

190 packages are looking for funding
  run `npm fund` for details

6 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
```
theborakompanioni: > I used nvm to switch to node version 15.0.0 and then ran `npm install`, [...]

Hey! Thanks for verifying!
Hmm.. that does not seem quite right.. with `engineStrict` enabled, you should be getting an error..
Would you mind double-checking that you are on the correct branch?
secondl1ght: Oops sorry, when I tested this it was late and you are correct - I was not on the right branch 😅 

Here is the actual ouput:
```
mk@pop-os:~/Desktop/CODE/joinmarket-webui$ npm install
npm ERR! code EBADENGINE
npm ERR! engine Unsupported engine
npm ERR! engine Not compatible with your version of node/npm: @eslint/eslintrc@1.0.5
npm ERR! notsup Not compatible with your version of node/npm: @eslint/eslintrc@1.0.5
npm ERR! notsup Required: {"node":"^12.22.0 || ^14.17.0 || >=16.0.0"}
npm ERR! notsup Actual:   {"npm":"7.0.2","node":"v15.0.0"}

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/mk/.npm/_logs/2022-04-01T13_53_01_832Z-debug.log
```

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 09130ba..e3c7597 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,14 +14,14 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        node-version: [16]
+        node-version: ['16.11.0']
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
       # Setup Node
       - name: Setup (Node.js ${{ matrix.node-version }})
-        uses: actions/setup-node@v1
+        uses: actions/setup-node@v3
         with:
           node-version: ${{ matrix.node-version }}
       # Install
diff --git a/.npmrc b/.npmrc
index b4d0cc1..fd341ed 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,1 +1,2 @@
 git-tag-version = false
+engine-strict = true
diff --git a/package.json b/package.json
index faefb03..333250d 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,8 @@
   "repository": "git@github.com:joinmarket-webui/joinmarket-webui.git",
   "license": "MIT",
   "engines": {
-    "node": ">=16.0.0",
-    "npm": ">=7.0.0"
+    "node": ">=16.11.0",
+    "npm": ">=8.0.0"
   },
   "homepage": ".",
   "devDependencies": {
diff --git a/src/components/App.jsx b/src/components/App.jsx
index 8420813..f659cff 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -31,6 +31,8 @@ export default function App() {
   const settings = useSettings()
   const websocketState = useWebsocketState()
 
+  const devMode = process.env.NODE_ENV === 'development'
+
   const startWallet = useCallback(
     (name, token) => {
       setSession({ name, token })
@@ -89,7 +91,7 @@ export default function App() {
           <Routes>
             <Route element={<Layout />}>
               <Route exact path="/" element={<Wallets startWallet={startWallet} stopWallet={stopWallet} />} />
-              <Route path="create-wallet" element={<CreateWallet startWallet={startWallet} />} />
+              <Route path="create-wallet" element={<CreateWallet startWallet={startWallet} devMode={devMode} />} />
               {currentWallet && (
                 <>
                   <Route path="send" element={<Send />} />
diff --git a/src/components/CreateWallet.css b/src/components/CreateWallet.css
new file mode 100644
index 0000000..8102e4b
--- /dev/null
+++ b/src/components/CreateWallet.css
@@ -0,0 +1,14 @@
+.create-wallet form input {
+  height: 3.5rem;
+  width: 100%;
+}
+
+.create-wallet button {
+  height: 3rem;
+  width: 100%;
+}
+
+.seedword-index-backup {
+  width: 5ch;
+  justify-content: right;
+}
diff --git a/src/components/CreateWallet.jsx b/src/components/CreateWallet.jsx
index 8ed0727..3c58f37 100644
--- a/src/components/CreateWallet.jsx
+++ b/src/components/CreateWallet.jsx
@@ -8,6 +8,35 @@ import ToggleSwitch from './ToggleSwitch'
 import { serialize, walletDisplayName } from '../utils'
 import { useServiceInfo } from '../context/ServiceInfoContext'
 import * as Api from '../libs/JmWalletApi'
+import './CreateWallet.css'
+
+const PreventLeavingPageByMistake = () => {
+  // prompt users before refreshing or closing the page when this component is present.
+  // Firefox will show: "This page is asking you to confirm that you want to leave [...]"
+  // Chrome: "Leave site? Changes you made may not be saved."
+  useEffect(() => {
+    const abortCtrl = new AbortController()
+
+    window.addEventListener(
+      'beforeunload',
+      (event) => {
+        // cancel the event as stated by the standard.
+        event.preventDefault()
+
+        // Chrome requires returnValue to be set.
+        event.returnValue = ''
+
+        // return something to trigger a dialog
+        return ''
+      },
+      { signal: abortCtrl.signal }
+    )
+
+    return () => abortCtrl.abort()
+  }, [])
+
+  return <></>
+}
 
 const WalletCreationForm = ({ createWallet, isCreating }) => {
   const { t } = useTranslation()
@@ -28,6 +57,7 @@ const WalletCreationForm = ({ createWallet, isCreating }) => {
 
   return (
     <>
+      {isCreating && <PreventLeavingPageByMistake />}
       <rb.Form onSubmit={onSubmit} validated={validated} noValidate>
         <rb.Form.Group className="mb-4" controlId="walletName">
           <rb.Form.Label>{t('create_wallet.label_wallet_name')}</rb.Form.Label>
@@ -102,11 +132,13 @@ const SeedWordInput = ({ number, targetWord, isValid, setIsValid }) => {
   )
 }
 
-const BackupConfirmation = ({ createdWallet, walletConfirmed, parentStepSetter }) => {
+const BackupConfirmation = ({ createdWallet, walletConfirmed, parentStepSetter, devMode }) => {
+  const seedphrase = createdWallet.seedphrase.split(' ')
+
   const { t } = useTranslation()
   const [seedBackup, setSeedBackup] = useState(false)
-  let seedphrase = createdWallet.seedphrase.split(' ')
   const [seedWordConfirmations, setSeedWordConfirmations] = useState(new Array(seedphrase.length).fill(false))
+  const [showSkipButton] = useState(devMode)
 
   useEffect(() => {
     setSeedBackup(seedWordConfirmations.every((wordConfirmed) => wordConfirmed))
@@ -152,9 +184,13 @@ const BackupConfirmation = ({ createdWallet, walletConfirmed, parentStepSetter }
       </rb.Form>
       {seedBackup && <div className="text-center text-success">{t('create_wallet.feedback_seed_confirmed')}</div>}
 
-      <div className="d-flex mt-4 mb-4 gap-3">
+      <rb.Button variant="dark" onClick={() => walletConfirmed()} disabled={!seedBackup}>
+        {t('create_wallet.confirmation_button_fund_wallet')}
+      </rb.Button>
+
+      <div className="d-flex mt-4 mb-4 gap-4">
         <rb.Button
-          variant="dark"
+          variant="outline-dark"
           disabled={seedBackup}
           onClick={() => {
             parentStepSetter()
@@ -163,19 +199,17 @@ const BackupConfirmation = ({ createdWallet, walletConfirmed, parentStepSetter }
           {t('create_wallet.back_button')}
         </rb.Button>
 
-        <rb.Button variant="dark" onClick={() => walletConfirmed()} disabled={!seedBackup}>
-          {t('create_wallet.confirmation_button_fund_wallet')}
-        </rb.Button>
+        {showSkipButton && (
+          <rb.Button variant="outline-dark" onClick={() => walletConfirmed()} disabled={seedBackup}>
+            {t('create_wallet.skip_button')}
+          </rb.Button>
+        )}
       </div>
-
-      <rb.Button variant="outline-dark" onClick={() => walletConfirmed()} disabled={seedBackup}>
-        {t('create_wallet.skip_button')}
-      </rb.Button>
     </div>
   )
 }
 
-const WalletCreationConfirmation = ({ createdWallet, walletConfirmed }) => {
+const WalletCreationConfirmation = ({ createdWallet, walletConfirmed, devMode }) => {
   const { t } = useTranslation()
   const [userConfirmed, setUserConfirmed] = useState(false)
   const [revealSensitiveInfo, setRevealSensitiveInfo] = useState(false)
@@ -189,54 +223,56 @@ const WalletCreationConfirmation = ({ createdWallet, walletConfirmed }) => {
     setStep(0)
   }
 
-  if (step === 0) {
-    return (
-      <div>
-        <div className="mb-4">
-          <div>{t('create_wallet.confirmation_label_wallet_name')}</div>
-          <div className="fs-4">{walletDisplayName(createdWallet.name)}</div>
-        </div>
-        <div className="mb-4">
-          <Seedphrase seedphrase={createdWallet.seedphrase} isBlurred={!revealSensitiveInfo} />
-        </div>
-        <div className="mb-4">
-          <div>{t('create_wallet.confirmation_label_password')}</div>
-          <div className={`fs-4${revealSensitiveInfo ? '' : ' blurred-text'}`}>
-            {!revealSensitiveInfo ? 'randomrandom' : createdWallet.password}
+  return (
+    <>
+      <PreventLeavingPageByMistake />
+      {step === 0 ? (
+        <div>
+          <div className="mb-4">
+            <div>{t('create_wallet.confirmation_label_wallet_name')}</div>
+            <div className="fs-4">{walletDisplayName(createdWallet.name)}</div>
           </div>
+          <div className="mb-4">
+            <Seedphrase seedphrase={createdWallet.seedphrase} isBlurred={!revealSensitiveInfo} />
+          </div>
+          <div className="mb-4">
+            <div>{t('create_wallet.confirmation_label_password')}</div>
+            <div className={`fs-4${revealSensitiveInfo ? '' : ' blurred-text'}`}>
+              {!revealSensitiveInfo ? 'randomrandom' : createdWallet.password}
+            </div>
+          </div>
+          <div className="mb-2">
+            <ToggleSwitch
+              label={t('create_wallet.confirmation_toggle_reveal_info')}
+              onToggle={(isToggled) => {
+                setRevealSensitiveInfo(isToggled)
+                setSensitiveInfoWasRevealed(true)
+              }}
+            />
+          </div>
+          <div className="mb-4">
+            <ToggleSwitch
+              label={t('create_wallet.confirmation_toggle_info_written_down')}
+              onToggle={(isToggled) => setUserConfirmed(isToggled)}
+            />
+          </div>
+          <rb.Button variant="dark" disabled={!sensitiveInfoWasRevealed || !userConfirmed} onClick={() => setStep(1)}>
+            {t('create_wallet.next_button')}
+          </rb.Button>
         </div>
-        <div className="mb-2">
-          <ToggleSwitch
-            label={t('create_wallet.confirmation_toggle_reveal_info')}
-            onToggle={(isToggled) => {
-              setRevealSensitiveInfo(isToggled)
-              setSensitiveInfoWasRevealed(true)
-            }}
-          />
-        </div>
-        <div className="mb-4">
-          <ToggleSwitch
-            label={t('create_wallet.confirmation_toggle_info_written_down')}
-            onToggle={(isToggled) => setUserConfirmed(isToggled)}
-          />
-        </div>
-        <rb.Button variant="dark" disabled={!sensitiveInfoWasRevealed || !userConfirmed} onClick={() => setStep(1)}>
-          {t('create_wallet.next_button')}
-        </rb.Button>
-      </div>
-    )
-  } else {
-    return (
-      <BackupConfirmation
-        parentStepSetter={childStepSetter}
-        createdWallet={createdWallet}
-        walletConfirmed={walletConfirmed}
-      />
-    )
-  }
+      ) : (
+        <BackupConfirmation
+          parentStepSetter={childStepSetter}
+          createdWallet={createdWallet}
+          walletConfirmed={walletConfirmed}
+          devMode={devMode}
+        />
+      )}
+    </>
+  )
 }
 
-export default function CreateWallet({ startWallet }) {
+export default function CreateWallet({ startWallet, devMode = false }) {
   const { t } = useTranslation()
   const serviceInfo = useServiceInfo()
   const navigate = useNavigate()
@@ -291,7 +327,9 @@ export default function CreateWallet({ startWallet }) {
       )}
       {alert && <rb.Alert variant={alert.variant}>{alert.message}</rb.Alert>}
       {canCreate && <WalletCreationForm createWallet={createWallet} isCreating={isCreating} />}
-      {isCreated && <WalletCreationConfirmation createdWallet={createdWallet} walletConfirmed={walletConfirmed} />}
+      {isCreated && (
+        <WalletCreationConfirmation createdWallet={createdWallet} walletConfirmed={walletConfirmed} devMode={devMode} />
+      )}
       {!canCreate && !isCreated && (
         <rb.Alert variant="warning">
           <Trans i18nKey="create_wallet.alert_other_wallet_unlocked">
diff --git a/src/components/Seedphrase.css b/src/components/Seedphrase.css
new file mode 100644
index 0000000..8ad770d
--- /dev/null
+++ b/src/components/Seedphrase.css
@@ -0,0 +1,16 @@
+.seedphrase {
+  gap: 0.3rem;
+}
+
+.seedword-index {
+  width: 2ch;
+}
+
+.seedphrase > div {
+  background-color: rgba(244, 244, 244, 1);
+  width: 9rem;
+}
+
+:root[data-theme='dark'] .seedphrase > div {
+  background-color: var(--bs-gray-800);
+}
diff --git a/src/components/Seedphrase.jsx b/src/components/Seedphrase.jsx
index a342b83..c372aad 100644
--- a/src/components/Seedphrase.jsx
+++ b/src/components/Seedphrase.jsx
@@ -1,4 +1,5 @@
 import React from 'react'
+import './Seedphrase.css'
 
 export default function Seedphrase({ seedphrase, isBlurred = true }) {
   return (
diff --git a/src/index.css b/src/index.css
index db46bb0..d5bce91 100644
--- a/src/index.css
+++ b/src/index.css
@@ -357,24 +357,7 @@ main {
   }
 }
 
-/* Wallet Creation Styles */
-
-.seedphrase {
-  gap: 0.3rem;
-}
-
-.seedphrase > div {
-  background-color: rgba(244, 244, 244, 1);
-  width: 8rem;
-}
-
-.seedword-index {
-  width: 2ch;
-}
-
-.seedword-index-backup {
-  width: 5ch;
-}
+/* Blurred Text Styles */
 
 .blurred-text {
   color: transparent;
@@ -386,20 +369,6 @@ main {
   text-shadow: 0 0 15px var(--bs-gray-400);
 }
 
-:root[data-theme='dark'] .seedphrase > div {
-  background-color: var(--bs-gray-800);
-}
-
-.create-wallet form input {
-  height: 3.5rem;
-  width: 100%;
-}
-
-.create-wallet button {
-  height: 3rem;
-  width: 100%;
-}
-
 /* Send, Receive, Earn Styles */
 
 .earn form input:not([type='checkbox']) {


Test patch:
diff --git a/src/components/CreateWallet.test.jsx b/src/components/CreateWallet.test.jsx
new file mode 100644
index 0000000..01c358c
--- /dev/null
+++ b/src/components/CreateWallet.test.jsx
@@ -0,0 +1,162 @@
+import React from 'react'
+import user from '@testing-library/user-event'
+import { render, screen, waitFor, waitForElementToBeRemoved } from '../testUtils'
+import { act } from 'react-dom/test-utils'
+
+import * as apiMock from '../libs/JmWalletApi'
+
+import CreateWallet from './CreateWallet'
+
+jest.mock('../libs/JmWalletApi', () => ({
+  ...jest.requireActual('../libs/JmWalletApi'),
+  getSession: jest.fn(),
+  postWalletCreate: jest.fn(),
+}))
+
+const NOOP = () => {}
+
+describe('<CreateWallet />', () => {
+  const setup = (props) => {
+    const startWallet = props?.startWallet || NOOP
+    const devMode = props?.devMode || false
+    render(<CreateWallet startWallet={startWallet} devMode={devMode} />)
+  }
+
+  beforeEach(() => {
+    const neverResolvingPromise = new Promise(() => {})
+    apiMock.getSession.mockReturnValue(neverResolvingPromise)
+  })
+
+  it('should render without errors', () => {
+    act(setup)
+
+    expect(screen.getByText('create_wallet.title')).toBeInTheDocument()
+    expect(screen.getByLabelText('create_wallet.label_wallet_name')).toBeInTheDocument()
+    expect(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name')).toBeInTheDocument()
+    expect(screen.getByLabelText('create_wallet.label_password')).toBeInTheDocument()
+    expect(screen.getByPlaceholderText('create_wallet.placeholder_password')).toBeInTheDocument()
+    expect(screen.getByText('create_wallet.button_create')).toBeInTheDocument()
+  })
+
+  it('should show validation messages to user if form is invalid', () => {
+    act(setup)
+
+    expect(screen.getByText('create_wallet.button_create')).toBeInTheDocument()
+
+    act(() => {
+      // click on the "create" button without filling the form
+      const createWalletButton = screen.getByText('create_wallet.button_create')
+      user.click(createWalletButton)
+    })
+
+    expect(screen.getByText('create_wallet.feedback_invalid_wallet_name')).toBeVisible()
+    expect(screen.getByText('create_wallet.feedback_invalid_password')).toBeVisible()
+  })
+
+  it('should advance to WalletCreationConfirmation after wallet is created', async () => {
+    const walletName = 'wallet'
+
+    apiMock.postWalletCreate.mockResolvedValueOnce({
+      ok: true,
+      json: () =>
+        Promise.resolve({
+          walletname: `${walletName}.jmdat`,
+          token: 'ANY_TOKEN',
+          seedphrase: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
+        }),
+    })
+
+    act(setup)
+
+    expect(screen.getByText('create_wallet.button_create')).toBeInTheDocument()
+    expect(screen.queryByText('create_wallet.title_wallet_created')).not.toBeInTheDocument()
+
+    await act(async () => {
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name'), walletName)
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_password'), 'password')
+      const createWalletButton = screen.getByText('create_wallet.button_create')
+      user.click(createWalletButton)
+
+      await waitFor(() => screen.findByText(/create_wallet.button_creating/))
+    })
+
+    expect(screen.queryByText('create_wallet.button_create')).not.toBeInTheDocument()
+    expect(screen.getByText('create_wallet.title_wallet_created')).toBeInTheDocument()
+  })
+
+  it('should verify that "skip" button is NOT visible when not in development mode', async () => {
+    const walletName = 'wallet'
+
+    apiMock.postWalletCreate.mockResolvedValueOnce({
+      ok: true,
+      json: () =>
+        Promise.resolve({
+          walletname: `${walletName}.jmdat`,
+          token: 'ANY_TOKEN',
+          seedphrase: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
+        }),
+    })
+
+    act(() => setup({ devMode: false }))
+
+    await act(async () => {
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name'), walletName)
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_password'), 'password')
+      const createWalletButton = screen.getByText('create_wallet.button_create')
+      user.click(createWalletButton)
+
+      await waitFor(() => screen.findByText(/create_wallet.button_creating/))
+
+      const revealToggle = screen.getByText('create_wallet.confirmation_toggle_reveal_info')
+      user.click(revealToggle)
+
+      const confirmToggle = screen.getByText('create_wallet.confirmation_toggle_info_written_down')
+      user.click(confirmToggle)
+
+      const nextButton = screen.getByText('create_wallet.next_button')
+      user.click(nextButton)
+    })
+
+    expect(screen.queryByText('create_wallet.skip_button')).not.toBeInTheDocument()
+    expect(screen.getByText('create_wallet.back_button')).toBeInTheDocument()
+    expect(screen.getByText('create_wallet.confirmation_button_fund_wallet')).toBeDisabled()
+  })
+
+  it('should verify that "skip" button IS visible in development mode', async () => {
+    const walletName = 'wallet'
+
+    apiMock.postWalletCreate.mockResolvedValueOnce({
+      ok: true,
+      json: () =>
+        Promise.resolve({
+          walletname: `${walletName}.jmdat`,
+          token: 'ANY_TOKEN',
+          seedphrase: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
+        }),
+    })
+
+    act(() => setup({ devMode: true }))
+
+    await act(async () => {
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name'), walletName)
+      user.type(screen.getByPlaceholderText('create_wallet.placeholder_password'), 'password')
+      const createWalletButton = screen.getByText('create_wallet.button_create')
+      user.click(createWalletButton)
+
+      await waitFor(() => screen.findByText(/create_wallet.button_creating/))
+
+      const revealToggle = screen.getByText('create_wallet.confirmation_toggle_reveal_info')
+      user.click(revealToggle)
+
+      const confirmToggle = screen.getByText('create_wallet.confirmation_toggle_info_written_down')
+      user.click(confirmToggle)
+
+      const nextButton = screen.getByText('create_wallet.next_button')
+      user.click(nextButton)
+    })
+
+    expect(screen.getByText('create_wallet.skip_button')).toBeInTheDocument()
+    expect(screen.getByText('create_wallet.back_button')).toBeInTheDocument()
+    expect(screen.getByText('create_wallet.confirmation_button_fund_wallet')).toBeDisabled()
+  })
+})