Add exit offers support to workflows#3452
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the two resilience cases: field present is parsed correctly, field absent (e.g. removed from backend) defaults to null without error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ows/PublishedWorkflowSerializationTest.kt
…adExitOffering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3452 +/- ##
==========================================
+ Coverage 79.55% 79.57% +0.01%
==========================================
Files 365 365
Lines 14681 14690 +9
Branches 2002 2006 +4
==========================================
+ Hits 11680 11689 +9
+ Misses 2191 2188 -3
- Partials 810 813 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Nothing observes it for recomposition. closePaywall reads it imperatively, so the MutableState/State machinery was pure overhead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| } | ||
|
|
||
| private var exitOfferData: ExitOfferData = ExitOfferData.Loading | ||
| private var preloadExitOfferingRequested = false |
There was a problem hiding this comment.
I wonder if we could put this into ExitOfferData to avoid having a separate boolean. something like an idle state, or requested state
There was a problem hiding this comment.
agreed. Attempted some clean up in 39829fc. What do you think
facumenzella
left a comment
There was a problem hiding this comment.
a few nits, but looks great 🚀
resolveIfNeeded had no way to distinguish "not yet resolved" from "resolved but offering not found" — both left preloadedOffering=null, so the guard (preloadedOffering != null) never fired for the failure case. Every locale/colour/options refresh reset exitOfferData through an intermediate Loading() that erased any prior resolution, causing the not-found lookup and its error log to repeat on each refresh. Adds preloadAttempted: Boolean to ExitOfferData.Configured as a proper terminal flag. The guard in resolveIfNeeded is updated to check preloadAttempted instead of preloadedOffering != null, and both success and failure paths now set it to true. cancelAndResetState no longer resets exit offer data to Loading; that intermediate state served no purpose for exit offers and was the structural cause of the state loss. updateExitOfferData carries forward a completed resolution when the same offeringId is re-set with the same availability, but lets resolveIfNeeded run again if fresh offerings now contain the previously-missing offering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> much simpler
597fa84 to
d3cbb91
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d3cbb91. Configure here.
**This is an automatic release.** ## RevenueCat SDK ### 📦 Dependency Updates * [RENOVATE] Update dependency gradle to v8.14.5 (#3459) via RevenueCat Git Bot (@RCGitBot) ## RevenueCatUI SDK ### ✨ New Features * Pre-warm image cache for workflow step states (#3447) via Cesar de la Vega (@vegaro) ### Paywallv2 #### ✨ New Features * Add `close_workflow` button action (#3453) via Cesar de la Vega (@vegaro) #### 🐞 Bugfixes * Fix preload VideoComponent fallback override images (#3449) via Cesar de la Vega (@vegaro) ### 🔄 Other Changes * Select blob source by priority and weighted random (#3458) via Toni Rico (@tonidero) * [AUTOMATIC] Update golden test files for backend integration tests (#3473) via RevenueCat Git Bot (@RCGitBot) * Clean up unreferenced topic files after successful remote-config refresh (#3439) via Toni Rico (@tonidero) * Cache remote config response in memory with TTL and persist to disk (#3457) via Toni Rico (@tonidero) * build(deps): bump fastlane from 2.233.1 to 2.234.0 (#3463) via dependabot[bot] (@dependabot[bot]) * Update codelabs links (#3460) via Jaewoong Eum (@skydoves) * Add RemoteConfigManager and TopicFetcher (#3437) via Toni Rico (@tonidero) * Add exit offers support to workflows (#3452) via Cesar de la Vega (@vegaro) * Update baseline profiles (#3461) via RevenueCat Git Bot (@RCGitBot) * Add network scaffolding for remote config endpoint (#3435) via Toni Rico (@tonidero) * test: cover singleStepFallbackId == initialStepId edge case (#3445) via Facundo Menzella (@facumenzella) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: this is a release/versioning update (SNAPSHOT -> final) plus docs deployment path changes, with no functional code changes beyond version constants. > > **Overview** > Finalizes the `10.6.0` release by switching all version references from `10.6.0-SNAPSHOT` to `10.6.0` (root `.version`, `gradle.properties`, `Config.frameworkVersion`, and sample/test app `libs.versions.toml` files). > > Updates documentation publishing to point at the `10.6.0` docs path (CircleCI S3 sync target and `docs/index.html` redirect), and prepends the `10.6.0` section to `CHANGELOG.md`/`CHANGELOG.latest.md`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4da1697. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->

Summary
Adds workflow exit-offer support by carrying
exit_offersfrom workflow screens into paywall component data, then preloading the configured dismiss offering for paywall activity/dialog dismissal flows.The exit offer is resolved from the canonical workflow step:
single_step_fallback_idwhen present, otherwise the terminal step discovered by walking the workflow graph. Preloading is now resilient to the activity/dialog callingpreloadExitOffering()before workflow data has finished loading.Fixes
Dismiss exit offers are now gated to the workflow step that owns the exit-offer config. If the user dismisses on an earlier workflow screen, the preloaded exit offering is ignored and the paywall closes normally. If the user dismisses on the latest/canonical screen, the exit offer is shown.
The view model also guards stale async offering/workflow loads with a revision counter so older loads cannot overwrite newer exit-offer data.
Testing
./gradlew :ui:revenuecatui:testDefaultsBc8DebugUnitTest --tests "com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModelWorkflowTest"./gradlew :ui:revenuecatui:testDefaultsBc7DebugUnitTest --tests "com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModelWorkflowTest"./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests "com.revenuecat.purchases.common.workflows.*" :ui:revenuecatui:testDefaultsBc8DebugUnitTest --tests "com.revenuecat.purchases.ui.revenuecatui.workflow.WorkflowScreenMapperTest"./gradlew detektAllNote
Medium Risk
Updates paywall dismissal behavior and exit-offer preloading for workflow-driven paywalls, which can affect close flows and offering resolution. Risk is moderate due to new state management around async updates and step-gated exit offers.
Overview
Adds workflow-level support for
exit_offersby deserializing them onWorkflowScreenand mapping them throughWorkflowScreenMapperintoPaywallComponentsData.Enhances workflow models with
PublishedWorkflow.dismissExitOffer(andWorkflowExitOffer) to resolve the dismiss exit-offer offering from the canonical step (single_step_fallback_id).Refactors
PaywallViewModelImplexit-offer handling to a dedicatedExitOfferDatastate, enabling safe preloading even ifpreloadExitOffering()is called before workflow/offering data arrives, and gating dismiss exit-offers to only trigger when the user closes on the configured workflow step.Adds focused unit tests covering canonical-step resolution, preload timing, offering refresh behavior, and close-paywall integration with
dismissRequestWithExitOffering.Reviewed by Cursor Bugbot for commit 070e138. Bugbot is set up for automated code reviews on this repo. Configure here.