mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-14 11:17:44 +00:00
* android, desktop: what's new in v7.1, get a supporter badge, Flat layout * android, desktop: what's new image opens Wefunder, sections get icons * android, desktop: crowdfunding image without the stripe at its left edge * android, desktop: Support SimpleX in settings, badge benefits shown * android, desktop: more space under the what's new crowdfunding image * ci: desktop references of v7.1 what's new and the settings badge row * android, desktop: crowdfunding page without contact button, badge texts * ci: desktop references of what's new with one rocket * ui: what's new in v7.1, badge links and benefits, investor offer in store * ui: localized crowdfunding texts on iOS, badge button kept in place * ci: iOS references of v7.1 what's new, badge row and store screens * ui: badge instead of the phone picture in what's new * ui: what's new crowdfunding section without the investor count * ui: what's new badges section without a picture * ci: what's new references without the picture and investor count * ui: investor offer on badge store screens in every country * ui: shorter subscription terms and store notes on badge screens * ios: subscription terms without the cancellation window * ios: subscription terms worded as on Google Play * ui: shorter badge errors and subscription notes * ui: translations of the shorter badge texts; shorter purchases alert on iOS * ui: shorter link, channel signing and badge texts, with translations * ui: Your badge for every badge except legend in settings * android, desktop: badge button at one height on the store screens; crowdfunding texts translatable * android, desktop: smaller crowdfunding image * android, desktop: what's new references with the smaller crowdfunding image
220 lines
8.0 KiB
Swift
220 lines
8.0 KiB
Swift
//
|
|
// BadgesCheckOrderView.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by spaced4ndy on 29.09.2026.
|
|
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import StoreKit
|
|
import SimpleXChat
|
|
|
|
struct BadgesCheckOrderView: View {
|
|
let level: BadgeLevel
|
|
let period: BadgePeriod
|
|
var startDate = Date()
|
|
var priceList: BadgePriceList? = nil
|
|
|
|
var body: some View {
|
|
BadgesCheckOrderLayout(level: level, period: period, startDate: startDate, priceList: priceList)
|
|
.task { await BadgeStore.shared.load() }
|
|
}
|
|
}
|
|
|
|
struct BadgesCheckOrderLayout: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
@EnvironmentObject var chatModel: ChatModel
|
|
@ObservedObject private var store = BadgeStore.shared
|
|
// store.forSale reads the active profile's badge
|
|
@ObservedObject private var badgeModel = BadgeModel.shared
|
|
let level: BadgeLevel
|
|
let period: BadgePeriod
|
|
var startDate = Date()
|
|
var priceList: BadgePriceList? = nil
|
|
@State private var purchasing = 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 {
|
|
VStack(alignment: .center, spacing: 16) {
|
|
Text("Check your order")
|
|
.font(.largeTitle)
|
|
.bold()
|
|
.foregroundColor(theme.colors.primary)
|
|
.multilineTextAlignment(.center)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
VStack(spacing: 0) {
|
|
orderRow("Your badge", Text(level.title))
|
|
orderRow("Duration", Text(period.label))
|
|
Divider()
|
|
.padding(.top, 8)
|
|
.padding(.bottom, 6)
|
|
orderRow("Total", period.priceText(prices.price(period)))
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.vertical, 12)
|
|
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
.padding(.top, 20)
|
|
|
|
if case let .price(p) = prices.price(period) {
|
|
switch period {
|
|
case .monthly: footnote("Renews at \(p) a month until you cancel it in the App Store.")
|
|
case .annual: footnote("Renews at \(p) a year until you cancel it in the App Store.")
|
|
case .oneMonth: EmptyView()
|
|
}
|
|
}
|
|
|
|
if !prices.forSale(period) && prices.subscribedBefore {
|
|
footnote("Subscribed on another profile.")
|
|
}
|
|
|
|
if !AppStore.canMakePayments {
|
|
footnote("Purchases are off on this device.")
|
|
}
|
|
|
|
Spacer(minLength: 20)
|
|
|
|
VStack(spacing: 10) {
|
|
payButton()
|
|
.padding(.vertical, 10)
|
|
BadgeBillingFooter(period: period, startDate: startDate)
|
|
BadgeInvestorOffer(level: level)
|
|
}
|
|
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
|
|
}
|
|
.padding(.horizontal, 25)
|
|
.padding(.top, 0)
|
|
.padding(.bottom, 20)
|
|
.frame(minHeight: g.size.height)
|
|
}
|
|
}
|
|
.frame(maxHeight: .infinity)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.alert(item: $alert) { $0.alert }
|
|
}
|
|
|
|
private func footnote(_ text: LocalizedStringKey) -> some View {
|
|
Text(text)
|
|
.font(.footnote)
|
|
.foregroundColor(theme.colors.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
|
|
private func orderRow(_ title: LocalizedStringKey, _ value: Text) -> some View {
|
|
HStack {
|
|
Text(title)
|
|
.foregroundColor(theme.colors.secondary)
|
|
Spacer()
|
|
value
|
|
.fontWeight(.medium)
|
|
}
|
|
.font(.body)
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
private func payButton() -> some View {
|
|
let price = prices.price(period)
|
|
let disabled = !price.canPurchase || purchasing || !store.canBuy(chatModel.currentUser?.userId) || !prices.forSale(period) || !AppStore.canMakePayments
|
|
return Button {
|
|
purchase()
|
|
} label: {
|
|
period.payText(price)
|
|
}
|
|
.buttonStyle(OnboardingButtonStyle(isDisabled: disabled))
|
|
.disabled(disabled)
|
|
}
|
|
|
|
private func purchase() {
|
|
purchasing = true
|
|
Task {
|
|
do {
|
|
if case let .purchased(receipt) = try await store.purchase(level, period), !receipt.signatureVerified {
|
|
await MainActor.run {
|
|
alert = SomeAlert(
|
|
alert: mkAlert(
|
|
title: "Cannot verify this purchase",
|
|
message: "Apple did not sign this purchase, so it cannot be verified."
|
|
),
|
|
id: "badgePurchaseUnverified"
|
|
)
|
|
}
|
|
}
|
|
await MainActor.run { purchasing = false }
|
|
} catch let error {
|
|
logger.error("BadgesCheckOrderView.purchase: \(String(describing: error))")
|
|
await MainActor.run {
|
|
purchasing = false
|
|
// thrown when the purchase sheet or its sign-in prompt is dismissed, which needs no alert
|
|
if case .userCancelled? = error as? StoreKitError { return }
|
|
alert = SomeAlert(
|
|
alert: Alert(
|
|
title: Text("Purchase error"),
|
|
message: Text(verbatim: redeemErrorText(error, purchase: true))
|
|
),
|
|
id: "badgePurchaseError"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RestorePurchasesButton: View {
|
|
@State private var restoring = false
|
|
|
|
var body: some View {
|
|
Button("Restore purchases") {
|
|
restoring = true
|
|
Task {
|
|
await restore()
|
|
await MainActor.run { restoring = false }
|
|
}
|
|
}
|
|
.font(.footnote)
|
|
.disabled(restoring)
|
|
}
|
|
|
|
private func restore() async {
|
|
do {
|
|
let found = try await BadgeStore.shared.restorePurchases()
|
|
if !found {
|
|
await MainActor.run {
|
|
showAlert(
|
|
NSLocalizedString("Nothing to restore", comment: "alert title"),
|
|
message: NSLocalizedString("No App Store purchase waiting to be added was found.", comment: "alert message")
|
|
)
|
|
}
|
|
}
|
|
} catch let error {
|
|
logger.error("RestorePurchasesButton: \(String(describing: error))")
|
|
if case .userCancelled? = error as? StoreKitError { return }
|
|
await MainActor.run {
|
|
showAlert(NSLocalizedString("Restore failed", comment: "alert title"), message: redeemErrorText(error, purchase: true))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct BadgesCheckOrderView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
NavigationView {
|
|
BadgesCheckOrderLayout(
|
|
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)
|
|
)
|
|
}
|
|
}
|
|
}
|