javascriptjavastripe-payments3d-secure

Stripe Subscriptions - Charing a 3D Secure card number returning subscription_payment_intent_requires_action


We are integrating 3D secure checkout with our current subscription billing solution. We offer a monthly plan with a 7 day free trial. For the integration, we are using a SetupIntent to determine if the customer's card information requires 3D secure authorization. Using Stripe's test card that requires 3D secure, we are calling the handleCardSetup() function in our javascript on our checkout page. Then after successful authorization, attaching the customer's payment method to the customer. Then continuing checkout, we create the subscription. We expand the latest_invoice.payment_intent on the subscription.

According to Stripe's documentation:

"SetupIntents are automatically created for subscriptions that don’t require an initial payment. If authentication and authorization are required, they’re executed as well. If both succeed, or if they aren’t needed, no action is required, and the subscription.pending_setup_intent field is null."

When I look at the response from calling Subscription.create(params), I see that the pending_setup_intent field is equal to null. However, when viewing the subscription on Stripe's dashboard, I see that the attempt to charge the card returned a 402 error with the following response:

{
  "error": {
    "code": "subscription_payment_intent_requires_action",
    "message": "Payment for this subscription requires additional user action before it can be completed successfully. Please refer to the use of the `enable_incomplete_payments` parameter here: https://stripe.com/docs/billing/lifecycle#incomplete-opt-in",
    "type": "card_error"
  }
}

What gives? Did I miss a step somewhere? We are currently on the latest version of Stripe's API: 2019-05-16. I've attached the code we use for creating the SetupIntent, attaching the PaymentMethod to the Customer, and creating the Subscription. Please let me know if there are any errors in my code.

SetupIntent:

public static String createSetupIntent() {
    try {
        Map<String, Object> setupIntentParams = new HashMap<>();
        ArrayList<String> paymentMethodTypes = new ArrayList<>();
        paymentMethodTypes.add("card");
        setupIntentParams.put("payment_method_types", paymentMethodTypes);
        SetupIntent setupIntent = SetupIntent.create(setupIntentParams);
        return setupIntent.getClientSecret();
    } catch (AuthenticationException | InvalidRequestException | ApiConnectionException | ApiException ex) {
        throw new Error("Unable to create SetupIntent", ex);
    } catch (StripeException ex) {
        throw new Error("Unable to create SetupIntent", ex);
    }
}

Javascript:

var customerInfo = {
  payment_method_data: {
    billing_details: {
      name: document.getElementById('cardholder_name').value,
      address: {
        postal_code: document.getElementById('cardholder_zip').value
      }
    }
  }
};

stripe.handleCardSetup(clientSecret, card, customerInfo).then(function (result) {
    if (result.error) {
        setStripeError(result.error);
        hideLoading('hosted_content');
    } else {
        var paymentMethodId = result.setupIntent.payment_method;

        $.post({
            url: backendURL + 'attachpaymentmethod',
            data: {payment_id: paymentMethodId},
            success: function (response) {
                document.getElementById('payment-form').submit(); //sends a post to endpoint that handles creating subscription
            },
            error: function (response) {
                hideLoading('hosted_content');
                setStripeError(response.responseJSON.error);
            }
        });

    }
});

PaymentMethod:

public static void updatePaymentMethod(User user, String paymentMethodId) throws CardException {
    Customer customer = getCustomer(user); //Retrieves customer if user has stripeId, otherwise create a new customer
    try {
        PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodId);
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("customer", customer.getId());
        paymentMethod.attach(params);
    } catch (AuthenticationException | InvalidRequestException | ApiConnectionException | ApiException ex) {
        throw new Error("Unable to update Stripe payment method", ex);
    } catch (StripeException ex) {
        throw new Error("Unable to update Stripe payment method", ex);
    }
}

Subscription:

Map<String, Object> planItem = new HashMap<>();
planItem.put("plan", planId);
Map<String, Object> items = new HashMap<>();
items.put("0", planItem);

List<String> expandList = new LinkedList<String>();
expandList.add("latest_invoice.payment_intent");

Map<String, Object> subscriptionOptions = new HashMap<>();
subscriptionOptions.put("customer", user.getStripeId());
subscriptionOptions.put("trial_period_days", trialDays);
subscriptionOptions.put("items", items);
subscriptionOptions.put("expand", expandList);
try {
    Subscription subscription = Subscription.create(subscriptionOptions);
    System.out.println(subscription); //pending_setup_intent is equal to null
} catch (AuthenticationException | InvalidRequestException | ApiConnectionException | ApiException ex) {
    throw new Error("Unable to create Stripe subscription", ex);
} catch (StripeException ex) {
    throw new Error("Unable to create Stripe subscription", ex);
}

Solution

  • I got in touch with Stripe support. They told the that the purpose of the test card I was using is to require 3DS authentication for every single transaction. That means that even thought I saved the card after authenticating it, the card will be declined on every single use unless it's authenticated. So my solution was to use a different test card, one that only requires authentication one time and that will be approved on each use afterwards.

    Furthermore, according to some documentation on SCA that I read here, recurring transactions on SCA required cards are exempt and will only require authentication once. Thus, using a test card that only requires authentication once is advised as its behavior is closer to what will be expected when SCA starts being enforced.