Skip to content

Commit

Permalink
feat: kyc allowed values (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
cajubelt authored Mar 5, 2024
1 parent a62d498 commit 75af5c7
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 16 deletions.
36 changes: 28 additions & 8 deletions scripts/generateWidgetUrl.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/* eslint-disable no-console */
import yargs from 'yargs'
import { ProviderIds } from '../src/types'
import { CryptoType, FiatType } from '@fiatconnect/fiatconnect-types'
import {
CryptoType,
FiatType,
quoteResponseSchema,
} from '@fiatconnect/fiatconnect-types'

function loadConfig() {
return yargs
Expand Down Expand Up @@ -80,10 +84,10 @@ export async function generateWidgetUrl() {
}),
},
)
const quoteJson = await response.json()
if (!response.ok) {
throw new Error(`quote response error json: ${JSON.stringify(quoteJson)}`)
throw new Error(`quote response error status: ${response.status}`)
}
const quoteJson = quoteResponseSchema.parse(await response.json())
const {
quote: { fiatAmount, quoteId, transferType },
kyc: { kycRequired, kycSchemas },
Expand All @@ -102,25 +106,41 @@ export async function generateWidgetUrl() {
`&quoteId=${quoteId}` +
`&country=${config.country}`
if (kycRequired) {
widgetUrl += `&kycSchema=${kycSchemas[0]}`
const kycSchema = kycSchemas[0].kycSchema
widgetUrl += `&kycSchema=${kycSchema}`
const kycAllowedValues = kycSchemas[0].allowedValues
if (kycAllowedValues && Object.keys(kycAllowedValues).length > 0) {
widgetUrl += `&kycAllowedValues=${JSON.stringify(kycAllowedValues)}`
}
}
const fiatAccountType = Object.keys(fiatAccountJson)[0]
const fiatAccountType = Object.keys(
fiatAccountJson,
)[0] as keyof typeof fiatAccountJson
if (!fiatAccountType) {
throw new Error('fiat account type not found in quote response')
}
widgetUrl += `&fiatAccountType=${fiatAccountType}`
const fiatAccountSchema =
fiatAccountJson[fiatAccountType].fiatAccountSchemas[0].fiatAccountSchema
fiatAccountJson[fiatAccountType]?.fiatAccountSchemas[0]?.fiatAccountSchema
if (!fiatAccountSchema) {
throw new Error('fiat account schema not found in quote response')
}
widgetUrl += `&fiatAccountSchema=${fiatAccountSchema}`
const userActionType =
fiatAccountJson[fiatAccountType].fiatAccountSchemas[0].userActionType
fiatAccountJson[fiatAccountType]?.fiatAccountSchemas[0].userActionType
if (userActionType) {
widgetUrl += `&userActionDetailsSchema=${userActionType}`
}

const fiatAccountAllowedValues =
fiatAccountJson[fiatAccountType]?.fiatAccountSchemas[0].allowedValues
if (
fiatAccountAllowedValues &&
Object.keys(fiatAccountAllowedValues).length > 0
) {
widgetUrl += `&fiatAccountAllowedValues=${JSON.stringify(
fiatAccountAllowedValues,
)}`
}
console.log(widgetUrl)
}

Expand Down
94 changes: 94 additions & 0 deletions src/components/KYCInfoFieldSection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { getDropdownValues } from './KYCInfoFieldSection'
import { idTypeReverseFormatter } from './kycInfo/PersonalDataAndDocumentsDetailed'

describe('KYCInfoFieldSection', () => {
describe('getDropdownValues', () => {
it('returns allowedValues if choices is undefined', () => {
const allowedValues: [string, ...string[]] = ['a', 'b']
const choices = undefined
const reverseFormatter = undefined
const result = getDropdownValues({
allowedValues,
choices,
reverseFormatter,
})
expect(result).toEqual(allowedValues)
})
it('returns choices if allowedValues is undefined', () => {
const allowedValues = undefined
const choices: [string, ...string[]] = ['a', 'b']
const reverseFormatter = undefined
const result = getDropdownValues({
allowedValues,
choices,
reverseFormatter,
})
expect(result).toEqual(choices)
})
it('returns choices that are in allowedValues', () => {
const allowedValues: [string, ...string[]] = ['a', 'b']
const choices: [string, ...string[]] = ['a', 'b', 'c']
const reverseFormatter = undefined
const result = getDropdownValues({
allowedValues,
choices,
reverseFormatter,
})
expect(result).toEqual(allowedValues)
})
it('returns allowedValues if no choices are in allowedValues', () => {
const allowedValues: [string, ...string[]] = ['a', 'b']
const choices: [string, ...string[]] = ['c', 'd']
const reverseFormatter = undefined
const result = getDropdownValues({
allowedValues,
choices,
reverseFormatter,
})
expect(result).toEqual(allowedValues)
})
it('returns human-readable values', () => {
const optionAid = 'a'
const optionAhumanReadable = 'Option A'
const optionBid = 'b'
const optionBhumanReadable = 'Option B'
const allowedValues: [string, ...string[]] = [optionAid, optionBid]
const choices: [string, ...string[]] = [
optionAhumanReadable,
optionBhumanReadable,
'Option C',
]
const reverseFormatter = (optionId: string) =>
`Option ${optionId.toUpperCase()}`
expect(
getDropdownValues({
allowedValues,
choices,
reverseFormatter,
}),
).toEqual(['Option A', 'Option B'])
expect(
getDropdownValues({
allowedValues,
choices: undefined,
reverseFormatter,
}),
).toEqual(['Option A', 'Option B'])
})
it('works for ID type field', () => {
const allowedValues: [string, ...string[]] = ['IDC', 'PAS']
const choices: [string, ...string[]] = [
'State-issued ID',
'Passport',
"Driver's License",
]
const reverseFormatter = idTypeReverseFormatter
const result = getDropdownValues({
allowedValues,
choices,
reverseFormatter,
})
expect(result).toEqual(['State-issued ID', 'Passport'])
})
})
})
61 changes: 60 additions & 1 deletion src/components/KYCInfoFieldSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,68 @@ interface Props {
) => void
setSubmitDisabled: (disabled: boolean) => void
kycSchemaMetadata: Record<string, KycFieldMetadata>
allowedValues?: Record<string, [string, ...string[]]>
}

function isNonemptyStringArray(
strings?: string[],
): strings is [string, ...string[]] {
return !!strings && strings.length > 0
}

function identity<T>(x: T): T {
return x
}

/**
* Merge 'allowedValues' and 'choices' into a single array and formats them for presentation to the end-user.
*
* @param allowedValues: a concept from the FiatConnect spec that allows the server to constraint the space of allowed values
* for some fields. This is useful for fields like "country" or "state" where the server would reject a request if the
* user submitted values from an unsupported region.
* @param choices: a FiatConnect Widget-specific concept that allows us to provide a dropdown when the spec already has a limited
* space of possible inputs for something like "identity card type"
* @param reverseFormatter: a function that returns a user-friendly representation of some value
*/
export function getDropdownValues({
allowedValues,
choices,
reverseFormatter,
}: {
allowedValues: [string, ...string[]] | undefined
choices: [string, ...string[]] | undefined
reverseFormatter?: (x: string) => string
}): [string, ...string[]] | undefined {
if (!allowedValues) {
return choices
}
const humanReadableAllowedValues = allowedValues.map(
reverseFormatter ?? identity,
)
if (!choices) {
return isNonemptyStringArray(humanReadableAllowedValues)
? humanReadableAllowedValues
: undefined
}
const output = choices.filter((choice) =>
humanReadableAllowedValues.includes(choice),
)
if (!isNonemptyStringArray(output)) {
// eslint-disable-next-line no-console
console.warn(
`No valid choices for given allowedValues ${allowedValues}. Just showing allowedValues directly`,
)
return humanReadableAllowedValues as [string, ...string[]]
}
return output
}

function KYCInfoFieldSection({
kycInfo,
setKycInfo,
setSubmitDisabled,
kycSchemaMetadata,
allowedValues,
}: Props) {
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined,
Expand Down Expand Up @@ -132,7 +187,11 @@ function KYCInfoFieldSection({
? fieldMetadata.reverseFormatter(getFieldValue(field))
: getFieldValue(field)) ?? ''
}
allowedValues={fieldMetadata.choices ?? undefined}
allowedValues={getDropdownValues({
allowedValues: allowedValues?.[field],
choices: fieldMetadata.choices,
reverseFormatter: fieldMetadata.reverseFormatter,
})}
/>
)
})}
Expand Down
1 change: 1 addition & 0 deletions src/components/KYCInfoScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export function KYCInfoScreen({
setKycInfo={setKycInfoWrapper}
setSubmitDisabled={setSubmitDisabled}
kycSchemaMetadata={kycSchemaToMetadata[params.kycSchema]}
allowedValues={params.kycAllowedValues}
/>
</div>
<div id="KYCBottomSection">
Expand Down
4 changes: 2 additions & 2 deletions src/components/PaymentInfoScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function PaymentInfoScreen({
fiatAccountDetails={fiatAccountDetails}
setFiatAccountDetails={setFiatAccountDetailsWrapper}
setSubmitDisabled={setSubmitDisabled}
allowedValues={params.allowedValues}
allowedValues={params.fiatAccountAllowedValues}
fiatAccountType={FiatAccountType.BankAccount}
fiatAccountSchemaMetadata={accountNumberSchemaMetadata}
/>
Expand All @@ -104,7 +104,7 @@ export function PaymentInfoScreen({
fiatAccountDetails={fiatAccountDetails}
setFiatAccountDetails={setFiatAccountDetailsWrapper}
setSubmitDisabled={setSubmitDisabled}
allowedValues={params.allowedValues}
allowedValues={params.fiatAccountAllowedValues}
fiatAccountType={FiatAccountType.MobileMoney}
fiatAccountSchemaMetadata={mobileMoneySchemaMetadata}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/components/kycInfo/PersonalDataAndDocumentsDetailed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IdentificationDocumentType } from '@fiatconnect/fiatconnect-types'
import { Buffer } from 'buffer'

enum IdTypeFriendlyName {
IDC = 'Statue-issued ID',
IDC = 'State-issued ID',
PAS = 'Passport',
DL = `Driver's License`,
}
Expand All @@ -24,7 +24,7 @@ function idTypeFormatter(friendlyName: string): string {
}
}

function idTypeReverseFormatter(idType: string | undefined): string {
export function idTypeReverseFormatter(idType: string | undefined): string {
switch (idType) {
case IdentificationDocumentType.IDC: {
return IdTypeFriendlyName.IDC
Expand Down
29 changes: 29 additions & 0 deletions src/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ describe('schema', () => {
fiatAccountSchema: 'AccountNumber',
userActionDetailsSchema: 'AccountNumberUserAction',
country: 'NG',
fiatAccountAllowedValues: JSON.stringify({
institutionName: ['Access Bank', 'Covenant Microfinance Bank'],
country: ['NG'],
}),
kycAllowedValues: JSON.stringify({
isoCountryCode: ['NG', 'GH'],
}),
}
it('accepts valid input', () => {
expect(queryParamsSchema.safeParse(validQueryParams).success).toEqual(
Expand All @@ -28,6 +35,20 @@ describe('schema', () => {
fiatAccountSchema: 'MobileMoney',
}).success,
).toEqual(true)

expect(
queryParamsSchema.safeParse({
...validQueryParams,
kycAllowedValues: undefined,
}).success,
).toEqual(true)

expect(
queryParamsSchema.safeParse({
...validQueryParams,
fiatAccountAllowedValues: undefined,
}).success,
).toEqual(true)
})

it('rejects invalid input', () => {
Expand All @@ -54,6 +75,14 @@ describe('schema', () => {
quoteId: undefined,
}).success,
).toEqual(false)

// invalid JSON for allowedValues
expect(
queryParamsSchema.safeParse({
...validQueryParams,
fiatAccountAllowedValues: 'invalid-json',
}).success,
).toEqual(false)
})
})
})
8 changes: 5 additions & 3 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const stringToJSONSchema = z
})

const stringifiedNumberSchema = z.string().regex(/^(\d+|\d*\.\d+)$/)
const allowedValuesSchema = stringToJSONSchema
.pipe(z.record(z.array(z.string()).nonempty()))
.optional()
export const queryParamsSchema = z.object({
providerId: z.nativeEnum(ProviderIds),
apiKey: z.string().optional(),
Expand Down Expand Up @@ -50,9 +53,8 @@ export const queryParamsSchema = z.object({
userActionDetailsSchema: z
.enum([TransferInUserActionDetails.AccountNumberUserAction])
.optional(),
allowedValues: stringToJSONSchema
.pipe(z.record(z.array(z.string()).nonempty()))
.optional(),
fiatAccountAllowedValues: allowedValuesSchema,
kycAllowedValues: allowedValuesSchema,
country: z.string(),
})
export type QueryParams = z.infer<typeof queryParamsSchema>

0 comments on commit 75af5c7

Please sign in to comment.