Files
purchases-ios/Sources/Error Handling/BackendError.swift
T
NachoSoto c531814dc6 Purchases.customerInfo(): added overload with a new CacheFetchPolicy (#1608)
Fixes [CF-19].
This allows specifying `CustomerInfoManager` fetching behavior, both as a public API for users, and also internally.
Some future uses of `CustomerInfoManager` might want to ensure that we don't rely on cached data, for example.

Thanks to the `APITester` we know that the existing methods still work, this is purely an additive change.

This is the core of the change:
```swift
/// Specifies the behavior for a caching API.
@objc(RCCacheFetchPolicy)
public enum CacheFetchPolicy: Int {
    /// Returns values from the cache, or throws an error if not available.
    case fromCacheOnly

    /// Always fetch the most up-to-date data.
    case fetchCurrent

    /// Returns the cached data if available and not stale, or fetches up-to-date data.
    case notStaleCachedOrFetched

    /// Default behavior: returns the cached data if available (even if stale), or fetches up-to-date data.
    case cachedOrFetched

    /// Default ``CacheFetchPolicy`` behavior.
    public static let `default`: Self = .cachedOrFetched
}
```

The new `CustomerInfoManagerGetCustomerInfoTests` check all the different combinations of fetching behavior.
I've taken some of the existing tests and refactored them to be `async`, as well as added a bunch more tests.

> **Note**: 
There is also one big hidden bug fix:

`Purchases.getCustomerInfo` now calls `BackendError.asPurchasesError`, so we don't end up returning an internal error type.
This was missed because we always return generic `Error` types, so we have no way to know at compile time that we only return `ErrorCode`s.

[CF-19]: https://revenuecats.atlassian.net/browse/CF-19?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-05-31 13:04:16 -07:00

208 lines
7.4 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// BackendError.swift
//
// Created by Nacho Soto on 4/7/22.
// swiftlint:disable multiline_parameters
import Foundation
/// An `Error` produced by ``Backend``.
enum BackendError: Error, Equatable {
case networkError(NetworkError)
case missingAppUserID(Source)
case emptySubscriberAttributes(Source)
case missingReceiptFile(Source)
case missingTransactionProductIdentifier(Source)
case missingCachedCustomerInfo(Source)
case unexpectedBackendResponse(UnexpectedBackendResponseError, extraContext: String?, Source)
}
extension BackendError {
static func missingAppUserID(
file: String = #fileID, function: String = #function, line: UInt = #line
) -> Self {
return .missingAppUserID(.init(file: file, function: function, line: line))
}
static func emptySubscriberAttributes(
file: String = #fileID, function: String = #function, line: UInt = #line
) -> Self {
return .emptySubscriberAttributes(.init(file: file, function: function, line: line))
}
static func missingTransactionProductIdentifier(
file: String = #fileID, function: String = #function, line: UInt = #line
) -> Self {
return .missingTransactionProductIdentifier(.init(file: file, function: function, line: line))
}
static func missingReceiptFile(
file: String = #fileID, function: String = #function, line: UInt = #line
) -> Self {
return .missingReceiptFile(.init(file: file, function: function, line: line))
}
static func missingCachedCustomerInfo(
file: String = #fileID, function: String = #function, line: UInt = #line
) -> Self {
return .missingCachedCustomerInfo(.init(file: file, function: function, line: line))
}
static func unexpectedBackendResponse(
_ error: UnexpectedBackendResponseError,
extraContext: String? = nil,
file: String = #fileID, function: String = #function, line: UInt = #line
) -> Self {
return .unexpectedBackendResponse(error,
extraContext: extraContext,
.init(file: file, function: function, line: line))
}
}
extension BackendError: ErrorCodeConvertible {
var asPurchasesError: Error {
switch self {
case let .networkError(error):
return error.asPurchasesError
case let .missingAppUserID(source):
return ErrorUtils.missingAppUserIDError(fileName: source.file,
functionName: source.function,
line: source.line)
case .emptySubscriberAttributes:
return ErrorCode.emptySubscriberAttributes
case let .missingReceiptFile(source):
return ErrorUtils.missingReceiptFileError(fileName: source.file,
functionName: source.function,
line: source.line)
case let .missingTransactionProductIdentifier(source):
return ErrorUtils.unknownError(
message: Strings.purchase.skpayment_missing_product_identifier.description,
fileName: source.file,
functionName: source.function,
line: source.line
)
case let .missingCachedCustomerInfo(source):
return ErrorUtils.customerInfoError(withMessage: Strings.purchase.missing_cached_customer_info.description,
fileName: source.file,
functionName: source.function,
line: source.line)
case let .unexpectedBackendResponse(error, extraContext, source):
return ErrorUtils.unexpectedBackendResponse(withSubError: error,
extraContext: extraContext,
fileName: source.file,
functionName: source.function,
line: source.line)
}
}
}
extension BackendError: DescribableError { }
extension BackendError {
/// Whether the operation producing this error actually synced the data.
var successfullySynced: Bool {
return self.networkError?.successfullySynced ?? false
}
/// Whether the operation producing this error can be completed.
/// If `false`, the underlying error was fatal.
var finishable: Bool {
return self.networkError?.finishable ?? false
}
private var networkError: NetworkError? {
switch self {
case let .networkError(networkError):
return networkError
case .missingAppUserID,
.emptySubscriberAttributes,
.missingReceiptFile,
.missingTransactionProductIdentifier,
.missingCachedCustomerInfo,
.unexpectedBackendResponse:
return nil
}
}
}
extension BackendError {
enum UnexpectedBackendResponseError: Error, Equatable {
/// Login call failed due to a problem with the response.
case loginResponseDecoding
/// Received a bad response after posting an offer- "offers" couldn't be read from response.
case postOfferIdBadResponse
/// Received a bad response after posting an offer- "offers" was totally missing.
case postOfferIdMissingOffersInResponse
/// Received a bad response after posting an offer- there was an issue with the signature.
case postOfferIdSignature
/// getOffer call failed with an invalid response.
case getOfferUnexpectedResponse
/// A call that is supposed to retrieve a CustomerInfo failed because the CustomerInfo in the response was nil.
case customerInfoNil
/// A call that is supposed to retrieve a CustomerInfo failed because the json object couldn't be parsed.
case customerInfoResponseParsing(error: NSError, json: String)
}
}
extension BackendError.UnexpectedBackendResponseError: DescribableError {
var description: String {
switch self {
case .loginResponseDecoding:
return "Unable to decode response returned from login."
case .postOfferIdBadResponse:
return "Unable to decode response returned from posting offer for signing."
case .postOfferIdMissingOffersInResponse:
return "Missing offers from response returned from posting offer for signing."
case .postOfferIdSignature:
return "Signature error encountered in response returned from posting offer for signing."
case .getOfferUnexpectedResponse:
return "Unknown error encountered while getting offerings."
case .customerInfoNil:
return "Unable to instantiate a CustomerInfoResponse, CustomerInfo in response was nil."
case .customerInfoResponseParsing:
return "Unable to instantiate a CustomerInfoResponse due to malformed json."
}
}
}
extension BackendError {
typealias Source = ErrorSource
}