Merge pull request #1620 from RafaelTaranto/feat/customer-email-auth

LAM-996 feat: customer auth via email
This commit is contained in:
Rafael Taranto 2023-11-28 19:05:23 +00:00 committed by GitHub
commit 20058f1efe
28 changed files with 341 additions and 45 deletions

View file

@ -26,4 +26,9 @@ const hasPhone = hasRequirement('sms')
const hasFacephoto = hasRequirement('facephoto')
const hasIdScan = hasRequirement('idCardData')
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan }
const AUTH_METHODS = {
SMS: 'SMS',
EMAIL: 'EMAIL'
}
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS }

View file

@ -6,13 +6,10 @@ const makeDir = require('make-dir')
const path = require('path')
const fs = require('fs')
const util = require('util')
const { sub, differenceInHours } = require('date-fns/fp')
const db = require('./db')
const BN = require('./bn')
const anonymous = require('../lib/constants').anonymousCustomer
const complianceOverrides = require('./compliance_overrides')
const users = require('./users')
const writeFile = util.promisify(fs.writeFile)
const notifierQueries = require('./notifier/queries')
const notifierUtils = require('./notifier/utils')
@ -43,6 +40,12 @@ function add (customer) {
.then(camelize)
}
function addWithEmail (customer) {
const sql = 'insert into customers (id, email, email_at) values ($1, $2, now()) returning *'
return db.one(sql, [uuid.v4(), customer.email])
.then(camelize)
}
/**
* Get single customer by phone
* Phone numbers are unique per customer
@ -60,6 +63,12 @@ function get (phone) {
.then(camelize)
}
function getWithEmail (email) {
const sql = 'select * from customers where email=$1'
return db.oneOrNone(sql, [email])
.then(camelize)
}
/**
* Update customer record
*
@ -308,7 +317,7 @@ const updateSubscriberData = (customerId, data, userToken) => {
*
* Used for the machine.
*/
function getById (id, userToken) {
function getById (id) {
const sql = 'select * from customers where id=$1'
return db.oneOrNone(sql, [id])
.then(assignCustomerData)
@ -349,6 +358,7 @@ function camelizeDeep (customer) {
function getComplianceTypes () {
return [
'sms',
'email',
'id_card_data',
'id_card_photo',
'front_camera',
@ -482,7 +492,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
phone, email, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_data_provided) AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
@ -491,9 +501,9 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
c.suspended_until > NOW() AS is_suspended,
c.front_camera_path, c.front_camera_override,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.phone, c.email, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
GREATEST(c.phone_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
GREATEST(c.phone_at, c.email_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
row_number() OVER (partition by c.id order by t.created desc) AS rn,
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
@ -540,7 +550,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
phone, phone_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
phone, phone_at, email, email_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, subscriber_info_at, custom_fields, notes, is_test_customer
@ -549,7 +559,7 @@ function getCustomerById (id) {
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
c.suspended_until > now() AS is_suspended,
c.front_camera_path, c.front_camera_override, c.front_camera_at,
c.phone, c.phone_at, c.phone_override, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
c.phone, c.phone_at, c.email, c.email_at, c.phone_override, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, c.subscriber_info, c.subscriber_info_at, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
@ -912,7 +922,9 @@ function disableTestCustomer (customerId) {
module.exports = {
add,
addWithEmail,
get,
getWithEmail,
batch,
getCustomersList,
getCustomerById,
@ -930,7 +942,5 @@ module.exports = {
updateEditedPhoto,
updateTxCustomerPhoto,
enableTestCustomer,
disableTestCustomer,
selectLatestData,
getEditedData
disableTestCustomer
}

View file

@ -3,7 +3,7 @@ const ph = require('./plugin-helper')
function sendMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = 'mailgun'
const pluginCode = settings.config.notifications_thirdParty_email
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
@ -11,4 +11,15 @@ function sendMessage (settings, rec) {
})
}
module.exports = {sendMessage}
function sendCustomerMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = settings.config.notifications_thirdParty_email
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
return plugin.sendMessage(account, rec)
})
}
module.exports = {sendMessage, sendCustomerMessage}

View file

@ -115,6 +115,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
configManager.getOperatorInfo(settings.config),
configManager.getReceipt(settings.config),
!!configManager.getCashOut(deviceId, settings.config).active,
configManager.getCustomerAuthenticationMethod(settings.config)
])
.then(([
enablePaperWalletOnly,
@ -125,6 +126,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
operatorInfo,
receiptInfo,
twoWayMode,
customerAuthentication,
]) =>
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
null :
@ -141,6 +143,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
},
machineInfo: { deviceId, deviceName },
twoWayMode,
customerAuthentication,
speedtestFiles,
urlsToPing,
}),

View file

@ -123,6 +123,11 @@ type Terms {
details: TermsDetails
}
enum CustomerAuthentication {
EMAIL
SMS
}
type StaticConfig {
configVersion: Int!
@ -132,6 +137,7 @@ type StaticConfig {
serverVersion: String!
timezone: Int!
twoWayMode: Boolean!
customerAuthentication: CustomerAuthentication!
localeInfo: LocaleInfo!
operatorInfo: OperatorInfo

View file

@ -53,6 +53,7 @@ const ALL_ACCOUNTS = [
{ code: 'telnyx', display: 'Telnyx', class: SMS },
{ code: 'vonage', display: 'Vonage', class: SMS },
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
{ code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true },
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },

View file

@ -12,6 +12,7 @@ const typeDef = gql`
frontCameraAt: Date
frontCameraOverride: String
phone: String
email: String
isAnonymous: Boolean
smsOverride: String
idCardData: JSONObject

View file

@ -1,3 +1,5 @@
const {AUTH_METHODS} = require('./compliance-triggers')
const _ = require('lodash/fp')
const { validate } = require('uuid')
@ -120,6 +122,10 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
const getTriggers = _.get('triggers')
function getCustomerAuthenticationMethod(config) {
return _.get('triggersConfig_customerAuthentication')(config)
}
/* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */
const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => {
return customInfoRequests
@ -193,4 +199,5 @@ module.exports = {
getCryptosFromWalletNamespace,
getCryptoUnits,
setTermsConditions,
getCustomerAuthenticationMethod,
}

View file

@ -763,7 +763,7 @@ function plugins (settings, deviceId) {
function getPhoneCode (phone) {
const notifications = configManager.getNotifications(settings.config)
const code = notifications.thirdParty_sms === 'mock-sms'
const code = settings.config.notifications_thirdParty_sms === 'mock-sms'
? '123'
: randomCode()
@ -779,6 +779,23 @@ function plugins (settings, deviceId) {
})
}
function getEmailCode (toEmail) {
const code = settings.config.notifications_thirdParty_email === 'mock-email'
? '123'
: randomCode()
const rec = {
email: {
toEmail,
subject: 'Your cryptomat code',
text: `Your cryptomat code: ${code}`
}
}
return email.sendCustomerMessage(settings, rec)
.then(() => code)
}
function sweepHdRow (row) {
const txId = row.id
const cryptoCode = row.crypto_code
@ -862,6 +879,7 @@ function plugins (settings, deviceId) {
isZeroConf,
getStatus,
getPhoneCode,
getEmailCode,
executeTrades,
pong,
clearOldLogs,

View file

@ -4,10 +4,25 @@ const NAME = 'Mailgun'
function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
const mailgun = Mailgun({apiKey, domain})
const to = req.email.toEmail ?? toEmail
const emailData = {
from: `Lamassu Server ${fromEmail}`,
to: toEmail,
to,
subject: rec.email.subject,
text: rec.email.body
}
return mailgun.messages().send(emailData)
}
function sendCustomerMessage ({apiKey, domain, fromEmail}, rec) {
const mailgun = Mailgun({apiKey, domain})
const to = req.email.toEmail
const emailData = {
from: fromEmail,
to,
subject: rec.email.subject,
text: rec.email.body
}
@ -17,5 +32,6 @@ function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
module.exports = {
NAME,
sendMessage
sendMessage,
sendCustomerMessage
}

View file

@ -0,0 +1,15 @@
const NAME = 'mock-email'
function sendMessage (settings, rec) {
console.log('sending email', rec)
}
function sendCustomerMessage(settings, rec) {
console.log('sending email', rec)
}
module.exports = {
NAME,
sendMessage,
sendCustomerMessage
}

View file

@ -45,12 +45,12 @@ function toCashOutTx (row) {
return _.set('direction', 'cashOut', newObj)
}
function fetchPhoneTx (phone) {
function fetchEmailOrPhoneTx (data, type) {
const sql = `select * from cash_out_txs
where phone=$1 and dispense=$2
where ${type === 'email' ? 'email' : 'phone'}=$1 and dispense=$2
and (extract(epoch from (now() - created))) * 1000 < $3`
const values = [phone, false, TRANSACTION_EXPIRATION]
const values = [data, false, TRANSACTION_EXPIRATION]
return db.any(sql, values)
.then(_.map(toCashOutTx))
@ -72,6 +72,13 @@ function fetchPhoneTx (phone) {
throw httpError('No transactions', 404)
})
}
function fetchEmailTx (email) {
return fetchEmailOrPhoneTx(email, 'email')
}
function fetchPhoneTx (phone) {
return fetchEmailOrPhoneTx(phone, 'phone')
}
function fetchStatusTx (txId, status) {
const sql = 'select * from cash_out_txs where id=$1'
@ -88,6 +95,7 @@ function fetchStatusTx (txId, status) {
module.exports = {
stateChange,
fetchPhoneTx,
fetchEmailTx,
fetchStatusTx,
httpError
}

View file

@ -77,7 +77,10 @@ app.use('/verify_user', verifyUserRoutes)
app.use('/verify_transaction', verifyTxRoutes)
app.use('/verify_promo_code', verifyPromoCodeRoutes)
// BACKWARDS_COMPATIBILITY 9.0
// machines before 9.0 still use the phone_code route
app.use('/phone_code', phoneCodeRoutes)
app.use('/customer', customerRoutes)
app.use('/tx', txRoutes)

View file

@ -20,6 +20,9 @@ const machineLoader = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
const T = require('../time')
const plugins = require('../plugins')
const Tx = require('../tx')
const loyalty = require('../loyalty')
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
if (_.isNil(patch.data)) {
@ -185,6 +188,70 @@ function sendSmsReceipt (req, res, next) {
})
}
function addOrUpdateCustomer (customerData, config, isEmailAuth) {
const triggers = configManager.getTriggers(config)
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
const customerKey = isEmailAuth ? customerData.email : customerData.phone
const getFunc = isEmailAuth ? customers.getWithEmail : customers.get
const addFunction = isEmailAuth ? customers.addWithEmail : customers.add
return getFunc(customerKey)
.then(customer => {
if (customer) return customer
return addFunction(customerData)
})
.then(customer => customers.getById(customer.id))
.then(customer => {
return Tx.customerHistory(customer.id, maxDaysThreshold)
.then(result => {
customer.txHistory = result
return customer
})
})
.then(customer => {
return loyalty.getCustomerActiveIndividualDiscount(customer.id)
.then(discount => ({ ...customer, discount }))
})
}
function getOrAddCustomerPhone (req, res, next) {
const customerData = req.body
const pi = plugins(req.settings, req.deviceId)
const phone = req.body.phone
return pi.getPhoneCode(phone)
.then(code => {
return addOrUpdateCustomer(customerData, req.settings.config, false)
.then(customer => respond(req, res, { code, customer }))
})
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
throw err
})
.catch(next)
}
function getOrAddCustomerEmail (req, res, next) {
const customerData = req.body
const pi = plugins(req.settings, req.deviceId)
const email = req.body.email
return pi.getEmailCode(email)
.then(code => {
return addOrUpdateCustomer(customerData, req.settings.config, true)
.then(customer => respond(req, res, { code, customer }))
})
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
throw err
})
.catch(next)
}
router.patch('/:id', updateCustomer)
router.patch('/:id/sanctions', triggerSanctions)
router.patch('/:id/block', triggerBlock)
@ -192,5 +259,7 @@ router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
router.post('/:id/smsreceipt', sendSmsReceipt)
router.post('/phone_code', getOrAddCustomerPhone)
router.post('/email_code', getOrAddCustomerEmail)
module.exports = router

View file

@ -66,8 +66,19 @@ function getPhoneTx (req, res, next) {
return next(httpError('Not Found', 404))
}
function getEmailTx (req, res, next) {
if (req.query.email) {
return helpers.fetchEmailTx(req.query.email)
.then(r => res.json(r))
.catch(next)
}
return next(httpError('Not Found', 404))
}
router.post('/', postTx)
router.get('/:id', getTx)
router.get('/', getPhoneTx)
router.get('/', getEmailTx)
module.exports = { postTx, getTx, getPhoneTx, router }
module.exports = { postTx, getTx, getPhoneTx, getEmailTx, router }

View file

@ -0,0 +1,14 @@
const db = require('./db')
exports.up = function (next) {
let sql = [
'ALTER TABLE customers ADD COLUMN email text unique',
'ALTER TABLE customers ADD COLUMN email_at timestamptz',
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,18 @@
const { migrationSaveConfig } = require('../lib/new-settings-loader')
exports.up = function (next) {
const triggersDefault = {
triggersConfig_customerAuthentication: 'SMS',
}
return migrationSaveConfig(triggersDefault)
.then(() => next())
.catch(err => {
console.log(err.message)
return next(err)
})
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,14 @@
const db = require('./db')
exports.up = function (next) {
let sql = [
'ALTER TABLE cash_in_txs ADD COLUMN email text',
'ALTER TABLE cash_out_txs ADD COLUMN email text',
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -57,6 +57,7 @@ const GET_CUSTOMER = gql`
frontCameraAt
frontCameraOverride
phone
email
isAnonymous
smsOverride
idCardData
@ -132,6 +133,7 @@ const SET_CUSTOMER = gql`
frontCameraPath
frontCameraOverride
phone
email
smsOverride
idCardData
idCardDataOverride
@ -516,6 +518,8 @@ const CustomerProfile = memo(() => {
})) ?? []
const classes = useStyles()
const email = R.path(['email'])(customerData)
const phone = R.path(['phone'])(customerData)
return (
<>
@ -532,10 +536,9 @@ const CustomerProfile = memo(() => {
<Label2 noMargin className={classes.labelLink}>
{name.length
? name
: getFormattedPhone(
R.path(['phone'])(customerData),
locale.country
)}
: email?.length
? email
: getFormattedPhone(phone, locale.country)}
</Label2>
</Breadcrumbs>
<div className={classes.panels}>

View file

@ -39,6 +39,7 @@ const GET_CUSTOMERS = gql`
id
idCardData
phone
email
totalTxs
totalSpent
lastActive

View file

@ -25,9 +25,9 @@ const CustomersList = ({
const elements = [
{
header: 'Phone',
header: 'Phone/email',
width: 199,
view: it => getFormattedPhone(it.phone, locale.country)
view: it => `${getFormattedPhone(it.phone, locale.country)} ${it.email}`
},
{
header: 'Name',

View file

@ -17,6 +17,9 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
const usSsn = R.path(['usSsn'])(customer)
const name = getName(customer)
const email = R.path(['email'])(customer)
const phone = R.path(['phone'])(customer)
const elements = [
{
@ -40,7 +43,12 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
value: usSsn
})
const name = getName(customer)
if (email)
elements.push({
header: 'Email',
size: 190,
value: email
})
return (
<Box display="flex">
@ -51,7 +59,9 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
<H2 noMargin>
{name.length
? name
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
: email?.length
? email
: getFormattedPhone(phone, locale.country)}
</H2>
</div>
<Box display="flex" mt="auto">

View file

@ -31,7 +31,8 @@ const ThirdPartyProvider = () => {
}
const ThirdPartySchema = Yup.object().shape({
sms: Yup.string('The sms must be a string').required('The sms is required')
sms: Yup.string('SMS must be a string').required('SMS is required'),
email: Yup.string('Email must be a string').required('Email is required')
})
const elements = [
@ -46,14 +47,30 @@ const ThirdPartyProvider = () => {
valueProp: 'code',
labelProp: 'display'
}
},
{
name: 'email',
size: 'sm',
view: getDisplayName('email'),
width: 175,
input: Autocomplete,
inputProps: {
options: filterOptions('email'),
valueProp: 'code',
labelProp: 'display'
}
}
]
const values = {
sms: data.sms ?? 'twilio',
email: data.email ?? 'mailgun'
}
return (
<EditableTable
name="thirdParty"
initialValues={{ sms: data.sms ?? 'twilio' }}
data={R.of({ sms: data.sms ?? 'twilio' })}
initialValues={values}
data={R.of(values)}
error={error?.message}
enableEdit
editWidth={174}

View file

@ -28,7 +28,8 @@ const TriggerView = ({
config,
toggleWizard,
addNewTriger,
customInfoRequests
customInfoRequests,
emailAuth
}) => {
const currency = R.path(['fiatCurrency'])(
fromNamespace(namespaces.LOCALE)(config)
@ -77,6 +78,7 @@ const TriggerView = ({
save={add}
onClose={() => toggleWizard(true)}
customInfoRequests={customInfoRequests}
emailAuth={emailAuth}
/>
)}
{R.isEmpty(triggers) && (

View file

@ -58,7 +58,7 @@ const GET_CUSTOM_REQUESTS = gql`
const Triggers = () => {
const classes = useStyles()
const [wizardType, setWizard] = useState(false)
const { data, loading: configLoading } = useQuery(GET_CONFIG)
const { data, loading: configLoading, refetch } = useQuery(GET_CONFIG)
const { data: customInfoReqData, loading: customInfoLoading } = useQuery(
GET_CUSTOM_REQUESTS
)
@ -72,6 +72,8 @@ const Triggers = () => {
const enabledCustomInfoRequests = R.filter(R.propEq('enabled', true))(
customInfoRequests
)
const emailAuth =
data?.config?.triggersConfig_customerAuthentication === 'EMAIL'
const triggers = fromServer(data?.config?.triggers ?? [])
const complianceConfig =
@ -141,6 +143,7 @@ const Triggers = () => {
inverseIcon: ReverseSettingsIcon,
forceDisable: !(subMenu === 'advancedSettings'),
toggle: show => {
refetch()
setSubMenu(show ? 'advancedSettings' : false)
}
},
@ -150,6 +153,7 @@ const Triggers = () => {
inverseIcon: ReverseCustomInfoIcon,
forceDisable: !(subMenu === 'customInfoRequests'),
toggle: show => {
refetch()
setSubMenu(show ? 'customInfoRequests' : false)
}
}
@ -216,6 +220,7 @@ const Triggers = () => {
toggleWizard={toggleWizard('newTrigger')}
addNewTriger={addNewTriger}
customInfoRequests={enabledCustomInfoRequests}
emailAuth={emailAuth}
/>
)}
{!loading && subMenu === 'advancedSettings' && (

View file

@ -48,14 +48,14 @@ const styles = {
const useStyles = makeStyles(styles)
const getStep = (step, currency, customInfoRequests) => {
const getStep = (step, currency, customInfoRequests, emailAuth) => {
switch (step) {
// case 1:
// return txDirection
case 1:
return type(currency)
case 2:
return requirements(customInfoRequests)
return requirements(customInfoRequests, emailAuth)
default:
return Fragment
}
@ -138,6 +138,8 @@ const getTypeText = (config, currency, classes) => {
const getRequirementText = (config, classes) => {
switch (config.requirement?.requirement) {
case 'email':
return <>asked to enter code provided through email verification</>
case 'sms':
return <>asked to enter code provided through SMS verification</>
case 'idCardPhoto':
@ -202,7 +204,14 @@ const GetValues = ({ setValues }) => {
return null
}
const Wizard = ({ onClose, save, error, currency, customInfoRequests }) => {
const Wizard = ({
onClose,
save,
error,
currency,
customInfoRequests,
emailAuth
}) => {
const classes = useStyles()
const [liveValues, setLiveValues] = useState({})
@ -211,7 +220,7 @@ const Wizard = ({ onClose, save, error, currency, customInfoRequests }) => {
})
const isLastStep = step === LAST_STEP
const stepOptions = getStep(step, currency, customInfoRequests)
const stepOptions = getStep(step, currency, customInfoRequests, emailAuth)
const onContinue = async it => {
const newConfig = R.merge(config, stepOptions.schema.cast(it))

View file

@ -94,6 +94,21 @@ const getDefaultSettings = () => {
labelProp: 'display',
valueProp: 'code'
}
},
{
name: 'customerAuthentication',
header: 'Customer Auth',
width: 196,
size: 'sm',
input: Autocomplete,
inputProps: {
options: [
{ code: 'SMS', display: 'SMS' },
{ code: 'EMAIL', display: 'EMAIL' }
],
labelProp: 'display',
valueProp: 'code'
}
}
]
}
@ -144,7 +159,8 @@ const getOverrides = customInfoRequests => {
const defaults = [
{
expirationTime: 'Forever',
automation: 'Automatic'
automation: 'Automatic',
customerAuth: 'SMS'
}
]

View file

@ -522,6 +522,7 @@ const requirementSchema = Yup.object()
const requirementOptions = [
{ display: 'SMS verification', code: 'sms' },
{ display: 'Email verification', code: 'email' },
{ display: 'ID card image', code: 'idCardPhoto' },
{ display: 'ID data', code: 'idCardData' },
{ display: 'Customer camera', code: 'facephoto' },
@ -544,7 +545,7 @@ const hasCustomRequirementError = (errors, touched, values) =>
(!values.requirement?.customInfoRequestId ||
!R.isNil(values.requirement?.customInfoRequestId))
const Requirement = ({ customInfoRequests }) => {
const Requirement = ({ customInfoRequests, emailAuth }) => {
const classes = useStyles()
const {
touched,
@ -567,9 +568,11 @@ const Requirement = ({ customInfoRequests }) => {
display: 'Custom information requirement',
code: 'custom'
}
const itemToRemove = emailAuth ? 'sms' : 'email'
const reqOptions = requirementOptions.filter(it => it.code !== itemToRemove)
const options = enableCustomRequirement
? [...requirementOptions, customInfoOption]
: [...requirementOptions]
? [...reqOptions, customInfoOption]
: [...reqOptions]
const titleClass = {
[classes.error]:
(!!errors.requirement && !isSuspend && !isCustom) ||
@ -621,11 +624,11 @@ const Requirement = ({ customInfoRequests }) => {
)
}
const requirements = customInfoRequests => ({
const requirements = (customInfoRequests, emailAuth) => ({
schema: requirementSchema,
options: requirementOptions,
Component: Requirement,
props: { customInfoRequests },
props: { customInfoRequests, emailAuth },
hasRequirementError: hasRequirementError,
hasCustomRequirementError: hasCustomRequirementError,
initialValues: {