ios: badge previews show fixed prices (#7917)

* ios: badge previews show fixed prices

* ci: references of badge previews with prices and of channel members

* ios: badge price list computes annual savings from the amounts

* ios: badge previews pass what is for sale and earlier subscriptions with the prices
This commit is contained in:
Evgeny @ SimpleX Chat authored and GitHub committed 2026-10-28 17:46:46 +00:00
1 parent 26c6b3172b
commit c7a6c65960
5 files changed
+73 -22

No files matched your search

+42 -10
View File
@@ -53,6 +53,38 @@ enum BadgePrice {
}
}
struct BadgePriceList {
let oneMonth: BadgePrice
let monthly: BadgePrice
let annual: BadgePrice
let monthlyAmount: Decimal?
let annualAmount: Decimal?
let subscriptionsForSale: Bool
let subscribedBefore: Bool
func price(_ period: BadgePeriod) -> BadgePrice {
switch period {
case .oneMonth: oneMonth
case .monthly: monthly
case .annual: annual
}
}
func forSale(_ period: BadgePeriod) -> Bool {
period == .oneMonth || subscriptionsForSale
}
// percentage the annual subscription saves against 12 monthly payments
var annualSavings: Int? {
guard let monthlyAmount, let annualAmount else { return nil }
let year = monthlyAmount * 12
guard year > 0, annualAmount < year else { return nil }
let saved = (year - annualAmount) / year * 100
let percent = Int(NSDecimalNumber(decimal: saved).doubleValue.rounded())
return percent > 0 ? percent : nil
}
}
struct BadgeStoreReceipt {
// the signed token the badge service verifies - never transaction.jsonRepresentation
let jws: String
@@ -169,16 +201,16 @@ final class BadgeStore: ObservableObject {
}
}
// percentage the annual subscription saves against 12 monthly payments
func annualSavings(_ level: BadgeLevel) -> Int? {
guard let monthly = products[badgeProductId(level, .monthly)],
let annual = products[badgeProductId(level, .annual)]
else { return nil }
let year = monthly.price * 12
guard year > 0, annual.price < year else { return nil }
let saved = (year - annual.price) / year * 100
let percent = Int(NSDecimalNumber(decimal: saved).doubleValue.rounded())
return percent > 0 ? percent : nil
func priceList(_ level: BadgeLevel, compact: Bool = true) -> BadgePriceList {
BadgePriceList(
oneMonth: price(level, .oneMonth, compact: compact),
monthly: price(level, .monthly, compact: compact),
annual: price(level, .annual, compact: compact),
monthlyAmount: products[badgeProductId(level, .monthly)]?.price,
annualAmount: products[badgeProductId(level, .annual)]?.price,
subscriptionsForSale: forSale(level, .monthly),
subscribedBefore: subscribedBefore
)
}
func load() async {
@@ -19,11 +19,16 @@ struct BadgesCheckOrderView: View {
let level: BadgeLevel
let period: BadgePeriod
var startDate = Date()
var priceList: BadgePriceList? = nil
@State private var purchasing = false
@State private var privacyPolicyActive = false
// presented from this view, not AlertManager: its host is behind the sheet these views open in
@State private var alert: SomeAlert?
private var prices: BadgePriceList {
priceList ?? store.priceList(level, compact: false)
}
var body: some View {
GeometryReader { g in
ScrollView {
@@ -41,7 +46,7 @@ struct BadgesCheckOrderView: View {
Divider()
.padding(.top, 8)
.padding(.bottom, 6)
orderRow("Total", period.priceText(store.price(level, period, compact: false)))
orderRow("Total", period.priceText(prices.price(period)))
}
.padding(.horizontal, 24)
.padding(.vertical, 12)
@@ -49,7 +54,7 @@ struct BadgesCheckOrderView: View {
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.top, 20)
if case let .price(p) = store.price(level, period, compact: false) {
if case let .price(p) = prices.price(period) {
switch period {
case .monthly: footnote("Payment is charged to your Apple ID when you confirm the purchase. The subscription renews automatically at \(p) per month unless you cancel it at least 24 hours before the end of the current period. You can manage and cancel it in your App Store account settings.")
case .annual: footnote("Payment is charged to your Apple ID when you confirm the purchase. The subscription renews automatically at \(p) per year unless you cancel it at least 24 hours before the end of the current period. You can manage and cancel it in your App Store account settings.")
@@ -57,7 +62,7 @@ struct BadgesCheckOrderView: View {
}
}
if !store.forSale(level, period) && store.subscribedBefore {
if !prices.forSale(period) && prices.subscribedBefore {
footnote("This Apple ID already has a badge subscription. Only the profile that bought it can continue it.")
}
@@ -124,8 +129,8 @@ struct BadgesCheckOrderView: View {
}
private func payButton() -> some View {
let price = store.price(level, period, compact: false)
let disabled = !price.canPurchase || purchasing || !store.canBuy(chatModel.currentUser?.userId) || !store.forSale(level, period) || !AppStore.canMakePayments
let price = prices.price(period)
let disabled = !price.canPurchase || purchasing || !store.canBuy(chatModel.currentUser?.userId) || !prices.forSale(period) || !AppStore.canMakePayments
return Button {
purchase()
} label: {
@@ -212,7 +217,12 @@ struct RestorePurchasesButton: View {
struct BadgesCheckOrderView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
BadgesCheckOrderView(level: .supporter, period: .oneMonth, startDate: previewDate)
BadgesCheckOrderView(
level: .supporter,
period: .oneMonth,
startDate: previewDate,
priceList: BadgePriceList(oneMonth: .price("$7.00"), monthly: .price("$7.00"), annual: .price("$42.00"), monthlyAmount: 7, annualAmount: 42, subscriptionsForSale: true, subscribedBefore: false)
)
}
}
}
@@ -75,12 +75,17 @@ struct BadgesHowLongView: View {
@ObservedObject private var badgeModel = BadgeModel.shared
let level: BadgeLevel
var startDate = Date()
var priceList: BadgePriceList? = nil
@State private var chosenPeriod: BadgePeriod? = nil
@State private var continueActive = false
private var prices: BadgePriceList {
priceList ?? store.priceList(level)
}
// monthly once subscriptions turn out to be for sale, unless a period was chosen before that
private var selectedPeriod: BadgePeriod {
chosenPeriod ?? (store.forSale(level, .monthly) ? .monthly : .oneMonth)
chosenPeriod ?? (prices.forSale(.monthly) ? .monthly : .oneMonth)
}
var body: some View {
@@ -112,7 +117,7 @@ struct BadgesHowLongView: View {
}
.fixedSize(horizontal: false, vertical: true)
if !store.forSale(level, .monthly) && store.subscribedBefore {
if !prices.forSale(.monthly) && prices.subscribedBefore {
Text("This Apple ID already has a badge subscription. Only the profile that bought it can continue it.")
.font(.footnote)
.foregroundColor(theme.colors.secondary)
@@ -142,7 +147,7 @@ struct BadgesHowLongView: View {
private func periodCard(_ period: BadgePeriod) -> some View {
let isSelected = period == selectedPeriod
let forSale = store.forSale(level, period)
let forSale = prices.forSale(period)
return Button {
chosenPeriod = period
} label: {
@@ -155,7 +160,7 @@ struct BadgesHowLongView: View {
.opacity(forSale ? 1 : 0.4)
Text(period.label)
.font(.subheadline)
period.priceText(store.price(level, period))
period.priceText(prices.price(period))
.font(.headline)
if let percent = savingsPercent(period) {
Text("\(percent)% off")
@@ -180,7 +185,7 @@ struct BadgesHowLongView: View {
}
private func savingsPercent(_ period: BadgePeriod) -> Int? {
period == .annual ? store.annualSavings(level) : nil
period == .annual ? prices.annualSavings : nil
}
private func continueButton() -> some View {
@@ -234,7 +239,11 @@ struct BadgeBillingFooter: View {
struct BadgesHowLongView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
BadgesHowLongView(level: .supporter, startDate: previewDate)
BadgesHowLongView(
level: .supporter,
startDate: previewDate,
priceList: BadgePriceList(oneMonth: .price("$7"), monthly: .price("$7"), annual: .price("$42"), monthlyAmount: 7, annualAmount: 42, subscriptionsForSale: true, subscribedBefore: false)
)
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB