Add push notification deep linking and sharing subscription checks
- Add deep link navigation from push notifications to specific task column on kanban board - Fix subscription check in push notification handler to allow navigation when limitations disabled - Add pendingNavigationTaskId to handle notifications when app isn't ready - Add ScrollViewReader to AllTasksView for programmatic scrolling to task column - Add canShareResidence() and canShareContractor() subscription checks (iOS & Android) - Add test APNS file for simulator push notification testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,8 @@ import com.example.casera.util.DateUtils
|
||||
import com.example.casera.viewmodel.ContractorViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.platform.rememberShareContractor
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -45,6 +47,8 @@ fun ContractorDetailScreen(
|
||||
|
||||
var showEditDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteConfirmation by remember { mutableStateOf(false) }
|
||||
var showUpgradePrompt by remember { mutableStateOf(false) }
|
||||
var upgradeTriggerKey by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val shareContractor = rememberShareContractor()
|
||||
|
||||
@@ -90,7 +94,15 @@ fun ContractorDetailScreen(
|
||||
actions = {
|
||||
when (val state = contractorState) {
|
||||
is ApiResult.Success -> {
|
||||
IconButton(onClick = { shareContractor(state.data) }) {
|
||||
IconButton(onClick = {
|
||||
val shareCheck = SubscriptionHelper.canShareContractor()
|
||||
if (shareCheck.allowed) {
|
||||
shareContractor(state.data)
|
||||
} else {
|
||||
upgradeTriggerKey = shareCheck.triggerKey
|
||||
showUpgradePrompt = true
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.Share, stringResource(Res.string.common_share))
|
||||
}
|
||||
IconButton(onClick = { viewModel.toggleFavorite(contractorId) }) {
|
||||
@@ -557,6 +569,21 @@ fun ContractorDetailScreen(
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (showUpgradePrompt && upgradeTriggerKey != null) {
|
||||
UpgradePromptDialog(
|
||||
triggerKey = upgradeTriggerKey!!,
|
||||
onDismiss = {
|
||||
showUpgradePrompt = false
|
||||
upgradeTriggerKey = null
|
||||
},
|
||||
onUpgrade = {
|
||||
// TODO: Navigate to subscription purchase screen
|
||||
showUpgradePrompt = false
|
||||
upgradeTriggerKey = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -345,6 +345,44 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
if (SubscriptionHelper.currentTier != "pro") {
|
||||
// Upgrade Benefits List
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = AppSpacing.sm),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.profile_upgrade_benefits_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
UpgradeBenefitRow(
|
||||
icon = Icons.Default.Home,
|
||||
text = stringResource(Res.string.profile_benefit_unlimited_properties)
|
||||
)
|
||||
UpgradeBenefitRow(
|
||||
icon = Icons.Default.Folder,
|
||||
text = stringResource(Res.string.profile_benefit_document_vault)
|
||||
)
|
||||
UpgradeBenefitRow(
|
||||
icon = Icons.Default.People,
|
||||
text = stringResource(Res.string.profile_benefit_residence_sharing)
|
||||
)
|
||||
UpgradeBenefitRow(
|
||||
icon = Icons.Default.Share,
|
||||
text = stringResource(Res.string.profile_benefit_contractor_sharing)
|
||||
)
|
||||
UpgradeBenefitRow(
|
||||
icon = Icons.Default.Notifications,
|
||||
text = stringResource(Res.string.profile_benefit_actionable_notifications)
|
||||
)
|
||||
UpgradeBenefitRow(
|
||||
icon = Icons.Default.Widgets,
|
||||
text = stringResource(Res.string.profile_benefit_widgets)
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { showUpgradePrompt = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -556,3 +594,26 @@ fun ProfileScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpgradeBenefitRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
text: String
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +442,15 @@ fun ResidenceDetailScreen(
|
||||
// Share button - only show for primary owners
|
||||
if (residence.ownerId == currentUser?.id) {
|
||||
IconButton(
|
||||
onClick = { shareResidence(residence) },
|
||||
onClick = {
|
||||
val shareCheck = SubscriptionHelper.canShareResidence()
|
||||
if (shareCheck.allowed) {
|
||||
shareResidence(residence)
|
||||
} else {
|
||||
upgradeTriggerKey = shareCheck.triggerKey
|
||||
showUpgradePrompt = true
|
||||
}
|
||||
},
|
||||
enabled = !shareState.isLoading
|
||||
) {
|
||||
if (shareState.isLoading) {
|
||||
@@ -459,7 +467,13 @@ fun ResidenceDetailScreen(
|
||||
// Manage Users button - only show for primary owners
|
||||
if (residence.ownerId == currentUser?.id) {
|
||||
IconButton(onClick = {
|
||||
showManageUsersDialog = true
|
||||
val shareCheck = SubscriptionHelper.canShareResidence()
|
||||
if (shareCheck.allowed) {
|
||||
showManageUsersDialog = true
|
||||
} else {
|
||||
upgradeTriggerKey = shareCheck.triggerKey
|
||||
showUpgradePrompt = true
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.People, contentDescription = stringResource(Res.string.properties_manage_users))
|
||||
}
|
||||
|
||||
@@ -70,15 +70,6 @@ fun OnboardingSubscriptionContent(
|
||||
MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
),
|
||||
SubscriptionBenefit(
|
||||
icon = Icons.Default.Notifications,
|
||||
title = stringResource(Res.string.onboarding_subscription_benefit_reminders),
|
||||
description = stringResource(Res.string.onboarding_subscription_benefit_reminders_desc),
|
||||
gradientColors = listOf(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
Color(0xFFFF9500)
|
||||
)
|
||||
),
|
||||
SubscriptionBenefit(
|
||||
icon = Icons.Default.Folder,
|
||||
title = stringResource(Res.string.onboarding_subscription_benefit_documents),
|
||||
@@ -98,13 +89,31 @@ fun OnboardingSubscriptionContent(
|
||||
)
|
||||
),
|
||||
SubscriptionBenefit(
|
||||
icon = Icons.Default.TrendingUp,
|
||||
title = stringResource(Res.string.onboarding_subscription_benefit_insights),
|
||||
description = stringResource(Res.string.onboarding_subscription_benefit_insights_desc),
|
||||
icon = Icons.Default.Share,
|
||||
title = stringResource(Res.string.onboarding_subscription_benefit_contractor_sharing),
|
||||
description = stringResource(Res.string.onboarding_subscription_benefit_contractor_sharing_desc),
|
||||
gradientColors = listOf(
|
||||
MaterialTheme.colorScheme.error,
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
Color(0xFFFF9500)
|
||||
)
|
||||
),
|
||||
SubscriptionBenefit(
|
||||
icon = Icons.Default.Notifications,
|
||||
title = stringResource(Res.string.onboarding_subscription_benefit_notifications),
|
||||
description = stringResource(Res.string.onboarding_subscription_benefit_notifications_desc),
|
||||
gradientColors = listOf(
|
||||
Color(0xFFFF3B30),
|
||||
Color(0xFFFF6961)
|
||||
)
|
||||
),
|
||||
SubscriptionBenefit(
|
||||
icon = Icons.Default.Widgets,
|
||||
title = stringResource(Res.string.onboarding_subscription_benefit_widgets),
|
||||
description = stringResource(Res.string.onboarding_subscription_benefit_widgets_desc),
|
||||
gradientColors = listOf(
|
||||
Color(0xFF007AFF),
|
||||
Color(0xFF5AC8FA)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -75,6 +75,24 @@ fun OnboardingValuePropsContent(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
),
|
||||
FeatureItem(
|
||||
icon = Icons.Default.Notifications,
|
||||
title = stringResource(Res.string.onboarding_feature_notifications_title),
|
||||
description = stringResource(Res.string.onboarding_feature_notifications_desc),
|
||||
gradientColors = listOf(
|
||||
Color(0xFFFF9500),
|
||||
Color(0xFFFF6961)
|
||||
)
|
||||
),
|
||||
FeatureItem(
|
||||
icon = Icons.Default.Widgets,
|
||||
title = stringResource(Res.string.onboarding_feature_widgets_title),
|
||||
description = stringResource(Res.string.onboarding_feature_widgets_desc),
|
||||
gradientColors = listOf(
|
||||
Color(0xFF34C759),
|
||||
Color(0xFF30D158)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -267,6 +267,50 @@ object SubscriptionHelper {
|
||||
return UsageCheck(true, null)
|
||||
}
|
||||
|
||||
// ===== RESIDENCE SHARING =====
|
||||
|
||||
/**
|
||||
* Check if user can share a residence (Pro feature).
|
||||
* Returns true (blocked) when limitations are ON and user is not pro.
|
||||
*/
|
||||
fun canShareResidence(): UsageCheck {
|
||||
val subscription = SubscriptionCache.currentSubscription.value
|
||||
?: return UsageCheck(true, null) // Allow if no subscription data (fallback)
|
||||
|
||||
if (!subscription.limitationsEnabled) {
|
||||
return UsageCheck(true, null) // Limitations disabled, allow everything
|
||||
}
|
||||
|
||||
if (currentTier == "pro") {
|
||||
return UsageCheck(true, null) // Pro tier can share
|
||||
}
|
||||
|
||||
// Free users cannot share residences
|
||||
return UsageCheck(false, "share_residence")
|
||||
}
|
||||
|
||||
// ===== CONTRACTOR SHARING =====
|
||||
|
||||
/**
|
||||
* Check if user can share a contractor (Pro feature).
|
||||
* Returns true (blocked) when limitations are ON and user is not pro.
|
||||
*/
|
||||
fun canShareContractor(): UsageCheck {
|
||||
val subscription = SubscriptionCache.currentSubscription.value
|
||||
?: return UsageCheck(true, null) // Allow if no subscription data (fallback)
|
||||
|
||||
if (!subscription.limitationsEnabled) {
|
||||
return UsageCheck(true, null) // Limitations disabled, allow everything
|
||||
}
|
||||
|
||||
if (currentTier == "pro") {
|
||||
return UsageCheck(true, null) // Pro tier can share
|
||||
}
|
||||
|
||||
// Free users cannot share contractors
|
||||
return UsageCheck(false, "share_contractor")
|
||||
}
|
||||
|
||||
// ===== DEPRECATED - Keep for backwards compatibility =====
|
||||
|
||||
@Deprecated("Use isContractorsBlocked() instead", ReplaceWith("isContractorsBlocked()"))
|
||||
|
||||
Reference in New Issue
Block a user