Skip to content

Commit

Permalink
feat(coinmarket): Add payment method filter (#12049)
Browse files Browse the repository at this point in the history
* feat(coinmarket): add filter in buy section

* feat(coinmarket): add logic to coinmarket filter
  • Loading branch information
adderpositive authored Apr 23, 2024
1 parent 429e4e9 commit d374579
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 40 deletions.
5 changes: 2 additions & 3 deletions packages/suite-web/e2e/tests/coinmarket/buy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
28 changes: 25 additions & 3 deletions packages/suite/src/hooks/wallet/useCoinmarketBuyOffers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<BuyTrade[] | undefined>(quotes);
const [innerAlternativeQuotes, setInnerAlternativeQuotes] = useState<BuyTrade[] | undefined>(
alternativeQuotes,
);

if (invityServerEnvironment) {
invityAPI.setInvityServersEnvironment(invityServerEnvironment);
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
112 changes: 112 additions & 0 deletions packages/suite/src/reducers/wallet/useCoinmarketFilterReducer.ts
Original file line number Diff line number Diff line change
@@ -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<ActionType>;
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,
},
};
};
4 changes: 4 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions packages/suite/src/types/wallet/coinmarketBuyOffers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,6 +25,8 @@ export type ContextValues = {
goToPayment: (address: string) => void;
timer: Timer;
getQuotes: () => Promise<void>;
innerQuotesFilterReducer: UseCoinmarketFilterReducerOutputProps;
innerAlternativeQuotesFilterReducer: UseCoinmarketFilterReducerOutputProps;
};

export type AddressOptionsFormState = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Wrapper data-test="@coinmarket/buy/filter">
<Select
onChange={(selected: PaymentMethodListProps) => {
dispatch({ type: 'FILTER_PAYMENT_METHOD', payload: selected.value });
}}
value={
state.paymentMethods.find(item => item.value === state.paymentMethod) ??
defaultMethod
}
options={[defaultMethod, ...state.paymentMethods]}
formatOptionLabel={(option: PaymentMethodListProps) => (
<Option>
{option.value !== '' ? (
<CoinmarketPaymentPlainType
method={option.value}
methodName={option.label}
/>
) : (
<div>{option.label}</div>
)}
</Option>
)}
data-test="@coinmarket/buy/filter-payment-method"
minValueWidth="100px"
/>
</Wrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { InvityAPIReloadQuotesAfterSeconds } from 'src/constants/wallet/coinmark
import { BuyQuote } from './BuyQuote';
import invityAPI from 'src/services/suite/invityAPI';
import { cryptoToCoinSymbol } from 'src/utils/wallet/coinmarket/cryptoSymbolUtils';
import { BuyQuoteFilter } from './BuyQuoteFilter';

const Wrapper = styled.div``;
const Quotes = styled.div``;
Expand Down Expand Up @@ -89,7 +90,8 @@ interface ListProps {
}

export const BuyQuoteList = ({ isAlternative, quotes }: ListProps) => {
const { quotesRequest, timer } = useCoinmarketBuyOffersContext();
const { quotesRequest, timer, innerQuotesFilterReducer, innerAlternativeQuotesFilterReducer } =
useCoinmarketBuyOffersContext();

if (!quotesRequest) return null;
const { fiatStringAmount, fiatCurrency, wantCrypto } = quotesRequest;
Expand Down Expand Up @@ -127,6 +129,13 @@ export const BuyQuoteList = ({ isAlternative, quotes }: ListProps) => {
/>
</OrigAmount>
)}
<BuyQuoteFilter
quotesFilterReducer={
!isAlternative
? innerQuotesFilterReducer
: innerAlternativeQuotesFilterReducer
}
/>
</Left>
{!isAlternative && !timer.isStopped && (
<Right>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ReactNode } from 'react';
import styled from 'styled-components';
import { typography } from '@trezor/theme';
import { BuyCryptoPaymentMethod, SavingsPaymentMethod, SellCryptoPaymentMethod } from 'invity-api';
import { Translation } from 'src/components/suite';

const Text = styled.div`
display: flex;
align-items: center;
${typography.body};
color: ${({ theme }) => theme.textDefault};
`;

interface CoinmarketPaymentTypeProps {
children?: ReactNode;
method?: BuyCryptoPaymentMethod | SellCryptoPaymentMethod | SavingsPaymentMethod;
methodName?: string;
}
type TranslatedPaymentMethod = 'bankTransfer' | 'creditCard';

type PaymentMethodId = `TR_PAYMENT_METHOD_${Uppercase<TranslatedPaymentMethod>}`;

const getPaymentMethod = (method: TranslatedPaymentMethod): PaymentMethodId =>
`TR_PAYMENT_METHOD_${method.toUpperCase() as Uppercase<TranslatedPaymentMethod>}`;

export const CoinmarketPaymentPlainType = ({
children,
method,
methodName,
}: CoinmarketPaymentTypeProps) => (
<div>
<Text>
{method ? (
<>
{method === 'bankTransfer' || method === 'creditCard' ? (
<Translation id={getPaymentMethod(method)} />
) : (
<Text>{methodName || method}</Text>
)}
</>
) : (
<Text>
<Translation id="TR_PAYMENT_METHOD_UNKNOWN" />
</Text>
)}
</Text>
{children}
</div>
);
Loading

0 comments on commit d374579

Please sign in to comment.