refactor: Use PaywallView instead of PaywallActivity on Android#841
Conversation
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>
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>
|
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' |
There was a problem hiding this comment.
how does EDM4U behave with compileOnly? I am going to see how it behaves when I test it but just in case
There was a problem hiding this comment.
Also, why not making it implementation directly if it's transitively being included by purchases-hybrid-common-ui
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
I am a bit confused by this... can you explain it to me?
There was a problem hiding this comment.
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.
… accessed on the UI thread Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| private static PresentedOfferingContext mapPresentedOfferingContext( | ||
| @Nullable String jsonString, | ||
| @Nullable String fallbackOfferingId | ||
| ) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
That's indeed a good point... I think we could just expose this extension function: https://github.com/RevenueCat/purchases-hybrid-common/blob/88114deea66a52a18eba826dc5d1d6533c144b8f/android/hybridcommon/src/main/java/com/revenuecat/purchases/hybridcommon/common.kt#L1600... Will try it out, thank you!
There was a problem hiding this comment.
This will make it so we can use the mapper from PHC
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This will be used in RevenueCat/purchases-unity#841 so we avoid duplicating the logic
vegaro
left a comment
There was a problem hiding this comment.
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)
## 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>
**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])
Summary
PaywallTrampolineActivityapproach (which launched a separatePaywallActivityon top of Unity) with aPaywallViewhosted inside a hardware-acceleratedDialogwindowPaywallsPresenter.Present()/PresentIfNeeded()) and iOS implementation remain unchangedPurchasesAreCompletedBy.MyApp#842Changes
New:
PaywallViewPresenter.javaDialogwithFLAG_HARDWARE_ACCELERATEDto ensure Compose + Coil can use hardware bitmaps (Unity's main window uses software rendering)PaywallBackPressedOwnerprovidesOnBackPressedDispatcherOwnerfor Compose'sBackHandler(Unity'sActivitydoesn't extendComponentActivity)PaywallListenertracks purchase/restore/error events and maps them to the existing result strings (PURCHASED,RESTORED,CANCELLED,ERROR,NOT_PRESENTED)presentPaywallIfNeededchecks entitlements viaPurchases.getSharedInstance().getCustomerInfo()before showing the paywallModified
RevenueCatUI.java— delegates toPaywallViewPresenterinstead ofPaywallTrampolineActivityAndroidManifest.xml— removedPaywallTrampolineActivityregistrationbuild.gradle— addedcompileOnlyforandroidx.compose.ui:ui-android(needed to resolvePaywallView's class hierarchy at compile time)Removed
PaywallTrampolineActivity.java— no longer neededPaywallUnityOptions.java—Parcelablewas only needed for Intent extrasTest plan
NOT_PRESENTEDresultCANCELLEDresultCANCELLEDresultPURCHASEDresultRESTOREDresultERRORresult🤖 Generated with Claude Code