diff --git a/apps/ios/Shared/Views/Badges/BadgeUserPreview.swift b/apps/ios/Shared/Views/Badges/BadgeUserPreview.swift new file mode 100644 index 0000000000..050ea45746 --- /dev/null +++ b/apps/ios/Shared/Views/Badges/BadgeUserPreview.swift @@ -0,0 +1,44 @@ +// +// BadgeUserPreview.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 30.07.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// Shows the user's avatar and display name with a synthesized preview badge at the selected +// level, using the same NameWithBadge helper that renders badges everywhere else in the app — +// so the preview matches what will actually appear on the user's name. Any picker affordance +// (e.g. a chevron) is supplied by the caller via `trailing`, and is laid out next to the +// name row without leaking into this view. +struct BadgeUserPreview: View { + @EnvironmentObject var chatModel: ChatModel + let level: BadgeLevel + let trailing: () -> Trailing + + init(level: BadgeLevel, @ViewBuilder trailing: @escaping () -> Trailing = { EmptyView() }) { + self.level = level + self.trailing = trailing + } + + var body: some View { + let user = chatModel.currentUser + let displayName = user?.displayName ?? NSLocalizedString("My nickname", comment: "badges preview placeholder") + let previewBadge = LocalBadge( + badge: BadgeInfo(badgeType: level.badgeType), + status: .active + ) + return VStack(spacing: 12) { + ProfileImage(imageStr: user?.image, size: 128) + HStack(alignment: .center, spacing: 6) { + NameWithBadge(Text(displayName).font(.largeTitle), previewBadge, .largeTitle) + .lineLimit(1) + .minimumScaleFactor(0.75) + trailing() + } + } + } +} diff --git a/apps/ios/Shared/Views/Badges/BadgesPayView.swift b/apps/ios/Shared/Views/Badges/BadgesPayView.swift index 4a1225dabd..6ae10f4c7d 100644 --- a/apps/ios/Shared/Views/Badges/BadgesPayView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesPayView.swift @@ -33,7 +33,6 @@ enum BadgePeriod: String, CaseIterable, Identifiable { } struct BadgesPayView: View { - @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme let level: BadgeLevel @State private var selectedPeriod: BadgePeriod = .subscribe @@ -49,7 +48,7 @@ struct BadgesPayView: View { .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) - userPreview() + BadgeUserPreview(level: level) .padding(.top, 4) Text(level.tagline) @@ -86,23 +85,6 @@ struct BadgesPayView: View { .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(.largeTitle) - .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 { diff --git a/apps/ios/Shared/Views/Badges/BadgesYourLevelView.swift b/apps/ios/Shared/Views/Badges/BadgesYourLevelView.swift index 47280d9254..b4a1caf57e 100644 --- a/apps/ios/Shared/Views/Badges/BadgesYourLevelView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesYourLevelView.swift @@ -72,10 +72,16 @@ enum BadgeLevel: String, CaseIterable, Identifiable { case .legend: "badge-legend" } } + + var badgeType: BadgeType { + switch self { + case .supporter: .supporter + case .legend: .legend + } + } } struct BadgesYourLevelView: View { - @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @State private var selectedLevel: BadgeLevel = .supporter @State private var continueActive = false @@ -92,8 +98,12 @@ struct BadgesYourLevelView: View { .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) - userPreview() - .padding(.top, 4) + BadgeUserPreview(level: selectedLevel) { + Image(systemName: "chevron.down") + .font(.body) + .foregroundColor(theme.colors.primary) + } + .padding(.top, 4) HStack(alignment: .top, spacing: 12) { levelCard(.supporter) @@ -118,29 +128,6 @@ struct BadgesYourLevelView: View { .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(.largeTitle) - .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 { diff --git a/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift index 3addca6021..0fbf791414 100644 --- a/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift +++ b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift @@ -123,13 +123,27 @@ struct SupportSimpleXBanner: View { #endif } + @ViewBuilder private func gradientBackground() -> some View { - let stops = colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops - return LinearGradient( - colors: [stops.first!.color, stops.last!.color], - startPoint: .bottomLeading, - endPoint: .topTrailing - ) + // Light: just the first and last stops of OnboardingCardView.lightStops as a simple + // 2-color gradient — the intermediate stops there include a hard blue plateau (0.0–0.5) + // that reads as "almost entirely blue" on this wide-short banner. + // Dark: the full OnboardingCardView.darkStops (all 5 locations) — the intermediate blue-to- + // navy transitions land nicely on this shape, giving depth we'd lose with only 2 colors. + if colorScheme == .light { + let stops = OnboardingCardView.lightStops + LinearGradient( + colors: [stops.first!.color, stops.last!.color], + startPoint: .bottomLeading, + endPoint: .topTrailing + ) + } else { + LinearGradient( + stops: OnboardingCardView.darkStops, + startPoint: .bottomLeading, + endPoint: .topTrailing + ) + } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 423828b13a..48309ed3ed 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -256,6 +256,7 @@ 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 */; }; + E5BADE010D000000BADE0007 /* BadgeUserPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE010E000000BADE0007 /* BadgeUserPreview.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 */; }; @@ -638,6 +639,7 @@ 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 = ""; }; + E5BADE010E000000BADE0007 /* BadgeUserPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeUserPreview.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 = ""; }; @@ -1064,6 +1066,7 @@ E5BADE0108000000BADE0004 /* BadgesRedeemCodeView.swift */, E5BADE010A000000BADE0005 /* BadgesHowItWorksView.swift */, E5BADE010C000000BADE0006 /* SupportSimpleXBanner.swift */, + E5BADE010E000000BADE0007 /* BadgeUserPreview.swift */, ); path = Badges; sourceTree = ""; @@ -1563,6 +1566,7 @@ E5BADE0107000000BADE0004 /* BadgesRedeemCodeView.swift in Sources */, E5BADE0109000000BADE0005 /* BadgesHowItWorksView.swift in Sources */, E5BADE010B000000BADE0006 /* SupportSimpleXBanner.swift in Sources */, + E5BADE010D000000BADE0007 /* BadgeUserPreview.swift in Sources */, 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */, 5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index e6940b11d4..4b4c666d48 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -275,11 +275,22 @@ public struct BadgeInfo: Codable, Hashable { public var badgeType: BadgeType public var badgeExpiry: Date? public var badgeExtra: String + + public init(badgeType: BadgeType, badgeExpiry: Date? = nil, badgeExtra: String = "") { + self.badgeType = badgeType + self.badgeExpiry = badgeExpiry + self.badgeExtra = badgeExtra + } } public struct LocalBadge: Codable, Hashable { public var badge: BadgeInfo public var status: BadgeStatus + + public init(badge: BadgeInfo, status: BadgeStatus) { + self.badge = badge + self.status = status + } } // the wire proof carried on a profile - opaque to the UI, only round-tripped back to the core (apiPrepareContact)