Files
honeyDueKMP/iosApp/iosApp/Subscription/SubscriptionCache.swift
Trey t cbe073aa21 Add push notification deep linking and sharing subscription checks
- 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>
2025-12-10 23:17:28 -06:00

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 = []
}
}
}