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,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