feat: add sms preview

feat: add reset to default button
feat: add attachable values
feat: connect the sms receipt notice to the sms receipt request
This commit is contained in:
Sérgio Salgado 2022-02-10 00:36:32 +00:00
parent 91e209b6ab
commit cd01d894a3
10 changed files with 186 additions and 69 deletions

View file

@ -1,13 +1,13 @@
const customSms = require('../../../sms-notices') const smsNotices = require('../../../sms-notices')
const resolvers = { const resolvers = {
Query: { Query: {
SMSNotices: () => customSms.getSMSNotices() SMSNotices: () => smsNotices.getSMSNotices()
}, },
Mutation: { Mutation: {
editSMSNotice: (...[, { id, event, message }]) => customSms.editSMSNotice(id, event, message), editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message),
enableSMSNotice: (...[, { id }]) => customSms.enableSMSNotice(id), enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
disableSMSNotice: (...[, { id }]) => customSms.disableSMSNotice(id) disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id)
} }
} }

View file

@ -772,7 +772,8 @@ function plugins (settings, deviceId) {
? '123' ? '123'
: randomCode() : randomCode()
return sms.getSms(CONFIRMATION_CODE, phone, { code }) const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
return sms.getSms(CONFIRMATION_CODE, phone, { code, timestamp })
.then(smsObj => { .then(smsObj => {
const rec = { const rec = {
sms: smsObj sms: smsObj

View file

@ -9,7 +9,7 @@ const NAME = 'FakeWallet'
const SECONDS = 1000 const SECONDS = 1000
const PUBLISH_TIME = 3 * SECONDS const PUBLISH_TIME = 3 * SECONDS
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
const CONFIRM_TIME = AUTHORIZE_TIME + 120 * SECONDS const CONFIRM_TIME = AUTHORIZE_TIME + 10 * SECONDS
let t0 let t0

View file

@ -190,6 +190,6 @@ router.patch('/:id/block', triggerBlock)
router.patch('/:id/suspend', triggerSuspend) router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData) router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto) router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
router.patch('/:id/smsreceipt', sendSmsReceipt) router.post('/:id/smsreceipt', sendSmsReceipt)
module.exports = router module.exports = router

View file

@ -36,12 +36,12 @@ const getSMSNotice = event => {
} }
const enableSMSNotice = id => { const enableSMSNotice = id => {
const sql = `UPDATE sms_notices SET enabled = true WHERE id=$1 LIMIT 1` const sql = `UPDATE sms_notices SET enabled = true WHERE id=$1`
return db.oneOrNone(sql, [id]) return db.oneOrNone(sql, [id])
} }
const disableSMSNotice = id => { const disableSMSNotice = id => {
const sql = `UPDATE sms_notices SET enabled = false WHERE id=$1 LIMIT 1` const sql = `UPDATE sms_notices SET enabled = false WHERE id=$1`
return db.oneOrNone(sql, [id]) return db.oneOrNone(sql, [id])
} }

View file

@ -1,17 +1,16 @@
const dateFormat = require('dateformat')
const ph = require('./plugin-helper') const ph = require('./plugin-helper')
const argv = require('minimist')(process.argv.slice(2)) const argv = require('minimist')(process.argv.slice(2))
const { utils: coinUtils } = require('lamassu-coins') const { utils: coinUtils } = require('lamassu-coins')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const customSms = require('./sms-notices') const smsNotices = require('./sms-notices')
const { RECEIPT } = require('./constants')
const getDefaultMessageContent = content => ({
smsCode: `Your cryptomat code: ${content.code}`,
cashOutDispenseReady: `Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [${content.timestamp}]`
})
function getSms (event, phone, content) { function getSms (event, phone, content) {
return customSms.getCustomMessage(event) return smsNotices.getSMSNotice(event)
.then(msg => { .then(msg => {
if (!_.isNil(msg)) { if (!_.isNil(msg)) {
var accMsg = msg.message var accMsg = msg.message
@ -22,11 +21,6 @@ function getSms (event, phone, content) {
body: messageContent body: messageContent
} }
} }
return {
toNumber: phone,
body: getDefaultMessageContent(content)[_.camelCase(event)]
}
}) })
} }
@ -110,13 +104,16 @@ function formatSmsReceipt (data, options) {
message = message.concat(`Address: ${data.address}\n`) message = message.concat(`Address: ${data.address}\n`)
} }
const request = { const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
sms: { const postReceiptSmsPromise = getSms(RECEIPT, data.customerPhone, { timestamp })
toNumber: data.customerPhone,
body: message return Promise.all([smsNotices.getSMSNotice(RECEIPT), postReceiptSmsPromise])
} .then(([res, postReceiptSms]) => ({
} sms: {
return request toNumber: data.customerPhone,
body: res.enabled ? message.concat('\n\n', postReceiptSms.body) : message
}
}))
} }
module.exports = { module.exports = {

View file

@ -14,6 +14,7 @@ exports.up = function (next) {
db.multi(sql, next) db.multi(sql, next)
.then(() => smsNotices.createSMSNotice('sms_code', 'SMS confirmation code', 'Your cryptomat code: #code', true, false)) .then(() => smsNotices.createSMSNotice('sms_code', 'SMS confirmation code', 'Your cryptomat code: #code', true, false))
.then(() => smsNotices.createSMSNotice('cash_out_dispense_ready', 'Cash is ready', 'Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [#timestamp]', true, false)) .then(() => smsNotices.createSMSNotice('cash_out_dispense_ready', 'Cash is ready', 'Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [#timestamp]', true, false))
.then(() => smsNotices.createSMSNotice('sms_receipt', 'SMS receipt', '', true, true))
} }
exports.down = function (next) { exports.down = function (next) {

View file

@ -6,11 +6,14 @@ import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import { DeleteDialog } from 'src/components/DeleteDialog' import { DeleteDialog } from 'src/components/DeleteDialog'
import { HoverableTooltip } from 'src/components/Tooltip'
import { IconButton } from 'src/components/buttons' import { IconButton } from 'src/components/buttons'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { H4, P, Label3 } from 'src/components/typography' import { H4, P, Label3 } from 'src/components/typography'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as ExpandIconClosed } from 'src/styling/icons/action/expand/closed.svg'
import { ReactComponent as ExpandIconOpen } from 'src/styling/icons/action/expand/open.svg'
import { ReactComponent as WhiteLogo } from 'src/styling/icons/menu/logo-white.svg' import { ReactComponent as WhiteLogo } from 'src/styling/icons/menu/logo-white.svg'
import styles from './SMSNotices.styles' import styles from './SMSNotices.styles'
@ -63,6 +66,26 @@ const multiReplace = (str, obj) => {
}) })
} }
const formatContent = content => {
const fragments = R.split(/\n/)(content)
return R.map((it, idx) => {
if (idx === fragments.length) return <>{it}</>
return (
<>
{it}
<br />
</>
)
}, fragments)
}
const TOOLTIPS = {
smsCode: ``,
cashOutDispenseReady: ``,
smsReceipt: formatContent(`The contents of this notice will be appended to the end of the SMS receipt sent, and not replace it.\n
To edit the contents of the SMS receipt, please go to the 'Receipt' tab`)
}
const SMSPreview = ({ sms, coords }) => { const SMSPreview = ({ sms, coords }) => {
const classes = useStyles(coords) const classes = useStyles(coords)
@ -78,7 +101,13 @@ const SMSPreview = ({ sms, coords }) => {
<WhiteLogo width={22} height={22} /> <WhiteLogo width={22} height={22} />
</div> </div>
<Paper className={classes.smsPreviewContent}> <Paper className={classes.smsPreviewContent}>
<P noMargin>{multiReplace(sms?.message, matches)}</P> <P noMargin>
{R.isEmpty(sms?.message) ? (
<i>No content available</i>
) : (
formatContent(multiReplace(sms?.message, matches))
)}
</P>
</Paper> </Paper>
<Label3>{format('HH:mm', new Date())}</Label3> <Label3>{format('HH:mm', new Date())}</Label3>
</div> </div>
@ -118,8 +147,8 @@ const SMSNotices = () => {
const loading = messagesLoading const loading = messagesLoading
const handleClose = () => { const handleClose = () => {
setSelectedSMS(null)
setShowModal(false) setShowModal(false)
setSelectedSMS(null)
setDeleteDialog(false) setDeleteDialog(false)
} }
@ -129,7 +158,17 @@ const SMSNotices = () => {
width: 500, width: 500,
size: 'sm', size: 'sm',
textAlign: 'left', textAlign: 'left',
view: it => R.prop('messageName', it) view: it =>
!R.isEmpty(TOOLTIPS[it.event]) ? (
<div className={classes.messageWithTooltip}>
{R.prop('messageName', it)}
<HoverableTooltip width={250}>
<P>{TOOLTIPS[it.event]}</P>
</HoverableTooltip>
</div>
) : (
R.prop('messageName', it)
)
}, },
{ {
header: 'Edit', header: 'Edit',
@ -180,9 +219,15 @@ const SMSNotices = () => {
5 - 5 -
e.currentTarget.getBoundingClientRect().bottom e.currentTarget.getBoundingClientRect().bottom
}) })
setPreviewOpen(true) R.equals(selectedSMS, it)
? setPreviewOpen(!previewOpen)
: setPreviewOpen(true)
}}> }}>
<EditIcon /> {R.equals(selectedSMS, it) && previewOpen ? (
<ExpandIconOpen />
) : (
<ExpandIconClosed />
)}
</IconButton> </IconButton>
) )
} }

View file

@ -1,4 +1,9 @@
import { spacer } from 'src/styling/variables' import {
spacer,
fontMonospaced,
fontSize5,
fontColor
} from 'src/styling/variables'
const styles = { const styles = {
header: { header: {
@ -52,6 +57,38 @@ const styles = {
width: 225, width: 225,
padding: 15, padding: 15,
borderRadius: '15px 15px 15px 0px' borderRadius: '15px 15px 15px 0px'
},
chipButtons: {
width: 480,
display: 'flex',
flexDirection: 'column',
alignItems: 'space-between',
'& > div': {
marginTop: 15
},
'& > div:first-child': {
marginTop: 0
},
'& > div > div': {
margin: [[0, 5, 0, 5]]
},
'& > div > div > span': {
lineHeight: '120%',
color: fontColor,
fontSize: fontSize5,
fontFamily: fontMonospaced,
fontWeight: 500
},
marginLeft: 'auto',
marginRight: 'auto'
},
resetToDefault: {
width: 145
},
messageWithTooltip: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
} }
} }

View file

@ -6,8 +6,12 @@ import * as Yup from 'yup'
import ErrorMessage from 'src/components/ErrorMessage' import ErrorMessage from 'src/components/ErrorMessage'
import Modal from 'src/components/Modal' import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons' import { ActionButton, Button } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/formik' import { TextInput } from 'src/components/inputs/formik'
import { Info2 } from 'src/components/typography'
import { ReactComponent as DefaultIconReverse } from 'src/styling/icons/button/retry/white.svg'
import { ReactComponent as DefaultIcon } from 'src/styling/icons/button/retry/zodiac.svg'
import { zircon } from 'src/styling/variables'
import styles from './SMSNotices.styles' import styles from './SMSNotices.styles'
@ -21,7 +25,7 @@ const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
return null return null
} }
const prefill = { const PREFILL = {
smsCode: { smsCode: {
validator: Yup.string() validator: Yup.string()
.required('The message content is required!') .required('The message content is required!')
@ -43,24 +47,28 @@ const prefill = {
validator: Yup.string() validator: Yup.string()
.required('The message content is required!') .required('The message content is required!')
.trim() .trim()
// .test({ },
// name: 'has-timestamp-tag', smsReceipt: {
// message: 'A #timestamp tag is missing from the message!', validator: Yup.string().trim()
// exclusive: false,
// test: value => value?.match(/#timestamp/g || [])?.length > 0
// })
// .test({
// name: 'has-single-timestamp-tag',
// message: 'There should be a single #timestamp tag!',
// exclusive: false,
// test: value => value?.match(/#timestamp/g || [])?.length === 1
// })
} }
} }
const chips = { const CHIPS = {
smsCode: [{ code: '#code', display: 'Confirmation code', removable: false }], smsCode: [
cashOutDispenseReady: [] { code: '#code', display: 'Confirmation code', obligatory: true },
{ code: '#timestamp', display: 'Timestamp', obligatory: false }
],
cashOutDispenseReady: [
{ code: '#timestamp', display: 'Timestamp', obligatory: false }
],
smsReceipt: [{ code: '#timestamp', display: 'Timestamp', obligatory: false }]
}
const DEFAULT_MESSAGES = {
smsCode: 'Your cryptomat code: #code',
cashOutDispenseReady:
'Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [#timestamp]',
smsReceipt: ''
} }
const SMSNoticesModal = ({ const SMSNoticesModal = ({
@ -80,7 +88,7 @@ const SMSNoticesModal = ({
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
event: Yup.string().required('An event is required!'), event: Yup.string().required('An event is required!'),
message: message:
prefill[sms?.event]?.validator ?? PREFILL[sms?.event]?.validator ??
Yup.string() Yup.string()
.required('The message content is required!') .required('The message content is required!')
.trim() .trim()
@ -108,7 +116,7 @@ const SMSNoticesModal = ({
<> <>
{showModal && ( {showModal && (
<Modal <Modal
title={`Edit SMS notice`} title={`SMS notice - ${sms?.messageName}`}
closeOnBackdropClick={true} closeOnBackdropClick={true}
width={600} width={600}
height={500} height={500}
@ -124,6 +132,17 @@ const SMSNoticesModal = ({
}> }>
{({ values, errors, touched, setFieldValue }) => ( {({ values, errors, touched, setFieldValue }) => (
<Form id="sms-notice" className={classes.form}> <Form id="sms-notice" className={classes.form}>
<ActionButton
color="primary"
Icon={DefaultIcon}
InverseIcon={DefaultIconReverse}
className={classes.resetToDefault}
type="button"
onClick={() =>
setFieldValue('message', DEFAULT_MESSAGES[sms?.event])
}>
Reset to default
</ActionButton>
<Field <Field
name="message" name="message"
label="Message content" label="Message content"
@ -132,22 +151,39 @@ const SMSNoticesModal = ({
rows={6} rows={6}
component={TextInput} component={TextInput}
/> />
{R.map( {R.length(CHIPS[sms?.event]) > 0 && (
it => ( <Info2 noMargin>Values to attach</Info2>
<Chip
label={it.display}
onClick={() => {
R.includes(it.code, values.message)
? setFieldValue('message', values.message)
: setFieldValue(
'message',
values.message.concat(' ', it.code, ' ')
)
}}
/>
),
chips[sms?.event]
)} )}
<div className={classes.chipButtons}>
{R.map(
it => (
<div>
{R.map(
ite => (
<Chip
label={ite.display}
size="small"
style={{ backgroundColor: zircon }}
disabled={R.includes(ite.code, values.message)}
className={classes.chip}
onClick={() => {
setFieldValue(
'message',
values.message.concat(
R.last(values.message) === ' ' ? '' : ' ',
ite.code
)
)
}}
/>
),
it
)}
</div>
),
R.splitEvery(3, CHIPS[sms?.event])
)}
</div>
<div className={classes.footer}> <div className={classes.footer}>
{getErrorMsg(errors, touched, creationError) && ( {getErrorMsg(errors, touched, creationError) && (
<ErrorMessage> <ErrorMessage>