feat: change customer screen transaction list part to the new design

feat: created the compliance details component (no data for now)

fix: added missing properties into the gql schema and the compliance
details component

feat: added another chip type for a neutral situation

style: change the property card style for the v1 specs

fix: added front facing camera override to schema and components

feat: added authorized override (status) column to the customers list
table

fix: moved name to the front of the phone on the customers list table

fix: added sanctions description text on it's card

fix: added id icon to the right of the customer name

feat: created subpage button component and use it in the customer
profile

feat: created an image popper component and use it in the customer
compliance page

fix: added varying sizes to the customer details and id data cards fields

refactor: simplify the compliance subpage code
This commit is contained in:
Liordino Neto 2020-10-16 18:11:45 -03:00 committed by Josh Harvey
parent 8ff0a7f79b
commit f53a934092
28 changed files with 744 additions and 260 deletions

View file

@ -99,7 +99,15 @@ function update (id, data, userToken, txId) {
*/
async function updateCustomer (id, data) {
const formattedData = _.pick(
['authorized_override', 'id_card_photo_override', 'id_card_data_override', 'sms_override', 'us_ssn_override'],
[
'authorized_override',
'id_card_photo_override',
'id_card_data_override',
'sms_override',
'us_ssn_override',
'sanctions_override',
'front_camera_override'
],
_.mapKeys(_.snakeCase, data))
const sql = Pgp.helpers.update(formattedData, _.keys(formattedData), 'customers') +
@ -429,14 +437,16 @@ function batch () {
* @returns {array} Array of customers with it's transactions aggregations
*/
function getCustomersList () {
const sql = `select id, name, authorized_override, front_camera_path, phone, sms_override,
id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path,
id_card_photo_override, total_txs, total_spent, created as last_active,
fiat as last_tx_fiat, fiat_code as last_tx_fiat_code, tx_class as last_tx_class
const sql = `select id, name, authorized_override, front_camera_path, front_camera_override,
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
fiat_code as last_tx_fiat_code, tx_class as last_tx_class
from (
select c.id, c.name, c.authorized_override, c.front_camera_path, c.phone, c.sms_override,
c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, c.id_card_photo_path,
c.id_card_photo_override, t.tx_class, t.fiat, t.fiat_code, t.created,
select c.id, c.name, c.authorized_override, c.front_camera_path, c.front_camera_override,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
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,
coalesce(sum(t.fiat) over (partition by c.id), 0) as total_spent
@ -463,14 +473,16 @@ function getCustomersList () {
* @returns {array} Array of customers with it's transactions aggregations
*/
function getCustomerById (id) {
const sql = `select id, name, authorized_override, front_camera_path, phone, sms_override,
id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path,
id_card_photo_override, total_txs, total_spent, created as last_active,
fiat as last_tx_fiat, fiat_code as last_tx_fiat_code, tx_class as last_tx_class
const sql = `select id, name, authorized_override, front_camera_path, front_camera_override,
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
fiat_code as last_tx_fiat_code, tx_class as last_tx_class
from (
select c.id, c.name, c.authorized_override, c.front_camera_path, c.phone, c.sms_override,
c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, c.id_card_photo_path,
c.id_card_photo_override, t.tx_class, t.fiat, t.fiat_code, t.created,
select c.id, c.name, c.authorized_override, c.front_camera_path, c.front_camera_override,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
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(t.fiat) over (partition by c.id) as total_spent

View file

@ -68,6 +68,7 @@ const typeDefs = gql`
name: String
authorizedOverride: String
frontCameraPath: String
frontCameraOverride: String
phone: String
smsOverride: String
idCardData: JSONObject
@ -75,6 +76,11 @@ const typeDefs = gql`
idCardDataExpiration: Date
idCardPhotoPath: String
idCardPhotoOverride: String
usSsn: String
usSsnOverride: String
sanctions: Boolean
sanctionsAt: Date
sanctionsOverride: String
totalTxs: Int
totalSpent: String
lastActive: Date
@ -88,6 +94,7 @@ const typeDefs = gql`
name: String
authorizedOverride: String
frontCameraPath: String
frontCameraOverride: String
phone: String
smsOverride: String
idCardData: JSONObject
@ -95,6 +102,11 @@ const typeDefs = gql`
idCardDataExpiration: Date
idCardPhotoPath: String
idCardPhotoOverride: String
usSsn: String
usSsnOverride: String
sanctions: Boolean
sanctionsAt: Date
sanctionsOverride: String
totalTxs: Int
totalSpent: String
lastActive: Date

View file

@ -0,0 +1,52 @@
import { makeStyles, ClickAwayListener } from '@material-ui/core'
import classnames from 'classnames'
import React, { memo, useState } from 'react'
import Popper from 'src/components/Popper'
import { FeatureButton } from 'src/components/buttons'
import { ReactComponent as ZoomIconInverse } from 'src/styling/icons/circle buttons/search/white.svg'
import { ReactComponent as ZoomIcon } from 'src/styling/icons/circle buttons/search/zodiac.svg'
import imagePopperStyles from './ImagePopper.styles'
const useStyles = makeStyles(imagePopperStyles)
const ImagePopper = memo(({ className, width, height, src }) => {
const classes = useStyles({ width, height })
const [popperAnchorEl, setPopperAnchorEl] = useState(null)
const handleOpenPopper = event => {
setPopperAnchorEl(popperAnchorEl ? null : event.currentTarget)
}
const handleClosePopper = () => {
setPopperAnchorEl(null)
}
const popperOpen = Boolean(popperAnchorEl)
const Image = ({ className }) => (
<img className={classnames(className)} src={src} alt="" />
)
return (
<ClickAwayListener onClickAway={handleClosePopper}>
<div className={classnames(classes.row, className)}>
<Image className={classes.unzoomedImg} />
<FeatureButton
Icon={ZoomIcon}
InverseIcon={ZoomIconInverse}
className={classes.button}
onClick={handleOpenPopper}
/>
<Popper open={popperOpen} anchorEl={popperAnchorEl} placement="top">
<div className={classes.popoverContent}>
<Image />
</div>
</Popper>
</div>
</ClickAwayListener>
)
})
export default ImagePopper

View file

@ -0,0 +1,20 @@
export default {
row: {
display: 'flex',
flexDirection: 'row'
},
unzoomedImg: ({ width, height }) => ({
objectFit: 'cover',
borderRadius: '8px 0px 0px 8px',
width,
height
}),
button: ({ height }) => ({
borderRadius: '0px 8px 8px 0px',
height
}),
popoverContent: {
display: 'block',
padding: [[10, 15]]
}
}

View file

@ -9,6 +9,8 @@ import {
secondaryColorDarker as spring4,
inputFontWeight,
spring3,
zircon,
primaryColor,
smallestFontSize,
inputFontFamily,
spacer,
@ -18,13 +20,15 @@ import {
const colors = {
error: tomato,
warning: pumpkin,
success: spring4
success: spring4,
neutral: primaryColor
}
const backgroundColors = {
error: mistyRose,
warning: linen,
success: spring3
success: spring3,
neutral: zircon
}
const useStyles = makeStyles({

View file

@ -0,0 +1,59 @@
import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import React, { memo, useState } from 'react'
import { ReactComponent as CancelIconInverse } from 'src/styling/icons/button/cancel/white.svg'
import subpageButtonStyles from './SubpageButton.styles'
const useStyles = makeStyles(subpageButtonStyles)
const SubpageButton = memo(
({ className, Icon, InverseIcon, toggle, children }) => {
const [active, setActive] = useState(false)
const classes = useStyles()
const classNames = {
[classes.button]: true,
[classes.normalButton]: !active,
[classes.activeButton]: active
}
const normalButton = <Icon className={classes.buttonIcon} />
const activeButton = (
<>
<InverseIcon
className={classnames(
classes.buttonIcon,
classes.buttonIconActiveLeft
)}
/>
{children}
<CancelIconInverse
className={classnames(
classes.buttonIcon,
classes.buttonIconActiveRight
)}
/>
</>
)
const innerToggle = () => {
const newActiveState = !active
toggle(newActiveState)
setActive(newActiveState)
}
return (
<button
className={classnames(classNames, className)}
onClick={innerToggle}>
{active ? activeButton : normalButton}
</button>
)
}
)
export default SubpageButton

View file

@ -0,0 +1,44 @@
import baseButtonStyles from 'src/components/buttons/BaseButton.styles'
import { offColor, white } from 'src/styling/variables'
const { baseButton } = baseButtonStyles
export default {
button: {
extend: baseButton,
padding: 0,
color: white,
borderRadius: baseButton.height / 2
},
normalButton: {
width: baseButton.height
},
activeButton: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
backgroundColor: offColor,
fontWeight: 'bold',
padding: '0 5px',
'&:hover': {
backgroundColor: offColor
}
},
buttonIcon: {
'& svg': {
width: 16,
height: 16,
overflow: 'visible',
'& g': {
strokeWidth: 1.8
}
}
},
buttonIconActiveLeft: {
marginRight: 12
},
buttonIconActiveRight: {
marginRight: 5,
marginLeft: 20
}
}

View file

@ -7,6 +7,7 @@ import IconButton from './IconButton'
import Link from './Link'
import SimpleButton from './SimpleButton'
import SupportLinkButton from './SupportLinkButton'
import SubpageButton from './SubpageButton'
export {
Button,
@ -18,4 +19,5 @@ export {
IDButton,
AddButton,
SupportLinkButton
SubpageButton
}

View file

@ -3,7 +3,7 @@ import { makeStyles, Breadcrumbs, Box } from '@material-ui/core'
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { memo } from 'react'
import React, { memo, useState } from 'react'
import { useHistory, useParams } from 'react-router-dom'
import { ActionButton } from 'src/components/buttons'
@ -16,25 +16,26 @@ import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/butto
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/block/white.svg'
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/zodiac.svg'
import { fromNamespace, namespaces } from 'src/utils/config'
import styles from './CustomerProfile.styles'
import {
CustomerDetails,
IdDataCard,
PhoneCard,
IdCardPhotoCard,
TransactionsList
TransactionsList,
ComplianceDetails
} from './components'
const useStyles = makeStyles(styles)
const GET_CUSTOMER = gql`
query customer($customerId: ID!) {
config
customer(customerId: $customerId) {
id
name
authorizedOverride
frontCameraPath
frontCameraOverride
phone
smsOverride
idCardData
@ -42,6 +43,11 @@ const GET_CUSTOMER = gql`
idCardDataExpiration
idCardPhotoPath
idCardPhotoOverride
usSsn
usSsnOverride
sanctions
sanctionsAt
sanctionsOverride
totalTxs
totalSpent
lastActive
@ -70,6 +76,7 @@ const SET_CUSTOMER = gql`
name
authorizedOverride
frontCameraPath
frontCameraOverride
phone
smsOverride
idCardData
@ -77,6 +84,11 @@ const SET_CUSTOMER = gql`
idCardDataExpiration
idCardPhotoPath
idCardPhotoOverride
usSsn
usSsnOverride
sanctions
sanctionsAt
sanctionsOverride
totalTxs
totalSpent
lastActive
@ -90,6 +102,7 @@ const SET_CUSTOMER = gql`
const CustomerProfile = memo(() => {
const classes = useStyles()
const history = useHistory()
const [showCompliance, setShowCompliance] = useState(false)
const { id: customerId } = useParams()
const { data: customerResponse, refetch: getCustomer, loading } = useQuery(
@ -111,6 +124,8 @@ const CustomerProfile = memo(() => {
}
})
const configData = R.path(['config'])(customerResponse) ?? []
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
const customerData = R.path(['customer'])(customerResponse) ?? []
const rawTransactions = R.path(['transactions'])(customerData) ?? []
const sortedTransactions = R.sort(R.descend(R.prop('cryptoAtoms')))(
@ -137,8 +152,15 @@ const CustomerProfile = memo(() => {
</Label2>
</Breadcrumbs>
<div>
<Box display="flex" justifyContent="space-between">
<CustomerDetails customer={customerData} />
<Box
className={classes.customerDetails}
display="flex"
justifyContent="space-between">
<CustomerDetails
customer={customerData}
locale={locale}
setShowCompliance={() => setShowCompliance(!showCompliance)}
/>
<div>
<Label1 className={classes.actionLabel}>Actions</Label1>
<ActionButton
@ -156,22 +178,21 @@ const CustomerProfile = memo(() => {
</ActionButton>
</div>
</Box>
<Box display="flex">
<IdDataCard
customerData={customerData}
updateCustomer={updateCustomer}
/>
<PhoneCard
customerData={customerData}
updateCustomer={updateCustomer}
/>
<IdCardPhotoCard
customerData={customerData}
updateCustomer={updateCustomer}
/>
</Box>
</div>
<TransactionsList data={sortedTransactions} loading={loading} />
{!showCompliance && (
<TransactionsList
customer={customerData}
data={sortedTransactions}
loading={loading}
/>
)}
{showCompliance && (
<ComplianceDetails
customer={customerData}
locale={locale}
updateCustomer={updateCustomer}
/>
)}
</>
)
})

View file

@ -11,5 +11,8 @@ export default {
actionLabel: {
color: comet,
margin: [[4, 0]]
},
customerDetails: {
marginBottom: 18
}
}

View file

@ -4,10 +4,13 @@ import * as R from 'ramda'
import React from 'react'
import { useHistory } from 'react-router-dom'
import { fromNamespace, namespaces } from 'src/utils/config'
import CustomersList from './CustomersList'
const GET_CUSTOMERS = gql`
{
config
customers {
id
name
@ -18,6 +21,7 @@ const GET_CUSTOMERS = gql`
lastTxFiat
lastTxFiatCode
lastTxClass
authorizedOverride
}
}
`
@ -29,6 +33,8 @@ const Customers = () => {
const handleCustomerClicked = customer =>
history.push(`/compliance/customer/${customer.id}`)
const configData = R.path(['config'])(customersResponse) ?? []
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
const customersData = R.sortWith([R.descend(R.prop('lastActive'))])(
R.path(['customers'])(customersResponse) ?? []
)
@ -36,6 +42,7 @@ const Customers = () => {
return (
<CustomersList
data={customersData}
locale={locale}
onClick={handleCustomerClicked}
loading={loading}
/>

View file

@ -4,6 +4,7 @@ import moment from 'moment'
import * as R from 'ramda'
import React from 'react'
import { MainStatus } from 'src/components/Status'
import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
@ -14,42 +15,58 @@ import styles from './CustomersList.styles'
const useStyles = makeStyles(styles)
const CustomersList = ({ data, onClick, loading }) => {
const CUSTOMER_VERIFIED = 'verified'
const CUSTOMER_BLOCKED = 'blocked'
const CustomersList = ({ data, locale, onClick, loading }) => {
const classes = useStyles()
const getAuthorizedStatus = authorizedOverride =>
authorizedOverride === CUSTOMER_VERIFIED
? { label: 'Authorized', type: 'success' }
: authorizedOverride === CUSTOMER_BLOCKED
? { label: 'Blocked', type: 'error' }
: { label: 'Suspended', type: 'warning' }
const elements = [
{
header: 'Phone',
width: 186,
view: it => parsePhoneNumberFromString(it.phone).formatInternational()
},
{
header: 'Name',
width: 277,
width: 241,
view: R.path(['name'])
},
{
header: 'Phone',
width: 172,
view: it =>
it.phone && locale.country
? parsePhoneNumberFromString(
it.phone,
locale.country
).formatInternational()
: ''
},
{
header: 'Total TXs',
width: 154,
width: 126,
textAlign: 'right',
view: it => `${Number.parseInt(it.totalTxs)}`
},
{
header: 'Total spent',
width: 188,
width: 152,
textAlign: 'right',
view: it =>
`${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode ?? ''}`
},
{
header: 'Last active',
width: 197,
width: 133,
view: it =>
ifNotNull(it.lastActive, moment.utc(it.lastActive).format('YYYY-MM-D'))
},
{
header: 'Last transaction',
width: 198,
width: 161,
textAlign: 'right',
view: it => {
const hasLastTx = !R.isNil(it.lastTxFiatCode)
@ -63,6 +80,13 @@ const CustomersList = ({ data, onClick, loading }) => {
</>
)
}
},
{
header: 'Status',
width: 188,
view: it => (
<MainStatus statuses={[getAuthorizedStatus(it.authorizedOverride)]} />
)
}
]

View file

@ -1,6 +1,6 @@
import typographyStyles from 'src/components/typography/styles'
import baseStyles from 'src/pages/Logs.styles'
import { zircon, primaryColor, fontSize4 } from 'src/styling/variables'
import { zircon, comet, primaryColor, fontSize4 } from 'src/styling/variables'
const { label1 } = typographyStyles
const { titleWrapper, titleAndButtonsContainer } = baseStyles
@ -80,5 +80,26 @@ export default {
},
customerName: {
marginBottom: 32
},
icon: {
marginRight: 11
},
name: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
value: {
height: 16
},
label: {
marginBottom: 4,
color: comet
},
idIcon: {
marginRight: 10
},
subpageButton: {
marginLeft: 16
}
}

View file

@ -0,0 +1,141 @@
import { Box } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import { parsePhoneNumberFromString } from 'libphonenumber-js'
import * as R from 'ramda'
import React from 'react'
import ImagePopper from 'src/components/ImagePopper'
import { H3, Info3 } from 'src/components/typography'
import {
PropertyCard,
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
} from 'src/pages/Customers/components/propertyCard'
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
import { URI } from 'src/utils/apollo'
import { complianceDetailsStyles } from './ComplianceDetails.styles'
import Field from './Field'
import { IdDataCard } from './'
const useStyles = makeStyles(complianceDetailsStyles)
const imageWidth = 165
const imageHeight = 45
const Photo = ({ show, src }) => {
const classes = useStyles({ width: imageWidth })
return (
<>
{show ? (
<ImagePopper src={src} width={imageWidth} height={imageHeight} />
) : (
<div className={classes.photoWrapper}>
<CrossedCameraIcon />
</div>
)}
</>
)
}
const ComplianceDetails = ({ customer, locale, updateCustomer }) => {
const classes = useStyles({ width: imageWidth })
const phone =
customer.phone && locale.country
? parsePhoneNumberFromString(
customer.phone,
locale.country
).formatInternational()
: ''
const sanctions = R.path(['sanctions'])(customer)
const sanctionsAt = R.path(['sanctionsAt'])(customer)
const sanctionsDisplay = !sanctionsAt
? 'Not checked yet'
: sanctions
? 'Passed'
: 'Failed'
return (
<div>
<H3>Compliance details</H3>
<div>
<IdDataCard customerData={customer} updateCustomer={updateCustomer} />
<Box className={classes.complianceDetailsGrid}>
<Box className={classes.firstColumn}>
<PropertyCard
title={'Phone nº'}
state={R.path(['smsOverride'])(customer)}
authorize={() =>
updateCustomer({ smsOverride: OVERRIDE_AUTHORIZED })
}
reject={() => updateCustomer({ smsOverride: OVERRIDE_REJECTED })}>
<Field label={'Phone'} display={phone} />
</PropertyCard>
<PropertyCard
title={'ID photo'}
state={R.path(['idCardPhotoOverride'])(customer)}
authorize={() =>
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED })
}
reject={() =>
updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED })
}>
<Photo
show={customer.idCardPhotoPath}
src={`${URI}/id-card-photo/${R.path(['idCardPhotoPath'])(
customer
)}`}
/>
</PropertyCard>
<PropertyCard
title={'Front facing camera'}
state={R.path(['frontCameraOverride'])(customer)}
authorize={() =>
updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED })
}
reject={() =>
updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED })
}>
<Photo
show={customer.frontCameraPath}
src={`${URI}/front-camera-photo/${R.path(['frontCameraPath'])(
customer
)}`}
/>
</PropertyCard>
</Box>
<Box className={classes.lastColumn}>
<PropertyCard
title={'US SSN'}
state={R.path(['usSsnOverride'])(customer)}
authorize={() =>
updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED })
}
reject={() =>
updateCustomer({ usSsnOverride: OVERRIDE_REJECTED })
}>
<Field label={'US SSN'} display={customer.usSsn} />
</PropertyCard>
<PropertyCard
title={'Sanctions check'}
state={R.path(['sanctionsOverride'])(customer)}
authorize={() =>
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED })
}
reject={() =>
updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED })
}>
<Info3>{sanctionsDisplay}</Info3>
</PropertyCard>
</Box>
</Box>
</div>
</div>
)
}
export default ComplianceDetails

View file

@ -0,0 +1,25 @@
const complianceDetailsStyles = {
complianceDetailsGrid: {
display: 'flex',
flexDirection: 'row'
},
firstColumn: {
display: 'flex',
flexDirection: 'column',
width: '100%',
marginRight: 10
},
lastColumn: {
display: 'flex',
flexDirection: 'column',
width: '100%',
marginLeft: 10
},
photoWrapper: ({ width }) => ({
display: 'flex',
justifyContent: 'center',
width
})
}
export { complianceDetailsStyles }

View file

@ -1,74 +1,44 @@
import { makeStyles, Box } from '@material-ui/core'
import moment from 'moment'
import { parsePhoneNumberFromString } from 'libphonenumber-js'
import * as R from 'ramda'
import React, { memo } from 'react'
import { SubpageButton } from 'src/components/buttons'
import { H2, Label1, P } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { comet } from 'src/styling/variables'
import { ifNotNull } from 'src/utils/nullCheck'
import { ReactComponent as IdIcon } from 'src/styling/icons/ID/card/zodiac.svg'
import { ReactComponent as LawIconInverse } from 'src/styling/icons/circle buttons/law/white.svg'
import { ReactComponent as LawIcon } from 'src/styling/icons/circle buttons/law/zodiac.svg'
import mainStyles from '../CustomersList.styles'
import FrontCameraPhoto from './FrontCameraPhoto'
const styles = {
icon: {
marginRight: 11
},
name: {
marginTop: 6
},
value: {
height: 16
},
label: {
marginBottom: 4,
color: comet
}
}
const useStyles = makeStyles(mainStyles)
const useStyles = makeStyles(styles)
const CustomerDetails = memo(({ customer }) => {
const CustomerDetails = memo(({ customer, locale, setShowCompliance }) => {
const classes = useStyles()
const LastTxIcon = customer.lastTxClass === 'cashOut' ? TxOutIcon : TxInIcon
const elements = [
{
header: 'Transactions',
header: 'Phone number',
size: 172,
value:
customer.phone && locale.country
? parsePhoneNumberFromString(
customer.phone,
locale.country
).formatInternational()
: ''
},
{
header: 'ID number',
size: 172,
value: R.path(['idCardData', 'documentNumber'])(customer) ?? ''
},
{
header: 'US SSN',
size: 127,
value: ifNotNull(
customer.totalTxs,
`${Number.parseInt(customer.totalTxs)}`
)
},
{
header: 'Transaction volume',
size: 167,
value: ifNotNull(
customer.totalSpent,
`${Number.parseFloat(customer.totalSpent)} ${customer.lastTxFiatCode}`
)
},
{
header: 'Last active',
size: 142,
value: ifNotNull(
customer.lastActive,
moment.utc(customer.lastActive).format('YYYY-MM-D')
)
},
{
header: 'Last transaction',
size: 198,
value: ifNotNull(
customer.lastTxFiat,
<>
<LastTxIcon className={classes.icon} />
{`${Number.parseFloat(customer.lastTxFiat)}
${customer.lastTxFiatCode}`}
</>
)
value: R.path(['usSsn'])(customer) ?? ''
}
]
@ -79,9 +49,17 @@ const CustomerDetails = memo(({ customer }) => {
/>
<Box display="flex" flexDirection="column">
<div className={classes.name}>
<IdIcon className={classes.idIcon} />
<H2 noMargin>
{R.path(['name'])(customer) ?? R.path(['phone'])(customer)}
</H2>
<SubpageButton
className={classes.subpageButton}
Icon={LawIcon}
InverseIcon={LawIconInverse}
toggle={setShowCompliance}>
Compliance details
</SubpageButton>
</div>
<Box display="flex" mt="auto">
{elements.map(({ size, header }, idx) => (

View file

@ -7,7 +7,6 @@ import { comet } from 'src/styling/variables'
const useStyles = makeStyles({
field: {
width: 144,
height: 46
},
label: {
@ -23,11 +22,13 @@ const useStyles = makeStyles({
}
})
const Field = memo(({ label, display, className }) => {
const Field = memo(({ label, display, size, className }) => {
const classes = useStyles()
return (
<div className={classnames(classes.field, className)}>
<div
className={classnames(classes.field, className)}
style={{ width: size }}>
<Label1 className={classes.label}>{label}</Label1>
<Info3 className={classes.value}>{display}</Info3>
</div>

View file

@ -32,8 +32,8 @@ const IdCardPhotoCard = memo(({ customerData, updateCustomer }) => {
return (
<PropertyCard
className={classes.idCardPhotoCard}
title={'ID card photo'}
// className={classes.idCardPhotoCard}
title={'ID photo'}
state={R.path(['idCardPhotoOverride'])(customerData)}
authorize={() =>
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED })

View file

@ -1,4 +1,4 @@
import { makeStyles, Box } from '@material-ui/core'
import { Box } from '@material-ui/core'
import moment from 'moment'
import * as R from 'ramda'
import React, { memo } from 'react'
@ -12,65 +12,64 @@ import { ifNotNull } from 'src/utils/nullCheck'
import Field from './Field'
const useStyles = makeStyles({
idDataCard: {
width: 550,
height: 240
},
column: {
marginBottom: 7
}
})
const IdDataCard = memo(({ customerData, updateCustomer }) => {
const classes = useStyles()
const idData = R.path(['idCardData'])(customerData)
const name = R.path(['firstName'])(idData) ?? ''
const lastName = R.path(['lastName'])(idData) ?? ''
const gender = R.path(['gender'])(idData)
const idNumber = R.path(['documentNumber'])(idData)
const country = R.path(['country'])(idData)
const rawExpirationDate = R.path(['expirationDate'])(idData)
const expirationDate = ifNotNull(
rawExpirationDate,
moment.utc(rawExpirationDate).format('YYYY-MM-D')
)
const rawDob = R.path(['dateOfBirth'])(idData)
const age = ifNotNull(
rawDob,
moment.utc().diff(moment.utc(rawDob).format('YYYY-MM-D'), 'years')
)
const elements = [
{
header: 'Name',
display: `${R.path(['firstName'])(idData)} ${R.path(['lastName'])(
idData
)}`,
size: 160
},
{
header: 'ID number',
display: R.path(['documentNumber'])(idData),
size: 190
},
{
header: 'Age',
display: ifNotNull(
rawDob,
moment.utc().diff(moment.utc(rawDob).format('YYYY-MM-D'), 'years')
),
size: 70
},
{
header: 'Gender',
display: R.path(['gender'])(idData),
size: 100
},
{
header: 'Country',
display: R.path(['country'])(idData),
size: 140
},
{
header: 'Expiration Date',
display: ifNotNull(
rawExpirationDate,
moment.utc(rawExpirationDate).format('YYYY-MM-D')
)
}
]
return (
<PropertyCard
className={classes.idDataCard}
title={'ID data'}
state={R.path(['idCardDataOverride'])(customerData)}
authorize={() =>
updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED })
}
reject={() => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED })}>
<div>
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
mb={1}>
<Field label={'Name'} display={`${name} ${lastName}`} />
<Field label={'ID number'} display={idNumber} />
<Field label={'Age'} display={age} />
</Box>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Field label={'Gender'} display={gender} />
<Field label={'Country'} display={country} />
<Field label={'Expiration date'} display={expirationDate} />
</Box>
</div>
<Box display="flex" alignItems="center">
{elements.map(({ header, display, size }, idx) => (
<Field key={idx} label={header} display={display} size={size} />
))}
</Box>
</PropertyCard>
)
})

View file

@ -1,4 +1,3 @@
import { makeStyles } from '@material-ui/core/styles'
import { parsePhoneNumberFromString } from 'libphonenumber-js'
import * as R from 'ramda'
import React, { memo } from 'react'
@ -11,35 +10,27 @@ import {
import Field from './Field'
const useStyles = makeStyles({
phoneCard: {
width: 300,
height: 240
}
})
const PhoneCard = memo(({ customerData, updateCustomer }) => {
const classes = useStyles()
return (
const PhoneCard = memo(
({ className, customerData, updateCustomer, locale }) => (
<PropertyCard
className={classes.phoneCard}
title={'Phone'}
className={className}
title={'Phone nº'}
state={R.path(['smsOverride'])(customerData)}
authorize={() => updateCustomer({ smsOverride: OVERRIDE_AUTHORIZED })}
reject={() => updateCustomer({ smsOverride: OVERRIDE_REJECTED })}>
<Field
label={'Phone'}
display={
R.path(['phone'])(customerData)
customerData.phone && locale.country
? parsePhoneNumberFromString(
R.path(['phone'])(customerData)
customerData.phone,
locale.country
).formatInternational()
: []
: ''
}
/>
</PropertyCard>
)
})
)
export default PhoneCard

View file

@ -1,25 +1,66 @@
import { makeStyles } from '@material-ui/core/styles'
import { makeStyles, Box } from '@material-ui/core'
import BigNumber from 'bignumber.js'
import moment from 'moment'
import * as R from 'ramda'
import React from 'react'
import DataTable from 'src/components/tables/DataTable'
import { H4, Label2 } from 'src/components/typography'
import { H3, H4, Label1, Label2, P } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { toUnit } from 'src/utils/coin'
import { ifNotNull } from 'src/utils/nullCheck'
import CopyToClipboard from '../../Transactions/CopyToClipboard'
import mainStyles from '../CustomersList.styles'
const useStyles = makeStyles(mainStyles)
const TransactionsList = ({ data, loading }) => {
const TransactionsList = ({ customer, data, loading }) => {
const classes = useStyles()
const LastTxIcon = customer.lastTxClass === 'cashOut' ? TxOutIcon : TxInIcon
const hasData = !(R.isEmpty(data) || R.isNil(data))
const elements = [
const summaryElements = [
{
header: 'Transactions',
size: 127,
value: ifNotNull(
customer.totalTxs,
`${Number.parseInt(customer.totalTxs)}`
)
},
{
header: 'Transaction volume',
size: 167,
value: ifNotNull(
customer.totalSpent,
`${Number.parseFloat(customer.totalSpent)} ${customer.lastTxFiatCode}`
)
},
{
header: 'Last active',
size: 142,
value: ifNotNull(
customer.lastActive,
moment.utc(customer.lastActive).format('YYYY-MM-D')
)
},
{
header: 'Last transaction',
size: 198,
value: ifNotNull(
customer.lastTxFiat,
<>
<LastTxIcon className={classes.icon} />
{`${Number.parseFloat(customer.lastTxFiat)}
${customer.lastTxFiatCode}`}
</>
)
}
]
const tableElements = [
{
header: 'Direction',
width: 207,
@ -79,6 +120,31 @@ const TransactionsList = ({ data, loading }) => {
return (
<>
<H3>Transactions</H3>
<Box display="flex" flexDirection="column">
<Box display="flex" mt="auto">
{summaryElements.map(({ size, header }, idx) => (
<Label1
noMargin
key={idx}
className={classes.label}
style={{ width: size }}>
{header}
</Label1>
))}
</Box>
<Box display="flex">
{summaryElements.map(({ size, value }, idx) => (
<P
noMargin
key={idx}
className={classes.value}
style={{ width: size }}>
{value}
</P>
))}
</Box>
</Box>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<H4>
@ -90,7 +156,7 @@ const TransactionsList = ({ data, loading }) => {
</H4>
</div>
</div>
{hasData && <DataTable elements={elements} data={data} />}
{hasData && <DataTable elements={tableElements} data={data} />}
</>
)
}

View file

@ -1,13 +1,6 @@
import ComplianceDetails from './ComplianceDetails'
import CustomerDetails from './CustomerDetails'
import IdCardPhotoCard from './IdCardPhotoCard'
import IdDataCard from './IdDataCard'
import PhoneCard from './PhoneCard'
import TransactionsList from './TransactionsList'
export {
CustomerDetails,
IdDataCard,
IdCardPhotoCard,
PhoneCard,
TransactionsList
}
export { CustomerDetails, IdDataCard, TransactionsList, ComplianceDetails }

View file

@ -3,6 +3,7 @@ import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import React, { memo } from 'react'
import { MainStatus } from 'src/components/Status'
import { ActionButton } from 'src/components/buttons'
import { H3 } from 'src/components/typography'
import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg'
@ -22,13 +23,6 @@ const PropertyCard = memo(
({ className, title, state, authorize, reject, children }) => {
const classes = useStyles()
const propertyCardClassNames = {
[classes.propertyCard]: true,
[classes.propertyCardPending]: state === OVERRIDE_PENDING,
[classes.propertyCardRejected]: state === OVERRIDE_REJECTED,
[classes.propertyCardAccepted]: state === OVERRIDE_AUTHORIZED
}
const label1ClassNames = {
[classes.label1]: true,
[classes.label1Pending]: state === OVERRIDE_PENDING,
@ -58,29 +52,27 @@ const PropertyCard = memo(
</ActionButton>
)
const authorizedAsString =
const authorized =
state === OVERRIDE_PENDING
? 'Pending'
? { label: 'Pending', type: 'neutral' }
: state === OVERRIDE_REJECTED
? 'Rejected'
: 'Accepted'
? { label: 'Rejected', type: 'error' }
: { label: 'Accepted', type: 'success' }
return (
<Paper
className={classnames(propertyCardClassNames, className)}
className={classnames(classes.propertyCard, className)}
elevation={0}>
<div className={classes.rowSpaceBetween}>
<H3>{title}</H3>
<H3 className={classes.propertyCardTopRow}>{title}</H3>
<div className={classes.propertyCardBottomRow}>
<div className={classnames(label1ClassNames)}>
{authorizedAsString}
<MainStatus statuses={[authorized]} />
</div>
</div>
<Paper className={classes.cardProperties} elevation={0}>
{children}
</Paper>
<div className={classes.buttonsWrapper}>
{state !== OVERRIDE_AUTHORIZED && AuthorizeButton()}
{state !== OVERRIDE_REJECTED && RejectButton()}
<div className={classes.buttonsWrapper}>
{authorize && state !== OVERRIDE_AUTHORIZED && AuthorizeButton()}
{reject && state !== OVERRIDE_REJECTED && RejectButton()}
</div>
</div>
</Paper>
)

View file

@ -1,39 +1,11 @@
import {
white,
zircon,
mistyRose,
tomato,
spring3,
spring4,
comet,
fontSize5
} from 'src/styling/variables'
import { white, tomato, spring4, comet } from 'src/styling/variables'
const propertyCardStyles = {
propertyCard: {
margin: [[32, 12, 0, 0]],
padding: [[0, 16]],
borderRadius: 8
},
propertyCardPending: {
backgroundColor: zircon
},
propertyCardRejected: {
backgroundColor: mistyRose
},
propertyCardAccepted: {
backgroundColor: spring3
},
label1: {
fontFamily: 'MuseoSans',
fontSize: fontSize5,
fontWeight: 500,
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 1.33,
letterSpacing: 'normal',
color: comet,
margin: [[4, 0]]
display: 'flex',
marginBottom: 2,
marginTop: 'auto',
width: 85
},
label1Pending: {
color: comet
@ -45,19 +17,32 @@ const propertyCardStyles = {
color: spring4
},
cardActionButton: {
display: 'flex',
height: 28,
marginRight: 'auto',
marginLeft: 12
},
cardProperties: {
propertyCardTopRow: {
display: 'flex',
margin: [[0, 10, 5, 0]]
},
propertyCardBottomRow: {
display: 'flex',
flexDirection: 'row',
height: 45
},
propertyCard: {
display: 'flex',
flexDirection: 'column',
borderRadius: 8,
width: '100%',
height: 'calc(100% - 104px)',
height: 100,
padding: [[20]],
boxSizing: 'border-box',
boxShadow: '0 0 8px 0 rgba(0, 0, 0, 0.04)',
border: 'solid 0',
backgroundColor: white
backgroundColor: white,
margin: [[20, 0, 0, 0]]
},
rowSpaceBetween: {
display: 'flex',
@ -65,11 +50,19 @@ const propertyCardStyles = {
alignItems: 'center',
justifyContent: 'space-between'
},
columnSpaceBetween: {
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'center',
justifyContent: 'space-between',
width: 90
},
buttonsWrapper: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 16,
marginBottom: 16
marginLeft: 'auto',
marginTop: 'auto'
}
}

View file

@ -3,12 +3,12 @@
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<desc>Created with Sketch.</desc>
<defs>
<path d="M11.10567,8.99966734 L17.5616855,15.4562245 C18.1453995,16.0371533 18.1457502,16.9810221 17.5632744,17.5635465 C17.2826878,17.8441564 16.9043531,17.9996031 16.509308,17.9996031 C16.1150437,17.9996031 15.7367759,17.8439272 15.4563692,17.5634971 L8.99995005,11.1055617 L2.54567824,17.5603569 C2.26578723,17.8430155 1.88614994,17.9996031 1.48961505,17.9996031 C1.09473832,17.9996031 0.717380733,17.844225 0.436725633,17.5635465 C-0.145575211,16.9811971 -0.145575211,16.0373273 0.436725633,15.4578096 L6.89433001,8.99966707 L0.438314479,2.54310994 C-0.145399464,1.96218116 -0.145750215,1.01831232 0.436725633,0.435787934 C1.01746304,-0.144997872 1.95893893,-0.144997872 2.54250446,0.435787934 L8.99995074,6.89377234 L15.4580075,0.434202817 C16.0398949,-0.144908147 16.9801496,-0.144559181 17.5632744,0.435787934 C18.1455752,1.0181373 18.1455752,1.96200713 17.5632744,2.54152483 L11.10567,8.99966734 Z" id="path-1"></path>
<path d="M11.10567,8.99966734 L17.5616855,15.4562245 C18.1453995,16.0371533 18.1457502,16.9810221 17.5632744,17.5635465 C17.2826878,17.8441564 16.9043531,17.9996031 16.509308,17.9996031 C16.1150437,17.9996031 15.7367759,17.8439272 15.4563692,17.5634971 L8.99995005,11.1055617 L2.54567824,17.5603569 C2.26578723,17.8430155 1.88614994,17.9996031 1.48961505,17.9996031 C1.09473832,17.9996031 0.717380733,17.844225 0.436725633,17.5635465 C-0.145575211,16.9811971 -0.145575211,16.0373273 0.436725633,15.4578096 L6.89433001,8.99966707 L0.438314479,2.54310994 C-0.145399464,1.96218116 -0.145750215,1.01831232 0.436725633,0.435787934 C1.01746304,-0.144997872 1.95893893,-0.144997872 2.54250446,0.435787934 L8.99995074,6.89377234 L15.4580075,0.434202817 C16.0398949,-0.144908147 16.9801496,-0.144559181 17.5632744,0.435787934 C18.1455752,1.0181373 18.1455752,1.96200713 17.5632744,2.54152483 L11.10567,8.99966734 Z" id="close_path-1"></path>
</defs>
<g id="icon/action/close/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
<mask id="close_mask-2" fill="white">
<use xlink:href="#close_path-1"></use>
</mask>
<use id="Mask" fill="#FFFFFF" xlink:href="#path-1"></use>
<use id="close_Mask" fill="#FFFFFF" xlink:href="#close_path-1"></use>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/sf-small/law/white</title>
<g id="icon/sf-small/law/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="bevel">
<line x1="8.4141" y1="7.4648" x2="14.0711" y2="1.8078" id="Stroke-1" stroke="#FFFFFF" stroke-width="2"></line>
<line x1="12.6568" y1="0.3936" x2="15.4858" y2="3.2216" id="Stroke-3" stroke="#FFFFFF" stroke-width="2"></line>
<line x1="7" y1="6.0498" x2="9.829" y2="8.8788" id="Stroke-6" stroke="#FFFFFF" stroke-width="2"></line>
<line x1="11.2427" y1="4.6357" x2="19.2427" y2="12.6357" id="Stroke-7" stroke="#FFFFFF" stroke-width="2"></line>
<line x1="3" y1="16.5" x2="10" y2="16.5" id="Stroke-9" stroke="#FFFFFF" stroke-width="2"></line>
<line x1="0" y1="19.5" x2="13" y2="19.5" id="Stroke-10" stroke="#FFFFFF" stroke-width="2"></line>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/sf-small/law/zodiac</title>
<g id="icon/sf-small/law/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="bevel">
<line x1="8.4141" y1="7.4648" x2="14.0711" y2="1.8078" id="Stroke-1" stroke="#1B2559" stroke-width="2"></line>
<line x1="12.6568" y1="0.3936" x2="15.4858" y2="3.2216" id="Stroke-3" stroke="#1B2559" stroke-width="2"></line>
<line x1="7" y1="6.0498" x2="9.829" y2="8.8788" id="Stroke-6" stroke="#1B2559" stroke-width="2"></line>
<line x1="11.2427" y1="4.6357" x2="19.2427" y2="12.6357" id="Stroke-7" stroke="#1B2559" stroke-width="2"></line>
<line x1="3" y1="16.5" x2="10" y2="16.5" id="Stroke-9" stroke="#1B2559" stroke-width="2"></line>
<line x1="0" y1="19.5" x2="13" y2="19.5" id="Stroke-10" stroke="#1B2559" stroke-width="2"></line>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

10
package-lock.json generated
View file

@ -4985,7 +4985,7 @@
"got": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz",
"integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==",
"integrity": "sha1-BUUP2ECU5rvqVvRRpDqcKJFmOFo=",
"requires": {
"decompress-response": "^3.2.0",
"duplexer3": "^0.1.4",
@ -5801,7 +5801,7 @@
"isurl": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz",
"integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==",
"integrity": "sha1-sn9PSfPNqj6kSgpbfzRi5u3DnWc=",
"requires": {
"has-to-string-tag-x": "^1.2.0",
"is-object": "^1.0.1"
@ -6571,7 +6571,7 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"requires": {
"brace-expansion": "^1.1.7"
}
@ -6915,7 +6915,7 @@
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
@ -7216,7 +7216,7 @@
"p-cancelable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
"integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw=="
"integrity": "sha1-ueEjgAvOu3rBOkeb4ZW1B7mNMPo="
},
"p-defer": {
"version": "1.0.0",