Files
purchases-ios/Sources/Error Handling/ErrorCode.swift
T
NachoSoto f05aabd176 HTTPClient: added signature validation and introduced ErrorCode.signatureVerificationFailed (#2272)
Fixes [CSDK-634].

## Changes:
- Created `HTTPResponseValidationResult`
- Added `HTTPResponseValidationResult` and `HTTPClient.ResponseHeaders`
to `HTTPResponse`
- Added new `ErrorCode.signatureVerificationFailed`
- Extracted `SigningType` to create `MockSigning` to test `HTTPClient`
without bothering with signatures
- Refactored `ETagManager` to use `HTTPResponse`, making it easier to
pass through response headers and verification results
- Stored `HTTPResponseValidationResult` in `ETagManager` cache
- `HTTPClient` handles signatures from response based on
`SystemInfo.responseVerificationLevel`
- Renamed `HTTPRequest.createIntegrityEnforcedRequestRequest` to
`HTTPRequest.createWithResponseVerification`
- Added lots of tests for this new behavior in `HTTPClient`
- Added lots of new tests for `ETagManager`

[CSDK-634]:
https://revenuecats.atlassian.net/browse/CSDK-634?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2023-02-16 03:06:59 -08:00

311 lines
13 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
//
// ErrorCode.swift
//
// Created by Joshua Liebowitz on 7/8/21.
//
import Foundation
/**
Error codes used by the Purchases SDK
*/
@objc(RCPurchasesErrorCode) public enum ErrorCode: Int, Error {
// swiftlint:disable missing_docs
@objc(RCUnknownError) case unknownError = 0
@objc(RCPurchaseCancelledError) case purchaseCancelledError = 1
@objc(RCStoreProblemError) case storeProblemError = 2
@objc(RCPurchaseNotAllowedError) case purchaseNotAllowedError = 3
@objc(RCPurchaseInvalidError) case purchaseInvalidError = 4
@objc(RCProductNotAvailableForPurchaseError) case productNotAvailableForPurchaseError = 5
@objc(RCProductAlreadyPurchasedError) case productAlreadyPurchasedError = 6
@objc(RCReceiptAlreadyInUseError) case receiptAlreadyInUseError = 7
@objc(RCInvalidReceiptError) case invalidReceiptError = 8
@objc(RCMissingReceiptFileError) case missingReceiptFileError = 9
@objc(RCNetworkError) case networkError = 10
@objc(RCInvalidCredentialsError) case invalidCredentialsError = 11
@objc(RCUnexpectedBackendResponseError) case unexpectedBackendResponseError = 12
@objc(RCReceiptInUseByOtherSubscriberError) case receiptInUseByOtherSubscriberError = 13
@objc(RCInvalidAppUserIdError) case invalidAppUserIdError = 14
@objc(RCOperationAlreadyInProgressForProductError) case operationAlreadyInProgressForProductError = 15
@objc(RCUnknownBackendError) case unknownBackendError = 16
@objc(RCInvalidAppleSubscriptionKeyError) case invalidAppleSubscriptionKeyError = 17
@objc(RCIneligibleError) case ineligibleError = 18
@objc(RCInsufficientPermissionsError) case insufficientPermissionsError = 19
@objc(RCPaymentPendingError) case paymentPendingError = 20
@objc(RCInvalidSubscriberAttributesError) case invalidSubscriberAttributesError = 21
@objc(RCLogOutAnonymousUserError) case logOutAnonymousUserError = 22
@objc(RCConfigurationError) case configurationError = 23
@objc(RCUnsupportedError) case unsupportedError = 24
@objc(RCEmptySubscriberAttributesError) case emptySubscriberAttributes = 25
@objc(RCProductDiscountMissingIdentifierError) case productDiscountMissingIdentifierError = 26
@objc(RCProductDiscountMissingSubscriptionGroupIdentifierError)
case productDiscountMissingSubscriptionGroupIdentifierError = 28
@objc(RCCustomerInfoError) case customerInfoError = 29
@objc(RCSystemInfoError) case systemInfoError = 30
@objc(RCBeginRefundRequestError)case beginRefundRequestError = 31
@objc(RCProductRequestTimedOut) case productRequestTimedOut = 32
@objc(RCAPIEndpointBlocked) case apiEndpointBlockedError = 33
@objc(RCInvalidPromotionalOfferError) case invalidPromotionalOfferError = 34
@objc(RCOfflineConnectionError) case offlineConnectionError = 35
@objc(RCSignatureVerificationFailed) case signatureVerificationFailed = 36
// swiftlint:enable missing_docs
}
extension ErrorCode {
/**
* When an ErrorCode has been deprecated and then removed, add it to the reserved list so that we do not
* accidentally reuse it. For example:
* `@objc(RCMissingAppUserIDForAliasCreationError) case missingAppUserIDForAliasCreationError = 27` was removed,
* so we add its rawValue of `27` to the `reservedRawValues` array. That way our unit tests will catch if we
* accidentally add `27` back into the enumeration.
*/
static var reservedRawValues: Set<RawValue> {
return [27]
}
}
extension ErrorCode: CaseIterable { }
extension ErrorCode: DescribableError {
// swiftlint:disable:next missing_docs
public var description: String {
switch self {
case .networkError:
return "Error performing request."
case .unknownError:
return "Unknown error."
case .purchaseCancelledError:
return "Purchase was cancelled."
case .storeProblemError:
#if os(macOS) || targetEnvironment(macCatalyst)
// See https://github.com/RevenueCat/purchases-ios/issues/370
return "There was a problem with the App Store. This could also indicate the purchase dialog was cancelled."
#else
return "There was a problem with the App Store."
#endif
case .purchaseNotAllowedError:
return "The device or user is not allowed to make the purchase."
case .purchaseInvalidError:
return "One or more of the arguments provided are invalid."
case .productNotAvailableForPurchaseError:
return "The product is not available for purchase."
case .productAlreadyPurchasedError:
return "This product is already active for the user."
case .receiptAlreadyInUseError:
return "There is already another active subscriber using the same receipt."
case .missingReceiptFileError:
return "The receipt is missing."
case .invalidCredentialsError:
return "There was a credentials issue. Check the underlying error for more details."
case .unexpectedBackendResponseError:
return "Received malformed response from the backend."
case .invalidReceiptError:
return "The receipt is not valid."
case .invalidAppUserIdError:
return "The app user id is not valid."
case .operationAlreadyInProgressForProductError:
return "The operation is already in progress for this product."
case .unknownBackendError:
return "There was an unknown backend error."
case .receiptInUseByOtherSubscriberError:
return "The receipt is in use by other subscriber."
case .invalidAppleSubscriptionKeyError:
return """
Apple Subscription Key is invalid or not present. In order to provide subscription offers, you must
first generate a subscription key.
Please see https://docs.revenuecat.com/docs/ios-subscription-offers for more info.
"""
case .ineligibleError:
return "The User is ineligible for that action."
case .insufficientPermissionsError:
return "App does not have sufficient permissions to make purchases"
case .paymentPendingError:
return "The payment is pending."
case .invalidSubscriberAttributesError:
return "One or more of the attributes sent could not be saved."
case .logOutAnonymousUserError:
return "LogOut was called but the current user is anonymous."
case .configurationError:
return "There is an issue with your configuration. Check the underlying error for more details."
case .unsupportedError:
return """
There was a problem with the operation. Looks like we doesn't support that yet.
Check the underlying error for more details.
"""
case .emptySubscriberAttributes:
return "A request for subscriber attributes returned none."
case .productDiscountMissingIdentifierError:
return """
The SKProductDiscount or Product.SubscriptionOffer wrapped
by StoreProductDiscount is missing an identifier.
This is a required property and likely an AppStore quirk that it is missing.
"""
case .productDiscountMissingSubscriptionGroupIdentifierError:
return "Unable to create a discount offer, the product is missing a subscriptionGroupIdentifier."
case .customerInfoError:
return "There was a problem related to the customer info."
case .systemInfoError:
return "There was a problem related to the system info."
case .beginRefundRequestError:
return "Error when trying to begin refund request."
case .productRequestTimedOut:
return "SKProductsRequest took too long to complete."
case .apiEndpointBlockedError:
return "Requests to RevenueCat are being blocked. See: https://rev.cat/dnsBlocking for more info."
case .invalidPromotionalOfferError:
return """
The information associated with this PromotionalOffer is not valid.
See https://rev.cat/ios-subscription-offers for more info.
"""
case .offlineConnectionError:
return "Error performing request because the internet connection appears to be offline."
case .signatureVerificationFailed:
return "Request failed signature verification."
@unknown default:
return "Something went wrong."
}
}
}
extension ErrorCode: CustomNSError {
// swiftlint:disable missing_docs
public var errorUserInfo: [String: Any] {
return [
NSDebugDescriptionErrorKey: self.description,
"rc_code_name": self.codeName
]
}
}
extension ErrorCode {
/**
* The error short string, based on the error code.
*/
var codeName: String {
switch self {
case .networkError:
return "NETWORK_ERROR"
case .unknownError:
return "UNKNOWN"
case .purchaseCancelledError:
return "PURCHASE_CANCELLED"
case .storeProblemError:
return "STORE_PROBLEM"
case .purchaseNotAllowedError:
return "PURCHASE_NOT_ALLOWED"
case .purchaseInvalidError:
return "PURCHASE_INVALID"
case .productNotAvailableForPurchaseError:
return "PRODUCT_NOT_AVAILABLE_FOR_PURCHASE"
case .productAlreadyPurchasedError:
return "PRODUCT_ALREADY_PURCHASED"
case .receiptAlreadyInUseError:
return "RECEIPT_ALREADY_IN_USE"
case .missingReceiptFileError:
return "MISSING_RECEIPT_FILE"
case .invalidCredentialsError:
return "INVALID_CREDENTIALS"
case .unexpectedBackendResponseError:
return "UNEXPECTED_BACKEND_RESPONSE_ERROR"
case .invalidReceiptError:
return "INVALID_RECEIPT"
case .invalidAppUserIdError:
return "INVALID_APP_USER_ID"
case .operationAlreadyInProgressForProductError:
return "OPERATION_ALREADY_IN_PROGRESS_FOR_PRODUCT_ERROR"
case .unknownBackendError:
return "UNKNOWN_BACKEND_ERROR"
case .receiptInUseByOtherSubscriberError:
return "RECEIPT_IN_USE_BY_OTHER_SUBSCRIBER"
case .invalidAppleSubscriptionKeyError:
return "INVALID_APPLE_SUBSCRIPTION_KEY"
case .ineligibleError:
return "INELIGIBLE_ERROR"
case .insufficientPermissionsError:
return "INSUFFICIENT_PERMISSIONS_ERROR"
case .paymentPendingError:
return "PAYMENT_PENDING_ERROR"
case .invalidSubscriberAttributesError:
return "INVALID_SUBSCRIBER_ATTRIBUTES"
case .logOutAnonymousUserError:
return "LOGOUT_CALLED_WITH_ANONYMOUS_USER"
case .configurationError:
return "CONFIGURATION_ERROR"
case .unsupportedError:
return "UNSUPPORTED_ERROR"
case .emptySubscriberAttributes:
return "EMPTY_SUBSCRIBER_ATTRIBUTES"
case .productDiscountMissingIdentifierError:
return "PRODUCT_DISCOUNT_MISSING_IDENTIFIER_ERROR"
case .productDiscountMissingSubscriptionGroupIdentifierError:
return "PRODUCT_DISCOUNT_MISSING_SUBSCRIPTION_GROUP_IDENTIFIER_ERROR"
case .customerInfoError:
return "CUSTOMER_INFO_ERROR"
case .systemInfoError:
return "SYSTEM_INFO_ERROR"
case .beginRefundRequestError:
return "BEGIN_REFUND_REQUEST_ERROR"
case .productRequestTimedOut:
return "PRODUCT_REQUEST_TIMED_OUT_ERROR"
case .apiEndpointBlockedError:
return "API_ENDPOINT_BLOCKED_ERROR"
case .invalidPromotionalOfferError:
return "INVALID_PROMOTIONAL_OFFER_ERROR"
case .offlineConnectionError:
return "OFFLINE_CONNECTION_ERROR"
case .signatureVerificationFailed:
return "SIGNATURE_VERIFICATION_FAILED"
@unknown default:
return "UNRECOGNIZED_ERROR"
}
}
}
// MARK: - PurchasesErrorConvertible
/// An `Error` that can be converted into a `PurchasesError`
protocol PurchasesErrorConvertible: Swift.Error {
/// Convert the receiver into a `PurchasesError` with all the necessary context.
///
/// ### Related symbols:
/// - ``ErrorUtils``
/// - ``ErrorCode``
var asPurchasesError: PurchasesError { get }
}
extension PurchasesErrorConvertible {
var asPublicError: PublicError {
return self.asPurchasesError.asPublicError
}
var description: String {
return self.asPurchasesError.localizedDescription
}
}