From 3a80f7ad45e1efdc94942556e427305f9764ead7 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Sat, 6 Sep 2025 09:26:52 +0200 Subject: [PATCH 01/26] frontend: refactor password input to functional component --- frontends/web/src/components/password.tsx | 497 +++++++++++----------- 1 file changed, 239 insertions(+), 258 deletions(-) diff --git a/frontends/web/src/components/password.tsx b/frontends/web/src/components/password.tsx index 62d3d4ba84..fa29f37c0c 100644 --- a/frontends/web/src/components/password.tsx +++ b/frontends/web/src/components/password.tsx @@ -1,6 +1,6 @@ /** * Copyright 2018 Shift Devices AG - * Copyright 2024 Shift Crypto AG + * Copyright 2024-2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ * limitations under the License. */ -import { Component, createRef } from 'react'; -import { TranslateProps, translate } from '@/decorators/translate'; +import { useEffect, useRef, useState, ChangeEvent, ClipboardEvent } from 'react'; +import { useTranslation } from 'react-i18next'; import { Input, Checkbox, Field } from './forms'; import { alertUser } from './alert/Alert'; import style from './password.module.css'; @@ -42,6 +42,7 @@ type TPropsPasswordInput = { onInput?: (event: React.ChangeEvent) => void; value: string; }; + export const PasswordInput = ({ seePlaintext, ...rest }: TPropsPasswordInput) => { return ( void; }; -type TPasswordSingleInputProps = TProps & TranslateProps; - -type TState = { - password: string; - seePlaintext: boolean; - capsLock: boolean; -}; - -class PasswordSingleInputClass extends Component { - private regex?: RegExp; - - state = { - password: '', - seePlaintext: false, - capsLock: false - }; - - password = createRef(); - - idPrefix = () => { - return this.props.idPrefix || ''; - }; - - handleCheckCaps = (event: KeyboardEvent) => { - const capsLock = hasCaps(event); - - if (capsLock !== null) { - this.setState({ capsLock }); +export const PasswordSingleInput = ({ + idPrefix = '', + pattern, + autoFocus, + disabled, + label, + placeholder, + title, + showLabel, + onValidPassword, +}: TProps) => { + const { t } = useTranslation(); + const [password, setPassword] = useState(''); + const [seePlaintext, setSeePlaintext] = useState(false); + const [capsLock, setCapsLock] = useState(false); + + const passwordRef = useRef(null); + const regexRef = useRef(); + + // Setup regex + autofocus + useEffect(() => { + if (pattern) { + regexRef.current = new RegExp(pattern); } - }; - - componentDidMount() { - window.addEventListener('keydown', this.handleCheckCaps); - if (this.props.pattern) { - this.regex = new RegExp(this.props.pattern); - } - if (this.props.autoFocus && this.password?.current) { - this.password.current.focus(); + if (autoFocus && passwordRef.current) { + passwordRef.current.focus(); } - } - - componentWillUnmount() { - window.removeEventListener('keydown', this.handleCheckCaps); - } - - tryPaste = (event: React.ClipboardEvent) => { - const target = event.currentTarget; - if (target.type === 'password') { + }, [pattern, autoFocus]); + + // Listen to caps lock key events + useEffect(() => { + const handleCheckCaps = (event: KeyboardEvent) => { + const result = hasCaps(event); + if (result !== null) { + setCapsLock(result); + } + }; + window.addEventListener('keydown', handleCheckCaps); + return () => { + window.removeEventListener('keydown', handleCheckCaps); + }; + }, []); + + const tryPaste = (event: ClipboardEvent) => { + if (event.currentTarget.type === 'password') { event.preventDefault(); - alertUser(this.props.t('password.warning.paste', { - label: this.props.label - })); + alertUser( + t('password.warning.paste', { + label, + }) + ); } }; - clear = () => { - this.setState({ - password: '', - seePlaintext: false, - capsLock: false - }); - }; - - validate = () => { - if (this.regex && this.password.current && !this.password.current.validity.valid) { - return this.props.onValidPassword(null); + const validate = (value: string) => { + if (regexRef.current && passwordRef.current && !passwordRef.current.validity.valid) { + onValidPassword(null); + return; } - if (this.state.password) { - this.props.onValidPassword(this.state.password); + if (value) { + onValidPassword(value); } else { - this.props.onValidPassword(null); + onValidPassword(null); } }; - handleFormChange = (event: React.ChangeEvent) => { - let value: string | boolean = event.target.value; + const handleFormChange = (event: ChangeEvent) => { if (event.target.type === 'checkbox') { - value = event.target.checked; + setSeePlaintext(event.target.checked); + } else { + const newPassword = event.target.value; + setPassword(newPassword); + validate(newPassword); } - const stateKey = event.target.id.slice(this.idPrefix().length) as keyof TState; - this.setState({ [stateKey]: value } as Pick, this.validate); }; - render() { - const { - t, - disabled, - label, - placeholder, - pattern, - title, - showLabel, - } = this.props; - const { - password, - seePlaintext, - capsLock, - } = this.state; - const warning = (capsLock && !seePlaintext) && ( - - ); - return ( - - }> - {warning} - - ); - } - -} - -const HOC = translate(undefined, { withRef: true })(PasswordSingleInputClass); -export { HOC as PasswordSingleInput }; + const warning = + capsLock && !seePlaintext ? ( + + ⇪ + + ) : null; + return ( + + } + > + {warning} + + ); +}; -type TPasswordRepeatProps = TPasswordSingleInputProps & { +type TPasswordRepeatProps = TProps & { repeatLabel?: string; repeatPlaceholder: string; }; -class PasswordRepeatInputClass extends Component { - private regex?: RegExp; - - state = { - password: '', - passwordRepeat: '', - seePlaintext: false, - capsLock: false - }; - - password = createRef(); - passwordRepeat = createRef(); - - idPrefix = () => { - return this.props.idPrefix || ''; - }; - - - handleCheckCaps = (event: KeyboardEvent) => { - const capsLock = hasCaps(event); - - if (capsLock !== null) { - this.setState({ capsLock }); +export const PasswordRepeatInput = ({ + idPrefix = '', + pattern, + autoFocus, + disabled, + label, + placeholder, + title, + repeatLabel, + repeatPlaceholder, + showLabel, + onValidPassword, +}: TPasswordRepeatProps) => { + const { t } = useTranslation(); + + const [password, setPassword] = useState(''); + const [passwordRepeat, setPasswordRepeat] = useState(''); + const [seePlaintext, setSeePlaintext] = useState(false); + const [capsLock, setCapsLock] = useState(false); + + const passwordRef = useRef(null); + const passwordRepeatRef = useRef(null); + const regexRef = useRef(); + + // Setup regex + autofocus + useEffect(() => { + if (pattern) { + regexRef.current = new RegExp(pattern); } - }; - - componentDidMount() { - window.addEventListener('keydown', this.handleCheckCaps); - if (this.props.pattern) { - this.regex = new RegExp(this.props.pattern); - } - if (this.props.autoFocus && this.password?.current) { - this.password.current.focus(); + if (autoFocus && passwordRef.current) { + passwordRef.current.focus(); } - } - - componentWillUnmount() { - window.removeEventListener('keydown', this.handleCheckCaps); - } - - tryPaste = (event: React.ClipboardEvent) => { - const target = event.currentTarget; - if (target.type === 'password') { + }, [pattern, autoFocus]); + + // Listen to caps lock key events + useEffect(() => { + const handleCheckCaps = (event: KeyboardEvent) => { + const result = hasCaps(event); + if (result !== null) { + setCapsLock(result); + } + }; + window.addEventListener('keydown', handleCheckCaps); + return () => { + window.removeEventListener('keydown', handleCheckCaps); + }; + }, []); + + const tryPaste = (event: ClipboardEvent) => { + if (event.currentTarget.type === 'password') { event.preventDefault(); - alertUser(this.props.t('password.warning.paste', { - label: this.props.label - })); + alertUser( + t('password.warning.paste', { + label, + }) + ); } }; - validate = () => { + const validate = (pwd: string, pwdRepeat: string) => { if ( - this.regex && this.password.current && this.passwordRepeat.current - && (!this.password.current.validity.valid || !this.passwordRepeat.current.validity.valid) + regexRef.current && + passwordRef.current && + passwordRepeatRef.current && + (!passwordRef.current.validity.valid || !passwordRepeatRef.current.validity.valid) ) { - return this.props.onValidPassword(null); + onValidPassword(null); + return; } - if (this.state.password && this.state.password === this.state.passwordRepeat) { - this.props.onValidPassword(this.state.password); + if (pwd && pwd === pwdRepeat) { + onValidPassword(pwd); } else { - this.props.onValidPassword(null); + onValidPassword(null); } }; - handleFormChange = (event: React.ChangeEvent) => { - let value: string | boolean = event.target.value; + const handleFormChange = (event: ChangeEvent) => { if (event.target.type === 'checkbox') { - value = event.target.checked; + setSeePlaintext(event.target.checked); + return; + } + + if (event.target.id.endsWith('passwordRepeat')) { + const newRepeat = event.target.value; + setPasswordRepeat(newRepeat); + validate(password, newRepeat); + } else { + const newPassword = event.target.value; + setPassword(newPassword); + validate(newPassword, passwordRepeat); } - const stateKey = event.target.id.slice(this.idPrefix().length); - this.setState({ [stateKey]: value } as Pick, this.validate); }; - render() { - const { - t, - disabled, - label, - placeholder, - pattern, - title, - repeatLabel, - repeatPlaceholder, - showLabel, - } = this.props; - const { - password, - passwordRepeat, - seePlaintext, - capsLock, - } = this.state; - const warning = (capsLock && !seePlaintext) && ( - - ); - return ( -
- - {warning} - - - - {warning} - - - - - -
- ); - } -} + const warning = + capsLock && !seePlaintext ? ( + + ⇪ + + ) : null; -const HOCRepeat = translate(undefined, { withRef: true })(PasswordRepeatInputClass); -export { HOCRepeat as PasswordRepeatInput }; + return ( +
+ + {warning} + + + + + + {warning} + + + + + + + +
+ ); +}; type MatchesPatternProps = { regex: RegExp | undefined; value: string; text: string | undefined; }; + const MatchesPattern = ({ regex, value = '', text }: MatchesPatternProps) => { if (!regex || !value.length || regex.test(value)) { return null; } - return ( -

{text}

+

+ {text} +

); }; - From 42bb33861847128e7f7e3d4f53534b1bf16cbd89 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Sat, 6 Sep 2025 10:37:45 +0200 Subject: [PATCH 02/26] frontend: refactor use-capslock into a hook --- frontends/web/src/components/password.tsx | 45 ++--------------------- frontends/web/src/hooks/keyboard.ts | 27 +++++++++++++- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/frontends/web/src/components/password.tsx b/frontends/web/src/components/password.tsx index fa29f37c0c..3f854fe072 100644 --- a/frontends/web/src/components/password.tsx +++ b/frontends/web/src/components/password.tsx @@ -17,21 +17,11 @@ import { useEffect, useRef, useState, ChangeEvent, ClipboardEvent } from 'react'; import { useTranslation } from 'react-i18next'; +import { useCapsLock } from '@/hooks/keyboard'; import { Input, Checkbox, Field } from './forms'; import { alertUser } from './alert/Alert'; import style from './password.module.css'; -const excludeKeys = /^(Shift|Alt|Backspace|CapsLock|Tab)$/i; - -const hasCaps = (event: KeyboardEvent) => { - const key = event.key; - // will return null, when we cannot clearly detect if capsLock is active or not - if (key.length > 1 || key.toUpperCase() === key.toLowerCase() || excludeKeys.test(key)) { - return null; - } - // ideally we return event.getModifierState('CapsLock')) but this currently does always return false in Qt - return key.toUpperCase() === key && key.toLowerCase() !== key && !event.shiftKey; -}; type TPropsPasswordInput = { seePlaintext?: boolean; @@ -76,9 +66,10 @@ export const PasswordSingleInput = ({ onValidPassword, }: TProps) => { const { t } = useTranslation(); + const capsLock = useCapsLock(); + const [password, setPassword] = useState(''); const [seePlaintext, setSeePlaintext] = useState(false); - const [capsLock, setCapsLock] = useState(false); const passwordRef = useRef(null); const regexRef = useRef(); @@ -93,20 +84,6 @@ export const PasswordSingleInput = ({ } }, [pattern, autoFocus]); - // Listen to caps lock key events - useEffect(() => { - const handleCheckCaps = (event: KeyboardEvent) => { - const result = hasCaps(event); - if (result !== null) { - setCapsLock(result); - } - }; - window.addEventListener('keydown', handleCheckCaps); - return () => { - window.removeEventListener('keydown', handleCheckCaps); - }; - }, []); - const tryPaste = (event: ClipboardEvent) => { if (event.currentTarget.type === 'password') { event.preventDefault(); @@ -196,11 +173,11 @@ export const PasswordRepeatInput = ({ onValidPassword, }: TPasswordRepeatProps) => { const { t } = useTranslation(); + const capsLock = useCapsLock(); const [password, setPassword] = useState(''); const [passwordRepeat, setPasswordRepeat] = useState(''); const [seePlaintext, setSeePlaintext] = useState(false); - const [capsLock, setCapsLock] = useState(false); const passwordRef = useRef(null); const passwordRepeatRef = useRef(null); @@ -216,20 +193,6 @@ export const PasswordRepeatInput = ({ } }, [pattern, autoFocus]); - // Listen to caps lock key events - useEffect(() => { - const handleCheckCaps = (event: KeyboardEvent) => { - const result = hasCaps(event); - if (result !== null) { - setCapsLock(result); - } - }; - window.addEventListener('keydown', handleCheckCaps); - return () => { - window.removeEventListener('keydown', handleCheckCaps); - }; - }, []); - const tryPaste = (event: ClipboardEvent) => { if (event.currentTarget.type === 'password') { event.preventDefault(); diff --git a/frontends/web/src/hooks/keyboard.ts b/frontends/web/src/hooks/keyboard.ts index 6fa388908c..a39b5b07be 100644 --- a/frontends/web/src/hooks/keyboard.ts +++ b/frontends/web/src/hooks/keyboard.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; /** * gets fired on each keydown and executes the provided callback. @@ -54,6 +54,31 @@ export const useEsc = ( }); }; +const excludeKeys = /^(Shift|Alt|Backspace|CapsLock|Tab)$/i; + +const hasCaps = (event: KeyboardEvent) => { + const key = event.key; + // will return null, when we cannot clearly detect if capsLock is active or not + if (key.length > 1 || key.toUpperCase() === key.toLowerCase() || excludeKeys.test(key)) { + return null; + } + // ideally we return event.getModifierState('CapsLock')) but this currently does always return false in Qt + return key.toUpperCase() === key && key.toLowerCase() !== key && !event.shiftKey; +}; + +export const useCapsLock = () => { + const [capsLock, setCapsLock] = useState(false); + + useKeydown((event) => { + const result = hasCaps(event); + if (result !== null) { + setCapsLock(result); + } + }); + + return capsLock; +}; + const FOCUSABLE_SELECTOR = ` a:not(:disabled), button:not(:disabled), From f49a51692afbf80a9f8cb159b07681cd43b91553 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Sat, 6 Sep 2025 10:43:50 +0200 Subject: [PATCH 03/26] frontend: use memo to keep the regex if needed --- frontends/web/src/components/password.tsx | 31 ++++++++++------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/frontends/web/src/components/password.tsx b/frontends/web/src/components/password.tsx index 3f854fe072..218f31010c 100644 --- a/frontends/web/src/components/password.tsx +++ b/frontends/web/src/components/password.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import { useEffect, useRef, useState, ChangeEvent, ClipboardEvent } from 'react'; +import { useEffect, useRef, useState, ChangeEvent, ClipboardEvent, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useCapsLock } from '@/hooks/keyboard'; import { Input, Checkbox, Field } from './forms'; @@ -72,17 +72,13 @@ export const PasswordSingleInput = ({ const [seePlaintext, setSeePlaintext] = useState(false); const passwordRef = useRef(null); - const regexRef = useRef(); - // Setup regex + autofocus + // Autofocus useEffect(() => { - if (pattern) { - regexRef.current = new RegExp(pattern); - } if (autoFocus && passwordRef.current) { passwordRef.current.focus(); } - }, [pattern, autoFocus]); + }, [autoFocus]); const tryPaste = (event: ClipboardEvent) => { if (event.currentTarget.type === 'password') { @@ -96,7 +92,7 @@ export const PasswordSingleInput = ({ }; const validate = (value: string) => { - if (regexRef.current && passwordRef.current && !passwordRef.current.validity.valid) { + if (passwordRef.current && !passwordRef.current.validity.valid) { onValidPassword(null); return; } @@ -181,17 +177,15 @@ export const PasswordRepeatInput = ({ const passwordRef = useRef(null); const passwordRepeatRef = useRef(null); - const regexRef = useRef(); - // Setup regex + autofocus + const regex = useMemo(() => (pattern ? new RegExp(pattern) : null), [pattern]); + + // Autofocus useEffect(() => { - if (pattern) { - regexRef.current = new RegExp(pattern); - } if (autoFocus && passwordRef.current) { passwordRef.current.focus(); } - }, [pattern, autoFocus]); + }, [autoFocus]); const tryPaste = (event: ClipboardEvent) => { if (event.currentTarget.type === 'password') { @@ -206,7 +200,6 @@ export const PasswordRepeatInput = ({ const validate = (pwd: string, pwdRepeat: string) => { if ( - regexRef.current && passwordRef.current && passwordRepeatRef.current && (!passwordRef.current.validity.valid || !passwordRepeatRef.current.validity.valid) @@ -264,7 +257,9 @@ export const PasswordRepeatInput = ({ {warning} - + {regex && ( + + )} - + {regex && ( + + )} Date: Mon, 8 Sep 2025 14:24:09 +0200 Subject: [PATCH 04/26] frontend: remove idprefix from password component --- frontends/web/src/components/password.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontends/web/src/components/password.tsx b/frontends/web/src/components/password.tsx index 218f31010c..aa28e848ec 100644 --- a/frontends/web/src/components/password.tsx +++ b/frontends/web/src/components/password.tsx @@ -43,7 +43,6 @@ export const PasswordInput = ({ seePlaintext, ...rest }: TPropsPasswordInput) => }; type TProps = { - idPrefix?: string; pattern?: string; autoFocus?: boolean; disabled?: boolean; @@ -55,7 +54,6 @@ type TProps = { }; export const PasswordSingleInput = ({ - idPrefix = '', pattern, autoFocus, disabled, @@ -113,12 +111,13 @@ export const PasswordSingleInput = ({ } }; - const warning = + const warning = ( capsLock && !seePlaintext ? ( - ) : null; + ) : null + ); return ( Date: Thu, 9 Oct 2025 11:33:44 +0200 Subject: [PATCH 05/26] frontend: update id and idprefix on password, passwordrepeat etc IdPrefix is not relly neeeded, however many components that use PasswordSingleInput or PasswordRepeatInput are untyped BitBox01 components. To be safe it would be better to convert to TypeScript. --- frontends/web/src/components/password.tsx | 23 +++++++++---------- .../settings/components/changepin.jsx | 3 +-- .../bitbox01/settings/components/reset.jsx | 2 +- .../web/src/routes/device/bitbox01/unlock.jsx | 1 - 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/frontends/web/src/components/password.tsx b/frontends/web/src/components/password.tsx index aa28e848ec..c959ba0048 100644 --- a/frontends/web/src/components/password.tsx +++ b/frontends/web/src/components/password.tsx @@ -26,7 +26,6 @@ import style from './password.module.css'; type TPropsPasswordInput = { seePlaintext?: boolean; id?: string; - idPrefix?: string; label: string; placeholder?: string; onInput?: (event: React.ChangeEvent) => void; @@ -150,13 +149,11 @@ export const PasswordSingleInput = ({ }; type TPasswordRepeatProps = TProps & { - idPrefix?: string; repeatLabel?: string; repeatPlaceholder: string; }; export const PasswordRepeatInput = ({ - idPrefix = '', pattern, autoFocus, disabled, @@ -219,15 +216,17 @@ export const PasswordRepeatInput = ({ setSeePlaintext(event.target.checked); return; } - - if (event.target.id.endsWith('passwordRepeat')) { - const newRepeat = event.target.value; - setPasswordRepeat(newRepeat); - validate(password, newRepeat); - } else { + switch (event.target.id) { + case 'passwordRepeatFirst': const newPassword = event.target.value; setPassword(newPassword); validate(newPassword, passwordRepeat); + break; + case 'passwordRepeatSecond': + const newRepeat = event.target.value; + setPasswordRepeat(newRepeat); + validate(password, newRepeat); + break; } }; @@ -246,7 +245,7 @@ export const PasswordRepeatInput = ({ type={seePlaintext ? 'text' : 'password'} pattern={pattern} title={title} - id={`${idPrefix}password`} + id="passwordRepeatFirst" label={label} placeholder={placeholder} onInput={handleFormChange} @@ -266,7 +265,7 @@ export const PasswordRepeatInput = ({ type={seePlaintext ? 'text' : 'password'} pattern={pattern} title={title} - id={`${idPrefix}passwordRepeat`} + id="passwordRepeatSecond" label={repeatLabel} placeholder={repeatPlaceholder} onInput={handleFormChange} @@ -283,7 +282,7 @@ export const PasswordRepeatInput = ({
{t('changePin.newTitle') &&

{t('changePin.newTitle')}

} diff --git a/frontends/web/src/routes/device/bitbox01/unlock.jsx b/frontends/web/src/routes/device/bitbox01/unlock.jsx index e07cb4d717..41b6a8bee4 100644 --- a/frontends/web/src/routes/device/bitbox01/unlock.jsx +++ b/frontends/web/src/routes/device/bitbox01/unlock.jsx @@ -133,7 +133,6 @@ class Unlock extends Component {
Date: Thu, 9 Oct 2025 11:39:33 +0200 Subject: [PATCH 06/26] frontend: move bitbox01 create.jsx to create.tsx Moving first to have a nicer diff in next commit. --- .../web/src/routes/device/bitbox01/{create.jsx => create.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontends/web/src/routes/device/bitbox01/{create.jsx => create.tsx} (100%) diff --git a/frontends/web/src/routes/device/bitbox01/create.jsx b/frontends/web/src/routes/device/bitbox01/create.tsx similarity index 100% rename from frontends/web/src/routes/device/bitbox01/create.jsx rename to frontends/web/src/routes/device/bitbox01/create.tsx From 228f208f43f8b7d805ca823c34741298b70418de Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 11:47:34 +0200 Subject: [PATCH 07/26] frontend: converted bb01 create to functional typescript component --- .../src/routes/device/bitbox01/backups.tsx | 2 +- .../web/src/routes/device/bitbox01/create.tsx | 179 +++++++++--------- 2 files changed, 89 insertions(+), 92 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/backups.tsx b/frontends/web/src/routes/device/bitbox01/backups.tsx index e442028515..fa2fcadaf0 100644 --- a/frontends/web/src/routes/device/bitbox01/backups.tsx +++ b/frontends/web/src/routes/device/bitbox01/backups.tsx @@ -26,7 +26,7 @@ import { Button } from '../../../components/forms'; import { BackupsListItem } from '../components/backup'; import style from '../components/backups.module.css'; import Check from './check'; -import Create from './create'; +import { Create } from './create'; import { Restore } from './restore'; type BackupsProps = { diff --git a/frontends/web/src/routes/device/bitbox01/create.tsx b/frontends/web/src/routes/device/bitbox01/create.tsx index 5ed32c551a..1d0f8465f7 100644 --- a/frontends/web/src/routes/device/bitbox01/create.tsx +++ b/frontends/web/src/routes/device/bitbox01/create.tsx @@ -1,5 +1,6 @@ /** * Copyright 2018 Shift Devices AG + * Copyright 2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,113 +15,109 @@ * limitations under the License. */ -import { Component } from 'react'; -import { withTranslation } from 'react-i18next'; -import { Button, Input } from '../../../components/forms'; -import { PasswordInput } from '../../../components/password'; -import { alertUser } from '../../../components/alert/Alert'; -import { apiPost } from '../../../utils/request'; -import { DialogLegacy } from '../../../components/dialog/dialog-legacy'; +import { useState, ChangeEvent, FormEvent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Input } from '@/components/forms'; +import { PasswordInput } from '@/components/password'; +import { alertUser } from '@/components/alert/Alert'; +import { apiPost } from '@/utils/request'; // TODO: use DialogButtons -import style from '../../../components/dialog/dialog.module.css'; +import { DialogLegacy } from '@/components/dialog/dialog-legacy'; +import style from '@/components/dialog/dialog.module.css'; -class Create extends Component { - state = { - waiting: false, - backupName: '', - recoveryPassword: '', - activeDialog: false, - }; +type Props = { + deviceID: string; + onCreate: () => void; +} + +export const Create = ({ deviceID, onCreate }: Props) => { + const { t } = useTranslation(); - abort = () => { - this.setState({ - waiting: false, - backupName: '', - recoveryPassword: '', - activeDialog: false, - }); + const [waiting, setWaiting] = useState(false); + const [backupName, setBackupName] = useState(''); + const [recoveryPassword, setRecoveryPassword] = useState(''); + const [activeDialog, setActiveDialog] = useState(false); + + const abort = () => { + setWaiting(false); + setBackupName(''); + setRecoveryPassword(''); + setActiveDialog(false); }; - handleFormChange = event => { - this.setState({ [event.target.id]: event.target.value }); + const handleFormChange = (event: ChangeEvent) => { + const { id, value } = event.target; + if (id === 'backupName') setBackupName(value); + if (id === 'recoveryPassword') setRecoveryPassword(value); }; - validate = () => { - return !this.state.waiting && this.state.backupName !== ''; + const validate = () => { + return !waiting && backupName.trim() !== ''; }; - create = event => { + const create = async (event: FormEvent) => { event.preventDefault(); - if (!this.validate()) { - return; - } - this.setState({ waiting: true }); - apiPost('devices/' + this.props.deviceID + '/backups/create', { - backupName: this.state.backupName, - recoveryPassword: this.state.recoveryPassword, - }).then(data => { - this.abort(); + if (!validate()) return; + + setWaiting(true); + try { + const data = await apiPost(`devices/${deviceID}/backups/create`, { + backupName, + recoveryPassword, + }); + + abort(); + if (!data.success) { alertUser(data.errorMessage); } else { - this.props.onCreate(); + onCreate(); if (!data.verification) { - alertUser(this.props.t('backup.create.verificationFailed')); + alertUser(t('backup.create.verificationFailed')); } } - }); + } catch (error) { + abort(); + alertUser(String(error)); + } }; - render() { - const { t } = this.props; - const { - waiting, - recoveryPassword, - backupName, - activeDialog, - } = this.state; - return ( -
- - { - activeDialog && ( - - - -

{t('backup.create.info')}

- -
- - -
- -
- ) - } -
- ); - } -} + return ( +
+ -export default withTranslation()(Create); + {activeDialog && ( + +
+ +

{t('backup.create.info')}

+ +
+ + +
+ +
+ )} +
+ ); +}; From 3323f8d687e0e1b465cfa4780b784bc50b136482 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 11:48:28 +0200 Subject: [PATCH 08/26] frontend: use dialogbuttons in bb01 create --- frontends/web/src/routes/device/bitbox01/create.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/create.tsx b/frontends/web/src/routes/device/bitbox01/create.tsx index 1d0f8465f7..3a48a73e75 100644 --- a/frontends/web/src/routes/device/bitbox01/create.tsx +++ b/frontends/web/src/routes/device/bitbox01/create.tsx @@ -21,9 +21,7 @@ import { Button, Input } from '@/components/forms'; import { PasswordInput } from '@/components/password'; import { alertUser } from '@/components/alert/Alert'; import { apiPost } from '@/utils/request'; -// TODO: use DialogButtons -import { DialogLegacy } from '@/components/dialog/dialog-legacy'; -import style from '@/components/dialog/dialog.module.css'; +import { DialogLegacy, DialogButtons } from '@/components/dialog/dialog-legacy'; type Props = { deviceID: string; @@ -107,14 +105,14 @@ export const Create = ({ deviceID, onCreate }: Props) => { onInput={handleFormChange} value={recoveryPassword} /> -
+ -
+ )} From 1f12140244273d5e4a06ed2067136fca1622623e Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 11:50:15 +0200 Subject: [PATCH 09/26] frontend: move bitbox01 changepin.jsx to changepin.tsx Moving first to have a nicer diff in the next commit. --- .../bitbox01/settings/components/{changepin.jsx => changepin.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontends/web/src/routes/device/bitbox01/settings/components/{changepin.jsx => changepin.tsx} (100%) diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.jsx b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx similarity index 100% rename from frontends/web/src/routes/device/bitbox01/settings/components/changepin.jsx rename to frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx From 401a418dac4052eb91753bc702897ea85c4c6679 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 11:57:24 +0200 Subject: [PATCH 10/26] frontend: converted bb01 changepin to functional typescript component --- .../settings/components/changepin.tsx | 207 +++++++++--------- .../device/bitbox01/settings/settings.tsx | 2 +- 2 files changed, 102 insertions(+), 107 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx index 253575a819..a5e1c5022b 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx @@ -1,6 +1,6 @@ /** * Copyright 2018 Shift Devices AG - * Copyright 2021 Shift Crypto AG + * Copyright 2021-2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,123 +15,118 @@ * limitations under the License. */ -import { Component } from 'react'; -import { withTranslation } from 'react-i18next'; -import { Button } from '../../../../../components/forms'; -import { alertUser } from '../../../../../components/alert/Alert'; -import { DialogLegacy, DialogButtons } from '../../../../../components/dialog/dialog-legacy'; -import { WaitDialog } from '../../../../../components/wait-dialog/wait-dialog'; -import { PasswordInput, PasswordRepeatInput } from '../../../../../components/password'; -import { apiPost } from '../../../../../utils/request'; -import { SettingsButton } from '../../../../../components/settingsButton/settingsButton'; - -class ChangePIN extends Component { - state = { - oldPIN: null, - newPIN: null, - errorCode: null, - isConfirming: false, - activeDialog: false, - }; +import { useState, ChangeEvent, FormEvent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/forms'; +import { alertUser } from '@/components/alert/Alert'; +import { DialogLegacy, DialogButtons } from '@/components/dialog/dialog-legacy'; +import { WaitDialog } from '@/components/wait-dialog/wait-dialog'; +import { PasswordInput, PasswordRepeatInput } from '@/components/password'; +import { apiPost } from '@/utils/request'; +import { SettingsButton } from '@/components/settingsButton/settingsButton'; + +type Props = { + deviceID: string; + disabled?: boolean; +} + +export const ChangePIN = ({ deviceID, disabled }: Props) => { + const { t } = useTranslation(); - abort = () => { - this.setState({ - oldPIN: null, - newPIN: null, - isConfirming: false, - activeDialog: false, - }); + const [oldPIN, setOldPIN] = useState(null); + const [newPIN, setNewPIN] = useState(null); + const [errorCode, setErrorCode] = useState(null); + const [isConfirming, setIsConfirming] = useState(false); + const [activeDialog, setActiveDialog] = useState(false); + + const abort = () => { + setOldPIN(null); + setNewPIN(null); + setIsConfirming(false); + setActiveDialog(false); + setErrorCode(null); }; - validate = () => { - return this.state.newPIN && this.state.oldPIN; + const validate = () => { + return Boolean(newPIN && oldPIN); }; - changePin = event => { + const changePin = async (event: FormEvent) => { event.preventDefault(); - if (!this.validate()) { - return; - } - this.setState({ - activeDialog: false, - isConfirming: true, - }); - apiPost('devices/' + this.props.deviceID + '/change-password', { - oldPIN: this.state.oldPIN, - newPIN: this.state.newPIN, - }).catch(() => {}).then(data => { - this.abort(); + if (!validate()) return; + + setActiveDialog(false); + setIsConfirming(true); + + try { + const data = await apiPost(`devices/${deviceID}/change-password`, { + oldPIN, + newPIN, + }); + + abort(); + if (!data.success) { - alertUser(this.props.t(`bitbox.error.e${data.code}`, { - defaultValue: data.errorMessage, - })); + alertUser( + t(`bitbox.error.e${data.code}`, { + defaultValue: data.errorMessage, + }) + ); + setErrorCode(data.code); } - }); + } catch (error) { + abort(); + alertUser(String(error)); + } }; - setValidOldPIN = e => { - this.setState({ oldPIN: e.target.value }); + const handleOldPINChange = (e: ChangeEvent) => { + setOldPIN(e.target.value); }; - setValidNewPIN = newPIN => { - this.setState({ newPIN }); + const handleNewPINValid = (pin: string | null) => { + setNewPIN(pin); }; - render() { - const { - t, - disabled, - } = this.props; - const { - oldPIN, - isConfirming, - activeDialog, - } = this.state; - return ( -
- this.setState({ activeDialog: true })}> - {t('button.changepin')} - - { - activeDialog && ( - -
- - {t('changePin.newTitle') &&

{t('changePin.newTitle')}

} - - - - - - -
- ) - } - { - isConfirming && ( - - ) - } -
- ); - } -} + return ( +
+ setActiveDialog(true)}> + {t('button.changepin')} + + + {activeDialog && ( + +
+ + + {t('changePin.newTitle') &&

{t('changePin.newTitle')}

} + + + + + + + + +
+ )} -export default withTranslation()(ChangePIN); + {isConfirming && } +
+ ); +}; diff --git a/frontends/web/src/routes/device/bitbox01/settings/settings.tsx b/frontends/web/src/routes/device/bitbox01/settings/settings.tsx index 6ef025cbc5..3cfabb3586 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/settings.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/settings.tsx @@ -25,7 +25,7 @@ import { Entry } from '../../../../components/guide/entry'; import { Header } from '../../../../components/layout'; import { Spinner } from '../../../../components/spinner/Spinner'; import Blink from './components/blink'; -import ChangePIN from './components/changepin'; +import { ChangePIN } from './components/changepin'; import Reset from './components/reset'; import UpgradeFirmware from '../components/upgradefirmware'; import { SettingsButton } from '../../../../components/settingsButton/settingsButton'; From d0611cd5b9055dc4daacdf49aae41d0a8d4544f0 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 11:58:40 +0200 Subject: [PATCH 11/26] frontend: remove errorcode state The errorCode state was not used, instead it just shows an alert message with either bitbox.error.e${data.code} or just the error. --- .../routes/device/bitbox01/settings/components/changepin.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx index a5e1c5022b..a28aa8c380 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx @@ -35,7 +35,6 @@ export const ChangePIN = ({ deviceID, disabled }: Props) => { const [oldPIN, setOldPIN] = useState(null); const [newPIN, setNewPIN] = useState(null); - const [errorCode, setErrorCode] = useState(null); const [isConfirming, setIsConfirming] = useState(false); const [activeDialog, setActiveDialog] = useState(false); @@ -44,7 +43,6 @@ export const ChangePIN = ({ deviceID, disabled }: Props) => { setNewPIN(null); setIsConfirming(false); setActiveDialog(false); - setErrorCode(null); }; const validate = () => { @@ -72,7 +70,6 @@ export const ChangePIN = ({ deviceID, disabled }: Props) => { defaultValue: data.errorMessage, }) ); - setErrorCode(data.code); } } catch (error) { abort(); From 7979dd0657bf50569f6c5b32e94f873835127314 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:01:15 +0200 Subject: [PATCH 12/26] frontend: move bitbox01 seed-create-new.jsx to .tsx Moving first to have a nicer diff in the next commit. --- .../bitbox01/setup/{seed-create-new.jsx => seed-create-new.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontends/web/src/routes/device/bitbox01/setup/{seed-create-new.jsx => seed-create-new.tsx} (100%) diff --git a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.jsx b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx similarity index 100% rename from frontends/web/src/routes/device/bitbox01/setup/seed-create-new.jsx rename to frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx From 74d8094a3f5e0eacfaa5f0a7cd06da398565c9bd Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:09:41 +0200 Subject: [PATCH 13/26] frontend: convert bb01 seed-create-new to functional typescript --- .../device/bitbox01/setup/seed-create-new.tsx | 424 +++++++++--------- 1 file changed, 213 insertions(+), 211 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx index 17f90e5435..a1340d884a 100644 --- a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx +++ b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx @@ -1,6 +1,6 @@ /** * Copyright 2018 Shift Devices AG - * Copyright 2022 Shift Crypto AG + * Copyright 2022-2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,18 @@ * limitations under the License. */ -import { Component, createRef } from 'react'; -import { withTranslation } from 'react-i18next'; -import { getDeviceInfo } from '../../../../api/bitbox01'; -import { apiPost } from '../../../../utils/request'; -import { PasswordRepeatInput } from '../../../../components/password'; -import { Button, Input, Checkbox } from '../../../../components/forms'; -import { Message } from '../../../../components/message/message'; -import { SwissMadeOpenSource, SwissMadeOpenSourceDark, Alert, Warning } from '../../../../components/icon'; -import { Header } from '../../../../components/layout'; -import { Spinner } from '../../../../components/spinner/Spinner'; -import { LanguageSwitch } from '../../../../components/language/language'; -import { getDarkmode } from '../../../../components/darkmode/darkmode'; +import { useEffect, useRef, useState, ChangeEvent, FormEvent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getDeviceInfo } from '@/api/bitbox01'; +import { apiPost } from '@/utils/request'; +import { PasswordRepeatInput } from '@/components/password'; +import { Button, Input, Checkbox } from '@/components/forms'; +import { Message } from '@/components/message/message'; +import { SwissMadeOpenSource, SwissMadeOpenSourceDark, Alert, Warning } from '@/components/icon'; +import { Header } from '@/components/layout'; +import { Spinner } from '@/components/spinner/Spinner'; +import { LanguageSwitch } from '@/components/language/language'; +import { getDarkmode } from '@/components/darkmode/darkmode'; import style from '../bitbox01.module.css'; const STATUS = Object.freeze({ @@ -36,229 +36,231 @@ const STATUS = Object.freeze({ ERROR: 'error', }); -class SeedCreateNew extends Component { - state = { - showInfo: true, - status: STATUS.CHECKING, - walletName: '', - backupPassword: '', - error: '', - agreements: { - password_change: false, - password_required: false, - funds_access: false, - }, - }; +type Props = { + deviceID: string; + goBack: () => void; + onSuccess: () => void; +} - walletNameInput = createRef(); +type Agreements = { + password_change: boolean; + password_required: boolean; + funds_access: boolean; +} - componentDidMount () { - this.checkSDcard(); - } +export const SeedCreateNew = ({ + deviceID, + goBack, + onSuccess, +}: Props) => { + const { t } = useTranslation(); - validate = () => { - if (!this.walletNameInput.current || !this.walletNameInput.current.validity.valid || !this.validAgreements()) { - return false; - } - return this.state.backupPassword && this.state.walletName !== ''; - }; + const [showInfo, setShowInfo] = useState(true); + const [status, setStatus] = useState(STATUS.CHECKING); + const [walletName, setWalletName] = useState(''); + const [backupPassword, setBackupPassword] = useState(''); + const [error, setError] = useState(''); + const [agreements, setAgreements] = useState({ + password_change: false, + password_required: false, + funds_access: false, + }); + + const walletNameInput = useRef(null); + + // --- Lifecycle --- + useEffect(() => { + checkSDcard(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - handleFormChange = ({ target }) => { - this.setState({ [target.id]: target.value }); + // --- Handlers --- + const handleFormChange = (event: ChangeEvent) => { + const { id, value } = event.target; + if (id === 'walletName') setWalletName(value); }; - handleSubmit = event => { - event.preventDefault(); - if (!this.validate()) { - return; - } - this.setState({ status: STATUS.CREATING, error: '' }); - apiPost('devices/' + this.props.deviceID + '/create-wallet', { - walletName: this.state.walletName, - backupPassword: this.state.backupPassword - }).then(data => { - if (!data.success) { - this.setState({ - status: STATUS.ERROR, - error: this.props.t(`seed.error.e${data.code}`, { - defaultValue: data.errorMessage - }), - }); - } else { - this.props.onSuccess(); - } - this.setState({ backupPassword: '' }); - }); + const handleAgreementChange = (event: ChangeEvent) => { + const { id, checked } = event.target; + setAgreements(prev => ({ ...prev, [id]: checked })); }; - setValidBackupPassword = backupPassword => { - this.setState({ backupPassword }); + const setValidBackupPassword = (password: string | null) => { + setBackupPassword(password === null ? '': password); }; - validAgreements = () => { - const { agreements } = this.state; - const invalid = Object.keys(agreements).map(agr => agreements[agr]).includes(false); - return !invalid; + const validAgreements = () => { + return Object.values(agreements).every(Boolean); }; - handleAgreementChange = ({ target }) => { - this.setState(state => ({ agreements: { - ...state.agreements, - [target.id]: target.checked - } })); + const validate = () => { + const walletInput = walletNameInput.current; + if (!walletInput || !walletInput.validity.valid || !validAgreements()) { + return false; + } + return backupPassword.trim() !== '' && walletName.trim() !== ''; }; - checkSDcard = () => { - getDeviceInfo(this.props.deviceID) - .then((deviceInfo) => { - if (deviceInfo?.sdcard) { - return this.setState({ status: STATUS.DEFAULT, error: '' }); - } - this.setState({ - status: STATUS.ERROR, - error: this.props.t('seed.error.e200'), - }); - setTimeout(this.checkSDcard, 2500); + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + if (!validate()) return; + + setStatus(STATUS.CREATING); + setError(''); + + try { + const data = await apiPost(`devices/${deviceID}/create-wallet`, { + walletName, + backupPassword, }); + + if (!data.success) { + setStatus(STATUS.ERROR); + setError( + t(`seed.error.e${data.code}`, { + defaultValue: data.errorMessage, + }) + ); + } else { + onSuccess(); + } + } catch (err) { + setStatus(STATUS.ERROR); + setError(String(err)); + } finally { + setBackupPassword(''); + } + }; + + const checkSDcard = async () => { + try { + const deviceInfo = await getDeviceInfo(deviceID); + if (deviceInfo?.sdcard) { + setStatus(STATUS.DEFAULT); + setError(''); + } else { + setStatus(STATUS.ERROR); + setError(t('seed.error.e200')); + setTimeout(checkSDcard, 2500); + } + } catch { + setStatus(STATUS.ERROR); + setError(t('seed.error.e200')); + setTimeout(checkSDcard, 2500); + } }; - handleStart = () => { - this.setState({ showInfo: false }); - this.checkSDcard(); + const handleStart = () => { + setShowInfo(false); + checkSDcard(); }; - renderSpinner() { - switch (this.state.status) { - case STATUS.CHECKING: - return ( - - ); - case STATUS.CREATING: - return ( - - ); - default: - return null; + const renderSpinner = () => { + switch (status) { + case STATUS.CHECKING: + return ; + case STATUS.CREATING: + return ; + default: + return null; } - } + }; - render() { - const { - t, - goBack, - } = this.props; - const { - showInfo, - status, - walletName, - error, - agreements, - } = this.state; - const content = showInfo ? ( -
-
    -
  1. {t('seed.info.description1')}
  2. -
  3. {t('seed.info.description2')}
  4. -
-

{t('seed.info.description3')}

-

{t('seed.info.description4')}

-
- - -
+ const content = showInfo ? ( +
+
    +
  1. {t('seed.info.description1')}
  2. +
  3. {t('seed.info.description2')}
  4. +
+

{t('seed.info.description3')}

+

{t('seed.info.description4')}

+
+ +
- ) : ( -
-
- - -
-
-
- -

{t('seed.description')}

-
- - - -
-
- - +
+ ) : ( + +
+ + +
+
+
+ +

{t('seed.description')}

- - ); + + + +
+
+ + +
+ + ); - return ( -
-
-
-
{t('welcome.title')}}> - -
-
-

{t('seed.info.title')}

- { - error && ( - - - { error } - - ) - } - {content} -
- {getDarkmode() ? : } -
+ return ( +
+
+
+
{t('welcome.title')}}> + +
+
+

{t('seed.info.title')}

+ {error && ( + + + {error} + + )} + {content} +
+ {getDarkmode() ? : }
- { this.renderSpinner() }
+ {renderSpinner()}
- ); - } -} - -export default withTranslation()(SeedCreateNew); +
+ ); +}; From c55163afa263fa224a10ae53e9b6fad606be07ee Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:10:03 +0200 Subject: [PATCH 14/26] frontend: fix typescript error --- .../web/src/routes/device/bitbox01/setup/seed-create-new.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx index a1340d884a..c32aff7da4 100644 --- a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx +++ b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx @@ -158,9 +158,9 @@ export const SeedCreateNew = ({ const renderSpinner = () => { switch (status) { case STATUS.CHECKING: - return ; + return ; case STATUS.CREATING: - return ; + return ; default: return null; } From 2859ecff33b5f6011b6f4d468d87f13e0bdedd9f Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:13:00 +0200 Subject: [PATCH 15/26] frontend: move bitbox01 check.jsx to .tsx Moving first to have a nicer diff in the next commit. --- frontends/web/src/routes/device/bitbox01/{check.jsx => check.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontends/web/src/routes/device/bitbox01/{check.jsx => check.tsx} (100%) diff --git a/frontends/web/src/routes/device/bitbox01/check.jsx b/frontends/web/src/routes/device/bitbox01/check.tsx similarity index 100% rename from frontends/web/src/routes/device/bitbox01/check.jsx rename to frontends/web/src/routes/device/bitbox01/check.tsx From ff7b70d08adec2d56f6551fe388983abac187e19 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:16:41 +0200 Subject: [PATCH 16/26] frontend: convert bb01 check to functional typescript --- .../src/routes/device/bitbox01/backups.tsx | 4 +- .../web/src/routes/device/bitbox01/check.tsx | 187 +++++++++--------- 2 files changed, 90 insertions(+), 101 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/backups.tsx b/frontends/web/src/routes/device/bitbox01/backups.tsx index fa2fcadaf0..dda01994c1 100644 --- a/frontends/web/src/routes/device/bitbox01/backups.tsx +++ b/frontends/web/src/routes/device/bitbox01/backups.tsx @@ -24,10 +24,10 @@ import { SimpleMarkup } from '../../../utils/markup'; import { alertUser } from '../../../components/alert/Alert'; import { Button } from '../../../components/forms'; import { BackupsListItem } from '../components/backup'; -import style from '../components/backups.module.css'; -import Check from './check'; +import { Check } from './check'; import { Create } from './create'; import { Restore } from './restore'; +import style from '../components/backups.module.css'; type BackupsProps = { deviceID: string; diff --git a/frontends/web/src/routes/device/bitbox01/check.tsx b/frontends/web/src/routes/device/bitbox01/check.tsx index f34a14270d..3ab0de0a26 100644 --- a/frontends/web/src/routes/device/bitbox01/check.tsx +++ b/frontends/web/src/routes/device/bitbox01/check.tsx @@ -1,5 +1,6 @@ /** * Copyright 2018 Shift Devices AG + * Copyright 2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,121 +15,109 @@ * limitations under the License. */ -import { Component } from 'react'; -import { withTranslation } from 'react-i18next'; -import { Button } from '../../../components/forms'; -import { DialogLegacy } from '../../../components/dialog/dialog-legacy'; -import { PasswordSingleInput } from '../../../components/password'; -import { apiPost } from '../../../utils/request'; +import { useState, FormEvent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/forms'; +import { DialogLegacy } from '@/components/dialog/dialog-legacy'; +import { PasswordSingleInput } from '@/components/password'; +import { apiPost } from '@/utils/request'; // TODO: use DialogButtons import style from '../../../components/dialog/dialog.module.css'; -class Check extends Component { - state = { - password: null, - activeDialog: false, - message: null, - }; +type Props = { + deviceID: string; + selectedBackup?: string; +} - abort = () => { - this.setState({ - password: null, - activeDialog: false, - message: null, - }); - }; +export const Check = ({ deviceID, selectedBackup }: Props) => { + const { t } = useTranslation(); + + const [password, setPassword] = useState(null); + const [activeDialog, setActiveDialog] = useState(false); + const [message, setMessage] = useState(null); - handleFormChange = event => { - this.setState({ [event.target.id]: event.target.value }); + const abort = () => { + setPassword(null); + setActiveDialog(false); + setMessage(null); }; - validate = () => { - return this.props.selectedBackup && this.state.password; + const validate = () => { + return Boolean(selectedBackup && password); }; - check = event => { + const handleCheck = async (event: FormEvent) => { event.preventDefault(); - if (!this.validate()) { - return; - } - this.setState({ message: this.props.t('backup.check.checking') }); + if (!validate()) return; - apiPost('devices/' + this.props.deviceID + '/backups/check', { - password: this.state.password, - filename: this.props.selectedBackup, - }).catch(() => {}).then(({ success, matches, errorMessage }) => { - let message; - if (success) { - if (matches) { - message = this.props.t('backup.check.ok'); - } else { - message = this.props.t('backup.check.notOK'); + setMessage(t('backup.check.checking')); + + try { + const { success, matches, errorMessage } = await apiPost( + `devices/${deviceID}/backups/check`, + { + password, + filename: selectedBackup, } + ); + + if (success) { + setMessage(matches ? t('backup.check.ok') : t('backup.check.notOK')); } else if (errorMessage) { - message = errorMessage; + setMessage(errorMessage); + } else { + setMessage(t('backup.check.error')); } - this.setState({ message }); - }); + } catch (err) { + setMessage(String(err)); + } }; - setValidPassword = password => { - this.setState({ password }); + const handleValidPassword = (pwd: string | null) => { + setPassword(pwd); }; - render() { - const { - t, - selectedBackup, - } = this.props; - const { - activeDialog, - message, - } = this.state; - return ( -
- - { - activeDialog && ( - - { message ? ( -
-

{message}

-
- -
-
- ) : ( -
- -
- - -
- - )} -
- ) - } -
- ); - } -} + return ( +
+ -export default withTranslation()(Check); + {activeDialog && ( + + {message ? ( +
+

{message}

+
+ +
+
+ ) : ( +
+ +
+ + +
+ + )} +
+ )} +
+ ); +}; From 25ea7bee90069f23e5706a111978a443cd522aec Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:21:25 +0200 Subject: [PATCH 17/26] frontend: use dialogbuttons in bb01 check --- frontends/web/src/routes/device/bitbox01/check.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/check.tsx b/frontends/web/src/routes/device/bitbox01/check.tsx index 3ab0de0a26..c0c958dc0a 100644 --- a/frontends/web/src/routes/device/bitbox01/check.tsx +++ b/frontends/web/src/routes/device/bitbox01/check.tsx @@ -18,11 +18,9 @@ import { useState, FormEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/forms'; -import { DialogLegacy } from '@/components/dialog/dialog-legacy'; +import { DialogLegacy, DialogButtons } from '@/components/dialog/dialog-legacy'; import { PasswordSingleInput } from '@/components/password'; import { apiPost } from '@/utils/request'; -// TODO: use DialogButtons -import style from '../../../components/dialog/dialog.module.css'; type Props = { deviceID: string; @@ -92,11 +90,11 @@ export const Check = ({ deviceID, selectedBackup }: Props) => { {message ? (

{message}

-
+ -
+
) : (
@@ -106,14 +104,14 @@ export const Check = ({ deviceID, selectedBackup }: Props) => { showLabel={t('backup.check.password.showLabel')} onValidPassword={handleValidPassword} /> -
+ -
+
)} From 38590d3dd627c6ec0d8dfa6fb052cc6c9606831c Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:23:25 +0200 Subject: [PATCH 18/26] frontend: move bitbox01 unlock.jsx to unlock.tsx Moving first to have a nicer diff in the next commit. --- .../web/src/routes/device/bitbox01/{unlock.jsx => unlock.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontends/web/src/routes/device/bitbox01/{unlock.jsx => unlock.tsx} (100%) diff --git a/frontends/web/src/routes/device/bitbox01/unlock.jsx b/frontends/web/src/routes/device/bitbox01/unlock.tsx similarity index 100% rename from frontends/web/src/routes/device/bitbox01/unlock.jsx rename to frontends/web/src/routes/device/bitbox01/unlock.tsx From 8a40b0aae2d1dc22acaf3246dbfe82de4c830175 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:31:52 +0200 Subject: [PATCH 19/26] frontend: convert bb01 unlock to functional typescript --- .../web/src/routes/device/bitbox01/unlock.tsx | 234 +++++++++--------- 1 file changed, 123 insertions(+), 111 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/unlock.tsx b/frontends/web/src/routes/device/bitbox01/unlock.tsx index 41b6a8bee4..497c83971c 100644 --- a/frontends/web/src/routes/device/bitbox01/unlock.tsx +++ b/frontends/web/src/routes/device/bitbox01/unlock.tsx @@ -1,5 +1,6 @@ /** * Copyright 2018 Shift Devices AG + * Copyright 2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,93 +15,102 @@ * limitations under the License. */ -import { Component } from 'react'; -import { route } from '../../../utils/route'; -import { apiGet, apiPost } from '../../../utils/request'; -import { Button } from '../../../components/forms'; -import { PasswordSingleInput } from '../../../components/password'; -import { Message } from '../../../components/message/message'; -import { AppLogo, AppLogoInverted, SwissMadeOpenSource, SwissMadeOpenSourceDark } from '../../../components/icon/logo'; -import { Guide } from '../../../components/guide/guide'; -import { Entry } from '../../../components/guide/entry'; -import { Header, Footer } from '../../../components/layout'; -import { Spinner } from '../../../components/spinner/Spinner'; -import { withTranslation } from 'react-i18next'; -import { getDarkmode } from '../../../components/darkmode/darkmode'; +import { useState, FormEvent, useCallback, ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { route } from '@/utils/route'; +import { apiGet, apiPost } from '@/utils/request'; +import { Button } from '@/components/forms'; +import { PasswordSingleInput } from '@/components/password'; +import { Message } from '@/components/message/message'; +import { AppLogo, AppLogoInverted, SwissMadeOpenSource, SwissMadeOpenSourceDark } from '@/components/icon/logo'; +import { Guide } from '@/components/guide/guide'; +import { Entry } from '@/components/guide/entry'; +import { Header, Footer } from '@/components/layout'; +import { Spinner } from '@/components/spinner/Spinner'; +import { getDarkmode } from '@/components/darkmode/darkmode'; const stateEnum = Object.freeze({ DEFAULT: 'default', WAITING: 'waiting', - ERROR: 'error' + ERROR: 'error', }); -class Unlock extends Component { - state = { - status: stateEnum.DEFAULT, - errorMessage: '', - errorCode: null, - remainingAttempts: null, - needsLongTouch: false, - password: '', - }; +type Props = { + deviceID: string; +} - handleFormChange = password => { - this.setState({ password }); - }; +type UnlockResponse = { + success: true; +} | { + success: false; + code?: number; + errorMessage?: string; + remainingAttempts?: number; + needsLongTouch?: boolean; +} + +export const Unlock = ({ deviceID }: Props) => { + const { t } = useTranslation(); + + const [status, setStatus] = useState(stateEnum.DEFAULT); + const [errorMessage, setErrorMessage] = useState(''); + const [errorCode, setErrorCode] = useState(null); + const [remainingAttempts, setRemainingAttempts] = useState(null); + const [needsLongTouch, setNeedsLongTouch] = useState(false); + const [password, setPassword] = useState(''); - validate = () => { - return this.state.password !== ''; + const validate = useCallback(() => password.trim() !== '', [password]); + + const handlePasswordChange = (pwd: string | null) => { + setPassword(pwd === null ? '' : pwd); }; - handleSubmit = event => { + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - if (!this.validate()) { - return; - } - this.setState({ - status: stateEnum.WAITING - }); - apiPost('devices/' + this.props.deviceID + '/login', { password: this.state.password }).then(data => { + if (!validate()) return; + + setStatus(stateEnum.WAITING); + + try { + const data: UnlockResponse = await apiPost(`devices/${deviceID}/login`, { password }); + if (data.success) { - apiGet('devices/' + this.props.deviceID + '/status').then(status => { - if (status === 'seeded') { - console.info('unlock.jsx route to /account-summary'); - route('/account-summary', true); - } - }); - } - if (!data.success) { + const deviceStatus = await apiGet(`devices/${deviceID}/status`); + if (deviceStatus === 'seeded') { + console.info('unlock.tsx route to /account-summary'); + route('/account-summary', true); + } + } else { if (data.code) { - this.setState({ errorCode: data.code }); + setErrorCode(data.code); } - if (data.remainingAttempts) { - this.setState({ remainingAttempts: data.remainingAttempts }); + if (data.remainingAttempts !== undefined) { + setRemainingAttempts(data.remainingAttempts); } - if (data.needsLongTouch) { - this.setState({ needsLongTouch: data.needsLongTouch }); + if (data.needsLongTouch !== undefined) { + setNeedsLongTouch(data.needsLongTouch); } - this.setState({ status: stateEnum.ERROR, errorMessage: data.errorMessage }); + + setErrorMessage(data.errorMessage || ''); + setStatus(stateEnum.ERROR); } - }); - this.setState({ password: '' }); + } catch (err) { + setErrorMessage(String(err)); + setStatus(stateEnum.ERROR); + } finally { + setPassword(''); + } }; - render() { - const { t } = this.props; - const { - status, - errorCode, - errorMessage, - remainingAttempts, - needsLongTouch, - } = this.state; - let submissionState = null; - switch (status) { + const darkmode = getDarkmode(); + + let submissionState: ReactNode = null; + switch (status) { case stateEnum.DEFAULT: submissionState =

{t('unlock.description')}

; break; case stateEnum.WAITING: - submissionState = ; + submissionState = ; break; case stateEnum.ERROR: submissionState = ( @@ -108,61 +118,63 @@ class Unlock extends Component { {t(`unlock.error.e${errorCode}`, { defaultValue: errorMessage, remainingAttempts, - context: needsLongTouch ? 'touch' : 'normal' + context: needsLongTouch ? 'touch' : 'normal', })} ); break; default: break; - } + } - const darkmode = getDarkmode(); - return ( -
-
-
-
{t('welcome.title')}} /> -
- {darkmode ? : } -
- {submissionState} - { - status !== stateEnum.WAITING && ( -
-
- -
-
- -
-
- ) - } -
+ return ( +
+
+
+
{t('welcome.title')}} /> +
+ {darkmode ? : } +
+ {submissionState} + {status !== stateEnum.WAITING && ( +
+
+ +
+
+ +
+
+ )}
-
- {darkmode ? : } -
+
+ {darkmode ? : } +
- - - -
- ); - } -} - -export default withTranslation()(Unlock); + + + + +
+ ); +}; From 565f2c76c6d70a32e6636a7617a2ed0d1aa49ff3 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:36:16 +0200 Subject: [PATCH 20/26] frontend: fix eslint errors Mostly by just running make webfix --- .../web/src/routes/device/bitbox01/check.tsx | 4 +- .../web/src/routes/device/bitbox01/create.tsx | 12 ++++-- .../settings/components/changepin.tsx | 6 ++- .../device/bitbox01/setup/seed-create-new.tsx | 24 ++++++----- .../web/src/routes/device/bitbox01/unlock.tsx | 42 ++++++++++--------- 5 files changed, 52 insertions(+), 36 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/check.tsx b/frontends/web/src/routes/device/bitbox01/check.tsx index c0c958dc0a..4064413b07 100644 --- a/frontends/web/src/routes/device/bitbox01/check.tsx +++ b/frontends/web/src/routes/device/bitbox01/check.tsx @@ -46,7 +46,9 @@ export const Check = ({ deviceID, selectedBackup }: Props) => { const handleCheck = async (event: FormEvent) => { event.preventDefault(); - if (!validate()) return; + if (!validate()) { + return; + } setMessage(t('backup.check.checking')); diff --git a/frontends/web/src/routes/device/bitbox01/create.tsx b/frontends/web/src/routes/device/bitbox01/create.tsx index 3a48a73e75..8ed38f7fbb 100644 --- a/frontends/web/src/routes/device/bitbox01/create.tsx +++ b/frontends/web/src/routes/device/bitbox01/create.tsx @@ -45,8 +45,12 @@ export const Create = ({ deviceID, onCreate }: Props) => { const handleFormChange = (event: ChangeEvent) => { const { id, value } = event.target; - if (id === 'backupName') setBackupName(value); - if (id === 'recoveryPassword') setRecoveryPassword(value); + if (id === 'backupName') { + setBackupName(value); + } + if (id === 'recoveryPassword') { + setRecoveryPassword(value); + } }; const validate = () => { @@ -55,7 +59,9 @@ export const Create = ({ deviceID, onCreate }: Props) => { const create = async (event: FormEvent) => { event.preventDefault(); - if (!validate()) return; + if (!validate()) { + return; + } setWaiting(true); try { diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx index a28aa8c380..1c7042bb25 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx @@ -51,7 +51,9 @@ export const ChangePIN = ({ deviceID, disabled }: Props) => { const changePin = async (event: FormEvent) => { event.preventDefault(); - if (!validate()) return; + if (!validate()) { + return; + } setActiveDialog(false); setIsConfirming(true); @@ -66,7 +68,7 @@ export const ChangePIN = ({ deviceID, disabled }: Props) => { if (!data.success) { alertUser( - t(`bitbox.error.e${data.code}`, { + t(`bitbox.error.e${data.code as string}`, { defaultValue: data.errorMessage, }) ); diff --git a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx index c32aff7da4..79fbabec0e 100644 --- a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx +++ b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx @@ -77,7 +77,9 @@ export const SeedCreateNew = ({ // --- Handlers --- const handleFormChange = (event: ChangeEvent) => { const { id, value } = event.target; - if (id === 'walletName') setWalletName(value); + if (id === 'walletName') { + setWalletName(value); + } }; const handleAgreementChange = (event: ChangeEvent) => { @@ -86,7 +88,7 @@ export const SeedCreateNew = ({ }; const setValidBackupPassword = (password: string | null) => { - setBackupPassword(password === null ? '': password); + setBackupPassword(password === null ? '' : password); }; const validAgreements = () => { @@ -103,7 +105,9 @@ export const SeedCreateNew = ({ const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - if (!validate()) return; + if (!validate()) { + return; + } setStatus(STATUS.CREATING); setError(''); @@ -117,7 +121,7 @@ export const SeedCreateNew = ({ if (!data.success) { setStatus(STATUS.ERROR); setError( - t(`seed.error.e${data.code}`, { + t(`seed.error.e${data.code as string}`, { defaultValue: data.errorMessage, }) ); @@ -157,12 +161,12 @@ export const SeedCreateNew = ({ const renderSpinner = () => { switch (status) { - case STATUS.CHECKING: - return ; - case STATUS.CREATING: - return ; - default: - return null; + case STATUS.CHECKING: + return ; + case STATUS.CREATING: + return ; + default: + return null; } }; diff --git a/frontends/web/src/routes/device/bitbox01/unlock.tsx b/frontends/web/src/routes/device/bitbox01/unlock.tsx index 497c83971c..478d5ab792 100644 --- a/frontends/web/src/routes/device/bitbox01/unlock.tsx +++ b/frontends/web/src/routes/device/bitbox01/unlock.tsx @@ -67,7 +67,9 @@ export const Unlock = ({ deviceID }: Props) => { const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - if (!validate()) return; + if (!validate()) { + return; + } setStatus(stateEnum.WAITING); @@ -106,25 +108,25 @@ export const Unlock = ({ deviceID }: Props) => { let submissionState: ReactNode = null; switch (status) { - case stateEnum.DEFAULT: - submissionState =

{t('unlock.description')}

; - break; - case stateEnum.WAITING: - submissionState = ; - break; - case stateEnum.ERROR: - submissionState = ( - - {t(`unlock.error.e${errorCode}`, { - defaultValue: errorMessage, - remainingAttempts, - context: needsLongTouch ? 'touch' : 'normal', - })} - - ); - break; - default: - break; + case stateEnum.DEFAULT: + submissionState =

{t('unlock.description')}

; + break; + case stateEnum.WAITING: + submissionState = ; + break; + case stateEnum.ERROR: + submissionState = ( + + {t(`unlock.error.e${errorCode || ''}`, { + defaultValue: errorMessage, + remainingAttempts, + context: needsLongTouch ? 'touch' : 'normal', + })} + + ); + break; + default: + break; } return ( From 555f2c8dabbaa23a15b39b416a8f5fe82cd51693 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:37:08 +0200 Subject: [PATCH 21/26] frontend: move bitbox01 reset.jsx to .tsx Moving first to have a nicer diff in the next commit. --- .../device/bitbox01/settings/components/{reset.jsx => reset.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontends/web/src/routes/device/bitbox01/settings/components/{reset.jsx => reset.tsx} (100%) diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/reset.jsx b/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx similarity index 100% rename from frontends/web/src/routes/device/bitbox01/settings/components/reset.jsx rename to frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx From bd5ab716eec68ef83c2c5bb53c9c963d4355929e Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 12:41:47 +0200 Subject: [PATCH 22/26] frontend: convert bitbox01 reset to functional typescript --- .../bitbox01/settings/components/reset.tsx | 186 +++++++++--------- .../device/bitbox01/settings/settings.tsx | 2 +- 2 files changed, 92 insertions(+), 96 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx b/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx index 060a3eede6..e7b498aa18 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx @@ -1,6 +1,6 @@ /** * Copyright 2018 Shift Devices AG - * Copyright 2021 Shift Crypto AG + * Copyright 2021-2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,113 +14,109 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { Component } from 'react'; -import { route } from '../../../../../utils/route'; -import { withTranslation } from 'react-i18next'; -import { Button, Checkbox } from '../../../../../components/forms'; -import { DialogLegacy, DialogButtons } from '../../../../../components/dialog/dialog-legacy'; -import { WaitDialog } from '../../../../../components/wait-dialog/wait-dialog'; -import { PasswordInput } from '../../../../../components/password'; -import { apiPost } from '../../../../../utils/request'; -import { alertUser } from '../../../../../components/alert/Alert'; +import { useState, ChangeEvent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { route } from '@/utils/route'; +import { Button, Checkbox } from '@/components/forms'; +import { DialogLegacy, DialogButtons } from '@/components/dialog/dialog-legacy'; +import { WaitDialog } from '@/components/wait-dialog/wait-dialog'; +import { PasswordInput } from '@/components/password'; +import { apiPost } from '@/utils/request'; +import { alertUser } from '@/components/alert/Alert'; +import { SettingsButton } from '@/components/settingsButton/settingsButton'; import style from '../../bitbox01.module.css'; -import { SettingsButton } from '../../../../../components/settingsButton/settingsButton'; - -class Reset extends Component { - state = { - pin: null, - isConfirming: false, - activeDialog: false, - understand: false, + +type Props = { + deviceID: string; +} + +export const Reset = ({ deviceID }: Props) => { + const { t } = useTranslation(); + + const [pin, setPin] = useState(null); + const [understand, setUnderstand] = useState(false); + const [activeDialog, setActiveDialog] = useState(false); + const [isConfirming, setIsConfirming] = useState(false); + + const handleUnderstandChange = (e: ChangeEvent) => { + setUnderstand(e.target.checked); + }; + + const setValidPIN = (e: ChangeEvent) => { + setPin(e.target.value); }; - handleUnderstandChange = (e) => { - this.setState({ understand: e.target.checked }); + const abort = () => { + setPin(null); + setUnderstand(false); + setActiveDialog(false); + setIsConfirming(false); }; - resetDevice = () => { - this.setState({ - activeDialog: false, - isConfirming: true, - }); - apiPost('devices/' + this.props.deviceID + '/reset', { pin: this.state.pin }).then(data => { - this.abort(); + const resetDevice = async () => { + setActiveDialog(false); + setIsConfirming(true); + + try { + const data = await apiPost(`devices/${deviceID}/reset`, { pin }); + + abort(); + if (data.success) { if (data.didReset) { route('/', true); } } else if (data.errorMessage) { - alertUser(this.props.t(`bitbox.error.e${data.code}`, { - defaultValue: data.errorMessage, - })); + alertUser( + t(`bitbox.error.e${data.code as string}`, { + defaultValue: data.errorMessage, + }) + ); } - }); + } catch (err) { + abort(); + alertUser(String(err)); + } }; - setValidPIN = e => { - this.setState({ pin: e.target.value }); - }; + return ( +
+ setActiveDialog(true)}> + {t('reset.title')} + - abort = () => { - this.setState({ - pin: null, - understand: false, - isConfirming: false, - activeDialog: false, - }); - }; + {activeDialog && ( + +

{t('reset.description')}

- render() { - const { t } = this.props; - const { - isConfirming, - activeDialog, - understand, - pin, - } = this.state; - return ( -
- this.setState({ activeDialog: true })}> - {t('reset.title')} - - { - activeDialog && ( - -

- {t('reset.description')} -

- -
- -
- - - - -
- ) - } - { isConfirming ? ( - - ) : null } -
- ); - } -} + + +
+ +
+ + + + + +
+ )} -export default withTranslation()(Reset); + {isConfirming && } +
+ ); +}; diff --git a/frontends/web/src/routes/device/bitbox01/settings/settings.tsx b/frontends/web/src/routes/device/bitbox01/settings/settings.tsx index 3cfabb3586..b7a4ec8491 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/settings.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/settings.tsx @@ -26,7 +26,7 @@ import { Header } from '../../../../components/layout'; import { Spinner } from '../../../../components/spinner/Spinner'; import Blink from './components/blink'; import { ChangePIN } from './components/changepin'; -import Reset from './components/reset'; +import { Reset } from './components/reset'; import UpgradeFirmware from '../components/upgradefirmware'; import { SettingsButton } from '../../../../components/settingsButton/settingsButton'; import { SettingsItem } from '../../../../components/settingsButton/settingsItem'; From 72b728f306c855d7c08c746425141a7fd182a659 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 13:37:55 +0200 Subject: [PATCH 23/26] frontend: move bitbox01 to .tsx Moving first to have a nicer diff in the next commit. --- .../web/src/routes/device/bitbox01/{bitbox01.jsx => bitbox01.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontends/web/src/routes/device/bitbox01/{bitbox01.jsx => bitbox01.tsx} (100%) diff --git a/frontends/web/src/routes/device/bitbox01/bitbox01.jsx b/frontends/web/src/routes/device/bitbox01/bitbox01.tsx similarity index 100% rename from frontends/web/src/routes/device/bitbox01/bitbox01.jsx rename to frontends/web/src/routes/device/bitbox01/bitbox01.tsx From 9010b9de19e3e6b1feac58c1372f2bc1535b5792 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 13:54:21 +0200 Subject: [PATCH 24/26] frontend: convert bitbox01 to functional typescript --- .../src/routes/device/bitbox01/bitbox01.tsx | 215 +++++++++--------- .../web/src/routes/device/deviceswitch.tsx | 9 +- 2 files changed, 111 insertions(+), 113 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/bitbox01.tsx b/frontends/web/src/routes/device/bitbox01/bitbox01.tsx index 46a6e54aeb..81e3cb7e82 100644 --- a/frontends/web/src/routes/device/bitbox01/bitbox01.tsx +++ b/frontends/web/src/routes/device/bitbox01/bitbox01.tsx @@ -1,5 +1,6 @@ /** * Copyright 2018 Shift Devices AG + * Copyright 2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,22 +15,20 @@ * limitations under the License. */ -import { Component } from 'react'; -import { AppUpgradeRequired } from '../../../components/appupgraderequired'; -import { apiGet } from '../../../utils/request'; -import { apiWebsocket } from '../../../utils/websocket'; -import Unlock from './unlock'; +import { useState, useEffect, useCallback } from 'react'; +import { AppUpgradeRequired } from '@/components/appupgraderequired'; +import { apiGet } from '@/utils/request'; +import { apiWebsocket } from '@/utils/websocket'; +import { Unlock } from './unlock'; import Bootloader from './upgrade/bootloader'; import RequireUpgrade from './upgrade/require_upgrade'; import Goal from './setup/goal'; import { SecurityInformation } from './setup/security-information'; -import SeedCreateNew from './setup/seed-create-new'; +import { SeedCreateNew } from './setup/seed-create-new'; import SeedRestore from './setup/seed-restore'; import { Initialize } from './setup/initialize'; import Success from './setup/success'; import Settings from './settings/settings'; -import { withTranslation } from 'react-i18next'; -import { AppContext } from '../../../contexts/AppContext'; const DeviceStatus = Object.freeze({ BOOTLOADER: 'bootloader', @@ -38,125 +37,125 @@ const DeviceStatus = Object.freeze({ LOGGED_IN: 'logged_in', SEEDED: 'seeded', REQUIRE_FIRMWARE_UPGRADE: 'require_firmware_upgrade', - REQUIRE_APP_UPGRADE: 'require_app_upgrade' + REQUIRE_APP_UPGRADE: 'require_app_upgrade', }); const GOAL = Object.freeze({ CREATE: 'create', - RESTORE: 'restore' + RESTORE: 'restore', }); -class Device extends Component { - static contextType = AppContext; +type DeviceStatusType = (typeof DeviceStatus)[keyof typeof DeviceStatus]; +type GoalType = (typeof GOAL)[keyof typeof GOAL]; - state = { - firmwareVersion: null, - deviceStatus: '', - goal: '', - success: null, - }; - - componentDidMount() { - this.onDeviceStatusChanged(); - this.unsubscribe = apiWebsocket(({ type, data, deviceID }) => { - if (type === 'device' && data === 'statusChanged' && deviceID === this.getDeviceID()) { - this.onDeviceStatusChanged(); - } - }); - } +type Props = { + deviceID: string; +} - componentWillUnmount() { - if (this.unsubscribe) { - this.unsubscribe(); - } - } +export const BitBox01 = ({ deviceID }: Props) => { + const [deviceStatus, setDeviceStatus] = useState(''); + const [goal, setGoal] = useState(null); + const [success, setSuccess] = useState(null); - onDeviceStatusChanged = () => { - apiGet('devices/' + this.props.deviceID + '/status').then(deviceStatus => { - this.setState({ deviceStatus }); + // --- Fetch device status --- + const onDeviceStatusChanged = useCallback(() => { + apiGet(`devices/${deviceID}/status`).then((status: DeviceStatusType) => { + setDeviceStatus(status); }); - }; + }, [deviceID]); - getDeviceID() { - return this.props.deviceID || null; - } + useEffect(() => { + onDeviceStatusChanged(); + const unsubscribe = apiWebsocket((data) => { + if ( + 'type' in data // check if TEventLegacy + && data.type === 'device' + && 'data' in data + && data.data === 'statusChanged' + && data.deviceID === deviceID) { + onDeviceStatusChanged(); + } + }); - handleCreate = () => { - this.setState({ goal: GOAL.CREATE }); - }; + return () => { + if (unsubscribe) { + unsubscribe(); + } + }; + }, [deviceID, onDeviceStatusChanged]); - handleRestore = () => { - this.setState({ goal: GOAL.RESTORE }); - }; + const handleCreate = () => setGoal(GOAL.CREATE); + const handleRestore = () => setGoal(GOAL.RESTORE); + const handleBack = () => setGoal(null); + const handleSuccess = () => setSuccess(true); + const handleHideSuccess = () => setSuccess(null); - handleBack = () => { - this.setState({ goal: null }); - }; + if (!deviceStatus) { + return null; + } - handleSuccess = () => { - this.setState({ success: true }); - }; + if (success) { + return ( + + ); + } - render() { - const { - deviceID, - } = this.props; - const { - deviceStatus, - goal, - success, - } = this.state; - if (!deviceStatus) { - return null; - } - if (success) { - return this.setState({ success: null })} />; + switch (deviceStatus) { + case DeviceStatus.BOOTLOADER: + return ( + + ); + case DeviceStatus.REQUIRE_FIRMWARE_UPGRADE: + return ( + + ); + case DeviceStatus.REQUIRE_APP_UPGRADE: + return ( + + ); + case DeviceStatus.INITIALIZED: + return ( + + ); + case DeviceStatus.UNINITIALIZED: + if (!goal) { + return ( + + ); } - switch (deviceStatus) { - case DeviceStatus.BOOTLOADER: - return ; - case DeviceStatus.REQUIRE_FIRMWARE_UPGRADE: - return ; - case DeviceStatus.REQUIRE_APP_UPGRADE: - return ; - case DeviceStatus.INITIALIZED: - return ; - case DeviceStatus.UNINITIALIZED: - if (!goal) { - return ; - } + return ( + + + + ); + case DeviceStatus.LOGGED_IN: + switch (goal) { + case GOAL.CREATE: return ( - - - + + ); + case GOAL.RESTORE: + return ( + ); - case DeviceStatus.LOGGED_IN: - switch (goal) { - case GOAL.CREATE: - return ( - - ); - case GOAL.RESTORE: - return ( - - ); - default: - return ; - } - case DeviceStatus.SEEDED: - return ; default: - return null; + return ( + + ); } + case DeviceStatus.SEEDED: + return ( + + ); + default: + return null; } -} - -export default withTranslation()(Device); +}; diff --git a/frontends/web/src/routes/device/deviceswitch.tsx b/frontends/web/src/routes/device/deviceswitch.tsx index e21237f61c..5fe47a26b1 100644 --- a/frontends/web/src/routes/device/deviceswitch.tsx +++ b/frontends/web/src/routes/device/deviceswitch.tsx @@ -1,5 +1,6 @@ /** * Copyright 2018 Shift Devices AG + * Copyright 2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +15,8 @@ * limitations under the License. */ -import { TDevices } from '@/api/devices'; -import BitBox01 from './bitbox01/bitbox01'; +import type { TDevices } from '@/api/devices'; +import { BitBox01 } from './bitbox01/bitbox01'; import { BitBox02 } from './bitbox02/bitbox02'; import { BitBox02Bootloader } from '@/components/devices/bitbox02bootloader/bitbox02bootloader'; import { Waiting } from './waiting'; @@ -26,7 +27,7 @@ type TProps = { hasAccounts: boolean; }; -const DeviceSwitch = ({ deviceID, devices, hasAccounts }: TProps) => { +export const DeviceSwitch = ({ deviceID, devices, hasAccounts }: TProps) => { const deviceIDs = Object.keys(devices); if (deviceID === null || !deviceIDs.includes(deviceID)) { @@ -50,5 +51,3 @@ const DeviceSwitch = ({ deviceID, devices, hasAccounts }: TProps) => { return ; } }; - -export { DeviceSwitch }; From 043348f20d9a938ccf00d1159fcf875a859d8390 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 9 Oct 2025 14:34:02 +0200 Subject: [PATCH 25/26] frontend: fix password too short message If BB01 password is too short the app used to display an error. Added the too short error back. --- frontends/web/src/components/password.tsx | 2 -- .../routes/device/bitbox01/settings/components/changepin.tsx | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontends/web/src/components/password.tsx b/frontends/web/src/components/password.tsx index c959ba0048..1920c89846 100644 --- a/frontends/web/src/components/password.tsx +++ b/frontends/web/src/components/password.tsx @@ -244,7 +244,6 @@ export const PasswordRepeatInput = ({ disabled={disabled} type={seePlaintext ? 'text' : 'password'} pattern={pattern} - title={title} id="passwordRepeatFirst" label={label} placeholder={placeholder} @@ -264,7 +263,6 @@ export const PasswordRepeatInput = ({ disabled={disabled} type={seePlaintext ? 'text' : 'password'} pattern={pattern} - title={title} id="passwordRepeatSecond" label={repeatLabel} placeholder={repeatPlaceholder} diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx index 1c7042bb25..6280e9dba8 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx @@ -107,6 +107,7 @@ export const ChangePIN = ({ deviceID, disabled }: Props) => { Date: Thu, 16 Oct 2025 14:39:30 +0200 Subject: [PATCH 26/26] frontend: fix new eslint rules by make webfix --- frontends/web/src/routes/device/bitbox01/bitbox01.tsx | 2 +- frontends/web/src/routes/device/bitbox01/check.tsx | 2 +- frontends/web/src/routes/device/bitbox01/create.tsx | 2 +- .../routes/device/bitbox01/settings/components/changepin.tsx | 2 +- .../src/routes/device/bitbox01/settings/components/reset.tsx | 2 +- .../web/src/routes/device/bitbox01/setup/seed-create-new.tsx | 4 ++-- frontends/web/src/routes/device/bitbox01/unlock.tsx | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontends/web/src/routes/device/bitbox01/bitbox01.tsx b/frontends/web/src/routes/device/bitbox01/bitbox01.tsx index 81e3cb7e82..9dca95a63c 100644 --- a/frontends/web/src/routes/device/bitbox01/bitbox01.tsx +++ b/frontends/web/src/routes/device/bitbox01/bitbox01.tsx @@ -50,7 +50,7 @@ type GoalType = (typeof GOAL)[keyof typeof GOAL]; type Props = { deviceID: string; -} +}; export const BitBox01 = ({ deviceID }: Props) => { const [deviceStatus, setDeviceStatus] = useState(''); diff --git a/frontends/web/src/routes/device/bitbox01/check.tsx b/frontends/web/src/routes/device/bitbox01/check.tsx index 4064413b07..f4360c67c3 100644 --- a/frontends/web/src/routes/device/bitbox01/check.tsx +++ b/frontends/web/src/routes/device/bitbox01/check.tsx @@ -25,7 +25,7 @@ import { apiPost } from '@/utils/request'; type Props = { deviceID: string; selectedBackup?: string; -} +}; export const Check = ({ deviceID, selectedBackup }: Props) => { const { t } = useTranslation(); diff --git a/frontends/web/src/routes/device/bitbox01/create.tsx b/frontends/web/src/routes/device/bitbox01/create.tsx index 8ed38f7fbb..6b5c920a29 100644 --- a/frontends/web/src/routes/device/bitbox01/create.tsx +++ b/frontends/web/src/routes/device/bitbox01/create.tsx @@ -26,7 +26,7 @@ import { DialogLegacy, DialogButtons } from '@/components/dialog/dialog-legacy'; type Props = { deviceID: string; onCreate: () => void; -} +}; export const Create = ({ deviceID, onCreate }: Props) => { const { t } = useTranslation(); diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx index 6280e9dba8..50c3f4c9ea 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/components/changepin.tsx @@ -28,7 +28,7 @@ import { SettingsButton } from '@/components/settingsButton/settingsButton'; type Props = { deviceID: string; disabled?: boolean; -} +}; export const ChangePIN = ({ deviceID, disabled }: Props) => { const { t } = useTranslation(); diff --git a/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx b/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx index e7b498aa18..3387f12bb4 100644 --- a/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx +++ b/frontends/web/src/routes/device/bitbox01/settings/components/reset.tsx @@ -28,7 +28,7 @@ import style from '../../bitbox01.module.css'; type Props = { deviceID: string; -} +}; export const Reset = ({ deviceID }: Props) => { const { t } = useTranslation(); diff --git a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx index 79fbabec0e..9d3e778fb7 100644 --- a/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx +++ b/frontends/web/src/routes/device/bitbox01/setup/seed-create-new.tsx @@ -40,13 +40,13 @@ type Props = { deviceID: string; goBack: () => void; onSuccess: () => void; -} +}; type Agreements = { password_change: boolean; password_required: boolean; funds_access: boolean; -} +}; export const SeedCreateNew = ({ deviceID, diff --git a/frontends/web/src/routes/device/bitbox01/unlock.tsx b/frontends/web/src/routes/device/bitbox01/unlock.tsx index 478d5ab792..c0ecfe612d 100644 --- a/frontends/web/src/routes/device/bitbox01/unlock.tsx +++ b/frontends/web/src/routes/device/bitbox01/unlock.tsx @@ -37,7 +37,7 @@ const stateEnum = Object.freeze({ type Props = { deviceID: string; -} +}; type UnlockResponse = { success: true; @@ -47,7 +47,7 @@ type UnlockResponse = { errorMessage?: string; remainingAttempts?: number; needsLongTouch?: boolean; -} +}; export const Unlock = ({ deviceID }: Props) => { const { t } = useTranslation();