From cd01d894a3ac35a06c20594cfb8296561ca5360c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Thu, 10 Feb 2022 00:36:32 +0000 Subject: [PATCH] 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 --- .../graphql/resolvers/sms.resolver.js | 10 +- lib/plugins.js | 3 +- lib/plugins/wallet/mock-wallet/mock-wallet.js | 2 +- lib/routes/customerRoutes.js | 2 +- lib/sms-notices.js | 4 +- lib/sms.js | 35 +++--- ...3996603839-change-custom-sms-to-notices.js | 1 + .../OperatorInfo/SMSNotices/SMSNotices.js | 55 ++++++++- .../SMSNotices/SMSNotices.styles.js | 39 ++++++- .../SMSNotices/SMSNoticesModal.js | 104 ++++++++++++------ 10 files changed, 186 insertions(+), 69 deletions(-) diff --git a/lib/new-admin/graphql/resolvers/sms.resolver.js b/lib/new-admin/graphql/resolvers/sms.resolver.js index 3709395d..8098837b 100644 --- a/lib/new-admin/graphql/resolvers/sms.resolver.js +++ b/lib/new-admin/graphql/resolvers/sms.resolver.js @@ -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) } } diff --git a/lib/plugins.js b/lib/plugins.js index 9e367387..7603bbb4 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -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 diff --git a/lib/plugins/wallet/mock-wallet/mock-wallet.js b/lib/plugins/wallet/mock-wallet/mock-wallet.js index 5ffc88e1..5f566446 100644 --- a/lib/plugins/wallet/mock-wallet/mock-wallet.js +++ b/lib/plugins/wallet/mock-wallet/mock-wallet.js @@ -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 diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js index 8b0e51d6..72d64a9a 100644 --- a/lib/routes/customerRoutes.js +++ b/lib/routes/customerRoutes.js @@ -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 diff --git a/lib/sms-notices.js b/lib/sms-notices.js index f2235069..ac2b86fd 100644 --- a/lib/sms-notices.js +++ b/lib/sms-notices.js @@ -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]) } diff --git a/lib/sms.js b/lib/sms.js index f8d1f18d..424df961 100644 --- a/lib/sms.js +++ b/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 = { diff --git a/migrations/1643996603839-change-custom-sms-to-notices.js b/migrations/1643996603839-change-custom-sms-to-notices.js index c2d9ffd3..9d1313d9 100644 --- a/migrations/1643996603839-change-custom-sms-to-notices.js +++ b/migrations/1643996603839-change-custom-sms-to-notices.js @@ -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) { diff --git a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.js b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.js index 2692a471..3e56aae1 100644 --- a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.js +++ b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.js @@ -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} +
+ + ) + }, 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 }) => { -

{multiReplace(sms?.message, matches)}

+

+ {R.isEmpty(sms?.message) ? ( + No content available + ) : ( + formatContent(multiReplace(sms?.message, matches)) + )} +

{format('HH:mm', new Date())} @@ -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]) ? ( +
+ {R.prop('messageName', it)} + +

{TOOLTIPS[it.event]}

+
+
+ ) : ( + 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) }}> - + {R.equals(selectedSMS, it) && previewOpen ? ( + + ) : ( + + )} ) } diff --git a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.styles.js b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.styles.js index 39d192b9..65e39737 100644 --- a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.styles.js +++ b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.styles.js @@ -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' } } diff --git a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNoticesModal.js b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNoticesModal.js index a5f17d9c..f6885bf2 100644 --- a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNoticesModal.js +++ b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNoticesModal.js @@ -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 && ( {({ values, errors, touched, setFieldValue }) => (
+ + setFieldValue('message', DEFAULT_MESSAGES[sms?.event]) + }> + Reset to default + - {R.map( - it => ( - { - 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 && ( + Values to attach )} +
+ {R.map( + it => ( +
+ {R.map( + ite => ( + { + setFieldValue( + 'message', + values.message.concat( + R.last(values.message) === ' ' ? '' : ' ', + ite.code + ) + ) + }} + /> + ), + it + )} +
+ ), + R.splitEvery(3, CHIPS[sms?.event]) + )} +
{getErrorMsg(errors, touched, creationError) && (