Search code examples
node.jsstripe-paymentssubscription3d-secure

Creating subscription with trial, implementing one-time authentication fails for 3D secure cards after trial ends


  • Stack: Nodejs/Vue/Stripe
  • NOTE: Logic works for the standard card (4242424242424242), when trial does not exist even 3D cards work.
  • This card is used for this test: 4000002500003155

https://stripe.com/docs/testing#regulatory-cards

I have an issue with 3d secure cards when trial ends.

Steps to reproduce: Node(SERVER):

  1. create customer
    const customer = await this.getInstance().customers.create({
       email: userData.email,
       name: userData.fullname,
     });
    
  2. create subscription
    const subscription = await this.getInstance().subscriptions.create({
       customer,
       items: [
       {
           price: priceId,
       },
       ],
       off_session: true,
       promotion_code,
       payment_behavior: 'default_incomplete',
       expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
       metadata,
       trial_from_plan: true,
    });
    
  3. return data to CLIENT side
      const invoice = subscription.latest_invoice as Stripe.Invoice;
      const intentType = subscription.pending_setup_intent ? 'setup' : 'payment';
      const intent = intentType === 'payment'
             ? (invoice.payment_intent as Stripe.PaymentIntent)
             : (subscription.pending_setup_intent as Stripe.SetupIntent);
      const formattedSubscription = pick(subscription,['cancel_at_period_end','cancel_at','canceled_at']);
    
      return {
           intentType,
           ...pick(intent, ['id', 'client_secret', 'status']),
           ...formattedSubscription,
         };
    

From the client side:

  1. Fetching response:
       const response = await this.$pricesService.buyPlan(this.selectedPlan.id, {
                fullname: this.nameOnTheCard,
                email: this.email,
                hash: this.couponApplied?.hash,
              });
              if (response.error) {
                throw new Error(response.error);
              }
    
      const { intentSecret, intentID, intentType, ...restOfBuyResponse } = response;
    
  2. Depending on the type of the intent:
     if (intentType === 'payment') {
        stripeResponse = await StripeInstance.confirmCardPayment(intentSecret, 
     {
          payment_method: { card: this.$refs.cardElement.$refs.element._element },
        });
      } else {
        stripeResponse = await StripeInstance.confirmCardSetup(intentSecret, {
          payment_method: { card: this.$refs.cardElement.$refs.element._element },
        });
      }
    
  3. Confirmation triggers 3d Secure modal and after confirming it, invoice on 0$ is paid.

Everything works as it should until this next point.

To test trail mode as fast as it can be done, I've made API to remove trial from the subscription

const subscription = await this.getInstance().subscriptions.update(subscriptionId, {
     trial_end: 'now',
});

After that, Invoice is OverDue, subscription is failed (with switching to pending). I've noticed that default_payment_method is absent, so I've even returned setup intent payment method from the fronted, and attached to the customer, even invoice settings. But no matter what I've modified, new invoice and payment intent never used that information.

Is this expected behavior due to some regulations or am I missing something?

Invoice

Subscriptions

Events


Solution

  • By default, both creating & updating a subscription is considered to be an on-session request and hence the card requires authentication.

    If this is purely for testing purposes, you can include off_session=true.

     const subscription = await this.getInstance().subscriptions.update(subscriptionId, {
        trial_end: 'now',
        off_session : true
     });
    

    If you let the trial for a subscription end and progress naturally (without making an API call to update the subscription), you'd see that 3DS will not be requested (using card ending with 3155).

    You try this out by creating a subscription with trial_end set to the nearest possible time. You can make an API call to finalize and pay the invoice if you don't want to wait for 1 hour for it to be done automatically.

    const subscription = await stripe.subscriptions.create({
      customer: 'cus_FfkjLYmWcepurw',
      items: [
        {price: 'price_1JXgRCJQtHgRImA7fDIHY4B2'},
      ],
      trial_end : 1634536700
    });