Skip to content

Add exit offers support to workflows#3452

Merged
vegaro merged 23 commits into
mainfrom
cesar/workflow-exit-offers
May 11, 2026
Merged

Add exit offers support to workflows#3452
vegaro merged 23 commits into
mainfrom
cesar/workflow-exit-offers

Conversation

@vegaro

@vegaro vegaro commented May 7, 2026

Copy link
Copy Markdown
Member

Summary

Adds workflow exit-offer support by carrying exit_offers from 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_id when present, otherwise the terminal step discovered by walking the workflow graph. Preloading is now resilient to the activity/dialog calling preloadExitOffering() 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 detektAll

Note

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_offers by deserializing them on WorkflowScreen and mapping them through WorkflowScreenMapper into PaywallComponentsData.

Enhances workflow models with PublishedWorkflow.dismissExitOffer (and WorkflowExitOffer) to resolve the dismiss exit-offer offering from the canonical step (single_step_fallback_id).

Refactors PaywallViewModelImpl exit-offer handling to a dedicated ExitOfferData state, enabling safe preloading even if preloadExitOffering() 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.

vegaro and others added 6 commits May 7, 2026 17:08
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>
…adExitOffering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.57%. Comparing base (1bfed5e) to head (070e138).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...uecat/purchases/common/workflows/WorkflowModels.kt 66.66% 0 Missing and 3 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vegaro vegaro marked this pull request as ready for review May 8, 2026 10:28
@vegaro vegaro requested a review from a team as a code owner May 8, 2026 10:28
vegaro and others added 2 commits May 8, 2026 16:04
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if we could put this into ExitOfferData to avoid having a separate boolean. something like an idle state, or requested state

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

agreed. Attempted some clean up in 39829fc. What do you think

@facumenzella facumenzella left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
@vegaro vegaro force-pushed the cesar/workflow-exit-offers branch from 597fa84 to d3cbb91 Compare May 11, 2026 10:25

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

@vegaro vegaro added this pull request to the merge queue May 11, 2026
Merged via the queue into main with commit 1bf6078 May 11, 2026
36 checks passed
@vegaro vegaro deleted the cesar/workflow-exit-offers branch May 11, 2026 12:03
github-merge-queue Bot pushed a commit that referenced this pull request May 13, 2026
**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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants