Skip to content

Commit

Permalink
[Wallet] Country feature flags and changes for countries where CP-DOT…
Browse files Browse the repository at this point in the history
…O is restricted (#5613)

### Description

This PR covers the following changes:
- adds country specific feature flags, based on the country of the entered phone number. Flags are defined in `src/flags.ts`, helpers are in `src/utils/countryFeatures.ts ` (there's a `useCountryFeatures` hook for easy usage within components).
- adds the CELO balance to the menu, (hidden if amount is <= 0.001)
- hides the CELO Buy/Sell buttons on the Exchange screen for CP-DOTO restricted countries (only PH for now). In that case we display a "Withdraw" button if the user has any CELO funds.

See [designs on Figma](https://www.figma.com/file/zt7aTQ5wuXycIwxq5oAmF9/Wallet--Refresh?node-id=3136%3A0) (not entirely up-to-date with latest discussions): 

### Other changes

- Upgrade `react-native-reanimated` to fix issue with mock during jest tests
- Add tests for previously uncovered components:
  - `DrawerNavigator`
  - `ExchangeHomeScreen`

### Tested

**Menu (PH, CP-DOTO restricted) with/without CELO balance**

<Img src="https://user-images.githubusercontent.com/57791/97578835-a3273300-19f1-11eb-99c9-008eead7097c.png" width="50%" /><Img src="https://user-images.githubusercontent.com/57791/97578944-c5b94c00-19f1-11eb-98c0-03b5d694c076.png" width="50%" />

**Exchange Home screen (PH, CP-DOTO restricted) with/without CELO balance**

<Img src="https://user-images.githubusercontent.com/57791/97579296-2ba5d380-19f2-11eb-8dea-07a5638f4998.png" width="50%" /><Img src="https://user-images.githubusercontent.com/57791/97579208-129d2280-19f2-11eb-8ac2-cfb898db5c4d.png" width="50%" />

**Menu (US, non CP-DOTO restricted) with/without CELO balance**

<Img src="https://user-images.githubusercontent.com/57791/97579821-d1594280-19f2-11eb-9131-8ba3cbb66c51.png" width="50%" /><Img src="https://user-images.githubusercontent.com/57791/97579855-dae2aa80-19f2-11eb-9afb-5df628b4e568.png" width="50%" />


**Exchange Home screen (non CP-DOTO restricted) with/without CELO balance**

<Img src="https://user-images.githubusercontent.com/57791/97579912-ef26a780-19f2-11eb-8046-41cdd582ffd8.png" width="50%" /><Img src="https://user-images.githubusercontent.com/57791/97579887-e504a900-19f2-11eb-8429-78fdad044673.png" width="50%" />

### Related issues

- Fixes #5444 
- Addresses part of #5425 (Adds CELO balance to the menu)

### Backwards compatibility

Yes
  • Loading branch information
jeanregisser authored Oct 29, 2020
1 parent 216de39 commit 1832aea
Show file tree
Hide file tree
Showing 19 changed files with 1,415 additions and 185 deletions.
8 changes: 7 additions & 1 deletion packages/mobile/__mocks__/react-native-reanimated.js
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
module.exports = require('react-native-reanimated/mock')
const Reanimated = require('react-native-reanimated/mock')

// The mock for `call` immediately calls the callback which is incorrect
// So we override it with a no-op
Reanimated.default.call = () => {}

module.exports = Reanimated
6 changes: 6 additions & 0 deletions packages/mobile/__mocks__/react-native-safe-area-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ module.exports = {
width,
height,
}),
useSafeArea: () => ({
top: 0,
right: 0,
bottom: 0,
left: 0,
}),
}
4 changes: 2 additions & 2 deletions packages/mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ PODS:
- React
- RNPermissions (2.1.5):
- React
- RNReanimated (1.9.0):
- RNReanimated (1.13.1):
- React
- RNScreens (2.7.0):
- React
Expand Down Expand Up @@ -837,7 +837,7 @@ SPEC CHECKSUMS:
RNKeychain: bf2d7e9a0ae7a073c07770dd2aa6d11c67581733
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
RNPermissions: ad71dd4f767ec254f2cd57592fbee02afee75467
RNReanimated: b5ccb50650ba06f6e749c7c329a1bc3ae0c88b43
RNReanimated: dd8c286ab5dd4ba36d3a7fef8bff7e08711b5476
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
RNSentry: 2bae4ffee2e66376ef42cb845a494c3bde17bc56
RNShare: fea1801315aa8875d6db73a4010b14afcd568765
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"react-native-permissions": "^2.1.5",
"react-native-progress": "^4.1.2",
"react-native-qrcode-svg": "^6.0.6",
"react-native-reanimated": "^1.9.0",
"react-native-reanimated": "^1.13.1",
"react-native-restart-android": "^0.0.7",
"react-native-safe-area-context": "^3.0.6",
"react-native-screens": "^2.7.0",
Expand Down
22 changes: 17 additions & 5 deletions packages/mobile/src/components/CurrencyDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface Props {
hideFullCurrencyName: boolean
style?: StyleProp<TextStyle>
currencyInfo?: CurrencyInfo
testID?: string
}

const BIG_SIGN_RATIO = 34 / 48
Expand Down Expand Up @@ -120,6 +121,17 @@ function getFormatFunction(formatType: FormatType): FormatFunction {
}
}

function getFullCurrencyName(currency: CURRENCY_ENUM | null) {
switch (currency) {
case CURRENCY_ENUM.DOLLAR:
return i18n.t('global:celoDollars')
case CURRENCY_ENUM.GOLD:
return i18n.t('global:celoGold')
default:
return null
}
}

export default function CurrencyDisplay({
type,
size,
Expand All @@ -134,6 +146,7 @@ export default function CurrencyDisplay({
hideFullCurrencyName,
style,
currencyInfo,
testID,
}: Props) {
let localCurrencyCode = useLocalCurrencyCode()
let dollarToLocalRate = useDollarToLocalRate()
Expand Down Expand Up @@ -169,6 +182,7 @@ export default function CurrencyDisplay({
const formattedValue =
value && displayCurrency ? formatAmount(value.absoluteValue(), displayCurrency) : '-'
const code = displayAmount?.currencyCode
const fullCurrencyName = getFullCurrencyName(displayCurrency)

const color = useColors
? currency === CURRENCY_ENUM.GOLD
Expand All @@ -189,7 +203,7 @@ export default function CurrencyDisplay({
const codeStyle = { fontSize: Math.round(fontSize * BIG_CODE_RATIO), lineHeight, color }

return (
<View style={[styles.bigContainer, style]}>
<View style={[styles.bigContainer, style]} testID={testID}>
{!hideSign && (
<Text numberOfLines={1} style={[fontStyles.regular, signStyle]}>
{sign}
Expand All @@ -213,14 +227,12 @@ export default function CurrencyDisplay({
}

return (
<Text numberOfLines={1} style={[style, { color }]}>
<Text numberOfLines={1} style={[style, { color }]} testID={testID}>
{!hideSign && sign}
{!hideSymbol && currencySymbol}
{formattedValue}
{!hideCode && !!code && ` ${code}`}
{!hideFullCurrencyName &&
code === CURRENCIES[CURRENCY_ENUM.DOLLAR].code &&
` ${i18n.t('global:celoDollars')}`}
{!hideFullCurrencyName && !!fullCurrencyName && ` ${fullCurrencyName}`}
</Text>
)
}
Expand Down
1 change: 0 additions & 1 deletion packages/mobile/src/exchange/CeloExchangeButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ const styles = StyleSheet.create({
},
buttonContainer: {
flexDirection: 'row',
flex: 1,
marginTop: 24,
marginBottom: 28,
marginHorizontal: 12,
Expand Down
3 changes: 2 additions & 1 deletion packages/mobile/src/exchange/CeloGoldOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React from 'react'
import { Trans, WithTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import CurrencyDisplay from 'src/components/CurrencyDisplay'
import { celoTokenBalanceSelector } from 'src/goldToken/selectors'
import useBalanceAutoRefresh from 'src/home/useBalanceAutoRefresh'
import { Namespaces, withTranslation } from 'src/i18n'
import useSelector from 'src/redux/useSelector'
Expand All @@ -18,7 +19,7 @@ type Props = WithTranslation & OwnProps

export function CeloGoldOverview({ t, testID }: Props) {
useBalanceAutoRefresh()
const goldBalance = useSelector((state) => state.goldToken.balance)
const goldBalance = useSelector(celoTokenBalanceSelector)

const goldBalanceAmount = goldBalance
? { value: goldBalance, currencyCode: CURRENCIES[CURRENCY_ENUM.GOLD].code }
Expand Down
128 changes: 67 additions & 61 deletions packages/mobile/src/exchange/ExchangeHomeScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,72 @@
// need to mock this as it expects to be called inside a provider
jest.mock('src/exchange/CeloGoldOverview', () => ({ default: () => 'AccountOverviewComponent' }))
// import * as React from 'react'
// import { MockedProvider } from 'react-apollo/test-utils'
import 'react-native'
// import { Provider } from 'react-redux'
// import * as renderer from 'react-test-renderer'
// import configureMockStore from 'redux-mock-store'
// import thunk from 'redux-thunk'
// import { ExchangeHomeScreen } from 'src/exchange/ExchangeHomeScreen'
// import { transactionQuery } from 'src/home/WalletHome'
// import i18n from 'src/i18n'
import React from 'react'
import { fireEvent, render } from 'react-native-testing-library'
import { Provider } from 'react-redux'
import ExchangeHomeScreen from 'src/exchange/ExchangeHomeScreen'
import { Screens } from 'src/navigator/Screens'
import { createMockStore, getMockStackScreenProps } from 'test/utils'

// const middlewares = [thunk]
// const mockStore = configureMockStore(middlewares)
// Mock this for now, as we get apollo issues
jest.mock('src/transactions/TransactionsList')

// const newDollarBalance = '189.9'
// const newGoldBalance = '207.81'
// const exchangeRate = '2'
const mockScreenProps = getMockStackScreenProps(Screens.ExchangeHomeScreen)

// const mocks = [
// {
// request: {
// query: transactionQuery,
// variables: {
// address: '',
// },
// },
// result: {
// data: {
// events: [],
// },
// },
// },
// ]
describe('ExchangeHomeScreen', () => {
it('renders and behaves correctly for non CP-DOTO restricted countries', () => {
const store = createMockStore({
goldToken: { balance: '2' },
stableToken: { balance: '10' },
exchange: { exchangeRatePair: { goldMaker: '0.11', dollarMaker: '10' } },
})

// failing due to various apollo issues with mocks
xit('renders correctly', () => {
// const store = mockStore({
// account: { devModeActive: false },
// transactions: { standbyTransactions: [] },
// web3: { accounts: {} },
// })
// const tree = renderer.create(
// <Provider store={store}>
// <MockedProvider mocks={mocks} addTypename={false}>
// <ExchangeHomeScreen
// dollarBalance={newDollarBalance}
// goldBalance={newGoldBalance}
// dollarPending={0}
// goldPending={0}
// fetchExchangeRate={jest.fn()}
// exchangeRate={exchangeRate}
// fetchGoldPendingBalance={jest.fn()}
// fetchDollarBalance={jest.fn()}
// fetchDollarPendingBalance={jest.fn()}
// fetchGoldBalance={jest.fn()}
// tReady={true}
// i18n={i18n}
// t={i18n.t}
// />
// </MockedProvider>
// </Provider>
// )
// expect(tree).toMatchSnapshot()
const tree = render(
<Provider store={store}>
<ExchangeHomeScreen {...mockScreenProps} />
</Provider>
)

expect(tree).toMatchSnapshot()

jest.clearAllMocks()
fireEvent.press(tree.getByTestId('BuyCelo'))
expect(mockScreenProps.navigation.navigate).toHaveBeenCalledWith(Screens.ExchangeTradeScreen, {
makerTokenDisplay: { makerToken: 'Celo Dollar', makerTokenBalance: '10' },
})

jest.clearAllMocks()
fireEvent.press(tree.getByTestId('SellCelo'))
expect(mockScreenProps.navigation.navigate).toHaveBeenCalledWith(Screens.ExchangeTradeScreen, {
makerTokenDisplay: { makerToken: 'Celo Gold', makerTokenBalance: '2' },
})

jest.clearAllMocks()
fireEvent.press(tree.getByTestId('WithdrawCELO'))
expect(mockScreenProps.navigation.navigate).toHaveBeenCalledWith(Screens.WithdrawCeloScreen)
})

it('renders and behaves correctly for CP-DOTO restricted countries', () => {
const store = createMockStore({
account: {
defaultCountryCode: '+63', // PH is restricted for CP-DOTO
},
goldToken: { balance: '2' },
stableToken: { balance: '10' },
exchange: { exchangeRatePair: { goldMaker: '0.11', dollarMaker: '10' } },
})

const tree = render(
<Provider store={store}>
<ExchangeHomeScreen {...mockScreenProps} />
</Provider>
)

expect(tree).toMatchSnapshot()

// Check we cannot buy/sell
expect(tree.queryByTestId('BuyCelo')).toBeFalsy()
expect(tree.queryByTestId('SellCelo')).toBeFalsy()

// Check we can withdraw
fireEvent.press(tree.getByTestId('WithdrawCELO'))
expect(mockScreenProps.navigation.navigate).toHaveBeenCalledWith(Screens.WithdrawCeloScreen)
})
})
24 changes: 17 additions & 7 deletions packages/mobile/src/exchange/ExchangeHomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import CeloGoldHistoryChart from 'src/exchange/CeloGoldHistoryChart'
import CeloGoldOverview from 'src/exchange/CeloGoldOverview'
import { useExchangeRate } from 'src/exchange/hooks'
import { exchangeHistorySelector } from 'src/exchange/reducer'
import RestrictedCeloExchange from 'src/exchange/RestrictedCeloExchange'
import { CURRENCY_ENUM } from 'src/geth/consts'
import { Namespaces } from 'src/i18n'
import InfoIcon from 'src/icons/InfoIcon'
Expand All @@ -34,6 +35,7 @@ import { StackParamList } from 'src/navigator/types'
import useSelector from 'src/redux/useSelector'
import DisconnectBanner from 'src/shared/DisconnectBanner'
import TransactionsList from 'src/transactions/TransactionsList'
import { useCountryFeatures } from 'src/utils/countryFeatures'
import { goldToDollarAmount } from 'src/utils/currencyExchange'
import { getLocalCurrencyDisplayValue } from 'src/utils/formatting'

Expand Down Expand Up @@ -86,6 +88,8 @@ function ExchangeHomeScreen({ navigation }: Props) {

const { t } = useTranslation(Namespaces.exchangeFlow9)

const { RESTRICTED_CP_DOTO } = useCountryFeatures()

// TODO: revert this back to `useLocalCurrencyCode()` when we have history data for cGDL to Local Currency.
const localCurrencyCode = null
const localExchangeRate = useSelector(getLocalCurrencyExchangeRate)
Expand Down Expand Up @@ -167,16 +171,22 @@ function ExchangeHomeScreen({ navigation }: Props) {
</View>

<CeloGoldHistoryChart />
<CeloExchangeButtons navigation={navigation} />
{RESTRICTED_CP_DOTO ? (
<RestrictedCeloExchange onPressWithdraw={goToWithdrawCelo} />
) : (
<CeloExchangeButtons navigation={navigation} />
)}
<ItemSeparator />
<CeloGoldOverview testID="ExchangeAccountOverview" />
<ItemSeparator />
<SettingsItemTextValue
title={t('withdrawCelo')}
onPress={goToWithdrawCelo}
testID={'WithdrawCELO'}
showChevron={true}
/>
{!RESTRICTED_CP_DOTO && (
<SettingsItemTextValue
title={t('withdrawCelo')}
onPress={goToWithdrawCelo}
testID={'WithdrawCELO'}
showChevron={true}
/>
)}
<SectionHead text={t('global:activity')} />
<TransactionsList currency={CURRENCY_ENUM.GOLD} />
</SafeAreaView>
Expand Down
43 changes: 43 additions & 0 deletions packages/mobile/src/exchange/RestrictedCeloExchange.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react'
import 'react-native'
import { fireEvent, render } from 'react-native-testing-library'
import { Provider } from 'react-redux'
import RestrictedCeloExchange from 'src/exchange/RestrictedCeloExchange'
import { createMockStore } from 'test/utils'

describe('RestrictedCeloExchange', () => {
it('allows withdrawing if the balance is enough', () => {
const store = createMockStore({
goldToken: { balance: '10' },
stableToken: { balance: '10' },
})
const onPressWithdraw = jest.fn()

const tree = render(
<Provider store={store}>
<RestrictedCeloExchange onPressWithdraw={onPressWithdraw} />
</Provider>
)

expect(onPressWithdraw).not.toHaveBeenCalled()

fireEvent.press(tree.getByTestId('WithdrawCELO'))
expect(onPressWithdraw).toHaveBeenCalled()
})

it('disallows withdrawing if the balance is NOT enough', () => {
const store = createMockStore({
goldToken: { balance: '0.001' },
})
const onPressWithdraw = jest.fn()

const tree = render(
<Provider store={store}>
<RestrictedCeloExchange onPressWithdraw={onPressWithdraw} />
</Provider>
)

expect(onPressWithdraw).not.toHaveBeenCalled()
expect(tree.queryByTestId('WithdrawCELO')).toBeFalsy()
})
})
Loading

0 comments on commit 1832aea

Please sign in to comment.