Add notification preferences UI and subscription verification on launch
- Add NotificationPreferencesScreen (Android) and NotificationPreferencesView (iOS) - Add NotificationPreferencesViewModel for shared business logic - Wire up notification preferences from ProfileScreen on both platforms - Add subscription verification on app launch for iOS (StoreKit) and Android (Google Play Billing) - Update SubscriptionApi to match Go backend endpoints (/subscription/purchase/) - Update StoreKit Configuration with correct product IDs and pricing ($2.99/month, $27.99/year) - Update Android placeholder prices to match App Store pricing - Fix NotificationPreference model to match Go backend schema 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import com.example.casera.ui.screens.MainScreen
|
||||
import com.example.casera.ui.screens.NotificationPreferencesScreen
|
||||
import com.example.casera.ui.screens.ProfileScreen
|
||||
import com.example.casera.ui.theme.MyCribTheme
|
||||
import com.example.casera.ui.theme.ThemeManager
|
||||
@@ -544,6 +545,17 @@ fun App(
|
||||
navController.navigate(LoginRoute) {
|
||||
popUpTo<ProfileRoute> { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateToNotificationPreferences = {
|
||||
navController.navigate(NotificationPreferencesRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<NotificationPreferencesRoute> {
|
||||
NotificationPreferencesScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,20 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DeviceRegistrationRequest(
|
||||
@SerialName("device_id")
|
||||
val deviceId: String,
|
||||
@SerialName("registration_id")
|
||||
val registrationId: String,
|
||||
val platform: String // "android" or "ios"
|
||||
val platform: String, // "android" or "ios"
|
||||
val name: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DeviceRegistrationResponse(
|
||||
val id: Int,
|
||||
val name: String? = null,
|
||||
@SerialName("device_id")
|
||||
val deviceId: String,
|
||||
@SerialName("registration_id")
|
||||
val registrationId: String,
|
||||
val platform: String,
|
||||
@@ -23,7 +29,6 @@ data class DeviceRegistrationResponse(
|
||||
|
||||
@Serializable
|
||||
data class NotificationPreference(
|
||||
val id: Int,
|
||||
@SerialName("task_due_soon")
|
||||
val taskDueSoon: Boolean = true,
|
||||
@SerialName("task_overdue")
|
||||
@@ -35,11 +40,7 @@ data class NotificationPreference(
|
||||
@SerialName("residence_shared")
|
||||
val residenceShared: Boolean = true,
|
||||
@SerialName("warranty_expiring")
|
||||
val warrantyExpiring: Boolean = true,
|
||||
@SerialName("created_at")
|
||||
val createdAt: String,
|
||||
@SerialName("updated_at")
|
||||
val updatedAt: String
|
||||
val warrantyExpiring: Boolean = true
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -115,3 +115,6 @@ object VerifyResetCodeRoute
|
||||
|
||||
@Serializable
|
||||
object ResetPasswordRoute
|
||||
|
||||
@Serializable
|
||||
object NotificationPreferencesRoute
|
||||
|
||||
@@ -22,7 +22,7 @@ object ApiConfig {
|
||||
fun getBaseUrl(): String {
|
||||
return when (CURRENT_ENV) {
|
||||
Environment.LOCAL -> "http://${getLocalhostAddress()}:8000/api"
|
||||
Environment.DEV -> "https://mycrib.treytartt.com/api"
|
||||
Environment.DEV -> "https://casera.treytartt.com/api"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ object ApiConfig {
|
||||
fun getMediaBaseUrl(): String {
|
||||
return when (CURRENT_ENV) {
|
||||
Environment.LOCAL -> "http://${getLocalhostAddress()}:8000"
|
||||
Environment.DEV -> "https://mycrib.treytartt.com"
|
||||
Environment.DEV -> "https://casera.treytartt.com"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ object ApiConfig {
|
||||
fun getEnvironmentName(): String {
|
||||
return when (CURRENT_ENV) {
|
||||
Environment.LOCAL -> "Local (${getLocalhostAddress()}:8000)"
|
||||
Environment.DEV -> "Dev Server (mycrib.treytartt.com)"
|
||||
Environment.DEV -> "Dev Server (casera.treytartt.com)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
*/
|
||||
suspend fun getNotificationPreferences(token: String): ApiResult<NotificationPreference> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/notifications/preferences/my_preferences/") {
|
||||
val response = client.get("$baseUrl/notifications/preferences/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
request: UpdateNotificationPreferencesRequest
|
||||
): ApiResult<NotificationPreference> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/notifications/preferences/update_preferences/") {
|
||||
val response = client.put("$baseUrl/notifications/preferences/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
|
||||
@@ -71,16 +71,21 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify/process iOS purchase with backend
|
||||
* Used for both new purchases and restore
|
||||
*/
|
||||
suspend fun verifyIOSReceipt(
|
||||
token: String,
|
||||
receiptData: String,
|
||||
transactionId: String
|
||||
): ApiResult<VerificationResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/subscription/verify-ios/") {
|
||||
val response = client.post("$baseUrl/subscription/purchase/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapOf(
|
||||
"platform" to "ios",
|
||||
"receipt_data" to receiptData,
|
||||
"transaction_id" to transactionId
|
||||
))
|
||||
@@ -96,16 +101,21 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify/process Android purchase with backend
|
||||
* Used for both new purchases and restore
|
||||
*/
|
||||
suspend fun verifyAndroidPurchase(
|
||||
token: String,
|
||||
purchaseToken: String,
|
||||
productId: String
|
||||
): ApiResult<VerificationResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/subscription/verify-android/") {
|
||||
val response = client.post("$baseUrl/subscription/purchase/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapOf(
|
||||
"platform" to "android",
|
||||
"purchase_token" to purchaseToken,
|
||||
"product_id" to productId
|
||||
))
|
||||
@@ -120,4 +130,39 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore subscription from iOS/Android receipt
|
||||
*/
|
||||
suspend fun restoreSubscription(
|
||||
token: String,
|
||||
platform: String,
|
||||
receiptData: String? = null,
|
||||
purchaseToken: String? = null,
|
||||
productId: String? = null
|
||||
): ApiResult<VerificationResponse> {
|
||||
return try {
|
||||
val body = mutableMapOf<String, String>("platform" to platform)
|
||||
if (platform == "ios" && receiptData != null) {
|
||||
body["receipt_data"] = receiptData
|
||||
} else if (platform == "android" && purchaseToken != null) {
|
||||
body["purchase_token"] = purchaseToken
|
||||
productId?.let { body["product_id"] = it }
|
||||
}
|
||||
|
||||
val response = client.post("$baseUrl/subscription/restore/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(body)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to restore subscription", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,20 @@ fun MainScreen(
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute)
|
||||
},
|
||||
onLogout = onLogout
|
||||
onLogout = onLogout,
|
||||
onNavigateToNotificationPreferences = {
|
||||
navController.navigate(NotificationPreferencesRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<NotificationPreferencesRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
NotificationPreferencesScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.viewmodel.NotificationPreferencesViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NotificationPreferencesScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: NotificationPreferencesViewModel = viewModel { NotificationPreferencesViewModel() }
|
||||
) {
|
||||
val preferencesState by viewModel.preferencesState.collectAsState()
|
||||
val updateState by viewModel.updateState.collectAsState()
|
||||
|
||||
var taskDueSoon by remember { mutableStateOf(true) }
|
||||
var taskOverdue by remember { mutableStateOf(true) }
|
||||
var taskCompleted by remember { mutableStateOf(true) }
|
||||
var taskAssigned by remember { mutableStateOf(true) }
|
||||
var residenceShared by remember { mutableStateOf(true) }
|
||||
var warrantyExpiring by remember { mutableStateOf(true) }
|
||||
|
||||
// Load preferences on first render
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadPreferences()
|
||||
}
|
||||
|
||||
// Update local state when preferences load
|
||||
LaunchedEffect(preferencesState) {
|
||||
if (preferencesState is ApiResult.Success) {
|
||||
val prefs = (preferencesState as ApiResult.Success).data
|
||||
taskDueSoon = prefs.taskDueSoon
|
||||
taskOverdue = prefs.taskOverdue
|
||||
taskCompleted = prefs.taskCompleted
|
||||
taskAssigned = prefs.taskAssigned
|
||||
residenceShared = prefs.residenceShared
|
||||
warrantyExpiring = prefs.warrantyExpiring
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Notifications", fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
// Header
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(60.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(
|
||||
"Notification Preferences",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
"Choose which notifications you'd like to receive",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (preferencesState) {
|
||||
is ApiResult.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
is ApiResult.Error -> {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
(preferencesState as ApiResult.Error).message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.loadPreferences() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ApiResult.Success, is ApiResult.Idle -> {
|
||||
// Task Notifications Section
|
||||
Text(
|
||||
"Task Notifications",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = AppSpacing.md)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = "Task Due Soon",
|
||||
description = "Reminders for upcoming tasks",
|
||||
icon = Icons.Default.Schedule,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = taskDueSoon,
|
||||
onCheckedChange = {
|
||||
taskDueSoon = it
|
||||
viewModel.updatePreference(taskDueSoon = it)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Task Overdue",
|
||||
description = "Alerts for overdue tasks",
|
||||
icon = Icons.Default.Warning,
|
||||
iconTint = MaterialTheme.colorScheme.error,
|
||||
checked = taskOverdue,
|
||||
onCheckedChange = {
|
||||
taskOverdue = it
|
||||
viewModel.updatePreference(taskOverdue = it)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Task Completed",
|
||||
description = "When someone completes a task",
|
||||
icon = Icons.Default.CheckCircle,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = taskCompleted,
|
||||
onCheckedChange = {
|
||||
taskCompleted = it
|
||||
viewModel.updatePreference(taskCompleted = it)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Task Assigned",
|
||||
description = "When a task is assigned to you",
|
||||
icon = Icons.Default.PersonAdd,
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
checked = taskAssigned,
|
||||
onCheckedChange = {
|
||||
taskAssigned = it
|
||||
viewModel.updatePreference(taskAssigned = it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Other Notifications Section
|
||||
Text(
|
||||
"Other Notifications",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = AppSpacing.md)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = "Property Shared",
|
||||
description = "When someone shares a property with you",
|
||||
icon = Icons.Default.Home,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = residenceShared,
|
||||
onCheckedChange = {
|
||||
residenceShared = it
|
||||
viewModel.updatePreference(residenceShared = it)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Warranty Expiring",
|
||||
description = "Reminders for expiring warranties",
|
||||
icon = Icons.Default.Description,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = warrantyExpiring,
|
||||
onCheckedChange = {
|
||||
warrantyExpiring = it
|
||||
viewModel.updatePreference(warrantyExpiring = it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationToggle(
|
||||
title: String,
|
||||
description: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
iconTint: androidx.compose.ui.graphics.Color,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconTint
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import androidx.compose.runtime.getValue
|
||||
fun ProfileScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onNavigateToNotificationPreferences: () -> Unit = {},
|
||||
viewModel: AuthViewModel = viewModel { AuthViewModel() }
|
||||
) {
|
||||
var firstName by remember { mutableStateOf("") }
|
||||
@@ -208,6 +209,45 @@ fun ProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Notification Preferences Section
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onNavigateToNotificationPreferences() },
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
text = "Notifications",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = "Manage notification preferences",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = "Notification preferences",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription Section - Only show if limitations are enabled
|
||||
if (currentSubscription?.limitationsEnabled == true) {
|
||||
Divider(modifier = Modifier.padding(vertical = AppSpacing.sm))
|
||||
|
||||
@@ -253,8 +253,8 @@ private fun SubscriptionProductsSection(
|
||||
// Monthly Option
|
||||
SubscriptionProductCard(
|
||||
productId = "com.example.casera.pro.monthly",
|
||||
name = "MyCrib Pro Monthly",
|
||||
price = "$4.99/month",
|
||||
name = "Casera Pro Monthly",
|
||||
price = "$2.99/month",
|
||||
description = "Billed monthly",
|
||||
savingsBadge = null,
|
||||
isSelected = false,
|
||||
@@ -265,10 +265,10 @@ private fun SubscriptionProductsSection(
|
||||
// Annual Option
|
||||
SubscriptionProductCard(
|
||||
productId = "com.example.casera.pro.annual",
|
||||
name = "MyCrib Pro Annual",
|
||||
price = "$39.99/year",
|
||||
name = "Casera Pro Annual",
|
||||
price = "$27.99/year",
|
||||
description = "Billed annually",
|
||||
savingsBadge = "Save 33%",
|
||||
savingsBadge = "Save 22%",
|
||||
isSelected = false,
|
||||
isProcessing = isProcessing,
|
||||
onSelect = { onProductSelected("com.example.casera.pro.annual") }
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.casera.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.casera.models.NotificationPreference
|
||||
import com.example.casera.models.UpdateNotificationPreferencesRequest
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NotificationPreferencesViewModel : ViewModel() {
|
||||
|
||||
private val _preferencesState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
|
||||
val preferencesState: StateFlow<ApiResult<NotificationPreference>> = _preferencesState
|
||||
|
||||
private val _updateState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
|
||||
val updateState: StateFlow<ApiResult<NotificationPreference>> = _updateState
|
||||
|
||||
fun loadPreferences() {
|
||||
viewModelScope.launch {
|
||||
_preferencesState.value = ApiResult.Loading
|
||||
val result = APILayer.getNotificationPreferences()
|
||||
_preferencesState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePreference(
|
||||
taskDueSoon: Boolean? = null,
|
||||
taskOverdue: Boolean? = null,
|
||||
taskCompleted: Boolean? = null,
|
||||
taskAssigned: Boolean? = null,
|
||||
residenceShared: Boolean? = null,
|
||||
warrantyExpiring: Boolean? = null
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_updateState.value = ApiResult.Loading
|
||||
val request = UpdateNotificationPreferencesRequest(
|
||||
taskDueSoon = taskDueSoon,
|
||||
taskOverdue = taskOverdue,
|
||||
taskCompleted = taskCompleted,
|
||||
taskAssigned = taskAssigned,
|
||||
residenceShared = residenceShared,
|
||||
warrantyExpiring = warrantyExpiring
|
||||
)
|
||||
val result = APILayer.updateNotificationPreferences(request)
|
||||
_updateState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Update the preferences state with the new values
|
||||
_preferencesState.value = ApiResult.Success(result.data)
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetUpdateState() {
|
||||
_updateState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user