Skip to content

Commit

Permalink
fix(cico): Add sorting to CICO currency bottom sheet (#4566)
Browse files Browse the repository at this point in the history
### 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 <[email protected]>
  • Loading branch information
finnian0826 and MuckT authored Dec 6, 2023
1 parent 9dc2d04 commit 8a513db
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 46 deletions.
137 changes: 93 additions & 44 deletions src/fiatExchanges/FiatExchangeCurrencyBottomSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
Expand All @@ -133,17 +142,24 @@ describe(FiatExchangeCurrencyBottomSheet, () => {
/>
</Provider>
)
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(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
Expand All @@ -153,15 +169,14 @@ describe(FiatExchangeCurrencyBottomSheet, () => {
/>
</Provider>
)
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(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
Expand All @@ -171,16 +186,23 @@ describe(FiatExchangeCurrencyBottomSheet, () => {
/>
</Provider>
)
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(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
Expand All @@ -190,15 +212,14 @@ describe(FiatExchangeCurrencyBottomSheet, () => {
/>
</Provider>
)
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(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
Expand All @@ -208,16 +229,23 @@ describe(FiatExchangeCurrencyBottomSheet, () => {
/>
</Provider>
)
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(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
Expand All @@ -227,10 +255,31 @@ describe(FiatExchangeCurrencyBottomSheet, () => {
/>
</Provider>
)
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(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
params={{
flow: FiatExchangeFlow.CashIn,
}}
/>
</Provider>
)
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')
})
})
7 changes: 5 additions & 2 deletions src/fiatExchanges/FiatExchangeCurrencyBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'

Expand All @@ -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())
Expand Down
6 changes: 6 additions & 0 deletions src/statsig/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
},
},
}
1 change: 1 addition & 0 deletions src/statsig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
29 changes: 29 additions & 0 deletions src/tokens/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 8a513db

Please sign in to comment.