chore: use monorepo organization
This commit is contained in:
parent
deaf7d6ecc
commit
a687827f7e
1099 changed files with 8184 additions and 11535 deletions
|
|
@ -0,0 +1,278 @@
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { H4, P, Label3 } from 'src/components/typography'
|
||||
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
|
||||
import ExpandIconClosed from 'src/styling/icons/action/expand/closed.svg?react'
|
||||
import ExpandIconOpen from 'src/styling/icons/action/expand/open.svg?react'
|
||||
import WhiteLogo from 'src/styling/icons/menu/logo-white.svg?react'
|
||||
|
||||
import { SupportLinkButton } from 'src/components/buttons'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
import CustomSMSModal from './SMSNoticesModal'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
|
||||
const GET_SMS_NOTICES = gql`
|
||||
query SMSNotices {
|
||||
SMSNotices {
|
||||
id
|
||||
event
|
||||
message
|
||||
messageName
|
||||
enabled
|
||||
allowToggle
|
||||
}
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
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}
|
||||
<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, timezone }) => {
|
||||
const matches = {
|
||||
'#code': 123,
|
||||
'#timestamp': formatDate(new Date(), timezone, 'HH:mm')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute w-88 overflow-visible"
|
||||
style={{ left: coords.x, bottom: coords.y }}>
|
||||
<div className="flex flex-row items-end gap-2">
|
||||
<div className="flex w-9 h-9 rounded-full bg-[#16D6D3] items-center justify-center">
|
||||
<WhiteLogo width={22} height={22} />
|
||||
</div>
|
||||
<Paper className="w-56 p-4 rounded-2xl">
|
||||
<P noMargin>
|
||||
{R.isEmpty(sms?.message) ? (
|
||||
<i>No content available</i>
|
||||
) : (
|
||||
formatContent(multiReplace(sms?.message, matches))
|
||||
)}
|
||||
</P>
|
||||
</Paper>
|
||||
<Label3>{formatDate(new Date(), timezone, 'HH:mm')}</Label3>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SMSNotices = () => {
|
||||
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 timezone = R.path(['config', 'locale_timezone'])(messagesData)
|
||||
|
||||
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]) ? (
|
||||
<div className="flex flex-row items-center">
|
||||
{R.prop('messageName', it)}
|
||||
<HelpTooltip width={250}>
|
||||
<P>{TOOLTIPS[it.event]}</P>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
) : (
|
||||
R.prop('messageName', it)
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Edit',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setPreviewOpen(false)
|
||||
setSelectedSMS(it)
|
||||
setShowModal(true)
|
||||
}}>
|
||||
<SvgIcon>
|
||||
<EditIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Enable',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<Switch
|
||||
disabled={!it.allowToggle}
|
||||
onClick={() => {
|
||||
it.enabled
|
||||
? disableMessage({ variables: { id: it.id } })
|
||||
: enableMessage({ variables: { id: it.id } })
|
||||
}}
|
||||
checked={it.enabled}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<IconButton
|
||||
onClick={e => {
|
||||
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)
|
||||
}}>
|
||||
<SvgIcon>
|
||||
{R.equals(selectedSMS, it) && previewOpen ? (
|
||||
<ExpandIconOpen />
|
||||
) : (
|
||||
<ExpandIconClosed />
|
||||
)}
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex relative items-center justify-between w-200">
|
||||
<H4>SMS notices</H4>
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring this panel, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/115001205591-SMS-Phone-Verification"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
{showModal && (
|
||||
<CustomSMSModal
|
||||
showModal={showModal}
|
||||
onClose={handleClose}
|
||||
sms={selectedSMS}
|
||||
creationError={errorMsg}
|
||||
submit={editMessage}
|
||||
/>
|
||||
)}
|
||||
{previewOpen && (
|
||||
<SMSPreview
|
||||
sms={selectedSMS}
|
||||
coords={previewCoords}
|
||||
timezone={timezone}
|
||||
/>
|
||||
)}
|
||||
<DataTable
|
||||
emptyText="No SMS notices so far"
|
||||
elements={elements}
|
||||
loading={loading}
|
||||
data={R.path(['SMSNotices'])(messagesData)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SMSNotices
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
import Chip from '@mui/material/Chip'
|
||||
import { Form, Formik, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2 } from 'src/components/typography'
|
||||
import DefaultIconReverse from 'src/styling/icons/button/retry/white.svg?react'
|
||||
import DefaultIcon from 'src/styling/icons/button/retry/zodiac.svg?react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { ActionButton, Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { zircon } from 'src/styling/variables'
|
||||
|
||||
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 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 && (
|
||||
<Modal
|
||||
title={`SMS notice - ${sms?.messageName}`}
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={500}
|
||||
open={true}
|
||||
handleClose={onClose}>
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, errors, touched) =>
|
||||
handleSubmit(values, errors, touched)
|
||||
}>
|
||||
{({ values, errors, touched, setFieldValue }) => (
|
||||
<Form id="sms-notice" className="flex flex-col h-full gap-5">
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={DefaultIcon}
|
||||
InverseIcon={DefaultIconReverse}
|
||||
className="w-37"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFieldValue('message', DEFAULT_MESSAGES[sms?.event])
|
||||
}>
|
||||
Reset to default
|
||||
</ActionButton>
|
||||
<Field
|
||||
name="message"
|
||||
label="Message content"
|
||||
fullWidth
|
||||
multiline={true}
|
||||
rows={6}
|
||||
component={TextInput}
|
||||
/>
|
||||
{R.length(CHIPS[sms?.event]) > 0 && (
|
||||
<Info2 noMargin>Values to attach</Info2>
|
||||
)}
|
||||
<div className="w-120">
|
||||
{R.splitEvery(3, CHIPS[sms?.event]).map((it, idx) => (
|
||||
<div key={idx} className="flex gap-2">
|
||||
{it.map((ite, idx2) => (
|
||||
<Chip
|
||||
key={idx2}
|
||||
label={ite.display}
|
||||
size="small"
|
||||
style={{ backgroundColor: zircon }}
|
||||
disabled={R.includes(ite.code, values.message)}
|
||||
className="p-2"
|
||||
onClick={() => {
|
||||
setFieldValue(
|
||||
'message',
|
||||
values.message.concat(
|
||||
R.last(values.message) === ' ' ? '' : ' ',
|
||||
ite.code
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row mt-auto mx-0 mb-6">
|
||||
{getErrorMsg(errors, touched, creationError) && (
|
||||
<ErrorMessage>
|
||||
{getErrorMsg(errors, touched, creationError)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="sms-notice"
|
||||
className="mt-auto ml-auto mr-0 mb-0">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SMSNoticesModal
|
||||
Loading…
Add table
Add a link
Reference in a new issue