Optimize subscription tier management and empty state logic
Changes: - Make currentTier a computed property from StoreKit instead of stored value - Initialize StoreKit at app launch and refresh on foreground for ready subscription status - Fix empty state logic: show empty state when limit > 0, upgrade prompt when limit = 0 - Add subscription status display in Profile screen (iOS/Android) - Add upgrade prompts to Residences and Tasks screens for free tier users - Improve SubscriptionHelper with better tier checking logic iOS: - SubscriptionCache: currentTier now computed from StoreKit.purchasedProductIDs - StoreKitManager: add refreshSubscriptionStatus() method - AppDelegate: initialize StoreKit at launch and refresh on app active - ContractorsListView: use shouldShowUpgradePrompt(currentCount:limitKey:) - WarrantiesTabContent/DocumentsTabContent: same empty state fix - ProfileTabView: display current tier and limitations status - ResidencesListView/ResidenceDetailView: add upgrade prompts for free users - AllTasksView: add upgrade prompt for free users Android: - ContractorsScreen/DocumentsScreen: fix empty state logic - ProfileScreen: display subscription status and limits - ResidencesScreen/ResidenceDetailScreen: add upgrade prompts - UpgradeFeatureScreen: improve UI layout Shared: - APILayer: add getSubscriptionStatus() method - SubscriptionHelper: add hasAccessToFeature() utility - Remove duplicate subscription checking logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,13 @@ import ComposeApp
|
||||
|
||||
struct ContractorsListView: View {
|
||||
@StateObject private var viewModel = ContractorViewModel()
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
@State private var searchText = ""
|
||||
@State private var showingAddSheet = false
|
||||
@State private var selectedSpecialty: String? = nil
|
||||
@State private var showFavoritesOnly = false
|
||||
@State private var showSpecialtyFilter = false
|
||||
@State private var showingUpgradePrompt = false
|
||||
|
||||
// Lookups from DataCache
|
||||
@State private var contractorSpecialties: [ContractorSpecialty] = []
|
||||
@@ -73,9 +75,18 @@ struct ContractorsListView: View {
|
||||
Spacer()
|
||||
} else if contractors.isEmpty {
|
||||
Spacer()
|
||||
EmptyContractorsView(
|
||||
hasFilters: showFavoritesOnly || selectedSpecialty != nil || !searchText.isEmpty
|
||||
)
|
||||
if !subscriptionCache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "contractors") {
|
||||
// User can add contractors (limit > 0) - show empty state
|
||||
EmptyContractorsView(
|
||||
hasFilters: showFavoritesOnly || selectedSpecialty != nil || !searchText.isEmpty
|
||||
)
|
||||
} else {
|
||||
// User is blocked (limit = 0) - show upgrade prompt
|
||||
UpgradeFeatureView(
|
||||
triggerKey: "view_contractors",
|
||||
icon: "person.2.fill"
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
} else {
|
||||
ScrollView {
|
||||
@@ -143,7 +154,15 @@ struct ContractorsListView: View {
|
||||
}
|
||||
|
||||
// Add Button
|
||||
Button(action: { showingAddSheet = true }) {
|
||||
Button(action: {
|
||||
// Check LIVE contractor count before adding
|
||||
let currentCount = viewModel.contractors.count
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "contractors") {
|
||||
showingUpgradePrompt = true
|
||||
} else {
|
||||
showingAddSheet = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(Color.appPrimary)
|
||||
@@ -161,6 +180,9 @@ struct ContractorsListView: View {
|
||||
)
|
||||
.presentationDetents([.large])
|
||||
}
|
||||
.sheet(isPresented: $showingUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "view_contractors", isPresented: $showingUpgradePrompt)
|
||||
}
|
||||
.onAppear {
|
||||
loadContractors()
|
||||
loadContractorSpecialties()
|
||||
|
||||
@@ -3,6 +3,7 @@ import ComposeApp
|
||||
|
||||
struct DocumentsTabContent: View {
|
||||
@ObservedObject var viewModel: DocumentViewModel
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
let searchText: String
|
||||
|
||||
var filteredDocuments: [Document] {
|
||||
@@ -27,11 +28,20 @@ struct DocumentsTabContent: View {
|
||||
Spacer()
|
||||
} else if filteredDocuments.isEmpty {
|
||||
Spacer()
|
||||
EmptyStateView(
|
||||
icon: "doc",
|
||||
title: "No documents found",
|
||||
message: "Add documents related to your residence"
|
||||
)
|
||||
if !subscriptionCache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "documents") {
|
||||
// User can add documents (limit > 0) - show empty state
|
||||
EmptyStateView(
|
||||
icon: "doc",
|
||||
title: "No documents found",
|
||||
message: "Add documents related to your residence"
|
||||
)
|
||||
} else {
|
||||
// User is blocked (limit = 0) - show upgrade prompt
|
||||
UpgradeFeatureView(
|
||||
triggerKey: "view_documents",
|
||||
icon: "doc.text.fill"
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
} else {
|
||||
ScrollView {
|
||||
|
||||
@@ -3,6 +3,7 @@ import ComposeApp
|
||||
|
||||
struct WarrantiesTabContent: View {
|
||||
@ObservedObject var viewModel: DocumentViewModel
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
let searchText: String
|
||||
|
||||
var filteredWarranties: [Document] {
|
||||
@@ -29,11 +30,20 @@ struct WarrantiesTabContent: View {
|
||||
Spacer()
|
||||
} else if filteredWarranties.isEmpty {
|
||||
Spacer()
|
||||
EmptyStateView(
|
||||
icon: "doc.text.viewfinder",
|
||||
title: "No warranties found",
|
||||
message: "Add warranties to track coverage periods"
|
||||
)
|
||||
if !subscriptionCache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "documents") {
|
||||
// User can add documents (limit > 0) - show empty state
|
||||
EmptyStateView(
|
||||
icon: "doc.text.viewfinder",
|
||||
title: "No warranties found",
|
||||
message: "Add warranties to track coverage periods"
|
||||
)
|
||||
} else {
|
||||
// User is blocked (limit = 0) - show upgrade prompt
|
||||
UpgradeFeatureView(
|
||||
triggerKey: "view_documents",
|
||||
icon: "doc.text.fill"
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
} else {
|
||||
ScrollView {
|
||||
|
||||
@@ -8,6 +8,7 @@ enum DocumentWarrantyTab {
|
||||
|
||||
struct DocumentsWarrantiesView: View {
|
||||
@StateObject private var documentViewModel = DocumentViewModel()
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
@State private var selectedTab: DocumentWarrantyTab = .warranties
|
||||
@State private var searchText = ""
|
||||
@State private var selectedCategory: String? = nil
|
||||
@@ -15,6 +16,7 @@ struct DocumentsWarrantiesView: View {
|
||||
@State private var showActiveOnly = true
|
||||
@State private var showFilterMenu = false
|
||||
@State private var showAddSheet = false
|
||||
@State private var showingUpgradePrompt = false
|
||||
|
||||
let residenceId: Int32?
|
||||
|
||||
@@ -154,7 +156,13 @@ struct DocumentsWarrantiesView: View {
|
||||
|
||||
// Add Button
|
||||
Button(action: {
|
||||
showAddSheet = true
|
||||
// Check LIVE document count before adding
|
||||
let currentCount = documentViewModel.documents.count
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "documents") {
|
||||
showingUpgradePrompt = true
|
||||
} else {
|
||||
showAddSheet = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title2)
|
||||
@@ -182,6 +190,9 @@ struct DocumentsWarrantiesView: View {
|
||||
documentViewModel: documentViewModel
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showingUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "view_documents", isPresented: $showingUpgradePrompt)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadWarranties() {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct ProfileTabView: View {
|
||||
@EnvironmentObject private var themeManager: ThemeManager
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
@StateObject private var storeKitManager = StoreKitManager.shared
|
||||
@State private var showingProfileEdit = false
|
||||
@State private var showingLogoutAlert = false
|
||||
@State private var showingThemeSelection = false
|
||||
@State private var showUpgradePrompt = false
|
||||
@State private var showRestoreSuccess = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
@@ -47,6 +52,61 @@ struct ProfileTabView: View {
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Subscription Section - Only show if limitations are enabled on backend
|
||||
if let subscription = subscriptionCache.currentSubscription, subscription.limitationsEnabled {
|
||||
Section("Subscription") {
|
||||
HStack {
|
||||
Image(systemName: "crown.fill")
|
||||
.foregroundColor(subscriptionCache.currentTier == "pro" ? Color.appAccent : Color.appTextSecondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(subscriptionCache.currentTier == "pro" ? "Pro Plan" : "Free Plan")
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
if subscriptionCache.currentTier == "pro",
|
||||
let expiresAt = subscription.expiresAt {
|
||||
Text("Active until \(formatDate(expiresAt))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
} else {
|
||||
Text("Limited features")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
if subscriptionCache.currentTier != "pro" {
|
||||
Button(action: { showUpgradePrompt = true }) {
|
||||
Label("Upgrade to Pro", systemImage: "arrow.up.circle.fill")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Label("Manage Subscription", systemImage: "gearshape.fill")
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
Task {
|
||||
await storeKitManager.restorePurchases()
|
||||
showRestoreSuccess = true
|
||||
}
|
||||
}) {
|
||||
Label("Restore Purchases", systemImage: "arrow.clockwise")
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
|
||||
Section("Appearance") {
|
||||
Button(action: {
|
||||
showingThemeSelection = true
|
||||
@@ -111,5 +171,23 @@ struct ProfileTabView: View {
|
||||
} message: {
|
||||
Text("Are you sure you want to log out?")
|
||||
}
|
||||
.sheet(isPresented: $showUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "user_profile", isPresented: $showUpgradePrompt)
|
||||
}
|
||||
.alert("Purchases Restored", isPresented: $showRestoreSuccess) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("Your purchases have been restored successfully.")
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDate(_ dateString: String) -> String {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
if let date = formatter.date(from: dateString) {
|
||||
let displayFormatter = DateFormatter()
|
||||
displayFormatter.dateStyle = .medium
|
||||
return displayFormatter.string(from: date)
|
||||
}
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
PushNotificationManager.shared.clearBadge()
|
||||
}
|
||||
|
||||
// Initialize StoreKit and check for existing subscriptions
|
||||
// This ensures we have the user's subscription status ready before they interact
|
||||
Task {
|
||||
_ = StoreKitManager.shared
|
||||
print("✅ StoreKit initialized at app launch")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -31,6 +38,12 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
Task { @MainActor in
|
||||
PushNotificationManager.shared.clearBadge()
|
||||
}
|
||||
|
||||
// Refresh StoreKit subscription status when app comes to foreground
|
||||
// This ensures we have the latest subscription state if it changed while app was in background
|
||||
Task {
|
||||
await StoreKitManager.shared.refreshSubscriptionStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Remote Notifications
|
||||
|
||||
@@ -25,7 +25,9 @@ struct ResidenceDetailView: View {
|
||||
@State private var showReportConfirmation = false
|
||||
@State private var showDeleteConfirmation = false
|
||||
@State private var isDeleting = false
|
||||
|
||||
@State private var showingUpgradePrompt = false
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
@@ -120,6 +122,9 @@ struct ResidenceDetailView: View {
|
||||
Text("Are you sure you want to archive \"\(task.title)\"? You can unarchive it later from archived tasks.")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "add_11th_task", isPresented: $showingUpgradePrompt)
|
||||
}
|
||||
|
||||
// MARK: onChange & lifecycle
|
||||
.onChange(of: viewModel.reportMessage) { message in
|
||||
@@ -255,7 +260,13 @@ private extension ResidenceDetailView {
|
||||
}
|
||||
|
||||
Button {
|
||||
showAddTask = true
|
||||
// Check LIVE task count before adding
|
||||
let totalTasks = tasksResponse?.columns.reduce(0) { $0 + $1.tasks.count } ?? 0
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTasks, limitKey: "tasks") {
|
||||
showingUpgradePrompt = true
|
||||
} else {
|
||||
showAddTask = true
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ struct ResidencesListView: View {
|
||||
@StateObject private var viewModel = ResidenceViewModel()
|
||||
@State private var showingAddResidence = false
|
||||
@State private var showingJoinResidence = false
|
||||
@State private var showingUpgradePrompt = false
|
||||
@StateObject private var authManager = AuthenticationManager.shared
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
|
||||
|
||||
var body: some View {
|
||||
@@ -71,7 +73,13 @@ struct ResidencesListView: View {
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
showingJoinResidence = true
|
||||
// Check if we should show upgrade prompt before joining
|
||||
let currentCount = viewModel.myResidences?.residences.count ?? 0
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "properties") {
|
||||
showingUpgradePrompt = true
|
||||
} else {
|
||||
showingJoinResidence = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "person.badge.plus")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
@@ -79,7 +87,13 @@ struct ResidencesListView: View {
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showingAddResidence = true
|
||||
// Check if we should show upgrade prompt before adding
|
||||
let currentCount = viewModel.myResidences?.residences.count ?? 0
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "properties") {
|
||||
showingUpgradePrompt = true
|
||||
} else {
|
||||
showingAddResidence = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
@@ -101,6 +115,9 @@ struct ResidencesListView: View {
|
||||
viewModel.loadMyResidences()
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $showingUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "add_second_property", isPresented: $showingUpgradePrompt)
|
||||
}
|
||||
.onAppear {
|
||||
if authManager.isAuthenticated {
|
||||
viewModel.loadMyResidences()
|
||||
|
||||
@@ -128,6 +128,13 @@ class StoreKitManager: ObservableObject {
|
||||
return false
|
||||
}
|
||||
|
||||
/// Refresh subscription status from StoreKit
|
||||
/// Call this when app comes to foreground to ensure we have latest status
|
||||
func refreshSubscriptionStatus() async {
|
||||
await updatePurchasedProducts()
|
||||
print("🔄 StoreKit: Subscription status refreshed")
|
||||
}
|
||||
|
||||
/// Update purchased product IDs
|
||||
@MainActor
|
||||
private func updatePurchasedProducts() async {
|
||||
|
||||
@@ -4,24 +4,104 @@ import ComposeApp
|
||||
/// Swift wrapper for accessing Kotlin SubscriptionCache
|
||||
class SubscriptionCacheWrapper: ObservableObject {
|
||||
static let shared = SubscriptionCacheWrapper()
|
||||
|
||||
|
||||
@Published var currentSubscription: SubscriptionStatus?
|
||||
@Published var upgradeTriggers: [String: UpgradeTriggerData] = [:]
|
||||
@Published var featureBenefits: [FeatureBenefit] = []
|
||||
@Published var promotions: [Promotion] = []
|
||||
|
||||
|
||||
/// Current tier based on StoreKit purchases
|
||||
var currentTier: String {
|
||||
// Check if user has any active subscriptions via StoreKit
|
||||
return StoreKitManager.shared.purchasedProductIDs.isEmpty ? "free" : "pro"
|
||||
}
|
||||
|
||||
/// Check if user should be blocked from adding an item based on LIVE count
|
||||
/// - Parameters:
|
||||
/// - currentCount: The actual current count from the data (e.g., viewModel.residences.count)
|
||||
/// - limitKey: The key to check ("properties", "tasks", "contractors", or "documents")
|
||||
/// - Returns: true if should show upgrade prompt (blocked), false if allowed
|
||||
func shouldShowUpgradePrompt(currentCount: Int, limitKey: String) -> Bool {
|
||||
// If limitations are disabled globally, never block
|
||||
guard let subscription = currentSubscription, subscription.limitationsEnabled else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Pro tier never gets blocked
|
||||
if currentTier == "pro" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the appropriate limits for the current tier from StoreKit
|
||||
guard let tierLimits = subscription.limits[currentTier] else {
|
||||
print("⚠️ No limits found for tier: \(currentTier)")
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the specific limit for this resource type
|
||||
let limit: Int?
|
||||
switch limitKey {
|
||||
case "properties":
|
||||
limit = tierLimits.properties != nil ? Int(truncating: tierLimits.properties!) : nil
|
||||
case "tasks":
|
||||
limit = tierLimits.tasks != nil ? Int(truncating: tierLimits.tasks!) : nil
|
||||
case "contractors":
|
||||
limit = tierLimits.contractors != nil ? Int(truncating: tierLimits.contractors!) : nil
|
||||
case "documents":
|
||||
limit = tierLimits.documents != nil ? Int(truncating: tierLimits.documents!) : nil
|
||||
default:
|
||||
print("⚠️ Unknown limit key: \(limitKey)")
|
||||
return false
|
||||
}
|
||||
|
||||
// nil limit means unlimited
|
||||
guard let actualLimit = limit else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Block if current count >= actualLimit
|
||||
return currentCount >= Int(actualLimit)
|
||||
}
|
||||
|
||||
/// Deprecated: Use shouldShowUpgradePrompt(currentCount:limitKey:) instead
|
||||
var shouldShowUpgradePrompt: Bool {
|
||||
currentTier == "free" && (currentSubscription?.limitationsEnabled ?? false)
|
||||
}
|
||||
|
||||
private init() {
|
||||
// Initialize with current values from Kotlin cache
|
||||
Task {
|
||||
await observeSubscriptionStatus()
|
||||
await observeUpgradeTriggers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func observeSubscriptionStatus() {
|
||||
// Update from Kotlin cache
|
||||
if let subscription = ComposeApp.SubscriptionCache.shared.currentSubscription.value as? SubscriptionStatus {
|
||||
self.currentSubscription = subscription
|
||||
print("📊 Subscription Status: currentTier=\(currentTier), limitationsEnabled=\(subscription.limitationsEnabled)")
|
||||
print(" 📊 Free Tier Limits - Properties: \(subscription.limits["free"]?.properties), Tasks: \(subscription.limits["free"]?.tasks), Contractors: \(subscription.limits["free"]?.contractors), Documents: \(subscription.limits["free"]?.documents)")
|
||||
print(" 📊 Pro Tier Limits - Properties: \(subscription.limits["pro"]?.properties), Tasks: \(subscription.limits["pro"]?.tasks), Contractors: \(subscription.limits["pro"]?.contractors), Documents: \(subscription.limits["pro"]?.documents)")
|
||||
} else {
|
||||
print("⚠️ No subscription status in cache")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func observeUpgradeTriggers() {
|
||||
// Update from Kotlin cache
|
||||
let kotlinTriggers = ComposeApp.SubscriptionCache.shared.upgradeTriggers.value as? [String: UpgradeTriggerData]
|
||||
if let triggers = kotlinTriggers {
|
||||
self.upgradeTriggers = triggers
|
||||
}
|
||||
}
|
||||
|
||||
func refreshFromCache() {
|
||||
Task {
|
||||
await observeSubscriptionStatus()
|
||||
await observeUpgradeTriggers()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +122,4 @@ class SubscriptionCacheWrapper: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
import ComposeApp
|
||||
|
||||
/// Swift wrapper for Kotlin SubscriptionHelper
|
||||
class SubscriptionHelper {
|
||||
static func canAddProperty() -> (allowed: Bool, triggerKey: String?) {
|
||||
let result = ComposeApp.SubscriptionHelper.shared.canAddProperty()
|
||||
return (result.allowed, result.triggerKey)
|
||||
}
|
||||
|
||||
static func canAddTask() -> (allowed: Bool, triggerKey: String?) {
|
||||
let result = ComposeApp.SubscriptionHelper.shared.canAddTask()
|
||||
return (result.allowed, result.triggerKey)
|
||||
}
|
||||
|
||||
static func shouldShowUpgradePromptForContractors() -> (showPrompt: Bool, triggerKey: String?) {
|
||||
let result = ComposeApp.SubscriptionHelper.shared.shouldShowUpgradePromptForContractors()
|
||||
return (result.allowed, result.triggerKey)
|
||||
}
|
||||
|
||||
static func shouldShowUpgradePromptForDocuments() -> (showPrompt: Bool, triggerKey: String?) {
|
||||
let result = ComposeApp.SubscriptionHelper.shared.shouldShowUpgradePromptForDocuments()
|
||||
return (result.allowed, result.triggerKey)
|
||||
}
|
||||
}
|
||||
@@ -3,34 +3,52 @@ import ComposeApp
|
||||
|
||||
struct UpgradeFeatureView: View {
|
||||
let triggerKey: String
|
||||
let featureName: String
|
||||
let featureDescription: String
|
||||
let icon: String
|
||||
|
||||
|
||||
@State private var showUpgradePrompt = false
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
|
||||
|
||||
// Look up trigger data from cache
|
||||
private var triggerData: UpgradeTriggerData? {
|
||||
subscriptionCache.upgradeTriggers[triggerKey]
|
||||
}
|
||||
|
||||
// Fallback values if trigger not found
|
||||
private var title: String {
|
||||
triggerData?.title ?? "Upgrade Required"
|
||||
}
|
||||
|
||||
private var message: String {
|
||||
triggerData?.message ?? "This feature is available with a Pro subscription."
|
||||
}
|
||||
|
||||
private var buttonText: String {
|
||||
triggerData?.buttonText ?? "Upgrade to Pro"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: AppSpacing.xl) {
|
||||
Spacer()
|
||||
|
||||
|
||||
// Feature Icon
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 80))
|
||||
.foregroundStyle(Color.appPrimary.gradient)
|
||||
|
||||
|
||||
// Title
|
||||
Text(featureName)
|
||||
Text(title)
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, AppSpacing.xl)
|
||||
|
||||
// Description
|
||||
Text(featureDescription)
|
||||
Text(message)
|
||||
.font(.body)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, AppSpacing.xl)
|
||||
|
||||
|
||||
// Upgrade Message
|
||||
Text("This feature is available with Pro")
|
||||
.font(.subheadline.weight(.medium))
|
||||
@@ -39,12 +57,12 @@ struct UpgradeFeatureView: View {
|
||||
.padding(.vertical, AppSpacing.sm)
|
||||
.background(Color.appAccent.opacity(0.1))
|
||||
.cornerRadius(AppRadius.md)
|
||||
|
||||
|
||||
// Upgrade Button
|
||||
Button(action: {
|
||||
showUpgradePrompt = true
|
||||
}) {
|
||||
Text("Upgrade to Pro")
|
||||
Text(buttonText)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextOnPrimary)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -54,7 +72,7 @@ struct UpgradeFeatureView: View {
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.xl)
|
||||
.padding(.top, AppSpacing.lg)
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
@@ -68,8 +86,6 @@ struct UpgradeFeatureView: View {
|
||||
#Preview {
|
||||
UpgradeFeatureView(
|
||||
triggerKey: "view_contractors",
|
||||
featureName: "Contractors",
|
||||
featureDescription: "Track and manage all your contractors in one place",
|
||||
icon: "person.2.fill"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,20 +4,28 @@ import ComposeApp
|
||||
struct AllTasksView: View {
|
||||
@StateObject private var taskViewModel = TaskViewModel()
|
||||
@StateObject private var residenceViewModel = ResidenceViewModel()
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
@State private var tasksResponse: TaskColumnsResponse?
|
||||
@State private var isLoadingTasks = false
|
||||
@State private var tasksError: String?
|
||||
@State private var showAddTask = false
|
||||
@State private var showEditTask = false
|
||||
@State private var showingUpgradePrompt = false
|
||||
@State private var selectedTaskForEdit: TaskDetail?
|
||||
@State private var selectedTaskForComplete: TaskDetail?
|
||||
|
||||
|
||||
@State private var selectedTaskForArchive: TaskDetail?
|
||||
@State private var showArchiveConfirmation = false
|
||||
|
||||
|
||||
@State private var selectedTaskForCancel: TaskDetail?
|
||||
@State private var showCancelConfirmation = false
|
||||
|
||||
// Count total tasks across all columns
|
||||
private var totalTaskCount: Int {
|
||||
guard let response = tasksResponse else { return 0 }
|
||||
return response.columns.reduce(0) { $0 + $1.tasks.count }
|
||||
}
|
||||
|
||||
private var hasNoTasks: Bool {
|
||||
guard let response = tasksResponse else { return true }
|
||||
return response.columns.allSatisfy { $0.tasks.isEmpty }
|
||||
@@ -46,6 +54,9 @@ struct AllTasksView: View {
|
||||
loadAllTasks()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "add_11th_task", isPresented: $showingUpgradePrompt)
|
||||
}
|
||||
.alert("Archive Task", isPresented: $showArchiveConfirmation) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
selectedTaskForArchive = nil
|
||||
@@ -129,7 +140,12 @@ struct AllTasksView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Button(action: {
|
||||
showAddTask = true
|
||||
// Check if we should show upgrade prompt before adding
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTaskCount, limitKey: "tasks") {
|
||||
showingUpgradePrompt = true
|
||||
} else {
|
||||
showAddTask = true
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "plus")
|
||||
@@ -224,7 +240,12 @@ struct AllTasksView: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
showAddTask = true
|
||||
// Check if we should show upgrade prompt before adding
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTaskCount, limitKey: "tasks") {
|
||||
showingUpgradePrompt = true
|
||||
} else {
|
||||
showAddTask = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user