Commit Graph
26 Commits
Author SHA1 Message Date
NachoSoto a2fc04c09e SubscriberAttribute: converted into struct (#1648)
### Motivation:

Initially I wanted to make `SubscriberAttribute.asDictionary` work using `Codable`. However, this isn't the right refactor because we need the actual raw types to be encoded inside `UserDefaults`.
At least this refactor provides several improvements:

### Changes:

- `SubscriberAttribute` now has value semantics
- ⚠️  `SubscriberAttribute.isSynced` used to be mutable, with reference semantics, which was dangerous
- `SubscriberAttribute: Equatable` implementation is now automatic
- Cleaned up `SubscriberAttribute.asDictionary` implementation, extracting keys into an `enum`
- Moved `Dictionary` -> `SubscriberAttribute` creation to the type itself
- ⚠️ `SubscriberAttribute.init(dictionary:)` returns an `Optional` instead of using force-unwrap, avoiding potential crashes
- Added more serialization / deserialization tests
- Removed `SubscriberAttributesMarshaller` and moved method into `SubscriberAttribute` extension
- Moved `SubscriberAttributeDict` into `SubscriberAttribute.Dictionary`
2022-06-17 10:00:51 -07:00
Joshua Liebowitz 858bef3f9b Extract Offerings-related things into OfferingsAPI (#1706)
Extract Offerings-related things into OfferingsAPI
2022-06-16 14:00:40 -07:00
Joshua Liebowitz d637b6c921 Refactor of login into a new IdentityAPI class (#1705)
Refactor of login into a new IdentityAPI class
Resolves [CSDK-153]
2022-06-16 11:17:29 -07:00
Joshua Liebowitz 80905f94c9 Extract Backend dependencies into a BackendConfiguration object (#1696)
Extract out various dependencies that are required to instantiate the `Backend` object.

Refactor to assist with the next few PRs that extract out other pieces
See [CSDK-82]
Resolves [CSDK-155]
2022-06-14 15:12:46 -07:00
Joshua Liebowitz 31f611aaa0 Remove create alias (#1695)
* Remove create alias
Resolves [CSDK-152]
2022-06-14 12:39:41 -07:00
NachoSoto 9679af3bc1 CustomerInfoDecodingTests: added test to check decoding invalid data behavior (#1659)
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.
2022-06-07 16:56:04 -07:00
Josh Holtz 5e527162e6 Fix decoding issue caused by LossyDictionary for subscriptions and active entitlements (#1651) 2022-06-03 13:05:55 -03:00
NachoSoto d410da0a2e [String: Any] no longer needs to conform to HTTPResponseBody (#1577)
There are no leftover usages of `HTTPClient` that need `[String: Any]` deserialization! 🎉
2022-05-26 13:55:50 -07:00
NachoSoto 59d492df88 GetIntroEligibilityOperation: replaced response parsing with Decodable (#1576)
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.
2022-05-26 13:54:45 -07:00
NachoSoto 438e74e6cf PostOfferForSigningOperation: changed response parsing to using Decodable (#1573)
## 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!
2022-05-26 13:45:31 -07:00
NachoSoto 395322ac7e Added tests and fix to ensure RawDataContainer includes all data (#1565)
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.
2022-05-24 14:23:07 -07:00
NachoSoto 29c034dfc8 Converted CustomerInfo and related types to use Codable (#1496)
### 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
2022-05-24 14:22:11 -07:00
NachoSoto 4ee86185c6 HTTPClient: added X-Is-Sandbox header (#1582)
Fixes [CF-649].

[CF-649]: https://revenuecats.atlassian.net/browse/CF-649?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-05-20 14:51:48 -07:00
Joshua Liebowitz 9bde89137c Create Configuration object and API to configure Purchases (#1556)
* Resolves [CF-443]
* Adds `configureWithConfiguration` to `RCPurchasesAPI.m`
* Create Configuration object and API to configure Purchases
* Update API doc for new Purchases API
* Add example code to Configuration documentation
* Adds deprecations for various Purchases.configure methods
2022-05-12 15:43:10 -07:00
NachoSoto 7530a6771d OfferingsManager/OfferingsFactory: using OfferingsResponse (#1435)
This takes full advantage of `Decodable` when decoding and creating `Offerings`.
2022-04-22 11:19:40 -07:00
NachoSoto 173e4f96e7 HTTPClient: refactored NetworkError creation (#1518)
Follow up to #1507.
2022-04-21 09:31:20 -07:00
NachoSoto 3a03d147c8 Added new ErrorCode.offlineConnectionError (#1507)
This is a special enough error that it's worth distinguishing from other `ErrorCode.networkError`s.

I see a couple benefits:
- When opening an app that has the SDK, this used to print lots of errors that just said "Error performing request". Now it's more clear what's happening
- Users can respond to failed calls to the SDK more appropriately, whereas before it might not be clear why a request failed.

Also fixed `logErrorIfNeeded` not passing the file/function/line parameters to the log functions.
2022-04-15 15:54:18 +02:00
NachoSoto ea7230de38 NetworkError.errorResponse: added status code to userInfo (#1480)
This could help debug these errors.

Thanks to the error refactor this only had to be put in one place.
2022-04-12 15:14:15 -07:00
NachoSoto 23f63c5ef7 HTTPClient now handles response deserialization + Introduced NetworkError and BackendError (#1425)
Closes #695 and finishes [CF-195]. 
Depends on ~~#1431, #1432, #1433, #1437~~.

## Goals:

- [x] Handle all requests / response / deserialization errors in `HTTPClient`. Clients of `HTTPClient` will only have to handle the "happy" path: #1431
- [x] `HTTPClient` shouldn't return `Result.success` with failed responses, forcing clients to verify the response is actually a failure: https://github.com/RevenueCat/purchases-ios/pull/1431
- [x] Abstract error response deserialization / error creation: #1427
- [x] Abstract attribute error parsing: #1427 
- [x] Move response deserialization to `HTTPClient` based on a `ResponseType: Decodable` type, so the completion block will simply return a `Result<HTTPResponse<ResponseType>, Error>`
- [x] `ETagManager` should store response `Data` instead of a deserialized `[String: Any]`
- [x] Improved error types in `HTTPClient` to fix tests: #1437
- [x] Dealt with non-backwards compatible `ETagManager: #1438

## Changes:
- Replaced `HTTPResponse`'s body from `[String: Any]` to a generic `HTTPResponseBody`.
- Created `HTTPResponseBody` to abstract `Decodable` and provide some default implementations for types like `Data,` `[String: Any]` (for backwards compatibility to types that aren't `Decodable` yet), and `Decodable` itself.
- New `HTTPResponse.Result` typealias (`Result<HTTPResponse<HTTPResponseBody>, Error>`) used everywhere. This will allow replacing `Error` with a more specific `Error` so we can forward known typed errors, and make sure that we don't end up with the wrong error type, or with a very complex error hierarchy and the details buried in `underlyingError`.
- Each layer (only a few for now) has its own error type: `NetworkError`, `BackendError`, `OfferingsManager.Error`.
- `HTTPClient` for example has to produce `NetworkError`, operations produce `BackendError`
- The parent `BackendError` can have specific errors like `.missingAppUserID`, but also be simply a child error `case networkError(NetworkError)`
- All of these conform to a `ErrorCodeConvertible`, so there is a single point of code that converts from simple and readable errors (like `BackendError.emptySubscriberAttributes`, `.unexpectedBackendResponse(.loginResponseDecoding)`) into errors with all the context using `ErrorUtils`
- Converted `DNSError` into `NetworkError.dnsError`. Its functionality remains unchanged.
- Removed `Backend.RCSuccessfullySyncedKey` and `ErrorDetails.finishableKey` in favor of tested properties on `NetworkError`

### Leftovers

There's a few things I've decided to not finish for now:
- [ ] `CustomerInfo` still does't conform to `Decodable` (manual deserialization is still supported by the current system): #1496
- `SubscriberAttributeDict` could also be `Codable`
- [x] Replace `OfferingsFactory` with `Decodable`: #1435

[CF-195]: https://revenuecats.atlassian.net/browse/CF-195?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-04-12 15:11:22 -07:00
NachoSoto 23afeb96d0 Added StoreKit2Setting to replace useStoreKit2IfAvailable (#1467)
This adds a third case `disabled` that allows tests to explicitly disable StoreKit 2.
Some types, like `TrialOrIntroPriceEligibilityChecker` and `TransactionsManager` always used SK2 because the implementation was better. But this meant that tests couldn't explicitly disable it (other than calling the internal methods directly).

This allows #1464 to be a lot simpler but being able to use the public `Purchases` methods, but forcing the implementation to use SK1.

Note that this is purely an implementation detail. `StoreKit2Setting` is `internal`, and the public API still only exposes the same boolean.

This basically solves [CF-497] too, since those tests will now force `TrialOrIntroPriceEligibilityChecker` to run the SK1 implementation.

[CF-497]: https://revenuecats.atlassian.net/browse/CF-497?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-04-11 11:41:47 -07:00
NachoSoto 3f1a527c9b Moved response error and status checking to HTTPClient (#1431)
Follow up to #1429. For #695.

This was duplicated in all operations.
`HTTPClient` now only forwards `Result.success` if the request actually succeeded,
leaving each `NetworkOperation` to just parse the body (which will become part of `HTTPClient` too).

Other changes:
- Simplified logic for when to retry requests in `HTTPClient` by using a new `Result.asOptionalResult`
- Removed several response handlers that don't need to do anything anymore
- Removed several tests that were basically duplicated:
  - The `finishable` checks are already tested as part of `ErrorResponse`.
  - All the checks for the error details are also part of `ErrorResponse`.
  - No longer need to have a test for each type of failed response since that's part of `HTTPClient` tests now.
2022-04-05 11:39:49 -07:00
NachoSoto f084b254ee Replaced manual error response deserialization with ErrorResponse (#1429)
Follow up to #1427.

The new way to create errors is like this:
```swift
ErrorResponse
    .from(response)
    .asBackendError(with: statusCode)
```

## Other changes:
- Removed `UserInfoAttributeParser`
- Simplified `JSONDecoder.decode` implementation by simply relying on `defaultJsonDecoder`
- Added support for "wrapped" error responses. This was previously handled by `UserInfoAttributeParser`.
- Removed all duplicated code parsing error responses
- The new `ErrorResponse` introduced in #1427 correctly decodes the format sent by the backend. Note that some tests had the wrong format, and `UserInfoAttributeParser` was parsing that wrong as well.

An upcoming PR will actually move these calls to `ErrorResponse` to `HTTPClient`.
2022-04-01 14:26:02 -07:00
NachoSoto ddb5a05a9b Created ErrorResponse to abstract error deserialization (#1427)
This is duplicated in most of the `NetworkOperation`s. This new shared implementation will be used by `HTTPClient`.

For #695.
See #1429 for how this is used.
2022-04-01 14:24:03 -07:00
NachoSoto 2504b953b8 Simplified HTTPClient.Completion and refactored response handling implementation (#1426)
For #695.

The biggest change is that this code at the beginning of `HTTPClient.handle` is now gone:
```swift
var statusCode: HTTPStatusCode = .networkConnectTimeoutError
var jsonObject: [String: Any]?
var httpResponse: HTTPResponse? = HTTPResponse(statusCode: statusCode, jsonObject: jsonObject)
var receivedJSONError: Error?
```

It was hard to trace which paths modified these default values. The new implementation has a mostly linear path (part of it abstracted in smaller methods now) and doesn’t have mutable values.

This is the new `handle` implementation:
```swift
func handle(urlResponse: URLResponse?,
            request: Request,
            urlRequest: URLRequest,
            data: Data?,
            error networkError: Error?) {
    let (response, shouldRetry) = self.parse(
        urlResponse: urlResponse,
        request: request,
        urlRequest: urlRequest,
        data: data,
        error: networkError
    )

    if shouldRetry {
        Logger.debug(Strings.network.retrying_request(httpMethod: request.method.httpMethod,
                                                      path: request.path))

        self.state.modify {
            $0.queuedRequests.insert(request.retriedRequest(), at: 0)
        }
    } else {
        request.completionHandler?(response)
    }

    self.beginNextRequest()
}
```

## All Changes:

- `HTTPClient.perform` no longer has a bunch of mutable variables with forking paths that make it hard to follow. Most of the response handling is now in a new `parse` method. This will simplify the task of continuing to refactor response deserialization.
- `HTTPClient`’s completion abstracts response into `HTTPResponse`
- `UserInfoAttributeParser` is now an `enum`
- `ETagManager` no longer takes an `Error`. The only thing it did with it is to return the default response.
- `HTTPResponse.jsonObject` is no longer `Optional`
- Moved `defaultJsonEncoder` / `defaultJsonDecoder` to `JSONDecoder+Extensions`. They used by `HTTPClient` and others soon.
2022-04-01 14:22:29 -07:00
NachoSoto 52435ca72b ETagManager: simplified implementation using SynchronizedUserDefaults (#1424)
Also removed the unused `queue`.
2022-03-29 13:25:57 -07:00
aboedo f5697de730 renamed Purchases folder to Sources (#1416)
* renamed `Purchases` folder to `Sources`

* temporarily enabling installation test jobs

* re-disabling installation test jobs
2022-03-28 11:53:24 -03:00