Chore: make notification center UI

Chore: fiatBalancesNotify refactor

Chore: removed now-unused code in some files

Feat: change column "detail" in database to use jsonb

Chore: add notification center background and button

Chore: notifications screen scaffolding

Fix: change position of notification UI

Feat: join backend and frontend

Feat: notification icons and machine names

Feat: add clear all button, stripe overlay on invalid notification

Fix: rework notification styles

Feat: use popper to render notifications

Feat: make notification center UI

Fix: fix css on notification center

Fix: fix invalidateNotification

Chore: apply PR requested changes

Fix: PR fixes

Fix: make toggleable body/root styles be handled by react

Chore: delete old notifier file

Fix: undo variable name changes for cryptobalance notifs
This commit is contained in:
Cesar 2020-12-17 22:18:35 +00:00 committed by Josh Harvey
parent 2a9e8dadba
commit c457faab40
37 changed files with 1337 additions and 1332 deletions

View file

@ -1,4 +1,5 @@
const db = require('./db')
const notifier = require('./notifier')
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
const getBlacklist = () => {
@ -13,11 +14,9 @@ const getBlacklist = () => {
// Delete row from blacklist table by crypto code and address
const deleteFromBlacklist = (cryptoCode, address) => {
const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2;
UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND detail IN ($3^)`
const detail = `'${cryptoCode}_BLOCKED_${address}', '${cryptoCode}_REUSED_${address}'`
return db.none(sql, [cryptoCode, address, detail])
const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2`
notifier.clearBlacklistNotification(cryptoCode, address)
return db.none(sql, [cryptoCode, address])
}
const insertIntoBlacklist = (cryptoCode, address) => {

View file

@ -8,7 +8,7 @@ const plugins = require('../plugins')
const logger = require('../logger')
const settingsLoader = require('../new-settings-loader')
const configManager = require('../new-config-manager')
const notifier = require("../notifier/index")
const notifier = require('../notifier')
const cashInAtomic = require('./cash-in-atomic')
const cashInLow = require('./cash-in-low')
@ -29,10 +29,10 @@ function post (machineTx, pi) {
.then(([{ config }, blacklistItems]) => {
const rejectAddressReuseActive = configManager.getCompliance(config).rejectAddressReuse
if (_.some(it => it.created_by_operator === true)(blacklistItems)) {
if (_.some(it => it.created_by_operator)(blacklistItems)) {
blacklisted = true
notifier.blacklistNotify(r.tx, false)
} else if (_.some(it => it.created_by_operator === false)(blacklistItems) && rejectAddressReuseActive) {
} else if (_.some(it => !it.created_by_operator)(blacklistItems) && rejectAddressReuseActive) {
notifier.blacklistNotify(r.tx, true)
addressReuse = true
}

View file

@ -9,8 +9,6 @@ const helper = require('./cash-out-helper')
const cashOutActions = require('./cash-out-actions')
const cashOutLow = require('./cash-out-low')
const notifier = require("../notifier/index")
const toObj = helper.toObj
module.exports = {atomic}
@ -124,10 +122,8 @@ function updateCassettes (t, tx) {
tx.deviceId
]
return t.one(sql, values).then(r => {
notifier.cashCassettesNotify(r, tx.deviceId)
return socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId}))
})
return t.one(sql, values)
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
}
function wasJustAuthorized (oldTx, newTx, isZeroConf) {

View file

@ -15,7 +15,8 @@ const complianceOverrides = require('./compliance_overrides')
const users = require('./users')
const options = require('./options')
const writeFile = util.promisify(fs.writeFile)
const notifierQueries = require('./notifier/queries')
const notifierUtils = require('./notifier/utils')
const NUM_RESULTS = 1000
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
const frontCameraBaseDir = _.get('frontCameraDir', options)
@ -115,7 +116,7 @@ async function updateCustomer (id, data, userToken) {
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
' where id=$1'
invalidateCustomerNotifications(id, formattedData)
invalidateCustomerNotifications(id, formattedData).catch(console.error)
await db.none(sql, [id])
@ -123,12 +124,10 @@ async function updateCustomer (id, data, userToken) {
}
const invalidateCustomerNotifications = (id, data) => {
let detail = '';
if(data.authorized_override === 'verified') {
detail = `BLOCKED_${id}`
}
const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND detail = $1`
return db.none(sql, [detail])
if (data.authorized_override !== 'verified') return Promise.resolve()
const detailB = notifierUtils.buildDetail({ code: 'BLOCKED', customerId: id })
return notifierQueries.invalidateNotification(detailB, 'compliance').catch(console.error)
}
/**

View file

@ -1,18 +1,17 @@
const _ = require('lodash/fp')
const axios = require('axios')
const logger = require('./logger')
const db = require('./db')
const pairing = require('./pairing')
const notifier = require('./notifier')
const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader')
module.exports = {getMachineName, getMachines, getMachine, getMachineNames, setMachine}
const notifierUtils = require('./notifier/utils')
const notifierQueries = require('./notifier/queries')
function getMachines () {
return db.any('select * from devices where display=TRUE order by created')
return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
.then(rr => rr.map(r => ({
deviceId: r.device_id,
cashbox: r.cashbox,
@ -83,31 +82,29 @@ function getMachineNames (config) {
* @returns {string} machine name
*/
function getMachineName (machineId) {
const sql = 'select * from devices where device_id=$1'
const sql = 'SELECT * FROM devices WHERE device_id=$1'
return db.oneOrNone(sql, [machineId])
.then(it => it.name)
}
function getMachine (machineId) {
const sql = 'select * from devices where device_id=$1'
const sql = 'SELECT * FROM devices WHERE device_id=$1'
return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res))
}
function renameMachine (rec) {
const sql = 'update devices set name=$1 where device_id=$2'
const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
return db.none(sql, [rec.newName, rec.deviceId])
}
function resetCashOutBills (rec) {
const sql = `
update devices set cassette1=$1, cassette2=$2 where device_id=$3;
update notifications set read = 't', valid = 'f' where read = 'f' AND valid = 't' AND device_id = $3 AND type = 'fiatBalance';
`
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2 WHERE device_id=$3;`
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
}
function emptyCashInBills (rec) {
const sql = 'update devices set cashbox=0 where device_id=$1'
const sql = 'UPDATE devices SET cashbox=0 WHERE device_id=$1'
return db.none(sql, [rec.deviceId])
}
@ -145,3 +142,5 @@ function setMachine (rec) {
default: throw new Error('No such action: ' + rec.action)
}
}
module.exports = { getMachineName, getMachines, getMachine, getMachineNames, setMachine }

View file

@ -12,8 +12,9 @@ const logs = require('../../logs')
const settingsLoader = require('../../new-settings-loader')
// const tokenManager = require('../../token-manager')
const blacklist = require('../../blacklist')
const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch
const machineEventsByIdBatch = require('../../postgresql_interface').machineEventsByIdBatch
const promoCodeManager = require('../../promo-codes')
const notifierQueries = require('../../notifier/queries')
const serverVersion = require('../../../package.json').version
const transactions = require('../transactions')
@ -247,6 +248,17 @@ const typeDefs = gql`
rate: Float
}
type Notification {
id: ID!
deviceName: String
type: String
detail: JSON
message: String
created: Date
read: Boolean
valid: Boolean
}
type Query {
countries: [Country]
currencies: [Currency]
@ -279,6 +291,8 @@ const typeDefs = gql`
promoCodes: [PromoCode]
cryptoRates: JSONObject
fiatRates: [Rate]
notifications: [Notification]
hasUnreadNotifications: Boolean
}
type SupportLogsResponse {
@ -312,6 +326,8 @@ const typeDefs = gql`
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
createPromoCode(code: String!, discount: Int!): PromoCode
deletePromoCode(codeId: ID!): PromoCode
clearNotification(id: ID!): Notification
clearAllNotifications: Notification
}
`
@ -373,7 +389,9 @@ const resolvers = {
}
})
}),
fiatRates: () => forex.getFiatRates()
fiatRates: () => forex.getFiatRates(),
notifications: () => notifierQueries.getNotifications(),
hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications()
},
Mutation: {
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }),
@ -397,7 +415,9 @@ const resolvers = {
blacklist.insertIntoBlacklist(cryptoCode, address),
// revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
createPromoCode: (...[, { code, discount }]) => promoCodeManager.createPromoCode(code, discount),
deletePromoCode: (...[, { codeId }]) => promoCodeManager.deletePromoCode(codeId)
deletePromoCode: (...[, { codeId }]) => promoCodeManager.deletePromoCode(codeId),
clearNotification: (...[, { id }]) => notifierQueries.markAsRead(id),
clearAllNotifications: () => notifierQueries.markAllAsRead()
}
}

View file

@ -44,18 +44,6 @@ const configSql = 'insert into user_config (type, data, valid, schema_version) v
function saveConfig (config) {
return loadLatestConfigOrNone()
.then(currentConfig => {
if(config.notifications_cryptoHighBalance || config.notifications_cryptoLowBalance) {
clearCryptoBalanceNotifications(currentConfig, config, false)
}
if(config.notifications_cryptoBalanceOverrides) {
clearCryptoBalanceNotifications(currentConfig.notifications_cryptoBalanceOverrides, config.notifications_cryptoBalanceOverrides, true)
}
if(config.notifications_fiatBalanceCassette1 || config.notifications_fiatBalanceCassette2) {
clearCassetteNotifications(currentConfig, config, false)
}
if(config.notifications_fiatBalanceOverrides) {
clearCassetteNotifications(currentConfig.notifications_fiatBalanceOverrides, config.notifications_fiatBalanceOverrides, true)
}
const newConfig = _.assign(currentConfig, config)
return db.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
})

View file

@ -1,351 +0,0 @@
const crypto = require('crypto')
const _ = require('lodash/fp')
const prettyMs = require('pretty-ms')
const numeral = require('numeral')
const dbm = require('./postgresql_interface')
const db = require('./db')
const T = require('./time')
const logger = require('./logger')
const STALE_STATE = 7 * T.minute
const NETWORK_DOWN_TIME = 1 * T.minute
const ALERT_SEND_INTERVAL = T.hour
const PING = 'PING'
const STALE = 'STALE'
const LOW_CRYPTO_BALANCE = 'LOW_CRYPTO_BALANCE'
const HIGH_CRYPTO_BALANCE = 'HIGH_CRYPTO_BALANCE'
const CASH_BOX_FULL = 'CASH_BOX_FULL'
const LOW_CASH_OUT = 'LOW_CASH_OUT'
const CODES_DISPLAY = {
PING: 'Machine Down',
STALE: 'Machine Stuck',
LOW_CRYPTO_BALANCE: 'Low Crypto Balance',
HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
CASH_BOX_FULL: 'Cash box full',
LOW_CASH_OUT: 'Low Cash-out'
}
let alertFingerprint
let lastAlertTime
function codeDisplay (code) {
return CODES_DISPLAY[code]
}
function jsonParse (event) {
return _.set('note', JSON.parse(event.note), event)
}
function sameState (a, b) {
return a.note.txId === b.note.txId && a.note.state === b.note.state
}
function sendNoAlerts (plugins, smsEnabled, emailEnabled) {
const subject = '[Lamassu] All clear'
let rec = {}
if (smsEnabled) {
rec = _.set(['sms', 'body'])(subject)(rec)
}
if (emailEnabled) {
rec = _.set(['email', 'subject'])(subject)(rec)
rec = _.set(['email', 'body'])('No errors are reported for your machines.')(rec)
}
return plugins.sendMessage(rec)
}
function checkNotification (plugins) {
const notifications = plugins.getNotificationConfig()
const isActive = it => it.active && (it.balance || it.errors)
const smsEnabled = isActive(notifications.sms)
const emailEnabled = isActive(notifications.email)
if (!smsEnabled && !emailEnabled) return Promise.resolve()
return checkStatus(plugins)
.then(alertRec => {
const currentAlertFingerprint = buildAlertFingerprint(alertRec, notifications)
if (!currentAlertFingerprint) {
const inAlert = !!alertFingerprint
alertFingerprint = null
lastAlertTime = null
if (inAlert) return sendNoAlerts(plugins, smsEnabled, emailEnabled)
}
const alertChanged = currentAlertFingerprint === alertFingerprint &&
lastAlertTime - Date.now() < ALERT_SEND_INTERVAL
if (alertChanged) return
let rec = {}
if (smsEnabled) {
rec = _.set(['sms', 'body'])(printSmsAlerts(alertRec, notifications.sms))(rec)
}
if (emailEnabled) {
rec = _.set(['email', 'subject'])(alertSubject(alertRec, notifications.email))(rec)
rec = _.set(['email', 'body'])(printEmailAlerts(alertRec, notifications.email))(rec)
}
alertFingerprint = currentAlertFingerprint
lastAlertTime = Date.now()
return plugins.sendMessage(rec)
})
.then(results => {
if (results && results.length > 0) logger.debug('Successfully sent alerts')
})
.catch(logger.error)
}
const getDeviceTime = _.flow(_.get('device_time'), Date.parse)
function dropRepeatsWith (comparator, arr) {
const iteratee = (acc, val) => val === acc.last
? acc
: { arr: _.concat(acc.arr, val), last: val }
return _.reduce(iteratee, { arr: [] }, arr).arr
}
function checkStuckScreen (deviceEvents, machineName) {
const sortedEvents = _.sortBy(getDeviceTime, _.map(jsonParse, deviceEvents))
const noRepeatEvents = dropRepeatsWith(sameState, sortedEvents)
const lastEvent = _.last(noRepeatEvents)
if (!lastEvent) {
return []
}
const state = lastEvent.note.state
const isIdle = lastEvent.note.isIdle
if (isIdle) {
return []
}
const age = Math.floor(lastEvent.age)
if (age > STALE_STATE) {
return [{ code: STALE, state, age, machineName }]
}
return []
}
function checkPing (device) {
const sql = `select (EXTRACT(EPOCH FROM (now() - updated))) * 1000 AS age from machine_pings
where device_id=$1`
const deviceId = device.deviceId
return db.oneOrNone(sql, [deviceId])
.then(row => {
if (!row) return [{ code: PING }]
if (row.age > NETWORK_DOWN_TIME) return [{ code: PING, age: row.age, machineName: device.name }]
return []
})
}
function checkPings (devices) {
const deviceIds = _.map('deviceId', devices)
const promises = _.map(checkPing, devices)
return Promise.all(promises)
.then(_.zipObject(deviceIds))
}
function checkStatus (plugins) {
const alerts = { devices: {}, deviceNames: {} }
return Promise.all([plugins.checkBalances(), dbm.machineEvents(), plugins.getMachineNames()])
.then(([balances, events, devices]) => {
return checkPings(devices)
.then(pings => {
alerts.general = _.filter(r => !r.deviceId, balances)
devices.forEach(function (device) {
const deviceId = device.deviceId
const deviceName = device.name
const deviceEvents = events.filter(function (eventRow) {
return eventRow.device_id === deviceId
})
const ping = pings[deviceId] || []
const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {}
alerts.devices[deviceId].balanceAlerts = _.filter(['deviceId', deviceId], balances)
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
alerts.deviceNames[deviceId] = deviceName
})
return alerts
})
})
}
function formatCurrency (num, code) {
return numeral(num).format('0,0.00') + ' ' + code
}
function emailAlert (alert) {
switch (alert.code) {
case PING:
if (alert.age) {
const pingAge = prettyMs(alert.age, { compact: true, verbose: true })
return `Machine down for ${pingAge}`
}
return 'Machine down for a while.'
case STALE:
const stuckAge = prettyMs(alert.age, { compact: true, verbose: true })
return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
case LOW_CRYPTO_BALANCE:
const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
return `Low balance in ${alert.cryptoCode} [${balance}]`
case HIGH_CRYPTO_BALANCE:
const highBalance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
return `High balance in ${alert.cryptoCode} [${highBalance}]`
case CASH_BOX_FULL:
return `Cash box full on ${alert.machineName} [${alert.notes} banknotes]`
case LOW_CASH_OUT:
return `Cassette for ${alert.denomination} ${alert.fiatCode} low [${alert.notes} banknotes]`
}
}
function emailAlerts (alerts) {
return alerts.map(emailAlert).join('\n') + '\n'
}
function printEmailAlerts (alertRec, config) {
let body = 'Errors were reported by your Lamassu Machines.\n'
if (config.balance && alertRec.general.length !== 0) {
body = body + '\nGeneral errors:\n'
body = body + emailAlerts(alertRec.general) + '\n'
}
_.keys(alertRec.devices).forEach(function (device) {
const deviceName = alertRec.deviceNames[device]
body = body + '\nErrors for ' + deviceName + ':\n'
let alerts = []
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
body = body + emailAlerts(alerts)
})
return body
}
function alertSubject (alertRec, config) {
let alerts = []
if (config.balance) {
alerts = _.concat(alerts, alertRec.general)
}
_.keys(alertRec.devices).forEach(function (device) {
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
})
if (alerts.length === 0) return null
const alertTypes = _.map(codeDisplay, _.uniq(_.map('code', alerts))).sort()
return '[Lamassu] Errors reported: ' + alertTypes.join(', ')
}
function printSmsAlerts (alertRec, config) {
let alerts = []
if (config.balance) {
alerts = _.concat(alerts, alertRec.general)
}
_.keys(alertRec.devices).forEach(function (device) {
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
})
if (alerts.length === 0) return null
const alertsMap = _.groupBy('code', alerts)
const alertTypes = _.map(entry => {
const code = entry[0]
const machineNames = _.filter(_.negate(_.isEmpty), _.map('machineName', entry[1]))
return {
codeDisplay: codeDisplay(code),
machineNames
}
}, _.toPairs(alertsMap))
const mapByCodeDisplay = _.map(it => _.isEmpty(it.machineNames)
? it.codeDisplay : `${it.codeDisplay} (${it.machineNames.join(', ')})`
)
const displayAlertTypes = _.compose(_.uniq, mapByCodeDisplay, _.sortBy('codeDisplay'))(alertTypes)
return '[Lamassu] Errors reported: ' + displayAlertTypes.join(', ')
}
function getAlertTypes (alertRec, config) {
let alerts = []
if (!config.active || (!config.balance && !config.errors)) return alerts
if (config.balance) {
alerts = _.concat(alerts, alertRec.general)
}
_.keys(alertRec.devices).forEach(function (device) {
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
})
return alerts
}
function buildAlertFingerprint (alertRec, notifications) {
const sms = getAlertTypes(alertRec, notifications.sms)
const email = getAlertTypes(alertRec, notifications.email)
if (sms.length === 0 && email.length === 0) return null
const smsTypes = _.map(codeDisplay, _.uniq(_.map('code', sms))).sort()
const emailTypes = _.map(codeDisplay, _.uniq(_.map('code', email))).sort()
const subject = _.concat(smsTypes, emailTypes).join(', ')
return crypto.createHash('sha256').update(subject).digest('hex')
}
module.exports = {
checkNotification,
checkPings,
checkStuckScreen
}

View file

@ -20,6 +20,14 @@ const NETWORK_DOWN_TIME = 1 * T.minute
const STALE_STATE = 7 * T.minute
const ALERT_SEND_INTERVAL = T.hour
const NOTIFICATION_TYPES = {
HIGH_VALUE_TX: 'highValueTransaction',
FIAT_BALANCE: 'fiatBalance',
CRYPTO_BALANCE: 'cryptoBalance',
COMPLIANCE: 'compliance',
ERROR: 'error'
}
module.exports = {
PING,
STALE,
@ -30,5 +38,6 @@ module.exports = {
CODES_DISPLAY,
NETWORK_DOWN_TIME,
STALE_STATE,
ALERT_SEND_INTERVAL
ALERT_SEND_INTERVAL,
NOTIFICATION_TYPES
}

View file

@ -1,5 +1,5 @@
const _ = require('lodash/fp')
const utils = require("./utils")
const utils = require('./utils')
const email = require('../email')
@ -19,19 +19,13 @@ function alertSubject(alertRec, config) {
alerts = _.concat(alerts, alertRec.general)
}
_.keys(alertRec.devices).forEach(function (device) {
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
})
_.forEach(device => {
alerts = _.concat(alerts, utils.deviceAlerts(config, alertRec, device))
}, _.keys(alertRec.devices))
if (alerts.length === 0) return null
const alertTypes = _.map(codeDisplay, _.uniq(_.map('code', alerts))).sort()
const alertTypes = _.flow(_.map('code'), _.uniq, _.map(utils.codeDisplay), _.sortBy(o => o))(alerts)
return '[Lamassu] Errors reported: ' + alertTypes.join(', ')
}
@ -39,31 +33,23 @@ function printEmailAlerts(alertRec, config) {
let body = 'Errors were reported by your Lamassu Machines.\n'
if (config.balance && alertRec.general.length !== 0) {
body = body + '\nGeneral errors:\n'
body = body + emailAlerts(alertRec.general) + '\n'
body += '\nGeneral errors:\n'
body += emailAlerts(alertRec.general) + '\n'
}
_.keys(alertRec.devices).forEach(function (device) {
_.forEach(device => {
const deviceName = alertRec.deviceNames[device]
body = body + '\nErrors for ' + deviceName + ':\n'
body += '\nErrors for ' + deviceName + ':\n'
let alerts = []
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
body = body + emailAlerts(alerts)
})
const alerts = utils.deviceAlerts(config, alertRec, device)
body += emailAlerts(alerts)
}, _.keys(alertRec.devices))
return body
}
function emailAlerts (alerts) {
return alerts.map(emailAlert).join('\n') + '\n'
return _.join('\n', _.map(emailAlert, alerts)) + '\n'
}
function emailAlert (alert) {
@ -98,5 +84,4 @@ function emailAlert(alert) {
const sendMessage = email.sendMessage
module.exports = { alertSubject, printEmailAlerts, sendMessage }

View file

@ -11,6 +11,13 @@ const utils = require('./utils')
const emailFuncs = require('./email')
const smsFuncs = require('./sms')
const { STALE, STALE_STATE, PING } = require('./codes')
const { NOTIFICATION_TYPES: {
HIGH_VALUE_TX,
FIAT_BALANCE,
CRYPTO_BALANCE,
COMPLIANCE,
ERROR }
} = require('./codes')
function buildMessage (alerts, notifications) {
const smsEnabled = utils.isActive(notifications.sms)
@ -50,23 +57,20 @@ function checkNotification(plugins) {
)
if (!currentAlertFingerprint) {
const inAlert = !!utils.getAlertFingerprint()
// (fingerprint = null, lastAlertTime = null)
// variables for setAlertFingerprint: (fingerprint = null, lastAlertTime = null)
utils.setAlertFingerprint(null, null)
if (inAlert) {
return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
}
}
if (utils.shouldNotAlert(currentAlertFingerprint)) {
return
if (inAlert) return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
}
if (utils.shouldNotAlert(currentAlertFingerprint)) return
const message = buildMessage(alerts, notifications)
utils.setAlertFingerprint(currentAlertFingerprint, Date.now())
return plugins.sendMessage(message)
})
.then(results => {
if (results && results.length > 0)
if (results && results.length > 0) {
logger.debug('Successfully sent alerts')
}
})
.catch(logger.error)
}
@ -85,7 +89,7 @@ function getAlerts(plugins) {
function buildAlerts (pings, balances, events, devices) {
const alerts = { devices: {}, deviceNames: {} }
alerts.general = _.filter(r => !r.deviceId, balances)
devices.forEach(function (device) {
_.forEach(device => {
const deviceId = device.deviceId
const deviceName = device.name
const deviceEvents = events.filter(function (eventRow) {
@ -94,15 +98,15 @@ function buildAlerts(pings, balances, events, devices) {
const ping = pings[deviceId] || []
const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {}
alerts.devices[deviceId].balanceAlerts = _.filter(
alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter(
['deviceId', deviceId],
balances
)
), alerts.devices)
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
alerts.deviceNames[deviceId] = deviceName
})
}, devices)
return alerts
}
@ -119,34 +123,30 @@ function checkStuckScreen(deviceEvents, machineName) {
)
const lastEvent = _.last(sortedEvents)
if (!lastEvent) {
return []
}
if (!lastEvent) return []
const state = lastEvent.note.state
const isIdle = lastEvent.note.isIdle
if (isIdle) {
return []
}
if (isIdle) return []
const age = Math.floor(lastEvent.age)
if (age > STALE_STATE) {
return [{ code: STALE, state, age, machineName }]
}
if (age > STALE_STATE) return [{ code: STALE, state, age, machineName }]
return []
}
async function transactionNotify (tx, rec) {
const settings = await settingsLoader.loadLatest()
function transactionNotify (tx, rec) {
return settingsLoader.loadLatest().then(settings => {
const notifSettings = configManager.getGlobalNotifications(settings.config)
const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity)
const isCashOut = tx.direction === 'cashOut'
// high value tx on database
if(highValueTx && tx.direction === 'cashIn' || highValueTx && tx.direction === 'cashOut' && rec.isRedemption) {
queries.addHighValueTx(tx)
if (highValueTx && (tx.direction === 'cashIn' || (tx.direction === 'cashOut' && rec.isRedemption))) {
const direction = tx.direction === 'cashOut' ? 'cash-out' : 'cash-in'
const message = `${tx.fiat} ${tx.fiatCode} ${direction} transaction`
const detailB = utils.buildDetail({ deviceId: tx.deviceId, direction, fiat: tx.fiat, fiatCode: tx.fiatCode, cryptoAddress: tx.toAddress })
queries.addNotification(HIGH_VALUE_TX, message, detailB)
}
// alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled
@ -163,11 +163,10 @@ async function transactionNotify (tx, rec) {
return Promise.all([
machineLoader.getMachineName(tx.deviceId),
customerPromise
])
.then(([machineName, customer]) => {
]).then(([machineName, customer]) => {
return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer)
}).then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx))
})
.then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx))
}
function sendRedemptionMessage (txId, error) {
@ -188,11 +187,11 @@ function sendRedemptionMessage(txId, error) {
return sendTransactionMessage(rec)
}
async function sendTransactionMessage(rec, isHighValueTx) {
const settings = await settingsLoader.loadLatest()
function sendTransactionMessage (rec, isHighValueTx) {
return settingsLoader.loadLatest().then(settings => {
const notifications = configManager.getGlobalNotifications(settings.config)
let promises = []
const promises = []
const emailActive =
notifications.email.active &&
@ -205,201 +204,156 @@ async function sendTransactionMessage(rec, isHighValueTx) {
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
return Promise.all(promises)
}
const cashCassettesNotify = (cassettes, deviceId) => {
settingsLoader.loadLatest()
.then(settings =>
[
configManager.getNotifications(null, deviceId, settings.config),
configManager.getCashOut(deviceId,settings.config).active
])
.then(([notifications, cashOutEnabled]) => {
const cassette1Count = cassettes.cassette1
const cassette2Count = cassettes.cassette2
const cassette1Threshold = notifications.fiatBalanceCassette1
const cassette2Threshold = notifications.fiatBalanceCassette2
if(cashOutEnabled) {
// we only want to add this notification if there isn't one already set and unread in the database
Promise.all([queries.getUnreadCassetteNotifications(1, deviceId), queries.getUnreadCassetteNotifications(2, deviceId)]).then(res => {
if(res[0].length === 0 && cassette1Count < cassette1Threshold) {
console.log("Adding fiatBalance alert for cashbox 1 in database - count & threshold: ", cassette1Count, cassette1Threshold )
return queries.addCashCassetteWarning(1, deviceId)
}
if(res[1].length === 0 && cassette2Count < cassette2Threshold) {
console.log("Adding fiatBalance alert for cashbox 2 in database - count & threshold: ", cassette2Count, cassette2Threshold )
return queries.addCashCassetteWarning(2, deviceId)
}
})
}
})
}
const clearOldCryptoNotifications = balances => {
return queries.getAllValidNotifications(CRYPTO_BALANCE).then(res => {
const filterByBalance = _.filter(notification => {
const { cryptoCode, code } = notification.detail
return !_.find(balance => balance.cryptoCode === cryptoCode && balance.code === code)(balances)
})
const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(res)
/*
Notes for new "valid" column on notifications table:
- We only want to add a new notification if it is present in the high or low warning consts.
- Before we add the notification we need to see if there is no "valid" notification in the database.
- Since the poller runs every few seconds, if the user marks it as read, this code would add a new notification
immediately. This new column helps us decide if a new notification should be added.
- "Valid" is defaulted to "true". When the cryptobalance goes over the low threshold or under the high threshold,
the notification will be marked as invalid. This will allow a new one to be sent. If the cryptobalance never goes
into the middle of the high and low thresholds, the old, "read" notification will still be relevant so we won't add
a new one.
*/
const clearOldCryptoNotifications = (balances) => {
// get valid crypto notifications from DB
// if that notification doesn't exist in balances, then make it invalid on the DB
queries.getAllValidNotifications('cryptoBalance').then(res => {
const notifications = _.map(it => {
return {
cryptoCode: it.detail.split('_')[0],
code: it.detail.split('_').splice(1).join('_')
}
const notInvalidated = _.filter(notification => {
return !_.find(id => notification.id === id)(indexesToInvalidate)
}, res)
_.forEach(notification => {
const idx = _.findIndex(balance => {
return balance.code === notification.code && balance.cryptoCode === notification.cryptoCode
}, balances)
if(idx !== -1) {
return
return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated)
})
}
// if the notification doesn't exist in the new balances object, then it is outdated and is not valid anymore
return queries.invalidateNotification(notification.id)
const cryptoBalancesNotify = (cryptoWarnings) => {
return clearOldCryptoNotifications(cryptoWarnings).then(notInvalidated => {
return cryptoWarnings.forEach(balance => {
// if notification exists in DB and wasnt invalidated then don't add a duplicate
if (_.find(o => {
const { code, cryptoCode } = o.detail
return code === balance.code && cryptoCode === balance.cryptoCode
}, notInvalidated)) return
const fiat = utils.formatCurrency(balance.fiatBalance.balance, balance.fiatCode)
const message = `${balance.code === 'HIGH_CRYPTO_BALANCE' ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]`
const detailB = utils.buildDetail({ cryptoCode: balance.cryptoCode, code: balance.code })
return queries.addNotification(CRYPTO_BALANCE, message, detailB)
})
})
}
const clearOldFiatNotifications = (balances) => {
return queries.getAllValidNotifications(FIAT_BALANCE).then(notifications => {
const filterByBalance = _.filter(notification => {
const { cassette, deviceId } = notification.detail
return !_.find(balance => balance.cassette === cassette && balance.deviceId === deviceId)(balances)
})
const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(notifications)
const notInvalidated = _.filter(notification => {
return !_.find(id => notification.id === id)(indexesToInvalidate)
}, notifications)
return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated)
})
}
const fiatBalancesNotify = (fiatWarnings) => {
return clearOldFiatNotifications(fiatWarnings).then(notInvalidated => {
return fiatWarnings.forEach(balance => {
if (_.find(o => {
const { cassette, deviceId } = o.detail
return cassette === balance.cassette && deviceId === balance.deviceId
}, notInvalidated)) return
const message = `Cash-out cassette ${balance.cassette} almost empty!`
const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette })
return queries.addNotification(FIAT_BALANCE, message, detailB)
})
})
}
const balancesNotify = (balances) => {
const highFilter = o => o.code === 'HIGH_CRYPTO_BALANCE'
const lowFilter = o => o.code === 'LOW_CRYPTO_BALANCE'
const highWarnings = _.filter(highFilter, balances)
const lowWarnings = _.filter(lowFilter, balances)
clearOldCryptoNotifications(balances)
highWarnings.forEach(warning => {
queries.getValidNotifications('cryptoBalance', `${warning.cryptoCode}_${warning.code}`).then(res => {
if (res.length > 0) {
return Promise.resolve()
}
console.log("Adding high balance alert for " + warning.cryptoCode + " - " + warning.fiatBalance.balance)
const balance = utils.formatCurrency(warning.fiatBalance.balance, warning.fiatCode)
return queries.addCryptoBalanceWarning(`${warning.cryptoCode}_${warning.code}`, `High balance in ${warning.cryptoCode} [${balance}]`)
})
})
lowWarnings.forEach(warning => {
queries.getValidNotifications('cryptoBalance', `${warning.cryptoCode}_${warning.code}`).then(res => {
if (res.length > 0) {
return Promise.resolve()
}
console.log("Adding low balance alert for " + warning.cryptoCode + " - " + warning.fiatBalance.balance)
const balance = utils.formatCurrency(warning.fiatBalance.balance, warning.fiatCode)
return queries.addCryptoBalanceWarning(`${warning.cryptoCode}_${warning.code}`, `Low balance in ${warning.cryptoCode} [${balance}]`)
})
})
const cryptoFilter = o => o.code === 'HIGH_CRYPTO_BALANCE' || o.code === 'LOW_CRYPTO_BALANCE'
const fiatFilter = o => o.code === 'LOW_CASH_OUT'
const cryptoWarnings = _.filter(cryptoFilter, balances)
const fiatWarnings = _.filter(fiatFilter, balances)
return Promise.all([cryptoBalancesNotify(cryptoWarnings), fiatBalancesNotify(fiatWarnings)]).catch(console.error)
}
const clearOldErrorNotifications = (alerts) => {
queries.getAllValidNotifications('error').then(res => {
_.forEach(notification => {
const idx = _.findIndex(alert => {
return alert.code === notification.detail.split('_')[0] && alert.deviceId === notification.device_id
}, alerts)
if(idx !== -1) {
return
}
// if the notification doesn't exist, then it is outdated and is not valid anymore
return queries.invalidateNotification(notification.id)
}, res)
const clearOldErrorNotifications = alerts => {
return queries.getAllValidNotifications(ERROR)
.then(res => {
// for each valid notification in DB see if it exists in alerts
// if the notification doesn't exist in alerts, it is not valid anymore
const filterByAlert = _.filter(notification => {
const { code, deviceId } = notification.detail
return !_.find(alert => alert.code === code && alert.deviceId === deviceId)(alerts)
})
const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res)
if (!indexesToInvalidate.length) return Promise.resolve()
return queries.batchInvalidate(indexesToInvalidate)
})
.catch(console.error)
}
const errorAlertsNotify = (alertRec) => {
let alerts = []
_.keys(alertRec.devices).forEach(function (device) {
// embed device ID in each alert object inside the deviceAlerts array
alertRec.devices[device].deviceAlerts = _.map(alert => {
return {...alert, deviceId: device}
}, alertRec.devices[device].deviceAlerts)
// concat every array into one
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
})
// now that we have all the alerts, we want to add PING and STALE alerts to the DB
// if there is a valid alert on the DB that doesn't exist on the new alerts array,
// that alert should be considered invalid
// after that, for the alerts array, we have to see if there is a valid alert of
// the sorts already on the DB
clearOldErrorNotifications(alerts)
const embedDeviceId = deviceId => _.assign({ deviceId })
const mapToAlerts = _.map(it => _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts))
const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices)
return clearOldErrorNotifications(alerts).then(() => {
_.forEach(alert => {
switch (alert.code) {
case PING:
return queries.getValidNotifications('error', PING, alert.deviceId).then(res => {
if(res.length > 0) {
return Promise.resolve()
}
console.log("Adding PING alert on database for " + alert.machineName)
case PING: {
const detailB = utils.buildDetail({ code: PING, age: alert.age ? alert.age : -1, deviceId: alert.deviceId })
return queries.getValidNotifications(ERROR, _.omit(['age'], detailB)).then(res => {
if (res.length > 0) return Promise.resolve()
const message = `Machine down`
return queries.addErrorNotification(`${PING}_${alert.age ? alert.age : '-1'}`, message, alert.deviceId)
return queries.addNotification(ERROR, message, detailB)
})
case STALE:
return queries.getValidNotifications('error', STALE, alert.deviceId).then(res => {
if(res.length > 0) {
return Promise.resolve()
}
console.log("Adding STALE alert on database for " + alert.machineName)
case STALE: {
const detailB = utils.buildDetail({ code: STALE, deviceId: alert.deviceId })
return queries.getValidNotifications(ERROR, detailB).then(res => {
if (res.length > 0) return Promise.resolve()
const message = `Machine is stuck on ${alert.state} screen`
return queries.addErrorNotification(STALE, message, alert.deviceId)
return queries.addNotification(ERROR, message, detailB)
})
default:
return
}
}
}, alerts)
}).catch(console.error)
}
const blacklistNotify = (tx, isAddressReuse) => {
let detail = ''
let message = ''
if(isAddressReuse) {
detail = `${tx.cryptoCode}_REUSED_${tx.toAddress}`
message = `Blocked reused address: ${tx.cryptoCode} ${tx.toAddress.substr(0,10)}...`
} else {
detail = `${tx.cryptoCode}_BLOCKED_${tx.toAddress}`
message = `Blocked blacklisted address: ${tx.cryptoCode} ${tx.toAddress.substr(0,10)}...`
const code = isAddressReuse ? 'REUSED' : 'BLOCKED'
const name = isAddressReuse ? 'reused' : 'blacklisted'
const detailB = utils.buildDetail({ cryptoCode: tx.cryptoCode, code, cryptoAddress: tx.toAddress })
const message = `Blocked ${name} address: ${tx.cryptoCode} ${tx.toAddress.substr(0, 10)}...`
return queries.addNotification(COMPLIANCE, message, detailB)
}
return queries.addComplianceNotification(tx.deviceId, detail, message)
const clearBlacklistNotification = (cryptoCode, cryptoAddress) => {
return queries.clearBlacklistNotification(cryptoCode, cryptoAddress).catch(console.error)
}
const clearOldCustomerSuspendedNotifications = (customerId, deviceId) => {
const detail = `SUSPENDED_${customerId}`
return queries.invalidateNotification(null, detail, deviceId)
const detailB = utils.buildDetail({ code: 'SUSPENDED', customerId, deviceId })
return queries.invalidateNotification(detailB, 'compliance')
}
const customerComplianceNotify = (customer, deviceId, prefix, days = null) => {
// prefix can be "BLOCKED", "SUSPENDED", etc
const detail = `${prefix}_${customer.id}`
const customerComplianceNotify = (customer, deviceId, code, days = null) => {
// code for now can be "BLOCKED", "SUSPENDED"
const detailB = utils.buildDetail({ customerId: customer.id, code, deviceId })
const date = new Date()
if (days) {
date.setDate(date.getDate() + days)
}
const message = prefix === "SUSPENDED" ? `Customer suspended until ${date.toLocaleString()}` : `Customer blocked`
const message = code === 'SUSPENDED' ? `Customer suspended until ${date.toLocaleString()}` : `Customer blocked`
// we have to clear every notification for this user where the suspension ended before the current date
clearOldCustomerSuspendedNotifications(customer.id, deviceId).then(() => {
return queries.getValidNotifications('compliance', detail, deviceId)
}).then(res => {
if (res.length > 0) {
return Promise.resolve()
}
return queries.addComplianceNotification(deviceId, detail, message)
return clearOldCustomerSuspendedNotifications(customer.id, deviceId)
.then(() => queries.getValidNotifications(COMPLIANCE, detailB))
.then(res => {
if (res.length > 0) return Promise.resolve()
return queries.addNotification(COMPLIANCE, message, detailB)
})
.catch(console.error)
}
module.exports = {
@ -408,7 +362,7 @@ module.exports = {
checkPings,
checkStuckScreen,
sendRedemptionMessage,
cashCassettesNotify,
blacklistNotify,
customerComplianceNotify,
clearBlacklistNotification
}

View file

@ -1,6 +1,9 @@
const { v4: uuidv4 } = require('uuid')
const pgp = require('pg-promise')()
const _ = require('lodash/fp')
const dbm = require('../postgresql_interface')
const db = require('../db')
const { v4: uuidv4 } = require('uuid')
// types of notifications able to be inserted into db:
/*
@ -11,27 +14,9 @@ compliance - notifications related to warnings triggered by compliance settings
error - notifications related to errors
*/
const addHighValueTx = (tx) => {
const sql = `INSERT INTO notifications (id, type, device_id, message, created) values ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
const direction = tx.direction === "cashOut" ? 'cash-out' : 'cash-in'
const message = `${tx.fiat} ${tx.fiatCode} ${direction} transaction`
return db.oneOrNone(sql, [uuidv4(), 'highValueTransaction', tx.deviceId, message])
}
const addCashCassetteWarning = (cassetteNumber, deviceId) => {
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)`
const message = `Cash-out cassette ${cassetteNumber} almost empty!`
return db.oneOrNone(sql, [uuidv4(), 'fiatBalance', cassetteNumber, deviceId, message])
}
const getUnreadCassetteNotifications = (cassetteNumber, deviceId) => {
const sql = `SELECT * FROM notifications WHERE read = 'f' AND device_id = $1 AND TYPE = 'fiatBalance' AND detail = '$2'`
return db.any(sql, [deviceId, cassetteNumber])
}
const addCryptoBalanceWarning = (detail, message) => {
const sql = `INSERT INTO notifications (id, type, detail, message, created) values ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
return db.oneOrNone(sql, [uuidv4(), 'cryptoBalance', detail, message])
const addNotification = (type, message, detail) => {
const sql = `INSERT INTO notifications (id, type, message, detail) VALUES ($1, $2, $3, $4)`
return db.oneOrNone(sql, [uuidv4(), type, message, detail])
}
const getAllValidNotifications = (type) => {
@ -39,48 +24,58 @@ const getAllValidNotifications = (type) => {
return db.any(sql, [type])
}
const addErrorNotification = (detail, message, deviceId) => {
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)`
return db.oneOrNone(sql, [uuidv4(), 'error', detail, deviceId, message])
const invalidateNotification = (detail, type) => {
detail = _.omitBy(_.isEmpty, detail)
const sql = `UPDATE notifications SET valid = 'f', read = 't', modified = CURRENT_TIMESTAMP WHERE valid = 't' AND type = $1 AND detail::jsonb @> $2::jsonb`
return db.none(sql, [type, detail])
}
const getValidNotifications = (type, detail, deviceId = null) => {
let sql;
if(!deviceId) {
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2`
}
else {
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2 AND device_id = $3`
}
return db.any(sql, [type, `%${detail}%`, deviceId])
const batchInvalidate = (ids) => {
const formattedIds = _.map(pgp.as.text, ids).join(',')
const sql = `UPDATE notifications SET valid = 'f', read = 't', modified = CURRENT_TIMESTAMP WHERE id IN ($1^)`
return db.none(sql, [formattedIds])
}
const invalidateNotification = (id, detail = null, deviceId = null) => {
let sql = ''
if(id) {
sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND id = $1`
}
else {
sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND detail LIKE $2`
sql = deviceId ? sql + ' AND device_id = $3' : sql
}
return db.none(sql, [id, `%${detail}%`, deviceId])
const clearBlacklistNotification = (cryptoCode, cryptoAddress) => {
const sql = `UPDATE notifications SET valid = 'f', read = 't', modified = CURRENT_TIMESTAMP WHERE type = 'compliance' AND detail->>'cryptoCode' = $1 AND detail->>'cryptoAddress' = $2 AND (detail->>'code' = 'BLOCKED' OR detail->>'code' = 'REUSED')`
return db.none(sql, [cryptoCode, cryptoAddress])
}
const addComplianceNotification = (deviceId, detail, message) => {
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, 'compliance', $2, $3, $4, CURRENT_TIMESTAMP)`
return db.oneOrNone(sql, [uuidv4(), detail, deviceId, message])
const getValidNotifications = (type, detail) => {
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail @> $2`
return db.any(sql, [type, detail])
}
const getNotifications = () => {
const sql = `SELECT * FROM notifications ORDER BY created DESC`
return db.any(sql)
}
const markAsRead = (id) => {
const sql = `UPDATE notifications SET read = 't', modified = CURRENT_TIMESTAMP WHERE id = $1`
return db.none(sql, [id])
}
const markAllAsRead = () => {
const sql = `UPDATE notifications SET read = 't'`
return db.none(sql)
}
const hasUnreadNotifications = () => {
const sql = `SELECT EXISTS (SELECT 1 FROM notifications WHERE read = 'f' LIMIT 1)`
return db.oneOrNone(sql).then(res => res.exists)
}
module.exports = {
machineEvents: dbm.machineEvents,
addHighValueTx,
addCashCassetteWarning,
addCryptoBalanceWarning,
addErrorNotification,
getUnreadCassetteNotifications,
addNotification,
getAllValidNotifications,
getValidNotifications,
invalidateNotification,
addComplianceNotification
batchInvalidate,
clearBlacklistNotification,
getValidNotifications,
getNotifications,
markAsRead,
markAllAsRead,
hasUnreadNotifications
}

View file

@ -9,15 +9,9 @@ function printSmsAlerts(alertRec, config) {
alerts = _.concat(alerts, alertRec.general)
}
_.keys(alertRec.devices).forEach(function (device) {
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
})
_.forEach(device => {
alerts = _.concat(alerts, utils.deviceAlerts(config, alertRec, device))
}, _.keys(alertRec.devices))
if (alerts.length === 0) return null
@ -27,12 +21,12 @@ function printSmsAlerts(alertRec, config) {
const code = entry[0]
const machineNames = _.filter(
_.negate(_.isEmpty),
_.map('machineName', entry[1]),
_.map('machineName', entry[1])
)
const cryptoCodes = _.filter(
_.negate(_.isEmpty),
_.map('cryptoCode', entry[1]),
_.map('cryptoCode', entry[1])
)
return {
@ -43,16 +37,11 @@ function printSmsAlerts(alertRec, config) {
}, _.toPairs(alertsMap))
const mapByCodeDisplay = _.map(it => {
if(_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) {
return it.codeDisplay
}
if(_.isEmpty(it.machineNames)) {
return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
}
else return `${it.codeDisplay} (${it.machineNames.join(', ')})`
if (_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) return it.codeDisplay
if (_.isEmpty(it.machineNames)) return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
return `${it.codeDisplay} (${it.machineNames.join(', ')})`
})
const displayAlertTypes = _.compose(
_.uniq,
mapByCodeDisplay,

View file

@ -2,8 +2,6 @@ const BigNumber = require('../../../lib/bn')
const notifier = require('..')
const utils = require('../utils')
const queries = require("../queries")
const emailFuncs = require('../email')
const smsFuncs = require('../sms')
afterEach(() => {
@ -96,6 +94,7 @@ const notifSettings = {
}
}
describe('checkNotifications', () => {
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => {
expect.assertions(1)
await expect(
@ -119,7 +118,9 @@ test('Exits checkNotifications with Promise.resolve() if SMS and Email are disab
})
).resolves.toBe(undefined)
})
})
describe('checkPings', () => {
test("Check Pings should return code PING for devices that haven't been pinged recently", () => {
expect(
notifier.checkPings([
@ -151,7 +152,7 @@ test('Checkpings returns empty array as the value for the id prop, if the lastPi
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': []
})
})
})
test('Check notification resolves to undefined if shouldNotAlert is called and is true', async () => {
const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert')
@ -190,7 +191,7 @@ test('If no alert fingerprint and inAlert is true, exits on call to sendNoAlerts
expect(mockSendNoAlerts).toHaveBeenCalledTimes(1)
})
// vvv tests for checkstuckscreen...
describe('checkStuckScreen', () => {
test('checkStuckScreen returns [] when no events are found', () => {
expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([])
})
@ -279,8 +280,9 @@ test('checkStuckScreen returns empty array if age < STALE_STATE', () => {
expect(result1).toEqual([])
expect(result2).toEqual([])
})
})
test("calls sendRedemptionMessage if !zeroConf and rec.isRedemption", async () => {
test('calls sendRedemptionMessage if !zeroConf and rec.isRedemption', () => {
const configManager = require('../../new-config-manager')
const settingsLoader = require('../../new-settings-loader')
@ -292,11 +294,10 @@ test("calls sendRedemptionMessage if !zeroConf and rec.isRedemption", async () =
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => rec)
getCashOut.mockReturnValue({ zeroConfLimit: -Infinity })
loadLatest.mockReturnValue({})
loadLatest.mockReturnValue(Promise.resolve({}))
getGlobalNotifications.mockReturnValue({ ...notifSettings, sms: { active: true, errors: true, transactions: true } })
const response = await notifier.transactionNotify(tx, {isRedemption: true})
return notifier.transactionNotify(tx, { isRedemption: true }).then(response => {
// this type of response implies sendRedemptionMessage was called
expect(response[0]).toMatchObject({
sms: {
@ -308,8 +309,9 @@ test("calls sendRedemptionMessage if !zeroConf and rec.isRedemption", async () =
}
})
})
})
test("calls sendTransactionMessage if !zeroConf and !rec.isRedemption", async () => {
test('calls sendTransactionMessage if !zeroConf and !rec.isRedemption', async () => {
const configManager = require('../../new-config-manager')
const settingsLoader = require('../../new-settings-loader')
const machineLoader = require('../../machine-loader')
@ -322,11 +324,11 @@ test("calls sendTransactionMessage if !zeroConf and !rec.isRedemption", async ()
// sendMessage on emailFuncs isn't called because it is disabled in getGlobalNotifications.mockReturnValue
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => ({ prop: rec }))
buildTransactionMessage.mockImplementation(() => ["mock message", false])
buildTransactionMessage.mockImplementation(() => ['mock message', false])
getMachineName.mockReturnValue("mockMachineName")
getMachineName.mockReturnValue('mockMachineName')
getCashOut.mockReturnValue({ zeroConfLimit: -Infinity })
loadLatest.mockReturnValue({})
loadLatest.mockReturnValue(Promise.resolve({}))
getGlobalNotifications.mockReturnValue({ ...notifSettings, sms: { active: true, errors: true, transactions: true } })
const response = await notifier.transactionNotify(tx, { isRedemption: false })

View file

@ -26,6 +26,7 @@ const notifications = {
email: { active: false, errors: false }
}
describe('buildAlertFingerprint', () => {
test('Build alert fingerprint returns null if no sms or email alerts', () => {
expect(
utils.buildAlertFingerprint(
@ -65,7 +66,9 @@ test('Build alert fingerprint returns hash if [email] or sms are enabled and the
})
).toBe('string')
})
})
describe('sendNoAlerts', () => {
test('Send no alerts returns empty object with sms and email disabled', () => {
expect(utils.sendNoAlerts(plugins, false, false)).toEqual({})
})
@ -98,3 +101,4 @@ test('Send no alerts returns object with email prop if only email is enabled', (
}
})
})
})

View file

@ -11,14 +11,26 @@ const {
ALERT_SEND_INTERVAL
} = require('./codes')
const DETAIL_TEMPLATE = {
deviceId: '',
cryptoCode: '',
code: '',
cassette: '',
age: '',
customerId: '',
cryptoAddress: '',
direction: '',
fiat: '',
fiatCode: ''
}
function parseEventNote (event) {
return _.set('note', JSON.parse(event.note), event)
}
function checkPing (device) {
const age = +Date.now() - +new Date(device.lastPing)
if (age > NETWORK_DOWN_TIME)
return [{ code: PING, age, machineName: device.name }]
const age = Date.now() - (new Date(device.lastPing).getTime())
if (age > NETWORK_DOWN_TIME) return [{ code: PING, age, machineName: device.name }]
return []
}
@ -49,27 +61,6 @@ const shouldNotAlert = currentAlertFingerprint => {
)
}
function getAlertTypes(alertRec, config) {
let alerts = []
if (!isActive(config)) return alerts
if (config.balance) {
alerts = _.concat(alerts, alertRec.general)
}
_.keys(alertRec.devices).forEach(function (device) {
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
})
return alerts
}
function buildAlertFingerprint (alertRec, notifications) {
const sms = getAlertTypes(alertRec, notifications.sms)
const email = getAlertTypes(alertRec, notifications.email)
@ -153,6 +144,42 @@ function formatAge (age, settings) {
return prettyMs(age, settings)
}
function buildDetail (obj) {
// obj validation
const objKeys = _.keys(obj)
const detailKeys = _.keys(DETAIL_TEMPLATE)
if ((_.difference(objKeys, detailKeys)).length > 0) {
return Promise.reject(new Error('Error when building detail object: invalid properties'))
}
return { ...DETAIL_TEMPLATE, ...obj }
}
function deviceAlerts (config, alertRec, device) {
let alerts = []
if (config.balance) {
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
}
if (config.errors) {
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
}
return alerts
}
function getAlertTypes (alertRec, config) {
let alerts = []
if (!isActive(config)) return alerts
if (config.balance) {
alerts = _.concat(alerts, alertRec.general)
}
_.forEach(device => {
alerts = _.concat(alerts, deviceAlerts(config, alertRec, device))
}, _.keys(alertRec.devices))
return alerts
}
module.exports = {
codeDisplay,
parseEventNote,
@ -167,5 +194,7 @@ module.exports = {
sendNoAlerts,
buildTransactionMessage,
formatCurrency,
formatAge
formatAge,
buildDetail,
deviceAlerts
}

View file

@ -23,7 +23,7 @@ const coinUtils = require('./coin-utils')
const commissionMath = require('./commission-math')
const promoCodes = require('./promo-codes')
const notifier = require('./notifier/index')
const notifier = require('./notifier')
const mapValuesWithKey = _.mapValues.convert({
cap: false
@ -55,8 +55,7 @@ function plugins (settings, deviceId) {
? undefined
: BN(1).add(BN(commissions.cashOut).div(100))
if (Date.now() - rateRec.timestamp > STALE_TICKER)
return logger.warn('Stale rate for ' + cryptoCode)
if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode)
const rate = rateRec.rates
withCommission ? rates[cryptoCode] = {
@ -90,10 +89,8 @@ function plugins (settings, deviceId) {
cryptoCodes.forEach((cryptoCode, i) => {
const balanceRec = balanceRecs[i]
if (!balanceRec)
return logger.warn('No balance for ' + cryptoCode + ' yet')
if (Date.now() - balanceRec.timestamp > STALE_BALANCE)
return logger.warn('Stale balance for ' + cryptoCode)
if (!balanceRec) return logger.warn('No balance for ' + cryptoCode + ' yet')
if (Date.now() - balanceRec.timestamp > STALE_BALANCE) return logger.warn('Stale balance for ' + cryptoCode)
balances[cryptoCode] = balanceRec.balance
})
@ -113,13 +110,10 @@ function plugins (settings, deviceId) {
const sumTxs = (sum, tx) => {
const bills = tx.bills
const sameDenominations = a => a[0].denomination === a[1].denomination
const doDenominationsMatch = _.every(
sameDenominations,
_.zip(cassettes, bills)
)
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
if (!doDenominationsMatch) {
throw new Error("Denominations don't add up, cassettes were changed.")
throw new Error('Denominations don\'t add up, cassettes were changed.')
}
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
@ -162,17 +156,6 @@ function plugins (settings, deviceId) {
? argv.cassettes.split(',')
: rec.counts
return Promise.all([
dbm.cassetteCounts(deviceId),
cashOutHelper.redeemableTxs(deviceId, excludeTxId)
]).then(([rec, _redeemableTxs]) => {
const redeemableTxs = _.reject(
_.matchesProperty('id', excludeTxId),
_redeemableTxs
)
const counts = argv.cassettes ? argv.cassettes.split(',') : rec.counts
const cassettes = [
{
denomination: parseInt(denominations[0], 10),
@ -206,23 +189,18 @@ function plugins (settings, deviceId) {
order by id desc
limit 1`
return db.one(sql, ['config']).then(row => row.id)
return db.one(sql, ['config'])
.then(row => row.id)
}
function mapCoinSettings (coinParams) {
const cryptoCode = coinParams[0]
const cryptoNetwork = coinParams[1]
const commissions = configManager.getCommissions(
cryptoCode,
deviceId,
settings.config
)
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
const minimumTx = BN(commissions.minimumTx)
const cashInFee = BN(commissions.fixedFee)
const cashInCommission = BN(commissions.cashIn)
const cashOutCommission = _.isNumber(commissions.cashOut)
? BN(commissions.cashOut)
: null
const cashOutCommission = _.isNumber(commissions.cashOut) ? BN(commissions.cashOut) : null
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
return {
@ -236,25 +214,15 @@ function plugins (settings, deviceId) {
}
}
function pollQueries (
serialNumber,
deviceTime,
deviceRec,
machineVersion,
machineModel
) {
function pollQueries (serialNumber, deviceTime, deviceRec, machineVersion, machineModel) {
const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies
const tickerPromises = cryptoCodes.map(c =>
ticker.getRates(settings, fiatCode, c)
)
const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
const testnetPromises = cryptoCodes.map(c =>
wallet.cryptoNetwork(settings, c)
)
const testnetPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c))
const pingPromise = recordPing(deviceTime, machineVersion, machineModel)
const currentConfigVersionPromise = fetchCurrentConfigVersion()
const currentAvailablePromoCodes = promoCodes.getNumberOfAvailablePromoCodes()
@ -289,12 +257,7 @@ function plugins (settings, deviceId) {
}
function sendCoins (tx) {
return wallet.sendCoins(
settings,
tx.toAddress,
tx.cryptoAtoms,
tx.cryptoCode
)
return wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
}
function recordPing (deviceTime, version, model) {
@ -305,18 +268,11 @@ function plugins (settings, deviceId) {
}
return Promise.all([
db.none(
`insert into machine_pings(device_id, device_time) values($1, $2)
ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`,
[deviceId, deviceTime]
),
db.none(
pgp.helpers.update(devices, null, 'devices') +
'WHERE device_id = ${deviceId}',
{
db.none(`insert into machine_pings(device_id, device_time) values($1, $2)
ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`, [deviceId, deviceTime]),
db.none(pgp.helpers.update(devices, null, 'devices') + 'WHERE device_id = ${deviceId}', {
deviceId
}
)
})
])
}
@ -348,15 +304,12 @@ function plugins (settings, deviceId) {
}
function fiatBalance (fiatCode, cryptoCode) {
const commissions = configManager.getCommissions(
cryptoCode,
deviceId,
settings.config
)
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
return Promise.all([
ticker.getRates(settings, fiatCode, cryptoCode),
wallet.balance(settings, cryptoCode)
]).then(([rates, balanceRec]) => {
])
.then(([rates, balanceRec]) => {
if (!rates || !balanceRec) return null
const rawRate = rates.rates.ask
@ -393,7 +346,8 @@ function plugins (settings, deviceId) {
}
}
return sms.sendMessage(settings, rec).then(() => {
return sms.sendMessage(settings, rec)
.then(() => {
const sql = 'update cash_out_txs set notified=$1 where id=$2'
const values = [true, tx.id]
@ -407,17 +361,14 @@ function plugins (settings, deviceId) {
}
function clearOldLogs () {
return logs.clearOldLogs().catch(logger.error)
return logs.clearOldLogs()
.catch(logger.error)
}
function pong () {
return db
.none(
`UPDATE server_events SET created=now() WHERE event_type=$1;
return db.none(`UPDATE server_events SET created=now() WHERE event_type=$1;
INSERT INTO server_events (event_type) SELECT $1
WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`,
['ping']
)
WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`, ['ping'])
.catch(logger.error)
}
@ -458,16 +409,13 @@ function plugins (settings, deviceId) {
const marketTradesQueues = tradesQueues[market]
if (!marketTradesQueues || marketTradesQueues.length === 0) return null
logger.debug(
'[%s] tradesQueues size: %d',
market,
marketTradesQueues.length
)
logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length)
logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0])
const t1 = Date.now()
const filtered = marketTradesQueues.filter(tradeEntry => {
const filtered = marketTradesQueues
.filter(tradeEntry => {
return t1 - tradeEntry.timestamp < TRADE_TTL
})
@ -480,14 +428,10 @@ function plugins (settings, deviceId) {
if (filtered.length === 0) return null
const cryptoAtoms = filtered.reduce(
(prev, current) => prev.plus(current.cryptoAtoms),
BN(0)
)
const cryptoAtoms = filtered
.reduce((prev, current) => prev.plus(current.cryptoAtoms), BN(0))
const timestamp = filtered
.map(r => r.timestamp)
.reduce((acc, r) => Math.max(acc, r), 0)
const timestamp = filtered.map(r => r.timestamp).reduce((acc, r) => Math.max(acc, r), 0)
const consolidatedTrade = {
fiatCode,
@ -503,15 +447,11 @@ function plugins (settings, deviceId) {
}
function executeTrades () {
return machineLoader
.getMachines()
return machineLoader.getMachines()
.then(devices => {
const deviceIds = devices.map(device => device.deviceId)
const lists = deviceIds.map(deviceId => {
const localeConfig = configManager.getLocale(
deviceId,
settings.config
)
const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies
@ -521,9 +461,8 @@ function plugins (settings, deviceId) {
}))
})
const tradesPromises = _.uniq(_.flatten(lists)).map(r =>
executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)
)
const tradesPromises = _.uniq(_.flatten(lists))
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))
return Promise.all(tradesPromises)
})
@ -538,7 +477,8 @@ function plugins (settings, deviceId) {
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
return executeTradeForType(tradeEntry).catch(err => {
return executeTradeForType(tradeEntry)
.catch(err => {
tradesQueues[market].push(tradeEntry)
if (err.name === 'orderTooSmall') return logger.debug(err.message)
logger.error(err)
@ -546,8 +486,7 @@ function plugins (settings, deviceId) {
}
function executeTradeForType (_tradeEntry) {
const expand = te =>
_.assign(te, {
const expand = te => _.assign(te, {
cryptoAtoms: te.cryptoAtoms.abs(),
type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell'
})
@ -555,26 +494,24 @@ function plugins (settings, deviceId) {
const tradeEntry = expand(_tradeEntry)
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
return execute(
settings,
tradeEntry.cryptoAtoms,
tradeEntry.fiatCode,
tradeEntry.cryptoCode
)
return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
.then(() => recordTrade(tradeEntry))
.catch(err => {
return recordTrade(tradeEntry, err).then(() => {
return recordTrade(tradeEntry, err)
.then(() => {
throw err
})
})
}
function convertBigNumFields (obj) {
const convert = (value, key) =>
_.includes(key, ['cryptoAtoms', 'fiat']) ? value.toString() : value
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
? value.toString()
: value
const convertKey = key =>
_.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
? key + '#'
: key
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
}
@ -604,10 +541,8 @@ function plugins (settings, deviceId) {
const notifications = configManager.getGlobalNotifications(settings.config)
let promises = []
if (notifications.email.active && rec.email)
promises.push(email.sendMessage(settings, rec))
if (notifications.sms.active && rec.sms)
promises.push(sms.sendMessage(settings, rec))
if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec))
if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec))
return Promise.all(promises)
}
@ -617,24 +552,16 @@ function plugins (settings, deviceId) {
}
function checkDeviceCashBalances (fiatCode, device) {
const cashOutConfig = configManager.getCashOut(
device.deviceId,
settings.config
)
const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config)
const denomination1 = cashOutConfig.top
const denomination2 = cashOutConfig.bottom
const cashOutEnabled = cashOutConfig.active
const notifications = configManager.getNotifications(
null,
device.deviceId,
settings.config
)
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
const machineName = device.name
const cashInAlert =
device.cashbox > notifications.cashInAlertThreshold
const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
? {
code: 'CASH_BOX_FULL',
machineName,
@ -643,8 +570,7 @@ function plugins (settings, deviceId) {
}
: null
const cassette1Alert =
cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1
const cassette1Alert = cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1
? {
code: 'LOW_CASH_OUT',
cassette: 1,
@ -656,8 +582,7 @@ function plugins (settings, deviceId) {
}
: null
const cassette2Alert =
cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2
const cassette2Alert = cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2
? {
code: 'LOW_CASH_OUT',
cassette: 2,
@ -673,8 +598,7 @@ function plugins (settings, deviceId) {
}
function checkCryptoBalances (fiatCode, devices) {
const fiatBalancePromises = cryptoCodes =>
_.map(c => fiatBalance(fiatCode, c), cryptoCodes)
const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes)
const fetchCryptoCodes = _deviceId => {
const localeConfig = configManager.getLocale(_deviceId, settings.config)
@ -685,23 +609,18 @@ function plugins (settings, deviceId) {
const cryptoCodes = union(devices)
const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode])
return Promise.all(fiatBalancePromises(cryptoCodes)).then(balances =>
_.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances))
)
return Promise.all(fiatBalancePromises(cryptoCodes))
.then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)))
}
function checkCryptoBalance (fiatCode, rec) {
const [cryptoCode, fiatBalance] = rec
if (!fiatBalance) return null
const notifications = configManager.getNotifications(
cryptoCode,
null,
settings.config
)
const lowAlertThreshold = notifications.cryptoLowBalance
const highAlertThreshold = notifications.cryptoHighBalance
const notifications = configManager.getNotifications(cryptoCode, null, settings.config)
const override = _.find(override => override.cryptoCurrency === cryptoCode, settings.config.notifications_cryptoBalanceOverrides)
const lowAlertThreshold = override ? override.lowBalance : notifications.cryptoLowBalance
const highAlertThreshold = override ? override.highBalance : notifications.cryptoHighBalance
const req = {
cryptoCode,
@ -709,17 +628,13 @@ function plugins (settings, deviceId) {
fiatCode
}
if (
_.isFinite(lowAlertThreshold) &&
BN(fiatBalance.balance).lt(lowAlertThreshold)
)
if (_.isFinite(lowAlertThreshold) && BN(fiatBalance.balance).lt(lowAlertThreshold)) {
return _.set('code')('LOW_CRYPTO_BALANCE')(req)
}
if (
_.isFinite(highAlertThreshold) &&
BN(fiatBalance.balance).gt(highAlertThreshold)
)
if (_.isFinite(highAlertThreshold) && BN(fiatBalance.balance).gt(highAlertThreshold)) {
return _.set('code')('HIGH_CRYPTO_BALANCE')(req)
}
return null
}
@ -728,23 +643,24 @@ function plugins (settings, deviceId) {
const localeConfig = configManager.getGlobalLocale(settings.config)
const fiatCode = localeConfig.fiatCurrency
return machineLoader.getMachines().then(devices => {
return machineLoader.getMachines()
.then(devices => {
return Promise.all([
checkCryptoBalances(fiatCode, devices),
checkDevicesCashBalances(fiatCode, devices)
]).then(_.flow(_.flattenDeep, _.compact))
])
.then(_.flow(_.flattenDeep, _.compact))
})
}
function randomCode () {
return BN(crypto.randomBytes(3).toString('hex'), 16)
.shift(-6)
.toFixed(6)
.slice(-6)
return BN(crypto.randomBytes(3).toString('hex'), 16).shift(-6).toFixed(6).slice(-6)
}
function getPhoneCode (phone) {
const code = argv.mockSms ? '123' : randomCode()
const code = argv.mockSms
? '123'
: randomCode()
const rec = {
sms: {
@ -753,14 +669,14 @@ function plugins (settings, deviceId) {
}
}
return sms.sendMessage(settings, rec).then(() => code)
return sms.sendMessage(settings, rec)
.then(() => code)
}
function sweepHdRow (row) {
const cryptoCode = row.crypto_code
return wallet
.sweep(settings, cryptoCode, row.hd_index)
return wallet.sweep(settings, cryptoCode, row.hd_index)
.then(txHash => {
if (txHash) {
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
@ -771,17 +687,14 @@ function plugins (settings, deviceId) {
return db.none(sql, row.id)
}
})
.catch(err =>
logger.error('[%s] Sweep error: %s', cryptoCode, err.message)
)
.catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message))
}
function sweepHd () {
const sql = `select id, crypto_code, hd_index from cash_out_txs
where hd_index is not null and not swept and status in ('confirmed', 'instant')`
return db
.any(sql)
return db.any(sql)
.then(rows => Promise.all(rows.map(sweepHdRow)))
.catch(err => logger.error(err))
}
@ -795,15 +708,14 @@ function plugins (settings, deviceId) {
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config)
const tickerPromises = cryptoCodes.map(c =>
ticker.getRates(settings, fiatCode, c)
)
const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
return Promise.all(tickerPromises)
}
function getRates () {
return getRawRates().then(buildRates)
return getRawRates()
.then(buildRates)
}
return {

View file

@ -1,7 +1,7 @@
const _ = require('lodash/fp')
const plugins = require('./plugins')
const notifier = require('./notifier/index')
const notifier = require('./notifier')
const T = require('./time')
const logger = require('./logger')
const cashOutTx = require('./cash-out/cash-out-tx')

View file

@ -28,7 +28,7 @@ const compliance = require('./compliance')
const promoCodes = require('./promo-codes')
const BN = require('./bn')
const commissionMath = require('./commission-math')
const notifier = require('./notifier/index')
const notifier = require('./notifier')
const version = require('../package.json').version
@ -324,7 +324,6 @@ function updateCustomer (req, res, next) {
}
function triggerSanctions (req, res, next) {
console.log("SANCTIONS TRIGGERED")
const id = req.params.id
customers.getById(id)

View file

@ -1,8 +1,6 @@
var db = require('./db')
function singleQuotify(item) {
return "'" + item + "'"
}
const singleQuotify = (item) => `'${item}'`
var types = [
'highValueTransaction',
@ -18,20 +16,17 @@ exports.up = function (next) {
const sql = [
`
CREATE TYPE notification_type AS ENUM ${'(' + types + ')'};
CREATE TABLE IF NOT EXISTS "notifications" (
CREATE TABLE "notifications" (
"id" uuid NOT NULL PRIMARY KEY,
"type" notification_type NOT NULL,
"detail" TEXT,
"device_id" TEXT,
"detail" JSONB,
"message" TEXT NOT NULL,
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
"created" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"read" BOOLEAN NOT NULL DEFAULT 'false',
"valid" BOOLEAN NOT NULL DEFAULT 'true',
CONSTRAINT fk_devices
FOREIGN KEY(device_id)
REFERENCES devices(device_id)
ON DELETE CASCADE
"modified" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX ON notifications (valid);
CREATE INDEX ON notifications (read);`
]

View file

@ -0,0 +1,144 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
import ActionButton from 'src/components/buttons/ActionButton'
import { H5 } from 'src/components/typography'
import { ReactComponent as NotificationIconZodiac } from 'src/styling/icons/menu/notification-zodiac.svg'
import { ReactComponent as ClearAllIconInverse } from 'src/styling/icons/stage/spring/empty.svg'
import { ReactComponent as ClearAllIcon } from 'src/styling/icons/stage/zodiac/empty.svg'
import { ReactComponent as ShowUnreadIcon } from 'src/styling/icons/stage/zodiac/full.svg'
import styles from './NotificationCenter.styles'
import NotificationRow from './NotificationRow'
const useStyles = makeStyles(styles)
const GET_NOTIFICATIONS = gql`
query getNotifications {
notifications {
id
deviceName
type
detail
message
created
read
valid
}
hasUnreadNotifications
machines {
deviceId
name
}
}
`
const CLEAR_NOTIFICATION = gql`
mutation clearNotification($id: ID!) {
clearNotification(id: $id) {
id
}
}
`
const CLEAR_ALL_NOTIFICATIONS = gql`
mutation clearAllNotifications {
clearAllNotifications {
id
}
}
`
const NotificationCenter = ({ close, notifyUnread }) => {
const classes = useStyles()
const { data, loading } = useQuery(GET_NOTIFICATIONS)
const [showingUnread, setShowingUnread] = useState(false)
const machines = R.compose(
R.map(R.prop('name')),
R.indexBy(R.prop('deviceId'))
)(data?.machines ?? [])
const notifications = R.path(['notifications'])(data) ?? []
const hasUnread = data?.hasUnreadNotifications ?? false
if (!hasUnread) {
notifyUnread && notifyUnread()
}
const [clearNotification] = useMutation(CLEAR_NOTIFICATION, {
onError: () => console.error('Error while clearing notification'),
refetchQueries: () => ['getNotifications']
})
const [clearAllNotifications] = useMutation(CLEAR_ALL_NOTIFICATIONS, {
onError: () => console.error('Error while clearing all notifications'),
refetchQueries: () => ['getNotifications']
})
const handleClearNotification = id => {
clearNotification({ variables: { id } })
}
const buildNotifications = () => {
const notificationsToShow =
!showingUnread || !hasUnread
? notifications
: R.filter(R.propEq('read', false))(notifications)
return notificationsToShow.map(n => {
return (
<NotificationRow
key={n.id}
id={n.id}
type={n.type}
detail={n.detail}
message={n.message}
deviceName={machines[n.detail.deviceId]}
created={n.created}
read={n.read}
valid={n.valid}
onClear={handleClearNotification}
/>
)
})
}
return (
<>
<div className={classes.container}>
<div className={classes.header}>
<H5 className={classes.headerText}>Notifications</H5>
<button onClick={close} className={classes.notificationIcon}>
<NotificationIconZodiac />
{hasUnread && <div className={classes.hasUnread} />}
</button>
</div>
<div className={classes.actionButtons}>
{hasUnread && (
<ActionButton
color="primary"
Icon={ShowUnreadIcon}
InverseIcon={ClearAllIconInverse}
className={classes.clearAllButton}
onClick={() => setShowingUnread(!showingUnread)}>
{showingUnread ? 'Show all' : 'Show unread'}
</ActionButton>
)}
<ActionButton
color="primary"
Icon={ClearAllIcon}
InverseIcon={ClearAllIconInverse}
className={classes.clearAllButton}
onClick={clearAllNotifications}>
Mark all as read
</ActionButton>
</div>
<div className={classes.notificationsList}>
{!loading && buildNotifications()}
</div>
</div>
<div className={classes.background} />
</>
)
}
export default NotificationCenter

View file

@ -0,0 +1,119 @@
import {
spacer,
white,
zircon,
secondaryColor,
spring3,
comet
} from 'src/styling/variables'
const styles = {
background: {
position: 'absolute',
width: '100vw',
height: '100vh',
left: 0,
top: 0,
zIndex: -1,
backgroundColor: white,
boxShadow: '0 0 14px 0 rgba(0, 0, 0, 0.24)'
},
container: {
left: -200,
top: -42,
backgroundColor: white,
height: '110vh'
},
header: {
display: 'flex',
justifyContent: 'space-between'
},
headerText: {
marginTop: spacer * 2.5,
marginLeft: spacer * 3
},
actionButtons: {
display: 'flex',
marginLeft: spacer * 2
},
notificationIcon: {
position: 'absolute',
left: spacer * 33,
top: spacer * 2 + 4,
cursor: 'pointer',
background: 'transparent',
boxShadow: '0px 0px 0px transparent',
border: '0px solid transparent',
textShadow: '0px 0px 0px transparent',
outline: 'none'
},
clearAllButton: {
marginTop: -spacer * 2,
marginLeft: spacer,
backgroundColor: zircon
},
notificationsList: {
width: 440,
height: '90vh',
maxHeight: '100vh',
marginTop: 8,
marginLeft: 0,
marginRight: 10,
overflowY: 'auto',
overflowX: 'hidden',
backgroundColor: white,
zIndex: 10
},
notificationRow: {
position: 'relative',
marginBottom: spacer / 2,
paddingTop: spacer * 1.5
},
unread: {
backgroundColor: spring3
},
notificationRowIcon: {
alignSelf: 'center',
'& > *': {
marginLeft: spacer * 3
}
},
unreadIcon: {
marginLeft: spacer,
width: '12px',
height: '12px',
backgroundColor: secondaryColor,
borderRadius: '50%',
cursor: 'pointer',
zIndex: 1
},
notificationTitle: {
margin: 0,
color: comet
},
notificationBody: {
margin: 0
},
notificationSubtitle: {
margin: 0,
marginBottom: spacer,
color: comet
},
stripes: {
position: 'absolute',
height: '100%',
top: '0px',
opacity: '60%'
},
hasUnread: {
position: 'absolute',
top: 0,
left: 16,
width: '9px',
height: '9px',
backgroundColor: secondaryColor,
borderRadius: '50%'
}
}
export default styles

View file

@ -0,0 +1,82 @@
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import prettyMs from 'pretty-ms'
import React from 'react'
import { Label1, Label2, TL2 } from 'src/components/typography'
import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg'
import { ReactComponent as Transaction } from 'src/styling/icons/arrow/transaction.svg'
import { ReactComponent as StripesSvg } from 'src/styling/icons/stripes.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
import styles from './NotificationCenter.styles'
const useStyles = makeStyles(styles)
const types = {
highValueTransaction: { display: 'Transactions', icon: <Transaction /> },
fiatBalance: { display: 'Maintenance', icon: <Wrench /> },
cryptoBalance: { display: 'Maintenance', icon: <Wrench /> },
compliance: { display: 'Compliance', icon: <WarningIcon /> },
error: { display: 'Error', icon: <WarningIcon /> }
}
const NotificationRow = ({
id,
type,
detail,
message,
deviceName,
created,
read,
valid,
onClear
}) => {
const classes = useStyles()
const buildType = () => {
return types[type].display
}
const buildAge = () => {
const createdDate = new Date(created)
const interval = +new Date() - createdDate
return prettyMs(interval, { compact: true, verbose: true })
}
return (
<Grid
container
className={classnames(
classes.notificationRow,
!read && valid ? classes.unread : ''
)}>
<Grid item xs={2} className={classes.notificationRowIcon}>
{types[type].icon}
</Grid>
<Grid item container xs={7} direction="row">
<Grid item xs={12}>
<Label2 className={classes.notificationTitle}>
{`${buildType()} ${deviceName ? '- ' + deviceName : ''}`}
</Label2>
</Grid>
<Grid item xs={12}>
<TL2 className={classes.notificationBody}>{message}</TL2>
</Grid>
<Grid item xs={12}>
<Label1 className={classes.notificationSubtitle}>
{buildAge(created)}
</Label1>
</Grid>
</Grid>
<Grid item xs={3} style={{ zIndex: 1 }}>
{!read && (
<div onClick={() => onClear(id)} className={classes.unreadIcon} />
)}
</Grid>
{!valid && <StripesSvg className={classes.stripes} />}
</Grid>
)
}
export default NotificationRow

View file

@ -0,0 +1,2 @@
import NotificationCenter from './NotificationCenter'
export default NotificationCenter

View file

@ -1,19 +1,31 @@
import { useQuery } from '@apollo/react-hooks'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import Popper from '@material-ui/core/Popper'
import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import gql from 'graphql-tag'
import React, { memo, useState } from 'react'
import { NavLink, useHistory } from 'react-router-dom'
import NotificationCenter from 'src/components/NotificationCenter'
import ActionButton from 'src/components/buttons/ActionButton'
import { H4 } from 'src/components/typography'
import AddMachine from 'src/pages/AddMachine'
import { ReactComponent as AddIconReverse } from 'src/styling/icons/button/add/white.svg'
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
import { ReactComponent as NotificationIcon } from 'src/styling/icons/menu/notification.svg'
import styles from './Header.styles'
const useStyles = makeStyles(styles)
const HAS_UNREAD = gql`
query getUnread {
hasUnreadNotifications
}
`
const Subheader = ({ item, classes }) => {
const [prev, setPrev] = useState(null)
@ -46,8 +58,10 @@ const Subheader = ({ item, classes }) => {
const Header = memo(({ tree }) => {
const [open, setOpen] = useState(false)
const [anchorEl, setAnchorEl] = React.useState(null)
const [active, setActive] = useState()
const { data, refetch } = useQuery(HAS_UNREAD)
const hasUnread = data?.hasUnreadNotifications ?? false
const history = useHistory()
const classes = useStyles()
@ -56,8 +70,26 @@ const Header = memo(({ tree }) => {
history.push('/maintenance/machine-status', { id: machine.deviceId })
}
// these inline styles prevent scroll bubbling: when the user reaches the bottom of the notifications list and keeps scrolling,
// the body scrolls, stealing the focus from the notification center, preventing the admin from scrolling the notifications back up
// on the first scroll, needing to move the mouse to recapture the focus on the notification center
// it also disables the scrollbars caused by the notification center's background to the right of the page, but keeps the scrolling on the body enabled
const onClickAway = () => {
setAnchorEl(null)
document.querySelector('#root').classList.remove('root-notifcenter-open')
document.querySelector('body').classList.remove('body-notifcenter-open')
}
const handleClick = event => {
setAnchorEl(anchorEl ? null : event.currentTarget)
document.querySelector('#root').classList.add('root-notifcenter-open')
document.querySelector('body').classList.add('body-notifcenter-open')
}
const popperOpen = Boolean(anchorEl)
const id = popperOpen ? 'notifications-popper' : undefined
return (
<header>
<header className={classes.headerContainer}>
<div className={classes.header}>
<div className={classes.content}>
<div
@ -87,6 +119,8 @@ const Header = memo(({ tree }) => {
</NavLink>
))}
</ul>
</nav>
<div className={classes.actionButtonsContainer}>
<ActionButton
color="secondary"
Icon={AddIcon}
@ -94,7 +128,34 @@ const Header = memo(({ tree }) => {
onClick={() => setOpen(true)}>
Add machine
</ActionButton>
</nav>
<ClickAwayListener onClickAway={onClickAway}>
<div>
<button
onClick={handleClick}
className={classes.notificationIcon}>
<NotificationIcon />
{hasUnread && <div className={classes.hasUnread} />}
</button>
<Popper
id={id}
open={popperOpen}
anchorEl={anchorEl}
className={classes.popper}
disablePortal={false}
modifiers={{
preventOverflow: {
enabled: true,
boundariesElement: 'viewport'
}
}}>
<NotificationCenter
close={onClickAway}
notifyUnread={refetch}
/>
</Popper>
</div>
</ClickAwayListener>
</div>
</div>
</div>
{active && active.children && (

View file

@ -5,6 +5,7 @@ import {
spacer,
white,
primaryColor,
secondaryColor,
placeholderColor,
subheaderColor,
fontColor
@ -21,6 +22,9 @@ if (version === 8) {
}
const styles = {
headerContainer: {
position: 'relative'
},
header: {
backgroundColor: primaryColor,
color: white,
@ -80,27 +84,6 @@ const styles = {
border: 'none',
color: white,
backgroundColor: 'transparent'
// '&:hover': {
// color: white
// },
// '&:hover::after': {
// width: '50%',
// marginLeft: '-25%'
// },
// position: 'relative',
// '&:after': {
// content: '""',
// display: 'block',
// background: white,
// width: 0,
// height: 4,
// left: '50%',
// marginLeft: 0,
// bottom: -8,
// position: 'absolute',
// borderRadius: 1000,
// transition: [['all', '0.2s', 'cubic-bezier(0.95, 0.1, 0.45, 0.94)']]
// }
},
forceSize: {
display: 'inline-block',
@ -167,6 +150,35 @@ const styles = {
},
logoLink: {
cursor: 'pointer'
},
actionButtonsContainer: {
zIndex: 1,
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
minWidth: 200,
transform: 'translateZ(0)'
},
notificationIcon: {
marginTop: spacer / 2,
cursor: 'pointer',
background: 'transparent',
boxShadow: '0px 0px 0px transparent',
border: '0px solid transparent',
textShadow: '0px 0px 0px transparent',
outline: 'none'
},
hasUnread: {
position: 'absolute',
top: 5,
left: 185,
width: '9px',
height: '9px',
backgroundColor: secondaryColor,
borderRadius: '50%'
},
popper: {
zIndex: 1
}
}

View file

@ -24,7 +24,7 @@ import {
TransactionsList,
ComplianceDetails
} from './components'
import { /* getFormattedPhone, */ getName } from './helper'
import { getFormattedPhone, getName } from './helper'
const useStyles = makeStyles(styles)
@ -147,13 +147,12 @@ const CustomerProfile = memo(() => {
Customers
</Label1>
<Label2 noMargin className={classes.labelLink}>
{name.length ? name : R.path(['phone'])(customerData)}
{/* {name.length
{name.length
? name
: getFormattedPhone(
R.path(['phone'])(customerData),
locale.country
)} */}
)}
</Label2>
</Breadcrumbs>
<div>

View file

@ -11,11 +11,7 @@ import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-ou
import { ifNotNull } from 'src/utils/nullCheck'
import styles from './CustomersList.styles'
import {
getAuthorizedStatus,
getName
/* getFormattedPhone */
} from './helper'
import { getAuthorizedStatus, getFormattedPhone, getName } from './helper'
const useStyles = makeStyles(styles)
@ -26,7 +22,7 @@ const CustomersList = ({ data, locale, onClick, loading }) => {
{
header: 'Phone',
width: 172,
view: it => it.phone // getFormattedPhone(it.phone, locale.country)
view: it => getFormattedPhone(it.phone, locale.country)
},
{
header: 'Name',

View file

@ -9,7 +9,7 @@ import { ReactComponent as LawIconInverse } from 'src/styling/icons/circle butto
import { ReactComponent as LawIcon } from 'src/styling/icons/circle buttons/law/zodiac.svg'
import mainStyles from '../CustomersList.styles'
import { /* getFormattedPhone, */ getName } from '../helper'
import { getFormattedPhone, getName } from '../helper'
import FrontCameraPhoto from './FrontCameraPhoto'
@ -22,7 +22,7 @@ const CustomerDetails = memo(({ customer, locale, setShowCompliance }) => {
{
header: 'Phone number',
size: 172,
value: customer.phone // getFormattedPhone(customer.phone, locale.country)
value: getFormattedPhone(customer.phone, locale.country)
},
{
header: 'ID number',
@ -47,8 +47,9 @@ const CustomerDetails = memo(({ customer, locale, setShowCompliance }) => {
<div className={classes.name}>
<IdIcon className={classes.idIcon} />
<H2 noMargin>
{name.length ? name : R.path(['phone'])(customer)}
{/* getFormattedPhone(R.path(['phone'])(customer), locale.country)} */}
{name.length
? name
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
</H2>
<SubpageButton
className={classes.subpageButton}

View file

@ -11,9 +11,10 @@ const getAuthorizedStatus = it =>
: { label: 'Authorized', type: 'success' }
const getFormattedPhone = (phone, country) => {
return phone && country
? parsePhoneNumberFromString(phone, country).formatInternational()
: ''
const phoneNumber =
phone && country ? parsePhoneNumberFromString(phone, country) : null
return phoneNumber ? phoneNumber.formatInternational() : phone
}
const getName = it => {

View file

@ -9,8 +9,8 @@ import { transformNumber } from 'src/utils/number'
import NotificationsCtx from '../NotificationsContext'
const HIGH_BALANCE_KEY = 'cryptoHighBalance'
const LOW_BALANCE_KEY = 'cryptoLowBalance'
const HIGH_BALANCE_KEY = 'highBalance'
const LOW_BALANCE_KEY = 'lowBalance'
const CRYPTOCURRENCY_KEY = 'cryptoCurrency'
const NAME = 'cryptoBalanceOverrides'

View file

@ -11,6 +11,18 @@ export default {
width: fill,
minHeight: fill
},
'.root-notifcenter-open': {
// for when notification center is open
overflowY: 'scroll',
position: 'absolute',
top: 0,
bottom: 0,
left: 0
},
'.body-notifcenter-open': {
// for when notification center is open
overflow: 'hidden'
},
html: {
height: fill
},

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="↳-notification-center" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="notification-center_v01a#2-(open)" transform="translate(-1023.000000, -459.000000)" stroke="#1B2559">
<g id="Group-5" transform="translate(1000.000000, 0.000000)">
<g id="icon/sf-small/wrench" transform="translate(24.000000, 460.000000)">
<path d="M15.7602493,3.10720971 L13.1962412,5.67121772 L10.3290323,5.67121772 L10.3290323,2.80400876 L12.8930403,0.24000075 C12.4378389,0.0872002725 11.9506373,0 11.4434358,0 C8.9282279,0 6.88822153,2.04000637 6.88822153,4.55681424 C6.88822153,5.08081588 6.98102182,5.58321745 7.14422233,6.05201891 L0.580201813,12.6168394 C-0.193400604,13.3904418 -0.193400604,14.6456458 0.580201813,15.4200482 C1.35460423,16.1936506 2.60980816,16.1936506 3.38341057,15.4200482 L9.94823109,8.85602767 C10.4170326,9.01922818 10.9186341,9.11202847 11.4434358,9.11202847 C13.9602436,9.11202847 16.00025,7.0720221 16.00025,4.55681424 C16.00025,4.04961265 15.9130497,3.56241113 15.7602493,3.10720971 Z" id="Stroke-1"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="↳-notification-center" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="notification-center_v01a#1-(marked-one-as-read)" transform="translate(-1023.000000, -123.000000)" stroke="#1B2559">
<g id="Group-5" transform="translate(1000.000000, 0.000000)">
<g id="Group-4" transform="translate(24.000000, 124.000000)">
<g id="Group-3">
<line x1="0" y1="4" x2="16" y2="4" id="Path-2"></line>
<polyline id="Path-3" points="12 0 16 4 12 8"></polyline>
</g>
<g id="Group-2" transform="translate(8.000000, 12.000000) scale(-1, 1) translate(-8.000000, -12.000000) translate(0.000000, 8.000000)">
<line x1="0" y1="4" x2="16" y2="4" id="Path-2-Copy"></line>
<polyline id="Path-3-Copy" points="12 0 16 4 12 8"></polyline>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="nav-/-primary-/-1440" transform="translate(-1295.000000, -19.000000)" stroke="#1B2559" stroke-width="2">
<g id="icon/menu/notification" transform="translate(1296.000000, 20.000000)">
<path d="M10.1052632,14.3157895 C10.1052632,15.2454737 9.35073684,16 8.42105263,16 C7.49136842,16 6.73684211,15.2454737 6.73684211,14.3157895" id="Stroke-1"></path>
<path d="M1.6,14.3157895 C0.7168,14.3157895 0,13.6031813 0,12.7251462 C0,11.8471111 0.7168,11.1345029 1.6,11.1345029 L1.6,6.3625731 C1.6,2.84884211 4.4656,0 8,0 C11.5344,0 14.4,2.84884211 14.4,6.3625731 L14.4,11.1345029 C15.2832,11.1345029 16,11.8471111 16,12.7251462 C16,13.6031813 15.2832,14.3157895 14.4,14.3157895 L1.6,14.3157895 Z" id="Stroke-3" stroke-linejoin="round"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
<desc>Created with Sketch.</desc>
<g id="icon/stage/zodiac/full" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<circle id="Oval-2-Copy" fill="#1B2559" cx="9" cy="9" r="8"></circle>
<circle id="Oval-Copy-5" stroke="#1B2559" stroke-width="2" transform="translate(9.000000, 9.000000) rotate(-270.000000) translate(-9.000000, -9.000000) " cx="9" cy="9" r="8"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 671 B