Merge pull request #1029 from josepfo/feat/twilio-api-data-ui
Feat: twilio api data UI
This commit is contained in:
commit
201fec33e4
11 changed files with 316 additions and 86 deletions
|
|
@ -121,7 +121,8 @@ async function updateCustomer (id, data, userToken) {
|
||||||
'us_ssn_override',
|
'us_ssn_override',
|
||||||
'sanctions_override',
|
'sanctions_override',
|
||||||
'front_camera_override',
|
'front_camera_override',
|
||||||
'suspended_until'
|
'suspended_until',
|
||||||
|
'phone_override'
|
||||||
],
|
],
|
||||||
_.mapKeys(_.snakeCase, data))
|
_.mapKeys(_.snakeCase, data))
|
||||||
|
|
||||||
|
|
@ -169,6 +170,7 @@ function edit (id, data, userToken) {
|
||||||
const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data)))
|
const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data)))
|
||||||
if (_.isEmpty(filteredData)) return getCustomerById(id)
|
if (_.isEmpty(filteredData)) return getCustomerById(id)
|
||||||
const formattedData = enhanceEditedPhotos(enhanceEditedFields(filteredData, userToken))
|
const formattedData = enhanceEditedPhotos(enhanceEditedFields(filteredData, userToken))
|
||||||
|
|
||||||
const defaultDbData = {
|
const defaultDbData = {
|
||||||
customer_id: id,
|
customer_id: id,
|
||||||
created: new Date(),
|
created: new Date(),
|
||||||
|
|
@ -688,18 +690,18 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
function getCustomerById (id) {
|
function getCustomerById (id) {
|
||||||
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
||||||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
|
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
|
||||||
phone, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
|
phone, phone_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||||
id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||||
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
|
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
|
||||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes, is_test_customer
|
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, subscriber_info_at, custom_fields, notes, is_test_customer
|
||||||
FROM (
|
FROM (
|
||||||
SELECT c.id, c.authorized_override,
|
SELECT c.id, c.authorized_override,
|
||||||
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
||||||
c.suspended_until > now() AS is_suspended,
|
c.suspended_until > now() AS is_suspended,
|
||||||
c.front_camera_path, c.front_camera_override, c.front_camera_at,
|
c.front_camera_path, c.front_camera_override, c.front_camera_at,
|
||||||
c.phone, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
|
c.phone, c.phone_at, c.phone_override, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
|
||||||
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
|
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
|
||||||
c.sanctions_at, c.sanctions_override, c.subscriber_info, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
|
c.sanctions_at, c.sanctions_override, c.subscriber_info, c.subscriber_info_at, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
|
||||||
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
|
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
|
||||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,
|
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,
|
||||||
sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (PARTITION BY c.id) AS total_spent, ccf.custom_fields
|
sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (PARTITION BY c.id) AS total_spent, ccf.custom_fields
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ const typeDef = gql`
|
||||||
lastTxClass: String
|
lastTxClass: String
|
||||||
transactions: [Transaction]
|
transactions: [Transaction]
|
||||||
subscriberInfo: JSONObject
|
subscriberInfo: JSONObject
|
||||||
|
phoneOverride: String
|
||||||
customFields: [CustomerCustomField]
|
customFields: [CustomerCustomField]
|
||||||
customInfoRequests: [CustomRequestData]
|
customInfoRequests: [CustomRequestData]
|
||||||
notes: [CustomerNote]
|
notes: [CustomerNote]
|
||||||
|
|
@ -63,12 +64,14 @@ const typeDef = gql`
|
||||||
lastTxClass: String
|
lastTxClass: String
|
||||||
suspendedUntil: Date
|
suspendedUntil: Date
|
||||||
subscriberInfo: Boolean
|
subscriberInfo: Boolean
|
||||||
|
phoneOverride: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input CustomerEdit {
|
input CustomerEdit {
|
||||||
idCardData: JSONObject
|
idCardData: JSONObject
|
||||||
idCardPhoto: UploadGQL
|
idCardPhoto: UploadGQL
|
||||||
usSsn: String
|
usSsn: String
|
||||||
|
subscriberInfo: JSONObject
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerNote {
|
type CustomerNote {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ const _ = require('lodash/fp')
|
||||||
const BN = require('../../../bn')
|
const BN = require('../../../bn')
|
||||||
const E = require('../../../error')
|
const E = require('../../../error')
|
||||||
const { utils: coinUtils } = require('lamassu-coins')
|
const { utils: coinUtils } = require('lamassu-coins')
|
||||||
const consoleLogLevel = require('console-log-level')
|
|
||||||
|
|
||||||
const NAME = 'FakeWallet'
|
const NAME = 'FakeWallet'
|
||||||
const BATCHABLE_COINS = ['BTC']
|
const BATCHABLE_COINS = ['BTC']
|
||||||
|
|
|
||||||
17
migrations/1641482376890-add-overrides-to-subscriber-info.js
Normal file
17
migrations/1641482376890-add-overrides-to-subscriber-info.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
var db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
`ALTER TABLE customers
|
||||||
|
ADD COLUMN phone_override VERIFICATION_TYPE NOT NULL DEFAULT 'automatic',
|
||||||
|
ADD COLUMN phone_override_by UUID,
|
||||||
|
ADD COLUMN phone_override_at TIMESTAMPTZ
|
||||||
|
`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,15 @@ import styles from './Button.styles'
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const ActionButton = memo(
|
const ActionButton = memo(
|
||||||
({ size = 'lg', children, className, buttonClassName, ...props }) => {
|
({
|
||||||
const classes = useStyles({ size })
|
size = 'lg',
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
buttonClassName,
|
||||||
|
backgroundColor,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({ size, backgroundColor })
|
||||||
return (
|
return (
|
||||||
<div className={classnames(className, classes.wrapper)}>
|
<div className={classnames(className, classes.wrapper)}>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import {
|
||||||
secondaryColor,
|
secondaryColor,
|
||||||
secondaryColorDark,
|
secondaryColorDark,
|
||||||
secondaryColorDarker,
|
secondaryColorDarker,
|
||||||
|
offColor,
|
||||||
|
offDarkColor,
|
||||||
|
offDarkerColor,
|
||||||
spacer
|
spacer
|
||||||
} from 'src/styling/variables'
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
|
|
@ -28,11 +31,11 @@ export default {
|
||||||
const shadowSize = height / 12
|
const shadowSize = height / 12
|
||||||
return { height: height + shadowSize / 2 }
|
return { height: height + shadowSize / 2 }
|
||||||
},
|
},
|
||||||
button: ({ size }) => {
|
button: ({ size, backgroundColor }) => {
|
||||||
const height = pickSize(size)
|
const height = pickSize(size)
|
||||||
const shadowSize = size === 'xl' ? 3 : height / 12
|
const shadowSize = size === 'xl' ? 3 : height / 12
|
||||||
const padding = size === 'xl' ? 20 : height / 2
|
const padding = size === 'xl' ? 20 : height / 2
|
||||||
|
const isGrey = backgroundColor === 'grey'
|
||||||
return {
|
return {
|
||||||
extend: size === 'xl' ? h1 : h3,
|
extend: size === 'xl' ? h1 : h3,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|
@ -40,7 +43,7 @@ export default {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
outline: 0,
|
outline: 0,
|
||||||
backgroundColor: secondaryColor,
|
backgroundColor: isGrey ? offDarkColor : secondaryColor,
|
||||||
'&:disabled': {
|
'&:disabled': {
|
||||||
backgroundColor: disabledColor,
|
backgroundColor: disabledColor,
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
|
|
@ -56,15 +59,19 @@ export default {
|
||||||
height,
|
height,
|
||||||
padding: `0 ${padding}px`,
|
padding: `0 ${padding}px`,
|
||||||
borderRadius: height / 4,
|
borderRadius: height / 4,
|
||||||
boxShadow: `0 ${shadowSize}px ${secondaryColorDark}`,
|
boxShadow: `0 ${shadowSize}px ${isGrey ? offColor : secondaryColorDark}`,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: secondaryColorDark,
|
backgroundColor: isGrey ? offColor : secondaryColorDark,
|
||||||
boxShadow: `0 ${shadowSize}px ${secondaryColorDarker}`
|
boxShadow: `0 ${shadowSize}px ${
|
||||||
|
isGrey ? offDarkerColor : secondaryColorDarker
|
||||||
|
}`
|
||||||
},
|
},
|
||||||
'&:active': {
|
'&:active': {
|
||||||
marginTop: shadowSize / 2,
|
marginTop: shadowSize / 2,
|
||||||
backgroundColor: secondaryColorDark,
|
backgroundColor: isGrey ? offDarkColor : secondaryColorDark,
|
||||||
boxShadow: `0 ${shadowSize / 2}px ${secondaryColorDarker}`
|
boxShadow: `0 ${shadowSize / 2}px ${
|
||||||
|
isGrey ? offDarkerColor : secondaryColorDarker
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
|
import { DialogActions, DialogContent, Dialog } from '@material-ui/core'
|
||||||
import Grid from '@material-ui/core/Grid'
|
import Grid from '@material-ui/core/Grid'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { parse, format } from 'date-fns/fp'
|
import { parse, format } from 'date-fns/fp'
|
||||||
import _ from 'lodash/fp'
|
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import { useState, React } from 'react'
|
import { useState, React } from 'react'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import ImagePopper from 'src/components/ImagePopper'
|
import ImagePopper from 'src/components/ImagePopper'
|
||||||
import { FeatureButton } from 'src/components/buttons'
|
import { FeatureButton, Button, IconButton } from 'src/components/buttons'
|
||||||
import { TextInput } from 'src/components/inputs/formik'
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
import { H3, Info3 } from 'src/components/typography'
|
import { H3, Info3, H2 } from 'src/components/typography'
|
||||||
import {
|
import {
|
||||||
OVERRIDE_AUTHORIZED,
|
OVERRIDE_AUTHORIZED,
|
||||||
OVERRIDE_REJECTED
|
OVERRIDE_REJECTED
|
||||||
|
|
@ -17,19 +17,22 @@ import {
|
||||||
import { ReactComponent as CardIcon } from 'src/styling/icons/ID/card/comet.svg'
|
import { ReactComponent as CardIcon } from 'src/styling/icons/ID/card/comet.svg'
|
||||||
import { ReactComponent as PhoneIcon } from 'src/styling/icons/ID/phone/comet.svg'
|
import { ReactComponent as PhoneIcon } from 'src/styling/icons/ID/phone/comet.svg'
|
||||||
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
|
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
|
||||||
|
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/comet.svg'
|
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/comet.svg'
|
||||||
import { ReactComponent as CustomerListViewReversedIcon } from 'src/styling/icons/circle buttons/customer-list-view/white.svg'
|
import { ReactComponent as CustomerListViewReversedIcon } from 'src/styling/icons/circle buttons/customer-list-view/white.svg'
|
||||||
import { ReactComponent as CustomerListViewIcon } from 'src/styling/icons/circle buttons/customer-list-view/zodiac.svg'
|
import { ReactComponent as CustomerListViewIcon } from 'src/styling/icons/circle buttons/customer-list-view/zodiac.svg'
|
||||||
import { ReactComponent as OverviewReversedIcon } from 'src/styling/icons/circle buttons/overview/white.svg'
|
import { ReactComponent as OverviewReversedIcon } from 'src/styling/icons/circle buttons/overview/white.svg'
|
||||||
import { ReactComponent as OverviewIcon } from 'src/styling/icons/circle buttons/overview/zodiac.svg'
|
import { ReactComponent as OverviewIcon } from 'src/styling/icons/circle buttons/overview/zodiac.svg'
|
||||||
import { URI } from 'src/utils/apollo'
|
import { URI } from 'src/utils/apollo'
|
||||||
|
import { onlyFirstToUpper } from 'src/utils/string'
|
||||||
|
|
||||||
import styles from './CustomerData.styles.js'
|
import styles from './CustomerData.styles.js'
|
||||||
import { EditableCard } from './components'
|
import { EditableCard } from './components'
|
||||||
import {
|
import {
|
||||||
customerDataElements,
|
customerDataElements,
|
||||||
customerDataSchemas,
|
customerDataSchemas,
|
||||||
formatDates
|
formatDates,
|
||||||
|
getFormattedPhone
|
||||||
} from './helper.js'
|
} from './helper.js'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
@ -62,6 +65,7 @@ const Photo = ({ show, src }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomerData = ({
|
const CustomerData = ({
|
||||||
|
locale,
|
||||||
customer,
|
customer,
|
||||||
updateCustomer,
|
updateCustomer,
|
||||||
replacePhoto,
|
replacePhoto,
|
||||||
|
|
@ -69,10 +73,12 @@ const CustomerData = ({
|
||||||
deleteEditedData,
|
deleteEditedData,
|
||||||
updateCustomRequest,
|
updateCustomRequest,
|
||||||
authorizeCustomRequest,
|
authorizeCustomRequest,
|
||||||
updateCustomEntry
|
updateCustomEntry,
|
||||||
|
retrieveAdditionalData
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [listView, setListView] = useState(false)
|
const [listView, setListView] = useState(false)
|
||||||
|
const [retrieve, setRetrieve] = useState(false)
|
||||||
|
|
||||||
const idData = R.path(['idCardData'])(customer)
|
const idData = R.path(['idCardData'])(customer)
|
||||||
const rawExpirationDate = R.path(['expirationDate'])(idData)
|
const rawExpirationDate = R.path(['expirationDate'])(idData)
|
||||||
|
|
@ -96,9 +102,12 @@ const CustomerData = ({
|
||||||
R.path(['customInfoRequests'])(customer) ?? []
|
R.path(['customInfoRequests'])(customer) ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const phone = R.path(['phone'])(customer)
|
||||||
|
const smsData = R.path(['subscriberInfo', 'result'])(customer)
|
||||||
|
|
||||||
const isEven = elem => elem % 2 === 0
|
const isEven = elem => elem % 2 === 0
|
||||||
|
|
||||||
const getVisibleCards = _.filter(elem => elem.isAvailable)
|
const getVisibleCards = R.filter(elem => elem.isAvailable)
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
idCardData: {
|
idCardData: {
|
||||||
|
|
@ -126,9 +135,34 @@ const CustomerData = ({
|
||||||
},
|
},
|
||||||
idCardPhoto: {
|
idCardPhoto: {
|
||||||
idCardPhoto: null
|
idCardPhoto: null
|
||||||
|
},
|
||||||
|
smsData: {
|
||||||
|
phoneNumber: getFormattedPhone(phone, locale.country)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const smsDataElements = [
|
||||||
|
{
|
||||||
|
name: 'phoneNumber',
|
||||||
|
label: 'Phone number',
|
||||||
|
component: TextInput,
|
||||||
|
editable: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const smsDataSchema = {
|
||||||
|
smsData: Yup.lazy(values => {
|
||||||
|
const additionalData = R.omit(['phoneNumber'])(values)
|
||||||
|
const fields = R.keys(additionalData)
|
||||||
|
if (R.length(fields) === 2) {
|
||||||
|
return Yup.object().shape({
|
||||||
|
[R.head(fields)]: Yup.string().required(),
|
||||||
|
[R.last(fields)]: Yup.string().required()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const cards = [
|
const cards = [
|
||||||
{
|
{
|
||||||
fields: customerDataElements.idCardData,
|
fields: customerDataElements.idCardData,
|
||||||
|
|
@ -141,19 +175,31 @@ const CustomerData = ({
|
||||||
deleteEditedData: () => deleteEditedData({ idCardData: null }),
|
deleteEditedData: () => deleteEditedData({ idCardData: null }),
|
||||||
save: values =>
|
save: values =>
|
||||||
editCustomer({
|
editCustomer({
|
||||||
idCardData: _.merge(idData, formatDates(values))
|
idCardData: R.merge(idData, formatDates(values))
|
||||||
}),
|
}),
|
||||||
validationSchema: customerDataSchemas.idCardData,
|
validationSchema: customerDataSchemas.idCardData,
|
||||||
initialValues: initialValues.idCardData,
|
initialValues: initialValues.idCardData,
|
||||||
isAvailable: !_.isNil(idData)
|
isAvailable: !R.isNil(idData)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SMS Confirmation',
|
fields: smsDataElements,
|
||||||
|
title: 'SMS data',
|
||||||
titleIcon: <PhoneIcon className={classes.cardIcon} />,
|
titleIcon: <PhoneIcon className={classes.cardIcon} />,
|
||||||
authorize: () => {},
|
state: R.path(['phoneOverride'])(customer),
|
||||||
reject: () => {},
|
authorize: () => updateCustomer({ phoneOverride: OVERRIDE_AUTHORIZED }),
|
||||||
save: () => {},
|
reject: () => updateCustomer({ phoneOverride: OVERRIDE_REJECTED }),
|
||||||
isAvailable: false
|
save: values => {
|
||||||
|
editCustomer({
|
||||||
|
subscriberInfo: {
|
||||||
|
result: R.merge(smsData, R.omit(['phoneNumber'])(values))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
validationSchema: smsDataSchema.smsData,
|
||||||
|
retrieveAdditionalData: () => setRetrieve(true),
|
||||||
|
initialValues: initialValues.smsData,
|
||||||
|
isAvailable: !R.isNil(phone),
|
||||||
|
hasAdditionalData: !R.isNil(smsData) && !R.isEmpty(smsData)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Name',
|
title: 'Name',
|
||||||
|
|
@ -171,7 +217,7 @@ const CustomerData = ({
|
||||||
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
|
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
|
||||||
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
|
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
|
||||||
children: <Info3>{sanctionsDisplay}</Info3>,
|
children: <Info3>{sanctionsDisplay}</Info3>,
|
||||||
isAvailable: !_.isNil(sanctions)
|
isAvailable: !R.isNil(sanctions)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fields: customerDataElements.frontCamera,
|
fields: customerDataElements.frontCamera,
|
||||||
|
|
@ -198,7 +244,7 @@ const CustomerData = ({
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
validationSchema: customerDataSchemas.frontCamera,
|
validationSchema: customerDataSchemas.frontCamera,
|
||||||
initialValues: initialValues.frontCamera,
|
initialValues: initialValues.frontCamera,
|
||||||
isAvailable: !_.isNil(customer.frontCameraPath)
|
isAvailable: !R.isNil(customer.frontCameraPath)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fields: customerDataElements.idCardPhoto,
|
fields: customerDataElements.idCardPhoto,
|
||||||
|
|
@ -223,7 +269,7 @@ const CustomerData = ({
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
validationSchema: customerDataSchemas.idCardPhoto,
|
validationSchema: customerDataSchemas.idCardPhoto,
|
||||||
initialValues: initialValues.idCardPhoto,
|
initialValues: initialValues.idCardPhoto,
|
||||||
isAvailable: !_.isNil(customer.idCardPhotoPath)
|
isAvailable: !R.isNil(customer.idCardPhotoPath)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fields: customerDataElements.usSsn,
|
fields: customerDataElements.usSsn,
|
||||||
|
|
@ -236,7 +282,7 @@ const CustomerData = ({
|
||||||
deleteEditedData: () => deleteEditedData({ usSsn: null }),
|
deleteEditedData: () => deleteEditedData({ usSsn: null }),
|
||||||
validationSchema: customerDataSchemas.usSsn,
|
validationSchema: customerDataSchemas.usSsn,
|
||||||
initialValues: initialValues.usSsn,
|
initialValues: initialValues.usSsn,
|
||||||
isAvailable: !_.isNil(customer.usSsn)
|
isAvailable: !R.isNil(customer.usSsn)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -319,6 +365,16 @@ const CustomerData = ({
|
||||||
})
|
})
|
||||||
}, R.path(['customFields'])(customer) ?? [])
|
}, R.path(['customFields'])(customer) ?? [])
|
||||||
|
|
||||||
|
R.forEach(it => {
|
||||||
|
initialValues.smsData[it] = smsData[it]
|
||||||
|
smsDataElements.push({
|
||||||
|
name: it,
|
||||||
|
label: onlyFirstToUpper(it),
|
||||||
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
|
})
|
||||||
|
}, R.keys(smsData) ?? [])
|
||||||
|
|
||||||
const editableCard = (
|
const editableCard = (
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
|
|
@ -329,10 +385,12 @@ const CustomerData = ({
|
||||||
fields,
|
fields,
|
||||||
save,
|
save,
|
||||||
deleteEditedData,
|
deleteEditedData,
|
||||||
|
retrieveAdditionalData,
|
||||||
children,
|
children,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
initialValues,
|
initialValues,
|
||||||
hasImage
|
hasImage,
|
||||||
|
hasAdditionalData
|
||||||
},
|
},
|
||||||
idx
|
idx
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -345,12 +403,14 @@ const CustomerData = ({
|
||||||
state={state}
|
state={state}
|
||||||
titleIcon={titleIcon}
|
titleIcon={titleIcon}
|
||||||
hasImage={hasImage}
|
hasImage={hasImage}
|
||||||
|
hasAdditionalData={hasAdditionalData}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
children={children}
|
children={children}
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
save={save}
|
save={save}
|
||||||
deleteEditedData={deleteEditedData}></EditableCard>
|
deleteEditedData={deleteEditedData}
|
||||||
|
retrieveAdditionalData={retrieveAdditionalData}></EditableCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -394,7 +454,7 @@ const CustomerData = ({
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{!_.isEmpty(customFields) && (
|
{!R.isEmpty(customFields) && (
|
||||||
<div className={classes.wrapper}>
|
<div className={classes.wrapper}>
|
||||||
<span className={classes.separator}>Custom data entry</span>
|
<span className={classes.separator}>Custom data entry</span>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
|
@ -429,8 +489,67 @@ const CustomerData = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<RetrieveDataDialog
|
||||||
|
setRetrieve={setRetrieve}
|
||||||
|
retrieveAdditionalData={retrieveAdditionalData}
|
||||||
|
open={retrieve}></RetrieveDataDialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RetrieveDataDialog = ({
|
||||||
|
setRetrieve,
|
||||||
|
retrieveAdditionalData,
|
||||||
|
open,
|
||||||
|
props
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
PaperProps={{
|
||||||
|
style: {
|
||||||
|
borderRadius: 8,
|
||||||
|
minWidth: 656,
|
||||||
|
bottom: 125,
|
||||||
|
right: 7
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...props}>
|
||||||
|
<div className={classes.closeButton}>
|
||||||
|
<IconButton
|
||||||
|
size={16}
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setRetrieve(false)}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<H2 className={classes.dialogTitle}>{'Retrieve API data from Twilio'}</H2>
|
||||||
|
<DialogContent className={classes.dialogContent}>
|
||||||
|
<Info3>{`With this action you'll be using Twilio's API to retrieve additional
|
||||||
|
data from this user. This includes name and address, if available.\n`}</Info3>
|
||||||
|
<Info3>{` There is a small cost from Twilio for each retrieval. Would you like
|
||||||
|
to proceed?`}</Info3>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions className={classes.dialogActions}>
|
||||||
|
<Button
|
||||||
|
backgroundColor="grey"
|
||||||
|
className={classes.cancelButton}
|
||||||
|
onClick={() => setRetrieve(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
retrieveAdditionalData()
|
||||||
|
setRetrieve(false)
|
||||||
|
}}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomerData
|
export default CustomerData
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { offColor } from 'src/styling/variables'
|
import { offColor, spacer } from 'src/styling/variables'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
header: {
|
header: {
|
||||||
|
|
@ -45,5 +45,26 @@ export default {
|
||||||
left: '100%',
|
left: '100%',
|
||||||
marginLeft: 15
|
marginLeft: 15
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
display: 'flex',
|
||||||
|
padding: [[spacer * 2, spacer * 2, 0, spacer * 2]],
|
||||||
|
paddingRight: spacer * 1.5,
|
||||||
|
justifyContent: 'end'
|
||||||
|
},
|
||||||
|
dialogTitle: {
|
||||||
|
margin: [[0, spacer * 2, spacer, spacer * 4 + spacer]]
|
||||||
|
},
|
||||||
|
dialogContent: {
|
||||||
|
width: 615,
|
||||||
|
marginLeft: 16
|
||||||
|
},
|
||||||
|
dialogActions: {
|
||||||
|
padding: spacer * 4,
|
||||||
|
paddingTop: spacer * 2
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
marginRight: 8,
|
||||||
|
padding: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ const GET_CUSTOMER = gql`
|
||||||
daysSuspended
|
daysSuspended
|
||||||
isSuspended
|
isSuspended
|
||||||
isTestCustomer
|
isTestCustomer
|
||||||
|
subscriberInfo
|
||||||
|
phoneOverride
|
||||||
customFields {
|
customFields {
|
||||||
id
|
id
|
||||||
label
|
label
|
||||||
|
|
@ -138,6 +140,7 @@ const SET_CUSTOMER = gql`
|
||||||
lastTxFiatCode
|
lastTxFiatCode
|
||||||
lastTxClass
|
lastTxClass
|
||||||
subscriberInfo
|
subscriberInfo
|
||||||
|
phoneOverride
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
@ -438,6 +441,16 @@ const CustomerProfile = memo(() => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const retrieveAdditionalData = () =>
|
||||||
|
setCustomer({
|
||||||
|
variables: {
|
||||||
|
customerId,
|
||||||
|
customerInput: {
|
||||||
|
subscriberInfo: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const onClickSidebarItem = code => setClickedItem(code)
|
const onClickSidebarItem = code => setClickedItem(code)
|
||||||
|
|
||||||
const configData = R.path(['config'])(customerResponse) ?? []
|
const configData = R.path(['config'])(customerResponse) ?? []
|
||||||
|
|
@ -558,25 +571,6 @@ const CustomerProfile = memo(() => {
|
||||||
}>
|
}>
|
||||||
{`${blocked ? 'Authorize' : 'Block'} customer`}
|
{`${blocked ? 'Authorize' : 'Block'} customer`}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.actionButton}
|
|
||||||
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
|
||||||
InverseIcon={
|
|
||||||
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
setCustomer({
|
|
||||||
variables: {
|
|
||||||
customerId,
|
|
||||||
customerInput: {
|
|
||||||
subscriberInfo: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
{`Retrieve information`}
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -628,6 +622,7 @@ const CustomerProfile = memo(() => {
|
||||||
{isCustomerData && (
|
{isCustomerData && (
|
||||||
<div>
|
<div>
|
||||||
<CustomerData
|
<CustomerData
|
||||||
|
locale={locale}
|
||||||
customer={customerData}
|
customer={customerData}
|
||||||
updateCustomer={updateCustomer}
|
updateCustomer={updateCustomer}
|
||||||
replacePhoto={replacePhoto}
|
replacePhoto={replacePhoto}
|
||||||
|
|
@ -635,7 +630,8 @@ const CustomerProfile = memo(() => {
|
||||||
deleteEditedData={deleteEditedData}
|
deleteEditedData={deleteEditedData}
|
||||||
updateCustomRequest={setCustomerCustomInfoRequest}
|
updateCustomRequest={setCustomerCustomInfoRequest}
|
||||||
authorizeCustomRequest={authorizeCustomRequest}
|
authorizeCustomRequest={authorizeCustomRequest}
|
||||||
updateCustomEntry={updateCustomEntry}></CustomerData>
|
updateCustomEntry={updateCustomEntry}
|
||||||
|
retrieveAdditionalData={retrieveAdditionalData}></CustomerData>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isNotes && (
|
{isNotes && (
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import { ReactComponent as EditReversedIcon } from 'src/styling/icons/action/edi
|
||||||
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/white.svg'
|
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/white.svg'
|
||||||
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/white.svg'
|
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/white.svg'
|
||||||
import { ReactComponent as CancelReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
|
import { ReactComponent as CancelReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
|
||||||
|
import { ReactComponent as DataReversedIcon } from 'src/styling/icons/button/data/white.svg'
|
||||||
|
import { ReactComponent as DataIcon } from 'src/styling/icons/button/data/zodiac.svg'
|
||||||
import { ReactComponent as ReplaceReversedIcon } from 'src/styling/icons/button/replace/white.svg'
|
import { ReactComponent as ReplaceReversedIcon } from 'src/styling/icons/button/replace/white.svg'
|
||||||
import { ReactComponent as SaveReversedIcon } from 'src/styling/icons/circle buttons/save/white.svg'
|
import { ReactComponent as SaveReversedIcon } from 'src/styling/icons/circle buttons/save/white.svg'
|
||||||
import { comet } from 'src/styling/variables'
|
import { comet } from 'src/styling/variables'
|
||||||
|
|
@ -67,6 +69,13 @@ const fieldStyles = {
|
||||||
fontSize: 14
|
fontSize: 14
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
readOnlyLabel: {
|
||||||
|
color: comet,
|
||||||
|
margin: [[3, 0, 3, 0]]
|
||||||
|
},
|
||||||
|
readOnlyValue: {
|
||||||
|
margin: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,6 +114,23 @@ const EditableField = ({ editing, field, value, size, ...props }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ReadOnlyField = ({ field, value, ...props }) => {
|
||||||
|
const classes = fieldUseStyles()
|
||||||
|
const classNames = {
|
||||||
|
[classes.field]: true,
|
||||||
|
[classes.notEditing]: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classnames(classNames)}>
|
||||||
|
<Label1 className={classes.readOnlyLabel}>{field.label}</Label1>
|
||||||
|
<P className={classes.readOnlyValue}>{value}</P>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const EditableCard = ({
|
const EditableCard = ({
|
||||||
fields,
|
fields,
|
||||||
save,
|
save,
|
||||||
|
|
@ -117,7 +143,9 @@ const EditableCard = ({
|
||||||
children,
|
children,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
initialValues,
|
initialValues,
|
||||||
deleteEditedData
|
deleteEditedData,
|
||||||
|
retrieveAdditionalData,
|
||||||
|
hasAdditionalData = true
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
|
@ -174,7 +202,7 @@ const EditableCard = ({
|
||||||
setEditing(false)
|
setEditing(false)
|
||||||
setError(false)
|
setError(false)
|
||||||
}}>
|
}}>
|
||||||
{({ values, touched, errors, setFieldValue }) => (
|
{({ setFieldValue }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<PromptWhenDirty />
|
<PromptWhenDirty />
|
||||||
<div className={classes.row}>
|
<div className={classes.row}>
|
||||||
|
|
@ -183,12 +211,19 @@ const EditableCard = ({
|
||||||
{!hasImage &&
|
{!hasImage &&
|
||||||
fields?.map((field, idx) => {
|
fields?.map((field, idx) => {
|
||||||
return idx >= 0 && idx < 4 ? (
|
return idx >= 0 && idx < 4 ? (
|
||||||
<EditableField
|
!field.editable ? (
|
||||||
field={field}
|
<ReadOnlyField
|
||||||
value={initialValues[field.name]}
|
field={field}
|
||||||
editing={editing}
|
value={initialValues[field.name]}
|
||||||
size={180}
|
/>
|
||||||
/>
|
) : (
|
||||||
|
<EditableField
|
||||||
|
field={field}
|
||||||
|
value={initialValues[field.name]}
|
||||||
|
editing={editing}
|
||||||
|
size={180}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : null
|
) : null
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -196,12 +231,19 @@ const EditableCard = ({
|
||||||
{!hasImage &&
|
{!hasImage &&
|
||||||
fields?.map((field, idx) => {
|
fields?.map((field, idx) => {
|
||||||
return idx >= 4 ? (
|
return idx >= 4 ? (
|
||||||
<EditableField
|
!field.editable ? (
|
||||||
field={field}
|
<ReadOnlyField
|
||||||
value={initialValues[field.name]}
|
field={field}
|
||||||
editing={editing}
|
value={initialValues[field.name]}
|
||||||
size={180}
|
/>
|
||||||
/>
|
) : (
|
||||||
|
<EditableField
|
||||||
|
field={field}
|
||||||
|
value={initialValues[field.name]}
|
||||||
|
editing={editing}
|
||||||
|
size={180}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : null
|
) : null
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -210,25 +252,34 @@ const EditableCard = ({
|
||||||
<div className={classes.edit}>
|
<div className={classes.edit}>
|
||||||
{!editing && (
|
{!editing && (
|
||||||
<div className={classes.editButton}>
|
<div className={classes.editButton}>
|
||||||
{// TODO: Remove false condition for next release
|
<div className={classes.deleteButton}>
|
||||||
false && (
|
{false && (
|
||||||
<div className={classes.deleteButton}>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="primary"
|
color="primary"
|
||||||
type="button"
|
type="button"
|
||||||
Icon={DeleteIcon}
|
Icon={DeleteIcon}
|
||||||
InverseIcon={DeleteReversedIcon}
|
InverseIcon={DeleteReversedIcon}
|
||||||
onClick={() => deleteEditedData()}>
|
onClick={() => deleteEditedData()}>
|
||||||
{`Delete`}
|
Delete
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
)}
|
||||||
)}
|
{!hasAdditionalData && (
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
Icon={DataIcon}
|
||||||
|
InverseIcon={DataReversedIcon}
|
||||||
|
onClick={() => retrieveAdditionalData()}>
|
||||||
|
Retrieve API data
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="primary"
|
color="primary"
|
||||||
Icon={EditIcon}
|
Icon={EditIcon}
|
||||||
InverseIcon={EditReversedIcon}
|
InverseIcon={EditReversedIcon}
|
||||||
onClick={() => setEditing(true)}>
|
onClick={() => setEditing(true)}>
|
||||||
{`Edit`}
|
Edit
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -355,37 +355,44 @@ const customerDataElements = {
|
||||||
{
|
{
|
||||||
name: 'firstName',
|
name: 'firstName',
|
||||||
label: 'First name',
|
label: 'First name',
|
||||||
component: TextInput
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'documentNumber',
|
name: 'documentNumber',
|
||||||
label: 'ID number',
|
label: 'ID number',
|
||||||
component: TextInput
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dateOfBirth',
|
name: 'dateOfBirth',
|
||||||
label: 'Birthdate',
|
label: 'Birthdate',
|
||||||
component: TextInput
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'gender',
|
name: 'gender',
|
||||||
label: 'Gender',
|
label: 'Gender',
|
||||||
component: TextInput
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lastName',
|
name: 'lastName',
|
||||||
label: 'Last name',
|
label: 'Last name',
|
||||||
component: TextInput
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'expirationDate',
|
name: 'expirationDate',
|
||||||
label: 'Expiration Date',
|
label: 'Expiration Date',
|
||||||
component: TextInput
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'country',
|
name: 'country',
|
||||||
label: 'Country',
|
label: 'Country',
|
||||||
component: TextInput
|
component: TextInput,
|
||||||
|
editable: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
usSsn: [
|
usSsn: [
|
||||||
|
|
@ -393,7 +400,8 @@ const customerDataElements = {
|
||||||
name: 'usSsn',
|
name: 'usSsn',
|
||||||
label: 'US SSN',
|
label: 'US SSN',
|
||||||
component: TextInput,
|
component: TextInput,
|
||||||
size: 190
|
size: 190,
|
||||||
|
editable: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
idCardPhoto: [{ name: 'idCardPhoto' }],
|
idCardPhoto: [{ name: 'idCardPhoto' }],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue