Phase 6: Add Android subscription UI with Jetpack Compose
Implemented Android subscription UI components: - UpgradeFeatureScreen: Full-screen view for restricted features (contractors, documents) * Shows feature icon, name, and description * Displays "This feature is available with Pro" badge * Opens UpgradePromptDialog on button click - UpgradePromptDialog: Modal dialog with upgrade options * Trigger-specific title and message from backend * Feature preview list with Material icons * "Upgrade to Pro" button with loading state * "Compare Free vs Pro" button * "Maybe Later" cancel option - FeatureComparisonDialog: Full-screen comparison table * Free vs Pro feature comparison * Displays data from SubscriptionCache.featureBenefits * Default features if no data loaded * Upgrade button - BillingManager: Google Play Billing Library placeholder * Singleton manager for in-app purchases * Product query placeholder * Purchase flow placeholder * Backend receipt verification placeholder * Restore purchases placeholder All components follow Material3 design system with theme-aware colors and spacing constants. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
package com.example.mycrib.platform
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Play Billing manager for in-app purchases
|
||||||
|
* NOTE: Requires Google Play Console configuration and product IDs
|
||||||
|
*/
|
||||||
|
class BillingManager private constructor(context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: BillingManager? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): BillingManager {
|
||||||
|
return INSTANCE ?: synchronized(this) {
|
||||||
|
INSTANCE ?: BillingManager(context.applicationContext).also { INSTANCE = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product ID for Pro subscription (configure in Google Play Console)
|
||||||
|
private val proSubscriptionProductID = "com.example.mycrib.pro.monthly"
|
||||||
|
|
||||||
|
private val _isLoading = MutableStateFlow(false)
|
||||||
|
val isLoading: StateFlow<Boolean> = _isLoading
|
||||||
|
|
||||||
|
private val _purchasedProductIDs = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
val purchasedProductIDs: StateFlow<Set<String>> = _purchasedProductIDs
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Start listening for purchases
|
||||||
|
// In production, initialize BillingClient here
|
||||||
|
println("BillingManager: Initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to Google Play Billing
|
||||||
|
*/
|
||||||
|
fun startConnection(onSuccess: () -> Unit, onError: (String) -> Unit) {
|
||||||
|
// In production, this would connect to BillingClient
|
||||||
|
// billingClient.startConnection(object : BillingClientStateListener {
|
||||||
|
// override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||||
|
// if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||||
|
// onSuccess()
|
||||||
|
// } else {
|
||||||
|
// onError("Billing setup failed")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// override fun onBillingServiceDisconnected() { /* Retry */ }
|
||||||
|
// })
|
||||||
|
|
||||||
|
println("BillingManager: Would connect to Google Play Billing")
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query available products
|
||||||
|
*/
|
||||||
|
suspend fun queryProducts(): List<ProductDetails> {
|
||||||
|
_isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// In production, this would query real products
|
||||||
|
// val params = QueryProductDetailsParams.newBuilder()
|
||||||
|
// .setProductList(listOf(
|
||||||
|
// Product.newBuilder()
|
||||||
|
// .setProductId(proSubscriptionProductID)
|
||||||
|
// .setProductType(BillingClient.ProductType.SUBS)
|
||||||
|
// .build()
|
||||||
|
// ))
|
||||||
|
// .build()
|
||||||
|
// val result = billingClient.queryProductDetails(params)
|
||||||
|
|
||||||
|
println("BillingManager: Would query products here")
|
||||||
|
return emptyList()
|
||||||
|
} finally {
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch purchase flow
|
||||||
|
*/
|
||||||
|
fun launchPurchaseFlow(activity: Activity, productId: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
|
||||||
|
// In production, this would launch the purchase UI
|
||||||
|
// val params = BillingFlowParams.newBuilder()
|
||||||
|
// .setProductDetailsParamsList(listOf(productDetailsParams))
|
||||||
|
// .build()
|
||||||
|
// val billingResult = billingClient.launchBillingFlow(activity, params)
|
||||||
|
|
||||||
|
println("BillingManager: Would launch purchase flow for: $productId")
|
||||||
|
onError("Purchase not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify purchase with backend
|
||||||
|
*/
|
||||||
|
suspend fun verifyPurchaseWithBackend(purchaseToken: String, productId: String) {
|
||||||
|
// TODO: Call backend API to verify purchase
|
||||||
|
// val api = SubscriptionApi()
|
||||||
|
// val result = api.verifyAndroidPurchase(
|
||||||
|
// token = tokenStorage.getToken(),
|
||||||
|
// purchaseToken = purchaseToken,
|
||||||
|
// productId = productId
|
||||||
|
// )
|
||||||
|
|
||||||
|
println("BillingManager: Would verify purchase with backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore purchases
|
||||||
|
*/
|
||||||
|
suspend fun restorePurchases() {
|
||||||
|
// In production, this would query purchase history
|
||||||
|
// val result = billingClient.queryPurchasesAsync(
|
||||||
|
// QueryPurchasesParams.newBuilder()
|
||||||
|
// .setProductType(BillingClient.ProductType.SUBS)
|
||||||
|
// .build()
|
||||||
|
// )
|
||||||
|
|
||||||
|
println("BillingManager: Would restore purchases here")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledge purchase (required by Google Play)
|
||||||
|
*/
|
||||||
|
private suspend fun acknowledgePurchase(purchaseToken: String) {
|
||||||
|
// In production, this would acknowledge the purchase
|
||||||
|
// val params = AcknowledgePurchaseParams.newBuilder()
|
||||||
|
// .setPurchaseToken(purchaseToken)
|
||||||
|
// .build()
|
||||||
|
// billingClient.acknowledgePurchase(params)
|
||||||
|
|
||||||
|
println("BillingManager: Would acknowledge purchase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for ProductDetails
|
||||||
|
* In production, use com.android.billingclient.api.ProductDetails
|
||||||
|
*/
|
||||||
|
data class ProductDetails(
|
||||||
|
val productId: String,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val price: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package com.example.mycrib.ui.subscription
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.example.mycrib.cache.SubscriptionCache
|
||||||
|
import com.example.mycrib.ui.theme.AppRadius
|
||||||
|
import com.example.mycrib.ui.theme.AppSpacing
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeatureComparisonDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onUpgrade: () -> Unit
|
||||||
|
) {
|
||||||
|
val subscriptionCache = SubscriptionCache
|
||||||
|
val featureBenefits = subscriptionCache.featureBenefits.value
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.9f)
|
||||||
|
.padding(AppSpacing.md),
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.lg),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Choose Your Plan",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Upgrade to Pro for unlimited access",
|
||||||
|
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(AppSpacing.lg))
|
||||||
|
|
||||||
|
// Comparison Table
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// Header Row
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.md),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Feature",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Free",
|
||||||
|
modifier = Modifier.width(80.dp),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Pro",
|
||||||
|
modifier = Modifier.width(80.dp),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Feature Rows
|
||||||
|
if (featureBenefits.isNotEmpty()) {
|
||||||
|
featureBenefits.forEach { benefit ->
|
||||||
|
ComparisonRow(
|
||||||
|
featureName = benefit.featureName,
|
||||||
|
freeText = benefit.freeTier,
|
||||||
|
proText = benefit.proTier
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default features if no data loaded
|
||||||
|
ComparisonRow("Properties", "1 property", "Unlimited")
|
||||||
|
HorizontalDivider()
|
||||||
|
ComparisonRow("Tasks", "10 tasks", "Unlimited")
|
||||||
|
HorizontalDivider()
|
||||||
|
ComparisonRow("Contractors", "Not available", "Unlimited")
|
||||||
|
HorizontalDivider()
|
||||||
|
ComparisonRow("Documents", "Not available", "Unlimited")
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade Button
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onUpgrade()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.lg),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Text("Upgrade to Pro", fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ComparisonRow(
|
||||||
|
featureName: String,
|
||||||
|
freeText: String,
|
||||||
|
proText: String
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.md),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
featureName,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
freeText,
|
||||||
|
modifier = Modifier.width(80.dp),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
proText,
|
||||||
|
modifier = Modifier.width(80.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.example.mycrib.ui.subscription
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.mycrib.ui.theme.AppRadius
|
||||||
|
import com.example.mycrib.ui.theme.AppSpacing
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun UpgradeFeatureScreen(
|
||||||
|
triggerKey: String,
|
||||||
|
featureName: String,
|
||||||
|
featureDescription: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onNavigateBack: () -> Unit
|
||||||
|
) {
|
||||||
|
var showUpgradeDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(featureName, fontWeight = FontWeight.SemiBold) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(Icons.Default.ArrowBack, "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.xl),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
|
||||||
|
) {
|
||||||
|
// Feature Icon
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(80.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
featureName,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
// Description
|
||||||
|
Text(
|
||||||
|
featureDescription,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
// Upgrade Badge
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"This feature is available with Pro",
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = AppSpacing.md,
|
||||||
|
vertical = AppSpacing.sm
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(AppSpacing.lg))
|
||||||
|
|
||||||
|
// Upgrade Button
|
||||||
|
Button(
|
||||||
|
onClick = { showUpgradeDialog = true },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = AppSpacing.lg),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Text("Upgrade to Pro", fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showUpgradeDialog) {
|
||||||
|
UpgradePromptDialog(
|
||||||
|
triggerKey = triggerKey,
|
||||||
|
onDismiss = { showUpgradeDialog = false },
|
||||||
|
onUpgrade = {
|
||||||
|
// TODO: Trigger Google Play Billing
|
||||||
|
showUpgradeDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.example.mycrib.ui.subscription
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.example.mycrib.cache.SubscriptionCache
|
||||||
|
import com.example.mycrib.ui.theme.AppRadius
|
||||||
|
import com.example.mycrib.ui.theme.AppSpacing
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UpgradePromptDialog(
|
||||||
|
triggerKey: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onUpgrade: () -> Unit
|
||||||
|
) {
|
||||||
|
val subscriptionCache = SubscriptionCache
|
||||||
|
val triggerData = subscriptionCache.upgradeTriggers.value[triggerKey]
|
||||||
|
var showFeatureComparison by remember { mutableStateOf(false) }
|
||||||
|
var isProcessing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showFeatureComparison) {
|
||||||
|
FeatureComparisonDialog(
|
||||||
|
onDismiss = { showFeatureComparison = false },
|
||||||
|
onUpgrade = onUpgrade
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.md),
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.xl),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||||
|
) {
|
||||||
|
// Icon
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Stars,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(60.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
triggerData?.title ?: "Upgrade to Pro",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message
|
||||||
|
Text(
|
||||||
|
triggerData?.message ?: "Unlock unlimited access to all features",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(AppSpacing.sm))
|
||||||
|
|
||||||
|
// Pro Features Preview
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(AppSpacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||||
|
) {
|
||||||
|
FeatureRow(Icons.Default.Home, "Unlimited properties")
|
||||||
|
FeatureRow(Icons.Default.CheckCircle, "Unlimited tasks")
|
||||||
|
FeatureRow(Icons.Default.People, "Contractor management")
|
||||||
|
FeatureRow(Icons.Default.Description, "Document & warranty storage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(AppSpacing.sm))
|
||||||
|
|
||||||
|
// Upgrade Button
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
isProcessing = true
|
||||||
|
onUpgrade()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isProcessing,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
if (isProcessing) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
triggerData?.buttonText ?: "Upgrade to Pro",
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare Plans
|
||||||
|
TextButton(onClick = { showFeatureComparison = true }) {
|
||||||
|
Text("Compare Free vs Pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Maybe Later")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FeatureRow(icon: androidx.compose.ui.graphics.vector.ImageVector, text: String) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user