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 = {
Query: {
SMSNotices: () => customSms.getSMSNotices()
SMSNotices: () => smsNotices.getSMSNotices()
},
Mutation: {
editSMSNotice: (...[, { id, event, message }]) => customSms.editSMSNotice(id, event, message),
enableSMSNotice: (...[, { id }]) => customSms.enableSMSNotice(id),
disableSMSNotice: (...[, { id }]) => customSms.disableSMSNotice(id)
editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message),
enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id)
}
}

View file

@ -772,7 +772,8 @@ function plugins (settings, deviceId) {
? '123'
: 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 => {
const rec = {
sms: smsObj

View file

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

View file

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

View file

@ -36,12 +36,12 @@ const getSMSNotice = event => {
}
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])
}
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])
}

View file

@ -1,17 +1,16 @@
const dateFormat = require('dateformat')
const ph = require('./plugin-helper')
const argv = require('minimist')(process.argv.slice(2))
const { utils: coinUtils } = require('lamassu-coins')
const _ = require('lodash/fp')
const customSms = require('./sms-notices')
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}]`
})
const smsNotices = require('./sms-notices')
const { RECEIPT } = require('./constants')
function getSms (event, phone, content) {
return customSms.getCustomMessage(event)
return smsNotices.getSMSNotice(event)
.then(msg => {
if (!_.isNil(msg)) {
var accMsg = msg.message
@ -22,11 +21,6 @@ function getSms (event, phone, content) {
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`)
}
const request = {
sms: {
toNumber: data.customerPhone,
body: message
}
}
return request
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
const postReceiptSmsPromise = getSms(RECEIPT, data.customerPhone, { timestamp })
return Promise.all([smsNotices.getSMSNotice(RECEIPT), postReceiptSmsPromise])
.then(([res, postReceiptSms]) => ({
sms: {
toNumber: data.customerPhone,
body: res.enabled ? message.concat('\n\n', postReceiptSms.body) : message
}
}))
}
module.exports = {

View file

@ -14,6 +14,7 @@ exports.up = function (next) {
db.multi(sql, next)
.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('sms_receipt', 'SMS receipt', '', true, true))
}
exports.down = function (next) {

View file

@ -6,11 +6,14 @@ import * as R from 'ramda'
import React, { useState } from 'react'
import { DeleteDialog } from 'src/components/DeleteDialog'
import { HoverableTooltip } from 'src/components/Tooltip'
import { IconButton } from 'src/components/buttons'
import { Switch } from 'src/components/inputs'
import DataTable from 'src/components/tables/DataTable'
import { H4, P, Label3 } from 'src/components/typography'
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 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 classes = useStyles(coords)
@ -78,7 +101,13 @@ const SMSPreview = ({ sms, coords }) => {
<WhiteLogo width={22} height={22} />
</div>
<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>
<Label3>{format('HH:mm', new Date())}</Label3>
</div>
@ -118,8 +147,8 @@ const SMSNotices = () => {
const loading = messagesLoading
const handleClose = () => {
setSelectedSMS(null)
setShowModal(false)
setSelectedSMS(null)
setDeleteDialog(false)
}
@ -129,7 +158,17 @@ const SMSNotices = () => {
width: 500,
size: 'sm',
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',
@ -180,9 +219,15 @@ const SMSNotices = () => {
5 -
e.currentTarget.getBoundingClientRect().bottom
})
setPreviewOpen(true)
R.equals(selectedSMS, it)
? setPreviewOpen(!previewOpen)
: setPreviewOpen(true)
}}>
<EditIcon />
{R.equals(selectedSMS, it) && previewOpen ? (
<ExpandIconOpen />
) : (
<ExpandIconClosed />
)}
</IconButton>
)
}

View file

@ -1,4 +1,9 @@
import { spacer } from 'src/styling/variables'
import {
spacer,
fontMonospaced,
fontSize5,
fontColor
} from 'src/styling/variables'
const styles = {
header: {
@ -52,6 +57,38 @@ const styles = {
width: 225,
padding: 15,
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 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 { 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'
@ -21,7 +25,7 @@ const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
return null
}
const prefill = {
const PREFILL = {
smsCode: {
validator: Yup.string()
.required('The message content is required!')
@ -43,24 +47,28 @@ const prefill = {
validator: Yup.string()
.required('The message content is required!')
.trim()
// .test({
// name: 'has-timestamp-tag',
// message: 'A #timestamp tag is missing from the message!',
// 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
// })
},
smsReceipt: {
validator: Yup.string().trim()
}
}
const chips = {
smsCode: [{ code: '#code', display: 'Confirmation code', removable: false }],
cashOutDispenseReady: []
const CHIPS = {
smsCode: [
{ 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 = ({
@ -80,7 +88,7 @@ const SMSNoticesModal = ({
const validationSchema = Yup.object().shape({
event: Yup.string().required('An event is required!'),
message:
prefill[sms?.event]?.validator ??
PREFILL[sms?.event]?.validator ??
Yup.string()
.required('The message content is required!')
.trim()
@ -108,7 +116,7 @@ const SMSNoticesModal = ({
<>
{showModal && (
<Modal
title={`Edit SMS notice`}
title={`SMS notice - ${sms?.messageName}`}
closeOnBackdropClick={true}
width={600}
height={500}
@ -124,6 +132,17 @@ const SMSNoticesModal = ({
}>
{({ values, errors, touched, setFieldValue }) => (
<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
name="message"
label="Message content"
@ -132,22 +151,39 @@ const SMSNoticesModal = ({
rows={6}
component={TextInput}
/>
{R.map(
it => (
<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]
{R.length(CHIPS[sms?.event]) > 0 && (
<Info2 noMargin>Values to attach</Info2>
)}
<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}>
{getErrorMsg(errors, touched, creationError) && (
<ErrorMessage>