diff --git a/apps/ios/Shared/Views/Badges/BadgeUserPreview.swift b/apps/ios/Shared/Views/Badges/BadgeUserPreview.swift index 050ea45746..d833bed337 100644 --- a/apps/ios/Shared/Views/Badges/BadgeUserPreview.swift +++ b/apps/ios/Shared/Views/Badges/BadgeUserPreview.swift @@ -9,11 +9,9 @@ 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. +// User's avatar + name with a preview badge at the selected level, rendered via NameWithBadge +// so the preview matches how the badge appears on the name elsewhere. Callers can inject a +// picker affordance (e.g. a chevron) into the name row via `trailing`. struct BadgeUserPreview: View { @EnvironmentObject var chatModel: ChatModel let level: BadgeLevel diff --git a/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift b/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift index f5ea5a8d59..fe3fa16c7a 100644 --- a/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift @@ -9,10 +9,8 @@ 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. +// Entry point for badges management. Subsequent screens push via NavigationLink; the enclosing +// NavigationView (settings or the chat-list banner sheet) provides the sliding animation. struct BadgesSupportSimplexView: View { @EnvironmentObject var theme: AppTheme @Environment(\.colorScheme) var colorScheme: ColorScheme @@ -21,13 +19,7 @@ struct BadgesSupportSimplexView: View { @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 - // - // No ScrollView here — matches SimpleXInfo/YourNetworkView: on tight screens the - // .scaledToFit hero shrinks to fit the vertical space left by the fixed-height title, - // body, info button and CTAs, instead of pushing content off-screen. + // TODO [badges] gate on user badge status (no badge → this view, active → "Manage your badge") GeometryReader { g in VStack(alignment: .center, spacing: 16) { Text("Support SimpleX") @@ -64,12 +56,8 @@ struct BadgesSupportSimplexView: View { .padding(.horizontal, 25) .padding(.top, 28) .padding(.bottom, 20) - // .frame(height:) instead of .frame(minHeight:) — locks the VStack to the - // proposed geometry. In the onboarding flow (full-screen presentation) minHeight is - // enough because the parent caps at the screen; inside the banner sheet's NavigationView - // the parent DOESN'T cap, so a minHeight-only VStack expands past the visible area, - // which both makes the hero gigantic and lets the sheet interpret the overflow as - // scroll content. Locking to the geometry keeps the layout inside the visible bounds. + // .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) @@ -123,9 +111,8 @@ struct BadgesSupportSimplexView: View { } } -// 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. +// Hero image reused across badges views and WhatsNewView v7.1. Fallback (no SIMPLEX_ASSETS) is a +// gradient card carrying the small supporter badge glyph. struct PhoneSupporterHero: View { @EnvironmentObject var theme: AppTheme @Environment(\.colorScheme) var colorScheme: ColorScheme diff --git a/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift index 0fbf791414..d49445c511 100644 --- a/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift +++ b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift @@ -9,105 +9,79 @@ 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 shows a confirmation alert (matching the Reachable-toolbar banner), and once -// dismissed the banner stays hidden until DEFAULT_SUPPORTER_BANNER_SHOWN is reset. The row-level -// show gate (chat count > 2 counting only visible chats) lives in ChatListView. +// Dumb chat-list card promoting the badges purchase flow. Both the show gate and the dismissal +// persistence live in ChatListView; this view just renders and reports taps. struct SupportSimpleXBanner: View { @EnvironmentObject var theme: AppTheme @Environment(\.colorScheme) var colorScheme: ColorScheme - @AppStorage(DEFAULT_SUPPORTER_BANNER_SHOWN) private var supporterBannerShown = false @State private var showDismissAlert = false let onTap: () -> Void + let onDismiss: () -> Void private let cardCornerRadius: CGFloat = 16 private let cardHeight: CGFloat = 72 - // 16pt matches OneHandUICard's segment icon .padding(.leading, 16) — so the banner's text - // aligns with the icon column of the Reachable-toolbar card above it in the list. + // matches OneHandUICard's segment icon leading so the text aligns with it in the list private let cardLeadingPadding: CGFloat = 16 private let cardTrailingPadding: CGFloat = 8 private let heroWidth: CGFloat = 110 - // Illustration is DRAWN at NATURAL aspect (0.795) via .aspectRatio(.fill) — for heroWidth 110 - // that gives a 110×138 image. The visible slot is heroWidth × heroVisibleHeight (110×108), top- - // aligned; the ~30pt of natural image below the slot is cut by .clipped(). Crop line lands - // ~78% into the phone body. + // shorter than the natural drawn height so .clipped() slices the phone body at card bottom private let heroVisibleHeight: CGFloat = 108 - // 28 (was 32) — hero right sits exactly at X's left edge (X's own trailing 16 + width 12 = 28). - // No overlap, no gap. Required so the wider 110pt hero still leaves subtitle room. + // hero right edge sits exactly at the dismiss X's left edge (X trailing 16 + width 12) private let heroTrailingPadding: CGFloat = 28 - // 6 (was 8) — small reduction to reclaim text width with the bigger hero. private let textToHeroGap: CGFloat = 6 var body: some View { - if !supporterBannerShown { - // The card Button is the base (its natural size is the card rectangle, cardHeight tall) - // and the hero image is added as an .overlay so it can visually extend past the card top - // without changing the layout size. The dismiss X lives as a sibling in the outer - // ZStack, aligned .topTrailing so it sits at the card's top-right; the X label uses - // asymmetric vertical padding (12 top / 4 bottom) so the icon has a visible gap from - // the card top edge without being pinned to the very top. - ZStack(alignment: .topTrailing) { - Button(action: onTap) { - HStack(spacing: 0) { - 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(minLength: heroWidth + heroTrailingPadding + textToHeroGap) + // Card is the Button; hero is an overlay so it can extend above the card top without + // affecting layout size. Dismiss X is a ZStack sibling anchored to the card's top-right. + ZStack(alignment: .topTrailing) { + Button(action: onTap) { + HStack(spacing: 0) { + 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) } - .padding(.leading, cardLeadingPadding) - .padding(.trailing, cardTrailingPadding) - .frame(height: cardHeight) - .background(gradientBackground()) - .clipShape(RoundedRectangle(cornerRadius: cardCornerRadius)) - } - .buttonStyle(.plain) - .overlay(alignment: .bottomTrailing) { - // Hero bottom-anchored to card bottom via .overlay(.bottomTrailing); the - // illustration extends UP above the card top only. No .offset — nothing must - // draw below the card. .clipped() inside heroThumbnail is a hard rectangular - // clip at the hero's own frame boundary to guarantee that. - heroThumbnail() - .padding(.trailing, heroTrailingPadding) - .allowsHitTesting(false) + Spacer(minLength: heroWidth + heroTrailingPadding + textToHeroGap) } + .padding(.leading, cardLeadingPadding) + .padding(.trailing, cardTrailingPadding) + .frame(height: cardHeight) + .background(gradientBackground()) + .clipShape(RoundedRectangle(cornerRadius: cardCornerRadius)) + } + .buttonStyle(.plain) + .overlay(alignment: .bottomTrailing) { + heroThumbnail() + .padding(.trailing, heroTrailingPadding) + .allowsHitTesting(false) + } - // X, sibling of the Button in ZStack, aligned to CARD top-right. - Image(systemName: "multiply") - .foregroundColor(theme.colors.secondary) - .frame(width: 12, height: 12) - .padding(.top, 12) - .padding(.bottom, 4) - .padding(.trailing, 16) - .padding(.leading, 4) - .contentShape(Rectangle()) - .onTapGesture { showDismissAlert = true } - } - .zIndex(1) - .alert(isPresented: $showDismissAlert) { - Alert( - title: Text("Support SimpleX"), - message: Text("You can support SimpleX later in Settings."), - dismissButton: .default(Text("Ok")) { - withAnimation { supporterBannerShown = true } - } - ) - } + Image(systemName: "multiply") + .foregroundColor(theme.colors.secondary) + .frame(width: 12, height: 12) + .padding(.top, 12) + .padding(.bottom, 4) + .padding(.trailing, 16) + .padding(.leading, 4) + .contentShape(Rectangle()) + .onTapGesture { showDismissAlert = true } + } + .alert(isPresented: $showDismissAlert) { + Alert( + title: Text("Support SimpleX"), + message: Text("You can support SimpleX later in Settings."), + dismissButton: .default(Text("Ok"), action: onDismiss) + ) } } @ViewBuilder private func heroThumbnail() -> some View { #if SIMPLEX_ASSETS - // .aspectRatio(.fill) draws at NATURAL aspect (0.795) filling the frame — image ends up - // ~110×138. .frame(width, heroVisibleHeight, alignment: .top) makes the visible slot - // shorter than the drawn image, top-aligned; .clipped() cuts the ~30pt of natural image - // that would otherwise overflow the slot. The crop line lands ~78% into the phone body. + // draws at natural aspect, top-aligned in a shorter slot; .clipped() cuts the overflow at card bottom Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light") .resizable() .aspectRatio(contentMode: .fill) @@ -125,11 +99,8 @@ struct SupportSimpleXBanner: View { @ViewBuilder private func gradientBackground() -> some View { - // 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. + // light: 2-color end-to-end (lightStops' 0.0–0.5 blue plateau would read as all-blue here). + // dark: full 5-stop darkStops — the intermediate blue-to-navy transitions land nicely. if colorScheme == .light { let stops = OnboardingCardView.lightStops LinearGradient( @@ -149,7 +120,7 @@ struct SupportSimpleXBanner: View { struct SupportSimpleXBanner_Previews: PreviewProvider { static var previews: some View { - SupportSimpleXBanner(onTap: {}) + SupportSimpleXBanner(onTap: {}, onDismiss: {}) .padding() } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index d80ef139b0..f941e97e99 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -374,19 +374,6 @@ struct ChatListView: View { !addressCreationCardShown && !chatModel.chats.isEmpty && !hasConversations } - // Count of user-facing conversations (excludes pending contact requests/connections, deleted - // chats and contact cards) — used as the show gate for SupportSimpleXBanner so the banner only - // appears once the user actually has a few conversations in the list. - private var visibleChatCount: Int { - chatModel.chats.reduce(into: 0) { count, chat in - switch chat.chatInfo { - case let .direct(contact): if !contact.chatDeleted && !contact.isContactCard { count += 1 } - case .group, .local: count += 1 - case .contactRequest, .contactConnection, .invalidJSON: break - } - } - } - private var hasConversations: Bool { chatModel.chats.contains { chat in switch chat.chatInfo { @@ -438,8 +425,13 @@ struct ChatListView: View { .listRowSeparator(.hidden) .listRowBackground(Color.clear) } - if !supporterBannerShown && visibleChatCount > 2 { - SupportSimpleXBanner(onTap: { showBadgesSheet = true }) + // +1 vs SimpleX Lock's threshold (ContentView) accounts for the always-present + // SimpleX Directory contact card, so this still needs 3 real conversations. + if !supporterBannerShown && chatModel.chats.count > 3 { + SupportSimpleXBanner( + onTap: { showBadgesSheet = true }, + onDismiss: { withAnimation { supporterBannerShown = true } } + ) .padding(.vertical, 3) .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .listRowSeparator(.hidden)