diff --git a/lib/constants.js b/lib/constants.js index 1efcf1d7..c4bc2fbc 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -20,6 +20,7 @@ const MANUAL = 'manual' const CASH_OUT_DISPENSE_READY = 'cash_out_dispense_ready' const CONFIRMATION_CODE = 'sms_code' +const RECEIPT = 'sms_receipt' const WALLET_SCORE_THRESHOLD = 9 @@ -37,5 +38,6 @@ module.exports = { CONFIRMATION_CODE, CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES, - WALLET_SCORE_THRESHOLD + WALLET_SCORE_THRESHOLD, + RECEIPT } diff --git a/lib/custom-sms.js b/lib/custom-sms.js deleted file mode 100644 index 8cb8546a..00000000 --- a/lib/custom-sms.js +++ /dev/null @@ -1,41 +0,0 @@ -const _ = require('lodash/fp') -const uuid = require('uuid') -const db = require('./db') - -const getCustomMessages = () => { - const sql = `SELECT * FROM custom_messages ORDER BY created` - return db.any(sql).then(res => _.map( - it => ({ - id: it.id, - event: _.camelCase(it.event), - message: it.message - }), res)) -} - -const createCustomMessage = (event, message) => { - const sql = `INSERT INTO custom_messages (id, event, message) VALUES ($1, $2, $3)` - return db.none(sql, [uuid.v4(), _.snakeCase(event), message]) -} - -const editCustomMessage = (id, event, message) => { - const sql = `UPDATE custom_messages SET event=$2, message=$3 WHERE id=$1` - return db.none(sql, [id, _.snakeCase(event), message]) -} - -const deleteCustomMessage = id => { - const sql = `DELETE FROM custom_messages WHERE id=$1` - return db.none(sql, [id]) -} - -const getCustomMessage = event => { - const sql = `SELECT * FROM custom_messages WHERE event=$1 LIMIT 1` - return db.oneOrNone(sql, [event]) -} - -module.exports = { - getCustomMessages, - createCustomMessage, - editCustomMessage, - deleteCustomMessage, - getCustomMessage -} diff --git a/lib/new-admin/graphql/resolvers/sms.resolver.js b/lib/new-admin/graphql/resolvers/sms.resolver.js index f159c8c4..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('../../../custom-sms') +const smsNotices = require('../../../sms-notices') const resolvers = { Query: { - customMessages: () => customSms.getCustomMessages() + SMSNotices: () => smsNotices.getSMSNotices() }, Mutation: { - createCustomMessage: (...[, { event, message }]) => customSms.createCustomMessage(event, message), - editCustomMessage: (...[, { id, event, message }]) => customSms.editCustomMessage(id, event, message), - deleteCustomMessage: (...[, { id }]) => customSms.deleteCustomMessage(id) + editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message), + enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id), + disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id) } } diff --git a/lib/new-admin/graphql/types/sms.type.js b/lib/new-admin/graphql/types/sms.type.js index a86947b7..b67ec1fa 100644 --- a/lib/new-admin/graphql/types/sms.type.js +++ b/lib/new-admin/graphql/types/sms.type.js @@ -1,25 +1,29 @@ const { gql } = require('apollo-server-express') const typeDef = gql` - type CustomMessage { + type SMSNotice { id: ID! - event: CustomMessageEvent! + event: SMSNoticeEvent! message: String! + messageName: String! + enabled: Boolean! + allowToggle: Boolean! } - enum CustomMessageEvent { + enum SMSNoticeEvent { smsCode cashOutDispenseReady + smsReceipt } type Query { - customMessages: [CustomMessage] @auth + SMSNotices: [SMSNotice] @auth } type Mutation { - createCustomMessage(event: CustomMessageEvent!, message: String!): CustomMessage @auth - editCustomMessage(id: ID!, event: CustomMessageEvent!, message: String!): CustomMessage @auth - deleteCustomMessage(id: ID!): CustomMessage @auth + editSMSNotice(id: ID!, event: SMSNoticeEvent!, message: String!): SMSNotice @auth + enableSMSNotice(id: ID!): SMSNotice @auth + disableSMSNotice(id: ID!): SMSNotice @auth } ` 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 08949aa9..b00c4d54 100644 --- a/lib/routes/customerRoutes.js +++ b/lib/routes/customerRoutes.js @@ -189,6 +189,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 new file mode 100644 index 00000000..ac2b86fd --- /dev/null +++ b/lib/sms-notices.js @@ -0,0 +1,56 @@ +const _ = require('lodash/fp') +const uuid = require('uuid') +const db = require('./db') + +const getSMSNotices = () => { + const sql = `SELECT * FROM sms_notices ORDER BY created` + return db.any(sql).then(res => _.map( + it => ({ + id: it.id, + event: _.camelCase(it.event), + message: it.message, + messageName: it.message_name, + enabled: it.enabled, + allowToggle: it.allow_toggle + }), res)) +} + +const createSMSNotice = (event, messageName, message, enabled, allowToggle) => { + const sql = `INSERT INTO sms_notices (id, event, message_name, message${enabled ? `, enabled`: ``}${allowToggle ? `, allowToggle`: ``}) VALUES ($1, $2, $3, $4${enabled ? `, $5`: ``}${allowToggle ? `, $6`: ``})` + return db.none(sql, [uuid.v4(), _.snakeCase(event), messageName, message, enabled, allowToggle]) +} + +const editSMSNotice = (id, event, message) => { + const sql = `UPDATE sms_notices SET event=$2, message=$3 WHERE id=$1` + return db.none(sql, [id, _.snakeCase(event), message]) +} + +const deleteSMSNotice = id => { + const sql = `DELETE FROM sms_notices WHERE id=$1` + return db.none(sql, [id]) +} + +const getSMSNotice = event => { + const sql = `SELECT * FROM sms_notices WHERE event=$1 LIMIT 1` + return db.oneOrNone(sql, [event]) +} + +const enableSMSNotice = id => { + 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` + return db.oneOrNone(sql, [id]) +} + +module.exports = { + getSMSNotices, + createSMSNotice, + editSMSNotice, + deleteSMSNotice, + getSMSNotice, + enableSMSNotice, + disableSMSNotice +} diff --git a/lib/sms.js b/lib/sms.js index 71b691c2..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('./custom-sms') - -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 new file mode 100644 index 00000000..9d1313d9 --- /dev/null +++ b/migrations/1643996603839-change-custom-sms-to-notices.js @@ -0,0 +1,22 @@ +var db = require('./db') +var smsNotices = require('../lib/sms-notices') + +exports.up = function (next) { + var sql = [ + `ALTER TABLE custom_messages RENAME TO sms_notices`, + `ALTER TYPE custom_message_event RENAME TO sms_notice_event`, + `ALTER TYPE sms_notice_event ADD VALUE 'sms_receipt'`, + `ALTER TABLE sms_notices ADD COLUMN message_name TEXT UNIQUE NOT NULL`, + `ALTER TABLE sms_notices ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true` + `ALTER TABLE sms_notices ADD COLUMN allow_toggle BOOLEAN NOT NULL DEFAULT true` + ] + + 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) { + next() +} diff --git a/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMS.js b/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMS.js deleted file mode 100644 index b1d3dac0..00000000 --- a/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMS.js +++ /dev/null @@ -1,188 +0,0 @@ -import { useQuery, useMutation } from '@apollo/react-hooks' -import { makeStyles, Box } from '@material-ui/core' -import gql from 'graphql-tag' -import * as R from 'ramda' -import React, { useState } from 'react' - -import { DeleteDialog } from 'src/components/DeleteDialog' -import { Link, IconButton } from 'src/components/buttons' -import DataTable from 'src/components/tables/DataTable' -import { H4 } from 'src/components/typography' -import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' -import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' - -import styles from './CustomSMS.styles' -import CustomSMSModal from './CustomSMSModal' - -const useStyles = makeStyles(styles) - -const GET_CUSTOM_MESSAGES = gql` - query customMessages { - customMessages { - id - event - message - } - } -` - -const CREATE_CUSTOM_MESSAGE = gql` - mutation createCustomMessage($event: CustomMessageEvent!, $message: String!) { - createCustomMessage(event: $event, message: $message) { - id - } - } -` - -const EDIT_CUSTOM_MESSAGE = gql` - mutation editCustomMessage( - $id: ID! - $event: CustomMessageEvent! - $message: String! - ) { - editCustomMessage(id: $id, event: $event, message: $message) { - id - } - } -` - -const DELETE_CUSTOM_MESSAGE = gql` - mutation deleteCustomMessage($id: ID!) { - deleteCustomMessage(id: $id) { - id - } - } -` - -const EVENT_OPTIONS = [ - { code: 'smsCode', display: 'On SMS confirmation code' }, - { code: 'cashOutDispenseReady', display: 'Cash out dispense ready' } -] - -const CustomSMS = () => { - const classes = useStyles() - - const [deleteDialog, setDeleteDialog] = useState(false) - const [showModal, setShowModal] = useState(false) - const [selectedSMS, setSelectedSMS] = useState(null) - const [errorMsg, setErrorMsg] = useState('') - - const { data: messagesData, loading: messagesLoading } = useQuery( - GET_CUSTOM_MESSAGES - ) - - const [createMessage] = useMutation(CREATE_CUSTOM_MESSAGE, { - onError: ({ msg }) => setErrorMsg(msg), - refetchQueries: () => ['customMessages'] - }) - - const [editMessage] = useMutation(EDIT_CUSTOM_MESSAGE, { - onError: ({ msg }) => setErrorMsg(msg), - refetchQueries: () => ['customMessages'] - }) - - const [deleteMessage] = useMutation(DELETE_CUSTOM_MESSAGE, { - onError: ({ msg }) => setErrorMsg(msg), - refetchQueries: () => ['customMessages'] - }) - - const loading = messagesLoading - - const handleClose = () => { - setSelectedSMS(null) - setShowModal(false) - setDeleteDialog(false) - } - - const handleOpen = () => { - setErrorMsg('') - setShowModal(true) - } - - const elements = [ - { - header: 'Event', - width: 600, - size: 'sm', - textAlign: 'left', - view: it => - R.find(ite => R.propEq('event', ite.code, it), EVENT_OPTIONS).display - }, - { - header: 'Edit', - width: 100, - size: 'sm', - textAlign: 'center', - view: it => ( - { - setSelectedSMS(it) - setShowModal(true) - }}> - - - ) - }, - { - header: 'Delete', - width: 100, - size: 'sm', - textAlign: 'center', - view: it => ( - { - setSelectedSMS(it) - setDeleteDialog(true) - }}> - - - ) - } - ] - - return ( - <> -
-

Custom SMS message

- - handleOpen()}> - Add custom SMS - - -
- {showModal && ( - - )} - { - handleClose() - }} - onConfirmed={() => { - handleClose() - deleteMessage({ - variables: { - id: selectedSMS.id - } - }) - }} - errorMessage={errorMsg} - /> - - - ) -} - -export default CustomSMS diff --git a/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMS.styles.js b/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMS.styles.js deleted file mode 100644 index 0ef30603..00000000 --- a/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMS.styles.js +++ /dev/null @@ -1,29 +0,0 @@ -import { spacer } from 'src/styling/variables' - -const styles = { - header: { - display: 'flex', - position: 'relative', - alignItems: 'center', - justifyContent: 'space-between', - width: 800 - }, - form: { - '& > *': { - marginTop: 20 - }, - display: 'flex', - flexDirection: 'column', - height: '100%' - }, - footer: { - display: 'flex', - flexDirection: 'row', - margin: [['auto', 0, spacer * 3, 0]] - }, - submit: { - margin: [['auto', 0, 0, 'auto']] - } -} - -export default styles diff --git a/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMSModal.js b/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMSModal.js deleted file mode 100644 index 7b993021..00000000 --- a/new-lamassu-admin/src/pages/OperatorInfo/CustomSMS/CustomSMSModal.js +++ /dev/null @@ -1,165 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { Form, Formik, Field } from 'formik' -import * as R from 'ramda' -import React, { useState } from 'react' -import * as Yup from 'yup' - -import ErrorMessage from 'src/components/ErrorMessage' -import Modal from 'src/components/Modal' -import { Button } from 'src/components/buttons' -import { Autocomplete, TextInput } from 'src/components/inputs/formik' - -import styles from './CustomSMS.styles' - -const useStyles = makeStyles(styles) - -const getErrorMsg = (formikErrors, formikTouched, mutationError) => { - if (!formikErrors || !formikTouched) return null - if (mutationError) return 'Internal server error' - if (formikErrors.event && formikTouched.event) return formikErrors.event - if (formikErrors.message && formikTouched.message) return formikErrors.message - return null -} - -const prefill = { - smsCode: { - validator: Yup.string() - .required('The message content is required!') - .trim() - .test({ - name: 'has-code-tag', - message: 'A #code tag is missing from the message!', - exclusive: false, - test: value => value?.match(/#code/g || [])?.length > 0 - }) - .test({ - name: 'has-single-code-tag', - message: 'There should be a single #code tag!', - exclusive: false, - test: value => value?.match(/#code/g || [])?.length === 1 - }) - }, - cashOutDispenseReady: { - 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 - }) - } -} - -const CustomSMSModal = ({ - showModal, - onClose, - sms, - eventOptions, - creationError, - submit -}) => { - const classes = useStyles() - - const [selectedEvent, setSelectedEvent] = useState(sms?.event) - - const initialValues = { - event: !R.isNil(sms) ? sms.event : '', - message: !R.isNil(sms) ? sms.message : '' - } - - const validationSchema = Yup.object().shape({ - event: Yup.string().required('An event is required!'), - message: - prefill[selectedEvent]?.validator ?? - Yup.string() - .required('The message content is required!') - .trim() - }) - - const handleSubmit = values => { - sms - ? submit({ - variables: { - id: sms.id, - event: values.event, - message: values.message - } - }) - : submit({ - variables: { - event: values.event, - message: values.message - } - }) - onClose() - } - - return ( - <> - {showModal && ( - - - handleSubmit(values, errors, touched) - }> - {({ values, errors, touched }) => ( -
- - -
- {getErrorMsg(errors, touched, creationError) && ( - - {getErrorMsg(errors, touched, creationError)} - - )} - -
- - )} -
-
- )} - - ) -} - -export default CustomSMSModal diff --git a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.js b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.js new file mode 100644 index 00000000..4a1bb469 --- /dev/null +++ b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.js @@ -0,0 +1,258 @@ +import { useQuery, useMutation } from '@apollo/react-hooks' +import { makeStyles, Paper } from '@material-ui/core' +import { format } from 'date-fns/fp' +import gql from 'graphql-tag' +import * as R from 'ramda' +import React, { useState } from 'react' + +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' +import CustomSMSModal from './SMSNoticesModal' + +const useStyles = makeStyles(styles) + +const GET_SMS_NOTICES = gql` + query SMSNotices { + SMSNotices { + id + event + message + messageName + enabled + allowToggle + } + } +` + +const EDIT_SMS_NOTICE = gql` + mutation editSMSNotice($id: ID!, $event: SMSNoticeEvent!, $message: String!) { + editSMSNotice(id: $id, event: $event, message: $message) { + id + } + } +` + +const ENABLE_SMS_NOTICE = gql` + mutation enableSMSNotice($id: ID!) { + enableSMSNotice(id: $id) { + id + } + } +` + +const DISABLE_SMS_NOTICE = gql` + mutation disableSMSNotice($id: ID!) { + disableSMSNotice(id: $id) { + id + } + } +` + +const multiReplace = (str, obj) => { + var re = new RegExp(Object.keys(obj).join('|'), 'gi') + + return str.replace(re, function(matched) { + return obj[matched.toLowerCase()] + }) +} + +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) + + const matches = { + '#code': 123, + '#timestamp': format('HH:mm', new Date()) + } + + return ( +
+
+
+ +
+ +

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

+
+ {format('HH:mm', new Date())} +
+
+ ) +} + +const SMSNotices = () => { + const classes = useStyles() + + const [showModal, setShowModal] = useState(false) + const [selectedSMS, setSelectedSMS] = useState(null) + const [previewOpen, setPreviewOpen] = useState(false) + const [previewCoords, setPreviewCoords] = useState({ x: 0, y: 0 }) + const [errorMsg, setErrorMsg] = useState('') + + const { data: messagesData, loading: messagesLoading } = useQuery( + GET_SMS_NOTICES + ) + + const [editMessage] = useMutation(EDIT_SMS_NOTICE, { + onError: ({ msg }) => setErrorMsg(msg), + refetchQueries: () => ['SMSNotices'] + }) + + const [enableMessage] = useMutation(ENABLE_SMS_NOTICE, { + onError: ({ msg }) => setErrorMsg(msg), + refetchQueries: () => ['SMSNotices'] + }) + + const [disableMessage] = useMutation(DISABLE_SMS_NOTICE, { + onError: ({ msg }) => setErrorMsg(msg), + refetchQueries: () => ['SMSNotices'] + }) + + const loading = messagesLoading + + const handleClose = () => { + setShowModal(false) + setSelectedSMS(null) + } + + const elements = [ + { + header: 'Message name', + width: 500, + size: 'sm', + textAlign: 'left', + view: it => + !R.isEmpty(TOOLTIPS[it.event]) ? ( +
+ {R.prop('messageName', it)} + +

{TOOLTIPS[it.event]}

+
+
+ ) : ( + R.prop('messageName', it) + ) + }, + { + header: 'Edit', + width: 100, + size: 'sm', + textAlign: 'center', + view: it => ( + { + setPreviewOpen(false) + setSelectedSMS(it) + setShowModal(true) + }}> + + + ) + }, + { + header: 'Enable', + width: 100, + size: 'sm', + textAlign: 'center', + view: it => ( + { + it.enabled + ? disableMessage({ variables: { id: it.id } }) + : enableMessage({ variables: { id: it.id } }) + }} + checked={it.enabled} + /> + ) + }, + { + header: '', + width: 100, + size: 'sm', + textAlign: 'center', + view: it => ( + { + setSelectedSMS(it) + setPreviewCoords({ + x: e.currentTarget.getBoundingClientRect().right + 50, + y: + window.innerHeight - + 5 - + e.currentTarget.getBoundingClientRect().bottom + }) + R.equals(selectedSMS, it) + ? setPreviewOpen(!previewOpen) + : setPreviewOpen(true) + }}> + {R.equals(selectedSMS, it) && previewOpen ? ( + + ) : ( + + )} + + ) + } + ] + + return ( + <> +
+

SMS notices

+
+ {showModal && ( + + )} + {previewOpen && } + + + ) +} + +export default SMSNotices diff --git a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.styles.js b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.styles.js new file mode 100644 index 00000000..65e39737 --- /dev/null +++ b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNotices.styles.js @@ -0,0 +1,95 @@ +import { + spacer, + fontMonospaced, + fontSize5, + fontColor +} from 'src/styling/variables' + +const styles = { + header: { + display: 'flex', + position: 'relative', + alignItems: 'center', + justifyContent: 'space-between', + width: 800 + }, + form: { + '& > *': { + marginTop: 20 + }, + display: 'flex', + flexDirection: 'column', + height: '100%' + }, + footer: { + display: 'flex', + flexDirection: 'row', + margin: [['auto', 0, spacer * 3, 0]] + }, + submit: { + margin: [['auto', 0, 0, 'auto']] + }, + smsPreview: { + position: 'absolute', + left: ({ x }) => x, + bottom: ({ y }) => y, + width: 350, + overflow: 'visible' + }, + smsPreviewContainer: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-end', + '& > *': { + marginRight: 10 + } + }, + smsPreviewIcon: { + display: 'flex', + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: '#16D6D3', + alignItems: 'center', + justifyContent: 'center' + }, + smsPreviewContent: { + 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' + } +} + +export default styles diff --git a/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNoticesModal.js b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNoticesModal.js new file mode 100644 index 00000000..f6885bf2 --- /dev/null +++ b/new-lamassu-admin/src/pages/OperatorInfo/SMSNotices/SMSNoticesModal.js @@ -0,0 +1,209 @@ +import { makeStyles, Chip } from '@material-ui/core' +import { Form, Formik, Field } from 'formik' +import * as R from 'ramda' +import React from 'react' +import * as Yup from 'yup' + +import ErrorMessage from 'src/components/ErrorMessage' +import Modal from 'src/components/Modal' +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' + +const useStyles = makeStyles(styles) + +const getErrorMsg = (formikErrors, formikTouched, mutationError) => { + if (!formikErrors || !formikTouched) return null + if (mutationError) return 'Internal server error' + if (formikErrors.event && formikTouched.event) return formikErrors.event + if (formikErrors.message && formikTouched.message) return formikErrors.message + return null +} + +const PREFILL = { + smsCode: { + validator: Yup.string() + .required('The message content is required!') + .trim() + .test({ + name: 'has-code', + message: 'The confirmation code is missing from the message!', + exclusive: false, + test: value => value?.match(/#code/g || [])?.length > 0 + }) + .test({ + name: 'has-single-code', + message: 'There should be a single confirmation code!', + exclusive: false, + test: value => value?.match(/#code/g || [])?.length === 1 + }) + }, + cashOutDispenseReady: { + validator: Yup.string() + .required('The message content is required!') + .trim() + }, + smsReceipt: { + validator: Yup.string().trim() + } +} + +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 = ({ + showModal, + onClose, + sms, + creationError, + submit +}) => { + const classes = useStyles() + + const initialValues = { + event: !R.isNil(sms) ? sms.event : '', + message: !R.isNil(sms) ? sms.message : '' + } + + const validationSchema = Yup.object().shape({ + event: Yup.string().required('An event is required!'), + message: + PREFILL[sms?.event]?.validator ?? + Yup.string() + .required('The message content is required!') + .trim() + }) + + const handleSubmit = values => { + sms + ? submit({ + variables: { + id: sms.id, + event: values.event, + message: values.message + } + }) + : submit({ + variables: { + event: values.event, + message: values.message + } + }) + onClose() + } + + return ( + <> + {showModal && ( + + + handleSubmit(values, errors, touched) + }> + {({ values, errors, touched, setFieldValue }) => ( +
+ + setFieldValue('message', DEFAULT_MESSAGES[sms?.event]) + }> + Reset to default + + + {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) && ( + + {getErrorMsg(errors, touched, creationError)} + + )} + +
+ + )} +
+
+ )} + + ) +} + +export default SMSNoticesModal diff --git a/new-lamassu-admin/src/routing/lamassu.routes.js b/new-lamassu-admin/src/routing/lamassu.routes.js index e647c65b..e33d690c 100644 --- a/new-lamassu-admin/src/routing/lamassu.routes.js +++ b/new-lamassu-admin/src/routing/lamassu.routes.js @@ -16,8 +16,8 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus' import Notifications from 'src/pages/Notifications/Notifications' import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar' import ContactInfo from 'src/pages/OperatorInfo/ContactInfo' -import CustomSMS from 'src/pages/OperatorInfo/CustomSMS/CustomSMS' import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting' +import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' import ServerLogs from 'src/pages/ServerLogs' import Services from 'src/pages/Services/Services' @@ -174,11 +174,11 @@ const getLamassuRoutes = () => [ component: ReceiptPrinting }, { - key: 'custom-sms', - label: 'Custom SMS', - route: '/settings/operator-info/custom-sms', + key: 'sms-notices', + label: 'SMS notices', + route: '/settings/operator-info/sms-notices', allowedRoles: [ROLES.USER, ROLES.SUPERUSER], - component: CustomSMS + component: SMSNotices }, { key: 'coin-atm-radar', diff --git a/new-lamassu-admin/src/routing/pazuz.routes.js b/new-lamassu-admin/src/routing/pazuz.routes.js index 153d0205..8789a1d1 100644 --- a/new-lamassu-admin/src/routing/pazuz.routes.js +++ b/new-lamassu-admin/src/routing/pazuz.routes.js @@ -19,8 +19,8 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus' import Notifications from 'src/pages/Notifications/Notifications' import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar' import ContactInfo from 'src/pages/OperatorInfo/ContactInfo' -import CustomSMS from 'src/pages/OperatorInfo/CustomSMS/CustomSMS' import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting' +import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' import ServerLogs from 'src/pages/ServerLogs' import Services from 'src/pages/Services/Services' @@ -169,11 +169,11 @@ const getPazuzRoutes = () => [ component: ReceiptPrinting }, { - key: 'custom-sms', - label: 'Custom SMS', - route: '/settings/operator-info/custom-sms', + key: 'sms-notices', + label: 'SMS notices', + route: '/settings/operator-info/sms-notices', allowedRoles: [ROLES.USER, ROLES.SUPERUSER], - component: CustomSMS + component: SMSNotices }, { key: 'coin-atm-radar', diff --git a/new-lamassu-admin/src/styling/icons/menu/logo-white.svg b/new-lamassu-admin/src/styling/icons/menu/logo-white.svg new file mode 100644 index 00000000..7de68c79 --- /dev/null +++ b/new-lamassu-admin/src/styling/icons/menu/logo-white.svg @@ -0,0 +1,20 @@ + + + + Created with Sketch. + + + + + + + + + \ No newline at end of file