Skip to content

Commit

Permalink
chore: remove swap buy amount flow (#5645)
Browse files Browse the repository at this point in the history
### Description

As the title, this PR includes:
- removing the buy experiment statsig variables
- removing the `updatedField` swap screen state variable and its
references (it always has the value `Field.FROM` now) - the
`updatedField` value is only changed on the `changeAmount` action.
- removing some tests about the buy flow

### Test plan

Unit tests pass, i can still swap

### Related issues

- Fixes RET-920

### Backwards compatibility

Y

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [ ] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
kathaypacific authored Jul 16, 2024
1 parent 7aefac0 commit c758465
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 151 deletions.
6 changes: 0 additions & 6 deletions src/statsig/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,6 @@ export const FeatureGates = {

export const ExperimentConfigs = {
// NOTE: the keys of defaultValues MUST be parameter names
[StatsigExperiments.SWAP_BUY_AMOUNT]: {
experimentName: StatsigExperiments.SWAP_BUY_AMOUNT,
defaultValues: {
swapBuyAmountEnabled: true,
},
},
[StatsigExperiments.ONBOARDING_TERMS_AND_CONDITIONS]: {
experimentName: StatsigExperiments.ONBOARDING_TERMS_AND_CONDITIONS,
defaultValues: {
Expand Down
1 change: 0 additions & 1 deletion src/statsig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export enum StatsigFeatureGates {
}

export enum StatsigExperiments {
SWAP_BUY_AMOUNT = 'swap_buy_amount',
ONBOARDING_TERMS_AND_CONDITIONS = 'onboarding_terms_and_conditions',
}

Expand Down
6 changes: 3 additions & 3 deletions src/swap/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import TokenDisplay from 'src/components/TokenDisplay'
import TokenIcon, { IconSize } from 'src/components/TokenIcon'
import Touchable from 'src/components/Touchable'
import DownArrowIcon from 'src/icons/DownArrowIcon'
import { NETWORK_NAMES } from 'src/shared/conts'
import Colors from 'src/styles/colors'
import fontStyles, { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { TokenBalance } from 'src/tokens/slice'
import { NETWORK_NAMES } from 'src/shared/conts'

interface Props {
onInputChange(value: string): void
onInputChange?(value: string): void
inputValue?: string | null
parsedInputValue?: BigNumber | null
onPressMax?(): void
Expand Down Expand Up @@ -106,7 +106,7 @@ const SwapAmountInput = ({
forwardedRef={textInputRef}
onChangeText={(value) => {
handleSetStartPosition(undefined)
onInputChange(value)
onInputChange?.(value)
}}
value={inputValue || undefined}
placeholder="0"
Expand Down
95 changes: 1 addition & 94 deletions src/swap/SwapScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,47 +507,6 @@ describe('SwapScreen', () => {
expect(getByText('swapScreen.confirmSwap')).not.toBeDisabled()
})

it('should keep the from amount in sync with the exchange rate', async () => {
mockFetch.mockResponse(
JSON.stringify({
...defaultQuote,
unvalidatedSwapTransaction: {
...defaultQuote.unvalidatedSwapTransaction,
price: '0.12345678',
},
})
)
const { swapScreen, swapFromContainer, swapToContainer, getByText, getByTestId } = renderScreen(
{}
)

selectSwapTokens('CELO', 'cUSD', swapScreen)
fireEvent.changeText(within(swapToContainer).getByTestId('SwapAmountInput/Input'), '1.234')

await act(() => {
jest.runOnlyPendingTimers()
})
expect(mockFetch.mock.calls.length).toEqual(1)
expect(mockFetch.mock.calls[0][0]).toEqual(
`${
networkConfig.getSwapQuoteUrl
}?buyToken=${mockCusdAddress}&buyIsNative=false&buyNetworkId=${
NetworkId['celo-alfajores']
}&sellToken=${mockCeloAddress}&sellIsNative=true&sellNetworkId=${
NetworkId['celo-alfajores']
}&buyAmount=1234000000000000000&userAddress=${mockAccount.toLowerCase()}&slippagePercentage=0.3`
)

expect(getByTestId('SwapTransactionDetails/ExchangeRate')).toHaveTextContent(
'1 CELO ≈ 8.10000 cUSD'
)
expect(within(swapFromContainer).getByTestId('SwapAmountInput/Input').props.value).toBe(
'0.15234566652'
)
expect(within(swapToContainer).getByTestId('SwapAmountInput/Input').props.value).toBe('1.234')
expect(getByText('swapScreen.confirmSwap')).not.toBeDisabled()
})

it('should allow selecting cross-chain tokens and show cross-chain message', async () => {
jest
.mocked(getFeatureGate)
Expand Down Expand Up @@ -884,55 +843,6 @@ describe('SwapScreen', () => {
expect(getByText('swapScreen.confirmSwap')).not.toBeDisabled()
})

it('should support to amount with comma as the decimal separator', async () => {
// This only changes the display format, the input is parsed with getNumberFormatSettings
BigNumber.config({
FORMAT: {
decimalSeparator: ',',
},
})
mockGetNumberFormatSettings.mockReturnValue({ decimalSeparator: ',' })
mockFetch.mockResponse(
JSON.stringify({
...defaultQuote,
unvalidatedSwapTransaction: {
...defaultQuote.unvalidatedSwapTransaction,
price: '0.12345678',
},
})
)
const { swapScreen, swapFromContainer, swapToContainer, getByText, getByTestId } = renderScreen(
{}
)

selectSwapTokens('CELO', 'cUSD', swapScreen)
fireEvent.changeText(within(swapToContainer).getByTestId('SwapAmountInput/Input'), '1,234')

await act(() => {
jest.runOnlyPendingTimers()
})

expect(mockFetch.mock.calls.length).toEqual(1)
expect(mockFetch.mock.calls[0][0]).toEqual(
`${
networkConfig.getSwapQuoteUrl
}?buyToken=${mockCusdAddress}&buyIsNative=false&buyNetworkId=${
NetworkId['celo-alfajores']
}&sellToken=${mockCeloAddress}&sellIsNative=true&sellNetworkId=${
NetworkId['celo-alfajores']
}&buyAmount=1234000000000000000&userAddress=${mockAccount.toLowerCase()}&slippagePercentage=0.3`
)

expect(getByTestId('SwapTransactionDetails/ExchangeRate')).toHaveTextContent(
'1 CELO ≈ 8,10000 cUSD'
)
expect(within(swapFromContainer).getByTestId('SwapAmountInput/Input').props.value).toBe(
'0,15234566652'
)
expect(within(swapToContainer).getByTestId('SwapAmountInput/Input').props.value).toBe('1,234')
expect(getByText('swapScreen.confirmSwap')).not.toBeDisabled()
})

it('should set max from value', async () => {
mockFetch.mockResponse(defaultQuoteResponse)
const { swapFromContainer, swapToContainer, getByText, getByTestId, swapScreen } = renderScreen(
Expand Down Expand Up @@ -1322,10 +1232,7 @@ describe('SwapScreen', () => {
expect(within(swapToContainer).getByTestId('SwapAmountInput/Input')).toBeTruthy()
})

it('should disable buy amount input when swap buy amount experiment is set is false', () => {
jest.mocked(getExperimentParams).mockReturnValue({
swapBuyAmountEnabled: false,
})
it('should disable editing of the buy token amount', () => {
const { swapFromContainer, swapToContainer, swapScreen } = renderScreen({})

selectSwapTokens('CELO', 'cUSD', swapScreen)
Expand Down
74 changes: 27 additions & 47 deletions src/swap/SwapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import { Screens } from 'src/navigator/Screens'
import { StackParamList } from 'src/navigator/types'
import { useDispatch, useSelector } from 'src/redux/hooks'
import { NETWORK_NAMES } from 'src/shared/conts'
import { getDynamicConfigParams, getExperimentParams, getFeatureGate } from 'src/statsig'
import { DynamicConfigs, ExperimentConfigs } from 'src/statsig/constants'
import { StatsigDynamicConfigs, StatsigExperiments, StatsigFeatureGates } from 'src/statsig/types'
import { getDynamicConfigParams, getFeatureGate } from 'src/statsig'
import { DynamicConfigs } from 'src/statsig/constants'
import { StatsigDynamicConfigs, StatsigFeatureGates } from 'src/statsig/types'
import colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
Expand Down Expand Up @@ -79,7 +79,6 @@ interface SwapState {
toTokenId: string | undefined
// Raw input values (can contain region specific decimal separators)
inputSwapAmount: SwapAmount
updatedField: Field
selectingField: Field | null
selectingNoUsdPriceToken: SelectingNoUsdPriceToken | null
confirmingSwap: boolean
Expand All @@ -94,7 +93,6 @@ function getInitialState(fromTokenId?: string, toTokenId?: string): SwapState {
fromTokenId,
toTokenId,
inputSwapAmount: DEFAULT_INPUT_SWAP_AMOUNT,
updatedField: Field.FROM,
selectingField: null,
selectingNoUsdPriceToken: null,
confirmingSwap: false,
Expand All @@ -107,11 +105,10 @@ const swapSlice = createSlice({
name: 'swapSlice',
initialState: getInitialState,
reducers: {
changeAmount: (state, action: PayloadAction<{ field: Field; value: string }>) => {
const { field, value } = action.payload
changeAmount: (state, action: PayloadAction<{ value: string }>) => {
const { value } = action.payload
state.confirmingSwap = false
state.startedSwapId = null
state.updatedField = field
if (!value) {
state.inputSwapAmount = DEFAULT_INPUT_SWAP_AMOUNT
return
Expand All @@ -121,13 +118,12 @@ const swapSlice = createSlice({
if (!sanitizedValue) {
return
}
state.inputSwapAmount[field] = sanitizedValue
state.inputSwapAmount[Field.FROM] = sanitizedValue
},
chooseMaxFromAmount: (state, action: PayloadAction<{ fromTokenBalance: BigNumber }>) => {
const { fromTokenBalance } = action.payload
state.confirmingSwap = false
state.startedSwapId = null
state.updatedField = Field.FROM
// We try the current balance first, and we will prompt the user if it's too high
state.inputSwapAmount[Field.FROM] = fromTokenBalance.toFormat({
decimalSeparator: getNumberFormatSettings().decimalSeparator,
Expand Down Expand Up @@ -168,21 +164,17 @@ const swapSlice = createSlice({
},
quoteUpdated: (state, action: PayloadAction<{ quote: QuoteResult | null }>) => {
const { quote } = action.payload
const { updatedField } = state
state.confirmingSwap = false
const otherField = updatedField === Field.FROM ? Field.TO : Field.FROM
if (!quote) {
state.inputSwapAmount[otherField] = ''
state.inputSwapAmount[Field.TO] = ''
return
}

const { decimalSeparator } = getNumberFormatSettings()
const parsedAmount = parseInputAmount(state.inputSwapAmount[updatedField], decimalSeparator)
const parsedAmount = parseInputAmount(state.inputSwapAmount[Field.FROM], decimalSeparator)

const newAmount = parsedAmount.multipliedBy(
new BigNumber(quote.price).pow(updatedField === Field.FROM ? 1 : -1)
)
state.inputSwapAmount[otherField] = newAmount.toFormat({
const newAmount = parsedAmount.multipliedBy(new BigNumber(quote.price))
state.inputSwapAmount[Field.TO] = newAmount.toFormat({
decimalSeparator,
})
},
Expand Down Expand Up @@ -244,9 +236,6 @@ export function SwapScreen({ route }: Props) {

const { decimalSeparator } = getNumberFormatSettings()

const { swapBuyAmountEnabled } = getExperimentParams(
ExperimentConfigs[StatsigExperiments.SWAP_BUY_AMOUNT]
)
const { maxSlippagePercentage, enableAppFee } = getDynamicConfigParams(
DynamicConfigs[StatsigDynamicConfigs.SWAP_CONFIG]
)
Expand All @@ -270,7 +259,6 @@ export function SwapScreen({ route }: Props) {
fromTokenId,
toTokenId,
inputSwapAmount,
updatedField,
selectingField,
selectingNoUsdPriceToken,
confirmingSwap,
Expand Down Expand Up @@ -327,7 +315,7 @@ export function SwapScreen({ route }: Props) {
(quote &&
(quote.fromTokenId !== fromToken?.tokenId ||
quote.toTokenId !== toToken?.tokenId ||
!quote.swapAmount.eq(parsedSwapAmount[updatedField]))) ||
!quote.swapAmount.eq(parsedSwapAmount[Field.FROM]))) ||
fetchingSwapQuote

const confirmSwapIsLoading = swapStatus === 'started'
Expand Down Expand Up @@ -355,18 +343,18 @@ export function SwapScreen({ route }: Props) {
quote &&
quote.toTokenId === toToken.tokenId &&
quote.fromTokenId === fromToken.tokenId &&
quote.swapAmount.eq(parsedSwapAmount[updatedField])
quote.swapAmount.eq(parsedSwapAmount[Field.FROM])

const debouncedRefreshQuote = setTimeout(() => {
if (fromToken && toToken && parsedSwapAmount[updatedField].gt(0) && !quoteKnown) {
void refreshQuote(fromToken, toToken, parsedSwapAmount, updatedField)
if (fromToken && toToken && parsedSwapAmount[Field.FROM].gt(0) && !quoteKnown) {
void refreshQuote(fromToken, toToken, parsedSwapAmount, Field.FROM)
}
}, FETCH_UPDATED_QUOTE_DEBOUNCE_TIME)

return () => {
clearTimeout(debouncedRefreshQuote)
}
}, [fromToken, toToken, parsedSwapAmount, updatedField, quote])
}, [fromToken, toToken, parsedSwapAmount, quote])

useEffect(() => {
localDispatch(quoteUpdated({ quote }))
Expand Down Expand Up @@ -394,10 +382,9 @@ export function SwapScreen({ route }: Props) {
[Field.FROM]: parsedSwapAmount[Field.FROM].toString(),
[Field.TO]: parsedSwapAmount[Field.TO].toString(),
},
updatedField,
updatedField: Field.FROM,
}

const swapAmountParam = updatedField === Field.FROM ? 'sellAmount' : 'buyAmount'
const { estimatedPriceImpact, price, allowanceTarget, appFeePercentageIncludedInPrice } = quote

const resultType = quote.preparedTransactions.type
Expand All @@ -417,8 +404,8 @@ export function SwapScreen({ route }: Props) {
fromTokenId: fromToken.tokenId,
fromTokenNetworkId: fromToken.networkId,
fromTokenIsImported: !!fromToken.isManuallyImported,
amount: inputSwapAmount[updatedField],
amountType: swapAmountParam,
amount: inputSwapAmount[Field.FROM],
amountType: 'sellAmount',
allowanceTarget,
estimatedPriceImpact,
price,
Expand Down Expand Up @@ -582,8 +569,8 @@ export function SwapScreen({ route }: Props) {
handleConfirmSelectToken(selectedToken, tokenPositionInList)
}

const handleChangeAmount = (fieldType: Field) => (value: string) => {
localDispatch(changeAmount({ field: fieldType, value }))
const handleChangeAmount = (value: string) => {
localDispatch(changeAmount({ value }))
if (!value) {
clearQuote()
}
Expand Down Expand Up @@ -733,8 +720,8 @@ export function SwapScreen({ route }: Props) {
fromTokenId: fromToken.tokenId,
fromTokenNetworkId: fromToken?.networkId,
fromTokenIsImported: !!fromToken.isManuallyImported,
amount: parsedSwapAmount[updatedField].toString(),
amountType: updatedField === Field.FROM ? 'sellAmount' : 'buyAmount',
amount: parsedSwapAmount[Field.FROM].toString(),
amountType: 'sellAmount',
priceImpact: quote.estimatedPriceImpact,
provider: quote.provider,
})
Expand Down Expand Up @@ -774,13 +761,13 @@ export function SwapScreen({ route }: Props) {
>
<View style={styles.swapAmountsContainer}>
<SwapAmountInput
onInputChange={handleChangeAmount(Field.FROM)}
onInputChange={handleChangeAmount}
inputValue={inputSwapAmount[Field.FROM]}
parsedInputValue={parsedSwapAmount[Field.FROM]}
onSelectToken={handleShowTokenSelect(Field.FROM)}
token={fromToken}
style={styles.fromSwapAmountInput}
loading={updatedField === Field.TO && quoteUpdatePending}
loading={false}
autoFocus
inputError={fromSwapAmountError}
onPressMax={handleSetMaxFromAmount}
Expand All @@ -802,15 +789,14 @@ export function SwapScreen({ route }: Props) {
</Touchable>
</View>
<SwapAmountInput
onInputChange={handleChangeAmount(Field.TO)}
parsedInputValue={parsedSwapAmount[Field.TO]}
inputValue={inputSwapAmount[Field.TO]}
onSelectToken={handleShowTokenSelect(Field.TO)}
token={toToken}
style={styles.toSwapAmountInput}
loading={updatedField === Field.FROM && quoteUpdatePending}
loading={quoteUpdatePending}
buttonPlaceholder={t('swapScreen.selectTokenLabel')}
editable={swapBuyAmountEnabled}
editable={false}
borderRadius={Spacing.Regular16}
/>

Expand Down Expand Up @@ -870,13 +856,7 @@ export function SwapScreen({ route }: Props) {
quote.preparedTransactions.type !== 'need-decrease-spend-amount-for-gas'
)
return
handleChangeAmount(updatedField)(
// ensure units are for the asset whose amount is being selected by the user
(updatedField === Field.FROM
? quote.preparedTransactions.decreasedSpendAmount
: quote.preparedTransactions.decreasedSpendAmount.times(quote.price)
).toString()
)
handleChangeAmount(quote.preparedTransactions.decreasedSpendAmount.toString())
}}
ctaLabel={t('swapScreen.decreaseSwapAmountForGasWarning.cta')}
style={styles.warning}
Expand Down

0 comments on commit c758465

Please sign in to comment.