React-stripe-elements: Validate Stripe Elements without calling `createToken`

Created on 3 Dec 2018  Â·  14Comments  Â·  Source: stripe/react-stripe-elements

Summary

Sorry if this is the wrong place for this but IRC and chat support was proving difficult -- I figured I'd hit the right people through GitHub.

I'm struggling to work out how one can trigger the Stripe Elements client-side validation without calling createToken i.e.

Given this form:

screenshot 2018-12-03 at 22 36 36

And this handler:

handleSubmit = async (e) => {
    e.preventDefault();
    const firstNameError = validateFirstName(this.props.firstName);
    const lastNameError = validateLastName(this.props.lastName);
    const stripeElementsErrors = this.props.stripe.validateElements(); // Does this method exist?
    if (!firstNameError && !lastNameError && !stripeElementErrors.length) {
        const { token, error } = await this.props.stripe.createToken({
             name: `${firstName} ${lastName}`,
        });
        // ...
    }
}

The key is I need to be able to render validation errors on both my non-stripe fields (firstName and lastName) as well as my Stripe fields before attempting to create the token; I can't call createToken if my billing fields are invalid.

Most helpful comment

Hi @richardscarrott, there isn't a way to do this either with react-stripe-elements or Stripe.js itself. The recommended pattern here is to:

  1. Assume that the user's input starts in an incomplete state and block form submission.
  2. Use an element.on('change', (event) => …) event handler to update your form's state.
  3. If there are errors in event.errors, display them and continue to block form submission.
  4. When you receive a change event with event.complete == true, allow form submission to proceed.

This will require tracking the Elements validation state inside your form component. Hope that clears things up! If you have any specific questions about a form you're building Stripe Support is the best avenue for support: https://support.stripe.com/email

This repository's issue tracker is for the development of the react-stripe-elements library itself rather than Stripe.js questions.

All 14 comments

Hi @richardscarrott, there isn't a way to do this either with react-stripe-elements or Stripe.js itself. The recommended pattern here is to:

  1. Assume that the user's input starts in an incomplete state and block form submission.
  2. Use an element.on('change', (event) => …) event handler to update your form's state.
  3. If there are errors in event.errors, display them and continue to block form submission.
  4. When you receive a change event with event.complete == true, allow form submission to proceed.

This will require tracking the Elements validation state inside your form component. Hope that clears things up! If you have any specific questions about a form you're building Stripe Support is the best avenue for support: https://support.stripe.com/email

This repository's issue tracker is for the development of the react-stripe-elements library itself rather than Stripe.js questions.

@fred-stripe can you provide an example of this?

I don't think this issue should be closed. TS raised a very valid use case for card validation without calling createToken. The solution provided sounds more like a hack than a proper solution to the actual issue.

As another (less dirty) hack, devs can add option to allow triggering change event for element. E.g. something like element.trigger('change');. It can be used on form submit event to make sure card details are filled in, without having to use createToken for this purpose.

@fred-stripe your solution requires someone to actually touch the fields. What if the user doesn't touch the fields? My other (billing) fields validate, but somehow these can't?

Why can't Stripe expose validate on the instance of Stripe.elements() ? Or validateElements on Stripe instance? This is a no-brainer!

I concur that this would be valuable. In the world of angular, you would normally write code along the lines of form.markAllAsTouched() which would then show the invalid highlighting. It would be useful to have a way of programatically touching the fields to display the highlighting/errors.

The demo here seems to do 'something' like the desired behaviour, but I am struggling to get my head around what is going on with it?
https://hhqhp.sse.codesandbox.io/ - demo
https://codesandbox.io/s/stripe-sample-accept-a-card-payment-hhqhp - sandbox

Can anyone advise how this demo is working?

EDIT - just noticed it is calling the stripe API regardless, even though it knows the element is empty, so the message would appear, but not any validation highlighting on fields (e.g. if you had a background color, or border color)

@adam-marshall that demo just calls handlePayment (it uses payment intents), which is similar to createToken. I simply changed the logic on my form not to disable the submit button if the form is invalid. The button runs my validations and regardless of the valid/invalid state tries to create the token.

I only proceed if both the form is valid and I got a token back.

I stil think the Elements API _should_ expose some kind of programmatic validation hook, without requiring a user action.

@psteininger I've settled on a similar solution now. I can check if error.type is 'validation_error' or not and then behave differently accordingly which gives me something at least. Does seem a waste of an API call if there was a way of exposing whether the elements are incomplete and if so manually trigger them to be touched, or as you say actually a .validate() hook.

I concur with everyone else: it's nuts that there's no way to validate the card element. That makes for a very poor API that requires shoddy workarounds like, "Don't worry; just listen for changes on the element and we'll tell you when it's not invalid anymore."

not having a state getter makes it possible to just click a custom submit button and fire a request to stripe. there's no way to distinguish the state this way

// The names are important
const [cardComplete, setCardComplete] = useState({
    cardNumber: false, cardExpiry: false, cardCvc: false
})

function handleCardElementOnChange(e) {
    setCardComplete({ ...cardComplete, [e.elementType]: e.complete })
}

return (
    <CardNumberElement id="cardNumber" onChange={handleCardElementOnChange} />
    <CardExpiryElement id="expiry" onChange={handleCardElementOnChange} />
    <CardCvcElement id="cvc" onChange={handleCardElementOnChange} />
)

// Note: id != e.elementType

use card._complete
it returns true if card elements are filled and valid and false otherwise

@santoshdnikam is ._complete a published, public API? Where I'm from, a leading underscore indicates private API, which means it may disappear or change in the future.

Currently the only way to imperatively get this state using officially documented APIs would be to check the CSS classes applied to the container into which the Element is mounted. Those can be personalized using the classes option when creating an Element. Using default classes the check would be:

cardElementContainer.classList.contains('StripeElement--complete');

That said we are aware that people have started using some of our internal private properties, and it's unlikely we would willingly break their usage in this case.

Listening to change events is still the canonical way to determine if the Element is complete/valid. An Element will never be complete without user interaction or this event triggering. We understand this is not imperative and requires your integration to save this state, but it should allow you to enable/disable your form submission as needed.

Was this page helpful?
0 / 5 - 0 ratings