Search code examples
stripe-paymentssubscriptionrecurring-billing

Stripe subscription, trial period and 3ds SCA


I'm implementing stripe subscription on my site for the first time and I'm having a problem with payments and requesting 3ds.

Before writing I carefully read the stripe documentation, in particular:

https://stripe.com/docs/payments/accept-a-payment-deferred?platform=web&type=subscription https://stripe.com/docs/billing/subscriptions/build-subscriptions?ui=elements#collect-payment https://stripe.com/docs/billing/subscriptions/overview

And I read a lot of SO answers, like Stripe Payment Element with subscription and trial period Stripe create subscription with SCA authentication Testing subscription trial periods with Stripe

In my case I want the user to immediately enter all the credit card data, so as to immediately create a customer, paymentMethod and subscription on stripe, and then leave a trial period (which I would like to be 12 hours, but perhaps stripe has a minimum of 2 days). At the end of the test I would like the payment to take place completely automatically, but up to now, in the test environment, I find my unpaid subscriptions marked with "Past due", due to the 3DS. I find invoice.payment_action_required and "...payment for an invoice for € xxx requires a verification step by the user".

I've read that the 3DS requirement may depend on the bank, but I wonder: how do most popular sites receive payments with subscriptions without asking the user to return to the site and enter the 3DS?

Is it possible, in some way, to immediately ask my customer for the 3DS when they enter their card details and automatically debit the amount at the end of the trial period?

This is my backend code:

/** create new customer */
export const stripeCustomerCreate = async (customer: StripeCustomerCreateReq): Promise<StripeCustomerCreateRes> => {
  const client = await stripeClient();

  let customerData: Stripe.CustomerCreateParams = {
    address: customer.address,
    description: customer.description,
    email: customer.email,
    name: customer.name,

    phone: customer.phone,
    shipping: customer.shipping,
  };

  if (customer.paymentMethod) {
    customerData = {
      ...customerData,
      payment_method: customer.paymentMethod,
      invoice_settings: {
        default_payment_method: customer.paymentMethod,
      },
    };
  }

  const newCustomer: Stripe.Customer = await client.customers.create(customerData);
  return newCustomer;
};

SUBSCRIPTIO CREATE

export const stripeSubscriptionCreate = async (
  subscriptionReq: StripeSubscriptionCreateReq,
): Promise<StripeSubscriptionCreateRes> => {
  try {

    const { customerId, priceId } = subscriptionReq;
    if (!customerId || !priceId) throw new Error('Missing data');

    const client = await stripeClient();
    const subscription = await client.subscriptions.create({
      customer: customerId,
      items: [{ price: priceId }],
      payment_behavior: 'default_incomplete',
      payment_settings: {
        payment_method_options: {
          card: {
            request_three_d_secure: 'any',
          },
        },
        payment_method_types: ['card'],
        save_default_payment_method: 'on_subscription',
      },
      trial_period_days: 1,
      expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
    });

    const pendingSetupIntent = subscription.pending_setup_intent as Stripe.SetupIntent | null;
    const invoice = subscription.latest_invoice as Stripe.Invoice | null;
    const paymentIntent = invoice?.payment_intent as Stripe.PaymentIntent;

    let resData: StripeSubscriptionCreateRes = {
      success: true,
      subscriptionId: subscription.id,
      type: '',
      clientSecret: '',
    };

    if (pendingSetupIntent !== null) {
      resData.type = 'setup';
      resData.clientSecret = pendingSetupIntent.client_secret;
    } else {
      resData.type = 'payment';
      resData.clientSecret = paymentIntent.client_secret;
    }
    return resData;
  } catch {
    return { success: false };
  }
};

FRONTEND (React)

const PaymentContainer: FunctionComponent<PaymentContainerProps> = ({ type }) => {
  const stripe = useStripe();
  const stripeElements = useElements();

  const handleSubmit = async (event: FormEvent) => {
    event.preventDefault();

    if (!stripe || !stripeElements) {
      return;
    }

    const confirmOptions = {
      elements: stripeElements,
      confirmParams: {
        return_url: `${window.location.origin}${buildRoute("ACCOUNT.WELCOMECOMPLETE")}`,
      },
    };

    const result =
      type === "setup"
        ? await stripe.confirmSetup(confirmOptions)
        : await stripe.confirmPayment(confirmOptions)

    if (result.error) {
      // Show error to your customer (for example, payment details incomplete)
      console.log(result.error.message);
    } else {
      // Your customer will be redirected to your `return_url`. For some payment
      // methods like iDEAL, your customer will be redirected to an intermediate
      // site first to authorize the payment, then redirected to the `return_url`.
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <Button type={"submit"} disabled={!stripe}>
        Submit
      </Button>
    </form>
  );
};

Solution

  • The reason why your Subscription is requesting for 3DS is likely because you you set request_three_d_secure=any when creating the Subscription. When you include that parameter, Stripe requires your customer to perform authentication to complete the payment successfully if 3DS authentication is available for a card. See https://stripe.com/docs/payments/3d-secure#manual-three-ds

    If you omit request_three_d_secure, the Subscription renewal should proceed without requesting for 3DS.