cashier-stripe: Stripe throws incomplete payment after first subscription with SCA

I’m working on updating my project with Cashier v10.0.0-beta.2 and the new Payment Intents API for a few days now and I’m still having issues with my customers having to provide their card details and 3D secure twice. I can’t say if I did something wrong or if this is a Cashier related issue so I’m asking here.

I updated my integration following these instruction.

My new customers have to fill a form with their personal information and select one or many subscription plans. At the end of the form there is a Stripe card element. Before the user submits the form, I use Stripe’s handleCardSetup method to verify the payment and then submit the form to my controller which creates the subscription(s).

The form contains a SetupIntent key which is generated by the controller before returning the view.

$setupIntent = (new User)->createSetupIntent();

return view('front.auth.register', ['setupIntent' => $setupIntent]);

This is my jQuery code for handling Stripe API.

$('#register-form').on('submit', function(e) {
    e.preventDefault();

    // Disable submit button
    $(this).find('button[type=submit]').prop('disabled', true);

    var clientSecret = $('#setupintent-secret').val();
    var cardholderName = $('#cardholder-name').val();

    stripe.handleCardSetup(
        clientSecret, cardElement, {
            payment_method_data: {
                billing_details: {
                    name: cardholderName
                }
            }
        }
    ).then(function(result) {
        if (result.error) {
            // Inform the user if there was an error
            $('#card-errors').html(result.error.message).slideDown(200);

            // Re-enable form submission
            $('#register-form').find('input[type=submit]').prop('disabled', false);
        } else {
            // Insert the payment method ID into the form so it gets submitted to the server
            var form = $('#register-form');

            form.append($('<input type="hidden" name="stripePaymentMethod" />').val(result.setupIntent.payment_method));

            // Submit form
            form.get(0).submit();
        }
    });
});

I get the Stripe’s 3D secure popin everytime I submit the form, but when I confirm the payment using the Complete authentication button, the SubscriptionBuilder’s create() method throws an IncompletePayment exception and I have to provide all the card details again. This seems wrong as I already verified the payment with 3D secure.

Here is the code which is handling the new subscription in my controller:

$subscription = $user->newSubscription($planType, $planId);

try {
    // Create subscription
    $subscription->create($request->input('stripePaymentMethod'), [
        'email' => $user->email
    ]);
} catch (IncompletePayment $e) {
    if ($e instanceof \Laravel\Cashier\Exceptions\PaymentActionRequired) {
        return redirect()->route('confirm_payment', [$e->payment->id, 'redirect' => route('home')]);
    } else {
        dump($e); die;
    }
}

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 3
  • Comments: 53 (25 by maintainers)

Most upvoted comments

As I said before: note that you’re all testing with a card number that’s specifically meant to always throw the 3D Secure error. That’s why I’m asking to actually try this on a live environment.

Okay guys I just tested setting off_session to true and this is working! I’m creating a PR right now for this.

Hey everyone. As so many people reported the issue as fixed because of the change I’m closing this. Thanks to everyone helped figuring this out. I’ll be tagging a new release in a few moments.

OMG! A needle in a haystack would be easier to find.

@driesvints Actually Stripe provides these two credit cards specifically to test 2 different behaviours:

4000002500003155 (Authentication required on setup or first transaction) This test card requires authentication for one-time payments. However, if you set up this card using the Setup Intents API and use the saved card for subsequent payments, no further authentication is needed. 4000002760003184 (Authentication required) This test card requires authentication on all transactions.

Unfortunately both cards have the same behaviour! I’m expecting double 3DS using the second credit card (because is required on all transactions), but the first one is not meant to always throw 3DS so I’m expecting 3DS just with the setupIntent

I can confirm what @marcbelletre says – adding off_session => true to buildPayload fixes the issue. I’ll leave the PR to Marc as he’s already on it 😃

You can definitely save payment methods with both. The difference is just the flow. With PaymentIntent you charge and might want to save a payment method. With SetupIntent it’s the other way around: you save it but might charge it at a later stage. You can optimise PaymentIntent for off-session payments as well.

FYI today stripe is going to have a livestream on youtube about saving and reusing cards with SCA. The livestream starts at 3pm GMT. Here’s the link to the event: https://stripe.events/developer-office-hours-08-28 I think it might be helpful to those who is struggling with this issue

Same here. After providing card details and on submission handleCardSetup is prompting for 3DS.

form.addEventListener('submit', async event => {
    event.preventDefault();
    ...
    stripe.handleCardSetup(
        clientSecret, cardElement, {
        payment_method_data: {
            billing_details: {
                ...
            }
        }
    }).then( function(result) {
        handleSetupIntent(result)
    });
});

function handleSetupIntent(result) {
    const {setupIntent, error} = result;
    if (error) {
        ...
    } else if (setupIntent) {
        var form = document.getElementById('payment-form');
        var hiddenInput = document.createElement('input');
        hiddenInput.setAttribute('type', 'hidden');
        hiddenInput.setAttribute('name', 'setupIntent');
        hiddenInput.setAttribute('value', setupIntent.payment_method);
        form.appendChild(hiddenInput);
        form.submit();
   }
}

Then form gets submitted. New subscription is created using setupIntent.payment_method, it throws IncompletePayment $exception and redirects to ‘cashier.payment’ asking for name, card and 3DS once again.

try {
    $subscription = $user->newSubscription('pro')->create(request()->setupIntent, $user_data);
} catch (IncompletePayment $exception) {
    return redirect()->route('cashier.payment', [$exception->payment->id, 'redirect' => route('home')]);
}

All webhooks are properly set up and working, $user->createSetupIntent(); passed to view with payment form to get client_secret.

Battling with this since few days. Would be grateful for help. As @driesvints asked - did anyone tried it on a live environment?

@SlyDave I just saw this as well. I think we have to add off_session => true to the array in SubscriptionBuilder@buildPayload. I’m trying this out right now. I don’t understand why a subscription would be charged on session by default though…

Just to clear up any possible confusion created by @TitasGailius 's comment about the differences between SetupIntent and PaymentIntent

PaymentIntent is used when intending to take immediate payment (on-session) and not save the card details to a Customer. Stripe automatically handles SCA if you use handleCardPayment()

SetupIntent is used when saving the card details to a Customer for later use (on or off session) - such as for payment of a subscription or after a trial period. or to simply store the details to short-cut the process for future purchases.

For a SetupIntent, Stripe automatically handles the creation of a mandate to allow future processing, (although you should update your legals and terms). They also automatically apply for exemptions for future SCA checks, however that doesn’t mean they will be granted. Stripe automatically handles the initial SCA if you use handleCardSetup() (you can also use Cashier to handle it, see the docs)

For off-session SCA you either need to listen to the webhooks and get the user back on session to get them to complete the SCA check, or configure Stripe via your Stripe Dashboard to send your Customer an email on your behalf and Stripe will complete the SCA check.

At least that is how it should work, The SetupIntent work flow doesn’t seem to be working for those in this thread (myself included). The initial SCA check is still occurs and works, but then the attempt to take payment triggers SCA again. – this seems to be a Stripe issue. but could be an issue with how the Intent is setup via Cashier… ?

I have the same issue.

Hi! I have the same issue. When the user has no trial period I create a PaymentIntent, then I go throw the handleCardPayment on js that prompts the 3d secure, but when I create a new subscription the IncompletePayment exception is thrown and the user must insert the credit card data again. That is a big issue because the user can use a different credit card, so the first charge will be performed with the last created card but the future charges will be performed with the first payment method collected.

Thank you for your help. I have set up the webhooks correctly in Stripe but the payments are marked as incomplete even after the 3D secure check. Here are some screenshots of my subscription process:

When I hit the submit button in my form I get this popin. Screenshot 2019-08-13 at 13 57 03

Then I get redirected to the payment confirmation page. Screenshot 2019-08-13 at 13 58 09

In my Stripe account I can see that the payment is indeed marked as incomplete. Screenshot 2019-08-13 at 14 00 01