fix: triggers wizard fields are more responsive to errors, improved UX
This commit is contained in:
parent
44d63337fe
commit
9094569b83
2 changed files with 150 additions and 52 deletions
|
|
@ -222,6 +222,42 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createErrorMessage = (errors, touched, values) => {
|
||||||
|
const triggerType = values?.triggerType
|
||||||
|
const containsType = R.contains(triggerType)
|
||||||
|
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||||
|
|
||||||
|
const hasRequirementError =
|
||||||
|
!!errors.requirement?.suspensionDays &&
|
||||||
|
!!touched.requirement?.suspensionDays &&
|
||||||
|
(!values.requirement?.suspensionDays ||
|
||||||
|
values.requirement?.suspensionDays < 0)
|
||||||
|
|
||||||
|
const hasAmountError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.threshold &&
|
||||||
|
!containsType(['consecutiveDays']) &&
|
||||||
|
(!values.threshold?.threshold || values.threshold?.threshold < 0)
|
||||||
|
|
||||||
|
const hasDaysError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.thresholdDays &&
|
||||||
|
!containsType(['txAmount']) &&
|
||||||
|
(!values.threshold?.thresholdDays || values.threshold?.thresholdDays < 0)
|
||||||
|
|
||||||
|
if (containsType(['txAmount', 'txVolume', 'txVelocity']) && hasAmountError)
|
||||||
|
return errors.threshold
|
||||||
|
|
||||||
|
if (
|
||||||
|
containsType(['txVolume', 'txVelocity', 'consecutiveDays']) &&
|
||||||
|
hasDaysError
|
||||||
|
)
|
||||||
|
return errors.threshold
|
||||||
|
|
||||||
|
if (isSuspend && hasRequirementError)
|
||||||
|
return errors.requirement?.suspensionDays
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -246,21 +282,28 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
||||||
/>
|
/>
|
||||||
<Formik
|
<Formik
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
validateOnChange={false}
|
validateOnChange={true}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
onSubmit={onContinue}
|
onSubmit={onContinue}
|
||||||
initialValues={stepOptions.initialValues}
|
initialValues={stepOptions.initialValues}
|
||||||
validationSchema={stepOptions.schema}>
|
validationSchema={stepOptions.schema}>
|
||||||
<Form className={classes.form}>
|
{({ errors, touched, values }) => (
|
||||||
<GetValues setValues={setLiveValues} />
|
<Form className={classes.form}>
|
||||||
<stepOptions.Component {...stepOptions.props} />
|
<GetValues setValues={setLiveValues} />
|
||||||
<div className={classes.submit}>
|
<stepOptions.Component {...stepOptions.props} />
|
||||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
<div className={classes.submit}>
|
||||||
<Button className={classes.button} type="submit">
|
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||||
{isLastStep ? 'Finish' : 'Next'}
|
{createErrorMessage(errors, touched, values) && (
|
||||||
</Button>
|
<ErrorMessage>
|
||||||
</div>
|
{createErrorMessage(errors, touched, values)}
|
||||||
</Form>
|
</ErrorMessage>
|
||||||
|
)}
|
||||||
|
<Button className={classes.button} type="submit">
|
||||||
|
{isLastStep ? 'Finish' : 'Next'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -258,25 +258,48 @@ const typeSchema = Yup.object()
|
||||||
.nullable()
|
.nullable()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.test(
|
.test(({ threshold, triggerType }, context) => {
|
||||||
'are-fields-set',
|
console.log(context)
|
||||||
'All fields must be set.',
|
const errorMessages = {
|
||||||
({ triggerType, threshold }, context) => {
|
txAmount: threshold => 'Amount must be greater than or equal to 0',
|
||||||
const validator = {
|
txVolume: threshold => {
|
||||||
txAmount: threshold => threshold.threshold >= 0,
|
const thresholdMessage = 'Volume must be greater than or equal to 0'
|
||||||
txVolume: threshold =>
|
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||||
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
const message = []
|
||||||
txVelocity: threshold =>
|
if (!threshold.threshold || threshold.threshold < 0)
|
||||||
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
message.push(thresholdMessage)
|
||||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
if (!threshold.thresholdDays || threshold.thresholdDays <= 0)
|
||||||
}
|
message.push(thresholdDaysMessage)
|
||||||
|
return message.join(', ')
|
||||||
return (
|
},
|
||||||
(triggerType && validator?.[triggerType](threshold)) ||
|
txVelocity: threshold => {
|
||||||
context.createError({ path: 'threshold' })
|
const thresholdMessage = 'Transactions must be greater than 0'
|
||||||
)
|
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||||
|
const message = []
|
||||||
|
if (!threshold.threshold || threshold.threshold <= 0)
|
||||||
|
message.push(thresholdMessage)
|
||||||
|
if (!threshold.thresholdDays || threshold.thresholdDays <= 0)
|
||||||
|
message.push(thresholdDaysMessage)
|
||||||
|
return message.join(', ')
|
||||||
|
},
|
||||||
|
consecutiveDays: threshold => 'Days must be greater than 0'
|
||||||
}
|
}
|
||||||
)
|
const thresholdValidator = {
|
||||||
|
txAmount: threshold => threshold.threshold >= 0,
|
||||||
|
txVolume: threshold =>
|
||||||
|
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
||||||
|
txVelocity: threshold =>
|
||||||
|
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
||||||
|
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
||||||
|
|
||||||
|
return context.createError({
|
||||||
|
path: 'threshold',
|
||||||
|
message: errorMessages[triggerType](threshold)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{ display: 'Transaction amount', code: 'txAmount' },
|
{ display: 'Transaction amount', code: 'txAmount' },
|
||||||
|
|
@ -299,11 +322,21 @@ const Type = ({ ...props }) => {
|
||||||
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
||||||
const isConsecutiveDaysEnabled = containsType(['consecutiveDays'])
|
const isConsecutiveDaysEnabled = containsType(['consecutiveDays'])
|
||||||
|
|
||||||
|
const hasAmountError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.threshold &&
|
||||||
|
!isConsecutiveDaysEnabled &&
|
||||||
|
(!values.threshold?.threshold || values.threshold?.threshold < 0)
|
||||||
|
const hasDaysError =
|
||||||
|
!!errors.threshold &&
|
||||||
|
!!touched.threshold?.thresholdDays &&
|
||||||
|
!containsType(['txAmount']) &&
|
||||||
|
(!values.threshold?.thresholdDays || values.threshold?.thresholdDays < 0)
|
||||||
|
|
||||||
|
const triggerTypeError = !!(hasDaysError || hasAmountError)
|
||||||
|
|
||||||
const thresholdClass = {
|
const thresholdClass = {
|
||||||
[classes.error]:
|
[classes.error]: triggerTypeError
|
||||||
errors.threshold &&
|
|
||||||
((!containsType(['consecutiveDays']) && touched.threshold?.threshold) ||
|
|
||||||
(!containsType(['txAmount']) && touched.threshold?.thresholdDays))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRadioGroupActive = () => {
|
const isRadioGroupActive = () => {
|
||||||
|
|
@ -343,6 +376,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.threshold"
|
name="threshold.threshold"
|
||||||
|
error={hasAmountError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>
|
<Info1 className={classnames(classes.description)}>
|
||||||
{props.currency}
|
{props.currency}
|
||||||
|
|
@ -356,6 +390,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.threshold"
|
name="threshold.threshold"
|
||||||
|
error={hasAmountError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>
|
<Info1 className={classnames(classes.description)}>
|
||||||
transactions
|
transactions
|
||||||
|
|
@ -377,6 +412,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.thresholdDays"
|
name="threshold.thresholdDays"
|
||||||
|
error={hasDaysError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>days</Info1>
|
<Info1 className={classnames(classes.description)}>days</Info1>
|
||||||
</>
|
</>
|
||||||
|
|
@ -388,6 +424,7 @@ const Type = ({ ...props }) => {
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
size="lg"
|
size="lg"
|
||||||
name="threshold.thresholdDays"
|
name="threshold.thresholdDays"
|
||||||
|
error={hasDaysError}
|
||||||
/>
|
/>
|
||||||
<Info1 className={classnames(classes.description)}>
|
<Info1 className={classnames(classes.description)}>
|
||||||
consecutive days
|
consecutive days
|
||||||
|
|
@ -411,20 +448,34 @@ const type = currency => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const requirementSchema = Yup.object().shape({
|
const requirementSchema = Yup.object()
|
||||||
requirement: Yup.object({
|
.shape({
|
||||||
requirement: Yup.string().required(),
|
requirement: Yup.object({
|
||||||
suspensionDays: Yup.number().when('requirement', {
|
requirement: Yup.string().required(),
|
||||||
is: value => value === 'suspend',
|
suspensionDays: Yup.number().when('requirement', {
|
||||||
then: Yup.number()
|
is: value => value === 'suspend',
|
||||||
.required()
|
then: Yup.number()
|
||||||
.min(1),
|
.required()
|
||||||
otherwise: Yup.number()
|
.min(1),
|
||||||
.nullable()
|
otherwise: Yup.number()
|
||||||
.transform(() => null)
|
.nullable()
|
||||||
|
.transform(() => null)
|
||||||
|
})
|
||||||
|
}).required()
|
||||||
|
})
|
||||||
|
.test(({ requirement }, context) => {
|
||||||
|
const requirementValidator = requirement =>
|
||||||
|
requirement.requirement === 'suspend'
|
||||||
|
? requirement.suspensionDays > 0
|
||||||
|
: true
|
||||||
|
|
||||||
|
if (requirement && requirementValidator(requirement)) return
|
||||||
|
|
||||||
|
return context.createError({
|
||||||
|
path: 'requirement',
|
||||||
|
message: 'Suspension days must be greater than 0'
|
||||||
})
|
})
|
||||||
}).required()
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const requirementOptions = [
|
const requirementOptions = [
|
||||||
{ display: 'SMS verification', code: 'sms' },
|
{ display: 'SMS verification', code: 'sms' },
|
||||||
|
|
@ -442,17 +493,20 @@ const Requirement = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { touched, errors, values } = useFormikContext()
|
const { touched, errors, values } = useFormikContext()
|
||||||
|
|
||||||
|
const hasRequirementError =
|
||||||
|
!!errors.requirement?.suspensionDays &&
|
||||||
|
!!touched.requirement?.suspensionDays &&
|
||||||
|
(!values.requirement?.suspensionDays ||
|
||||||
|
values.requirement?.suspensionDays < 0)
|
||||||
|
|
||||||
|
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||||
|
|
||||||
const titleClass = {
|
const titleClass = {
|
||||||
[classes.error]:
|
[classes.error]:
|
||||||
!R.isEmpty(R.omit(['suspensionDays'], errors.requirement)) ||
|
!R.isEmpty(R.omit(['suspensionDays'], errors.requirement)) ||
|
||||||
(errors.requirement &&
|
(isSuspend && hasRequirementError)
|
||||||
touched.requirement &&
|
|
||||||
errors.requirement.suspensionDays &&
|
|
||||||
touched.requirement.suspensionDays)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
|
|
@ -474,6 +528,7 @@ const Requirement = () => {
|
||||||
label="Days"
|
label="Days"
|
||||||
size="lg"
|
size="lg"
|
||||||
name="requirement.suspensionDays"
|
name="requirement.suspensionDays"
|
||||||
|
error={hasRequirementError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue