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 }) => (
-
- )}
-
-
- )}
- >
- )
-}
-
-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 }) => (
+
+ )}
+
+
+ )}
+ >
+ )
+}
+
+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 @@
+
+
\ No newline at end of file