Skip to content

Add PurchaseLogic support for paywall presentation when PurchasesAreCompletedBy.MyApp#842

Merged
tonidero merged 20 commits into
mainfrom
feat/purchase-logic-paywall
Mar 11, 2026
Merged

Add PurchaseLogic support for paywall presentation when PurchasesAreCompletedBy.MyApp#842
tonidero merged 20 commits into
mainfrom
feat/purchase-logic-paywall

Conversation

@tonidero

@tonidero tonidero commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add PurchaseLogic parameter to paywall presentation, allowing customers in PurchasesAreCompletedBy.MyApp mode to handle purchases and restores themselves through the paywall UI
  • New public types: PurchaseLogic (with PerformPurchaseHandler and PerformRestoreHandler delegates) and PurchaseLogicResult enum
  • Optional purchaseLogic parameter added to all PaywallOptions constructors
  • Uses existing HybridPurchaseLogicBridge from purchases-hybrid-common on both platforms
  • Subtester: adds a "Paywall w/ PurchaseLogic" button that calls PurchasePackage within the handler

Android-specific

  • Dialog uses FLAG_NOT_FOCUSABLE to prevent Unity's message processing from breaking after ProxyBillingActivity (Google Play billing) lifecycle transitions
  • AndroidJavaProxy.Invoke override handles method dispatch as Unity's proxy matching doesn't always resolve direct C# methods
  • SynchronizationContext captured at setup time to ensure async continuations run on the main thread

iOS-specific

  • Uses PaywallProxy.presentPaywall(options:purchaseLogicBridge:paywallResultHandler:)
  • Updated non-PurchaseLogic path to use the non-deprecated API with nil bridge

Related PRs

Test plan

  • Present paywall with PurchaseLogic on Android — purchase, cancel, and error flows
  • Present paywall with PurchaseLogic on iOS — purchase, cancel, and error flows
  • Present paywall WITHOUT PurchaseLogic — verify no regression on both platforms
  • Verify back button / dismiss works on both platforms
  • Test with exit offers (requires purchases-ios#6391)

🤖 Generated with Claude Code

tonidero and others added 4 commits March 3, 2026 13:02
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>
Adds support for custom purchase and restore handlers when presenting
paywalls in PurchasesAreCompletedBy.MyApp mode. This allows customers
to handle purchases and restores themselves through the paywall UI,
matching the existing functionality in React Native and Capacitor SDKs.

New public API:
- PurchaseLogic class with PerformPurchaseHandler and PerformRestoreHandler delegates
- PurchaseLogicResult enum (Success, Cancellation, Error)
- Optional purchaseLogic parameter on PaywallOptions constructors

Implementation details:
- Uses HybridPurchaseLogicBridge from purchases-hybrid-common on both iOS and Android
- Android: Dialog uses FLAG_NOT_FOCUSABLE to prevent Unity's message processing
  from breaking after billing Activity lifecycle transitions
- Android: AndroidJavaProxy.Invoke override handles method dispatch since Unity's
  proxy matching doesn't always resolve the direct C# methods
- Android: SynchronizationContext captured at setup time and used to Post async
  work, ensuring continuations run on the main thread
- iOS: Uses PaywallProxy.presentPaywall(options:purchaseLogicBridge:paywallResultHandler:)
- Subtester: Adds a "Paywall w/ PurchaseLogic" button (disabled in RevenueCat mode)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonidero tonidero added the pr:feat A new feature label Mar 3, 2026
@tonidero tonidero changed the title Add PurchaseLogic support for paywall presentation Add PurchaseLogic support for paywall presentation when PurchasesAreCompletedBy.MyApp Mar 4, 2026
@tonidero tonidero marked this pull request as ready for review March 4, 2026 09:50
@tonidero tonidero requested a review from a team as a code owner March 4, 2026 09:50
@tonidero tonidero marked this pull request as draft March 4, 2026 10:46
The paywall Dialog used FLAG_NOT_FOCUSABLE permanently, which prevented
all back navigation (both KEYCODE_BACK and gesture back on API 33+)
from reaching the Dialog.

Now FLAG_NOT_FOCUSABLE is only toggled on during active PurchaseLogic
operations (where it's needed for correct threading) and off otherwise.
Back navigation is handled via:
- Dialog.onBackPressed() override for pre-API 33 / button navigation
- OnBackInvokedCallback on the Dialog's window for API 33+ gesture nav

Both paths dispatch through the OnBackPressedDispatcher for Compose's
BackHandler, with a fallback to direct dismissal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonidero tonidero marked this pull request as ready for review March 4, 2026 12:10
@tonidero tonidero requested review from a team, facumenzella and vegaro March 4, 2026 12:10
tonidero and others added 2 commits March 5, 2026 16:28
Make PerformPurchaseHandler accept a PurchaseLogicPurchaseParams object
instead of a bare Package, so additional purchase parameters can be added
in the future without breaking the API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Comment thread RevenueCatUI/Scripts/Platforms/Android/AndroidPaywallPresenter.cs Outdated
Comment thread RevenueCatUI/Scripts/PurchaseLogic.cs
tonidero and others added 2 commits March 11, 2026 10:29
…wall

Resolve conflicts in PaywallViewPresenter.java by adopting the refactored
structure from feat/paywall-view-android (extracted methods, edge-to-edge
support, cleaner back press routing) while preserving PurchaseLogic additions
(HybridPurchaseLogicBridge, setDialogNotFocusable, onPurchaseLogicResult).

Conflict resolutions:
- Imports: merged both sets (DialogInterface/KeyEvent from origin + Handler/Looper from HEAD)
- onError: adopted origin's behavior (RESULT_NOT_PRESENTED) per commit c574d13
- Dialog creation: use origin's createDialog() with HEAD's FLAG_NOT_FOCUSABLE comment
- PaywallListener: use origin's extracted createPaywallListener() with HEAD's setDialogNotFocusable calls
- Back press: use origin's setupBackPressRouting (OnKeyListener/OnBackInvokedCallback)
- dismissDialog: use origin's simpler version + HEAD's currentPurchaseLogicBridge cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonidero tonidero requested a review from vegaro March 11, 2026 10:05

private static async void OnPerformPurchaseAsync(string requestId, string packageJson)
{
if (s_currentPurchaseLogic?.PerformPurchase == null)

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.

super minor. since you're checking this is not null earlier (in the constructor) I believe this is dead code

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.

I think it could still be relevant in case we haven't set the s_currentPurchaseLogic property before this is called. Which normally it shouldn't... but I guess there is no harm on keeping this?

internal PurchaseLogicPurchaseParams(Purchases.Package packageToPurchase)
{
PackageToPurchase = packageToPurchase;
}

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.

we added replacement mode and subscription option in Android as well. We can add that in another PR

tonidero added a commit that referenced this pull request Mar 11, 2026
## 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:
#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.java` — `Parcelable` 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](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Base automatically changed from feat/paywall-view-android to main March 11, 2026 11:24
Resolve conflicts after squash-merge of feat/paywall-view-android into main.
All conflicts were PurchaseLogic additions (unique to this branch) vs main
(which doesn't have them): kept HEAD's PurchaseLogic code in both files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonidero tonidero enabled auto-merge (squash) March 11, 2026 11:39
@tonidero tonidero merged commit d4dd929 into main Mar 11, 2026
8 checks passed
@tonidero tonidero deleted the feat/purchase-logic-paywall branch March 11, 2026 11:47
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:feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants