From 8a513dbaee7df8b514d76559f157de4b9cf619c9 Mon Sep 17 00:00:00 2001 From: Finnian Jacobson-Schulte <140328381+finnian0826@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:45:57 -0600 Subject: [PATCH] fix(cico): Add sorting to CICO currency bottom sheet (#4566) ### Description No ordering before, now tokens are sorted before being displayed in the bottom sheet. Dynamic config: https://console.statsig.com/4plizaPmWwPL21ASV4QAO0/dynamic_configs/cico_token_info Dynamic config to review: https://console.statsig.com/4plizaPmWwPL21ASV4QAO0/dynamic_configs/cico_token_info/preview/6mIWqOsAuVnZiwwU7D4EL3 ### Test plan Updated unit tests to check for ordering. Manual test shows correct order, logged ordering to confirm that sorting was done correctly. ![cicoOrder](https://github.com/valora-inc/wallet/assets/140328381/bd230ee8-d3df-4527-afa0-3046e28d23c9) ### Related issues N/A ### Backwards compatibility Yes --------- Co-authored-by: Tom McGuire --- .../FiatExchangeCurrencyBottomSheet.test.tsx | 137 ++++++++++++------ .../FiatExchangeCurrencyBottomSheet.tsx | 7 +- src/statsig/constants.ts | 6 + src/statsig/types.ts | 1 + src/tokens/utils.ts | 29 ++++ 5 files changed, 134 insertions(+), 46 deletions(-) diff --git a/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.test.tsx b/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.test.tsx index e1d2fa48200..66c3fca3ce4 100644 --- a/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.test.tsx +++ b/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.test.tsx @@ -29,7 +29,7 @@ const MOCK_STORE_DATA = { networkId: NetworkId['celo-alfajores'], name: 'cUSD', address: mockCusdAddress, - balance: '0', + balance: '50', priceUsd: '1', symbol: 'cUSD', priceFetchedAt: mockDate, @@ -43,7 +43,7 @@ const MOCK_STORE_DATA = { networkId: NetworkId['celo-alfajores'], name: 'cEUR', address: mockCeurAddress, - balance: '50', + balance: '0', priceUsd: '0.5', symbol: 'cEUR', isSupercharged: true, @@ -57,7 +57,7 @@ const MOCK_STORE_DATA = { networkId: NetworkId['celo-alfajores'], name: 'Celo', address: mockCeloAddress, - balance: '100', + balance: '0', priceUsd: '0.2', symbol: 'CELO', isSupercharged: true, @@ -71,7 +71,7 @@ const MOCK_STORE_DATA = { networkId: NetworkId['celo-alfajores'], name: 'cREAL', address: mockCrealAddress, - balance: '20', + balance: '10', priceUsd: '0.75', symbol: 'cREAL', isSupercharged: true, @@ -83,7 +83,7 @@ const MOCK_STORE_DATA = { tokenId: mockEthTokenId, networkId: NetworkId['ethereum-sepolia'], name: 'Ether', - balance: '1', + balance: '0', priceUsd: '0.1000', symbol: 'ETH', isSupercharged: true, @@ -120,10 +120,19 @@ describe(FiatExchangeCurrencyBottomSheet, () => { const mockStore = createMockStore(MOCK_STORE_DATA) beforeEach(() => { jest.clearAllMocks() + jest.mocked(getDynamicConfigParams).mockReturnValue({ + showCico: ['celo-alfajores'], + tokenInfo: { + [mockEthTokenId]: { cicoOrder: 1 }, + [mockCeloTokenId]: { cicoOrder: 2 }, + [mockCusdTokenId]: { cicoOrder: 3 }, + [mockCeurTokenId]: { cicoOrder: 4 }, + [mockCrealTokenId]: { cicoOrder: 5 }, + }, + }) }) it('shows the correct tokens for cash in (multichain disabled, no ETH)', () => { - jest.mocked(getDynamicConfigParams).mockReturnValue({ showCico: ['celo-alfajores'] }) - const { queryByTestId } = render( + const { queryByTestId, getAllByTestId } = render( { /> ) - expect(queryByTestId('cUSDSymbol')).toBeTruthy() - expect(queryByTestId('cEURSymbol')).toBeTruthy() - expect(queryByTestId('cREALSymbol')).toBeTruthy() - expect(queryByTestId('CELOSymbol')).toBeTruthy() + expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('CELO') + expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('cUSD') + expect(getAllByTestId('TokenBalanceItem')[2]).toHaveTextContent('cEUR') + expect(getAllByTestId('TokenBalanceItem')[3]).toHaveTextContent('cREAL') expect(queryByTestId('ETHSymbol')).toBeFalsy() }) it('shows the correct tokens for cash in (multichain)', () => { - jest - .mocked(getDynamicConfigParams) - .mockReturnValue({ showCico: ['celo-alfajores', 'ethereum-sepolia'] }) - const { queryByTestId } = render( + jest.mocked(getDynamicConfigParams).mockReturnValue({ + showCico: ['celo-alfajores', 'ethereum-sepolia'], + tokenInfo: { + [mockEthTokenId]: { cicoOrder: 1 }, + [mockCeloTokenId]: { cicoOrder: 2 }, + [mockCusdTokenId]: { cicoOrder: 3 }, + [mockCeurTokenId]: { cicoOrder: 4 }, + [mockCrealTokenId]: { cicoOrder: 5 }, + }, + }) + const { getAllByTestId } = render( { /> ) - expect(queryByTestId('cUSDSymbol')).toBeTruthy() - expect(queryByTestId('cEURSymbol')).toBeTruthy() - expect(queryByTestId('cREALSymbol')).toBeTruthy() - expect(queryByTestId('CELOSymbol')).toBeTruthy() - expect(queryByTestId('ETHSymbol')).toBeTruthy() + expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('ETH') + expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('CELO') + expect(getAllByTestId('TokenBalanceItem')[2]).toHaveTextContent('cUSD') + expect(getAllByTestId('TokenBalanceItem')[3]).toHaveTextContent('cEUR') + expect(getAllByTestId('TokenBalanceItem')[4]).toHaveTextContent('cREAL') }) it('shows the correct tokens for cash out', () => { - jest.mocked(getDynamicConfigParams).mockReturnValue({ showCico: ['celo-alfajores'] }) - const { queryByTestId } = render( + const { queryByTestId, getAllByTestId } = render( { /> ) - expect(queryByTestId('cUSDSymbol')).toBeTruthy() - expect(queryByTestId('cEURSymbol')).toBeTruthy() + expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('CELO') + expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('cUSD') + expect(getAllByTestId('TokenBalanceItem')[2]).toHaveTextContent('cEUR') expect(queryByTestId('cREALSymbol')).toBeFalsy() - expect(queryByTestId('CELOSymbol')).toBeTruthy() }) it('shows the correct tokens for cash out (multichain)', () => { - jest - .mocked(getDynamicConfigParams) - .mockReturnValue({ showCico: ['celo-alfajores', 'ethereum-sepolia'] }) - const { queryByTestId } = render( + jest.mocked(getDynamicConfigParams).mockReturnValue({ + showCico: ['celo-alfajores', 'ethereum-sepolia'], + tokenInfo: { + [mockEthTokenId]: { cicoOrder: 1 }, + [mockCeloTokenId]: { cicoOrder: 2 }, + [mockCusdTokenId]: { cicoOrder: 3 }, + [mockCeurTokenId]: { cicoOrder: 4 }, + [mockCrealTokenId]: { cicoOrder: 5 }, + }, + }) + const { queryByTestId, getAllByTestId } = render( { /> ) - expect(queryByTestId('cUSDSymbol')).toBeTruthy() - expect(queryByTestId('cEURSymbol')).toBeTruthy() + expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('CELO') + expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('cUSD') + expect(getAllByTestId('TokenBalanceItem')[2]).toHaveTextContent('cEUR') expect(queryByTestId('cREALSymbol')).toBeFalsy() - expect(queryByTestId('CELOSymbol')).toBeTruthy() expect(queryByTestId('ETHSymbol')).toBeFalsy() }) it('shows the correct tokens for cash spend', () => { - jest.mocked(getDynamicConfigParams).mockReturnValue({ showCico: ['celo-alfajores'] }) - const { queryByTestId } = render( + const { queryByTestId, getAllByTestId } = render( { /> ) - expect(queryByTestId('cUSDSymbol')).toBeTruthy() - expect(queryByTestId('cEURSymbol')).toBeTruthy() - expect(queryByTestId('cREALSymbol')).toBeFalsy() + expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('cUSD') + expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('cEUR') expect(queryByTestId('CELOSymbol')).toBeFalsy() + expect(queryByTestId('cREALSymbol')).toBeFalsy() }) it('shows the correct tokens for cash spend (multichain)', () => { - jest - .mocked(getDynamicConfigParams) - .mockReturnValue({ showCico: ['celo-alfajores', 'ethereum-sepolia'] }) - const { queryByTestId } = render( + jest.mocked(getDynamicConfigParams).mockReturnValue({ + showCico: ['celo-alfajores', 'ethereum-sepolia'], + tokenInfo: { + [mockEthTokenId]: { cicoOrder: 1 }, + [mockCeloTokenId]: { cicoOrder: 2 }, + [mockCusdTokenId]: { cicoOrder: 3 }, + [mockCeurTokenId]: { cicoOrder: 4 }, + [mockCrealTokenId]: { cicoOrder: 5 }, + }, + }) + const { queryByTestId, getAllByTestId } = render( { /> ) - expect(queryByTestId('cUSDSymbol')).toBeTruthy() - expect(queryByTestId('cEURSymbol')).toBeTruthy() - expect(queryByTestId('cREALSymbol')).toBeFalsy() + expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('cUSD') + expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('cEUR') expect(queryByTestId('CELOSymbol')).toBeFalsy() + expect(queryByTestId('cREALSymbol')).toBeFalsy() expect(queryByTestId('ETHSymbol')).toBeFalsy() }) + it('shows the correct order when cicoOrder missing/same value', () => { + jest.mocked(getDynamicConfigParams).mockReturnValue({ + showCico: ['celo-alfajores', 'ethereum-sepolia'], + tokenInfo: { [mockCusdTokenId]: { cicoOrder: 1 }, [mockCrealTokenId]: { cicoOrder: 1 } }, + }) + const { getAllByTestId } = render( + + + + ) + expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('cUSD') + expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('cREAL') + expect(getAllByTestId('TokenBalanceItem')[2]).toHaveTextContent('cEUR') + expect(getAllByTestId('TokenBalanceItem')[3]).toHaveTextContent('CELO') + expect(getAllByTestId('TokenBalanceItem')[4]).toHaveTextContent('ETH') + }) }) diff --git a/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.tsx b/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.tsx index 4d05b043ba0..12da7c659e5 100644 --- a/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.tsx +++ b/src/fiatExchanges/FiatExchangeCurrencyBottomSheet.tsx @@ -1,5 +1,5 @@ import { BottomSheetScreenProps } from '@th3rdwave/react-navigation-bottom-sheet' -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet, Text } from 'react-native' import { useDispatch } from 'react-redux' @@ -13,6 +13,7 @@ import { Spacing } from 'src/styles/styles' import { TokenBalanceItem } from 'src/tokens/TokenBalanceItem' import { useCashInTokens, useCashOutTokens, useSpendTokens } from 'src/tokens/hooks' import { TokenBalance } from 'src/tokens/slice' +import { sortCicoTokens } from 'src/tokens/utils' import { resolveCurrency } from 'src/utils/currencies' import { CICOFlow, FiatExchangeFlow } from './utils' @@ -25,13 +26,15 @@ function FiatExchangeCurrencyBottomSheet({ route }: Props) { const cashInTokens = useCashInTokens() const cashOutTokens = useCashOutTokens(true) const spendTokens = useSpendTokens() - const tokenList = + const unsortedTokenList = flow === FiatExchangeFlow.CashIn ? cashInTokens : flow === FiatExchangeFlow.CashOut ? cashOutTokens : spendTokens + const tokenList = useMemo(() => unsortedTokenList.sort(sortCicoTokens), [unsortedTokenList]) + // Fetch FiatConnect providers silently in the background early in the CICO funnel useEffect(() => { dispatch(fetchFiatConnectProviders()) diff --git a/src/statsig/constants.ts b/src/statsig/constants.ts index 60023f2b057..79d14455647 100644 --- a/src/statsig/constants.ts +++ b/src/statsig/constants.ts @@ -95,4 +95,10 @@ export const DynamicConfigs = { maxSlippagePercentage: '0.3', }, }, + [StatsigDynamicConfigs.CICO_TOKEN_INFO]: { + configName: StatsigDynamicConfigs.CICO_TOKEN_INFO, + defaultValues: { + tokenInfo: {} as { [tokenId: string]: { cicoOrder: number } }, + }, + }, } diff --git a/src/statsig/types.ts b/src/statsig/types.ts index a07c3458566..264d847512d 100644 --- a/src/statsig/types.ts +++ b/src/statsig/types.ts @@ -4,6 +4,7 @@ export enum StatsigDynamicConfigs { MULTI_CHAIN_FEATURES = 'multi_chain_features', DAPP_WEBVIEW_CONFIG = 'dapp_webview_config', SWAP_CONFIG = 'swap_config', + CICO_TOKEN_INFO = 'cico_token_info', } export enum StatsigFeatureGates { diff --git a/src/tokens/utils.ts b/src/tokens/utils.ts index 1cbf9f4ca15..aa786f7a782 100644 --- a/src/tokens/utils.ts +++ b/src/tokens/utils.ts @@ -87,6 +87,35 @@ export function sortFirstStableThenCeloThenOthersByUsdBalance( return usdBalance(token2).comparedTo(usdBalance(token1)) } +/** + * + * Sorts by: + * 1. cicoOrder value, smallest first + * 1.1. If both tokens have cicoOrder value, sort by sortFirstStableThenCeloThenOthersByUsdBalance + * 2. If only one token has cicoOrder value, it goes first + * 3. If neither token has cicoOrder value, sort by sortFirstStableThenCeloThenOthersByUsdBalance + */ +export function sortCicoTokens(token1: TokenBalance, token2: TokenBalance): number { + const cicoTokenInfo = getDynamicConfigParams( + DynamicConfigs[StatsigDynamicConfigs.CICO_TOKEN_INFO] + ).tokenInfo + if ( + (!cicoTokenInfo[token1.tokenId]?.cicoOrder && !cicoTokenInfo[token2.tokenId]?.cicoOrder) || + cicoTokenInfo[token1.tokenId]?.cicoOrder === cicoTokenInfo[token2.tokenId]?.cicoOrder + ) { + return sortFirstStableThenCeloThenOthersByUsdBalance(token1, token2) + } + if (!cicoTokenInfo[token1.tokenId]?.cicoOrder) { + return 1 + } + if (!cicoTokenInfo[token2.tokenId]?.cicoOrder) { + return -1 + } + return cicoTokenInfo[token1.tokenId]?.cicoOrder < cicoTokenInfo[token2.tokenId]?.cicoOrder + ? -1 + : 1 +} + export function usdBalance(token: TokenBalance): BigNumber { return token.balance.times(token.priceUsd ?? 0) }