Files
purchases-ios/Sources/Error Handling/BackendErrorCode.swift
T
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

125 lines
4.0 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
//
// BackendErrorCode.swift
//
// Created by Joshua Liebowitz on 7/12/21.
//
import Foundation
/**
Error codes sent by the RevenueCat backend. This only includes the errors that matter to the SDK
*/
enum BackendErrorCode: Int, Error {
case unknownBackendError = -1 // Some backend problem we don't know the specifics of.
case unknownError = 0 // We don't know what happened.
case invalidPlatform = 7000
case storeProblem = 7101
case cannotTransferPurchase = 7102
case invalidReceiptToken = 7103
case invalidAppStoreSharedSecret = 7104
case invalidPaymentModeOrIntroPriceNotProvided = 7105
case productIdForGoogleReceiptNotProvided = 7106
case invalidPlayStoreCredentials = 7107
case internalServerError = 7110
case emptyAppUserId = 7220
case invalidAuthToken = 7224
case invalidAPIKey = 7225
case badRequest = 7226
case playStoreQuotaExceeded = 7229
case playStoreInvalidPackageName = 7230
case playStoreGenericError = 7231
case userIneligibleForPromoOffer = 7232
case invalidAppleSubscriptionKey = 7234
case subscriptionNotFoundForCustomer = 7259
case invalidSubscriberAttributes = 7263
case invalidSubscriberAttributesBody = 7264
/**
* - Parameter code: Generally comes from the backend in json. This may be a String, or an Int, or nothing.
*/
init(code: Any?) {
let codeInt = BackendErrorCode.extractCodeNumber(from: code)
guard let codeInt = codeInt else {
self = .unknownBackendError
return
}
self = BackendErrorCode(rawValue: codeInt) ?? .unknownBackendError
}
static func extractCodeNumber(from codeObject: Any?) -> Int? {
// The code can be a String or Int
if let codeString = codeObject as? String {
return Int(codeString) ?? nil
}
return codeObject as? Int
}
}
extension BackendErrorCode: ExpressibleByIntegerLiteral {
init(integerLiteral value: IntegerLiteralType) {
self = BackendErrorCode(rawValue: value) ?? .unknownBackendError
}
}
extension BackendErrorCode {
// swiftlint:disable cyclomatic_complexity
/// Turns ``BackendErrorCode``(RCBackendErrorCode) codes into ``ErrorCode``(RCPurchasesErrorCode) error codes
func toPurchasesErrorCode() -> ErrorCode {
// swiftlint:enable cyclomatic_complexity
switch self {
case .invalidPlatform:
return .configurationError
case .storeProblem:
return .storeProblemError
case .cannotTransferPurchase:
return .receiptAlreadyInUseError
case .invalidReceiptToken:
return .invalidReceiptError
case .invalidAppStoreSharedSecret,
.invalidAuthToken,
.invalidAPIKey:
return .invalidCredentialsError
case .invalidPaymentModeOrIntroPriceNotProvided,
.productIdForGoogleReceiptNotProvided:
return .purchaseInvalidError
case .emptyAppUserId:
return .invalidAppUserIdError
case .invalidAppleSubscriptionKey:
return .invalidAppleSubscriptionKeyError
case .userIneligibleForPromoOffer:
return .ineligibleError
case .invalidSubscriberAttributes,
.invalidSubscriberAttributesBody:
return .invalidSubscriberAttributesError
case .unknownBackendError,
.playStoreInvalidPackageName,
.playStoreQuotaExceeded,
.playStoreGenericError,
.invalidPlayStoreCredentials,
.subscriptionNotFoundForCustomer,
.badRequest,
.internalServerError:
return .unknownBackendError
case .unknownError:
return .unknownError
}
}
}