mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-12 15:58:03 +00:00
* ios, android, desktop: previews draw fixed sample data instead of calling the core * desktop: database error preview passes the keychain setting * ios: snapshot fixtures use fixed parsed markdown instead of the core * ios: snapshot fixtures: raw string delimiters past the # in markdown JSON * desktop: snapshot test sets the app bar height as the app does * docs: plan to replace preview-only parameters with test measures * ios, desktop: previews run master's code; badges and version views split Preview-only parameters are removed. BadgesCheckOrderView, BadgesHowLongView and VersionView are split into a loader and a layout, which their previews draw. Snapshot tests set a fixed QR picture: the environment value fixedQRCode on iOS, LocalFixedQRCode on desktop. * ios: snapshot fixtures build parsed markdown with initializers The table held the JSON of the Linux core, which the iOS app does not decode. The values are those the core returns for each text. * ci: references of fixed QR codes and version; ios: call overlay preview as on master iOS references from https://github.com/evgeny-simplex/simplex-chat/actions/runs/40944118370, desktop references from https://github.com/evgeny-simplex/simplex-chat/actions/runs/40932566864. ActiveCallOverlay_Previews, its scene and its references are restored as on master. * ios, desktop: snapshot tests render every preview, no exclusion lists BadgesChooseBadgeView, ServersSummaryView and TerminalView are split into a loader and a layout. AddChannelView, AddGroupView, GroupProfileView and UserProfileView layouts take the name and description validators, so desktop previews render without the core library. NewChatView's preview opens the Connect tab, UserProfilesView's preview has its own users. AppearanceSettings, MigrateFromDevice and the duplicate UserProfileLayoutEditOn previews are deleted. * ios, desktop: previews show short links; tests draw the fixed QR code with each app's generator The snapshot tests draw the fixed QR picture for a short address link with generateImage on iOS and qrCodeBitmap on desktop, in place of committed images. Previews of group links, addresses, a pending connection and QR codes pass short links, as the apps show them. The fixture markdown table equals the output of master's parser. * ios, desktop: excluded previews and their views as on master * ci: references of short-link QR codes
240 lines
9.7 KiB
Swift
240 lines
9.7 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
|
|
@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 {
|
|
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("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.")
|
|
case .oneMonth: EmptyView()
|
|
}
|
|
}
|
|
|
|
if !prices.forSale(period) && prices.subscribedBefore {
|
|
footnote("This Apple ID already has a badge subscription. Only the profile that bought it can continue it.")
|
|
}
|
|
|
|
if !AppStore.canMakePayments {
|
|
footnote("In-app purchases are not allowed on this device.")
|
|
}
|
|
|
|
Spacer(minLength: 20)
|
|
|
|
VStack(spacing: 10) {
|
|
payButton()
|
|
.padding(.vertical, 10)
|
|
BadgeBillingFooter(period: period, startDate: startDate)
|
|
HStack(spacing: 20) {
|
|
Link("Terms of use", destination: appleStandardEULAURL)
|
|
ZStack {
|
|
Button("Privacy policy") { privacyPolicyActive = true }
|
|
NavigationLink(isActive: $privacyPolicyActive) {
|
|
ConditionsTextView()
|
|
.padding()
|
|
.navigationTitle("Privacy policy")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
} label: {
|
|
EmptyView()
|
|
}
|
|
.frame(width: 1, height: 1)
|
|
.hidden()
|
|
}
|
|
}
|
|
.font(.footnote)
|
|
}
|
|
.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: "The store returned a transaction that Apple has not signed. SimpleX cannot verify this purchase with the App Store."
|
|
),
|
|
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"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apple's standard licence agreement, which an app selling subscriptions links as its terms of use
|
|
let appleStandardEULAURL = URL(string: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/")!
|
|
|
|
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)
|
|
)
|
|
}
|
|
}
|
|
}
|