Skip to content

Commit

Permalink
fix(payments): better validation for Stripe elements
Browse files Browse the repository at this point in the history
- add onBlur handling to trigger validation

- support for custom onValidate prop for Stripe fields

fixes #2262
  • Loading branch information
lmorchard committed Aug 26, 2019
1 parent 2ac0eff commit fffd3e9
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 19 deletions.
70 changes: 65 additions & 5 deletions packages/fxa-payments-server/src/components/fields.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState, useCallback, useContext, useRef } from 'react';
import { render, cleanup, fireEvent, prettyDOM } from '@testing-library/react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { ReactStripeElements } from 'react-stripe-elements';
import {
FieldGroup,
Form,
Expand Down Expand Up @@ -288,12 +287,23 @@ describe('StripeElement', () => {
constructor(props: MockStripeElementProps) {
super(props);
}
handleClick = (ev: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
handleClick = (ev: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
ev.preventDefault();
this.props.onChange(value);
};
handleBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
ev.preventDefault();
this.props.onBlur();
};
render() {
return <button data-testid="mockStripe" onClick={this.handleClick} />;
return (
<input
type="text"
data-testid="mockStripe"
onClick={this.handleClick}
onBlur={this.handleBlur}
/>
);
}
};

Expand All @@ -310,7 +320,10 @@ describe('StripeElement', () => {
});

it('does nothing if value is not yet complete', () => {
const MockStripeElement = buildMockStripeElement({ complete: false, error: null });
const MockStripeElement = buildMockStripeElement({
complete: false,
error: null,
});
const validatorStateRef = mkValidatorStateRef();
const { getByTestId } = render(
<TestForm validatorStateRef={validatorStateRef}>
Expand Down Expand Up @@ -356,6 +369,53 @@ describe('StripeElement', () => {
});
});

it('runs validation for empty value if focused and blurred', () => {
const MockStripeElement = buildMockStripeElement(undefined);
const validatorStateRef = mkValidatorStateRef();
const { container, getByTestId } = render(
<TestForm validatorStateRef={validatorStateRef}>
<StripeElement
data-testid="input-1"
label="Frobnitz"
name="input-1"
required={true}
component={MockStripeElement}
/>
</TestForm>
);

fireEvent.blur(getByTestId('mockStripe'));

const tooltipEl = container.querySelector('aside.tooltip');
expect(tooltipEl).not.toBeNull();
expect((tooltipEl as Element).textContent).toContain('Frobnitz is required');
});

it('supports a custom onValidate function', () => {
const MockStripeElement = buildMockStripeElement(
{ value: 'foo', error: 'not this', complete: true }
);
const validatorStateRef = mkValidatorStateRef();
const expectedError = 'My hovercraft is full of eels';
const onValidate = jest.fn(value => ({ value, error: expectedError }));
const { container, getByTestId } = render(
<TestForm validatorStateRef={validatorStateRef}>
<StripeElement
data-testid="input-1"
name="input-1"
component={MockStripeElement}
onValidate={onValidate}
/>
</TestForm>
);
fireEvent.click(getByTestId('mockStripe'));
expect(onValidate).toBeCalled();

const tooltipEl = container.querySelector('aside.tooltip');
expect(tooltipEl).not.toBeNull();
expect((tooltipEl as Element).textContent).toContain(expectedError);
});

it('trims off trailing periods from error messages (issue #1718)', () => {
const MockStripeElement = buildMockStripeElement({
error: { message: 'period.' },
Expand Down
53 changes: 39 additions & 14 deletions packages/fxa-payments-server/src/components/fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export const Input = (props: InputProps) => {
);
};

type StripeElementProps = FieldProps & {
type StripeElementProps = { onValidate?: OnValidateFunction } & FieldProps & {
component: any;
} & ReactStripeElements.ElementProps;

Expand All @@ -170,32 +170,56 @@ export const StripeElement = (props: StripeElementProps) => {
component: StripeElementComponent,
name,
tooltip,
onValidate,
required = false,
label,
className,
...childProps
} = props;
const { validator } = useContext(FormContext) as FormContextValue;

const elementValue = useRef<stripe.elements.ElementChangeResponse>();

const onChange = useCallback(
(value: stripe.elements.ElementChangeResponse) => {
if (value !== null) {
if (value.error && value.error.message) {
let error = value.error.message;
// Issue #1718 - remove periods from error messages from Stripe
// for consistency with our own errors
if (error.endsWith('.')) {
error = error.slice(0, -1);
}
validator.updateField({ name, value, valid: false, error });
} else if (value.complete) {
validator.updateField({ name, value, valid: true });
}
}
elementValue.current = value;
validateElementValue();
},
[name, validator]
);

const onBlur = useCallback(
() => validateElementValue(),
[name, validator]
);

const validateElementValue = () => {
const value = elementValue.current;
if (onValidate) {
const { value: newValue, error } = onValidate(value);
validator.updateField({
name,
value: newValue,
error,
valid: error === null,
});
} else if (!value) {
if (required) {
validator.updateField({ name, value, error: `${label} is required` });
}
} else if (value.error && value.error.message) {
let error = value.error.message;
// Issue #1718 - remove periods from error messages from Stripe
// for consistency with our own errors
if (error.endsWith('.')) {
error = error.slice(0, -1);
}
validator.updateField({ name, value, valid: false, error });
} else if (value.complete) {
validator.updateField({ name, value, valid: true });
}
};

const tooltipParentRef = useRef<any>(null);
const stripeElementRef = (el: any) => {
// HACK: Stripe elements stash their underlying DOM element in el._ref,
Expand All @@ -222,6 +246,7 @@ export const StripeElement = (props: StripeElementProps) => {
...childProps,
ref: stripeElementRef,
onChange,
onBlur,
}}
/>
</Field>
Expand Down

0 comments on commit fffd3e9

Please sign in to comment.