Add 22 new UI tests across 8 test files covering Home, Schedule, Progress, Settings, TabNavigation, TripSaving, and TripOptions. Add accessibility identifiers to 11 view files for test element discovery. Fix sport chip assertion logic (all sports start selected, tap deselects), scroll container issues on iOS 26 nested ScrollViews, toggle interaction, and delete trip flow. Update QA coverage map from 32 to 54 automated test cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.7 KiB
Swift
100 lines
3.7 KiB
Swift
//
|
|
// PaywallView.swift
|
|
// SportsTime
|
|
//
|
|
// Full-screen paywall for Pro subscription using SubscriptionStoreView.
|
|
//
|
|
|
|
import SwiftUI
|
|
import StoreKit
|
|
|
|
struct PaywallView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
private let storeManager = StoreManager.shared
|
|
let source: String
|
|
|
|
init(source: String = "unknown") {
|
|
self.source = source
|
|
}
|
|
|
|
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)
|
|
|
|
Text("Upgrade to Pro")
|
|
.font(.largeTitle.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")
|
|
}
|
|
.padding(.top, Theme.Spacing.sm)
|
|
}
|
|
.padding(Theme.Spacing.lg)
|
|
}
|
|
.storeButton(.visible, for: .restorePurchases)
|
|
.subscriptionStoreControlStyle(.prominentPicker)
|
|
.subscriptionStoreButtonLabel(.displayName.multiline)
|
|
.onInAppPurchaseStart { product in
|
|
AnalyticsManager.shared.trackPurchaseStarted(productId: product.id, source: source)
|
|
}
|
|
.onInAppPurchaseCompletion { product, result in
|
|
switch result {
|
|
case .success(.success(_)):
|
|
AnalyticsManager.shared.trackPurchaseCompleted(productId: product.id, source: source)
|
|
Task { @MainActor in
|
|
await storeManager.updateEntitlements()
|
|
storeManager.trackSubscriptionAnalytics(source: "purchase_success")
|
|
}
|
|
dismiss()
|
|
case .success(.userCancelled):
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "user_cancelled")
|
|
case .success(.pending):
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "pending")
|
|
case .failure(let error):
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: error.localizedDescription)
|
|
@unknown default:
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "unknown_result")
|
|
}
|
|
}
|
|
.task {
|
|
await storeManager.loadProducts()
|
|
}
|
|
.onAppear {
|
|
AnalyticsManager.shared.trackPaywallViewed(source: source)
|
|
}
|
|
}
|
|
|
|
private func featurePill(icon: String, text: String) -> some View {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: icon)
|
|
.font(.caption2)
|
|
.accessibilityHidden(true)
|
|
Text(text)
|
|
.font(.caption2)
|
|
}
|
|
.accessibilityElement(children: .combine)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(Theme.warmOrange.opacity(0.08), in: Capsule())
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
PaywallView()
|
|
}
|