Show full paywall inline and disable add button when upgrade required

- Update UpgradeFeatureView to display complete paywall experience
  (promo content, subscription products, purchase flow) instead of
  simple teaser
- Disable add button in Documents view when upgrade screen is showing
- Disable add button in Contractors view when upgrade screen is showing
- Gray out disabled add buttons to indicate non-interactive state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-24 22:05:59 -06:00
parent 67e0057bfa
commit f3fbee1e27
3 changed files with 191 additions and 46 deletions

View File

@@ -26,6 +26,11 @@ struct ContractorsListView: View {
viewModel.contractors 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 { var body: some View {
ZStack { ZStack {
Color.appBackgroundPrimary.ignoresSafeArea() Color.appBackgroundPrimary.ignoresSafeArea()
@@ -130,7 +135,7 @@ struct ContractorsListView: View {
.foregroundColor(selectedSpecialty != nil ? Color.appPrimary : Color.appTextSecondary) .foregroundColor(selectedSpecialty != nil ? Color.appPrimary : Color.appTextSecondary)
} }
// Add Button // Add Button (disabled when showing upgrade screen)
Button(action: { Button(action: {
// Check LIVE contractor count before adding // Check LIVE contractor count before adding
let currentCount = viewModel.contractors.count let currentCount = viewModel.contractors.count
@@ -142,8 +147,9 @@ struct ContractorsListView: View {
}) { }) {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.font(.title2) .font(.title2)
.foregroundColor(Color.appPrimary) .foregroundColor(shouldShowUpgrade ? Color.appTextSecondary.opacity(0.5) : Color.appPrimary)
} }
.disabled(shouldShowUpgrade)
.accessibilityIdentifier(AccessibilityIdentifiers.Contractor.addButton) .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.addButton)
} }
} }

View File

@@ -28,6 +28,11 @@ struct DocumentsWarrantiesView: View {
documentViewModel.documents.filter { $0.documentType != "warranty" } 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 { var body: some View {
ZStack { ZStack {
Color.appBackgroundPrimary.ignoresSafeArea() Color.appBackgroundPrimary.ignoresSafeArea()
@@ -154,7 +159,7 @@ struct DocumentsWarrantiesView: View {
.foregroundColor((selectedCategory != nil || selectedDocType != nil) ? Color.appPrimary : Color.appTextSecondary) .foregroundColor((selectedCategory != nil || selectedDocType != nil) ? Color.appPrimary : Color.appTextSecondary)
} }
// Add Button // Add Button (disabled when showing upgrade screen)
Button(action: { Button(action: {
// Check LIVE document count before adding // Check LIVE document count before adding
let currentCount = documentViewModel.documents.count let currentCount = documentViewModel.documents.count
@@ -166,8 +171,9 @@ struct DocumentsWarrantiesView: View {
}) { }) {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.font(.title2) .font(.title2)
.foregroundColor(Color.appPrimary) .foregroundColor(shouldShowUpgrade ? Color.appTextSecondary.opacity(0.5) : Color.appPrimary)
} }
.disabled(shouldShowUpgrade)
} }
} }
} }

View File

@@ -1,12 +1,18 @@
import SwiftUI import SwiftUI
import ComposeApp import ComposeApp
import StoreKit
struct UpgradeFeatureView: View { struct UpgradeFeatureView: View {
let triggerKey: String let triggerKey: String
let icon: 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 subscriptionCache = SubscriptionCacheWrapper.shared
@StateObject private var storeKit = StoreKitManager.shared
// Look up trigger data from cache // Look up trigger data from cache
private var triggerData: UpgradeTriggerData? { private var triggerData: UpgradeTriggerData? {
@@ -27,58 +33,185 @@ struct UpgradeFeatureView: View {
} }
var body: some View { var body: some View {
VStack(spacing: AppSpacing.xl) { ScrollView {
Spacer() VStack(spacing: AppSpacing.xl) {
// Icon
Image(systemName: "star.circle.fill")
.font(.system(size: 60))
.foregroundStyle(Color.appAccent.gradient)
.padding(.top, AppSpacing.xl)
// Feature Icon // Title
Image(systemName: icon) Text(title)
.font(.system(size: 80)) .font(.title2.weight(.bold))
.foregroundStyle(Color.appPrimary.gradient) .foregroundColor(Color.appTextPrimary)
.multilineTextAlignment(.center)
.padding(.horizontal)
// Title // Message
Text(title) Text(message)
.font(.title.weight(.bold)) .font(.body)
.foregroundColor(Color.appTextPrimary) .foregroundColor(Color.appTextSecondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, AppSpacing.xl) .padding(.horizontal)
// Description // Pro Features Preview - Dynamic content or fallback
Text(message) Group {
.font(.body) if let promoContent = triggerData?.promoHtml, !promoContent.isEmpty {
.foregroundColor(Color.appTextSecondary) PromoContentView(content: promoContent)
.multilineTextAlignment(.center) .padding()
.padding(.horizontal, AppSpacing.xl) } 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 // Subscription Products
Text("This feature is available with Pro") if storeKit.isLoading {
.font(.subheadline.weight(.medium)) ProgressView()
.foregroundColor(Color.appAccent) .tint(Color.appPrimary)
.padding(.horizontal, AppSpacing.md) .padding()
.padding(.vertical, AppSpacing.sm) } else if !storeKit.products.isEmpty {
.background(Color.appAccent.opacity(0.1)) VStack(spacing: AppSpacing.md) {
.cornerRadius(AppRadius.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 // Error Message
Button(action: { if let error = errorMessage {
showUpgradePrompt = true HStack {
}) { Image(systemName: "exclamationmark.triangle.fill")
Text(buttonText) .foregroundColor(Color.appError)
.font(.headline) Text(error)
.foregroundColor(Color.appTextOnPrimary) .font(.subheadline)
.frame(maxWidth: .infinity) .foregroundColor(Color.appError)
}
.padding() .padding()
.background(Color.appPrimary) .background(Color.appError.opacity(0.1))
.cornerRadius(AppRadius.md) .cornerRadius(AppRadius.md)
} .padding(.horizontal)
.padding(.horizontal, AppSpacing.xl) }
.padding(.top, AppSpacing.lg)
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) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.appBackgroundPrimary) .background(Color.appBackgroundPrimary)
.sheet(isPresented: $showUpgradePrompt) { .sheet(isPresented: $showFeatureComparison) {
UpgradePromptView(triggerKey: triggerKey, isPresented: $showUpgradePrompt) 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"
}
}
} }
} }
} }