Repository: joinmarket-webui/jam
Pull request #668 (2023-09-26 11:32:02)

feat: display ui/backend version
Displays the ui/backend version in the info modal.

<img src="https://github.com/joinmarket-webui/jam/assets/3358649/5231d444-8051-4d67-8260-28c34b395513" width=250 />
<img src="https://github.com/joinmarket-webui/jam/assets/3358649/988d5f1a-22a0-45be-b02e-fc31cc0d5ef1" width=250 />


Patch:
diff --git a/src/components/Footer.jsx b/src/components/Footer.tsx
similarity index 86%
rename from src/components/Footer.jsx
rename to src/components/Footer.tsx
index 3b4da06..956c3d5 100644
--- a/src/components/Footer.jsx
+++ b/src/components/Footer.tsx
@@ -1,21 +1,30 @@
-import React, { useState, useEffect, useMemo } from 'react'
+import { useState, useEffect, useMemo } from 'react'
 import * as rb from 'react-bootstrap'
 import { Trans, useTranslation } from 'react-i18next'
 import { useSettings, useSettingsDispatch } from '../context/SettingsContext'
+import { useServiceInfo } from '../context/ServiceInfoContext'
 import { useWebsocketState } from '../context/WebsocketContext'
 import { useCurrentWallet } from '../context/WalletContext'
 import Sprite from './Sprite'
 import Cheatsheet from './Cheatsheet'
 import packageInfo from '../../package.json'
+import { isDevMode } from '../constants/debugFeatures'
+import { toSemVer } from '../utils'
+
+const APP_DISPLAY_VERSION = (() => {
+  const version = toSemVer(packageInfo.version)
+  return !isDevMode() ? version.raw : `${version.major}.${version.minor}.${version.patch + 1}dev`
+})()
 
 export default function Footer() {
   const { t } = useTranslation()
   const settings = useSettings()
+  const serviceInfo = useServiceInfo()
   const settingsDispatch = useSettingsDispatch()
   const websocketState = useWebsocketState()
   const currentWallet = useCurrentWallet()
 
-  const [websocketConnected, setWebsocketConnected] = useState()
+  const [websocketConnected, setWebsocketConnected] = useState(false)
   const [showBetaWarning, setShowBetaWarning] = useState(false)
   const [showCheatsheet, setShowCheatsheet] = useState(false)
 
@@ -27,7 +36,7 @@ export default function Footer() {
   }, [websocketState])
 
   useEffect(() => {
-    let timer
+    let timer: NodeJS.Timeout
     // show the cheatsheet once after the first wallet has been created
     if (cheatsheetEnabled && settings.showCheatsheet) {
       timer = setTimeout(() => {
@@ -46,9 +55,14 @@ export default function Footer() {
           <rb.Card className="warning-card translate-middle shadow-lg">
             <rb.Card.Body>
               <rb.Card.Title className="text-center mb-3">{t('footer.warning_alert_title')}</rb.Card.Title>
-              <p className="text-secondary">{t('footer.warning_alert_text')}</p>
+              <p>{t('footer.warning_alert_text')}</p>
+              <p className="text-secondary">
+                JoinMarket: v{serviceInfo?.server?.version?.raw || 'unknown'}
+                <br />
+                Jam: v{APP_DISPLAY_VERSION}
+              </p>
               <div className="text-center mt-3">
-                <rb.Button variant="secondary" onClick={() => setShowBetaWarning(false)}>
+                <rb.Button variant="dark" onClick={() => setShowBetaWarning(false)}>
                   {t('footer.warning_alert_button_ok')}
                 </rb.Button>
               </div>
@@ -100,7 +114,7 @@ export default function Footer() {
                 rel="noopener noreferrer"
                 className="d-flex align-items-center text-secondary"
               >
-                v{packageInfo.version}
+                v{APP_DISPLAY_VERSION}
               </a>
             </div>
             <div className="d-flex gap-2 pe-2">
diff --git a/src/context/ServiceInfoContext.tsx b/src/context/ServiceInfoContext.tsx
index c68b366..6c6355e 100644
--- a/src/context/ServiceInfoContext.tsx
+++ b/src/context/ServiceInfoContext.tsx
@@ -5,6 +5,7 @@ import { useCurrentWallet, useSetCurrentWallet } from './WalletContext'
 import { useWebsocket } from './WebsocketContext'
 import { clearSession } from '../session'
 import { CJ_STATE_TAKER_RUNNING, CJ_STATE_MAKER_RUNNING } from '../constants/config'
+import { toSemVer, UNKNOWN_VERSION } from '../utils'
 
 import * as Api from '../libs/JmWalletApi'
 
@@ -57,8 +58,6 @@ 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 }
@@ -89,21 +88,6 @@ type ServiceInfoUpdate =
   | 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
-  }
-
-  return {
-    major: parseInt(arr[1], 10),
-    minor: parseInt(arr[2], 10),
-    patch: parseInt(arr[3], 10),
-    raw: data.version,
-  }
-}
-
 interface ServiceInfoContextEntry {
   serviceInfo: ServiceInfo | null
   reloadServiceInfo: ({ signal }: { signal: AbortSignal }) => Promise<ServiceInfo>
@@ -134,7 +118,7 @@ const ServiceInfoProvider = ({ children }: React.PropsWithChildren<{}>) => {
       .then((data: JmGetInfoData) => {
         dispatchServiceInfo({
           server: {
-            version: toSemVer(data),
+            version: toSemVer(data.version),
           },
         })
       })
diff --git a/src/utils.ts b/src/utils.ts
index d02c7eb..c5979d0 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -71,3 +71,20 @@ export const factorToPercentage = (val: number, precision = 6) => {
 }
 
 export const isValidNumber = (val: number | undefined) => typeof val === 'number' && !isNaN(val)
+
+export const UNKNOWN_VERSION: SemVer = { major: 0, minor: 0, patch: 0, raw: 'unknown' }
+
+const versionRegex = new RegExp(/^v?(\d+)\.(\d+)\.(\d+).*$/)
+export const toSemVer = (raw?: string): SemVer => {
+  const arr = versionRegex.exec(raw || '')
+  if (!arr || arr.length < 4) {
+    return UNKNOWN_VERSION
+  }
+
+  return {
+    major: parseInt(arr[1], 10),
+    minor: parseInt(arr[2], 10),
+    patch: parseInt(arr[3], 10),
+    raw,
+  }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 4736e37..c0a4863 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,6 +7,7 @@
     "strict": true,
     "jsx": "react-jsx",
     "skipLibCheck": true,
-    "allowJs": true
+    "allowJs": true,
+    "resolveJsonModule": true
   }
 }


Test patch:
diff --git a/src/utils.test.ts b/src/utils.test.ts
index 50127a8..a2ab28f 100644
--- a/src/utils.test.ts
+++ b/src/utils.test.ts
@@ -1,4 +1,4 @@
-import { shortenStringMiddle, percentageToFactor, factorToPercentage } from './utils'
+import { shortenStringMiddle, percentageToFactor, factorToPercentage, toSemVer, UNKNOWN_VERSION } from './utils'
 
 describe('shortenStringMiddle', () => {
   it('should shorten string in the middle', () => {
@@ -69,3 +69,47 @@ describe('factorToPercentage/percentageToFactor', () => {
     expect(testInverse(233.7)).toBe(233.7)
   })
 })
+
+describe('toSemVer', () => {
+  it('should parse version correctly', () => {
+    expect(toSemVer('0.0.1')).toEqual({
+      major: 0,
+      minor: 0,
+      patch: 1,
+      raw: '0.0.1',
+    })
+    expect(toSemVer('0.9.11dev')).toEqual({
+      major: 0,
+      minor: 9,
+      patch: 11,
+      raw: '0.9.11dev',
+    })
+    expect(toSemVer('1.0.0-beta.2')).toEqual({
+      major: 1,
+      minor: 0,
+      patch: 0,
+      raw: '1.0.0-beta.2',
+    })
+    expect(toSemVer('21.42.1337-dev.2+devel.99ff4cd')).toEqual({
+      major: 21,
+      minor: 42,
+      patch: 1337,
+      raw: '21.42.1337-dev.2+devel.99ff4cd',
+    })
+  })
+  it('should parse invalid version as UNKNOWN', () => {
+    expect(toSemVer(undefined)).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer(' ')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('🧡')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21.42')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21.42.')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21.42.💯')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21.42.-1')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21.42.-1')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21.-1.42')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('-1.21.42')).toBe(UNKNOWN_VERSION)
+    expect(toSemVer('21million')).toBe(UNKNOWN_VERSION)
+  })
+})