diff --git a/PurchasesCoreSwift/Misc/DateExtensions.swift b/PurchasesCoreSwift/Misc/DateExtensions.swift index 347f979332..0137f88961 100644 --- a/PurchasesCoreSwift/Misc/DateExtensions.swift +++ b/PurchasesCoreSwift/Misc/DateExtensions.swift @@ -5,10 +5,23 @@ import Foundation +enum DateExtensionsError: Error { + case invalidDateComponents(_ dateComponents: DateComponents) +} + +extension DateExtensionsError: CustomStringConvertible { + public var description: String { + switch self { + case .invalidDateComponents(let dateComponents): + return "invalid date components: \(dateComponents.description)" + } + } +} + extension Date { // swiftlint:disable:next function_parameter_count - static func from(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) -> Date { + static func from(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) throws -> Date { let calendar = Calendar(identifier: .gregorian) var dateComponents = DateComponents() dateComponents.year = year @@ -17,7 +30,9 @@ extension Date { dateComponents.hour = hour dateComponents.minute = minute dateComponents.second = second - guard let date = calendar.date(from: dateComponents) else { fatalError() } + guard let date = calendar.date(from: dateComponents) else { + throw DateExtensionsError.invalidDateComponents(dateComponents) + } return date } } diff --git a/PurchasesCoreSwift/Purchasing/ProductsManager.swift b/PurchasesCoreSwift/Purchasing/ProductsManager.swift index a35c3538e3..92511b4130 100644 --- a/PurchasesCoreSwift/Purchasing/ProductsManager.swift +++ b/PurchasesCoreSwift/Purchasing/ProductsManager.swift @@ -54,10 +54,15 @@ extension ProductsManager: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { queue.async { [self] in Logger.rcSuccess(Strings.network.skproductsrequest_received_response) - guard let requestProducts = self.productsByRequests[request] else { fatalError("couldn't find request") } + guard let requestProducts = self.productsByRequests[request] else { + Logger.error("requested products not found for request: \(request)") + return + } guard let completionBlocks = self.completionHandlers[requestProducts] else { - fatalError("couldn't find completion") + Logger.error("callback not found for failing request: \(request)") + return } + self.completionHandlers.removeValue(forKey: requestProducts) self.productsByRequests.removeValue(forKey: request) @@ -76,9 +81,13 @@ extension ProductsManager: SKProductsRequestDelegate { func request(_ request: SKRequest, didFailWithError error: Error) { queue.async { [self] in Logger.appleError(String(format: Strings.network.skproductsrequest_failed, error.localizedDescription)) - guard let products = self.productsByRequests[request] else { fatalError("couldn't find request") } + guard let products = self.productsByRequests[request] else { + Logger.error("requested products not found for request: \(request)") + return + } guard let completionBlocks = self.completionHandlers[products] else { - fatalError("couldn't find completion") + Logger.error("callback not found for failing request: \(request)") + return } self.completionHandlers.removeValue(forKey: products) diff --git a/PurchasesCoreSwift/Purchasing/TransactionsFactory.swift b/PurchasesCoreSwift/Purchasing/TransactionsFactory.swift index f3660e65fe..c14a0aed7c 100644 --- a/PurchasesCoreSwift/Purchasing/TransactionsFactory.swift +++ b/PurchasesCoreSwift/Purchasing/TransactionsFactory.swift @@ -15,7 +15,7 @@ import Foundation subscriptionsData.flatMap { (productId: String, transactionData: [[String: Any]]) -> [Transaction] in transactionData.map { Transaction(with: $0, productId: productId, dateFormatter: dateFormatter) - } + }.compactMap { $0 } }.sorted { $0.purchaseDate < $1.purchaseDate } diff --git a/PurchasesCoreSwift/Transaction.swift b/PurchasesCoreSwift/Transaction.swift index 5f38818f85..6b1821819f 100644 --- a/PurchasesCoreSwift/Transaction.swift +++ b/PurchasesCoreSwift/Transaction.swift @@ -21,14 +21,13 @@ import Foundation super.init() } - init(with serverResponse: [String: Any], productId: String, dateFormatter: DateFormatter) { + init?(with serverResponse: [String: Any], productId: String, dateFormatter: DateFormatter) { guard let revenueCatId = serverResponse["id"] as? String, let dateString = serverResponse["purchase_date"] as? String, let purchaseDate = dateFormatter.date(from: dateString) else { - fatalError(""" - Couldn't initialize Transaction from dictionary. - Reason: unexpected format. Dictionary: \(serverResponse). - """) + Logger.error("Couldn't initialize Transaction from dictionary. " + + "Reason: unexpected format. Dictionary: \(serverResponse).") + return nil } self.revenueCatId = revenueCatId diff --git a/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/AppleReceiptBuilderTests.swift b/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/AppleReceiptBuilderTests.swift index 51c9a9dc2c..a3d744973f 100644 --- a/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/AppleReceiptBuilderTests.swift +++ b/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/AppleReceiptBuilderTests.swift @@ -11,7 +11,7 @@ class AppleReceiptBuilderTests: XCTestCase { let bundleId = "com.revenuecat.test" let applicationVersion = "3.2.1" let originalApplicationVersion = "1.2.2" - let creationDate = Date.from(year: 2020, month: 3, day: 23, hour: 15, minute: 5, second: 3) + let creationDate = try! Date.from(year: 2020, month: 3, day: 23, hour: 15, minute: 5, second: 3) override func setUp() { super.setUp() @@ -61,7 +61,7 @@ class AppleReceiptBuilderTests: XCTestCase { } func testBuildGetsExpiresDate() { - let expirationDate = Date.from(year: 2020, month: 7, day: 4, hour: 5, minute: 3, second: 2) + let expirationDate = try! Date.from(year: 2020, month: 7, day: 4, hour: 5, minute: 3, second: 2) let expirationDateContainer = containerFactory.receiptAttributeContainer(attributeType: ReceiptAttributeType.expirationDate, expirationDate) diff --git a/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/InAppPurchaseBuilderTests.swift b/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/InAppPurchaseBuilderTests.swift index ef9d4f3868..b7d0105fcd 100644 --- a/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/InAppPurchaseBuilderTests.swift +++ b/PurchasesCoreSwiftTests/LocalReceiptParsing/Builders/InAppPurchaseBuilderTests.swift @@ -15,10 +15,10 @@ class InAppPurchaseBuilderTests: XCTestCase { let transactionId = "089230953203" let originalTransactionId = "089230953101" let productType = InAppPurchaseProductType.autoRenewableSubscription - let purchaseDate = Date.from(year: 2019, month: 5, day: 3, hour: 1, minute: 55, second: 1) - let originalPurchaseDate = Date.from(year: 2018, month: 6, day: 22, hour: 1, minute: 55, second: 1) - let expiresDate = Date.from(year: 2018, month: 6, day: 22, hour: 1, minute: 55, second: 1) - let cancellationDate = Date.from(year: 2019, month: 7, day: 4, hour: 7, minute: 1, second: 45) + let purchaseDate = try! Date.from(year: 2019, month: 5, day: 3, hour: 1, minute: 55, second: 1) + let originalPurchaseDate = try! Date.from(year: 2018, month: 6, day: 22, hour: 1, minute: 55, second: 1) + let expiresDate = try! Date.from(year: 2018, month: 6, day: 22, hour: 1, minute: 55, second: 1) + let cancellationDate = try! Date.from(year: 2019, month: 7, day: 4, hour: 7, minute: 1, second: 45) let isInTrialPeriod = false let isInIntroOfferPeriod = true let webOrderLineItemId = Int64(897501072) diff --git a/PurchasesCoreSwiftTests/Purchasing/TransactionsFactoryTests.swift b/PurchasesCoreSwiftTests/Purchasing/TransactionsFactoryTests.swift index a93b2ea93a..91189eafdb 100644 --- a/PurchasesCoreSwiftTests/Purchasing/TransactionsFactoryTests.swift +++ b/PurchasesCoreSwiftTests/Purchasing/TransactionsFactoryTests.swift @@ -81,4 +81,27 @@ class TransactionsFactoryTests: XCTestCase { expect(list).to(beEmpty()) } + func testBuildsCorrectlyEvenIfSomeTransactionsCantBeBuilt() { + let subscriptionsData = [ + "lifetime_access": [ + [ + "id": "d6c097ba74", + "is_sandbox": true, + "original_purchase_date": "2018-07-11T18:36:20Z", + "purchase_date": "2018-07-11T18:36:20Z", + "store": "app_store" + ] + ], + "invalid_non_transaction": [ + [ + "ioasgioaew": 0832 + ] + ] + ] + + let nonSubscriptionTransactions = transactionsFactory.nonSubscriptionTransactions(withSubscriptionsData: subscriptionsData, dateFormatter: dateFormatter) + expect(nonSubscriptionTransactions.count) == 1 + expect(nonSubscriptionTransactions.first!.productId) == "lifetime_access" + } + }