Add task completion animations, subscription trials, and quiet debug console

- Completion animations: play user-selected animation on task card after completing,
  with DataManager guard to prevent race condition during animation playback.
  Works in both AllTasksView and ResidenceDetailView. Animation preference
  persisted via @AppStorage and configurable from Settings.
- Subscription: add trial fields (trialStart, trialEnd, trialActive) and
  subscriptionSource to model, cross-platform purchase guard, trial banner
  in upgrade prompt, and platform-aware subscription management in profile.
- Analytics: disable PostHog SDK debug logging and remove console print
  statements to reduce debug console noise.
- Documents: remove redundant nested do-catch blocks in ViewModel wrapper.
- Widgets: add debounced timeline reloads and thread-safe file I/O queue.
- Onboarding: fix animation leak on disappear, remove unused state vars.
- Remove unused files (ContentView, StateFlowExtensions, CustomView).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-05 11:35:08 -06:00
parent c5f2bee83f
commit 98dbacdea0
73 changed files with 1770 additions and 529 deletions

View File

@@ -5,12 +5,18 @@ import kotlinx.serialization.Serializable
@Serializable
data class SubscriptionStatus(
val tier: String = "free",
@SerialName("is_active") val isActive: Boolean = false,
@SerialName("subscribed_at") val subscribedAt: String? = null,
@SerialName("expires_at") val expiresAt: String? = null,
@SerialName("auto_renew") val autoRenew: Boolean = true,
val usage: UsageStats,
val limits: Map<String, TierLimits>, // {"free": {...}, "pro": {...}}
@SerialName("limitations_enabled") val limitationsEnabled: Boolean = false // Master toggle
@SerialName("limitations_enabled") val limitationsEnabled: Boolean = false, // Master toggle
@SerialName("trial_start") val trialStart: String? = null,
@SerialName("trial_end") val trialEnd: String? = null,
@SerialName("trial_active") val trialActive: Boolean = false,
@SerialName("subscription_source") val subscriptionSource: String? = null,
)
@Serializable

View File

@@ -45,12 +45,13 @@ object SubscriptionHelper {
/**
* Derive the current subscription tier from DataManager.
* "pro" if the backend subscription has a non-empty expiresAt (active paid plan),
* "pro" if the backend subscription has an active trial or a non-empty expiresAt (active paid plan),
* "free" otherwise.
*/
val currentTier: String
get() {
val subscription = DataManager.subscription.value ?: return "free"
if (subscription.trialActive) return "pro"
val expiresAt = subscription.expiresAt
return if (!expiresAt.isNullOrEmpty()) "pro" else "free"
}
@@ -68,6 +69,25 @@ object SubscriptionHelper {
return currentTier == "pro"
}
/**
* Check if the user can purchase a subscription on the given platform.
* If the user already has an active Pro subscription from a different platform,
* purchasing on this platform is not allowed.
*
* @param currentPlatform The platform attempting the purchase ("ios", "android", or "stripe")
* @return UsageCheck with allowed=false if already subscribed on another platform
*/
fun canPurchaseOnPlatform(currentPlatform: String): UsageCheck {
val subscription = DataManager.subscription.value
?: return UsageCheck(allowed = true, triggerKey = null)
if (currentTier != "pro") return UsageCheck(allowed = true, triggerKey = null)
val source = subscription.subscriptionSource
if (source != null && source != currentPlatform) {
return UsageCheck(allowed = false, triggerKey = "already_subscribed_other_platform")
}
return UsageCheck(allowed = true, triggerKey = null)
}
// ===== PROPERTY (RESIDENCE) =====
/**