fix: triggers wizard fields are more responsive to errors, improved UX

This commit is contained in:
Sérgio Salgado 2021-03-29 15:29:06 +01:00 committed by Josh Harvey
parent 44d63337fe
commit 9094569b83
2 changed files with 150 additions and 52 deletions

View file

@ -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>
</> </>

View file

@ -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}
/> />
)} )}
</> </>