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 StoreProduct.pricePerYear #2462

Merged
merged 1 commit into from
May 11, 2023
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
9 changes: 9 additions & 0 deletions Sources/Purchasing/StoreKitAbstractions/StoreProduct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ internal protocol StoreProductType: Sendable {
///
/// #### Related Symbols
/// - ``pricePerMonth``
/// - ``pricePerYear``
var price: Decimal { get }

/// The price of this product using ``priceFormatter``.
Expand Down Expand Up @@ -202,6 +203,7 @@ public extension StoreProduct {
///
/// #### Related Symbols
/// - ``pricePerMonth``
/// - ``pricePerYear``
@objc(price) var priceDecimalNumber: NSDecimalNumber {
return self.price as NSDecimalNumber
}
Expand All @@ -213,6 +215,13 @@ public extension StoreProduct {
return self.subscriptionPeriod?.pricePerMonth(withTotalPrice: self.price) as NSDecimalNumber?
}

/// Calculates the price of this subscription product per year.
/// - Returns: `nil` if the product is not a subscription.
@available(iOS 11.2, macOS 10.13.2, tvOS 11.2, watchOS 6.2, *)
@objc var pricePerYear: NSDecimalNumber? {
return self.subscriptionPeriod?.pricePerYear(withTotalPrice: self.price) as NSDecimalNumber?
}

/// The price of the `introductoryPrice` formatted using ``priceFormatter``.
/// - Returns: `nil` if there is no `introductoryPrice`.
@objc var localizedIntroductoryPriceString: String? {
Expand Down
43 changes: 32 additions & 11 deletions Sources/Purchasing/StoreKitAbstractions/SubscriptionPeriod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,38 @@ extension SubscriptionPeriod.Unit: Sendable {}
extension SubscriptionPeriod: Sendable {}

extension SubscriptionPeriod {

func pricePerMonth(withTotalPrice price: Decimal) -> Decimal {
let periodsPerMonth: Decimal = {
switch self.unit {
case .day: return 1 / 30
case .week: return 1 / 4
case .month: return 1
case .year: return 12
}
}() * Decimal(self.value)

return (price as NSDecimalNumber)
.dividing(by: periodsPerMonth as NSDecimalNumber,
return self.pricePerPeriod(for: self.unitsPerMonth, totalPrice: price)
}

func pricePerYear(withTotalPrice price: Decimal) -> Decimal {
return self.pricePerPeriod(for: self.unitsPerYear, totalPrice: price)
}

private var unitsPerMonth: Decimal {
switch self.unit {
case .day: return 1 / 30
case .week: return 1 / 4
case .month: return 1
case .year: return 12
}
}

private var unitsPerYear: Decimal {
switch self.unit {
case .day: return 1 / 365
case .week: return 1 / 52.14 // Number of weeks in a year
case .month: return 1 / 12
Copy link
Contributor

Choose a reason for hiding this comment

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

This was interesting to me... We seem to be doing integer division here, but seems that the swift compiler is smart enough to do the conversion to Decimal automatically 👍

case .year: return 1
}
}

private func pricePerPeriod(for units: Decimal, totalPrice: Decimal) -> Decimal {
let periods: Decimal = units * Decimal(self.value)

return (totalPrice as NSDecimalNumber)
.dividing(by: periods as NSDecimalNumber,
withBehavior: Self.roundingBehavior) as Decimal
}

Expand All @@ -125,6 +145,7 @@ extension SubscriptionPeriod {
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)

}

extension SubscriptionPeriod.Unit: CustomDebugStringConvertible {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ + (void)checkAPI {
RCStoreProductDiscount *introductoryPrice = product.introductoryDiscount;
NSArray<RCStoreProductDiscount *> *discounts = product.discounts;
NSDecimalNumber *pricePerMonth = product.pricePerMonth;
NSDecimalNumber *pricePerYear = product.pricePerYear;
NSString *localizedIntroductoryPriceString = product.localizedIntroductoryPriceString;

SKProduct *sk1 = product.sk1Product;
Expand All @@ -46,6 +47,7 @@ + (void)checkAPI {
introductoryPrice,
discounts,
pricePerMonth,
pricePerYear,
localizedIntroductoryPriceString,
sk1
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func checkStoreProductAPI() {
let discounts: [StoreProductDiscount] = product.discounts

let pricePerMonth: NSDecimalNumber? = product.pricePerMonth
let pricePerYear: NSDecimalNumber? = product.pricePerYear
let localizedIntroductoryPriceString: String? = product.localizedIntroductoryPriceString
let sk1Product: SK1Product? = product.sk1Product
let sk2Product: SK2Product? = product.sk2Product
Expand Down Expand Up @@ -56,6 +57,7 @@ func checkStoreProductAPI() {
introductoryPrice!,
discounts,
pricePerMonth!,
pricePerYear!,
localizedIntroductoryPriceString!,
sk1Product!,
sk2Product!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ class SubscriptionPeriodTests: TestCase {
}
}

func testPricePerYear() {
let expectations: [(period: SubscriptionPeriod, price: Decimal, expected: Decimal)] = [
(p(1, .day), 1, 365),
(p(1, .day), 2, 730),
(p(15, .day), 5, 121.66),
(p(1, .week), 10, 521.4),
(p(2, .week), 10, 260.7),
(p(1, .month), 14.99, 179.88),
(p(1, .month), 5, 60),
(p(2, .month), 30, 180),
(p(3, .month), 40, 160),
(p(1, .year), 120, 120),
(p(1, .year), 29.99, 29.99),
(p(2, .year), 50, 25),
(p(3, .year), 720, 240)
]

for expectation in expectations {
let pricePerYear = expectation.period.pricePerYear(withTotalPrice: expectation.price) as NSDecimalNumber
let result = Double(truncating: pricePerYear)
let expected = Double(truncating: expectation.expected as NSDecimalNumber)

expect(result).to(beCloseTo(expected),
description: "\(expectation.price) / \(expectation.period.debugDescription)")
}
}

private func p(_ value: Int, _ unit: SubscriptionPeriod.Unit) -> SubscriptionPeriod {
return .init(value: value, unit: unit)
}
Expand Down