Feat: Restore gating in paywalls UI#3171
Merged
Merged
Conversation
Introduce a Resumable functional interface and onRestoreInitiated callback to let clients pause/prepare before restore proceeds. Added Resumable.kt and updated CustomerCenterListener and PaywallListener APIs with default implementations that immediately resume. Propagated support across the UI and internals: PaywallActivity, PaywallView, OriginalTemplatePaywallFooterView, CustomerCenterView, and api-tester stubs were updated to accept and forward Resumable. ViewModels (CustomerCenterViewModel, PaywallViewModel) now suspend and await the listener decision (using suspendCoroutine) and abort the restore when the listener resumes with false. Tests were updated to mock Resumable behaviour and add coverage for the cancellation path.
… into PF-343/resumable-restore-purchases
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.
- ✅ Fixed: Cancelled restore leaves non-dismissable dialog stuck on screen
- When restore initiation is cancelled,
restorePurchases()now clearsrestorePurchasesStatebefore returning, and the restore-cancellation test now verifies the dialog state is reset.
- When restore initiation is cancelled,
Or push these changes by commenting:
@cursor push 5271dcc692
Preview (5271dcc692)
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/viewmodel/CustomerCenterViewModel.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/viewmodel/CustomerCenterViewModel.kt
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/viewmodel/CustomerCenterViewModel.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/viewmodel/CustomerCenterViewModel.kt
@@ -510,6 +510,13 @@
if (!shouldResumeRestorePurchases(listener, "listener") ||
!shouldResumeRestorePurchases(purchases.customerCenterListener, "purchases.customerCenterListener")
) {
+ _state.update { currentState ->
+ if (currentState is CustomerCenterState.Success) {
+ currentState.copy(restorePurchasesState = null)
+ } else {
+ currentState
+ }
+ }
return
}
notifyListenersForRestoreStarted()
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/data/CustomerCenterViewModelTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/data/CustomerCenterViewModelTests.kt
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/data/CustomerCenterViewModelTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/customercenter/data/CustomerCenterViewModelTests.kt
@@ -963,13 +963,30 @@
listener = directListener
)
+ model.state.filterIsInstance<CustomerCenterState.Success>().first()
+
+ val missingPurchasePath = HelpPath(
+ id = "missing_purchase_id",
+ title = "Restore Purchases",
+ type = HelpPath.PathType.MISSING_PURCHASE
+ )
+ model.pathButtonPressed(mockk(relaxed = true), missingPurchasePath, null)
+
+ model.state.first {
+ it is CustomerCenterState.Success &&
+ it.restorePurchasesState == RestorePurchasesState.RESTORING
+ }
+
model.restorePurchases()
+ val updatedState = model.state.value as CustomerCenterState.Success
+
verify(exactly = 1) { directListener.onRestoreInitiated(any()) }
verify(exactly = 0) { purchasesListener.onRestoreInitiated(any()) }
coVerify(exactly = 0) { purchases.awaitRestore() }
verify(exactly = 0) { directListener.onRestoreStarted() }
verify(exactly = 0) { purchasesListener.onRestoreStarted() }
+ assertThat(updatedState.restorePurchasesState).isNull()
}
@TestThis Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
… into PF-343/resumable-restore-purchases
MonikaMateska
requested changes
Mar 17, 2026
Replace suspendCoroutine with suspendCancellableCoroutine in CustomerCenterViewModel and PaywallViewModel so the coroutine waiting on onRestoreInitiated can be cancelled. Move the restore-detail log message out of the listener callback to run after the continuation resumes. Add suspendCancellableCoroutine imports and remove the now-unused suspendCoroutine import.
MonikaMateska
approved these changes
Mar 18, 2026
📸 Snapshot Test582 unchanged
🛸 Powered by Emerge Tools |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3171 +/- ##
==========================================
- Coverage 79.39% 79.37% -0.02%
==========================================
Files 356 357 +1
Lines 14346 14349 +3
Branches 1959 1959
==========================================
Hits 11390 11390
- Misses 2152 2155 +3
Partials 804 804 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Contributor
Author
Merge activity
|
github-merge-queue Bot
pushed a commit
that referenced
this pull request
Mar 26, 2026
**This is an automatic release.** ## RevenueCat SDK ### 🐞 Bugfixes * [EXTERNAL] fix: ensure activity is attached before showing in-app messages (#3274) contributed by @matteinn (#3275) via Toni Rico (@tonidero) * Ensure MediaPlayer has dedicated thread owner that is not the main thread (#3148) via Jacob Rakidzich (@JZDesign) * Fix heartbeat monitor and Slack notifications for nightly integration tests (#3259) via Rick (@rickvdl) ## RevenueCatUI SDK ### Paywallv2 #### ✨ New Features * Feat: Restore gating in paywalls UI (#3171) via Jacob Rakidzich (@JZDesign) ### 🔄 Other Changes * security: pin GitHub Actions to SHA hashes (#3272) via Alfonso Embid-Desmet (@alfondotnet) * Bump activesupport from 8.0.2 to 8.0.4.1 (#3270) via dependabot[bot] (@dependabot[bot]) * Merge release PR after deploy (#3269) via Antonio Pallares (@ajpallares) * Require PR approval before release tagging (#3268) via Antonio Pallares (@ajpallares) * Bump json from 2.18.1 to 2.19.2 (#3261) via dependabot[bot] (@dependabot[bot]) * feat(ads): update admob sample app (#3264) via Peter Porfy (@peterporfy) * feat(ads): add vanilla-ad-tracker-sample (#3263) via Peter Porfy (@peterporfy) * [Purchase Tester]: Persist appUserId on login screen across app launches (#3266) via Will Taylor (@fire-at-will) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Mostly release automation and versioning changes, but it modifies CI/orb references and deploy/merge automation, which could affect the release pipeline if misconfigured. > > **Overview** > Cuts the `9.28.0` release by removing `-SNAPSHOT` across version sources (`.version`, `gradle.properties`, `Config.frameworkVersion`) and updating sample/test-app dependency pins to `9.28.0`. > > Updates release documentation: publishes Dokka docs to the `9.28.0` S3 path, updates `docs/index.html` redirect to `9.28.0`, and rolls `CHANGELOG.latest.md` into a new `9.28.0` section in `CHANGELOG.md`. > > Tweaks release tooling/CI: pins `fastlane-plugin-revenuecat_internal` to a specific git ref (and bumps a few Ruby deps), switches the CircleCI `revenuecat/sdks-common-config` orb to a dev commit, and adds a temporary `test_merge_queue` workflow to exercise `revenuecat/merge-release-pr` with `use_merge_queue: true`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5050888. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Checklist
purchases-iosand hybridsMotivation
We have customers who like the
onPurchaseInitiatedflow where they can block and resume or cancel the flow so that they can authenticate or perform a parental gate. We've received a request to do the same for restoring purchases.Description
Introduce a
Resumablefunctional interface andonRestoreInitiatedcallback to let clients pause/prepare before restore proceeds. AddedResumable.ktand updatedCustomerCenterListenerandPaywallListenerAPIs with default implementations that immediately resume. Propagated support across the UI and internals:PaywallActivity,PaywallView,OriginalTemplatePaywallFooterView,CustomerCenterView, and api-tester stubs were updated to accept and forwardResumable. ViewModels (CustomerCenterViewModel,PaywallViewModel) now suspend and await the listener decision (usingsuspendCoroutine) and abort the restore when the listener resumes with false. Tests were updated to mockResumablebehaviour and add coverage for the cancellation path.Note
Medium Risk
Adds a new restore-gating callback (
onRestoreInitiated) that can pause or cancel restore flows, changing restore behavior and listener contracts across UI and Customer Center. Moderate risk due to coroutine gating and potential hangs if clients never resume, but defaults preserve existing behavior.Overview
Adds a resumable “restore initiated” gate to both Customer Center and paywall restore flows via a new
Resumablefunctional interface andonRestoreInitiated(resume)callback (defaulting to immediate resume).Updates the UI restore implementations (
PaywallViewModel.handleRestorePurchases,CustomerCenterViewModel.restorePurchases) to suspend until listeners resume and to abort restore when resumed withfalse, and wires the callback through wrapper listeners inPaywallActivity/View wrappers.Refreshes public API surface files (
api.txt/api-defaults*.txt), api-tester stubs, and unit tests to cover the new callback and the “cancel restore” path.Written by Cursor Bugbot for commit 8c441a3. This will update automatically on new commits. Configure here.