#xenoliss_checkout-setup-subscriptions-3ds

1 messages ยท Page 1 of 1 (latest)

cunning flumeBOT
#

๐Ÿ‘‹ Welcome to your new thread!

โฒ๏ธ We'll be here soon! Typically we respond in a few minutes, but sometimes we might take a bit longer if the server is busy or if you have a particularly tricky question.

โฑ๏ธ We close idle threads, which makes them read-only. Once a thread is closed it won't be reopened, but you can always start a new thread if you have another question.

๐Ÿ”— This thread will always be available, even after it's closed. You can find it again using Discord's search, or you can save this link: https://discord.com/channels/841573134531821608/1395140788681310250

๐Ÿ“ Have more to share? Add more details, code, screenshots, videos, etc. below.

tired crane
#

Hello there

heady pagoda
#

Trying now.

tired crane
#

It is to enable multiple PaymentIntents to be associated to an Invoice

heady pagoda
#

Also most generally I'm wondering if I'm doing things right. I am VERY new to Strip (first time using it actually) I have a maybe unusual flow (well I'm sure other people might have already faced it) which is:

  1. User registers payment but is not charged anything yet
  2. User can purchase things which trigger an immediate bill for a new subscription (at prorata) with a billing anchor set to 1st day next month
  3. User can buy more things that will be billed immediately at prorata again and update the existing subscription quantity.

I am using

  • stripe.checkout.sessions.create in setup mode for 1.
  • TBD for 2. but ideally something simple to implement that support 3DS but without affecting too much UX (e.g if no 3DS is needed the payment should just be processed in the background without any redirection)
  • For 3. I think the only option (to update a subscription quantity) is via a stripe.subscriptionItems.update. Is that right?

Thanks a lot for the help ๐Ÿ™

tired crane
#

Yeah that is mostly correct... for step 3 do you expect them to pay immediately for those new items or have it be a proration item to be picked up with the next Invoice?

#

If you update the quantity then the default will be to just have it create a proration item since the billing period isn't actually changing here

cunning flumeBOT
heady pagoda
#

Quick additional context: the things user buy are sort of "memberships" so they can buy 1, 2, 3 etc memberships. Each membership has the same cost X. When buying the very first membership it bills the user and creates the suibscription. When buying new memberships it update the existing subscription quantity and bill immediately.

for step 3 do you expect them to pay immediately for those new items or have it be a proration item to be picked up with the next Invoice
I think the most straightforward is to have them pay at purchase time? Like when they buy something a new membership they immediately pay for it (at prorata).

#

If you update the quantity then the default will be to just have it create a proration item since the billing period isn't actually changing here
The code for this is just that currently (though everything has been designed without 3DS so it kind of breaks now which is why I oepned this ticket):

await stripe.subscriptionItems.update(existingItem.id, {
  quantity: existingItem.quantity + 1,
  proration_behavior: "always_invoice",
});

PS: It looks like this code is vulnerable to a race condition but I don't feel like going into a full payment queue right now...

proper hazel
#

Hi hi! Iโ€™m going to be taking over for my colleague here. Just give me a sec to grok all this.

heady pagoda
#

(appreciate the responsiveness, really impressive)

proper hazel
#

Thanks - we try. ๐Ÿ™‚

heady pagoda
#

Out of curiosity do you offer paid support? Where I could chat live/share screen with one member of your team ?

proper hazel
#

Not that I'm aware of? but I'll pass that along.

#

Ok so:

  1. Checkout to get payment details;

That's sorted. From there it's basically this, right?

if isNew(user):
  createSubscription(user, items)
else:
  updateSubscription(user, items)
heady pagoda
#

Yes for the setup I just relly on your checkout sessions and assume it will handle 3DS properly if needed.

#

And yes for the pseudo code

proper hazel
#

It will, yup.

Ok awesome. Now we just need to figure out createSubscritption() and updateSubscritption() for your usecase. ๐Ÿ™‚

heady pagoda
#

(will have to drop for about 30mins but will be back right after just in case)

proper hazel
#

Ok, will keep this open since I'll still be here then. ๐Ÿ‘

heady pagoda
proper hazel
#

No worries. ๐Ÿ™‚

heady pagoda
# proper hazel Ok so: 1) [Checkout to get payment details](https://docs.stripe.com/payments/ch...

Here is my current code (that does not work well with 3DS):

async function chargeForStudy(service: {
  id: string;
  stripeCustomerId: string | null;
  stripeSubscriptionId: string | null;
}) {
  if (!service.stripeCustomerId) {
    return {
      success: false,
      message: "Service not registered as a customer",
    } as const;
  }

  try {
    // Create new subscription with billing cycle anchor set to 1st of next month
    if (!service.stripeSubscriptionId) {
      // Calculate billing cycle anchor for 1st of next month
      const now = new Date();
      const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
      const billingCycleAnchor = Math.floor(nextMonth.getTime() / 1000);

      const subscription = await stripe.subscriptions.create({
        customer: service.stripeCustomerId,
        items: [{ price: PRICE_ID }],
        billing_cycle_anchor: billingCycleAnchor,
        metadata: {
          serviceId: service.id,
        },
      });

      return {
        success: true,
        message: "New subscription created",
        subscriptionId: subscription.id,
      } as const;
    }
#
 // Get existing subscription to check for existing items with this price
    else {
      const subscription = await stripe.subscriptions.retrieve(
        service.stripeSubscriptionId,
        { expand: ["items"] }
      );

      // Find existing subscription item with the same price
      const existingItem = subscription.items.data.find(
        (item) => item.price.id === PRICE_ID
      );

      // If the item does not exist yet, there is an issue with the subscription
      if (!existingItem || existingItem.quantity === undefined) {
        return {
          success: false,
          message: "Subscription not configured correctly",
        } as const;
      }

      await stripe.subscriptionItems.update(existingItem.id, {
        quantity: existingItem.quantity + 1,
        proration_behavior: "always_invoice",
      });

      return {
        success: true,
        message: "Subscription quantity updated",
      } as const;
    }
  } catch (error) {
    console.error(`Error charging for study - ${error}`);

    return {
      success: false,
      message: "Internal server error",
    } as const;
  }
}
proper hazel
#

Why does it not work well with 3DS?

heady pagoda
#

Because the user is never prompted to complete the 3DS verification process and the payment is just left pending. He has to manually go to his billing portal to pay the open invoice...

#

It switches the subscription status to incomplete

proper hazel
#

Right right, ok. And the user is always 'on session' when this runs, right? Because they've clicked something to trigger this, right?

heady pagoda
#

Yes I assume renewal should not trigger a 3DS since this happens off-session

proper hazel
#

Wait, does this happen on-session or off-session?

heady pagoda
#

The code you see above should always happen on-session because it is triggered by a user click to purchase an item

proper hazel
#

Right, ok, that's whta I thought. ๐Ÿ™‚ Ok one sec while I absorb the codez

heady pagoda
#

a few open questions I have as well:

  1. What are the mandatory subscription status I need to track? I thought of ACTIVE, PAST_DUE but now with 3DS seems like INCOMPLETE might be needed as well.
  2. Is it good practice to store the subscription status & quantity in my DB or should I rather alway query it from stripe API?
  3. what are the strictly necessary webhooks I need to properly handle all cases. More specifically do I even need to listen for the invoice.paid/invoice.payment_failed events if all I care about is the subscription status?
proper hazel
#

I think those might get answered as part of solving parts 2 and 3. ๐Ÿ™‚

heady pagoda
#

Also I am fine with a subscription being PAST_DUE but still giving access to the user. I assume after a week Stripe will switch it to cancel (configured from the dahsboard) if it has not been paid for at which point I will remove access to the user.
Not sure how that changes if the subscription can now be INCOMPLETE as well

proper hazel
#
define chargeForStudy(service):
  if !service.customer:
    return "No Customer"

  if !service.subscription:
    createSubscription(service)
  else:
    updateSubscription(service) 

Makes sense so far, ya?

#

Ooo

#
  if !service.subscription:
    createSubscription(service, onNeeds3DS)
  else:
    updateSubscription(service, onNeeds3DS) 
#

Hm.

#

Ok, so the last link I sent is how you can get Stripe to automatically follow up for you. The first link is what you need to build out.

heady pagoda
#

I see, gotta do some lecture

proper hazel
#

In your 'on-session' code, you'll want to expand and look at latest_invoice.payments.data.payment.payment_intent and its status.

heady pagoda
#

Also it's not extremely clear to me what are Payment and Setup intents. When are they used and what for? I've never created any in my existing code.

proper hazel
heady pagoda
#

So the TLDR is to mostly keep the code as is but check if there is a required action and if so open the 3DS popup from the client secret right?

proper hazel
#

Basically, ya. ๐Ÿ™‚

#

And then also listen for invoice.payment_action_required, and if you get that event, contact the associated user to let them know the payment didn't complete and you need them to try again.

#

Does that all make sense?

heady pagoda
#

invoice.payment_action_required
This sould only ever happen on-session no?

#

If so I wonder if listening to the event is necessary given that I would have already redirected the user to complete the 3DS thing

cunning flumeBOT
proper hazel
#

Yes, but if someone doesn't complete the bank redirect, or leaves your page, or times out etc, your code wouldn't be able to redirect them, so listening to this event would be one way of dealing with that.

You also likely want to listen to invoice.paid events for once it's successful - and you can 'provision' the stuff, and to invoice.payment_failed events for if it fails so you can ask them to try a different payment method.

heady pagoda
#

I see. I need to read more but the chat has been really helpfull

proper hazel
#

I'm glad! ๐Ÿ™‚ There are a lot of moving parts, but once you understand how they fit together, you can build a heck of a machine out of 'em. ๐Ÿ™‚

heady pagoda
proper hazel
#

It's super-hard to balance powerful complexity with easy simplicity ๐Ÿ˜… but we do our best. ๐Ÿ™‚

I have to โ๏ธ but snufkin can help if anything else comes up. Good luck - and have fun! ๐Ÿ™‚

cunning flumeBOT
heady pagoda
#

I should be good for some time thanks for the help. Keep the feed open though as I might come back to it time to time

zinc vale
#

FYI: We only keep threads open for ~30-45m of inactivity before we close them. However, this thread will always be available for you to review and if you have a new question you can always ask it using the form in #help

heady pagoda
#

I do have one question on setup session. Can I have the session automatically set the customer payment method the default one? And can I also pre-set the customer name and metadata?

zinc vale
#

Sorry I don't know what you mean by setup session. Are you using Checkout Sessions here? Or are you referring to Setup Intents?

heady pagoda
#

I mean when creating a checkout session in setup mode

zinc vale
#

Okay. So you cannot pre-set the Customer name but you can specify the Customer email using the customer_email parameter

#

Unfortunately, if you want to set the saved Payment Method as default, you will need to do that in a separate API call after the Checkout Session completes

heady pagoda
#

Yeah that's what I was experiencing.

I am using the session.complete web hook to register the default payment (and change the customer name)

zinc vale
#

I think that is the best practice, given your approach here.