#pavlos_error

1 messages ยท Page 1 of 1 (latest)

ember heraldBOT
#

๐Ÿ‘‹ 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/1219998460535767070

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

marsh skyBOT
stoic ingot
#

Our API used to create a subscription by doing this:

this._stripe.subscriptions.create({
      customer: ...,
      items: [
        {
          price: ...
        }
      ],
      payment_behavior: "default_incomplete",
      payment_settings: {
        save_default_payment_method: "on_subscription"
      },
      expand: ["latest_invoice.payment_intent", "pending_setup_intent"],
      trial_period_days: ...
    });

Then, we retrieved the client secret from either the payment intent of the first invoice of the subscription, OR the setup intent of the subscription if the trial_period_days was set (in which case there was no first payment).

This client secret was returned to the iOS client that called completion on the Apple Pay context with that. This used to work. However, recently, we added a default payment method parameter to the above Stripe.subscriptions.create method.

this._stripe.subscriptions.create({
      ...
      default_payment_method: ...,
      ...
    });

When we tried this, this call stopped working with the error mentioned above. We realise this is because the payment method itself is not attached to the customer yet, because the Apple Pay completion has not been called. However, we're not sure how to proceed here.

normal grotto
#

I'm not sure I grasp the change where you added default_payment_method

That parameter should only be used in a case where you have the payment method attached to a customer and setup for future usage

stoic ingot
#

Before we did this, the subscription created had unspecified payment method (it was set to the customer's default).

#

Whereas we wanted to use the payment method being setup at that moment from the user.

#

i.e. the subscription was set up like this:

normal grotto
#

Hmm.. Thinking..

stoic ingot
#

Just for reference, for the card flow, we had created two endpoints:

  1. An endpoint that, given a customer, creates a setup intent and returns a client secret to the clients.
  2. An endpoint that, given a Stripe payment method ID, creates a Stripe subscription and returns a client secret (either payment intent client secret or setup intent client secret, if free trial was used).

This worked quite well for both cards and Google Pay.

For example, for cards, the clients call (1), they receive the setup intent client secret, show the user the card details form to fill, which creates a Stripe payment method and then call (2) with that Stripe payment method ID. If that gives them a payment intent client secret, they confirm, and we have a working Stripe subscription with the first payment confirmed.

normal grotto
#

Just for reference, for the card flow, we had created two endpoints:
An endpoint that, given a customer, creates a setup intent and returns a client secret to the clients.
An endpoint that, given a Stripe payment method ID, creates a Stripe subscription and returns a client secret (either payment intent client secret or setup intent client secret, if free trial was used).

This worked quite well for both cards and Google Pay.

Does that not work with Apple Pay?

stoic ingot
#

No, unfortunately, no.

We have tried 2 ways to use these endpoints in the Apple Pay flow, but we have encountered problems with both.

  1. The first thing we tried was calling the endpoint (1) within the Apple Pay context, then call completion with the client secret returned, and then calling endpoint (2) in case of success, but we see that when we do that, the payment from the subscription's invoice is stuck in requires_confirmation status.
normal grotto
#

also I'm a bit confused.. What are you setting default_payment_method to when creating the subscription

      ...
      default_payment_method: ...,
      ...
    });

The payment method wouldn't exists at that point given you're doing cleint-side confirmation no?

stoic ingot
#

It does exist, when the Apple Pay context starts, it gives us a Stripe.PaymentMethod

#

it's just not attached to the customer

normal grotto
#

It does exist, when the Apple Pay context starts, it gives us a Stripe.PaymentMethod
Isn't the subscription created server-side before ApplePay context starts?

stoic ingot
normal grotto
#

Can you share the exact code you're using client-side and server-side?

stoic ingot
#

Sure

#

I'll paste the server-side code first

#

Endpoint (1):

public static readonly initiateStripe = async (req: CustomRequest, res: Response): Promise<Response> => {
    const customerId = req.user.providers?.stripe?.id;
    if (!customerId) {
      throw new InternalServerError(`User does not have Stripe customer ID!`);
    }

    const { client_secret, id } = await StripeService.Instance.createSetupIntent(customerId);
    const { secret } = await StripeService.Instance.createEphemeralKey(customerId);

    return res.status(200).json({ clientSecret: client_secret, setupIntentId: id, ephemeralKey: secret });
  };
#

Endpoint (2):

  public static readonly initiateStripeSubscription = async (req: CustomRequest, res: Response): Promise<Response> => {
    const { price, paymentMethod } = req.body;

    const initiateStripeData = await SubscriptionService.initiateStripeSubscription(
      req.user,
      paymentMethod,
      price as plansConfig.PriceType
    );

    return res.status(200).json(initiateStripeData);
  };
#

Where the SubscriptionService.initiateStripeSubscription implementation is the following:

  public static async initiateStripe(
    user: UserDocument,
    paymentMethodId: string,
    price: plansConfig.PriceType
  ): Promise<InitiateStripeType> {
    const customerId = user.providers?.stripe?.id;
    if (!customerId) {
      throw new InternalServerError("User does not have Stripe customer ID!");
    }

    const stripePaymentMethod = await StripeService.Instance.retrievePaymentMethod(paymentMethodId);
    const shouldUseFreeTrial = // some check on our database

    return shouldUseFreeTrial
        ? await SubscriptionService._initiateStripeWithFreeTrial(user, price, stripePaymentMethod)
        : await SubscriptionService._initiateStripeWithInvoice(user, price, stripePaymentMethod);
  }
#

And those two methods that you see above:

  private static async _initiateStripeWithFreeTrial(
    user: UserDocument,
    price: plansConfig.PriceType,
    stripePaymentMethod: string,
  ): Promise<InitiateStripeWithFreeTrialType> {
    return this._stripe.subscriptions.create({
      customer: user.providers.stripe.id,
      items: [
        {
          price: priceId // (from our internal mapping)
        }
      ],
      default_payment_method: stripePaymentMethod,
      payment_behavior: "default_incomplete",
      payment_settings: {
        save_default_payment_method: "on_subscription"
      },
      expand: ["latest_invoice.payment_intent", "pending_setup_intent"],
      trial_period_days: 31
    });
  }

  private static async _initiateStripeWithInvoice(
    user: UserDocument,
    price: plansConfig.PriceType,
    stripePaymentMethod: string,
  ): Promise<InitiateStripeType> {
    const stripeSubscription = this._stripe.subscriptions.create({
      customer: user.providers.stripe.id,
      items: [
        {
          price: priceId // (from our internal mapping)
        }
      ],
      default_payment_method: stripePaymentMethod,
      payment_behavior: "default_incomplete",
      payment_settings: {
        save_default_payment_method: "on_subscription"
      },
      expand: ["latest_invoice.payment_intent", "pending_setup_intent"]
    });

    const paymentIntent = (stripeSubscription.latest_invoice as Stripe.Invoice)
      ?.payment_intent as Stripe.PaymentIntent;

    return {
      clientSecret: paymentIntent.client_secret,
      paymentIntentId: paymentIntent.id
    };
  }
#

That's pretty much the server-side code with some internal parts omitted.

normal grotto
#

Okay. So your server-side flow is:

  1. Create a customer
  2. Create a SetupIntent
  3. Confirm SetupIntent client-side
  4. Create a Subscription and set default_payment_method as the payment method saved in step - 3?
stoic ingot
#

yes, pretty much

#

I have omitted creating the customer as it's in another flow

#

but we can assume the customer is created correctly

normal grotto
#

How are you doing step 3 though?

#

I don't see the code for that anywhere

stoic ingot
#

I uploaded a file with the iOS code for Apple Pay

#

have you received that? It didn't let me paste it as a message because of its size

normal grotto
#

I only see didCreatePaymentMethod function in there

stoic ingot
#

Isn't (3) done by calling completion with the given client secret?

#

This is what we see in the docs:

Implement applePayContext(_:didCreatePaymentMethod:completion:) to call the completion block with the PaymentIntent client secret retrieved from the endpoint above.

After you call the completion block, STPApplePayContext completes the payment, dismisses the Apple Pay sheet, and calls applePayContext(_:didCompleteWithStatus:error:) with the status of the payment. Implement this method to show a receipt to your customer.

normal grotto
#

I think I see the issue here. completion block doesn't run till the end.

So the SetupIntent doesn't get confirmed until after the subscription has been created with payment method ID set as default_payment_method.

You'd need to find a way to call StripePaymentHelper.shared.subscriptionInitiateStripe after completion handler is done

stoic ingot
#

Right, this is what we thought as well. And then we moved to this way of doing this:

//MARK: - ApplePayContextDelegate
extension NewPricingPlansViewController: ApplePayContextDelegate {
    
    func applePayContext(_ context: StripeApplePay.STPApplePayContext, didCreatePaymentMethod paymentMethod: StripeCore.StripeAPI.PaymentMethod, paymentInformation: PKPayment, completion: @escaping StripeApplePay.STPIntentClientSecretCompletionBlock) {
        self.forceShowSpinner()
        
        paymentMethodId = paymentMethod.id
        
        StripePaymentHelper.shared.payWithPaymentMethodInitiateStripe() { result in
            switch result {
            case .success(let item):
                completion(item.clientSecret, nil)
            case .failure(_):
                self.handleStripePaymentFlowFailure()
                
                completion(nil, nil)
            }
        }
    }
#

And then after this is completed:

    func applePayContext(_ context: StripeApplePay.STPApplePayContext, didCompleteWith status: StripeApplePay.STPApplePayContext.PaymentStatus, error: Error?) {
        guard let selectedPricingPlan = currentSelectedPricingPlan else { return }

        let forcedAnnualPlan = pricingPlansDatasource.searchUnfilteredPlansForPlan(withType: selectedPricingPlan.pricingPlanType == .goldMonthly ? .goldYearly : .plusYearly)
        let finalPlan = isAnnualToggleOn ? forcedAnnualPlan : selectedPricingPlan
        let finalPlanId = (finalPlan?.id ?? "")

        switch status {
        case .success:
            StripePaymentHelper.shared.subscriptionInitiateStripe(price: finalPlanId, paymentMethod: paymentMethodId ?? "") { result in
                switch result {
                case .success(let initiateStripeReturnItem):
                    if let intentId = initiateStripeReturnItem.stripePaymentKeysItem?.intentId {
                        StripePaymentHelper.shared.subscriptionCompletedStripe(paymentIntentId: intentId, isSilentRequest: false) { result in
                            switch result {
                            case .success(_):
                                self.handleStripePaymentFlowSuccess()
                            case .failure(_):
                                self.handleStripePaymentFlowFailure()
                            }
                        }
                    }else if let  _ = initiateStripeReturnItem.subscription {
                        self.handleStripePaymentFlowSuccess()
                    }else{
                        self.handleStripePaymentFlowFailure()
                    }
                case .failure(_):
                    self.handleStripePaymentFlowFailure()
                }
            }
        case .error:
            self.handleStripePaymentFlowFailure()
        case .userCancellation:
            self.forceHideSpinner()
        
            return
        }
    }
}
#

Which unfortunately also didn't work, because this leaves the payment intent of the subscription's invoice as requires_confirmation

#

So basically in the above, we first call completion with the client secret from (1), dismiss the modal, which brings the payment method in a good state - attached to the customer. But then how do we confirm the first payment of the subscription? ๐Ÿค”

#

Basically we're stuck in a dead end where we can't create the subscription before calling completion because of the payment method not being attached to the client yet, but we also can't call it after completion because that way the payment is never confirmed.

normal grotto
#

Hmm if you're setting default_payment_method correctly then the subscription endpoint should just attempt to charge it.

Can you share an example subscription ID where you seeing subscription invoice stuck in requires_confirmation status

stoic ingot
#

sub_1OwOe1J54lNDIU9BYoh51ObF

normal grotto
#

Hmm for some reason auto_advance on the invoice is disabled ๐Ÿค”

stoic ingot
#

What is auto_advance?

#

I can try calling the API, but in terms of flow, where would this fit in?

marsh skyBOT
normal grotto
#

Just trying to see if it yields any errors.
auto_advance is what Stripe uses to trigger invoice finalization and automatic payment

stoic ingot
#

Would it be the same if I click on this?

#

Instead of calling the API?

normal grotto
#

Yep

stoic ingot
#

Yes, this worked

#

it moved the invoice to paid

#

and the subscription to active

normal grotto
#

hmm okay.. let me see why the invoice didn't trigger automatic payment

#

I think it's the payment_behavior: default_incomplete that's causing this

#

Can you omit that and test again?

#

@stoic ingot

stoic ingot
#

Ah, sorry

#

Just saw this

#

I can try removing this, yes

#

I'll get back to you in a bit

#

I haven't forgotten about this, just trying to get this E2E test running...