From d1429071f678a74a071b9ac21e757b382e56ce54 Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 20 Feb 2026 13:51:38 -0600 Subject: [PATCH] redesign PaywallView with premium header, feature grid, and ticket separator Replace the generic paywall header with a branded "SportsTime Pro / Your All-Access Pass" hero section, 4 uniform feature cards (GeometryReader-sized squares), and a dashed ticket perforation separator. Add "Show Paywall" debug button in Settings. Co-Authored-By: Claude Opus 4.6 --- .../Features/Paywall/Views/PaywallView.swift | 108 +++++++++++++----- .../Settings/Views/SettingsView.swift | 6 + 2 files changed, 86 insertions(+), 28 deletions(-) diff --git a/SportsTime/Features/Paywall/Views/PaywallView.swift b/SportsTime/Features/Paywall/Views/PaywallView.swift index 8b803e2..4b1fc64 100644 --- a/SportsTime/Features/Paywall/Views/PaywallView.swift +++ b/SportsTime/Features/Paywall/Views/PaywallView.swift @@ -21,29 +21,57 @@ struct PaywallView: View { var body: some View { SubscriptionStoreView(subscriptions: storeManager.products.filter { $0.subscription != nil }) { - VStack(spacing: Theme.Spacing.md) { - Image(systemName: "star.circle.fill") - .font(.system(.largeTitle, design: .default).weight(.regular)) - .foregroundStyle(Theme.warmOrange) - .accessibilityHidden(true) + VStack(spacing: 0) { + // Hero section + VStack(spacing: Theme.Spacing.sm) { + Image(systemName: "shield.lefthalf.filled.badge.checkmark") + .font(.system(size: 28)) + .foregroundStyle(Theme.warmOrange) + .accessibilityHidden(true) - Text("Upgrade to Pro") - .font(.largeTitle.bold()) - .foregroundStyle(Theme.textPrimary(colorScheme)) - .accessibilityIdentifier("paywall.title") + Text("SportsTime Pro") + .font(.title2.bold()) + .foregroundStyle(Theme.textPrimary(colorScheme)) + .accessibilityIdentifier("paywall.title") - Text("Unlock the full SportsTime experience") - .font(.body) - .foregroundStyle(Theme.textSecondary(colorScheme)) - - HStack(spacing: Theme.Spacing.lg) { - featurePill(icon: "infinity", text: "Unlimited Trips") - featurePill(icon: "doc.fill", text: "PDF Export") - featurePill(icon: "trophy.fill", text: "Progress") + Text("Your All-Access Pass") + .font(.subheadline) + .foregroundStyle(Theme.textSecondary(colorScheme)) } - .padding(.top, Theme.Spacing.sm) + .padding(.vertical, Theme.Spacing.xl) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .fill(Theme.warmOrange.opacity(0.06)) + ) + .padding(.horizontal, Theme.Spacing.md) + + // Feature grid — GeometryReader to make all cards identical squares + GeometryReader { geo in + let spacing = Theme.Spacing.sm + let hPadding = Theme.Spacing.md * 2 + let cardSize = (geo.size.width - hPadding - spacing * 3) / 4 + + HStack(spacing: spacing) { + featureCard(icon: "infinity", label: "Unlimited\nTrips", size: cardSize) + featureCard(icon: "doc.text.fill", label: "PDF\nExport", size: cardSize) + featureCard(icon: "building.2.fill", label: "Stadium\nTracking", size: cardSize) + featureCard(icon: "trophy.fill", label: "Achieve-\nments", size: cardSize) + } + .padding(.horizontal, Theme.Spacing.md) + } + .frame(height: 90) + .padding(.top, Theme.Spacing.md) + + // Dashed ticket perforation separator + Line() + .stroke(style: StrokeStyle(lineWidth: 1.5, dash: [6, 4])) + .foregroundStyle(Theme.textMuted(colorScheme).opacity(0.4)) + .frame(height: 1) + .padding(.horizontal, Theme.Spacing.lg) + .padding(.top, Theme.Spacing.lg) + .padding(.bottom, Theme.Spacing.sm) } - .padding(Theme.Spacing.lg) } .storeButton(.visible, for: .restorePurchases) .subscriptionStoreControlStyle(.prominentPicker) @@ -78,19 +106,43 @@ struct PaywallView: View { } } - private func featurePill(icon: String, text: String) -> some View { - HStack(spacing: 4) { + // MARK: - Feature Card + + private func featureCard(icon: String, label: String, size: CGFloat) -> some View { + VStack(spacing: 4) { Image(systemName: icon) - .font(.caption2) + .font(.system(size: 18)) + .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) - Text(text) - .font(.caption2) + + Text(label) + .font(.system(size: 10, weight: .medium)) + .multilineTextAlignment(.center) + .foregroundStyle(Theme.textSecondary(colorScheme)) + .lineLimit(2) + .minimumScaleFactor(0.8) } .accessibilityElement(children: .combine) - .foregroundStyle(Theme.textMuted(colorScheme)) - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background(Theme.warmOrange.opacity(0.08), in: Capsule()) + .frame(width: size, height: size) + .background( + RoundedRectangle(cornerRadius: Theme.CornerRadius.small) + .fill(Theme.warmOrange.opacity(0.08)) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.small) + .strokeBorder(Theme.warmOrange.opacity(0.15), lineWidth: 1) + ) + ) + } +} + +// MARK: - Dashed Line Shape + +private struct Line: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: 0, y: rect.midY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) + return path } } diff --git a/SportsTime/Features/Settings/Views/SettingsView.swift b/SportsTime/Features/Settings/Views/SettingsView.swift index 1fd9ca5..f9c4258 100644 --- a/SportsTime/Features/Settings/Views/SettingsView.swift +++ b/SportsTime/Features/Settings/Views/SettingsView.swift @@ -543,6 +543,12 @@ struct SettingsView: View { Label("Show Onboarding Flow", systemImage: "play.circle") } + Button { + showPaywall = true + } label: { + Label("Show Paywall", systemImage: "creditcard.fill") + } + Button { UserDefaults.standard.removeObject(forKey: "hasSeenOnboardingPaywall") } label: {