chore: use monorepo organization

This commit is contained in:
Rafael Taranto 2025-05-12 10:52:54 +01:00
parent deaf7d6ecc
commit a687827f7e
1099 changed files with 8184 additions and 11535 deletions

View file

@ -0,0 +1,43 @@
.radioGroup {
flex-direction: row;
width: 600px;
}
.radioLabel {
width: 150px;
height: 48px;
}
.mailgunRadioGroup {
flex-direction: row;
width: 768px;
}
.mailgunRadioLabel {
width: 300px;
height: 48px;
}
.mdForm {
width: 385px;
}
.infoMessage {
display: flex;
margin-bottom: 20px;
}
.infoMessage > p {
width: 330px;
margin-top: 4px;
margin-left: 16px;
}
.actionButton {
margin-bottom: 32px;
}
.actionButtonLink {
text-decoration: none;
color: var(--zodiac);
}

View file

@ -0,0 +1,94 @@
import { useQuery, gql } from '@apollo/client'
import Dialog from '@mui/material/Dialog'
import DialogContent from '@mui/material/DialogContent'
import classnames from 'classnames'
import React, { useState, useContext } from 'react'
import { useHistory } from 'react-router-dom'
import { getWizardStep, STEPS } from 'src/pages/Wizard/helper'
import AppContext from 'src/AppContext'
import Footer from './components/Footer'
const GET_DATA = gql`
query getData {
config
accounts
cryptoCurrencies {
code
display
}
}
`
const Wizard = ({ fromAuthRegister }) => {
const { data, loading } = useQuery(GET_DATA)
const history = useHistory()
const { setWizardTested } = useContext(AppContext)
const [step, setStep] = useState(0)
const [open, setOpen] = useState(true)
const [footerExp, setFooterExp] = useState(false)
if (loading) {
return <></>
}
const wizardStep = getWizardStep(data?.config, data?.cryptoCurrencies)
const shouldGoBack =
history.length && !history.location.state?.fromAuthRegister
if (wizardStep === 0) {
setWizardTested(true)
shouldGoBack ? history.goBack() : history.push('/')
}
const isWelcome = step === 0
const start = () => {
setFooterExp(false)
}
const doContinue = () => {
if (step >= STEPS.length - 1) {
setOpen(false)
history.push('/')
}
const nextStep = step === 0 && wizardStep ? wizardStep : step + 1
setFooterExp(true)
setStep(nextStep)
}
const current = STEPS[step]
const classNames = {
'flex flex-col justify-between py-4 px-0 bg-white': true,
'bg-[url(/wizard-background.svg)] bg-no-repeat bg-center bg-fixed bg-cover':
isWelcome,
'blur-sm pointer-events-none': footerExp
}
return (
<Dialog fullScreen open={open}>
<DialogContent className={classnames(classNames)}>
<current.Component doContinue={doContinue} isActive={!footerExp} />
</DialogContent>
{!isWelcome && (
<Footer
currentStep={step}
steps={STEPS.length - 1}
exImage={current.exImage}
subtitle={current.subtitle}
text={current.text}
open={footerExp}
start={start}
/>
)}
</Dialog>
)
}
export default Wizard

View file

@ -0,0 +1,62 @@
import { useQuery, useMutation, gql } from '@apollo/client'
import * as R from 'ramda'
import React from 'react'
import Section from 'src/components/layout/Section'
import TitleSection from 'src/components/layout/TitleSection'
import { mainFields, defaults, getSchema } from 'src/pages/Commissions/helper'
import { Table as EditableTable } from 'src/components/editableTable'
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
const GET_DATA = gql`
query getData {
config
}
`
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) {
saveConfig(config: $config)
}
`
function Commissions({ isActive, doContinue }) {
const { data } = useQuery(GET_DATA)
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: doContinue
})
const save = it => {
const config = toNamespace('commissions')(it.commissions[0])
return saveConfig({ variables: { config } })
}
const currency = R.path(['fiatCurrency'])(
fromNamespace(namespaces.LOCALE)(data?.config)
)
const locale = fromNamespace(namespaces.LOCALE)(data?.config)
return (
<div className="w-[1132px] h-full mx-auto flex-1 flex flex-col">
<TitleSection title="Commissions" />
<Section>
<EditableTable
title="Default setup"
rowSize="lg"
titleLg
name="commissions"
initialValues={defaults}
enableEdit
forceAdd={isActive}
save={save}
validationSchema={getSchema(locale)}
data={[]}
elements={mainFields(currency)}
/>
</Section>
</div>
)
}
export default Commissions

View file

@ -0,0 +1,104 @@
import Drawer from '@mui/material/Drawer'
import Grid from '@mui/material/Grid'
import React, { useState } from 'react'
import Modal from 'src/components/Modal'
import Stepper from 'src/components/Stepper'
import { P, H2, Info2 } from 'src/components/typography'
import { Button, Link } from 'src/components/buttons'
function Footer({ currentStep, steps, subtitle, text, exImage, open, start }) {
const [fullExample, setFullExample] = useState(false)
return (
<Drawer
anchor={'bottom'}
open={true}
variant={'persistent'}
classes={{ paperAnchorDockedBottom: 'border-t-0 shadow-sm' }}>
<div className={`py-8 flex-grow ${open ? 'h-[264px]' : 'h-[84px]'}`}>
<Grid
container
direction="row"
justifyContent="center"
alignItems="baseline">
<Grid
item
xs={5}
container
direction={open ? 'column' : 'row'}
justifyContent="flex-start"
alignItems="baseline">
<H2 className="m-0 mr-8">Setup Lamassu Admin</H2>
<Info2 className="mt-2 mb-2 leading-tight inline">{subtitle}</Info2>
{open && <P>{text}</P>}
</Grid>
<Grid
item
xs={4}
container
direction="column"
justifyContent="flex-start"
alignItems="flex-end"
spacing={5}>
<Grid item xs={12}>
{steps && currentStep && (
<Stepper currentStep={currentStep} steps={steps} />
)}
</Grid>
</Grid>
</Grid>
{open && (
<Grid
container
direction="row"
justifyContent="center"
alignItems="baseline">
<Grid
item
xs={5}
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start">
<Link
onClick={() => {
setFullExample(true)
}}>
See full example
</Link>
</Grid>
<Grid
item
xs={4}
container
direction="column"
justifyContent="flex-start"
alignItems="flex-end"
spacing={5}>
<Grid item>
<Button size="lg" onClick={start}>
Get Started
</Button>
</Grid>
</Grid>
</Grid>
)}
</div>
<Modal
closeOnEscape={true}
closeOnBackdropClick={true}
className="bg-transparent shadow-none"
xl={true}
width={1152 + 120 + 56}
handleClose={() => {
setFullExample(false)
}}
open={fullExample}>
<img width={1152} src={exImage} alt="" />
</Modal>
</Drawer>
)
}
export default Footer

View file

@ -0,0 +1,94 @@
import { useQuery, useMutation, gql } from '@apollo/client'
import * as R from 'ramda'
import React from 'react'
import Section from 'src/components/layout/Section'
import TitleSection from 'src/components/layout/TitleSection'
import { Table as EditableTable } from 'src/components/editableTable'
import {
mainFields,
localeDefaults as defaults,
LocaleSchema as schema
} from 'src/pages/Locales/helper'
import { toNamespace } from 'src/utils/config'
import { getConfiguredCoins } from '../helper'
const GET_DATA = gql`
query getData {
config
accounts
currencies {
code
display
}
countries {
code
display
}
cryptoCurrencies {
code
display
}
languages {
code
display
}
machines {
name
deviceId
}
}
`
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) {
saveConfig(config: $config)
}
`
function Locales({ isActive, doContinue }) {
const { data } = useQuery(GET_DATA)
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: doContinue
})
const save = it => {
const config = toNamespace('locale')(it.locale[0])
return saveConfig({ variables: { config } })
}
const cryptoCurrencies = getConfiguredCoins(
data?.config || {},
data?.cryptoCurrencies || []
)
const onChangeCoin = (prev, curr, setValue) => setValue(curr)
return (
<div className="w-[1132px] h-full mx-auto flex-1 flex flex-col">
<TitleSection title="Locales" />
<Section>
<EditableTable
title="Default settings"
rowSize="lg"
titleLg
name="locale"
initialValues={defaults}
forceAdd={isActive}
enableEdit
save={save}
validationSchema={schema}
data={[]}
elements={mainFields(
R.mergeRight(data, { cryptoCurrencies }),
onChangeCoin
)}
/>
</Section>
</div>
)
}
export default Locales

View file

@ -0,0 +1,123 @@
import { useMutation, useQuery, gql } from "@apollo/client";
import React, { useState, useEffect } from 'react'
import { H4, Info3 } from 'src/components/typography'
import FormRenderer from 'src/pages/Services/FormRenderer'
import InverseLinkIcon from 'src/styling/icons/action/external link/white.svg?react'
import LinkIcon from 'src/styling/icons/action/external link/zodiac.svg?react'
import WarningIcon from 'src/styling/icons/warning-icon/comet.svg?react'
import { ActionButton } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs'
import mailgunSchema from 'src/pages/Services/schemas/mailgun'
import classes from 'src/pages/Wizard/Radio.module.css'
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
const GET_CONFIG = gql`
{
config
accounts
}
`
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) {
saveConfig(config: $config)
}
`
const SAVE_ACCOUNTS = gql`
mutation Save($accounts: JSONObject) {
saveAccounts(accounts: $accounts)
}
`
const options = [
{
code: 'enabled',
display: 'Yes, send notifications to my email'
},
{
code: 'disabled',
display: "No, don't send email notifications"
}
]
const Mailgun = () => {
const { data } = useQuery(GET_CONFIG)
const [saveConfig] = useMutation(SAVE_CONFIG)
const [saveAccounts] = useMutation(SAVE_ACCOUNTS)
const [emailActive, setEmailActive] = useState(false)
const accounts = data?.accounts ?? []
const emailConfig =
data?.config &&
fromNamespace(namespaces.NOTIFICATIONS + '_email')(data.config)
useEffect(() => {
if (emailActive) return
emailConfig && setEmailActive(emailConfig?.active ? 'enabled' : 'disabled')
}, [emailActive, emailConfig])
const handleRadio = enabled => {
setEmailActive(enabled)
save(enabled === 'enabled')
}
const save = active => {
const config = toNamespace(`notifications_email`)({ active })
return saveConfig({ variables: { config } })
}
const saveAccount = mailgun => {
const accounts = { mailgun }
return saveAccounts({ variables: { accounts } })
}
return (
<div className={classes.mdForm}>
<H4>Do you want to get notifications via email?</H4>
<RadioGroup
labelClassName={classes.mailgunRadioLabel}
className={classes.mailgunRadioGroup}
options={options}
value={emailActive}
onChange={event => handleRadio(event.target.value)}
/>
<div className={classes.infoMessage}>
<WarningIcon />
<Info3>
To get email notifications, youll need to set up Mailgun. Check out
our article on how to set it up.
</Info3>
</div>
<ActionButton
className={classes.actionButton}
color="primary"
Icon={LinkIcon}
InverseIcon={InverseLinkIcon}>
<a
className={classes.actionButtonLink}
target="_blank"
rel="noopener noreferrer"
href="https://support.lamassu.is/hc/en-us/articles/115001203991-Email-notifications-with-Mailgun">
Email notifications with Mailgun
</a>
</ActionButton>
{emailActive === 'enabled' && (
<>
<H4>Mailgun credentials</H4>
<FormRenderer
value={accounts.mailgun}
save={saveAccount}
elements={mailgunSchema.elements}
validationSchema={mailgunSchema.getValidationSchema(accounts.mailgun)}
buttonLabel={'Save'}
/>
</>
)}
</div>
)
}
export default Mailgun

View file

@ -0,0 +1,89 @@
import Grid from '@mui/material/Grid'
import React, { useState } from 'react'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection'
import Notifications from 'src/pages/Notifications/Notifications'
import { namespaces } from 'src/utils/config'
import Mailgun from './Mailgun'
const EMAIL = 'Email'
const SETUP_CHANNELS = 'Setup channels'
const TRANSACTION_ALERTS = 'Transaction alerts'
const FIAT_BALANCE_ALERTS = 'Fiat balance alerts'
const CRYPTO_BALANCE_ALERTS = 'Crypto balance alerts'
const pages = [
EMAIL,
SETUP_CHANNELS,
TRANSACTION_ALERTS,
FIAT_BALANCE_ALERTS,
CRYPTO_BALANCE_ALERTS
]
const N = () => {
const [selected, setSelected] = useState(EMAIL)
const isSelected = it => selected === it
return (
<div className="w-[1132px] h-full mx-auto flex-1 flex flex-col">
<TitleSection title="Notifications"></TitleSection>
<Grid container className="flex-1 h-full">
<Sidebar
data={pages}
isSelected={isSelected}
displayName={it => it}
onClick={it => setSelected(it)}
/>
<div className="ml-12 pt-4">
{isSelected(EMAIL) && <Mailgun />}
{isSelected(SETUP_CHANNELS) && (
<Notifications
name={namespaces.NOTIFICATIONS}
wizard={true}
displayCryptoAlerts={false}
displayOverrides={false}
displayTitle={false}
displayTransactionAlerts={false}
displayFiatAlerts={false}
/>
)}
{isSelected(TRANSACTION_ALERTS) && (
<Notifications
name={namespaces.NOTIFICATIONS}
displayCryptoAlerts={false}
displayOverrides={false}
displayTitle={false}
displaySetup={false}
displayFiatAlerts={false}
/>
)}
{isSelected(FIAT_BALANCE_ALERTS) && (
<Notifications
name={namespaces.NOTIFICATIONS}
displayCryptoAlerts={false}
displayOverrides={false}
displayTitle={false}
displayTransactionAlerts={false}
displaySetup={false}
/>
)}
{isSelected(CRYPTO_BALANCE_ALERTS) && (
<Notifications
name={namespaces.NOTIFICATIONS}
displaySetup={false}
displayOverrides={false}
displayTitle={false}
displayTransactionAlerts={false}
displayFiatAlerts={false}
/>
)}
</div>
</Grid>
</div>
)
}
export default N

View file

@ -0,0 +1,12 @@
import React from 'react'
// import OperatorInfo from 'src/pages/OperatorInfo'
function WizardOperatorInfo() {
return (
<div className="w-[1132px] h-full mx-auto flex-1 flex flex-col">
{/* <OperatorInfo wizard={true}></OperatorInfo> */}
</div>
)
}
export default WizardOperatorInfo

View file

@ -0,0 +1,138 @@
import { useMutation, useQuery, gql } from '@apollo/client'
import classnames from 'classnames'
import React, { useState } from 'react'
import { HelpTooltip } from 'src/components/Tooltip'
import { H1, H4, Label1, P } from 'src/components/typography'
import FormRenderer from 'src/pages/Services/FormRenderer'
import WarningIcon from 'src/styling/icons/warning-icon/comet.svg?react'
import { Button, SupportLinkButton } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs'
import twilio from 'src/pages/Services/schemas/twilio'
import sharedClasses from './Wallet/Shared.module.css'
import classes from './Twilio.module.css'
const GET_CONFIG = gql`
{
config
accounts
}
`
const SAVE_ACCOUNTS = gql`
mutation Save($accounts: JSONObject) {
saveAccounts(accounts: $accounts)
}
`
const options = [
{
code: 'enable',
display: 'Yes, I will'
},
{
code: 'disable',
display: 'No, not for now'
}
]
function Twilio({ doContinue }) {
const [selected, setSelected] = useState(null)
const [error, setError] = useState(false)
const { data, refetch } = useQuery(GET_CONFIG)
const [saveAccounts] = useMutation(SAVE_ACCOUNTS, {
onCompleted: doContinue
})
const accounts = data?.accounts ?? []
const onSelect = e => {
setSelected(e.target.value)
setError(false)
}
const clickContinue = () => {
if (!selected) return setError(true)
doContinue()
}
const save = twilio => {
const accounts = { twilio }
return saveAccounts({ variables: { accounts } }).then(() => refetch())
}
const titleClasses = {
'ml-2 mb-2': true,
[sharedClasses.error]: error
}
return (
<div className={classes.wrapper}>
<div className={classes.content}>
<H1>Twilio (SMS service)</H1>
<div className="flex items-end">
<H4 noMargin className={classnames(titleClasses)}>
Will you setup a two way machine or compliance?
</H4>
<HelpTooltip width={304}>
<P>
Two-way machines allow your customers not only to buy (cash-in)
but also sell cryptocurrencies (cash-out).
</P>
<P>
Youll need an SMS service for cash-out transactions and for any
compliance triggers
</P>
</HelpTooltip>
</div>
<RadioGroup
labelClassName={classes.radioLabel}
className={sharedClasses.radioGroup}
options={options}
value={selected}
onChange={onSelect}
/>
<div className="flex gap-4 mt-5 mb-8 items-center">
<WarningIcon />
<Label1 noMargin>
To set up Twilio please read the instructions from our support
portal.
</Label1>
</div>
<SupportLinkButton
link="https://support.lamassu.is/hc/en-us/articles/115001203951-Twilio-for-SMS"
label="Twilio for SMS"
/>
{selected === 'enable' && (
<>
<H4 noMargin>Enter credentials</H4>
<FormRenderer
xs={6}
save={save}
value={accounts.twilio}
elements={twilio.elements}
validationSchema={twilio.getValidationSchema(accounts.twilio)}
buttonLabel={'Continue'}
buttonClass={sharedClasses.formButton}
/>
</>
)}
{selected !== 'enable' && (
<Button
size="lg"
onClick={clickContinue}
className={sharedClasses.button}>
Continue
</Button>
)}
</div>
</div>
)
}
export default Twilio

View file

@ -0,0 +1,24 @@
.content {
width: 820px;
}
.radioLabel {
width: 280px;
height: 48px;
}
.wrapper {
width: 1200px;
height: 100px;
margin: 0 auto;
}
.title {
margin-left: 8px;
margin-bottom: 5px;
}
.info {
margin-top: 20px;
margin-bottom: 20px;
}

View file

@ -0,0 +1,98 @@
import { useQuery, useMutation, gql } from "@apollo/client";
import * as R from 'ramda'
import React, { useState } from 'react'
import { P, H4 } from 'src/components/typography'
import { getElements, WalletSchema } from 'src/pages/Wallet/helper'
import { Button } from 'src/components/buttons'
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
import { toNamespace, namespaces } from 'src/utils/config'
import classes from './Shared.module.css'
const GET_INFO = gql`
query getData {
config
accounts
accountsConfig {
code
display
class
cryptos
}
cryptoCurrencies {
code
display
}
}
`
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject, $accounts: JSONObject) {
saveConfig(config: $config)
saveAccounts(accounts: $accounts)
}
`
const AllSet = ({ data: currentData, doContinue }) => {
const { data } = useQuery(GET_INFO)
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: doContinue
})
const [error, setError] = useState(false)
const coin = currentData?.coin
const accountsConfig = data?.accountsConfig
const cryptoCurrencies = data?.cryptoCurrencies ?? []
const save = () => {
const adjustedData = {
zeroConfLimit: 0,
...currentData
}
if (!WalletSchema.isValidSync(adjustedData)) return setError(true)
const withCoin = toNamespace(coin, R.omit('coin', adjustedData))
const config = toNamespace(namespaces.WALLETS)(withCoin)
setError(false)
return saveConfig({ variables: { config } })
}
const presentableData = R.pipe(
R.omit(['coin', 'zeroConf', 'zeroConfLimit']),
toNamespace(coin)
)(currentData)
const presentableElements = R.filter(
R.pipe(
R.prop('name'),
R.flip(R.includes)(['zeroConf', 'zeroConfLimit']),
R.not()
),
getElements(cryptoCurrencies, accountsConfig, null, true)
)
return (
<>
<H4 className={error && classes.error}>All set</H4>
<P>
These are your wallet settings. You can later edit these and add
additional coins.
</P>
<EditableTable
rowSize="lg"
titleLg
name="All set"
namespaces={[coin]}
data={presentableData}
elements={presentableElements}
/>
<Button size="lg" onClick={save} className={classes.button}>
Continue
</Button>
</>
)
}
export default AllSet

View file

@ -0,0 +1,96 @@
import { useMutation, useQuery, gql } from "@apollo/client";
import React, { useState } from 'react'
import { P, H4 } from 'src/components/typography'
import FormRenderer from 'src/pages/Services/FormRenderer'
import { SupportLinkButton, Button } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs'
import blockcypherSchema from 'src/pages/Services/schemas/blockcypher'
import classes from './Shared.module.css'
const GET_CONFIG = gql`
{
accounts
}
`
const SAVE_ACCOUNTS = gql`
mutation SaveAccountsBC($accounts: JSONObject) {
saveAccounts(accounts: $accounts)
}
`
const options = [
{
code: 'enable',
display: 'I will enable cash-out'
},
{
code: 'disable',
display: "I won't enable cash-out"
}
]
const Blockcypher = ({ addData }) => {
const { data } = useQuery(GET_CONFIG)
const [saveConfig] = useMutation(SAVE_ACCOUNTS, {
onCompleted: () => addData({ zeroConf: 'blockcypher' })
})
const [selected, setSelected] = useState(null)
const [error, setError] = useState(false)
const accounts = data?.accounts ?? []
const onSelect = e => {
setSelected(e.target.value)
setError(false)
}
const save = blockcypher => {
const accounts = { blockcypher }
return saveConfig({ variables: { accounts } })
}
return (
<>
<H4 className={error && classes.error}>Blockcypher</H4>
<P>
If you are enabling cash-out services, create a Blockcypher account.
</P>
<SupportLinkButton
link="https://support.lamassu.is/hc/en-us/articles/115001209472-Blockcypher"
label="Configuring Blockcypher"
/>
<RadioGroup
labelClassName={classes.radioLabel}
className={classes.radioGroup}
options={options}
value={selected}
onChange={onSelect}
/>
<div className={classes.mdForm}>
{selected === 'disable' && (
<Button
size="lg"
onClick={() => addData({ zeroConf: 'none', zeroConfLimit: 0 })}
className={classes.button}>
Continue
</Button>
)}
{selected === 'enable' && (
<FormRenderer
value={accounts.blockcypher}
save={save}
elements={blockcypherSchema.elements}
validationSchema={blockcypherSchema.getValidationSchema}
buttonLabel={'Continue'}
buttonClass={classes.formButton}
/>
)}
</div>
</>
)
}
export default Blockcypher

View file

@ -0,0 +1,74 @@
import { useQuery, gql } from "@apollo/client";
import { Formik, Form, Field } from 'formik'
import React, { useState } from 'react'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import { H4 } from 'src/components/typography'
import * as Yup from 'yup'
import { Button } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs/formik'
import classes from './Shared.module.css'
const GET_CONFIG = gql`
{
cryptoCurrencies {
code
display
}
}
`
const schema = Yup.object().shape({
coin: Yup.string().required()
})
const ChooseCoin = ({ addData }) => {
const [error, setError] = useState(false)
const { data } = useQuery(GET_CONFIG)
const cryptoCurrencies = data?.cryptoCurrencies ?? []
const onSubmit = it => {
if (!schema.isValidSync(it)) return setError(true)
if (it.coin !== 'BTC') {
return addData({ coin: it.coin, zeroConf: 'none', zeroConfLimit: 0 })
}
addData(it)
}
return (
<>
<H4 className={error && classes.error}>
Choose your first cryptocurrency
</H4>
<Formik
validateOnBlur={false}
validateOnChange={false}
enableReinitialize
initialValues={{ coin: '' }}
onSubmit={onSubmit}>
<Form onChange={() => setError(false)}>
<PromptWhenDirty />
<Field
component={RadioGroup}
name="coin"
labelClassName={classes.radioLabel}
className={classes.radioGroup}
options={cryptoCurrencies}
/>
{
<Button size="lg" type="submit" className={classes.button}>
Continue
</Button>
}
</Form>
</Formik>
</>
)
}
export default ChooseCoin

View file

@ -0,0 +1,126 @@
import { useQuery, useMutation, gql } from "@apollo/client";
import { getEquivalentCode } from '@lamassu/coins/lightUtils'
import * as R from 'ramda'
import React, { useState } from 'react'
import { H4, Info3 } from 'src/components/typography'
import FormRenderer from 'src/pages/Services/FormRenderer'
import WarningIcon from 'src/styling/icons/warning-icon/comet.svg?react'
import { Button, SupportLinkButton } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs'
import _schema from 'src/pages/Services/schemas'
import classes from './Shared.module.css'
import { getItems } from './getItems'
const GET_CONFIG = gql`
{
accounts
accountsConfig {
code
display
class
cryptos
}
cryptoCurrencies {
code
display
}
}
`
const SAVE_ACCOUNTS = gql`
mutation Save($accounts: JSONObject) {
saveAccounts(accounts: $accounts)
}
`
const isConfigurable = it =>
!R.isNil(it) && !R.contains(it)(['mock-exchange', 'no-exchange'])
const ChooseExchange = ({ data: currentData, addData }) => {
const { data } = useQuery(GET_CONFIG)
const [saveAccounts] = useMutation(SAVE_ACCOUNTS, {
onCompleted: () => submit()
})
const [selected, setSelected] = useState(null)
const [error, setError] = useState(false)
const schema = _schema()
const accounts = data?.accounts ?? []
const accountsConfig = data?.accountsConfig ?? []
const coin = getEquivalentCode(currentData.coin)
const exchanges = getItems(accountsConfig, accounts, 'exchange', coin)
const submit = () => {
if (!selected) return setError(true)
addData({ exchange: selected })
}
const saveExchange = name => exchange => {
const accounts = { [name]: exchange }
return saveAccounts({ variables: { accounts } })
}
const onSelect = e => {
setSelected(e.target.value)
setError(false)
}
const supportArticles = {
kraken:
'https://support.lamassu.is/hc/en-us/articles/115001206891-Kraken-trading',
itbit:
'https://support.lamassu.is/hc/en-us/articles/360026195032-itBit-trading',
bitstamp:
'https://support.lamassu.is/hc/en-us/articles/115001206911-Bitstamp-trading'
}
return (
<div className={classes.mdForm}>
<H4 className={error && classes.error}>Choose your exchange</H4>
<RadioGroup
labelClassName={classes.radioLabel}
className={classes.radioGroup}
options={R.union(exchanges.filled, exchanges.unfilled)}
value={selected}
onChange={onSelect}
/>
{!isConfigurable(selected) && (
<Button size="lg" onClick={submit} className={classes.button}>
Continue
</Button>
)}
{isConfigurable(selected) && (
<>
<div className={classes.infoMessage}>
<WarningIcon />
<Info3>
Make sure you set up {schema[selected].name} to enter the
necessary information below. Please follow the instructions on our
support page if you havent.
</Info3>
</div>
<SupportLinkButton
link={supportArticles[selected]}
label={`${schema[selected].name} trading`}
/>
<H4 noMargin>Enter exchange information</H4>
<FormRenderer
value={accounts[selected]}
save={saveExchange(selected)}
elements={schema[selected].elements}
validationSchema={schema[selected].getValidationSchema(accounts[selected])}
buttonLabel={'Continue'}
buttonClass={classes.formButton}
/>
</>
)}
</div>
)
}
export default ChooseExchange

View file

@ -0,0 +1,63 @@
import { useQuery, gql } from "@apollo/client";
import { getEquivalentCode } from '@lamassu/coins/lightUtils'
import * as R from 'ramda'
import React, { useState } from 'react'
import { H4 } from 'src/components/typography'
import { Button } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs'
import classes from './Shared.module.css'
import { getItems } from './getItems'
const GET_CONFIG = gql`
{
accountsConfig {
code
display
class
cryptos
}
}
`
const ChooseTicker = ({ data: currentData, addData }) => {
const { data } = useQuery(GET_CONFIG)
const [selected, setSelected] = useState(null)
const [error, setError] = useState(false)
const accounts = data?.accounts ?? []
const accountsConfig = data?.accountsConfig ?? []
const coin = getEquivalentCode(currentData.coin)
const tickers = getItems(accountsConfig, accounts, 'ticker', coin)
const submit = () => {
if (!selected) return setError(true)
addData({ ticker: selected })
}
const onSelect = e => {
setSelected(e.target.value)
setError(false)
}
return (
<div className={classes.mdForm}>
<H4 className={error && classes.error}>Choose your ticker</H4>
<RadioGroup
labelClassName={classes.radioLabel}
className={classes.radioGroup}
options={R.union(tickers.filled, tickers.unfilled)}
value={selected}
onChange={onSelect}
/>
<Button size="lg" onClick={submit} className={classes.button}>
Continue
</Button>
</div>
)
}
export default ChooseTicker

View file

@ -0,0 +1,185 @@
import { useQuery, useMutation, gql } from "@apollo/client";
import * as R from 'ramda'
import React, { useState } from 'react'
import { H4, Info3 } from 'src/components/typography'
import FormRenderer from 'src/pages/Services/FormRenderer'
import WarningIcon from 'src/styling/icons/warning-icon/comet.svg?react'
import { Button, SupportLinkButton } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs'
import _schema from 'src/pages/Services/schemas'
import bitgo from 'src/pages/Services/schemas/singlebitgo'
import classes from './Shared.module.css'
import { getItems } from './getItems'
const GET_CONFIG = gql`
{
accounts
accountsConfig {
code
display
class
cryptos
}
cryptoCurrencies {
code
display
}
}
`
const SAVE_ACCOUNTS = gql`
mutation Save($accounts: JSONObject) {
saveAccounts(accounts: $accounts)
}
`
const isConfigurable = it =>
R.contains(it)(['infura', 'bitgo', 'trongrid', 'galoy'])
const isLocalHosted = it =>
R.contains(it)([
'bitcoind',
'geth',
'litecoind',
'dashd',
'zcashd',
'bitcoincashd'
])
const ChooseWallet = ({ data: currentData, addData }) => {
// no need to fetch exchange config here
const schema = _schema()
const { data } = useQuery(GET_CONFIG)
const [saveAccounts] = useMutation(SAVE_ACCOUNTS, {
onCompleted: () => submit()
})
const [selected, setSelected] = useState(null)
const [error, setError] = useState(false)
const accounts = data?.accounts ?? []
const accountsConfig = data?.accountsConfig ?? []
const coin = currentData.coin
const wallets = getItems(accountsConfig, accounts, 'wallet', coin)
const saveWallet = name => wallet => {
const accounts = { [name]: wallet }
return saveAccounts({ variables: { accounts } })
}
const submit = () => {
if (!selected) return setError(true)
addData({ wallet: selected })
}
const onSelect = e => {
setSelected(e.target.value)
setError(false)
}
return (
<div className={classes.mdForm}>
<H4 className={error && classes.error}>Choose your wallet</H4>
<RadioGroup
labelClassName={classes.radioLabel}
className={classes.radioGroup}
options={R.union(wallets.filled, wallets.unfilled)}
value={selected}
onChange={onSelect}
/>
{isLocalHosted(selected) && (
<>
<div className={classes.infoMessage}>
<WarningIcon />
<Info3>
To set up {selected} please read the node wallet instructions from
our support portal.
</Info3>
</div>
<SupportLinkButton
link="https://support.lamassu.is/hc/en-us/articles/115001209552-Setting-up-your-node-wallets"
label="Support article"
/>
</>
)}
{!isConfigurable(selected) && (
<Button size="lg" onClick={submit} className={classes.button}>
Continue
</Button>
)}
{selected === 'bitgo' && (
<>
<div className={classes.infoMessage}>
<WarningIcon />
<Info3>
Make sure you set up a BitGo wallet to enter the necessary
information below. Please follow the instructions on our support
page if you havent.
</Info3>
</div>
<SupportLinkButton
link="https://support.lamassu.is/hc/en-us/articles/360024455592-Setting-up-BitGo"
label="Support article"
/>
<H4 noMargin>Enter wallet information</H4>
<FormRenderer
value={accounts.bitgo}
save={saveWallet(selected)}
elements={bitgo(coin).elements}
validationSchema={bitgo(coin).validationSchema}
buttonLabel={'Continue'}
buttonClass={classes.formButton}
/>
</>
)}
{selected === 'infura' && (
<>
<H4 noMargin>Enter wallet information</H4>
<FormRenderer
value={accounts.infura}
save={saveWallet(selected)}
elements={schema.infura.elements}
validationSchema={schema.infura.getValidationSchema(
accounts.infura
)}
buttonLabel={'Continue'}
buttonClass={classes.formButton}
/>
</>
)}
{selected === 'trongrid' && (
<>
<H4 noMargin>Enter wallet information</H4>
<FormRenderer
value={accounts.trongrid}
save={saveWallet(selected)}
elements={schema.trongrid.elements}
validationSchema={schema.trongrid.getValidationSchema(
accounts.trongrid
)}
buttonLabel={'Continue'}
buttonClass={classes.formButton}
/>
</>
)}
{selected === 'galoy' && (
<>
<H4 noMargin>Enter wallet information</H4>
<FormRenderer
value={accounts.galoy}
save={saveWallet(selected)}
elements={schema.galoy.elements}
validationSchema={schema.galoy.getValidationSchema(accounts.galoy)}
buttonLabel={'Continue'}
buttonClass={classes.formButton}
/>
</>
)}
</div>
)
}
export default ChooseWallet

View file

@ -0,0 +1,45 @@
.radioGroup {
flex-direction: row;
width: 600px;
}
.radioLabel {
width: 150px;
height: 48px;
}
.mdForm {
width: 385px;
}
.infoMessage {
display: flex;
margin-bottom: 20px;
}
.infoMessage > p {
width: 330px;
margin-top: 4px;
margin-left: 16px;
}
.actionButton {
margin-bottom: 32px;
}
.actionButtonLink {
text-decoration: none;
color: var(--zodiac);
}
.error {
color: var(--tomato);
}
.button {
margin-top: 40px;
}
.formButton {
margin: 24px 0 0;
}

View file

@ -0,0 +1,72 @@
import * as R from 'ramda'
import React, { useState } from 'react'
import Sidebar, { Stepper } from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection'
import AllSet from './AllSet'
import Blockcypher from './Blockcypher'
import ChooseCoin from './ChooseCoin'
import ChooseExchange from './ChooseExchange'
import ChooseTicker from './ChooseTicker'
import ChooseWallet from './ChooseWallet'
const steps = [
{
label: 'Choose cryptocurrency',
component: ChooseCoin
},
{
label: 'Choose wallet',
component: ChooseWallet
},
{
label: 'Choose ticker',
component: ChooseTicker
},
{
label: 'Exchange',
component: ChooseExchange
},
{
label: 'Blockcypher',
component: Blockcypher
},
{
label: 'All set',
component: AllSet
}
]
const Wallet = ({ doContinue }) => {
const [step, setStep] = useState(0)
const [data, setData] = useState({})
const mySteps = data?.coin === 'BTC' ? steps : R.remove(4, 1, steps)
const Component = mySteps[step].component
const addData = it => {
setData(R.merge(data, it))
setStep(step + 1)
}
return (
<div className="w-[1132px] h-full mx-auto flex-1 flex flex-col">
<div className="flex justify-between items-center">
<TitleSection title="Wallet settings"></TitleSection>
</div>
<div className="flex flex-1 flex-row">
<Sidebar>
{mySteps.map((it, idx) => (
<Stepper key={idx} step={step} it={it} idx={idx} steps={mySteps} />
))}
</Sidebar>
<div className="ml-12">
<Component data={data} addData={addData} doContinue={doContinue} />
</div>
</div>
</div>
)
}
export default Wallet

View file

@ -0,0 +1,23 @@
import * as R from 'ramda'
import _schema from 'src/pages/Services/schemas'
const contains = crypto => R.compose(R.contains(crypto), R.prop('cryptos'))
const sameClass = type => R.propEq('class', type)
const filterConfig = (crypto, type) =>
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
export const getItems = (accountsConfig, accounts, type, crypto) => {
const schema = _schema()
const fConfig = filterConfig(crypto, type)(accountsConfig)
const find = code => accounts && accounts[code]
const [filled, unfilled] = R.partition(({ code }) => {
const account = find(code)
if (!schema[code]) return true
const { getValidationSchema } = schema[code]
return getValidationSchema(account).isValidSync(account)
})(fConfig)
return { filled, unfilled }
}

View file

@ -0,0 +1,22 @@
import React from 'react'
import { H1, P } from 'src/components/typography'
import { Button } from 'src/components/buttons'
function Welcome({ doContinue }) {
return (
<div className="text-center flex flex-col items-center justify-center w-full h-full">
<H1 className="text-5xl">Welcome to the Lamassu Admin</H1>
<P className="text-2xl mb-14 text-comet">
To get you started, weve put together a wizard that will
<br />
help set up what you need before pairing your machines.
</P>
<Button size="xl" onClick={doContinue}>
Get started
</Button>
</div>
)
}
export default Welcome

View file

@ -0,0 +1,131 @@
import * as R from 'ramda'
import React from 'react'
import { getSchema as CommissionsSchema } from 'src/pages/Commissions/helper'
import { WalletSchema } from 'src/pages/Wallet/helper'
import { LocaleSchema } from 'src/pages/Locales/helper'
import { fromNamespace, namespaces } from 'src/utils/config'
import Commissions from './components/Commissions'
import Locale from './components/Locales'
import Twilio from './components/Twilio'
import Wallet from './components/Wallet/Wallet'
import Welcome from './components/Welcome'
const getConfiguredCoins = (config, crypto) => {
const wallet = fromNamespace(namespaces.WALLETS, config)
return R.filter(it =>
WalletSchema.isValidSync(fromNamespace(it.code, wallet))
)(crypto)
}
const hasValidWallet = (config, crypto) => {
const wallet = fromNamespace(namespaces.WALLETS, config)
const coins = R.map(it => fromNamespace(it.code, wallet))(crypto)
const hasValidConfig = R.compose(
R.any(R.identity),
R.map(it => WalletSchema.isValidSync(it))
)(coins)
return hasValidConfig
}
const hasValidLocale = config => {
const locale = fromNamespace(namespaces.LOCALE, config)
return LocaleSchema.isValidSync(locale)
}
const hasValidCommissions = config => {
const commission = fromNamespace(namespaces.COMMISSIONS, config)
const locale = fromNamespace(namespaces.LOCALE, config)
return CommissionsSchema(locale).isValidSync(commission)
}
const getWizardStep = (config, crypto) => {
if (!config) return 0
const validWallet = hasValidWallet(config, crypto)
if (!validWallet) return 1
const validLocale = hasValidLocale(config)
if (!validLocale) return 2
const validCommission = hasValidCommissions(config)
if (!validCommission) return 3
return 0
}
const STEPS = [
{
id: 'welcome',
Component: Welcome
},
{
id: 'wallet',
Component: Wallet,
exImage: '/assets/wizard/fullexample.wallet.png',
subtitle: 'Wallet settings',
text: `Your wallet settings are the first step for this wizard.
We'll start by setting up one of cryptocurrencies to get you up and running,
but you can later set up as many as you want.`
},
{
id: 'locale',
Component: Locale,
exImage: '/assets/wizard/fullexample.locale.png',
subtitle: 'Locales',
text: `From the Locales panel, you can define default settings
that will be applied to all machines you add to your network later on.
These settings may be overridden for specific machines in the Overrides section.`
},
{
id: 'twilio',
Component: Twilio,
exImage: '/assets/wizard/fullexample.twilio.png',
subtitle: 'Twilio (SMS service)',
text: (
<>
Twilio is used for SMS operator notifications, phone number collection
for compliance, and 1-confirmation redemptions on cash-out transactions.
<br />
You'll need to configure Twilio if you're offering cash-out or any
compliance options
</>
)
},
{
id: 'commissions',
Component: Commissions,
exImage: '/assets/wizard/fullexample.commissions.png',
subtitle: 'Commissions',
text: `From the Commissions page, you can define all the commissions of your
machines. The values set here will be default values of all machines
you'll later add to your network. Default settings keep you from
having to enter the same values everytime you add a new machine. Once
a machine is added, you may override these values per machine and per
cryptocurrency in the overrides section.`
}
// {
// id: 'notifications',
// Component: Notifications,
// exImage: '/assets/wizard/fullexample.notifications.png',
// subtitle: 'Notifications',
// text: `Your notification settings will allow customize what notifications you
// get and where. You can later override all default balance alerts setup
// here.`
// },
// {
// id: 'operatorInfo',
// Component: WizardOperatorInfo,
// exImage: '/assets/wizard/fullexample.operatorinfo.png',
// subtitle: 'Operator info',
// text: `Your contact information is important for your customer to be able
// to contact you in case theres a problem with one of your machines.
// In this page, you also be able to set up what you want to share with
// Coin ATM Radar and add the Terms & Services text that is displayed by your machines.`
// }
]
export { getWizardStep, STEPS, getConfiguredCoins }

View file

@ -0,0 +1,3 @@
import Wizard from './Wizard'
export default Wizard