Files
simplex-chat/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift
T
spaced4ndy e3e4709381 ui: buy a badge in the app through the App Store or Google Play (#7608)
* ui: buy a badge in the app through the App Store or Google Play

* core, ios, android: create the badge store purchase record before the store charges

* core, ios, android: fixes from the store lane review

* ios, android: show a store purchase's badge only on the profile it was credited to
2026-10-01 08:46:35 +00:00

210 lines
6.8 KiB
Swift

//
// BadgesSupportSimplexView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 28.07.2026.
// Copyright © 2026 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct BadgesSupportSimplexView: View {
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var chatModel: ChatModel
@ObservedObject private var store = BadgeStore.shared
// set true when presented as a sheet root (from the chat-list banner) — that path doesn't
// reserve nav-bar space like a NavigationLink push does, so the title lands too close to the top
var showsAsSheet: Bool = false
@State private var whyBuiltActive = false
@State private var howItWorksActive = false
@State private var chooseLevelActive = false
var body: some View {
GeometryReader { g in
VStack(alignment: .center, spacing: 16) {
Text("Support SimpleX")
.font(.largeTitle)
.bold()
.foregroundColor(theme.colors.primary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
Text("Get a badge to send larger files (2-5GB) that stay available longer (7-21 days), and to show it on your profile.")
.font(.body)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
howItWorksButton()
Spacer(minLength: 0)
PhoneSupporterHero()
.frame(maxWidth: g.size.width * 0.55)
.layoutPriority(-1)
Spacer(minLength: 0)
VStack(spacing: 10) {
chooseLevelButton()
.padding(.vertical, 10)
Group {
if badgeBrowserAllowed {
buyInBrowserButton()
} else {
RedeemCodeButton()
}
}
.frame(height: 22)
}
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
}
.padding(.horizontal, 25)
.padding(.top, showsAsSheet ? 48 : 0)
.padding(.bottom, 20)
// .frame(height:) not minHeight — inside the banner sheet's NavigationView minHeight
// would let the VStack expand past the visible area and inflate the hero.
.frame(height: g.size.height)
}
.frame(maxHeight: .infinity)
.navigationBarTitleDisplayMode(.inline)
}
private func chooseLevelButton() -> some View {
let disabled = !store.canBuy(chatModel.currentUser?.userId)
return ZStack {
Button {
chooseLevelActive = true
} label: {
Text("Choose your badge")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: disabled))
.disabled(disabled)
NavigationLink(isActive: $chooseLevelActive) {
BadgesChooseBadgeView()
.modifier(ThemedBackground())
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
}
private func whyBuiltButton() -> some View {
ZStack {
Button { whyBuiltActive = true } label: {
HStack(spacing: 4) {
Image(systemName: "info.circle")
Text("Why SimpleX is built.").fontWeight(.medium)
}
.font(.body)
}
NavigationLink(isActive: $whyBuiltActive) {
WhySimpleX(onboarding: false, titleColor: theme.colors.primary, createProfileNavLinkActive: .constant(false))
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
}
private func howItWorksButton() -> some View {
ZStack {
Button { howItWorksActive = true } label: {
HStack(spacing: 4) {
Image(systemName: "info.circle")
Text("How badges protect your privacy").fontWeight(.medium)
}
.font(.body)
}
NavigationLink(isActive: $howItWorksActive) {
BadgesHowItWorksView()
.modifier(ThemedBackground())
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
}
private func buyInBrowserButton() -> some View {
Button {
UIApplication.shared.open(URL(string: badgePageUrl)!)
} label: {
Text("Buy in browser")
.font(.body)
.fontWeight(.medium)
.foregroundColor(theme.colors.primary)
}
}
}
struct RedeemCodeButton: View {
@EnvironmentObject var theme: AppTheme
@State private var redeemCodeActive = false
var body: some View {
ZStack {
Button {
redeemCodeActive = true
} label: {
Text("Redeem badge code")
.font(.body)
.fontWeight(.medium)
.foregroundColor(theme.colors.primary)
}
NavigationLink(isActive: $redeemCodeActive) {
BadgesRedeemCodeView()
.modifier(ThemedBackground())
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
}
}
struct PhoneSupporterHero: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View {
#if SIMPLEX_ASSETS
Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
#else
ZStack {
let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5)
LinearGradient(
stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
startPoint: gp.start,
endPoint: gp.end
)
Image("badge-supporter")
.resizable()
.scaledToFit()
.frame(width: 96)
}
.aspectRatio(1.0, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 24))
.frame(maxWidth: .infinity)
#endif
}
}
struct BadgesSupportSimplexView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
BadgesSupportSimplexView()
}
}
}