Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add currentOffering(forPlacement: String) to Offerings #3707

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b19a9af
include current offering ids by placement object in the Offerings str…
Jan 19, 2024
ed8895a
docs docs docs
Jan 19, 2024
2ba6ba7
adds tests
Jan 22, 2024
93ffbde
add APITesters tests
Jan 22, 2024
0c95edf
rename parameter for getCurrentOffering(for:) to getCurrentOffering()…
Jan 25, 2024
422cbc2
Refactor structure + func to new data structure
Feb 15, 2024
64b9802
WIP: adds context for placementIdentifier
Feb 15, 2024
687d4b3
Fix StoreKit view extension compile error for presented offering context
joshdholtz Feb 16, 2024
5a208a9
WIP update methods to return placement + update test purchase app
Feb 23, 2024
4d1f107
fix tests
Feb 26, 2024
afd340b
fix more tests :()
Feb 26, 2024
fbebf2b
upload obj c test
Feb 26, 2024
89ddaac
Fixed some lint
joshdholtz Feb 27, 2024
5ccb6b9
Fix compile error in backend tests
joshdholtz Feb 27, 2024
cf36d01
Fix API tester
joshdholtz Feb 27, 2024
cf30753
One more api tester fix
joshdholtz Feb 27, 2024
7e2f47b
This should pass
joshdholtz Feb 27, 2024
e7828ca
This is it
joshdholtz Feb 27, 2024
cda6a23
Some fixes from PR review
joshdholtz Feb 27, 2024
2d6262c
Passing PresentedOfferingContext further
joshdholtz Feb 27, 2024
c282c1c
Added a conveince initializer fo PresentedOfferingContext so Obj-c co…
joshdholtz Feb 27, 2024
f9ee234
Replaced some empty placements with nil
joshdholtz Feb 27, 2024
cfd5da7
Empty commit to branch snapshot updates from
joshdholtz Feb 27, 2024
fd3427c
Add new backend post receipt test for presented offering context
joshdholtz Feb 27, 2024
a2a3406
Generating new test snapshots for `guido/fia-2856-rework-ios-add-getc…
RCGitBot Feb 27, 2024
589a7b0
Generating new test snapshots for `guido/fia-2856-rework-ios-add-getc…
RCGitBot Feb 27, 2024
dbeef9e
Generating new test snapshots for `guido/fia-2856-rework-ios-add-getc…
RCGitBot Feb 27, 2024
8bc381f
Generating new test snapshots for `guido/fia-2856-rework-ios-add-getc…
RCGitBot Feb 27, 2024
6e80a44
Generating new test snapshots for `guido/fia-2856-rework-ios-add-getc…
RCGitBot Feb 27, 2024
bc77744
Generating new test snapshots for `guido/fia-2856-rework-ios-add-getc…
RCGitBot Feb 27, 2024
3eb9898
Generating new test snapshots for `guido/fia-2856-rework-ios-add-getc…
RCGitBot Feb 27, 2024
24d2c01
Rename getCurrentOffering to currentOffering
joshdholtz Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Identity/CustomerInfoManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ private extension CustomerInfoManager {

let transactionData = PurchasedTransactionData(
appUserID: appUserID,
presentedOfferingID: nil,
presentedOfferingContext: nil,
unsyncedAttributes: [:],
storefront: await Storefront.currentStorefront,
source: Self.sourceForUnfinishedTransaction
Expand Down
12 changes: 11 additions & 1 deletion Sources/Logging/Strings/PurchaseStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -306,14 +307,23 @@ 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))"

if let offeringIdentifier = offeringID {
message += " in Offering '\(offeringIdentifier)'"
}

if let placementIdentifier = placementID {
message += " with Placement '\(placementIdentifier)'"
}

if let paywallSessionID {
message += " with paywall session '\(paywallSessionID)'"
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/Networking/Operations/PostReceiptDataOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions Sources/Networking/Responses/OfferingsResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {}
63 changes: 63 additions & 0 deletions Sources/Purchasing/Offerings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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]}
}
Comment on lines +124 to +131
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

placements.offeringIdsByPlacement[placementIdentifier] returns at Offering?? because placements.offeringIdsByPlacement is a [String: String?]

The result of could be an explicit nil value. In that case, we don't want to use the fallback offering.

If placements.offeringIdsByPlacement[placementIdentifier] is not found and goes into the else, it means no key was found and we should use the fallback offering.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick but should we have an internal copy constructor from package so we can create a new package with a new context without having to pass all the parameters again?

)
}

return Offering(identifier: self.identifier,
serverDescription: self.serverDescription,
metadata: self.metadata,
paywall: self.paywall,
availablePackages: updatedPackages
)
}
}
11 changes: 11 additions & 0 deletions Sources/Purchasing/OfferingsFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class OfferingsFactory {

return Offerings(offerings: offerings,
currentOfferingID: data.currentOfferingId,
placements: createPlacement(with: data.placements),
response: data)
}

Expand Down Expand Up @@ -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:
Expand Down
20 changes: 17 additions & 3 deletions Sources/Purchasing/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm late to the game here, but... what do I do with this class as a developer? Is it so I can use it for analytics?
The docstring for the class doesn't really provide any context on it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aboedo Oops, missed this comment from yesterday! This is mainly for internal usage. Instead of passing offeringIdentifier from get offerings to post receipt, this objects gets passed around instead. It holds offering identifier, placement information, and targeting information.

@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 }

Expand Down Expand Up @@ -91,7 +104,8 @@ import Foundation
identifier: identifier,
packageType: packageType,
storeProduct: storeProduct,
presentedOfferingContext: PresentedOfferingContext(offeringIdentifier: offeringIdentifier)
presentedOfferingContext: PresentedOfferingContext(offeringIdentifier: offeringIdentifier,
placementIdentifier: nil)
)
}

Expand Down
21 changes: 11 additions & 10 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions Sources/Purchasing/Purchases/TransactionPoster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
))

Expand Down
12 changes: 11 additions & 1 deletion Sources/Support/PaywallExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
joshdholtz marked this conversation as resolved.
Show resolved Hide resolved
}?.presentedOfferingContext

Purchases.shared.cachePresentedOfferingContext(
.init(offeringIdentifier: offering.identifier),
offeringContext ?? .init(
offeringIdentifier: offering.identifier,
placementIdentifier: nil
),
productIdentifier: product.id
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: "")
tonidero marked this conversation as resolved.
Show resolved Hide resolved
}
Loading