diff --git a/apps/ios/Shared/Views/Badges/BadgesHowItWorksView.swift b/apps/ios/Shared/Views/Badges/BadgesHowItWorksView.swift new file mode 100644 index 0000000000..492b55ffab --- /dev/null +++ b/apps/ios/Shared/Views/Badges/BadgesHowItWorksView.swift @@ -0,0 +1,47 @@ +// +// BadgesHowItWorksView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.07.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// Draft explanation of how private badges work. TODO [badges]: replace lorem ipsum with the real +// copy once the badge protocol and privacy properties are documented. +struct BadgesHowItWorksView: View { + @EnvironmentObject var theme: AppTheme + + var body: some View { + VStack(alignment: .leading) { + Text("How private badges work") + .font(.largeTitle) + .bold() + .foregroundColor(theme.colors.primary) + .padding(.top, 8) + .padding(.bottom, 16) + ScrollView { + VStack(alignment: .leading, spacing: 12) { + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + Text("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.") + Text("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + } + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + } + Spacer() + } + .padding(.horizontal, 25) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} + +struct BadgesHowItWorksView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + BadgesHowItWorksView() + } + } +} diff --git a/apps/ios/Shared/Views/Badges/BadgesPayView.swift b/apps/ios/Shared/Views/Badges/BadgesPayView.swift new file mode 100644 index 0000000000..ba0b3d86d7 --- /dev/null +++ b/apps/ios/Shared/Views/Badges/BadgesPayView.swift @@ -0,0 +1,159 @@ +// +// BadgesPayView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.07.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// Draft billing periods used by the badges UI while the API/state machine is still being designed. +// TODO [badges]: replace with types produced by the badge purchase API when it lands. +enum BadgePeriod: String, CaseIterable, Identifiable { + case oneMonth + case subscribe + + var id: String { rawValue } + + var icon: String { + switch self { + case .oneMonth: "calendar" + case .subscribe: "arrow.clockwise" + } + } + + var label: LocalizedStringKey { + switch self { + case .oneMonth: "1 month" + case .subscribe: "Subscribe" + } + } +} + +struct BadgesPayView: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + let level: BadgeLevel + @State private var selectedPeriod: BadgePeriod = .subscribe + + var body: some View { + GeometryReader { g in + ScrollView { + VStack(alignment: .center, spacing: 16) { + Text(level.title) + .font(.largeTitle) + .bold() + .foregroundColor(theme.colors.primary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + userPreview() + .padding(.top, 4) + + Text(level.tagline) + .font(.body) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 4) + + HStack(alignment: .top, spacing: 12) { + periodCard(.oneMonth) + periodCard(.subscribe) + } + .padding(.top, 12) + + Spacer(minLength: 20) + + payButton() + + Text(billingFooter) + .font(.footnote) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 4) + .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0) + } + .padding(.horizontal, 25) + .padding(.top, 8) + .padding(.bottom, 20) + .frame(minHeight: g.size.height) + } + } + .frame(maxHeight: .infinity) + } + + private func userPreview() -> some View { + let user = chatModel.currentUser + return VStack(spacing: 12) { + ProfileImage(imageStr: user?.image, size: 128) + HStack(alignment: .center, spacing: 6) { + Text(user?.displayName ?? NSLocalizedString("My nickname", comment: "badges preview placeholder")) + .font(.title2) + .fontWeight(.semibold) + .lineLimit(1) + .minimumScaleFactor(0.75) + Image(level.badgeAsset) + .resizable() + .scaledToFit() + .frame(width: 28, height: 28) + } + } + } + + private func periodCard(_ period: BadgePeriod) -> some View { + let isSelected = period == selectedPeriod + return Button { + withAnimation { selectedPeriod = period } + } label: { + VStack(spacing: 12) { + Image(systemName: period.icon) + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + .foregroundColor(isSelected ? theme.colors.primary : theme.colors.secondary) + .padding(.top, 20) + Text(period.label) + .font(.title3) + .fontWeight(.bold) + .padding(.bottom, 20) + } + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(isSelected ? theme.colors.primary : Color.clear, lineWidth: 2) + ) + } + .buttonStyle(.plain) + } + + private func payButton() -> some View { + Button { + // TODO [badges] wire to purchase API when it lands. + } label: { + Text(selectedPeriod == .subscribe ? level.payMonthlyLabel : level.payOnceLabel) + } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + } + + // TODO [badges] source the actual renewal/end date from the purchase state machine when wired. + private var billingFooter: LocalizedStringKey { + switch selectedPeriod { + case .subscribe: "Renews on July 22, 2026. Cancel anytime." + case .oneMonth: "Ends on August 22, 2026." + } + } +} + +struct BadgesPayView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + BadgesPayView(level: .supporter) + } + } +} diff --git a/apps/ios/Shared/Views/Badges/BadgesRedeemCodeView.swift b/apps/ios/Shared/Views/Badges/BadgesRedeemCodeView.swift new file mode 100644 index 0000000000..efe2892ef6 --- /dev/null +++ b/apps/ios/Shared/Views/Badges/BadgesRedeemCodeView.swift @@ -0,0 +1,37 @@ +// +// BadgesRedeemCodeView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.07.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// Draft entry-point for redeeming an investor badge code. TODO [badges]: implement input field, +// server verification and success/failure states when the redeem API is defined. +struct BadgesRedeemCodeView: View { + @EnvironmentObject var theme: AppTheme + + var body: some View { + VStack(alignment: .leading) { + Text("Redeem badge code") + .font(.largeTitle) + .bold() + .foregroundColor(theme.colors.primary) + .padding(.top, 8) + Spacer() + } + .padding(.horizontal, 25) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} + +struct BadgesRedeemCodeView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + BadgesRedeemCodeView() + } + } +} diff --git a/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift b/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift new file mode 100644 index 0000000000..f6eabe4d97 --- /dev/null +++ b/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift @@ -0,0 +1,154 @@ +// +// BadgesSupportSimplexView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.07.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// Entry point for badges management. The subsequent screens (level selection, pay, redeem code, +// how it works) are pushed via NavigationLink from this view, so the enclosing NavigationView — +// either the settings NavigationView or the sheet NavigationView presented from the chat list +// banner — provides the sliding animation. +struct BadgesSupportSimplexView: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.colorScheme) var colorScheme: ColorScheme + @State private var showWhySimpleX = false + @State private var chooseLevelActive = false + @State private var redeemCodeActive = false + + var body: some View { + // TODO [badges] when the state machine lands, gate on user badge status: + // - no badge → this view (support prompt) + // - active badge → a "Manage your badge" view + GeometryReader { g in + ScrollView { + VStack(alignment: .center, spacing: 16) { + Text("Support SimpleX") + .font(.largeTitle) + .bold() + .foregroundColor(theme.colors.primary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Text("SimpleX doesn't sell ads or data. It's funded by its users and by investors who share the mission. You can support the project and show a badge on your profile.") + .font(.body) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Button { showWhySimpleX = true } label: { + Label("Why SimpleX is built.", systemImage: "info.circle") + .font(.headline) + } + + PhoneSupporterHero() + .padding(.top, 12) + .padding(.horizontal, 25) + + Spacer(minLength: 20) + + chooseLevelButton() + + redeemCodeButton() + .padding(.top, 4) + .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0) + } + .padding(.horizontal, 25) + .padding(.top, 8) + .padding(.bottom, 20) + .frame(minHeight: g.size.height) + } + } + .frame(maxHeight: .infinity) + .modifier(ThemedBackground()) + .sheet(isPresented: $showWhySimpleX) { + WhySimpleX(onboarding: false, createProfileNavLinkActive: .constant(false)) + } + } + + private func chooseLevelButton() -> some View { + ZStack { + Button { + chooseLevelActive = true + } label: { + Text("Choose your level") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + + NavigationLink(isActive: $chooseLevelActive) { + BadgesYourLevelView() + .modifier(ThemedBackground()) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func redeemCodeButton() -> 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() + } + } +} + +// Hero image reused across badges views and the WhatsNewView v7.1 entry. Falls back to a gradient +// card carrying the small supporter badge glyph when SIMPLEX_ASSETS is not defined — matching the +// onboarding placeholder convention. +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() + } + } +} diff --git a/apps/ios/Shared/Views/Badges/BadgesYourLevelView.swift b/apps/ios/Shared/Views/Badges/BadgesYourLevelView.swift new file mode 100644 index 0000000000..def9280fcf --- /dev/null +++ b/apps/ios/Shared/Views/Badges/BadgesYourLevelView.swift @@ -0,0 +1,224 @@ +// +// BadgesYourLevelView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.07.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// Draft levels used by the badges UI while the API/state machine is still being designed. TODO [badges]: +// replace with types produced by the badge purchase API when it lands. +enum BadgeLevel: String, CaseIterable, Identifiable { + case supporter + case legend + + var id: String { rawValue } + + var title: LocalizedStringKey { + switch self { + case .supporter: "Supporter" + case .legend: "Legend" + } + } + + var filesDescription: LocalizedStringKey { + switch self { + case .supporter: "Send 2GB files" + case .legend: "Send 5GB files" + } + } + + var monthlyPrice: LocalizedStringKey { + switch self { + case .supporter: "$7/month" + case .legend: "$70/month" + } + } + + var oneMonthPrice: LocalizedStringKey { + switch self { + case .supporter: "$7" + case .legend: "$70" + } + } + + var payMonthlyLabel: LocalizedStringKey { + switch self { + case .supporter: "Pay $7/month" + case .legend: "Pay $70/month" + } + } + + var payOnceLabel: LocalizedStringKey { + switch self { + case .supporter: "Pay $7" + case .legend: "Pay $70" + } + } + + var tagline: LocalizedStringKey { + switch self { + case .supporter: "Optional profile badge\nand 2GB files" + case .legend: "Optional profile badge\nand 5GB files" + } + } + + var badgeAsset: String { + switch self { + case .supporter: "badge-supporter" + case .legend: "badge-legend" + } + } +} + +struct BadgesYourLevelView: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var selectedLevel: BadgeLevel = .supporter + @State private var continueActive = false + @State private var howItWorksActive = false + + var body: some View { + GeometryReader { g in + ScrollView { + VStack(alignment: .center, spacing: 16) { + Text("Your level") + .font(.largeTitle) + .bold() + .foregroundColor(theme.colors.primary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + userPreview() + .padding(.top, 4) + + HStack(alignment: .top, spacing: 12) { + levelCard(.supporter) + levelCard(.legend) + } + .padding(.top, 8) + + Spacer(minLength: 20) + + continueButton() + + howItWorksButton() + .padding(.top, 4) + .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0) + } + .padding(.horizontal, 25) + .padding(.top, 8) + .padding(.bottom, 20) + .frame(minHeight: g.size.height) + } + } + .frame(maxHeight: .infinity) + } + + // The avatar + name preview shows the user how their profile will look with the selected badge. + // Uses the current user's real profile image and display name; when SIMPLEX_ASSETS is absent the + // avatar falls back to ProfileImage's own default. TODO [badges] wire real LocalBadge preview. + private func userPreview() -> some View { + let user = chatModel.currentUser + return VStack(spacing: 12) { + ProfileImage(imageStr: user?.image, size: 128) + HStack(alignment: .center, spacing: 6) { + Text(user?.displayName ?? NSLocalizedString("My nickname", comment: "badges preview placeholder")) + .font(.title2) + .fontWeight(.semibold) + .lineLimit(1) + .minimumScaleFactor(0.75) + Image(selectedLevel.badgeAsset) + .resizable() + .scaledToFit() + .frame(width: 28, height: 28) + Image(systemName: "chevron.down") + .font(.body) + .foregroundColor(theme.colors.primary) + } + } + } + + private func levelCard(_ level: BadgeLevel) -> some View { + let isSelected = level == selectedLevel + return Button { + withAnimation { selectedLevel = level } + } label: { + VStack(spacing: 10) { + Image(level.badgeAsset) + .resizable() + .scaledToFit() + .frame(width: 60, height: 60) + .padding(.top, 20) + Text(level.title) + .font(.title3) + .fontWeight(.bold) + Text(level.filesDescription) + .font(.subheadline) + .foregroundColor(theme.colors.secondary) + Text(level.monthlyPrice) + .font(.body) + .padding(.bottom, 20) + } + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(isSelected ? theme.colors.primary : Color.clear, lineWidth: 2) + ) + } + .buttonStyle(.plain) + } + + private func continueButton() -> some View { + ZStack { + Button { + continueActive = true + } label: { + Text("Continue") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + + NavigationLink(isActive: $continueActive) { + BadgesPayView(level: selectedLevel) + .modifier(ThemedBackground()) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func howItWorksButton() -> some View { + ZStack { + Button { + howItWorksActive = true + } label: { + Label("How private badges work", systemImage: "info.circle") + .font(.headline) + } + + NavigationLink(isActive: $howItWorksActive) { + BadgesHowItWorksView() + .modifier(ThemedBackground()) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } +} + +struct BadgesYourLevelView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + BadgesYourLevelView() + } + } +} diff --git a/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift new file mode 100644 index 0000000000..df4fccd2ae --- /dev/null +++ b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift @@ -0,0 +1,89 @@ +// +// SupportSimpleXBanner.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.07.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// A dismissible chat-list card promoting the badges purchase flow. Tapping the card opens the +// badges entry view (BadgesSupportSimplexView) — the same first screen the settings row opens. +// The dismiss X hides the card permanently until DEFAULT_SUPPORTER_BANNER_SHOWN is reset. +struct SupportSimpleXBanner: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.colorScheme) var colorScheme: ColorScheme + @AppStorage(DEFAULT_SUPPORTER_BANNER_SHOWN) private var supporterBannerShown = false + let onTap: () -> Void + + var body: some View { + if !supporterBannerShown { + ZStack(alignment: .topTrailing) { + Button(action: onTap) { + HStack(alignment: .center, spacing: 8) { + VStack(alignment: .leading, spacing: 4) { + Text("Support SimpleX") + .font(.headline) + .foregroundColor(theme.colors.primary) + Text("Get badge + files up to 5GB") + .font(.subheadline) + .foregroundColor(theme.colors.onBackground) + } + Spacer() + heroThumbnail() + } + .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) + .background(gradientBackground()) + .clipShape(RoundedRectangle(cornerRadius: 20)) + } + .buttonStyle(.plain) + + Button { + withAnimation { supporterBannerShown = true } + } label: { + Image(systemName: "xmark") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(theme.colors.secondary) + .frame(width: 24, height: 24) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(6) + } + } + } + + @ViewBuilder + private func heroThumbnail() -> some View { + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light") + .resizable() + .scaledToFit() + .frame(width: 90, height: 90) + #else + Image("badge-supporter") + .resizable() + .scaledToFit() + .frame(width: 56, height: 56) + #endif + } + + // Matches the onboarding card gradient so the banner reads as part of the same visual family. + private func gradientBackground() -> some View { + let gp = OnboardingCardView.gradientPoints(aspectRatio: 4.0, scale: colorScheme == .light ? 1.2 : 1.5) + return LinearGradient( + stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + } +} + +struct SupportSimpleXBanner_Previews: PreviewProvider { + static var previews: some View { + SupportSimpleXBanner(onTap: {}) + .padding() + } +} diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index b05e0696e3..b5052046f3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -174,7 +174,9 @@ struct ChatListView: View { @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false + @AppStorage(DEFAULT_SUPPORTER_BANNER_SHOWN) private var supporterBannerShown = false @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @State private var showBadgesSheet = false // Spec: spec/client/chat-list.md#body var body: some View { @@ -211,6 +213,11 @@ struct ChatListView: View { NewChatSheet() .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) } + .appSheet(isPresented: $showBadgesSheet) { + NavigationView { + BadgesSupportSimplexView() + } + } .onChange(of: activeUserPickerSheet) { if $0 != nil { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { @@ -418,6 +425,13 @@ struct ChatListView: View { .listRowSeparator(.hidden) .listRowBackground(Color.clear) } + if !supporterBannerShown { + SupportSimpleXBanner(onTap: { showBadgesSheet = true }) + .padding(.vertical, 6) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } if #available(iOS 16.0, *) { ForEach(cs, id: \.viewId) { chat in ChatListNavLink(chat: chat, parentSheet: $sheet) diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index b7753e8539..87a01b5d96 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -686,6 +686,18 @@ private let versionDescriptions: [VersionDescription] = [ )) ] ), + // TODO [badges] finalise copy + Read more link before v7.1 ships. + VersionDescription( + version: "v7.1", + post: nil, + features: [ + .view(FeatureView( + icon: nil, + title: "Supporter badge", + view: { SupporterBadgeWhatsNew() } + )) + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -700,6 +712,42 @@ func shouldShowWhatsNew() -> Bool { return v != lastVersion } +fileprivate struct SupporterBadgeWhatsNew: View { + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + + var body: some View { + HStack(alignment: .top, spacing: 12) { + VStack(alignment: .leading, spacing: 6) { + Text("Supporter badge ❤️") + .font(.title3) + .bold() + Text("Help keep the network running — send files up to 2 GB.") + .multilineTextAlignment(.leading) + .lineLimit(10) + } + .frame(maxWidth: .infinity, alignment: .leading) + + heroThumbnail() + } + } + + @ViewBuilder + private func heroThumbnail() -> some View { + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light") + .resizable() + .scaledToFit() + .frame(width: 110, height: 110) + #else + Image("badge-supporter") + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + #endif + } +} + fileprivate struct NewOperatorsView: View { var body: some View { VStack(alignment: .leading) { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 4bd5f7db1b..cc75fd1fad 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -56,6 +56,7 @@ let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness" let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail" let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown" let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown" +let DEFAULT_SUPPORTER_BANNER_SHOWN = "supporterBannerShown" let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial" let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" @@ -117,6 +118,7 @@ let appDefaults: [String: Any] = [ DEFAULT_CHAT_ITEM_TAIL: true, DEFAULT_ONE_HAND_UI_CARD_SHOWN: false, DEFAULT_ADDRESS_CREATION_CARD_SHOWN: false, + DEFAULT_SUPPORTER_BANNER_SHOWN: false, DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial, DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, @@ -148,6 +150,7 @@ let hintDefaults = [ DEFAULT_LA_NOTICE_SHOWN, DEFAULT_ONE_HAND_UI_CARD_SHOWN, DEFAULT_ADDRESS_CREATION_CARD_SHOWN, + DEFAULT_SUPPORTER_BANNER_SHOWN, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN, DEFAULT_SIGN_MESSAGE_ALERT_SHOWN, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE, @@ -341,6 +344,21 @@ struct SettingsView: View { } } + Section { + NavigationLink { + BadgesSupportSimplexView() + .modifier(ThemedBackground()) + } label: { + ZStack(alignment: .leading) { + Image("badge-supporter") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + Text("Supporter perks").padding(.leading, indent) + } + } + } + Section(header: Text("Advanced settings").foregroundColor(theme.colors.secondary)) { NavigationLink { NetworkAndServers() diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index ba73586836..423828b13a 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -250,6 +250,12 @@ D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; + E5BADE0101000000BADE0001 /* BadgesSupportSimplexView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE0102000000BADE0001 /* BadgesSupportSimplexView.swift */; }; + E5BADE0103000000BADE0002 /* BadgesYourLevelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE0104000000BADE0002 /* BadgesYourLevelView.swift */; }; + E5BADE0105000000BADE0003 /* BadgesPayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE0106000000BADE0003 /* BadgesPayView.swift */; }; + E5BADE0107000000BADE0004 /* BadgesRedeemCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE0108000000BADE0004 /* BadgesRedeemCodeView.swift */; }; + E5BADE0109000000BADE0005 /* BadgesHowItWorksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE010A000000BADE0005 /* BadgesHowItWorksView.swift */; }; + E5BADE010B000000BADE0006 /* SupportSimpleXBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE010C000000BADE0006 /* SupportSimpleXBanner.swift */; }; E559A0A12E3F77EE00B26F74 /* CommandsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */; }; E5A0B0012F960000AAAA0001 /* YourNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A0B0022F960000AAAA0001 /* YourNetwork.swift */; }; E5AEC0AB2F91A6EB00270665 /* CIChatLinkHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AEC0AA2F91A6EA00270665 /* CIChatLinkHeader.swift */; }; @@ -626,6 +632,12 @@ D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = ""; }; + E5BADE0102000000BADE0001 /* BadgesSupportSimplexView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesSupportSimplexView.swift; sourceTree = ""; }; + E5BADE0104000000BADE0002 /* BadgesYourLevelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesYourLevelView.swift; sourceTree = ""; }; + E5BADE0106000000BADE0003 /* BadgesPayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesPayView.swift; sourceTree = ""; }; + E5BADE0108000000BADE0004 /* BadgesRedeemCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesRedeemCodeView.swift; sourceTree = ""; }; + E5BADE010A000000BADE0005 /* BadgesHowItWorksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesHowItWorksView.swift; sourceTree = ""; }; + E5BADE010C000000BADE0006 /* SupportSimpleXBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportSimpleXBanner.swift; sourceTree = ""; }; E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandsMenuView.swift; sourceTree = ""; }; E5A0B0022F960000AAAA0001 /* YourNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourNetwork.swift; sourceTree = ""; }; E5AEC0AA2F91A6EA00270665 /* CIChatLinkHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatLinkHeader.swift; sourceTree = ""; }; @@ -784,6 +796,7 @@ 8C7D94982B8894D300B7B9E1 /* Migration */, 5CA8D01B2AD9B076001FD661 /* RemoteAccess */, 5CB924DF27A8678B00ACCCDD /* UserSettings */, + E5BADE0100000000BADE0000 /* Badges */, 5C2E261127A30FEA00F70299 /* TerminalView.swift */, ); path = Views; @@ -1042,6 +1055,19 @@ path = ChatList; sourceTree = ""; }; + E5BADE0100000000BADE0000 /* Badges */ = { + isa = PBXGroup; + children = ( + E5BADE0102000000BADE0001 /* BadgesSupportSimplexView.swift */, + E5BADE0104000000BADE0002 /* BadgesYourLevelView.swift */, + E5BADE0106000000BADE0003 /* BadgesPayView.swift */, + E5BADE0108000000BADE0004 /* BadgesRedeemCodeView.swift */, + E5BADE010A000000BADE0005 /* BadgesHowItWorksView.swift */, + E5BADE010C000000BADE0006 /* SupportSimpleXBanner.swift */, + ); + path = Badges; + sourceTree = ""; + }; 5CDCAD462818589900503DA2 /* SimpleX NSE */ = { isa = PBXGroup; children = ( @@ -1531,6 +1557,12 @@ 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */, + E5BADE0101000000BADE0001 /* BadgesSupportSimplexView.swift in Sources */, + E5BADE0103000000BADE0002 /* BadgesYourLevelView.swift in Sources */, + E5BADE0105000000BADE0003 /* BadgesPayView.swift in Sources */, + E5BADE0107000000BADE0004 /* BadgesRedeemCodeView.swift in Sources */, + E5BADE0109000000BADE0005 /* BadgesHowItWorksView.swift in Sources */, + E5BADE010B000000BADE0006 /* SupportSimpleXBanner.swift in Sources */, 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */, 5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,