diff --git a/iosApp/iosApp/Contractor/ContractorsListView.swift b/iosApp/iosApp/Contractor/ContractorsListView.swift index f08f61f..50567ad 100644 --- a/iosApp/iosApp/Contractor/ContractorsListView.swift +++ b/iosApp/iosApp/Contractor/ContractorsListView.swift @@ -26,6 +26,11 @@ struct ContractorsListView: View { viewModel.contractors } + // Check if upgrade screen should be shown (disables add button) + private var shouldShowUpgrade: Bool { + subscriptionCache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "contractors") + } + var body: some View { ZStack { Color.appBackgroundPrimary.ignoresSafeArea() @@ -130,7 +135,7 @@ struct ContractorsListView: View { .foregroundColor(selectedSpecialty != nil ? Color.appPrimary : Color.appTextSecondary) } - // Add Button + // Add Button (disabled when showing upgrade screen) Button(action: { // Check LIVE contractor count before adding let currentCount = viewModel.contractors.count @@ -142,8 +147,9 @@ struct ContractorsListView: View { }) { Image(systemName: "plus.circle.fill") .font(.title2) - .foregroundColor(Color.appPrimary) + .foregroundColor(shouldShowUpgrade ? Color.appTextSecondary.opacity(0.5) : Color.appPrimary) } + .disabled(shouldShowUpgrade) .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.addButton) } } diff --git a/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift b/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift index eb510f9..be385b4 100644 --- a/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift +++ b/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift @@ -28,6 +28,11 @@ struct DocumentsWarrantiesView: View { documentViewModel.documents.filter { $0.documentType != "warranty" } } + // Check if upgrade screen should be shown (disables add button) + private var shouldShowUpgrade: Bool { + subscriptionCache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "documents") + } + var body: some View { ZStack { Color.appBackgroundPrimary.ignoresSafeArea() @@ -154,7 +159,7 @@ struct DocumentsWarrantiesView: View { .foregroundColor((selectedCategory != nil || selectedDocType != nil) ? Color.appPrimary : Color.appTextSecondary) } - // Add Button + // Add Button (disabled when showing upgrade screen) Button(action: { // Check LIVE document count before adding let currentCount = documentViewModel.documents.count @@ -166,8 +171,9 @@ struct DocumentsWarrantiesView: View { }) { Image(systemName: "plus.circle.fill") .font(.title2) - .foregroundColor(Color.appPrimary) + .foregroundColor(shouldShowUpgrade ? Color.appTextSecondary.opacity(0.5) : Color.appPrimary) } + .disabled(shouldShowUpgrade) } } } diff --git a/iosApp/iosApp/Subscription/UpgradeFeatureView.swift b/iosApp/iosApp/Subscription/UpgradeFeatureView.swift index d876660..b14bc85 100644 --- a/iosApp/iosApp/Subscription/UpgradeFeatureView.swift +++ b/iosApp/iosApp/Subscription/UpgradeFeatureView.swift @@ -1,12 +1,18 @@ import SwiftUI import ComposeApp +import StoreKit struct UpgradeFeatureView: View { let triggerKey: String let icon: String - @State private var showUpgradePrompt = false + @State private var showFeatureComparison = false + @State private var isProcessing = false + @State private var selectedProduct: Product? + @State private var errorMessage: String? + @State private var showSuccessAlert = false @StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared + @StateObject private var storeKit = StoreKitManager.shared // Look up trigger data from cache private var triggerData: UpgradeTriggerData? { @@ -27,58 +33,185 @@ struct UpgradeFeatureView: View { } var body: some View { - VStack(spacing: AppSpacing.xl) { - Spacer() + ScrollView { + VStack(spacing: AppSpacing.xl) { + // Icon + Image(systemName: "star.circle.fill") + .font(.system(size: 60)) + .foregroundStyle(Color.appAccent.gradient) + .padding(.top, AppSpacing.xl) - // Feature Icon - Image(systemName: icon) - .font(.system(size: 80)) - .foregroundStyle(Color.appPrimary.gradient) + // Title + Text(title) + .font(.title2.weight(.bold)) + .foregroundColor(Color.appTextPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal) - // Title - Text(title) - .font(.title.weight(.bold)) - .foregroundColor(Color.appTextPrimary) - .multilineTextAlignment(.center) - .padding(.horizontal, AppSpacing.xl) + // Message + Text(message) + .font(.body) + .foregroundColor(Color.appTextSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal) - // Description - Text(message) - .font(.body) - .foregroundColor(Color.appTextSecondary) - .multilineTextAlignment(.center) - .padding(.horizontal, AppSpacing.xl) + // Pro Features Preview - Dynamic content or fallback + Group { + if let promoContent = triggerData?.promoHtml, !promoContent.isEmpty { + PromoContentView(content: promoContent) + .padding() + } else { + // Fallback to static features if no promo content + VStack(alignment: .leading, spacing: AppSpacing.md) { + FeatureRow(icon: "house.fill", text: "Unlimited properties") + FeatureRow(icon: "checkmark.circle.fill", text: "Unlimited tasks") + FeatureRow(icon: "person.2.fill", text: "Contractor management") + FeatureRow(icon: "doc.fill", text: "Document & warranty storage") + } + .padding() + } + } + .background(Color.appBackgroundSecondary) + .cornerRadius(AppRadius.lg) + .padding(.horizontal) - // Upgrade Message - Text("This feature is available with Pro") - .font(.subheadline.weight(.medium)) - .foregroundColor(Color.appAccent) - .padding(.horizontal, AppSpacing.md) - .padding(.vertical, AppSpacing.sm) - .background(Color.appAccent.opacity(0.1)) - .cornerRadius(AppRadius.md) + // Subscription Products + if storeKit.isLoading { + ProgressView() + .tint(Color.appPrimary) + .padding() + } else if !storeKit.products.isEmpty { + VStack(spacing: AppSpacing.md) { + ForEach(storeKit.products, id: \.id) { product in + SubscriptionProductButton( + product: product, + isSelected: selectedProduct?.id == product.id, + isProcessing: isProcessing, + onSelect: { + selectedProduct = product + handlePurchase(product) + } + ) + } + } + .padding(.horizontal) + } else { + // Fallback upgrade button if products fail to load + Button(action: { + Task { await storeKit.loadProducts() } + }) { + HStack { + if isProcessing { + ProgressView() + .tint(Color.appTextOnPrimary) + } else { + Text("Retry Loading Products") + .fontWeight(.semibold) + } + } + .frame(maxWidth: .infinity) + .foregroundColor(Color.appTextOnPrimary) + .padding() + .background(Color.appPrimary) + .cornerRadius(AppRadius.md) + } + .disabled(isProcessing) + .padding(.horizontal) + } - // Upgrade Button - Button(action: { - showUpgradePrompt = true - }) { - Text(buttonText) - .font(.headline) - .foregroundColor(Color.appTextOnPrimary) - .frame(maxWidth: .infinity) + // Error Message + if let error = errorMessage { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(Color.appError) + Text(error) + .font(.subheadline) + .foregroundColor(Color.appError) + } .padding() - .background(Color.appPrimary) + .background(Color.appError.opacity(0.1)) .cornerRadius(AppRadius.md) - } - .padding(.horizontal, AppSpacing.xl) - .padding(.top, AppSpacing.lg) + .padding(.horizontal) + } - Spacer() + // Compare Plans + Button(action: { + showFeatureComparison = true + }) { + Text("Compare Free vs Pro") + .font(.subheadline) + .foregroundColor(Color.appPrimary) + } + + // Restore Purchases + Button(action: { + handleRestore() + }) { + Text("Restore Purchases") + .font(.caption) + .foregroundColor(Color.appTextSecondary) + } + .padding(.bottom, AppSpacing.xxxl) + } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.appBackgroundPrimary) - .sheet(isPresented: $showUpgradePrompt) { - UpgradePromptView(triggerKey: triggerKey, isPresented: $showUpgradePrompt) + .sheet(isPresented: $showFeatureComparison) { + FeatureComparisonView(isPresented: $showFeatureComparison) + } + .alert("Subscription Active", isPresented: $showSuccessAlert) { + Button("Done") { } + } message: { + Text("You now have full access to all Pro features!") + } + .task { + // Refresh subscription cache to get latest upgrade triggers + subscriptionCache.refreshFromCache() + await storeKit.loadProducts() + } + } + + private func handlePurchase(_ product: Product) { + isProcessing = true + errorMessage = nil + + Task { + do { + let transaction = try await storeKit.purchase(product) + + await MainActor.run { + isProcessing = false + + if transaction != nil { + // Purchase successful + showSuccessAlert = true + } + } + } catch { + await MainActor.run { + isProcessing = false + errorMessage = "Purchase failed: \(error.localizedDescription)" + } + } + } + } + + private func handleRestore() { + isProcessing = true + errorMessage = nil + + Task { + await storeKit.restorePurchases() + + await MainActor.run { + isProcessing = false + + if !storeKit.purchasedProductIDs.isEmpty { + showSuccessAlert = true + } else { + errorMessage = "No purchases found to restore" + } + } } } }