Rebrand from MyCrib to Casera

- Rename Kotlin package from com.example.mycrib to com.example.casera
- Update Android app name, namespace, and application ID
- Update iOS bundle identifiers and project settings
- Rename iOS directories (MyCribTests -> CaseraTests, etc.)
- Update deep link schemes from mycrib:// to casera://
- Update app group identifiers
- Update subscription product IDs
- Update all UI strings and branding

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-28 21:10:38 -06:00
parent 8dbc816a33
commit c6eef720ed
215 changed files with 767 additions and 767 deletions

View File

@@ -0,0 +1,188 @@
package com.example.casera.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.casera.cache.SubscriptionCache
import com.example.casera.ui.theme.AppRadius
import com.example.casera.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
)
}
}

View File

@@ -0,0 +1,362 @@
package com.example.casera.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.*
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.casera.cache.SubscriptionCache
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
/**
* Full inline paywall screen for upgrade prompts.
* Shows feature benefits, subscription products with pricing, and action buttons.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpgradeFeatureScreen(
triggerKey: String,
icon: ImageVector,
onNavigateBack: () -> Unit
) {
var showFeatureComparison by remember { mutableStateOf(false) }
var isProcessing by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
var showSuccessAlert by remember { mutableStateOf(false) }
// Look up trigger data from cache
val triggerData by remember { derivedStateOf {
SubscriptionCache.upgradeTriggers.value[triggerKey]
} }
// Fallback values if trigger not found
val title = triggerData?.title ?: "Upgrade Required"
val message = triggerData?.message ?: "This feature is available with a Pro subscription."
val buttonText = triggerData?.buttonText ?: "Upgrade to Pro"
Scaffold(
topBar = {
TopAppBar(
title = { Text(title, fontWeight = FontWeight.SemiBold) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(AppSpacing.xl))
// Feature Icon (star gradient like iOS)
Icon(
imageVector = Icons.Default.Stars,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.tertiary
)
Spacer(Modifier.height(AppSpacing.lg))
// Title
Text(
title,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = AppSpacing.lg)
)
Spacer(Modifier.height(AppSpacing.md))
// Description
Text(
message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = AppSpacing.lg)
)
Spacer(Modifier.height(AppSpacing.xl))
// Pro Features Preview Card
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg),
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surfaceVariant
) {
Column(
modifier = Modifier.padding(AppSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
) {
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.xl))
// Subscription Products Section
// Note: On Android, BillingManager provides real pricing
// This is a placeholder showing static options
SubscriptionProductsSection(
isProcessing = isProcessing,
onProductSelected = { productId ->
// Trigger purchase flow
// On Android, this connects to BillingManager
isProcessing = true
errorMessage = null
// Purchase will be handled by platform-specific code
},
onRetryLoad = {
// Retry loading products
}
)
// Error Message
errorMessage?.let { error ->
Spacer(Modifier.height(AppSpacing.md))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
) {
Row(
modifier = Modifier.padding(AppSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
Text(
error,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
}
Spacer(Modifier.height(AppSpacing.lg))
// Compare Plans
TextButton(onClick = { showFeatureComparison = true }) {
Text("Compare Free vs Pro")
}
// Restore Purchases
TextButton(onClick = {
// Trigger restore purchases
isProcessing = true
errorMessage = null
}) {
Text(
"Restore Purchases",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(Modifier.height(AppSpacing.xl * 2))
}
if (showFeatureComparison) {
FeatureComparisonDialog(
onDismiss = { showFeatureComparison = false },
onUpgrade = {
// Trigger upgrade
showFeatureComparison = false
}
)
}
if (showSuccessAlert) {
AlertDialog(
onDismissRequest = { showSuccessAlert = false },
title = { Text("Subscription Active") },
text = { Text("You now have full access to all Pro features!") },
confirmButton = {
TextButton(onClick = {
showSuccessAlert = false
onNavigateBack()
}) {
Text("Done")
}
}
)
}
}
}
@Composable
private fun FeatureRow(icon: ImageVector, text: String) {
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
private fun SubscriptionProductsSection(
isProcessing: Boolean,
onProductSelected: (String) -> Unit,
onRetryLoad: () -> Unit
) {
// Static subscription options (pricing will be updated by platform billing)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
) {
// Monthly Option
SubscriptionProductCard(
productId = "com.example.casera.pro.monthly",
name = "MyCrib Pro Monthly",
price = "$4.99/month",
description = "Billed monthly",
savingsBadge = null,
isSelected = false,
isProcessing = isProcessing,
onSelect = { onProductSelected("com.example.casera.pro.monthly") }
)
// Annual Option
SubscriptionProductCard(
productId = "com.example.casera.pro.annual",
name = "MyCrib Pro Annual",
price = "$39.99/year",
description = "Billed annually",
savingsBadge = "Save 33%",
isSelected = false,
isProcessing = isProcessing,
onSelect = { onProductSelected("com.example.casera.pro.annual") }
)
}
}
@Composable
private fun SubscriptionProductCard(
productId: String,
name: String,
price: String,
description: String,
savingsBadge: String?,
isSelected: Boolean,
isProcessing: Boolean,
onSelect: () -> Unit
) {
Card(
onClick = onSelect,
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
colors = CardDefaults.cardColors(
containerColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surface
),
border = if (isSelected)
androidx.compose.foundation.BorderStroke(2.dp, MaterialTheme.colorScheme.primary)
else
null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Text(
name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
savingsBadge?.let { badge ->
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.tertiaryContainer
) {
Text(
badge,
modifier = Modifier.padding(
horizontal = AppSpacing.sm,
vertical = 2.dp
),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.tertiary,
fontWeight = FontWeight.Bold
)
}
}
}
Text(
description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (isProcessing) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
} else {
Text(
price,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}

View File

@@ -0,0 +1,155 @@
package com.example.casera.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.casera.cache.SubscriptionCache
import com.example.casera.ui.theme.AppRadius
import com.example.casera.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
)
}
}