mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
⚠️ 🎉 This also changes integration tests to use `EntitlementVerificationLevel.enforced` so that integration tests fail if signatures are invalid. #### Depends on: - https://github.com/RevenueCat/khepri/pull/5191 - https://github.com/RevenueCat/khepri/pull/5204 - #2214 - #2215 - #2216 - #2272 _Marking this as `feat`ure because it contains a new error in `PurchasesDiagnostics.Error`_
719 lines
29 KiB
Swift
719 lines
29 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
|
|
//
|
|
// RCPurchasesErrorUtils.swift
|
|
//
|
|
// Created by Cesar de la Vega on 7/21/21.
|
|
//
|
|
|
|
import Foundation
|
|
import StoreKit
|
|
|
|
// swiftlint:disable file_length multiline_parameters type_body_length
|
|
|
|
enum ErrorUtils {
|
|
|
|
/**
|
|
* Constructs an NSError with the ``ErrorCode/networkError`` code and a populated `NSUnderlyingErrorKey` in
|
|
* the `NSError.userInfo` dictionary.
|
|
*
|
|
* - Parameter underlyingError: The value of the `NSUnderlyingErrorKey` key.
|
|
*
|
|
* - Note: This error is used when there is an error performing network request returns an error or when there
|
|
* is an `NSJSONSerialization` error.
|
|
*/
|
|
static func networkError(
|
|
message: String? = nil,
|
|
withUnderlyingError underlyingError: Error? = nil,
|
|
extraUserInfo: [NSError.UserInfoKey: Any] = [:],
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
|
|
let errorCode: ErrorCode
|
|
if case NetworkError.dnsError(_, _, _)? = underlyingError {
|
|
errorCode = .apiEndpointBlockedError
|
|
} else {
|
|
errorCode = .networkError
|
|
}
|
|
|
|
return error(with: errorCode,
|
|
message: message ?? underlyingError?.localizedDescription,
|
|
underlyingError: underlyingError,
|
|
extraUserInfo: extraUserInfo,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an NSError with the ``ErrorCode/offlineConnection`` code.
|
|
*/
|
|
static func offlineConnectionError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: .offlineConnectionError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an NSError with the ``ErrorCode/signatureVerificationFailed`` code.
|
|
*/
|
|
static func signatureVerificationFailedError(
|
|
path: HTTPRequest.Path,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(
|
|
with: .signatureVerificationFailed,
|
|
message: "Request to \(path.relativePath) failed verification",
|
|
extraUserInfo: [
|
|
"request_path": path.relativePath
|
|
],
|
|
fileName: fileName, functionName: functionName, line: line
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Maps a ``BackendErrorCode`` code to a ``ErrorCode``. code. Constructs an Error with the mapped code and adds a
|
|
* `NSUnderlyingErrorKey` in the `NSError.userInfo` dictionary. The backend error code will be mapped using
|
|
* ``BackendErrorCode/toPurchasesErrorCode()``.
|
|
*
|
|
* - Parameter backendCode: The code of the error.
|
|
* - Parameter originalBackendErrorCode: the original numerical value of this error.
|
|
* - Parameter backendMessage: The message of the errror contained under the `NSUnderlyingErrorKey` key.
|
|
*
|
|
* - Note: This error is used when an network request returns an error. The backend error returned is wrapped in
|
|
* this internal error code.
|
|
*/
|
|
static func backendError(
|
|
withBackendCode backendCode: BackendErrorCode,
|
|
originalBackendErrorCode: Int,
|
|
backendMessage: String?,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return backendError(withBackendCode: backendCode,
|
|
originalBackendErrorCode: originalBackendErrorCode,
|
|
backendMessage: backendMessage,
|
|
extraUserInfo: [:],
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/unexpectedBackendResponseError`` code.
|
|
*
|
|
* - Note: This error is used when a network request returns an unexpected response.
|
|
*/
|
|
static func unexpectedBackendResponseError(
|
|
extraUserInfo: [NSError.UserInfoKey: Any] = [:],
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.unexpectedBackendResponseError, extraUserInfo: extraUserInfo,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/unexpectedBackendResponseError`` code which contains an underlying
|
|
* ``UnexpectedBackendResponseSubErrorCode``
|
|
*
|
|
* - Note: This error is used when a network request returns an unexpected response and we can determine some
|
|
* of what went wrong with the response.
|
|
*/
|
|
static func unexpectedBackendResponse(
|
|
withSubError subError: Error?,
|
|
extraContext: String? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return backendResponseError(withSubError: subError,
|
|
extraContext: extraContext,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/missingReceiptFileError`` code.
|
|
*
|
|
* - Note: This error is used when the receipt is missing in the device. This can happen if the user is in
|
|
* sandbox or if there are no previous purchases.
|
|
*/
|
|
static func missingReceiptFileError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.missingReceiptFileError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/emptySubscriberAttributes`` code.
|
|
*/
|
|
static func emptySubscriberAttributesError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: .emptySubscriberAttributes,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/invalidAppUserIdError`` code.
|
|
*
|
|
* - Note: This error is used when the appUserID can't be found in user defaults. This can happen if user defaults
|
|
* are removed manually or if the OS deletes entries when running out of space.
|
|
*/
|
|
static func missingAppUserIDError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.invalidAppUserIdError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/productDiscountMissingIdentifierError`` code.
|
|
*
|
|
* - Note: This error code is used when attemping to post data about product discounts but the discount is
|
|
* missing an indentifier.
|
|
*/
|
|
static func productDiscountMissingIdentifierError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.productDiscountMissingIdentifierError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/productDiscountMissingSubscriptionGroupIdentifierError`` code.
|
|
*
|
|
* - Note: This error code is used when attemping to post data about product discounts but the discount is
|
|
* missing a subscriptionGroupIndentifier.
|
|
*/
|
|
static func productDiscountMissingSubscriptionGroupIdentifierError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.productDiscountMissingSubscriptionGroupIdentifierError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/logOutAnonymousUserError`` code.
|
|
*
|
|
* - Note: This error is used when logOut is called but the current user is anonymous,
|
|
* as noted by ``Purchases/isAnonymous`` property.
|
|
*/
|
|
static func logOutAnonymousUserError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.logOutAnonymousUserError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/paymentPendingError`` code.
|
|
*
|
|
* - Note: This error is used during an “ask to buy” flow for a payment. The completion block of the purchasing
|
|
* function will get this error to indicate the guardian has to complete the purchase.
|
|
*/
|
|
static func paymentDeferredError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.paymentPendingError, message: "The payment is deferred.",
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/unknownError`` code and optional message.
|
|
*/
|
|
static func unknownError(
|
|
message: String? = nil, error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: ErrorCode.unknownError, message: message, underlyingError: error,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/operationAlreadyInProgressForProductError`` code.
|
|
*
|
|
* - Note: This error is used when a purchase is initiated for a product, but there's already a purchase for the
|
|
* same product in progress.
|
|
*/
|
|
static func operationAlreadyInProgressError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return error(with: ErrorCode.operationAlreadyInProgressForProductError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/configurationError`` code.
|
|
*
|
|
* - Note: This error is used when the configuration in App Store Connect doesn't match the configuration
|
|
* in the RevenueCat dashboard.
|
|
*/
|
|
static func configurationError(
|
|
message: String? = nil,
|
|
underlyingError: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return self.error(with: ErrorCode.configurationError,
|
|
message: message, underlyingError: underlyingError,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Maps an `SKError` to a Error with an ``ErrorCode``. Adds a underlying error in the `NSError.userInfo` dictionary.
|
|
*
|
|
* - Parameter skError: The originating `SKError`.
|
|
*/
|
|
static func purchasesError(
|
|
withSKError error: Error,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
switch error {
|
|
case let skError as SKError:
|
|
return skError.asPurchasesError
|
|
|
|
default:
|
|
return ErrorUtils.unknownError(
|
|
error: error,
|
|
fileName: fileName, functionName: functionName, line: line
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps a `StoreKitError` or `Product.PurchaseError` to an `Error` with an ``ErrorCode``.
|
|
* Adds a underlying error in the `NSError.userInfo` dictionary.
|
|
*
|
|
* - Parameter storeKitError: The originating `StoreKitError` or `Product.PurchaseError`.
|
|
*/
|
|
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
|
|
static func purchasesError(
|
|
withStoreKitError error: Error,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
switch error {
|
|
case let storeKitError as StoreKitError:
|
|
return storeKitError.asPurchasesError
|
|
case let purchasesError as Product.PurchaseError:
|
|
return purchasesError.asPurchasesError
|
|
default:
|
|
return ErrorUtils.unknownError(
|
|
error: error,
|
|
fileName: fileName, functionName: functionName, line: line
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps an untyped `Error` into a `PurchasesError`.
|
|
* If the error is already a `PurchasesError`, this simply returns the same value,
|
|
* otherwise it wraps it into a ``ErrorCode/unknownError``.
|
|
*/
|
|
static func purchasesError(
|
|
withUntypedError error: Error,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
func handlePublicError(_ error: PublicError) -> PurchasesError {
|
|
if let errorCode = ErrorCode(rawValue: error.code) {
|
|
return .init(error: errorCode, userInfo: error.userInfo)
|
|
} else {
|
|
return createUnknownError()
|
|
}
|
|
}
|
|
|
|
func createUnknownError() -> PurchasesError {
|
|
ErrorUtils.unknownError(
|
|
error: error,
|
|
fileName: fileName, functionName: functionName, line: line
|
|
)
|
|
}
|
|
|
|
if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) {
|
|
switch error {
|
|
case let purchasesError as PurchasesError:
|
|
return purchasesError
|
|
// This line crashes on iOS 12.x only (see https://github.com/RevenueCat/purchases-ios/pull/1982).
|
|
case let convertible as PurchasesErrorConvertible:
|
|
return convertible.asPurchasesError
|
|
case let error as PublicError where error.domain == ErrorCode.errorDomain:
|
|
return handlePublicError(error)
|
|
default:
|
|
return createUnknownError()
|
|
}
|
|
} else {
|
|
switch error {
|
|
case let purchasesError as PurchasesError:
|
|
return purchasesError
|
|
// `as PurchasesErrorConvertible` crashes on iOS 12.x, so these explicit casts are a workaround.
|
|
// We can't guarantee that these are exhaustive, but at least this covers the most common cases,
|
|
// and the `default` below ensures that we don't crash for the rest.
|
|
case let backendError as BackendError:
|
|
return backendError.asPurchasesError
|
|
case let networkError as NetworkError:
|
|
return networkError.asPurchasesError
|
|
case let error as PublicError where error.domain == ErrorCode.errorDomain:
|
|
return handlePublicError(error)
|
|
default:
|
|
return createUnknownError()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/purchaseCancelledError`` code.
|
|
*
|
|
* - Note: This error is used when a purchase is cancelled by the user.
|
|
*/
|
|
static func purchaseCancelledError(
|
|
error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
let errorCode = ErrorCode.purchaseCancelledError
|
|
return ErrorUtils.error(with: errorCode,
|
|
message: errorCode.description,
|
|
underlyingError: error,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/productNotAvailableForPurchaseError`` code.
|
|
*
|
|
* #### Related Articles
|
|
* - [`StoreKitError.notAvailableInStorefront`](https://rev.cat/storekit-error-not-available-in-storefront)
|
|
*/
|
|
static func productNotAvailableForPurchaseError(
|
|
error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: .productNotAvailableForPurchaseError,
|
|
underlyingError: error)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/productAlreadyPurchasedError`` code.
|
|
*/
|
|
static func productAlreadyPurchasedError(
|
|
error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: .productAlreadyPurchasedError,
|
|
underlyingError: error)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/purchaseNotAllowedError`` code.
|
|
*/
|
|
static func purchaseNotAllowedError(
|
|
error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: .purchaseNotAllowedError,
|
|
underlyingError: error)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/purchaseInvalidError`` code.
|
|
*/
|
|
static func purchaseInvalidError(
|
|
error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: .purchaseInvalidError,
|
|
underlyingError: error)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/ineligibleError`` code.
|
|
*/
|
|
static func ineligibleError(
|
|
error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: .ineligibleError,
|
|
underlyingError: error)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/ineligibleError`` code.
|
|
*/
|
|
static func invalidPromotionalOfferError(
|
|
error: Error? = nil,
|
|
message: String? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: .invalidPromotionalOfferError,
|
|
message: message,
|
|
underlyingError: error)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/storeProblemError`` code.
|
|
*
|
|
* - Note: This error is used when there is a problem with the App Store.
|
|
*/
|
|
static func storeProblemError(
|
|
withMessage message: String? = nil, error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
let errorCode = ErrorCode.storeProblemError
|
|
return ErrorUtils.error(with: errorCode,
|
|
message: message,
|
|
underlyingError: error,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/customerInfoError`` code.
|
|
*
|
|
* - Note: This error is used when there is a problem related to the customer info.
|
|
*/
|
|
static func customerInfoError(
|
|
withMessage message: String? = nil, error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
let errorCode = ErrorCode.customerInfoError
|
|
return ErrorUtils.error(with: errorCode,
|
|
message: message,
|
|
underlyingError: error,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/systemInfoError`` code.
|
|
*
|
|
* - Note: This error is used when there is a problem related to the system info.
|
|
*/
|
|
static func systemInfoError(
|
|
withMessage message: String, error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
let errorCode = ErrorCode.systemInfoError
|
|
return ErrorUtils.error(with: errorCode,
|
|
message: message,
|
|
underlyingError: error,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/beginRefundRequestError`` code.
|
|
*
|
|
* - Note: This error is used when there is a problem beginning a refund request.
|
|
*/
|
|
static func beginRefundRequestError(
|
|
withMessage message: String, error: Error? = nil,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
let errorCode = ErrorCode.beginRefundRequestError
|
|
return ErrorUtils.error(with: errorCode,
|
|
message: message,
|
|
underlyingError: error,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
/**
|
|
* Constructs an Error with the ``ErrorCode/productRequestTimedOut`` code.
|
|
*
|
|
* - Note: This error is used when fetching products times out.
|
|
*/
|
|
static func productRequestTimedOutError(
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
return ErrorUtils.error(with: .productRequestTimedOut,
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
}
|
|
|
|
extension ErrorUtils {
|
|
|
|
static func backendError(withBackendCode backendCode: BackendErrorCode,
|
|
originalBackendErrorCode: Int,
|
|
message: String? = nil,
|
|
backendMessage: String? = nil,
|
|
extraUserInfo: [NSError.UserInfoKey: Any] = [:],
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
let errorCode = backendCode.toPurchasesErrorCode()
|
|
let underlyingError = self.backendUnderlyingError(backendCode: backendCode,
|
|
originalBackendErrorCode: originalBackendErrorCode,
|
|
backendMessage: backendMessage)
|
|
|
|
return error(with: errorCode,
|
|
message: message ?? backendMessage,
|
|
underlyingError: underlyingError,
|
|
extraUserInfo: extraUserInfo + [
|
|
.backendErrorCode: originalBackendErrorCode
|
|
],
|
|
fileName: fileName, functionName: functionName, line: line)
|
|
}
|
|
|
|
}
|
|
|
|
private extension ErrorUtils {
|
|
|
|
static func error(with code: ErrorCode,
|
|
message: String? = nil,
|
|
underlyingError: Error? = nil,
|
|
extraUserInfo: [NSError.UserInfoKey: Any] = [:],
|
|
fileName: String = #fileID,
|
|
functionName: String = #function,
|
|
line: UInt = #line) -> PurchasesError {
|
|
let localizedDescription: String
|
|
|
|
if let message = message, message != code.description {
|
|
// Print both ErrorCode and message only if they're different
|
|
localizedDescription = "\(code.description) \(message)"
|
|
} else {
|
|
localizedDescription = code.description
|
|
}
|
|
|
|
var userInfo = extraUserInfo
|
|
userInfo[NSLocalizedDescriptionKey as NSError.UserInfoKey] = localizedDescription
|
|
if let underlyingError = underlyingError {
|
|
userInfo[NSUnderlyingErrorKey as NSError.UserInfoKey] = underlyingError
|
|
}
|
|
userInfo[.readableErrorCode] = code.codeName
|
|
userInfo[.file] = "\(fileName):\(line)"
|
|
userInfo[.function] = functionName
|
|
|
|
Self.logErrorIfNeeded(
|
|
code,
|
|
localizedDescription: localizedDescription,
|
|
fileName: fileName, functionName: functionName, line: line
|
|
)
|
|
|
|
return .init(error: code, userInfo: userInfo)
|
|
}
|
|
|
|
static func backendResponseError(
|
|
withSubError subError: Error?,
|
|
extraContext: String?,
|
|
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
|
|
) -> PurchasesError {
|
|
var userInfo: [NSError.UserInfoKey: Any] = [:]
|
|
let describableSubError = subError as? DescribableError
|
|
let errorDescription = describableSubError?.description ?? ErrorCode.unexpectedBackendResponseError.description
|
|
userInfo[NSLocalizedDescriptionKey as NSError.UserInfoKey] = errorDescription
|
|
userInfo[NSUnderlyingErrorKey as NSError.UserInfoKey] = subError
|
|
userInfo[.readableErrorCode] = ErrorCode.unexpectedBackendResponseError.codeName
|
|
userInfo[.extraContext] = extraContext
|
|
userInfo[.file] = "\(fileName):\(line)"
|
|
userInfo[.function] = functionName
|
|
|
|
return .init(error: .unexpectedBackendResponseError, userInfo: userInfo)
|
|
}
|
|
|
|
static func backendUnderlyingError(backendCode: BackendErrorCode,
|
|
originalBackendErrorCode: Int,
|
|
backendMessage: String?) -> NSError {
|
|
return backendCode.addingUserInfo([
|
|
NSLocalizedDescriptionKey as NSError.UserInfoKey: backendMessage ?? "",
|
|
.backendErrorCode: originalBackendErrorCode
|
|
])
|
|
}
|
|
|
|
// swiftlint:disable:next function_body_length
|
|
private static func logErrorIfNeeded(_ code: ErrorCode,
|
|
localizedDescription: String,
|
|
fileName: String = #fileID,
|
|
functionName: String = #function,
|
|
line: UInt = #line) {
|
|
switch code {
|
|
case .networkError,
|
|
.unknownError,
|
|
.receiptAlreadyInUseError,
|
|
.unexpectedBackendResponseError,
|
|
.invalidReceiptError,
|
|
.invalidAppUserIdError,
|
|
.invalidCredentialsError,
|
|
.operationAlreadyInProgressForProductError,
|
|
.unknownBackendError,
|
|
.invalidSubscriberAttributesError,
|
|
.logOutAnonymousUserError,
|
|
.receiptInUseByOtherSubscriberError,
|
|
.configurationError,
|
|
.unsupportedError,
|
|
.emptySubscriberAttributes,
|
|
.productDiscountMissingIdentifierError,
|
|
.productDiscountMissingSubscriptionGroupIdentifierError,
|
|
.customerInfoError,
|
|
.systemInfoError,
|
|
.beginRefundRequestError,
|
|
.apiEndpointBlockedError,
|
|
.invalidPromotionalOfferError,
|
|
.offlineConnectionError,
|
|
.signatureVerificationFailed:
|
|
Logger.error(
|
|
localizedDescription,
|
|
fileName: fileName,
|
|
functionName: functionName,
|
|
line: line
|
|
)
|
|
|
|
case .purchaseCancelledError,
|
|
.storeProblemError,
|
|
.purchaseNotAllowedError,
|
|
.purchaseInvalidError,
|
|
.productNotAvailableForPurchaseError,
|
|
.productAlreadyPurchasedError,
|
|
.missingReceiptFileError,
|
|
.invalidAppleSubscriptionKeyError,
|
|
.ineligibleError,
|
|
.insufficientPermissionsError,
|
|
.paymentPendingError,
|
|
.productRequestTimedOut:
|
|
Logger.appleError(
|
|
localizedDescription,
|
|
fileName: fileName,
|
|
functionName: functionName,
|
|
line: line
|
|
)
|
|
|
|
@unknown default:
|
|
Logger.error(
|
|
localizedDescription,
|
|
fileName: fileName,
|
|
functionName: functionName,
|
|
line: line
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Error {
|
|
|
|
@_disfavoredOverload
|
|
func addingUserInfo<Result: NSError>(_ userInfo: [NSError.UserInfoKey: Any]) -> Result {
|
|
return self.addingUserInfo(userInfo as [String: Any])
|
|
}
|
|
|
|
func addingUserInfo<Result: NSError>(_ userInfo: [String: Any]) -> Result {
|
|
let nsError = self as NSError
|
|
return Result.init(domain: nsError.domain,
|
|
code: nsError.code,
|
|
userInfo: nsError.userInfo + userInfo)
|
|
}
|
|
|
|
}
|
|
|
|
/// Represents where an `Error` was created
|
|
struct ErrorSource {
|
|
|
|
let file: String
|
|
let function: String
|
|
let line: UInt
|
|
|
|
}
|
|
|
|
/// `Equatable` conformance allows `Error` types that contain source information
|
|
/// to easily conform to `Equatable`.
|
|
extension ErrorSource: Equatable {
|
|
|
|
/// However, for ease of testing, we don't actually care if the source of the errors matches
|
|
/// since expectations will be created in the test and therefore will never match.
|
|
static func == (lhs: ErrorSource, rhs: ErrorSource) -> Bool {
|
|
return true
|
|
}
|
|
|
|
}
|