Merge branch 'dev' into backport/csv-empty-txs
This commit is contained in:
commit
0c289f7d37
104 changed files with 1784 additions and 380 deletions
|
|
@ -55,6 +55,20 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||||
|
common.logger.info(`fallbackfee already defined, skipping...`)
|
||||||
|
} else {
|
||||||
|
common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`)
|
||||||
|
common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||||
|
common.logger.info(`rpcworkqueue already defined, skipping...`)
|
||||||
|
} else {
|
||||||
|
common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`)
|
||||||
|
common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||||
|
}
|
||||||
|
|
||||||
if (isCurrentlyRunning && !isDevMode()) {
|
if (isCurrentlyRunning && !isDevMode()) {
|
||||||
common.logger.info('Starting wallet...')
|
common.logger.info('Starting wallet...')
|
||||||
common.es(`sudo supervisorctl start bitcoin`)
|
common.es(`sudo supervisorctl start bitcoin`)
|
||||||
|
|
|
||||||
|
|
@ -148,18 +148,17 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
// Important: We don't know what kind of error this is
|
// Important: We don't know what kind of error this is
|
||||||
// so not safe to assume that funds weren't sent.
|
// so not safe to assume that funds weren't sent.
|
||||||
// Therefore, don't set sendPending to false except for
|
|
||||||
// errors (like InsufficientFundsError) that are guaranteed
|
// Setting sendPending to true ensures that the transaction gets
|
||||||
// not to send.
|
// silently terminated and no retries are done
|
||||||
const sendPending = err.name !== 'InsufficientFundsError'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendTime: 'now()^',
|
sendTime: 'now()^',
|
||||||
error: err.message,
|
error: err.message,
|
||||||
errorCode: err.name,
|
errorCode: err.name,
|
||||||
sendPending
|
sendPending: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(sendRec => {
|
.then(sendRec => {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||||
|
|
||||||
function convertBigNumFields (obj) {
|
function convertBigNumFields (obj) {
|
||||||
const convert = (value, key) => {
|
const convert = (value, key) => {
|
||||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat' ])) {
|
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee', 'fixedFeeCrypto' ])) {
|
||||||
return value.toString()
|
return value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
||||||
const cashInFee = showCommissions ? commissions.cashIn / 100 : null
|
const cashInFee = showCommissions ? commissions.cashIn / 100 : null
|
||||||
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
|
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
|
||||||
const cashInFixedFee = showCommissions ? commissions.fixedFee : null
|
const cashInFixedFee = showCommissions ? commissions.fixedFee : null
|
||||||
|
const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null
|
||||||
const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null
|
const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null
|
||||||
const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null
|
const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
||||||
cashInFee,
|
cashInFee,
|
||||||
cashOutFee,
|
cashOutFee,
|
||||||
cashInFixedFee,
|
cashInFixedFee,
|
||||||
|
cashOutFixedFee,
|
||||||
cashInRate,
|
cashInRate,
|
||||||
cashOutRate
|
cashOutRate
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function getWithEmail (email) {
|
||||||
*
|
*
|
||||||
* @param {string} id Customer's id
|
* @param {string} id Customer's id
|
||||||
* @param {object} data Fields to update
|
* @param {object} data Fields to update
|
||||||
* @param {string} Acting user's token
|
* @param {string} userToken Acting user's token
|
||||||
*
|
*
|
||||||
* @returns {Promise} Newly updated Customer
|
* @returns {Promise} Newly updated Customer
|
||||||
*/
|
*/
|
||||||
|
|
@ -114,6 +114,7 @@ function update (id, data, userToken) {
|
||||||
async function updateCustomer (id, data, userToken) {
|
async function updateCustomer (id, data, userToken) {
|
||||||
const formattedData = _.pick(
|
const formattedData = _.pick(
|
||||||
[
|
[
|
||||||
|
'sanctions',
|
||||||
'authorized_override',
|
'authorized_override',
|
||||||
'id_card_photo_override',
|
'id_card_photo_override',
|
||||||
'id_card_data_override',
|
'id_card_data_override',
|
||||||
|
|
@ -229,7 +230,7 @@ function enhanceEditedPhotos (fields) {
|
||||||
/**
|
/**
|
||||||
* Remove the edited data from the db record
|
* Remove the edited data from the db record
|
||||||
*
|
*
|
||||||
* @name enhanceOverrideFields
|
* @name deleteEditedData
|
||||||
* @function
|
* @function
|
||||||
*
|
*
|
||||||
* @param {string} id Customer's id
|
* @param {string} id Customer's id
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
'cashInCommission',
|
'cashInCommission',
|
||||||
'cashInFee',
|
'cashInFee',
|
||||||
'cashOutCommission',
|
'cashOutCommission',
|
||||||
|
'cashOutFee',
|
||||||
'cryptoCode',
|
'cryptoCode',
|
||||||
'cryptoCodeDisplay',
|
'cryptoCodeDisplay',
|
||||||
'cryptoNetwork',
|
'cryptoNetwork',
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ type Coin {
|
||||||
display: String!
|
display: String!
|
||||||
minimumTx: String!
|
minimumTx: String!
|
||||||
cashInFee: String!
|
cashInFee: String!
|
||||||
|
cashOutFee: String!
|
||||||
cashInCommission: String!
|
cashInCommission: String!
|
||||||
cashOutCommission: String!
|
cashOutCommission: String!
|
||||||
cryptoNetwork: String!
|
cryptoNetwork: String!
|
||||||
|
|
|
||||||
15
lib/middlewares/addRWBytes.js
Normal file
15
lib/middlewares/addRWBytes.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
const addRWBytes = () => (req, res, next) => {
|
||||||
|
const handle = () => {
|
||||||
|
res.removeListener('finish', handle)
|
||||||
|
res.removeListener('close', handle)
|
||||||
|
res.bytesRead = req.connection.bytesRead
|
||||||
|
res.bytesWritten = req.connection.bytesWritten
|
||||||
|
}
|
||||||
|
|
||||||
|
res.on('finish', handle)
|
||||||
|
res.on('close', handle)
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = addRWBytes
|
||||||
|
|
@ -14,6 +14,7 @@ const machine = require('./machine.resolver')
|
||||||
const notification = require('./notification.resolver')
|
const notification = require('./notification.resolver')
|
||||||
const pairing = require('./pairing.resolver')
|
const pairing = require('./pairing.resolver')
|
||||||
const rates = require('./rates.resolver')
|
const rates = require('./rates.resolver')
|
||||||
|
const sanctions = require('./sanctions.resolver')
|
||||||
const scalar = require('./scalar.resolver')
|
const scalar = require('./scalar.resolver')
|
||||||
const settings = require('./settings.resolver')
|
const settings = require('./settings.resolver')
|
||||||
const sms = require('./sms.resolver')
|
const sms = require('./sms.resolver')
|
||||||
|
|
@ -37,6 +38,7 @@ const resolvers = [
|
||||||
notification,
|
notification,
|
||||||
pairing,
|
pairing,
|
||||||
rates,
|
rates,
|
||||||
|
sanctions,
|
||||||
scalar,
|
scalar,
|
||||||
settings,
|
settings,
|
||||||
sms,
|
sms,
|
||||||
|
|
|
||||||
13
lib/new-admin/graphql/resolvers/sanctions.resolver.js
Normal file
13
lib/new-admin/graphql/resolvers/sanctions.resolver.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
const sanctions = require('../../../sanctions')
|
||||||
|
const authentication = require('../modules/userManagement')
|
||||||
|
|
||||||
|
const resolvers = {
|
||||||
|
Query: {
|
||||||
|
checkAgainstSanctions: (...[, { customerId }, context]) => {
|
||||||
|
const token = authentication.getToken(context)
|
||||||
|
return sanctions.checkByUser(customerId, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = resolvers
|
||||||
|
|
@ -14,6 +14,7 @@ const machine = require('./machine.type')
|
||||||
const notification = require('./notification.type')
|
const notification = require('./notification.type')
|
||||||
const pairing = require('./pairing.type')
|
const pairing = require('./pairing.type')
|
||||||
const rates = require('./rates.type')
|
const rates = require('./rates.type')
|
||||||
|
const sanctions = require('./sanctions.type')
|
||||||
const scalar = require('./scalar.type')
|
const scalar = require('./scalar.type')
|
||||||
const settings = require('./settings.type')
|
const settings = require('./settings.type')
|
||||||
const sms = require('./sms.type')
|
const sms = require('./sms.type')
|
||||||
|
|
@ -37,6 +38,7 @@ const types = [
|
||||||
notification,
|
notification,
|
||||||
pairing,
|
pairing,
|
||||||
rates,
|
rates,
|
||||||
|
sanctions,
|
||||||
scalar,
|
scalar,
|
||||||
settings,
|
settings,
|
||||||
sms,
|
sms,
|
||||||
|
|
|
||||||
13
lib/new-admin/graphql/types/sanctions.type.js
Normal file
13
lib/new-admin/graphql/types/sanctions.type.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
const { gql } = require('apollo-server-express')
|
||||||
|
|
||||||
|
const typeDef = gql`
|
||||||
|
type SanctionMatches {
|
||||||
|
ofacSanctioned: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
checkAgainstSanctions(customerId: ID): SanctionMatches @auth
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
module.exports = typeDef
|
||||||
|
|
@ -23,7 +23,7 @@ const typeDef = gql`
|
||||||
errorCode: String
|
errorCode: String
|
||||||
operatorCompleted: Boolean
|
operatorCompleted: Boolean
|
||||||
sendPending: Boolean
|
sendPending: Boolean
|
||||||
cashInFee: String
|
fixedFee: String
|
||||||
minimumTx: Float
|
minimumTx: Float
|
||||||
customerId: ID
|
customerId: ID
|
||||||
isAnonymous: Boolean
|
isAnonymous: Boolean
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,20 @@ function batch (
|
||||||
excludeTestingCustomers = false,
|
excludeTestingCustomers = false,
|
||||||
simplified
|
simplified
|
||||||
) {
|
) {
|
||||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addProfits, addNames)
|
|
||||||
const isCsvExport = _.isBoolean(simplified)
|
const isCsvExport = _.isBoolean(simplified)
|
||||||
|
const packager = _.flow(
|
||||||
|
_.flatten,
|
||||||
|
_.orderBy(_.property('created'), ['desc']),
|
||||||
|
_.map(_.flow(
|
||||||
|
camelize,
|
||||||
|
_.mapKeys(k =>
|
||||||
|
k == 'cashInFee' ? 'fixedFee' :
|
||||||
|
k
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
addProfits,
|
||||||
|
addNames
|
||||||
|
)
|
||||||
|
|
||||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||||
c.phone AS customer_phone,
|
c.phone AS customer_phone,
|
||||||
|
|
@ -154,7 +166,7 @@ function batch (
|
||||||
function advancedBatch (data) {
|
function advancedBatch (data) {
|
||||||
const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms',
|
const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms',
|
||||||
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
||||||
'dispense', 'notified', 'redeem', 'phone', 'error',
|
'dispense', 'notified', 'redeem', 'phone', 'error', 'fixedFee',
|
||||||
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
||||||
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
||||||
'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6',
|
'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6',
|
||||||
|
|
@ -170,7 +182,9 @@ function advancedBatch (data) {
|
||||||
...it,
|
...it,
|
||||||
status: getStatus(it),
|
status: getStatus(it),
|
||||||
fiatProfit: getProfit(it).toString(),
|
fiatProfit: getProfit(it).toString(),
|
||||||
cryptoAmount: getCryptoAmount(it).toString()
|
cryptoAmount: getCryptoAmount(it).toString(),
|
||||||
|
fixedFee: it.fixedFee ?? null,
|
||||||
|
fee: it.fee ?? null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data)
|
return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data)
|
||||||
|
|
|
||||||
|
|
@ -100,16 +100,19 @@ function loadAccounts (schemaVersion) {
|
||||||
.then(_.compose(_.defaultTo({}), _.get('data.accounts')))
|
.then(_.compose(_.defaultTo({}), _.get('data.accounts')))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideSecretFields (accounts) {
|
||||||
|
return _.flow(
|
||||||
|
_.filter(path => !_.isEmpty(_.get(path, accounts))),
|
||||||
|
_.reduce(
|
||||||
|
(accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts),
|
||||||
|
accounts
|
||||||
|
)
|
||||||
|
)(SECRET_FIELDS)
|
||||||
|
}
|
||||||
|
|
||||||
function showAccounts (schemaVersion) {
|
function showAccounts (schemaVersion) {
|
||||||
return loadAccounts(schemaVersion)
|
return loadAccounts(schemaVersion)
|
||||||
.then(accounts => {
|
.then(hideSecretFields)
|
||||||
const filledSecretPaths = _.compact(_.map(path => {
|
|
||||||
if (!_.isEmpty(_.get(path, accounts))) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}, SECRET_FIELDS))
|
|
||||||
return _.compose(_.map(path => _.assoc(path, PASSWORD_FILLED), filledSecretPaths))(accounts)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertConfigRow = (dbOrTx, data) =>
|
const insertConfigRow = (dbOrTx, data) =>
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,7 @@ function plugins (settings, deviceId) {
|
||||||
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||||
const minimumTx = new BN(commissions.minimumTx)
|
const minimumTx = new BN(commissions.minimumTx)
|
||||||
const cashInFee = new BN(commissions.fixedFee)
|
const cashInFee = new BN(commissions.fixedFee)
|
||||||
|
const cashOutFee = new BN(commissions.cashOutFixedFee)
|
||||||
const cashInCommission = new BN(commissions.cashIn)
|
const cashInCommission = new BN(commissions.cashIn)
|
||||||
const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null
|
const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null
|
||||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||||
|
|
@ -261,6 +262,7 @@ function plugins (settings, deviceId) {
|
||||||
isCashInOnly: Boolean(cryptoRec.isCashinOnly),
|
isCashInOnly: Boolean(cryptoRec.isCashinOnly),
|
||||||
minimumTx: BN.max(minimumTx, cashInFee),
|
minimumTx: BN.max(minimumTx, cashInFee),
|
||||||
cashInFee,
|
cashInFee,
|
||||||
|
cashOutFee,
|
||||||
cashInCommission,
|
cashInCommission,
|
||||||
cashOutCommission,
|
cashOutCommission,
|
||||||
cryptoNetwork,
|
cryptoNetwork,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@ const nocache = require('nocache')
|
||||||
|
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
|
||||||
|
const addRWBytes = require('./middlewares/addRWBytes')
|
||||||
const authorize = require('./middlewares/authorize')
|
const authorize = require('./middlewares/authorize')
|
||||||
|
const computeSchema = require('./middlewares/compute-schema')
|
||||||
const errorHandler = require('./middlewares/errorHandler')
|
const errorHandler = require('./middlewares/errorHandler')
|
||||||
const filterOldRequests = require('./middlewares/filterOldRequests')
|
const filterOldRequests = require('./middlewares/filterOldRequests')
|
||||||
const computeSchema = require('./middlewares/compute-schema')
|
|
||||||
const findOperatorId = require('./middlewares/operatorId')
|
const findOperatorId = require('./middlewares/operatorId')
|
||||||
const populateDeviceId = require('./middlewares/populateDeviceId')
|
const populateDeviceId = require('./middlewares/populateDeviceId')
|
||||||
const populateSettings = require('./middlewares/populateSettings')
|
const populateSettings = require('./middlewares/populateSettings')
|
||||||
|
|
@ -50,11 +51,24 @@ const configRequiredRoutes = [
|
||||||
]
|
]
|
||||||
|
|
||||||
// middleware setup
|
// middleware setup
|
||||||
|
app.use(addRWBytes())
|
||||||
app.use(compression({ threshold: 500 }))
|
app.use(compression({ threshold: 500 }))
|
||||||
app.use(helmet())
|
app.use(helmet())
|
||||||
app.use(nocache())
|
app.use(nocache())
|
||||||
app.use(express.json({ limit: '2mb' }))
|
app.use(express.json({ limit: '2mb' }))
|
||||||
app.use(morgan(':method :url :status :response-time ms -- :req[content-length]/:res[content-length] b', { stream: logger.stream }))
|
|
||||||
|
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
||||||
|
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
||||||
|
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
|
||||||
|
|
||||||
|
app.use('/robots.txt', (req, res) => {
|
||||||
|
res.type('text/plain')
|
||||||
|
res.send("User-agent: *\nDisallow: /")
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendStatus(404)
|
||||||
|
})
|
||||||
|
|
||||||
// app /pair and /ca routes
|
// app /pair and /ca routes
|
||||||
app.use('/', pairingRoutes)
|
app.use('/', pairingRoutes)
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ function updateCustomer (req, res, next) {
|
||||||
.then(_.merge(patch))
|
.then(_.merge(patch))
|
||||||
.then(newPatch => customers.updatePhotoCard(id, newPatch))
|
.then(newPatch => customers.updatePhotoCard(id, newPatch))
|
||||||
.then(newPatch => customers.updateFrontCamera(id, newPatch))
|
.then(newPatch => customers.updateFrontCamera(id, newPatch))
|
||||||
.then(newPatch => customers.update(id, newPatch, null, txId))
|
.then(newPatch => customers.update(id, newPatch, null))
|
||||||
.then(customer => {
|
.then(customer => {
|
||||||
createPendingManualComplianceNotifs(settings, customer, deviceId)
|
createPendingManualComplianceNotifs(settings, customer, deviceId)
|
||||||
respond(req, res, { customer })
|
respond(req, res, { customer })
|
||||||
|
|
|
||||||
44
lib/sanctions.js
Normal file
44
lib/sanctions.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
const ofac = require('./ofac')
|
||||||
|
const T = require('./time')
|
||||||
|
const logger = require('./logger')
|
||||||
|
const customers = require('./customers')
|
||||||
|
|
||||||
|
const sanctionStatus = {
|
||||||
|
loaded: false,
|
||||||
|
timestamp: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadOrUpdateSanctions = () => {
|
||||||
|
if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) {
|
||||||
|
logger.info('No sanction lists loaded. Loading sanctions...')
|
||||||
|
return ofac.load()
|
||||||
|
.then(() => {
|
||||||
|
logger.info('OFAC sanction list loaded!')
|
||||||
|
sanctionStatus.loaded = true
|
||||||
|
sanctionStatus.timestamp = Date.now()
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
logger.error('Couldn\'t load OFAC sanction list!')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkByUser = (customerId, userToken) => {
|
||||||
|
return Promise.all([loadOrUpdateSanctions(), customers.getCustomerById(customerId)])
|
||||||
|
.then(([, customer]) => {
|
||||||
|
const { firstName, lastName, dateOfBirth } = customer?.idCardData
|
||||||
|
const birthdate = _.replace(/-/g, '')(dateOfBirth)
|
||||||
|
const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false })
|
||||||
|
const isOfacSanctioned = _.size(ofacMatches) > 0
|
||||||
|
customers.updateCustomer(customerId, { sanctions: !isOfacSanctioned }, userToken)
|
||||||
|
|
||||||
|
return { ofacSanctioned: isOfacSanctioned }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
checkByUser
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,7 @@ function massage (tx, pi) {
|
||||||
: {
|
: {
|
||||||
cryptoAtoms: new BN(r.cryptoAtoms),
|
cryptoAtoms: new BN(r.cryptoAtoms),
|
||||||
fiat: new BN(r.fiat),
|
fiat: new BN(r.fiat),
|
||||||
|
fixedFee: new BN(r.fixedFee),
|
||||||
rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null,
|
rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null,
|
||||||
commissionPercentage: new BN(r.commissionPercentage)
|
commissionPercentage: new BN(r.commissionPercentage)
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +70,7 @@ function cancel (txId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function customerHistory (customerId, thresholdDays) {
|
function customerHistory (customerId, thresholdDays) {
|
||||||
const sql = `SELECT * FROM (
|
const sql = `SELECT ch.id, ch.created, ch.fiat, ch.direction FROM (
|
||||||
SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction,
|
SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction,
|
||||||
((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired
|
((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired
|
||||||
FROM cash_in_txs txIn
|
FROM cash_in_txs txIn
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = next => db.multi([
|
||||||
|
'ALTER TABLE cash_out_txs ADD COLUMN fixed_fee numeric(14, 5) NOT NULL DEFAULT 0;'
|
||||||
|
], next)
|
||||||
|
|
||||||
|
exports.down = next => next()
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
const { saveConfig } = require('../lib/new-settings-loader')
|
||||||
|
|
||||||
|
exports.up = next => saveConfig({ 'commissions_cashOutFixedFee': 0 })
|
||||||
|
.then(next)
|
||||||
|
.catch(next)
|
||||||
|
|
||||||
|
exports.down = next => next()
|
||||||
2
new-lamassu-admin/public/robots.txt
Normal file
2
new-lamassu-admin/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
|
@ -13,9 +13,10 @@ const useStyles = makeStyles({
|
||||||
display: 'flex'
|
display: 'flex'
|
||||||
},
|
},
|
||||||
imgInner: {
|
imgInner: {
|
||||||
objectFit: 'cover',
|
objectFit: 'contain',
|
||||||
objectPosition: 'center',
|
objectPosition: 'center',
|
||||||
width: 500,
|
width: 500,
|
||||||
|
height: 400,
|
||||||
marginBottom: 40
|
marginBottom: 40
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -105,20 +105,29 @@ const Popover = ({
|
||||||
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const arrowClasses = {
|
const getArrowClasses = placement => ({
|
||||||
[classes.arrow]: true,
|
[classes.arrow]: true,
|
||||||
[classes.arrowBottom]: props.placement === 'bottom',
|
[classes.arrowBottom]: placement === 'bottom',
|
||||||
[classes.arrowTop]: props.placement === 'top',
|
[classes.arrowTop]: placement === 'top',
|
||||||
[classes.arrowRight]: props.placement === 'right',
|
[classes.arrowRight]: placement === 'right',
|
||||||
[classes.arrowLeft]: props.placement === 'left'
|
[classes.arrowLeft]: placement === 'left'
|
||||||
|
})
|
||||||
|
|
||||||
|
const flipPlacements = {
|
||||||
|
top: ['bottom'],
|
||||||
|
bottom: ['top'],
|
||||||
|
left: ['right'],
|
||||||
|
right: ['left']
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiers = R.merge(props.modifiers, {
|
const modifiers = R.mergeDeepLeft(props.modifiers, {
|
||||||
flip: {
|
flip: {
|
||||||
enabled: false
|
enabled: R.defaultTo(false, props.flip),
|
||||||
|
allowedAutoPlacements: flipPlacements[props.placement],
|
||||||
|
boundary: 'clippingParents'
|
||||||
},
|
},
|
||||||
preventOverflow: {
|
preventOverflow: {
|
||||||
enabled: true,
|
enabled: R.defaultTo(true, props.preventOverflow),
|
||||||
boundariesElement: 'scrollParent'
|
boundariesElement: 'scrollParent'
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
|
|
@ -126,7 +135,7 @@ const Popover = ({
|
||||||
offset: '0, 10'
|
offset: '0, 10'
|
||||||
},
|
},
|
||||||
arrow: {
|
arrow: {
|
||||||
enabled: true,
|
enabled: R.defaultTo(true, props.showArrow),
|
||||||
element: arrowRef
|
element: arrowRef
|
||||||
},
|
},
|
||||||
computeStyle: {
|
computeStyle: {
|
||||||
|
|
@ -134,6 +143,12 @@ const Popover = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (props.preventOverflow === false) {
|
||||||
|
modifiers.hide = {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MaterialPopper
|
<MaterialPopper
|
||||||
|
|
@ -141,10 +156,15 @@ const Popover = ({
|
||||||
modifiers={modifiers}
|
modifiers={modifiers}
|
||||||
className={classes.popover}
|
className={classes.popover}
|
||||||
{...props}>
|
{...props}>
|
||||||
<Paper className={classnames(classes.root, className)}>
|
{({ placement }) => (
|
||||||
<span className={classnames(arrowClasses)} ref={setArrowRef} />
|
<Paper className={classnames(classes.root, className)}>
|
||||||
{children}
|
<span
|
||||||
</Paper>
|
className={classnames(getArrowClasses(placement))}
|
||||||
|
ref={setArrowRef}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
</MaterialPopper>
|
</MaterialPopper>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@ const useStyles = makeStyles({
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
marginTop: 4
|
marginTop: 4
|
||||||
},
|
},
|
||||||
|
relativelyPositioned: {
|
||||||
|
position: 'relative'
|
||||||
|
},
|
||||||
|
safeSpace: {
|
||||||
|
position: 'absolute',
|
||||||
|
backgroundColor: '#0000',
|
||||||
|
height: 40,
|
||||||
|
left: '-50%',
|
||||||
|
width: '200%'
|
||||||
|
},
|
||||||
popoverContent: ({ width }) => ({
|
popoverContent: ({ width }) => ({
|
||||||
width,
|
width,
|
||||||
padding: [[10, 15]]
|
padding: [[10, 15]]
|
||||||
|
|
@ -27,6 +37,10 @@ const usePopperHandler = width => {
|
||||||
setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
|
setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openHelpPopper = event => {
|
||||||
|
setHelpPopperAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
const handleCloseHelpPopper = () => {
|
const handleCloseHelpPopper = () => {
|
||||||
setHelpPopperAnchorEl(null)
|
setHelpPopperAnchorEl(null)
|
||||||
}
|
}
|
||||||
|
|
@ -38,25 +52,32 @@ const usePopperHandler = width => {
|
||||||
helpPopperAnchorEl,
|
helpPopperAnchorEl,
|
||||||
helpPopperOpen,
|
helpPopperOpen,
|
||||||
handleOpenHelpPopper,
|
handleOpenHelpPopper,
|
||||||
|
openHelpPopper,
|
||||||
handleCloseHelpPopper
|
handleCloseHelpPopper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tooltip = memo(({ children, width, Icon = HelpIcon }) => {
|
const HelpTooltip = memo(({ children, width }) => {
|
||||||
const handler = usePopperHandler(width)
|
const handler = usePopperHandler(width)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||||
<div>
|
<div
|
||||||
|
className={handler.classes.relativelyPositioned}
|
||||||
|
onMouseLeave={handler.handleCloseHelpPopper}>
|
||||||
|
{handler.helpPopperOpen && (
|
||||||
|
<div className={handler.classes.safeSpace}></div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={handler.classes.transparentButton}
|
className={handler.classes.transparentButton}
|
||||||
onClick={handler.handleOpenHelpPopper}>
|
onMouseEnter={handler.openHelpPopper}>
|
||||||
<Icon />
|
<HelpIcon />
|
||||||
</button>
|
</button>
|
||||||
<Popper
|
<Popper
|
||||||
open={handler.helpPopperOpen}
|
open={handler.helpPopperOpen}
|
||||||
anchorEl={handler.helpPopperAnchorEl}
|
anchorEl={handler.helpPopperAnchorEl}
|
||||||
|
arrowEnabled={true}
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
<div className={handler.classes.popoverContent}>{children}</div>
|
<div className={handler.classes.popoverContent}>{children}</div>
|
||||||
</Popper>
|
</Popper>
|
||||||
|
|
@ -69,31 +90,30 @@ const HoverableTooltip = memo(({ parentElements, children, width }) => {
|
||||||
const handler = usePopperHandler(width)
|
const handler = usePopperHandler(width)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||||
{!R.isNil(parentElements) && (
|
<div>
|
||||||
<div
|
{!R.isNil(parentElements) && (
|
||||||
onMouseEnter={handler.handleOpenHelpPopper}
|
<div onMouseEnter={handler.handleOpenHelpPopper}>
|
||||||
onMouseLeave={handler.handleCloseHelpPopper}>
|
{parentElements}
|
||||||
{parentElements}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{R.isNil(parentElements) && (
|
||||||
{R.isNil(parentElements) && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onMouseEnter={handler.handleOpenHelpPopper}
|
||||||
onMouseEnter={handler.handleOpenHelpPopper}
|
className={handler.classes.transparentButton}>
|
||||||
onMouseLeave={handler.handleCloseHelpPopper}
|
<HelpIcon />
|
||||||
className={handler.classes.transparentButton}>
|
</button>
|
||||||
<HelpIcon />
|
)}
|
||||||
</button>
|
<Popper
|
||||||
)}
|
open={handler.helpPopperOpen}
|
||||||
<Popper
|
anchorEl={handler.helpPopperAnchorEl}
|
||||||
open={handler.helpPopperOpen}
|
placement="bottom">
|
||||||
anchorEl={handler.helpPopperAnchorEl}
|
<div className={handler.classes.popoverContent}>{children}</div>
|
||||||
placement="bottom">
|
</Popper>
|
||||||
<div className={handler.classes.popoverContent}>{children}</div>
|
</div>
|
||||||
</Popper>
|
</ClickAwayListener>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export { Tooltip, HoverableTooltip }
|
export { HoverableTooltip, HelpTooltip }
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ const styles = {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
'& img': {
|
'& img': {
|
||||||
maxHeight: 145
|
height: 145,
|
||||||
|
minWidth: 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +128,8 @@ const IDButton = memo(
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
arrowSize={3}
|
arrowSize={3}
|
||||||
placement="top">
|
placement="top"
|
||||||
|
flip>
|
||||||
<div className={classes.popoverContent}>
|
<div className={classes.popoverContent}>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
TDoubleLevelHead,
|
TDoubleLevelHead,
|
||||||
ThDoubleLevel
|
ThDoubleLevel
|
||||||
} from 'src/components/fake-table/Table'
|
} from 'src/components/fake-table/Table'
|
||||||
import { startCase } from 'src/utils/string'
|
import { sentenceCase } from 'src/utils/string'
|
||||||
|
|
||||||
import TableCtx from './Context'
|
import TableCtx from './Context'
|
||||||
|
|
||||||
|
|
@ -22,22 +22,27 @@ const styles = {
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const groupSecondHeader = elements => {
|
const groupSecondHeader = elements => {
|
||||||
const [toSHeader, noSHeader] = R.partition(R.has('doubleHeader'))(elements)
|
const doubleHeader = R.prop('doubleHeader')
|
||||||
|
const sameDoubleHeader = (a, b) => doubleHeader(a) === doubleHeader(b)
|
||||||
if (!toSHeader.length) {
|
const group = R.pipe(
|
||||||
return [elements, THead]
|
R.groupWith(sameDoubleHeader),
|
||||||
}
|
R.map(group =>
|
||||||
|
R.isNil(doubleHeader(group[0])) // No doubleHeader
|
||||||
const index = R.indexOf(toSHeader[0], elements)
|
? group
|
||||||
const width = R.compose(R.sum, R.map(R.path(['width'])))(toSHeader)
|
: [
|
||||||
|
{
|
||||||
const innerElements = R.insert(
|
width: R.sum(R.map(R.prop('width'), group)),
|
||||||
index,
|
elements: group,
|
||||||
{ width, elements: toSHeader, name: toSHeader[0].doubleHeader },
|
name: doubleHeader(group[0])
|
||||||
noSHeader
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
R.reduce(R.concat, [])
|
||||||
)
|
)
|
||||||
|
|
||||||
return [innerElements, TDoubleLevelHead]
|
return R.all(R.pipe(doubleHeader, R.isNil), elements)
|
||||||
|
? [elements, THead]
|
||||||
|
: [group(elements), TDoubleLevelHead]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
|
|
@ -99,7 +104,7 @@ const Header = () => {
|
||||||
<>{attachOrderedByToComplexHeader(header) ?? header}</>
|
<>{attachOrderedByToComplexHeader(header) ?? header}</>
|
||||||
) : (
|
) : (
|
||||||
<span className={orderClasses}>
|
<span className={orderClasses}>
|
||||||
{!R.isNil(display) ? display : startCase(name)}{' '}
|
{!R.isNil(display) ? display : sentenceCase(name)}{' '}
|
||||||
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
|
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ const MachineActions = memo(({ machine, onActionSuccess }) => {
|
||||||
display: 'Restart services for'
|
display: 'Restart services for'
|
||||||
})
|
})
|
||||||
}}>
|
}}>
|
||||||
Restart Services
|
Restart services
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{machine.model === 'aveiro' && (
|
{machine.model === 'aveiro' && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import * as R from 'ramda'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
|
|
||||||
import AppContext from 'src/AppContext'
|
import AppContext from 'src/AppContext'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
import { H4, Info2, P } from 'src/components/typography'
|
import { H4, Info2, P } from 'src/components/typography'
|
||||||
|
|
@ -128,9 +128,9 @@ const Accounting = () => {
|
||||||
<span className={classes.operation}>
|
<span className={classes.operation}>
|
||||||
{it.description}
|
{it.description}
|
||||||
{!!it.extraInfo && (
|
{!!it.extraInfo && (
|
||||||
<HoverableTooltip width={175}>
|
<HelpTooltip width={175}>
|
||||||
<P>{it.extraInfo}</P>
|
<P>{it.extraInfo}</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,26 @@ import LegendEntry from './components/LegendEntry'
|
||||||
import HourOfDayWrapper from './components/wrappers/HourOfDayWrapper'
|
import HourOfDayWrapper from './components/wrappers/HourOfDayWrapper'
|
||||||
import OverTimeWrapper from './components/wrappers/OverTimeWrapper'
|
import OverTimeWrapper from './components/wrappers/OverTimeWrapper'
|
||||||
import TopMachinesWrapper from './components/wrappers/TopMachinesWrapper'
|
import TopMachinesWrapper from './components/wrappers/TopMachinesWrapper'
|
||||||
|
import VolumeOverTimeWrapper from './components/wrappers/VolumeOverTimeWrapper'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const MACHINE_OPTIONS = [{ code: 'all', display: 'All machines' }]
|
const MACHINE_OPTIONS = [{ code: 'all', display: 'All machines' }]
|
||||||
const REPRESENTING_OPTIONS = [
|
const REPRESENTING_OPTIONS = [
|
||||||
{ code: 'overTime', display: 'Over time' },
|
{ code: 'overTime', display: 'Over time' },
|
||||||
{ code: 'topMachines', display: 'Top Machines' },
|
{ code: 'volumeOverTime', display: 'Volume' },
|
||||||
|
{ code: 'topMachines', display: 'Top machines' },
|
||||||
{ code: 'hourOfTheDay', display: 'Hour of the day' }
|
{ code: 'hourOfTheDay', display: 'Hour of the day' }
|
||||||
]
|
]
|
||||||
const PERIOD_OPTIONS = [
|
const PERIOD_OPTIONS = [
|
||||||
{ code: 'day', display: 'Last 24 hours' },
|
{ code: 'day', display: 'Last 24 hours' },
|
||||||
|
{ code: 'threeDays', display: 'Last 3 days' },
|
||||||
{ code: 'week', display: 'Last 7 days' },
|
{ code: 'week', display: 'Last 7 days' },
|
||||||
{ code: 'month', display: 'Last 30 days' }
|
{ code: 'month', display: 'Last 30 days' }
|
||||||
]
|
]
|
||||||
const TIME_OPTIONS = {
|
const TIME_OPTIONS = {
|
||||||
day: DAY,
|
day: DAY,
|
||||||
|
threeDays: 3 * DAY,
|
||||||
week: WEEK,
|
week: WEEK,
|
||||||
month: MONTH
|
month: MONTH
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +81,7 @@ const GET_TRANSACTIONS = gql`
|
||||||
hasError: error
|
hasError: error
|
||||||
deviceId
|
deviceId
|
||||||
fiat
|
fiat
|
||||||
cashInFee
|
fixedFee
|
||||||
fiatCode
|
fiatCode
|
||||||
cryptoAtoms
|
cryptoAtoms
|
||||||
cryptoCode
|
cryptoCode
|
||||||
|
|
@ -223,13 +227,11 @@ const Analytics = () => {
|
||||||
previous: filteredData(period.code).previous.length
|
previous: filteredData(period.code).previous.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const avgAmount = {
|
const median = values => (values.length === 0 ? 0 : R.median(values))
|
||||||
current:
|
|
||||||
R.sum(R.map(d => d.fiat, filteredData(period.code).current)) /
|
const medianAmount = {
|
||||||
(txs.current === 0 ? 1 : txs.current),
|
current: median(R.map(d => d.fiat, filteredData(period.code).current)),
|
||||||
previous:
|
previous: median(R.map(d => d.fiat, filteredData(period.code).previous))
|
||||||
R.sum(R.map(d => d.fiat, filteredData(period.code).previous)) /
|
|
||||||
(txs.previous === 0 ? 1 : txs.previous)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const txVolume = {
|
const txVolume = {
|
||||||
|
|
@ -265,6 +267,20 @@ const Analytics = () => {
|
||||||
currency={fiatLocale}
|
currency={fiatLocale}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'volumeOverTime':
|
||||||
|
return (
|
||||||
|
<VolumeOverTimeWrapper
|
||||||
|
title="Transactions volume over time"
|
||||||
|
representing={representing}
|
||||||
|
period={period}
|
||||||
|
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
|
||||||
|
machines={machineOptions}
|
||||||
|
selectedMachine={machine}
|
||||||
|
handleMachineChange={setMachine}
|
||||||
|
timezone={timezone}
|
||||||
|
currency={fiatLocale}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case 'topMachines':
|
case 'topMachines':
|
||||||
return (
|
return (
|
||||||
<TopMachinesWrapper
|
<TopMachinesWrapper
|
||||||
|
|
@ -347,9 +363,9 @@ const Analytics = () => {
|
||||||
/>
|
/>
|
||||||
<div className={classes.verticalLine} />
|
<div className={classes.verticalLine} />
|
||||||
<OverviewEntry
|
<OverviewEntry
|
||||||
label="Avg. txn amount"
|
label="Median amount"
|
||||||
value={avgAmount.current}
|
value={medianAmount.current}
|
||||||
oldValue={avgAmount.previous}
|
oldValue={medianAmount.previous}
|
||||||
currency={fiatLocale}
|
currency={fiatLocale}
|
||||||
/>
|
/>
|
||||||
<div className={classes.verticalLine} />
|
<div className={classes.verticalLine} />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
import { offDarkColor, tomato, neon, java } from 'src/styling/variables'
|
import {
|
||||||
|
offColor,
|
||||||
|
offDarkColor,
|
||||||
|
tomato,
|
||||||
|
neon,
|
||||||
|
java
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
|
import typographyStyles from '../../components/typography/styles'
|
||||||
|
|
||||||
|
const { label1 } = typographyStyles
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
overviewLegend: {
|
overviewLegend: {
|
||||||
|
|
@ -135,6 +145,18 @@ const styles = {
|
||||||
topMachinesRadio: {
|
topMachinesRadio: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
graphHeaderSwitchBox: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
'& > *': {
|
||||||
|
margin: 0
|
||||||
|
},
|
||||||
|
'& > :first-child': {
|
||||||
|
marginBottom: 2,
|
||||||
|
extend: label1,
|
||||||
|
color: offColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,12 @@ const GraphTooltip = ({
|
||||||
|
|
||||||
const formattedDateInterval = !R.includes('hourOfDay', representing.code)
|
const formattedDateInterval = !R.includes('hourOfDay', representing.code)
|
||||||
? [
|
? [
|
||||||
formatDate(
|
formatDate(dateInterval[1], null, 'MMM d'),
|
||||||
dateInterval[1],
|
formatDate(dateInterval[1], null, 'HH:mm'),
|
||||||
null,
|
formatDate(dateInterval[0], null, 'HH:mm')
|
||||||
period.code === 'day' ? 'MMM d, HH:mm' : 'MMM d'
|
|
||||||
),
|
|
||||||
formatDate(
|
|
||||||
dateInterval[0],
|
|
||||||
null,
|
|
||||||
period.code === 'day' ? 'HH:mm' : 'MMM d'
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
|
formatDate(dateInterval[1], null, 'MMM d'),
|
||||||
formatDateNonUtc(dateInterval[1], 'HH:mm'),
|
formatDateNonUtc(dateInterval[1], 'HH:mm'),
|
||||||
formatDateNonUtc(dateInterval[0], 'HH:mm')
|
formatDateNonUtc(dateInterval[0], 'HH:mm')
|
||||||
]
|
]
|
||||||
|
|
@ -55,10 +49,11 @@ const GraphTooltip = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className={classes.dotOtWrapper}>
|
<Paper className={classes.dotOtWrapper}>
|
||||||
|
{!R.includes('hourOfDay', representing.code) && (
|
||||||
|
<Info2 noMargin>{`${formattedDateInterval[0]}`}</Info2>
|
||||||
|
)}
|
||||||
<Info2 noMargin>
|
<Info2 noMargin>
|
||||||
{period.code === 'day' || R.includes('hourOfDay', representing.code)
|
{`${formattedDateInterval[1]} - ${formattedDateInterval[2]}`}
|
||||||
? `${formattedDateInterval[0]} - ${formattedDateInterval[1]}`
|
|
||||||
: `${formattedDateInterval[0]}`}
|
|
||||||
</Info2>
|
</Info2>
|
||||||
<P noMargin className={classes.dotOtTransactionAmount}>
|
<P noMargin className={classes.dotOtTransactionAmount}>
|
||||||
{R.length(data)}{' '}
|
{R.length(data)}{' '}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Box } from '@material-ui/core'
|
import { Box } from '@material-ui/core'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { Select } from 'src/components/inputs'
|
import { Select, Switch } from 'src/components/inputs'
|
||||||
import { H2 } from 'src/components/typography'
|
import { H2 } from 'src/components/typography'
|
||||||
import { primaryColor } from 'src/styling/variables'
|
import { primaryColor } from 'src/styling/variables'
|
||||||
|
|
||||||
|
|
@ -25,11 +25,13 @@ const OverTimeDotGraphHeader = ({
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [logarithmic, setLogarithmic] = useState()
|
||||||
|
|
||||||
const legend = {
|
const legend = {
|
||||||
cashIn: <div className={classes.cashInIcon}></div>,
|
cashIn: <div className={classes.cashInIcon}></div>,
|
||||||
cashOut: <div className={classes.cashOutIcon}></div>,
|
cashOut: <div className={classes.cashOutIcon}></div>,
|
||||||
transaction: <div className={classes.txIcon}></div>,
|
transaction: <div className={classes.txIcon}></div>,
|
||||||
average: (
|
median: (
|
||||||
<svg height="12" width="18">
|
<svg height="12" width="18">
|
||||||
<path
|
<path
|
||||||
stroke={primaryColor}
|
stroke={primaryColor}
|
||||||
|
|
@ -53,10 +55,14 @@ const OverTimeDotGraphHeader = ({
|
||||||
IconElement={legend.transaction}
|
IconElement={legend.transaction}
|
||||||
label={'One transaction'}
|
label={'One transaction'}
|
||||||
/>
|
/>
|
||||||
<LegendEntry IconElement={legend.average} label={'Average'} />
|
<LegendEntry IconElement={legend.median} label={'Median'} />
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.graphHeaderRight}>
|
<div className={classes.graphHeaderRight}>
|
||||||
|
<div className={classes.graphHeaderSwitchBox}>
|
||||||
|
<span>Log. scale</span>
|
||||||
|
<Switch onChange={event => setLogarithmic(event.target.checked)} />
|
||||||
|
</div>
|
||||||
<Select
|
<Select
|
||||||
label="Machines"
|
label="Machines"
|
||||||
onSelectedItemChange={handleMachineChange}
|
onSelectedItemChange={handleMachineChange}
|
||||||
|
|
@ -74,6 +80,7 @@ const OverTimeDotGraphHeader = ({
|
||||||
currency={currency}
|
currency={currency}
|
||||||
selectedMachine={selectedMachine}
|
selectedMachine={selectedMachine}
|
||||||
machines={machines}
|
machines={machines}
|
||||||
|
log={logarithmic}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { Select, Switch } from 'src/components/inputs'
|
||||||
|
import { H2 } from 'src/components/typography'
|
||||||
|
import { neon, java } from 'src/styling/variables'
|
||||||
|
|
||||||
|
import styles from '../../Analytics.styles'
|
||||||
|
import Graph from '../../graphs/Graph'
|
||||||
|
import LegendEntry from '../LegendEntry'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const VolumeOverTimeGraphHeader = ({
|
||||||
|
title,
|
||||||
|
representing,
|
||||||
|
period,
|
||||||
|
data,
|
||||||
|
machines,
|
||||||
|
selectedMachine,
|
||||||
|
handleMachineChange,
|
||||||
|
timezone,
|
||||||
|
currency
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [logarithmic, setLogarithmic] = useState()
|
||||||
|
|
||||||
|
const legend = {
|
||||||
|
cashIn: (
|
||||||
|
<svg height="12" width="18">
|
||||||
|
<path
|
||||||
|
stroke={java}
|
||||||
|
strokeWidth="3"
|
||||||
|
d="M 3 6 l 12 0"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
cashOut: (
|
||||||
|
<svg height="12" width="18">
|
||||||
|
<path
|
||||||
|
stroke={neon}
|
||||||
|
strokeWidth="3"
|
||||||
|
d="M 3 6 l 12 0"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.graphHeaderWrapper}>
|
||||||
|
<div className={classes.graphHeaderLeft}>
|
||||||
|
<H2 noMargin>{title}</H2>
|
||||||
|
<Box className={classes.graphLegend}>
|
||||||
|
<LegendEntry IconElement={legend.cashIn} label={'Cash-in'} />
|
||||||
|
<LegendEntry IconElement={legend.cashOut} label={'Cash-out'} />
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
<div className={classes.graphHeaderRight}>
|
||||||
|
<div className={classes.graphHeaderSwitchBox}>
|
||||||
|
<span>Log. scale</span>
|
||||||
|
<Switch onChange={event => setLogarithmic(event.target.checked)} />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
label="Machines"
|
||||||
|
onSelectedItemChange={handleMachineChange}
|
||||||
|
items={machines}
|
||||||
|
default={machines[0]}
|
||||||
|
selectedItem={selectedMachine}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Graph
|
||||||
|
representing={representing}
|
||||||
|
period={period}
|
||||||
|
data={data}
|
||||||
|
timezone={timezone}
|
||||||
|
currency={currency}
|
||||||
|
selectedMachine={selectedMachine}
|
||||||
|
machines={machines}
|
||||||
|
log={logarithmic}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VolumeOverTimeGraphHeader
|
||||||
|
|
@ -5,6 +5,7 @@ import GraphTooltip from '../components/tooltips/GraphTooltip'
|
||||||
|
|
||||||
import HourOfDayBarGraph from './HourOfDayBarGraph'
|
import HourOfDayBarGraph from './HourOfDayBarGraph'
|
||||||
import OverTimeDotGraph from './OverTimeDotGraph'
|
import OverTimeDotGraph from './OverTimeDotGraph'
|
||||||
|
import OverTimeLineGraph from './OverTimeLineGraph'
|
||||||
import TopMachinesBarGraph from './TopMachinesBarGraph'
|
import TopMachinesBarGraph from './TopMachinesBarGraph'
|
||||||
|
|
||||||
const GraphWrapper = ({
|
const GraphWrapper = ({
|
||||||
|
|
@ -15,7 +16,8 @@ const GraphWrapper = ({
|
||||||
currency,
|
currency,
|
||||||
selectedMachine,
|
selectedMachine,
|
||||||
machines,
|
machines,
|
||||||
selectedDay
|
selectedDay,
|
||||||
|
log
|
||||||
}) => {
|
}) => {
|
||||||
const [selectionCoords, setSelectionCoords] = useState(null)
|
const [selectionCoords, setSelectionCoords] = useState(null)
|
||||||
const [selectionDateInterval, setSelectionDateInterval] = useState(null)
|
const [selectionDateInterval, setSelectionDateInterval] = useState(null)
|
||||||
|
|
@ -33,6 +35,20 @@ const GraphWrapper = ({
|
||||||
setSelectionDateInterval={setSelectionDateInterval}
|
setSelectionDateInterval={setSelectionDateInterval}
|
||||||
setSelectionData={setSelectionData}
|
setSelectionData={setSelectionData}
|
||||||
selectedMachine={selectedMachine}
|
selectedMachine={selectedMachine}
|
||||||
|
log={log}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'volumeOverTime':
|
||||||
|
return (
|
||||||
|
<OverTimeLineGraph
|
||||||
|
data={data}
|
||||||
|
period={period}
|
||||||
|
timezone={timezone}
|
||||||
|
setSelectionCoords={setSelectionCoords}
|
||||||
|
setSelectionDateInterval={setSelectionDateInterval}
|
||||||
|
setSelectionData={setSelectionData}
|
||||||
|
selectedMachine={selectedMachine}
|
||||||
|
log={log}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'topMachinesVolume':
|
case 'topMachinesVolume':
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
fontSecondary,
|
fontSecondary,
|
||||||
subheaderColor
|
subheaderColor
|
||||||
} from 'src/styling/variables'
|
} from 'src/styling/variables'
|
||||||
|
import { numberToFiatAmount } from 'src/utils/number'
|
||||||
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||||
|
|
||||||
const Graph = ({
|
const Graph = ({
|
||||||
|
|
@ -23,7 +24,8 @@ const Graph = ({
|
||||||
timezone,
|
timezone,
|
||||||
setSelectionCoords,
|
setSelectionCoords,
|
||||||
setSelectionData,
|
setSelectionData,
|
||||||
setSelectionDateInterval
|
setSelectionDateInterval,
|
||||||
|
log = false
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
|
|
||||||
|
|
@ -36,7 +38,7 @@ const Graph = ({
|
||||||
top: 25,
|
top: 25,
|
||||||
right: 3.5,
|
right: 3.5,
|
||||||
bottom: 27,
|
bottom: 27,
|
||||||
left: 36.5
|
left: 38
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
@ -46,6 +48,7 @@ const Graph = ({
|
||||||
|
|
||||||
const periodDomains = {
|
const periodDomains = {
|
||||||
day: [NOW - DAY, NOW],
|
day: [NOW - DAY, NOW],
|
||||||
|
threeDays: [NOW - 3 * DAY, NOW],
|
||||||
week: [NOW - WEEK, NOW],
|
week: [NOW - WEEK, NOW],
|
||||||
month: [NOW - MONTH, NOW]
|
month: [NOW - MONTH, NOW]
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +61,12 @@ const Graph = ({
|
||||||
tick: d3.utcHour.every(1),
|
tick: d3.utcHour.every(1),
|
||||||
labelFormat: '%H:%M'
|
labelFormat: '%H:%M'
|
||||||
},
|
},
|
||||||
|
threeDays: {
|
||||||
|
freq: 12,
|
||||||
|
step: 6 * 60 * 60 * 1000,
|
||||||
|
tick: d3.utcDay.every(1),
|
||||||
|
labelFormat: '%a %d'
|
||||||
|
},
|
||||||
week: {
|
week: {
|
||||||
freq: 7,
|
freq: 7,
|
||||||
step: 24 * 60 * 60 * 1000,
|
step: 24 * 60 * 60 * 1000,
|
||||||
|
|
@ -164,7 +173,7 @@ const Graph = ({
|
||||||
.domain(periodDomains[period.code])
|
.domain(periodDomains[period.code])
|
||||||
.range([GRAPH_MARGIN.left, GRAPH_WIDTH])
|
.range([GRAPH_MARGIN.left, GRAPH_WIDTH])
|
||||||
|
|
||||||
const y = d3
|
const yLin = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain([
|
.domain([
|
||||||
0,
|
0,
|
||||||
|
|
@ -173,6 +182,16 @@ const Graph = ({
|
||||||
.nice()
|
.nice()
|
||||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||||
|
|
||||||
|
const yLog = d3
|
||||||
|
.scaleLog()
|
||||||
|
.domain([
|
||||||
|
(d3.min(data, d => new BigNumber(d.fiat).toNumber()) ?? 1) * 0.9,
|
||||||
|
(d3.max(data, d => new BigNumber(d.fiat).toNumber()) ?? 1000) * 1.1
|
||||||
|
])
|
||||||
|
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||||
|
|
||||||
|
const y = log ? yLog : yLin
|
||||||
|
|
||||||
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
|
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
|
||||||
const fullBreakpoints = [
|
const fullBreakpoints = [
|
||||||
graphLimits[1],
|
graphLimits[1],
|
||||||
|
|
@ -219,14 +238,11 @@ const Graph = ({
|
||||||
d.getTime() + d.getTimezoneOffset() * MINUTE
|
d.getTime() + d.getTimezoneOffset() * MINUTE
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.tickSizeOuter(0)
|
||||||
)
|
)
|
||||||
.call(g => g.select('.domain').remove())
|
|
||||||
.call(g =>
|
.call(g =>
|
||||||
g
|
g
|
||||||
.append('line')
|
.select('.domain')
|
||||||
.attr('x1', GRAPH_MARGIN.left)
|
|
||||||
.attr('y1', -GRAPH_HEIGHT + GRAPH_MARGIN.top + GRAPH_MARGIN.bottom)
|
|
||||||
.attr('x2', GRAPH_MARGIN.left)
|
|
||||||
.attr('stroke', primaryColor)
|
.attr('stroke', primaryColor)
|
||||||
.attr('stroke-width', 1)
|
.attr('stroke-width', 1)
|
||||||
),
|
),
|
||||||
|
|
@ -237,18 +253,23 @@ const Graph = ({
|
||||||
g =>
|
g =>
|
||||||
g
|
g
|
||||||
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
||||||
.call(d3.axisLeft(y).ticks(GRAPH_HEIGHT / 100))
|
.call(
|
||||||
.call(g => g.select('.domain').remove())
|
d3
|
||||||
.call(g =>
|
.axisLeft(y)
|
||||||
g
|
.ticks(GRAPH_HEIGHT / 100)
|
||||||
.selectAll('.tick line')
|
.tickSizeOuter(0)
|
||||||
.filter(d => d === 0)
|
.tickFormat(d => {
|
||||||
.clone()
|
if (log && !['1', '2', '5'].includes(d.toString()[0])) return ''
|
||||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.left)
|
|
||||||
.attr('stroke-width', 1)
|
if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
|
||||||
.attr('stroke', primaryColor)
|
|
||||||
),
|
return numberToFiatAmount(d)
|
||||||
[GRAPH_MARGIN, y]
|
})
|
||||||
|
)
|
||||||
|
.select('.domain')
|
||||||
|
.attr('stroke', primaryColor)
|
||||||
|
.attr('stroke-width', 1),
|
||||||
|
[GRAPH_MARGIN, y, log]
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildGrid = useCallback(
|
const buildGrid = useCallback(
|
||||||
|
|
@ -477,25 +498,23 @@ const Graph = ({
|
||||||
|
|
||||||
const buildAvg = useCallback(
|
const buildAvg = useCallback(
|
||||||
g => {
|
g => {
|
||||||
|
const median = d3.median(data, d => new BigNumber(d.fiat).toNumber()) ?? 0
|
||||||
|
|
||||||
|
if (log && median === 0) return
|
||||||
|
|
||||||
g.attr('stroke', primaryColor)
|
g.attr('stroke', primaryColor)
|
||||||
.attr('stroke-width', 3)
|
.attr('stroke-width', 3)
|
||||||
.attr('stroke-dasharray', '10, 5')
|
.attr('stroke-dasharray', '10, 5')
|
||||||
.call(g =>
|
.call(g =>
|
||||||
g
|
g
|
||||||
.append('line')
|
.append('line')
|
||||||
.attr(
|
.attr('y1', 0.5 + y(median))
|
||||||
'y1',
|
.attr('y2', 0.5 + y(median))
|
||||||
0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
|
|
||||||
)
|
|
||||||
.attr(
|
|
||||||
'y2',
|
|
||||||
0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
|
|
||||||
)
|
|
||||||
.attr('x1', GRAPH_MARGIN.left)
|
.attr('x1', GRAPH_MARGIN.left)
|
||||||
.attr('x2', GRAPH_WIDTH)
|
.attr('x2', GRAPH_WIDTH)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[GRAPH_MARGIN, y, data]
|
[GRAPH_MARGIN, y, data, log]
|
||||||
)
|
)
|
||||||
|
|
||||||
const drawData = useCallback(
|
const drawData = useCallback(
|
||||||
|
|
@ -554,5 +573,6 @@ export default memo(
|
||||||
Graph,
|
Graph,
|
||||||
(prev, next) =>
|
(prev, next) =>
|
||||||
R.equals(prev.period, next.period) &&
|
R.equals(prev.period, next.period) &&
|
||||||
R.equals(prev.selectedMachine, next.selectedMachine)
|
R.equals(prev.selectedMachine, next.selectedMachine) &&
|
||||||
|
R.equals(prev.log, next.log)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,654 @@
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import * as d3 from 'd3'
|
||||||
|
import { getTimezoneOffset } from 'date-fns-tz'
|
||||||
|
import {
|
||||||
|
add,
|
||||||
|
addMilliseconds,
|
||||||
|
compareDesc,
|
||||||
|
differenceInMilliseconds,
|
||||||
|
format,
|
||||||
|
startOfWeek,
|
||||||
|
startOfYear
|
||||||
|
} from 'date-fns/fp'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
java,
|
||||||
|
neon,
|
||||||
|
subheaderDarkColor,
|
||||||
|
offColor,
|
||||||
|
fontColor,
|
||||||
|
primaryColor,
|
||||||
|
fontSecondary,
|
||||||
|
subheaderColor
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
import { numberToFiatAmount } from 'src/utils/number'
|
||||||
|
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||||
|
|
||||||
|
const Graph = ({
|
||||||
|
data,
|
||||||
|
period,
|
||||||
|
timezone,
|
||||||
|
setSelectionCoords,
|
||||||
|
setSelectionData,
|
||||||
|
setSelectionDateInterval,
|
||||||
|
log = false
|
||||||
|
}) => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
|
||||||
|
const GRAPH_POPOVER_WIDTH = 150
|
||||||
|
const GRAPH_POPOVER_MARGIN = 25
|
||||||
|
const GRAPH_HEIGHT = 401
|
||||||
|
const GRAPH_WIDTH = 1163
|
||||||
|
const GRAPH_MARGIN = useMemo(
|
||||||
|
() => ({
|
||||||
|
top: 25,
|
||||||
|
right: 3.5,
|
||||||
|
bottom: 27,
|
||||||
|
left: 38
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const offset = getTimezoneOffset(timezone)
|
||||||
|
const NOW = Date.now() + offset
|
||||||
|
|
||||||
|
const periodDomains = {
|
||||||
|
day: [NOW - DAY, NOW],
|
||||||
|
threeDays: [NOW - 3 * DAY, NOW],
|
||||||
|
week: [NOW - WEEK, NOW],
|
||||||
|
month: [NOW - MONTH, NOW]
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataPoints = useMemo(
|
||||||
|
() => ({
|
||||||
|
day: {
|
||||||
|
freq: 24,
|
||||||
|
step: 60 * 60 * 1000,
|
||||||
|
tick: d3.utcHour.every(1),
|
||||||
|
labelFormat: '%H:%M'
|
||||||
|
},
|
||||||
|
threeDays: {
|
||||||
|
freq: 12,
|
||||||
|
step: 6 * 60 * 60 * 1000,
|
||||||
|
tick: d3.utcDay.every(1),
|
||||||
|
labelFormat: '%a %d'
|
||||||
|
},
|
||||||
|
week: {
|
||||||
|
freq: 7,
|
||||||
|
step: 24 * 60 * 60 * 1000,
|
||||||
|
tick: d3.utcDay.every(1),
|
||||||
|
labelFormat: '%a %d'
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
freq: 30,
|
||||||
|
step: 24 * 60 * 60 * 1000,
|
||||||
|
tick: d3.utcDay.every(1),
|
||||||
|
labelFormat: '%d'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getPastAndCurrentDayLabels = useCallback(d => {
|
||||||
|
const currentDate = new Date(d)
|
||||||
|
const currentDateDay = currentDate.getUTCDate()
|
||||||
|
const currentDateWeekday = currentDate.getUTCDay()
|
||||||
|
const currentDateMonth = currentDate.getUTCMonth()
|
||||||
|
|
||||||
|
const previousDate = new Date(currentDate.getTime())
|
||||||
|
previousDate.setUTCDate(currentDateDay - 1)
|
||||||
|
|
||||||
|
const previousDateDay = previousDate.getUTCDate()
|
||||||
|
const previousDateWeekday = previousDate.getUTCDay()
|
||||||
|
const previousDateMonth = previousDate.getUTCMonth()
|
||||||
|
|
||||||
|
const daysOfWeek = Array.from(Array(7)).map((_, i) =>
|
||||||
|
format('EEE', add({ days: i }, startOfWeek(new Date())))
|
||||||
|
)
|
||||||
|
|
||||||
|
const months = Array.from(Array(12)).map((_, i) =>
|
||||||
|
format('LLL', add({ months: i }, startOfYear(new Date())))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
previous:
|
||||||
|
currentDateMonth !== previousDateMonth
|
||||||
|
? months[previousDateMonth]
|
||||||
|
: `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`,
|
||||||
|
current:
|
||||||
|
currentDateMonth !== previousDateMonth
|
||||||
|
? months[currentDateMonth]
|
||||||
|
: `${daysOfWeek[currentDateWeekday]} ${currentDateDay}`
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const buildTicks = useCallback(
|
||||||
|
domain => {
|
||||||
|
const points = []
|
||||||
|
|
||||||
|
const roundDate = d => {
|
||||||
|
const step = dataPoints[period.code].step
|
||||||
|
return new Date(Math.ceil(d.valueOf() / step) * step)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i <= dataPoints[period.code].freq; i++) {
|
||||||
|
const stepDate = new Date(NOW - i * dataPoints[period.code].step)
|
||||||
|
if (roundDate(stepDate) > domain[1]) continue
|
||||||
|
if (stepDate < domain[0]) continue
|
||||||
|
points.push(roundDate(stepDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
},
|
||||||
|
[NOW, dataPoints, period.code]
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildAreas = useCallback(
|
||||||
|
domain => {
|
||||||
|
const points = []
|
||||||
|
|
||||||
|
points.push(domain[1])
|
||||||
|
|
||||||
|
const roundDate = d => {
|
||||||
|
const step = dataPoints[period.code].step
|
||||||
|
return new Date(Math.ceil(d.valueOf() / step) * step)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i <= dataPoints[period.code].freq; i++) {
|
||||||
|
const stepDate = new Date(NOW - i * dataPoints[period.code].step)
|
||||||
|
if (roundDate(stepDate) > new Date(domain[1])) continue
|
||||||
|
if (stepDate < new Date(domain[0])) continue
|
||||||
|
points.push(roundDate(stepDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
points.push(domain[0])
|
||||||
|
|
||||||
|
return points
|
||||||
|
},
|
||||||
|
[NOW, dataPoints, period.code]
|
||||||
|
)
|
||||||
|
|
||||||
|
const x = d3
|
||||||
|
.scaleUtc()
|
||||||
|
.domain(periodDomains[period.code])
|
||||||
|
.range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
|
||||||
|
|
||||||
|
// Create a second X axis for mouseover events to be placed correctly across the entire graph width and not limited by X's domain
|
||||||
|
const x2 = d3
|
||||||
|
.scaleUtc()
|
||||||
|
.domain(periodDomains[period.code])
|
||||||
|
.range([GRAPH_MARGIN.left, GRAPH_WIDTH])
|
||||||
|
|
||||||
|
const bins = buildAreas(x.domain())
|
||||||
|
.sort((a, b) => compareDesc(a.date, b.date))
|
||||||
|
.map(addMilliseconds(-dataPoints[period.code].step))
|
||||||
|
.map((date, i, dates) => {
|
||||||
|
// move first and last bin in such way
|
||||||
|
// that all bin have uniform width
|
||||||
|
if (i === 0)
|
||||||
|
return addMilliseconds(dataPoints[period.code].step, dates[1])
|
||||||
|
else if (i === dates.length - 1)
|
||||||
|
return addMilliseconds(
|
||||||
|
-dataPoints[period.code].step,
|
||||||
|
dates[dates.length - 2]
|
||||||
|
)
|
||||||
|
else return date
|
||||||
|
})
|
||||||
|
.map(date => {
|
||||||
|
const middleOfBin = addMilliseconds(
|
||||||
|
dataPoints[period.code].step / 2,
|
||||||
|
date
|
||||||
|
)
|
||||||
|
|
||||||
|
const txs = data.filter(tx => {
|
||||||
|
const txCreated = new Date(tx.created)
|
||||||
|
const shift = new Date(txCreated.getTime() + offset)
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.abs(differenceInMilliseconds(shift, middleOfBin)) <
|
||||||
|
dataPoints[period.code].step / 2
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const cashIn = txs
|
||||||
|
.filter(tx => tx.txClass === 'cashIn')
|
||||||
|
.reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0)
|
||||||
|
|
||||||
|
const cashOut = txs
|
||||||
|
.filter(tx => tx.txClass === 'cashOut')
|
||||||
|
.reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0)
|
||||||
|
|
||||||
|
return { date: middleOfBin, cashIn, cashOut }
|
||||||
|
})
|
||||||
|
|
||||||
|
const min = d3.min(bins, d => Math.min(d.cashIn, d.cashOut)) ?? 0
|
||||||
|
const max = d3.max(bins, d => Math.max(d.cashIn, d.cashOut)) ?? 1000
|
||||||
|
|
||||||
|
const yLin = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([0, (max === min ? min + 1000 : max) * 1.03])
|
||||||
|
.nice()
|
||||||
|
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||||
|
|
||||||
|
const yLog = d3
|
||||||
|
.scaleLog()
|
||||||
|
.domain([
|
||||||
|
min === 0 ? 0.9 : min * 0.9,
|
||||||
|
(max === min ? min + Math.pow(10, 2 * min + 1) : max) * 2
|
||||||
|
])
|
||||||
|
.clamp(true)
|
||||||
|
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||||
|
|
||||||
|
const y = log ? yLog : yLin
|
||||||
|
|
||||||
|
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
|
||||||
|
const fullBreakpoints = [
|
||||||
|
graphLimits[1],
|
||||||
|
...R.filter(it => it > dataLimits[0] && it < dataLimits[1], breakpoints),
|
||||||
|
dataLimits[0]
|
||||||
|
]
|
||||||
|
|
||||||
|
const intervals = []
|
||||||
|
for (let i = 0; i < fullBreakpoints.length - 1; i++) {
|
||||||
|
intervals.push([fullBreakpoints[i], fullBreakpoints[i + 1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
return intervals
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAreaIntervalByX = (intervals, xValue) => {
|
||||||
|
return R.find(it => xValue <= it[0] && xValue >= it[1], intervals) ?? [0, 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDateIntervalByX = (areas, intervals, xValue) => {
|
||||||
|
const flattenIntervals = R.uniq(R.flatten(intervals))
|
||||||
|
|
||||||
|
// flattenIntervals and areas should have the same number of elements
|
||||||
|
for (let i = intervals.length - 1; i >= 0; i--) {
|
||||||
|
if (xValue < flattenIntervals[i]) {
|
||||||
|
return [areas[i], areas[i + 1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildXAxis = useCallback(
|
||||||
|
g =>
|
||||||
|
g
|
||||||
|
.attr(
|
||||||
|
'transform',
|
||||||
|
`translate(0, ${GRAPH_HEIGHT - GRAPH_MARGIN.bottom})`
|
||||||
|
)
|
||||||
|
.call(
|
||||||
|
d3
|
||||||
|
.axisBottom(x)
|
||||||
|
.ticks(dataPoints[period.code].tick)
|
||||||
|
.tickFormat(d => {
|
||||||
|
return d3.timeFormat(dataPoints[period.code].labelFormat)(
|
||||||
|
d.getTime() + d.getTimezoneOffset() * MINUTE
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
|
.call(g =>
|
||||||
|
g
|
||||||
|
.select('.domain')
|
||||||
|
.attr('stroke', primaryColor)
|
||||||
|
.attr('stroke-width', 1)
|
||||||
|
),
|
||||||
|
[GRAPH_MARGIN, dataPoints, period.code, x]
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildYAxis = useCallback(
|
||||||
|
g =>
|
||||||
|
g
|
||||||
|
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
||||||
|
.call(
|
||||||
|
d3
|
||||||
|
.axisLeft(y)
|
||||||
|
.ticks(GRAPH_HEIGHT / 100)
|
||||||
|
.tickSizeOuter(0)
|
||||||
|
.tickFormat(d => {
|
||||||
|
if (log && !['1', '2', '5'].includes(d.toString()[0])) return ''
|
||||||
|
|
||||||
|
if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
|
||||||
|
|
||||||
|
return numberToFiatAmount(d)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.select('.domain')
|
||||||
|
.attr('stroke', primaryColor)
|
||||||
|
.attr('stroke-width', 1),
|
||||||
|
[GRAPH_MARGIN, y, log]
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildGrid = useCallback(
|
||||||
|
g => {
|
||||||
|
g.attr('stroke', subheaderDarkColor)
|
||||||
|
.attr('fill', subheaderDarkColor)
|
||||||
|
// Vertical lines
|
||||||
|
.call(g =>
|
||||||
|
g
|
||||||
|
.append('g')
|
||||||
|
.selectAll('line')
|
||||||
|
.data(buildTicks(x.domain()))
|
||||||
|
.join('line')
|
||||||
|
.attr('x1', d => 0.5 + x(d))
|
||||||
|
.attr('x2', d => 0.5 + x(d))
|
||||||
|
.attr('y1', GRAPH_MARGIN.top)
|
||||||
|
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
|
||||||
|
)
|
||||||
|
// Horizontal lines
|
||||||
|
.call(g =>
|
||||||
|
g
|
||||||
|
.append('g')
|
||||||
|
.selectAll('line')
|
||||||
|
.data(
|
||||||
|
d3
|
||||||
|
.axisLeft(y)
|
||||||
|
.scale()
|
||||||
|
.ticks(GRAPH_HEIGHT / 100)
|
||||||
|
)
|
||||||
|
.join('line')
|
||||||
|
.attr('y1', d => 0.5 + y(d))
|
||||||
|
.attr('y2', d => 0.5 + y(d))
|
||||||
|
.attr('x1', GRAPH_MARGIN.left)
|
||||||
|
.attr('x2', GRAPH_WIDTH)
|
||||||
|
)
|
||||||
|
// Vertical transparent rectangles for events
|
||||||
|
.call(g =>
|
||||||
|
g
|
||||||
|
.append('g')
|
||||||
|
.selectAll('line')
|
||||||
|
.data(buildAreas(x.domain()))
|
||||||
|
.join('rect')
|
||||||
|
.attr('x', d => x(d))
|
||||||
|
.attr('y', GRAPH_MARGIN.top)
|
||||||
|
.attr('width', d => {
|
||||||
|
const xValue = Math.round(x(d) * 100) / 100
|
||||||
|
const intervals = getAreaInterval(
|
||||||
|
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||||
|
x.range(),
|
||||||
|
x2.range()
|
||||||
|
)
|
||||||
|
const interval = getAreaIntervalByX(intervals, xValue)
|
||||||
|
return Math.round((interval[0] - interval[1]) * 100) / 100
|
||||||
|
})
|
||||||
|
.attr(
|
||||||
|
'height',
|
||||||
|
GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top
|
||||||
|
)
|
||||||
|
.attr('stroke', 'transparent')
|
||||||
|
.attr('fill', 'transparent')
|
||||||
|
.on('mouseover', d => {
|
||||||
|
const xValue = Math.round(d.target.x.baseVal.value * 100) / 100
|
||||||
|
const areas = buildAreas(x.domain())
|
||||||
|
const intervals = getAreaInterval(
|
||||||
|
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||||
|
x.range(),
|
||||||
|
x2.range()
|
||||||
|
)
|
||||||
|
|
||||||
|
const dateInterval = getDateIntervalByX(areas, intervals, xValue)
|
||||||
|
if (!dateInterval) return
|
||||||
|
const filteredData = data.filter(it => {
|
||||||
|
const created = new Date(it.created)
|
||||||
|
const tzCreated = created.setTime(created.getTime() + offset)
|
||||||
|
return (
|
||||||
|
tzCreated > new Date(dateInterval[1]) &&
|
||||||
|
tzCreated <= new Date(dateInterval[0])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const rectXCoords = {
|
||||||
|
left: R.clone(d.target.getBoundingClientRect().x),
|
||||||
|
right: R.clone(
|
||||||
|
d.target.getBoundingClientRect().x +
|
||||||
|
d.target.getBoundingClientRect().width
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const xCoord =
|
||||||
|
d.target.x.baseVal.value < 0.75 * GRAPH_WIDTH
|
||||||
|
? rectXCoords.right + GRAPH_POPOVER_MARGIN
|
||||||
|
: rectXCoords.left -
|
||||||
|
GRAPH_POPOVER_WIDTH -
|
||||||
|
GRAPH_POPOVER_MARGIN
|
||||||
|
const yCoord = R.clone(d.target.getBoundingClientRect().y)
|
||||||
|
|
||||||
|
setSelectionDateInterval(dateInterval)
|
||||||
|
setSelectionData(filteredData)
|
||||||
|
setSelectionCoords({
|
||||||
|
x: Math.round(xCoord),
|
||||||
|
y: Math.round(yCoord)
|
||||||
|
})
|
||||||
|
|
||||||
|
d3.select(d.target).attr('fill', subheaderColor)
|
||||||
|
})
|
||||||
|
.on('mouseleave', d => {
|
||||||
|
d3.select(d.target).attr('fill', 'transparent')
|
||||||
|
setSelectionDateInterval(null)
|
||||||
|
setSelectionData(null)
|
||||||
|
setSelectionCoords(null)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// Thick vertical lines
|
||||||
|
.call(g =>
|
||||||
|
g
|
||||||
|
.append('g')
|
||||||
|
.selectAll('line')
|
||||||
|
.data(
|
||||||
|
buildTicks(x.domain()).filter(x => {
|
||||||
|
if (period.code === 'day') return x.getUTCHours() === 0
|
||||||
|
return x.getUTCDate() === 1
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.join('line')
|
||||||
|
.attr('class', 'dateSeparator')
|
||||||
|
.attr('x1', d => 0.5 + x(d))
|
||||||
|
.attr('x2', d => 0.5 + x(d))
|
||||||
|
.attr('y1', GRAPH_MARGIN.top - 50)
|
||||||
|
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
|
||||||
|
.attr('stroke-width', 5)
|
||||||
|
.join('text')
|
||||||
|
)
|
||||||
|
// Left side breakpoint label
|
||||||
|
.call(g => {
|
||||||
|
const separator = d3
|
||||||
|
?.select('.dateSeparator')
|
||||||
|
?.node()
|
||||||
|
?.getBBox()
|
||||||
|
|
||||||
|
if (!separator) return
|
||||||
|
|
||||||
|
const breakpoint = buildTicks(x.domain()).filter(x => {
|
||||||
|
if (period.code === 'day') return x.getUTCHours() === 0
|
||||||
|
return x.getUTCDate() === 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const labels = getPastAndCurrentDayLabels(breakpoint)
|
||||||
|
|
||||||
|
return g
|
||||||
|
.append('text')
|
||||||
|
.attr('x', separator.x - 10)
|
||||||
|
.attr('y', separator.y + 33)
|
||||||
|
.attr('text-anchor', 'end')
|
||||||
|
.attr('dy', '.25em')
|
||||||
|
.text(labels.previous)
|
||||||
|
})
|
||||||
|
// Right side breakpoint label
|
||||||
|
.call(g => {
|
||||||
|
const separator = d3
|
||||||
|
?.select('.dateSeparator')
|
||||||
|
?.node()
|
||||||
|
?.getBBox()
|
||||||
|
|
||||||
|
if (!separator) return
|
||||||
|
|
||||||
|
const breakpoint = buildTicks(x.domain()).filter(x => {
|
||||||
|
if (period.code === 'day') return x.getUTCHours() === 0
|
||||||
|
return x.getUTCDate() === 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const labels = getPastAndCurrentDayLabels(breakpoint)
|
||||||
|
|
||||||
|
return g
|
||||||
|
.append('text')
|
||||||
|
.attr('x', separator.x + 10)
|
||||||
|
.attr('y', separator.y + 33)
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('dy', '.25em')
|
||||||
|
.text(labels.current)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[
|
||||||
|
GRAPH_MARGIN,
|
||||||
|
buildTicks,
|
||||||
|
getPastAndCurrentDayLabels,
|
||||||
|
x,
|
||||||
|
x2,
|
||||||
|
y,
|
||||||
|
period,
|
||||||
|
buildAreas,
|
||||||
|
data,
|
||||||
|
offset,
|
||||||
|
setSelectionCoords,
|
||||||
|
setSelectionData,
|
||||||
|
setSelectionDateInterval
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const formatTicksText = useCallback(
|
||||||
|
() =>
|
||||||
|
d3
|
||||||
|
.selectAll('.tick text')
|
||||||
|
.style('stroke', fontColor)
|
||||||
|
.style('fill', fontColor)
|
||||||
|
.style('stroke-width', 0.5)
|
||||||
|
.style('font-family', fontSecondary),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const formatText = useCallback(
|
||||||
|
() =>
|
||||||
|
d3
|
||||||
|
.selectAll('text')
|
||||||
|
.style('stroke', offColor)
|
||||||
|
.style('fill', offColor)
|
||||||
|
.style('stroke-width', 0.5)
|
||||||
|
.style('font-family', fontSecondary),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const formatTicks = useCallback(() => {
|
||||||
|
d3.selectAll('.tick line')
|
||||||
|
.style('stroke', primaryColor)
|
||||||
|
.style('fill', primaryColor)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const drawData = useCallback(
|
||||||
|
g => {
|
||||||
|
g.append('clipPath')
|
||||||
|
.attr('id', 'clip-path')
|
||||||
|
.append('rect')
|
||||||
|
.attr('x', GRAPH_MARGIN.left)
|
||||||
|
.attr('y', GRAPH_MARGIN.top)
|
||||||
|
.attr('width', GRAPH_WIDTH)
|
||||||
|
.attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top)
|
||||||
|
.attr('fill', java)
|
||||||
|
|
||||||
|
g.append('g')
|
||||||
|
.attr('clip-path', 'url(#clip-path)')
|
||||||
|
.selectAll('circle .cashIn')
|
||||||
|
.data(bins)
|
||||||
|
.join('circle')
|
||||||
|
.attr('cx', d => x(d.date))
|
||||||
|
.attr('cy', d => y(d.cashIn))
|
||||||
|
.attr('fill', java)
|
||||||
|
.attr('r', d => (d.cashIn === 0 ? 0 : 3.5))
|
||||||
|
|
||||||
|
g.append('path')
|
||||||
|
.datum(bins)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', java)
|
||||||
|
.attr('stroke-width', 3)
|
||||||
|
.attr('clip-path', 'url(#clip-path)')
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
d3
|
||||||
|
.line()
|
||||||
|
.curve(d3.curveMonotoneX)
|
||||||
|
.x(d => x(d.date))
|
||||||
|
.y(d => y(d.cashIn))
|
||||||
|
)
|
||||||
|
|
||||||
|
g.append('g')
|
||||||
|
.attr('clip-path', 'url(#clip-path)')
|
||||||
|
.selectAll('circle .cashIn')
|
||||||
|
.data(bins)
|
||||||
|
.join('circle')
|
||||||
|
.attr('cx', d => x(d.date))
|
||||||
|
.attr('cy', d => y(d.cashOut))
|
||||||
|
.attr('fill', neon)
|
||||||
|
.attr('r', d => (d.cashOut === 0 ? 0 : 3.5))
|
||||||
|
|
||||||
|
g.append('path')
|
||||||
|
.datum(bins)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', neon)
|
||||||
|
.attr('stroke-width', 3)
|
||||||
|
.attr('clip-path', 'url(#clip-path)')
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
d3
|
||||||
|
.line()
|
||||||
|
.curve(d3.curveMonotoneX)
|
||||||
|
.x(d => x(d.date))
|
||||||
|
.y(d => y(d.cashOut))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[x, y, bins, GRAPH_MARGIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
const drawChart = useCallback(() => {
|
||||||
|
const svg = d3
|
||||||
|
.select(ref.current)
|
||||||
|
.attr('viewBox', [0, 0, GRAPH_WIDTH, GRAPH_HEIGHT])
|
||||||
|
|
||||||
|
svg.append('g').call(buildGrid)
|
||||||
|
svg.append('g').call(drawData)
|
||||||
|
svg.append('g').call(buildXAxis)
|
||||||
|
svg.append('g').call(buildYAxis)
|
||||||
|
svg.append('g').call(formatTicksText)
|
||||||
|
svg.append('g').call(formatText)
|
||||||
|
svg.append('g').call(formatTicks)
|
||||||
|
|
||||||
|
return svg.node()
|
||||||
|
}, [
|
||||||
|
buildGrid,
|
||||||
|
buildXAxis,
|
||||||
|
buildYAxis,
|
||||||
|
drawData,
|
||||||
|
formatText,
|
||||||
|
formatTicks,
|
||||||
|
formatTicksText
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
d3.select(ref.current)
|
||||||
|
.selectAll('*')
|
||||||
|
.remove()
|
||||||
|
drawChart()
|
||||||
|
}, [drawChart])
|
||||||
|
|
||||||
|
return <svg ref={ref} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(
|
||||||
|
Graph,
|
||||||
|
(prev, next) =>
|
||||||
|
R.equals(prev.period, next.period) &&
|
||||||
|
R.equals(prev.selectedMachine, next.selectedMachine) &&
|
||||||
|
R.equals(prev.log, next.log)
|
||||||
|
)
|
||||||
|
|
@ -7,8 +7,13 @@ import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { Link, Button, IconButton } from 'src/components/buttons'
|
import {
|
||||||
|
Link,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
SupportLinkButton
|
||||||
|
} from 'src/components/buttons'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import Sidebar from 'src/components/layout/Sidebar'
|
import Sidebar from 'src/components/layout/Sidebar'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
|
@ -251,13 +256,13 @@ const Blacklist = () => {
|
||||||
value={enablePaperWalletOnly}
|
value={enablePaperWalletOnly}
|
||||||
/>
|
/>
|
||||||
<Label2>{enablePaperWalletOnly ? 'On' : 'Off'}</Label2>
|
<Label2>{enablePaperWalletOnly ? 'On' : 'Off'}</Label2>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
The "Enable paper wallet (only)" option means that only paper
|
The "Enable paper wallet (only)" option means that only paper
|
||||||
wallets will be printed for users, and they won't be permitted
|
wallets will be printed for users, and they won't be permitted
|
||||||
to scan an address from their own wallet.
|
to scan an address from their own wallet.
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
|
|
@ -273,13 +278,21 @@ const Blacklist = () => {
|
||||||
value={rejectAddressReuse}
|
value={rejectAddressReuse}
|
||||||
/>
|
/>
|
||||||
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
The "Reject reused addresses" option means that all addresses
|
The "Reject reused addresses" option means that all addresses
|
||||||
that are used once will be automatically rejected if there's
|
that are used once will be automatically rejected if there's
|
||||||
an attempt to use them again on a new transaction.
|
an attempt to use them again on a new transaction.
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
<P>
|
||||||
|
For details please read the relevant knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360033622211-Reject-Address-Reuse"
|
||||||
|
label="Reject Address Reuse"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<BlacklistTable
|
<BlacklistTable
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { SupportLinkButton } from 'src/components/buttons'
|
||||||
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
|
@ -92,7 +93,21 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
||||||
return (
|
return (
|
||||||
!loading && (
|
!loading && (
|
||||||
<>
|
<>
|
||||||
<TitleSection title="Cash-out">
|
<TitleSection
|
||||||
|
title="Cash-out"
|
||||||
|
appendix={
|
||||||
|
<HelpTooltip width={320}>
|
||||||
|
<P>
|
||||||
|
For details on configuring cash-out, please read the relevant
|
||||||
|
knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/115003720192-Enabling-cash-out-on-the-admin"
|
||||||
|
label="Enabling cash-out on the admin"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
|
}>
|
||||||
<div className={classes.fudgeFactor}>
|
<div className={classes.fudgeFactor}>
|
||||||
<P>Transaction fudge factor</P>
|
<P>Transaction fudge factor</P>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -105,7 +120,7 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
||||||
<Label2 className={classes.switchLabel}>
|
<Label2 className={classes.switchLabel}>
|
||||||
{fudgeFactorActive ? 'On' : 'Off'}
|
{fudgeFactorActive ? 'On' : 'Off'}
|
||||||
</Label2>
|
</Label2>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
Automatically accept customer deposits as complete if their
|
Automatically accept customer deposits as complete if their
|
||||||
received amount is 100 crypto atoms or less.
|
received amount is 100 crypto atoms or less.
|
||||||
|
|
@ -114,7 +129,13 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
||||||
(Crypto atoms are the smallest unit in each cryptocurrency.
|
(Crypto atoms are the smallest unit in each cryptocurrency.
|
||||||
E.g., satoshis in Bitcoin, or wei in Ethereum.)
|
E.g., satoshis in Bitcoin, or wei in Ethereum.)
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
<P>For details please read the relevant knowledgebase article:</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360050838011-Automatically-accepting-undersent-deposits-with-Fudge-Factor-"
|
||||||
|
label="Lamassu Support Article"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
</TitleSection>
|
</TitleSection>
|
||||||
<EditableTable
|
<EditableTable
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ const WizardStep = ({
|
||||||
to zero. Make sure you physically put cash inside the cash
|
to zero. Make sure you physically put cash inside the cash
|
||||||
cassettes to allow the machine to dispense it to your users. If
|
cassettes to allow the machine to dispense it to your users. If
|
||||||
you already did, make sure you set the correct cash cassette bill
|
you already did, make sure you set the correct cash cassette bill
|
||||||
count for this machine on your Cash Boxes & Cassettes tab under
|
count for this machine on your Cash boxes & cassettes tab under
|
||||||
Maintenance.
|
Maintenance.
|
||||||
</P>
|
</P>
|
||||||
<Info2 className={classes.title}>Default Commissions</Info2>
|
<Info2 className={classes.title}>Default Commissions</Info2>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@ import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { SupportLinkButton } from 'src/components/buttons'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import { ReactComponent as ReverseListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/white.svg'
|
import { ReactComponent as ReverseListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/white.svg'
|
||||||
import { ReactComponent as ListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/zodiac.svg'
|
import { ReactComponent as ListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/zodiac.svg'
|
||||||
import { ReactComponent as OverrideLabelIcon } from 'src/styling/icons/status/spring2.svg'
|
import { ReactComponent as OverrideLabelIcon } from 'src/styling/icons/status/spring2.svg'
|
||||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
|
import { P } from '../../components/typography'
|
||||||
|
|
||||||
import CommissionsDetails from './components/CommissionsDetails'
|
import CommissionsDetails from './components/CommissionsDetails'
|
||||||
import CommissionsList from './components/CommissionsList'
|
import CommissionsList from './components/CommissionsList'
|
||||||
|
|
||||||
|
|
@ -118,6 +122,24 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
iconClassName={classes.listViewButton}
|
iconClassName={classes.listViewButton}
|
||||||
|
appendix={
|
||||||
|
<HelpTooltip width={320}>
|
||||||
|
<P>
|
||||||
|
For details about commissions, please read the relevant
|
||||||
|
knowledgebase articles:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/115001211752-Fixed-fees-Minimum-transaction"
|
||||||
|
label="Fixed fees & Minimum transaction"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360061558352-Commissions-and-Profit-Calculations"
|
||||||
|
label="SCommissions and Profit Calculations"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!showMachines && !loading && (
|
{!showMachines && !loading && (
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const SHOW_ALL = {
|
||||||
const ORDER_OPTIONS = [
|
const ORDER_OPTIONS = [
|
||||||
{
|
{
|
||||||
code: 'machine',
|
code: 'machine',
|
||||||
display: 'Machine Name'
|
display: 'Machine name'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'cryptoCurrencies',
|
code: 'cryptoCurrencies',
|
||||||
|
|
@ -53,7 +53,7 @@ const ORDER_OPTIONS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'fixedFee',
|
code: 'fixedFee',
|
||||||
display: 'Fixed Fee'
|
display: 'Fixed fee'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'minimumTx',
|
code: 'minimumTx',
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cryptoCurrencies',
|
name: 'cryptoCurrencies',
|
||||||
width: 280,
|
width: 145,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
view: displayCodeArray(cryptoData),
|
view: displayCodeArray(cryptoData),
|
||||||
input: Autocomplete,
|
input: Autocomplete,
|
||||||
|
|
@ -108,7 +108,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
||||||
header: cashInHeader,
|
header: cashInHeader,
|
||||||
name: 'cashIn',
|
name: 'cashIn',
|
||||||
display: 'Cash-in',
|
display: 'Cash-in',
|
||||||
width: 130,
|
width: 123,
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
suffix: '%',
|
suffix: '%',
|
||||||
|
|
@ -121,7 +121,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
||||||
header: cashOutHeader,
|
header: cashOutHeader,
|
||||||
name: 'cashOut',
|
name: 'cashOut',
|
||||||
display: 'Cash-out',
|
display: 'Cash-out',
|
||||||
width: 130,
|
width: 127,
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
suffix: '%',
|
suffix: '%',
|
||||||
|
|
@ -133,7 +133,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
||||||
{
|
{
|
||||||
name: 'fixedFee',
|
name: 'fixedFee',
|
||||||
display: 'Fixed fee',
|
display: 'Fixed fee',
|
||||||
width: 144,
|
width: 126,
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
doubleHeader: 'Cash-in only',
|
doubleHeader: 'Cash-in only',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
|
|
@ -146,7 +146,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
||||||
{
|
{
|
||||||
name: 'minimumTx',
|
name: 'minimumTx',
|
||||||
display: 'Minimum Tx',
|
display: 'Minimum Tx',
|
||||||
width: 169,
|
width: 140,
|
||||||
doubleHeader: 'Cash-in only',
|
doubleHeader: 'Cash-in only',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
editingAlign: 'right',
|
editingAlign: 'right',
|
||||||
|
|
@ -156,6 +156,20 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
||||||
inputProps: {
|
inputProps: {
|
||||||
decimalPlaces: 2
|
decimalPlaces: 2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cashOutFixedFee',
|
||||||
|
display: 'Fixed fee',
|
||||||
|
width: 134,
|
||||||
|
doubleHeader: 'Cash-out only',
|
||||||
|
textAlign: 'center',
|
||||||
|
editingAlign: 'right',
|
||||||
|
input: NumberInput,
|
||||||
|
suffix: currency,
|
||||||
|
bold: bold,
|
||||||
|
inputProps: {
|
||||||
|
decimalPlaces: 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +232,21 @@ const mainFields = currency => [
|
||||||
inputProps: {
|
inputProps: {
|
||||||
decimalPlaces: 2
|
decimalPlaces: 2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cashOutFixedFee',
|
||||||
|
display: 'Fixed fee',
|
||||||
|
width: 169,
|
||||||
|
size: 'lg',
|
||||||
|
doubleHeader: 'Cash-out only',
|
||||||
|
textAlign: 'center',
|
||||||
|
editingAlign: 'right',
|
||||||
|
input: NumberInput,
|
||||||
|
suffix: currency,
|
||||||
|
bold: bold,
|
||||||
|
inputProps: {
|
||||||
|
decimalPlaces: 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -245,7 +274,7 @@ const getSchema = locale => {
|
||||||
.max(percentMax)
|
.max(percentMax)
|
||||||
.required(),
|
.required(),
|
||||||
fixedFee: Yup.number()
|
fixedFee: Yup.number()
|
||||||
.label('Fixed Fee')
|
.label('Cash-in fixed fee')
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(highestBill)
|
.max(highestBill)
|
||||||
.required(),
|
.required(),
|
||||||
|
|
@ -253,6 +282,11 @@ const getSchema = locale => {
|
||||||
.label('Minimum Tx')
|
.label('Minimum Tx')
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(highestBill)
|
.max(highestBill)
|
||||||
|
.required(),
|
||||||
|
cashOutFixedFee: Yup.number()
|
||||||
|
.label('Cash-out fixed fee')
|
||||||
|
.min(0)
|
||||||
|
.max(highestBill)
|
||||||
.required()
|
.required()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +360,7 @@ const getOverridesSchema = (values, rawData, locale) => {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.label('Crypto Currencies')
|
.label('Crypto currencies')
|
||||||
.required()
|
.required()
|
||||||
.min(1),
|
.min(1),
|
||||||
cashIn: Yup.number()
|
cashIn: Yup.number()
|
||||||
|
|
@ -340,7 +374,7 @@ const getOverridesSchema = (values, rawData, locale) => {
|
||||||
.max(percentMax)
|
.max(percentMax)
|
||||||
.required(),
|
.required(),
|
||||||
fixedFee: Yup.number()
|
fixedFee: Yup.number()
|
||||||
.label('Fixed Fee')
|
.label('Cash-in fixed fee')
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(highestBill)
|
.max(highestBill)
|
||||||
.required(),
|
.required(),
|
||||||
|
|
@ -348,6 +382,11 @@ const getOverridesSchema = (values, rawData, locale) => {
|
||||||
.label('Minimum Tx')
|
.label('Minimum Tx')
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(highestBill)
|
.max(highestBill)
|
||||||
|
.required(),
|
||||||
|
cashOutFixedFee: Yup.number()
|
||||||
|
.label('Cash-out fixed fee')
|
||||||
|
.min(0)
|
||||||
|
.max(highestBill)
|
||||||
.required()
|
.required()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -356,7 +395,8 @@ const defaults = {
|
||||||
cashIn: '',
|
cashIn: '',
|
||||||
cashOut: '',
|
cashOut: '',
|
||||||
fixedFee: '',
|
fixedFee: '',
|
||||||
minimumTx: ''
|
minimumTx: '',
|
||||||
|
cashOutFixedFee: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const overridesDefaults = {
|
const overridesDefaults = {
|
||||||
|
|
@ -365,7 +405,8 @@ const overridesDefaults = {
|
||||||
cashIn: '',
|
cashIn: '',
|
||||||
cashOut: '',
|
cashOut: '',
|
||||||
fixedFee: '',
|
fixedFee: '',
|
||||||
minimumTx: ''
|
minimumTx: '',
|
||||||
|
cashOutFixedFee: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrder = ({ machine, cryptoCurrencies }) => {
|
const getOrder = ({ machine, cryptoCurrencies }) => {
|
||||||
|
|
@ -385,6 +426,7 @@ const createCommissions = (cryptoCode, deviceId, isDefault, config) => {
|
||||||
fixedFee: config.fixedFee,
|
fixedFee: config.fixedFee,
|
||||||
cashOut: config.cashOut,
|
cashOut: config.cashOut,
|
||||||
cashIn: config.cashIn,
|
cashIn: config.cashIn,
|
||||||
|
cashOutFixedFee: config.cashOutFixedFee,
|
||||||
machine: deviceId,
|
machine: deviceId,
|
||||||
cryptoCurrencies: [cryptoCode],
|
cryptoCurrencies: [cryptoCode],
|
||||||
default: isDefault,
|
default: isDefault,
|
||||||
|
|
@ -437,7 +479,7 @@ const getListCommissionsSchema = locale => {
|
||||||
.label('Machine')
|
.label('Machine')
|
||||||
.required(),
|
.required(),
|
||||||
cryptoCurrencies: Yup.array()
|
cryptoCurrencies: Yup.array()
|
||||||
.label('Crypto Currency')
|
.label('Crypto currency')
|
||||||
.required()
|
.required()
|
||||||
.min(1),
|
.min(1),
|
||||||
cashIn: Yup.number()
|
cashIn: Yup.number()
|
||||||
|
|
@ -451,7 +493,7 @@ const getListCommissionsSchema = locale => {
|
||||||
.max(percentMax)
|
.max(percentMax)
|
||||||
.required(),
|
.required(),
|
||||||
fixedFee: Yup.number()
|
fixedFee: Yup.number()
|
||||||
.label('Fixed Fee')
|
.label('Cash-in fixed fee')
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(highestBill)
|
.max(highestBill)
|
||||||
.required(),
|
.required(),
|
||||||
|
|
@ -459,6 +501,11 @@ const getListCommissionsSchema = locale => {
|
||||||
.label('Minimum Tx')
|
.label('Minimum Tx')
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(highestBill)
|
.max(highestBill)
|
||||||
|
.required(),
|
||||||
|
cashOutFixedFee: Yup.number()
|
||||||
|
.label('Cash-out fixed fee')
|
||||||
|
.min(0)
|
||||||
|
.max(highestBill)
|
||||||
.required()
|
.required()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -487,7 +534,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
||||||
{
|
{
|
||||||
name: 'cryptoCurrencies',
|
name: 'cryptoCurrencies',
|
||||||
display: 'Crypto Currency',
|
display: 'Crypto Currency',
|
||||||
width: 255,
|
width: 150,
|
||||||
view: R.prop(0),
|
view: R.prop(0),
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
editable: false
|
editable: false
|
||||||
|
|
@ -496,7 +543,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
||||||
header: cashInHeader,
|
header: cashInHeader,
|
||||||
name: 'cashIn',
|
name: 'cashIn',
|
||||||
display: 'Cash-in',
|
display: 'Cash-in',
|
||||||
width: 130,
|
width: 120,
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
suffix: '%',
|
suffix: '%',
|
||||||
|
|
@ -509,7 +556,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
||||||
header: cashOutHeader,
|
header: cashOutHeader,
|
||||||
name: 'cashOut',
|
name: 'cashOut',
|
||||||
display: 'Cash-out',
|
display: 'Cash-out',
|
||||||
width: 140,
|
width: 126,
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
greenText: true,
|
greenText: true,
|
||||||
|
|
@ -522,7 +569,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
||||||
{
|
{
|
||||||
name: 'fixedFee',
|
name: 'fixedFee',
|
||||||
display: 'Fixed fee',
|
display: 'Fixed fee',
|
||||||
width: 144,
|
width: 140,
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
doubleHeader: 'Cash-in only',
|
doubleHeader: 'Cash-in only',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
|
|
@ -535,7 +582,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
||||||
{
|
{
|
||||||
name: 'minimumTx',
|
name: 'minimumTx',
|
||||||
display: 'Minimum Tx',
|
display: 'Minimum Tx',
|
||||||
width: 144,
|
width: 140,
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
doubleHeader: 'Cash-in only',
|
doubleHeader: 'Cash-in only',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
|
|
@ -544,6 +591,20 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
||||||
inputProps: {
|
inputProps: {
|
||||||
decimalPlaces: 2
|
decimalPlaces: 2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cashOutFixedFee',
|
||||||
|
display: 'Fixed fee',
|
||||||
|
width: 140,
|
||||||
|
input: NumberInput,
|
||||||
|
doubleHeader: 'Cash-out only',
|
||||||
|
textAlign: 'center',
|
||||||
|
editingAlign: 'right',
|
||||||
|
suffix: currency,
|
||||||
|
textStyle: obj => getTextStyle(obj),
|
||||||
|
inputProps: {
|
||||||
|
decimalPlaces: 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,13 @@ const CustomerData = ({
|
||||||
authorizeCustomRequest,
|
authorizeCustomRequest,
|
||||||
updateCustomEntry,
|
updateCustomEntry,
|
||||||
retrieveAdditionalDataDialog,
|
retrieveAdditionalDataDialog,
|
||||||
setRetrieve
|
setRetrieve,
|
||||||
|
checkAgainstSanctions
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [listView, setListView] = useState(false)
|
const [listView, setListView] = useState(false)
|
||||||
|
const [previewPhoto, setPreviewPhoto] = useState(null)
|
||||||
|
const [previewCard, setPreviewCard] = useState(null)
|
||||||
|
|
||||||
const idData = R.path(['idCardData'])(customer)
|
const idData = R.path(['idCardData'])(customer)
|
||||||
const rawExpirationDate = R.path(['expirationDate'])(idData)
|
const rawExpirationDate = R.path(['expirationDate'])(idData)
|
||||||
|
|
@ -172,6 +175,12 @@ const CustomerData = ({
|
||||||
idCardData: R.merge(idData, formatDates(values))
|
idCardData: R.merge(idData, formatDates(values))
|
||||||
}),
|
}),
|
||||||
validationSchema: customerDataSchemas.idCardData,
|
validationSchema: customerDataSchemas.idCardData,
|
||||||
|
checkAgainstSanctions: () =>
|
||||||
|
checkAgainstSanctions({
|
||||||
|
variables: {
|
||||||
|
customerId: R.path(['id'])(customer)
|
||||||
|
}
|
||||||
|
}),
|
||||||
initialValues: initialValues.idCardData,
|
initialValues: initialValues.idCardData,
|
||||||
isAvailable: !R.isNil(idData),
|
isAvailable: !R.isNil(idData),
|
||||||
editable: true
|
editable: true
|
||||||
|
|
@ -213,9 +222,6 @@ const CustomerData = ({
|
||||||
{
|
{
|
||||||
title: 'Name',
|
title: 'Name',
|
||||||
titleIcon: <EditIcon className={classes.editIcon} />,
|
titleIcon: <EditIcon className={classes.editIcon} />,
|
||||||
authorize: () => {},
|
|
||||||
reject: () => {},
|
|
||||||
save: () => {},
|
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
editable: true
|
editable: true
|
||||||
},
|
},
|
||||||
|
|
@ -226,7 +232,7 @@ const CustomerData = ({
|
||||||
authorize: () =>
|
authorize: () =>
|
||||||
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
|
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
|
||||||
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
|
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
|
||||||
children: <Info3>{sanctionsDisplay}</Info3>,
|
children: () => <Info3>{sanctionsDisplay}</Info3>,
|
||||||
isAvailable: !R.isNil(sanctions),
|
isAvailable: !R.isNil(sanctions),
|
||||||
editable: true
|
editable: true
|
||||||
},
|
},
|
||||||
|
|
@ -238,20 +244,33 @@ const CustomerData = ({
|
||||||
authorize: () =>
|
authorize: () =>
|
||||||
updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }),
|
updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }),
|
||||||
reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }),
|
reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }),
|
||||||
save: values =>
|
save: values => {
|
||||||
replacePhoto({
|
setPreviewPhoto(null)
|
||||||
|
return replacePhoto({
|
||||||
newPhoto: values.frontCamera,
|
newPhoto: values.frontCamera,
|
||||||
photoType: 'frontCamera'
|
photoType: 'frontCamera'
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
|
cancel: () => setPreviewPhoto(null),
|
||||||
deleteEditedData: () => deleteEditedData({ frontCamera: null }),
|
deleteEditedData: () => deleteEditedData({ frontCamera: null }),
|
||||||
children: customer.frontCameraPath ? (
|
children: values => {
|
||||||
<Photo
|
if (values.frontCamera !== previewPhoto) {
|
||||||
show={customer.frontCameraPath}
|
setPreviewPhoto(values.frontCamera)
|
||||||
src={`${URI}/front-camera-photo/${R.path(['frontCameraPath'])(
|
}
|
||||||
customer
|
|
||||||
)}`}
|
return customer.frontCameraPath ? (
|
||||||
/>
|
<Photo
|
||||||
) : null,
|
show={customer.frontCameraPath}
|
||||||
|
src={
|
||||||
|
!R.isNil(previewPhoto)
|
||||||
|
? URL.createObjectURL(previewPhoto)
|
||||||
|
: `${URI}/front-camera-photo/${R.path(['frontCameraPath'])(
|
||||||
|
customer
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
},
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
validationSchema: customerDataSchemas.frontCamera,
|
validationSchema: customerDataSchemas.frontCamera,
|
||||||
initialValues: initialValues.frontCamera,
|
initialValues: initialValues.frontCamera,
|
||||||
|
|
@ -266,18 +285,33 @@ const CustomerData = ({
|
||||||
authorize: () =>
|
authorize: () =>
|
||||||
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }),
|
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }),
|
||||||
reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }),
|
reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }),
|
||||||
save: values =>
|
save: values => {
|
||||||
replacePhoto({
|
setPreviewCard(null)
|
||||||
|
return replacePhoto({
|
||||||
newPhoto: values.idCardPhoto,
|
newPhoto: values.idCardPhoto,
|
||||||
photoType: 'idCardPhoto'
|
photoType: 'idCardPhoto'
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
|
cancel: () => setPreviewCard(null),
|
||||||
deleteEditedData: () => deleteEditedData({ idCardPhoto: null }),
|
deleteEditedData: () => deleteEditedData({ idCardPhoto: null }),
|
||||||
children: customer.idCardPhotoPath ? (
|
children: values => {
|
||||||
<Photo
|
if (values.idCardPhoto !== previewCard) {
|
||||||
show={customer.idCardPhotoPath}
|
setPreviewCard(values.idCardPhoto)
|
||||||
src={`${URI}/id-card-photo/${R.path(['idCardPhotoPath'])(customer)}`}
|
}
|
||||||
/>
|
|
||||||
) : null,
|
return customer.idCardPhotoPath ? (
|
||||||
|
<Photo
|
||||||
|
show={customer.idCardPhotoPath}
|
||||||
|
src={
|
||||||
|
!R.isNil(previewCard)
|
||||||
|
? URL.createObjectURL(previewCard)
|
||||||
|
: `${URI}/id-card-photo/${R.path(['idCardPhotoPath'])(
|
||||||
|
customer
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
},
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
validationSchema: customerDataSchemas.idCardPhoto,
|
validationSchema: customerDataSchemas.idCardPhoto,
|
||||||
initialValues: initialValues.idCardPhoto,
|
initialValues: initialValues.idCardPhoto,
|
||||||
|
|
@ -292,6 +326,7 @@ const CustomerData = ({
|
||||||
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
|
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
|
||||||
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
|
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
|
||||||
save: values => editCustomer(values),
|
save: values => editCustomer(values),
|
||||||
|
children: () => {},
|
||||||
deleteEditedData: () => deleteEditedData({ usSsn: null }),
|
deleteEditedData: () => deleteEditedData({ usSsn: null }),
|
||||||
validationSchema: customerDataSchemas.usSsn,
|
validationSchema: customerDataSchemas.usSsn,
|
||||||
initialValues: initialValues.usSsn,
|
initialValues: initialValues.usSsn,
|
||||||
|
|
@ -427,6 +462,7 @@ const CustomerData = ({
|
||||||
titleIcon,
|
titleIcon,
|
||||||
fields,
|
fields,
|
||||||
save,
|
save,
|
||||||
|
cancel,
|
||||||
deleteEditedData,
|
deleteEditedData,
|
||||||
retrieveAdditionalData,
|
retrieveAdditionalData,
|
||||||
children,
|
children,
|
||||||
|
|
@ -434,7 +470,8 @@ const CustomerData = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
hasImage,
|
hasImage,
|
||||||
hasAdditionalData,
|
hasAdditionalData,
|
||||||
editable
|
editable,
|
||||||
|
checkAgainstSanctions
|
||||||
},
|
},
|
||||||
idx
|
idx
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -453,8 +490,10 @@ const CustomerData = ({
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
save={save}
|
save={save}
|
||||||
|
cancel={cancel}
|
||||||
deleteEditedData={deleteEditedData}
|
deleteEditedData={deleteEditedData}
|
||||||
retrieveAdditionalData={retrieveAdditionalData}
|
retrieveAdditionalData={retrieveAdditionalData}
|
||||||
|
checkAgainstSanctions={checkAgainstSanctions}
|
||||||
editable={editable}></EditableCard>
|
editable={editable}></EditableCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
import { useQuery, useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||||
import {
|
import {
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
|
|
@ -292,6 +292,14 @@ const GET_ACTIVE_CUSTOM_REQUESTS = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const CHECK_AGAINST_SANCTIONS = gql`
|
||||||
|
query checkAgainstSanctions($customerId: ID) {
|
||||||
|
checkAgainstSanctions(customerId: $customerId) {
|
||||||
|
ofacSanctioned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const CustomerProfile = memo(() => {
|
const CustomerProfile = memo(() => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
|
|
@ -400,6 +408,10 @@ const CustomerProfile = memo(() => {
|
||||||
onCompleted: () => getCustomer()
|
onCompleted: () => getCustomer()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [checkAgainstSanctions] = useLazyQuery(CHECK_AGAINST_SANCTIONS, {
|
||||||
|
onCompleted: () => getCustomer()
|
||||||
|
})
|
||||||
|
|
||||||
const updateCustomer = it =>
|
const updateCustomer = it =>
|
||||||
setCustomer({
|
setCustomer({
|
||||||
variables: {
|
variables: {
|
||||||
|
|
@ -662,6 +674,7 @@ const CustomerProfile = memo(() => {
|
||||||
authorizeCustomRequest={authorizeCustomRequest}
|
authorizeCustomRequest={authorizeCustomRequest}
|
||||||
updateCustomEntry={updateCustomEntry}
|
updateCustomEntry={updateCustomEntry}
|
||||||
setRetrieve={setRetrieve}
|
setRetrieve={setRetrieve}
|
||||||
|
checkAgainstSanctions={checkAgainstSanctions}
|
||||||
retrieveAdditionalDataDialog={
|
retrieveAdditionalDataDialog={
|
||||||
<RetrieveDataDialog
|
<RetrieveDataDialog
|
||||||
onDismissed={() => {
|
onDismissed={() => {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const CustomersList = ({
|
||||||
view: getName
|
view: getName
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total TXs',
|
header: 'Total Txs',
|
||||||
width: 126,
|
width: 126,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
view: it => `${Number.parseInt(it.totalTxs)}`
|
view: it => `${Number.parseInt(it.totalTxs)}`
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'customerData',
|
code: 'customerData',
|
||||||
display: 'Customer Data',
|
display: 'Customer data',
|
||||||
Icon: CustomerDataIcon,
|
Icon: CustomerDataIcon,
|
||||||
InverseIcon: CustomerDataReversedIcon
|
InverseIcon: CustomerDataReversedIcon
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ import { makeStyles } from '@material-ui/core/styles'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { Form, Formik, Field as FormikField } from 'formik'
|
import { Form, Formik, Field as FormikField } from 'formik'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import { useState, React } from 'react'
|
import { useState, React, useRef } from 'react'
|
||||||
|
|
||||||
import ErrorMessage from 'src/components/ErrorMessage'
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||||
import { MainStatus } from 'src/components/Status'
|
import { MainStatus } from 'src/components/Status'
|
||||||
|
// import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { ActionButton } from 'src/components/buttons'
|
import { ActionButton } from 'src/components/buttons'
|
||||||
import { Label1, P, H3 } from 'src/components/typography'
|
import { Label1, P, H3 } from 'src/components/typography'
|
||||||
import {
|
import {
|
||||||
|
|
@ -132,23 +133,27 @@ const ReadOnlyField = ({ field, value, ...props }) => {
|
||||||
|
|
||||||
const EditableCard = ({
|
const EditableCard = ({
|
||||||
fields,
|
fields,
|
||||||
save,
|
save = () => {},
|
||||||
authorize,
|
cancel = () => {},
|
||||||
|
authorize = () => {},
|
||||||
hasImage,
|
hasImage,
|
||||||
reject,
|
reject = () => {},
|
||||||
state,
|
state,
|
||||||
title,
|
title,
|
||||||
titleIcon,
|
titleIcon,
|
||||||
children,
|
children = () => {},
|
||||||
validationSchema,
|
validationSchema,
|
||||||
initialValues,
|
initialValues,
|
||||||
deleteEditedData,
|
deleteEditedData,
|
||||||
retrieveAdditionalData,
|
retrieveAdditionalData,
|
||||||
hasAdditionalData = true,
|
hasAdditionalData = true,
|
||||||
editable
|
editable,
|
||||||
|
checkAgainstSanctions
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const formRef = useRef()
|
||||||
|
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [input, setInput] = useState(null)
|
const [input, setInput] = useState(null)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
|
@ -178,7 +183,7 @@ const EditableCard = ({
|
||||||
<H3 className={classes.cardTitle}>{title}</H3>
|
<H3 className={classes.cardTitle}>{title}</H3>
|
||||||
{
|
{
|
||||||
// TODO: Enable for next release
|
// TODO: Enable for next release
|
||||||
/* <HoverableTooltip width={304}></HoverableTooltip> */
|
/* <HelpTooltip width={304}></HelpTooltip> */
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{state && authorize && (
|
{state && authorize && (
|
||||||
|
|
@ -187,8 +192,9 @@ const EditableCard = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children(formRef.current?.values ?? {})}
|
||||||
<Formik
|
<Formik
|
||||||
|
innerRef={formRef}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
|
|
@ -273,6 +279,16 @@ const EditableCard = ({
|
||||||
Retrieve API data
|
Retrieve API data
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
{checkAgainstSanctions && (
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
Icon={DataIcon}
|
||||||
|
InverseIcon={DataReversedIcon}
|
||||||
|
onClick={() => checkAgainstSanctions()}>
|
||||||
|
Check against OFAC sanction list
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{editable && (
|
{editable && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|
@ -314,7 +330,7 @@ const EditableCard = ({
|
||||||
{editing && (
|
{editing && (
|
||||||
<div className={classes.editingWrapper}>
|
<div className={classes.editingWrapper}>
|
||||||
<div className={classes.replace}>
|
<div className={classes.replace}>
|
||||||
{hasImage && (
|
{hasImage && state !== OVERRIDE_PENDING && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -359,6 +375,7 @@ const EditableCard = ({
|
||||||
color="secondary"
|
color="secondary"
|
||||||
Icon={CancelReversedIcon}
|
Icon={CancelReversedIcon}
|
||||||
InverseIcon={CancelReversedIcon}
|
InverseIcon={CancelReversedIcon}
|
||||||
|
onClick={() => cancel()}
|
||||||
type="reset">
|
type="reset">
|
||||||
Cancel
|
Cancel
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const IdDataCard = memo(({ customerData, updateCustomer }) => {
|
||||||
size: 160
|
size: 160
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Birth Date',
|
header: 'Birth date',
|
||||||
display:
|
display:
|
||||||
(rawDob &&
|
(rawDob &&
|
||||||
format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDob))) ??
|
format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDob))) ??
|
||||||
|
|
@ -61,7 +61,7 @@ const IdDataCard = memo(({ customerData, updateCustomer }) => {
|
||||||
size: 120
|
size: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Expiration Date',
|
header: 'Expiration date',
|
||||||
display: ifNotNull(
|
display: ifNotNull(
|
||||||
rawExpirationDate,
|
rawExpirationDate,
|
||||||
format('yyyy-MM-dd', rawExpirationDate)
|
format('yyyy-MM-dd', rawExpirationDate)
|
||||||
|
|
|
||||||
|
|
@ -411,7 +411,7 @@ const customerDataElements = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'expirationDate',
|
name: 'expirationDate',
|
||||||
label: 'Expiration Date',
|
label: 'Expiration date',
|
||||||
component: TextInput,
|
component: TextInput,
|
||||||
editable: true
|
editable: true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,7 @@ import React, { useEffect, useRef, useCallback } from 'react'
|
||||||
|
|
||||||
import { backgroundColor, zircon, primaryColor } from 'src/styling/variables'
|
import { backgroundColor, zircon, primaryColor } from 'src/styling/variables'
|
||||||
|
|
||||||
const transactionProfit = tx => {
|
const transactionProfit = R.prop('profit')
|
||||||
const cashInFee = tx.cashInFee ? Number.parseFloat(tx.cashInFee) : 0
|
|
||||||
const commission =
|
|
||||||
Number.parseFloat(tx.commissionPercentage) * Number.parseFloat(tx.fiat)
|
|
||||||
return commission + cashInFee
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockPoint = (tx, offsetMs, profit) => {
|
const mockPoint = (tx, offsetMs, profit) => {
|
||||||
const date = new Date(new Date(tx.created).getTime() + offsetMs).toISOString()
|
const date = new Date(new Date(tx.created).getTime() + offsetMs).toISOString()
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
fontSecondary,
|
fontSecondary,
|
||||||
backgroundColor
|
backgroundColor
|
||||||
} from 'src/styling/variables'
|
} from 'src/styling/variables'
|
||||||
|
import { numberToFiatAmount } from 'src/utils/number'
|
||||||
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||||
|
|
||||||
const Graph = ({ data, timeFrame, timezone }) => {
|
const Graph = ({ data, timeFrame, timezone }) => {
|
||||||
|
|
@ -172,7 +173,16 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
||||||
g =>
|
g =>
|
||||||
g
|
g
|
||||||
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
||||||
.call(d3.axisLeft(y).ticks(5))
|
.call(
|
||||||
|
d3
|
||||||
|
.axisLeft(y)
|
||||||
|
.ticks(5)
|
||||||
|
.tickFormat(d => {
|
||||||
|
if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
|
||||||
|
|
||||||
|
return numberToFiatAmount(d)
|
||||||
|
})
|
||||||
|
)
|
||||||
.call(g => g.select('.domain').remove())
|
.call(g => g.select('.domain').remove())
|
||||||
.selectAll('text')
|
.selectAll('text')
|
||||||
.attr('dy', '-0.25rem'),
|
.attr('dy', '-0.25rem'),
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const GET_DATA = gql`
|
||||||
transactions(excludeTestingCustomers: $excludeTestingCustomers) {
|
transactions(excludeTestingCustomers: $excludeTestingCustomers) {
|
||||||
fiatCode
|
fiatCode
|
||||||
fiat
|
fiat
|
||||||
cashInFee
|
fixedFee
|
||||||
commissionPercentage
|
commissionPercentage
|
||||||
created
|
created
|
||||||
txClass
|
txClass
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ const Funding = () => {
|
||||||
{funding.length && (
|
{funding.length && (
|
||||||
<div className={classes.total}>
|
<div className={classes.total}>
|
||||||
<Label1 className={classes.totalTitle}>
|
<Label1 className={classes.totalTitle}>
|
||||||
Total Crypto Balance
|
Total crypto balance
|
||||||
</Label1>
|
</Label1>
|
||||||
<Info1 noMargin>
|
<Info1 noMargin>
|
||||||
{getConfirmedTotal(funding)}
|
{getConfirmedTotal(funding)}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { Link } from 'src/components/buttons'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { Link, SupportLinkButton } from 'src/components/buttons'
|
||||||
import { Table as EditableTable } from 'src/components/editableTable'
|
import { Table as EditableTable } from 'src/components/editableTable'
|
||||||
import Section from 'src/components/layout/Section'
|
import Section from 'src/components/layout/Section'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
|
@ -61,8 +62,9 @@ const GET_DATA = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const SAVE_CONFIG = gql`
|
const SAVE_CONFIG = gql`
|
||||||
mutation Save($config: JSONObject) {
|
mutation Save($config: JSONObject, $accounts: JSONObject) {
|
||||||
saveConfig(config: $config)
|
saveConfig(config: $config)
|
||||||
|
saveAccounts(accounts: $accounts)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -134,9 +136,9 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
||||||
return save(newConfig)
|
return save(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = config => {
|
const save = (config, accounts) => {
|
||||||
setDataToSave(null)
|
setDataToSave(null)
|
||||||
return saveConfig({ variables: { config } })
|
return saveConfig({ variables: { config, accounts } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveOverrides = it => {
|
const saveOverrides = it => {
|
||||||
|
|
@ -162,8 +164,8 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
||||||
const onEditingDefault = (it, editing) => setEditingDefault(editing)
|
const onEditingDefault = (it, editing) => setEditingDefault(editing)
|
||||||
const onEditingOverrides = (it, editing) => setEditingOverrides(editing)
|
const onEditingOverrides = (it, editing) => setEditingOverrides(editing)
|
||||||
|
|
||||||
const wizardSave = it =>
|
const wizardSave = (config, accounts) =>
|
||||||
save(toNamespace(namespaces.WALLETS)(it)).then(it => {
|
save(toNamespace(namespaces.WALLETS)(config), accounts).then(it => {
|
||||||
onChangeFunction()
|
onChangeFunction()
|
||||||
setOnChangeFunction(null)
|
setOnChangeFunction(null)
|
||||||
return it
|
return it
|
||||||
|
|
@ -176,7 +178,22 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
||||||
close={() => setDataToSave(null)}
|
close={() => setDataToSave(null)}
|
||||||
save={() => dataToSave && save(dataToSave)}
|
save={() => dataToSave && save(dataToSave)}
|
||||||
/>
|
/>
|
||||||
<TitleSection title="Locales" />
|
<TitleSection
|
||||||
|
title="Locales"
|
||||||
|
appendix={
|
||||||
|
<HelpTooltip width={320}>
|
||||||
|
<P>
|
||||||
|
For details on configuring languages, please read the relevant
|
||||||
|
knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360016257471-Setting-multiple-machine-languages"
|
||||||
|
label="Setting multiple machine languages"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Section>
|
<Section>
|
||||||
<EditableTable
|
<EditableTable
|
||||||
title="Default settings"
|
title="Default settings"
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ const LocaleSchema = Yup.object().shape({
|
||||||
.label('Country')
|
.label('Country')
|
||||||
.required(),
|
.required(),
|
||||||
fiatCurrency: Yup.string()
|
fiatCurrency: Yup.string()
|
||||||
.label('Fiat Currency')
|
.label('Fiat currency')
|
||||||
.required(),
|
.required(),
|
||||||
languages: Yup.array()
|
languages: Yup.array()
|
||||||
.label('Languages')
|
.label('Languages')
|
||||||
|
|
@ -165,7 +165,7 @@ const LocaleSchema = Yup.object().shape({
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(4),
|
.max(4),
|
||||||
cryptoCurrencies: Yup.array()
|
cryptoCurrencies: Yup.array()
|
||||||
.label('Crypto Currencies')
|
.label('Crypto currencies')
|
||||||
.required()
|
.required()
|
||||||
.min(1),
|
.min(1),
|
||||||
timezone: Yup.string()
|
timezone: Yup.string()
|
||||||
|
|
@ -186,7 +186,7 @@ const OverridesSchema = Yup.object().shape({
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(4),
|
.max(4),
|
||||||
cryptoCurrencies: Yup.array()
|
cryptoCurrencies: Yup.array()
|
||||||
.label('Crypto Currencies')
|
.label('Crypto currencies')
|
||||||
.required()
|
.required()
|
||||||
.min(1)
|
.min(1)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import * as Yup from 'yup'
|
||||||
|
|
||||||
import ErrorMessage from 'src/components/ErrorMessage'
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { Button } from 'src/components/buttons'
|
import { Button } from 'src/components/buttons'
|
||||||
import { NumberInput, Autocomplete } from 'src/components/inputs/formik'
|
import { NumberInput, Autocomplete } from 'src/components/inputs/formik'
|
||||||
import { H3, TL1, P } from 'src/components/typography'
|
import { H3, TL1, P } from 'src/components/typography'
|
||||||
|
|
@ -99,7 +99,7 @@ const IndividualDiscountModal = ({
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.discountRateWrapper}>
|
<div className={classes.discountRateWrapper}>
|
||||||
<H3>Define discount rate</H3>
|
<H3>Define discount rate</H3>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
This is a percentage discount off of your existing
|
This is a percentage discount off of your existing
|
||||||
commission rates for a customer entering this code at
|
commission rates for a customer entering this code at
|
||||||
|
|
@ -110,7 +110,7 @@ const IndividualDiscountModal = ({
|
||||||
code is set for 50%, then you'll instead be charging 4%
|
code is set for 50%, then you'll instead be charging 4%
|
||||||
on transactions using the code.
|
on transactions using the code.
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.discountInput}>
|
<div className={classes.discountInput}>
|
||||||
<Field
|
<Field
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import * as Yup from 'yup'
|
||||||
|
|
||||||
import ErrorMessage from 'src/components/ErrorMessage'
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { Button } from 'src/components/buttons'
|
import { Button } from 'src/components/buttons'
|
||||||
import { TextInput, NumberInput } from 'src/components/inputs/formik'
|
import { TextInput, NumberInput } from 'src/components/inputs/formik'
|
||||||
import { H3, TL1, P } from 'src/components/typography'
|
import { H3, TL1, P } from 'src/components/typography'
|
||||||
|
|
@ -69,7 +69,7 @@ const PromoCodesModal = ({ showModal, onClose, errorMsg, addCode }) => {
|
||||||
/>
|
/>
|
||||||
<div className={classes.modalLabel2Wrapper}>
|
<div className={classes.modalLabel2Wrapper}>
|
||||||
<H3 className={classes.modalLabel2}>Define discount rate</H3>
|
<H3 className={classes.modalLabel2}>Define discount rate</H3>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
This is a percentage discount off of your existing
|
This is a percentage discount off of your existing
|
||||||
commission rates for a customer entering this code at the
|
commission rates for a customer entering this code at the
|
||||||
|
|
@ -80,7 +80,7 @@ const PromoCodesModal = ({ showModal, onClose, errorMsg, addCode }) => {
|
||||||
set for 50%, then you'll instead be charging 4% on
|
set for 50%, then you'll instead be charging 4% on
|
||||||
transactions using the code.
|
transactions using the code.
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.discountInput}>
|
<div className={classes.discountInput}>
|
||||||
<Field
|
<Field
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ const Logs = () => {
|
||||||
<>
|
<>
|
||||||
<div className={classes.titleWrapper}>
|
<div className={classes.titleWrapper}>
|
||||||
<div className={classes.titleAndButtonsContainer}>
|
<div className={classes.titleAndButtonsContainer}>
|
||||||
<Title>Machine Logs</Title>
|
<Title>Machine logs</Title>
|
||||||
{logsResponse && (
|
{logsResponse && (
|
||||||
<div className={classes.buttonsWrapper}>
|
<div className={classes.buttonsWrapper}>
|
||||||
<LogsDowloaderPopover
|
<LogsDowloaderPopover
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,11 @@ const Commissions = ({ name: SCREEN_KEY, id: deviceId }) => {
|
||||||
cashIn: config.cashIn,
|
cashIn: config.cashIn,
|
||||||
cashOut: config.cashOut,
|
cashOut: config.cashOut,
|
||||||
fixedFee: config.fixedFee,
|
fixedFee: config.fixedFee,
|
||||||
minimumTx: config.minimumTx
|
minimumTx: config.minimumTx,
|
||||||
|
cashOutFixedFee: config.cashOutFixedFee
|
||||||
},
|
},
|
||||||
R.project(
|
R.project(
|
||||||
['cashIn', 'cashOut', 'fixedFee', 'minimumTx'],
|
['cashIn', 'cashOut', 'fixedFee', 'minimumTx', 'cashOutFixedFee'],
|
||||||
R.filter(
|
R.filter(
|
||||||
o =>
|
o =>
|
||||||
R.includes(coin.code, o.cryptoCurrencies) ||
|
R.includes(coin.code, o.cryptoCurrencies) ||
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,14 @@ const getOverridesFields = currency => {
|
||||||
doubleHeader: 'Cash-in only',
|
doubleHeader: 'Cash-in only',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
suffix: currency
|
suffix: currency
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cashOutFixedFee',
|
||||||
|
display: 'Fixed fee',
|
||||||
|
width: 155,
|
||||||
|
doubleHeader: 'Cash-out only',
|
||||||
|
textAlign: 'right',
|
||||||
|
suffix: currency
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ const GET_TRANSACTIONS = gql`
|
||||||
hasError: error
|
hasError: error
|
||||||
deviceId
|
deviceId
|
||||||
fiat
|
fiat
|
||||||
cashInFee
|
fixedFee
|
||||||
fiatCode
|
fiatCode
|
||||||
cryptoAtoms
|
cryptoAtoms
|
||||||
cryptoCode
|
cryptoCode
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ import React, { useState } from 'react'
|
||||||
|
|
||||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { IconButton, Button } from 'src/components/buttons'
|
import { HelpTooltip } from 'src/components/Tooltip.js'
|
||||||
|
import { IconButton, Button, SupportLinkButton } from 'src/components/buttons'
|
||||||
import { RadioGroup } from 'src/components/inputs'
|
import { RadioGroup } from 'src/components/inputs'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import { EmptyTable } from 'src/components/table'
|
import { EmptyTable } from 'src/components/table'
|
||||||
|
|
@ -204,7 +205,7 @@ const CashCassettes = () => {
|
||||||
!dataLoading && (
|
!dataLoading && (
|
||||||
<>
|
<>
|
||||||
<TitleSection
|
<TitleSection
|
||||||
title="Cash Boxes & Cassettes"
|
title="Cash boxes & cassettes"
|
||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
text: 'Cash box history',
|
text: 'Cash box history',
|
||||||
|
|
@ -229,7 +230,20 @@ const CashCassettes = () => {
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
iconClassName={classes.listViewButton}
|
iconClassName={classes.listViewButton}
|
||||||
className={classes.tableWidth}>
|
className={classes.tableWidth}
|
||||||
|
appendix={
|
||||||
|
<HelpTooltip width={220}>
|
||||||
|
<P>
|
||||||
|
For details on configuring cash boxes and cassettes, please read
|
||||||
|
the relevant knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/4420839641229-Cash-Boxes-Cassettess"
|
||||||
|
label="Cash Boxes & Cassettes"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
|
}>
|
||||||
{!showHistory && (
|
{!showHistory && (
|
||||||
<Box alignItems="center" justifyContent="flex-end">
|
<Box alignItems="center" justifyContent="flex-end">
|
||||||
<Label1 className={classes.cashboxReset}>Cash box resets</Label1>
|
<Label1 className={classes.cashboxReset}>Cash box resets</Label1>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default {
|
||||||
height: 36
|
height: 36
|
||||||
},
|
},
|
||||||
tBody: {
|
tBody: {
|
||||||
maxHeight: '65vh',
|
maxHeight: 'calc(100vh - 350px)',
|
||||||
overflow: 'auto'
|
overflow: 'auto'
|
||||||
},
|
},
|
||||||
tableWidth: {
|
tableWidth: {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ const styles = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between'
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
tableWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: 1,
|
||||||
|
marginBottom: 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +164,7 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'billCount',
|
name: 'billCount',
|
||||||
header: 'Bill Count',
|
header: 'Bill count',
|
||||||
width: 115,
|
width: 115,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
input: NumberInput,
|
input: NumberInput,
|
||||||
|
|
@ -243,13 +249,15 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<div className={classes.tableWrapper}>
|
||||||
loading={loading}
|
<DataTable
|
||||||
name="cashboxHistory"
|
loading={loading}
|
||||||
elements={elements}
|
name="cashboxHistory"
|
||||||
data={batches}
|
elements={elements}
|
||||||
emptyText="No cash box batches so far"
|
data={batches}
|
||||||
/>
|
emptyText="No cash box batches so far"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
||||||
<Item xs>
|
<Item xs>
|
||||||
<Container className={classes.row}>
|
<Container className={classes.row}>
|
||||||
<Item xs={2}>
|
<Item xs={2}>
|
||||||
<Label>Machine Model</Label>
|
<Label>Machine model</Label>
|
||||||
<span>{modelPrettifier[machine.model]}</span>
|
<span>{modelPrettifier[machine.model]}</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item xs={4}>
|
<Item xs={4}>
|
||||||
|
|
@ -126,7 +126,7 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item xs={2}>
|
<Item xs={2}>
|
||||||
<Label>Packet Loss</Label>
|
<Label>Packet loss</Label>
|
||||||
<span>
|
<span>
|
||||||
{machine.packetLoss
|
{machine.packetLoss
|
||||||
? new BigNumber(machine.packetLoss).toFixed(3).toString() +
|
? new BigNumber(machine.packetLoss).toFixed(3).toString() +
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ const MachineStatus = () => {
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
header: 'Machine Name',
|
header: 'Machine name',
|
||||||
width: 250,
|
width: 250,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
|
|
@ -111,7 +111,7 @@ const MachineStatus = () => {
|
||||||
: 'unknown'
|
: 'unknown'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Software Version',
|
header: 'Software version',
|
||||||
width: 200,
|
width: 200,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
|
|
@ -134,7 +134,7 @@ const MachineStatus = () => {
|
||||||
<>
|
<>
|
||||||
<div className={classes.titleWrapper}>
|
<div className={classes.titleWrapper}>
|
||||||
<div className={classes.titleAndButtonsContainer}>
|
<div className={classes.titleAndButtonsContainer}>
|
||||||
<Title>Machine Status</Title>
|
<Title>Machine status</Title>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.headerLabels}>
|
<div className={classes.headerLabels}>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import React from 'react'
|
||||||
|
|
||||||
import ErrorMessage from 'src/components/ErrorMessage'
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import Stepper from 'src/components/Stepper'
|
import Stepper from 'src/components/Stepper'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { Button } from 'src/components/buttons'
|
import { Button } from 'src/components/buttons'
|
||||||
import { Cashbox } from 'src/components/inputs/cashbox/Cashbox'
|
import { Cashbox } from 'src/components/inputs/cashbox/Cashbox'
|
||||||
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
|
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
|
||||||
|
|
@ -245,12 +245,12 @@ const WizardStep = ({
|
||||||
classes.centerAlignment
|
classes.centerAlignment
|
||||||
)}>
|
)}>
|
||||||
<P>Since previous update</P>
|
<P>Since previous update</P>
|
||||||
<HoverableTooltip width={215}>
|
<HelpTooltip width={215}>
|
||||||
<P>
|
<P>
|
||||||
Number of bills inside the cash box, since the last
|
Number of bills inside the cash box, since the last
|
||||||
cash box changes.
|
cash box changes.
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { SupportLinkButton } from 'src/components/buttons'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import { P } from 'src/components/typography'
|
import { P } from 'src/components/typography'
|
||||||
import FormRenderer from 'src/pages/Services/FormRenderer'
|
import FormRenderer from 'src/pages/Services/FormRenderer'
|
||||||
|
|
@ -159,7 +161,24 @@ const Notifications = ({
|
||||||
!loading && (
|
!loading && (
|
||||||
<>
|
<>
|
||||||
<NotificationsCtx.Provider value={contextValue}>
|
<NotificationsCtx.Provider value={contextValue}>
|
||||||
{displayTitle && <TitleSection title="Notifications" />}
|
{displayTitle && (
|
||||||
|
<TitleSection
|
||||||
|
title="Notifications"
|
||||||
|
appendix={
|
||||||
|
<HelpTooltip width={250}>
|
||||||
|
<P>
|
||||||
|
For details on configuring notifications, please read the
|
||||||
|
relevant knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/115001210592-Enabling-notifications"
|
||||||
|
label="Enabling notifications"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{displayThirdPartyProvider && (
|
{displayThirdPartyProvider && (
|
||||||
<Section
|
<Section
|
||||||
title="Third party providers"
|
title="Third party providers"
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const CryptoBalanceAlerts = ({ section, fieldWidth }) => {
|
||||||
section={section}
|
section={section}
|
||||||
decoration={currency}
|
decoration={currency}
|
||||||
className={classes.cryptoBalanceAlertsForm}
|
className={classes.cryptoBalanceAlertsForm}
|
||||||
title="Default (Low Balance)"
|
title="Default (Low balance)"
|
||||||
label="Alert me under"
|
label="Alert me under"
|
||||||
editing={isEditing(LOW_BALANCE_KEY)}
|
editing={isEditing(LOW_BALANCE_KEY)}
|
||||||
disabled={isDisabled(LOW_BALANCE_KEY)}
|
disabled={isDisabled(LOW_BALANCE_KEY)}
|
||||||
|
|
@ -49,7 +49,7 @@ const CryptoBalanceAlerts = ({ section, fieldWidth }) => {
|
||||||
save={save}
|
save={save}
|
||||||
decoration={currency}
|
decoration={currency}
|
||||||
className={classes.cryptoBalanceAlertsSecondForm}
|
className={classes.cryptoBalanceAlertsSecondForm}
|
||||||
title="Default (High Balance)"
|
title="Default (High balance)"
|
||||||
label="Alert me over"
|
label="Alert me over"
|
||||||
editing={isEditing(HIGH_BALANCE_KEY)}
|
editing={isEditing(HIGH_BALANCE_KEY)}
|
||||||
disabled={isDisabled(HIGH_BALANCE_KEY)}
|
disabled={isDisabled(HIGH_BALANCE_KEY)}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ const CryptoBalanceOverrides = ({ section }) => {
|
||||||
.nullable()
|
.nullable()
|
||||||
.required(),
|
.required(),
|
||||||
[LOW_BALANCE_KEY]: Yup.number()
|
[LOW_BALANCE_KEY]: Yup.number()
|
||||||
.label('Low Balance')
|
.label('Low balance')
|
||||||
.when(HIGH_BALANCE_KEY, {
|
.when(HIGH_BALANCE_KEY, {
|
||||||
is: HIGH_BALANCE_KEY => !HIGH_BALANCE_KEY,
|
is: HIGH_BALANCE_KEY => !HIGH_BALANCE_KEY,
|
||||||
then: Yup.number().required()
|
then: Yup.number().required()
|
||||||
|
|
@ -73,7 +73,7 @@ const CryptoBalanceOverrides = ({ section }) => {
|
||||||
.max(CURRENCY_MAX)
|
.max(CURRENCY_MAX)
|
||||||
.nullable(),
|
.nullable(),
|
||||||
[HIGH_BALANCE_KEY]: Yup.number()
|
[HIGH_BALANCE_KEY]: Yup.number()
|
||||||
.label('High Balance')
|
.label('High balance')
|
||||||
.when(LOW_BALANCE_KEY, {
|
.when(LOW_BALANCE_KEY, {
|
||||||
is: LOW_BALANCE_KEY => !LOW_BALANCE_KEY,
|
is: LOW_BALANCE_KEY => !LOW_BALANCE_KEY,
|
||||||
then: Yup.number().required()
|
then: Yup.number().required()
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from 'src/components/fake-table/Table'
|
} from 'src/components/fake-table/Table'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||||
import { startCase } from 'src/utils/string'
|
import { sentenceCase } from 'src/utils/string'
|
||||||
|
|
||||||
import NotificationsCtx from '../NotificationsContext'
|
import NotificationsCtx from '../NotificationsContext'
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ const Row = ({
|
||||||
return (
|
return (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td width={channelSize}>
|
<Td width={channelSize}>
|
||||||
{shouldUpperCase ? R.toUpper(namespace) : startCase(namespace)}
|
{shouldUpperCase ? R.toUpper(namespace) : sentenceCase(namespace)}
|
||||||
</Td>
|
</Td>
|
||||||
<Cell name="balance" disabled={disabled} />
|
<Cell name="balance" disabled={disabled} />
|
||||||
<Cell name="transactions" disabled={disabled} />
|
<Cell name="transactions" disabled={disabled} />
|
||||||
|
|
@ -127,7 +127,7 @@ const Setup = ({ wizard, forceDisable }) => {
|
||||||
<Th width={channelSize - widthAdjust}>Channel</Th>
|
<Th width={channelSize - widthAdjust}>Channel</Th>
|
||||||
{Object.keys(sizes).map(it => (
|
{Object.keys(sizes).map(it => (
|
||||||
<Th key={it} width={sizes[it] - widthAdjust} textAlign="center">
|
<Th key={it} width={sizes[it] - widthAdjust} textAlign="center">
|
||||||
{startCase(it)}
|
{sentenceCase(it)}
|
||||||
</Th>
|
</Th>
|
||||||
))}
|
))}
|
||||||
</THead>
|
</THead>
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ import { makeStyles } from '@material-ui/core/styles'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import React, { memo } from 'react'
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import { H4, P, Label2 } from 'src/components/typography'
|
import { H4, P, Label2 } from 'src/components/typography'
|
||||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
|
import { SupportLinkButton } from '../../components/buttons'
|
||||||
|
|
||||||
import { global } from './OperatorInfo.styles'
|
import { global } from './OperatorInfo.styles'
|
||||||
|
|
||||||
const useStyles = makeStyles(global)
|
const useStyles = makeStyles(global)
|
||||||
|
|
@ -66,7 +68,7 @@ const CoinATMRadar = memo(({ wizard }) => {
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
<H4>Coin ATM Radar share settings</H4>
|
<H4>Coin ATM Radar share settings</H4>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={320}>
|
||||||
<P>
|
<P>
|
||||||
For details on configuring this panel, please read the relevant
|
For details on configuring this panel, please read the relevant
|
||||||
knowledgebase article{' '}
|
knowledgebase article{' '}
|
||||||
|
|
@ -78,7 +80,12 @@ const CoinATMRadar = memo(({ wizard }) => {
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360023720472-Coin-ATM-Radar"
|
||||||
|
label="Lamassu Support Article"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<Row
|
<Row
|
||||||
title={'Share information?'}
|
title={'Share information?'}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ import * as Yup from 'yup'
|
||||||
|
|
||||||
import ErrorMessage from 'src/components/ErrorMessage'
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||||
import { Link, IconButton } from 'src/components/buttons'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { Link, IconButton, SupportLinkButton } from 'src/components/buttons'
|
||||||
import Switch from 'src/components/inputs/base/Switch'
|
import Switch from 'src/components/inputs/base/Switch'
|
||||||
import { TextInput } from 'src/components/inputs/formik'
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
import { P, H4, Info3, Label1, Label2, Label3 } from 'src/components/typography'
|
import { P, H4, Info3, Label1, Label2, Label3 } from 'src/components/typography'
|
||||||
|
|
@ -136,7 +137,7 @@ const ContactInfo = ({ wizard }) => {
|
||||||
const fields = [
|
const fields = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Full name',
|
label: 'Company name',
|
||||||
value: info.name ?? '',
|
value: info.name ?? '',
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
|
|
@ -160,7 +161,7 @@ const ContactInfo = ({ wizard }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'companyNumber',
|
name: 'companyNumber',
|
||||||
label: 'Company number',
|
label: 'Company registration number',
|
||||||
value: info.companyNumber ?? '',
|
value: info.companyNumber ?? '',
|
||||||
component: TextInput
|
component: TextInput
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +190,17 @@ const ContactInfo = ({ wizard }) => {
|
||||||
<>
|
<>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
<H4>Contact information</H4>
|
<H4>Contact information</H4>
|
||||||
|
<HelpTooltip width={320}>
|
||||||
|
<P>
|
||||||
|
For details on configuring this panel, please read the relevant
|
||||||
|
knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360033051732-Enabling-Operator-Info"
|
||||||
|
label="Lamassu Support Article"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.switchRow}>
|
<div className={classes.switchRow}>
|
||||||
<P>Info card enabled?</P>
|
<P>Info card enabled?</P>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { memo } from 'react'
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import { H4, P, Label2 } from 'src/components/typography'
|
import { H4, P, Label2 } from 'src/components/typography'
|
||||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
|
import { SupportLinkButton } from '../../components/buttons'
|
||||||
|
|
||||||
import { global } from './OperatorInfo.styles'
|
import { global } from './OperatorInfo.styles'
|
||||||
|
|
||||||
const useStyles = makeStyles(global)
|
const useStyles = makeStyles(global)
|
||||||
|
|
@ -47,6 +50,17 @@ const ReceiptPrinting = memo(({ wizard }) => {
|
||||||
<>
|
<>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
<H4>Receipt options</H4>
|
<H4>Receipt options</H4>
|
||||||
|
<HelpTooltip width={320}>
|
||||||
|
<P>
|
||||||
|
For details on configuring this panel, please read the relevant
|
||||||
|
knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360058513951-Receipt-options-printers"
|
||||||
|
label="Lamassu Support Article"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.switchRow}>
|
<div className={classes.switchRow}>
|
||||||
<P>Enable receipt printing</P>
|
<P>Enable receipt printing</P>
|
||||||
|
|
@ -109,7 +123,7 @@ const ReceiptPrinting = memo(({ wizard }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'companyNumber',
|
name: 'companyNumber',
|
||||||
display: 'Company number'
|
display: 'Company registration number'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'machineLocation',
|
name: 'machineLocation',
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { IconButton } from 'src/components/buttons'
|
import { IconButton, SupportLinkButton } from 'src/components/buttons'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
import { H4, P, Label3 } from 'src/components/typography'
|
import { H4, P, Label3 } from 'src/components/typography'
|
||||||
|
|
@ -162,9 +162,9 @@ const SMSNotices = () => {
|
||||||
!R.isEmpty(TOOLTIPS[it.event]) ? (
|
!R.isEmpty(TOOLTIPS[it.event]) ? (
|
||||||
<div className={classes.messageWithTooltip}>
|
<div className={classes.messageWithTooltip}>
|
||||||
{R.prop('messageName', it)}
|
{R.prop('messageName', it)}
|
||||||
<HoverableTooltip width={250}>
|
<HelpTooltip width={250}>
|
||||||
<P>{TOOLTIPS[it.event]}</P>
|
<P>{TOOLTIPS[it.event]}</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
R.prop('messageName', it)
|
R.prop('messageName', it)
|
||||||
|
|
@ -237,6 +237,17 @@ const SMSNotices = () => {
|
||||||
<>
|
<>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
<H4>SMS notices</H4>
|
<H4>SMS notices</H4>
|
||||||
|
<HelpTooltip width={320}>
|
||||||
|
<P>
|
||||||
|
For details on configuring this panel, please read the relevant
|
||||||
|
knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/115001205591-SMS-Phone-Verification"
|
||||||
|
label="Lamassu Support Article"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<CustomSMSModal
|
<CustomSMSModal
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ import * as Yup from 'yup'
|
||||||
|
|
||||||
import ErrorMessage from 'src/components/ErrorMessage'
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||||
import { Link, IconButton } from 'src/components/buttons'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { Link, IconButton, SupportLinkButton } from 'src/components/buttons'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import { TextInput } from 'src/components/inputs/formik'
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
import { H4, Info2, Info3, Label2, Label3, P } from 'src/components/typography'
|
import { H4, Info2, Info3, Label2, Label3, P } from 'src/components/typography'
|
||||||
|
|
@ -171,6 +172,17 @@ const TermsConditions = () => {
|
||||||
<>
|
<>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
<H4>Terms & Conditions</H4>
|
<H4>Terms & Conditions</H4>
|
||||||
|
<HelpTooltip width={320}>
|
||||||
|
<P>
|
||||||
|
For details on configuring this panel, please read the relevant
|
||||||
|
knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360015982211-Terms-and-Conditions"
|
||||||
|
label="Lamassu Support Article"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.switchRow}>
|
<div className={classes.switchRow}>
|
||||||
<P>Show on screen</P>
|
<P>Show on screen</P>
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ const Services = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.wrapper}>
|
<div className={classes.wrapper}>
|
||||||
<TitleSection title="3rd Party Services" />
|
<TitleSection title="Third-Party services" />
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
{R.values(schemas).map(schema => (
|
{R.values(schemas).map(schema => (
|
||||||
<Grid item key={schema.code}>
|
<Grid item key={schema.code}>
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ export default {
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'apiKey',
|
code: 'apiKey',
|
||||||
display: 'API Key',
|
display: 'API key',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'privateKey',
|
code: 'privateKey',
|
||||||
display: 'Private Key',
|
display: 'Private key',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ export default {
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'apiKey',
|
code: 'apiKey',
|
||||||
display: 'API Key',
|
display: 'API key',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'privateKey',
|
code: 'privateKey',
|
||||||
display: 'Private Key',
|
display: 'Private key',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default {
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'token',
|
code: 'token',
|
||||||
display: 'API Token',
|
display: 'API token',
|
||||||
component: TextInput,
|
component: TextInput,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
|
|
@ -47,52 +47,52 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'BTCWalletId',
|
code: 'BTCWalletId',
|
||||||
display: 'BTC Wallet ID',
|
display: 'BTC wallet ID',
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'BTCWalletPassphrase',
|
code: 'BTCWalletPassphrase',
|
||||||
display: 'BTC Wallet Passphrase',
|
display: 'BTC wallet passphrase',
|
||||||
component: SecretInput
|
component: SecretInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'LTCWalletId',
|
code: 'LTCWalletId',
|
||||||
display: 'LTC Wallet ID',
|
display: 'LTC wallet ID',
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'LTCWalletPassphrase',
|
code: 'LTCWalletPassphrase',
|
||||||
display: 'LTC Wallet Passphrase',
|
display: 'LTC wallet passphrase',
|
||||||
component: SecretInput
|
component: SecretInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'ZECWalletId',
|
code: 'ZECWalletId',
|
||||||
display: 'ZEC Wallet ID',
|
display: 'ZEC wallet ID',
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'ZECWalletPassphrase',
|
code: 'ZECWalletPassphrase',
|
||||||
display: 'ZEC Wallet Passphrase',
|
display: 'ZEC wallet passphrase',
|
||||||
component: SecretInput
|
component: SecretInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'BCHWalletId',
|
code: 'BCHWalletId',
|
||||||
display: 'BCH Wallet ID',
|
display: 'BCH wallet ID',
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'BCHWalletPassphrase',
|
code: 'BCHWalletPassphrase',
|
||||||
display: 'BCH Wallet Passphrase',
|
display: 'BCH wallet passphrase',
|
||||||
component: SecretInput
|
component: SecretInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'DASHWalletId',
|
code: 'DASHWalletId',
|
||||||
display: 'DASH Wallet ID',
|
display: 'DASH wallet ID',
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'DASHWalletPassphrase',
|
code: 'DASHWalletPassphrase',
|
||||||
display: 'DASH Wallet Passphrase',
|
display: 'DASH wallet passphrase',
|
||||||
component: SecretInput
|
component: SecretInput
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'key',
|
code: 'key',
|
||||||
display: 'API Key',
|
display: 'API key',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'secret',
|
code: 'secret',
|
||||||
display: 'API Secret',
|
display: 'API secret',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ export default {
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'token',
|
code: 'token',
|
||||||
display: 'API Token',
|
display: 'API token',
|
||||||
component: TextInput,
|
component: TextInput,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'confidenceFactor',
|
code: 'confidenceFactor',
|
||||||
display: 'Confidence Factor',
|
display: 'Confidence factor',
|
||||||
component: NumberInput,
|
component: NumberInput,
|
||||||
face: true
|
face: true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,21 @@ export default {
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'apiKey',
|
code: 'apiKey',
|
||||||
display: 'API Key',
|
display: 'API key',
|
||||||
|
component: TextInputFormik,
|
||||||
|
face: true,
|
||||||
|
long: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'uid',
|
||||||
|
display: 'User ID',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'privateKey',
|
code: 'privateKey',
|
||||||
display: 'Private Key',
|
display: 'Private key',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -28,6 +35,9 @@ export default {
|
||||||
apiKey: Yup.string('The API key must be a string')
|
apiKey: Yup.string('The API key must be a string')
|
||||||
.max(100, 'The API key is too long')
|
.max(100, 'The API key is too long')
|
||||||
.required('The API key is required'),
|
.required('The API key is required'),
|
||||||
|
uid: Yup.string('The User ID must be a string')
|
||||||
|
.max(100, 'The User ID is too long')
|
||||||
|
.required('The User ID is required'),
|
||||||
privateKey: Yup.string('The private key must be a string')
|
privateKey: Yup.string('The private key must be a string')
|
||||||
.max(100, 'The private key is too long')
|
.max(100, 'The private key is too long')
|
||||||
.test(secretTest(account?.privateKey, 'private key'))
|
.test(secretTest(account?.privateKey, 'private key'))
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'clientKey',
|
code: 'clientKey',
|
||||||
display: 'Client Key',
|
display: 'Client key',
|
||||||
component: TextInputFormik
|
component: TextInputFormik
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'clientSecret',
|
code: 'clientSecret',
|
||||||
display: 'Client Secret',
|
display: 'Client secret',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ export default {
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'apiKey',
|
code: 'apiKey',
|
||||||
display: 'API Key',
|
display: 'API key',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'privateKey',
|
code: 'privateKey',
|
||||||
display: 'Private Key',
|
display: 'Private key',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default {
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'apiKey',
|
code: 'apiKey',
|
||||||
display: 'API Key',
|
display: 'API key',
|
||||||
component: TextInputFormik
|
component: TextInputFormik
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -19,13 +19,13 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'fromEmail',
|
code: 'fromEmail',
|
||||||
display: 'From Email',
|
display: 'From email',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true
|
face: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'toEmail',
|
code: 'toEmail',
|
||||||
display: 'To Email',
|
display: 'To email',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true
|
face: true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const singleBitgo = code => ({
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'token',
|
code: 'token',
|
||||||
display: 'API Token',
|
display: 'API token',
|
||||||
component: TextInput,
|
component: TextInput,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
|
|
@ -34,12 +34,12 @@ const singleBitgo = code => ({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: `${code}WalletId`,
|
code: `${code}WalletId`,
|
||||||
display: `${code} Wallet ID`,
|
display: `${code} wallet ID`,
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: `${code}WalletPassphrase`,
|
code: `${code}WalletPassphrase`,
|
||||||
display: `${code} Wallet Passphrase`,
|
display: `${code} wallet passphrase`,
|
||||||
component: SecretInput
|
component: SecretInput
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,18 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'authToken',
|
code: 'authToken',
|
||||||
display: 'Auth Token',
|
display: 'Auth token',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'fromNumber',
|
code: 'fromNumber',
|
||||||
display: 'Twilio Number (international format)',
|
display: 'Twilio number (international format)',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true
|
face: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'toNumber',
|
code: 'toNumber',
|
||||||
display: 'Notifications Number (international format)',
|
display: 'Notifications number (international format)',
|
||||||
component: TextInputFormik,
|
component: TextInputFormik,
|
||||||
face: true
|
face: true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ const SessionManagement = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TitleSection title="Session Management" />
|
<TitleSection title="Session management" />
|
||||||
<DataTable
|
<DataTable
|
||||||
loading={loading}
|
loading={loading}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const CopyToClipboard = ({
|
||||||
buttonClassname,
|
buttonClassname,
|
||||||
children,
|
children,
|
||||||
wrapperClassname,
|
wrapperClassname,
|
||||||
|
removeSpace = true,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
|
@ -46,7 +47,8 @@ const CopyToClipboard = ({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className={classnames(classes.buttonWrapper, buttonClassname)}>
|
<div className={classnames(classes.buttonWrapper, buttonClassname)}>
|
||||||
<ReactCopyToClipboard text={R.replace(/\s/g, '')(children)}>
|
<ReactCopyToClipboard
|
||||||
|
text={removeSpace ? R.replace(/\s/g, '')(children) : children}>
|
||||||
<button
|
<button
|
||||||
aria-describedby={id}
|
aria-describedby={id}
|
||||||
onClick={event => handleClick(event)}>
|
onClick={event => handleClick(event)}>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import * as R from 'ramda'
|
||||||
import React, { memo, useState } from 'react'
|
import React, { memo, useState } from 'react'
|
||||||
|
|
||||||
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { IDButton, ActionButton } from 'src/components/buttons'
|
import { IDButton, ActionButton } from 'src/components/buttons'
|
||||||
import { P, Label1 } from 'src/components/typography'
|
import { P, Label1 } from 'src/components/typography'
|
||||||
import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg'
|
import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg'
|
||||||
|
|
@ -131,11 +131,12 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const commission = BigNumber(tx.profit).toFixed(2, 1) // ROUND_DOWN
|
const commission = BigNumber(tx.profit).toFixed(2, 1) // ROUND_DOWN
|
||||||
const commissionPercentage =
|
const commissionPercentage = BigNumber(
|
||||||
Number.parseFloat(tx.commissionPercentage, 2) * 100
|
Number.parseFloat(tx.commissionPercentage, 2) * 100
|
||||||
const cashInFee = isCashIn ? Number.parseFloat(tx.cashInFee) : 0
|
).toFixed(2, 1) // ROUND_DOWN
|
||||||
|
const fixedFee = Number.parseFloat(tx.fixedFee) || 0
|
||||||
const fiat = BigNumber(tx.fiat)
|
const fiat = BigNumber(tx.fiat)
|
||||||
.minus(cashInFee)
|
.minus(fixedFee)
|
||||||
.toFixed(2, 1) // ROUND_DOWN
|
.toFixed(2, 1) // ROUND_DOWN
|
||||||
const crypto = getCryptoAmount(tx)
|
const crypto = getCryptoAmount(tx)
|
||||||
const cryptoFee = tx.fee ? `${getCryptoFeeAmount(tx)} ${tx.fiatCode}` : 'N/A'
|
const cryptoFee = tx.fee ? `${getCryptoFeeAmount(tx)} ${tx.fiatCode}` : 'N/A'
|
||||||
|
|
@ -191,6 +192,13 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
<>
|
<>
|
||||||
<Label>Transaction status</Label>
|
<Label>Transaction status</Label>
|
||||||
<span className={classes.bold}>{getStatus(tx)}</span>
|
<span className={classes.bold}>{getStatus(tx)}</span>
|
||||||
|
{getStatusDetails(tx) ? (
|
||||||
|
<CopyToClipboard removeSpace={false} className={classes.errorCopy}>
|
||||||
|
{getStatusDetails(tx)}
|
||||||
|
</CopyToClipboard>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -350,7 +358,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Fixed fee</Label>
|
<Label>Fixed fee</Label>
|
||||||
<div>{isCashIn ? `${cashInFee} ${tx.fiatCode}` : 'N/A'}</div>
|
<div>{`${fixedFee} ${tx.fiatCode}`}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.secondRow}>
|
<div className={classes.secondRow}>
|
||||||
|
|
@ -358,9 +366,9 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
<div className={classes.addressHeader}>
|
<div className={classes.addressHeader}>
|
||||||
<Label>Address</Label>
|
<Label>Address</Label>
|
||||||
{!R.isNil(tx.walletScore) && (
|
{!R.isNil(tx.walletScore) && (
|
||||||
<HoverableTooltip parentElements={walletScoreEl}>
|
<HelpTooltip parentElements={walletScoreEl}>
|
||||||
{`Chain analysis score: ${tx.walletScore}/10`}
|
{`Chain analysis score: ${tx.walletScore}/10`}
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -392,13 +400,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.lastRow}>
|
<div className={classes.lastRow}>
|
||||||
<div className={classes.status}>
|
<div className={classes.status}>
|
||||||
{getStatusDetails(tx) ? (
|
{errorElements}
|
||||||
<HoverableTooltip parentElements={errorElements} width={200}>
|
|
||||||
<P>{getStatusDetails(tx)}</P>
|
|
||||||
</HoverableTooltip>
|
|
||||||
) : (
|
|
||||||
errorElements
|
|
||||||
)}
|
|
||||||
{((tx.txClass === 'cashOut' && getStatus(tx) === 'Pending') ||
|
{((tx.txClass === 'cashOut' && getStatus(tx) === 'Pending') ||
|
||||||
(tx.txClass === 'cashIn' && getStatus(tx) === 'Batched')) && (
|
(tx.txClass === 'cashIn' && getStatus(tx) === 'Batched')) && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import typographyStyles from 'src/components/typography/styles'
|
import typographyStyles from 'src/components/typography/styles'
|
||||||
import { offColor, comet, white, tomato } from 'src/styling/variables'
|
import { offColor, comet, white, tomato } from 'src/styling/variables'
|
||||||
|
|
||||||
const { p } = typographyStyles
|
const { p, label3 } = typographyStyles
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
wrapper: {
|
wrapper: {
|
||||||
|
|
@ -137,5 +137,10 @@ export default {
|
||||||
},
|
},
|
||||||
swept: {
|
swept: {
|
||||||
width: 250
|
width: 250
|
||||||
|
},
|
||||||
|
errorCopy: {
|
||||||
|
extend: label3,
|
||||||
|
lineBreak: 'normal',
|
||||||
|
maxWidth: 180
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||||
import SearchBox from 'src/components/SearchBox'
|
import SearchBox from 'src/components/SearchBox'
|
||||||
import SearchFilter from 'src/components/SearchFilter'
|
import SearchFilter from 'src/components/SearchFilter'
|
||||||
import Title from 'src/components/Title'
|
import Title from 'src/components/Title'
|
||||||
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { SupportLinkButton } from 'src/components/buttons'
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
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'
|
||||||
|
|
@ -105,7 +107,7 @@ const GET_TRANSACTIONS = gql`
|
||||||
deviceId
|
deviceId
|
||||||
fiat
|
fiat
|
||||||
fee
|
fee
|
||||||
cashInFee
|
fixedFee
|
||||||
fiatCode
|
fiatCode
|
||||||
cryptoAtoms
|
cryptoAtoms
|
||||||
cryptoCode
|
cryptoCode
|
||||||
|
|
@ -233,7 +235,22 @@ const Transactions = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
view: it => getStatus(it),
|
view: it => {
|
||||||
|
if (getStatus(it) === 'Pending')
|
||||||
|
return (
|
||||||
|
<div className={classes.pendingBox}>
|
||||||
|
{'Pending'}
|
||||||
|
<HelpTooltip width={285}>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/115001210452-Cancelling-cash-out-transactions"
|
||||||
|
label="Cancelling cash-out transactions"
|
||||||
|
bottomSpace="0"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
else return getStatus(it)
|
||||||
|
},
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
width: 80
|
width: 80
|
||||||
|
|
@ -323,7 +340,7 @@ const Transactions = () => {
|
||||||
loading={filtersLoading}
|
loading={filtersLoading}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
options={filterOptions}
|
options={filterOptions}
|
||||||
inputPlaceholder={'Search Transactions'}
|
inputPlaceholder={'Search transactions'}
|
||||||
onChange={onFilterChange}
|
onChange={onFilterChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { Link, SupportLinkButton } from 'src/components/buttons'
|
import { Link, SupportLinkButton } from 'src/components/buttons'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
|
@ -187,13 +187,13 @@ const Triggers = () => {
|
||||||
<Label2 className={classes.switchLabel}>
|
<Label2 className={classes.switchLabel}>
|
||||||
{rejectAddressReuse ? 'On' : 'Off'}
|
{rejectAddressReuse ? 'On' : 'Off'}
|
||||||
</Label2>
|
</Label2>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
This option requires a user to scan a different cryptocurrency
|
This option requires a user to scan a different cryptocurrency
|
||||||
address if they attempt to scan one that had been previously
|
address if they attempt to scan one that had been previously
|
||||||
used for a transaction in your network
|
used for a transaction in your network
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ const Users = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TitleSection title="User Management" />
|
<TitleSection title="User management" />
|
||||||
<Box
|
<Box
|
||||||
marginBottom={3}
|
marginBottom={3}
|
||||||
marginTop={-5}
|
marginTop={-5}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
|
import { SupportLinkButton } from 'src/components/buttons'
|
||||||
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import FormRenderer from 'src/pages/Services/FormRenderer'
|
import FormRenderer from 'src/pages/Services/FormRenderer'
|
||||||
|
|
@ -13,6 +15,8 @@ import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle
|
||||||
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
||||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||||
|
|
||||||
|
import { P } from '../../components/typography'
|
||||||
|
|
||||||
import AdvancedWallet from './AdvancedWallet'
|
import AdvancedWallet from './AdvancedWallet'
|
||||||
import styles from './Wallet.styles.js'
|
import styles from './Wallet.styles.js'
|
||||||
import Wizard from './Wizard'
|
import Wizard from './Wizard'
|
||||||
|
|
@ -115,7 +119,7 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
||||||
<>
|
<>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
<TitleSection
|
<TitleSection
|
||||||
title="Wallet Settings"
|
title="Wallet settings"
|
||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
text: 'Advanced settings',
|
text: 'Advanced settings',
|
||||||
|
|
@ -124,6 +128,19 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
||||||
toggle: setAdvancedSettings
|
toggle: setAdvancedSettings
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
appendix={
|
||||||
|
<HelpTooltip width={340}>
|
||||||
|
<P>
|
||||||
|
For details on configuring wallets, please read the relevant
|
||||||
|
knowledgebase article:
|
||||||
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360000725832-Wallets-Exchange-Linkage-and-Volatility"
|
||||||
|
label="Wallets, Exchange Linkage, and Volatility"
|
||||||
|
bottomSpace="1"
|
||||||
|
/>
|
||||||
|
</HelpTooltip>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!advancedSettings && (
|
{!advancedSettings && (
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ const getAdvancedWalletElements = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'allowTransactionBatching',
|
name: 'allowTransactionBatching',
|
||||||
header: `Allow BTC Transaction Batching`,
|
header: `Allow BTC transaction batching`,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
width: 260,
|
width: 260,
|
||||||
|
|
@ -119,7 +119,7 @@ const getAdvancedWalletElements = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'feeMultiplier',
|
name: 'feeMultiplier',
|
||||||
header: `BTC Miner's Fee`,
|
header: `BTC miner's fee`,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
width: 250,
|
width: 250,
|
||||||
|
|
@ -179,7 +179,7 @@ const getAdvancedWalletElementsOverrides = (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'feeMultiplier',
|
name: 'feeMultiplier',
|
||||||
header: `Miner's Fee`,
|
header: `Miner's fee`,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
width: 250,
|
width: 250,
|
||||||
|
|
@ -280,7 +280,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'zeroConf',
|
name: 'zeroConf',
|
||||||
header: 'Confidence Checking',
|
header: 'Confidence checking',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
view: (it, row) => {
|
view: (it, row) => {
|
||||||
|
|
@ -304,7 +304,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'zeroConfLimit',
|
name: 'zeroConfLimit',
|
||||||
header: '0-conf Limit',
|
header: '0-conf limit',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
view: (it, row) =>
|
view: (it, row) =>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import gql from 'graphql-tag'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import InfoMessage from 'src/components/InfoMessage'
|
import InfoMessage from 'src/components/InfoMessage'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import { Button, SupportLinkButton } from 'src/components/buttons'
|
import { Button, SupportLinkButton } from 'src/components/buttons'
|
||||||
import { RadioGroup } from 'src/components/inputs'
|
import { RadioGroup } from 'src/components/inputs'
|
||||||
import { H1, H4, P } from 'src/components/typography'
|
import { H1, H4, P } from 'src/components/typography'
|
||||||
|
|
@ -102,7 +102,7 @@ function Twilio({ doContinue }) {
|
||||||
<H4 noMargin className={classnames(titleClasses)}>
|
<H4 noMargin className={classnames(titleClasses)}>
|
||||||
Will you setup a two way machine or compliance?
|
Will you setup a two way machine or compliance?
|
||||||
</H4>
|
</H4>
|
||||||
<HoverableTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
Two-way machines allow your customers not only to buy (cash-in)
|
Two-way machines allow your customers not only to buy (cash-in)
|
||||||
but also sell cryptocurrencies (cash-out).
|
but also sell cryptocurrencies (cash-out).
|
||||||
|
|
@ -111,7 +111,7 @@ function Twilio({ doContinue }) {
|
||||||
You’ll need an SMS service for cash-out transactions and for any
|
You’ll need an SMS service for cash-out transactions and for any
|
||||||
compliance triggers
|
compliance triggers
|
||||||
</P>
|
</P>
|
||||||
</HoverableTooltip>
|
</HelpTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue