Merge branch 'dev' into update_install-nix
This commit is contained in:
commit
3817ee63fe
77 changed files with 1163 additions and 817 deletions
|
|
@ -33,7 +33,7 @@ npm install
|
||||||
## Generate certificates
|
## Generate certificates
|
||||||
|
|
||||||
```
|
```
|
||||||
bash bin/cert-gen.sh
|
bash tools/cert-gen.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ npm install
|
||||||
## Generate certificates
|
## Generate certificates
|
||||||
|
|
||||||
```
|
```
|
||||||
bash bin/cert-gen.sh
|
bash tools/cert-gen.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,7 @@ keypool=10000
|
||||||
prune=4000
|
prune=4000
|
||||||
daemon=0
|
daemon=0
|
||||||
addresstype=p2sh-segwit
|
addresstype=p2sh-segwit
|
||||||
walletrbf=1`
|
walletrbf=1
|
||||||
|
bind=0.0.0.0:8332
|
||||||
|
rpcport=8333`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,5 @@ keypool=10000
|
||||||
prune=4000
|
prune=4000
|
||||||
daemon=0
|
daemon=0
|
||||||
bind=0.0.0.0:8334
|
bind=0.0.0.0:8334
|
||||||
rpcport=8335
|
rpcport=8335`
|
||||||
`
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,24 +25,24 @@ const BINARIES = {
|
||||||
dir: 'bitcoin-0.20.1/bin'
|
dir: 'bitcoin-0.20.1/bin'
|
||||||
},
|
},
|
||||||
ETH: {
|
ETH: {
|
||||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.25-e7872729.tar.gz',
|
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.8-26675454.tar.gz',
|
||||||
dir: 'geth-linux-amd64-1.9.25-e7872729'
|
dir: 'geth-linux-amd64-1.10.8-26675454'
|
||||||
},
|
},
|
||||||
ZEC: {
|
ZEC: {
|
||||||
url: 'https://z.cash/downloads/zcash-4.3.0-linux64-debian-stretch.tar.gz',
|
url: 'https://z.cash/downloads/zcash-4.4.1-linux64-debian-stretch.tar.gz',
|
||||||
dir: 'zcash-4.3.0/bin'
|
dir: 'zcash-4.4.1/bin'
|
||||||
},
|
},
|
||||||
DASH: {
|
DASH: {
|
||||||
url: 'https://github.com/dashpay/dash/releases/download/v0.16.1.1/dashcore-0.16.1.1-x86_64-linux-gnu.tar.gz',
|
url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz',
|
||||||
dir: 'dashcore-0.16.1/bin'
|
dir: 'dashcore-0.17.0/bin'
|
||||||
},
|
},
|
||||||
LTC: {
|
LTC: {
|
||||||
url: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
url: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
||||||
dir: 'litecoin-0.18.1/bin'
|
dir: 'litecoin-0.18.1/bin'
|
||||||
},
|
},
|
||||||
BCH: {
|
BCH: {
|
||||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v22.2.0/bitcoin-cash-node-22.2.0-x86_64-linux-gnu.tar.gz',
|
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v23.1.0/bitcoin-cash-node-23.1.0-x86_64-linux-gnu.tar.gz',
|
||||||
dir: 'bitcoin-cash-node-22.2.0/bin',
|
dir: 'bitcoin-cash-node-23.1.0/bin',
|
||||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ function buildConfig () {
|
||||||
rpcpassword=${common.randomPass()}
|
rpcpassword=${common.randomPass()}
|
||||||
dbcache=500
|
dbcache=500
|
||||||
keypool=10000
|
keypool=10000
|
||||||
litemode=1
|
disablegovernance=1
|
||||||
prune=4000
|
prune=4000
|
||||||
txindex=0
|
txindex=0
|
||||||
enableprivatesend=1
|
enablecoinjoin=1
|
||||||
privatesendautostart=1`
|
coinjoinautostart=1`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ module.exports = {setup}
|
||||||
function setup (dataDir) {
|
function setup (dataDir) {
|
||||||
const coinRec = coinUtils.getCryptoCurrency('ETH')
|
const coinRec = coinUtils.getCryptoCurrency('ETH')
|
||||||
common.firewall([coinRec.defaultPort])
|
common.firewall([coinRec.defaultPort])
|
||||||
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="fast" --cache 2048 --maxpeers 40 --rpc`
|
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --rpc`
|
||||||
common.writeSupervisorConfig(coinRec, cmd)
|
common.writeSupervisorConfig(coinRec, cmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ const logger = common.logger
|
||||||
|
|
||||||
const PLUGINS = {
|
const PLUGINS = {
|
||||||
BTC: require('./bitcoin.js'),
|
BTC: require('./bitcoin.js'),
|
||||||
LTC: require('./litecoin.js'),
|
BCH: require('./bitcoincash.js'),
|
||||||
ETH: require('./ethereum.js'),
|
|
||||||
DASH: require('./dash.js'),
|
DASH: require('./dash.js'),
|
||||||
ZEC: require('./zcash.js'),
|
ETH: require('./ethereum.js'),
|
||||||
BCH: require('./bitcoincash.js')
|
LTC: require('./litecoin.js'),
|
||||||
|
ZEC: require('./zcash.js')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {run}
|
module.exports = {run}
|
||||||
|
|
@ -57,7 +57,8 @@ function processCryptos (codes) {
|
||||||
|
|
||||||
const selectedCryptos = _.map(code => _.find(['code', code], cryptos), codes)
|
const selectedCryptos = _.map(code => _.find(['code', code], cryptos), codes)
|
||||||
_.forEach(setupCrypto, selectedCryptos)
|
_.forEach(setupCrypto, selectedCryptos)
|
||||||
common.es('sudo service supervisor restart')
|
common.es('sudo supervisorctl reread')
|
||||||
|
common.es('sudo supervisorctl update')
|
||||||
|
|
||||||
const blockchainDir = options.blockchainDir
|
const blockchainDir = options.blockchainDir
|
||||||
const backupDir = path.resolve(os.homedir(), 'backups')
|
const backupDir = path.resolve(os.homedir(), 'backups')
|
||||||
|
|
@ -104,9 +105,7 @@ function run () {
|
||||||
name: c.display,
|
name: c.display,
|
||||||
value: c.code,
|
value: c.code,
|
||||||
checked,
|
checked,
|
||||||
disabled: c.cryptoCode === 'ETH'
|
disabled: checked && 'Installed'
|
||||||
? 'Use admin\'s Infura plugin'
|
|
||||||
: checked && 'Installed'
|
|
||||||
}
|
}
|
||||||
}, cryptos)
|
}, cryptos)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ function atomic (tx, pi, fromClient) {
|
||||||
const isolationLevel = pgp.txMode.isolationLevel
|
const isolationLevel = pgp.txMode.isolationLevel
|
||||||
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
||||||
function transaction (t) {
|
function transaction (t) {
|
||||||
const sql = 'select * from cash_out_txs where id=$1'
|
const sql = 'SELECT * FROM cash_out_txs WHERE id=$1 FOR UPDATE'
|
||||||
|
|
||||||
return t.oneOrNone(sql, [tx.id])
|
return t.oneOrNone(sql, [tx.id])
|
||||||
.then(toObj)
|
.then(toObj)
|
||||||
.then(oldTx => {
|
.then(oldTx => {
|
||||||
|
|
@ -72,7 +73,7 @@ function preProcess (t, oldTx, newTx, pi) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasError = !oldTx.error && newTx.error
|
const hasError = !oldTx.error && newTx.error
|
||||||
const hasDispenseOccurred = !dispenseOccurred(oldTx.bills) && dispenseOccurred(newTx.bills)
|
const hasDispenseOccurred = !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
|
||||||
|
|
||||||
if (hasError || hasDispenseOccurred) {
|
if (hasError || hasDispenseOccurred) {
|
||||||
return cashOutActions.logDispense(t, updatedTx)
|
return cashOutActions.logDispense(t, updatedTx)
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,11 @@ function migrateCommissions (config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { global, scoped } = getConfigFields(_.keys(codes), config)
|
const { global, scoped } = getConfigFields(_.keys(codes), config)
|
||||||
|
const defaultCashOutCommissions = { code: 'cashOutCommission', value: 0, scope: global[0].scope }
|
||||||
|
const isCashOutDisabled =
|
||||||
|
_.isEmpty(_.filter(commissionElement => commissionElement.code === 'cashOutCommission', global))
|
||||||
|
const globalWithDefaults =
|
||||||
|
isCashOutDisabled ? _.concat(global, defaultCashOutCommissions) : global
|
||||||
|
|
||||||
const machineAndCryptoScoped = scoped.filter(
|
const machineAndCryptoScoped = scoped.filter(
|
||||||
f => f.scope.machine !== GLOBAL_SCOPE.machine && f.scope.crypto.length === 1
|
f => f.scope.machine !== GLOBAL_SCOPE.machine && f.scope.crypto.length === 1
|
||||||
|
|
@ -112,7 +117,7 @@ function migrateCommissions (config) {
|
||||||
const allCommissionsOverrides = withCryptoScoped.concat(filteredMachineScoped)
|
const allCommissionsOverrides = withCryptoScoped.concat(filteredMachineScoped)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
..._.fromPairs(global.map(f => [`commissions_${codes[f.code]}`, f.value])),
|
..._.fromPairs(globalWithDefaults.map(f => [`commissions_${codes[f.code]}`, f.value])),
|
||||||
...(allCommissionsOverrides.length > 0 && {
|
...(allCommissionsOverrides.length > 0 && {
|
||||||
commissions_overrides: allCommissionsOverrides.map(s => ({
|
commissions_overrides: allCommissionsOverrides.map(s => ({
|
||||||
..._.fromPairs(s.values.map(f => [codes[f.code], f.value])),
|
..._.fromPairs(s.values.map(f => [codes[f.code], f.value])),
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ const NUM_RESULTS = 1000
|
||||||
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
|
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
|
||||||
const frontCameraBaseDir = _.get('frontCameraDir', options)
|
const frontCameraBaseDir = _.get('frontCameraDir', options)
|
||||||
const operatorDataDir = _.get('operatorDataDir', options)
|
const operatorDataDir = _.get('operatorDataDir', options)
|
||||||
|
const sms = require('./sms')
|
||||||
|
const settingsLoader = require('./new-settings-loader')
|
||||||
|
|
||||||
|
const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add new customer
|
* Add new customer
|
||||||
|
|
@ -116,12 +120,20 @@ async function updateCustomer (id, data, userToken) {
|
||||||
const enhancedUpdateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken))
|
const enhancedUpdateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken))
|
||||||
const updateData = updateOverride(enhancedUpdateData)
|
const updateData = updateOverride(enhancedUpdateData)
|
||||||
|
|
||||||
|
if (!_.isEmpty(updateData)) {
|
||||||
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
||||||
' where id=$1'
|
' where id=$1'
|
||||||
invalidateCustomerNotifications(id, formattedData)
|
|
||||||
|
|
||||||
await db.none(sql, [id])
|
await db.none(sql, [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.subscriberInfo) {
|
||||||
|
Promise.all([getCustomerById(id), settingsLoader.loadLatest()])
|
||||||
|
.then(([customer, config]) => sms.getLookup(config, customer.phone))
|
||||||
|
.then(res => updateSubscriberData(id, res, userToken))
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
|
invalidateCustomerNotifications(id, formattedData)
|
||||||
return getCustomerById(id)
|
return getCustomerById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,6 +144,11 @@ const invalidateCustomerNotifications = (id, data) => {
|
||||||
return notifierQueries.invalidateNotification(detailB, 'compliance')
|
return notifierQueries.invalidateNotification(detailB, 'compliance')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateSubscriberData = (customerId, data, userToken) => {
|
||||||
|
const sql = `UPDATE customers SET subscriber_info=$1, subscriber_info_at=now(), subscriber_info_by=$2 WHERE id=$3`
|
||||||
|
return db.none(sql, [data, userToken, customerId])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get customer by id
|
* Get customer by id
|
||||||
*
|
*
|
||||||
|
|
@ -264,16 +281,19 @@ function getComplianceTypes () {
|
||||||
function updateOverride (fields) {
|
function updateOverride (fields) {
|
||||||
const updateableFields = [
|
const updateableFields = [
|
||||||
'id_card_data',
|
'id_card_data',
|
||||||
'id_card_photo',
|
'id_card_photo_path',
|
||||||
'front_camera',
|
'front_camera_path',
|
||||||
'authorized',
|
'authorized',
|
||||||
'us_ssn'
|
'us_ssn'
|
||||||
]
|
]
|
||||||
|
|
||||||
const updatedFields = _.intersection(updateableFields, _.keys(fields))
|
const removePathSuffix = _.map(_.replace('_path', ''))
|
||||||
const atFields = _.fromPairs(_.map(f => [`${f}_override`, 'automatic'], updatedFields))
|
const getPairs = _.map(f => [`${f}_override`, 'automatic'])
|
||||||
|
|
||||||
return _.merge(fields, atFields)
|
const updatedFields = _.intersection(updateableFields, _.keys(fields))
|
||||||
|
const overrideFields = _.compose(_.fromPairs, getPairs, removePathSuffix)(updatedFields)
|
||||||
|
|
||||||
|
return _.merge(fields, overrideFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
function enhanceAtFields (fields) {
|
function enhanceAtFields (fields) {
|
||||||
|
|
@ -450,7 +470,10 @@ function batch () {
|
||||||
*
|
*
|
||||||
* @returns {array} Array of customers with it's transactions aggregations
|
* @returns {array} Array of customers with it's transactions aggregations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function getCustomersList (phone = null, name = null, address = null, id = null) {
|
function getCustomersList (phone = null, name = null, address = null, id = null) {
|
||||||
|
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,
|
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,
|
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,
|
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||||
|
|
@ -465,21 +488,22 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
||||||
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
|
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||||
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
||||||
|
coalesce(sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id), 0) as total_spent
|
||||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
||||||
coalesce(sum(t.fiat) OVER (partition by c.id), 0) AS total_spent
|
coalesce(sum(t.fiat) OVER (partition by c.id), 0) AS total_spent
|
||||||
FROM customers c LEFT OUTER JOIN (
|
FROM customers c LEFT OUTER JOIN (
|
||||||
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id
|
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||||
FROM cash_in_txs WHERE send_confirmed = true UNION
|
FROM cash_in_txs WHERE send_confirmed = true UNION
|
||||||
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id
|
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||||
FROM cash_out_txs WHERE confirmed_at IS NOT NULL) t ON c.id = t.customer_id
|
FROM cash_out_txs WHERE confirmed_at IS NOT NULL) t ON c.id = t.customer_id
|
||||||
WHERE c.id != $1
|
WHERE c.id != $2
|
||||||
) AS cl WHERE rn = 1
|
) AS cl WHERE rn = 1
|
||||||
AND ($3 IS NULL OR phone = $3)
|
AND ($4 IS NULL OR phone = $4)
|
||||||
AND ($4 IS NULL OR concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $4 OR id_card_data::json->>'firstName' = $4 OR id_card_data::json->>'lastName' = $4)
|
AND ($5 IS NULL OR concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $5 OR id_card_data::json->>'firstName' = $5 OR id_card_data::json->>'lastName' = $5)
|
||||||
AND ($5 IS NULL OR id_card_data::json->>'address' = $5)
|
AND ($6 IS NULL OR id_card_data::json->>'address' = $6)
|
||||||
AND ($6 IS NULL OR id_card_data::json->>'documentNumber' = $6)
|
AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7)
|
||||||
limit $2`
|
limit $3`
|
||||||
return db.any(sql, [ anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
|
return db.any(sql, [ passableErrorCdoes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
|
||||||
.then(customers => Promise.all(_.map(customer => {
|
.then(customers => Promise.all(_.map(customer => {
|
||||||
return populateOverrideUsernames(customer)
|
return populateOverrideUsernames(customer)
|
||||||
.then(camelize)
|
.then(camelize)
|
||||||
|
|
@ -494,11 +518,12 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
* @returns {array} Array of customers with it's transactions aggregations
|
* @returns {array} Array of customers with it's transactions aggregations
|
||||||
*/
|
*/
|
||||||
function getCustomerById (id) {
|
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,
|
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,
|
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,
|
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||||
sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
|
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
|
fiat_code as last_tx_fiat_code, tx_class as last_tx_class, subscriber_info
|
||||||
from (
|
from (
|
||||||
select c.id, c.authorized_override,
|
select c.id, c.authorized_override,
|
||||||
greatest(0, date_part('day', c.suspended_until - now())) as days_suspended,
|
greatest(0, date_part('day', c.suspended_until - now())) as days_suspended,
|
||||||
|
|
@ -506,18 +531,18 @@ function getCustomerById (id) {
|
||||||
c.front_camera_path, c.front_camera_override,
|
c.front_camera_path, c.front_camera_override,
|
||||||
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
|
c.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.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
||||||
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
|
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||||
row_number() over (partition by c.id order by t.created desc) as rn,
|
row_number() over (partition by c.id order by t.created desc) as rn,
|
||||||
sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs,
|
sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs,
|
||||||
sum(t.fiat) over (partition by c.id) as total_spent
|
sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id) as total_spent
|
||||||
from customers c left outer join (
|
from customers c left outer join (
|
||||||
select 'cashIn' as tx_class, id, fiat, fiat_code, created, customer_id
|
select 'cashIn' as tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||||
from cash_in_txs where send_confirmed = true union
|
from cash_in_txs where send_confirmed = true union
|
||||||
select 'cashOut' as tx_class, id, fiat, fiat_code, created, customer_id
|
select 'cashOut' as tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||||
from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id
|
from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id
|
||||||
where c.id = $1
|
where c.id = $2
|
||||||
) as cl where rn = 1`
|
) as cl where rn = 1`
|
||||||
return db.oneOrNone(sql, [id])
|
return db.oneOrNone(sql, [passableErrorCodes, id])
|
||||||
.then(populateOverrideUsernames)
|
.then(populateOverrideUsernames)
|
||||||
.then(camelize)
|
.then(camelize)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ const stripDefaultDbFuncs = dbCtx => {
|
||||||
manyOrNone: dbCtx.$manyOrNone,
|
manyOrNone: dbCtx.$manyOrNone,
|
||||||
tx: dbCtx.$tx,
|
tx: dbCtx.$tx,
|
||||||
task: dbCtx.$task,
|
task: dbCtx.$task,
|
||||||
batch: dbCtx.batch
|
batch: dbCtx.batch,
|
||||||
|
multi: dbCtx.$multi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,6 +68,7 @@ const pgp = Pgp({
|
||||||
obj.$one = (query, variables) => obj.__taskEx(t => t.one(query, variables))
|
obj.$one = (query, variables) => obj.__taskEx(t => t.one(query, variables))
|
||||||
obj.$none = (query, variables) => obj.__taskEx(t => t.none(query, variables))
|
obj.$none = (query, variables) => obj.__taskEx(t => t.none(query, variables))
|
||||||
obj.$any = (query, variables) => obj.__taskEx(t => t.any(query, variables))
|
obj.$any = (query, variables) => obj.__taskEx(t => t.any(query, variables))
|
||||||
|
obj.$multi = (query, variables) => obj.__taskEx(t => t.multi(query, variables))
|
||||||
// when opts is not defined "cb" occupies the "opts" spot of the arguments
|
// when opts is not defined "cb" occupies the "opts" spot of the arguments
|
||||||
obj.$tx = (opts, cb) => typeof opts === 'function' ? _tx(obj, {}, opts) : _tx(obj, opts, cb)
|
obj.$tx = (opts, cb) => typeof opts === 'function' ? _tx(obj, {}, opts) : _tx(obj, opts, cb)
|
||||||
obj.$task = (opts, cb) => typeof opts === 'function' ? _task(obj, {}, opts) : _task(obj, opts, cb)
|
obj.$task = (opts, cb) => typeof opts === 'function' ? _task(obj, {}, opts) : _task(obj, opts, cb)
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,11 @@ function update (deviceId, logLines) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearOldLogs () {
|
function clearOldLogs () {
|
||||||
const sql = `delete from logs
|
const sqls = `delete from logs
|
||||||
where timestamp < now() - interval '3 days'`
|
where timestamp < now() - interval '3 days';
|
||||||
|
delete from server_logs
|
||||||
return db.none(sql)
|
where timestamp < now() - interval '3 days';`
|
||||||
|
return db.multi(sqls)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
|
function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ const settingsLoader = require('./new-settings-loader')
|
||||||
const notifierUtils = require('./notifier/utils')
|
const notifierUtils = require('./notifier/utils')
|
||||||
const notifierQueries = require('./notifier/queries')
|
const notifierQueries = require('./notifier/queries')
|
||||||
|
|
||||||
|
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||||
|
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||||
|
const stuckStatus = { label: 'Stuck', type: 'error' }
|
||||||
|
|
||||||
function getMachines () {
|
function getMachines () {
|
||||||
return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
|
return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
|
||||||
.then(rr => rr.map(r => ({
|
.then(rr => rr.map(r => ({
|
||||||
|
|
@ -36,11 +40,32 @@ function getConfig (defaultConfig) {
|
||||||
return settingsLoader.loadLatest().config
|
return settingsLoader.loadLatest().config
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachineNames (config) {
|
const getStatus = (ping, stuck) => {
|
||||||
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
if (ping && ping.age) return unresponsiveStatus
|
||||||
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
|
||||||
const stuckStatus = { label: 'Stuck', type: 'error' }
|
|
||||||
|
|
||||||
|
if (stuck && stuck.age) return stuckStatus
|
||||||
|
|
||||||
|
return fullyFunctionalStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
function addName (pings, events, config) {
|
||||||
|
return machine => {
|
||||||
|
const cashOutConfig = configManager.getCashOut(machine.deviceId, config)
|
||||||
|
|
||||||
|
const cashOut = !!cashOutConfig.active
|
||||||
|
|
||||||
|
const statuses = [
|
||||||
|
getStatus(
|
||||||
|
_.first(pings[machine.deviceId]),
|
||||||
|
_.first(checkStuckScreen(events, machine.name))
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return _.assign(machine, { cashOut, statuses })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMachineNames (config) {
|
||||||
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
|
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
|
||||||
.then(([rawMachines, config, heartbeat, performance]) => Promise.all(
|
.then(([rawMachines, config, heartbeat, performance]) => Promise.all(
|
||||||
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
|
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
|
||||||
|
|
@ -91,9 +116,27 @@ function getMachineName (machineId) {
|
||||||
.then(it => it.name)
|
.then(it => it.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachine (machineId) {
|
function getMachine (machineId, config) {
|
||||||
const sql = 'SELECT * FROM devices WHERE device_id=$1'
|
const sql = 'SELECT * FROM devices WHERE device_id=$1'
|
||||||
return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res))
|
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => ({
|
||||||
|
deviceId: r.device_id,
|
||||||
|
cashbox: r.cashbox,
|
||||||
|
cassette1: r.cassette1,
|
||||||
|
cassette2: r.cassette2,
|
||||||
|
version: r.version,
|
||||||
|
model: r.model,
|
||||||
|
pairedAt: new Date(r.created),
|
||||||
|
lastPing: new Date(r.last_online),
|
||||||
|
name: r.name,
|
||||||
|
paired: r.paired
|
||||||
|
}))
|
||||||
|
|
||||||
|
return Promise.all([queryMachine, dbm.machineEvents(), config])
|
||||||
|
.then(([machine, events, config]) => {
|
||||||
|
const pings = checkPings([machine])
|
||||||
|
|
||||||
|
return [machine].map(addName(pings, events, config))[0]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameMachine (rec) {
|
function renameMachine (rec) {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const ALL_ACCOUNTS = [
|
||||||
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
||||||
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
||||||
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH, USDT] },
|
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH, USDT] },
|
||||||
{ code: 'geth', display: 'geth (DEPRECATED)', class: WALLET, cryptos: [ETH, USDT], deprecated: true },
|
{ code: 'geth', display: 'geth', class: WALLET, cryptos: [ETH, USDT] },
|
||||||
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
||||||
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
||||||
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
||||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status),
|
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status),
|
||||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone }]) =>
|
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone, simplified }]) =>
|
||||||
transactions.batch(from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status)
|
transactions.batch(from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, simplified)
|
||||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']), { fields: txLogFields })),
|
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']), { fields: txLogFields })),
|
||||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||||
transactions.getTx(id, txClass).then(data =>
|
transactions.getTx(id, txClass).then(data =>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const typeDef = gql`
|
||||||
lastTxFiatCode: String
|
lastTxFiatCode: String
|
||||||
lastTxClass: String
|
lastTxClass: String
|
||||||
transactions: [Transaction]
|
transactions: [Transaction]
|
||||||
|
subscriberInfo: JSONObject
|
||||||
}
|
}
|
||||||
|
|
||||||
input CustomerInput {
|
input CustomerInput {
|
||||||
|
|
@ -53,6 +54,7 @@ const typeDef = gql`
|
||||||
lastTxFiatCode: String
|
lastTxFiatCode: String
|
||||||
lastTxClass: String
|
lastTxClass: String
|
||||||
suspendedUntil: Date
|
suspendedUntil: Date
|
||||||
|
subscriberInfo: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ const typeDef = gql`
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String): [Transaction] @auth
|
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String): [Transaction] @auth
|
||||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, timezone: String): String @auth
|
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, timezone: String, simplified: Boolean): String @auth
|
||||||
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||||
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||||
transactionFilters: [Filter] @auth
|
transactionFilters: [Filter] @auth
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ function batch (
|
||||||
fiatCode = null,
|
fiatCode = null,
|
||||||
cryptoCode = null,
|
cryptoCode = null,
|
||||||
toAddress = null,
|
toAddress = null,
|
||||||
status = null
|
status = null,
|
||||||
|
simplified = false
|
||||||
) {
|
) {
|
||||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||||
|
|
||||||
|
|
@ -99,6 +100,55 @@ function batch (
|
||||||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])
|
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])
|
||||||
])
|
])
|
||||||
.then(packager)
|
.then(packager)
|
||||||
|
.then(res => {
|
||||||
|
if (simplified) return simplifiedBatch(res)
|
||||||
|
else return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifiedBatch (data) {
|
||||||
|
const fields = ['txClass', 'id', 'created', 'machineName',
|
||||||
|
'cryptoCode', 'fiat', 'fiatCode', 'phone', 'toAddress',
|
||||||
|
'txHash', 'dispense', 'error', 'status', 'fiatProfit', 'cryptoAmount']
|
||||||
|
|
||||||
|
const addSimplifiedFields = _.map(it => ({
|
||||||
|
...it,
|
||||||
|
status: getStatus(it),
|
||||||
|
fiatProfit: getProfit(it).toString(),
|
||||||
|
cryptoAmount: getCryptoAmount(it)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return _.compose(_.map(_.pick(fields)), addSimplifiedFields)(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode).toString()
|
||||||
|
|
||||||
|
const getProfit = it => {
|
||||||
|
const getCommissionFee = it => BN(it.commissionPercentage).mul(BN(it.fiat))
|
||||||
|
if (!it.cashInFee) return getCommissionFee(it)
|
||||||
|
return getCommissionFee(it).add(BN(it.cashInFee))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCashOutStatus = it => {
|
||||||
|
if (it.hasError) return 'Error'
|
||||||
|
if (it.dispense) return 'Success'
|
||||||
|
if (it.expired) return 'Expired'
|
||||||
|
return 'Pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCashInStatus = it => {
|
||||||
|
if (it.operatorCompleted) return 'Cancelled'
|
||||||
|
if (it.hasError) return 'Error'
|
||||||
|
if (it.sendConfirmed) return 'Sent'
|
||||||
|
if (it.expired) return 'Expired'
|
||||||
|
return 'Pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatus = it => {
|
||||||
|
if (it.txClass === 'cashOut') {
|
||||||
|
return getCashOutStatus(it)
|
||||||
|
}
|
||||||
|
return getCashInStatus(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustomerTransactionsBatch (ids) {
|
function getCustomerTransactionsBatch (ids) {
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,25 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
|
||||||
|
|
||||||
const getTriggers = _.get('triggers')
|
const getTriggers = _.get('triggers')
|
||||||
|
|
||||||
|
const getTriggersAutomation = config => {
|
||||||
|
const defaultAutomation = _.get('triggersConfig_automation')(config)
|
||||||
|
const requirements = {
|
||||||
|
sanctions: defaultAutomation,
|
||||||
|
idCardPhoto: defaultAutomation,
|
||||||
|
idCardData: defaultAutomation,
|
||||||
|
facephoto: defaultAutomation,
|
||||||
|
usSsn: defaultAutomation
|
||||||
|
}
|
||||||
|
|
||||||
|
const overrides = _.get('triggersConfig_overrides')(config)
|
||||||
|
|
||||||
|
const requirementsOverrides = _.reduce((acc, override) => {
|
||||||
|
return _.assign(acc, { [override.requirement]: override.automation })
|
||||||
|
}, {}, overrides)
|
||||||
|
|
||||||
|
return _.assign(requirements, requirementsOverrides)
|
||||||
|
}
|
||||||
|
|
||||||
const splitGetFirst = _.compose(_.head, _.split('_'))
|
const splitGetFirst = _.compose(_.head, _.split('_'))
|
||||||
|
|
||||||
const getCryptosFromWalletNamespace = config => {
|
const getCryptosFromWalletNamespace = config => {
|
||||||
|
|
@ -128,6 +147,7 @@ module.exports = {
|
||||||
getTermsConditions,
|
getTermsConditions,
|
||||||
getAllCryptoCurrencies,
|
getAllCryptoCurrencies,
|
||||||
getTriggers,
|
getTriggers,
|
||||||
|
getTriggersAutomation,
|
||||||
getCashOut,
|
getCashOut,
|
||||||
getCryptosFromWalletNamespace
|
getCryptosFromWalletNamespace
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
const rate = rawRate.div(cashInCommission)
|
const rate = rawRate.div(cashInCommission)
|
||||||
|
|
||||||
const lowBalanceMargin = new BN(1.03)
|
const lowBalanceMargin = new BN(1.05)
|
||||||
|
|
||||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||||
const unitScale = cryptoRec.unitScale
|
const unitScale = cryptoRec.unitScale
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
exports.NAME = 'MockSMS'
|
const NAME = 'MockSMS'
|
||||||
|
|
||||||
exports.sendMessage = function sendMessage (account, rec) {
|
function getLookup (account, number) {
|
||||||
|
console.log('Looking up number: %j', number)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (_.endsWith('666', number)) {
|
||||||
|
reject (new Error(`${exports.NAME} mocked error!`))
|
||||||
|
} else {
|
||||||
|
setTimeout(resolve, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage (account, rec) {
|
||||||
console.log('Sending SMS: %j', rec)
|
console.log('Sending SMS: %j', rec)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) {
|
if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) {
|
||||||
|
|
@ -12,3 +23,9 @@ exports.sendMessage = function sendMessage (account, rec) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
NAME,
|
||||||
|
sendMessage,
|
||||||
|
getLookup
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,27 @@ function sendMessage (account, rec) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLookup (account, number) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
const client = twilio(account.accountSid, account.authToken)
|
||||||
|
return client.lookups.v1.phoneNumbers(number)
|
||||||
|
.fetch({ addOns: ['lamassu_ekata'] })
|
||||||
|
})
|
||||||
|
.then(info => info.addOns.results['lamassu_ekata'])
|
||||||
|
.catch(err => {
|
||||||
|
if (_.includes(err.code, BAD_NUMBER_CODES)) {
|
||||||
|
const badNumberError = new Error(err.message)
|
||||||
|
badNumberError.name = 'BadNumberError'
|
||||||
|
throw badNumberError
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Twilio error: ${err.message}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
NAME,
|
NAME,
|
||||||
sendMessage
|
sendMessage,
|
||||||
|
getLookup
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ function balance (account, cryptoCode, settings, operatorId) {
|
||||||
|
|
||||||
const pendingBalance = (address, cryptoCode) => {
|
const pendingBalance = (address, cryptoCode) => {
|
||||||
const promises = [_balance(true, address, cryptoCode), _balance(false, address, cryptoCode)]
|
const promises = [_balance(true, address, cryptoCode), _balance(false, address, cryptoCode)]
|
||||||
return Promise.all(promises).then(([pending, confirmed]) => pending - confirmed)
|
return Promise.all(promises).then(([pending, confirmed]) => pending.minus(confirmed))
|
||||||
}
|
}
|
||||||
const confirmedBalance = (address, cryptoCode) => _balance(false, address, cryptoCode)
|
const confirmedBalance = (address, cryptoCode) => _balance(false, address, cryptoCode)
|
||||||
|
|
||||||
|
|
@ -126,6 +126,7 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
|
||||||
const contract = web3.eth.contract(ABI.ERC20).at(coins.utils.getErc20Token(cryptoCode).contractAddress)
|
const contract = web3.eth.contract(ABI.ERC20).at(coins.utils.getErc20Token(cryptoCode).contractAddress)
|
||||||
|
|
||||||
const rawTx = {
|
const rawTx = {
|
||||||
|
chainId: 1,
|
||||||
nonce: txCount,
|
nonce: txCount,
|
||||||
gasPrice: hex(gasPrice),
|
gasPrice: hex(gasPrice),
|
||||||
gasLimit: gas,
|
gasLimit: gas,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ function poll (req, res, next) {
|
||||||
const hasLightning = checkHasLightning(settings)
|
const hasLightning = checkHasLightning(settings)
|
||||||
|
|
||||||
const triggers = configManager.getTriggers(settings.config)
|
const triggers = configManager.getTriggers(settings.config)
|
||||||
|
const triggersAutomation = configManager.getTriggersAutomation(settings.config)
|
||||||
|
|
||||||
const operatorInfo = configManager.getOperatorInfo(settings.config)
|
const operatorInfo = configManager.getOperatorInfo(settings.config)
|
||||||
const machineInfo = { deviceId: req.deviceId, deviceName: req.deviceName }
|
const machineInfo = { deviceId: req.deviceId, deviceName: req.deviceName }
|
||||||
|
|
@ -82,7 +83,8 @@ function poll (req, res, next) {
|
||||||
receipt,
|
receipt,
|
||||||
operatorInfo,
|
operatorInfo,
|
||||||
machineInfo,
|
machineInfo,
|
||||||
triggers
|
triggers,
|
||||||
|
triggersAutomation
|
||||||
}
|
}
|
||||||
|
|
||||||
// BACKWARDS_COMPATIBILITY 7.5
|
// BACKWARDS_COMPATIBILITY 7.5
|
||||||
|
|
|
||||||
21
lib/sms.js
21
lib/sms.js
|
|
@ -1,15 +1,28 @@
|
||||||
const ph = require('./plugin-helper')
|
const ph = require('./plugin-helper')
|
||||||
const argv = require('minimist')(process.argv.slice(2))
|
const argv = require('minimist')(process.argv.slice(2))
|
||||||
|
|
||||||
function sendMessage (settings, rec) {
|
function getPlugin (settings) {
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
|
const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
|
||||||
const plugin = ph.load(ph.SMS, pluginCode)
|
const plugin = ph.load(ph.SMS, pluginCode)
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
|
|
||||||
|
return { plugin, account }
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage (settings, rec) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
const { plugin, account } = getPlugin(settings)
|
||||||
return plugin.sendMessage(account, rec)
|
return plugin.sendMessage(account, rec)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {sendMessage}
|
function getLookup (settings, number) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
const { plugin, account } = getPlugin(settings)
|
||||||
|
return plugin.getLookup(account, number)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { sendMessage, getLookup }
|
||||||
|
|
|
||||||
17
lib/tx.js
17
lib/tx.js
|
|
@ -3,6 +3,9 @@ const db = require('./db')
|
||||||
const BN = require('./bn')
|
const BN = require('./bn')
|
||||||
const CashInTx = require('./cash-in/cash-in-tx')
|
const CashInTx = require('./cash-in/cash-in-tx')
|
||||||
const CashOutTx = require('./cash-out/cash-out-tx')
|
const CashOutTx = require('./cash-out/cash-out-tx')
|
||||||
|
const T = require('./time')
|
||||||
|
|
||||||
|
const REDEEMABLE_AGE = T.day
|
||||||
|
|
||||||
function process (tx, pi) {
|
function process (tx, pi) {
|
||||||
const mtx = massage(tx, pi)
|
const mtx = massage(tx, pi)
|
||||||
|
|
@ -64,23 +67,27 @@ function cancel (txId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function customerHistory (customerId, thresholdDays) {
|
function customerHistory (customerId, thresholdDays) {
|
||||||
const sql = ` SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction
|
const sql = `SELECT * FROM (
|
||||||
|
SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction,
|
||||||
|
((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired
|
||||||
FROM cash_in_txs txIn
|
FROM cash_in_txs txIn
|
||||||
WHERE txIn.customer_id = $1
|
WHERE txIn.customer_id = $1
|
||||||
AND txIn.created > now() - interval $2
|
AND txIn.created > now() - interval $2
|
||||||
AND fiat > 0
|
AND fiat > 0
|
||||||
UNION
|
UNION
|
||||||
SELECT txOut.id, txOut.created, txOut.fiat, 'cashOut' AS direction
|
SELECT txOut.id, txOut.created, txOut.fiat, 'cashOut' AS direction,
|
||||||
|
(extract(epoch FROM (now() - greatest(txOut.created, txOut.confirmed_at))) * 1000) >= $4 AS expired
|
||||||
FROM cash_out_txs txOut
|
FROM cash_out_txs txOut
|
||||||
WHERE txOut.customer_id = $1
|
WHERE txOut.customer_id = $1
|
||||||
AND txOut.created > now() - interval $2
|
AND txOut.created > now() - interval $2
|
||||||
AND (timedout = true OR error_code != 'operatorCancel')
|
AND error_code IS DISTINCT FROM 'operatorCancel'
|
||||||
AND fiat > 0
|
AND fiat > 0
|
||||||
ORDER BY created;`
|
) ch WHERE NOT ch.expired ORDER BY ch.created`
|
||||||
|
|
||||||
const days = _.isNil(thresholdDays) ? 0 : thresholdDays
|
const days = _.isNil(thresholdDays) ? 0 : thresholdDays
|
||||||
return db.any(sql, [customerId, `${days} days`])
|
return db.any(sql, [customerId, `${days} days`, '60 minutes', REDEEMABLE_AGE])
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { post, cancel, customerHistory }
|
module.exports = { post, cancel, customerHistory }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const settingsLoader = require('../lib/admin/settings-loader')
|
|
||||||
const machineLoader = require('../lib/machine-loader')
|
const machineLoader = require('../lib/machine-loader')
|
||||||
const { saveConfig, saveAccounts } = require('../lib/new-settings-loader')
|
const { saveConfig, saveAccounts, loadLatest } = require('../lib/new-settings-loader')
|
||||||
const { migrate } = require('../lib/config-migration')
|
const { migrate } = require('../lib/config-migration')
|
||||||
|
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const OLD_SETTINGS_LOADER_SCHEMA_VERSION = 1
|
||||||
|
|
||||||
module.exports.up = function (next) {
|
module.exports.up = function (next) {
|
||||||
function migrateConfig(settings) {
|
function migrateConfig (settings) {
|
||||||
const newSettings = migrate(settings.config, settings.accounts)
|
const newSettings = migrate(settings.config, settings.accounts)
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
saveConfig(newSettings.config),
|
saveConfig(newSettings.config),
|
||||||
|
|
@ -14,22 +17,34 @@ module.exports.up = function (next) {
|
||||||
.then(() => next())
|
.then(() => next())
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsLoader.loadLatest(false)
|
loadLatest(OLD_SETTINGS_LOADER_SCHEMA_VERSION)
|
||||||
.then(async settings => ({
|
.then(async settings => {
|
||||||
|
if (_.isEmpty(settings.config)) {
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
machines: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
settings,
|
settings,
|
||||||
machines: await machineLoader.getMachineNames(settings.config)
|
machines: await machineLoader.getMachineNames(settings.config)
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
.then(({ settings, machines }) => {
|
.then(({ settings, machines }) => {
|
||||||
|
if (_.isEmpty(settings.config)) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
const sql = machines
|
const sql = machines
|
||||||
? machines.map(m => `update devices set name = '${m.name}' where device_id = '${m.deviceId}'`)
|
? machines.map(m => `update devices set name = '${m.name}' where device_id = '${m.deviceId}'`)
|
||||||
: []
|
: []
|
||||||
return db.multi(sql, () => migrateConfig(settings))
|
return db.multi(sql, () => migrateConfig(settings))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.message = 'lamassu-server is not configured')
|
if (err.message === 'lamassu-server is not configured') {
|
||||||
next()
|
return next()
|
||||||
|
}
|
||||||
console.log(err.message)
|
console.log(err.message)
|
||||||
|
return next(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
const sql = [
|
||||||
|
`ALTER TYPE compliance_type ADD VALUE 'us_ssn'`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
15
migrations/1628100660620-subscriber-info.js
Normal file
15
migrations/1628100660620-subscriber-info.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
var db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
`ALTER TABLE customers ADD COLUMN subscriber_info JSON`,
|
||||||
|
`ALTER TABLE customers ADD COLUMN subscriber_info_at TIMESTAMPTZ`,
|
||||||
|
`ALTER TABLE customers ADD COLUMN subscriber_info_by UUID REFERENCES users(id)`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
34
new-lamassu-admin/package-lock.json
generated
34
new-lamassu-admin/package-lock.json
generated
|
|
@ -14909,6 +14909,11 @@
|
||||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-finite": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w=="
|
||||||
|
},
|
||||||
"is-fullwidth-code-point": {
|
"is-fullwidth-code-point": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||||
|
|
@ -17152,7 +17157,7 @@
|
||||||
"version": "git+https://github.com/lamassu/lamassu-coins.git#f80395e4bab0fccc860de166c97e981ca3ae66a6",
|
"version": "git+https://github.com/lamassu/lamassu-coins.git#f80395e4bab0fccc860de166c97e981ca3ae66a6",
|
||||||
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bech32": "^1.1.3",
|
"bech32": "2.0.0",
|
||||||
"bignumber.js": "^9.0.0",
|
"bignumber.js": "^9.0.0",
|
||||||
"bitcoinjs-lib": "4.0.3",
|
"bitcoinjs-lib": "4.0.3",
|
||||||
"bs58check": "^2.0.2",
|
"bs58check": "^2.0.2",
|
||||||
|
|
@ -17160,6 +17165,13 @@
|
||||||
"crypto-js": "^3.1.9-1",
|
"crypto-js": "^3.1.9-1",
|
||||||
"ethereumjs-icap": "^0.3.1",
|
"ethereumjs-icap": "^0.3.1",
|
||||||
"lodash": "^4.17.10"
|
"lodash": "^4.17.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bech32": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"language-subtag-registry": {
|
"language-subtag-registry": {
|
||||||
|
|
@ -19150,6 +19162,11 @@
|
||||||
"json-parse-better-errors": "^1.0.1"
|
"json-parse-better-errors": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parse-ms": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0="
|
||||||
|
},
|
||||||
"parse5": {
|
"parse5": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||||
|
|
@ -19431,6 +19448,11 @@
|
||||||
"semver-compare": "^1.0.0"
|
"semver-compare": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plur": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY="
|
||||||
|
},
|
||||||
"pnp-webpack-plugin": {
|
"pnp-webpack-plugin": {
|
||||||
"version": "1.6.4",
|
"version": "1.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
|
||||||
|
|
@ -20672,6 +20694,16 @@
|
||||||
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
|
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"pretty-ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=",
|
||||||
|
"requires": {
|
||||||
|
"is-finite": "^1.0.1",
|
||||||
|
"parse-ms": "^1.0.0",
|
||||||
|
"plur": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"prismjs": {
|
"prismjs": {
|
||||||
"version": "1.23.0",
|
"version": "1.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
"libphonenumber-js": "^1.7.50",
|
"libphonenumber-js": "^1.7.50",
|
||||||
"match-sorter": "^4.2.0",
|
"match-sorter": "^4.2.0",
|
||||||
"moment": "2.24.0",
|
"moment": "2.24.0",
|
||||||
|
"pretty-ms": "^2.1.0",
|
||||||
"qrcode.react": "0.9.3",
|
"qrcode.react": "0.9.3",
|
||||||
"ramda": "^0.26.1",
|
"ramda": "^0.26.1",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,8 @@ const styles = {
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
const ALL = 'all'
|
const ALL = 'all'
|
||||||
const RANGE = 'range'
|
const RANGE = 'range'
|
||||||
|
const ADVANCED = 'advanced'
|
||||||
|
const SIMPLIFIED = 'simplified'
|
||||||
|
|
||||||
const LogsDownloaderPopover = ({
|
const LogsDownloaderPopover = ({
|
||||||
name,
|
name,
|
||||||
|
|
@ -136,9 +138,12 @@ const LogsDownloaderPopover = ({
|
||||||
args,
|
args,
|
||||||
title,
|
title,
|
||||||
getLogs,
|
getLogs,
|
||||||
timezone
|
timezone,
|
||||||
|
simplified
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedRadio, setSelectedRadio] = useState(ALL)
|
const [selectedRadio, setSelectedRadio] = useState(ALL)
|
||||||
|
const [selectedAdvancedRadio, setSelectedAdvancedRadio] = useState(ADVANCED)
|
||||||
|
|
||||||
const [range, setRange] = useState({ from: null, until: null })
|
const [range, setRange] = useState({ from: null, until: null })
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const [fetchLogs] = useLazyQuery(query, {
|
const [fetchLogs] = useLazyQuery(query, {
|
||||||
|
|
@ -158,6 +163,11 @@ const LogsDownloaderPopover = ({
|
||||||
if (selectedRadio === ALL) setRange({ from: null, until: null })
|
if (selectedRadio === ALL) setRange({ from: null, until: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAdvancedRadioButtons = evt => {
|
||||||
|
const selectedAdvancedRadio = R.path(['target', 'value'])(evt)
|
||||||
|
setSelectedAdvancedRadio(selectedAdvancedRadio)
|
||||||
|
}
|
||||||
|
|
||||||
const handleRangeChange = useCallback(
|
const handleRangeChange = useCallback(
|
||||||
(from, until) => {
|
(from, until) => {
|
||||||
setRange({ from, until })
|
setRange({ from, until })
|
||||||
|
|
@ -165,11 +175,12 @@ const LogsDownloaderPopover = ({
|
||||||
[setRange]
|
[setRange]
|
||||||
)
|
)
|
||||||
|
|
||||||
const downloadLogs = (range, args, fetchLogs) => {
|
const downloadLogs = (range, args) => {
|
||||||
if (selectedRadio === ALL) {
|
if (selectedRadio === ALL) {
|
||||||
fetchLogs({
|
fetchLogs({
|
||||||
variables: {
|
variables: {
|
||||||
...args
|
...args,
|
||||||
|
simplified: selectedAdvancedRadio === SIMPLIFIED
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +194,8 @@ const LogsDownloaderPopover = ({
|
||||||
variables: {
|
variables: {
|
||||||
...args,
|
...args,
|
||||||
from: range.from,
|
from: range.from,
|
||||||
until: range.until
|
until: range.until,
|
||||||
|
simplified: selectedAdvancedRadio === SIMPLIFIED
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +233,11 @@ const LogsDownloaderPopover = ({
|
||||||
{ display: 'Date range', code: RANGE }
|
{ display: 'Date range', code: RANGE }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const advancedRadioButtonOptions = [
|
||||||
|
{ display: 'Advanced logs', code: ADVANCED },
|
||||||
|
{ display: 'Simplified logs', code: SIMPLIFIED }
|
||||||
|
]
|
||||||
|
|
||||||
const open = Boolean(anchorEl)
|
const open = Boolean(anchorEl)
|
||||||
const id = open ? 'date-range-popover' : undefined
|
const id = open ? 'date-range-popover' : undefined
|
||||||
|
|
||||||
|
|
@ -265,10 +282,20 @@ const LogsDownloaderPopover = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{simplified && (
|
||||||
|
<div className={classes.radioButtonsContainer}>
|
||||||
|
<RadioGroup
|
||||||
|
name="simplified-tx-logs"
|
||||||
|
value={selectedAdvancedRadio}
|
||||||
|
options={advancedRadioButtonOptions}
|
||||||
|
ariaLabel="simplified-tx-logs"
|
||||||
|
onChange={handleAdvancedRadioButtons}
|
||||||
|
className={classes.radioButtons}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={classes.download}>
|
<div className={classes.download}>
|
||||||
<Link
|
<Link color="primary" onClick={() => downloadLogs(range, args)}>
|
||||||
color="primary"
|
|
||||||
onClick={() => downloadLogs(range, args, fetchLogs)}>
|
|
||||||
Download
|
Download
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import moment from 'moment'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
import Calendar from './Calendar'
|
import Calendar from './Calendar'
|
||||||
|
|
@ -37,7 +38,7 @@ const DateRangePicker = ({ minDate, maxDate, className, onRangeChange }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (from && !to && day.isSameOrAfter(from, 'day')) {
|
if (from && !to && day.isSameOrAfter(from, 'day')) {
|
||||||
setTo(day)
|
setTo(moment(day.toDate().setHours(23, 59, 59, 999)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import React, { memo, useState } from 'react'
|
||||||
|
|
||||||
|
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
||||||
|
import ActionButton from 'src/components/buttons/ActionButton'
|
||||||
|
import { ReactComponent as EditReversedIcon } from 'src/styling/icons/button/edit/white.svg'
|
||||||
|
import { ReactComponent as EditIcon } from 'src/styling/icons/button/edit/zodiac.svg'
|
||||||
|
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
|
||||||
|
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
|
||||||
|
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
|
||||||
|
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
|
||||||
|
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
|
||||||
|
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
|
||||||
|
|
||||||
|
import { machineActionsStyles } from './MachineActions.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(machineActionsStyles)
|
||||||
|
|
||||||
|
const MACHINE_ACTION = gql`
|
||||||
|
mutation MachineAction(
|
||||||
|
$deviceId: ID!
|
||||||
|
$action: MachineAction!
|
||||||
|
$newName: String
|
||||||
|
) {
|
||||||
|
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
|
||||||
|
deviceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MACHINE = gql`
|
||||||
|
query getMachine($deviceId: ID!) {
|
||||||
|
machine(deviceId: $deviceId) {
|
||||||
|
latestEvent {
|
||||||
|
note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const isStaticState = machineState => {
|
||||||
|
if (!machineState) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const staticStates = [
|
||||||
|
'chooseCoin',
|
||||||
|
'idle',
|
||||||
|
'pendingIdle',
|
||||||
|
'dualIdle',
|
||||||
|
'networkDown',
|
||||||
|
'unpaired',
|
||||||
|
'maintenance',
|
||||||
|
'virgin',
|
||||||
|
'wifiList'
|
||||||
|
]
|
||||||
|
return staticStates.includes(machineState)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getState = machineEventsLazy =>
|
||||||
|
JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}')
|
||||||
|
.state
|
||||||
|
|
||||||
|
const Label = ({ children }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return <div className={classes.label}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const MachineActions = memo(({ machine, onActionSuccess }) => {
|
||||||
|
const [action, setAction] = useState({ command: null })
|
||||||
|
const [errorMessage, setErrorMessage] = useState(null)
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const warningMessage = (
|
||||||
|
<span className={classes.warning}>
|
||||||
|
A user may be in the middle of a transaction and they could lose their
|
||||||
|
funds if you continue.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
const [fetchMachineEvents, { loading: loadingEvents }] = useLazyQuery(
|
||||||
|
MACHINE,
|
||||||
|
{
|
||||||
|
variables: {
|
||||||
|
deviceId: machine.deviceId
|
||||||
|
},
|
||||||
|
onCompleted: machineEventsLazy => {
|
||||||
|
const message = !isStaticState(getState(machineEventsLazy))
|
||||||
|
? warningMessage
|
||||||
|
: null
|
||||||
|
setAction(action => ({ ...action, message }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
|
||||||
|
onError: ({ message }) => {
|
||||||
|
const errorMessage = message ?? 'An error ocurred'
|
||||||
|
setErrorMessage(errorMessage)
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
onActionSuccess && onActionSuccess()
|
||||||
|
setAction({ command: null })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const confirmDialogOpen = Boolean(action.command)
|
||||||
|
const disabled = !!(action?.command === 'restartServices' && loadingEvents)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Label>Actions</Label>
|
||||||
|
<div className={classes.stack}>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
className={classes.mr}
|
||||||
|
Icon={EditIcon}
|
||||||
|
InverseIcon={EditReversedIcon}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() =>
|
||||||
|
setAction({
|
||||||
|
command: 'rename',
|
||||||
|
display: 'Rename',
|
||||||
|
confirmationMessage: 'Write the new name for this machine'
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
Rename
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
className={classes.mr}
|
||||||
|
Icon={UnpairIcon}
|
||||||
|
InverseIcon={UnpairReversedIcon}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() =>
|
||||||
|
setAction({
|
||||||
|
command: 'unpair',
|
||||||
|
display: 'Unpair'
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
Unpair
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
className={classes.mr}
|
||||||
|
Icon={RebootIcon}
|
||||||
|
InverseIcon={RebootReversedIcon}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() =>
|
||||||
|
setAction({
|
||||||
|
command: 'reboot',
|
||||||
|
display: 'Reboot'
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
Reboot
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
className={classes.mr}
|
||||||
|
Icon={ShutdownIcon}
|
||||||
|
InverseIcon={ShutdownReversedIcon}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() =>
|
||||||
|
setAction({
|
||||||
|
command: 'shutdown',
|
||||||
|
display: 'Shutdown',
|
||||||
|
message:
|
||||||
|
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
Shutdown
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
className={classes.inlineChip}
|
||||||
|
Icon={RebootIcon}
|
||||||
|
InverseIcon={RebootReversedIcon}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
fetchMachineEvents()
|
||||||
|
setAction({
|
||||||
|
command: 'restartServices',
|
||||||
|
display: 'Restart services for'
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
Restart Services
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
disabled={disabled}
|
||||||
|
open={confirmDialogOpen}
|
||||||
|
title={`${action?.display} this machine?`}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
toBeConfirmed={machine.name}
|
||||||
|
message={action?.message}
|
||||||
|
confirmationMessage={action?.confirmationMessage}
|
||||||
|
saveButtonAlwaysEnabled={action?.command === 'rename'}
|
||||||
|
onConfirmed={value => {
|
||||||
|
setErrorMessage(null)
|
||||||
|
machineAction({
|
||||||
|
variables: {
|
||||||
|
deviceId: machine.deviceId,
|
||||||
|
action: `${action?.command}`,
|
||||||
|
...(action?.command === 'rename' && { newName: value })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onDissmised={() => {
|
||||||
|
setAction({ command: null })
|
||||||
|
setErrorMessage(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MachineActions
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import typographyStyles from 'src/components/typography/styles'
|
||||||
|
import { offColor, spacer, errorColor } from 'src/styling/variables'
|
||||||
|
|
||||||
|
const { label1 } = typographyStyles
|
||||||
|
|
||||||
|
const machineActionsStyles = {
|
||||||
|
label: {
|
||||||
|
extend: label1,
|
||||||
|
color: offColor,
|
||||||
|
marginBottom: 4
|
||||||
|
},
|
||||||
|
inlineChip: {
|
||||||
|
marginInlineEnd: '0.25em'
|
||||||
|
},
|
||||||
|
stack: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'start'
|
||||||
|
},
|
||||||
|
mr: {
|
||||||
|
marginRight: spacer,
|
||||||
|
marginBottom: spacer
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
color: errorColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { machineActionsStyles }
|
||||||
|
|
@ -49,7 +49,7 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [showMachines, setShowMachines] = useState(false)
|
const [showMachines, setShowMachines] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const { data } = useQuery(GET_DATA)
|
const { data, loading } = useQuery(GET_DATA)
|
||||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||||
refetchQueries: () => ['getData'],
|
refetchQueries: () => ['getData'],
|
||||||
onError: error => setError(error)
|
onError: error => setError(error)
|
||||||
|
|
@ -118,7 +118,7 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
||||||
iconClassName={classes.listViewButton}
|
iconClassName={classes.listViewButton}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!showMachines && (
|
{!showMachines && !loading && (
|
||||||
<CommissionsDetails
|
<CommissionsDetails
|
||||||
config={config}
|
config={config}
|
||||||
locale={localeConfig}
|
locale={localeConfig}
|
||||||
|
|
@ -130,7 +130,7 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
||||||
classes={classes}
|
classes={classes}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showMachines && (
|
{showMachines && !loading && (
|
||||||
<CommissionsList
|
<CommissionsList
|
||||||
config={config}
|
config={config}
|
||||||
localeConfig={localeConfig}
|
localeConfig={localeConfig}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ const CommissionsList = memo(
|
||||||
const [coinFilter, setCoinFilter] = useState(SHOW_ALL)
|
const [coinFilter, setCoinFilter] = useState(SHOW_ALL)
|
||||||
const [orderProp, setOrderProp] = useState(ORDER_OPTIONS[0])
|
const [orderProp, setOrderProp] = useState(ORDER_OPTIONS[0])
|
||||||
|
|
||||||
const coins = R.prop('cryptoCurrencies', localeConfig)
|
const coins = R.prop('cryptoCurrencies', localeConfig) ?? []
|
||||||
|
|
||||||
const getMachineCoins = deviceId => {
|
const getMachineCoins = deviceId => {
|
||||||
const override = R.prop('overrides', localeConfig)?.find(
|
const override = R.prop('overrides', localeConfig)?.find(
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,7 @@ const createCommissions = (cryptoCode, deviceId, isDefault, config) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCommissions = (cryptoCode, deviceId, config) => {
|
const getCommissions = (cryptoCode, deviceId, config) => {
|
||||||
const overrides = R.prop('overrides', config)
|
const overrides = R.prop('overrides', config) ?? []
|
||||||
|
|
||||||
if (!overrides && R.isEmpty(overrides)) {
|
if (!overrides && R.isEmpty(overrides)) {
|
||||||
return createCommissions(cryptoCode, deviceId, true, config)
|
return createCommissions(cryptoCode, deviceId, true, config)
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ const SET_CUSTOMER = gql`
|
||||||
lastTxFiat
|
lastTxFiat
|
||||||
lastTxFiatCode
|
lastTxFiatCode
|
||||||
lastTxClass
|
lastTxClass
|
||||||
|
subscriberInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
@ -202,6 +203,24 @@ const CustomerProfile = memo(() => {
|
||||||
}>
|
}>
|
||||||
{`${blocked ? 'Authorize' : 'Block'} customer`}
|
{`${blocked ? 'Authorize' : 'Block'} customer`}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||||
|
InverseIcon={
|
||||||
|
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setCustomer({
|
||||||
|
variables: {
|
||||||
|
customerId,
|
||||||
|
customerInput: {
|
||||||
|
subscriberInfo: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
{`Retrieve information`}
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const IdDataCard = memo(({ customerData, updateCustomer }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Gender',
|
header: 'Gender',
|
||||||
display: R.path(['gender'])(idData),
|
display: R.path(['gender'])(idData) ?? R.path(['sex'])(idData),
|
||||||
size: 80
|
size: 80
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,27 +31,29 @@ BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const { data, loading } = useQuery(GET_DATA)
|
const { data } = useQuery(GET_DATA)
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [delayedExpand, setDelayedExpand] = useState(null)
|
const [delayedExpand, setDelayedExpand] = useState(null)
|
||||||
|
|
||||||
|
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
|
||||||
const classes = useStyles({
|
const classes = useStyles({
|
||||||
bigFooter: R.keys(data?.cryptoRates?.withCommissions).length > 8,
|
bigFooter: R.keys(withCommissions).length > 8,
|
||||||
expanded
|
expanded
|
||||||
})
|
})
|
||||||
|
const config = R.path(['config'])(data) ?? {}
|
||||||
|
const canExpand = R.keys(withCommissions).length > 4
|
||||||
|
|
||||||
const canExpand = R.keys(data?.cryptoRates.withCommissions ?? []).length > 4
|
const wallets = fromNamespace('wallets')(config)
|
||||||
|
const cryptoCurrencies = R.path(['cryptoCurrencies'])(data) ?? []
|
||||||
const wallets = fromNamespace('wallets')(data?.config)
|
const accountsConfig = R.path(['accountsConfig'])(data) ?? []
|
||||||
|
const localeFiatCurrency = R.path(['locale_fiatCurrency'])(config) ?? ''
|
||||||
|
|
||||||
const renderFooterItem = key => {
|
const renderFooterItem = key => {
|
||||||
const idx = R.findIndex(R.propEq('code', key))(data.cryptoCurrencies)
|
const idx = R.findIndex(R.propEq('code', key))(cryptoCurrencies)
|
||||||
const tickerCode = wallets[`${key}_ticker`]
|
const tickerCode = wallets[`${key}_ticker`]
|
||||||
const tickerIdx = R.findIndex(R.propEq('code', tickerCode))(
|
const tickerIdx = R.findIndex(R.propEq('code', tickerCode))(accountsConfig)
|
||||||
data.accountsConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
const tickerName = data.accountsConfig[tickerIdx].display
|
const tickerName = tickerIdx > -1 ? accountsConfig[tickerIdx].display : ''
|
||||||
|
|
||||||
const cashInNoCommission = parseFloat(
|
const cashInNoCommission = parseFloat(
|
||||||
R.path(['cryptoRates', 'withoutCommissions', key, 'cashIn'])(data)
|
R.path(['cryptoRates', 'withoutCommissions', key, 'cashIn'])(data)
|
||||||
|
|
@ -74,12 +76,10 @@ const Footer = () => {
|
||||||
)
|
)
|
||||||
).toFormat(2)
|
).toFormat(2)
|
||||||
|
|
||||||
const localeFiatCurrency = data.config.locale_fiatCurrency
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid key={key} item xs={3}>
|
<Grid key={key} item xs={3}>
|
||||||
<Label2 className={classes.label}>
|
<Label2 className={classes.label}>
|
||||||
{data.cryptoCurrencies[idx].display}
|
{cryptoCurrencies[idx].display}
|
||||||
</Label2>
|
</Label2>
|
||||||
<div className={classes.headerLabels}>
|
<div className={classes.headerLabels}>
|
||||||
<div className={classes.headerLabel}>
|
<div className={classes.headerLabel}>
|
||||||
|
|
@ -116,15 +116,11 @@ const Footer = () => {
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
/>
|
/>
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
{!loading && data && (
|
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid container className={classes.footerContainer}>
|
<Grid container className={classes.footerContainer}>
|
||||||
{R.keys(data.cryptoRates.withCommissions).map(key =>
|
{R.keys(withCommissions).map(key => renderFooterItem(key))}
|
||||||
renderFooterItem(key)
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.footer} />
|
<div className={classes.footer} />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { Status } from 'src/components/Status'
|
||||||
import { Label2, TL2 } from 'src/components/typography'
|
import { Label2, TL2 } from 'src/components/typography'
|
||||||
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
// 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 TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
|
import { ReactComponent as MachineLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
||||||
|
|
||||||
import styles from './MachinesTable.styles'
|
import styles from './MachinesTable.styles'
|
||||||
|
|
||||||
|
|
@ -99,10 +100,19 @@ const MachinesTable = ({ machines, numToRender }) => {
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
onClick={() => redirect(machine)}
|
onClick={() => redirect(machine)}
|
||||||
className={classnames(classes.row, classes.clickableRow)}
|
className={classnames(classes.row)}
|
||||||
key={machine.deviceId + idx}>
|
key={machine.deviceId + idx}>
|
||||||
<StyledCell align="left">
|
<StyledCell
|
||||||
|
align="left"
|
||||||
|
className={classes.machineNameWrapper}>
|
||||||
<TL2>{machine.name}</TL2>
|
<TL2>{machine.name}</TL2>
|
||||||
|
<MachineLinkIcon
|
||||||
|
className={classnames(
|
||||||
|
classes.machineRedirectIcon,
|
||||||
|
classes.clickableRow
|
||||||
|
)}
|
||||||
|
onClick={() => redirect(machine)}
|
||||||
|
/>
|
||||||
</StyledCell>
|
</StyledCell>
|
||||||
<StyledCell>
|
<StyledCell>
|
||||||
<Status status={machine.statuses[0]} />
|
<Status status={machine.statuses[0]} />
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,14 @@ const styles = {
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
machineNameWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
machineRedirectIcon: {
|
||||||
|
marginLeft: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
||||||
name: 'cashbox',
|
name: 'cashbox',
|
||||||
header: 'Cashbox',
|
header: 'Cashbox',
|
||||||
width: 240,
|
width: 240,
|
||||||
stripe: true,
|
stripe: false,
|
||||||
view: value => (
|
view: value => (
|
||||||
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
|
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
|
||||||
),
|
),
|
||||||
|
|
@ -90,7 +90,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
||||||
view: (value, { deviceId }) => (
|
view: (value, { deviceId }) => (
|
||||||
<CashOut
|
<CashOut
|
||||||
className={classes.cashbox}
|
className={classes.cashbox}
|
||||||
denomination={getCashoutSettings(deviceId)?.bottom}
|
denomination={getCashoutSettings(deviceId)?.top}
|
||||||
currency={{ code: fiatCurrency }}
|
currency={{ code: fiatCurrency }}
|
||||||
notes={value}
|
notes={value}
|
||||||
/>
|
/>
|
||||||
|
|
@ -109,7 +109,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
||||||
return (
|
return (
|
||||||
<CashOut
|
<CashOut
|
||||||
className={classes.cashbox}
|
className={classes.cashbox}
|
||||||
denomination={getCashoutSettings(deviceId)?.top}
|
denomination={getCashoutSettings(deviceId)?.bottom}
|
||||||
currency={{ code: fiatCurrency }}
|
currency={{ code: fiatCurrency }}
|
||||||
notes={value}
|
notes={value}
|
||||||
/>
|
/>
|
||||||
|
|
@ -145,7 +145,6 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
||||||
disableRowEdit={isCashOutDisabled}
|
disableRowEdit={isCashOutDisabled}
|
||||||
name="cashboxes"
|
name="cashboxes"
|
||||||
elements={elements}
|
elements={elements}
|
||||||
enableEdit
|
|
||||||
data={[machine] || []}
|
data={[machine] || []}
|
||||||
save={onSave}
|
save={onSave}
|
||||||
validationSchema={ValidationSchema}
|
validationSchema={ValidationSchema}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,15 @@
|
||||||
import { useMutation } from '@apollo/react-hooks'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
|
||||||
import { Status } from 'src/components/Status'
|
import { Status } from 'src/components/Status'
|
||||||
import ActionButton from 'src/components/buttons/ActionButton'
|
import MachineActions from 'src/components/machineActions/MachineActions'
|
||||||
import { H3, Label3, P } from 'src/components/typography'
|
import { H3, Label3, P } from 'src/components/typography'
|
||||||
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
|
|
||||||
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
|
|
||||||
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
|
|
||||||
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
|
|
||||||
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
|
|
||||||
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
|
|
||||||
|
|
||||||
import styles from '../Machines.styles'
|
import styles from '../Machines.styles'
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const MACHINE_ACTION = gql`
|
|
||||||
mutation MachineAction(
|
|
||||||
$deviceId: ID!
|
|
||||||
$action: MachineAction!
|
|
||||||
$newName: String
|
|
||||||
) {
|
|
||||||
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
|
|
||||||
deviceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const makeLastPing = lastPing => {
|
const makeLastPing = lastPing => {
|
||||||
if (!lastPing) return null
|
if (!lastPing) return null
|
||||||
const now = moment()
|
const now = moment()
|
||||||
|
|
@ -51,25 +30,8 @@ const makeLastPing = lastPing => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Overview = ({ data, onActionSuccess }) => {
|
const Overview = ({ data, onActionSuccess }) => {
|
||||||
const [action, setAction] = useState('')
|
|
||||||
const [confirmActionDialogOpen, setConfirmActionDialogOpen] = useState(false)
|
|
||||||
const [errorMessage, setErrorMessage] = useState(null)
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const [machineAction] = useMutation(MACHINE_ACTION, {
|
|
||||||
onError: ({ message }) => {
|
|
||||||
const errorMessage = message ?? 'An error ocurred'
|
|
||||||
setErrorMessage(errorMessage)
|
|
||||||
},
|
|
||||||
onCompleted: () => {
|
|
||||||
onActionSuccess && onActionSuccess()
|
|
||||||
setConfirmActionDialogOpen(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const confirmActionDialog = action =>
|
|
||||||
setAction(action) || setConfirmActionDialogOpen(true)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes.row}>
|
<div className={classes.row}>
|
||||||
|
|
@ -101,78 +63,10 @@ const Overview = ({ data, onActionSuccess }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.row}>
|
<div className={classes.row}>
|
||||||
<div className={classes.rowItem}>
|
<MachineActions
|
||||||
<Label3 className={classes.label3}>Latency</Label3>
|
machine={data}
|
||||||
<P>
|
onActionSuccess={onActionSuccess}></MachineActions>
|
||||||
{data.responseTime
|
|
||||||
? new BigNumber(data.responseTime).toFixed(3).toString() + ' ms'
|
|
||||||
: 'unavailable'}
|
|
||||||
</P>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={classes.row}>
|
|
||||||
<div className={classes.rowItem}>
|
|
||||||
<Label3 className={classes.label3}>Packet Loss</Label3>
|
|
||||||
<P>
|
|
||||||
{data.packetLoss
|
|
||||||
? new BigNumber(data.packetLoss).toFixed(3).toString() + ' %'
|
|
||||||
: 'unavailable'}
|
|
||||||
</P>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={classes.row}>
|
|
||||||
<div className={classes.rowItem}>
|
|
||||||
{' '}
|
|
||||||
<Label3 className={classes.label3}>Actions</Label3>
|
|
||||||
{data.name && (
|
|
||||||
<div className={classes.actionButtonsContainer}>
|
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.actionButton}
|
|
||||||
Icon={UnpairIcon}
|
|
||||||
InverseIcon={UnpairReversedIcon}
|
|
||||||
onClick={() => confirmActionDialog('Unpair')}>
|
|
||||||
Unpair
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.actionButton}
|
|
||||||
Icon={RebootIcon}
|
|
||||||
InverseIcon={RebootReversedIcon}
|
|
||||||
onClick={() => confirmActionDialog('Reboot')}>
|
|
||||||
Reboot
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
className={classes.actionButton}
|
|
||||||
color="primary"
|
|
||||||
Icon={ShutdownIcon}
|
|
||||||
InverseIcon={ShutdownReversedIcon}
|
|
||||||
onClick={() => confirmActionDialog('Shutdown')}>
|
|
||||||
Shutdown
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ConfirmDialog
|
|
||||||
open={confirmActionDialogOpen}
|
|
||||||
title={`${action} this machine?`}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
toBeConfirmed={data.name}
|
|
||||||
onConfirmed={() => {
|
|
||||||
setErrorMessage(null)
|
|
||||||
machineAction({
|
|
||||||
variables: {
|
|
||||||
deviceId: data.deviceId,
|
|
||||||
action: `${action}`.toLowerCase()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onDissmised={() => {
|
|
||||||
setConfirmActionDialogOpen(false)
|
|
||||||
setErrorMessage(null)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ const DataTable = ({
|
||||||
useEffect(() => setExpanded(initialExpanded), [initialExpanded])
|
useEffect(() => setExpanded(initialExpanded), [initialExpanded])
|
||||||
|
|
||||||
const coreWidth = R.compose(R.sum, R.map(R.prop('width')))(elements)
|
const coreWidth = R.compose(R.sum, R.map(R.prop('width')))(elements)
|
||||||
const expWidth = 1000 - coreWidth
|
const expWidth = 850 - coreWidth
|
||||||
const width = coreWidth + (expandable ? expWidth : 0)
|
const width = coreWidth + (expandable ? expWidth : 0)
|
||||||
|
|
||||||
const classes = useStyles({ width })
|
const classes = useStyles({ width })
|
||||||
|
|
@ -166,7 +166,7 @@ const DataTable = ({
|
||||||
{() => (
|
{() => (
|
||||||
<List
|
<List
|
||||||
// this has to be in a style because of how the component works
|
// this has to be in a style because of how the component works
|
||||||
style={{ overflow: 'inherit', outline: 'none' }}
|
style={{ overflowX: 'inherit', outline: 'none' }}
|
||||||
{...props}
|
{...props}
|
||||||
height={data.length * 62 + extraHeight}
|
height={data.length * 62 + extraHeight}
|
||||||
width={width}
|
width={width}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ const GET_TRANSACTIONS = gql`
|
||||||
customerIdCardPhotoPath
|
customerIdCardPhotoPath
|
||||||
customerFrontCameraPath
|
customerFrontCameraPath
|
||||||
customerPhone
|
customerPhone
|
||||||
|
discount
|
||||||
|
customerId
|
||||||
|
isAnonymous
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
@ -95,13 +98,13 @@ const Transactions = ({ id }) => {
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
header: '',
|
header: '',
|
||||||
width: 62,
|
width: 0,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
|
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Customer',
|
header: 'Customer',
|
||||||
width: 162,
|
width: 122,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
view: getCustomerDisplayName
|
view: getCustomerDisplayName
|
||||||
},
|
},
|
||||||
|
|
@ -128,20 +131,20 @@ const Transactions = ({ id }) => {
|
||||||
className: classes.overflowTd,
|
className: classes.overflowTd,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
width: 170
|
width: 140
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Date (UTC)',
|
header: 'Date (UTC)',
|
||||||
view: it => moment.utc(it.created).format('YYYY-MM-DD'),
|
view: it => moment.utc(it.created).format('YYYY-MM-DD'),
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
width: 150
|
width: 140
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
view: it => getStatus(it),
|
view: it => getStatus(it),
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
width: 80
|
width: 20
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -162,8 +165,7 @@ const Transactions = ({ id }) => {
|
||||||
loading={loading || id === null}
|
loading={loading || id === null}
|
||||||
emptyText="No transactions so far"
|
emptyText="No transactions so far"
|
||||||
elements={elements}
|
elements={elements}
|
||||||
// need to splice because back end query could return double NUM_LOG_RESULTS because it doesnt merge the txIn and the txOut results before applying the limit
|
data={R.path(['transactions'])(txResponse)}
|
||||||
data={R.path(['transactions'])(txResponse)} // .splice(0,NUM_LOG_RESULTS)}
|
|
||||||
Details={DetailsRow}
|
Details={DetailsRow}
|
||||||
expandable
|
expandable
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import NavigateNextIcon from '@material-ui/icons/NavigateNext'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React from 'react'
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { TL1, TL2, Label3 } from 'src/components/typography'
|
import { TL1, TL2, Label3 } from 'src/components/typography'
|
||||||
|
|
@ -19,11 +19,9 @@ import Transactions from './MachineComponents/Transactions'
|
||||||
import styles from './Machines.styles'
|
import styles from './Machines.styles'
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const getMachineInfo = R.compose(R.find, R.propEq('name'))
|
|
||||||
|
|
||||||
const GET_INFO = gql`
|
const GET_INFO = gql`
|
||||||
query getInfo {
|
query getMachine($deviceId: ID!) {
|
||||||
machines {
|
machine(deviceId: $deviceId) {
|
||||||
name
|
name
|
||||||
deviceId
|
deviceId
|
||||||
paired
|
paired
|
||||||
|
|
@ -41,33 +39,32 @@ const GET_INFO = gql`
|
||||||
downloadSpeed
|
downloadSpeed
|
||||||
responseTime
|
responseTime
|
||||||
packetLoss
|
packetLoss
|
||||||
|
latestEvent {
|
||||||
|
note
|
||||||
|
}
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const getMachines = R.path(['machines'])
|
const getMachineID = path => path.slice(path.lastIndexOf('/') + 1)
|
||||||
|
|
||||||
const Machines = () => {
|
const Machines = () => {
|
||||||
const { data, refetch, loading } = useQuery(GET_INFO)
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [selectedMachine, setSelectedMachine] = useState('')
|
const { data, refetch } = useQuery(GET_INFO, {
|
||||||
|
variables: {
|
||||||
|
deviceId: getMachineID(location.pathname)
|
||||||
|
}
|
||||||
|
})
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const machines = getMachines(data) ?? []
|
|
||||||
const machineInfo = getMachineInfo(selectedMachine)(machines) ?? {}
|
|
||||||
const timezone = R.path(['config', 'locale_timezone'], data) ?? {}
|
const timezone = R.path(['config', 'locale_timezone'], data) ?? {}
|
||||||
|
|
||||||
// pre-selects first machine from the list, if there is a machine configured.
|
const machine = R.path(['machine'])(data) ?? {}
|
||||||
useEffect(() => {
|
const config = R.path(['config'])(data) ?? {}
|
||||||
if (!loading && data && data.machines) {
|
|
||||||
if (location.state && location.state.selectedMachine) {
|
const machineName = R.path(['name'])(machine) ?? null
|
||||||
setSelectedMachine(location.state.selectedMachine)
|
const machineID = R.path(['deviceId'])(machine) ?? null
|
||||||
} else {
|
|
||||||
setSelectedMachine(R.path(['machines', 0, 'name'])(data) ?? '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [loading, data, location.state])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container className={classes.grid}>
|
<Grid container className={classes.grid}>
|
||||||
|
|
@ -81,10 +78,10 @@ const Machines = () => {
|
||||||
</Label3>
|
</Label3>
|
||||||
</Link>
|
</Link>
|
||||||
<TL2 noMargin className={classes.subtitle}>
|
<TL2 noMargin className={classes.subtitle}>
|
||||||
{selectedMachine}
|
{machineName}
|
||||||
</TL2>
|
</TL2>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<Overview data={machineInfo} onActionSuccess={refetch} />
|
<Overview data={machine} onActionSuccess={refetch} />
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|
@ -102,26 +99,23 @@ const Machines = () => {
|
||||||
<div
|
<div
|
||||||
className={classnames(classes.detailItem, classes.detailsMargin)}>
|
className={classnames(classes.detailItem, classes.detailsMargin)}>
|
||||||
<TL1 className={classes.subtitle}>{'Details'}</TL1>
|
<TL1 className={classes.subtitle}>{'Details'}</TL1>
|
||||||
<Details data={machineInfo} timezone={timezone} />
|
<Details data={machine} timezone={timezone} />
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.detailItem}>
|
<div className={classes.detailItem}>
|
||||||
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
|
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
|
||||||
<Cassettes
|
<Cassettes
|
||||||
refetchData={refetch}
|
refetchData={refetch}
|
||||||
machine={machineInfo}
|
machine={machine}
|
||||||
config={data?.config ?? false}
|
config={config ?? false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.transactionsItem}>
|
<div className={classes.transactionsItem}>
|
||||||
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
|
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
|
||||||
<Transactions id={machineInfo?.deviceId ?? null} />
|
<Transactions id={machineID} />
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.detailItem}>
|
<div className={classes.detailItem}>
|
||||||
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
|
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
|
||||||
<Commissions
|
<Commissions name={'commissions'} id={machineID} />
|
||||||
name={'commissions'}
|
|
||||||
id={machineInfo?.deviceId ?? null}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { spacer, comet } from 'src/styling/variables'
|
||||||
spacer,
|
|
||||||
fontPrimary,
|
|
||||||
primaryColor,
|
|
||||||
white,
|
|
||||||
comet
|
|
||||||
} from 'src/styling/variables'
|
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
grid: {
|
grid: {
|
||||||
|
|
@ -18,16 +12,6 @@ const styles = {
|
||||||
marginLeft: spacer * 6,
|
marginLeft: spacer * 6,
|
||||||
maxWidth: 900
|
maxWidth: 900
|
||||||
},
|
},
|
||||||
footer: {
|
|
||||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
lineHeight: '120%',
|
|
||||||
color: primaryColor,
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily: fontPrimary,
|
|
||||||
fontWeight: 900
|
|
||||||
},
|
|
||||||
subtitle: {
|
subtitle: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|
@ -39,15 +23,6 @@ const styles = {
|
||||||
color: comet,
|
color: comet,
|
||||||
marginTop: 0
|
marginTop: 0
|
||||||
},
|
},
|
||||||
white: {
|
|
||||||
color: white
|
|
||||||
},
|
|
||||||
deleteButton: {
|
|
||||||
paddingLeft: 13
|
|
||||||
},
|
|
||||||
addressRow: {
|
|
||||||
marginLeft: 8
|
|
||||||
},
|
|
||||||
row: {
|
row: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -60,16 +35,10 @@ const styles = {
|
||||||
detailItem: {
|
detailItem: {
|
||||||
marginBottom: spacer * 4
|
marginBottom: spacer * 4
|
||||||
},
|
},
|
||||||
transactionsItem: {
|
|
||||||
marginBottom: -spacer * 4
|
|
||||||
},
|
|
||||||
actionButtonsContainer: {
|
actionButtonsContainer: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
},
|
},
|
||||||
actionButton: {
|
|
||||||
marginRight: 8
|
|
||||||
},
|
|
||||||
breadcrumbsContainer: {
|
breadcrumbsContainer: {
|
||||||
marginTop: 32
|
marginTop: 32
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,15 @@ const CashCassettesFooter = ({
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const cashout = config && fromNamespace('cashOut')(config)
|
const cashout = config && fromNamespace('cashOut')(config)
|
||||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||||
const reducerFn = (acc, { cassette1, cassette2, id }) => [
|
const reducerFn = (acc, { cassette1, cassette2, id }) => {
|
||||||
(acc[0] += cassette1 * getCashoutSettings(id).top),
|
const topDenomination = getCashoutSettings(id).top ?? 0
|
||||||
(acc[1] += cassette2 * getCashoutSettings(id).bottom)
|
const bottomDenomination = getCashoutSettings(id).bottom ?? 0
|
||||||
|
return [
|
||||||
|
(acc[0] += cassette1 * topDenomination),
|
||||||
|
(acc[1] += cassette2 * bottomDenomination)
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0], machines))
|
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0], machines))
|
||||||
|
|
||||||
/* const totalInCashBox = R.sum(
|
/* const totalInCashBox = R.sum(
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,16 @@
|
||||||
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
|
||||||
import { Grid /*, Divider */ } from '@material-ui/core'
|
import { Grid /*, Divider */ } from '@material-ui/core'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import gql from 'graphql-tag'
|
import BigNumber from 'bignumber.js'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
|
||||||
// import { Status } from 'src/components/Status'
|
// import { Status } from 'src/components/Status'
|
||||||
import ActionButton from 'src/components/buttons/ActionButton'
|
|
||||||
import { ReactComponent as EditReversedIcon } from 'src/styling/icons/button/edit/white.svg'
|
|
||||||
import { ReactComponent as EditIcon } from 'src/styling/icons/button/edit/zodiac.svg'
|
|
||||||
// import { ReactComponent as LinkIcon } from 'src/styling/icons/button/link/zodiac.svg'
|
// import { ReactComponent as LinkIcon } from 'src/styling/icons/button/link/zodiac.svg'
|
||||||
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
|
import MachineActions from 'src/components/machineActions/MachineActions'
|
||||||
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
|
|
||||||
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
|
|
||||||
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
|
|
||||||
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
|
|
||||||
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
|
|
||||||
import { modelPrettifier } from 'src/utils/machine'
|
import { modelPrettifier } from 'src/utils/machine'
|
||||||
import { formatDate } from 'src/utils/timezones'
|
import { formatDate } from 'src/utils/timezones'
|
||||||
|
|
||||||
import { labelStyles, machineDetailsStyles } from './MachineDetailsCard.styles'
|
import { labelStyles, machineDetailsStyles } from './MachineDetailsCard.styles'
|
||||||
|
|
||||||
const MACHINE_ACTION = gql`
|
|
||||||
mutation MachineAction(
|
|
||||||
$deviceId: ID!
|
|
||||||
$action: MachineAction!
|
|
||||||
$newName: String
|
|
||||||
) {
|
|
||||||
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
|
|
||||||
deviceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const MACHINE = gql`
|
|
||||||
query getMachine($deviceId: ID!) {
|
|
||||||
machine(deviceId: $deviceId) {
|
|
||||||
latestEvent {
|
|
||||||
note
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
// const supportArtices = [
|
// const supportArtices = [
|
||||||
// {
|
// {
|
||||||
// // Default article for non-maped statuses
|
// // Default article for non-maped statuses
|
||||||
|
|
@ -54,24 +22,6 @@ const MACHINE = gql`
|
||||||
// // TODO add Stuck and Fully Functional statuses articles for the new-admins
|
// // TODO add Stuck and Fully Functional statuses articles for the new-admins
|
||||||
// ]
|
// ]
|
||||||
|
|
||||||
const isStaticState = machineState => {
|
|
||||||
if (!machineState) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const staticStates = [
|
|
||||||
'chooseCoin',
|
|
||||||
'idle',
|
|
||||||
'pendingIdle',
|
|
||||||
'dualIdle',
|
|
||||||
'networkDown',
|
|
||||||
'unpaired',
|
|
||||||
'maintenance',
|
|
||||||
'virgin',
|
|
||||||
'wifiList'
|
|
||||||
]
|
|
||||||
return staticStates.includes(machineState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// const article = ({ code: status }) =>
|
// const article = ({ code: status }) =>
|
||||||
// supportArtices.find(({ code: article }) => article === status)
|
// supportArtices.find(({ code: article }) => article === status)
|
||||||
|
|
||||||
|
|
@ -97,51 +47,9 @@ const Item = ({ children, ...props }) => (
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
|
|
||||||
const getState = machineEventsLazy =>
|
|
||||||
JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}')
|
|
||||||
.state
|
|
||||||
|
|
||||||
const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
||||||
const [action, setAction] = useState({ command: null })
|
|
||||||
const [errorMessage, setErrorMessage] = useState(null)
|
|
||||||
const classes = useMDStyles()
|
const classes = useMDStyles()
|
||||||
|
|
||||||
const warningMessage = (
|
|
||||||
<span className={classes.warning}>
|
|
||||||
A user may be in the middle of a transaction and they could lose their
|
|
||||||
funds if you continue.
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
|
|
||||||
const [fetchMachineEvents, { loading: loadingEvents }] = useLazyQuery(
|
|
||||||
MACHINE,
|
|
||||||
{
|
|
||||||
variables: {
|
|
||||||
deviceId: machine.deviceId
|
|
||||||
},
|
|
||||||
onCompleted: machineEventsLazy => {
|
|
||||||
const message = !isStaticState(getState(machineEventsLazy))
|
|
||||||
? warningMessage
|
|
||||||
: null
|
|
||||||
setAction(action => ({ ...action, message }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
|
|
||||||
onError: ({ message }) => {
|
|
||||||
const errorMessage = message ?? 'An error ocurred'
|
|
||||||
setErrorMessage(errorMessage)
|
|
||||||
},
|
|
||||||
onCompleted: () => {
|
|
||||||
onActionSuccess && onActionSuccess()
|
|
||||||
setAction({ command: null })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const confirmDialogOpen = Boolean(action.command)
|
|
||||||
const disabled = !!(action?.command === 'restartServices' && loadingEvents)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={classes.wrapper}>
|
<Container className={classes.wrapper}>
|
||||||
{/* <Item xs={5}>
|
{/* <Item xs={5}>
|
||||||
|
|
@ -181,30 +89,6 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
||||||
flexItem
|
flexItem
|
||||||
className={classes.separator}
|
className={classes.separator}
|
||||||
/> */}
|
/> */}
|
||||||
<ConfirmDialog
|
|
||||||
disabled={disabled}
|
|
||||||
open={confirmDialogOpen}
|
|
||||||
title={`${action?.display} this machine?`}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
toBeConfirmed={machine.name}
|
|
||||||
message={action?.message}
|
|
||||||
confirmationMessage={action?.confirmationMessage}
|
|
||||||
saveButtonAlwaysEnabled={action?.command === 'rename'}
|
|
||||||
onConfirmed={value => {
|
|
||||||
setErrorMessage(null)
|
|
||||||
machineAction({
|
|
||||||
variables: {
|
|
||||||
deviceId: machine.deviceId,
|
|
||||||
action: `${action?.command}`,
|
|
||||||
...(action?.command === 'rename' && { newName: value })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onDissmised={() => {
|
|
||||||
setAction({ command: null })
|
|
||||||
setErrorMessage(null)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Item xs>
|
<Item xs>
|
||||||
<Container className={classes.row}>
|
<Container className={classes.row}>
|
||||||
<Item xs={2}>
|
<Item xs={2}>
|
||||||
|
|
@ -219,166 +103,38 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item xs={6}>
|
<Item xs={6}>
|
||||||
<Label>Actions</Label>
|
<MachineActions
|
||||||
<div className={classes.stack}>
|
machine={machine}
|
||||||
<ActionButton
|
onActionSuccess={onActionSuccess}></MachineActions>
|
||||||
className={classes.mr}
|
</Item>
|
||||||
disabled={loading}
|
<Item xs={2}>
|
||||||
color="primary"
|
<Label>Network speed</Label>
|
||||||
Icon={EditIcon}
|
<span>
|
||||||
InverseIcon={EditReversedIcon}
|
{machine.downloadSpeed
|
||||||
onClick={() =>
|
? new BigNumber(machine.downloadSpeed).toFixed(4).toString() +
|
||||||
setAction({
|
' MB/s'
|
||||||
command: 'rename',
|
: 'unavailable'}
|
||||||
display: 'Rename',
|
</span>
|
||||||
confirmationMessage: 'Write the new name for this machine'
|
</Item>
|
||||||
})
|
<Item xs={2}>
|
||||||
}>
|
<Label>Latency</Label>
|
||||||
Rename
|
<span>
|
||||||
</ActionButton>
|
{machine.responseTime
|
||||||
<ActionButton
|
? new BigNumber(machine.responseTime).toFixed(3).toString() +
|
||||||
color="primary"
|
' ms'
|
||||||
className={classes.mr}
|
: 'unavailable'}
|
||||||
Icon={UnpairIcon}
|
</span>
|
||||||
InverseIcon={UnpairReversedIcon}
|
</Item>
|
||||||
disabled={loading}
|
<Item xs={2}>
|
||||||
onClick={() =>
|
<Label>Packet Loss</Label>
|
||||||
setAction({
|
<span>
|
||||||
command: 'unpair',
|
{machine.packetLoss
|
||||||
display: 'Unpair'
|
? new BigNumber(machine.packetLoss).toFixed(3).toString() +
|
||||||
})
|
' %'
|
||||||
}>
|
: 'unavailable'}
|
||||||
Unpair
|
</span>
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.mr}
|
|
||||||
Icon={RebootIcon}
|
|
||||||
InverseIcon={RebootReversedIcon}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() =>
|
|
||||||
setAction({
|
|
||||||
command: 'reboot',
|
|
||||||
display: 'Reboot'
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
Reboot
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
className={classes.mr}
|
|
||||||
disabled={loading}
|
|
||||||
color="primary"
|
|
||||||
Icon={ShutdownIcon}
|
|
||||||
InverseIcon={ShutdownReversedIcon}
|
|
||||||
onClick={() =>
|
|
||||||
setAction({
|
|
||||||
command: 'shutdown',
|
|
||||||
display: 'Shutdown',
|
|
||||||
message:
|
|
||||||
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
Shutdown
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.inlineChip}
|
|
||||||
Icon={RebootIcon}
|
|
||||||
InverseIcon={RebootReversedIcon}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() => {
|
|
||||||
fetchMachineEvents()
|
|
||||||
setAction({
|
|
||||||
command: 'restartServices',
|
|
||||||
display: 'Restart services for'
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
Restart Services
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Item>
|
</Item>
|
||||||
</Container>
|
</Container>
|
||||||
{/* <Container>
|
|
||||||
<Item>
|
|
||||||
<Label>Actions</Label>
|
|
||||||
<div className={classes.stack}>
|
|
||||||
<ActionButton
|
|
||||||
className={classes.mr}
|
|
||||||
disabled={loading}
|
|
||||||
color="primary"
|
|
||||||
Icon={EditIcon}
|
|
||||||
InverseIcon={EditReversedIcon}
|
|
||||||
onClick={() =>
|
|
||||||
setAction({
|
|
||||||
command: 'rename',
|
|
||||||
display: 'Rename',
|
|
||||||
confirmationMessage: 'Write the new name for this machine'
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
Rename
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.mr}
|
|
||||||
Icon={UnpairIcon}
|
|
||||||
InverseIcon={UnpairReversedIcon}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() =>
|
|
||||||
setAction({
|
|
||||||
command: 'unpair',
|
|
||||||
display: 'Unpair'
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
Unpair
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.mr}
|
|
||||||
Icon={RebootIcon}
|
|
||||||
InverseIcon={RebootReversedIcon}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() =>
|
|
||||||
setAction({
|
|
||||||
command: 'reboot',
|
|
||||||
display: 'Reboot'
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
Reboot
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
className={classes.mr}
|
|
||||||
disabled={loading}
|
|
||||||
color="primary"
|
|
||||||
Icon={ShutdownIcon}
|
|
||||||
InverseIcon={ShutdownReversedIcon}
|
|
||||||
onClick={() =>
|
|
||||||
setAction({
|
|
||||||
command: 'shutdown',
|
|
||||||
display: 'Shutdown',
|
|
||||||
message:
|
|
||||||
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
Shutdown
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
color="primary"
|
|
||||||
className={classes.inlineChip}
|
|
||||||
Icon={RebootIcon}
|
|
||||||
InverseIcon={RebootReversedIcon}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() => {
|
|
||||||
fetchMachineEvents()
|
|
||||||
setAction({
|
|
||||||
command: 'restartServices',
|
|
||||||
display: 'Restart services for'
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
Restart Services
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Item>
|
|
||||||
</Container> */}
|
|
||||||
</Item>
|
</Item>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,10 @@ import {
|
||||||
detailsRowStyles,
|
detailsRowStyles,
|
||||||
labelStyles
|
labelStyles
|
||||||
} from 'src/pages/Transactions/Transactions.styles'
|
} from 'src/pages/Transactions/Transactions.styles'
|
||||||
import {
|
import { spacer, comet, primaryColor, fontSize4 } from 'src/styling/variables'
|
||||||
spacer,
|
|
||||||
comet,
|
|
||||||
primaryColor,
|
|
||||||
fontSize4,
|
|
||||||
errorColor
|
|
||||||
} from 'src/styling/variables'
|
|
||||||
|
|
||||||
const machineDetailsStyles = {
|
const machineDetailsStyles = {
|
||||||
...detailsRowStyles,
|
...detailsRowStyles,
|
||||||
colDivider: {
|
|
||||||
width: 1,
|
|
||||||
margin: [[spacer * 2, spacer * 4]],
|
|
||||||
backgroundColor: comet,
|
|
||||||
border: 'none'
|
|
||||||
},
|
|
||||||
inlineChip: {
|
|
||||||
marginInlineEnd: '0.25em'
|
|
||||||
},
|
|
||||||
stack: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row'
|
|
||||||
},
|
|
||||||
wrapper: {
|
wrapper: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
// marginTop: 24,
|
// marginTop: 24,
|
||||||
|
|
@ -53,12 +34,6 @@ const machineDetailsStyles = {
|
||||||
color: primaryColor,
|
color: primaryColor,
|
||||||
textDecoration: 'none'
|
textDecoration: 'none'
|
||||||
},
|
},
|
||||||
divider: {
|
|
||||||
margin: '0 1rem'
|
|
||||||
},
|
|
||||||
mr: {
|
|
||||||
marginRight: spacer
|
|
||||||
},
|
|
||||||
separator: {
|
separator: {
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 170,
|
height: 170,
|
||||||
|
|
@ -66,9 +41,6 @@ const machineDetailsStyles = {
|
||||||
marginRight: 60,
|
marginRight: 60,
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
background: fade(comet, 0.5)
|
background: fade(comet, 0.5)
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
color: errorColor
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useQuery } from '@apollo/react-hooks'
|
import { useQuery } from '@apollo/react-hooks'
|
||||||
import { makeStyles } from '@material-ui/core'
|
import { makeStyles } from '@material-ui/core'
|
||||||
import BigNumber from 'bignumber.js'
|
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
|
|
@ -62,7 +61,7 @@ const MachineStatus = () => {
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
header: 'Machine Name',
|
header: 'Machine Name',
|
||||||
width: 150,
|
width: 250,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
view: m => (
|
view: m => (
|
||||||
|
|
@ -80,48 +79,18 @@ const MachineStatus = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
width: 150,
|
width: 350,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
view: m => <MainStatus statuses={m.statuses} />
|
view: m => <MainStatus statuses={m.statuses} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Last ping',
|
header: 'Last ping',
|
||||||
width: 175,
|
width: 200,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
view: m => (m.lastPing ? moment(m.lastPing).fromNow() : 'unknown')
|
view: m => (m.lastPing ? moment(m.lastPing).fromNow() : 'unknown')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: 'Network speed',
|
|
||||||
width: 150,
|
|
||||||
size: 'sm',
|
|
||||||
textAlign: 'left',
|
|
||||||
view: m =>
|
|
||||||
m.downloadSpeed
|
|
||||||
? new BigNumber(m.downloadSpeed).toFixed(4).toString() + ' MB/s'
|
|
||||||
: 'unavailable'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Latency',
|
|
||||||
width: 150,
|
|
||||||
size: 'sm',
|
|
||||||
textAlign: 'left',
|
|
||||||
view: m =>
|
|
||||||
m.responseTime
|
|
||||||
? new BigNumber(m.responseTime).toFixed(3).toString() + ' ms'
|
|
||||||
: 'unavailable'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Packet Loss',
|
|
||||||
width: 125,
|
|
||||||
size: 'sm',
|
|
||||||
textAlign: 'left',
|
|
||||||
view: m =>
|
|
||||||
m.packetLoss
|
|
||||||
? new BigNumber(m.packetLoss).toFixed(3).toString() + ' %'
|
|
||||||
: 'unavailable'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Software Version',
|
header: 'Software Version',
|
||||||
width: 200,
|
width: 200,
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
) : (
|
) : (
|
||||||
errorElements
|
errorElements
|
||||||
)}
|
)}
|
||||||
{tx.txClass === 'cashOut' && getStatus(tx) !== 'Cancelled' && (
|
{tx.txClass === 'cashOut' && getStatus(tx) === 'Pending' && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="primary"
|
color="primary"
|
||||||
Icon={CancelIcon}
|
Icon={CancelIcon}
|
||||||
|
|
@ -338,4 +338,8 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(DetailsRow)
|
export default memo(
|
||||||
|
DetailsRow,
|
||||||
|
(prev, next) =>
|
||||||
|
prev.it.id === next.it.id && prev.it.hasError === next.it.hasError
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,14 @@ const GET_DATA = gql`
|
||||||
|
|
||||||
const GET_TRANSACTIONS_CSV = gql`
|
const GET_TRANSACTIONS_CSV = gql`
|
||||||
query transactions(
|
query transactions(
|
||||||
|
$simplified: Boolean
|
||||||
$limit: Int
|
$limit: Int
|
||||||
$from: Date
|
$from: Date
|
||||||
$until: Date
|
$until: Date
|
||||||
$timezone: String
|
$timezone: String
|
||||||
) {
|
) {
|
||||||
transactionsCsv(
|
transactionsCsv(
|
||||||
|
simplified: $simplified
|
||||||
limit: $limit
|
limit: $limit
|
||||||
from: $from
|
from: $from
|
||||||
until: $until
|
until: $until
|
||||||
|
|
@ -194,13 +196,13 @@ const Transactions = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Crypto',
|
header: 'Crypto',
|
||||||
width: 144,
|
width: 150,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
view: it =>
|
view: it =>
|
||||||
`${coinUtils
|
`${coinUtils.toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode)} ${
|
||||||
.toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode)
|
it.cryptoCode
|
||||||
.toFormat(5)} ${it.cryptoCode}`
|
}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Address',
|
header: 'Address',
|
||||||
|
|
@ -277,9 +279,8 @@ const Transactions = () => {
|
||||||
title="Download logs"
|
title="Download logs"
|
||||||
name="transactions"
|
name="transactions"
|
||||||
query={GET_TRANSACTIONS_CSV}
|
query={GET_TRANSACTIONS_CSV}
|
||||||
args={{ timezone }}
|
|
||||||
getLogs={logs => R.path(['transactionsCsv'])(logs)}
|
getLogs={logs => R.path(['transactionsCsv'])(logs)}
|
||||||
timezone={timezone}
|
simplified
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import Stepper from 'src/components/Stepper'
|
||||||
import { Button } from 'src/components/buttons'
|
import { Button } from 'src/components/buttons'
|
||||||
import { H5, Info3 } from 'src/components/typography'
|
import { H5, Info3 } from 'src/components/typography'
|
||||||
import { comet } from 'src/styling/variables'
|
import { comet } from 'src/styling/variables'
|
||||||
|
import { singularOrPlural } from 'src/utils/string'
|
||||||
|
|
||||||
import { type, requirements } from './helper'
|
import { type, requirements } from './helper'
|
||||||
|
|
||||||
|
|
@ -105,21 +106,29 @@ const getTypeText = (config, currency, classes) => {
|
||||||
<>
|
<>
|
||||||
makes {orUnderline(config.threshold.threshold, classes)} {currency}{' '}
|
makes {orUnderline(config.threshold.threshold, classes)} {currency}{' '}
|
||||||
worth of transactions within{' '}
|
worth of transactions within{' '}
|
||||||
{orUnderline(config.threshold.thresholdDays, classes)} days
|
{orUnderline(config.threshold.thresholdDays, classes)}{' '}
|
||||||
|
{singularOrPlural(config.threshold.thresholdDays, 'day', 'days')}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'txVelocity':
|
case 'txVelocity':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
makes {orUnderline(config.threshold.threshold, classes)} transactions
|
makes {orUnderline(config.threshold.threshold, classes)}{' '}
|
||||||
in {orUnderline(config.threshold.thresholdDays, classes)} days
|
{singularOrPlural(
|
||||||
|
config.threshold.threshold,
|
||||||
|
'transaction',
|
||||||
|
'transactions'
|
||||||
|
)}{' '}
|
||||||
|
in {orUnderline(config.threshold.thresholdDays, classes)}{' '}
|
||||||
|
{singularOrPlural(config.threshold.thresholdDays, 'day', 'days')}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'consecutiveDays':
|
case 'consecutiveDays':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
at least one transaction every day for{' '}
|
at least one transaction every day for{' '}
|
||||||
{orUnderline(config.threshold.thresholdDays, classes)} days
|
{orUnderline(config.threshold.thresholdDays, classes)}{' '}
|
||||||
|
{singularOrPlural(config.threshold.thresholdDays, 'day', 'days')}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
|
|
@ -147,7 +156,8 @@ const getRequirementText = (config, classes) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
suspended for{' '}
|
suspended for{' '}
|
||||||
{orUnderline(config.requirement.suspensionDays, classes)} days
|
{orUnderline(config.requirement.suspensionDays, classes)}{' '}
|
||||||
|
{singularOrPlural(config.requirement.suspensionDays, 'day', 'days')}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'block':
|
case 'block':
|
||||||
|
|
@ -212,6 +222,41 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createErrorMessage = (errors, touched, values) => {
|
||||||
|
const triggerType = values?.triggerType
|
||||||
|
const containsType = R.contains(triggerType)
|
||||||
|
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||||
|
|
||||||
|
const hasRequirementError =
|
||||||
|
!!errors.requirement &&
|
||||||
|
!!touched.requirement?.suspensionDays &&
|
||||||
|
(!values.requirement?.suspensionDays ||
|
||||||
|
values.requirement?.suspensionDays < 0)
|
||||||
|
|
||||||
|
const hasAmountError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.threshold &&
|
||||||
|
!containsType(['consecutiveDays']) &&
|
||||||
|
(!values.threshold?.threshold || values.threshold?.threshold < 0)
|
||||||
|
|
||||||
|
const hasDaysError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.thresholdDays &&
|
||||||
|
!containsType(['txAmount']) &&
|
||||||
|
(!values.threshold?.thresholdDays || values.threshold?.thresholdDays < 0)
|
||||||
|
|
||||||
|
if (containsType(['txAmount', 'txVolume', 'txVelocity']) && hasAmountError)
|
||||||
|
return errors.threshold
|
||||||
|
|
||||||
|
if (
|
||||||
|
containsType(['txVolume', 'txVelocity', 'consecutiveDays']) &&
|
||||||
|
hasDaysError
|
||||||
|
)
|
||||||
|
return errors.threshold
|
||||||
|
|
||||||
|
if (isSuspend && hasRequirementError) return errors.requirement
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -236,21 +281,28 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
||||||
/>
|
/>
|
||||||
<Formik
|
<Formik
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
validateOnChange={false}
|
validateOnChange={true}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
onSubmit={onContinue}
|
onSubmit={onContinue}
|
||||||
initialValues={stepOptions.initialValues}
|
initialValues={stepOptions.initialValues}
|
||||||
validationSchema={stepOptions.schema}>
|
validationSchema={stepOptions.schema}>
|
||||||
|
{({ errors, touched, values }) => (
|
||||||
<Form className={classes.form}>
|
<Form className={classes.form}>
|
||||||
<GetValues setValues={setLiveValues} />
|
<GetValues setValues={setLiveValues} />
|
||||||
<stepOptions.Component {...stepOptions.props} />
|
<stepOptions.Component {...stepOptions.props} />
|
||||||
<div className={classes.submit}>
|
<div className={classes.submit}>
|
||||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||||
|
{createErrorMessage(errors, touched, values) && (
|
||||||
|
<ErrorMessage>
|
||||||
|
{createErrorMessage(errors, touched, values)}
|
||||||
|
</ErrorMessage>
|
||||||
|
)}
|
||||||
<Button className={classes.button} type="submit">
|
<Button className={classes.button} type="submit">
|
||||||
{isLastStep ? 'Finish' : 'Next'}
|
{isLastStep ? 'Finish' : 'Next'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultSchema,
|
defaultSchema,
|
||||||
overridesSchema,
|
getOverridesSchema,
|
||||||
defaults,
|
defaults,
|
||||||
overridesDefaults,
|
overridesDefaults,
|
||||||
getDefaultSettings,
|
getDefaultSettings,
|
||||||
|
|
@ -95,7 +95,7 @@ const AdvancedTriggersSettings = memo(() => {
|
||||||
enableCreate
|
enableCreate
|
||||||
initialValues={overridesDefaults}
|
initialValues={overridesDefaults}
|
||||||
save={saveOverrides}
|
save={saveOverrides}
|
||||||
validationSchema={overridesSchema}
|
validationSchema={getOverridesSchema(requirementsOverrides)}
|
||||||
data={requirementsOverrides}
|
data={requirementsOverrides}
|
||||||
elements={getOverrides()}
|
elements={getOverrides()}
|
||||||
setEditing={onEditingOverrides}
|
setEditing={onEditingOverrides}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,23 @@
|
||||||
|
import * as R from 'ramda'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
|
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
|
||||||
import { getView, requirementOptions } from 'src/pages/Triggers/helper'
|
import { getView } from 'src/pages/Triggers/helper'
|
||||||
|
|
||||||
|
const advancedRequirementOptions = [
|
||||||
|
{ display: 'Sanctions', code: 'sanctions' },
|
||||||
|
{ display: 'ID card image', code: 'idCardPhoto' },
|
||||||
|
{ display: 'ID data', code: 'idCardData' },
|
||||||
|
{ display: 'Customer camera', code: 'facephoto' },
|
||||||
|
{ display: 'US SSN', code: 'usSsn' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const displayRequirement = code => {
|
||||||
|
return R.prop(
|
||||||
|
'display',
|
||||||
|
R.find(R.propEq('code', code))(advancedRequirementOptions)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const defaultSchema = Yup.object().shape({
|
const defaultSchema = Yup.object().shape({
|
||||||
expirationTime: Yup.string()
|
expirationTime: Yup.string()
|
||||||
|
|
@ -13,10 +29,24 @@ const defaultSchema = Yup.object().shape({
|
||||||
.required()
|
.required()
|
||||||
})
|
})
|
||||||
|
|
||||||
const overridesSchema = Yup.object().shape({
|
const getOverridesSchema = values => {
|
||||||
|
return Yup.object().shape({
|
||||||
id: Yup.string()
|
id: Yup.string()
|
||||||
.label('Requirement')
|
.label('Requirement')
|
||||||
.required(),
|
.required()
|
||||||
|
.test({
|
||||||
|
test() {
|
||||||
|
const { requirement } = this.parent
|
||||||
|
if (R.find(R.propEq('requirement', requirement))(values)) {
|
||||||
|
return this.createError({
|
||||||
|
message: `Requirement ${displayRequirement(
|
||||||
|
requirement
|
||||||
|
)} already overriden`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}),
|
||||||
expirationTime: Yup.string()
|
expirationTime: Yup.string()
|
||||||
.label('Expiration time')
|
.label('Expiration time')
|
||||||
.required(),
|
.required(),
|
||||||
|
|
@ -24,7 +54,8 @@ const overridesSchema = Yup.object().shape({
|
||||||
.label('Automation')
|
.label('Automation')
|
||||||
.matches(/(Manual|Automatic)/)
|
.matches(/(Manual|Automatic)/)
|
||||||
.required()
|
.required()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getDefaultSettings = () => {
|
const getDefaultSettings = () => {
|
||||||
return [
|
return [
|
||||||
|
|
@ -60,10 +91,10 @@ const getOverrides = () => {
|
||||||
header: 'Requirement',
|
header: 'Requirement',
|
||||||
width: 196,
|
width: 196,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
view: getView(requirementOptions, 'display'),
|
view: getView(advancedRequirementOptions, 'display'),
|
||||||
input: Autocomplete,
|
input: Autocomplete,
|
||||||
inputProps: {
|
inputProps: {
|
||||||
options: requirementOptions,
|
options: advancedRequirementOptions,
|
||||||
labelProp: 'display',
|
labelProp: 'display',
|
||||||
valueProp: 'code'
|
valueProp: 'code'
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +139,7 @@ const overridesDefaults = {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
defaultSchema,
|
defaultSchema,
|
||||||
overridesSchema,
|
getOverridesSchema,
|
||||||
defaults,
|
defaults,
|
||||||
overridesDefaults,
|
overridesDefaults,
|
||||||
getDefaultSettings,
|
getDefaultSettings,
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ import * as R from 'ramda'
|
||||||
import React, { memo } from 'react'
|
import React, { memo } from 'react'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import {
|
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
|
||||||
NumberInput,
|
|
||||||
TextInput,
|
|
||||||
RadioGroup
|
|
||||||
} from 'src/components/inputs/formik'
|
|
||||||
import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography'
|
import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography'
|
||||||
import { errorColor } from 'src/styling/variables'
|
import { errorColor } from 'src/styling/variables'
|
||||||
import { transformNumber } from 'src/utils/number'
|
import { transformNumber } from 'src/utils/number'
|
||||||
|
|
@ -100,16 +96,9 @@ const threshold = Yup.object().shape({
|
||||||
|
|
||||||
const requirement = Yup.object().shape({
|
const requirement = Yup.object().shape({
|
||||||
requirement: Yup.string().required(),
|
requirement: Yup.string().required(),
|
||||||
suspensionDays: Yup.number().when('requirement', {
|
suspensionDays: Yup.number()
|
||||||
is: 'suspend',
|
.transform(transformNumber)
|
||||||
then: Yup.number()
|
|
||||||
.required()
|
|
||||||
.min(1)
|
|
||||||
.label('Invalid value'),
|
|
||||||
otherwise: Yup.number()
|
|
||||||
.nullable()
|
.nullable()
|
||||||
.transform(() => null)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const Schema = Yup.object()
|
const Schema = Yup.object()
|
||||||
|
|
@ -119,11 +108,28 @@ const Schema = Yup.object()
|
||||||
threshold
|
threshold
|
||||||
// direction
|
// direction
|
||||||
})
|
})
|
||||||
.test(
|
.test(({ threshold, triggerType }, context) => {
|
||||||
'are-fields-set',
|
const errorMessages = {
|
||||||
'Invalid values',
|
txAmount: threshold => 'Amount must be greater than or equal to 0',
|
||||||
({ threshold, triggerType }, context) => {
|
txVolume: threshold => {
|
||||||
const validator = {
|
const thresholdMessage = 'Volume must be greater than or equal to 0'
|
||||||
|
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||||
|
const message = []
|
||||||
|
if (threshold.threshold < 0) message.push(thresholdMessage)
|
||||||
|
if (threshold.thresholdDays <= 0) message.push(thresholdDaysMessage)
|
||||||
|
return message.join(', ')
|
||||||
|
},
|
||||||
|
txVelocity: threshold => {
|
||||||
|
const thresholdMessage = 'Transactions must be greater than 0'
|
||||||
|
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||||
|
const message = []
|
||||||
|
if (threshold.threshold <= 0) message.push(thresholdMessage)
|
||||||
|
if (threshold.thresholdDays <= 0) message.push(thresholdDaysMessage)
|
||||||
|
return message.join(', ')
|
||||||
|
},
|
||||||
|
consecutiveDays: threshold => 'Days must be greater than 0'
|
||||||
|
}
|
||||||
|
const thresholdValidator = {
|
||||||
txAmount: threshold => threshold.threshold >= 0,
|
txAmount: threshold => threshold.threshold >= 0,
|
||||||
txVolume: threshold =>
|
txVolume: threshold =>
|
||||||
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
||||||
|
|
@ -131,12 +137,27 @@ const Schema = Yup.object()
|
||||||
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
||||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
(triggerType && validator?.[triggerType](threshold)) ||
|
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
||||||
context.createError({ path: 'threshold' })
|
|
||||||
)
|
return context.createError({
|
||||||
}
|
path: 'threshold',
|
||||||
)
|
message: errorMessages[triggerType](threshold)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.test(({ requirement }, context) => {
|
||||||
|
const requirementValidator = requirement =>
|
||||||
|
requirement.requirement === 'suspend'
|
||||||
|
? requirement.suspensionDays > 0
|
||||||
|
: true
|
||||||
|
|
||||||
|
if (requirement && requirementValidator(requirement)) return
|
||||||
|
|
||||||
|
return context.createError({
|
||||||
|
path: 'requirement',
|
||||||
|
message: 'Suspension days must be greater than 0'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Direction V2 only
|
// Direction V2 only
|
||||||
// const directionSchema = Yup.object().shape({ direction })
|
// const directionSchema = Yup.object().shape({ direction })
|
||||||
|
|
@ -237,11 +258,32 @@ const typeSchema = Yup.object()
|
||||||
.nullable()
|
.nullable()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.test(
|
.test(({ threshold, triggerType }, context) => {
|
||||||
'are-fields-set',
|
const errorMessages = {
|
||||||
'All fields must be set.',
|
txAmount: threshold => 'Amount must be greater than or equal to 0',
|
||||||
({ triggerType, threshold }, context) => {
|
txVolume: threshold => {
|
||||||
const validator = {
|
const thresholdMessage = 'Volume must be greater than or equal to 0'
|
||||||
|
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||||
|
const message = []
|
||||||
|
if (!threshold.threshold || threshold.threshold < 0)
|
||||||
|
message.push(thresholdMessage)
|
||||||
|
if (!threshold.thresholdDays || threshold.thresholdDays <= 0)
|
||||||
|
message.push(thresholdDaysMessage)
|
||||||
|
return message.join(', ')
|
||||||
|
},
|
||||||
|
txVelocity: threshold => {
|
||||||
|
const thresholdMessage = 'Transactions must be greater than 0'
|
||||||
|
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||||
|
const message = []
|
||||||
|
if (!threshold.threshold || threshold.threshold <= 0)
|
||||||
|
message.push(thresholdMessage)
|
||||||
|
if (!threshold.thresholdDays || threshold.thresholdDays <= 0)
|
||||||
|
message.push(thresholdDaysMessage)
|
||||||
|
return message.join(', ')
|
||||||
|
},
|
||||||
|
consecutiveDays: threshold => 'Days must be greater than 0'
|
||||||
|
}
|
||||||
|
const thresholdValidator = {
|
||||||
txAmount: threshold => threshold.threshold >= 0,
|
txAmount: threshold => threshold.threshold >= 0,
|
||||||
txVolume: threshold =>
|
txVolume: threshold =>
|
||||||
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
||||||
|
|
@ -250,12 +292,13 @@ const typeSchema = Yup.object()
|
||||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
||||||
(triggerType && validator?.[triggerType](threshold)) ||
|
|
||||||
context.createError({ path: 'threshold' })
|
return context.createError({
|
||||||
)
|
path: 'threshold',
|
||||||
}
|
message: errorMessages[triggerType](threshold)
|
||||||
)
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{ display: 'Transaction amount', code: 'txAmount' },
|
{ display: 'Transaction amount', code: 'txAmount' },
|
||||||
|
|
@ -266,7 +309,13 @@ const typeOptions = [
|
||||||
|
|
||||||
const Type = ({ ...props }) => {
|
const Type = ({ ...props }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { errors, touched, values } = useFormikContext()
|
const {
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
values,
|
||||||
|
setTouched,
|
||||||
|
handleChange
|
||||||
|
} = useFormikContext()
|
||||||
|
|
||||||
const typeClass = {
|
const typeClass = {
|
||||||
[classes.error]: errors.triggerType && touched.triggerType
|
[classes.error]: errors.triggerType && touched.triggerType
|
||||||
|
|
@ -278,11 +327,21 @@ const Type = ({ ...props }) => {
|
||||||
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
||||||
const isConsecutiveDaysEnabled = containsType(['consecutiveDays'])
|
const isConsecutiveDaysEnabled = containsType(['consecutiveDays'])
|
||||||
|
|
||||||
|
const hasAmountError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.threshold &&
|
||||||
|
!isConsecutiveDaysEnabled &&
|
||||||
|
(!values.threshold?.threshold || values.threshold?.threshold < 0)
|
||||||
|
const hasDaysError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.thresholdDays &&
|
||||||
|
!containsType(['txAmount']) &&
|
||||||
|
(!values.threshold?.thresholdDays || values.threshold?.thresholdDays < 0)
|
||||||
|
|
||||||
|
const triggerTypeError = !!(hasDaysError || hasAmountError)
|
||||||
|
|
||||||
const thresholdClass = {
|
const thresholdClass = {
|
||||||
[classes.error]:
|
[classes.error]: triggerTypeError
|
||||||
errors.threshold &&
|
|
||||||
((!containsType(['consecutiveDays']) && touched.threshold?.threshold) ||
|
|
||||||
(!containsType(['txAmount']) && touched.threshold?.thresholdDays))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRadioGroupActive = () => {
|
const isRadioGroupActive = () => {
|
||||||
|
|
@ -306,6 +365,13 @@ const Type = ({ ...props }) => {
|
||||||
labelClassName={classes.radioLabel}
|
labelClassName={classes.radioLabel}
|
||||||
radioClassName={classes.radio}
|
radioClassName={classes.radio}
|
||||||
className={classes.radioGroup}
|
className={classes.radioGroup}
|
||||||
|
onChange={e => {
|
||||||
|
handleChange(e)
|
||||||
|
setTouched({
|
||||||
|
threshold: false,
|
||||||
|
thresholdDays: false
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={classes.thresholdWrapper}>
|
<div className={classes.thresholdWrapper}>
|
||||||
|
|
@ -322,6 +388,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.threshold"
|
name="threshold.threshold"
|
||||||
|
error={hasAmountError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>
|
<Info1 className={classnames(classes.description)}>
|
||||||
{props.currency}
|
{props.currency}
|
||||||
|
|
@ -335,6 +402,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.threshold"
|
name="threshold.threshold"
|
||||||
|
error={hasAmountError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>
|
<Info1 className={classnames(classes.description)}>
|
||||||
transactions
|
transactions
|
||||||
|
|
@ -356,6 +424,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.thresholdDays"
|
name="threshold.thresholdDays"
|
||||||
|
error={hasDaysError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>days</Info1>
|
<Info1 className={classnames(classes.description)}>days</Info1>
|
||||||
</>
|
</>
|
||||||
|
|
@ -367,6 +436,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.thresholdDays"
|
name="threshold.thresholdDays"
|
||||||
|
error={hasDaysError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>
|
<Info1 className={classnames(classes.description)}>
|
||||||
consecutive days
|
consecutive days
|
||||||
|
|
@ -390,20 +460,34 @@ const type = currency => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const requirementSchema = Yup.object().shape({
|
const requirementSchema = Yup.object()
|
||||||
|
.shape({
|
||||||
requirement: Yup.object({
|
requirement: Yup.object({
|
||||||
requirement: Yup.string().required(),
|
requirement: Yup.string().required(),
|
||||||
suspensionDays: Yup.number().when('requirement', {
|
suspensionDays: Yup.number().when('requirement', {
|
||||||
is: value => value === 'suspend',
|
is: value => value === 'suspend',
|
||||||
then: Yup.number()
|
then: Yup.number()
|
||||||
.required()
|
.nullable()
|
||||||
.min(1),
|
.transform(transformNumber),
|
||||||
otherwise: Yup.number()
|
otherwise: Yup.number()
|
||||||
.nullable()
|
.nullable()
|
||||||
.transform(() => null)
|
.transform(() => null)
|
||||||
})
|
})
|
||||||
}).required()
|
}).required()
|
||||||
})
|
})
|
||||||
|
.test(({ requirement }, context) => {
|
||||||
|
const requirementValidator = requirement =>
|
||||||
|
requirement.requirement === 'suspend'
|
||||||
|
? requirement.suspensionDays > 0
|
||||||
|
: true
|
||||||
|
|
||||||
|
if (requirement && requirementValidator(requirement)) return
|
||||||
|
|
||||||
|
return context.createError({
|
||||||
|
path: 'requirement',
|
||||||
|
message: 'Suspension days must be greater than 0'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const requirementOptions = [
|
const requirementOptions = [
|
||||||
{ display: 'SMS verification', code: 'sms' },
|
{ display: 'SMS verification', code: 'sms' },
|
||||||
|
|
@ -419,19 +503,27 @@ const requirementOptions = [
|
||||||
|
|
||||||
const Requirement = () => {
|
const Requirement = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { touched, errors, values } = useFormikContext()
|
const {
|
||||||
|
touched,
|
||||||
|
errors,
|
||||||
|
values,
|
||||||
|
handleChange,
|
||||||
|
setTouched
|
||||||
|
} = useFormikContext()
|
||||||
|
|
||||||
|
const hasRequirementError =
|
||||||
|
!!errors.requirement &&
|
||||||
|
!!touched.requirement?.suspensionDays &&
|
||||||
|
(!values.requirement?.suspensionDays ||
|
||||||
|
values.requirement?.suspensionDays < 0)
|
||||||
|
|
||||||
|
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||||
|
|
||||||
const titleClass = {
|
const titleClass = {
|
||||||
[classes.error]:
|
[classes.error]:
|
||||||
!R.isEmpty(R.omit(['suspensionDays'], errors.requirement)) ||
|
(!!errors.requirement && !isSuspend) || (isSuspend && hasRequirementError)
|
||||||
(errors.requirement &&
|
|
||||||
touched.requirement &&
|
|
||||||
errors.requirement.suspensionDays &&
|
|
||||||
touched.requirement.suspensionDays)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
|
|
@ -444,6 +536,12 @@ const Requirement = () => {
|
||||||
labelClassName={classes.specialLabel}
|
labelClassName={classes.specialLabel}
|
||||||
radioClassName={classes.radio}
|
radioClassName={classes.radio}
|
||||||
className={classnames(classes.radioGroup, classes.specialGrid)}
|
className={classnames(classes.radioGroup, classes.specialGrid)}
|
||||||
|
onChange={e => {
|
||||||
|
handleChange(e)
|
||||||
|
setTouched({
|
||||||
|
suspensionDays: false
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isSuspend && (
|
{isSuspend && (
|
||||||
|
|
@ -453,6 +551,7 @@ const Requirement = () => {
|
||||||
label="Days"
|
label="Days"
|
||||||
size="lg"
|
size="lg"
|
||||||
name="requirement.suspensionDays"
|
name="requirement.suspensionDays"
|
||||||
|
error={hasRequirementError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -504,7 +603,7 @@ const RequirementInput = () => {
|
||||||
bold
|
bold
|
||||||
className={classes.suspensionDays}
|
className={classes.suspensionDays}
|
||||||
name="requirement.suspensionDays"
|
name="requirement.suspensionDays"
|
||||||
component={TextInput}
|
component={NumberInput}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,12 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'zeroConf',
|
name: 'zeroConf',
|
||||||
|
header: 'Confidence Checking',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
view: getDisplayName('zeroConf'),
|
view: getDisplayName('zeroConf'),
|
||||||
input: Autocomplete,
|
input: Autocomplete,
|
||||||
width: 190 - widthAdjust,
|
width: 220 - widthAdjust,
|
||||||
inputProps: {
|
inputProps: {
|
||||||
options: getOptions('zeroConf'),
|
options: getOptions('zeroConf'),
|
||||||
valueProp: 'code',
|
valueProp: 'code',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
|
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
|
||||||
<path fill="#F7931A" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
<path fill="#0AC18E" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
||||||
<path fill="#FFF" transform="rotate(-28 52 32)" d="m66.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
<path fill="#FFF" transform="rotate(-28 52 32)" d="m66.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -26,4 +26,7 @@ const startCase = R.compose(
|
||||||
splitOnUpper
|
splitOnUpper
|
||||||
)
|
)
|
||||||
|
|
||||||
export { startCase, onlyFirstToUpper, formatLong }
|
const singularOrPlural = (amount, singularStr, pluralStr) =>
|
||||||
|
parseInt(amount) === 1 ? singularStr : pluralStr
|
||||||
|
|
||||||
|
export { startCase, onlyFirstToUpper, formatLong, singularOrPlural }
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lamassu-server",
|
"name": "lamassu-server",
|
||||||
"version": "7.5.0-beta.3",
|
"version": "7.5.3",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -14372,7 +14372,7 @@
|
||||||
"version": "git+https://github.com/lamassu/lamassu-coins.git#de843fb210ad8adfa29a0441796125fcb0ab3b67",
|
"version": "git+https://github.com/lamassu/lamassu-coins.git#de843fb210ad8adfa29a0441796125fcb0ab3b67",
|
||||||
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bech32": "^1.1.3",
|
"bech32": "2.0.0",
|
||||||
"bignumber.js": "^9.0.0",
|
"bignumber.js": "^9.0.0",
|
||||||
"bitcoinjs-lib": "4.0.3",
|
"bitcoinjs-lib": "4.0.3",
|
||||||
"bs58check": "^2.0.2",
|
"bs58check": "^2.0.2",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "lamassu-server",
|
"name": "lamassu-server",
|
||||||
"description": "bitcoin atm client server protocol module",
|
"description": "bitcoin atm client server protocol module",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"version": "7.5.0-beta.3",
|
"version": "7.5.3",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"author": "Lamassu (https://lamassu.is)",
|
"author": "Lamassu (https://lamassu.is)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.js": "/static/js/main.b833e621.chunk.js",
|
"main.js": "/static/js/main.fc66d358.chunk.js",
|
||||||
"main.js.map": "/static/js/main.b833e621.chunk.js.map",
|
"main.js.map": "/static/js/main.fc66d358.chunk.js.map",
|
||||||
"runtime-main.js": "/static/js/runtime-main.ee1cbb9c.js",
|
"runtime-main.js": "/static/js/runtime-main.ee1cbb9c.js",
|
||||||
"runtime-main.js.map": "/static/js/runtime-main.ee1cbb9c.js.map",
|
"runtime-main.js.map": "/static/js/runtime-main.ee1cbb9c.js.map",
|
||||||
"static/js/2.346a7f7f.chunk.js": "/static/js/2.346a7f7f.chunk.js",
|
"static/js/2.ee7f7ea2.chunk.js": "/static/js/2.ee7f7ea2.chunk.js",
|
||||||
"static/js/2.346a7f7f.chunk.js.map": "/static/js/2.346a7f7f.chunk.js.map",
|
"static/js/2.ee7f7ea2.chunk.js.map": "/static/js/2.ee7f7ea2.chunk.js.map",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"static/js/2.346a7f7f.chunk.js.LICENSE.txt": "/static/js/2.346a7f7f.chunk.js.LICENSE.txt",
|
"static/js/2.ee7f7ea2.chunk.js.LICENSE.txt": "/static/js/2.ee7f7ea2.chunk.js.LICENSE.txt",
|
||||||
"static/media/cash-in.c06970a7.svg": "/static/media/cash-in.c06970a7.svg",
|
"static/media/cash-in.c06970a7.svg": "/static/media/cash-in.c06970a7.svg",
|
||||||
"static/media/cash-out.f029ae96.svg": "/static/media/cash-out.f029ae96.svg",
|
"static/media/cash-out.f029ae96.svg": "/static/media/cash-out.f029ae96.svg",
|
||||||
"static/media/cashbox-empty.828bd3b9.svg": "/static/media/cashbox-empty.828bd3b9.svg",
|
"static/media/cashbox-empty.828bd3b9.svg": "/static/media/cashbox-empty.828bd3b9.svg",
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"static/media/false.7f926859.svg": "/static/media/false.7f926859.svg",
|
"static/media/false.7f926859.svg": "/static/media/false.7f926859.svg",
|
||||||
"static/media/full.67b8cd67.svg": "/static/media/full.67b8cd67.svg",
|
"static/media/full.67b8cd67.svg": "/static/media/full.67b8cd67.svg",
|
||||||
"static/media/icon-bitcoin-colour.bd8da481.svg": "/static/media/icon-bitcoin-colour.bd8da481.svg",
|
"static/media/icon-bitcoin-colour.bd8da481.svg": "/static/media/icon-bitcoin-colour.bd8da481.svg",
|
||||||
"static/media/icon-bitcoincash-colour.3b27f3ed.svg": "/static/media/icon-bitcoincash-colour.3b27f3ed.svg",
|
"static/media/icon-bitcoincash-colour.ed917caa.svg": "/static/media/icon-bitcoincash-colour.ed917caa.svg",
|
||||||
"static/media/icon-dash-colour.e01c021b.svg": "/static/media/icon-dash-colour.e01c021b.svg",
|
"static/media/icon-dash-colour.e01c021b.svg": "/static/media/icon-dash-colour.e01c021b.svg",
|
||||||
"static/media/icon-ethereum-colour.761723a2.svg": "/static/media/icon-ethereum-colour.761723a2.svg",
|
"static/media/icon-ethereum-colour.761723a2.svg": "/static/media/icon-ethereum-colour.761723a2.svg",
|
||||||
"static/media/icon-litecoin-colour.bd861b5e.svg": "/static/media/icon-litecoin-colour.bd861b5e.svg",
|
"static/media/icon-litecoin-colour.bd861b5e.svg": "/static/media/icon-litecoin-colour.bd861b5e.svg",
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/js/runtime-main.ee1cbb9c.js",
|
"static/js/runtime-main.ee1cbb9c.js",
|
||||||
"static/js/2.346a7f7f.chunk.js",
|
"static/js/2.ee7f7ea2.chunk.js",
|
||||||
"static/js/main.b833e621.chunk.js"
|
"static/js/main.fc66d358.chunk.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="robots" content="noindex"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Lamassu Admin</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" class="root"></div><script>!function(e){function r(r){for(var n,a,l=r[0],i=r[1],f=r[2],c=0,s=[];c<l.length;c++)a=l[c],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var i=t[l];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="/";var l=this["webpackJsonplamassu-admin"]=this["webpackJsonplamassu-admin"]||[],i=l.push.bind(l);l.push=r,l=l.slice();for(var f=0;f<l.length;f++)r(l[f]);var p=i;t()}([])</script><script src="/static/js/2.346a7f7f.chunk.js"></script><script src="/static/js/main.b833e621.chunk.js"></script></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="robots" content="noindex"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Lamassu Admin</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" class="root"></div><script>!function(e){function r(r){for(var n,a,l=r[0],i=r[1],f=r[2],c=0,s=[];c<l.length;c++)a=l[c],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var i=t[l];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="/";var l=this["webpackJsonplamassu-admin"]=this["webpackJsonplamassu-admin"]||[],i=l.push.bind(l);l.push=r,l=l.slice();for(var f=0;f<l.length;f++)r(l[f]);var p=i;t()}([])</script><script src="/static/js/2.ee7f7ea2.chunk.js"></script><script src="/static/js/main.fc66d358.chunk.js"></script></body></html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
public/static/js/2.ee7f7ea2.chunk.js.map
Normal file
1
public/static/js/2.ee7f7ea2.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/static/js/main.fc66d358.chunk.js
Normal file
2
public/static/js/main.fc66d358.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
public/static/js/main.fc66d358.chunk.js.map
Normal file
1
public/static/js/main.fc66d358.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
|
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
|
||||||
<path fill="#F7931A" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
<path fill="#0AC18E" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
||||||
<path fill="#FFF" transform="rotate(-28 52 32)" d="m66.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
<path fill="#FFF" transform="rotate(-28 52 32)" d="m66.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Loading…
Add table
Add a link
Reference in a new issue