Merge branch 'dev' into refact/lam-108/dev/rbf-transactions
This commit is contained in:
commit
83de7a750d
45 changed files with 682 additions and 403 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -44,5 +44,6 @@ connections=40
|
|||
keypool=10000
|
||||
prune=4000
|
||||
daemon=0
|
||||
addresstype=p2sh-segwit`
|
||||
addresstype=p2sh-segwit
|
||||
changetype=bech32`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
22
new-lamassu-admin/package-lock.json
generated
22
new-lamassu-admin/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export default {
|
|||
confirmationCode: {
|
||||
extend: base,
|
||||
fontSize: codeInputFontSize,
|
||||
fontFamily: fontPrimary,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 900
|
||||
},
|
||||
inline: {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ const styles = {
|
|||
},
|
||||
error: {
|
||||
color: errorColor
|
||||
},
|
||||
enterButton: {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
81
new-lamassu-admin/src/pages/Customers/CustomerPhotos.js
Normal file
81
new-lamassu-admin/src/pages/Customers/CustomerPhotos.js
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -80,8 +80,11 @@ const mainStyles = {
|
|||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'& > div': {
|
||||
marginLeft: 24
|
||||
},
|
||||
'& > div:first-child': {
|
||||
marginRight: 24
|
||||
marginLeft: 0
|
||||
},
|
||||
'& span': {
|
||||
extend: label1,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue