* This mostly works
* Some cleanup and todos
* Fixed
* BROKEN but need to rebase
* The rework happened
* This is feeling pretty good
* This works
* Fix lint
* Oops
* Or this
* Removed size and implemented top and bottom pager
* Added a todo and compile error
* Made page control optional and added optional size that defaults to fit fit
* Fixes from PR review
* Fix podlint
* feat: add support for presenting draft paywalls under `@_spi(Internal)` attribute
* fix: initialize `useDraftPaywall` property of `PaywallView`
* feat: have `PaywallView` actually show draft paywall when `useDraftPaywall` is `true`
* remove lint warning
* silence linter warning
* tests: add unit tests for `paywallComponents` and `draftPaywallComponents` in `OfferingsResponse`
* tests: move test offerings with v2 paywalls into a new file not to affect existing decode + encode test
* remove unnecessary property wrapper
* silence lint warning
* feat: mock fetched `StoreProducts` when in UI Preview mode
* feat: improve mocking of products in UI Preview mode to have consistent paywall previews
* fix: mock of products where packages are not unique
* test: add unit tests for OfferingsManager in UI preview mode
* address PR #4735 comments
* We can't just remove PAYWALL_COMPONENT because we don't support macos and tv and things were failing
* Move PartialComponent outside of PaywallComponent extension
* Fix some lint errors
* Wrong thing
* Undid things i shouldn't have done
* Removed more
* Added comment
* Removed/fix some places for the check thingies
* RevenueCat module compiles on all the things
* RevenueCatUI compiles in all the platforms now
* Make all the things compile again
* Make work for visionOS
* Fix some platform and xcode version issues
* Fix failing spm ios 15 test
* Undo circleci changes
* Replace smooth with easeInOut
* Only run the thing i want
* Attempt to remove circular
* Explicit returns
* Does this pass
* This is it
* Swift 5.9 for this
* All tests again
* Undid public testing processor stuff
* Undo this too
* Swift compiler check out pill style circular
* Fixes from PR review
* Fix compile for mac
* This compiled... somehow
* Fixed lint
* Some cleanup
* Fix when forground alias color has error
* Fixed when foreground color has an error
* Commented out code removed
* Added tests
* Removed compiler flags because extra work to handle this on CI unit tests when we are releasing this soon and this isn't really that dangerous
* Fix lint
* Some naming improvements
* Passing UIConfig into Offering
* Reorged some data structures
* Fixed some lint
* This
* Added root paywall background
* Added some previews
* Undo comment about fallback
* Fix some lint
* Fit lint... again
* Added backgroud style to text and stack and fixed preview
* Introduce PurchaseParams to allow passing extra configuration info when making a purchase
* simplify naming
* fallback to old method when flag is not enabled
* Add $(inherited) to the test targets so they pick up compiler flags from Local.xcconfig
* Include product/package inside PurchaseParams
* fix tests
* add compiler flag check
* update purchase tester
* fix docs
* add tests
* Update Sources/Networking/Operations/PostReceiptDataOperation.swift
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
---------
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
* Support custom URL paths in ManageSubscriptionsView
* Fix tests
* More test fixes
* Add parsing and processing tests
* Consolidate SafariView
* Rename method
* Another rename
* Fix import issue with SafariView not being compatible to some platforms
* Style fix
* A mixture of changes while putting the demo together
* Doing some manual fixes for max image height
* Removed unused file and renamed struct
* Fixed lint
* Fix tests
* Image default value on component and not localization
* Fixed sample paywall
* A few more small fixes
* Fix lint
- Adds localized string support to paywall components
-This is primarily done in `TemplateComponentsView`'s static function
`chooseLocalization`.
- Validates that promised localized strings are actually available.
-This is done as component view models are created (see
`TextComponentViewModel`), by throwing if an expected key is not
present:
`self.text = try localizedStrings.string(key: component.textLid)`
-This pattern should be followed for every property that represents a
localized string id, which ensures a non-nil value for every value.
This PR makes all paywall component views rely on intermediate view
models for better separation of concerns. The Component is decoded from
the server response, the ComponentViewModel organizes the component's
data into a view-friendly format, including translating "raw" types into
SwiftUI types when needed, and the ComponentView renders the component.
[ComponentView] --owns---> [ComponentViewModel] --owns---> [Component].
There has been file re-organization, including grouping all of our model
type extensions and the code that transforms them into SwiftUI
equivalents into their own files. Colors are now decoded without relying
on template paywall code.
---------
Co-authored-by: Josh Holtz <me@joshholtz.com>
Initial components merge. Notes:
- Linting enabled except for public documentation which will come as the
API matures and stabilizes.
- Took out unused files (tier-related ones), which will come back in a
future version. The comments were useful and will be referenced when
they come back so thank you for the comments on these.
- Left in tvOS and watchOS availability checks for now - may revisit
this later.
- Everything is codable.
- Features added:
- Layout components allow other components to be laid out along x/y/z
axes.
- Text component supports font weight. (Changing actual font will come
later.)
- LinkButton component links out to external website of choice.
- RemoteImage allows the image to be arbitrarily styled by the calling
code by passing the image back in a closure.
- Image component supports corner rounding an overlaid gradient. (These
will likely be pulled out into common code somewhere in a future PR.)
- Support for OfferingResponse that includes a separate
`PaywallComponentsData` structure, which is separate from `PaywallData`
used for template-based paywalls.
---------
Co-authored-by: Josh Holtz <me@joshholtz.com>
### Motivation
Get more context with targeting information
### Description
- Parse targeting information into `Offerings`
- Add targeting context into `Offerings.current`
- Add targeting context into `getCurrentOffering(forPlacement: String)`
- Post targeting context in post receipts
---------
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
### Motivation
New function to get `Offering` by a placement identifier
### Description
New `currentOffering(forPlacement: String): Offering?` method on
`Offerings`
- Added demo button for this in purchase tester app
```swift
// Example usage
if let offering = offerings.currentOffering(forPlacement: "onboarding") {
showPaywall(offering)
}
```
#### Logic flow
- If a placement identifier explicitly exists within the
`placements.offeringIdsByPlacement` dictionary, the method attempts to
fetch the corresponding `Offering` without resorting to a fallback
- Example: when there is a `nil` / null value for a placement, we don't
want to use the fallback
- If the placement identifier does not exist, the method then attempts
to fetch a fallback offering defined by `placements.fallbackOfferingId`
#### New placements object in offerings response
```js
{
// other offerings stuff
"placements": {
"fallback_offering_id": "offering_a", // can be null
"offering_ids_by_placement": {
"onboarding_start": null,
"onboarding_end": "offering_b",
}
}
}
```
---------
Co-authored-by: Guido Torres <guido@revenuecat.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
- Added `Offering.paywall`
- Decoding `PaywallData` in `OfferingsResponse`, using
`IgnoreDecodeErrors` (this will be better after #2778)
- New `PaywallData` `struct`
- Added new APIs to testers
- Testing paywall deserialization from `Offerings`
- Testing paywall deserialization separately to check edge cases
### Description
Reported in https://github.com/RevenueCat/purchases-flutter/issues/738.
After https://github.com/RevenueCat/purchases-ios/pull/2654, we were
using the new Google Play identifiers that include the plan id in
expiration dates, but we didn't change the system that parses purchase
dates. Later on, when we map purchases, we iterate over the expiration
dates map, and since that id wasn't found in the purchase dates, it was
null, causing the issue in flutter.
This makes the purchase date calculation use the same algorithm that we
use for expiration dates to get the new google play product identifiers.
### Motivation
Addresses https://github.com/RevenueCat/purchases-flutter/issues/692
### Description
Shows the product id as `<product>:<base_plan>` if the subscription was
purchased from Google Play. This uses the `product_plan_identifier` that
is returned with the subscription.
### Motivation
Added `metadata` to `Offering` 😊
### Description
- Added `metadata` to `Offering`
- Had to use a private internal `Metadata` struct to make Swift compiler
happy
- Updated unit tests
- Updated backend integration tests
- Updated load shedder tests
This new cache is used only when the server is down, so paywalls can
continue to work in that case.
### Example:
```
- DEBUG: ℹ️ Configuring SDK using RevenueCat's UserDefaults suite.
- DEBUG: ℹ️ No cached Offerings, fetching from network
- DEBUG: ℹ️ Offerings cache is stale, updating from network in foreground
- WARN: ⚠️ Error fetching offerings: server appears down
- DEBUG: ℹ️ MockProductsManager: no stubbed products, returning fake products for ["monthly_freetrial"]
- DEBUG: ℹ️ Vending Offerings from disk cache
```
See also #1658.
This updates the behavior of `CustomerInfoResponse` decoding so that having invalid content inside of one of the structures fails more loudly instead of getting ignored.
For that, `@DefaultValue` no longer ignores errors, and instead `@IgnoreDecodeErrors` must be used.
For some of the values in `CustomerInfoResponse` this makes sense (like `Store`), but not for things like subscriptions where we would be losing data.
The new response is now represented by `GetIntroEligibilityResponse`, and has dedicated tests (`GetIntroEligibilityDecodingTests`) using a JSON fixture.
The operation becomes much simpler now, plus I've done a lot of code removing that was redundant.
The actual response handling is now 3 lines:
```swift
let result: [String: IntroEligibility] = Set(productIdentifiers)
.dictionaryWithValues { eligibilities?[$0] ?? .unknown }
.mapValues(IntroEligibility.init)
```
All the tests in `BackendGetIntroEligibilityTests` pass with no changes.
## Changes:
- Added `PostOfferResponse`
- Added `PostOfferDecodingTests` tests
- Using the automatic deserialization by `HTTPClient` by simply changing the callback to `HTTPResponse<PostOfferResponse>.Result`
- All tests in `BackendPostOfferForSigningTests` (which are very exhaustive) pass with no changes!
Follow up to #1496. Depends on ~~#1567, #1568, #1604~~.
The solution implemented in #1496 was very simple, but [as pointed out by @joshdholtz](https://github.com/RevenueCat/purchases-ios/pull/1496#discussion_r869463028), it was only encoding the data that was formally decoded by the `struct`s.
We had no test coverage for this, so it went unnoticed.
The new solution adds a new field:
```swift
@IgnoreEncodable @IgnoreHashable
var rawData: [String: Any]
```
Which, together with a new helper method `decodeRawData()`, allows storing all the data to be used by `CustomerInfo`'s `RawDataContainer` implementation.
Unfortunately, despite many attempts, I realized that this can't be abstracted into a property wrapper, because a `KeyedDecodingContainer` can't access the parent `Decoder`.
That's why the `CustomerInfoResponse` and `CustomerInfoResponse.Entitlement` must manually call this method.
### Depends on:
- #1537
- #1540
- #1541
- #1543
- #1546
- #1547
- #1550
- #1551,
- #1565
### Changes
- Removed all custom deserialization code thanks to #1537, #1541, and #1543.
- The format of the customer info response is now very concise and only in one place (`CustomerInfoResponse`) instead of being passed through dictionaries and magic strings everywhere.
- `CustomerInfo` was combining the dictionaries inside of `"subscriptions"` and `"non_subscriptions"`, despite them having different formats. This is now made explicit through type conversions between the two, (see `CustomerInfoResponse.allTransactionsByProductId`).
- Removed all custom `CustomerInfo` errors since deserialization is automatic, and the underlying `Error` information will be provided thanks to `ErrorUtils.logDecodingError`.
- `CustomerInfo` deserialization always wraps errors into `ErrorCode.customerInfoError`, which is now also covered in a new test.
- Added a new snapshot test that covers `CustomerInfo` serialization by storing the result in a JSON file.
### Other changes:
- `CustomerInfo` equality is now automatic through the `CustomerInfoResponse` `Equatable` conformance.
- Simplified `schemaVersion` handling
- All tests still use `CustomerInfo(data: [String: Any])`, but that method is only visible for tests now, and it uses the underlying `Decodable` implementation (see `CustomerInfo+TestExtensions.swift`).
- Added extra tests for `CustomerInfoResponse` that use a fixture `CustomerInfo.json`, which is easier to maintain than having JSON in code.
- `RawDataContainer` implementation is changed in #1565.
### TODO:
- [x] Fix all tests
- [x] Update `RawValueContainer`
- [x] Consider recovering `CustomerInfoError`
- [x] Look at all previously logged errors
- [x] Finish `CustomerInfoResponseTests`
- [x] Handle all TODOs
- [x] Make `RawDataContainer` actually store all the original data: #1565