- Add deep link navigation from push notifications to specific task column on kanban board - Fix subscription check in push notification handler to allow navigation when limitations disabled - Add pendingNavigationTaskId to handle notifications when app isn't ready - Add ScrollViewReader to AllTasksView for programmatic scrolling to task column - Add canShareResidence() and canShareContractor() subscription checks (iOS & Android) - Add test APNS file for simulator push notification testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
185 lines
7.3 KiB
Swift
185 lines
7.3 KiB
Swift
import SwiftUI
|
|
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)
|
|
}
|
|
|
|
/// Check if user can share residences (Pro feature)
|
|
/// - Returns: true if allowed, false if should show upgrade prompt
|
|
func canShareResidence() -> Bool {
|
|
// If limitations are disabled globally, allow
|
|
guard let subscription = currentSubscription, subscription.limitationsEnabled else {
|
|
return true
|
|
}
|
|
// Pro tier can share
|
|
return currentTier == "pro"
|
|
}
|
|
|
|
/// Check if user can share contractors (Pro feature)
|
|
/// - Returns: true if allowed, false if should show upgrade prompt
|
|
func canShareContractor() -> Bool {
|
|
// If limitations are disabled globally, allow
|
|
guard let subscription = currentSubscription, subscription.limitationsEnabled else {
|
|
return true
|
|
}
|
|
// Pro tier can share
|
|
return currentTier == "pro"
|
|
}
|
|
|
|
private init() {
|
|
// Start observation of Kotlin cache
|
|
Task { @MainActor in
|
|
// Initial sync
|
|
self.observeSubscriptionStatusSync()
|
|
self.observeUpgradeTriggersSync()
|
|
|
|
// Poll for updates periodically (workaround for Kotlin StateFlow observation)
|
|
while true {
|
|
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
|
self.observeSubscriptionStatusSync()
|
|
self.observeUpgradeTriggersSync()
|
|
}
|
|
}
|
|
}
|
|
|
|
@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 { @MainActor in
|
|
observeSubscriptionStatusSync()
|
|
observeUpgradeTriggersSync()
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func observeSubscriptionStatusSync() {
|
|
if let subscription = ComposeApp.SubscriptionCache.shared.currentSubscription.value as? SubscriptionStatus {
|
|
self.currentSubscription = subscription
|
|
// Sync subscription status with widget
|
|
syncWidgetSubscriptionStatus(subscription: subscription)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func observeUpgradeTriggersSync() {
|
|
let kotlinTriggers = ComposeApp.SubscriptionCache.shared.upgradeTriggers.value as? [String: UpgradeTriggerData]
|
|
if let triggers = kotlinTriggers {
|
|
self.upgradeTriggers = triggers
|
|
}
|
|
}
|
|
|
|
func updateSubscription(_ subscription: SubscriptionStatus) {
|
|
ComposeApp.SubscriptionCache.shared.updateSubscriptionStatus(subscription: subscription)
|
|
DispatchQueue.main.async {
|
|
self.currentSubscription = subscription
|
|
// Sync subscription status with widget
|
|
self.syncWidgetSubscriptionStatus(subscription: subscription)
|
|
}
|
|
}
|
|
|
|
/// Sync subscription status with widget extension
|
|
private func syncWidgetSubscriptionStatus(subscription: SubscriptionStatus) {
|
|
let limitationsEnabled = subscription.limitationsEnabled
|
|
let isPremium = currentTier == "pro"
|
|
WidgetDataManager.shared.saveSubscriptionStatus(
|
|
limitationsEnabled: limitationsEnabled,
|
|
isPremium: isPremium
|
|
)
|
|
}
|
|
|
|
func clear() {
|
|
ComposeApp.SubscriptionCache.shared.clear()
|
|
DispatchQueue.main.async {
|
|
self.currentSubscription = nil
|
|
self.upgradeTriggers = [:]
|
|
self.featureBenefits = []
|
|
self.promotions = []
|
|
}
|
|
}
|
|
}
|
|
|