Search code examples
reactjs.netstripe-payments

Stripe IntegrationError: elements.submit() must be called before stripe.confirmPayment() in subscription flow


I am encountering an IntegrationError while integrating Stripe for a subscription flow. The error message I receive is:

IntegrationError: elements.submit() must be called before stripe.confirmPayment(). Call elements.submit() as soon as your customer presses pay, prior to any asynchronous work. Integration guide: https://stripe.com/docs/payments/accept-a-payment-deferred

Context:

SetupIntent: I create a SetupIntent on the server and pass the clientSecret to the client.

PaymentIntent: I also create a PaymentIntent as part of the subscription creation process and receive the client_secret from the latest invoice's payment intent.

Flow:

  1. SetupIntent: Create SetupIntent on Server-side. pass the client_secret to client-side.

  2. Setup Elements: Create an options object with the clientSecret and pass it to Elements tag.

  3. Subscription Builder: the user builds their subscription details

  4. Form Submission: The user fills out and submits the payment form.

  5. Form Validation: I call elements.submit() to validate the form fields.

  6. Confirm SetupIntent: I call stripe.confirmSetup() with the clientSecret from the SetupIntent.

  7. Get Subscription: I make a call to server-side and pass the product/pricing ids of the products that the user picked out. server returns a stripe subscription object with a PaymentIntent.

  8. Confirm PaymentIntent: I call stripe.confirmPayment() with the client_secret from the PaymentIntent that is attached to the returned Stripe Subscription object.

Code Snippet:

const handleSubscriptionSubmit = async (e) => {
  e.preventDefault();

  if (!stripe || !elements) {
    toast.error('Error making payment!');
    return;
  }

  //disable loading for now
  //setLoading(true);

  // Validate the form fields
  const { error: submitError } = await elements.submit();
  if (submitError) {
    setLoading(false);
    console.error(submitError);
    return;
  }

  const address = await elements.getElement('address').getValue();
  if (!address.complete || !email.complete) {
    toast.error('Missing required Email or Address field(s)!');
    setLoading(false);
    return;
  }

  // Confirm the SetupIntent
  const { setupError } = await stripe.confirmSetup({
    elements,
    clientSecret: setupIntentOptions.clientSecret,
    redirect: 'if_required',
  });
  if (setupError) {
    toast.error('Error making payment setup!');
    setLoading(false);
    return;
  }

  formik.setFieldValue('phone', address.value.phone);
  formik.setFieldValue('email', email.value.email);
  formik.setFieldValue('payer', address.value.name);
  formik.setFieldValue('address', address.value.address.line1);
  formik.setFieldValue('city', address.value.address.city);
  formik.setFieldValue('state', address.value.address.state);
  formik.setFieldValue('zip', address.value.address.postal_code);

  // Get the subscription intent
  const subscriptionIntent = await getSubscriptionIntent();
  console.log(subscriptionIntent);
  if (!subscriptionIntent) {
    setLoading(false);
    return;
  }

  // Confirm the PaymentIntent
  const secret = subscriptionIntent.latest_invoice.payment_intent.client_secret;
  const { error } = await stripe.confirmPayment({
    elements,
    clientSecret: secret,
    redirect: 'if_required',
  });

  if (error) {
    toast.error(error.message);
    setLoading(false);
    return;
  }

  setTimeout(async () => {
    await submitOrder();
  });
  setLoading(false);
};

Issue: Despite calling elements.submit() before stripe.confirmPayment(), I still receive the IntegrationError indicating that elements.submit() must be called before stripe.confirmPayment(). This suggests that my setup might be interpreted as a deferred setup, even though I have a SetupIntent.

Request: Could you please help me understand why this error is occurring and how to resolve it? Specifically, I need guidance on whether my flow is correct for handling both SetupIntent and PaymentIntent in a subscription setup, and if there are any additional steps or adjustments needed to avoid this error.


Solution

  • It looks like you are confirming two separate intents after calling elements.submit(). This isn't necessary with the Stripe API, so it is likely that elements.submit() is only expecting you to make one confirm call after submitting your payment element. The PaymentIntent that is created by the Subscription's first Invoice will have setup_future_usage set to true which will tell Stripe to save the payment method while taking payment. If you remove the SetupIntent and only confirm the Subscription's first payment intent, the PaymentMethod should be saved automatically and attached to the Subscription's Customer.