Skip to content

Paywalls: add PaywallEvent model#3156

Merged
NachoSoto merged 3 commits into
paywallsfrom
paywalls-events-model
Sep 7, 2023
Merged

Paywalls: add PaywallEvent model#3156
NachoSoto merged 3 commits into
paywallsfrom
paywalls-events-model

Conversation

@NachoSoto

Copy link
Copy Markdown
Contributor

This will represent events created by RevenueCatUI.

@NachoSoto NachoSoto requested a review from a team September 6, 2023 23:16
@NachoSoto NachoSoto force-pushed the paywalls-events-model branch from ce13ed3 to 0ed0ba4 Compare September 6, 2023 23:17
@NachoSoto NachoSoto changed the base branch from paywalls to paywalls-revision September 6, 2023 23:26
@NachoSoto NachoSoto force-pushed the paywalls-events-model branch 2 times, most recently from cbd7c6f to c12d84b Compare September 6, 2023 23:31
@NachoSoto NachoSoto force-pushed the paywalls-events-model branch from c12d84b to 5b5ffd8 Compare September 6, 2023 23:35
@NachoSoto NachoSoto force-pushed the paywalls-events-model branch 2 times, most recently from 8e5d700 to 26a3d77 Compare September 7, 2023 00:31

@joshdholtz joshdholtz left a comment

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.

💪 🚀

Base automatically changed from paywalls-revision to paywalls September 7, 2023 18:48
@NachoSoto NachoSoto force-pushed the paywalls-events-model branch from 26a3d77 to c0300f9 Compare September 7, 2023 18:51
This will represent events created by `RevenueCatUI`.
@NachoSoto NachoSoto force-pushed the paywalls-events-model branch from c0300f9 to 446fd68 Compare September 7, 2023 18:53
@NachoSoto NachoSoto enabled auto-merge (squash) September 7, 2023 20:01
@NachoSoto NachoSoto disabled auto-merge September 7, 2023 20:02
@NachoSoto NachoSoto merged commit 4c32567 into paywalls Sep 7, 2023
@NachoSoto NachoSoto deleted the paywalls-events-model branch September 7, 2023 20:02
NachoSoto added a commit that referenced this pull request Sep 7, 2023
This will represent events created by `RevenueCatUI`.

@aboedo aboedo left a comment

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.

late to the game but looks great

NachoSoto added a commit that referenced this pull request Sep 7, 2023
This will represent events created by `RevenueCatUI`.
NachoSoto added a commit that referenced this pull request Sep 7, 2023
This will represent events created by `RevenueCatUI`.
NachoSoto added a commit that referenced this pull request Sep 8, 2023
This will represent events created by `RevenueCatUI`.
NachoSoto added a commit that referenced this pull request Sep 14, 2023
This will represent events created by `RevenueCatUI`.
NachoSoto added a commit that referenced this pull request Sep 14, 2023
This will represent events created by `RevenueCatUI`.
NachoSoto added a commit that referenced this pull request Sep 15, 2023
This will represent events created by `RevenueCatUI`.
NachoSoto added a commit that referenced this pull request Sep 15, 2023
**This is an automatic release.**
### New Features
#### ✨ Introducing RevenueCatUI 📱

RevenueCat's Paywalls allow you to to remotely configure your entire
paywall view without any code changes or app updates.
Our paywall templates use native code to deliver smooth, intuitive
experiences to your customers when you’re ready to deliver them an
Offering; and you can use our Dashboard to pick the right template and
configuration to meet your needs.

To use RevenueCat Paywalls on iOS, simply:

1. Create a Paywall on the Dashboard for the `Offering` you intend to
serve to your customers
2. Add the `RevenueCatUI` SPM dependency to your project
3. `import RevenueCatUI` at the point in the user experience when you
want to display a paywall:

```swift
import RevenueCatUI
import SwiftUI

struct YourApp: View {

    var body: some View {
        YourContent()
            .presentPaywallIfNeeded(
                requiredEntitlementIdentifier: "pro",
                purchaseCompleted: { customerInfo in
                    print("Purchase completed: \(customerInfo)")
                },
                restoreCompleted: { customerInfo in
                    print("Purchases restored: \(customerInfo)")
                }
            )
    }

}
```

You can find more information in [our
documentation](https://rev.cat/paywalls).

<details>

<summary>List of changes</summary>
* NachoSoto: `Paywalls`: renamed `PaywallEvent.view` to `.impression`
(#3212)
* NachoSoto: `Paywalls`: loading indicator for in-progress purchases
(#3217)
*  NachoSoto: `Paywalls`: fixed template 4 bottom padding (#3211)
* NachoSoto: `Paywalls`: only pre-warm images/intro-eligibility for
`Offerings.current` (#3210)
* NachoSoto: `Paywalls`: fixed mock intro eligibility on snapshot tests
(#3205)
*  NachoSoto: `Paywalls`: fixed SimpleApp release build (#3203)
*  NachoSoto: `Paywalls`: improved `DebugErrorView` layout (#3204)
* NachoSoto: `Paywalls`: refactored `PurchaseHandler` extracting
protocol (#3196)
*  NachoSoto: `Paywalls`: automatically flush events (#3177)
* NachoSoto: `Paywalls`: fixed `TemplateBackgroundImageView` aspect
ratio (#3201)
*  NachoSoto: `Paywalls`: fixed broken layout on template 4 (#3202)
*  NachoSoto: `Paywalls`: events unit and integration tests (#3169)
*  NachoSoto: `Paywalls`: send events to `Purchases` (#3164)
*  NachoSoto: `Paywalls`: convert empty images into `nil` (#3195)
*  NachoSoto: `Paywalls`: new `onRestoreCompleted` handler (#3190)
* NachoSoto: `Paywalls`: fixed `IntroEligibilityViewModel` data lifetime
(#3194)
* NachoSoto: `Paywalls`: test plan for running non-snapshot tests
(#3188)
*  NachoSoto: `Paywalls`: polish template 4 (#3183)
* NachoSoto: `Paywalls`: fixed data flow resulting in multiple
`PurchaseHandler` instances (#3187)
* Cesar de la Vega: `Paywalls`: update `blurred_background_image` key in
`PaywallData` test fixture (#3186)
*  NachoSoto: `Paywalls`: added `Purchases.track(paywallEvent:)` (#3160)
* NachoSoto: `Paywalls`: don't apply dark appearance with no dark mode
colors (#3184)
* NachoSoto: `Paywalls`: fixed template 2 + `.condensedFooter` + iPad
(#3185)
* NachoSoto: `Paywalls`: new `{{ sub_duration_in_months }}` variable
(#3173)
*  NachoSoto: `Paywalls`: created `PaywallEventsManager` (#3159)
* NachoSoto: `Paywalls`: implemented `PostPaywallEventsOperation`
(#3158)
* NachoSoto: `Paywalls`: new `{{ sub_relative_discount }}` variable
(#3131)
*  Charlie Chapman: `Paywalls`: improved `FooterView` (#3171)
* NachoSoto: `Paywalls`: fixed `FooterView` horizontal centering (#3172)
*  NachoSoto: `Paywalls`: created `PaywallEventStore` (#3157)
*  NachoSoto: `Paywalls`: add `PaywallEvent` model (#3156)
*  NachoSoto: `Paywalls`: add `PaywallData.revision` (#3155)
* NachoSoto: `Paywalls`: support fuzzy-Locale search in `iOS 15` (#3162)
* NachoSoto: `PaywallData`: added `@NonEmptyString` to `subtitle` and
`offerName` (#3150)
* NachoSoto: `Paywalls`: add paywall for Load Shedder integration tests
(#3151)
* NachoSoto: `Paywalls`: fixed error view being displayed on release
builds (#3141)
* NachoSoto: `Paywalls`: improved `{{ total_price_and_per_month }}` to
include period (#3136)
* NachoSoto: `Paywalls`: `{{ price_per_period }}` now takes
`SubscriptionPeriod.value` into account (#3133)
*  NachoSoto: `Paywalls`: add Arabic to SimpleApp for testing (#3132)
* NachoSoto: `Paywalls`: update snapshot generation with new separate
git repo (#3116)
*  NachoSoto: `Paywalls`: add support for CTA button gradients (#3121)
*  NachoSoto: `Paywalls`: template 5 (#3095)
* NachoSoto: `Paywalls`: replaced submodule with `gitignore`d reference
(#3125)
* NachoSoto: `Catalyst`: fixed a couple of Catalyst build warnings
(#3120)
* NachoSoto: `Paywalls`: reference test snapshots from submodule (#3115)
*  NachoSoto: `Paywalls`: removed `presentedPaywallViewMode` (#3109)
* NachoSoto: `Paywalls`: remove duplicate `RevenueCat` scheme to fix
Carthage (#3105)
*  NachoSoto: `Paywalls`: fixed iOS 12 build (#3104)
*  NachoSoto: `Paywalls`: fixed template 2 inconsistent spacing (#3091)
*  NachoSoto: `Paywalls`: improved test custom paywall (#3089)
*  NachoSoto: `Paywalls`: avoid warming up cache multiple times (#3068)
*  NachoSoto: `Paywalls`: added all localization (#3080)
* NachoSoto: `Paywalls`: temporarily disable `PaywallTemplate.template4`
(#3088)
*  NachoSoto: `Paywalls`: enabled `Catalyst` support (#3087)
*  NachoSoto: `Paywalls`: iPad polish (#3061)
*  NachoSoto: `Paywalls`: added MIT license to all headers (#3084)
* NachoSoto: `Paywalls`: improved unselected package background color
(#3079)
*  NachoSoto: `Paywalls`: handle already purchased state (#3046)
* NachoSoto: `Paywalls`: only dismiss `PaywallView` when explicitly
presenting it with `.presentPaywallIfNeeded` (#3075)
* NachoSoto: `Paywalls`: add support for generating snapshots on CI
(#3055)
* NachoSoto: `Paywalls`: removed unnecessary `PaywallFooterView` (#3064)
* Josh Holtz: `Paywalls`: new `PaywallFooterView` to replace `modes`
(#3051)
*  Josh Holtz: `Paywalls`: rename card to footer (#3049)
* NachoSoto: `Paywalls`: changed `total_price_and_per_month` to include
period (#3044)
* NachoSoto: `Paywalls`: internal documentation for implementing
templates (#3053)
*  NachoSoto: `Paywalls`: finished `iOS 15` support (#3043)
* NachoSoto: `Paywalls`: validate `PaywallData` to ensure displayed data
is always correct (#3019)
* NachoSoto: `Paywalls`: fixed `total_price_and_per_month` for custom
monthly packages (#3027)
*  NachoSoto: `Paywalls`: tweaking colors on template 2&3 (#3011)
*  NachoSoto: `Paywalls`: changed snapshots to scale 1 (#3016)
* NachoSoto: `Paywalls`: replaced `defaultLocale` with
`preferredLocales` (#3003)
* NachoSoto: `Paywalls`: improved `PaywallDisplayMode.condensedCard`
layout (#3001)
*  NachoSoto: `Paywalls`: `.card` and `.condensedCard` modes (#2995)
*  NachoSoto: `Paywalls`: prevent multiple concurrent purchases (#2991)
*  NachoSoto: `Paywalls`: improved variable warning (#2984)
*  NachoSoto: `Paywalls`: fixed horizontal padding on template 1 (#2987)
* NachoSoto: `Paywalls`: changed `FooterView` to always use `text1`
color (#2992)
*  NachoSoto: `Paywalls`: retry test failures (#2985)
* NachoSoto: `Paywalls`: send presented `PaywallViewMode` with purchases
(#2859)
*  NachoSoto: `Paywalls`: added support for custom fonts (#2988)
* NachoSoto: `Paywalls`: improved template 2 unselected packages (#2982)
* Josh Holtz: `Paywalls`: fix template 2 selected text offer details
color (#2975)
*  NachoSoto: `Paywalls`: warm-up image cache (#2978)
*  NachoSoto: `Paywalls`: extracted `PaywallCacheWarming` (#2977)
*  NachoSoto: `Paywalls`: fixed color in template 3 (#2980)
*  NachoSoto: `Paywalls`: improved default template (#2973)
*  NachoSoto: `Paywalls`: added links to documentation (#2974)
*  NachoSoto: `Paywalls`: updated template names (#2971)
*  NachoSoto: `Paywalls`: updated variable names (#2970)
* NachoSoto: `Paywalls`: added JSON debug screen to
`debugRevenueCatOverlay` (#2972)
*  NachoSoto: `Paywalls`: multi-package horizontal template  (#2949)
*  NachoSoto: `Paywalls`: fixed template 3 icon aspect ratio (#2969)
*  NachoSoto: `Paywalls`: iOS 17 tests on CI (#2955)
*  NachoSoto: `Paywalls`: deploy `debug` sample app (#2966)
*  NachoSoto: `Paywalls`: sort offerings list in sample app (#2965)
*  NachoSoto: `Paywalls`: initial iOS 15 support (#2933)
* NachoSoto: `Paywalls`: changed default `PaywallData` to display
available packages (#2964)
*  NachoSoto: `Paywalls`: changed `offerDetails` to be optional (#2963)
*  NachoSoto: `Paywalls`: markdown support (#2961)
*  NachoSoto: `Paywalls`: updated icon set to match frontend (#2962)
*  NachoSoto: `Paywalls`: added support for `PackageType.custom` (#2959)
* NachoSoto: `Paywalls`: fixed `tvOS` compilation by making it
explicitly unavailable (#2956)
* NachoSoto: `Paywalls`: fix crash when computing localization with
duplicate packages (#2958)
*  NachoSoto: `Paywalls`: UIKit `PaywallViewController` (#2934)
* NachoSoto: `Paywalls`: `presentPaywallIfNecessary` ->
`presentPaywallIfNeeded` (#2953)
* NachoSoto: `Paywalls`: added support for custom and lifetime products
(#2941)
* NachoSoto: `Paywalls`: changed `SamplePaywallsList` to work offline
(#2937)
* NachoSoto: `Paywalls`: fixed header image mask on first template
(#2936)
*  NachoSoto: `Paywalls`: new `subscription_duration` variable (#2942)
* NachoSoto: `Paywalls`: removed `mode` parameter from
`presentPaywallIfNecessary` (#2940)
*  NachoSoto: `Paywalls`: improved `RemoteImage` error layout (#2939)
* NachoSoto: `Paywalls`: added default close button when using
`presentPaywallIfNecessary` (#2935)
* NachoSoto: `Paywalls`: added ability to preview templates in a
`.sheet` (#2938)
*  NachoSoto: `Paywalls`: avoid recomputing variable `Regex` (#2944)
*  NachoSoto: `Paywalls`: improved `FooterView` scaling (#2948)
* NachoSoto: `Paywalls`: added ability to calculate and localize
subscription discounts (#2943)
*  NachoSoto: `Offering`: improved description (#2912)
*  NachoSoto: `Paywalls`: fixed `FooterView` color in template 1 (#2951)
*  NachoSoto: `Paywalls`: fixed `View.scrollableIfNecessary` (#2947)
* NachoSoto: `Paywalls`: improved `IntroEligibilityStateView` to avoid
layout changes (#2946)
* NachoSoto: `Paywalls`: updated offerings snapshot with new asset base
URL (#2950)
* NachoSoto: `Paywalls`: extracted `TemplateBackgroundImageView` (#2945)
*  NachoSoto: `Paywalls`: more polish from design feedback (#2932)
*  NachoSoto: `Paywalls`: more unit tests for purchasing state (#2931)
*  NachoSoto: `Paywalls`: new `.onPurchaseCompleted` modifier (#2930)
* NachoSoto: `Paywalls`: fixed `LoadingPaywallView` displaying a
progress view (#2929)
* NachoSoto: `Paywalls`: added default template to `SamplePaywallsList`
(#2928)
*  NachoSoto: `Paywalls`: added a few more logs (#2927)
*  NachoSoto: `Paywalls` added individual previews for templates (#2924)
*  NachoSoto: `Paywalls`: improved default paywall configuration (#2926)
* NachoSoto: `Paywalls`: moved purchasing state to `PurchaseHandler`
(#2923)
*  NachoSoto: `Paywalls`: updated Integration Test snapshot (#2921)
* NachoSoto: `Paywalls`: pre-warm intro eligibility in background thread
(#2925)
*  NachoSoto: `Paywalls`: removed "couldn't find package" log (#2922)
* NachoSoto: `Paywalls`: SimpleApp reads API key from Xcode Cloud
environment (#2919)
* NachoSoto: `Paywalls`: improved template accessibility support (#2920)
* NachoSoto: `Paywalls`: work around SwiftUI bug to allow embedding
`PaywallView` inside `NavigationStack` (#2918)
*  NachoSoto: `Paywalls`: some basic polish from design feedback (#2917)
* NachoSoto: `Paywalls`: added `OfferingsList` to preview all paywalls
(#2916)
* NachoSoto: `Paywalls`: fixed tappable area for a couple of buttons
(#2915)
*  NachoSoto: `Paywalls`: new `text1` and `text2` colors (#2903)
* NachoSoto: `Paywalls`: updated multi-package bold template design
(#2908)
*  NachoSoto: `Paywalls`: added sample paywalls to `SimpleApp` (#2907)
*  NachoSoto: `Paywalls`: one package with features template (#2902)
*  NachoSoto: `Paywalls`: initial support for icons (#2882)
* NachoSoto: `Paywalls`: extracted intro eligibility out of templates
(#2901)
*  NachoSoto: `Paywalls`: changed `subtitle` to be optional (#2900)
* NachoSoto: `Paywalls`: added "features" to `LocalizedConfiguration`
(#2899)
* NachoSoto: `Paywalls`: fixed `{{ total_price_and_per_month }}` (#2881)
*  NachoSoto: `Paywalls`: updated template names (#2878)
*  NachoSoto: `Paywalls`: added accent colors (#2883)
* NachoSoto: `Paywalls`: changed images representation to an object
(#2875)
*  NachoSoto: `Paywalls`: added `offerName` parameter (#2877)
*  NachoSoto: `Paywalls`: new `{{ period }}` variable (#2876)
*  NachoSoto: `Paywalls`: disabled `PaywallViewMode`s for now (#2874)
* NachoSoto: `Paywalls`: added new `defaultPackage` configuration
(#2871)
*  NachoSoto: `Paywalls`: fixed tests on CI (#2872)
* NachoSoto: `Paywalls`: pre-fetch intro eligibility for paywalls
(#2860)
*  Andy Boedo: `Paywalls`: clean up the error view (#2873)
* NachoSoto: `Paywalls`: new API for easily displaying `PaywallView`
with just one line (#2869)
*  NachoSoto: `Paywalls`: handle missing paywalls gracefully (#2855)
* NachoSoto: `Paywalls`: temporarily disable non-fullscreen
`PaywallView`s (#2868)
* NachoSoto: `Paywalls`: added test to ensure package selection
maintains order (#2853)
* NachoSoto: `Paywalls`: added new `blurredBackgroundImage`
configuration (#2852)
*  NachoSoto: `Paywalls`: fuzzy `Locale` lookups (#2847)
*  NachoSoto: `Paywalls`: basic localization support (#2851)
*  NachoSoto: `Paywalls`: added `FooterView` (#2850)
*  NachoSoto: `Paywalls`: multi-package template (#2840)
*  NachoSoto: `Paywalls`: disable animations during unit tests (#2848)
* NachoSoto: `Paywalls`: `TrialOrIntroEligibilityChecker.eligibility(for
packages:)` (#2846)
* NachoSoto: `Paywalls`: added new `total_price_and_per_month` variable
(#2845)
*  NachoSoto: `Paywalls`: extracted `PurchaseButton` (#2839)
*  NachoSoto: `Paywalls`: extracted `IntroEligibilityStateView` (#2837)
* NachoSoto: `Paywalls`: support for multiple `PaywallViewMode`s (#2834)
* NachoSoto: `Paywalls`: add support for multiple images in template
configuration (#2832)
* NachoSoto: `Paywalls`: extracted configuration processing into a new
`TemplateViewConfiguration` (#2830)
* NachoSoto: `Paywalls`: improved support for dynamic type with
snapshots (#2827)
* NachoSoto: `Paywalls`: disable `macOS`/`macCatalyst`/`watchOS` for now
(#2821)
* NachoSoto: `Paywalls`: using new color information in template (#2823)
*  NachoSoto: `Paywalls`: set up CI tests and API Tester (#2816)
*  NachoSoto: `Paywalls`: added support for decoding colors (#2822)
* NachoSoto: `Paywalls`: ignore empty strings in
`LocalizedConfiguration` (#2818)
*  NachoSoto: `Paywalls`: updated `PaywallData` field names (#2817)
*  NachoSoto: `Paywalls`: added support for purchasing (#2812)
* NachoSoto: `Paywalls`: added tests for `PackageType` filtering (#2810)
* Andy Boedo: `Paywalls`: changed variable handling to use Swift `Regex`
(#2811)
*  NachoSoto: `Paywalls`: added `price` variable (#2809)
*  NachoSoto: `Paywalls`: determine intro eligibility (#2808)
*  NachoSoto: `Paywalls`: added header image to configuration (#2800)
*  NachoSoto: `Paywalls`: added `packages` to configuration (#2798)
* NachoSoto: `Paywalls`: add support for displaying
`StoreProductDiscount`s (#2796)
*  NachoSoto: `Paywalls`: added support for variables (#2793)
* NachoSoto: `Paywalls`: using `PaywallData` and setting up basic
template loading (#2781)
*  NachoSoto: `Paywalls`: initial configuration types (#2780)
*  NachoSoto: `Paywalls`: initial `RevenueCatUI` target setup (#2776)

</details>

### Other Changes

* `Debug`: add `Offering` metadata to debug screen (#3137) via NachoSoto
(@NachoSoto)
* `TestStoreProduct`: new `locale` parameter (#3134) via NachoSoto
(@NachoSoto)
* `Integration Tests`: fixed more flaky failures (#3218) via NachoSoto
(@NachoSoto)

---------

Co-authored-by: NachoSoto <ignaciosoto90@gmail.com>
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
facumenzella added a commit that referenced this pull request Feb 27, 2026
Closes test coverage gap identified from Android PR #3156. Adds 15 new
test cases covering intro/promo offer negative cases, type safety,
override precedence, and cross-offer independence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
facumenzella added a commit that referenced this pull request Mar 6, 2026
* [Conditional Configurability][1] Add new condition types for conditional configurability

Add ExtendedCondition enum as @_spi(Internal) to support new condition types
while keeping the public Condition API stable. This includes:

- ExtendedCondition enum with associated values for variableCondition,
  selectedPackageCondition, extended introOffer/promoOffer conditions
- EqualityOperator and ArrayOperator enums for condition evaluation
- ConditionValue enum for type-safe variable values
- ComponentOverride now stores ExtendedCondition internally, exposes
  public Condition via computed property
- Updated PresentedPartials to use ExtendedCondition
- Comprehensive deserialization tests

The public Condition enum remains unchanged as String raw-value to
maintain API stability. New condition types map to .unsupported in
the public API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix CI: Add @_spi(Internal) import to PresentedPartialsTests

The existing test file needs SPI access to use ExtendedCondition
cases after the restructuring for conditional configurability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add missing deserialization error handling tests

Match Android test coverage for edge cases:
- Unknown operator falls back to unsupported
- Wrong field types fall back to unsupported
- Empty JSON object falls back to unsupported
- JSON without type field falls back to unsupported

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix ExtendedCondition decoder to handle missing type field gracefully

The decoder now wraps the entire decoding logic in a do-catch block,
so that empty JSON objects or JSON without a "type" field fall back
to `.unsupported` instead of throwing a keyNotFound error.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* same line

* Add clarifying comment about Condition's unused Codable implementation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add ConditionDeserializationTests.swift to Xcode project

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Simplify ExtendedCondition by removing redundant legacy cases

Normalize legacy intro_offer/promo_offer conditions to their extended form
(operator: .equals, value: true) during deserialization instead of having
separate cases for legacy and extended forms.

This simplifies the enum and evaluation logic while maintaining full
backwards compatibility with legacy JSON.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove redundant Condition suffix from ExtendedCondition cases

Rename for consistency with other cases (.compact, .medium, etc.):
- introOfferCondition → introOffer
- promoOfferCondition → promoOffer
- variableCondition → variable
- selectedPackageCondition → selectedPackage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [Conditional Configurability][2] Implement `selected_package` condition evaluation (#6286)

* [Conditional Configurability][2] Add ConditionContext and evaluation logic

- Add ConditionContext struct for passing evaluation context
- Add buildPartial overload that accepts ConditionContext
- Add shouldApply overload with full condition evaluation
- Add evaluateCondition for all condition types
- Add helper methods for variable, package, and boolean condition evaluation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix availability annotations and CustomVariableValue usage

- Add @available(iOS 15.0, ...) to ConditionContext and related methods
- Fix matchesValue to use CustomVariableValue getters (.stringValue,
  .boolValue, .doubleValue) instead of pattern matching on private enum

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Simplify shouldApply by delegating to iOS 15+ version

The else branch is dead code since Paywalls is iOS 15+ anyway.
This consolidates the condition evaluation logic in one function.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* [Conditional Configurability][3] Implement variable and extended intro/promo condition evaluation (#6287)

* [Conditional Configurability][3] Add condition evaluation tests

Add comprehensive tests for the condition evaluation logic:
- Selected package condition tests (in/not in operators)
- Variable condition tests (string, int, not found)
- Extended intro offer condition tests (equals/not equals)
- Multiple conditions (AND logic) tests
- Unsupported condition tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add missing condition evaluation tests and fix availability

- Add promo offer condition tests with operators
- Add variable boolean, double, and type mismatch tests
- Add intro offer negative test case
- Fix @available annotations for ConditionContext and related methods
- Fix matchesValue to use CustomVariableValue getters

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix type-strict variable condition evaluation and remove unrelated files

- Add isString, isNumber, isBool type checking methods to CustomVariableValue
- Update matchesValue to perform type-strict comparison (string != int, etc.)
- Remove accidentally committed PR_6285_review.md and package-lock.json
- Revert unrelated Package.resolved changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* [Conditional Configurability][4] Validate unsupported conditions in toPresentedOverrides (#6288)

* [Conditional Configurability][3] Add condition evaluation tests

Add comprehensive tests for the condition evaluation logic:
- Selected package condition tests (in/not in operators)
- Variable condition tests (string, int, not found)
- Extended intro offer condition tests (equals/not equals)
- Multiple conditions (AND logic) tests
- Unsupported condition tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add missing condition evaluation tests and fix availability

- Add promo offer condition tests with operators
- Add variable boolean, double, and type mismatch tests
- Add intro offer negative test case
- Fix @available annotations for ConditionContext and related methods
- Fix matchesValue to use CustomVariableValue getters

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix type-strict variable condition evaluation and remove unrelated files

- Add isString, isNumber, isBool type checking methods to CustomVariableValue
- Update matchesValue to perform type-strict comparison (string != int, etc.)
- Remove accidentally committed PR_6285_review.md and package-lock.json
- Revert unrelated Package.resolved changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [Conditional Configurability][4] Add unsupported condition detection

- Add `PaywallError.unsupportedCondition` case for future fallback support
- Add `containsUnsupportedConditions()` method to detect unsupported conditions
- Add logging string for unsupported condition warning
- Add comprehensive tests for unsupported condition detection

Note: The actual fallback mechanism (throwing and catching at paywall level)
will be implemented in a follow-up PR to avoid cascading changes to all
component ViewModels.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Make CustomVariableValue.number and .bool public

Allow developers to pass numeric and boolean custom variables to paywalls,
enabling condition evaluation based on these types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update API tester to use public .number() and .bool() methods

The CustomVariableValue factory methods .number() and .bool() are now
public. Update the API tester to use these methods directly instead of
using .string() for all values.

Existing test coverage in VariableHandlerV2Tests.swift already covers
these factory methods comprehensively.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Integrate unsupported condition validation into toPresentedOverrides

This matches Android's approach where toPresentedOverrides checks for
unsupported conditions and throws PaywallError.unsupportedCondition,
triggering fallback to the default paywall.

Changes:
- toPresentedOverrides now throws when unsupported conditions are found
- Updated all view model initializers to be throwing
- Updated ViewModelFactory to use try when instantiating view models
- Used try? with fallback for video backgrounds in SwiftUI contexts
- Updated preview code with try!/force_try

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add tests for toPresentedOverrides throwing on unsupported conditions

Add test coverage verifying that toPresentedOverrides throws
PaywallError.unsupportedCondition when any override contains an
unsupported condition type.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [Conditional Configurability][5] PR comments and fallback paywall (#6319)

* Integrate unsupported condition validation into toPresentedOverrides

This matches Android's approach where toPresentedOverrides checks for
unsupported conditions and throws PaywallError.unsupportedCondition,
triggering fallback to the default paywall.

Changes:
- toPresentedOverrides now throws when unsupported conditions are found
- Updated all view model initializers to be throwing
- Updated ViewModelFactory to use try when instantiating view models
- Used try? with fallback for video backgrounds in SwiftUI contexts
- Updated preview code with try!/force_try

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* suggestions

^ Conflicts:
^	RevenueCatUI/Data/CustomPaywallVariables.swift
^	RevenueCatUI/Templates/V2/ViewModelHelpers/PresentedPartials.swift

* internal bool and number

* fallback paywall

* Fix compilation error and SwiftLint violations

- Fix call to renamed method: containsUnsupportedConditions -> hasUnsupportedCondition
- Rename variable 'c' to 'component' to satisfy identifier_name rule
- Add swiftlint:disable for cyclomatic_complexity

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove accidental .package.resolved file

* Remove extra blank lines

---------

Co-authored-by: Facundo Menzella <facumenzella@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* Add non-throwing factory for video backgrounds

- Add VideoComponentViewModel.forBackground() for video backgrounds
- Video backgrounds don't have overrides, so no validation needed
- Remove try? fallback pattern in DisplayableColor.swift
- Consistent with other components: early validation, non-throwing where safe

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cesar de la Vega <664544+vegaro@users.noreply.github.com>

* [bug-bash] Fix selectedPackage conditional visibility not working

The selectedPackage condition was never evaluated correctly because
components were calling buildPartial without passing ConditionContext.
This meant selectedPackageId was always nil during condition evaluation.

Updated all component ViewModels to:
- Accept selectedPackageId parameter in their styles() method
- Create ConditionContext with the selected package ID
- Pass conditionContext to buildPartial()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [bug-bash] Add warning log for unrecognized paywall condition types

Developers will now see a warning when the SDK encounters a condition type
it doesn't recognize, helping them identify when they need to update the SDK.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [bug-bash] Add support for multiple_intro_offers condition type

This condition is supported in Android. On iOS, it always evaluates to false
since iOS does not support multiple intro offers. Adding this prevents the
paywall from falling back to the default when this condition is present.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [bug-bash] Pass custom variables to ConditionContext for variable conditions

Custom variable conditions were always evaluating to false because
the customVariables dictionary was empty. Components now get the
customPaywallVariables from the SwiftUI environment and pass them
to their ViewModel's styles() method, which forwards them to
ConditionContext for proper condition evaluation.

Updated components: Stack, Image, Icon, Video, Timeline, Carousel.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [bug-bash] Make number() and bool() public for CustomVariableValue

Developers need to be able to create numeric and boolean custom
variable values, not just strings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix tabs override condition conversion to preserve extended conditions (#6361)

* Add missing condition evaluation unit tests for PresentedPartials

Closes test coverage gap identified from Android PR #3156. Adds 15 new
test cases covering intro/promo offer negative cases, type safety,
override precedence, and cross-offer independence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add multiple_intro_offers compatibility guardrail tests for iOS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* add bool and number support in paywalls tester (#6394)

* Split intro/promo offer condition JSON keys for backward compatibility with older SDKs (#6397)

Splits legacy boolean conditions (introOffer/promoOffer) from operator-based ones
(introOfferCondition/promoOfferCondition) and updates JSON keys so old SDKs safely
ignore new condition types. Also renames variable → variable_condition and
selected_package → selected_package_condition.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Review fixes: remove multipleIntroOffers from public API, restore private storage, epsilon double comparison

- Remove `multipleIntroOffers` from public `Condition` enum to avoid source-breaking
  change on @Frozen enum. It remains in `ExtendedCondition` and maps to `.unsupported`.
- Restore `Storage` and `storage` to `private` on `CustomVariableValue` — condition
  evaluation uses the existing `isString`/`isNumber`/`isBool` helpers instead.
- Replace exact `==` double comparison with epsilon-based `doublesMatch` to handle
  floating point representation differences from JSON round-trips.
- Add tests for epsilon comparison edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused legacy buildPartial and shouldApply overloads

All callers use the new buildPartial with conditionContext parameter.
Remove the legacy overload (without conditionContext) and its shouldApply
wrapper that had an unnecessary #available gate returning false on pre-iOS 15.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix condition evaluation not using dashboard default custom variables

ConditionContext was only receiving developer-provided custom variables
from the SwiftUI environment, but not the dashboard-configured defaults.
This caused variable-based conditions (e.g. "something == false") to
always fail when the developer didn't explicitly provide the variable,
even if a default was set in the dashboard.

The fix merges defaultCustomVariables into ConditionContext at init time
(developer values take priority), and passes them from all 8 view model
sites that create ConditionContext.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Wire up conditional configurability for TabsComponent

TabsComponentViewModel had presentedOverrides stored but never evaluated.
Added a styles() method that creates a ConditionContext, calls buildPartial,
and returns a TabsComponentStyle with overrides applied. The view now
evaluates conditions and checks visibility before rendering.

Also updated TabsComponentStyle.init to accept Background? instead of
ColorScheme? to match the actual data model, and added a clarifying
comment to CountdownComponentViewModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for intro/promo offer conditions with wrong value types

Verify that conditions with non-boolean values (e.g., string instead of
bool) gracefully fall back to .unsupported, triggering the fallback paywall.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Disable autocapitalization on custom variables value TextField

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add bug bash unit tests for conditional configurability (#6398)

Add unit tests derived from the bug bash test plan to cover:
- Override precedence with visibility (two/three overrides, last-matching wins)
- Same condition evaluated independently on multiple components
- Condition + selected state interaction
- Variable match/mismatch for text replacement scenarios
- Intro offer with different eligibility states
- Different condition types on sibling components
- containsUnsupportedConditions for carousel, tabs, button, package, deep nesting

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Emerge snapshot previews for conditional configurability (#6400)

* Add Emerge snapshot previews for conditional configurability

Add SwiftUI previews that test basic show/hide behavior using
variable conditions on Text and Stack components. These will be
automatically picked up by Emerge's SnapshotPreviews for visual
regression testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Improve snapshot preview names for easier debugging

Make display names include the condition, operator, value, and expected
outcome so broken snapshots can be diagnosed at a glance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add ConditionalConfigurabilityPreview.swift to xcodeproj

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix notEquals preview: use matching variable value so condition triggers

The preview set tier="free" with a notEquals "free" condition, which
evaluates to false — the override never applied. Changed to tier="premium"
so the condition matches and the text is correctly hidden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Show default paywall instead of fallback for unsupported conditions (#6406)

* Show default paywall instead of fallback when unsupported conditions found

Per the spec, when the SDK encounters condition types it doesn't
recognize, it should render the same V2 paywall with only legacy
overrides applied — not fall back to a completely different V1 paywall.

When any unsupported condition is found, all rule-based overrides
(variable, selected_package, intro_offer_condition,
promo_offer_condition, and unsupported) are discarded. Only legacy
overrides (compact, medium, expanded, selected, introOffer,
promoOffer, multipleIntroOffers) are kept. This aligns with Android
PR #3175.

Changes:
- Add `isRule` property to ExtendedCondition to distinguish
  conditional configurability rules from legacy base conditions
- Update toPresentedOverrides to filter out all rule-based overrides
  when any unsupported condition is present
- Remove throw from PaywallsV2View (keep warning log)
- Remove PaywallError.unsupportedCondition (no longer thrown)
- Update log message to explain overrides will be ignored

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Propagate discardRules globally when any component has unsupported conditions

When any component in the paywall tree contains unsupported conditions,
all rule-based overrides are now discarded globally across every component,
not just per-component. This matches the spec requirement for rendering the
"default paywall" with only legacy overrides applied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Classify .unsupported as non-rule since it pre-dates conditional configurability

Only the extended conditions introduced by conditional configurability
(variable, selectedPackage, introOfferCondition, promoOfferCondition) are
classified as rules. .unsupported existed before coco as a fallback for
unrecognized condition types and always evaluates to false at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix button sheet detection, add cross-subtree tests, align log wording

- Fix containsUnsupportedConditions() to check button sheet destination
  stacks, not just the button's own stack
- Add cross-subtree integration tests: unsupported in button sheet and
  nested tabs triggers global discard on sibling components
- Update warning log to match Android: "Unsupported paywall rule
  encountered. Rendering paywall without conditional configurability rules."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove redundant local hasUnsupportedCondition check from toPresentedOverrides

The global discardRules flag already walks the entire component tree,
so the per-component hasUnsupportedCondition() fallback in
toPresentedOverrides was redundant. Simplify to only check discardRules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Recurse into timeline item sub-components for unsupported condition detection

TimelineComponent.containsUnsupportedConditions() was only checking
the timeline's own overrides and each item's direct overrides, but not
recursing into item.title, item.description, or item.icon. This meant
unsupported conditions in those sub-components wouldn't set the global
discardRules flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: drop unsupported overrides, deduplicate tree walk, add @_spi

- Filter also drops .unsupported overrides when discardRules is true
- Move warning log after factory call, read factory.discardRules
  instead of traversing the component tree twice
- Mark isRule as @_spi(Internal)
- Add tests: discardRules drops .unsupported and mixed legacy+rule overrides

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add malformed condition logging and fix missing test file in Xcode project

Log a warning when a known condition type fails to decode instead of
silently falling back to .unsupported. Also add ToPresentedOverridesTests.swift
to the Xcode project to fix the Danger bot missing-file warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix selected_package condition evaluating against wrong package inside package components (#6412)

PackageComponentView overrides PackageContext for its children (for variable
processing), which caused selected_package conditions to always evaluate against
the package component's own ID instead of the globally selected package.

Fix: introduce a separate @Environment(\.selectedPackageId) key set at the root
that is NOT overridden by PackageComponentView. All component views now read
selectedPackageId from this environment key for condition evaluation.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Rename condOp to conditionOperator in PresentedPartials

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add SelectedPackageId.swift to RevenueCat.xcodeproj

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add .number() and .bool() to CustomVariableValue API tester (#6415)

These were promoted from internal to public in #6285.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
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