From d374579acbdd24179c0250664e9ce7dedd90900e Mon Sep 17 00:00:00 2001 From: Martin Homola Date: Tue, 23 Apr 2024 19:42:22 +0200 Subject: [PATCH] feat(coinmarket): Add payment method filter (#12049) * feat(coinmarket): add filter in buy section * feat(coinmarket): add logic to coinmarket filter --- .../e2e/tests/coinmarket/buy.test.ts | 5 +- .../hooks/wallet/useCoinmarketBuyOffers.ts | 28 ++++- .../wallet/useCoinmarketFilterReducer.ts | 112 ++++++++++++++++++ packages/suite/src/support/messages.ts | 4 + .../src/types/wallet/coinmarketBuyOffers.ts | 3 + .../buy/offers/Offers/List/BuyQuoteFilter.tsx | 60 ++++++++++ .../buy/offers/Offers/List/BuyQuoteList.tsx | 11 +- .../common/CoinmarketPaymentPlainType.tsx | 49 ++++++++ .../common/CoinmarketPaymentType.tsx | 36 +----- 9 files changed, 268 insertions(+), 40 deletions(-) create mode 100644 packages/suite/src/reducers/wallet/useCoinmarketFilterReducer.ts create mode 100644 packages/suite/src/views/wallet/coinmarket/buy/offers/Offers/List/BuyQuoteFilter.tsx create mode 100644 packages/suite/src/views/wallet/coinmarket/common/CoinmarketPaymentPlainType.tsx diff --git a/packages/suite-web/e2e/tests/coinmarket/buy.test.ts b/packages/suite-web/e2e/tests/coinmarket/buy.test.ts index 3953361b522..d6ed03bd2f2 100644 --- a/packages/suite-web/e2e/tests/coinmarket/buy.test.ts +++ b/packages/suite-web/e2e/tests/coinmarket/buy.test.ts @@ -178,9 +178,8 @@ describe('Coinmarket buy', () => { .invoke('text') .should('be.equal', 'banxa'); cy.wrap(wrapper) - .find('[class*="CoinmarketPaymentType__Text"]') - .invoke('text') - .should('be.equal', 'Bank Transfer'); + .find('[class*="CoinmarketPaymentPlainType__Text"]') + .should('contain.text', 'Bank Transfer'); cy.wrap(wrapper) .find('[class*="Status__Text"]') .invoke('text') diff --git a/packages/suite/src/hooks/wallet/useCoinmarketBuyOffers.ts b/packages/suite/src/hooks/wallet/useCoinmarketBuyOffers.ts index 833c0d3d876..5f659b365a2 100644 --- a/packages/suite/src/hooks/wallet/useCoinmarketBuyOffers.ts +++ b/packages/suite/src/hooks/wallet/useCoinmarketBuyOffers.ts @@ -16,6 +16,7 @@ import * as routerActions from 'src/actions/suite/routerActions'; import { UseOffersProps, ContextValues } from 'src/types/wallet/coinmarketBuyOffers'; import { useCoinmarketNavigation } from 'src/hooks/wallet/useCoinmarketNavigation'; import { InvityAPIReloadQuotesAfterSeconds } from 'src/constants/wallet/coinmarket/metadata'; +import { useCoinmarketFilterReducer } from '../../reducers/wallet/useCoinmarketFilterReducer'; export const useOffers = ({ selectedAccount }: UseOffersProps) => { const timer = useTimer(); @@ -52,10 +53,13 @@ export const useOffers = ({ selectedAccount }: UseOffersProps) => { const { addressVerified, alternativeQuotes, buyInfo, isFromRedirect, quotes, quotesRequest } = useSelector(state => state.wallet.coinmarket.buy); + const innerQuotesFilterReducer = useCoinmarketFilterReducer(quotes); + const innerAlternativeQuotesFilterReducer = useCoinmarketFilterReducer(alternativeQuotes); const [innerQuotes, setInnerQuotes] = useState(quotes); const [innerAlternativeQuotes, setInnerAlternativeQuotes] = useState( alternativeQuotes, ); + if (invityServerEnvironment) { invityAPI.setInvityServersEnvironment(invityServerEnvironment); } @@ -73,14 +77,29 @@ export const useOffers = ({ selectedAccount }: UseOffersProps) => { } const [quotes, alternativeQuotes] = processQuotes(allQuotes); setInnerQuotes(quotes); + innerQuotesFilterReducer.dispatch({ + type: 'FILTER_SET_PAYMENT_METHODS', + payload: quotes, + }); setInnerAlternativeQuotes(alternativeQuotes); + innerAlternativeQuotesFilterReducer.dispatch({ + type: 'FILTER_SET_PAYMENT_METHODS', + payload: alternativeQuotes, + }); } else { setInnerQuotes(undefined); setInnerAlternativeQuotes(undefined); } timer.reset(); } - }, [account.descriptor, quotesRequest, selectedQuote, timer]); + }, [ + selectedQuote, + quotesRequest, + timer, + account.descriptor, + innerQuotesFilterReducer, + innerAlternativeQuotesFilterReducer, + ]); useEffect(() => { if (!quotesRequest) { @@ -183,8 +202,11 @@ export const useOffers = ({ selectedAccount }: UseOffersProps) => { providersInfo: buyInfo?.providerInfos, quotesRequest, addressVerified, - quotes: innerQuotes, - alternativeQuotes: innerAlternativeQuotes, + quotes: innerQuotesFilterReducer.actions.handleFilterQuotes(innerQuotes), + alternativeQuotes: + innerAlternativeQuotesFilterReducer.actions.handleFilterQuotes(innerAlternativeQuotes), + innerQuotesFilterReducer, + innerAlternativeQuotesFilterReducer, selectQuote, account, timer, diff --git a/packages/suite/src/reducers/wallet/useCoinmarketFilterReducer.ts b/packages/suite/src/reducers/wallet/useCoinmarketFilterReducer.ts new file mode 100644 index 00000000000..1822e460220 --- /dev/null +++ b/packages/suite/src/reducers/wallet/useCoinmarketFilterReducer.ts @@ -0,0 +1,112 @@ +import { BuyCryptoPaymentMethod, BuyTrade } from 'invity-api'; +import { Dispatch, useCallback, useEffect, useReducer } from 'react'; + +type PaymentMethodValueProps = BuyCryptoPaymentMethod | ''; + +export interface PaymentMethodListProps { + value: PaymentMethodValueProps; + label: string; +} + +interface InitialStateProps { + paymentMethod: PaymentMethodValueProps; + paymentMethods: PaymentMethodListProps[]; +} + +type ActionType = + | { + type: 'FILTER_PAYMENT_METHOD'; + payload: PaymentMethodValueProps; + } + | { + type: 'FILTER_SET_PAYMENT_METHODS'; + payload: BuyTrade[]; + }; + +export interface UseCoinmarketFilterReducerOutputProps { + state: InitialStateProps; + dispatch: Dispatch; + actions: { + handleFilterQuotes: (quotes: BuyTrade[] | undefined) => BuyTrade[] | undefined; + }; +} + +const getPaymentMethods = (quotes: BuyTrade[]): PaymentMethodListProps[] => { + const newPaymentMethods: PaymentMethodListProps[] = []; + + quotes.forEach(quote => { + const { paymentMethod } = quote; + const isNotInArray = !newPaymentMethods.some(item => item.value === paymentMethod); + + if (typeof paymentMethod !== 'undefined' && isNotInArray) { + const label = quote.paymentMethodName ?? paymentMethod; + + newPaymentMethods.push({ value: paymentMethod, label }); + } + }); + + return newPaymentMethods; +}; + +const reducer = (state: InitialStateProps, action: ActionType) => { + switch (action.type) { + case 'FILTER_PAYMENT_METHOD': + return { + ...state, + paymentMethod: action.payload, + }; + case 'FILTER_SET_PAYMENT_METHODS': { + const newPaymentMethods: PaymentMethodListProps[] = getPaymentMethods(action.payload); + + return { + ...state, + paymentMethods: newPaymentMethods, + }; + } + default: + return state; + } +}; + +export const useCoinmarketFilterReducer = ( + quotes: BuyTrade[] | undefined, +): UseCoinmarketFilterReducerOutputProps => { + const initialState: InitialStateProps = { + paymentMethod: '', + paymentMethods: quotes ? getPaymentMethods(quotes) : [], + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + // if payment method is not in payment methods then reset + useEffect(() => { + const isMethodInPaymentMethods = state.paymentMethods.find( + item => item.value === state.paymentMethod, + ); + + if (!isMethodInPaymentMethods) { + dispatch({ type: 'FILTER_PAYMENT_METHOD', payload: '' }); + } + }, [state.paymentMethod, state.paymentMethods]); + + const handleFilterQuotes = useCallback( + (quotes: BuyTrade[] | undefined) => { + if (!quotes) return; + + return quotes.filter(quote => { + if (state.paymentMethod === '') return true; // all + + return quote.paymentMethod === state.paymentMethod; + }); + }, + [state.paymentMethod], + ); + + return { + state, + dispatch, + actions: { + handleFilterQuotes, + }, + }; +}; diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 1623624fa4e..389bf3355fc 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -4718,6 +4718,10 @@ export default defineMessages({ id: 'TR_PAYMENT_METHOD_UNKNOWN', defaultMessage: 'Unknown', }, + TR_PAYMENT_METHOD_ALL: { + id: 'TR_PAYMENT_METHOD_ALL', + defaultMessage: 'All payment methods', + }, TR_OFFER_FEE_INFO: { id: 'TR_OFFER_FEE_INFO', defaultMessage: diff --git a/packages/suite/src/types/wallet/coinmarketBuyOffers.ts b/packages/suite/src/types/wallet/coinmarketBuyOffers.ts index d9782d3d7d3..fd6185c7e1b 100644 --- a/packages/suite/src/types/wallet/coinmarketBuyOffers.ts +++ b/packages/suite/src/types/wallet/coinmarketBuyOffers.ts @@ -6,6 +6,7 @@ import type { AppState } from 'src/types/suite'; import type { Account } from 'src/types/wallet'; import type { BuyInfo } from 'src/actions/wallet/coinmarketBuyActions'; import type { WithSelectedAccountLoadedProps } from 'src/components/wallet'; +import { UseCoinmarketFilterReducerOutputProps } from 'src/reducers/wallet/useCoinmarketFilterReducer'; export type UseOffersProps = WithSelectedAccountLoadedProps; @@ -24,6 +25,8 @@ export type ContextValues = { goToPayment: (address: string) => void; timer: Timer; getQuotes: () => Promise; + innerQuotesFilterReducer: UseCoinmarketFilterReducerOutputProps; + innerAlternativeQuotesFilterReducer: UseCoinmarketFilterReducerOutputProps; }; export type AddressOptionsFormState = { diff --git a/packages/suite/src/views/wallet/coinmarket/buy/offers/Offers/List/BuyQuoteFilter.tsx b/packages/suite/src/views/wallet/coinmarket/buy/offers/Offers/List/BuyQuoteFilter.tsx new file mode 100644 index 00000000000..470b7499178 --- /dev/null +++ b/packages/suite/src/views/wallet/coinmarket/buy/offers/Offers/List/BuyQuoteFilter.tsx @@ -0,0 +1,60 @@ +import { Select } from '@trezor/components'; +import styled from 'styled-components'; +import { useTranslation } from 'src/hooks/suite'; +import { + PaymentMethodListProps, + UseCoinmarketFilterReducerOutputProps, +} from 'src/reducers/wallet/useCoinmarketFilterReducer'; +import { CoinmarketPaymentPlainType } from 'src/views/wallet/coinmarket/common/CoinmarketPaymentPlainType'; +import { spacingsPx } from '@trezor/theme'; + +const Wrapper = styled.div` + margin-top: ${spacingsPx.lg}; +`; + +const Option = styled.div` + display: flex; + align-items: center; +`; + +interface BuyQuoteFilter { + quotesFilterReducer: UseCoinmarketFilterReducerOutputProps; +} + +export const BuyQuoteFilter = ({ quotesFilterReducer }: BuyQuoteFilter) => { + const { state, dispatch } = quotesFilterReducer; + const { translationString } = useTranslation(); + const defaultMethod: PaymentMethodListProps = { + value: '', + label: translationString('TR_PAYMENT_METHOD_ALL'), + }; + + return ( + +