diff --git a/RevenueCatUI/Data/TestData.swift b/RevenueCatUI/Data/TestData.swift index 73220396b6..2a68a3646a 100644 --- a/RevenueCatUI/Data/TestData.swift +++ b/RevenueCatUI/Data/TestData.swift @@ -440,7 +440,8 @@ internal enum TestData { #endif static let customerInfo: CustomerInfo = { - let json = """ + return .decode( + """ { "schema_version": "4", "request_date": "2022-03-08T17:42:58Z", @@ -463,13 +464,7 @@ internal enum TestData { } } """ - - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .iso8601 - - // swiftlint:disable:next force_try - return try! decoder.decode(CustomerInfo.self, from: Data(json.utf8)) + ) }() static let localization1: PaywallData.LocalizedConfiguration = .init( @@ -540,4 +535,17 @@ extension PackageType { } +extension CustomerInfo { + + static func decode(_ json: String) -> Self { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 + + // swiftlint:disable:next force_try + return try! decoder.decode(Self.self, from: Data(json.utf8)) + } + +} + #endif diff --git a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift index 3dc0bc3d02..b88004dd80 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift @@ -19,16 +19,16 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) extension PurchaseHandler { - static func mock() -> Self { + static func mock(_ customerInfo: CustomerInfo = TestData.customerInfo) -> Self { return self.init( purchases: MockPurchases { _ in return ( transaction: nil, - customerInfo: TestData.customerInfo, + customerInfo: customerInfo, userCancelled: false ) } restorePurchases: { - return TestData.customerInfo + return customerInfo } trackEvent: { event in Logger.debug("Tracking event: \(event)") } diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 3cb01a3b0c..22261d526e 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -91,8 +91,10 @@ extension PurchaseHandler { return result } + /// - Returns: `success` is `true` only when the resulting `CustomerInfo` + /// had any transactions @MainActor - func restorePurchases() async throws -> CustomerInfo { + func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { self.actionInProgress = true defer { self.actionInProgress = false } @@ -103,7 +105,8 @@ extension PurchaseHandler { self.restoredCustomerInfo = customerInfo } - return customerInfo + return (info: customerInfo, + success: customerInfo.hasActiveSubscriptionsOrNonSubscriptions) } func trackPaywallImpression(_ eventData: PaywallEvent.Data) { @@ -220,3 +223,11 @@ private extension PaywallEvent.Data { } } + +private extension CustomerInfo { + + var hasActiveSubscriptionsOrNonSubscriptions: Bool { + return !self.activeSubscriptions.isEmpty || !self.nonSubscriptions.isEmpty + } + +} diff --git a/RevenueCatUI/Views/FooterView.swift b/RevenueCatUI/Views/FooterView.swift index 82862ca2cf..2886e0a8e2 100644 --- a/RevenueCatUI/Views/FooterView.swift +++ b/RevenueCatUI/Views/FooterView.swift @@ -167,8 +167,10 @@ private struct RestorePurchasesButton: View { var body: some View { AsyncButton { - _ = try await self.purchaseHandler.restorePurchases() - self.displayRestoredAlert = true + let success = try await self.purchaseHandler.restorePurchases().success + if success { + self.displayRestoredAlert = true + } } label: { if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { ViewThatFits { diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index 24b00f4919..803839c466 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -70,6 +70,27 @@ class PurchaseCompletedHandlerTests: TestCase { expect(customerInfo).toEventually(be(TestData.customerInfo)) } + func testOnRestoreCompleted() throws { + var customerInfo: CustomerInfo? + + try PaywallView( + offering: Self.offering.withLocalImages, + customerInfo: TestData.customerInfo, + introEligibility: .producing(eligibility: .eligible), + purchaseHandler: Self.purchaseHandler + ) + .onRestoreCompleted { + customerInfo = $0 + } + .addToHierarchy() + + Task { + _ = try await Self.purchaseHandler.restorePurchases() + } + + expect(customerInfo).toEventually(be(TestData.customerInfo)) + } + private static let purchaseHandler: PurchaseHandler = .mock() private static let offering = TestData.offeringWithNoIntroOffer private static let package = TestData.annualPackage diff --git a/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift b/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift index 0c3b3af819..dbc8d0032c 100644 --- a/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift +++ b/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift @@ -55,13 +55,114 @@ class PurchaseHandlerTests: TestCase { func testRestorePurchases() async throws { let handler: PurchaseHandler = .mock() - _ = try await handler.restorePurchases() + let result = try await handler.restorePurchases() + + expect(result.info) === TestData.customerInfo + expect(result.success) == false expect(handler.restored) == true expect(handler.restoredCustomerInfo) === TestData.customerInfo expect(handler.purchasedCustomerInfo).to(beNil()) expect(handler.actionInProgress) == false } + func testRestorePurchasesWithActiveSubscriptions() async throws { + let handler: PurchaseHandler = .mock(Self.customerInfoWithSubscriptions) + + let result = try await handler.restorePurchases() + expect(result.info) === Self.customerInfoWithSubscriptions + expect(result.success) == true + } + + func testRestorePurchasesWithNonSubscriptions() async throws { + let handler: PurchaseHandler = .mock(Self.customerInfoWithNonSubscriptions) + + let result = try await handler.restorePurchases() + expect(result.info) === Self.customerInfoWithNonSubscriptions + expect(result.success) == true + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension PurchaseHandlerTests { + + static let customerInfoWithSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + } + } + } + """ + ) + }() + + static let customerInfoWithNonSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + "com.revenuecat.product.tip": [ + { + "purchase_date": "2022-02-11T00:03:28Z", + "original_purchase_date": "2022-03-10T00:04:28Z", + "id": "17459f5ff7", + "store_transaction_id": "340001090153249", + "store": "app_store", + "is_sandbox": false + } + ] + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + }, + "entitlements": { + } + } + } + """ + ) + }() + } #endif