From 22609f379f0ec6c571d7ba323dfe43492b27c74f Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 18 May 2026 19:45:52 +0000 Subject: [PATCH] backend/handlers: return bootloader errors in response --- .../bitbox02bootloader/handlers/handlers.go | 73 ++++++++++++++----- backend/handlers/handlers.go | 2 +- frontends/web/src/api/bitbox02bootloader.ts | 24 ++++-- .../bitbox02bootloader/bitbox02bootloader.tsx | 43 +++++++++-- .../toggleshowfirmwarehash.tsx | 25 +++++-- 5 files changed, 131 insertions(+), 36 deletions(-) diff --git a/backend/devices/bitbox02bootloader/handlers/handlers.go b/backend/devices/bitbox02bootloader/handlers/handlers.go index 4c7673e065..d0d5e30b5b 100644 --- a/backend/devices/bitbox02bootloader/handlers/handlers.go +++ b/backend/devices/bitbox02bootloader/handlers/handlers.go @@ -7,7 +7,6 @@ import ( "net/http" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox02bootloader" - "github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" "github.com/BitBoxSwiss/bitbox02-api-go/api/bootloader" "github.com/gorilla/mux" "github.com/sirupsen/logrus" @@ -32,7 +31,7 @@ type Handlers struct { // NewHandlers creates a new Handlers instance. func NewHandlers( - handleFunc func(string, func(*http.Request) (interface{}, error)) *mux.Route, + handleFunc func(string, func(*http.Request) interface{}) *mux.Route, log *logrus.Entry, ) *Handlers { handlers := &Handlers{log: log.WithField("device", "bitbox02-bootloader")} @@ -61,34 +60,74 @@ func (handlers *Handlers) Uninit() { handlers.device = nil } -func (handlers *Handlers) getStatusHandler(_ *http.Request) (interface{}, error) { - return handlers.device.Status(), nil +type bootloaderResponse struct { + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage,omitempty"` } -func (handlers *Handlers) postUpgradeFirmwareHandler(_ *http.Request) (interface{}, error) { - return nil, handlers.device.UpgradeFirmware() +func (handlers *Handlers) errorResponse(err error) bootloaderResponse { + handlers.log.WithError(err).Error("BitBox02 bootloader request failed") + return bootloaderResponse{Success: false, ErrorMessage: err.Error()} } -func (handlers *Handlers) postRebootHandler(_ *http.Request) (interface{}, error) { - return nil, handlers.device.Reboot() +func (handlers *Handlers) getStatusHandler(_ *http.Request) interface{} { + return handlers.device.Status() } -func (handlers *Handlers) getShowFirmwareHashEnabledHandler(_ *http.Request) (interface{}, error) { - return handlers.device.ShowFirmwareHashEnabled() +func (handlers *Handlers) postUpgradeFirmwareHandler(_ *http.Request) interface{} { + if err := handlers.device.UpgradeFirmware(); err != nil { + return handlers.errorResponse(err) + } + return bootloaderResponse{Success: true} +} + +func (handlers *Handlers) postRebootHandler(_ *http.Request) interface{} { + if err := handlers.device.Reboot(); err != nil { + return handlers.errorResponse(err) + } + return bootloaderResponse{Success: true} +} + +func (handlers *Handlers) getShowFirmwareHashEnabledHandler(_ *http.Request) interface{} { + type response struct { + Success bool `json:"success"` + Enabled bool `json:"enabled"` + } + + enabled, err := handlers.device.ShowFirmwareHashEnabled() + if err != nil { + return handlers.errorResponse(err) + } + return response{Success: true, Enabled: enabled} } -func (handlers *Handlers) postSetShowFirmwareHashEnabledHandler(r *http.Request) (interface{}, error) { +func (handlers *Handlers) postSetShowFirmwareHashEnabledHandler(r *http.Request) interface{} { var enabled bool if err := json.NewDecoder(r.Body).Decode(&enabled); err != nil { - return nil, errp.WithStack(err) + return bootloaderResponse{Success: false, ErrorMessage: err.Error()} } - return nil, handlers.device.SetShowFirmwareHashEnabled(enabled) + if err := handlers.device.SetShowFirmwareHashEnabled(enabled); err != nil { + return handlers.errorResponse(err) + } + return bootloaderResponse{Success: true} } -func (handlers *Handlers) getInfoHandler(_ *http.Request) (interface{}, error) { - return handlers.device.Info() +func (handlers *Handlers) getInfoHandler(_ *http.Request) interface{} { + type response struct { + Success bool `json:"success"` + Info *bitbox02bootloader.Info `json:"info,omitempty"` + } + + info, err := handlers.device.Info() + if err != nil { + return handlers.errorResponse(err) + } + return response{Success: true, Info: info} } -func (handlers *Handlers) postScreenRotateHandler(_ *http.Request) (interface{}, error) { - return nil, handlers.device.ScreenRotate() +func (handlers *Handlers) postScreenRotateHandler(_ *http.Request) interface{} { + if err := handlers.device.ScreenRotate(); err != nil { + return handlers.errorResponse(err) + } + return bootloaderResponse{Success: true} } diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 04d48c5fef..049f0e69db 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -325,7 +325,7 @@ func NewHandlers( getBitBox02BootloaderHandlers := func(deviceID string) *bitbox02bootloaderHandlers.Handlers { defer handlersMapLock.Lock()() if _, ok := bitbox02BootloaderHandlersMap[deviceID]; !ok { - bitbox02BootloaderHandlersMap[deviceID] = bitbox02bootloaderHandlers.NewHandlers(getAPIRouter( + bitbox02BootloaderHandlersMap[deviceID] = bitbox02bootloaderHandlers.NewHandlers(getAPIRouterNoError( apiRouter.PathPrefix(fmt.Sprintf("/devices/bitbox02-bootloader/%s", deviceID)).Subrouter(), ), log) } diff --git a/frontends/web/src/api/bitbox02bootloader.ts b/frontends/web/src/api/bitbox02bootloader.ts index d6ff9c75ae..aaaf938c12 100644 --- a/frontends/web/src/api/bitbox02bootloader.ts +++ b/frontends/web/src/api/bitbox02bootloader.ts @@ -2,6 +2,7 @@ import { apiGet, apiPost } from '@/utils/request'; import { subscribeEndpoint, TSubscriptionCallback } from './subscribe'; +import type { SuccessResponse } from './response'; export type TStatus = { upgrading: boolean; @@ -42,32 +43,43 @@ type TInfo = { additionalUpgradeFollows: boolean; }; +type TBootloaderErrorResponse = { + success: false; + errorMessage?: string; +}; + +export type TBootloaderResponse = SuccessResponse | TBootloaderErrorResponse; + +type TInfoResponse = (SuccessResponse & { info: TInfo }) | TBootloaderErrorResponse; + +type TShowFirmwareHashResponse = (SuccessResponse & { enabled: boolean }) | TBootloaderErrorResponse; + export const getInfo = ( deviceID: string, -): Promise => { +): Promise => { return apiGet(`devices/bitbox02-bootloader/${deviceID}/info`); }; export const upgradeFirmware = ( deviceID: string, -): Promise => { +): Promise => { return apiPost(`devices/bitbox02-bootloader/${deviceID}/upgrade-firmware`); }; export const reboot = ( deviceID: string, -): Promise => { +): Promise => { return apiPost(`devices/bitbox02-bootloader/${deviceID}/reboot`); }; export const screenRotate = ( deviceID: string, -): Promise => { +): Promise => { return apiPost(`devices/bitbox02-bootloader/${deviceID}/screen-rotate`); }; export const getShowFirmwareHash = (deviceID: string) => { - return (): Promise => { + return (): Promise => { return apiGet(`devices/bitbox02-bootloader/${deviceID}/show-firmware-hash-enabled`); }; }; @@ -75,7 +87,7 @@ export const getShowFirmwareHash = (deviceID: string) => { export const setShowFirmwareHash = ( deviceID: string, enabled: boolean, -) => { +): Promise => { return apiPost( `devices/bitbox02-bootloader/${deviceID}/set-firmware-hash-enabled`, enabled, diff --git a/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx b/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx index 6ad72c2ce8..9f001ebaff 100644 --- a/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx +++ b/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import * as bitbox02BootloaderAPI from '@/api/bitbox02bootloader'; import { useDarkmode } from '@/hooks/darkmode'; @@ -23,10 +24,33 @@ export const BitBox02Bootloader = ({ deviceID }: TProps) => { () => bitbox02BootloaderAPI.getStatus(deviceID), bitbox02BootloaderAPI.syncStatus(deviceID), ); - const info = useLoad(() => bitbox02BootloaderAPI.getInfo(deviceID)); - if (info === undefined) { + const infoResponse = useLoad(() => bitbox02BootloaderAPI.getInfo(deviceID)); + const [requestError, setRequestError] = useState(); + + const runAction = async (action: () => Promise) => { + const result = await action(); + if (!result.success) { + setRequestError(result.errorMessage || t('genericError')); + return; + } + setRequestError(undefined); + }; + + if (infoResponse === undefined) { return null; } + if (!infoResponse.success) { + return ( + + + + {infoResponse.errorMessage || t('genericError')} + + + + ); + } + const { info } = infoResponse; let contents; if (status && status.upgrading) { @@ -85,14 +109,14 @@ export const BitBox02Bootloader = ({ deviceID }: TProps) => { { info.canUpgrade ? ( ) : null } { !info.erased && ( )} @@ -101,7 +125,7 @@ export const BitBox02Bootloader = ({ deviceID }: TProps) => { {t('bb02Bootloader.orientation')}  @@ -113,7 +137,9 @@ export const BitBox02Bootloader = ({ deviceID }: TProps) => {

- +
@@ -134,6 +160,11 @@ export const BitBox02Bootloader = ({ deviceID }: TProps) => { {status.errMsg} )} + {requestError && ( + + {requestError} + + )} {contents} diff --git a/frontends/web/src/components/devices/bitbox02bootloader/toggleshowfirmwarehash.tsx b/frontends/web/src/components/devices/bitbox02bootloader/toggleshowfirmwarehash.tsx index 1fdbb0d6bd..6a10000377 100644 --- a/frontends/web/src/components/devices/bitbox02bootloader/toggleshowfirmwarehash.tsx +++ b/frontends/web/src/components/devices/bitbox02bootloader/toggleshowfirmwarehash.tsx @@ -8,23 +8,36 @@ import { Toggle } from '@/components/toggle/toggle'; type Props = { deviceID: string; + onError: (message: string | undefined) => void; }; -export const ToggleShowFirmwareHash = ({ deviceID }: Props) => { +export const ToggleShowFirmwareHash = ({ deviceID, onError }: Props) => { const { t } = useTranslation(); const [enabledState, setEnabledState] = useState(false); const enabledConfig = useLoad(getShowFirmwareHash(deviceID)); useEffect(() => { - if (enabledConfig !== undefined) { - setEnabledState(enabledConfig); + if (enabledConfig === undefined) { + return; } - }, [enabledConfig]); + if (!enabledConfig.success) { + onError(enabledConfig.errorMessage || t('genericError')); + return; + } + onError(undefined); + setEnabledState(enabledConfig.enabled); + }, [enabledConfig, onError, t]); - const handleToggle = (event: ChangeEvent) => { + const handleToggle = async (event: ChangeEvent) => { const enabled = event.target.checked; - setShowFirmwareHash(deviceID, enabled); setEnabledState(enabled); + const result = await setShowFirmwareHash(deviceID, enabled); + if (!result.success) { + setEnabledState(!enabled); + onError(result.errorMessage || t('genericError')); + return; + } + onError(undefined); }; return (