Skip to content

refactor: Use PaywallView instead of PaywallActivity on Android#841

Merged
tonidero merged 10 commits into
mainfrom
feat/paywall-view-android
Mar 11, 2026
Merged

refactor: Use PaywallView instead of PaywallActivity on Android#841
tonidero merged 10 commits into
mainfrom
feat/paywall-view-android

Conversation

@tonidero

@tonidero tonidero commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the PaywallTrampolineActivity approach (which launched a separate PaywallActivity on top of Unity) with a PaywallView hosted inside a hardware-accelerated Dialog window
  • The paywall is now presented on top of the Unity UI without leaving the current Activity
  • The C# public API (PaywallsPresenter.Present() / PresentIfNeeded()) and iOS implementation remain unchanged
  • The final objective in a follow-up PR is to add PurchaseLogic functionality to this API: Add PurchaseLogic support for paywall presentation when PurchasesAreCompletedBy.MyApp #842

Changes

New: PaywallViewPresenter.java

  • Creates a fullscreen Dialog with FLAG_HARDWARE_ACCELERATED to ensure Compose + Coil can use hardware bitmaps (Unity's main window uses software rendering)
  • PaywallBackPressedOwner provides OnBackPressedDispatcherOwner for Compose's BackHandler (Unity's Activity doesn't extend ComponentActivity)
  • PaywallListener tracks purchase/restore/error events and maps them to the existing result strings (PURCHASED, RESTORED, CANCELLED, ERROR, NOT_PRESENTED)
  • presentPaywallIfNeeded checks entitlements via Purchases.getSharedInstance().getCustomerInfo() before showing the paywall

Modified

  • RevenueCatUI.java — delegates to PaywallViewPresenter instead of PaywallTrampolineActivity
  • AndroidManifest.xml — removed PaywallTrampolineActivity registration
  • build.gradle — added compileOnly for androidx.compose.ui:ui-android (needed to resolve PaywallView's class hierarchy at compile time)

Removed

  • PaywallTrampolineActivity.java — no longer needed
  • PaywallUnityOptions.javaParcelable was only needed for Intent extras

Test plan

  • Present paywall unconditionally — verify it appears overlaid on Unity content
  • Present paywall if needed (user has entitlement) — verify NOT_PRESENTED result
  • Present paywall if needed (user lacks entitlement) — verify paywall appears
  • Dismiss via close button — verify CANCELLED result
  • Dismiss via back button — verify CANCELLED result
  • Complete a purchase — verify PURCHASED result
  • Complete a restore — verify RESTORED result
  • Trigger a purchase error then dismiss — verify ERROR result
  • Verify iOS paywall still works unchanged

🤖 Generated with Claude Code

Replace the PaywallTrampolineActivity (which launched a separate
PaywallActivity on top of Unity) with a PaywallView hosted in a
hardware-accelerated Dialog window. This presents the paywall on top
of the Unity UI without leaving the current Activity.

Key changes:
- New PaywallViewPresenter that creates a fullscreen Dialog with
  FLAG_HARDWARE_ACCELERATED to avoid software rendering crashes
- PaywallBackPressedOwner provides OnBackPressedDispatcherOwner
  for Compose's BackHandler (Unity doesn't use ComponentActivity)
- PaywallListener tracks purchase/restore/error events and maps
  them to the same result strings (PURCHASED, RESTORED, etc.)
- presentPaywallIfNeeded checks entitlements via Purchases SDK
- Removed PaywallTrampolineActivity and PaywallUnityOptions
- C# public API and iOS remain unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonidero tonidero added pr:feat A new feature pr:other Changes to our CI configuration files and scripts and removed pr:feat A new feature labels Mar 3, 2026
@tonidero tonidero changed the title feat: Use PaywallView instead of PaywallActivity on Android refactor: Use PaywallView instead of PaywallActivity on Android Mar 3, 2026
tonidero and others added 3 commits March 3, 2026 15:39
Explicitly set window layout to MATCH_PARENT for both dimensions
to guarantee fullscreen on tablets and foldables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonidero

tonidero commented Mar 4, 2026

Copy link
Copy Markdown
Contributor Author

This is quite a bit change... I've been testing it and seems to work but would appreciate a thorough review @vegaro @facumenzella since you worked on this and @RevenueCat/coresdk and ideally some testing 🙏

Add currentDialog != null check in the PaywallView dismiss handler
to prevent sendPaywallResult from being called twice if the
onDismissListener fires before the dismiss handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
// Required at compile time so the Java compiler can resolve PaywallView's
// class hierarchy (PaywallView -> CompatComposeView -> AbstractComposeView -> ViewGroup).
// At runtime this is provided transitively by purchases-hybrid-common-ui.
compileOnly 'androidx.compose.ui:ui-android:1.7.0'

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.

how does EDM4U behave with compileOnly? I am going to see how it behaves when I test it but just in case

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.

Also, why not making it implementation directly if it's transitively being included by purchases-hybrid-common-ui

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

how does EDM4U behave with compileOnly? I am going to see how it behaves when I test it but just in case

I didn't have any issues with making it work with Subtester... So I think it works fine.

why not making it implementation directly

Indeed... My reasoning was that we only need this to compile, and there is no actual usages of this code (aside from the one in purchases-android), so it's better to reflect that with the proper usage of the APIs here I guess... But TBH, not a strong opinion. Happy to change to implementation if you prefer.

private static final String RESULT_NOT_PRESENTED = "NOT_PRESENTED";

private static volatile Dialog currentDialog;
private static volatile String lastResult;

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.

These are marked as volatile, but not synchronized. For example we have if (currentDialog != null). Two threads could evaluate it to true if executed at the same time. We should probably be adding synchronization

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You do have a good point, but I believe it shouldn't be a problem since handling of these properties always happens in the UI thread (we do a bunch of calls in the runOnUiThread method + others are system calls that should be called on the ui thread as well). I guess we could try to make it more robust at the risk of more complexity. Wdyt?

@tonidero tonidero Mar 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

At least we should definitely add a comment though. Edit: Added here

PaywallBackPressedOwner() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);

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 am a bit confused by this... can you explain it to me?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

More or less 😅. First of all, we need to override the back button behavior so the back button is not passed on to the unity activity but handled by our Dialog. Note that the normal back button handling doesn't work mostly because the Unity activity is a base Activity.

In order to solve this we create this class, which is a custom OnBackPressedDispatcherOwner, which needs it's own lifecycleRegistry to work. And for this lifecycleRegistry to work, it needs to receive at least some events so the lifecycle is considered "started".
These events mostly try to mimick a normal lifecycle within the dialog.

I do think it's quite brittle :( But got stuck trying other approaches to override the back button behavior.

tonidero and others added 2 commits March 10, 2026 10:19
private static PresentedOfferingContext mapPresentedOfferingContext(
@Nullable String jsonString,
@Nullable String fallbackOfferingId
) {

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.

it sucks to have to do this here, because we don't have tests mainly... but I don't think it's worth it to add a new constructor of PaywallView. But maybe we can move this to phc? and create a new constructor of PresentedOfferingContext from json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This will make it so we can use the mapper from PHC

@vegaro

This comment was marked as outdated.

@vegaro

This comment was marked as outdated.

@vegaro

This comment was marked as outdated.

tonidero added a commit to RevenueCat/purchases-hybrid-common that referenced this pull request Mar 10, 2026
This will be used in
RevenueCat/purchases-unity#841 so we avoid
duplicating the logic
@tonidero tonidero requested a review from vegaro March 10, 2026 17:19

@vegaro vegaro 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.

Looks great @tonidero . Thank you so much for tackling this. Tested it and edge to edge works fine, and landscape. Same for the result of the paywall which looks to be returning correctly.

I wasn't able to test without edge to edge though (using an older device)

@tonidero tonidero enabled auto-merge (squash) March 11, 2026 09:29
@tonidero tonidero disabled auto-merge March 11, 2026 09:31
@tonidero tonidero merged commit 0e6cf92 into main Mar 11, 2026
8 checks passed
@tonidero tonidero deleted the feat/paywall-view-android branch March 11, 2026 11:24
tonidero added a commit that referenced this pull request Mar 11, 2026
## Summary
- With the changes to use a view instead of the PaywallActivity in #841,
we broke a situation that some devs used to override the activity
orientation to present their paywall. This PR fixes that breaking
behavior change.
- Reads `android:screenOrientation` from the `PaywallActivity` manifest
entry (which users may override via manifest merging) and applies it
using Unity's `Screen.orientation` API before presenting the paywall
- Restores the original orientation when the paywall is dismissed
- Preserves backward compatibility for users who set `screenOrientation`
on `PaywallActivity` now that the paywall uses a Dialog instead of a
separate Activity

## Test plan
- [ ] Override `PaywallActivity` `screenOrientation` to `landscape` in
app manifest and verify paywall displays in landscape
- [ ] Verify orientation restores to original after paywall is dismissed
- [ ] Verify no orientation change when no manifest override is present
- [ ] Test with both portrait and landscape overrides

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ajpallares pushed a commit that referenced this pull request Mar 11, 2026
**This is an automatic release.**

## RevenueCat SDK
### ✨ New Features
* Add PurchaseLogic support for paywall presentation when
`PurchasesAreCompletedBy.MyApp` (#842) via Toni Rico (@tonidero)
### 📦 Dependency Updates
* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 17.46.1 (#854) via
RevenueCat Git Bot (@RCGitBot)
* [Android
9.23.1](https://github.com/RevenueCat/purchases-android/releases/tag/9.23.1)
* [Android
9.23.0](https://github.com/RevenueCat/purchases-android/releases/tag/9.23.0)
* [iOS
5.61.0](https://github.com/RevenueCat/purchases-ios/releases/tag/5.61.0)
* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 17.46.0 (#853) via
RevenueCat Git Bot (@RCGitBot)
* [Android
9.23.1](https://github.com/RevenueCat/purchases-android/releases/tag/9.23.1)
* [Android
9.23.0](https://github.com/RevenueCat/purchases-android/releases/tag/9.23.0)
* [iOS
5.61.0](https://github.com/RevenueCat/purchases-ios/releases/tag/5.61.0)
* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 17.44.0 (#850) via
RevenueCat Git Bot (@RCGitBot)
* [Android
9.23.1](https://github.com/RevenueCat/purchases-android/releases/tag/9.23.1)
* [Android
9.23.0](https://github.com/RevenueCat/purchases-android/releases/tag/9.23.0)
* [iOS
5.61.0](https://github.com/RevenueCat/purchases-ios/releases/tag/5.61.0)

## RevenueCatUI SDK
### ✨ New Features
* Add full screen paywall presentation support (#839) via Cesar de la
Vega (@vegaro)

### 🔄 Other Changes
* Redo Subtester app and fix Android emulators support (#855) via Cesar
de la Vega (@vegaro)
* Support PaywallActivity screenOrientation manifest override (#844) via
Toni Rico (@tonidero)
* refactor: Use PaywallView instead of PaywallActivity on Android (#841)
via Toni Rico (@tonidero)
* Add API tests for Paywalls and CustomerCenter presenters (#735) via
Facundo Menzella (@facumenzella)
* Bump fastlane-plugin-revenuecat_internal from `f5c099b` to `e146447`
(#852) via dependabot[bot] (@dependabot[bot])
* Bump fastlane-plugin-revenuecat_internal from `8cd957f` to `f5c099b`
(#848) via dependabot[bot] (@dependabot[bot])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:other Changes to our CI configuration files and scripts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants