Skip to content

Commit

Permalink
feat: update copy in buy and swap flows for UK compliance (#6100)
Browse files Browse the repository at this point in the history
### Description

As the title

### Test plan
![Simulator Screenshot - iPhone 15 Pro - 2024-09-27 at 13 33
41](https://github.com/user-attachments/assets/8b6a7f25-a984-4af6-9f6e-400e82a4b90f)
![Simulator Screenshot - iPhone 15 Pro - 2024-09-27 at 13 33
15](https://github.com/user-attachments/assets/12169a14-46e6-475d-9722-2cfd0931db98)

### Related issues

- Fixes RET-1202

### 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)

---------

Co-authored-by: Jean Regisser <jean.regisser@gmail.com>
  • Loading branch information
kathaypacific and jeanregisser authored Sep 30, 2024
1 parent fcdb2af commit 36b4546
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 28 deletions.
2 changes: 1 addition & 1 deletion __mocks__/react-i18next.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const renderTrans = ({ i18nKey, tOptions, context, children }) => {

// Output the key and any params sent to the translation function.
const translationFunction = (key, params) => {
if (typeof params !== 'object' || Object.keys(params).length === 0) {
if (typeof params !== 'object' || Object.values(params).every((value) => value === undefined)) {
return key
}
return [key, JSON.stringify(params)].join(', ')
Expand Down
4 changes: 4 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@
"somePaymentsUnavailable": "Purchases are powered by our partner providers. Some payment methods are unavailable in your region.",
"disclaimerWithSomePaymentsUnavailable": "Payments are powered by network partners. Fees may vary. Some payment methods are unavailable in your region. <0>Learn more.</0>",
"disclaimer": "Payments are powered by network partners. Fees may vary. <0>Learn more.</0>",
"disclaimerUK": "This is not an invitation to buy cryptoassets. We only provide a list of providers who are selling cryptoassets with price information and a link to their website.",
"learnMore": "Learn more.",
"whyMissingPayments": "Why don't I see more payment methods?",
"dismiss": "Dismiss",
Expand Down Expand Up @@ -1697,6 +1698,7 @@
"selectCurrencyTitle": "Add Funds",
"exchangeAmountTitle": "Add {{currency}}",
"selectProviderHeader": "Select Payment Method",
"selectProviderHeader_UK": "Provider Details",
"depositExchangeTitle": "Deposit Crypto"
},
"cashOut": {
Expand Down Expand Up @@ -1787,11 +1789,13 @@
"title": "Swap",
"review": "Review",
"confirmSwap": "Confirm Swap",
"confirmSwap_UK": "I Want To Swap",
"swapFrom": "FROM",
"swapTo": "TO",
"insufficientFunds": "Insufficient {{token}} balance. Try a smaller amount or choose a different asset.",
"fetchSwapQuoteFailed": "Sorry for the delay! It's taking longer than usual to get the best exchange rate. Please try again in a few minutes.",
"disclaimer": "Best rate determined by decentralized sources. <0>Learn more</0>",
"disclaimer_UK": "This is not an invitation to sell or buy cryptoassets. By clicking I Want To Swap you are instructing a third party swap router to execute your swap transaction at the best rate. Best rate provided by network partners. <0>Learn more</0>",
"swapFromTokenSelection": "SWAP FROM",
"swapToTokenSelection": "SWAP TO",
"tokenUsdValueUnknown": "-",
Expand Down
23 changes: 23 additions & 0 deletions src/fiatExchanges/FiatExchangeCurrencyBottomSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,29 @@ describe(FiatExchangeCurrencyBottomSheet, () => {
expect(getByText('tokenBottomSheet.filters.selectNetwork')).toBeTruthy()
})

it('hides the popular filter for UK compliance', () => {
jest
.mocked(getFeatureGate)
.mockImplementation(
(feature) =>
feature === StatsigFeatureGates.SHOW_CASH_IN_TOKEN_FILTERS ||
feature === StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT
)
const { queryByText, getByText } = render(
<Provider store={mockStore}>
<MockedNavigator
component={FiatExchangeCurrencyBottomSheet}
params={{ flow: FiatExchangeFlow.CashIn }}
/>
</Provider>
)

expect(queryByText('tokenBottomSheet.filters.popular')).toBeFalsy()
expect(getByText('tokenBottomSheet.filters.stablecoins')).toBeTruthy()
expect(getByText('tokenBottomSheet.filters.gasTokens')).toBeTruthy()
expect(getByText('tokenBottomSheet.filters.selectNetwork')).toBeTruthy()
})

it('popular filter filters correctly', () => {
jest
.mocked(getFeatureGate)
Expand Down
19 changes: 13 additions & 6 deletions src/fiatExchanges/FiatExchangeCurrencyBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ function useFilterChips(
() => new Set(feeCurrencies.map((currency) => currency.tokenId)),
[feeCurrencies]
)

const showUKCompliantVariant = getFeatureGate(StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)

if (
flow !== FiatExchangeFlow.CashIn ||
!getFeatureGate(StatsigFeatureGates.SHOW_CASH_IN_TOKEN_FILTERS)
Expand All @@ -44,12 +47,16 @@ function useFilterChips(
).popularTokenIds

return [
{
id: 'popular',
name: t('tokenBottomSheet.filters.popular'),
filterFn: (token: TokenBalance) => popularTokenIds.includes(token.tokenId),
isSelected: false,
},
...(showUKCompliantVariant
? []
: [
{
id: 'popular',
name: t('tokenBottomSheet.filters.popular'),
filterFn: (token: TokenBalance) => popularTokenIds.includes(token.tokenId),
isSelected: false,
},
]),
{
id: 'stablecoins',
name: t('tokenBottomSheet.filters.stablecoins'),
Expand Down
7 changes: 6 additions & 1 deletion src/fiatExchanges/PaymentMethodSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { CICOFlow, PaymentMethod } from 'src/fiatExchanges/utils'
import InfoIcon from 'src/icons/InfoIcon'
import { getLocalCurrencyCode, usdToLocalCurrencyRateSelector } from 'src/localCurrency/selectors'
import { useDispatch, useSelector } from 'src/redux/hooks'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { useTokenInfo } from 'src/tokens/hooks'
Expand Down Expand Up @@ -61,6 +63,8 @@ export function PaymentMethodSection({
const [expanded, setExpanded] = useState(isExpandable)
const [newDialogVisible, setNewDialogVisible] = useState(false)

const showUKCompliantVariant = getFeatureGate(StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)

useEffect(() => {
if (sectionQuotes.length) {
AppAnalytics.track(FiatExchangeEvents.cico_providers_section_impression, {
Expand Down Expand Up @@ -258,7 +262,8 @@ export function PaymentMethodSection({
<Text style={styles.expandedInfo}>{renderInfoText(normalizedQuote)}</Text>
{index === 0 &&
!!tokenInfo &&
normalizedQuote.getFeeInCrypto(usdToLocalRate, tokenInfo) && (
normalizedQuote.getFeeInCrypto(usdToLocalRate, tokenInfo) &&
!showUKCompliantVariant && (
<Text testID={`${paymentMethod}/bestRate`} style={styles.expandedTag}>
{t('selectProviderScreen.bestRate')}
</Text>
Expand Down
24 changes: 23 additions & 1 deletion src/fiatExchanges/SelectProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { FetchMock } from 'jest-fetch-mock/types'
import * as React from 'react'
import { Provider } from 'react-redux'
import { MockStoreEnhanced } from 'redux-mock-store'
import { FiatExchangeEvents } from 'src/analytics/Events'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { FiatExchangeEvents } from 'src/analytics/Events'
import SelectProviderScreen from 'src/fiatExchanges/SelectProvider'
import { SelectProviderExchangesLink, SelectProviderExchangesText } from 'src/fiatExchanges/types'
import { LocalCurrencyCode } from 'src/localCurrency/consts'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getExperimentParams, getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import { NetworkId } from 'src/transactions/types'
import { CiCoCurrency } from 'src/utils/currencies'
import { createMockStore, getMockStackScreenProps } from 'test/utils'
Expand Down Expand Up @@ -168,6 +169,27 @@ describe(SelectProviderScreen, () => {
)
await waitFor(() => expect(fetchExchanges).toHaveBeenCalledWith('MX', mockCusdTokenId))
})
it('shows an additional disclaimer for UK compliance', async () => {
jest
.mocked(getFeatureGate)
.mockImplementation((feature) => feature === StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)
const { getByText } = render(
<Provider
store={createMockStore({
...MOCK_STORE_DATA,
fiatConnect: {
quotesError: null,
quotesLoading: false,
quotes: [mockFiatConnectQuotes[4]],
},
})}
>
<SelectProviderScreen {...mockScreenProps()} />
</Provider>
)

await waitFor(() => expect(getByText('selectProviderScreen.disclaimerUK')).toBeTruthy())
})
it('shows spinner and avoids publishing analytics event if quotes still loading', async () => {
const { getByTestId } = render(
<Provider
Expand Down
36 changes: 27 additions & 9 deletions src/fiatExchanges/SelectProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { RouteProp } from '@react-navigation/native'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import _ from 'lodash'
import React, { useEffect, useMemo, useState } from 'react'
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { useAsync } from 'react-async-hook'
import { Trans, useTranslation } from 'react-i18next'
import { ActivityIndicator, ScrollView, StyleSheet, Text, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { showError } from 'src/alert/actions'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { FiatExchangeEvents } from 'src/analytics/Events'
Expand Down Expand Up @@ -36,7 +37,6 @@ import {
} from 'src/fiatconnect/selectors'
import { fetchFiatConnectQuotes } from 'src/fiatconnect/slice'
import { readOnceFromFirebase } from 'src/firebase/firebase'
import i18n from 'src/i18n'
import {
getDefaultLocalCurrencyCode,
getLocalCurrencyCode,
Expand All @@ -48,9 +48,9 @@ import { Screens } from 'src/navigator/Screens'
import { StackParamList } from 'src/navigator/types'
import { userLocationDataSelector } from 'src/networkInfo/selectors'
import { useDispatch, useSelector } from 'src/redux/hooks'
import { getDynamicConfigParams } from 'src/statsig'
import { getDynamicConfigParams, getFeatureGate } from 'src/statsig'
import { DynamicConfigs } from 'src/statsig/constants'
import { StatsigDynamicConfigs } from 'src/statsig/types'
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 @@ -104,6 +104,7 @@ export default function SelectProviderScreen({ route, navigation }: Props) {
const tokenInfo = useTokenInfo(tokenId)

const { links } = getDynamicConfigParams(DynamicConfigs[StatsigDynamicConfigs.APP_CONFIG])
const showUKCompliantVariant = getFeatureGate(StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)

if (!tokenInfo) {
throw new Error(`Token info not found for token ID ${tokenId}`)
Expand All @@ -113,6 +114,7 @@ export default function SelectProviderScreen({ route, navigation }: Props) {
const coinbasePayEnabled = useSelector(coinbasePayEnabledSelector)
const appIdResponse = useAsync(async () => readOnceFromFirebase('coinbasePay/appId'), [])
const appId = appIdResponse.result
const insets = useSafeAreaInsets()

useEffect(() => {
if (FETCH_FIATCONNECT_QUOTES) {
Expand All @@ -133,6 +135,17 @@ export default function SelectProviderScreen({ route, navigation }: Props) {
}
}, [fiatConnectQuotesError])

useLayoutEffect(() => {
navigation.setOptions({
headerTitle:
route.params.flow === CICOFlow.CashIn
? t(`fiatExchangeFlow.cashIn.selectProviderHeader`, {
context: showUKCompliantVariant ? 'UK' : undefined,
})
: t(`fiatExchangeFlow.cashOut.selectProviderHeader`),
})
}, [route.params.flow])

const asyncExchanges = useAsync(async () => {
try {
const availableExchanges = await fetchExchanges(
Expand Down Expand Up @@ -292,7 +305,7 @@ export default function SelectProviderScreen({ route, navigation }: Props) {
}

return (
<ScrollView>
<ScrollView contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, Spacing.Thick24) }}>
<AmountSpentInfo {...route.params} />
{paymentMethodSections.map((paymentMethod) => (
<PaymentMethodSection
Expand Down Expand Up @@ -329,6 +342,11 @@ export default function SelectProviderScreen({ route, navigation }: Props) {
analyticsData={analyticsData}
/>

{showUKCompliantVariant && (
<View style={styles.disclaimerUKContainer}>
<Text style={styles.disclaimerText}>{t('selectProviderScreen.disclaimerUK')}</Text>
</View>
)}
{somePaymentMethodsUnavailable ? (
<LimitedPaymentMethods flow={flow} />
) : (
Expand Down Expand Up @@ -613,6 +631,10 @@ const styles = StyleSheet.create({
disclaimerContainer: {
padding: Spacing.Regular16,
},
disclaimerUKContainer: {
paddingTop: Spacing.Thick24,
paddingHorizontal: Spacing.Regular16,
},
disclaimerText: {
...typeScale.bodySmall,
color: colors.gray4,
Expand Down Expand Up @@ -653,8 +675,4 @@ SelectProviderScreen.navigationOptions = ({
eventProperties={{ flow: route.params.flow }}
/>
),
headerTitle:
route.params.flow === CICOFlow.CashIn
? i18n.t(`fiatExchangeFlow.cashIn.selectProviderHeader`)
: i18n.t(`fiatExchangeFlow.cashOut.selectProviderHeader`),
})
1 change: 1 addition & 0 deletions src/statsig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum StatsigFeatureGates {
SHOW_MULTIPLE_EARN_POOLS = 'show_multiple_earn_pools',
SHOW_APPLE_IN_CAB = 'show_apple_in_cab',
SHOW_SWAP_AND_DEPOSIT = 'show_swap_and_deposit',
SHOW_UK_COMPLIANT_VARIANT = 'show_uk_compliant_variant',
}

export enum StatsigExperiments {
Expand Down
19 changes: 19 additions & 0 deletions src/swap/SwapScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,25 @@ describe('SwapScreen', () => {
expect(within(swapToContainer).getByTestId('SwapAmountInput/TokenSelect')).toBeTruthy()
})

it('should display the UK compliant variants', () => {
const mockedPopularTokens = [mockUSDCTokenId, mockPoofTokenId]
jest.mocked(getDynamicConfigParams).mockReturnValue({
popularTokenIds: mockedPopularTokens,
maxSlippagePercentage: '0.3',
})
jest
.mocked(getFeatureGate)
.mockImplementation((gate) => gate === StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)

const { getByText, tokenBottomSheets } = renderScreen({})

expect(getByText('swapScreen.confirmSwap, {"context":"UK"}')).toBeTruthy()
expect(getByText('swapScreen.disclaimer, {"context":"UK"}')).toBeTruthy()
// popular token filter chip is not shown
expect(within(tokenBottomSheets[0]).queryByText('tokenBottomSheet.filters.popular')).toBeFalsy()
expect(within(tokenBottomSheets[1]).queryByText('tokenBottomSheet.filters.popular')).toBeFalsy()
})

it('should display the token set via fromTokenId prop', () => {
const { swapFromContainer, swapToContainer } = renderScreen({ fromTokenId: mockCeurTokenId })

Expand Down
8 changes: 6 additions & 2 deletions src/swap/SwapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export function SwapScreen({ route }: Props) {
const estimatedDurationBottomSheetRef = useRef<BottomSheetModalRefType>(null)

const allowCrossChainSwaps = getFeatureGate(StatsigFeatureGates.ALLOW_CROSS_CHAIN_SWAPS)
const showUKCompliantVariant = getFeatureGate(StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)

const { decimalSeparator } = getNumberFormatSettings()

Expand Down Expand Up @@ -964,14 +965,17 @@ export function SwapScreen({ route }: Props) {
)}
</View>
<Text style={styles.disclaimerText}>
<Trans i18nKey="swapScreen.disclaimer">
<Trans
i18nKey="swapScreen.disclaimer"
context={showUKCompliantVariant ? 'UK' : undefined}
>
<Text style={styles.disclaimerLink} onPress={onPressLearnMore}></Text>
</Trans>
</Text>
<Button
testID="ConfirmSwapButton"
onPress={handleConfirmSwap}
text={t('swapScreen.confirmSwap')}
text={t('swapScreen.confirmSwap', { context: showUKCompliantVariant ? 'UK' : undefined })}
size={BtnSizes.FULL}
disabled={!allowSwap}
showLoading={confirmSwapIsLoading}
Expand Down
23 changes: 15 additions & 8 deletions src/swap/useFilterChips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ export default function useFilterChip(
preselectedNetworkId?: NetworkId
): FilterChip<TokenBalance>[] {
const { t } = useTranslation()

const showSwapTokenFilters = getFeatureGate(StatsigFeatureGates.SHOW_SWAP_TOKEN_FILTERS)
const recentlySwappedTokens = useSelector(lastSwappedSelector)
const tokensWithBalance = useTokensWithTokenBalance()
const showUKCompliantVariant = getFeatureGate(StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)
const popularTokenIds: string[] = getDynamicConfigParams(
DynamicConfigs[StatsigDynamicConfigs.SWAP_CONFIG]
).popularTokenIds

const recentlySwappedTokens = useSelector(lastSwappedSelector)
const tokensWithBalance = useTokensWithTokenBalance()
const supportedNetworkIds = getSupportedNetworkIdsForSwap()

if (!showSwapTokenFilters) {
Expand All @@ -36,12 +39,16 @@ export default function useFilterChip(
filterFn: (token: TokenBalance) => token.balance.gte(TOKEN_MIN_AMOUNT),
isSelected: selectingField === Field.FROM && tokensWithBalance.length > 0,
},
{
id: 'popular',
name: t('tokenBottomSheet.filters.popular'),
filterFn: (token: TokenBalance) => popularTokenIds.includes(token.tokenId),
isSelected: false,
},
...(showUKCompliantVariant
? []
: [
{
id: 'popular',
name: t('tokenBottomSheet.filters.popular'),
filterFn: (token: TokenBalance) => popularTokenIds.includes(token.tokenId),
isSelected: false,
},
]),
{
id: 'recently-swapped',
name: t('tokenBottomSheet.filters.recentlySwapped'),
Expand Down

0 comments on commit 36b4546

Please sign in to comment.