#pavlos_error
1 messages ยท Page 1 of 1 (latest)
๐ 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.
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.
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
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:
Hmm.. Thinking..
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.
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.
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?
No, unfortunately, no.
We have tried 2 ways to use these endpoints in the Apple Pay flow, but we have encountered problems with both.
- The first thing we tried was calling the endpoint (1) within the Apple Pay context, then call
completionwith 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 inrequires_confirmationstatus.
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?
It does exist, when the Apple Pay context starts, it gives us a Stripe.PaymentMethod
it's just not attached to the customer
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?
The second thing we tried was what I mentioned in the beginning i.e. instead of calling completion with the client secret returned from (1), instead, calling completion with the client secret returned from (2), but this doesn't work when we specify a default_payment_method as I mentioned.
Can you share the exact code you're using client-side and server-side?
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.
Okay. So your server-side flow is:
- Create a customer
- Create a SetupIntent
- Confirm SetupIntent client-side
- Create a Subscription and set
default_payment_methodas the payment method saved in step - 3?
yes, pretty much
I have omitted creating the customer as it's in another flow
but we can assume the customer is created correctly
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
I only see didCreatePaymentMethod function in there
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.
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
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.
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
sub_1OwOe1J54lNDIU9BYoh51ObF
Hmm for some reason auto_advance on the invoice is disabled ๐ค
What happens if you try to pay the invoice by calling the API?
https://docs.stripe.com/api/invoices/pay
What is auto_advance?
I can try calling the API, but in terms of flow, where would this fit in?
Just trying to see if it yields any errors.
auto_advance is what Stripe uses to trigger invoice finalization and automatic payment
Yep
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
https://docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior
Use default_incomplete to create Subscriptions with status=incomplete when the first invoice requires payment, otherwise start as active.
Can you omit that and test again?
@stoic ingot