Merge pull request #1029 from josepfo/feat/twilio-api-data-ui

Feat: twilio api data UI
This commit is contained in:
Rafael Taranto 2022-01-21 15:22:32 +00:00 committed by GitHub
commit 201fec33e4
11 changed files with 316 additions and 86 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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']

View 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()
}

View file

@ -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

View file

@ -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
}`
} }
} }
} }

View file

@ -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

View file

@ -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
} }
} }

View file

@ -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 && (

View file

@ -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 ? (
!field.editable ? (
<ReadOnlyField
field={field}
value={initialValues[field.name]}
/>
) : (
<EditableField <EditableField
field={field} field={field}
value={initialValues[field.name]} value={initialValues[field.name]}
editing={editing} editing={editing}
size={180} 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 ? (
!field.editable ? (
<ReadOnlyField
field={field}
value={initialValues[field.name]}
/>
) : (
<EditableField <EditableField
field={field} field={field}
value={initialValues[field.name]} value={initialValues[field.name]}
editing={editing} editing={editing}
size={180} 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
false && (
<div className={classes.deleteButton}> <div className={classes.deleteButton}>
{false && (
<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>
)} )}

View file

@ -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' }],