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

Use SK2 RenewalInfo to get renewal prices & currency #4608

Merged
merged 23 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
128 changes: 125 additions & 3 deletions RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import Foundation
import RevenueCat
import StoreKit

// swiftlint:disable nesting
struct PurchaseInformation {
Expand Down Expand Up @@ -47,6 +48,7 @@ struct PurchaseInformation {
init(entitlement: EntitlementInfo? = nil,
subscribedProduct: StoreProduct? = nil,
transaction: Transaction,
renewalPrice: PriceDetails? = nil,
dateFormatter: DateFormatter = DateFormatter()) {
dateFormatter.dateStyle = .medium

Expand All @@ -60,7 +62,11 @@ struct PurchaseInformation {
self.expirationOrRenewal = entitlement.expirationOrRenewal(dateFormatter: dateFormatter)
self.productIdentifier = entitlement.productIdentifier
self.store = entitlement.store
self.price = entitlement.priceBestEffort(product: subscribedProduct)
if let renewalPrice {
self.price = renewalPrice
} else {
self.price = entitlement.priceBestEffort(product: subscribedProduct)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fall back to our old calculation method if the RenewalInfo method failed for some reason

}
} else {
switch transaction.type {
case .subscription(let isActive, let willRenew, let expiresDate):
Expand All @@ -81,8 +87,15 @@ struct PurchaseInformation {

self.productIdentifier = transaction.productIdentifier
self.store = transaction.store
self.price = transaction.store == .promotional ? .free
: (subscribedProduct.map { .paid($0.localizedPriceString) } ?? .unknown)
if transaction.store == .promotional {
self.price = .free
} else {
if let renewalPrice {
self.price = renewalPrice
} else {
self.price = subscribedProduct.map { .paid($0.localizedPriceString) } ?? .unknown
}
}
}
}

Expand Down Expand Up @@ -123,6 +136,115 @@ struct PurchaseInformation {
}
// swiftlint:enable nesting

extension PurchaseInformation {

/// Provides detailed information about a user's purchase, including entitlement and renewal price.
///
/// This function fetches the renewal price details for the given product asynchronously from
/// StoreKit 2 and constructs a `PurchaseInformation` object with the provided
/// transaction, entitlement, and subscribed product details.
///
/// - Parameters:
/// - entitlement: Optional entitlement information associated with the purchase.
/// - subscribedProduct: The product the user has subscribed to, represented as a `StoreProduct`.
/// - transaction: The transaction information for the purchase.
/// - Returns: A `PurchaseInformation` object containing the purchase details, including the renewal price.
///
/// - Availability: iOS 15.0+
@available(iOS 15.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
static func purchaseInformationUsingRenewalInfo(
entitlement: EntitlementInfo? = nil,
subscribedProduct: StoreProduct,
transaction: Transaction
) async -> PurchaseInformation {
let renewalPriceDetails = await Self.extractPriceDetailsFromRenwalInfo(forProduct: subscribedProduct)
return PurchaseInformation(
entitlement: entitlement,
subscribedProduct: subscribedProduct,
transaction: transaction,
renewalPrice: renewalPriceDetails
)
}

@available(iOS 15.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
private static func extractPriceDetailsFromRenwalInfo(
fire-at-will marked this conversation as resolved.
Show resolved Hide resolved
forProduct product: StoreProduct
) async -> PriceDetails? {
if #available(macOS 12.0, tvOS 15.0, *) {
fire-at-will marked this conversation as resolved.
Show resolved Hide resolved
guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else {
// If StoreKit.Product.subscription is nil, then the product isn't a subscription
// If statuses is empty, the subscriber was never subscribed to a product in the subscription group.
return nil
}

guard let purchaseSubscriptionStatus = statuses.first(where: {
do {
return try $0.transaction.payloadValue.ownershipType == .purchased
} catch {
return false
}
}) else {
return nil
}

switch purchaseSubscriptionStatus.renewalInfo {
case .unverified:
return nil
case .verified(let renewalInfo):
guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil }
guard let currencyCode = product.currencyCode else { return nil }

let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyCode

guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil }

return .paid(formattedPrice)
}
} else {
return nil
}
}

@available(iOS 15.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
private static func currencyCode(
fromRenewalInfo renewalInfo: Product.SubscriptionInfo.RenewalInfo,
locale: Locale = Locale.current
) -> String? {
let currencyCode: String
// macOS 13.0 check is required for the compiler despite the function being marked
// as unavailable on macOS
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) {
guard let currency = renewalInfo.currency else { return nil }
if currency.isISOCurrency {
return currency.identifier
} else {
return nil
}
} else {
if #available(macOS 12.0, tvOS 15.0, *) {
#if os(visionOS)
return nil
#else
return renewalInfo.currencyCode
#endif
} else {
return nil
}
}
}
}

fileprivate extension EntitlementInfo {

func priceBestEffort(product: StoreProduct?) -> PurchaseInformation.PriceDetails {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private extension CustomerCenterViewModel {
entitlement: EntitlementInfo?) async throws -> PurchaseInformation {
if transaction.store == .appStore {
if let product = await purchasesProvider.products([transaction.productIdentifier]).first {
return PurchaseInformation(
return await PurchaseInformation.purchaseInformationUsingRenewalInfo(
entitlement: entitlement,
subscribedProduct: product,
transaction: transaction
Expand Down