diff --git a/Sources/Identity/CustomerInfoManager.swift b/Sources/Identity/CustomerInfoManager.swift index e0028e86b7..888b03ba66 100644 --- a/Sources/Identity/CustomerInfoManager.swift +++ b/Sources/Identity/CustomerInfoManager.swift @@ -329,7 +329,7 @@ private extension CustomerInfoManager { let transactionData = PurchasedTransactionData( appUserID: appUserID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: [:], storefront: await Storefront.currentStorefront, source: Self.sourceForUnfinishedTransaction diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index 96162d52f5..2ea45915d8 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -82,6 +82,7 @@ enum PurchaseStrings { productID: String, transactionDate: Date, offeringID: String?, + placementID: String?, paywallSessionID: UUID?) case caching_presented_offering_identifier(offeringID: String, productID: String) case payment_queue_wrapper_delegate_call_sk1_enabled @@ -306,7 +307,12 @@ extension PurchaseStrings: LogMessage { case let .sk2_transactions_update_received_transaction(productID): return "StoreKit.Transaction.updates: received transaction for product '\(productID)'" - case let .transaction_poster_handling_transaction(transactionID, productID, date, offeringID, paywallSessionID): + case let .transaction_poster_handling_transaction(transactionID, + productID, + date, + offeringID, + placementID, + paywallSessionID): var message = "TransactionPoster: handling transaction '\(transactionID)' " + "for product '\(productID)' (date: \(date))" @@ -314,6 +320,10 @@ extension PurchaseStrings: LogMessage { message += " in Offering '\(offeringIdentifier)'" } + if let placementIdentifier = placementID { + message += " with Placement '\(placementIdentifier)'" + } + if let paywallSessionID { message += " with paywall session '\(paywallSessionID)'" } diff --git a/Sources/Networking/Operations/PostReceiptDataOperation.swift b/Sources/Networking/Operations/PostReceiptDataOperation.swift index b28704b5c9..4f3646f617 100644 --- a/Sources/Networking/Operations/PostReceiptDataOperation.swift +++ b/Sources/Networking/Operations/PostReceiptDataOperation.swift @@ -124,6 +124,7 @@ extension PostReceiptDataOperation { let isRestore: Bool let productData: ProductRequestData? let presentedOfferingIdentifier: String? + let presentedPlacementIdentifier: String? let paywall: Paywall? let observerMode: Bool let initiationSource: ProductRequestData.InitiationSource @@ -160,7 +161,8 @@ extension PostReceiptDataOperation.PostData { receipt: receipt, isRestore: data.source.isRestore, productData: productData, - presentedOfferingIdentifier: data.presentedOfferingID, + presentedOfferingIdentifier: data.presentedOfferingContext?.offeringIdentifier, + presentedPlacementIdentifier: data.presentedOfferingContext?.placementIdentifier, paywall: data.paywall, observerMode: observerMode, initiationSource: data.source.initiationSource, diff --git a/Sources/Networking/Responses/OfferingsResponse.swift b/Sources/Networking/Responses/OfferingsResponse.swift index 5e425d82e6..1548f4217c 100644 --- a/Sources/Networking/Responses/OfferingsResponse.swift +++ b/Sources/Networking/Responses/OfferingsResponse.swift @@ -35,10 +35,14 @@ struct OfferingsResponse { var metadata: [String: AnyDecodable] } - + struct Placements { + let fallbackOfferingId: String? + @DefaultDecodable.EmptyDictionary + var offeringIdsByPlacement: [String: String?] + } let currentOfferingId: String? let offerings: [Offering] - + let placements: Placements? } extension OfferingsResponse { @@ -56,6 +60,7 @@ extension OfferingsResponse { extension OfferingsResponse.Offering.Package: Codable, Equatable {} extension OfferingsResponse.Offering: Codable, Equatable {} +extension OfferingsResponse.Placements: Codable, Equatable {} extension OfferingsResponse: Codable, Equatable {} extension OfferingsResponse: HTTPResponseBody {} diff --git a/Sources/Purchasing/Offerings.swift b/Sources/Purchasing/Offerings.swift index 133095dce8..39360021b8 100644 --- a/Sources/Purchasing/Offerings.swift +++ b/Sources/Purchasing/Offerings.swift @@ -28,6 +28,19 @@ import Foundation */ @objc(RCOfferings) public final class Offerings: NSObject { + internal final class Placements: NSObject { + let fallbackOfferingId: String? + let offeringIdsByPlacement: [String: String?] + + init( + fallbackOfferingId: String?, + offeringIdsByPlacement: [String: String?] + ) { + self.fallbackOfferingId = fallbackOfferingId + self.offeringIdsByPlacement = offeringIdsByPlacement + } + } + /** Dictionary of all Offerings (``Offering``) objects keyed by their identifier. This dictionary can also be accessed by using an index subscript on ``Offerings``, e.g. `offerings["offering_id"]`. To access the current offering use @@ -48,19 +61,23 @@ import Foundation internal let response: OfferingsResponse private let currentOfferingID: String? + private let placements: Placements? init( offerings: [String: Offering], currentOfferingID: String?, + placements: Placements?, response: OfferingsResponse ) { self.all = offerings self.currentOfferingID = currentOfferingID self.response = response + self.placements = placements } } +extension Offerings.Placements: Sendable {} extension Offerings: Sendable {} public extension Offerings { @@ -93,4 +110,50 @@ public extension Offerings { return description } + /** + Retrieves a current offering for a placement identifier, use this to access offerings defined by targeting + placements configured in the RevenueCat dashboard, + e.g. `offerings.currentOffering(forPlacement: "placement_id")`. + */ + @objc(currentOfferingForPlacement:) + func currentOffering(forPlacement placementIdentifier: String) -> Offering? { + guard let placements = self.placements else { + return nil + } + + let returnOffering: Offering? + if let explicitOfferingId: String? = placements.offeringIdsByPlacement[placementIdentifier] { + // Don't use fallback since placement id was explicity set in the dictionary + returnOffering = explicitOfferingId.flatMap { self.all[$0] } + } else { + // Use fallback since the placement didn't exist + returnOffering = placements.fallbackOfferingId.flatMap { self.all[$0]} + } + + return returnOffering?.copyWith(placementIdentifier: placementIdentifier) + } +} + +private extension Offering { + func copyWith(placementIdentifier: String?) -> Offering { + let updatedPackages = self.availablePackages.map { pkg in + let newContext = PresentedOfferingContext( + offeringIdentifier: pkg.presentedOfferingContext.offeringIdentifier, + placementIdentifier: placementIdentifier + ) + + return Package(identifier: pkg.identifier, + packageType: pkg.packageType, + storeProduct: pkg.storeProduct, + presentedOfferingContext: newContext + ) + } + + return Offering(identifier: self.identifier, + serverDescription: self.serverDescription, + metadata: self.metadata, + paywall: self.paywall, + availablePackages: updatedPackages + ) + } } diff --git a/Sources/Purchasing/OfferingsFactory.swift b/Sources/Purchasing/OfferingsFactory.swift index 542ca674d4..f717899f6d 100644 --- a/Sources/Purchasing/OfferingsFactory.swift +++ b/Sources/Purchasing/OfferingsFactory.swift @@ -31,6 +31,7 @@ class OfferingsFactory { return Offerings(offerings: offerings, currentOfferingID: data.currentOfferingId, + placements: createPlacement(with: data.placements), response: data) } @@ -68,6 +69,16 @@ class OfferingsFactory { offeringIdentifier: offeringIdentifier) } + func createPlacement( + with data: OfferingsResponse.Placements? + ) -> Offerings.Placements? { + guard let data else { + return nil + } + + return .init(fallbackOfferingId: data.fallbackOfferingId, + offeringIdsByPlacement: data.offeringIdsByPlacement) + } } // @unchecked because: diff --git a/Sources/Purchasing/Package.swift b/Sources/Purchasing/Package.swift index a90de66b03..9107ddd259 100644 --- a/Sources/Purchasing/Package.swift +++ b/Sources/Purchasing/Package.swift @@ -19,18 +19,31 @@ import Foundation /// @objc(RCPresentedOfferingContext) public final class PresentedOfferingContext: NSObject { - /// The identifier of the ``Offering`` containing this Package. + /// The identifier of the ``Offering`` containing this ``Package``. @objc public let offeringIdentifier: String + /// The placement identifier this ``Package`` was obtained from. + @objc public let placementIdentifier: String? + /// Initialize a ``PresentedOfferingContext``. @objc public init( - offeringIdentifier: String + offeringIdentifier: String, + placementIdentifier: String? ) { self.offeringIdentifier = offeringIdentifier + self.placementIdentifier = placementIdentifier super.init() } + /// Initialize a ``PresentedOfferingContext``. + @objc + public convenience init( + offeringIdentifier: String + ) { + self.init(offeringIdentifier: offeringIdentifier, placementIdentifier: nil) + } + public override func isEqual(_ object: Any?) -> Bool { guard let other = object as? PresentedOfferingContext else { return false } @@ -91,7 +104,8 @@ import Foundation identifier: identifier, packageType: packageType, storeProduct: storeProduct, - presentedOfferingContext: PresentedOfferingContext(offeringIdentifier: offeringIdentifier) + presentedOfferingContext: PresentedOfferingContext(offeringIdentifier: offeringIdentifier, + placementIdentifier: nil) ) } diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index 67375393f5..0709023bcd 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -875,12 +875,13 @@ extension PurchasesOrchestrator: StoreKit2TransactionListenerDelegate { _ listener: StoreKit2TransactionListenerType, updatedTransaction transaction: StoreTransactionType ) async throws { + let storefront = await self.storefront(from: transaction) let subscriberAttributes = self.unsyncedAttributes let adServicesToken = await self.attribution.unsyncedAdServicesToken let transactionData: PurchasedTransactionData = .init( appUserID: self.appUserID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: subscriberAttributes, aadAttributionToken: adServicesToken, storefront: storefront, @@ -1060,7 +1061,7 @@ private extension PurchasesOrchestrator { self.createProductRequestData(with: receiptData) { productRequestData in let transactionData: PurchasedTransactionData = .init( appUserID: currentAppUserID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: unsyncedAttributes, storefront: productRequestData?.storefront, source: .init(isRestore: isRestore, initiationSource: initiationSource) @@ -1107,7 +1108,7 @@ private extension PurchasesOrchestrator { self.createProductRequestData(with: transaction.productIdentifier) { productRequestData in let transactionData: PurchasedTransactionData = .init( appUserID: currentAppUserID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: unsyncedAttributes, storefront: transaction.storefront, source: .init(isRestore: isRestore, initiationSource: initiationSource) @@ -1170,13 +1171,13 @@ private extension PurchasesOrchestrator { func handlePurchasedTransaction(_ purchasedTransaction: StoreTransaction, storefront: StorefrontType?, restored: Bool) { - let offeringContext = self.getAndRemovePresentedOfferingIdentifier(for: purchasedTransaction) + let offeringContext = self.getAndRemovePresentedOfferingContext(for: purchasedTransaction) let paywall = self.getAndRemovePresentedPaywall() let unsyncedAttributes = self.unsyncedAttributes self.attribution.unsyncedAdServicesToken { adServicesToken in let transactionData: PurchasedTransactionData = .init( appUserID: self.appUserID, - presentedOfferingID: offeringContext?.offeringIdentifier, + presentedOfferingContext: offeringContext, presentedPaywall: paywall, unsyncedAttributes: unsyncedAttributes, aadAttributionToken: adServicesToken, @@ -1243,14 +1244,14 @@ private extension PurchasesOrchestrator { self.presentedPaywall.value = nil } - func getAndRemovePresentedOfferingIdentifier(for productIdentifier: String) -> PresentedOfferingContext? { + func getAndRemovePresentedOfferingContext(for productIdentifier: String) -> PresentedOfferingContext? { return self.presentedOfferingContextsByProductID.modify { $0.removeValue(forKey: productIdentifier) } } - func getAndRemovePresentedOfferingIdentifier(for transaction: StoreTransaction) -> PresentedOfferingContext? { - return self.getAndRemovePresentedOfferingIdentifier(for: transaction.productIdentifier) + func getAndRemovePresentedOfferingContext(for transaction: StoreTransaction) -> PresentedOfferingContext? { + return self.getAndRemovePresentedOfferingContext(for: transaction.productIdentifier) } func getAndRemovePresentedPaywall() -> PaywallEvent? { @@ -1427,13 +1428,13 @@ extension PurchasesOrchestrator { _ initiationSource: ProductRequestData.InitiationSource ) async throws -> CustomerInfo { let storefront = await Storefront.currentStorefront - let offeringData = self.getAndRemovePresentedOfferingIdentifier(for: transaction) + let offeringContext = self.getAndRemovePresentedOfferingContext(for: transaction) let paywall = self.getAndRemovePresentedPaywall() let unsyncedAttributes = self.unsyncedAttributes let adServicesToken = await self.attribution.unsyncedAdServicesToken let transactionData: PurchasedTransactionData = .init( appUserID: self.appUserID, - presentedOfferingID: offeringData?.offeringIdentifier, + presentedOfferingContext: offeringContext, presentedPaywall: paywall, unsyncedAttributes: unsyncedAttributes, aadAttributionToken: adServicesToken, diff --git a/Sources/Purchasing/Purchases/TransactionPoster.swift b/Sources/Purchasing/Purchases/TransactionPoster.swift index 959dcf6540..0ace188224 100644 --- a/Sources/Purchasing/Purchases/TransactionPoster.swift +++ b/Sources/Purchasing/Purchases/TransactionPoster.swift @@ -25,7 +25,7 @@ struct PurchaseSource: Equatable { struct PurchasedTransactionData { var appUserID: String - var presentedOfferingID: String? + var presentedOfferingContext: PresentedOfferingContext? var presentedPaywall: PaywallEvent? var unsyncedAttributes: SubscriberAttribute.Dictionary? var aadAttributionToken: String? @@ -89,7 +89,8 @@ final class TransactionPoster: TransactionPosterType { transactionID: transaction.transactionIdentifier, productID: transaction.productIdentifier, transactionDate: transaction.purchaseDate, - offeringID: data.presentedOfferingID, + offeringID: data.presentedOfferingContext?.offeringIdentifier, + placementID: data.presentedOfferingContext?.placementIdentifier, paywallSessionID: data.presentedPaywall?.data.sessionIdentifier )) diff --git a/Sources/Support/PaywallExtensions.swift b/Sources/Support/PaywallExtensions.swift index fc85a860f8..c7fb83fe57 100644 --- a/Sources/Support/PaywallExtensions.swift +++ b/Sources/Support/PaywallExtensions.swift @@ -124,8 +124,18 @@ private extension View { Logger.appleWarning(Strings.configure.sk2_required_for_swiftui_paywalls) } + // Find offering context from a matching package. + // Packages could have the same product but each product has the same offering + // context so that is okay if that happens. + let offeringContext = offering.availablePackages.first { + $0.storeProduct.productIdentifier == product.id + }?.presentedOfferingContext + Purchases.shared.cachePresentedOfferingContext( - .init(offeringIdentifier: offering.identifier), + offeringContext ?? .init( + offeringIdentifier: offering.identifier, + placementIdentifier: nil + ), productIdentifier: product.id ) } diff --git a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift index 81feb40555..d8022d9ae3 100644 --- a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift +++ b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift @@ -11,8 +11,10 @@ import StoreKit func checkPresentedOfferingContextAPI(context: PresentedOfferingContext! = nil) { let _: String = context.offeringIdentifier + let _: String? = context.placementIdentifier } private func checkCreatePresentedOfferingContextAPI() { let _: PresentedOfferingContext = .init(offeringIdentifier: "") + let _: PresentedOfferingContext = .init(offeringIdentifier: "", placementIdentifier: "") } diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj index b8fb64a6ae..0b03aace81 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 2C396F5E281C64B700669657 /* AdServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C396F5D281C64B700669657 /* AdServices.framework */; platformFilters = (ios, maccatalyst, macos, ); }; - 2CA5383C2B7D44610037D96F /* RCPresentedOfferingContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA5383B2B7D44610037D96F /* RCPresentedOfferingContext.m */; }; + 2CA5383C2B7D44610037D96F /* RCPresentedOfferingContextAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA5383B2B7D44610037D96F /* RCPresentedOfferingContextAPI.m */; }; 2DD77909270E23870079CBD4 /* RCAttributionNetworkAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614EC26EBE84F007DDB75 /* RCAttributionNetworkAPI.m */; }; 2DD7790B270E23870079CBD4 /* RCCustomerInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F026EBE84F007DDB75 /* RCCustomerInfoAPI.m */; }; 2DD7790C270E23870079CBD4 /* RCPurchasesErrorCodeAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614EE26EBE84F007DDB75 /* RCPurchasesErrorCodeAPI.m */; }; @@ -54,8 +54,8 @@ /* Begin PBXFileReference section */ 2C396F5D281C64B700669657 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; }; - 2CA5383B2B7D44610037D96F /* RCPresentedOfferingContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCPresentedOfferingContext.m; sourceTree = ""; }; - 2CA5383E2B7D44A20037D96F /* RCPresentedOfferingContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCPresentedOfferingContext.h; sourceTree = ""; }; + 2CA5383B2B7D44610037D96F /* RCPresentedOfferingContextAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCPresentedOfferingContextAPI.m; sourceTree = ""; }; + 2CA5383E2B7D44A20037D96F /* RCPresentedOfferingContextAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCPresentedOfferingContextAPI.h; sourceTree = ""; }; 2DD778F5270E235B0079CBD4 /* ObjCAPITester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ObjCAPITester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4F3409392A37E5930050EA0E /* RCOtherAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCOtherAPI.h; sourceTree = ""; }; 4F34093A2A37E5930050EA0E /* RCOtherAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCOtherAPI.m; sourceTree = ""; }; @@ -170,8 +170,8 @@ 4F34093A2A37E5930050EA0E /* RCOtherAPI.m */, A5D614E126EBE84F007DDB75 /* RCPackageAPI.h */, A5D614DE26EBE84F007DDB75 /* RCPackageAPI.m */, - 2CA5383E2B7D44A20037D96F /* RCPresentedOfferingContext.h */, - 2CA5383B2B7D44610037D96F /* RCPresentedOfferingContext.m */, + 2CA5383E2B7D44A20037D96F /* RCPresentedOfferingContextAPI.h */, + 2CA5383B2B7D44610037D96F /* RCPresentedOfferingContextAPI.m */, B3A4C835280DE95000D4AE17 /* RCPromotionalOfferAPI.h */, B3A4C836280DE95000D4AE17 /* RCPromotionalOfferAPI.m */, A5D614E526EBE84F007DDB75 /* RCPurchasesAPI.h */, @@ -305,7 +305,7 @@ 2DD7790F270E23870079CBD4 /* RCPurchasesAPI.m in Sources */, B378153D2857A750000A7B93 /* RCAttributionAPI.m in Sources */, 5740FCD82996DB3300E049F9 /* RCVerificationResultAPI.m in Sources */, - 2CA5383C2B7D44610037D96F /* RCPresentedOfferingContext.m in Sources */, + 2CA5383C2B7D44610037D96F /* RCPresentedOfferingContextAPI.m in Sources */, 5758EE5227864A8500B3B703 /* RCStoreProductAPI.m in Sources */, 2DD77911270E23870079CBD4 /* RCEntitlementInfoAPI.m in Sources */, 2DD77912270E23870079CBD4 /* RCOfferingsAPI.m in Sources */, diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingsAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingsAPI.m index d50018c814..4d34b8e1ab 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingsAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingsAPI.m @@ -15,6 +15,7 @@ @implementation RCOfferingsAPI + (void)checkAPI { RCOfferings *o; RCOffering *of = o.current; + of = [o currentOfferingForPlacement:@""]; NSDictionary *a = o.all; of = [o offeringWithIdentifier:nil]; of = [o offeringWithIdentifier:@""]; diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContext.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContext.m deleted file mode 100644 index da419cb43d..0000000000 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContext.m +++ /dev/null @@ -1,22 +0,0 @@ -// -// RCPresentedOfferingContext.m -// ObjCAPITester -// -// Created by Josh Holtz on 2/14/24. -// - -#import "RCPresentedOfferingContext.h" - -@import StoreKit; -@import RevenueCat; - -@implementation RCPresentOfferingContextAPI - -+ (void)checkAPI { - RCPresentedOfferingContext *poc = [[RCPresentedOfferingContext alloc] initWithOfferingIdentifier:@""]; - NSString *oid = poc.offeringIdentifier; - - NSLog(poc, oid); -} - -@end diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContext.h b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContextAPI.h similarity index 86% rename from Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContext.h rename to Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContextAPI.h index 59ba5134be..de67ea9474 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContext.h +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContextAPI.h @@ -1,5 +1,5 @@ // -// RCPresentedOfferingContext.h +// RCPresentedOfferingContextAPI.h // ObjCAPITester // // Created by Josh Holtz on 2/14/24. diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContextAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContextAPI.m new file mode 100644 index 0000000000..e48ea0da6c --- /dev/null +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPresentedOfferingContextAPI.m @@ -0,0 +1,25 @@ +// +// RCPresentedOfferingContextAPI.m +// ObjCAPITester +// +// Created by Josh Holtz on 2/14/24. +// + +#import "RCPresentedOfferingContextAPI.h" + +@import StoreKit; +@import RevenueCat; + +@implementation RCPresentOfferingContextAPI + ++ (void)checkAPI { + RCPresentedOfferingContext *poc = [[RCPresentedOfferingContext alloc] initWithOfferingIdentifier:@""]; + poc = [[RCPresentedOfferingContext alloc] initWithOfferingIdentifier:@"" placementIdentifier:nil]; + poc = [[RCPresentedOfferingContext alloc] initWithOfferingIdentifier:@"" placementIdentifier:@""]; + NSString *oid = poc.offeringIdentifier; + NSString *pid = poc.placementIdentifier; + + NSLog(poc, oid, pid); +} + +@end diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/main.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/main.m index 638aaeb501..f7f5f7b0d5 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/main.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/main.m @@ -17,7 +17,7 @@ #import "RCOfferingAPI.h" #import "RCOfferingsAPI.h" #import "RCPackageAPI.h" -#import "RCPresentedOfferingContext.h" +#import "RCPresentedOfferingContextAPI.h" #import "RCPromotionalOfferAPI.h" #import "RCPurchasesAPI.h" #import "RCPurchasesErrorCodeAPI.h" diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingsAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingsAPI.swift index 747eeb1976..6a59356acf 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingsAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingsAPI.swift @@ -17,6 +17,7 @@ import RevenueCat var offs: Offerings! func checkOfferingsAPI() { var off: Offering? = offs.current + off = offs.currentOffering(forPlacement: "") let all: [String: Offering] = offs.all off = offs.offering(identifier: nil) off = offs.offering(identifier: "") diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift index e6c060d40b..ea87b7e224 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PresentedOfferingContextAPI.swift @@ -11,8 +11,10 @@ import StoreKit func checkPresentedOfferingContextAPI(context: PresentedOfferingContext! = nil) { let _: String = context.offeringIdentifier + let _: String? = context.placementIdentifier } private func checkCreatePresentedOfferingContextAPI() { let _: PresentedOfferingContext = .init(offeringIdentifier: "") + let _: PresentedOfferingContext = .init(offeringIdentifier: "", placementIdentifier: "") } diff --git a/Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift b/Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift index cc748fc89c..60ff5d6460 100644 --- a/Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift @@ -126,7 +126,8 @@ private extension PaywallEventsIntegrationTests { transactionID: transaction.transactionIdentifier, productID: self.package.storeProduct.productIdentifier, transactionDate: transaction.purchaseDate, - offeringID: self.package.offeringIdentifier, + offeringID: self.package.presentedOfferingContext.offeringIdentifier, + placementID: self.package.presentedOfferingContext.placementIdentifier, paywallSessionID: sessionID ) ) diff --git a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift index 32cc6e577c..7793d939a0 100644 --- a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift @@ -65,7 +65,8 @@ class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests { transactionID: transaction.transactionIdentifier, productID: package.storeProduct.productIdentifier, transactionDate: transaction.purchaseDate, - offeringID: package.offeringIdentifier, + offeringID: package.presentedOfferingContext.offeringIdentifier, + placementID: package.presentedOfferingContext.placementIdentifier, paywallSessionID: nil ) ) @@ -93,7 +94,10 @@ class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests { let package = try await self.monthlyPackage try self.purchases.cachePresentedOfferingContext( - PresentedOfferingContext(offeringIdentifier: package.offeringIdentifier), + PresentedOfferingContext( + offeringIdentifier: package.offeringIdentifier, + placementIdentifier: "a_placement" + ), productIdentifier: package.storeProduct.productIdentifier ) @@ -104,7 +108,8 @@ class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests { transactionID: transaction.transactionIdentifier, productID: package.storeProduct.productIdentifier, transactionDate: transaction.purchaseDate, - offeringID: package.offeringIdentifier, + offeringID: package.presentedOfferingContext.offeringIdentifier, + placementID: package.presentedOfferingContext.placementIdentifier, paywallSessionID: nil ) ) diff --git a/Tests/StoreKitUnitTests/OfferingsManagerStoreKitTests.swift b/Tests/StoreKitUnitTests/OfferingsManagerStoreKitTests.swift index a47e57ec60..5f1c81de66 100644 --- a/Tests/StoreKitUnitTests/OfferingsManagerStoreKitTests.swift +++ b/Tests/StoreKitUnitTests/OfferingsManagerStoreKitTests.swift @@ -95,7 +95,7 @@ private extension OfferingsManagerStoreKitTests { packages: [ .init(identifier: "$rc_monthly", platformProductIdentifier: StoreKitConfigTestCase.productID) ]) - ] + ], placements: nil ) } diff --git a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift index 9a059e25b5..bc5ed3f4a6 100644 --- a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift +++ b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift @@ -269,7 +269,9 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { expect(self.backend.invokedPostReceiptDataCount) == 1 expect(self.backend.invokedPostReceiptDataParameters?.productData).toNot(beNil()) - expect(self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingID) == "offering" + expect( + self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingContext?.offeringIdentifier + ) == "offering" expect(self.backend.invokedPostReceiptDataParameters?.transactionData.source.initiationSource) == .purchase } @@ -625,7 +627,9 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { expect(self.backend.invokedPostReceiptDataCount) == 1 expect(self.backend.invokedPostReceiptDataParameters?.productData).toNot(beNil()) - expect(self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingID) == "offering" + expect( + self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingContext?.offeringIdentifier + ) == "offering" } func testPurchaseSK1PackageWithNoProductIdentifierDoesNotPostReceipt() async throws { @@ -805,7 +809,9 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { expect(self.backend.invokedPostReceiptDataCount) == 1 expect(self.backend.invokedPostReceiptDataParameters?.productData).toNot(beNil()) - expect(self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingID).to(beNil()) + expect( + self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingContext?.offeringIdentifier + ).to(beNil()) expect(self.backend.invokedPostReceiptDataParameters?.transactionData.source.initiationSource) == .purchase } @@ -915,7 +921,9 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { expect(self.backend.invokedPostReceiptDataCount) == 1 expect(self.backend.invokedPostReceiptDataParameters?.productData).toNot(beNil()) - expect(self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingID) == "offering" + expect( + self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingContext?.offeringIdentifier + ) == "offering" } @available(iOS 15.0, tvOS 15.0, macOS 12.0, *) @@ -1599,7 +1607,9 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { expect(self.backend.invokedPostReceiptDataCount) == 1 expect(self.backend.invokedPostReceiptDataParameters?.productData).toNot(beNil()) expect(self.backend.invokedPostReceiptDataParameters?.data) == .jws(transaction.jwsRepresentation) - expect(self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingID).to(beNil()) + expect( + self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingContext?.offeringIdentifier + ).to(beNil()) expect(self.backend.invokedPostReceiptDataParameters?.transactionData.source.initiationSource) == .purchase } @@ -1639,7 +1649,9 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { expect(self.backend.invokedPostReceiptDataCount) == 1 expect(self.backend.invokedPostReceiptDataParameters?.productData).toNot(beNil()) expect(self.backend.invokedPostReceiptDataParameters?.data) == .sk2receipt(receipt) - expect(self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingID).to(beNil()) + expect( + self.backend.invokedPostReceiptDataParameters?.transactionData.presentedOfferingContext?.offeringIdentifier + ).to(beNil()) expect(self.backend.invokedPostReceiptDataParameters?.transactionData.source.initiationSource) == .purchase } diff --git a/Tests/StoreKitUnitTests/Support/DebugViewSwiftUITests.swift b/Tests/StoreKitUnitTests/Support/DebugViewSwiftUITests.swift index a31ced9764..6a1146c8ac 100644 --- a/Tests/StoreKitUnitTests/Support/DebugViewSwiftUITests.swift +++ b/Tests/StoreKitUnitTests/Support/DebugViewSwiftUITests.swift @@ -51,6 +51,7 @@ class DebugViewSwiftUITests: TestCase { model.offerings = .loaded(.init( offerings: [:], currentOfferingID: nil, + placements: .init(fallbackOfferingId: "", offeringIdsByPlacement: [:]), response: .mockResponse )) diff --git a/Tests/TestingApps/PurchaseTesterSwiftUI/Shared/Views/HomeView.swift b/Tests/TestingApps/PurchaseTesterSwiftUI/Shared/Views/HomeView.swift index 7517f2e62b..e3d3f3266f 100644 --- a/Tests/TestingApps/PurchaseTesterSwiftUI/Shared/Views/HomeView.swift +++ b/Tests/TestingApps/PurchaseTesterSwiftUI/Shared/Views/HomeView.swift @@ -19,12 +19,14 @@ struct HomeView: View { @State var offerings: [RevenueCat.Offering] = [] @State private var debugOverlayVisible = false - @State private var showingAlert = false + @State private var showLoginPrompt = false @State private var newAppUserID: String = "" + @State private var showPlacementPrompt = false + @State private var newPlacementID: String = "" + @State private var placementOffering: Offering? = nil @State private var cacheFetchPolicy: CacheFetchPolicy = .default @State private var error: Error? - private var content: some View { VStack(alignment: .leading) { CustomerInfoHeaderView(debugOverlayVisible: self.$debugOverlayVisible) { action in @@ -47,6 +49,11 @@ struct HomeView: View { } Section("Functions") { + Button { + self.showPlacementPrompt = true + } label: { + Text("Find Placement") + } Button { Task { do { @@ -152,7 +159,7 @@ struct HomeView: View { message: { Text($0.subtitle) } ) .navigationTitle("PurchaseTester") - .textFieldAlert(isShowing: self.$showingAlert, title: "App User ID", fields: [("User ID", "ID of your user", self.$newAppUserID)]) { + .textFieldAlert(isShowing: self.$showLoginPrompt, title: "App User ID", fields: [("User ID", "ID of your user", self.$newAppUserID)]) { guard !self.newAppUserID.isEmpty else { return } @@ -168,8 +175,30 @@ struct HomeView: View { } } } + .textFieldAlert(isShowing: self.$showPlacementPrompt, title: "Placement", fields: [("Placement Name", "ID of your placement", self.$newPlacementID)]) { + guard !self.newPlacementID.isEmpty else { + return + } + + Task { + do { + let offerings = try await Purchases.shared.offerings() + self.placementOffering = offerings.currentOffering(forPlacement: newPlacementID) + + } catch { + + self.error = error + } + } + } + .sheet(item: self.$placementOffering) { + OfferingDetailView(offering: $0) + } } + + + var body: some View { #if DEBUG && !os(visionOS) && !os(watchOS) if #available(iOS 16.0, macOS 13.0, *) { @@ -197,7 +226,7 @@ struct HomeView: View { private func showLogin() { self.newAppUserID = "" - self.showingAlert = true + self.showLoginPrompt = true } private func logOut() async { diff --git a/Tests/UnitTests/Caching/DeviceCacheTests.swift b/Tests/UnitTests/Caching/DeviceCacheTests.swift index 850789a0c4..9064670c24 100644 --- a/Tests/UnitTests/Caching/DeviceCacheTests.swift +++ b/Tests/UnitTests/Caching/DeviceCacheTests.swift @@ -483,7 +483,8 @@ private extension DeviceCacheTests { "platform_product_identifier": "com.myproduct.annual"}, {"identifier": "$rc_six_month", "platform_product_identifier": "com.myproduct.sixMonth"} - ] + ], + "current_offering_ids_by_placement": {"placement_identifier": "\(offeringIdentifier)"}, } """ let offeringsData: OfferingsResponse.Offering = try JSONDecoder.default.decode( @@ -496,8 +497,11 @@ private extension DeviceCacheTests { return Offerings( offerings: [offeringIdentifier: offering], currentOfferingID: "base", - response: .init(currentOfferingId: "base", offerings: [offeringsData]) - ) + placements: .init(fallbackOfferingId: "", offeringIdsByPlacement: [:]), + response: .init(currentOfferingId: "base", + offerings: [offeringsData], + placements: .init(fallbackOfferingId: "", offeringIdsByPlacement: .init(wrappedValue: [:])) + )) } } @@ -507,9 +511,11 @@ private extension Offerings { static let empty: Offerings = .init( offerings: [:], currentOfferingID: "", + placements: .init(fallbackOfferingId: "", offeringIdsByPlacement: [:]), response: .init( currentOfferingId: "", - offerings: [] + offerings: [], + placements: .init(fallbackOfferingId: "", offeringIdsByPlacement: .init(wrappedValue: [:])) ) ) diff --git a/Tests/UnitTests/Identity/CustomerInfoManagerPostReceiptTests.swift b/Tests/UnitTests/Identity/CustomerInfoManagerPostReceiptTests.swift index f246aa611f..c6bce8ed63 100644 --- a/Tests/UnitTests/Identity/CustomerInfoManagerPostReceiptTests.swift +++ b/Tests/UnitTests/Identity/CustomerInfoManagerPostReceiptTests.swift @@ -76,7 +76,7 @@ class CustomerInfoManagerPostReceiptTests: BaseCustomerInfoManagerTests { expect(parameters.transaction as? StoreTransaction) === transaction expect(parameters.data.appUserID) == Self.userID - expect(parameters.data.presentedOfferingID).to(beNil()) + expect(parameters.data.presentedOfferingContext?.offeringIdentifier).to(beNil()) expect(parameters.data.unsyncedAttributes).to(beEmpty()) expect(parameters.data.source.isRestore) == false expect(parameters.data.source.initiationSource) == .queue diff --git a/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift b/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift index b98a026587..a9f2c7323b 100644 --- a/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift +++ b/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift @@ -32,9 +32,14 @@ class PurchasesDiagnosticsTests: TestCase { self.purchases.mockedHealthRequestResponse = .success(()) self.purchases.mockedCustomerInfoResponse = .success(.emptyInfo) - self.purchases.mockedOfferingsResponse = .success(.init(offerings: [:], - currentOfferingID: nil, - response: .init(currentOfferingId: nil, offerings: []))) + self.purchases.mockedOfferingsResponse = .success( + .init(offerings: [:], + currentOfferingID: nil, + placements: nil, + response: .init(currentOfferingId: nil, + offerings: [], + placements: nil)) + ) } func testFailingHealthRequest() async throws { diff --git a/Tests/UnitTests/Mocks/MockOfferingsFactory.swift b/Tests/UnitTests/Mocks/MockOfferingsFactory.swift index 9ef134945d..c94b397404 100644 --- a/Tests/UnitTests/Mocks/MockOfferingsFactory.swift +++ b/Tests/UnitTests/Mocks/MockOfferingsFactory.swift @@ -17,7 +17,8 @@ class MockOfferingsFactory: OfferingsFactory { if emptyOfferings { return Offerings(offerings: [:], currentOfferingID: "base", - response: .init(currentOfferingId: "base", offerings: [])) + placements: nil, + response: .init(currentOfferingId: "base", offerings: [], placements: nil)) } if nilOfferings { return nil @@ -40,12 +41,14 @@ class MockOfferingsFactory: OfferingsFactory { ] )], currentOfferingID: "base", + placements: nil, response: .init(currentOfferingId: "base", offerings: [ .init(identifier: "base", description: "This is the base offering", packages: [ .init(identifier: "", platformProductIdentifier: "$rc_monthly") ]) - ]) + ], placements: nil) + ) } } @@ -60,7 +63,7 @@ extension OfferingsResponse { packages: [ .init(identifier: "$rc_monthly", platformProductIdentifier: "monthly_freetrial") ]) - ] + ], placements: nil ) } diff --git a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift index 91d3c03c5b..cd285799f6 100644 --- a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift @@ -43,7 +43,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -74,7 +74,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .purchase) @@ -108,7 +108,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: .createMockProductData(), transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .purchase) @@ -137,7 +137,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .purchase) @@ -150,7 +150,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -180,7 +180,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .purchase) @@ -194,7 +194,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: !isRestore, initiationSource: .queue) @@ -223,7 +223,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -237,7 +237,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -266,7 +266,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .purchase) @@ -281,7 +281,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .purchase) @@ -310,7 +310,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: "offering_a", + presentedOfferingContext: .init(offeringIdentifier: "offering_a"), unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -324,7 +324,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: "offering_b", + presentedOfferingContext: .init(offeringIdentifier: "offering_b"), unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -364,7 +364,52 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: offeringIdentifier, + presentedOfferingContext: .init(offeringIdentifier: offeringIdentifier), + unsyncedAttributes: nil, + storefront: nil, + source: .init(isRestore: false, initiationSource: .purchase) + ), + observerMode: false, + completion: { _ in + completed() + }) + } + + expect(self.httpClient.calls).to(haveCount(1)) + } + + func testPostsReceiptDataWithPresentedOfferingContext() throws { + self.httpClient.mock( + requestPath: .postReceiptData, + response: .init(statusCode: .success, response: Self.validCustomerResponse) + ) + + let productIdentifier = "a_great_product" + let offeringIdentifier = "a_offering" + let placementIdentifier = "a_placement" + let price: Decimal = 10.98 + let group = "sub_group" + + let currencyCode = "BFD" + + let context = PresentedOfferingContext( + offeringIdentifier: offeringIdentifier, + placementIdentifier: placementIdentifier + ) + + let productData: ProductRequestData = .createMockProductData(productIdentifier: productIdentifier, + paymentMode: nil, + currencyCode: currencyCode, + price: price, + subscriptionGroup: group) + + waitUntil { completed in + self.backend.post(receipt: Self.receipt, + productData: productData, + transactionData: .init( + appUserID: Self.userID, + presentedOfferingContext: context, + presentedPaywall: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .purchase) @@ -415,7 +460,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: offeringIdentifier, + presentedOfferingContext: .init(offeringIdentifier: offeringIdentifier), presentedPaywall: .impression(paywallEventCreationData, paywallEventData), unsyncedAttributes: nil, storefront: nil, @@ -444,7 +489,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .queue) @@ -540,7 +585,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .queue) @@ -580,7 +625,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .purchase) @@ -607,7 +652,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: true, initiationSource: .queue) @@ -636,7 +681,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -659,7 +704,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -705,7 +750,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .queue) @@ -733,7 +778,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -747,7 +792,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: "offering_a", + presentedOfferingContext: .init(offeringIdentifier: "offering_a"), unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .queue) @@ -778,7 +823,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .purchase) @@ -813,7 +858,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: .createMockProductData(), transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .purchase) @@ -848,7 +893,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .purchase) @@ -879,7 +924,7 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: isRestore, initiationSource: .purchase) @@ -913,7 +958,7 @@ class BackendPostReceiptWithSignatureVerificationTests: BaseBackendPostReceiptDa productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .purchase) @@ -944,7 +989,7 @@ class BackendPostReceiptWithSignatureVerificationTests: BaseBackendPostReceiptDa productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .purchase) @@ -988,7 +1033,7 @@ class BackendPostReceiptCustomEntitlementsTests: BaseBackendPostReceiptDataTests productData: nil, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .queue) @@ -1032,7 +1077,7 @@ private extension BaseBackendPostReceiptDataTests { productData: productData, transactionData: .init( appUserID: Self.userID, - presentedOfferingID: nil, + presentedOfferingContext: nil, unsyncedAttributes: nil, storefront: nil, source: .init(isRestore: false, initiationSource: .queue) diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS12-testPostsReceiptDataWithPresentedOfferingContext.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS12-testPostsReceiptDataWithPresentedOfferingContext.1.json new file mode 100644 index 0000000000..bf23d4ff96 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS12-testPostsReceiptDataWithPresentedOfferingContext.1.json @@ -0,0 +1,40 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application\/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "app_user_id" : "user", + "attributes" : { + "$attConsentStatus" : { + "updated_at_ms" : 1678307200000, + "value" : "notDetermined" + } + }, + "currency" : "BFD", + "fetch_token" : "YW4gYXdlc29tZSByZWNlaXB0", + "initiation_source" : "purchase", + "is_restore" : false, + "observer_mode" : false, + "presented_offering_identifier" : "a_offering", + "price" : "10.98", + "product_id" : "a_great_product", + "store_country" : "ESP", + "subscription_group_id" : "sub_group" + }, + "method" : "POST", + "url" : "https:\/\/api.revenuecat.com\/v1\/receipts" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS13-testPostsReceiptDataWithPresentedOfferingContext.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS13-testPostsReceiptDataWithPresentedOfferingContext.1.json new file mode 100644 index 0000000000..f6d147ea96 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS13-testPostsReceiptDataWithPresentedOfferingContext.1.json @@ -0,0 +1,41 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Storefront" : "USA", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "app_user_id" : "user", + "attributes" : { + "$attConsentStatus" : { + "updated_at_ms" : 1678307200000, + "value" : "notDetermined" + } + }, + "currency" : "BFD", + "fetch_token" : "YW4gYXdlc29tZSByZWNlaXB0", + "initiation_source" : "purchase", + "is_restore" : false, + "observer_mode" : false, + "presented_offering_identifier" : "a_offering", + "price" : "10.98", + "product_id" : "a_great_product", + "store_country" : "ESP", + "subscription_group_id" : "sub_group" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/receipts" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS14-testPostsReceiptDataWithPresentedOfferingContext.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS14-testPostsReceiptDataWithPresentedOfferingContext.1.json new file mode 100644 index 0000000000..1614019366 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS14-testPostsReceiptDataWithPresentedOfferingContext.1.json @@ -0,0 +1,41 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Storefront" : "USA", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "app_user_id" : "user", + "attributes" : { + "$attConsentStatus" : { + "updated_at_ms" : 1678307200000, + "value" : "authorized" + } + }, + "currency" : "BFD", + "fetch_token" : "YW4gYXdlc29tZSByZWNlaXB0", + "initiation_source" : "purchase", + "is_restore" : false, + "observer_mode" : false, + "presented_offering_identifier" : "a_offering", + "price" : "10.98", + "product_id" : "a_great_product", + "store_country" : "ESP", + "subscription_group_id" : "sub_group" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/receipts" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS15-testPostsReceiptDataWithPresentedOfferingContext.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS15-testPostsReceiptDataWithPresentedOfferingContext.1.json new file mode 100644 index 0000000000..1614019366 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS15-testPostsReceiptDataWithPresentedOfferingContext.1.json @@ -0,0 +1,41 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Storefront" : "USA", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "app_user_id" : "user", + "attributes" : { + "$attConsentStatus" : { + "updated_at_ms" : 1678307200000, + "value" : "authorized" + } + }, + "currency" : "BFD", + "fetch_token" : "YW4gYXdlc29tZSByZWNlaXB0", + "initiation_source" : "purchase", + "is_restore" : false, + "observer_mode" : false, + "presented_offering_identifier" : "a_offering", + "price" : "10.98", + "product_id" : "a_great_product", + "store_country" : "ESP", + "subscription_group_id" : "sub_group" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/receipts" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS16-testPostsReceiptDataWithPresentedOfferingContext.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS16-testPostsReceiptDataWithPresentedOfferingContext.1.json new file mode 100644 index 0000000000..1614019366 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS16-testPostsReceiptDataWithPresentedOfferingContext.1.json @@ -0,0 +1,41 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Storefront" : "USA", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "app_user_id" : "user", + "attributes" : { + "$attConsentStatus" : { + "updated_at_ms" : 1678307200000, + "value" : "authorized" + } + }, + "currency" : "BFD", + "fetch_token" : "YW4gYXdlc29tZSByZWNlaXB0", + "initiation_source" : "purchase", + "is_restore" : false, + "observer_mode" : false, + "presented_offering_identifier" : "a_offering", + "price" : "10.98", + "product_id" : "a_great_product", + "store_country" : "ESP", + "subscription_group_id" : "sub_group" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/receipts" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS17-testPostsReceiptDataWithPresentedOfferingContext.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS17-testPostsReceiptDataWithPresentedOfferingContext.1.json new file mode 100644 index 0000000000..1614019366 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/iOS17-testPostsReceiptDataWithPresentedOfferingContext.1.json @@ -0,0 +1,41 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Apple-Device-Identifier" : "5D7C0074-07E4-4564-AAA4-4008D0640881", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "true", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Storefront" : "USA", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "app_user_id" : "user", + "attributes" : { + "$attConsentStatus" : { + "updated_at_ms" : 1678307200000, + "value" : "authorized" + } + }, + "currency" : "BFD", + "fetch_token" : "YW4gYXdlc29tZSByZWNlaXB0", + "initiation_source" : "purchase", + "is_restore" : false, + "observer_mode" : false, + "presented_offering_identifier" : "a_offering", + "price" : "10.98", + "product_id" : "a_great_product", + "store_country" : "ESP", + "subscription_group_id" : "sub_group" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/receipts" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/macOS-testPostsReceiptDataWithPresentedOfferingContext.1.json b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/macOS-testPostsReceiptDataWithPresentedOfferingContext.1.json new file mode 100644 index 0000000000..5d988bc0d2 --- /dev/null +++ b/Tests/UnitTests/Networking/Backend/__Snapshots__/BackendPostReceiptDataTests/macOS-testPostsReceiptDataWithPresentedOfferingContext.1.json @@ -0,0 +1,40 @@ +{ + "headers" : { + "Authorization" : "Bearer asharedsecret", + "content-type" : "application/json", + "X-Client-Build-Version" : "12345", + "X-Client-Bundle-ID" : "com.apple.dt.xctest.tool", + "X-Client-Version" : "17.0.0", + "X-Is-Sandbox" : "false", + "X-Observer-Mode-Enabled" : "false", + "X-Platform" : "iOS", + "X-Platform-Flavor" : "native", + "X-Platform-Version" : "Version 17.0.0 (Build 21A342)", + "X-Storefront" : "USA", + "X-StoreKit2-Enabled" : "false", + "X-Version" : "4.0.0" + }, + "request" : { + "body" : { + "app_user_id" : "user", + "attributes" : { + "$attConsentStatus" : { + "updated_at_ms" : 1678307200000, + "value" : "authorized" + } + }, + "currency" : "BFD", + "fetch_token" : "YW4gYXdlc29tZSByZWNlaXB0", + "initiation_source" : "purchase", + "is_restore" : false, + "observer_mode" : false, + "presented_offering_identifier" : "a_offering", + "price" : "10.98", + "product_id" : "a_great_product", + "store_country" : "ESP", + "subscription_group_id" : "sub_group" + }, + "method" : "POST", + "url" : "https://api.revenuecat.com/v1/receipts" + } +} \ No newline at end of file diff --git a/Tests/UnitTests/Paywalls/PaywallCacheWarmingTests.swift b/Tests/UnitTests/Paywalls/PaywallCacheWarmingTests.swift index 640ebb2aba..b5547b1ad8 100644 --- a/Tests/UnitTests/Paywalls/PaywallCacheWarmingTests.swift +++ b/Tests/UnitTests/Paywalls/PaywallCacheWarmingTests.swift @@ -190,6 +190,7 @@ private extension PaywallCacheWarmingTests { return .init( offerings: Set(offerings).dictionaryWithKeys(\.identifier), currentOfferingID: Self.offeringIdentifier, + placements: nil, response: offeringsResponse ) } diff --git a/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift b/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift index 8d91064692..8a71d9cc7c 100644 --- a/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift +++ b/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift @@ -159,7 +159,8 @@ extension OfferingsManagerTests { func testOfferingsForAppUserIDReturnsConfigurationErrorIfBackendReturnsEmpty() throws { // given self.mockOfferings.stubbedGetOfferingsCompletionResult = .success( - .init(currentOfferingId: "", offerings: []) + .init(currentOfferingId: "", offerings: [], + placements: nil) ) self.mockOfferingsFactory.emptyOfferings = true @@ -209,7 +210,8 @@ extension OfferingsManagerTests { func testOfferingsLogsErrorInformationIfBackendReturnsEmpty() throws { // given self.mockOfferings.stubbedGetOfferingsCompletionResult = .success( - .init(currentOfferingId: "", offerings: []) + .init(currentOfferingId: "", offerings: [], + placements: nil) ) self.mockOfferingsFactory.emptyOfferings = true @@ -461,7 +463,8 @@ private extension OfferingsManagerTests { packages: [ .init(identifier: "$rc_monthly", platformProductIdentifier: "monthly_freetrial") ]) - ] + ], + placements: nil ) static let backendOfferingsResponseWithUnknownProducts: OfferingsResponse = .init( currentOfferingId: "base", @@ -472,7 +475,8 @@ private extension OfferingsManagerTests { .init(identifier: "$rc_monthly", platformProductIdentifier: "monthly_freetrial"), .init(identifier: "$rc_yearly", platformProductIdentifier: "yearly_freetrial") ]) - ] + ], + placements: nil ) static let unexpectedBackendResponseError: BackendError = .unexpectedBackendResponse( .customerInfoNil @@ -498,6 +502,7 @@ private extension OfferingsManagerTests { } .dictionaryWithKeys(\.identifier), currentOfferingID: MockData.anyBackendOfferingsResponse.currentOfferingId, + placements: nil, response: MockData.anyBackendOfferingsResponse ) } diff --git a/Tests/UnitTests/Purchasing/OfferingsTests.swift b/Tests/UnitTests/Purchasing/OfferingsTests.swift index c18e8061ed..d6e262b91f 100644 --- a/Tests/UnitTests/Purchasing/OfferingsTests.swift +++ b/Tests/UnitTests/Purchasing/OfferingsTests.swift @@ -113,7 +113,8 @@ class OfferingsTests: TestCase { packages: [ .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly") ]) - ] + ], + placements: nil ) ) @@ -146,7 +147,8 @@ class OfferingsTests: TestCase { .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly"), .init(identifier: "custom_package", platformProductIdentifier: "com.myproduct.custom") ]) - ] + ], + placements: nil ) ) ) @@ -165,6 +167,103 @@ class OfferingsTests: TestCase { expect(offeringB.availablePackages[safe: 1]?.packageType) == .custom } + func testOfferingIdsByPlacementWithFallbackOffering() throws { + let annualProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.annual") + let monthlyProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.monthly") + let customProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.custom") + let products = [ + "com.myproduct.annual": StoreProduct(sk1Product: annualProduct), + "com.myproduct.monthly": StoreProduct(sk1Product: monthlyProduct), + "com.myproduct.custom": StoreProduct(sk1Product: customProduct) + ] + let offerings = try XCTUnwrap( + self.offeringsFactory.createOfferings( + from: products, + data: .init( + currentOfferingId: "offering_a", + offerings: [ + .init(identifier: "offering_a", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.annual") + ]), + .init(identifier: "offering_b", + description: "This is the base offering b", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly"), + .init(identifier: "custom_package", platformProductIdentifier: "com.myproduct.custom") + ]), + .init(identifier: "offering_c", + description: "This is the base offering b", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly"), + .init(identifier: "custom_package", platformProductIdentifier: "com.myproduct.custom") + ]) + ], + placements: .init(fallbackOfferingId: "offering_c", + offeringIdsByPlacement: .init(wrappedValue: [ + "placement_name": "offering_b", + "placement_name_with_nil": nil + ])) + ) + ) + ) + + let offeringA = try XCTUnwrap(offerings["offering_a"]) + let offeringB = try XCTUnwrap(offerings["offering_b"]) + let offeringC = try XCTUnwrap(offerings["offering_c"]) + expect(offerings.current) === offeringA + expect(offerings.currentOffering(forPlacement: "placement_name")!.identifier) == offeringB.identifier + expect(offerings.currentOffering(forPlacement: "placement_name_with_nil")).to(beNil()) + expect(offerings.currentOffering( + forPlacement: "unexisting_placement_name")!.identifier + ) == offeringC.identifier + } + + func testOfferingIdsByPlacementWithNullFallbackOffering() throws { + let annualProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.annual") + let monthlyProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.monthly") + let customProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.custom") + let products = [ + "com.myproduct.annual": StoreProduct(sk1Product: annualProduct), + "com.myproduct.monthly": StoreProduct(sk1Product: monthlyProduct), + "com.myproduct.custom": StoreProduct(sk1Product: customProduct) + ] + let offerings = try XCTUnwrap( + self.offeringsFactory.createOfferings( + from: products, + data: .init( + currentOfferingId: "offering_a", + offerings: [ + .init(identifier: "offering_a", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.annual") + ]), + .init(identifier: "offering_b", + description: "This is the base offering b", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly"), + .init(identifier: "custom_package", platformProductIdentifier: "com.myproduct.custom") + ]) + ], + placements: .init(fallbackOfferingId: nil, + offeringIdsByPlacement: .init(wrappedValue: [ + "placement_name": "offering_b", + "placement_name_with_nil": nil + ])) + ) + ) + ) + + let offeringA = try XCTUnwrap(offerings["offering_a"]) + let offeringB = try XCTUnwrap(offerings["offering_b"]) + expect(offerings.current) === offeringA + expect(offerings.currentOffering(forPlacement: "placement_name")!.identifier) == offeringB.identifier + expect(offerings.currentOffering(forPlacement: "placement_name_with_nil")).to(beNil()) + expect(offerings.currentOffering(forPlacement: "unexisting_placement_name")).to(beNil()) + } + func testOfferingsWithMetadataIsCreated() throws { let metadata: [String: AnyDecodable] = [ "int": 5, @@ -213,7 +312,8 @@ class OfferingsTests: TestCase { packages: [ .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly") ]) - ] + ], + placements: .init(fallbackOfferingId: "", offeringIdsByPlacement: .init(wrappedValue: [:])) ) ) ) @@ -337,7 +437,8 @@ class OfferingsTests: TestCase { packages: [ .init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.annual") ]) - ] + ], + placements: nil ) let offerings = try XCTUnwrap( self.offeringsFactory.createOfferings(from: storeProductsByID, data: response) @@ -376,7 +477,8 @@ private extension OfferingsTests { packages: [ .init(identifier: identifier, platformProductIdentifier: productIdentifier) ]) - ] + ], + placements: nil ) ) ) diff --git a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift index da15a0ebfc..36b7aa2bd9 100644 --- a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift @@ -470,7 +470,7 @@ extension BasePurchasesTests { self.postedDiscounts = productData.discounts } - self.postedOfferingIdentifier = transactionData.presentedOfferingID + self.postedOfferingIdentifier = transactionData.presentedOfferingContext?.offeringIdentifier self.postedObserverMode = observerMode self.postedInitiationSource = transactionData.source.initiationSource diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift index 32c720239e..26e29800e6 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift @@ -134,6 +134,7 @@ class PurchasesGetOfferingsTests: BasePurchasesTests { offering.identifier: offering ], currentOfferingID: offering.identifier, + placements: nil, response: offeringsResponse ) diff --git a/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift b/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift index c2ab508666..5e97e0c9e4 100644 --- a/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift @@ -439,7 +439,7 @@ private func match(_ data: PurchasedTransactionData) -> Nimble.Predicate