diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/SubscriptionCache.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/SubscriptionCache.kt new file mode 100644 index 0000000..751cfca --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/SubscriptionCache.kt @@ -0,0 +1,37 @@ +package com.example.mycrib.cache + +import androidx.compose.runtime.mutableStateOf +import com.example.mycrib.models.FeatureBenefit +import com.example.mycrib.models.Promotion +import com.example.mycrib.models.SubscriptionStatus +import com.example.mycrib.models.UpgradeTriggerData + +object SubscriptionCache { + val currentSubscription = mutableStateOf(null) + val upgradeTriggers = mutableStateOf>(emptyMap()) + val featureBenefits = mutableStateOf>(emptyList()) + val promotions = mutableStateOf>(emptyList()) + + fun updateSubscriptionStatus(subscription: SubscriptionStatus) { + currentSubscription.value = subscription + } + + fun updateUpgradeTriggers(triggers: Map) { + upgradeTriggers.value = triggers + } + + fun updateFeatureBenefits(benefits: List) { + featureBenefits.value = benefits + } + + fun updatePromotions(promos: List) { + promotions.value = promos + } + + fun clear() { + currentSubscription.value = null + upgradeTriggers.value = emptyMap() + featureBenefits.value = emptyList() + promotions.value = emptyList() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Subscription.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Subscription.kt new file mode 100644 index 0000000..e1ec4ec --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Subscription.kt @@ -0,0 +1,71 @@ +package com.example.mycrib.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SubscriptionStatus( + val tier: String, // "free" or "pro" + @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: TierLimits, + @SerialName("limitations_enabled") val limitationsEnabled: Boolean = false // Master toggle +) + +@Serializable +data class UsageStats( + @SerialName("properties_count") val propertiesCount: Int, + @SerialName("tasks_count") val tasksCount: Int, + @SerialName("contractors_count") val contractorsCount: Int, + @SerialName("documents_count") val documentsCount: Int +) + +@Serializable +data class TierLimits( + val properties: Int? = null, // null = unlimited + val tasks: Int? = null, + val contractors: Int? = null, + val documents: Int? = null +) + +@Serializable +data class UpgradeTriggerData( + val title: String, + val message: String, + @SerialName("button_text") val buttonText: String +) + +@Serializable +data class FeatureBenefit( + @SerialName("feature_name") val featureName: String, + @SerialName("free_tier") val freeTier: String, + @SerialName("pro_tier") val proTier: String +) + +@Serializable +data class Promotion( + @SerialName("promotion_id") val promotionId: String, + val title: String, + val message: String, + val link: String? = null +) + +@Serializable +data class ReceiptVerificationRequest( + @SerialName("receipt_data") val receiptData: String +) + +@Serializable +data class PurchaseVerificationRequest( + @SerialName("purchase_token") val purchaseToken: String, + @SerialName("product_id") val productId: String +) + +@Serializable +data class VerificationResponse( + val success: Boolean, + val tier: String? = null, + val error: String? = null +) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/SubscriptionApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/SubscriptionApi.kt new file mode 100644 index 0000000..b47e3a9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/SubscriptionApi.kt @@ -0,0 +1,64 @@ +package com.example.mycrib.network + +import com.example.mycrib.models.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* + +class SubscriptionApi { + private val baseUrl = ApiConfig.baseUrl + + suspend fun getSubscriptionStatus(token: String): ApiResult { + return apiCall { + httpClient.get("$baseUrl/subscription/status/") { + header("Authorization", "Token $token") + }.body() + } + } + + suspend fun getUpgradeTriggers(token: String): ApiResult> { + return apiCall { + httpClient.get("$baseUrl/subscription/upgrade-triggers/") { + header("Authorization", "Token $token") + }.body() + } + } + + suspend fun getFeatureBenefits(): ApiResult> { + return apiCall { + httpClient.get("$baseUrl/subscription/feature-benefits/").body() + } + } + + suspend fun getActivePromotions(token: String): ApiResult> { + return apiCall { + httpClient.get("$baseUrl/subscription/promotions/") { + header("Authorization", "Token $token") + }.body() + } + } + + suspend fun verifyIOSReceipt(token: String, receiptData: String): ApiResult { + return apiCall { + httpClient.post("$baseUrl/subscription/verify-ios/") { + header("Authorization", "Token $token") + contentType(ContentType.Application.Json) + setBody(ReceiptVerificationRequest(receiptData)) + }.body() + } + } + + suspend fun verifyAndroidPurchase( + token: String, + purchaseToken: String, + productId: String + ): ApiResult { + return apiCall { + httpClient.post("$baseUrl/subscription/verify-android/") { + header("Authorization", "Token $token") + contentType(ContentType.Application.Json) + setBody(PurchaseVerificationRequest(purchaseToken, productId)) + }.body() + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/utils/SubscriptionHelper.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/utils/SubscriptionHelper.kt new file mode 100644 index 0000000..99631af --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/utils/SubscriptionHelper.kt @@ -0,0 +1,83 @@ +package com.example.mycrib.utils + +import com.example.mycrib.cache.SubscriptionCache + +object SubscriptionHelper { + data class UsageCheck(val allowed: Boolean, val triggerKey: String?) + + fun canAddProperty(): UsageCheck { + val subscription = SubscriptionCache.currentSubscription.value + ?: return UsageCheck(true, null) // Allow if no subscription data + + // If limitations are disabled globally, allow everything + if (!subscription.limitationsEnabled) { + return UsageCheck(true, null) + } + + if (subscription.tier == "pro") { + return UsageCheck(true, null) + } + + if (subscription.usage.propertiesCount >= (subscription.limits.properties ?: 1)) { + return UsageCheck(false, "add_second_property") + } + + return UsageCheck(true, null) + } + + fun canAddTask(): UsageCheck { + val subscription = SubscriptionCache.currentSubscription.value + ?: return UsageCheck(true, null) + + // If limitations are disabled globally, allow everything + if (!subscription.limitationsEnabled) { + return UsageCheck(true, null) + } + + if (subscription.tier == "pro") { + return UsageCheck(true, null) + } + + if (subscription.usage.tasksCount >= (subscription.limits.tasks ?: 10)) { + return UsageCheck(false, "add_11th_task") + } + + return UsageCheck(true, null) + } + + fun shouldShowUpgradePromptForContractors(): UsageCheck { + val subscription = SubscriptionCache.currentSubscription.value + ?: return UsageCheck(false, null) + + // If limitations are disabled globally, don't show prompt + if (!subscription.limitationsEnabled) { + return UsageCheck(false, null) + } + + // Pro users don't see the prompt + if (subscription.tier == "pro") { + return UsageCheck(false, null) + } + + // Free users see the upgrade prompt + return UsageCheck(true, "view_contractors") + } + + fun shouldShowUpgradePromptForDocuments(): UsageCheck { + val subscription = SubscriptionCache.currentSubscription.value + ?: return UsageCheck(false, null) + + // If limitations are disabled globally, don't show prompt + if (!subscription.limitationsEnabled) { + return UsageCheck(false, null) + } + + // Pro users don't see the prompt + if (subscription.tier == "pro") { + return UsageCheck(false, null) + } + + // Free users see the upgrade prompt + return UsageCheck(true, "view_documents") + } +}