Skip to content

Commit

Permalink
feat(fiatconnect): get account number regex from remote config (#2979)
Browse files Browse the repository at this point in the history
  • Loading branch information
satish-ravi authored Oct 12, 2022
1 parent bddd383 commit 5b27d40
Show file tree
Hide file tree
Showing 15 changed files with 743 additions and 61 deletions.
5 changes: 3 additions & 2 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1420,8 +1420,9 @@
},
"accountNumber": {
"label": "Account Number",
"placeholderText": "Enter your 10 digit account number",
"errorMessage": "Account number should be 10 digits long and only contain numbers"
"placeholderText": "Enter Account Number",
"errorMessageDigitLength": "Account number must be {{length}} digits",
"errorMessageDigit": "Account number must contain only digits"
}
},
"webView": {
Expand Down
2 changes: 2 additions & 0 deletions src/app/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { FETCH_TIMEOUT_DURATION } from 'src/config'
import { SuperchargeTokenConfigByToken } from 'src/consumerIncentives/types'
import { handleDappkitDeepLink } from 'src/dappkit/dappkit'
import { DappConnectInfo } from 'src/dapps/types'
import { FiatAccountSchemaCountryOverrides } from 'src/fiatconnect/types'
import { FiatExchangeFlow } from 'src/fiatExchanges/utils'
import { appVersionDeprecationChannel, fetchRemoteConfigValues } from 'src/firebase/firebase'
import { receiveAttestationMessage } from 'src/identity/actions'
Expand Down Expand Up @@ -190,6 +191,7 @@ export interface RemoteConfigValues {
celoWithdrawalEnabledInExchange: boolean
fiatConnectCashInEnabled: boolean
fiatConnectCashOutEnabled: boolean
fiatAccountSchemaCountryOverrides: FiatAccountSchemaCountryOverrides
dappConnectInfo: DappConnectInfo
visualizeNFTsEnabledInHomeAssetsPage: boolean
coinbasePayEnabled: boolean
Expand Down
57 changes: 49 additions & 8 deletions src/fiatconnect/FiatDetailsScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { Provider } from 'react-redux'
import { FiatExchangeEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { FiatConnectQuoteSuccess } from 'src/fiatconnect'
import { SendingFiatAccountStatus, submitFiatAccount } from 'src/fiatconnect/slice'
import { FiatAccountSchemaCountryOverrides } from 'src/fiatconnect/types'
import FiatConnectQuote from 'src/fiatExchanges/quotes/FiatConnectQuote'
import { CICOFlow } from 'src/fiatExchanges/utils'
import { navigate, navigateBack } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { createMockStore, getMockStackScreenProps } from 'test/utils'
import { mockFiatConnectProviderIcon, mockFiatConnectQuotes, mockNavigation } from 'test/values'
import FiatDetailsScreen from './FiatDetailsScreen'
import { submitFiatAccount, SendingFiatAccountStatus } from 'src/fiatconnect/slice'

jest.mock('src/alert/actions')
jest.mock('src/analytics/ValoraAnalytics')
Expand All @@ -29,7 +30,7 @@ jest.mock('src/utils/Logger', () => ({
}))

const fakeInstitutionName = 'CapitalTwo Bank'
const fakeAccountNumber = '1234567890'
const fakeAccountNumber = '1234567'
let mockResult = Result.ok({
fiatAccountId: '1234',
accountName: '7890',
Expand All @@ -45,7 +46,17 @@ jest.mock('@fiatconnect/fiatconnect-sdk', () => ({
jest.mock('src/fiatconnect/clients')
jest.useFakeTimers()

const store = createMockStore({})
const schemaCountryOverrides: FiatAccountSchemaCountryOverrides = {
NG: {
[FiatAccountSchema.AccountNumber]: {
accountNumber: {
regex: '^[0-9]{10}$',
errorString: 'errorMessageDigitLength',
},
},
},
}
const store = createMockStore({ fiatConnect: { schemaCountryOverrides } })
const quoteWithAllowedValues = new FiatConnectQuote({
quote: mockFiatConnectQuotes[1] as FiatConnectQuoteSuccess,
fiatAccountType: FiatAccountType.BankAccount,
Expand Down Expand Up @@ -217,7 +228,7 @@ describe('FiatDetailsScreen', () => {
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()
jest.advanceTimersByTime(1500)
expect(queryByTestId('errorMessage-accountNumber')).toBeTruthy()
expect(queryByText('fiatAccountSchema.accountNumber.errorMessage')).toBeTruthy()
expect(queryByText('fiatAccountSchema.accountNumber.errorMessageDigit')).toBeTruthy()
expect(queryByTestId('submitButton')).toBeDisabled()
})
it('shows validation error if the input field does not fulfill the requirement immediately on blur', () => {
Expand All @@ -237,7 +248,31 @@ describe('FiatDetailsScreen', () => {
// Should see an error message saying the account number field is invalid
// immediately since the field loses focus
expect(queryByTestId('errorMessage-accountNumber')).toBeTruthy()
expect(queryByText('fiatAccountSchema.accountNumber.errorMessage')).toBeTruthy()
expect(queryByText('fiatAccountSchema.accountNumber.errorMessageDigit')).toBeTruthy()
expect(queryByTestId('submitButton')).toBeDisabled()
})
it('shows country specific validation error using overrides', () => {
const mockStore = createMockStore({
fiatConnect: { schemaCountryOverrides },
networkInfo: { userLocationData: { countryCodeAlpha2: 'NG' } },
})
const { queryByText, getByTestId, queryByTestId } = render(
<Provider store={mockStore}>
<FiatDetailsScreen {...mockScreenProps} />
</Provider>
)

expect(queryByText('fiatAccountSchema.institutionName.label')).toBeTruthy()
expect(queryByText('fiatAccountSchema.accountNumber.label')).toBeTruthy()
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()

fireEvent.changeText(getByTestId('input-accountNumber'), '123456')
fireEvent(getByTestId('input-accountNumber'), 'blur')

// Should see an error message saying the account number field is invalid
// immediately since the field loses focus
expect(queryByTestId('errorMessage-accountNumber')).toBeTruthy()
expect(queryByText('fiatAccountSchema.accountNumber.errorMessageDigitLength')).toBeTruthy()
expect(queryByTestId('submitButton')).toBeDisabled()
})
it('dispatches to saga when validation passes after pressing submit', async () => {
Expand All @@ -251,7 +286,7 @@ describe('FiatDetailsScreen', () => {
fireEvent.changeText(getByTestId('input-accountNumber'), fakeAccountNumber)

const mockFiatAccountData = {
accountName: 'CapitalTwo Bank (...7890)',
accountName: 'CapitalTwo Bank (...4567)',
institutionName: fakeInstitutionName,
accountNumber: fakeAccountNumber,
country: 'US',
Expand All @@ -270,7 +305,10 @@ describe('FiatDetailsScreen', () => {
})
it('shows spinner while fiat account is sending', () => {
const mockStore = createMockStore({
fiatConnect: { sendingFiatAccountStatus: SendingFiatAccountStatus.Sending },
fiatConnect: {
sendingFiatAccountStatus: SendingFiatAccountStatus.Sending,
schemaCountryOverrides,
},
})
const { queryByTestId } = render(
<Provider store={mockStore}>
Expand All @@ -281,7 +319,10 @@ describe('FiatDetailsScreen', () => {
})
it('shows checkmark if fiat account and KYC have been approved', () => {
const mockStore = createMockStore({
fiatConnect: { sendingFiatAccountStatus: SendingFiatAccountStatus.KycApproved },
fiatConnect: {
sendingFiatAccountStatus: SendingFiatAccountStatus.KycApproved,
schemaCountryOverrides,
},
})
const { queryByTestId } = render(
<Provider store={mockStore}>
Expand Down
93 changes: 60 additions & 33 deletions src/fiatconnect/FiatDetailsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ import CancelButton from 'src/components/CancelButton'
import KeyboardAwareScrollView from 'src/components/KeyboardAwareScrollView'
import KeyboardSpacer from 'src/components/KeyboardSpacer'
import TextInput, { LINE_HEIGHT } from 'src/components/TextInput'
import { submitFiatAccount, SendingFiatAccountStatus } from 'src/fiatconnect/slice'
import { sendingFiatAccountStatusSelector } from 'src/fiatconnect/selectors'
import {
schemaCountryOverridesSelector,
sendingFiatAccountStatusSelector,
} from 'src/fiatconnect/selectors'
import { SendingFiatAccountStatus, submitFiatAccount } from 'src/fiatconnect/slice'
import { FiatAccountSchemaCountryOverrides } from 'src/fiatconnect/types'
import i18n from 'src/i18n'
import Checkmark from 'src/icons/Checkmark'
import { styles as headerStyles } from 'src/navigator/Headers'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
Expand All @@ -31,7 +36,6 @@ import colors from 'src/styles/colors'
import fontStyles from 'src/styles/fonts'
import variables from 'src/styles/variables'
import { getObfuscatedAccountNumber } from './index'
import Checkmark from 'src/icons/Checkmark'

export const TAG = 'FIATCONNECT/FiatDetailsScreen'

Expand Down Expand Up @@ -66,33 +70,52 @@ type FiatAccountFormSchema<T extends FiatAccountSchema> = {
| ComputedParam<FiatAccountSchemas[T], Property>
}

const getAccountNumberSchema = (implicitParams: {
country: string
fiatAccountType: FiatAccountType
}): FiatAccountFormSchema<FiatAccountSchema.AccountNumber> => ({
institutionName: {
name: 'institutionName',
label: i18n.t('fiatAccountSchema.institutionName.label'),
regex: /.+/,
placeholderText: i18n.t('fiatAccountSchema.institutionName.placeholderText'),
keyboardType: 'default',
},
accountNumber: {
name: 'accountNumber',
label: i18n.t('fiatAccountSchema.accountNumber.label'),
regex: /^[0-9]{10}$/,
placeholderText: i18n.t('fiatAccountSchema.accountNumber.placeholderText'),
errorMessage: i18n.t('fiatAccountSchema.accountNumber.errorMessage'),
keyboardType: 'number-pad',
},
country: { name: 'country', value: implicitParams.country },
fiatAccountType: { name: 'fiatAccountType', value: FiatAccountType.BankAccount },
accountName: {
name: 'accountName',
computeValue: ({ institutionName, accountNumber }) =>
`${institutionName} (${getObfuscatedAccountNumber(accountNumber!)})`,
const getAccountNumberSchema = (
implicitParams: {
country: string
fiatAccountType: FiatAccountType
},
})
countryOverrides?: FiatAccountSchemaCountryOverrides
): FiatAccountFormSchema<FiatAccountSchema.AccountNumber> => {
// NOTE: the schema for overrides supports overriding any field's regex or
// errorMessage, but the below currently applies it to just the
// `accountNumber` field in the `AccountNumber` schema.
// This can be extended to support overriding other params and applied more
// generically if more fields/schemas require it.
const overrides = countryOverrides?.[implicitParams.country]?.[FiatAccountSchema.AccountNumber]

return {
institutionName: {
name: 'institutionName',
label: i18n.t('fiatAccountSchema.institutionName.label'),
regex: /.+/,
placeholderText: i18n.t('fiatAccountSchema.institutionName.placeholderText'),
keyboardType: 'default',
},
accountNumber: {
name: 'accountNumber',
label: i18n.t('fiatAccountSchema.accountNumber.label'),
regex: overrides?.accountNumber?.regex
? new RegExp(overrides.accountNumber.regex)
: /^[0-9]+$/,
placeholderText: i18n.t('fiatAccountSchema.accountNumber.placeholderText'),
errorMessage: overrides?.accountNumber?.errorString
? i18n.t(
`fiatAccountSchema.accountNumber.${overrides.accountNumber.errorString}`,
overrides.accountNumber.errorParams
)
: i18n.t('fiatAccountSchema.accountNumber.errorMessageDigit'),
keyboardType: 'number-pad',
},
country: { name: 'country', value: implicitParams.country },
fiatAccountType: { name: 'fiatAccountType', value: FiatAccountType.BankAccount },
accountName: {
name: 'accountName',
computeValue: ({ institutionName, accountNumber }) =>
`${institutionName} (${getObfuscatedAccountNumber(accountNumber!)})`,
},
}
}

const FiatDetailsScreen = ({ route, navigation }: Props) => {
const { t } = useTranslation()
Expand All @@ -103,6 +126,7 @@ const FiatDetailsScreen = ({ route, navigation }: Props) => {
const fieldValues = useRef<string[]>([])
const userCountry = useSelector(userLocationDataSelector)
const dispatch = useDispatch()
const schemaCountryOverrides = useSelector(schemaCountryOverridesSelector)

const fiatAccountSchema = quote.getFiatAccountSchema()

Expand Down Expand Up @@ -158,10 +182,13 @@ const FiatDetailsScreen = ({ route, navigation }: Props) => {
const getSchema = (fiatAccountSchema: FiatAccountSchema) => {
switch (fiatAccountSchema) {
case FiatAccountSchema.AccountNumber:
return getAccountNumberSchema({
country: userCountry.countryCodeAlpha2 || 'US',
fiatAccountType: quote.getFiatAccountType(),
})
return getAccountNumberSchema(
{
country: userCountry.countryCodeAlpha2 || 'US',
fiatAccountType: quote.getFiatAccountType(),
},
schemaCountryOverrides
)
default:
throw new Error('Unsupported schema type')
}
Expand Down
2 changes: 2 additions & 0 deletions src/fiatconnect/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const sendingFiatAccountStatusSelector = (state: RootState) =>
state.fiatConnect.sendingFiatAccountStatus
export const kycTryAgainLoadingSelector = (state: RootState) => state.fiatConnect.kycTryAgainLoading
export const cachedQuoteParamsSelector = (state: RootState) => state.fiatConnect.cachedQuoteParams
export const schemaCountryOverridesSelector = (state: RootState) =>
state.fiatConnect.schemaCountryOverrides
41 changes: 26 additions & 15 deletions src/fiatconnect/slice.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {
FiatAccountType,
FiatType,
ObfuscatedFiatAccountData,
KycSchema,
ObfuscatedFiatAccountData,
} from '@fiatconnect/fiatconnect-types'
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { isEqual } from 'lodash'
import FiatConnectQuote from 'src/fiatExchanges/quotes/FiatConnectQuote'
import { CICOFlow } from 'src/fiatExchanges/utils'
import { Actions as AppActions, UpdateConfigValuesAction } from 'src/app/actions'
import {
FiatConnectProviderInfo,
FiatConnectQuoteError,
FiatConnectQuoteSuccess,
} from 'src/fiatconnect'
import { FiatAccountSchemaCountryOverrides } from 'src/fiatconnect/types'
import FiatConnectQuote from 'src/fiatExchanges/quotes/FiatConnectQuote'
import { CICOFlow } from 'src/fiatExchanges/utils'
import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper'
import { CiCoCurrency, Currency } from 'src/utils/currencies'

Expand Down Expand Up @@ -54,6 +56,7 @@ export interface State {
[kycSchema: string]: CachedQuoteParams
}
}
schemaCountryOverrides: FiatAccountSchemaCountryOverrides
}

const initialState: State = {
Expand All @@ -68,6 +71,7 @@ const initialState: State = {
sendingFiatAccountStatus: SendingFiatAccountStatus.NotSending,
kycTryAgainLoading: false,
cachedQuoteParams: {},
schemaCountryOverrides: {},
}

export type FiatAccount = ObfuscatedFiatAccountData & {
Expand Down Expand Up @@ -269,18 +273,25 @@ export const slice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(REHYDRATE, (state, action: RehydrateAction) => ({
...state,
...getRehydratePayload(action, 'fiatConnect'),
quotes: [], // reset quotes since we want to always re-fetch a new set of quotes
quotesLoading: false,
quotesError: null,
transfer: null,
attemptReturnUserFlowLoading: false,
selectFiatConnectQuoteLoading: false,
sendingFiatAccountStatus: SendingFiatAccountStatus.NotSending,
kycTryAgainLoading: false,
}))
builder
.addCase(
AppActions.UPDATE_REMOTE_CONFIG_VALUES,
(state, action: UpdateConfigValuesAction) => {
state.schemaCountryOverrides = action.configValues.fiatAccountSchemaCountryOverrides
}
)
.addCase(REHYDRATE, (state, action: RehydrateAction) => ({
...state,
...getRehydratePayload(action, 'fiatConnect'),
quotes: [], // reset quotes since we want to always re-fetch a new set of quotes
quotesLoading: false,
quotesError: null,
transfer: null,
attemptReturnUserFlowLoading: false,
selectFiatConnectQuoteLoading: false,
sendingFiatAccountStatus: SendingFiatAccountStatus.NotSending,
kycTryAgainLoading: false,
}))
},
})

Expand Down
13 changes: 13 additions & 0 deletions src/fiatconnect/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FiatAccountSchemas } from '@fiatconnect/fiatconnect-types'

export interface FiatAccountSchemaCountryOverrides {
[country: string]: {
[Schema in keyof Partial<FiatAccountSchemas>]: {
[Property in keyof Partial<FiatAccountSchemas[Schema]>]: {
regex: string
errorString: string
errorParams?: Record<string, any>
}
}
}
}
4 changes: 4 additions & 0 deletions src/firebase/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export async function fetchRemoteConfigValues(): Promise<RemoteConfigValues | nu
// RemoteConfigValues is in app/saga.ts

const superchargeConfigByTokenString = flags.superchargeTokenConfigByToken?.asString()
const fiatAccountSchemaCountryOverrides = flags.fiatAccountSchemaCountryOverrides?.asString()

return {
hideVerification: flags.hideVerification.asBoolean(),
Expand Down Expand Up @@ -293,6 +294,9 @@ export async function fetchRemoteConfigValues(): Promise<RemoteConfigValues | nu
celoWithdrawalEnabledInExchange: flags.celoWithdrawalEnabledInExchange.asBoolean(),
fiatConnectCashInEnabled: flags.fiatConnectCashInEnabled.asBoolean(),
fiatConnectCashOutEnabled: flags.fiatConnectCashOutEnabled.asBoolean(),
fiatAccountSchemaCountryOverrides: fiatAccountSchemaCountryOverrides
? JSON.parse(fiatAccountSchemaCountryOverrides)
: {},
dappConnectInfo: flags.dappConnectInfo.asString() as DappConnectInfo,
visualizeNFTsEnabledInHomeAssetsPage: flags.visualizeNFTsEnabledInHomeAssetsPage.asBoolean(),
coinbasePayEnabled: flags.coinbasePayEnabled.asBoolean(),
Expand Down
Loading

0 comments on commit 5b27d40

Please sign in to comment.