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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user