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:
parent
91e209b6ab
commit
cd01d894a3
10 changed files with 186 additions and 69 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
||||
|
|
|
|||
35
lib/sms.js
35
lib/sms.js
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue