Skip to content

feat(paywalls): Add callbacks to presentPaywall and presentPaywallIfNeeded - #1898

Draft
AlvaroBrey wants to merge 3 commits into
mainfrom
feat/present-paywall-callbacks
Draft

AlvaroBrey wants to merge 3 commits into
mainfrom
feat/present-paywall-callbacks

Conversation

@AlvaroBrey

@AlvaroBrey AlvaroBrey commented Sep 15, 2026

Copy link
Copy Markdown
Member
  • Adds the PaywallView callbacks (onPurchaseStarted, onPurchaseCompleted, onPurchaseCancelled, onPurchaseError, onRestoreCompleted, onRestoreError, onWebCheckoutOpened, onUrlOpened, onInteraction) as named optional parameters of presentPaywall() and presentPaywallIfNeeded(), so modally presented paywalls report the same events as the embedded view. No onDismiss: the returned Future<PaywallResult> completes on dismissal.
  • Callbacks ride a dedicated com.revenuecat.purchasesui/PresentedPaywall channel handled by the existing PaywallViewMethodHandler. The plugin channel's onRestore* and onDismiss method names already belong to the customer center handler.
  • Native attaches a listener only when a callback is passed (hasListener). On Android that listener is a non-serializable arg, so a paywall recreated after process death is dismissed with ERROR; presentations without callbacks keep today's behavior.
  • Events are process-wide: one presented paywall at a time, the same assumption presentCustomerCenter() callbacks make.
  • onPurchasePackageInitiated is not exposed; the native wrapper auto-resumes, as it does for PaywallView.
  • Fixes onPurchaseCompleted never firing on iOS when a purchase completes with no StoreKit transaction: the handler parsed the absent transaction and threw. It affected PaywallView and the footer view already, since all four surfaces share PaywallViewMethodHandler; the callback now receives an empty StoreTransaction.
  • Part of SDK-4498.

Checklist

  • A description about what and why you are contributing, even if it's trivial.
  • The issue number(s) or PR number(s) in the description if you are contributing in response to those.
  • If applicable, unit tests.
Agent description

Motivation

PaywallView gained onInteraction in #1897, but apps that present paywalls modally still only get the final PaywallResult. They cannot route interactions to analytics or react to purchase events while the paywall is up. Both natives already have the hook unused: PresentPaywallOptions.paywallListener on Android and PaywallProxy.delegate on iOS.

Description

  • purchases_ui_flutter.dart: both present methods build a PaywallViewMethodHandler when any callback is given, install it on the presented-paywall channel, send 'hasListener': true, and clear the handler in a finally once the result arrives. Without callbacks the argument map is unchanged.
  • Android PurchasesUiFlutterPlugin.kt: a second MethodChannel and a PaywallListenerWrapper passed as paywallListener when hasListener. The wrapper bodies moved to a shared forwardingPaywallListener(channel) that views/PaywallView.kt now uses too, so both surfaces send the same method names. onPurchasePackageInitiated is left to PHC's default, which resumes the purchase.
  • iOS PurchasesUiFlutterPlugin.swift: PresentedPaywallDelegateForwarder mirrors the PurchasesUiPaywallView delegate extension on a second FlutterMethodChannel. It is set as paywallProxy.delegate before presenting and dropped in the result handler. It does not implement the purchase-initiated delegate method, so PaywallProxy resumes automatically. macOS is untouched (same file, #if os(iOS)).
  • api_tester: _checkPresentPaywallWithListeners covers every callback on both methods.

Regression gates

  • paywall_view_method_handler_test.dart: onPurchaseCompleted fires when there is no store transaction, which throws type 'Null' is not a subtype of type 'Map<dynamic, dynamic>' without the handler guard.

  • presentPaywall callbacks › delivers callbacks while presented and stops after the result: a call on the presented-paywall channel reaches the callback while the native result is pending, and no longer after it resolves.

  • presentPaywall callbacks › sends hasListener only when a callback is given; the existing exact-match argument tests prove the key is absent otherwise.

Rejected:

  • Reusing the purchases_ui_flutter channel with prefixed method names: duplicates the parsing already in PaywallViewMethodHandler and keeps the customer center switch growing.
  • A PaywallListener object parameter: diverges from both presentCustomerCenter() and PaywallView, which take individual callbacks.
  • Always attaching the native listener: changes process-death behavior for every caller, not only those that opted into callbacks.

Note

Medium Risk
Touches purchase/restore event plumbing across Dart, Android, and iOS; behavior changes only when callbacks are passed, but incorrect listener lifecycle could drop or duplicate events during modal presentation.

Overview
Modal paywalls can now emit the same lifecycle events as embedded PaywallView. RevenueCatUI.presentPaywall() and presentPaywallIfNeeded() accept optional callbacks for purchase, restore, web checkout, URL opens, and paywall interactions; dismissal still comes from the returned Future<PaywallResult> (no onDismiss).

When any callback is provided, Dart registers PaywallViewMethodHandler on a new com.revenuecat.purchasesui/PresentedPaywall channel, passes hasListener: true to native, and clears the handler when the presentation completes. Android attaches forwardingPaywallListener (extracted from inline PaywallView code) only when hasListener is set. iOS adds PresentedPaywallDelegateForwarder on the same channel and tears it down in the paywall result handler.

Bugfix: PaywallViewMethodHandler no longer crashes on onPurchaseCompleted when storeTransaction is null (iOS purchases without a StoreKit transaction); callers get an empty StoreTransaction instead.

Reviewed by Cursor Bugbot for commit 06db779. Bugbot is set up for automated code reviews on this repo. Configure here.

@AlvaroBrey AlvaroBrey self-assigned this Sep 15, 2026
@AlvaroBrey
AlvaroBrey force-pushed the feat/present-paywall-callbacks branch from 7d29307 to cfbc180 Compare September 15, 2026 07:02
@AlvaroBrey
AlvaroBrey force-pushed the feat/present-paywall-callbacks branch from cfbc180 to 997960f Compare September 15, 2026 08:44
@AlvaroBrey

Copy link
Copy Markdown
Member Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 06db779. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant