How to run validation before completing a purchase using default PaywallView on iOS?
Hey everyone!
I'm using RevenueCat’s default PaywallView in my iOS app, and I’d like to know if there’s a supported way to run a custom validation before the actual purchase happens, using the default paywall UI.
Here’s the use case I need to support:
A user logs in to App Account A using Apple ID X, then subscribes successfully.
Later, they log out and log in with App Account B, but still using the same Apple ID X.
Before allowing them to subscribe again, I want to detect that Apple ID X already has a subscription tied to a different app user ID.
To do this, I use restorePurchases() and compare the originalAppUserId. But currently, I can only run this validation before presenting the paywall, which isn’t ideal UX-wise.
I saw that originalTemplatePaywallFooter accepts a purchaseStarted callback, like:
To trigger my validation only when the user taps "Subscribe", inside the default PaywallView, and prevent the purchase if the validation fails (e.g., by showing an alert instead).
Is there a way to hook into that flow without fully replacing the default paywall UI?
Thanks in advance!
Page 1 / 1
Hey @paulo-ricardo ,
Your use case is valid and can be supported with RevenueCat's current Paywalls v2 setup, but a few adjustments are needed to ensure the implementation is both reliable and App Store compliant.
Making sure I understand your use case first, before going into the implementation details:
Your user (App User ID “A123”) purchases a subscription for Apple ID X;
Your goal is to detect when another user (App User ID “B456”) tries to subscribe using that same Apple ID X (same device for example) but that already has a subscription tied to a different user App User ID “A123”;
the app should decide to allow/block that B456 attempt to subscribe.
Assuming this, whether that second subscription is blocked or allowed depends on your project's restore behavior setting. You can see it in your project settings:
If you're using “Keep with original App User ID”, then:
RevenueCat will block the new purchase if the receipt is already associated with another App User ID.
The SDK will return an error like RCReceiptAlreadyInUseError.
If you're using “Transfer to current App User ID”, then:
The purchase or restore would transfer the entitlement to the new user B456.
As to the implementation and listeners not working, you're currently trying to use:
This only works with legacy Paywalls (v1) mentioned here.
If you're using Paywalls v2 (created via the visual editor), this modifier no longer integrates with the purchase flow and won’t provide the control you're expecting.
This is the correct place for it and ensures you remain compliant with Apple.
Testing Notes
Test in Sandbox with shared Apple IDs to validate your logic.
Use debug logging to gain insight during development: Purchases.logLevel = .debug
Let me know how it goes or if you'd like to review your current setup to ensure you're fully aligned.
Best, Gui
Hi @guilherme, thanks a lot for the detailed response!
I had already tried using .onPurchaseStarted before, and after your suggestion, I gave it another shot following your example — but unfortunately, I still couldn’t get it to work as expected. When implementing it exactly as shown, I encountered the following error:
Contextual closure type '@MainActor @Sendable (Package) -> Void' expects 1 argument, but 2 were used in closure body
I’m currently using SDK version 5.23.0.
Regarding the project settings, I’ve set the Restore Behavior to “Keep with original App User ID”, since the behavior I want is:
If a user is signed in with Apple ID X and logged into App Account A, they should be able to subscribe without any issues.
However, if the user logs out of Account A and logs into Account B (still using the same Apple ID , and tries to subscribe, this attempt should be blocked.
On the other hand, if the subscription on Account A was canceled and has already expired, meaning the Apple ID X no longer has any active subscription, the user should be allowed to subscribe on Account B.
Worth mentioning that all feature access control is handled in my .NET API, based on RevenueCat webhook events.
I’ve integrated with the webhooks, and whenever I receive an event, I call the RevenueCat API at the endpoint /v1/subscribers/{appUserId}. I then map the response, store it in my database, and propagate the access permissions to the app via JWT, ensuring that access is in sync between RevenueCat and our backend.
One important issue I’ve noticed is that if Account B tries to purchase a subscription — even when Account A’s subscription is already expired — the new subscription ends up being attached to Account A, not B. This is problematic, since Account B should not be allowed to modify or reactivate a subscription for Account A.
If you have any suggestions on how to resolve the issue with .onPurchaseStarted expecting a single argument, or a better way to handle this flow using SDK 5.23.0, that would be greatly appreciated!
Thanks again!
Hi @guilherme, just a quick follow-up to confirm the intended flow for my app and make sure it aligns with how RevenueCat handles things internally.
To access the paywall, the user must first log into the app (let’s say with Account A). During the login process, we call Purchases.logIn(appUserId) using a custom user ID (the same ID used in our backend database).
If the user logs out to switch accounts, we explicitly call Purchases.logOut() to reset the RevenueCat session before logging in with a new user.
Before displaying the paywall or allowing a subscription purchase, I want to check whether the currently logged Apple ID has an active subscription associated with another app user ID (for example, Account . If it does, the purchase should not be allowed.
However, if the Apple ID does not have any active subscriptions, or only has expired subscriptions tied to another user (e.g., Account , then it should be allowed to proceed with the purchase under Account A — and the new subscription must be tied to Account A only, not to the previous one (B).
Can you confirm whether this logic is valid and reliably supported with the current SDK behavior (SDK 5.23.0) and the “Keep with original App User ID” restore behavior?
Thanks again for your help — I just want to make sure we’re enforcing subscription ownership correctly across accounts.
Hey @paulo-ricardo ,
You're absolutely right, thanks for catching the .onPurchaseStarted parameters issue! After diving a bit more into the documentation it is not a correct approach. I’ve updated the earlier post to avoid confusion.
As these are 2 (yet related to your use case) behaviors to achieve, I'll just break them down again in:
1. Controlling What Happens When the Paywall CTA Is Tapped
With the current SDK and default PaywallView, it’s important to know:
the .onPurchaseStarted modifier does not let you block the purchase or run validation asynchronously - as you noted too
there's no built-in way to prevent a purchase when the user taps "Subscribe" - but this is a good piece of feedback that I will pass internally too
Best approach would be:
pre-validate before showing the paywall - check if the originalAppUserId matches the current appUserID, as you mentioned you were doing already. Indeed seems to be the most solid approach.
if the purchase proceeds, handle potential errors like RCReceiptAlreadyInUseError using .onPurchaseFailure, but might feel a bit clunky in the UX.
To truly block the purchase flow at tap-time, you’d need a custom paywall UI, which gives you full control, but adds implementation overhead.
2. Managing Subscription Ownership Across App User IDs
From your description:
"if the Apple ID does not have any active subscriptions, or only has expired subscriptions tied to another user (e.g., Account , then it should be allowed to proceed with the purchase under Account A — and the new subscription must be tied to Account A only, not to the previous one (B).”
That’s a perfectly valid and supported flow, but not with Keep with original App User ID. With that setting, as you noted too:
the receipt is permanently tied to the first App User ID that made a purchase (e.g., A123)
even after expiry, any new purchases or restores will still associate with A123
there is no way for B456 to take over, even if the original subscription is long expired
To get the behavior you're looking for, your restore has to be "Transfer if there are no active subscriptions". With it, the behavior is to:
prevents transfers while the original subscription is active (so B456 can’t hijack mid-sub lifetime);
allows a different App User ID (like B456) to take over ownership, but only after expiry;
makes the ownership reclaimable, based on who acts next after expiry.
Comparison table for app user A123 and B456, in a timeline from top to bottom (example):
Scenario
Who owns the receipt in RC? (Keep with original App User ID)
Who owns the receipt in Rc? (Transfer if no active subs)
A123 subscribes
A123
A123
B456 logs in (sub active)
A123
A123 (B456 gets error)
Sub expires → B456 restores or purchases
A123
Ownership transfers to B456
A123 logs back in and tries restore/purchase
A123 (still owns)
A123 no longer owns - B456 owns it now
Sub expires again → A123 restores or purchases
A123
Ownership transfers back to A123
So, ownership can move post-expiry, depending on who takes action first.
As for your login / logout flow, you're doing this the right way. This flow ensures RevenueCat resets the session and receipt context correctly between users.
To note something important, regarding anonymous users, as they could claim a subscription but since there’s no anonymous state involved in your case, as per using the IDs from your own side, you avoid aliasing issues that could happen there (which is great!).
Best, Gui
Hey @guilherme, thanks again for the thorough explanation — really appreciate the support!
At the moment, I’m using a flow that prevents access to the paywall if the current Apple ID is already associated with an existing app user account. That means, after a first successful purchase, the app user ID is permanently linked to the Apple ID, and further attempts to subscribe with another app user are blocked. This has been working well so far.
That said, I’ll be testing the “Transfer if there are no active subscriptions” restore behavior, which seems to be the missing piece to make the ownership flow work exactly the way I intended. Thanks a lot for highlighting that — it makes perfect sense, and I’ll follow up once I’ve had a chance to run those tests.
I'm also planning to implement a custom paywall to allow users to always view the subscription options, but with the subscribe button disabled if the Apple ID is already linked to another app user with an active subscription. In that case, I’ll show a message explaining why the button is disabled — this should result in a smoother and clearer UX.
Regarding the note you made about:
“Avoid using restorePurchases programmatically. Using it when the user taps ‘Subscribe’ is not recommended because s...]”
I actually came across that recommendation in the documentation after we talked, and I’ve updated my implementation to use syncPurchases instead, following the best practice.
Just one last set of questions regarding the “Transfer if there are no active subscriptions” behavior:
Let’s say:
App user ID A123 (with originalAppUserId = anonimo123) completes a purchase using Apple ID X.
Later, the subscription expires.
App user ID B456 (with originalAppUserId = anonimo456) then makes a new purchase using the same Apple ID X.
In this case:
Will the originalAppUserId on the receipt be updated from anonimous123 to anonimous456?
Or is it the originalAppUserId of the appUserId that gets updated?
Do both A123 and B456 still maintain their individual identity and separation as customers in the RevenueCat dashboard — each with their own purchase history?
Again, thank you so much for all the help — your guidance has been extremely valuable throughout this process!
Hey @paulo-ricardo ,
Absolutely! Happy to clarify things here. So, to expand on the Transfer if there are no active subscriptions behavior further, and based on your question(s):
Does originalAppUserId get updated when ownership transfers?
No, the originalAppUserId is a RevenueCat only field tied to the customer profile, not the receipt.
This field is set when the customer is first created within the RevenueCat system and it never changes, even after ownership of a subscription is transferred.
Taking your example:
A123 will always have their RevenueCat originalAppUserId = anonimo123;
B456 will always have their RevenueCat originalAppUserId = anonimo456;
when the subscription is transferred to B456 (once A123’s expires), B456 becomes the new owner, but each customer's originalAppUserId remains unchanged. So A123 still keeps anonimo123 and B456 the anonimo456;
the only thing that changes is the association of the ownership of the receipt. First was with A123 and now is with B456.
Do A123 and B456 remain separate?
Yes, they remain fully distinct customer profiles in RevenueCat, each with their own ID and historical activity and only one of them (the current owner) will show the active subscription and entitlements.
So in short:
both A123 and B456 maintain their individual identity and separation as customers in the RevenueCat dashboard, each with their own purchase history;
originalAppUserId is never updated, it reflects the user profile's creation point, not receipt ownership;
The transfer only affects which profile currently owns the subscription.
NOTE:
Everything above applies as described because we're dealing with identified App User IDs only.
If anonymous App User IDs (those starting with $RCAnonymousID:) are involved, the flow can differ — especially around:
aliasing and merging behavior
when anonymous users are upgraded to identified users
how customer profiles are linked or retained
If you’re working with any flows that involve anonymous IDs, feel free to reach out — happy to walk through those specifics with you.
Hope this helps! If you run into anything else while testing or need a second set of eyes, just let me know.
Best, Gui
Hey @guilherme ,
Thanks again for taking the time to clarify everything — your explanation really helped me lock down the behavior I was expecting.
I wanted to share the exact flow I implemented, in case it's helpful for others who may be integrating RevenueCat with their own backend systems:
Final Flow (Only using identified users)
Login Flow
Every time the user logs into the app, I first call Purchases.shared.logOut() just to ensure there’s no active session lingering.
Right after that, I call Purchases.shared.logIn(appUserId), passing the user ID I use in my backend.
This ensures that every transaction is always tied to a known, identified user. Anonymous IDs are never used.
Paywall Behavior
The paywall is only shown if the user is logged in, so no purchases can happen under anonymous IDs.
If the user already has an active subscription, I display a card with subscription info, and clicking it opens the native Apple subscription management screen via Purchases.shared.showManageSubscriptions().
If the user doesn’t have a subscription, I show a card inviting them to subscribe. When they click:
I run a check (see image below) with Purchases.shared.syncPurchases() to see if the current Apple account already has an active subscription (linked to another app user).
If it does, I show an alert explaining that the Apple ID is already linked to another account.
Subscription Check Code (Swift)
RevenueCat Configuration
I use the "Transfer if there are no active subscriptions" mode in RevenueCat to allow subscription transfers only when there’s no active subscription tied to the Apple ID.
Backend Sync via Webhooks
Every time there's a transaction (purchase, renewal, cancellation, etc), a webhook is triggered.
I extract the appUserId from the webhook.
Then I call the /v1/subscribers/{appUserId} RevenueCat API to get the up-to-date subscription data.
I store that data in my database and control all access based on my backend, not through the SDK entitlements.
Everything is working great now, and I really appreciate your help getting all of this clarified — especially around how originalAppUserId works and how transfers behave. Your answers (and patience) saved me a lot of trial and error.