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 support for product_mapping in promotional offers #4489

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ enum CustomerCenterConfigTestData {
iosOfferId: "offer_id",
eligible: true,
title: "title",
subtitle: "subtitle"
subtitle: "subtitle",
productMapping: ["monthly": "offer_id"]
))
),
.init(
Expand Down
128 changes: 93 additions & 35 deletions RevenueCatUI/CustomerCenter/Data/LoadPromotionalOfferUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,41 +43,20 @@ class LoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType {
do {
let customerInfo = try await self.purchasesProvider.customerInfo()

guard let productIdentifier = customerInfo.earliestExpiringAppStoreEntitlement()?.productIdentifier,
let subscribedProduct = await self.purchasesProvider.products([productIdentifier]).first else {
Logger.warning(Strings.could_not_offer_for_any_active_subscriptions)
return .failure(CustomerCenterError.couldNotFindSubscriptionInformation)
}

let exactMatch = subscribedProduct.discounts.first { discount in
discount.offerIdentifier == promoOfferDetails.iosOfferId
}

let discount: StoreProductDiscount?
if let exactMatch = exactMatch {
discount = exactMatch
} else {
discount = subscribedProduct.discounts.first { discount in
guard let offerIdentifier = discount.offerIdentifier else {
return false
}
return offerIdentifier.hasSuffix("_\(promoOfferDetails.iosOfferId)")
}
}

guard let discount = discount else {
let message =
Strings.could_not_offer_for_active_subscriptions(promoOfferDetails.iosOfferId, productIdentifier)
Logger.debug(message)
return .failure(CustomerCenterError.couldNotFindSubscriptionInformation)
}

let promotionalOffer = try await self.purchasesProvider.promotionalOffer(forProductDiscount: discount,
product: subscribedProduct)
let promotionalOfferData = PromotionalOfferData(promotionalOffer: promotionalOffer,
product: subscribedProduct,
promoOfferDetails: promoOfferDetails)
return .success(promotionalOfferData)
let subscribedProduct = try await getActiveSubscription(customerInfo)
let discount = try findDiscount(for: subscribedProduct,
productIdentifier: subscribedProduct.productIdentifier,
promoOfferDetails: promoOfferDetails)

let promotionalOffer = try await self.purchasesProvider.promotionalOffer(
forProductDiscount: discount,
product: subscribedProduct
)
return .success(PromotionalOfferData(
promotionalOffer: promotionalOffer,
product: subscribedProduct,
promoOfferDetails: promoOfferDetails
))
} catch {
Logger.warning(Strings.error_fetching_promotional_offer(error))
return .failure(CustomerCenterError.couldNotFindOfferForActiveProducts)
Expand All @@ -86,4 +65,83 @@ class LoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType {

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
private extension LoadPromotionalOfferUseCase {

private func getActiveSubscription(_ customerInfo: CustomerInfo) async throws -> StoreProduct {
guard let productIdentifier = customerInfo.earliestExpiringAppStoreEntitlement()?.productIdentifier,
let subscribedProduct = await self.purchasesProvider.products([productIdentifier]).first else {
Logger.warning(Strings.could_not_offer_for_any_active_subscriptions)
throw CustomerCenterError.couldNotFindSubscriptionInformation
}
return subscribedProduct
}

private func findDiscount(
for product: StoreProduct,
productIdentifier: String,
promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer
) throws -> StoreProductDiscount {
let discount = if !promoOfferDetails.productMapping.isEmpty {
findMappedDiscount(for: product,
productIdentifier: productIdentifier,
promoOfferDetails: promoOfferDetails)
} else {
findLegacyDiscount(for: product, promoOfferDetails: promoOfferDetails)
}
Comment on lines +88 to +94
Copy link
Member

Choose a reason for hiding this comment

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

TIL let foo = if ... else works. I would have used a ternary but I feel like this might be more readable


guard let discount = discount else {
logDiscountError(productIdentifier: productIdentifier, promoOfferDetails: promoOfferDetails)
throw CustomerCenterError.couldNotFindSubscriptionInformation
}

return discount
}

private func findMappedDiscount(
for product: StoreProduct,
productIdentifier: String,
promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer
) -> StoreProductDiscount? {
product.discounts.first { $0.offerIdentifier == promoOfferDetails.productMapping[productIdentifier] }
}

private func findLegacyDiscount(
for product: StoreProduct,
promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer
) -> StoreProductDiscount? {
// Try exact match first
if let exactMatch = product.discounts.first(where: {
$0.offerIdentifier == promoOfferDetails.iosOfferId
}) {
Comment on lines +117 to +119
Copy link
Member

Choose a reason for hiding this comment

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

FFTI, but you can also do it like this

Suggested change
if let exactMatch = product.discounts.first(where: {
$0.offerIdentifier == promoOfferDetails.iosOfferId
}) {
if let exactMatch = product.discounts.first {
$0.offerIdentifier == promoOfferDetails.iosOfferId
} {

Then again maybe the brackets add some clarity since it ends in a { too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It actually fails because of the ending {

Copy link
Member

Choose a reason for hiding this comment

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

oh, then nevermind

return exactMatch
}

// Fall back to suffix matching
return product.discounts.first { $0.offerIdentifier?.hasSuffix("_\(promoOfferDetails.iosOfferId)") == true }
}

private func logDiscountError(
productIdentifier: String,
promoOfferDetails: CustomerCenterConfigData.HelpPath.PromotionalOffer
) {
let message = if !promoOfferDetails.productMapping.isEmpty {
Strings.could_not_offer_for_active_subscriptions(
promoOfferDetails.productMapping[productIdentifier] ?? "nil",
productIdentifier
)
} else {
Strings.could_not_offer_for_active_subscriptions(
promoOfferDetails.iosOfferId,
productIdentifier
)
}
Logger.debug(message)
}

}

#endif
9 changes: 8 additions & 1 deletion Sources/CustomerCenter/CustomerCenterConfigData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,18 @@ public struct CustomerCenterConfigData {
public let eligible: Bool
public let title: String
public let subtitle: String
public let productMapping: [String: String]

public init(iosOfferId: String, eligible: Bool, title: String, subtitle: String) {
public init(iosOfferId: String,
eligible: Bool,
title: String,
subtitle: String,
productMapping: [String: String]) {
self.iosOfferId = iosOfferId
self.eligible = eligible
self.title = title
self.subtitle = subtitle
self.productMapping = productMapping
}

}
Expand Down Expand Up @@ -511,6 +517,7 @@ extension CustomerCenterConfigData.HelpPath.PromotionalOffer {
self.eligible = response.eligible
self.title = response.title
self.subtitle = response.subtitle
self.productMapping = response.productMapping
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ struct CustomerCenterConfigResponse {
let eligible: Bool
let title: String
let subtitle: String
let productMapping: [String: String]

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,11 @@ class ManageSubscriptionsViewModelTests: TestCase {
let promoOfferDetails = CustomerCenterConfigData.HelpPath.PromotionalOffer(iosOfferId: offerIdentifier,
eligible: false,
title: "Wait",
subtitle: "Here's an offer for you")
subtitle: "Here's an offer for you",
productMapping: [
"product_id": "offer_id"
]
)
let loadPromotionalOfferUseCase = MockLoadPromotionalOfferUseCase()
loadPromotionalOfferUseCase.mockedProduct = product
loadPromotionalOfferUseCase.mockedPromoOfferDetails = promoOfferDetails
Expand Down Expand Up @@ -527,7 +531,10 @@ class ManageSubscriptionsViewModelTests: TestCase {
CustomerCenterConfigData.HelpPath.PromotionalOffer(iosOfferId: offerIdentifierInJSON,
eligible: true,
title: "Wait",
subtitle: "Here's an offer for you")
subtitle: "Here's an offer for you",
productMapping: [
"product_id": "offer_id"
])
let loadPromotionalOfferUseCase = MockLoadPromotionalOfferUseCase()
loadPromotionalOfferUseCase.mockedProduct = product
loadPromotionalOfferUseCase.mockedPromoOfferDetails = promoOfferDetails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ enum SubscriptionInformationFixtures {
iosOfferId: "offer_id",
eligible: false,
title: "title",
subtitle: "subtitle"
subtitle: "subtitle",
productMapping: ["product_id": "offer_id"]
))
)
]
Expand All @@ -76,7 +77,8 @@ enum SubscriptionInformationFixtures {
iosOfferId: offerID,
eligible: true,
title: "title",
subtitle: "subtitle"
subtitle: "subtitle",
productMapping: ["product_id": "offer_id"]
))
)
]
Expand Down
15 changes: 12 additions & 3 deletions Tests/UnitTests/CustomerCenter/CustomerCenterConfigDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ class CustomerCenterConfigDataTests: TestCase {
promotionalOffer: .init(iosOfferId: "offer_id",
eligible: true,
title: "Wait!",
subtitle: "Before you go"),
subtitle: "Before you go",
productMapping: [
"product_id": "offer_id"
]),
feedbackSurvey: nil
),
.init(
Expand All @@ -78,7 +81,10 @@ class CustomerCenterConfigDataTests: TestCase {
promotionalOffer: .init(iosOfferId: "offer_id_1",
eligible: true,
title: "Wait!",
subtitle: "Before you go"))
subtitle: "Before you go",
productMapping: [
"product_id": "offer_id"
]))
])
),
.init(
Expand All @@ -90,7 +96,10 @@ class CustomerCenterConfigDataTests: TestCase {
promotionalOffer: .init(iosOfferId: "offer_id",
eligible: true,
title: "Wait!",
subtitle: "Before you go"),
subtitle: "Before you go",
productMapping: [
"product_id": "offer_id"
]),
feedbackSurvey: nil
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ private extension BackendGetCustomerCenterConfigTests {
"ios_offer_id": "rc-refund-offer",
"eligible": true,
"title": "Wait!",
"subtitle": "Here's an offer for you"
"subtitle": "Here's an offer for you",
"product_mapping": [
"product_id": "offer_id"
]
] as [String: Any],
"title": "Request a refund",
"type": "REFUND_REQUEST"
Expand All @@ -318,7 +321,10 @@ private extension BackendGetCustomerCenterConfigTests {
"ios_offer_id": "rc-cancel-offer",
"eligible": false,
"title": "Wait!",
"subtitle": "Here's an offer for you"
"subtitle": "Here's an offer for you",
"product_mapping": [
"product_id": "offer_id"
]
] as [String: Any],
"title": "Too expensive"
] as [String: Any],
Expand All @@ -328,7 +334,10 @@ private extension BackendGetCustomerCenterConfigTests {
"ios_offer_id": "rc-cancel-offer",
"eligible": false,
"title": "Wait!",
"subtitle": "Here's an offer for you"
"subtitle": "Here's an offer for you",
"product_mapping": [
"product_id": "offer_id"
]
] as [String: Any],
"title": "Don't use the app"
] as [String: Any],
Expand Down