Skip to content

Conversation

joshdholtz
Copy link
Member

Summary

  • Add new offerCode destination type to PaywallButtonComponent with proper encoding/decoding
  • Implement navigation handling in ButtonComponentView to present the system offer code redemption sheet
  • Add cache invalidation after offer code redemption to ensure customer info is refreshed

Test plan

  • Verify paywall buttons can be configured with offer_code destination in JSON
  • Test that tapping a button with offer code destination presents the system redemption sheet
  • Confirm customer info cache is invalidated after offer code redemption
  • Ensure existing button functionality remains unaffected

🤖 Generated with Claude Code

- Add offerCode destination type to PaywallButtonComponent
- Implement offer code redemption sheet handling in ButtonComponentView
- Add offerCodeRedemptionSheet navigation destination to ButtonComponentViewModel
- Invalidate customer info cache after offer code redemption

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

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

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

Just a question but looking great!!

}
}

private func openCodeREDemptionSheet() {
Purchases.shared.presentCodeRedemptionSheet()
Copy link
Contributor

Choose a reason for hiding this comment

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

Not part of this PR, since I see we were already calling Purchases.shared when invalidating the customer info when opening a web paywall link... But I do think these methods should ideally belong to the view model, not to the view.

Copy link
Member

Choose a reason for hiding this comment

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

Besides what Toni says, we should probably avoid calling Purchases.shared directly and go through the existing PurchaseHandler instance instead (the person working on the RC app will later thank us for this 😄).

Copy link
Member

Choose a reason for hiding this comment

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

As talked with Josh privately, removing the Purchases.shared here is more intricate than it looks. So it'll be tackled in a future PR (thank you Swift protocols for not allowing @Availability checks in your methods)

await self.updateCustomerInfo()
}
.onChange(of: scenePhase) { newPhase in
// Used when Offer Code Redemption sheet dismisses
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I'm wrong but would this be called every time the app becomes active even if it didn't go through the offer code redemption sheet, but something else? I guess it's not a huge deal... but we should be careful about invalidating customer info too often IMO, since it could lead to a user temporarily losing entitlements momentarily. If you can confirm this would only happen on offer code redemption sheet dismissal, I'm ok with this :)

Copy link
Member

Choose a reason for hiding this comment

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

Good point from Toni here. The code currently only checks when the scenePhase passes to active. Perhaps we could restrict this to make it closer to the "when Offer Code Redemption sheet dismisses" event by using https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-4psgg and only do updateCustomerInfo() if newPhase == .active && oldPhase == .inactive (this way we avoid calling it when coming from the .background)

Copy link
Member Author

Choose a reason for hiding this comment

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

Screenshot 2025-07-31 at 2 45 57 PM

🫠

Copy link
Member Author

Choose a reason for hiding this comment

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

GOT IT

Copy link
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

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

I have a couple of more comments, building on Toni's

}
}

private func openCodeREDemptionSheet() {
Purchases.shared.presentCodeRedemptionSheet()
Copy link
Member

Choose a reason for hiding this comment

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

Besides what Toni says, we should probably avoid calling Purchases.shared directly and go through the existing PurchaseHandler instance instead (the person working on the RC app will later thank us for this 😄).

await self.updateCustomerInfo()
}
.onChange(of: scenePhase) { newPhase in
// Used when Offer Code Redemption sheet dismisses
Copy link
Member

Choose a reason for hiding this comment

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

Good point from Toni here. The code currently only checks when the scenePhase passes to active. Perhaps we could restrict this to make it closer to the "when Offer Code Redemption sheet dismisses" event by using https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-4psgg and only do updateCustomerInfo() if newPhase == .active && oldPhase == .inactive (this way we avoid calling it when coming from the .background)

Copy link

emerge-tools bot commented Aug 1, 2025

📸 Snapshot Test

1 modified, 704 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
RevenueCat
com.revenuecat.PaywallsTester.mac-catalyst-scaled-to-match-ipad
0 0 0 0 235 0 N/A
RevenueCat
com.revenuecat.PaywallsTester
0 0 0 0 235 0 N/A
RevenueCat
com.revenuecat.PaywallsTester.mac-catalyst-optimized-for-mac
0 0 1 0 234 0 ✅ Approved

🛸 Powered by Emerge Tools

Copy link
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

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

Let's goo! :shipit:
(just one small nit)

Comment on lines 74 to 78
if old == new {
action(.new(new))
} else {
action(.changed(old: old, new: new))
}
Copy link
Member

Choose a reason for hiding this comment

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

Total nit, and I understand that it could be misleading to say changed when old == new, but that would be the fault of the system API calling onChange when the value didn't change. This way, we don't lose the oldValue information (and we wouldn't be getting the Customer Info below if both old and new were .active

Suggested change
if old == new {
action(.new(new))
} else {
action(.changed(old: old, new: new))
}
action(.changed(old: old, new: new))

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh yeah, good point! Will change this 👍

}
}

private func openCodeREDemptionSheet() {
Purchases.shared.presentCodeRedemptionSheet()
Copy link
Member

Choose a reason for hiding this comment

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

As talked with Josh privately, removing the Purchases.shared here is more intricate than it looks. So it'll be tackled in a future PR (thank you Swift protocols for not allowing @Availability checks in your methods)

@joshdholtz joshdholtz merged commit 994bee3 into main Aug 1, 2025
12 checks passed
@joshdholtz joshdholtz deleted the paywall-button-offer-code-redemption branch August 1, 2025 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants