Merge branch 'dev' into refact/lam-108/dev/rbf-transactions

This commit is contained in:
André Sá 2021-12-22 11:19:55 +00:00
commit 83de7a750d
45 changed files with 682 additions and 403 deletions

View file

@ -45,6 +45,7 @@ keypool=10000
prune=4000
daemon=0
addresstype=p2sh-segwit
changetype=bech32
walletrbf=1
bind=0.0.0.0:8332
rpcport=8333`

View file

@ -44,5 +44,6 @@ connections=40
keypool=10000
prune=4000
daemon=0
addresstype=p2sh-segwit`
addresstype=p2sh-segwit
changetype=bech32`
}

View file

@ -683,18 +683,18 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
*/
function getCustomerById (id) {
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_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,
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,
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, created 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
FROM (
SELECT c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
c.suspended_until > now() AS is_suspended,
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.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.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, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes,
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,

View file

@ -56,7 +56,7 @@ function batch (
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired
FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
INNER JOIN devices d ON txs.device_id = d.device_id
LEFT JOIN devices d ON txs.device_id = d.device_id
WHERE txs.created >= $2 AND txs.created <= $3 ${
id !== null ? `AND txs.device_id = $6` : ``
}
@ -87,7 +87,7 @@ function batch (
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress'
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
INNER JOIN devices d ON txs.device_id = d.device_id
LEFT JOIN devices d ON txs.device_id = d.device_id
WHERE txs.created >= $2 AND txs.created <= $3 ${
id !== null ? `AND txs.device_id = $6` : ``
}
@ -130,7 +130,10 @@ function simplifiedBatch (data) {
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode).toString()
const getProfit = it => {
const getCommissionFee = it => BN(it.commissionPercentage).times(BN(it.fiat))
const discountValue = _.isNil(it.discount) ? BN(100) : BN(100).minus(it.discount)
const discountPercentage = BN(discountValue).div(100)
const commissionPercentage = BN(it.commissionPercentage).times(discountPercentage)
const getCommissionFee = it => BN(commissionPercentage).times(BN(it.fiat))
if (!it.cashInFee) return getCommissionFee(it)
return getCommissionFee(it).plus(BN(it.cashInFee))
}

View file

@ -6889,11 +6889,14 @@
}
},
"apollo-upload-client": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-16.0.0.tgz",
"integrity": "sha512-aLhYucyA0T8aBEQ5g+p13qnR9RUyL8xqb8FSZ7e/Kw2KUOsotLUlFluLobqaE7JSUFwc6sKfXIcwB7y4yEjbZg==",
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz",
"integrity": "sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA==",
"requires": {
"extract-files": "^11.0.0"
"@babel/runtime": "^7.9.2",
"apollo-link": "^1.2.12",
"apollo-link-http-common": "^0.2.14",
"extract-files": "^8.0.0"
}
},
"apollo-utilities": {
@ -12617,9 +12620,9 @@
}
},
"extract-files": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz",
"integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ=="
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz",
"integrity": "sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ=="
},
"extsprintf": {
"version": "1.3.0",
@ -27096,6 +27099,11 @@
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"ua-parser-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz",
"integrity": "sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg=="
},
"unfetch": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",

View file

@ -14,7 +14,7 @@
"apollo-link": "^1.2.14",
"apollo-link-error": "^1.1.13",
"apollo-link-http": "^1.5.17",
"apollo-upload-client": "^16.0.0",
"apollo-upload-client": "^13.0.0",
"axios": "0.21.1",
"base-64": "^1.0.0",
"bignumber.js": "9.0.0",
@ -47,6 +47,7 @@
"react-use": "15.3.2",
"react-virtualized": "^9.21.2",
"sanctuary": "^2.0.1",
"ua-parser-js": "^1.0.2",
"uuid": "^7.0.2",
"yup": "0.32.9"
},

View file

@ -79,6 +79,7 @@ const Tr = ({
onClick,
error,
errorMessage,
shouldShowError,
children,
className,
size,
@ -99,7 +100,9 @@ const Tr = ({
<Card className={classnames(classNames, className)} onClick={onClick}>
<CardContent classes={cardClasses}>
<div className={classes.mainContent}>{children}</div>
{error && <div className={classes.errorContent}>{errorMessage}</div>}
{error && shouldShowError && (
<div className={classes.errorContent}>{errorMessage}</div>
)}
</CardContent>
</Card>
</>

View file

@ -9,10 +9,12 @@ const styles = {
borderRadius: '4px'
},
focus: {
color: primaryColor,
border: '2px solid',
borderColor: primaryColor,
borderRadius: '4px'
borderRadius: '4px',
'&:focus': {
outline: 'none'
}
},
error: {
borderColor: errorColor

View file

@ -28,6 +28,7 @@ const useStyles = makeStyles(styles)
const Row = ({
id,
index,
elements,
data,
width,
@ -48,9 +49,11 @@ const Row = ({
[classes.row]: true,
[classes.expanded]: expanded
}
return (
<div className={classes.rowWrapper}>
<div className={classnames({ [classes.before]: expanded && id !== 0 })}>
<div
className={classnames({ [classes.before]: expanded && index !== 0 })}>
<Tr
size={size}
className={classnames(trClasses)}
@ -58,8 +61,9 @@ const Row = ({
expandable && expandRow(id, data)
onClick && onClick(data)
}}
error={data.error}
errorMessage={data.errorMessage}>
error={data.error || data.hasError}
shouldShowError={false}
errorMessage={data.errorMessage || data.hasError}>
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
<Td key={idx} {...props}>
{view(data)}
@ -142,6 +146,7 @@ const DataTable = ({
width={width}
size={rowSize}
id={data[index].id ? data[index].id : index}
index={index}
expWidth={expWidth}
elements={elements}
data={data[index]}

View file

@ -129,7 +129,7 @@ export default {
confirmationCode: {
extend: base,
fontSize: codeInputFontSize,
fontFamily: fontPrimary,
fontFamily: fontSecondary,
fontWeight: 900
},
inline: {

View file

@ -114,7 +114,7 @@ const Accounting = () => {
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse)
const loading = operatorLoading && configLoading
const loading = operatorLoading || configLoading
const operatorData = R.path(['operatorByUsername'], opData)
@ -170,26 +170,22 @@ const Accounting = () => {
]
return (
!loading && (
<>
<TitleSection title="Accounting" />
<Assets
balance={
operatorData.fiatBalances[operatorData.preferredFiatCurrency]
}
hedgingReserve={operatorData.hedgingReserve ?? 0}
currency={operatorData.preferredFiatCurrency}
/>
<H4 className={classes.tableTitle}>Fiat balance history</H4>
<DataTable
loading={false}
emptyText="No transactions so far"
elements={elements}
data={operatorData.fundings ?? []}
rowSize="sm"
/>
</>
)
<>
<TitleSection title="Accounting" />
<Assets
balance={operatorData.fiatBalances[operatorData.preferredFiatCurrency]}
hedgingReserve={operatorData.hedgingReserve ?? 0}
currency={operatorData.preferredFiatCurrency}
/>
<H4 className={classes.tableTitle}>Fiat balance history</H4>
<DataTable
loading={loading}
emptyText="No transactions so far"
elements={elements}
data={operatorData.fundings ?? []}
rowSize="sm"
/>
</>
)
}

View file

@ -14,6 +14,7 @@ import { Button } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/formik'
import Sidebar from 'src/components/layout/Sidebar'
import { Info2, P } from 'src/components/typography'
import { ReactComponent as CameraIcon } from 'src/styling/icons/ID/photo/zodiac.svg'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg'
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
@ -70,8 +71,18 @@ const QrCodeComponent = ({ classes, qrCode, name, count, onPaired }) => {
Scan QR code with your new cryptomat
</Info2>
<div className={classes.qrCodeWrapper}>
<div>
<QRCode size={240} fgColor={primaryColor} value={qrCode} />
<div className={classes.qrCodeImageWrapper}>
<QRCode
size={280}
fgColor={primaryColor}
includeMargin
value={qrCode}
className={classes.qrCodeBorder}
/>
<div className={classes.qrCodeScanMessage}>
<CameraIcon />
<P noMargin>Snap a picture and scan</P>
</div>
</div>
<div className={classes.qrTextWrapper}>
<div className={classes.qrTextInfoWrapper}>

View file

@ -126,6 +126,23 @@ const styles = {
},
errorMessage: {
color: errorColor
},
qrCodeImageWrapper: {
display: 'flex',
flexDirection: 'column',
backgroundColor: 'white',
border: `5px solid ${primaryColor}`,
padding: 5,
borderRadius: 15
},
qrCodeScanMessage: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
margin: [[0, 0, 20, 20]],
'& > p': {
marginLeft: 10
}
}
}

View file

@ -1,6 +1,7 @@
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import base64 from 'base-64'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useContext, useState } from 'react'
import { useHistory } from 'react-router-dom'
@ -120,14 +121,20 @@ const Input2FAState = ({ state, dispatch }) => {
<TL1 className={classes.info}>
Enter your two-factor authentication code
</TL1>
<CodeInput
name="2fa"
value={state.twoFAField}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput
name="2fa"
value={state.twoFAField}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
<button onClick={handleSubmit} className={classes.enterButton} />
</Form>
</Formik>
<div className={classes.twofaFooter}>
{errorMessage && <P className={classes.errorMessage}>{errorMessage}</P>}
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>

View file

@ -1,6 +1,7 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Grid } from '@material-ui/core'
import Paper from '@material-ui/core/Paper'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag'
import QRCode from 'qrcode.react'
import React, { useReducer, useState } from 'react'
@ -101,6 +102,20 @@ const Reset2FA = () => {
return null
}
const handleSubmit = () => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
reset2FA({
variables: {
token: token,
userID: state.userID,
code: twoFAConfirmation
}
})
}
return (
<Grid
container
@ -152,33 +167,30 @@ const Reset2FA = () => {
</ActionButton>
</div>
<div className={classes.confirm2FAInput}>
<CodeInput
name="2fa"
value={twoFAConfirmation}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput
name="2fa"
value={twoFAConfirmation}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
<button
onClick={handleSubmit}
className={classes.enterButton}
/>
</Form>
</Formik>
</div>
<div className={classes.twofaFooter}>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<Button
onClick={() => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
reset2FA({
variables: {
token: token,
userID: state.userID,
code: twoFAConfirmation
}
})
}}
onClick={handleSubmit}
buttonClassName={classes.loginButton}>
Done
</Button>

View file

@ -1,6 +1,7 @@
import { useMutation, useQuery, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import base64 from 'base-64'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag'
import QRCode from 'qrcode.react'
import React, { useContext, useState } from 'react'
@ -125,6 +126,14 @@ const Setup2FAState = ({ state, dispatch }) => {
return null
}
const handleSubmit = () => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
setup2FA(mutationOptions)
}
return (
secret &&
otpauth && (
@ -159,28 +168,26 @@ const Setup2FAState = ({ state, dispatch }) => {
</ActionButton>
</div>
<div className={classes.confirm2FAInput}>
<CodeInput
name="2fa"
value={twoFAConfirmation}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput
name="2fa"
value={twoFAConfirmation}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
<button onClick={handleSubmit} className={classes.enterButton} />
</Form>
</Formik>
</div>
<div className={classes.twofaFooter}>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<Button
onClick={() => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
setup2FA(mutationOptions)
}}
buttonClassName={classes.loginButton}>
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
Done
</Button>
</div>

View file

@ -100,6 +100,9 @@ const styles = {
},
error: {
color: errorColor
},
enterButton: {
display: 'none'
}
}

View file

@ -0,0 +1,81 @@
import { makeStyles, Paper } from '@material-ui/core'
import { format } from 'date-fns/fp'
import * as R from 'ramda'
import { React, useState } from 'react'
import { InformativeDialog } from 'src/components/InformativeDialog'
import { Label2, H3 } from 'src/components/typography'
import { ReactComponent as CameraIcon } from 'src/styling/icons/ID/photo/comet.svg'
import { URI } from 'src/utils/apollo'
import styles from './CustomerPhotos.styles'
import PhotosCarousel from './components/PhotosCarousel'
const useStyles = makeStyles(styles)
const CustomerPhotos = ({ photosData }) => {
const classes = useStyles()
const [photosDialog, setPhotosDialog] = useState(false)
const [photoClickedIndex, setPhotoClickIndex] = useState(null)
const orderedPhotosData = !R.isNil(photoClickedIndex)
? R.compose(R.flatten, R.reverse, R.splitAt(photoClickedIndex))(photosData)
: photosData
return (
<div>
<div className={classes.header}>
<H3 className={classes.title}>{'Photos & files'}</H3>
</div>
<div className={classes.photosChipList}>
{photosData.map((elem, idx) => (
<PhotoCard
idx={idx}
date={elem.date}
src={`${URI}/${elem.photoDir}/${elem.path}`}
setPhotosDialog={setPhotosDialog}
setPhotoClickIndex={setPhotoClickIndex}
/>
))}
</div>
<InformativeDialog
open={photosDialog}
title={`Photo roll`}
data={<PhotosCarousel photosData={orderedPhotosData} />}
onDissmised={() => {
setPhotosDialog(false)
setPhotoClickIndex(null)
}}
/>
</div>
)
}
export const PhotoCard = ({
idx,
date,
src,
setPhotosDialog,
setPhotoClickIndex
}) => {
const classes = useStyles()
return (
<Paper
className={classes.photoCardChip}
onClick={() => {
setPhotoClickIndex(idx)
setPhotosDialog(true)
}}>
<img className={classes.image} src={src} alt="" />
<div className={classes.footer}>
<CameraIcon />
<Label2 className={classes.date}>
{format('yyyy-MM-dd', new Date(date))}
</Label2>
</div>
</Paper>
)
}
export default CustomerPhotos

View file

@ -0,0 +1,37 @@
const styles = {
header: {
display: 'flex',
flexDirection: 'row'
},
title: {
marginTop: 7,
marginRight: 24,
marginBottom: 32
},
photosChipList: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap'
},
image: {
objectFit: 'cover',
objectPosition: 'center',
width: 224,
height: 200,
borderTopLeftRadius: 4,
borderTopRightRadius: 4
},
photoCardChip: {
margin: [[0, 16, 0, 0]]
},
footer: {
display: 'flex',
flexDirection: 'row',
margin: [[8, 0, 0, 8]]
},
date: {
margin: [[0, 0, 8, 12]]
}
}
export default styles

View file

@ -24,6 +24,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
import CustomerData from './CustomerData'
import CustomerNotes from './CustomerNotes'
import CustomerPhotos from './CustomerPhotos'
import styles from './CustomerProfile.styles'
import {
CustomerDetails,
@ -31,7 +32,7 @@ import {
CustomerSidebar,
Wizard
} from './components'
import { getFormattedPhone, getName } from './helper'
import { getFormattedPhone, getName, formatPhotosData } from './helper'
const useStyles = makeStyles(styles)
@ -367,12 +368,24 @@ const CustomerProfile = memo(() => {
const isCustomerData = clickedItem === 'customerData'
const isOverview = clickedItem === 'overview'
const isNotes = clickedItem === 'notes'
const isPhotos = clickedItem === 'photos'
const loading = customerLoading && configLoading
const frontCameraData = R.pick(['frontCameraPath', 'frontCameraAt'])(
customerData
)
const txPhotosData =
sortedTransactions &&
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
sortedTransactions
)
const photosData = formatPhotosData(R.append(frontCameraData, txPhotosData))
const loading = customerLoading || configLoading
const timezone = R.path(['config', 'locale_timezone'], configResponse)
const classes = useStyles({ blocked })
const classes = useStyles()
return (
<>
@ -406,29 +419,26 @@ const CustomerProfile = memo(() => {
/>
</div>
<Label1 className={classes.actionLabel}>Actions</Label1>
<div>
<div className={classes.actionBar}>
<ActionButton
className={classes.customerManualDataEntry}
className={classes.actionButton}
color="primary"
Icon={DataIcon}
InverseIcon={DataReversedIcon}
onClick={() => setWizard(true)}>
{`Manual data entry`}
</ActionButton>
</div>
<div>
<ActionButton
className={classes.customerDiscount}
className={classes.actionButton}
color="primary"
Icon={Discount}
InverseIcon={DiscountReversedIcon}
onClick={() => {}}>
{`Add individual discount`}
</ActionButton>
</div>
<div>
{isSuspended && (
<ActionButton
className={classes.actionButton}
color="primary"
Icon={AuthorizeIcon}
InverseIcon={AuthorizeReversedIcon}
@ -442,7 +452,7 @@ const CustomerProfile = memo(() => {
)}
<ActionButton
color="primary"
className={classes.customerBlock}
className={classes.actionButton}
Icon={blocked ? AuthorizeIcon : BlockIcon}
InverseIcon={
blocked ? AuthorizeReversedIcon : BlockReversedIcon
@ -458,7 +468,7 @@ const CustomerProfile = memo(() => {
</ActionButton>
<ActionButton
color="primary"
className={classes.retrieveInformation}
className={classes.actionButton}
Icon={blocked ? AuthorizeIcon : BlockIcon}
InverseIcon={
blocked ? AuthorizeReversedIcon : BlockReversedIcon
@ -488,6 +498,7 @@ const CustomerProfile = memo(() => {
justifyContent="space-between">
<CustomerDetails
customer={customerData}
photosData={photosData}
locale={locale}
setShowCompliance={() => setShowCompliance(!showCompliance)}
/>
@ -524,6 +535,11 @@ const CustomerProfile = memo(() => {
timezone={timezone}></CustomerNotes>
</div>
)}
{isPhotos && (
<div>
<CustomerPhotos photosData={photosData} />
</div>
)}
</div>
{wizard && (
<Wizard

View file

@ -15,29 +15,16 @@ export default {
customerDetails: {
marginBottom: 18
},
customerBlock: props => ({
actionButton: {
margin: [[0, 0, 4, 0]],
display: 'flex',
flexDirection: 'row',
margin: [[0, 0, 4, 0]],
padding: [[0, props.blocked ? 35 : 48, 0]]
}),
customerDiscount: {
display: 'flex',
flexDirection: 'row',
margin: [[0, 0, 4, 0]],
padding: [[0, 23.5, 0]]
justifyContent: 'center'
},
customerManualDataEntry: {
actionBar: {
display: 'flex',
flexDirection: 'row',
margin: [[8, 0, 4, 0]],
padding: [[0, 40.5, 0]]
},
retrieveInformation: {
display: 'flex',
flexDirection: 'row',
margin: [[0, 0, 4, 0]],
padding: [[0, 32.5, 0]]
flexDirection: 'column',
width: 219
},
panels: {
display: 'flex'

View file

@ -12,88 +12,73 @@ import PhotosCard from './PhotosCard'
const useStyles = makeStyles(mainStyles)
const CustomerDetails = memo(
({ txData, customer, locale, setShowCompliance }) => {
const classes = useStyles()
const CustomerDetails = memo(({ customer, photosData, locale }) => {
const classes = useStyles()
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
const usSsn = R.path(['usSsn'])(customer)
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
const usSsn = R.path(['usSsn'])(customer)
const elements = [
{
header: 'Phone number',
size: 172,
value: getFormattedPhone(customer.phone, locale.country)
}
]
const elements = [
{
header: 'Phone number',
size: 172,
value: getFormattedPhone(customer.phone, locale.country)
}
]
if (idNumber)
elements.push({
header: 'ID number',
size: 172,
value: idNumber
})
if (idNumber)
elements.push({
header: 'ID number',
size: 172,
value: idNumber
})
if (usSsn)
elements.push({
header: 'US SSN',
size: 127,
value: usSsn
})
if (usSsn)
elements.push({
header: 'US SSN',
size: 127,
value: usSsn
})
const name = getName(customer)
const name = getName(customer)
return (
<Box display="flex">
<PhotosCard
frontCameraData={R.pick(['frontCameraPath', 'frontCameraAt'])(
customer
)}
txPhotosData={
txData &&
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
txData
)
}
/>
<Box display="flex" flexDirection="column">
<div className={classes.name}>
<IdIcon className={classes.idIcon} />
<H2 noMargin>
{name.length
? name
: getFormattedPhone(
R.path(['phone'])(customer),
locale.country
)}
</H2>
</div>
<Box display="flex" mt="auto">
{elements.map(({ size, header }, idx) => (
<Label1
noMargin
key={idx}
className={classes.label}
style={{ width: size }}>
{header}
</Label1>
))}
</Box>
<Box display="flex">
{elements.map(({ size, value }, idx) => (
<P
noMargin
key={idx}
className={classes.value}
style={{ width: size }}>
{value}
</P>
))}
</Box>
return (
<Box display="flex">
<PhotosCard photosData={photosData} />
<Box display="flex" flexDirection="column">
<div className={classes.name}>
<IdIcon className={classes.idIcon} />
<H2 noMargin>
{name.length
? name
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
</H2>
</div>
<Box display="flex" mt="auto">
{elements.map(({ size, header }, idx) => (
<Label1
noMargin
key={idx}
className={classes.label}
style={{ width: size }}>
{header}
</Label1>
))}
</Box>
<Box display="flex">
{elements.map(({ size, value }, idx) => (
<P
noMargin
key={idx}
className={classes.value}
style={{ width: size }}>
{value}
</P>
))}
</Box>
</Box>
)
}
)
</Box>
)
})
export default CustomerDetails

View file

@ -8,6 +8,8 @@ import { ReactComponent as NoteReversedIcon } from 'src/styling/icons/customer-n
import { ReactComponent as NoteIcon } from 'src/styling/icons/customer-nav/note/white.svg'
import { ReactComponent as OverviewReversedIcon } from 'src/styling/icons/customer-nav/overview/comet.svg'
import { ReactComponent as OverviewIcon } from 'src/styling/icons/customer-nav/overview/white.svg'
import { ReactComponent as PhotosReversedIcon } from 'src/styling/icons/customer-nav/photos/comet.svg'
import { ReactComponent as Photos } from 'src/styling/icons/customer-nav/photos/white.svg'
import styles from './CustomerSidebar.styles.js'
@ -33,6 +35,12 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
display: 'Notes',
Icon: NoteIcon,
InverseIcon: NoteReversedIcon
},
{
code: 'photos',
display: 'Photos & files',
Icon: Photos,
InverseIcon: PhotosReversedIcon
}
]

View file

@ -4,58 +4,21 @@ import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import React, { memo, useState } from 'react'
import { Carousel } from 'src/components/Carousel'
import { InformativeDialog } from 'src/components/InformativeDialog'
import { Info2, Label1 } from 'src/components/typography'
import { Info2 } from 'src/components/typography'
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
import { URI } from 'src/utils/apollo'
import CopyToClipboard from '../../Transactions/CopyToClipboard'
import styles from './PhotosCard.styles'
import PhotosCarousel from './PhotosCarousel'
const useStyles = makeStyles(styles)
const Label = ({ children }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
}
const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
const PhotosCard = memo(({ photosData }) => {
const classes = useStyles()
const [photosDialog, setPhotosDialog] = useState(false)
const mapKeys = pair => {
const [key, value] = pair
if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') {
return ['path', value]
}
if (key === 'txCustomerPhotoAt' || key === 'frontCameraAt') {
return ['date', value]
}
return pair
}
const addPhotoDir = R.map(it => {
const hasFrontCameraData = R.has('id')(it)
return hasFrontCameraData
? { ...it, photoDir: 'operator-data/customersphotos' }
: { ...it, photoDir: 'front-camera-photo' }
})
const standardizeKeys = R.map(
R.compose(R.fromPairs, R.map(mapKeys), R.toPairs)
)
const filterByPhotoAvailable = R.filter(
tx => !R.isNil(tx.date) && !R.isNil(tx.path)
)
const photosData = filterByPhotoAvailable(
addPhotoDir(standardizeKeys(R.append(frontCameraData, txPhotosData)))
)
const singlePhoto = R.head(photosData)
return (
@ -97,41 +60,4 @@ const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
)
})
export const PhotosCarousel = memo(({ photosData }) => {
const classes = useStyles()
const [currentIndex, setCurrentIndex] = useState(0)
const isFaceCustomerPhoto = !R.has('id')(photosData[currentIndex])
const slidePhoto = index => setCurrentIndex(index)
return (
<>
<Carousel photosData={photosData} slidePhoto={slidePhoto} />
{!isFaceCustomerPhoto && (
<div className={classes.firstRow}>
<Label>Session ID</Label>
<CopyToClipboard>
{photosData && photosData[currentIndex]?.id}
</CopyToClipboard>
</div>
)}
<div className={classes.secondRow}>
<div>
<div>
<Label>Date</Label>
<div>{photosData && photosData[currentIndex]?.date}</div>
</div>
</div>
<div>
<Label>Taken by</Label>
<div>
{!isFaceCustomerPhoto ? 'Acceptance of T&C' : 'Compliance scan'}
</div>
</div>
</div>
</>
)
})
export default PhotosCard

View file

@ -1,7 +1,4 @@
import typographyStyles from 'src/components/typography/styles'
import { zircon, backgroundColor, offColor } from 'src/styling/variables'
const { p } = typographyStyles
import { zircon, backgroundColor } from 'src/styling/variables'
export default {
photo: {
@ -41,43 +38,5 @@ export default {
alignItems: 'center',
justifyContent: 'center',
display: 'flex'
},
label: {
color: offColor,
margin: [[0, 0, 6, 0]]
},
firstRow: {
padding: [[8]],
display: 'flex',
flexDirection: 'column'
},
secondRow: {
extend: p,
display: 'flex',
padding: [[8]],
'& > div': {
display: 'flex',
flexDirection: 'column',
'& > div': {
width: 144,
height: 37,
marginBottom: 15,
marginRight: 55
}
}
},
imgWrapper: {
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
width: 550,
height: 550
},
imgInner: {
objectFit: 'cover',
objectPosition: 'center',
width: 550,
height: 550,
marginBottom: 40
}
}

View file

@ -0,0 +1,56 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import React, { memo, useState } from 'react'
import { Carousel } from 'src/components/Carousel'
import { Label1 } from 'src/components/typography'
import CopyToClipboard from '../../Transactions/CopyToClipboard'
import styles from './PhotosCarousel.styles'
const useStyles = makeStyles(styles)
const PhotosCarousel = memo(({ photosData }) => {
const classes = useStyles()
const [currentIndex, setCurrentIndex] = useState(0)
const Label = ({ children }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
}
const isFaceCustomerPhoto = !R.has('id')(photosData[currentIndex])
const slidePhoto = index => setCurrentIndex(index)
return (
<>
<Carousel photosData={photosData} slidePhoto={slidePhoto} />
{!isFaceCustomerPhoto && (
<div className={classes.firstRow}>
<Label>Session ID</Label>
<CopyToClipboard>
{photosData && photosData[currentIndex]?.id}
</CopyToClipboard>
</div>
)}
<div className={classes.secondRow}>
<div>
<div>
<Label>Date</Label>
<div>{photosData && photosData[currentIndex]?.date}</div>
</div>
</div>
<div>
<Label>Taken by</Label>
<div>
{!isFaceCustomerPhoto ? 'Acceptance of T&C' : 'Compliance scan'}
</div>
</div>
</div>
</>
)
})
export default PhotosCarousel

View file

@ -0,0 +1,31 @@
import typographyStyles from 'src/components/typography/styles'
import { offColor } from 'src/styling/variables'
const { p } = typographyStyles
export default {
label: {
color: offColor,
margin: [[0, 0, 6, 0]]
},
firstRow: {
padding: [[8]],
display: 'flex',
flexDirection: 'column'
},
secondRow: {
extend: p,
display: 'flex',
padding: [[8]],
'& > div': {
display: 'flex',
flexDirection: 'column',
'& > div': {
width: 144,
height: 37,
marginBottom: 15,
marginRight: 55
}
}
}
}

View file

@ -70,12 +70,7 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
const tableElements = [
{
header: 'Machine',
width: 160,
view: R.path(['machineName'])
},
{
width: 125,
width: 40,
view: it => (
<>
{it.txClass === 'cashOut' ? (
@ -86,6 +81,11 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
</>
)
},
{
header: 'Machine',
width: 160,
view: R.path(['machineName'])
},
{
header: 'Transaction ID',
width: 145,

View file

@ -5,10 +5,12 @@ import CustomerSidebar from './CustomerSidebar'
import EditableCard from './EditableCard'
import Field from './Field'
import IdDataCard from './IdDataCard'
import PhotosCarousel from './PhotosCarousel'
import TransactionsList from './TransactionsList'
import Upload from './Upload'
export {
PhotosCarousel,
CustomerDetails,
IdDataCard,
TransactionsList,

View file

@ -209,10 +209,41 @@ const entryType = {
initialValues: { entryType: '' }
}
const mapKeys = pair => {
const [key, value] = pair
if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') {
return ['path', value]
}
if (key === 'txCustomerPhotoAt' || key === 'frontCameraAt') {
return ['date', value]
}
return pair
}
const addPhotoDir = R.map(it => {
const hasFrontCameraData = R.has('id')(it)
return hasFrontCameraData
? { ...it, photoDir: 'operator-data/customersphotos' }
: { ...it, photoDir: 'front-camera-photo' }
})
const standardizeKeys = R.map(R.compose(R.fromPairs, R.map(mapKeys), R.toPairs))
const filterByPhotoAvailable = R.filter(
tx => !R.isNil(tx.date) && !R.isNil(tx.path)
)
const formatPhotosData = R.compose(
filterByPhotoAvailable,
addPhotoDir,
standardizeKeys
)
export {
getAuthorizedStatus,
getFormattedPhone,
getName,
entryType,
customElements
customElements,
formatPhotosData
}

View file

@ -64,7 +64,9 @@ const IndividualDiscounts = () => {
const [showModal, setShowModal] = useState(false)
const toggleModal = () => setShowModal(!showModal)
const { data: discountResponse, loading } = useQuery(GET_INDIVIDUAL_DISCOUNTS)
const { data: discountResponse, loading: discountLoading } = useQuery(
GET_INDIVIDUAL_DISCOUNTS
)
const { data: customerData, loading: customerLoading } = useQuery(
GET_CUSTOMERS
)
@ -160,24 +162,22 @@ const IndividualDiscounts = () => {
}
]
const isLoading = loading || customerLoading
const loading = discountLoading || customerLoading
return (
<>
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && (
<Box
marginBottom={4}
marginTop={-7}
className={classes.tableWidth}
display="flex"
justifyContent="flex-end">
<Link color="primary" onClick={toggleModal}>
Add new code
</Link>
</Box>
)}
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && (
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
<>
<Box
marginBottom={4}
marginTop={-7}
className={classes.tableWidth}
display="flex"
justifyContent="flex-end">
<Link color="primary" onClick={toggleModal}>
Add new code
</Link>
</Box>
<DataTable
elements={elements}
data={R.path(['individualDiscounts'])(discountResponse)}
@ -196,7 +196,7 @@ const IndividualDiscounts = () => {
/>
</>
)}
{!isLoading && R.isEmpty(discountResponse.individualDiscounts) && (
{!loading && R.isEmpty(discountResponse.individualDiscounts) && (
<Box display="flex" alignItems="left" flexDirection="column">
<Label3>
It seems there are no active individual customer discounts on your

View file

@ -81,16 +81,21 @@ const Logs = () => {
const deviceId = selected?.deviceId
const { data: machineResponse } = useQuery(GET_MACHINES)
const { data: machineResponse, loading: machinesLoading } = useQuery(
GET_MACHINES
)
const { data: configResponse } = useQuery(GET_DATA)
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse)
const { data: logsResponse, loading } = useQuery(GET_MACHINE_LOGS, {
variables: { deviceId, limit: NUM_LOG_RESULTS },
skip: !selected,
onCompleted: () => setSaveMessage('')
})
const { data: logsResponse, loading: logsLoading } = useQuery(
GET_MACHINE_LOGS,
{
variables: { deviceId, limit: NUM_LOG_RESULTS },
skip: !selected,
onCompleted: () => setSaveMessage('')
}
)
if (machineResponse?.machines?.length && !selected) {
setSelected(machineResponse?.machines[0])
@ -100,6 +105,8 @@ const Logs = () => {
return R.path(['deviceId'])(selected) === it.deviceId
}
const loading = machinesLoading || configLoading || logsLoading
return (
<>
<div className={classes.titleWrapper}>

View file

@ -82,7 +82,7 @@ const Transactions = ({ id }) => {
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configData)
const loading = txLoading && configLoading
const loading = txLoading || configLoading
if (!loading && txResponse) {
txResponse.transactions = txResponse.transactions.splice(0, 5)

View file

@ -85,7 +85,7 @@ const CashboxHistory = ({ machines, currency }) => {
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configData)
const loading = batchesLoading && configLoading
const loading = batchesLoading || configLoading
const batches = R.path(['cashboxBatches'])(batchesData)
@ -252,16 +252,13 @@ const CashboxHistory = ({ machines, currency }) => {
]
return (
<>
{!loading && (
<DataTable
name="cashboxHistory"
elements={elements}
data={batches}
emptyText="No cash box batches so far"
/>
)}
</>
<DataTable
loading={loading}
name="cashboxHistory"
elements={elements}
data={batches}
emptyText="No cashbox batches so far"
/>
)
}

View file

@ -54,7 +54,11 @@ const MachineStatus = () => {
const history = useHistory()
const { state } = useLocation()
const addedMachineId = state?.id
const { data: machinesResponse, refetch, loading } = useQuery(GET_MACHINES)
const {
data: machinesResponse,
refetch,
loading: machinesLoading
} = useQuery(GET_MACHINES)
const { data: configResponse, configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse)
@ -114,6 +118,8 @@ const MachineStatus = () => {
<MachineDetailsRow it={it} onActionSuccess={refetch} timezone={timezone} />
)
const loading = machinesLoading || configLoading
return (
<>
<div className={classes.titleWrapper}>
@ -132,7 +138,7 @@ const MachineStatus = () => {
</div>
</div>
<DataTable
loading={loading && configLoading}
loading={loading}
elements={elements}
data={machines}
Details={InnerMachineDetailsRow}

View file

@ -98,13 +98,13 @@ const Logs = () => {
const [saveMessage, setSaveMessage] = useState(null)
const [logLevel, setLogLevel] = useState(SHOW_ALL)
const { data, loading } = useQuery(GET_SERVER_DATA, {
const { data, loading: dataLoading } = useQuery(GET_SERVER_DATA, {
onCompleted: () => setSaveMessage(''),
variables: {
limit: NUM_LOG_RESULTS
}
})
const { data: configResponse, configLoading } = useQuery(GET_DATA)
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse)
const defaultLogLevels = [
@ -132,6 +132,8 @@ const Logs = () => {
setLogLevel(logLevel)
}
const loading = dataLoading || configLoading
return (
<>
<div className={classes.titleWrapper}>
@ -206,8 +208,8 @@ const Logs = () => {
))}
</TableBody>
</Table>
{loading && configLoading && <H4>{'Loading...'}</H4>}
{!loading && !configLoading && !data?.serverLogs?.length && (
{loading && <H4>{'Loading...'}</H4>}
{!loading && !data?.serverLogs?.length && (
<H4>{'No activity so far'}</H4>
)}
</div>

View file

@ -48,7 +48,7 @@ const SessionManagement = () => {
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse)
const loading = sessionsLoading && configLoading
const loading = sessionsLoading || configLoading
const elements = [
{
@ -107,15 +107,14 @@ const SessionManagement = () => {
]
return (
!loading && (
<>
<TitleSection title="Session Management" />
<DataTable
elements={elements}
data={R.path(['sessions'])(tknResponse)}
/>
</>
)
<>
<TitleSection title="Session Management" />
<DataTable
loading={loading}
elements={elements}
data={R.path(['sessions'])(tknResponse)}
/>
</>
)
}

View file

@ -15,6 +15,8 @@ import DataTable from 'src/components/tables/DataTable'
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 { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
import { ReactComponent as CustomerLinkWhiteIcon } from 'src/styling/icons/month arrows/right_white.svg'
import { errorColor } from 'src/styling/variables'
import { formatDate } from 'src/utils/timezones'
import DetailsRow from './DetailsCard'
@ -124,13 +126,13 @@ const Transactions = () => {
const history = useHistory()
const [filters, setFilters] = useState([])
const { data: filtersResponse, loading: loadingFilters } = useQuery(
const { data: filtersResponse, loading: filtersLoading } = useQuery(
GET_TRANSACTION_FILTERS
)
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
const {
data: txData,
loading: loadingTransactions,
loading: transactionsLoading,
refetch,
startPolling,
stopPolling
@ -185,7 +187,11 @@ const Transactions = () => {
<div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div>
{!it.isAnonymous && (
<div onClick={() => redirect(it.customerId)}>
<CustomerLinkIcon className={classes.customerLinkIcon} />
{it.hasError ? (
<CustomerLinkWhiteIcon className={classes.customerLinkIcon} />
) : (
<CustomerLinkIcon className={classes.customerLinkIcon} />
)}
</div>
)}
</div>
@ -294,6 +300,14 @@ const Transactions = () => {
const filterOptions = R.path(['transactionFilters'])(filtersResponse)
const loading = transactionsLoading || filtersLoading || configLoading
const errorLabel = (
<svg width={12} height={12}>
<rect width={12} height={12} rx={3} fill={errorColor} />
</svg>
)
return (
<>
<div className={classes.titleWrapper}>
@ -301,7 +315,7 @@ const Transactions = () => {
<Title>Transactions</Title>
<div className={classes.buttonsWrapper}>
<SearchBox
loading={loadingFilters}
loading={filtersLoading}
filters={filters}
options={filterOptions}
inputPlaceholder={'Search Transactions'}
@ -331,6 +345,10 @@ const Transactions = () => {
<TxOutIcon />
<span>Cash-out</span>
</div>
<div>
{errorLabel}
<span>Transaction error</span>
</div>
</div>
</div>
{filters.length > 0 && (
@ -342,7 +360,7 @@ const Transactions = () => {
/>
)}
<DataTable
loading={loadingTransactions && configLoading}
loading={loading}
emptyText="No transactions so far"
elements={elements}
data={txList}

View file

@ -80,8 +80,11 @@ const mainStyles = {
display: 'flex',
alignItems: 'center'
},
'& > div': {
marginLeft: 24
},
'& > div:first-child': {
marginRight: 24
marginLeft: 0
},
'& span': {
extend: label1,

View file

@ -1,5 +1,6 @@
import { useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useState } from 'react'
@ -48,6 +49,14 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
return null
}
const handleSubmit = () => {
if (twoFACode.length !== 6) {
setInvalidCode(true)
return
}
confirm2FA({ variables: { code: twoFACode } })
}
return (
showModal && (
<Modal
@ -61,28 +70,26 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
To make changes on this user, please confirm this action by entering
your two-factor authentication code below.
</P>
<CodeInput
name="2fa"
value={twoFACode}
onChange={handleCodeChange}
numInputs={6}
error={invalidCode}
containerStyle={classes.codeContainer}
shouldAutoFocus
/>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput
name="2fa"
value={twoFACode}
onChange={handleCodeChange}
numInputs={6}
error={invalidCode}
containerStyle={classes.codeContainer}
shouldAutoFocus
/>
<button onClick={handleSubmit} className={classes.enterButton} />
</Form>
</Formik>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<div className={classes.footer}>
<Button
className={classes.submit}
onClick={() => {
if (twoFACode.length !== 6) {
setInvalidCode(true)
return
}
confirm2FA({ variables: { code: twoFACode } })
}}>
<Button className={classes.submit} onClick={handleSubmit}>
Confirm
</Button>
</div>

View file

@ -38,7 +38,8 @@ const SAVE_ACCOUNTS = gql`
}
`
const isConfigurable = it => !R.isNil(it) && !R.contains(it)(['mock-exchange'])
const isConfigurable = it =>
!R.isNil(it) && !R.contains(it)(['mock-exchange', 'no-exchange'])
const ChooseExchange = ({ data: currentData, addData }) => {
const classes = useStyles()

View file

@ -0,0 +1,10 @@
<?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/customer-nav/photos/comet</title>
<g id="icon/customer-nav/photos/comet" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="#5F668A" stroke-width="2" x="1" y="1" width="18" height="18" rx="1"></rect>
<circle id="Oval" stroke="#5F668A" stroke-width="2" cx="15" cy="5" r="1"></circle>
<polyline id="Path" stroke="#5F668A" stroke-width="2" stroke-linejoin="round" points="1 19 7 13 13 19"></polyline>
<path d="M13.3333333,14 L18,19 L13.3333333,19 L11,16.5 L13.3333333,14 Z" id="Combined-Shape" stroke="#5F668A" stroke-width="2" stroke-linejoin="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 850 B

View file

@ -0,0 +1,10 @@
<?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/customer-nav/photos/white</title>
<g id="icon/customer-nav/photos/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="#FFFFFF" stroke-width="2" x="1" y="1" width="18" height="18" rx="1"></rect>
<circle id="Oval" stroke="#FFFFFF" stroke-width="2" cx="15" cy="5" r="1"></circle>
<polyline id="Path" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round" points="1 19 7 13 13 19"></polyline>
<path d="M13.3333333,14 L18,19 L13.3333333,19 L11,16.5 L13.3333333,14 Z" id="Combined-Shape" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 850 B

View file

@ -0,0 +1,24 @@
<?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">
<defs>
<circle id="path-1-right" cx="10" cy="10" r="10"></circle>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="pop-up/action/download-logs/date-range-copy-2" transform="translate(-232.000000, -187.000000)">
<g id="icon/sf-contain-b-copy-4" transform="translate(242.000000, 197.000000) scale(-1, 1) rotate(-270.000000) translate(-242.000000, -197.000000) translate(232.000000, 187.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1-right"></use>
</mask>
<use id="Mask" fill="#FFFFFF" fill-rule="nonzero" xlink:href="#path-1-right"></use>
<g id="icon/sf-small/wizzard" mask="url(#mask-2)" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(6.666667, 6.000000)" id="Group">
<g>
<polyline id="Path-3" stroke="#1B2559" stroke-width="2" points="0 4.83333333 3.33333333 8.16666667 6.66666667 4.83333333"></polyline>
<line x1="3.33333333" y1="0.25" x2="3.33333333" y2="6.5" id="Path-4" stroke="#1B2559" stroke-width="2"></line>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -77,7 +77,6 @@
"socket.io-client": "^2.0.3",
"talisman": "^0.20.0",
"twilio": "^3.6.1",
"ua-parser-js": "^0.7.22",
"uuid": "8.3.2",
"web3": "^0.20.6",
"winston": "^2.4.2",