Skip to content

Add purchaseInterruptedError for UPI/external payment app flows#6207

Open
facumenzella wants to merge 5 commits intomainfrom
fix/purchase-interrupted-upi-payments
Open

Add purchaseInterruptedError for UPI/external payment app flows#6207
facumenzella wants to merge 5 commits intomainfrom
fix/purchase-interrupted-upi-payments

Conversation

@facumenzella
Copy link
Member

@facumenzella facumenzella commented Feb 5, 2026

Summary

When users complete purchases via third-party payment apps (e.g., UPI in India), switching to the external app causes AMSError.paymentSheetFailed to be returned. This error was previously mapped to PURCHASE_CANCELLED, even though the user may be completing (not cancelling) the payment.

Changes

  • Adds new error code purchaseInterruptedError (code 43, codeName PURCHASE_INTERRUPTED)
  • Tracks app background state during in-flight purchases via purchaseBackgroundedState
  • When AMSError.paymentSheetFailed is received AND app was backgrounded during purchase, returns purchaseInterruptedError instead of purchaseCancelledError
  • purchaseInterruptedError.isCancelledError returns false, so userCancelled will be false for interrupted purchases

Behavior

Scenario Error Returned userCancelled
User taps "Cancel" button purchaseCancelledError true
User switches to UPI app (app backgrounds) purchaseInterruptedError false

Developer Guidance

When receiving purchaseInterruptedError, developers should call getCustomerInfo() to verify actual entitlement status, as the payment may have succeeded in the external app.

Test plan

  • Builds successfully
  • ErrorCodeTests pass (including new testPurchaseInterruptedError)
  • Manual testing with UPI payment flow on device in India

Addresses #6194

@facumenzella facumenzella marked this pull request as ready for review February 6, 2026 11:15
@facumenzella facumenzella requested a review from a team as a code owner February 6, 2026 11:15
@claude
Copy link

claude bot commented Feb 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

facumenzella and others added 4 commits February 10, 2026 12:02
When users complete purchases via third-party payment apps (e.g., UPI in India),
switching to the external app causes AMSError.paymentSheetFailed to be returned.
This error was previously mapped to PURCHASE_CANCELLED, even though the user may
be completing (not cancelling) the payment.

This change:
- Adds new error code `purchaseInterruptedError` (code 43)
- Tracks app background state during in-flight purchases
- When AMSError.paymentSheetFailed is received AND app was backgrounded,
  returns `purchaseInterruptedError` instead of `purchaseCancelledError`
- `purchaseInterruptedError.isCancelledError` returns false, so `userCancelled`
  will be false for interrupted purchases

Developers should handle this error by calling `getCustomerInfo()` to verify
actual entitlement status.

Fixes #6194

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Store notification observer and remove in deinit to prevent leaks
- Clean up purchaseBackgroundedState in handlePurchasedTransaction (SK1)
- Clean up purchaseBackgroundedState in handleDeferredTransaction
- Add test for UPI/external payment app interrupted flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add purchaseInterruptedError (code 43) to the public API baseline.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The error message for storeProblemError differs between macOS and iOS.
Update test to handle both platforms.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@facumenzella facumenzella force-pushed the fix/purchase-interrupted-upi-payments branch from 7d51c3d to 03569a2 Compare February 10, 2026 11:04
@facumenzella facumenzella requested a review from a team as a code owner February 10, 2026 11:04
…ction

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Logger.debug(Strings.purchase.purchase_interrupted_external_app(
productIdentifier: skError.userInfo["productId"] as? String ?? "unknown"
))
return ErrorUtils.purchaseInterruptedError(error: error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think changing the return error code in this case is a kind of behavioral breaking change... For example, we will likely need to update our hybrids to handle this appropriately as well... Maybe we could do it if we do a major, but maybe it's worth exposing this information some other way for now...

Could we maybe add this information as part of the error's userInfo or something like that? for devs that want to handle this differently than a normal cancellation? Not sure how common this use case would be. If developer should handle this case differently than a cancellation, then maybe this is ok. If it's uncommon, let's avoid the behavior change if we can 🙏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, cc @ajpallares , not sure if this can have any effects on the RCT project we're working on?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think about this 🤔
I think I like it? Given that introducing a breaking change will most likely open other doors for issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing is whether we might want to reuse the paymentPendingError error code for this scenario, instead of introducing a new one... but it would still be a bit of a behavioral breaking change...

In any case, it would be good to get some @RevenueCat/catforms feedback on this PR.

Copy link

@helians11 helians11 Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPIs are very common in India, Most of the users make the payment by UPI only. There should be a way for us to distinguish user cancellation vs payment pending/in progress so that we can show the right state to the users.
Currently for this to work we have to keep the user waiting for 5 mins. We keep this payment timeout and keep on checking the payment status in background. Although this works, but the problem here is that even if the user is cancelling the payment or the purchase flow then also they will have to wait for 5 mins as we have no way to tell if user has actually cancelled it or the payment is in progress state.
Not just the UPI, even if you try to initiate the payment on IOS and you don't have the apple id logged in or payment method added then also it throws this error that user cancelled even though you are trying to login or adding the payment method.
I hope this gets resolved for good user experience and better tracking.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the fact that this is a breaking behavioral change, I think I agree with @tonidero and would be in favor of adding this to userInfo for now and potentially throwing a typed error for this in a future major SDK update.

Copy link
Member

@rickvdl rickvdl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting edge case, thanks for digging into this!

Maybe this also affects RCT and / or is a potential case we should cover there, so it might make sense if @ajpallares could also have a look at it.

Copy link
Contributor

@fire-at-will fire-at-will left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @tonidero's idea of exposing this through the userInfo instead of a new error type for now to avoid making a breaking behavior change. That said, the detection logic seems reasonable to me :)

A few other questions that aren't specific to a single line of code:

  1. Is there any documentation from Apple's side on how to use UPI apps with StoreKit? I'm not finding any, but I could just be missing it
  2. It looks like this implementation only covers StoreKit 1 purchases. I can't tell from the referenced issue's sample code, but should we also be handling StoreKit 2 purchases as well?
  3. Is it possible for us to easily test purchases made in the UPI flow to verify that this works as expected?

self.purchaseInitiatedPaywall.value = nil
}

// MARK: - Purchase Background State Tracking (for UPI/external payment app detection)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// MARK: - Purchase Background State Tracking (for UPI/external payment app detection)
// MARK: - Purchase Background State Tracking (for UPI/external payment app detection)

Nit: Mark should be indented

Logger.debug(Strings.purchase.purchase_interrupted_external_app(
productIdentifier: skError.userInfo["productId"] as? String ?? "unknown"
))
return ErrorUtils.purchaseInterruptedError(error: error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the fact that this is a breaking behavioral change, I think I agree with @tonidero and would be in favor of adding this to userInfo for now and potentially throwing a typed error for this in a future major SDK update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants