diff --git a/apps/ios/Shared/Views/Badges/BadgeStore.swift b/apps/ios/Shared/Views/Badges/BadgeStore.swift index 68a49bb6da..558f5343f2 100644 --- a/apps/ios/Shared/Views/Badges/BadgeStore.swift +++ b/apps/ios/Shared/Views/Badges/BadgeStore.swift @@ -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 { diff --git a/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift b/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift index 2684629d43..3c3ee66318 100644 --- a/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift @@ -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) + ) } } } diff --git a/apps/ios/Shared/Views/Badges/BadgesHowLongView.swift b/apps/ios/Shared/Views/Badges/BadgesHowLongView.swift index 3d3d7559ac..fb3dbb077f 100644 --- a/apps/ios/Shared/Views/Badges/BadgesHowLongView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesHowLongView.swift @@ -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) + ) } } } diff --git a/apps/ios/SimpleX Snapshots/References/ChannelMembersView-dark-large.png b/apps/ios/SimpleX Snapshots/References/ChannelMembersView-dark-large.png index 817170c0cd..d3ac0fd978 100644 Binary files a/apps/ios/SimpleX Snapshots/References/ChannelMembersView-dark-large.png and b/apps/ios/SimpleX Snapshots/References/ChannelMembersView-dark-large.png differ diff --git a/apps/ios/SimpleX Snapshots/References/ChannelMembersView-light-large.png b/apps/ios/SimpleX Snapshots/References/ChannelMembersView-light-large.png index be82d30201..b3331f9b2b 100644 Binary files a/apps/ios/SimpleX Snapshots/References/ChannelMembersView-light-large.png and b/apps/ios/SimpleX Snapshots/References/ChannelMembersView-light-large.png differ