Skip to content

Commit

Permalink
feat(mobile-money): Add Mobile Money support (#31)
Browse files Browse the repository at this point in the history
* Implement Mobile Money support

* Update test
  • Loading branch information
jophish authored Dec 13, 2023
1 parent 8dfa811 commit b75fd91
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 104 deletions.
132 changes: 132 additions & 0 deletions src/components/PaymentInfoFieldSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useEffect, useMemo, useState } from 'react'
import { PaymentInfoLineItem } from './PaymentInfoLineItem'
import { FiatAccountType } from '@fiatconnect/fiatconnect-types'
import { FiatAccountFieldMetadata } from '../types'
import styled from 'styled-components'

interface Props {
country: string
fiatAccountDetails: Record<string, string>
setFiatAccountDetails: (newDetails: Record<string, string>) => void
setSubmitDisabled: (disabled: boolean) => void
allowedValues?: Record<string, [string, ...string[]]>
fiatAccountType: FiatAccountType
fiatAccountSchemaMetadata: Record<string, FiatAccountFieldMetadata>
}

function PaymentInfoFieldSection({
country,
fiatAccountDetails,
setFiatAccountDetails,
setSubmitDisabled,
allowedValues,
fiatAccountType,
fiatAccountSchemaMetadata,
}: Props) {
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined,
)

const userFields = Object.entries(fiatAccountSchemaMetadata)
.filter(([_, metadata]) => metadata.userField)
.map(([field, _]) => field)

useEffect(() => {
const initialDetails = {
fiatAccountType,
country,
}

const defaultFields = userFields.reduce((acc, field) => {
return {
...acc,
...(allowedValues?.[field] ? { [field]: allowedValues[field][0] } : {}),
}
}, {})

setFiatAccountDetailsWrapper({
...initialDetails,
...defaultFields,
})
}, []) // Empty array required to only run this effect once ever

useMemo(() => {
const requiredFields = Object.entries(fiatAccountSchemaMetadata)
.filter(([_, metadata]) => metadata.required)
.map(([field, _]) => field)
const fieldValues = requiredFields.map((field) => fiatAccountDetails[field])
if (!fieldValues.every((value) => !!value)) {
setErrorMessage(undefined)
setSubmitDisabled(true)
return
}

for (const field of requiredFields) {
const fieldMetadata = fiatAccountSchemaMetadata[field]
if (!fieldMetadata.validator) {
continue
}
const { valid, error } = fieldMetadata.validator(
fiatAccountDetails[field],
)
if (!valid) {
setSubmitDisabled(true)
setErrorMessage(error)
return
}
}
setSubmitDisabled(false)
setErrorMessage(undefined)
}, [fiatAccountDetails])

const setFiatAccountDetailsWrapper = (newDetails: Record<string, string>) => {
const formattedDetails: Record<string, string> = {}
for (const [field, value] of Object.entries(newDetails)) {
const fieldMetadata = fiatAccountSchemaMetadata[field]
const formattedValue = fieldMetadata?.formatter
? fieldMetadata.formatter(value)
: value
formattedDetails[field] = formattedValue
}
setFiatAccountDetails(formattedDetails)
}

return (
<Container>
{userFields.map((field) => {
return (
<PaymentInfoLineItem
key={field}
title={fiatAccountSchemaMetadata[field].displayInfo?.title ?? ''}
placeholder={
fiatAccountSchemaMetadata[field].displayInfo?.placeholder ?? ''
}
onChange={(value) =>
setFiatAccountDetailsWrapper({ [field]: value })
}
value={fiatAccountDetails[field] ?? ''}
allowedValues={allowedValues?.[field]}
/>
)
})}
{errorMessage && <ErrorSection>{errorMessage}</ErrorSection>}
</Container>
)
}

const ErrorSection = styled.div`
align-self: center;
text-align: center;
color: #ff7e7e;
`
const Container = styled.div`
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex-direction: column;
gap: 20px;
padding-top: 20px;
width: 100%;
`

export default PaymentInfoFieldSection
File renamed without changes.
24 changes: 21 additions & 3 deletions src/components/PaymentInfoScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { Steps } from '../types'
import { fiatAccountSchemaToPaymentMethod } from '../constants'
import {
FiatAccountSchema,
FiatAccountType,
PostFiatAccountRequestBody,
ObfuscatedFiatAccountData,
} from '@fiatconnect/fiatconnect-types'
import { AccountNumberSection } from './paymentInfo/AccountNumber'
import accountNumberSchemaMetadata from './paymentInfo/AccountNumber'
import mobileMoneySchemaMetadata from './paymentInfo/MobileMoney'
import PaymentInfoFieldSection from './PaymentInfoFieldSection'
import { addFiatAccount, getLinkedAccount } from '../FiatConnectClient'
import { useFiatConnectConfig } from '../hooks'
import { providerIdToProviderName } from '../constants'
Expand Down Expand Up @@ -87,18 +90,33 @@ export function PaymentInfoScreen({
switch (params.fiatAccountSchema) {
case FiatAccountSchema.AccountNumber: {
return (
<AccountNumberSection
<PaymentInfoFieldSection
country={params.country}
fiatAccountDetails={fiatAccountDetails}
setFiatAccountDetails={setFiatAccountDetailsWrapper}
setSubmitDisabled={setSubmitDisabled}
allowedValues={params.allowedValues}
fiatAccountType={FiatAccountType.BankAccount}
fiatAccountSchemaMetadata={accountNumberSchemaMetadata}
/>
)
}
case FiatAccountSchema.MobileMoney: {
return (
<PaymentInfoFieldSection
country={params.country}
fiatAccountDetails={fiatAccountDetails}
setFiatAccountDetails={setFiatAccountDetailsWrapper}
setSubmitDisabled={setSubmitDisabled}
allowedValues={params.allowedValues}
fiatAccountType={FiatAccountType.MobileMoney}
fiatAccountSchemaMetadata={mobileMoneySchemaMetadata}
/>
)
}
default: {
onError(
'There was an error signing in with Bitmama.',
'There was an error signing in.',
'This may be due to a misconfiguration by your wallet provider.',
)
}
Expand Down
128 changes: 33 additions & 95 deletions src/components/paymentInfo/AccountNumber.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,36 @@
import React, { useState, useEffect, useMemo } from 'react'
import {
FiatAccountSchema,
FiatAccountType,
} from '@fiatconnect/fiatconnect-types'
import { PaymentInfoLineItem } from './PaymentInfoLineItem'
import styled from 'styled-components'
import { FiatAccountFieldMetadata } from '../../types'

interface Props {
country: string
fiatAccountDetails: Record<string, string>
setFiatAccountDetails: (newDetails: Record<string, string>) => void
setSubmitDisabled: (disabled: boolean) => void
allowedValues?: Record<string, [string, ...string[]]>
const accountNumberSchemaMetadata: Record<string, FiatAccountFieldMetadata> = {
country: {
required: true,
},
fiatAccountType: {
required: true,
},
institutionName: {
required: true,
userField: true,
displayInfo: {
title: 'Institution Name',
placeholder: 'Your Institution Name',
},
},
accountName: {
required: true,
userField: true,
displayInfo: {
title: 'Account Name',
placeholder: 'Your Account Name',
},
},
accountNumber: {
required: true,
userField: true,
displayInfo: {
title: 'Account Number',
placeholder: '1234567890',
},
},
}

const REQUIRED_FIELDS = [
'country',
'fiatAccountType',
'accountName',
'institutionName',
'accountNumber',
]

export function AccountNumberSection({
country,
fiatAccountDetails,
setFiatAccountDetails,
setSubmitDisabled,
allowedValues,
}: Props) {
useEffect(() => {
const initialDetails = {
fiatAccountType: FiatAccountType.BankAccount,
country,
}
setFiatAccountDetails({
...initialDetails,
...(allowedValues?.institutionName
? { institutionName: allowedValues.institutionName[0] }
: {}),
...(allowedValues?.accountName
? { accountName: allowedValues.accountName[0] }
: {}),
...(allowedValues?.accountNumber
? { accountNumber: allowedValues.accountNumber[0] }
: {}),
})
}, []) // Empty array required to only run this effect once ever

// TODO: Can this be re-used among the other fiat account schema implementations?
useMemo(() => {
const fieldValues = REQUIRED_FIELDS.map(
(field) => fiatAccountDetails[field],
)
if (fieldValues.every((value) => !!value)) {
setSubmitDisabled(false)
} else {
setSubmitDisabled(true)
}
}, [fiatAccountDetails])

return (
<Container>
<PaymentInfoLineItem
title={'Institution Name'}
placeholder={'Your Institution Name'}
onChange={(value) => setFiatAccountDetails({ institutionName: value })}
value={fiatAccountDetails.institutionName ?? ''}
allowedValues={allowedValues?.institutionName}
/>
<PaymentInfoLineItem
title={'Account Name'}
placeholder={'Your Account Name'}
onChange={(value) => setFiatAccountDetails({ accountName: value })}
value={fiatAccountDetails.accountName ?? ''}
allowedValues={allowedValues?.accountName}
/>
<PaymentInfoLineItem
title={'Account Number'}
placeholder={'1234567890'}
onChange={(value) => setFiatAccountDetails({ accountNumber: value })}
value={fiatAccountDetails.accountNumber ?? ''}
allowedValues={allowedValues?.accountNumber}
/>
</Container>
)
}

const Container = styled.div`
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex-direction: column;
gap: 20px;
padding-top: 20px;
width: 100%;
`
export default accountNumberSchemaMetadata
63 changes: 63 additions & 0 deletions src/components/paymentInfo/MobileMoney.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FiatAccountFieldMetadata } from '../../types'

const mobileFormatter = (input: string) => {
if (input.length && input[0] !== '+') {
return `+${input}`
}
return input
}

const mobileValidator = (input: string) => {
const regex = /^\+[0-9]{6,15}$/
const valid = regex.test(input)
const errorMessage = 'Invalid Phone Number'
return {
valid,
error: valid ? undefined : errorMessage,
}
}

const mobileMoneySchemaMetadata: Record<string, FiatAccountFieldMetadata> = {
country: {
required: true,
},
fiatAccountType: {
required: true,
},
institutionName: {
required: true,
userField: true,
displayInfo: {
title: 'Institution Name',
placeholder: 'Your Institution Name',
},
},
accountName: {
required: true,
userField: true,
displayInfo: {
title: 'Account Name',
placeholder: 'Your Account Name',
},
},
mobile: {
required: true,
userField: true,
formatter: mobileFormatter,
validator: mobileValidator,
displayInfo: {
title: 'Mobile Number',
placeholder: '+234123234566',
},
},
operator: {
required: true,
userField: true,
displayInfo: {
title: 'Operator',
placeholder: 'Your Mobile Operator',
},
},
}

export default mobileMoneySchemaMetadata
Loading

0 comments on commit b75fd91

Please sign in to comment.