Skip to content

Commit

Permalink
Introduced NetworkError and BackendError (#1437)
Browse files Browse the repository at this point in the history
## Changes:

- 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`

```diff
struct HTTPResponse<Body: HTTPResponseBody> {
-   typealias Result = Swift.Result<Self, Error>
+   typealias Result = Swift.Result<Self, NetworkError>
}
```

- 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`
- Tests also become simpler:

```diff
-        expect(receivedError?.domain).toEventually(equal(RCPurchasesErrorCodeDomain))
-        expect(receivedError?.code).toEventually(
-            equal(ErrorCode.unexpectedBackendResponseError.rawValue))
-        expect(receivedUnderlyingError?.code).toEventually(
-            equal(UnexpectedBackendResponseSubErrorCode.postOfferIdMissingOffersInResponse.rawValue))
+
+        expect(receivedError) == .unexpectedBackendResponse(.postOfferIdMissingOffersInResponse)
```
- Converted `DNSError` into `NetworkError.dnsError`. Its functionality remains unchanged.
- Removed `Backend.RCSuccessfullySyncedKey` and `ErrorDetails.finishableKey` in favor of tested properties on `NetworkError`
  • Loading branch information
NachoSoto authored Apr 12, 2022
1 parent 1f28630 commit 19e6a9e
Show file tree
Hide file tree
Showing 67 changed files with 1,272 additions and 674 deletions.
24 changes: 20 additions & 4 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@
572247D127BEC28E00C524A7 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 572247D027BEC28E00C524A7 /* Array+Extensions.swift */; };
572247F727BF1ADF00C524A7 /* ArrayExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 572247F627BF1ADF00C524A7 /* ArrayExtensionsTests.swift */; };
5722482727C2BD3200C524A7 /* LockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722482627C2BD3200C524A7 /* LockTests.swift */; };
5733B18E27FF586A00EC2045 /* BackendError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5733B18D27FF586A00EC2045 /* BackendError.swift */; };
5733B1A427FF9F8300EC2045 /* NetworkErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5733B1A327FF9F8300EC2045 /* NetworkErrorTests.swift */; };
5733B1A827FFBCC800EC2045 /* BackendErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5733B1A727FFBCC800EC2045 /* BackendErrorTests.swift */; };
5733B1AA27FFBCF900EC2045 /* BaseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5733B1A927FFBCF900EC2045 /* BaseErrorTests.swift */; };
5738F46E278CAC520096D623 /* StoreTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5738F46D278CAC520096D623 /* StoreTransactionTests.swift */; };
5738F489278CC2500096D623 /* MockTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492726B9956C00D32E58 /* MockTransaction.swift */; };
5746508C27586B2E0053AB09 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5746508B27586B2E0053AB09 /* Result+Extensions.swift */; };
Expand Down Expand Up @@ -262,6 +266,7 @@
57CFB96D27FE0E79002A6730 /* MockCurrentUserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CFB96B27FE0E79002A6730 /* MockCurrentUserProvider.swift */; };
57CFB98427FE2258002A6730 /* StoreKit2Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CFB98327FE2258002A6730 /* StoreKit2Setting.swift */; };
57D04BB827D947C6006DAC06 /* HTTPResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D04BB727D947C6006DAC06 /* HTTPResponseTests.swift */; };
57D5414227F656D9004CC35C /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D5414127F656D9004CC35C /* NetworkError.swift */; };
57DC9F4627CC2E4900DA6AF9 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DC9F4527CC2E4900DA6AF9 /* HTTPRequest.swift */; };
57DC9F4A27CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DC9F4927CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift */; };
57E0473B277260DE0082FE91 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 57E0473A277260DE0082FE91 /* SnapshotTesting */; };
Expand Down Expand Up @@ -294,7 +299,6 @@
B300E4C026D4371200B22262 /* SKPaymentTransactionExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492526B994B400D32E58 /* SKPaymentTransactionExtensionsTests.swift */; };
B300E4C226D439B700B22262 /* IntroEligibilityCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E354B18710B488B8B0D443 /* IntroEligibilityCalculatorTests.swift */; };
B302206A27271BCB008F1A0D /* Decoder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B302206927271BCB008F1A0D /* Decoder+Extensions.swift */; };
B302206C2727436F008F1A0D /* UnexpectedBackendResponseSubErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B302206B2727436F008F1A0D /* UnexpectedBackendResponseSubErrorCode.swift */; };
B302206E2728B798008F1A0D /* BackendErrorStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B302206D2728B798008F1A0D /* BackendErrorStrings.swift */; };
B3022072272B3DDC008F1A0D /* DescribableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3022071272B3DDC008F1A0D /* DescribableError.swift */; };
B3083A132699334C007B5503 /* Offering.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3083A122699334C007B5503 /* Offering.swift */; };
Expand Down Expand Up @@ -640,6 +644,10 @@
572247D027BEC28E00C524A7 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
572247F627BF1ADF00C524A7 /* ArrayExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensionsTests.swift; sourceTree = "<group>"; };
5722482627C2BD3200C524A7 /* LockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockTests.swift; sourceTree = "<group>"; };
5733B18D27FF586A00EC2045 /* BackendError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendError.swift; sourceTree = "<group>"; };
5733B1A327FF9F8300EC2045 /* NetworkErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkErrorTests.swift; sourceTree = "<group>"; };
5733B1A727FFBCC800EC2045 /* BackendErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendErrorTests.swift; sourceTree = "<group>"; };
5733B1A927FFBCF900EC2045 /* BaseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseErrorTests.swift; sourceTree = "<group>"; };
5738F46D278CAC520096D623 /* StoreTransactionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreTransactionTests.swift; sourceTree = "<group>"; };
5746508B27586B2E0053AB09 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = "<group>"; };
5746508D275949F20053AB09 /* DispatchTimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchTimeInterval+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -686,6 +694,7 @@
57CFB96B27FE0E79002A6730 /* MockCurrentUserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCurrentUserProvider.swift; sourceTree = "<group>"; };
57CFB98327FE2258002A6730 /* StoreKit2Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2Setting.swift; sourceTree = "<group>"; };
57D04BB727D947C6006DAC06 /* HTTPResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponseTests.swift; sourceTree = "<group>"; };
57D5414127F656D9004CC35C /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
57DC9F4527CC2E4900DA6AF9 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = "<group>"; };
57DC9F4927CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatusCodeTests.swift; sourceTree = "<group>"; };
57E0474B27729A1E0082FE91 /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = "<group>"; };
Expand Down Expand Up @@ -713,7 +722,6 @@
A56F9AB026990E9200AFC48F /* CustomerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfo.swift; sourceTree = "<group>"; };
A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginRefundRequestHelper.swift; sourceTree = "<group>"; };
B302206927271BCB008F1A0D /* Decoder+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decoder+Extensions.swift"; sourceTree = "<group>"; };
B302206B2727436F008F1A0D /* UnexpectedBackendResponseSubErrorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnexpectedBackendResponseSubErrorCode.swift; sourceTree = "<group>"; };
B302206D2728B798008F1A0D /* BackendErrorStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendErrorStrings.swift; sourceTree = "<group>"; };
B3022071272B3DDC008F1A0D /* DescribableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescribableError.swift; sourceTree = "<group>"; };
B3083A122699334C007B5503 /* Offering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Offering.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1399,6 +1407,7 @@
B3766F1D26BDA95100141450 /* IntroEligibilityResponse.swift */,
B34605D0279A6E600031CA74 /* SubscribersAPI.swift */,
575137CE27F50D2F0064AB2C /* HTTPResponseBody.swift */,
57D5414127F656D9004CC35C /* NetworkError.swift */,
);
path = Networking;
sourceTree = "<group>";
Expand All @@ -1413,6 +1422,9 @@
576C8AD827D2BCB90058FA6E /* HTTPRequestTests.swift */,
57DC9F4927CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift */,
57D04BB727D947C6006DAC06 /* HTTPResponseTests.swift */,
5733B1A327FF9F8300EC2045 /* NetworkErrorTests.swift */,
5733B1A727FFBCC800EC2045 /* BackendErrorTests.swift */,
5733B1A927FFBCF900EC2045 /* BaseErrorTests.swift */,
);
path = Networking;
sourceTree = "<group>";
Expand Down Expand Up @@ -1562,7 +1574,7 @@
35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */,
2D971CC02744364C0093F35F /* SKError+Extensions.swift */,
57BD50A927692B7500211D6D /* StoreKitError+Extensions.swift */,
B302206B2727436F008F1A0D /* UnexpectedBackendResponseSubErrorCode.swift */,
5733B18D27FF586A00EC2045 /* BackendError.swift */,
);
path = "Error Handling";
sourceTree = "<group>";
Expand Down Expand Up @@ -2091,7 +2103,6 @@
B3022072272B3DDC008F1A0D /* DescribableError.swift in Sources */,
57BD50AA27692B7500211D6D /* StoreKitError+Extensions.swift in Sources */,
F5C0196926E880800005D61E /* StoreKitStrings.swift in Sources */,
B302206C2727436F008F1A0D /* UnexpectedBackendResponseSubErrorCode.swift in Sources */,
2D4E926526990AB1000E10B0 /* StoreKitWrapper.swift in Sources */,
2DDF419624F6F331005BC22D /* ProductsRequestFactory.swift in Sources */,
2DDF419724F6F331005BC22D /* DateExtensions.swift in Sources */,
Expand Down Expand Up @@ -2203,6 +2214,7 @@
57EAE527274324C60060EB74 /* Lock.swift in Sources */,
B32B74FF26868AEB005647BF /* Package.swift in Sources */,
2DDF41B324F6F387005BC22D /* InAppPurchaseBuilder.swift in Sources */,
57D5414227F656D9004CC35C /* NetworkError.swift in Sources */,
F5BE447B269E4A7500254A30 /* TrackingManagerProxy.swift in Sources */,
5746508C27586B2E0053AB09 /* Result+Extensions.swift in Sources */,
B34D2AA0269606E400D88C3A /* IntroEligibility.swift in Sources */,
Expand All @@ -2216,6 +2228,7 @@
9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */,
B35042C626CDD3B100905B95 /* PurchasesDelegate.swift in Sources */,
0313FD41268A506400168386 /* DateProvider.swift in Sources */,
5733B18E27FF586A00EC2045 /* BackendError.swift in Sources */,
B39E811A268E849900D31189 /* AttributionNetwork.swift in Sources */,
37E35C8515C5E2D01B0AF5C1 /* Strings.swift in Sources */,
2D9F4A5526C30CA800B07B43 /* PurchasesOrchestrator.swift in Sources */,
Expand Down Expand Up @@ -2263,6 +2276,7 @@
2DDF41E124F6F527005BC22D /* MockReceiptParser.swift in Sources */,
351B514D26D44A8600BD2BD7 /* MockHTTPClient.swift in Sources */,
5796A38E27D6BB7D00653165 /* BackendCreateAliasTests.swift in Sources */,
5733B1AA27FFBCF900EC2045 /* BaseErrorTests.swift in Sources */,
578FB10E27ADDA8000F70709 /* AvailabilityChecks.swift in Sources */,
B3CD0D8227F23705000793F5 /* BackendPostReceiptDataTestsiOS14AndAbove.swift in Sources */,
35F82BB226A98EC50051DF03 /* AttributionDataMigratorTests.swift in Sources */,
Expand Down Expand Up @@ -2312,6 +2326,7 @@
351B51B626D450E800BD2BD7 /* ReceiptFetcherTests.swift in Sources */,
5796A3C027D7D64500653165 /* ResultExtensionsTests.swift in Sources */,
351B51A726D450D400BD2BD7 /* SystemInfoTests.swift in Sources */,
5733B1A827FFBCC800EC2045 /* BackendErrorTests.swift in Sources */,
351B515626D44B2300BD2BD7 /* MockNotificationCenter.swift in Sources */,
351B515226D44AF000BD2BD7 /* MockReceiptFetcher.swift in Sources */,
351B51C226D450E800BD2BD7 /* ProductRequestDataTests.swift in Sources */,
Expand Down Expand Up @@ -2342,6 +2357,7 @@
B300E4BF26D436F900B22262 /* LogIntent.swift in Sources */,
351B513F26D4496000BD2BD7 /* MockIdentityManager.swift in Sources */,
B319514926C19856002CA9AC /* NSData+RCExtensionsTests.swift in Sources */,
5733B1A427FF9F8300EC2045 /* NetworkErrorTests.swift in Sources */,
351B517026D44E8D00BD2BD7 /* MockDateProvider.swift in Sources */,
2D1C3F3926B9D8B800112626 /* MockBundle.swift in Sources */,
351B515E26D44B9900BD2BD7 /* MockPurchasesDelegate.swift in Sources */,
Expand Down
193 changes: 193 additions & 0 deletions Sources/Error Handling/BackendError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//
// 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 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 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 .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,
.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

}
8 changes: 8 additions & 0 deletions Sources/Error Handling/BackendErrorCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ enum BackendErrorCode: Int, Error {

}

extension BackendErrorCode: ExpressibleByIntegerLiteral {

init(integerLiteral value: IntegerLiteralType) {
self = BackendErrorCode(rawValue: value) ?? .unknownBackendError
}

}

extension BackendErrorCode {

// swiftlint:disable cyclomatic_complexity
Expand Down
Loading

0 comments on commit 19e6a9e

Please sign in to comment.