From f00a7c713ed685bf7f1b36c3856e02113ed9b58b Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 27 Apr 2026 18:09:29 +0000 Subject: [PATCH 1/3] frontend: add moonpay error test --- .../web/src/routes/market/moonpay.test.tsx | 121 ++++++++++++++++++ frontends/web/src/routes/market/moonpay.tsx | 2 +- 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 frontends/web/src/routes/market/moonpay.test.tsx diff --git a/frontends/web/src/routes/market/moonpay.test.tsx b/frontends/web/src/routes/market/moonpay.test.tsx new file mode 100644 index 0000000000..6effca7eea --- /dev/null +++ b/frontends/web/src/routes/market/moonpay.test.tsx @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 + +import '../../../__mocks__/i18n'; +import type { ReactNode } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@/components/layout', () => ({ + Header: ({ title }: { title: ReactNode }) =>
{title}
, +})); +vi.mock('@/components/spinner/Spinner', () => ({ + Spinner: ({ text }: { text?: string }) =>
{text}
, +})); +vi.mock('@/hooks/backbutton', () => ({ + UseBackButton: () => null, + UseDisableBackButton: () => null, +})); +vi.mock('@/hooks/darkmode', () => ({ + useDarkmode: () => ({ isDarkMode: false, toggleDarkmode: vi.fn() }), +})); +vi.mock('@/hooks/vendor-iframe', () => ({ + useVendorIframeResizeHeight: () => ({ + containerRef: { current: null }, + height: 480, + iframeLoaded: false, + onIframeLoad: vi.fn(), + }), + useVendorTerms: () => ({ + agreedTerms: true, + setAgreedTerms: vi.fn(), + }), +})); +vi.mock('./guide', () => ({ + MarketGuide: () => null, +})); +vi.mock('@/api/market', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getMoonpayBuyInfo: vi.fn(), + }; +}); +vi.mock('@/contexts/ConfigProvider', () => ({ + useConfig: vi.fn(() => ({ + config: { frontend: { skipMoonpayDisclaimer: true }, backend: {} } as TConfig, + setConfig: vi.fn(), + })), +})); + +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import type { TAccount } from '@/api/account'; +import type { TConfig } from '@/api/config'; +import * as marketApi from '@/api/market'; +import { useConfig } from '@/contexts/ConfigProvider'; +import { Moonpay } from './moonpay'; + +const account: TAccount = { + keystore: { + connected: true, + lastConnected: '', + name: 'BitBox02', + rootFingerprint: 'f23ab988', + watchonly: false, + }, + active: true, + blockExplorerTxPrefix: '', + code: 'btc-account', + coinCode: 'btc', + coinName: 'Bitcoin', + coinUnit: 'BTC', + isToken: false, + name: 'Bitcoin Account', +}; + +const mockUseConfig = vi.mocked(useConfig); + +describe('routes/market/moonpay', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseConfig.mockReturnValue({ + config: { frontend: { skipMoonpayDisclaimer: true }, backend: {} } as TConfig, + setConfig: vi.fn(), + }); + }); + + it('renders the MoonPay iframe on success', async () => { + vi.mocked(marketApi.getMoonpayBuyInfo).mockReturnValue(() => Promise.resolve({ + success: true, + url: 'https://buy.moonpay.com?walletAddress=bc1qexample', + address: 'bc1qexample', + })); + + render( + + + + ); + + const iframe = await screen.findByTitle('Moonpay'); + expect(iframe).toHaveAttribute( + 'src', + 'https://buy.moonpay.com?walletAddress=bc1qexample&colorCode=%235E94BF&theme=light', + ); + }); + + it('renders an error message on failure', async () => { + vi.mocked(marketApi.getMoonpayBuyInfo).mockReturnValue(() => Promise.resolve({ + success: false, + errorMessage: 'Account is not valid.', + })); + + render( + + + + ); + + expect(await screen.findByText('Account is not valid.')).toBeInTheDocument(); + expect(screen.queryByTitle('Moonpay')).not.toBeInTheDocument(); + }); +}); diff --git a/frontends/web/src/routes/market/moonpay.tsx b/frontends/web/src/routes/market/moonpay.tsx index c001f85f8c..35a907f466 100644 --- a/frontends/web/src/routes/market/moonpay.tsx +++ b/frontends/web/src/routes/market/moonpay.tsx @@ -10,11 +10,11 @@ import { getMoonpayBuyInfo } from '@/api/market'; import { MarketGuide } from './guide'; import { Header } from '@/components/layout'; import { MobileHeader } from '../settings/components/mobile-header'; +import { Message } from '@/components/message/message'; import { Spinner } from '@/components/spinner/Spinner'; import { findAccount, isBitcoinOnly } from '@/routes/account/utils'; import { MoonpayTerms } from '@/components/terms/moonpay-terms'; import { useVendorIframeResizeHeight, useVendorTerms } from '@/hooks/vendor-iframe'; -import { Message } from '@/components/message/message'; import style from './iframe.module.css'; type TProps = { From bfd2c904a407736cc2e9401bbab1b032a05ee5a2 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 27 Apr 2026 19:56:29 +0000 Subject: [PATCH 2/3] frontend: surface open errors --- frontends/web/src/components/anchor/anchor.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontends/web/src/components/anchor/anchor.tsx b/frontends/web/src/components/anchor/anchor.tsx index 8d5363217e..f3dac6b265 100644 --- a/frontends/web/src/components/anchor/anchor.tsx +++ b/frontends/web/src/components/anchor/anchor.tsx @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 import { ReactNode, SyntheticEvent } from 'react'; +import { useTranslation } from 'react-i18next'; import { open } from '@/api/system'; +import { alertUser } from '@/components/alert/Alert'; import { runningInIOS } from '@/utils/env'; import style from './anchor.module.css'; @@ -32,6 +34,8 @@ export const A = ({ children, ...props }: TProps) => { + const { t } = useTranslation(); + return ( { e.preventDefault(); - open(href).catch(console.error); + open(href) + .then(response => { + if (!response.success) { + alertUser(response.errorMessage + ? t('unknownError', { errorMessage: response.errorMessage }) + : t('genericError')); + } + }) + .catch(console.error); }} tabIndex={0} {...props}> From cb934b2468f9c5ff900299cdf795cf18b9b4ae1f Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 27 Apr 2026 21:21:11 +0000 Subject: [PATCH 3/3] backend: log no-error handler failures --- backend/handlers/handlers.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 55e460c2a5..09505eeccb 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -174,7 +174,7 @@ func NewHandlers( WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, }, - log: logging.Get().WithGroup("handlers"), + log: log, } getAPIRouter := func(subrouter *mux.Router) func(string, func(*http.Request) (interface{}, error)) *mux.Route { @@ -552,9 +552,11 @@ func (handlers *Handlers) postAppConfig(r *http.Request) interface{} { appConfig := config.AppConfig{} if err := json.NewDecoder(r.Body).Decode(&appConfig); err != nil { + handlers.log.WithField("handler", "postAppConfig").WithError(err).Error("handler failed") return response{Success: false, ErrorMessage: err.Error()} } if err := handlers.backend.Config().SetAppConfig(appConfig); err != nil { + handlers.log.WithField("handler", "postAppConfig").WithError(err).Error("handler failed") return response{Success: false, ErrorMessage: err.Error()} } return response{Success: true} @@ -591,9 +593,11 @@ func (handlers *Handlers) postOpen(r *http.Request) interface{} { var url string if err := json.NewDecoder(r.Body).Decode(&url); err != nil { + handlers.log.WithField("handler", "postOpen").WithError(err).Error("handler failed") return response{Success: false, ErrorMessage: err.Error()} } if err := handlers.backend.SystemOpen(url); err != nil { + handlers.log.WithField("handler", "postOpen").WithError(err).Error("handler failed") return response{Success: false, ErrorMessage: err.Error()} } return response{Success: true} @@ -907,6 +911,7 @@ func (handlers *Handlers) getAccountsBalanceSummary(*http.Request) interface{} { totalBalance, err := handlers.backend.AccountsBalanceSummary() if err != nil { + handlers.log.WithField("handler", "getAccountsBalanceSummary").WithError(err).Error("handler failed") return response{Success: false} } return response{Success: true, TotalBalance: totalBalance} @@ -1517,6 +1522,7 @@ func (handlers *Handlers) getMarketMoonpayBuyInfo(r *http.Request) interface{} { acct, err := handlers.backend.GetAccountFromCode(accountsTypes.Code(mux.Vars(r)["code"])) if err != nil { + handlers.log.WithField("handler", "getMarketMoonpayBuyInfo").WithError(err).Error("handler failed") return result{Success: false, ErrorMessage: err.Error()} } @@ -1532,6 +1538,7 @@ func (handlers *Handlers) getMarketMoonpayBuyInfo(r *http.Request) interface{} { } buy, err := market.MoonpayInfo(acct, params) if err != nil { + handlers.log.WithField("handler", "getMarketMoonpayBuyInfo").WithError(err).Error("handler failed") return result{Success: false, ErrorMessage: err.Error()} } return result{