Files
honeyDueKMP/composeApp/src/commonMain/kotlin/com/example/casera/utils/SubscriptionHelper.kt
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

322 lines
9.9 KiB
Kotlin

package com.example.casera.utils
import com.example.casera.cache.SubscriptionCache
/**
* Helper for checking subscription limits and determining when to show upgrade prompts.
*
* RULES:
* 1. Backend limitations OFF: Never show upgrade view, allow everything
* 2. Backend limitations ON + limit=0: Show upgrade view, block access entirely (no add button)
* 3. Backend limitations ON + limit>0: Allow access with add button, show upgrade when limit reached
*
* These rules apply to: residence, task, contractors, documents
*/
object SubscriptionHelper {
/**
* Result of a usage/access check
* @param allowed Whether the action is allowed
* @param triggerKey The upgrade trigger key to use if not allowed (null if allowed)
*/
data class UsageCheck(val allowed: Boolean, val triggerKey: String?)
// NOTE: For Android, currentTier should be set from Google Play Billing
// For iOS, tier is managed by SubscriptionCacheWrapper from StoreKit
var currentTier: String = "free"
// ===== PROPERTY (RESIDENCE) =====
/**
* Check if the user should see an upgrade view instead of the residences screen.
* Returns true (blocked) only when limitations are ON and limit=0.
*/
fun isResidencesBlocked(): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(false, null) // Allow access while loading
if (!subscription.limitationsEnabled) {
return UsageCheck(false, null) // Limitations disabled, never block
}
if (currentTier == "pro") {
return UsageCheck(false, null) // Pro users never blocked
}
val limit = subscription.limits[currentTier]?.properties
// If limit is 0, block access entirely
if (limit == 0) {
return UsageCheck(true, "add_second_property")
}
return UsageCheck(false, null) // limit > 0 or unlimited, allow access
}
/**
* Check if user can add a property (when trying to add, not for blocking the screen).
* Used when limit > 0 and user has reached the limit.
*/
fun canAddProperty(currentCount: Int = 0): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(true, null) // Allow if no subscription data
if (!subscription.limitationsEnabled) {
return UsageCheck(true, null) // Limitations disabled, allow everything
}
if (currentTier == "pro") {
return UsageCheck(true, null) // Pro tier gets unlimited access
}
// Get limit for current tier (null = unlimited)
val limit = subscription.limits[currentTier]?.properties
// null means unlimited
if (limit == null) {
return UsageCheck(true, null)
}
// If limit is 0, they shouldn't even be here (screen should be blocked)
// But if they somehow are, block the add
if (limit == 0) {
return UsageCheck(false, "add_second_property")
}
// limit > 0: check if they've reached it
if (currentCount >= limit) {
return UsageCheck(false, "add_second_property")
}
return UsageCheck(true, null)
}
// ===== TASKS =====
/**
* Check if the user should see an upgrade view instead of the tasks screen.
* Returns true (blocked) only when limitations are ON and limit=0.
*/
fun isTasksBlocked(): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(false, null) // Allow access while loading
if (!subscription.limitationsEnabled) {
return UsageCheck(false, null)
}
if (currentTier == "pro") {
return UsageCheck(false, null)
}
val limit = subscription.limits[currentTier]?.tasks
if (limit == 0) {
return UsageCheck(true, "add_11th_task")
}
return UsageCheck(false, null)
}
/**
* Check if user can add a task (when trying to add).
*/
fun canAddTask(currentCount: Int = 0): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(true, null)
if (!subscription.limitationsEnabled) {
return UsageCheck(true, null)
}
if (currentTier == "pro") {
return UsageCheck(true, null)
}
val limit = subscription.limits[currentTier]?.tasks
if (limit == null) {
return UsageCheck(true, null) // Unlimited
}
if (limit == 0) {
return UsageCheck(false, "add_11th_task")
}
if (currentCount >= limit) {
return UsageCheck(false, "add_11th_task")
}
return UsageCheck(true, null)
}
// ===== CONTRACTORS =====
/**
* Check if the user should see an upgrade view instead of the contractors screen.
* Returns true (blocked) only when limitations are ON and limit=0.
*/
fun isContractorsBlocked(): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(false, null) // Allow access while loading
if (!subscription.limitationsEnabled) {
return UsageCheck(false, null)
}
if (currentTier == "pro") {
return UsageCheck(false, null)
}
val limit = subscription.limits[currentTier]?.contractors
if (limit == 0) {
return UsageCheck(true, "view_contractors")
}
return UsageCheck(false, null)
}
/**
* Check if user can add a contractor (when trying to add).
*/
fun canAddContractor(currentCount: Int = 0): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(true, null)
if (!subscription.limitationsEnabled) {
return UsageCheck(true, null)
}
if (currentTier == "pro") {
return UsageCheck(true, null)
}
val limit = subscription.limits[currentTier]?.contractors
if (limit == null) {
return UsageCheck(true, null)
}
if (limit == 0) {
return UsageCheck(false, "view_contractors")
}
if (currentCount >= limit) {
return UsageCheck(false, "view_contractors")
}
return UsageCheck(true, null)
}
// ===== DOCUMENTS =====
/**
* Check if the user should see an upgrade view instead of the documents screen.
* Returns true (blocked) only when limitations are ON and limit=0.
*/
fun isDocumentsBlocked(): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(false, null) // Allow access while loading
if (!subscription.limitationsEnabled) {
return UsageCheck(false, null)
}
if (currentTier == "pro") {
return UsageCheck(false, null)
}
val limit = subscription.limits[currentTier]?.documents
if (limit == 0) {
return UsageCheck(true, "view_documents")
}
return UsageCheck(false, null)
}
/**
* Check if user can add a document (when trying to add).
*/
fun canAddDocument(currentCount: Int = 0): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(true, null)
if (!subscription.limitationsEnabled) {
return UsageCheck(true, null)
}
if (currentTier == "pro") {
return UsageCheck(true, null)
}
val limit = subscription.limits[currentTier]?.documents
if (limit == null) {
return UsageCheck(true, null)
}
if (limit == 0) {
return UsageCheck(false, "view_documents")
}
if (currentCount >= limit) {
return UsageCheck(false, "view_documents")
}
return UsageCheck(true, null)
}
// ===== RESIDENCE SHARING =====
/**
* Check if user can share a residence (Pro feature).
* Returns true (blocked) when limitations are ON and user is not pro.
*/
fun canShareResidence(): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(true, null) // Allow if no subscription data (fallback)
if (!subscription.limitationsEnabled) {
return UsageCheck(true, null) // Limitations disabled, allow everything
}
if (currentTier == "pro") {
return UsageCheck(true, null) // Pro tier can share
}
// Free users cannot share residences
return UsageCheck(false, "share_residence")
}
// ===== CONTRACTOR SHARING =====
/**
* Check if user can share a contractor (Pro feature).
* Returns true (blocked) when limitations are ON and user is not pro.
*/
fun canShareContractor(): UsageCheck {
val subscription = SubscriptionCache.currentSubscription.value
?: return UsageCheck(true, null) // Allow if no subscription data (fallback)
if (!subscription.limitationsEnabled) {
return UsageCheck(true, null) // Limitations disabled, allow everything
}
if (currentTier == "pro") {
return UsageCheck(true, null) // Pro tier can share
}
// Free users cannot share contractors
return UsageCheck(false, "share_contractor")
}
// ===== DEPRECATED - Keep for backwards compatibility =====
@Deprecated("Use isContractorsBlocked() instead", ReplaceWith("isContractorsBlocked()"))
fun shouldShowUpgradePromptForContractors(): UsageCheck = isContractorsBlocked()
@Deprecated("Use isDocumentsBlocked() instead", ReplaceWith("isDocumentsBlocked()"))
fun shouldShowUpgradePromptForDocuments(): UsageCheck = isDocumentsBlocked()
}