From da97daaa02e49ca047e60c6f8474bc761ceccb3c Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 21 Nov 2024 18:22:13 +0100 Subject: [PATCH 01/19] adds SubscriptionInfo --- RevenueCat.xcodeproj/project.pbxproj | 4 + Sources/Identity/CustomerInfo.swift | 20 +++++ Sources/Identity/SubscriptionInfo.swift | 80 +++++++++++++++++++ .../Responses/CustomerInfoResponse.swift | 10 ++- 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 Sources/Identity/SubscriptionInfo.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 1125674632..314967ea80 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -294,6 +294,7 @@ 35D83300262FAD8000E60AC5 /* ETagManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D832FF262FAD8000E60AC5 /* ETagManagerTests.swift */; }; 35D8330A262FBA9A00E60AC5 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E357D16038F07915D7825D /* MockUserDefaults.swift */; }; 35D83312262FBD4200E60AC5 /* MockETagManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D83311262FBD4200E60AC5 /* MockETagManager.swift */; }; + 35DE0DB62CEF9E8F00EB83E9 /* SubscriptionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35DE0DB52CEF9E8C00EB83E9 /* SubscriptionInfo.swift */; }; 35E840CC270FB70D00899AE2 /* ManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */; }; 35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */; }; 35F249CA2C493D970058993A /* LoadPromotionalOfferUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F249C92C493D970058993A /* LoadPromotionalOfferUseCase.swift */; }; @@ -1536,6 +1537,7 @@ 35D832F3262E606500E60AC5 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; 35D832FF262FAD8000E60AC5 /* ETagManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ETagManagerTests.swift; sourceTree = ""; }; 35D83311262FBD4200E60AC5 /* MockETagManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockETagManager.swift; sourceTree = ""; }; + 35DE0DB52CEF9E8C00EB83E9 /* SubscriptionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfo.swift; sourceTree = ""; }; 35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityCheckerSK1Tests.swift; sourceTree = ""; }; 35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsHelper.swift; sourceTree = ""; }; 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockManageSubscriptionsHelper.swift; sourceTree = ""; }; @@ -4767,6 +4769,7 @@ B3A36AAC26BC76230059EDEA /* Identity */ = { isa = PBXGroup; children = ( + 35DE0DB52CEF9E8C00EB83E9 /* SubscriptionInfo.swift */, A56F9AB026990E9200AFC48F /* CustomerInfo.swift */, 57F3C10429B7B22E0004FD7E /* CustomerInfo+ActiveDates.swift */, 4F15B4A02A6774C9005BEFE8 /* CustomerInfo+NonSubscriptions.swift */, @@ -5733,6 +5736,7 @@ FD43D2FC2C41864000077235 /* TimeInterval+Extensions.swift in Sources */, 4F7DBFBD2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift in Sources */, 5766AB4728401B8400FA6091 /* PackageType.swift in Sources */, + 35DE0DB62CEF9E8F00EB83E9 /* SubscriptionInfo.swift in Sources */, B3F3E8DA277158FE0047A5B9 /* DNSChecker.swift in Sources */, A525BF4B26C320D100C354C4 /* SubscriberAttributesManager.swift in Sources */, 2D1015DA275959840086173F /* StoreTransaction.swift in Sources */, diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index a7bd7cae06..285ead5300 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -88,6 +88,9 @@ import Foundation */ @objc public let originalApplicationVersion: String? + /// Dictionary of all subscription product identifiers and their subscription info + @objc public let subscriptions: [String: SubscriptionInfo] + /// Get the expiration date for a given product identifier. You should use Entitlements though! /// - Parameter productIdentifier: Product identifier for product /// - Returns: The expiration date for `productIdentifier`, `nil` if product never purchased @@ -208,6 +211,23 @@ import Foundation self.purchaseDatesByProductId = Self.extractPurchaseDates(subscriber) self.allPurchasedProductIdentifiers = Set(self.expirationDatesByProductId.keys) .union(self.nonSubscriptions.map { $0.productIdentifier }) + + self.subscriptions = subscriber.subscriptions.mapValues { subscriptionData in + SubscriptionInfo( + purchaseDate: subscriptionData.purchaseDate, + originalPurchaseDate: subscriptionData.originalPurchaseDate, + expiresDate: subscriptionData.expiresDate, + store: subscriptionData.store, + isSandbox: subscriptionData.isSandbox, + unsubscribeDetectedAt: subscriptionData.unsubscribeDetectedAt, + billingIssuesDetectedAt: subscriptionData.billingIssuesDetectedAt, + gracePeriodExpiresDate: subscriptionData.gracePeriodExpiresDate, + ownershipType: subscriptionData.ownershipType, + periodType: subscriptionData.periodType, + refundedAt: subscriptionData.refundedAt, + storeTransactionId: subscriptionData.storeTransactionId + ) + } } private let expirationDatesByProductId: [String: Date?] diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift new file mode 100644 index 0000000000..7921333e4c --- /dev/null +++ b/Sources/Identity/SubscriptionInfo.swift @@ -0,0 +1,80 @@ +// +// 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 +// +// SubscriptionInfo.swift +// +// Created by Cesar de la Vega on 21/11/24. + +import Foundation + +@objc(RCSubscriptionInfo) public final class SubscriptionInfo: NSObject { + + @objc public let purchaseDate: Date? + @objc public let originalPurchaseDate: Date? + @objc public let expiresDate: Date? + @objc public let store: Store + @objc public let isSandbox: Bool + @objc public let unsubscribeDetectedAt: Date? + @objc public let billingIssuesDetectedAt: Date? + @objc public let gracePeriodExpiresDate: Date? + @objc public let ownershipType: PurchaseOwnershipType + @objc public let periodType: PeriodType + @objc public let refundedAt: Date? + @objc public let storeTransactionId: String + + init(purchaseDate: Date?, + originalPurchaseDate: Date?, + expiresDate: Date?, + store: Store, + isSandbox: Bool, + unsubscribeDetectedAt: Date?, + billingIssuesDetectedAt: Date?, + gracePeriodExpiresDate: Date?, + ownershipType: PurchaseOwnershipType, + periodType: PeriodType, + refundedAt: Date?, + storeTransactionId: String) { + self.purchaseDate = purchaseDate + self.originalPurchaseDate = originalPurchaseDate + self.expiresDate = expiresDate + self.store = store + self.isSandbox = isSandbox + self.unsubscribeDetectedAt = unsubscribeDetectedAt + self.billingIssuesDetectedAt = billingIssuesDetectedAt + self.gracePeriodExpiresDate = gracePeriodExpiresDate + self.ownershipType = ownershipType + self.periodType = periodType + self.refundedAt = refundedAt + self.storeTransactionId = storeTransactionId + + super.init() + } + + public override var description: String { + return """ + SubscriptionInfo { + purchaseDate: \(String(describing: purchaseDate)), + originalPurchaseDate: \(String(describing: originalPurchaseDate)), + expiresDate: \(String(describing: expiresDate)), + store: \(store), + isSandbox: \(isSandbox), + unsubscribeDetectedAt: \(String(describing: unsubscribeDetectedAt)), + billingIssuesDetectedAt: \(String(describing: billingIssuesDetectedAt)), + gracePeriodExpiresDate: \(String(describing: gracePeriodExpiresDate)), + ownershipType: \(ownershipType), + periodType: \(String(describing: periodType)), + refundedAt: \(String(describing: refundedAt)), + storeTransactionId: \(storeTransactionId) + } + """ + } + +} + +extension SubscriptionInfo: Sendable {} diff --git a/Sources/Networking/Responses/CustomerInfoResponse.swift b/Sources/Networking/Responses/CustomerInfoResponse.swift index 3d3513fe4b..79fa93c95e 100644 --- a/Sources/Networking/Responses/CustomerInfoResponse.swift +++ b/Sources/Networking/Responses/CustomerInfoResponse.swift @@ -49,7 +49,9 @@ extension CustomerInfoResponse { @IgnoreDecodeErrors var periodType: PeriodType + // TODO: verify if nullable var purchaseDate: Date? + // TODO: verify if nullable var originalPurchaseDate: Date? var expiresDate: Date? @IgnoreDecodeErrors @@ -62,6 +64,10 @@ extension CustomerInfoResponse { var ownershipType: PurchaseOwnershipType var productPlanIdentifier: String? var metadata: [String: String]? + var gracePeriodExpiresDate: Date? + var refundedAt: Date? + // TODO: verify if nullable + var storeTransactionId: String } @@ -209,7 +215,8 @@ extension CustomerInfoResponse.Subscription { isSandbox: Bool, unsubscribeDetectedAt: Date? = nil, billingIssuesDetectedAt: Date? = nil, - ownershipType: PurchaseOwnershipType = .defaultValue + ownershipType: PurchaseOwnershipType = .defaultValue, + storeTransactionId: String = "" ) { self.periodType = periodType self.purchaseDate = purchaseDate @@ -220,6 +227,7 @@ extension CustomerInfoResponse.Subscription { self.unsubscribeDetectedAt = unsubscribeDetectedAt self.billingIssuesDetectedAt = billingIssuesDetectedAt self.ownershipType = ownershipType + self.storeTransactionId = storeTransactionId } var asTransaction: CustomerInfoResponse.Transaction { From 3cbefb90c1b2c304404ca2e6d4ad8791b5e6b510 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 22 Nov 2024 14:46:38 +0100 Subject: [PATCH 02/19] fix nullabilities --- Sources/Networking/Responses/CustomerInfoResponse.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/Networking/Responses/CustomerInfoResponse.swift b/Sources/Networking/Responses/CustomerInfoResponse.swift index 79fa93c95e..de5fc30f57 100644 --- a/Sources/Networking/Responses/CustomerInfoResponse.swift +++ b/Sources/Networking/Responses/CustomerInfoResponse.swift @@ -49,9 +49,7 @@ extension CustomerInfoResponse { @IgnoreDecodeErrors var periodType: PeriodType - // TODO: verify if nullable - var purchaseDate: Date? - // TODO: verify if nullable + var purchaseDate: Date var originalPurchaseDate: Date? var expiresDate: Date? @IgnoreDecodeErrors @@ -66,8 +64,7 @@ extension CustomerInfoResponse { var metadata: [String: String]? var gracePeriodExpiresDate: Date? var refundedAt: Date? - // TODO: verify if nullable - var storeTransactionId: String + var storeTransactionId: String? } From 01cd40b77034a5d0ad174d9ca2ffa4a1271db83f Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 22 Nov 2024 15:21:56 +0100 Subject: [PATCH 03/19] purchaseDate is not nullable --- Sources/Identity/SubscriptionInfo.swift | 8 ++++---- Sources/Networking/Responses/CustomerInfoResponse.swift | 6 +++--- Sources/Purchasing/NonSubscriptionTransaction.swift | 5 ++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index 7921333e4c..6ff7411985 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -15,7 +15,7 @@ import Foundation @objc(RCSubscriptionInfo) public final class SubscriptionInfo: NSObject { - @objc public let purchaseDate: Date? + @objc public let purchaseDate: Date @objc public let originalPurchaseDate: Date? @objc public let expiresDate: Date? @objc public let store: Store @@ -26,9 +26,9 @@ import Foundation @objc public let ownershipType: PurchaseOwnershipType @objc public let periodType: PeriodType @objc public let refundedAt: Date? - @objc public let storeTransactionId: String + @objc public let storeTransactionId: String? - init(purchaseDate: Date?, + init(purchaseDate: Date, originalPurchaseDate: Date?, expiresDate: Date?, store: Store, @@ -39,7 +39,7 @@ import Foundation ownershipType: PurchaseOwnershipType, periodType: PeriodType, refundedAt: Date?, - storeTransactionId: String) { + storeTransactionId: String?) { self.purchaseDate = purchaseDate self.originalPurchaseDate = originalPurchaseDate self.expiresDate = expiresDate diff --git a/Sources/Networking/Responses/CustomerInfoResponse.swift b/Sources/Networking/Responses/CustomerInfoResponse.swift index de5fc30f57..0f22d1f8a9 100644 --- a/Sources/Networking/Responses/CustomerInfoResponse.swift +++ b/Sources/Networking/Responses/CustomerInfoResponse.swift @@ -70,7 +70,7 @@ extension CustomerInfoResponse { struct Transaction { - var purchaseDate: Date? + var purchaseDate: Date var originalPurchaseDate: Date? var transactionIdentifier: String? var storeTransactionIdentifier: String? @@ -177,7 +177,7 @@ extension CustomerInfoResponse.Subscriber { extension CustomerInfoResponse.Transaction { init( - purchaseDate: Date?, + purchaseDate: Date, originalPurchaseDate: Date?, transactionIdentifier: String?, storeTransactionIdentifier: String?, @@ -205,7 +205,7 @@ extension CustomerInfoResponse.Subscription { init( periodType: PeriodType = .defaultValue, - purchaseDate: Date? = nil, + purchaseDate: Date, originalPurchaseDate: Date? = nil, expiresDate: Date? = nil, store: Store = .defaultValue, diff --git a/Sources/Purchasing/NonSubscriptionTransaction.swift b/Sources/Purchasing/NonSubscriptionTransaction.swift index a6b66406be..65e2c44518 100644 --- a/Sources/Purchasing/NonSubscriptionTransaction.swift +++ b/Sources/Purchasing/NonSubscriptionTransaction.swift @@ -36,8 +36,7 @@ public final class NonSubscriptionTransaction: NSObject { init?(with transaction: CustomerInfoResponse.Transaction, productID: String) { guard let transactionIdentifier = transaction.transactionIdentifier, - let storeTransactionIdentifier = transaction.storeTransactionIdentifier, - let purchaseDate = transaction.purchaseDate else { + let storeTransactionIdentifier = transaction.storeTransactionIdentifier else { Logger.error("Couldn't initialize NonSubscriptionTransaction. " + "Reason: missing data: \(transaction).") return nil @@ -45,7 +44,7 @@ public final class NonSubscriptionTransaction: NSObject { self.transactionIdentifier = transactionIdentifier self.storeTransactionIdentifier = storeTransactionIdentifier - self.purchaseDate = purchaseDate + self.purchaseDate = transaction.purchaseDate self.productIdentifier = productID } From b40eca3ee50fc26f529886b840e2413c225ffb72 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 22 Nov 2024 15:37:34 +0100 Subject: [PATCH 04/19] add isActive --- Sources/Identity/CustomerInfo.swift | 3 ++- Sources/Identity/SubscriptionInfo.swift | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index 285ead5300..1bfab6ebb4 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -225,7 +225,8 @@ import Foundation ownershipType: subscriptionData.ownershipType, periodType: subscriptionData.periodType, refundedAt: subscriptionData.refundedAt, - storeTransactionId: subscriptionData.storeTransactionId + storeTransactionId: subscriptionData.storeTransactionId, + requestDate: response.requestDate ) } } diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index 6ff7411985..64c09ea4a2 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -27,6 +27,7 @@ import Foundation @objc public let periodType: PeriodType @objc public let refundedAt: Date? @objc public let storeTransactionId: String? + @objc public let isActive: Bool init(purchaseDate: Date, originalPurchaseDate: Date?, @@ -39,7 +40,8 @@ import Foundation ownershipType: PurchaseOwnershipType, periodType: PeriodType, refundedAt: Date?, - storeTransactionId: String?) { + storeTransactionId: String?, + requestDate: Date) { self.purchaseDate = purchaseDate self.originalPurchaseDate = originalPurchaseDate self.expiresDate = expiresDate @@ -52,6 +54,7 @@ import Foundation self.periodType = periodType self.refundedAt = refundedAt self.storeTransactionId = storeTransactionId + self.isActive = CustomerInfo.isDateActive(expirationDate: expiresDate, for: requestDate) super.init() } @@ -70,7 +73,8 @@ import Foundation ownershipType: \(ownershipType), periodType: \(String(describing: periodType)), refundedAt: \(String(describing: refundedAt)), - storeTransactionId: \(storeTransactionId) + storeTransactionId: \(String(describing: storeTransactionId)), + isActive: \(isActive) } """ } From c32aec0046143824085071480b52a686f6819ca9 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 22 Nov 2024 16:09:45 +0100 Subject: [PATCH 05/19] add store --- Sources/Purchasing/NonSubscriptionTransaction.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Purchasing/NonSubscriptionTransaction.swift b/Sources/Purchasing/NonSubscriptionTransaction.swift index 65e2c44518..5bbed0d84d 100644 --- a/Sources/Purchasing/NonSubscriptionTransaction.swift +++ b/Sources/Purchasing/NonSubscriptionTransaction.swift @@ -34,6 +34,11 @@ public final class NonSubscriptionTransaction: NSObject { /// The unique identifier for the transaction created by the Store. @objc public let storeTransactionIdentifier: String + /** + * The ``Store``tore where this transaction was performed from. + */ + @objc public let store: Store + init?(with transaction: CustomerInfoResponse.Transaction, productID: String) { guard let transactionIdentifier = transaction.transactionIdentifier, let storeTransactionIdentifier = transaction.storeTransactionIdentifier else { @@ -46,6 +51,7 @@ public final class NonSubscriptionTransaction: NSObject { self.storeTransactionIdentifier = storeTransactionIdentifier self.purchaseDate = transaction.purchaseDate self.productIdentifier = productID + self.store = transaction.store } public override var description: String { From 8e67e052577d9ea0ad0774c63f322a97faa758d1 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 22 Nov 2024 16:57:11 +0100 Subject: [PATCH 06/19] add the product identifier --- Sources/Identity/CustomerInfo.swift | 9 +++++---- Sources/Identity/SubscriptionInfo.swift | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index 1bfab6ebb4..f6c0c71d87 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -212,8 +212,9 @@ import Foundation self.allPurchasedProductIdentifiers = Set(self.expirationDatesByProductId.keys) .union(self.nonSubscriptions.map { $0.productIdentifier }) - self.subscriptions = subscriber.subscriptions.mapValues { subscriptionData in - SubscriptionInfo( + self.subscriptions = Dictionary(uniqueKeysWithValues: subscriber.subscriptions.map { (key, subscriptionData) in + (key, SubscriptionInfo( + productIdentifier: key, purchaseDate: subscriptionData.purchaseDate, originalPurchaseDate: subscriptionData.originalPurchaseDate, expiresDate: subscriptionData.expiresDate, @@ -227,8 +228,8 @@ import Foundation refundedAt: subscriptionData.refundedAt, storeTransactionId: subscriptionData.storeTransactionId, requestDate: response.requestDate - ) - } + )) + }) } private let expirationDatesByProductId: [String: Date?] diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index 64c09ea4a2..6444c4cf71 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -15,6 +15,8 @@ import Foundation @objc(RCSubscriptionInfo) public final class SubscriptionInfo: NSObject { + /// The product identifier. + @objc public let productIdentifier: String @objc public let purchaseDate: Date @objc public let originalPurchaseDate: Date? @objc public let expiresDate: Date? @@ -29,7 +31,8 @@ import Foundation @objc public let storeTransactionId: String? @objc public let isActive: Bool - init(purchaseDate: Date, + init(productIdentifier: String, + purchaseDate: Date, originalPurchaseDate: Date?, expiresDate: Date?, store: Store, @@ -42,6 +45,7 @@ import Foundation refundedAt: Date?, storeTransactionId: String?, requestDate: Date) { + self.productIdentifier = productIdentifier self.purchaseDate = purchaseDate self.originalPurchaseDate = originalPurchaseDate self.expiresDate = expiresDate From 0f107592e711a2efba9c60829350c0b21f07debc Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 22 Nov 2024 17:24:28 +0100 Subject: [PATCH 07/19] add willRenew --- Sources/Identity/SubscriptionInfo.swift | 8 +++++++- Sources/Purchasing/EntitlementInfo.swift | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index 6444c4cf71..e6a2839539 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -30,6 +30,7 @@ import Foundation @objc public let refundedAt: Date? @objc public let storeTransactionId: String? @objc public let isActive: Bool + @objc public let willRenew: Bool init(productIdentifier: String, purchaseDate: Date, @@ -59,6 +60,10 @@ import Foundation self.refundedAt = refundedAt self.storeTransactionId = storeTransactionId self.isActive = CustomerInfo.isDateActive(expirationDate: expiresDate, for: requestDate) + self.willRenew = EntitlementInfo.willRenewWithExpirationDate(expirationDate: expiresDate, + store: store, + unsubscribeDetectedAt: unsubscribeDetectedAt, + billingIssueDetectedAt: billingIssuesDetectedAt) super.init() } @@ -78,7 +83,8 @@ import Foundation periodType: \(String(describing: periodType)), refundedAt: \(String(describing: refundedAt)), storeTransactionId: \(String(describing: storeTransactionId)), - isActive: \(isActive) + isActive: \(isActive), + willRenew: \(willRenew) } """ } diff --git a/Sources/Purchasing/EntitlementInfo.swift b/Sources/Purchasing/EntitlementInfo.swift index 987584e773..2f24ff4e5b 100644 --- a/Sources/Purchasing/EntitlementInfo.swift +++ b/Sources/Purchasing/EntitlementInfo.swift @@ -299,7 +299,7 @@ public extension EntitlementInfo { // MARK: - Internal -private extension EntitlementInfo { +extension EntitlementInfo { static func willRenewWithExpirationDate(expirationDate: Date?, store: Store, From 088bf60d638abdb3fd71fa47087be56dea02ab34 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 25 Nov 2024 14:59:21 +0100 Subject: [PATCH 08/19] private extension --- .../Purchasing/CustomerInfoTests.swift | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/Tests/UnitTests/Purchasing/CustomerInfoTests.swift b/Tests/UnitTests/Purchasing/CustomerInfoTests.swift index 7fb725a951..a735504d58 100644 --- a/Tests/UnitTests/Purchasing/CustomerInfoTests.swift +++ b/Tests/UnitTests/Purchasing/CustomerInfoTests.swift @@ -22,14 +22,6 @@ class EmptyCustomerInfoTests: TestCase { class BasicCustomerInfoTests: TestCase { - private static func date(withDaysAgo days: Int) throws -> Date { - return try XCTUnwrap(Calendar.current.date(byAdding: .day, value: days, to: Date())) - } - - private static let expiredSubscriptionDate = ISO8601DateFormatter.default.string( - // swiftlint:disable:next force_try - from: try! BasicCustomerInfoTests.date(withDaysAgo: -1) - ) static let validSubscriberResponse: [String: Any] = [ "request_date": "2018-10-19T02:40:36Z", "request_date_ms": Int64(1563379533946), @@ -915,9 +907,35 @@ class BasicCustomerInfoTests: TestCase { expect(self.customerInfo.copy(with: .verifiedOnDevice).isComputedOffline) == true } - // MARK: - Private +} + +extension CustomerInfo { - private func verifyCopy( + convenience init?(testData: [String: Any]) { + do { + try self.init(data: testData) + } catch { + let errorDescription = (error as? DescribableError)?.description ?? error.localizedDescription + Logger.error("Caught error creating testData, this is probably expected, right? \(errorDescription).") + + return nil + } + } + +} + +private extension BasicCustomerInfoTests { + + static func date(withDaysAgo days: Int) throws -> Date { + return try XCTUnwrap(Calendar.current.date(byAdding: .day, value: days, to: Date())) + } + + static let expiredSubscriptionDate = ISO8601DateFormatter.default.string( + // swiftlint:disable:next force_try + from: try! BasicCustomerInfoTests.date(withDaysAgo: -1) + ) + + func verifyCopy( of customerInfo: CustomerInfo, onlyModifiesEntitlementVerification newVerification: VerificationResult ) { @@ -930,7 +948,7 @@ class BasicCustomerInfoTests: TestCase { expect(copyWithOriginalVerification) == customerInfo } - private func verifyCopy( + func verifyCopy( of customerInfo: CustomerInfo, onlyModifiesRequestDate newRequestDate: Date ) { @@ -946,21 +964,6 @@ class BasicCustomerInfoTests: TestCase { } -extension CustomerInfo { - - convenience init?(testData: [String: Any]) { - do { - try self.init(data: testData) - } catch { - let errorDescription = (error as? DescribableError)?.description ?? error.localizedDescription - Logger.error("Caught error creating testData, this is probably expected, right? \(errorDescription).") - - return nil - } - } - -} - private extension BasicCustomerInfoTests { static let sampleTestDataWithEntitlements: [String: Any] = [ From 31dd70c9b4baf8c69a4a62a0281df2c78a6a1781 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 25 Nov 2024 15:37:55 +0100 Subject: [PATCH 09/19] made storeTransactionId nil --- Sources/Networking/Responses/CustomerInfoResponse.swift | 2 +- .../OfflineEntitlements/CustomerInfoResponseHandlerTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Networking/Responses/CustomerInfoResponse.swift b/Sources/Networking/Responses/CustomerInfoResponse.swift index 0f22d1f8a9..230db607bc 100644 --- a/Sources/Networking/Responses/CustomerInfoResponse.swift +++ b/Sources/Networking/Responses/CustomerInfoResponse.swift @@ -213,7 +213,7 @@ extension CustomerInfoResponse.Subscription { unsubscribeDetectedAt: Date? = nil, billingIssuesDetectedAt: Date? = nil, ownershipType: PurchaseOwnershipType = .defaultValue, - storeTransactionId: String = "" + storeTransactionId: String? = nil ) { self.periodType = periodType self.purchaseDate = purchaseDate diff --git a/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift b/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift index f6f903b858..6948681859 100644 --- a/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift +++ b/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift @@ -390,7 +390,7 @@ private extension BaseCustomerInfoResponseHandlerTests { static let purchasedProduct: PurchasedSK2Product = .init( productIdentifier: "product", - subscription: .init(), + subscription: .init(purchaseDate: Date()), entitlement: .init(productIdentifier: "entitlement", rawData: [:]) ) static let mapping: ProductEntitlementMapping = .init(entitlementsByProduct: [ From fd54b5fb22641c945adfe1064a98e689d3a4ea24 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 25 Nov 2024 16:28:08 +0100 Subject: [PATCH 10/19] fix tests and docs --- Sources/Identity/SubscriptionInfo.swift | 39 ++++++++++++++ .../Purchasing/CustomerInfoTests.swift | 52 ++++++++++++++----- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index e6a2839539..76efe87436 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -17,19 +17,58 @@ import Foundation /// The product identifier. @objc public let productIdentifier: String + + /// Date when the last subscription period started. @objc public let purchaseDate: Date + + /// Date when this subscription first started. This property does not update with renewals. + /// This property also does not update for product changes within a subscription group or + /// resubscriptions by lapsed subscribers. @objc public let originalPurchaseDate: Date? + + /// Date when the subscription expires/expired @objc public let expiresDate: Date? + + /// Store where the subscription was purchased. @objc public let store: Store + + /// Whether or not the purchase was made in sandbox mode. @objc public let isSandbox: Bool + + /// Date when RevenueCat detected that auto-renewal was turned off for this subsription. + /// Note the subscription may still be active, check the ``expiresDate`` attribute. @objc public let unsubscribeDetectedAt: Date? + + /// Date when RevenueCat detected any billing issues with this subscription. + /// If and when the billing issue gets resolved, this field is set to nil. + /// Note the subscription may still be active, check the ``expiresDate`` attribute. @objc public let billingIssuesDetectedAt: Date? + + /// Date when any grace period for this subscription expires/expired. + /// nil if the customer has never been in a grace period. @objc public let gracePeriodExpiresDate: Date? + + /// How the Customer received access to this subscription: + /// - ``purchased``: The customer bought the subscription. + /// - ``familyShared``: The Customer has access to the product via their family. @objc public let ownershipType: PurchaseOwnershipType + + /// Type of the current subscription period: + /// - ``normal``: The product is in a normal period (default) + /// - ``trial``: The product is in a free trial period + /// - ``intro``: The product is in an introductory pricing period @objc public let periodType: PeriodType + + /// Date when RevenueCat detected a refund of this subscription. @objc public let refundedAt: Date? + + /// The transaction id in the store of the subscription. @objc public let storeTransactionId: String? + + /// Whether the subscription is currently active. @objc public let isActive: Bool + + /// Whether the subscription will renew at the next billing period. @objc public let willRenew: Bool init(productIdentifier: String, diff --git a/Tests/UnitTests/Purchasing/CustomerInfoTests.swift b/Tests/UnitTests/Purchasing/CustomerInfoTests.swift index a735504d58..4a8ffee11a 100644 --- a/Tests/UnitTests/Purchasing/CustomerInfoTests.swift +++ b/Tests/UnitTests/Purchasing/CustomerInfoTests.swift @@ -43,6 +43,7 @@ class BasicCustomerInfoTests: TestCase { ] as [String: Any], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal", "is_sandbox": false @@ -55,6 +56,7 @@ class BasicCustomerInfoTests: TestCase { "purchase_date": "2018-05-20T06:24:50Z" ], "onemonth": [ + "purchase_date": "2000-07-30T02:40:36Z", "expires_date": BasicCustomerInfoTests.expiredSubscriptionDate, "period_type": "normal", "is_sandbox": false @@ -95,17 +97,29 @@ class BasicCustomerInfoTests: TestCase { ] as [String: Any] ] - static let validTwoProductsJSON = "{" + - "\"request_date\": \"2018-05-20T06:24:50Z\"," + - "\"subscriber\": {" + - "\"first_seen\": \"2018-05-20T06:24:50Z\"," + - "\"original_application_version\": \"1.0\"," + - "\"original_app_user_id\": \"abcd\"," + - "\"other_purchases\": {}," + - "\"subscriptions\":{" + - "\"product_a\": {\"expires_date\": \"2018-05-27T06:24:50Z\",\"period_type\": \"normal\"}," + - "\"product_b\": {\"expires_date\": \"2018-05-27T05:24:50Z\",\"period_type\": \"normal\"}" + - "}}}" + static let validTwoProductsJSON = """ + { + "request_date": "2018-05-20T06:24:50Z", + "subscriber": { + "first_seen": "2018-05-20T06:24:50Z", + "original_application_version": "1.0", + "original_app_user_id": "abcd", + "other_purchases": {}, + "subscriptions": { + "product_a": { + "purchase_date": "2018-04-27T06:24:50Z", + "expires_date": "2018-05-27T06:24:50Z", + "period_type": "normal" + }, + "product_b": { + "purchase_date": "2018-04-27T05:24:50Z", + "expires_date": "2018-05-27T05:24:50Z", + "period_type": "normal" + } + } + } + } + """ private var customerInfo: CustomerInfo! @@ -389,18 +403,22 @@ class BasicCustomerInfoTests: TestCase { ], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "threemonth_freetrial": [ + "purchase_date": "1989-08-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z", "period_type": "normal" ], "pro.1": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "pro.2": [ + "purchase_date": "1990-07-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z", "period_type": "normal" ] @@ -460,9 +478,11 @@ class BasicCustomerInfoTests: TestCase { ], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z" ], "threemonth_freetrial": [ + "purchase_date": "1990-08-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z" ] ], @@ -539,6 +559,7 @@ class BasicCustomerInfoTests: TestCase { "original_app_user_id": "", "subscriptions": [ "pro.1": [ + "purchase_date": "2018-07-30T02:40:36Z", "expires_date": "2018-12-19T02:40:36Z" ]], "other_purchases": [:] as [String: Any], @@ -557,6 +578,7 @@ class BasicCustomerInfoTests: TestCase { "original_app_user_id": "", "subscriptions": [ "pro.1": [ + "purchase_date": "2018-07-30T02:40:36Z", "expires_date": "2018-12-19T02:40:36Z" ] ], @@ -776,13 +798,16 @@ class BasicCustomerInfoTests: TestCase { "non_subscriptions": [:] as [String: Any], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "twomonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "period_type": "normal" ], "threemonth_freetrial": [ + "purchase_date": "1990-07-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z" ] ], @@ -820,13 +845,16 @@ class BasicCustomerInfoTests: TestCase { "non_subscriptions": [:] as [String: Any], "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "expires_date": "2100-08-30T02:40:36Z", "period_type": "normal" ], "twomonth_freetrial": [ + "purchase_date": "2100-07-30T02:40:36Z", "period_type": "normal" ], "threemonth_freetrial": [ + "purchase_date": "1990-07-30T02:40:36Z", "expires_date": "1990-08-30T02:40:36Z" ] ], @@ -934,7 +962,7 @@ private extension BasicCustomerInfoTests { // swiftlint:disable:next force_try from: try! BasicCustomerInfoTests.date(withDaysAgo: -1) ) - + func verifyCopy( of customerInfo: CustomerInfo, onlyModifiesEntitlementVerification newVerification: VerificationResult From 60c50d60c122bb177f6fb36e56e1694e44a691f8 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 25 Nov 2024 18:46:39 +0100 Subject: [PATCH 11/19] fix tests --- .../BeginRefundRequestHelperTests.swift | 14 +++++++++----- .../Networking/Backend/BaseBackendTest.swift | 1 + .../BackendSubscriberAttributesTests.swift | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift b/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift index 8b57577b51..acc2d18c7e 100644 --- a/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift +++ b/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift @@ -238,7 +238,7 @@ private extension BeginRefundRequestHelperTests { "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], "subscriptions": [ - "onemonth_freetrial": [:] as [String: Any] + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] ], "entitlements": [ "\(mockEntitlementID)": [ @@ -260,8 +260,8 @@ private extension BeginRefundRequestHelperTests { "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], "subscriptions": [ - "onemonth_freetrial": [:] as [String: Any], - "onemonth_freetrial2": [:] as [String: Any] + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any], + "onemonth_freetrial2": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] ], "entitlements": [ "\(mockEntitlementID)": [ @@ -287,7 +287,9 @@ private extension BeginRefundRequestHelperTests { "original_application_version": "2083", "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], - "subscriptions": [:] as [String: Any], + "subscriptions": [ + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any], + ], "entitlements": [ "\(mockEntitlementID)": [ "expires_date": "2000-08-30T02:40:36Z", @@ -307,7 +309,9 @@ private extension BeginRefundRequestHelperTests { "original_application_version": "2083", "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], - "subscriptions": [:] as [String: Any], + "subscriptions": [ + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any], + ], "entitlements": [ "pro": [ "expires_date": "2100-08-30T02:40:36Z", diff --git a/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift b/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift index be6b606075..9d16422d86 100644 --- a/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift +++ b/Tests/UnitTests/Networking/Backend/BaseBackendTest.swift @@ -148,6 +148,7 @@ extension BaseBackendTests { "original_app_user_id": "", "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2017-07-30T02:40:36Z", "expires_date": "2017-08-30T02:40:36Z" ] ] diff --git a/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift index 0b57c289a2..9a63e90000 100644 --- a/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift @@ -41,6 +41,7 @@ class BackendSubscriberAttributesTests: TestCase { "original_app_user_id": "app_user_id", "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2017-07-30T02:40:36Z", "expires_date": "2017-08-30T02:40:36Z" ] ] From 2e4ca768c982e790c35a580a4044e9a772abf804 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 25 Nov 2024 18:54:37 +0100 Subject: [PATCH 12/19] fix more tests --- .../BeginRefundRequestHelperTests.swift | 4 +- .../Identity/CustomerInfoManagerTests.swift | 54 ++++++++++++++++--- .../Backend/BackendGetCustomerInfoTests.swift | 4 +- .../Backend/BackendPostReceiptDataTests.swift | 5 +- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift b/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift index acc2d18c7e..d7d0c2b80a 100644 --- a/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift +++ b/Tests/StoreKitUnitTests/BeginRefundRequestHelperTests.swift @@ -288,7 +288,7 @@ private extension BeginRefundRequestHelperTests { "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], "subscriptions": [ - "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any], + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] ], "entitlements": [ "\(mockEntitlementID)": [ @@ -310,7 +310,7 @@ private extension BeginRefundRequestHelperTests { "first_seen": "2019-06-17T16:05:33Z", "non_subscriptions": [:] as [String: Any], "subscriptions": [ - "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any], + "onemonth_freetrial": ["purchase_date": "2018-10-26T23:17:53Z"] as [String: Any] ], "entitlements": [ "pro": [ diff --git a/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift b/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift index 80bd9b21ff..d94d4c45c4 100644 --- a/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift +++ b/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift @@ -264,11 +264,31 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "original_app_user_id": Self.appUserID, "first_seen": "2019-06-17T16:05:33Z", "subscriptions": [ - "product_a": ["expires_date": "2098-05-27T06:24:50Z", "period_type": "normal"], - "Product_B": ["expires_date": "2098-05-27T06:24:50Z", "period_type": "normal"], - "ProductC": ["expires_date": "2098-05-27T06:24:50Z", "period_type": "normal"], - "Pro": ["expires_date": "2098-05-27T06:24:50Z", "period_type": "normal"], - "ProductD": ["expires_date": "2018-05-27T06:24:50Z", "period_type": "normal"] + "product_a": [ + "purchase_date": "2098-04-27T06:24:50Z", + "expires_date": "2098-05-27T06:24:50Z", + "period_type": "normal" + ], + "Product_B": [ + "purchase_date": "2098-04-27T06:24:50Z", + "expires_date": "2098-05-27T06:24:50Z", + "period_type": "normal" + ], + "ProductC": [ + "purchase_date": "2098-04-27T06:24:50Z", + "expires_date": "2098-05-27T06:24:50Z", + "period_type": "normal" + ], + "Pro": [ + "purchase_date": "2098-04-27T06:24:50Z", + "expires_date": "2098-05-27T06:24:50Z", + "period_type": "normal" + ], + "ProductD": [ + "purchase_date": "2018-04-27T06:24:50Z", + "expires_date": "2018-05-27T06:24:50Z", + "period_type": "normal" + ] ] as [String: Any], "other_purchases": [:] as [String: Any] ] as [String: Any] @@ -300,7 +320,13 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "subscriber": [ "original_app_user_id": Self.appUserID, "first_seen": "2019-06-17T16:05:33Z", - "subscriptions": ["product_a": ["expires_date": "2018-05-27T06:24:50Z", "period_type": "normal"]], + "subscriptions": [ + "product_a": [ + "purchase_date": "2018-04-27T06:24:50Z", + "expires_date": "2018-05-27T06:24:50Z", + "period_type": "normal" + ] + ], "other_purchases": [:] as [String: Any] ] as [String: Any] ]) @@ -329,7 +355,13 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "subscriber": [ "original_app_user_id": Self.appUserID, "first_seen": "2019-06-17T16:05:33Z", - "subscriptions": ["product_a": ["expires_date": "2018-05-27T06:24:50Z", "period_type": "normal"]], + "subscriptions": [ + "product_a": [ + "purchase_date": "2018-04-27T06:24:50Z", + "expires_date": "2018-05-27T06:24:50Z", + "period_type": "normal" + ] + ], "other_purchases": [:] as [String: Any] ] as [String: Any] ] @@ -350,7 +382,13 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "subscriber": [ "original_app_user_id": Self.appUserID, "first_seen": "2019-06-17T16:05:33Z", - "subscriptions": ["product_a": ["expires_date": "2018-05-27T06:24:50Z", "period_type": "normal"]], + "subscriptions": [ + "product_a": [ + "purchase_date": "2018-04-27T06:24:50Z", + "expires_date": "2018-05-27T06:24:50Z", + "period_type": "normal" + ] + ], "other_purchases": [:] as [String: Any] ] as [String: Any] ] diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift index 802147175a..d3a505b0ea 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift @@ -109,7 +109,9 @@ class BackendGetCustomerInfoTests: BaseBackendTests { "subscriber": [ "first_seen": "2019-07-17T00:05:54Z", "original_app_user_id": "user", - "subscriptions": [:] as [String: Any] + "subscriptions": [ + "purchase_date": "2017-07-30T02:40:36Z" + ] as [String: Any] ] as [String: Any] ] let path: HTTPRequest.Path = .getCustomerInfo(appUserID: Self.userID) diff --git a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift index 429b4ca680..df7c16ed8f 100644 --- a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift @@ -595,7 +595,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { var dateComponent = DateComponents() dateComponent.month = 1 let futureDateString = ISO8601DateFormatter() - .string(from: Calendar.current.date(byAdding: dateComponent, to: Date())!) + .string(from: Calendar.current.date(byAdding: dateComponent, to: today)!) let getCustomerInfoPath: HTTPRequest.Path = .getCustomerInfo(appUserID: Self.userID) @@ -606,6 +606,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { "original_app_user_id": "ORIGINAL", "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2024-11-25T00:05:54Z", "expires_date": futureDateString ] ] @@ -619,9 +620,11 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { "original_app_user_id": "UPDATED", "subscriptions": [ "onemonth_freetrial": [ + "purchase_date": "2024-11-25T00:05:54Z", "expires_date": futureDateString ], "twomonth_awesome": [ + "purchase_date": "2024-11-25T00:05:54Z", "expires_date": futureDateString ] ] From fbb7caa46030d0e8586b301d14fd50ab239bcc8a Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 26 Nov 2024 12:38:26 +0100 Subject: [PATCH 13/19] fix compilation --- Sources/Identity/SubscriptionInfo.swift | 1 + .../Networking/Backend/BackendPostReceiptDataTests.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index 76efe87436..01b91ec926 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -13,6 +13,7 @@ import Foundation +/// Subscription purchases of the Customer @objc(RCSubscriptionInfo) public final class SubscriptionInfo: NSObject { /// The product identifier. diff --git a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift index df7c16ed8f..ddef8149f7 100644 --- a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift @@ -594,6 +594,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { func testGetsUpdatedSubscriberInfoAfterPost() { var dateComponent = DateComponents() dateComponent.month = 1 + let today = Date() let futureDateString = ISO8601DateFormatter() .string(from: Calendar.current.date(byAdding: dateComponent, to: today)!) From c93a14d975d747d963c7c37b7cdbdab73cd68192 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 26 Nov 2024 16:07:39 +0100 Subject: [PATCH 14/19] fix test --- .../Networking/Backend/BackendGetCustomerInfoTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift index d3a505b0ea..a9f22bf6fc 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift @@ -110,7 +110,9 @@ class BackendGetCustomerInfoTests: BaseBackendTests { "first_seen": "2019-07-17T00:05:54Z", "original_app_user_id": "user", "subscriptions": [ - "purchase_date": "2017-07-30T02:40:36Z" + "com.revenuecat.product": [ + "purchase_date": "2017-07-30T02:40:36Z" + ] ] as [String: Any] ] as [String: Any] ] From 48fddd8302b898441cc1375808503a22b7f6820c Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 26 Nov 2024 16:26:35 +0100 Subject: [PATCH 15/19] API tests and other fixes --- Sources/Identity/SubscriptionInfo.swift | 10 +- .../AllAPITests.xcodeproj/project.pbxproj | 4 + .../SwiftAPITester/CustomerInfoAPI.swift | 4 +- .../SwiftAPITester/SubscriptionInfoAPI.swift | 34 +++++++ .../Identity/CustomerInfoManagerTests.swift | 98 +++++++++++++++++-- .../Backend/BackendGetCustomerInfoTests.swift | 6 +- 6 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index 01b91ec926..0b2d087ec3 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -50,14 +50,14 @@ import Foundation @objc public let gracePeriodExpiresDate: Date? /// How the Customer received access to this subscription: - /// - ``purchased``: The customer bought the subscription. - /// - ``familyShared``: The Customer has access to the product via their family. + /// - ``PurchaseOwnershipType/purchased``: The customer bought the subscription. + /// - ``PurchaseOwnershipType/familyShared``: The Customer has access to the product via their family. @objc public let ownershipType: PurchaseOwnershipType /// Type of the current subscription period: - /// - ``normal``: The product is in a normal period (default) - /// - ``trial``: The product is in a free trial period - /// - ``intro``: The product is in an introductory pricing period + /// - ``PeriodType/normal``: The product is in a normal period (default) + /// - ``PeriodType/trial``: The product is in a free trial period + /// - ``PeriodType/intro``: The product is in an introductory pricing period @objc public let periodType: PeriodType /// Date when RevenueCat detected a refund of this subscription. diff --git a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj index 2e51ea5554..dc4229cd13 100644 --- a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj +++ b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ 2D4C62D62C5D41E200A29FD2 /* RevenueCat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D42C5D41E200A29FD2 /* RevenueCat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 2D4C62D92C5D41EC00A29FD2 /* RevenueCat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */; }; 2D4C62DA2C5D41EC00A29FD2 /* RevenueCat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3502630E2CF61E9F00894270 /* SubscriptionInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -342,6 +343,7 @@ 2D4C62D02C5D41D400A29FD2 /* ReceiptParser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReceiptParser.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C62D42C5D41E200A29FD2 /* RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoAPI.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -529,6 +531,7 @@ 2D4C61B42C5AD61800A29FD2 /* SwiftAPITester */ = { isa = PBXGroup; children = ( + 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */, 2D4C61D32C5AD62900A29FD2 /* AttributionNetworkAPI.swift */, 2D4C61CC2C5AD62900A29FD2 /* AttributionAPI.swift */, 2D4C61D42C5AD62900A29FD2 /* ConfigurationAPI.swift */, @@ -989,6 +992,7 @@ 2D4C61DA2C5AD62A00A29FD2 /* RefundRequestStatusAPI.swift in Sources */, 2D4C61F52C5AD62A00A29FD2 /* PromotionalOfferAPI.swift in Sources */, 2D4C61E92C5AD62A00A29FD2 /* StoreProductAPI.swift in Sources */, + 3502630E2CF61E9F00894270 /* SubscriptionInfoAPI.swift in Sources */, 2D4C61EA2C5AD62A00A29FD2 /* StorefrontAPI.swift in Sources */, 2D4C61E42C5AD62A00A29FD2 /* main.swift in Sources */, 2D4C61E62C5AD62A00A29FD2 /* StoreProductDiscountAPI.swift in Sources */, diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift index de2811be07..6df5ec0280 100644 --- a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift @@ -40,8 +40,10 @@ func checkCustomerInfoAPI() { let _: String = customerInfo.id + let subs: [String: SubscriptionInfo] = customerInfo.subscriptions + print(customerInfo!, entitlementInfo, asubs, appis, led!, nst, oav!, opd!, rDate!, fSeen, - oaud!, murl!, edfpi!, pdfpi!, exdf!, pdfe!, desc, rawData) + oaud!, murl!, edfpi!, pdfpi!, exdf!, pdfe!, desc, rawData, subs) } func checkCacheFetchPolicyEnum(_ policy: CacheFetchPolicy) { diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift new file mode 100644 index 0000000000..ba92f1d0f6 --- /dev/null +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift @@ -0,0 +1,34 @@ +// +// 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 +// +// SubscriptionInfoAPI.swift +// +// Created by Cesar de la Vega on 26/11/24. + +import Foundation +import RevenueCat + +var subscription: SubscriptionInfo! +func checkSubscriptionInfoAPI() { + let pId: String = subscription.productIdentifier + let pd: Date = subscription.purchaseDate + let opd: Date? = subscription.originalPurchaseDate + let eDate: Date? = subscription.expiresDate + let store: Store = subscription.store + let iss: Bool = subscription.isSandbox + let uda: Date? = subscription.unsubscribeDetectedAt + let bida: Date? = subscription.billingIssuesDetectedAt + let gped: Date? = subscription.gracePeriodExpiresDate + let oType: PurchaseOwnershipType = subscription.ownershipType + let pType: PeriodType = subscription.periodType + let rAt: Date? = subscription.refundedAt + let txId: String? = subscription.storeTransactionId + let isActive: Bool = subscription.isActive + let willRenew: Bool = subscription.willRenew +} diff --git a/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift b/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift index d94d4c45c4..7e668f23a8 100644 --- a/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift +++ b/Tests/UnitTests/Identity/CustomerInfoManagerTests.swift @@ -267,27 +267,77 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "product_a": [ "purchase_date": "2098-04-27T06:24:50Z", "expires_date": "2098-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": nil, + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": nil, + "ownership_type": "PURCHASED", + "refunded_at": nil, + "store_transaction_id": "1", + "auto_resume_date": nil ], "Product_B": [ - "purchase_date": "2098-04-27T06:24:50Z", + "purchase_date": "2022-04-12T00:03:28Z", "expires_date": "2098-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": "2098-05-18T06:24:50Z", + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": "2098-05-14T06:24:50Z", + "ownership_type": "PURCHASED", + "refunded_at": "2098-05-16T06:24:50Z", + "store_transaction_id": "1", + "auto_resume_date": nil ], "ProductC": [ "purchase_date": "2098-04-27T06:24:50Z", "expires_date": "2098-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": nil, + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": nil, + "ownership_type": "PURCHASED", + "refunded_at": nil, + "store_transaction_id": "1", + "auto_resume_date": nil ], "Pro": [ "purchase_date": "2098-04-27T06:24:50Z", "expires_date": "2098-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": nil, + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": nil, + "ownership_type": "PURCHASED", + "refunded_at": nil, + "store_transaction_id": "1", + "auto_resume_date": nil ], "ProductD": [ "purchase_date": "2018-04-27T06:24:50Z", "expires_date": "2018-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": nil, + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": nil, + "ownership_type": "PURCHASED", + "refunded_at": nil, + "store_transaction_id": "1", + "auto_resume_date": nil ] ] as [String: Any], "other_purchases": [:] as [String: Any] @@ -324,7 +374,17 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "product_a": [ "purchase_date": "2018-04-27T06:24:50Z", "expires_date": "2018-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": nil, + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": nil, + "ownership_type": "PURCHASED", + "refunded_at": nil, + "store_transaction_id": "1", + "auto_resume_date": nil ] ], "other_purchases": [:] as [String: Any] @@ -359,7 +419,17 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "product_a": [ "purchase_date": "2018-04-27T06:24:50Z", "expires_date": "2018-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": nil, + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": nil, + "ownership_type": "PURCHASED", + "refunded_at": nil, + "store_transaction_id": "1", + "auto_resume_date": nil ] ], "other_purchases": [:] as [String: Any] @@ -386,7 +456,17 @@ class CustomerInfoManagerTests: BaseCustomerInfoManagerTests { "product_a": [ "purchase_date": "2018-04-27T06:24:50Z", "expires_date": "2018-05-27T06:24:50Z", - "period_type": "normal" + "period_type": "normal", + "billing_issues_detected_at": nil, + "grace_period_expires_date": nil, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": nil, + "ownership_type": "PURCHASED", + "refunded_at": nil, + "store_transaction_id": "1", + "auto_resume_date": nil ] ], "other_purchases": [:] as [String: Any] diff --git a/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift index a9f22bf6fc..802147175a 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetCustomerInfoTests.swift @@ -109,11 +109,7 @@ class BackendGetCustomerInfoTests: BaseBackendTests { "subscriber": [ "first_seen": "2019-07-17T00:05:54Z", "original_app_user_id": "user", - "subscriptions": [ - "com.revenuecat.product": [ - "purchase_date": "2017-07-30T02:40:36Z" - ] - ] as [String: Any] + "subscriptions": [:] as [String: Any] ] as [String: Any] ] let path: HTTPRequest.Path = .getCustomerInfo(appUserID: Self.userID) From 4b0cd05889f6c59fa3617efe8ecae57c6e352801 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 3 Dec 2024 12:54:29 +0100 Subject: [PATCH 16/19] Update Sources/Purchasing/NonSubscriptionTransaction.swift Co-authored-by: Andy Boedo --- Sources/Purchasing/NonSubscriptionTransaction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Purchasing/NonSubscriptionTransaction.swift b/Sources/Purchasing/NonSubscriptionTransaction.swift index 5bbed0d84d..9f66970ec5 100644 --- a/Sources/Purchasing/NonSubscriptionTransaction.swift +++ b/Sources/Purchasing/NonSubscriptionTransaction.swift @@ -35,7 +35,7 @@ public final class NonSubscriptionTransaction: NSObject { @objc public let storeTransactionIdentifier: String /** - * The ``Store``tore where this transaction was performed from. + * The ``Store`` where this transaction was performed. */ @objc public let store: Store From 0d388c5012cd03870f1bd447289e4abbcd87dca0 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 3 Dec 2024 19:35:00 +0100 Subject: [PATCH 17/19] use ProductIdentifier and subscriptionsByProductIdentifier --- Sources/Identity/CustomerInfo.swift | 21 ++++++++---- Sources/Identity/SubscriptionInfo.swift | 2 +- .../AllAPITests.xcodeproj/project.pbxproj | 8 +++++ .../ObjcAPITester/RCSubscriptionInfoAPI.h | 19 +++++++++++ .../ObjcAPITester/RCSubscriptionInfoAPI.m | 34 +++++++++++++++++++ .../SwiftAPITester/CustomerInfoAPI.swift | 6 +++- .../SwiftAPITester/SubscriptionInfoAPI.swift | 1 + 7 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.h create mode 100644 Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.m diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index f6c0c71d87..4e670c0d5a 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -14,6 +14,11 @@ import Foundation +/** + An identifier used to identify a product. + */ +public typealias ProductIdentifier = String + /** A container for the most recent customer info returned from `Purchases`. These objects are non-mutable and do not update automatically. @@ -24,10 +29,10 @@ import Foundation @objc public let entitlements: EntitlementInfos /// All *subscription* product identifiers with expiration dates in the future. - @objc public var activeSubscriptions: Set { self.activeKeys(dates: self.expirationDatesByProductId) } + @objc public var activeSubscriptions: Set { self.activeKeys(dates: self.expirationDatesByProductId) } /// All product identifiers purchases by the user regardless of expiration. - @objc public let allPurchasedProductIdentifiers: Set + @objc public let allPurchasedProductIdentifiers: Set /// Returns the latest expiration date of all products, nil if there are none. @objc public var latestExpirationDate: Date? { @@ -89,19 +94,19 @@ import Foundation @objc public let originalApplicationVersion: String? /// Dictionary of all subscription product identifiers and their subscription info - @objc public let subscriptions: [String: SubscriptionInfo] + @objc public let subscriptionsByProductIdentifier: [ProductIdentifier: SubscriptionInfo] /// Get the expiration date for a given product identifier. You should use Entitlements though! /// - Parameter productIdentifier: Product identifier for product /// - Returns: The expiration date for `productIdentifier`, `nil` if product never purchased - @objc public func expirationDate(forProductIdentifier productIdentifier: String) -> Date? { + @objc public func expirationDate(forProductIdentifier productIdentifier: ProductIdentifier) -> Date? { return expirationDatesByProductId[productIdentifier] ?? nil } /// Get the latest purchase or renewal date for a given product identifier. You should use Entitlements though! /// - Parameter productIdentifier: Product identifier for subscription product /// - Returns: The purchase date for `productIdentifier`, `nil` if product never purchased - @objc public func purchaseDate(forProductIdentifier productIdentifier: String) -> Date? { + @objc public func purchaseDate(forProductIdentifier productIdentifier: ProductIdentifier) -> Date? { return purchaseDatesByProductId[productIdentifier] ?? nil } @@ -146,6 +151,8 @@ import Foundation let verificationResult = self.entitlements.verification.debugDescription + let subscriptionsDescription = self.subscriptionsByProductIdentifier.mapValues { $0.description } + return """ <\(String(describing: CustomerInfo.self)): originalApplicationVersion=\(self.originalApplicationVersion ?? ""), @@ -153,6 +160,7 @@ import Foundation activeEntitlements=\(activeEntitlementsDescription), activeSubscriptions=\(activeSubsDescription), nonSubscriptions=\(self.nonSubscriptions), + subscriptions=\(subscriptionsDescription), requestDate=\(String(describing: self.requestDate)), firstSeen=\(String(describing: self.firstSeen)), originalAppUserId=\(self.originalAppUserId), @@ -212,7 +220,8 @@ import Foundation self.allPurchasedProductIdentifiers = Set(self.expirationDatesByProductId.keys) .union(self.nonSubscriptions.map { $0.productIdentifier }) - self.subscriptions = Dictionary(uniqueKeysWithValues: subscriber.subscriptions.map { (key, subscriptionData) in + self.subscriptionsByProductIdentifier = + Dictionary(uniqueKeysWithValues: subscriber.subscriptions.map { (key, subscriptionData) in (key, SubscriptionInfo( productIdentifier: key, purchaseDate: subscriptionData.purchaseDate, diff --git a/Sources/Identity/SubscriptionInfo.swift b/Sources/Identity/SubscriptionInfo.swift index 0b2d087ec3..c79e9a7f32 100644 --- a/Sources/Identity/SubscriptionInfo.swift +++ b/Sources/Identity/SubscriptionInfo.swift @@ -17,7 +17,7 @@ import Foundation @objc(RCSubscriptionInfo) public final class SubscriptionInfo: NSObject { /// The product identifier. - @objc public let productIdentifier: String + @objc public let productIdentifier: ProductIdentifier /// Date when the last subscription period started. @objc public let purchaseDate: Date diff --git a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj index dc4229cd13..ca3babde38 100644 --- a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj +++ b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj @@ -122,6 +122,8 @@ 2D4C62D92C5D41EC00A29FD2 /* RevenueCat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */; }; 2D4C62DA2C5D41EC00A29FD2 /* RevenueCat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3502630E2CF61E9F00894270 /* SubscriptionInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */; }; + 35370AC52CFF8304004F0A64 /* RCSubscriptionInfoAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */; }; + 35370AC82CFF8317004F0A64 /* RCSubscriptionInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -344,6 +346,8 @@ 2D4C62D42C5D41E200A29FD2 /* RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C62D82C5D41EC00A29FD2 /* RevenueCat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RevenueCat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoAPI.swift; sourceTree = ""; }; + 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCSubscriptionInfoAPI.h; sourceTree = ""; }; + 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCSubscriptionInfoAPI.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -458,6 +462,8 @@ 2D4C613E2C5AD30400A29FD2 /* ObjcAPITester */ = { isa = PBXGroup; children = ( + 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */, + 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */, 2D4C61732C5AD31900A29FD2 /* main.m */, 2D4C61582C5AD31600A29FD2 /* RCAttributionAPI.h */, 2D4C614C2C5AD31500A29FD2 /* RCAttributionAPI.m */, @@ -640,6 +646,7 @@ buildActionMask = 2147483647; files = ( 2D4C61402C5AD30400A29FD2 /* ObjcAPITester.h in Headers */, + 35370AC52CFF8304004F0A64 /* RCSubscriptionInfoAPI.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -977,6 +984,7 @@ 2D4C617C2C5AD31A00A29FD2 /* RCStoreKitVersionAPI.m in Sources */, 2D4C61902C5AD31A00A29FD2 /* main.m in Sources */, 2D4C61932C5AD31A00A29FD2 /* RCPurchasesAPI.m in Sources */, + 35370AC82CFF8317004F0A64 /* RCSubscriptionInfoAPI.m in Sources */, 2D4C618C2C5AD31A00A29FD2 /* RCPurchasesDiagnosticsAPI.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.h b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.h new file mode 100644 index 0000000000..a87eafb710 --- /dev/null +++ b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.h @@ -0,0 +1,19 @@ +// +// RCSubscriptionInfoAPI.h +// AllAPITests +// +// Created by Cesar de la Vega on 3/12/24. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCSubscriptionInfoAPI : NSObject + ++ (void)checkAPI; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.m b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.m new file mode 100644 index 0000000000..e165b18d11 --- /dev/null +++ b/Tests/APITesters/AllAPITests/ObjcAPITester/RCSubscriptionInfoAPI.m @@ -0,0 +1,34 @@ +// +// RCSubscriptionInfoAPI.m +// AllAPITests +// +// Created by Cesar de la Vega on 3/12/24. +// + +#import "RCSubscriptionInfoAPI.h" + +@import RevenueCat; + +@implementation RCSubscriptionInfoAPI + ++ (void)checkAPI { + RCSubscriptionInfo *subscription; + + NSString *productIdentifier __unused = subscription.productIdentifier; + NSDate *purchaseDate __unused = subscription.purchaseDate; + NSDate *originalPurchaseDate __unused = subscription.originalPurchaseDate; + NSDate *expiresDate __unused = subscription.expiresDate; + RCStore store __unused = subscription.store; + BOOL isSandbox __unused = subscription.isSandbox; + NSDate *unsubscribeDetectedAt __unused = subscription.unsubscribeDetectedAt; + NSDate *billingIssuesDetectedAt __unused = subscription.billingIssuesDetectedAt; + NSDate *gracePeriodExpiresDate __unused = subscription.gracePeriodExpiresDate; + RCPurchaseOwnershipType ownershipType __unused = subscription.ownershipType; + RCPeriodType periodType __unused = subscription.periodType; + NSDate *refundedAt __unused = subscription.refundedAt; + NSString *storeTransactionId __unused = subscription.storeTransactionId; + BOOL isActive __unused = subscription.isActive; + BOOL willRenew __unused = subscription.willRenew; +} + +@end \ No newline at end of file diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift index 6df5ec0280..362f8dd90a 100644 --- a/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/CustomerInfoAPI.swift @@ -17,7 +17,9 @@ import RevenueCat var customerInfo: CustomerInfo! func checkCustomerInfoAPI() { let entitlementInfo: EntitlementInfos = customerInfo.entitlements + let asubsp: Set = customerInfo.activeSubscriptions let asubs: Set = customerInfo.activeSubscriptions + let appisp: Set = customerInfo.allPurchasedProductIdentifiers let appis: Set = customerInfo.allPurchasedProductIdentifiers let led: Date? = customerInfo.latestExpirationDate @@ -40,7 +42,9 @@ func checkCustomerInfoAPI() { let _: String = customerInfo.id - let subs: [String: SubscriptionInfo] = customerInfo.subscriptions + let subs: [String: SubscriptionInfo] = customerInfo.subscriptionsByProductIdentifier + + let subsp: [ProductIdentifier: SubscriptionInfo] = customerInfo.subscriptionsByProductIdentifier print(customerInfo!, entitlementInfo, asubs, appis, led!, nst, oav!, opd!, rDate!, fSeen, oaud!, murl!, edfpi!, pdfpi!, exdf!, pdfe!, desc, rawData, subs) diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift index ba92f1d0f6..829d5e62b7 100644 --- a/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/SubscriptionInfoAPI.swift @@ -17,6 +17,7 @@ import RevenueCat var subscription: SubscriptionInfo! func checkSubscriptionInfoAPI() { let pId: String = subscription.productIdentifier + let pIdP: ProductIdentifier = subscription.productIdentifier let pd: Date = subscription.purchaseDate let opd: Date? = subscription.originalPurchaseDate let eDate: Date? = subscription.expiresDate From a051d9fb15c9188414785ff617398811acaa84a1 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 4 Dec 2024 09:27:55 +0100 Subject: [PATCH 18/19] fix lint --- Sources/Identity/CustomerInfo.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index 4e670c0d5a..acd93c24c2 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -29,7 +29,9 @@ public typealias ProductIdentifier = String @objc public let entitlements: EntitlementInfos /// All *subscription* product identifiers with expiration dates in the future. - @objc public var activeSubscriptions: Set { self.activeKeys(dates: self.expirationDatesByProductId) } + @objc public var activeSubscriptions: Set { + self.activeKeys(dates: self.expirationDatesByProductId) + } /// All product identifiers purchases by the user regardless of expiration. @objc public let allPurchasedProductIdentifiers: Set From ede24843659320ae5af61a7c254adc8d55b2e870 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 4 Dec 2024 09:56:40 +0100 Subject: [PATCH 19/19] disable file_length --- Sources/Identity/CustomerInfo.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Identity/CustomerInfo.swift b/Sources/Identity/CustomerInfo.swift index acd93c24c2..03a4587190 100644 --- a/Sources/Identity/CustomerInfo.swift +++ b/Sources/Identity/CustomerInfo.swift @@ -12,6 +12,7 @@ // Created by Madeline Beyl on 7/9/21. // +// swiftlint:disable file_length import Foundation /**