-
Notifications
You must be signed in to change notification settings - Fork 120
convert some handlers to getAPIRouterNoError #4067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b1ce90e
1e4aef3
0ea5762
357d0cb
546c2dc
33677f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -171,7 +171,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 { | ||
|
|
@@ -200,10 +200,10 @@ func NewHandlers( | |
| getAPIRouterNoError(apiRouter)("/qr", handlers.getQRCode).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/config", handlers.getAppConfig).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/config/default", handlers.getDefaultConfig).Methods("GET") | ||
| getAPIRouter(apiRouter)("/config", handlers.postAppConfig).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/config", handlers.postAppConfig).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/native-locale", handlers.getNativeLocale).Methods("GET") | ||
| getAPIRouter(apiRouter)("/notify-user", handlers.postNotify).Methods("POST") | ||
| getAPIRouter(apiRouter)("/open", handlers.postOpen).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/open", handlers.postOpen).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/update", handlers.getUpdate).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/banners/{key}", handlers.getBanners).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/using-mobile-data", handlers.getUsingMobileData).Methods("GET") | ||
|
|
@@ -220,7 +220,7 @@ func NewHandlers( | |
| getAPIRouterNoError(apiRouter)("/accounts", handlers.getAccounts).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/swap/accounts", handlers.getSwapAccounts).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/swap/status", handlers.getSwapStatus).Methods("GET") | ||
| getAPIRouter(apiRouter)("/accounts/balance-summary", handlers.getAccountsBalanceSummary).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/accounts/balance-summary", handlers.getAccountsBalanceSummary).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/set-account-active", handlers.postSetAccountActive).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/set-token-active", handlers.postSetTokenActive).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/set-account-receive-script-type", handlers.postSetAccountReceiveScriptType).Methods("POST") | ||
|
|
@@ -248,7 +248,7 @@ func NewHandlers( | |
| getAPIRouterNoError(apiRouter)("/market/btcdirect/info/{action}/{code}", handlers.getMarketBtcDirectInfo).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/swap/quote", handlers.postSwapkitQuote).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/swap/sign", handlers.postSwapSign).Methods("POST") | ||
| getAPIRouter(apiRouter)("/market/moonpay/buy-info/{code}", handlers.getMarketMoonpayBuyInfo).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/market/moonpay/buy-info/{code}", handlers.getMarketMoonpayBuyInfo).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/market/pocket/api-url/{action}", handlers.getMarketPocketURL).Methods("GET") | ||
| getAPIRouterNoError(apiRouter)("/market/pocket/verify-address", handlers.postPocketWidgetVerifyAddress).Methods("POST") | ||
| getAPIRouterNoError(apiRouter)("/market/bitrefill/info/{action}/{code}", handlers.getMarketBitrefillInfo).Methods("GET") | ||
|
|
@@ -541,12 +541,22 @@ func (handlers *Handlers) getDefaultConfig(*http.Request) interface{} { | |
| return handlers.backend.DefaultAppConfig() | ||
| } | ||
|
|
||
| func (handlers *Handlers) postAppConfig(r *http.Request) (interface{}, error) { | ||
| func (handlers *Handlers) postAppConfig(r *http.Request) interface{} { | ||
| type response struct { | ||
| Success bool `json:"success"` | ||
| ErrorMessage string `json:"errorMessage,omitempty"` | ||
| } | ||
|
|
||
| appConfig := config.AppConfig{} | ||
| if err := json.NewDecoder(r.Body).Decode(&appConfig); err != nil { | ||
| return nil, errp.WithStack(err) | ||
| 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 nil, handlers.backend.Config().SetAppConfig(appConfig) | ||
| return response{Success: true} | ||
| } | ||
|
|
||
| // getNativeLocaleHandler returns user preferred UI language as reported | ||
|
|
@@ -567,12 +577,22 @@ func (handlers *Handlers) postNotify(r *http.Request) (interface{}, error) { | |
| return nil, nil | ||
| } | ||
|
|
||
| func (handlers *Handlers) postOpen(r *http.Request) (interface{}, error) { | ||
| func (handlers *Handlers) postOpen(r *http.Request) interface{} { | ||
| type response struct { | ||
| Success bool `json:"success"` | ||
| ErrorMessage string `json:"errorMessage,omitempty"` | ||
|
Comment on lines
+582
to
+583
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] I'm almost thinking -but not sure- we could have a base response type like this one, and then most of the others would just either use this directly, or extend it with other fields |
||
| } | ||
|
|
||
| var url string | ||
| if err := json.NewDecoder(r.Body).Decode(&url); err != nil { | ||
| return nil, errp.WithStack(err) | ||
| handlers.log.WithField("handler", "postOpen").WithError(err).Error("handler failed") | ||
| return response{Success: false, ErrorMessage: err.Error()} | ||
| } | ||
| return nil, handlers.backend.SystemOpen(url) | ||
| 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} | ||
| } | ||
|
|
||
| func (handlers *Handlers) getUpdate(*http.Request) interface{} { | ||
|
|
@@ -870,17 +890,18 @@ func (handlers *Handlers) postBtcFormatUnit(r *http.Request) interface{} { | |
| } | ||
|
|
||
| // getAccountsBalanceSummary returns the total balance summary of all coins and accounts. | ||
| func (handlers *Handlers) getAccountsBalanceSummary(*http.Request) (interface{}, error) { | ||
| func (handlers *Handlers) getAccountsBalanceSummary(*http.Request) interface{} { | ||
| type response struct { | ||
| Success bool `json:"success"` | ||
| TotalBalance *backend.AccountsBalanceSummary `json:"accountsBalanceSummary"` | ||
| } | ||
|
|
||
| totalBalance, err := handlers.backend.AccountsBalanceSummary() | ||
| if err != nil { | ||
| return response{Success: false}, nil | ||
| handlers.log.WithField("handler", "getAccountsBalanceSummary").WithError(err).Error("handler failed") | ||
| return response{Success: false} | ||
| } | ||
| return response{Success: true, TotalBalance: totalBalance}, nil | ||
| return response{Success: true, TotalBalance: totalBalance} | ||
| } | ||
|
|
||
| func (handlers *Handlers) postSetAccountActive(r *http.Request) interface{} { | ||
|
|
@@ -1452,10 +1473,18 @@ func (handlers *Handlers) getMarketVendors(r *http.Request) interface{} { | |
| return supported | ||
| } | ||
|
|
||
| func (handlers *Handlers) getMarketMoonpayBuyInfo(r *http.Request) (interface{}, error) { | ||
| func (handlers *Handlers) getMarketMoonpayBuyInfo(r *http.Request) interface{} { | ||
| type result struct { | ||
| Success bool `json:"success"` | ||
| ErrorMessage string `json:"errorMessage,omitempty"` | ||
| URL string `json:"url,omitempty"` | ||
| Address string `json:"address,omitempty"` | ||
| } | ||
|
|
||
| acct, err := handlers.backend.GetAccountFromCode(accountsTypes.Code(mux.Vars(r)["code"])) | ||
| if err != nil { | ||
| return nil, err | ||
| handlers.log.WithField("handler", "getMarketMoonpayBuyInfo").WithError(err).Error("handler failed") | ||
| return result{Success: false, ErrorMessage: err.Error()} | ||
| } | ||
|
|
||
| lang := handlers.backend.Config().AppConfig().Backend.UserLanguage | ||
|
|
@@ -1470,16 +1499,14 @@ func (handlers *Handlers) getMarketMoonpayBuyInfo(r *http.Request) (interface{}, | |
| } | ||
| buy, err := market.MoonpayInfo(acct, params) | ||
| if err != nil { | ||
| return nil, err | ||
| handlers.log.WithField("handler", "getMarketMoonpayBuyInfo").WithError(err).Error("handler failed") | ||
| return result{Success: false, ErrorMessage: err.Error()} | ||
| } | ||
| resp := struct { | ||
| URL string `json:"url"` | ||
| Address string `json:"address"` | ||
| }{ | ||
| return result{ | ||
| Success: true, | ||
| URL: buy.URL, | ||
| Address: buy.Address, | ||
| } | ||
| return resp, nil | ||
| } | ||
|
|
||
| func (handlers *Handlers) getMarketBtcDirectInfo(r *http.Request) interface{} { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,8 +48,12 @@ export const getMarketDeals = ( | |
| }; | ||
|
|
||
| export type MoonpayBuyInfo = { | ||
| success: true; | ||
| url: string; | ||
| address: string; | ||
| } | { | ||
| success: false; | ||
| errorMessage?: string; | ||
| }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fine for now, but we should try to do something like SuccessResponse & { |
||
|
|
||
| export const getMoonpayBuyInfo = (code: AccountCode) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,13 @@ export const notifyUser = (text: string) => { | |
| return apiPost('notify-user', { text }); | ||
| }; | ||
|
|
||
| export const open = (href: string) => { | ||
| type TOpenResponse = { | ||
| success: true; | ||
| } | { | ||
| success: false; | ||
| errorMessage?: string; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Many types use SuccessResponse and FailResponse, but this has slightly different properties. Instead of "errorMessage" it has just "message". Therer is a comment that this uses export type SuccessResponse = {
success: true;
};
// if the backend uses maybeBB02Err
export type FailResponse = {
code?: number;
message?: string;
success: false;
};https://github.com/BitBoxSwiss/bitbox-wallet-app/blob/master/frontends/web/src/api/response.ts I have no strong opinion on which one we should prefer, but it would be nice if we could just use those everywhere. type TOpenResponse = SuccessResponse | FailResponse;Would it make sense to refactor maybeBB02Err to use errorMessage etc? (also some endpoints return "errMsg")
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Almost all endpoints outside use errorMessage I think. Makes sense to me to make BitBox02 handlers use the same, but that's for another PR, as it is unrelated to this one.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok then we should change FailResponse to use errorMessage in the future and use and extend from SuccessResponse FailResponse everywhere. Should we rename SuccessResponse to SuccessResponseLegacy and already start with the new one? But I'd love if in the end all use the same and we dont end up having a mix again. |
||
| }; | ||
|
|
||
| export const open = (href: string): Promise<TOpenResponse> => { | ||
| return apiPost('open', href); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <span | ||
| className={` | ||
|
|
@@ -41,7 +45,15 @@ export const A = ({ | |
| title={props.title || href} | ||
| onClick={(e: SyntheticEvent) => { | ||
| e.preventDefault(); | ||
| open(href).catch(console.error); | ||
| open(href) | ||
| .then(response => { | ||
| if (!response.success) { | ||
| alertUser(response.errorMessage | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At some point we should stop using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can there be a solution that is as easy to use as alertUser? Making ad-hoc ways to display errors everywhere is difficult (for me). Maybe you could make a PR sometime that removes all uses of alertUser and replaces it with a streamlined way of showing such messages?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is pretty easy have a look: https://github.com/BitBoxSwiss/bitbox-wallet-app/pull/4082/changes. But I agree it would be nice to have a good reusable way. In the future we may want to have a global notification center / error component. 🤷
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue with that one is that it's a lot more effort - need to think where to place the error message, test if it looks good, etc. alertUser "just works" - would be nice to have something that is as easy to use.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's harder yes but much nicer UI than popup. we also have popup over popup, somewhere in one case 3 popups over each other each with different dimension and visible. I'll find a screenshot once. also from our friend gpt:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep as is for this PR, but if you don't mind maybe well change it for smth better and remove alertUser ok? |
||
| ? t('unknownError', { errorMessage: response.errorMessage }) | ||
| : t('genericError')); | ||
| } | ||
| }) | ||
| .catch(console.error); | ||
|
benma marked this conversation as resolved.
|
||
| }} | ||
| tabIndex={0} | ||
| {...props}> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| // 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 }) => <div>{title}</div>, | ||
| })); | ||
| vi.mock('@/components/spinner/Spinner', () => ({ | ||
| Spinner: ({ text }: { text?: string }) => <div>{text}</div>, | ||
| })); | ||
| vi.mock('@/hooks/backbutton', () => ({ | ||
| 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<typeof import('@/api/market')>(); | ||
| return { | ||
| ...actual, | ||
| getMoonpayBuyInfo: vi.fn(), | ||
| }; | ||
| }); | ||
| vi.mock('@/utils/config', async (importOriginal) => { | ||
| const actual = await importOriginal<typeof import('@/utils/config')>(); | ||
| return { | ||
| ...actual, | ||
| getConfig: vi.fn(), | ||
| }; | ||
| }); | ||
|
|
||
| import { render, screen } from '@testing-library/react'; | ||
| import type { TAccount } from '@/api/account'; | ||
| import * as marketApi from '@/api/market'; | ||
| import * as config from '@/utils/config'; | ||
| 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', | ||
| }; | ||
|
|
||
| describe('routes/market/moonpay', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| vi.mocked(config.getConfig).mockResolvedValue({ | ||
| frontend: { | ||
| skipMoonpayDisclaimer: true, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| 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(<Moonpay accounts={[account]} code={account.code} />); | ||
|
|
||
| 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(<Moonpay accounts={[account]} code={account.code} />); | ||
|
|
||
| expect(await screen.findByText('Account is not valid.')).toBeInTheDocument(); | ||
| expect(screen.queryByTitle('Moonpay')).not.toBeInTheDocument(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,20 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { apiGet, apiPost } from '@/utils/request'; | ||
| import { runningInQtWebEngine, runningOnMobile } from '@/utils/env'; | ||
|
|
||
| type TConfig = { | ||
| backend?: unknown; | ||
| frontend?: unknown; | ||
| }; | ||
|
|
||
| type TSetConfigResponse = null | undefined | { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this correct that it can return null | undefined ? |
||
| success: true; | ||
| } | { | ||
| success: false; | ||
| errorMessage?: string; | ||
| }; | ||
|
|
||
| let pendingConfig: TConfig = {}; | ||
|
|
||
| /** | ||
|
|
@@ -32,7 +40,10 @@ export const setConfig = (object: TConfig) => { | |
| }); | ||
| pendingConfig = nextConfig; | ||
| return apiPost('config', nextConfig) | ||
| .then(() => { | ||
| .then((response: TSetConfigResponse) => { | ||
| if (response?.success === false && !runningInQtWebEngine() && !runningOnMobile()) { | ||
| throw new Error(response.errorMessage || 'Failed to update configuration'); | ||
| } | ||
|
benma marked this conversation as resolved.
|
||
| pendingConfig = {}; | ||
| return nextConfig; | ||
|
benma marked this conversation as resolved.
|
||
| }); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.