From e508f07987c26fda081c02bdf070ad6a3a520f6a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 10:52:32 +0000 Subject: [PATCH 1/4] Fix SK2 computed prices showing incorrectly rounded values in production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Product.priceFormatStyle carries storefront-specific rounding rules in production that differ from StoreKit config file testing. This caused computed period prices (weeklyPrice, dailyPrice, monthlyPrice, yearlyPrice) to display incorrectly — e.g. a £4.99/week product showing as £5.00/week on the UK App Store. Switch from priceFormatStyle to NumberFormatter (via the existing PriceFormatterProvider) for formatting computed prices, matching how the SK1 path works. localizedPrice still uses priceFormatStyle since it formats the product's own original price. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 ++++ .../StoreProduct/SK2StoreProduct.swift | 33 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eca15b1de..eb81113cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub. +## 4.14.3 + +### Fixes + +- Fixed computed period prices (`weeklyPrice`, `dailyPrice`, `monthlyPrice`, `yearlyPrice`) displaying incorrectly rounded values on StoreKit 2 in production. For example, a £4.99/week product could show as £5.00/week. This was caused by Apple's `priceFormatStyle` applying storefront-specific rounding to computed values. + ## 4.14.2 ### Enhancements diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift index d8e2a4c34..f1b34183c 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift @@ -18,6 +18,7 @@ import StoreKit @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) struct SK2StoreProduct: StoreProductType { + private let priceFormatterProvider = PriceFormatterProvider() let entitlements: Set init( @@ -61,6 +62,16 @@ struct SK2StoreProduct: StoreProductType { return underlyingSK2Product.price.formatted(underlyingSK2Product.priceFormatStyle) } + /// A `NumberFormatter` for formatting computed prices (daily, weekly, monthly, yearly). + /// Unlike `priceFormatStyle`, this does not apply storefront-specific rounding + /// that can cause values like £4.99 to display as £5.00 in production. + private var priceFormatter: NumberFormatter { + priceFormatterProvider.priceFormatterForSK2( + withCurrencyCode: underlyingSK2Product.priceFormatStyle.currencyCode, + locale: underlyingSK2Product.priceFormatStyle.locale + ) + } + var localizedSubscriptionPeriod: String { guard let subscriptionPeriod = underlyingSK2Product.subscription?.subscriptionPeriod else { return "" @@ -273,7 +284,8 @@ struct SK2StoreProduct: StoreProductType { periods = Decimal(numberOfUnits) } - return (inputPrice / periods).roundedPrice().formatted(underlyingSK2Product.priceFormatStyle) + let result = (inputPrice / periods).roundedPrice() + return priceFormatter.string(from: NSDecimalNumber(decimal: result)) ?? "n/a" } var weeklyPrice: String { @@ -298,7 +310,8 @@ struct SK2StoreProduct: StoreProductType { periods = Decimal(numberOfUnits) } - return (inputPrice / periods).roundedPrice().formatted(underlyingSK2Product.priceFormatStyle) + let result = (inputPrice / periods).roundedPrice() + return priceFormatter.string(from: NSDecimalNumber(decimal: result)) ?? "n/a" } var monthlyPrice: String { @@ -323,7 +336,8 @@ struct SK2StoreProduct: StoreProductType { periods = Decimal(numberOfUnits) } - return (inputPrice / periods).roundedPrice().formatted(underlyingSK2Product.priceFormatStyle) + let result = (inputPrice / periods).roundedPrice() + return priceFormatter.string(from: NSDecimalNumber(decimal: result)) ?? "n/a" } var yearlyPrice: String { @@ -348,7 +362,8 @@ struct SK2StoreProduct: StoreProductType { periods = Decimal(numberOfUnits) } - return (inputPrice / periods).roundedPrice().formatted(underlyingSK2Product.priceFormatStyle) + let result = (inputPrice / periods).roundedPrice() + return priceFormatter.string(from: NSDecimalNumber(decimal: result)) ?? "n/a" } var hasFreeTrial: Bool { @@ -593,22 +608,22 @@ struct SK2StoreProduct: StoreProductType { func trialPeriodPricePerUnit(_ unit: SubscriptionPeriod.Unit) -> String { guard let introductoryDiscount = introductoryDiscount else { - return Decimal(0).formatted(underlyingSK2Product.priceFormatStyle) + return priceFormatter.string(from: 0.00) ?? "$0.00" } if introductoryDiscount.price == 0.00 { - return Decimal(0).formatted(underlyingSK2Product.priceFormatStyle) + return priceFormatter.string(from: 0.00) ?? "$0.00" } let introMonthlyPrice = introductoryDiscount.pricePerUnit(unit) - return introMonthlyPrice.formatted(underlyingSK2Product.priceFormatStyle) + return priceFormatter.string(from: NSDecimalNumber(decimal: introMonthlyPrice)) ?? "$0.00" } var localizedTrialPeriodPrice: String { guard let price = underlyingSK2Product.subscription?.introductoryOffer?.price else { - return Decimal(0).formatted(underlyingSK2Product.priceFormatStyle) + return priceFormatter.string(from: 0.00) ?? "$0.00" } - return price.formatted(underlyingSK2Product.priceFormatStyle) + return priceFormatter.string(from: NSDecimalNumber(decimal: price)) ?? "$0.00" } } From 0fec0cbe0c4fdf9cb0530bf6ae4781c71338a6dc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 11:32:50 +0000 Subject: [PATCH 2/4] Revert localizedTrialPeriodPrice to priceFormatStyle This property reads the intro offer price directly from StoreKit (not a computed value), so it should use priceFormatStyle like localizedPrice. Co-Authored-By: Claude Opus 4.6 --- .../StoreKit/Products/StoreProduct/SK2StoreProduct.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift index f1b34183c..134b77fbb 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift @@ -621,9 +621,9 @@ struct SK2StoreProduct: StoreProductType { var localizedTrialPeriodPrice: String { guard let price = underlyingSK2Product.subscription?.introductoryOffer?.price else { - return priceFormatter.string(from: 0.00) ?? "$0.00" + return Decimal(0).formatted(underlyingSK2Product.priceFormatStyle) } - return priceFormatter.string(from: NSDecimalNumber(decimal: price)) ?? "$0.00" + return price.formatted(underlyingSK2Product.priceFormatStyle) } } From 43ac90ac209e00cb7e10dc87cf42b069c6afe620 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 11:37:44 +0000 Subject: [PATCH 3/4] Replace hardcoded "$0.00" fallbacks with "n/a" in trialPeriodPricePerUnit Avoids leaking a USD currency symbol in non-USD locales when NumberFormatter.string(from:) returns nil. Co-Authored-By: Claude Opus 4.6 --- .../StoreKit/Products/StoreProduct/SK2StoreProduct.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift index 134b77fbb..35d12a5db 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift @@ -608,15 +608,15 @@ struct SK2StoreProduct: StoreProductType { func trialPeriodPricePerUnit(_ unit: SubscriptionPeriod.Unit) -> String { guard let introductoryDiscount = introductoryDiscount else { - return priceFormatter.string(from: 0.00) ?? "$0.00" + return priceFormatter.string(from: 0.00) ?? "n/a" } if introductoryDiscount.price == 0.00 { - return priceFormatter.string(from: 0.00) ?? "$0.00" + return priceFormatter.string(from: 0.00) ?? "n/a" } let introMonthlyPrice = introductoryDiscount.pricePerUnit(unit) - return priceFormatter.string(from: NSDecimalNumber(decimal: introMonthlyPrice)) ?? "$0.00" + return priceFormatter.string(from: NSDecimalNumber(decimal: introMonthlyPrice)) ?? "n/a" } var localizedTrialPeriodPrice: String { From a1b53fe36cdca66d71882daca6011c60213bf874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:41:43 +0100 Subject: [PATCH 4/4] Update Superwall_Advanced-Products.storekit --- .../Superwall_Advanced-Products.storekit | 24 +++++++++---------- Sources/SuperwallKit/Misc/Constants.swift | 2 +- SuperwallKit.podspec | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Examples/Advanced/Advanced/Superwall_Advanced-Products.storekit b/Examples/Advanced/Advanced/Superwall_Advanced-Products.storekit index 77764447b..120fe605d 100644 --- a/Examples/Advanced/Advanced/Superwall_Advanced-Products.storekit +++ b/Examples/Advanced/Advanced/Superwall_Advanced-Products.storekit @@ -17,55 +17,51 @@ ], "settings" : { + "_askToBuyEnabled" : false, + "_billingGracePeriodEnabled" : false, + "_billingIssuesEnabled" : false, "_compatibilityTimeRate" : { "3" : 6 }, + "_disableDialogs" : false, "_failTransactionsEnabled" : false, "_locale" : "en_US", + "_renewalBillingIssuesEnabled" : false, "_storefront" : "USA", "_storeKitErrors" : [ { - "current" : null, "enabled" : false, "name" : "Load Products" }, { - "current" : null, "enabled" : false, "name" : "Purchase" }, { - "current" : null, "enabled" : false, "name" : "Verification" }, { - "current" : null, "enabled" : false, "name" : "App Store Sync" }, { - "current" : null, "enabled" : false, "name" : "Subscription Status" }, { - "current" : null, "enabled" : false, "name" : "App Transaction" }, { - "current" : null, "enabled" : false, "name" : "Manage Subscriptions Sheet" }, { - "current" : null, "enabled" : false, "name" : "Refund Request Sheet" }, { - "current" : null, "enabled" : false, "name" : "Offer Code Redeem Sheet" } @@ -91,7 +87,9 @@ "familyShareable" : false, "groupNumber" : 1, "internalID" : "38D8615F", - "introductoryOffer" : null, + "introductoryOffers" : [ + + ], "localizations" : [ { "description" : "Access to diamond features", @@ -119,7 +117,9 @@ "familyShareable" : false, "groupNumber" : 2, "internalID" : "E02B772A", - "introductoryOffer" : null, + "introductoryOffers" : [ + + ], "localizations" : [ { "description" : "Access to pro features", @@ -140,7 +140,7 @@ } ], "version" : { - "major" : 4, + "major" : 5, "minor" : 0 } } diff --git a/Sources/SuperwallKit/Misc/Constants.swift b/Sources/SuperwallKit/Misc/Constants.swift index 41e6bcf49..41dd498ce 100644 --- a/Sources/SuperwallKit/Misc/Constants.swift +++ b/Sources/SuperwallKit/Misc/Constants.swift @@ -18,5 +18,5 @@ let sdkVersion = """ */ let sdkVersion = """ -4.14.2 +4.14.3 """ diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index 824ebdc63..8650ecd4f 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SuperwallKit" - s.version = "4.14.2" + s.version = "4.14.3" s.summary = "Superwall: In-App Paywalls Made Easy" s.description = "Paywall infrastructure for mobile apps :) we make things like editing your paywall and running price tests as easy as clicking a few buttons. superwall.com"