Feat: add notification center row in notification settings table

This commit is contained in:
Cesar 2021-01-20 12:22:02 +00:00 committed by Josh Harvey
parent 1ab4b68168
commit 34f2b84fe2
8 changed files with 109 additions and 69 deletions

View file

@ -3,7 +3,8 @@ const axios = require('axios')
const db = require('./db') const db = require('./db')
const pairing = require('./pairing') const pairing = require('./pairing')
const notifier = require('./notifier') const checkPings = require('./notifier').checkPings
const checkStuckScreen = require('./notifier').checkStuckScreen
const dbm = require('./postgresql_interface') const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager') const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader') const settingsLoader = require('./new-settings-loader')
@ -41,7 +42,7 @@ function getMachineNames (config) {
return Promise.all([getMachines(), getConfig(config)]) return Promise.all([getMachines(), getConfig(config)])
.then(([machines, config]) => Promise.all( .then(([machines, config]) => Promise.all(
[machines, notifier.checkPings(machines), dbm.machineEvents(), config] [machines, checkPings(machines), dbm.machineEvents(), config]
)) ))
.then(([machines, pings, events, config]) => { .then(([machines, pings, events, config]) => {
const getStatus = (ping, stuck) => { const getStatus = (ping, stuck) => {
@ -60,7 +61,7 @@ function getMachineNames (config) {
const statuses = [ const statuses = [
getStatus( getStatus(
_.first(pings[r.deviceId]), _.first(pings[r.deviceId]),
_.first(notifier.checkStuckScreen(events, r.name)) _.first(checkStuckScreen(events, r.name))
) )
] ]

View file

@ -22,6 +22,7 @@ const ALERT_SEND_INTERVAL = T.hour
const NOTIFICATION_TYPES = { const NOTIFICATION_TYPES = {
HIGH_VALUE_TX: 'highValueTransaction', HIGH_VALUE_TX: 'highValueTransaction',
NORMAL_VALUE_TX: 'transaction',
FIAT_BALANCE: 'fiatBalance', FIAT_BALANCE: 'fiatBalance',
CRYPTO_BALANCE: 'cryptoBalance', CRYPTO_BALANCE: 'cryptoBalance',
COMPLIANCE: 'compliance', COMPLIANCE: 'compliance',

View file

@ -2,7 +2,6 @@ const _ = require('lodash/fp')
const configManager = require('../new-config-manager') const configManager = require('../new-config-manager')
const logger = require('../logger') const logger = require('../logger')
const machineLoader = require('../machine-loader')
const queries = require('./queries') const queries = require('./queries')
const settingsLoader = require('../new-settings-loader') const settingsLoader = require('../new-settings-loader')
const customers = require('../customers') const customers = require('../customers')
@ -10,14 +9,17 @@ const customers = require('../customers')
const utils = require('./utils') const utils = require('./utils')
const emailFuncs = require('./email') const emailFuncs = require('./email')
const smsFuncs = require('./sms') const smsFuncs = require('./sms')
const codes = require('./codes')
const { STALE, STALE_STATE, PING } = require('./codes') const { STALE, STALE_STATE, PING } = require('./codes')
const { NOTIFICATION_TYPES: { const { NOTIFICATION_TYPES: {
HIGH_VALUE_TX, HIGH_VALUE_TX,
NORMAL_VALUE_TX,
FIAT_BALANCE, FIAT_BALANCE,
CRYPTO_BALANCE, CRYPTO_BALANCE,
COMPLIANCE, COMPLIANCE,
ERROR } ERROR }
} = require('./codes') } = codes
function buildMessage (alerts, notifications) { function buildMessage (alerts, notifications) {
const smsEnabled = utils.isActive(notifications.sms) const smsEnabled = utils.isActive(notifications.sms)
@ -50,7 +52,7 @@ function checkNotification (plugins) {
return getAlerts(plugins) return getAlerts(plugins)
.then(alerts => { .then(alerts => {
errorAlertsNotify(alerts) notifyIfActive('errors', alerts).catch(console.error)
const currentAlertFingerprint = utils.buildAlertFingerprint( const currentAlertFingerprint = utils.buildAlertFingerprint(
alerts, alerts,
notifications notifications
@ -81,7 +83,7 @@ function getAlerts (plugins) {
queries.machineEvents(), queries.machineEvents(),
plugins.getMachineNames() plugins.getMachineNames()
]).then(([balances, events, devices]) => { ]).then(([balances, events, devices]) => {
balancesNotify(balances) notifyIfActive('balance', balances).catch(console.error)
return buildAlerts(checkPings(devices), balances, events, devices) return buildAlerts(checkPings(devices), balances, events, devices)
}) })
} }
@ -136,17 +138,24 @@ function checkStuckScreen (deviceEvents, machineName) {
return [] return []
} }
function notifCenterTransactionNotify (isHighValue, direction, fiat, fiatCode, deviceId, cryptoAddress) {
const messageSuffix = isHighValue ? 'High value' : ''
const message = `${messageSuffix} ${fiat} ${fiatCode} ${direction} transaction`
const detailB = utils.buildDetail({ deviceId: deviceId, direction, fiat, fiatCode, cryptoAddress })
return queries.addNotification(isHighValue ? HIGH_VALUE_TX : NORMAL_VALUE_TX, message, detailB)
}
function transactionNotify (tx, rec) { function transactionNotify (tx, rec) {
return settingsLoader.loadLatest().then(settings => { return settingsLoader.loadLatest().then(settings => {
const notifSettings = configManager.getGlobalNotifications(settings.config) const notifSettings = configManager.getGlobalNotifications(settings.config)
const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity) const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity)
const isCashOut = tx.direction === 'cashOut' const isCashOut = tx.direction === 'cashOut'
// high value tx on database
if (highValueTx && (tx.direction === 'cashIn' || (tx.direction === 'cashOut' && rec.isRedemption))) { // for notification center
const direction = tx.direction === 'cashOut' ? 'cash-out' : 'cash-in' const directionDisplay = tx.direction === 'cashOut' ? 'cash-out' : 'cash-in'
const message = `${tx.fiat} ${tx.fiatCode} ${direction} transaction` const readyToNotify = tx.direction === 'cashIn' || (tx.direction === 'cashOut' && rec.isRedemption)
const detailB = utils.buildDetail({ deviceId: tx.deviceId, direction, fiat: tx.fiat, fiatCode: tx.fiatCode, cryptoAddress: tx.toAddress }) if (readyToNotify) {
queries.addNotification(HIGH_VALUE_TX, message, detailB) notifyIfActive('transactions', highValueTx, directionDisplay, tx.fiat, tx.fiatCode, tx.deviceId, tx.toAddress).catch(console.error)
} }
// alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled // alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled
@ -161,7 +170,7 @@ function transactionNotify (tx, rec) {
if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error) if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error)
return Promise.all([ return Promise.all([
machineLoader.getMachineName(tx.deviceId), queries.getMachineName(tx.deviceId),
customerPromise customerPromise
]).then(([machineName, customer]) => { ]).then(([machineName, customer]) => {
return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer) return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer)
@ -356,6 +365,23 @@ const customerComplianceNotify = (customer, deviceId, code, days = null) => {
.catch(console.error) .catch(console.error)
} }
const notificationCenterFunctions = {
'compliance': customerComplianceNotify,
'balance': balancesNotify,
'errors': errorAlertsNotify,
'transactions': notifCenterTransactionNotify
}
// for notification center, check if type of notification is active before calling the respective notify function
const notifyIfActive = (type, ...args) => {
return settingsLoader.loadLatest().then(settings => {
const notificationSettings = configManager.getGlobalNotifications(settings.config).notificationCenter
if (!notificationCenterFunctions[type]) return Promise.reject(new Error(`Notification of type ${type} does not exist`))
if (!(notificationSettings.active && notificationSettings[type])) return Promise.resolve()
return notificationCenterFunctions[type](...args)
})
}
module.exports = { module.exports = {
transactionNotify, transactionNotify,
checkNotification, checkNotification,
@ -363,6 +389,6 @@ module.exports = {
checkStuckScreen, checkStuckScreen,
sendRedemptionMessage, sendRedemptionMessage,
blacklistNotify, blacklistNotify,
customerComplianceNotify, clearBlacklistNotification,
clearBlacklistNotification notifyIfActive
} }

View file

@ -14,75 +14,82 @@ compliance - notifications related to warnings triggered by compliance settings
error - notifications related to errors error - notifications related to errors
*/ */
function getMachineName (machineId) {
const sql = 'SELECT * FROM devices WHERE device_id=$1'
return db.oneOrNone(sql, [machineId])
.then(it => it.name)
}
const addNotification = (type, message, detail) => { const addNotification = (type, message, detail) => {
const sql = `INSERT INTO notifications (id, type, message, detail) values ($1, $2, $3, $4)` const sql = `INSERT INTO notifications (id, type, message, detail) values ($1, $2, $3, $4)`
return db.oneOrNone(sql, [uuidv4(), type, message, detail]) return db.oneOrNone(sql, [uuidv4(), type, message, detail])
} }
const getAllValidNotifications = (type) => { const getAllValidNotifications = (type) => {
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'` const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'`
return db.any(sql, [type]) return db.any(sql, [type])
} }
const invalidateNotification = (detail, type) => { const invalidateNotification = (detail, type) => {
detail = _.omitBy(_.isEmpty, detail) detail = _.omitBy(_.isEmpty, detail)
const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND type = $1 AND detail::jsonb @> $2::jsonb` const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND type = $1 AND detail::jsonb @> $2::jsonb`
return db.none(sql, [type, detail]) return db.none(sql, [type, detail])
} }
const batchInvalidate = (ids) => { const batchInvalidate = (ids) => {
const formattedIds = _.map(pgp.as.text, ids).join(',') const formattedIds = _.map(pgp.as.text, ids).join(',')
const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE id IN ($1^)` const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE id IN ($1^)`
return db.none(sql, [formattedIds]) return db.none(sql, [formattedIds])
} }
const clearBlacklistNotification = (cryptoCode, cryptoAddress) => { const clearBlacklistNotification = (cryptoCode, cryptoAddress) => {
const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE type = 'compliance' AND detail->>'cryptoCode' = $1 AND detail->>'cryptoAddress' = $2 AND (detail->>'code' = 'BLOCKED' OR detail->>'code' = 'REUSED')` const sql = `UPDATE notifications SET valid = 'f', read = 't' 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]) return db.none(sql, [cryptoCode, cryptoAddress])
} }
const getValidNotifications = (type, detail) => { const getValidNotifications = (type, detail) => {
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail @> $2` const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail @> $2`
return db.any(sql, [type, detail]) return db.any(sql, [type, detail])
} }
const getNotifications = () => { const getNotifications = () => {
const sql = `SELECT * FROM notifications ORDER BY created DESC` const sql = `SELECT * FROM notifications ORDER BY created DESC`
return db.any(sql) return db.any(sql)
} }
const markAsRead = (id) => { const markAsRead = (id) => {
const sql = `UPDATE notifications SET read = 't' WHERE id = $1` const sql = `UPDATE notifications SET read = 't' WHERE id = $1`
return db.none(sql, [id]) return db.none(sql, [id])
} }
const markAllAsRead = () => { const markAllAsRead = () => {
const sql = `UPDATE notifications SET read = 't'` const sql = `UPDATE notifications SET read = 't'`
return db.none(sql) return db.none(sql)
} }
const hasUnreadNotifications = () => { const hasUnreadNotifications = () => {
const sql = `SELECT EXISTS (SELECT 1 FROM notifications WHERE read = 'f' LIMIT 1)` const sql = `SELECT EXISTS (SELECT 1 FROM notifications WHERE read = 'f' LIMIT 1)`
return db.oneOrNone(sql).then(res => res.exists) return db.oneOrNone(sql).then(res => res.exists)
} }
const getAlerts = () => { const getAlerts = () => {
const types = ['fiatBalance', 'cryptoBalance', 'error'] const types = ['fiatBalance', 'cryptoBalance', 'error']
const sql = `SELECT * FROM notifications WHERE valid = 't' AND type IN ($1:list) ORDER BY created DESC` const sql = `SELECT * FROM notifications WHERE valid = 't' AND type IN ($1:list) ORDER BY created DESC`
return db.any(sql, [types]) return db.any(sql, [types])
} }
module.exports = { module.exports = {
machineEvents: dbm.machineEvents, machineEvents: dbm.machineEvents,
addNotification, addNotification,
getAllValidNotifications, getAllValidNotifications,
invalidateNotification, invalidateNotification,
batchInvalidate, batchInvalidate,
clearBlacklistNotification, clearBlacklistNotification,
getValidNotifications, getValidNotifications,
getNotifications, getNotifications,
markAsRead, markAsRead,
markAllAsRead, markAllAsRead,
hasUnreadNotifications, hasUnreadNotifications,
getAlerts getAlerts,
getMachineName
} }

View file

@ -343,7 +343,7 @@ function triggerBlock (req, res, next) {
customers.update(id, { authorizedOverride: 'blocked' }) customers.update(id, { authorizedOverride: 'blocked' })
.then(customer => { .then(customer => {
notifier.customerComplianceNotify(customer, req.deviceId, 'BLOCKED') notifier.notifyIfActive('compliance', customer, req.deviceId, 'BLOCKED').catch(console.error)
return respond(req, res, { customer }) return respond(req, res, { customer })
}) })
.catch(next) .catch(next)
@ -362,7 +362,7 @@ function triggerSuspend (req, res, next) {
date.setDate(date.getDate() + days); date.setDate(date.getDate() + days);
customers.update(id, { suspendedUntil: date }) customers.update(id, { suspendedUntil: date })
.then(customer => { .then(customer => {
notifier.customerComplianceNotify(customer, req.deviceId, 'SUSPENDED', days) notifier.notifyIfActive('compliance', customer, req.deviceId, 'SUSPENDED', days).catch(console.error)
return respond(req, res, { customer }) return respond(req, res, { customer })
}) })
.catch(next) .catch(next)

View file

@ -4,6 +4,7 @@ const singleQuotify = (item) => `'${item}'`
var types = [ var types = [
'highValueTransaction', 'highValueTransaction',
'transaction',
'fiatBalance', 'fiatBalance',
'cryptoBalance', 'cryptoBalance',
'compliance', 'compliance',

View file

@ -2,6 +2,7 @@ import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames' import classnames from 'classnames'
import prettyMs from 'pretty-ms' import prettyMs from 'pretty-ms'
import * as R from 'ramda'
import React from 'react' import React from 'react'
import { Label1, Label2, TL2 } from 'src/components/typography' import { Label1, Label2, TL2 } from 'src/components/typography'
@ -14,6 +15,7 @@ import styles from './NotificationCenter.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const types = { const types = {
transaction: { display: 'Transactions', icon: <Transaction /> },
highValueTransaction: { display: 'Transactions', icon: <Transaction /> }, highValueTransaction: { display: 'Transactions', icon: <Transaction /> },
fiatBalance: { display: 'Maintenance', icon: <Wrench /> }, fiatBalance: { display: 'Maintenance', icon: <Wrench /> },
cryptoBalance: { display: 'Maintenance', icon: <Wrench /> }, cryptoBalance: { display: 'Maintenance', icon: <Wrench /> },
@ -34,15 +36,18 @@ const NotificationRow = ({
}) => { }) => {
const classes = useStyles() const classes = useStyles()
const buildType = () => { const typeDisplay = R.path([type, 'display'])(types) ?? null
return types[type].display const icon = R.path([type, 'icon'])(types) ?? <Wrench />
} const age = prettyMs(new Date().getTime() - new Date(created).getTime(), {
compact: true,
const buildAge = () => { verbose: true
const createdDate = new Date(created) })
const interval = +new Date() - createdDate const notificationTitle =
return prettyMs(interval, { compact: true, verbose: true }) typeDisplay && deviceName
} ? `${typeDisplay} - ${deviceName}`
: !typeDisplay && deviceName
? `${deviceName}`
: `${typeDisplay}`
return ( return (
<Grid <Grid
@ -52,21 +57,19 @@ const NotificationRow = ({
!read && valid ? classes.unread : '' !read && valid ? classes.unread : ''
)}> )}>
<Grid item xs={2} className={classes.notificationRowIcon}> <Grid item xs={2} className={classes.notificationRowIcon}>
{types[type].icon} {icon}
</Grid> </Grid>
<Grid item container xs={7} direction="row"> <Grid item container xs={7} direction="row">
<Grid item xs={12}> <Grid item xs={12}>
<Label2 className={classes.notificationTitle}> <Label2 className={classes.notificationTitle}>
{`${buildType()} ${deviceName ? '- ' + deviceName : ''}`} {notificationTitle}
</Label2> </Label2>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TL2 className={classes.notificationBody}>{message}</TL2> <TL2 className={classes.notificationBody}>{message}</TL2>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Label1 className={classes.notificationSubtitle}> <Label1 className={classes.notificationSubtitle}>{age}</Label1>
{buildAge(created)}
</Label1>
</Grid> </Grid>
</Grid> </Grid>
<Grid item xs={3} style={{ zIndex: 1 }}> <Grid item xs={3} style={{ zIndex: 1 }}>

View file

@ -87,6 +87,7 @@ const Setup = ({ wizard, forceDisable }) => {
<TBody> <TBody>
<Row namespace="email" forceDisable={forceDisable} /> <Row namespace="email" forceDisable={forceDisable} />
<Row namespace="sms" forceDisable={forceDisable} /> <Row namespace="sms" forceDisable={forceDisable} />
<Row namespace="notificationCenter" forceDisable={forceDisable} />
</TBody> </TBody>
</Table> </Table>
) )