P2 Stream E: FeatureComparisonScreen (replaces FeatureComparisonDialog)
Full-screen feature comparison matching iOS FeatureComparisonView. Two-column table, iOS-equivalent row set, CTA to upgrade flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import com.tt.honeyDue.ui.screens.TasksScreen
|
||||
import com.tt.honeyDue.ui.screens.VerifyEmailScreen
|
||||
import com.tt.honeyDue.ui.screens.VerifyResetCodeScreen
|
||||
import com.tt.honeyDue.ui.screens.onboarding.OnboardingScreen
|
||||
import com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen
|
||||
import com.tt.honeyDue.ui.screens.theme.ThemeSelectionScreen
|
||||
import com.tt.honeyDue.viewmodel.OnboardingViewModel
|
||||
import com.tt.honeyDue.viewmodel.PasswordResetViewModel
|
||||
@@ -653,6 +654,17 @@ fun App(
|
||||
)
|
||||
}
|
||||
|
||||
composable<FeatureComparisonRoute> {
|
||||
// P2 Stream E — full-screen Free vs. Pro comparison.
|
||||
FeatureComparisonScreen(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToUpgrade = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(UpgradeRoute)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable<EditTaskRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<EditTaskRoute>()
|
||||
EditTaskScreen(
|
||||
|
||||
@@ -29,6 +29,7 @@ object AnalyticsEvents {
|
||||
const val RESIDENCE_SCREEN_SHOWN = "residence_screen_shown"
|
||||
const val NEW_RESIDENCE_SCREEN_SHOWN = "new_residence_screen_shown"
|
||||
const val RESIDENCE_CREATED = "residence_created"
|
||||
const val RESIDENCE_JOINED = "residence_joined"
|
||||
const val RESIDENCE_LIMIT_REACHED = "residence_limit_reached"
|
||||
|
||||
// Task
|
||||
@@ -74,4 +75,11 @@ object AnalyticsEvents {
|
||||
const val NOTIFICATION_SETTINGS_SCREEN_SHOWN = "notification_settings_screen_shown"
|
||||
const val SETTINGS_SCREEN_SHOWN = "settings_screen_shown"
|
||||
const val THEME_CHANGED = "theme_changed"
|
||||
|
||||
// Subscription / Paywall
|
||||
// PAYWALL_COMPARE_CTA: fired when the user taps the "Upgrade to Pro" CTA
|
||||
// from the FeatureComparisonScreen (the full-screen Free vs. Pro
|
||||
// comparison table). Lets the funnel attribute conversions to the
|
||||
// comparison surface rather than the generic upgrade entry point.
|
||||
const val PAYWALL_COMPARE_CTA = "paywall_compare_cta"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ object ResidencesRoute
|
||||
@Serializable
|
||||
object AddResidenceRoute
|
||||
|
||||
// Full-screen join-via-share-code flow (P2 Stream F — replaces
|
||||
// the old JoinResidenceDialog). Matches iOS JoinResidenceView.
|
||||
@Serializable
|
||||
object JoinResidenceRoute
|
||||
|
||||
@Serializable
|
||||
data class EditResidenceRoute(
|
||||
val residenceId: Int,
|
||||
|
||||
@@ -25,7 +25,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.tt.honeyDue.ui.components.ApiResultHandler
|
||||
import com.tt.honeyDue.ui.components.JoinResidenceDialog
|
||||
import com.tt.honeyDue.ui.components.common.StatItem
|
||||
import com.tt.honeyDue.ui.components.residence.TaskStatChip
|
||||
import com.tt.honeyDue.viewmodel.ResidenceViewModel
|
||||
@@ -45,6 +44,7 @@ import org.jetbrains.compose.resources.stringResource
|
||||
fun ResidencesScreen(
|
||||
onResidenceClick: (Int) -> Unit,
|
||||
onAddResidence: () -> Unit,
|
||||
onJoinResidence: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onNavigateToProfile: () -> Unit = {},
|
||||
shouldRefresh: Boolean = false,
|
||||
@@ -53,7 +53,6 @@ fun ResidencesScreen(
|
||||
) {
|
||||
val myResidencesState by viewModel.myResidencesState.collectAsState()
|
||||
val totalSummary by DataManager.totalSummary.collectAsState()
|
||||
var showJoinDialog by remember { mutableStateOf(false) }
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
var showUpgradePrompt by remember { mutableStateOf(false) }
|
||||
var upgradeTriggerKey by remember { mutableStateOf<String?>(null) }
|
||||
@@ -92,18 +91,6 @@ fun ResidencesScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (showJoinDialog) {
|
||||
JoinResidenceDialog(
|
||||
onDismiss = {
|
||||
showJoinDialog = false
|
||||
},
|
||||
onJoined = {
|
||||
// Reload residences after joining
|
||||
viewModel.loadMyResidences()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showUpgradePrompt && upgradeTriggerKey != null) {
|
||||
UpgradePromptDialog(
|
||||
triggerKey = upgradeTriggerKey!!,
|
||||
@@ -143,7 +130,7 @@ fun ResidencesScreen(
|
||||
IconButton(onClick = {
|
||||
val (allowed, triggerKey) = canAddProperty()
|
||||
if (allowed) {
|
||||
showJoinDialog = true
|
||||
onJoinResidence()
|
||||
} else {
|
||||
upgradeTriggerKey = triggerKey
|
||||
showUpgradePrompt = true
|
||||
@@ -279,7 +266,7 @@ fun ResidencesScreen(
|
||||
onClick = {
|
||||
val (allowed, triggerKey) = canAddProperty()
|
||||
if (allowed) {
|
||||
showJoinDialog = true
|
||||
onJoinResidence()
|
||||
} else {
|
||||
upgradeTriggerKey = triggerKey
|
||||
showUpgradePrompt = true
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.tt.honeyDue.ui.screens.subscription
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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 com.tt.honeyDue.analytics.AnalyticsEvents
|
||||
import com.tt.honeyDue.analytics.PostHogAnalytics
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.FeatureBenefit
|
||||
import com.tt.honeyDue.ui.components.common.StandardCard
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
|
||||
/**
|
||||
* FeatureComparisonScreen — full-screen Free vs. Pro comparison matching
|
||||
* iOS `iosApp/iosApp/Subscription/FeatureComparisonView.swift`.
|
||||
*
|
||||
* Replaces the old `ui/subscription/FeatureComparisonDialog.kt` (deleted
|
||||
* in P2 Stream E). The iOS view is a navigation-stack destination, not a
|
||||
* dialog — this screen mirrors that layout:
|
||||
*
|
||||
* - Header ("Choose Your Plan" + subtitle)
|
||||
* - Two-column comparison table rendered inside a [StandardCard]
|
||||
* (Feature | Free | Pro) with N rows sourced from
|
||||
* `DataManager.featureBenefits`, falling back to a 4-row default list.
|
||||
* - CTA button at the bottom ("Upgrade to Pro") which fires analytics
|
||||
* event [AnalyticsEvents.PAYWALL_COMPARE_CTA] and invokes
|
||||
* [onNavigateToUpgrade].
|
||||
* - Toolbar back button dismisses.
|
||||
*
|
||||
* Testable behavior lives on [FeatureComparisonScreenState] so it can be
|
||||
* exercised in `commonTest` without standing up the Compose recomposer.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FeatureComparisonScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToUpgrade: () -> Unit,
|
||||
) {
|
||||
val benefits by DataManager.featureBenefits.collectAsState()
|
||||
val rows = FeatureComparisonScreenState.resolveFeatureRows(benefits)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Choose Your Plan",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
FeatureComparisonScreenState.onClose(onBack = onNavigateBack)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Close",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { paddingValues: PaddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
) {
|
||||
Text(
|
||||
text = "Choose Your Plan",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = "Upgrade to Pro for unlimited access",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
StandardCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = 0.dp,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
ComparisonHeaderRow()
|
||||
HorizontalDivider()
|
||||
|
||||
rows.forEachIndexed { index, row ->
|
||||
ComparisonRow(
|
||||
featureName = row.featureName,
|
||||
freeText = row.freeTierText,
|
||||
proText = row.proTierText,
|
||||
freeHas = FeatureComparisonScreenState.freeHasFeature(row),
|
||||
proHas = FeatureComparisonScreenState.premiumHasFeature(row),
|
||||
)
|
||||
if (index != rows.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
FeatureComparisonScreenState.onUpgradeTap(
|
||||
onNavigateToUpgrade = onNavigateToUpgrade,
|
||||
captureEvent = { event, props ->
|
||||
PostHogAnalytics.capture(event, props)
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "Upgrade to Pro",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComparisonHeaderRow() {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Feature",
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = "Free",
|
||||
modifier = Modifier.width(80.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = "Pro",
|
||||
modifier = Modifier.width(80.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComparisonRow(
|
||||
featureName: String,
|
||||
freeText: String,
|
||||
proText: String,
|
||||
freeHas: Boolean,
|
||||
proHas: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = featureName,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.width(80.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
TierCell(
|
||||
text = freeText,
|
||||
hasFeature = freeHas,
|
||||
emphasize = false,
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.width(80.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
TierCell(
|
||||
text = proText,
|
||||
hasFeature = proHas,
|
||||
emphasize = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single tier cell showing a check/cross glyph stacked with the limit
|
||||
* text. Matches the iOS `ComparisonRow` visual language where "Not
|
||||
* available" reads as the absence of the feature.
|
||||
*/
|
||||
@Composable
|
||||
private fun TierCell(
|
||||
text: String,
|
||||
hasFeature: Boolean,
|
||||
emphasize: Boolean,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (hasFeature) Icons.Default.Check else Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (hasFeature) {
|
||||
if (emphasize) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = if (emphasize) FontWeight.Medium else FontWeight.Normal,
|
||||
color = if (emphasize && hasFeature) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State helper for [FeatureComparisonScreen].
|
||||
*
|
||||
* Pulled out as a plain object so the behavior is unit-testable in
|
||||
* `commonTest` without standing up the Compose recomposer — same pattern
|
||||
* as [com.tt.honeyDue.ui.screens.theme.ThemeSelectionScreenState].
|
||||
*
|
||||
* The iOS reference (`FeatureComparisonView.swift`) sources rows from
|
||||
* `SubscriptionCacheWrapper.shared.featureBenefits` with a 4-row default
|
||||
* list when the cache is empty. This object exposes:
|
||||
*
|
||||
* - [defaultFeatureRows] — the same 4-row default (Properties, Tasks,
|
||||
* Contractors, Documents).
|
||||
* - [resolveFeatureRows] — server-driven benefits if present, default
|
||||
* otherwise.
|
||||
* - [freeHasFeature] / [premiumHasFeature] — per-row booleans matching
|
||||
* iOS: a tier "has" the feature unless its text reads "Not available"
|
||||
* (case-insensitive).
|
||||
* - [onUpgradeTap] — fires [AnalyticsEvents.PAYWALL_COMPARE_CTA] and
|
||||
* navigates to the upgrade flow.
|
||||
* - [onClose] — dismisses the screen.
|
||||
*/
|
||||
object FeatureComparisonScreenState {
|
||||
|
||||
fun defaultFeatureRows(): List<FeatureBenefit> = listOf(
|
||||
FeatureBenefit(
|
||||
featureName = "Properties",
|
||||
freeTierText = "1 property",
|
||||
proTierText = "Unlimited",
|
||||
),
|
||||
FeatureBenefit(
|
||||
featureName = "Tasks",
|
||||
freeTierText = "10 tasks",
|
||||
proTierText = "Unlimited",
|
||||
),
|
||||
FeatureBenefit(
|
||||
featureName = "Contractors",
|
||||
freeTierText = "Not available",
|
||||
proTierText = "Unlimited",
|
||||
),
|
||||
FeatureBenefit(
|
||||
featureName = "Documents",
|
||||
freeTierText = "Not available",
|
||||
proTierText = "Unlimited",
|
||||
),
|
||||
)
|
||||
|
||||
fun resolveFeatureRows(serverBenefits: List<FeatureBenefit>): List<FeatureBenefit> {
|
||||
return if (serverBenefits.isNotEmpty()) serverBenefits else defaultFeatureRows()
|
||||
}
|
||||
|
||||
fun freeHasFeature(benefit: FeatureBenefit): Boolean =
|
||||
!isUnavailable(benefit.freeTierText)
|
||||
|
||||
fun premiumHasFeature(benefit: FeatureBenefit): Boolean =
|
||||
!isUnavailable(benefit.proTierText)
|
||||
|
||||
private fun isUnavailable(text: String): Boolean =
|
||||
text.trim().equals("Not available", ignoreCase = true)
|
||||
|
||||
/**
|
||||
* CTA handler. Fires the paywall analytics event and navigates to
|
||||
* the upgrade flow. The [captureEvent] parameter is injected so tests
|
||||
* can record without touching the PostHog SDK.
|
||||
*/
|
||||
fun onUpgradeTap(
|
||||
onNavigateToUpgrade: () -> Unit,
|
||||
captureEvent: (String, Map<String, Any>?) -> Unit,
|
||||
) {
|
||||
captureEvent(AnalyticsEvents.PAYWALL_COMPARE_CTA, null)
|
||||
onNavigateToUpgrade()
|
||||
}
|
||||
|
||||
fun onClose(onBack: () -> Unit) {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package com.tt.honeyDue.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.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
|
||||
@Composable
|
||||
fun FeatureComparisonDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onUpgrade: () -> Unit
|
||||
) {
|
||||
val featureBenefits = DataManager.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.freeTierText,
|
||||
proText = benefit.proTierText
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import com.tt.honeyDue.utils.SubscriptionProducts
|
||||
@@ -191,12 +192,12 @@ fun UpgradeFeatureScreen(
|
||||
}
|
||||
|
||||
if (showFeatureComparison) {
|
||||
FeatureComparisonDialog(
|
||||
onDismiss = { showFeatureComparison = false },
|
||||
onUpgrade = {
|
||||
// Trigger upgrade
|
||||
showFeatureComparison = false
|
||||
}
|
||||
// P2 Stream E — replaces the old FeatureComparisonDialog with
|
||||
// the full-screen FeatureComparisonScreen. Render as overlay
|
||||
// so dismiss returns to this paywall (matches iOS sheet).
|
||||
FeatureComparisonScreen(
|
||||
onNavigateBack = { showFeatureComparison = false },
|
||||
onNavigateToUpgrade = { showFeatureComparison = false },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
|
||||
@@ -26,9 +27,15 @@ fun UpgradePromptDialog(
|
||||
var isProcessing by remember { mutableStateOf(false) }
|
||||
|
||||
if (showFeatureComparison) {
|
||||
FeatureComparisonDialog(
|
||||
onDismiss = { showFeatureComparison = false },
|
||||
onUpgrade = onUpgrade
|
||||
// P2 Stream E — migrated from FeatureComparisonDialog to the
|
||||
// full-screen FeatureComparisonScreen. Tapping the CTA invokes
|
||||
// the existing onUpgrade flow (BillingManager / StoreKit).
|
||||
FeatureComparisonScreen(
|
||||
onNavigateBack = { showFeatureComparison = false },
|
||||
onNavigateToUpgrade = {
|
||||
showFeatureComparison = false
|
||||
onUpgrade()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
|
||||
Reference in New Issue
Block a user