Skip to content

Fix: erroneous initializations of the PurchaseHandler from a button#6827

Merged
alexrepty merged 1 commit into
mainfrom
jzdesign/correct-initialization-churn-from-customer-center
Jun 4, 2026
Merged

Fix: erroneous initializations of the PurchaseHandler from a button#6827
alexrepty merged 1 commit into
mainfrom
jzdesign/correct-initialization-churn-from-customer-center

Conversation

@JZDesign

@JZDesign JZDesign commented May 20, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • If applicable, unit tests
  • If applicable, create follow-up issues for purchases-android and hybrids

Motivation

All buttons initialized new purchase handlers whether or not the customer center would get presented by the button. This resulted in 1 purchase handler instance per button, and there were many created during the draw phase only to get deinitialized in moments

Description

Conditionally apply the logic that caused that problem


Note

Low Risk
Scoped presentation and handler reuse for Customer Center buttons; no auth or purchase API surface changes beyond using the existing environment handler.

Overview
Paywalls V2 button views no longer attach Customer Center presentation (and a fresh PurchaseHandler) on every draw. ButtonComponentView now uses applyIf so .presentCustomerCenter runs only when the button action is navigate to Customer Center, via a new opensCustomerCenter check on ButtonComponentViewModel.

An internal presentCustomerCenter(isPresented:purchaseHandler:onDismiss:) overload wires the sheet through PresentingCustomerCenterModifier with the paywall’s existing purchaseHandler from the environment instead of PurchaseHandler.default(), so Customer Center shares the same handler instance when it is shown.

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

@JZDesign JZDesign requested review from a team as code owners May 20, 2026 23:39
@alexrepty alexrepty added the pr:fix A bug fix label Jun 3, 2026
@alexrepty alexrepty force-pushed the jzdesign/correct-initialization-churn-from-customer-center branch from 1318ff7 to d0d43b2 Compare June 3, 2026 18:21
@facumenzella

facumenzella commented Jun 4, 2026

Copy link
Copy Markdown
Member

Nice catch! I think we introduced this when we added Paywall support for CustomerCenter.

I suspect the root cause is that PresentingCustomerCenterModifier holds the handler as a @StateObject. However, inside the paywall the handler is already owned and injected by PaywallsV2View (.environmentObject(self.purchaseHandler)), and ButtonComponentView reads it as @EnvironmentObject. So having the modifier own a second one is what lead to the bug in my opinion.

If the paywall path instead observed the existing handler rather than owning it, the binding alone would be enough. The A small paywall-internal modifier could do:

  private struct PaywallCustomerCenterSheetModifier: ViewModifier {
      @EnvironmentObject private var purchaseHandler: PurchaseHandler
      @Binding var isPresented: Bool
      let onDismiss: (() -> Void)?

      func body(content: Content) -> some View {
          content.sheet(isPresented: $isPresented, onDismiss: onDismiss) {
              CustomerCenterView()
                  ....
          }
      }
  }

Totally fine to ship as-is and tackle the ownership cleanup separately.

@alexrepty

Copy link
Copy Markdown
Contributor

Did some quick sanity check testing

  • Customer center still shows up as it should
  • No evidence of extraneous PurchaseHandler initializations during redraws observed

@alexrepty

alexrepty commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Nice catch! I think we introduced this when we added Paywall support for CustomerCenter.

I suspect the root cause is that PresentingCustomerCenterModifier holds the handler as a @StateObject. However, inside the paywall the handler is already owned and injected by PaywallsV2View (.environmentObject(self.purchaseHandler)), and ButtonComponentView reads it as @EnvironmentObject. So having the modifier own a second one is what lead to the bug in my opinion.

If the paywall path instead observed the existing handler rather than owning it, the binding alone would be enough. The A small paywall-internal modifier could do:

  private struct PaywallCustomerCenterSheetModifier: ViewModifier {
      @EnvironmentObject private var purchaseHandler: PurchaseHandler
      @Binding var isPresented: Bool
      let onDismiss: (() -> Void)?

      func body(content: Content) -> some View {
          content.sheet(isPresented: $isPresented, onDismiss: onDismiss) {
              CustomerCenterView()
                  ....
          }
      }
  }

Totally fine to ship as-is and tackle the ownership cleanup separately.

Created PWENG-101 for this change, so we can unblock this PR and merge.

@alexrepty alexrepty merged commit e37680d into main Jun 4, 2026
43 checks passed
@alexrepty alexrepty deleted the jzdesign/correct-initialization-churn-from-customer-center branch June 4, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants