From 22bf109cf7c53809f0ecc14fc152ab2487936bff Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 4 Dec 2025 20:04:42 -0600 Subject: [PATCH] Add email notification preference for task completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add emailTaskCompleted field to NotificationPreference model - Add email preference toggle to notification settings UI (iOS & Android) - Add localized strings for email notifications section - Update ViewModel to support email preference updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../composeResources/values/strings.xml | 5 +++ .../com/example/casera/models/Notification.kt | 10 ++++-- .../screens/NotificationPreferencesScreen.kt | 32 ++++++++++++++++++ .../NotificationPreferencesViewModel.kt | 6 ++-- iosApp/iosApp/Helpers/L10n.swift | 5 +++ iosApp/iosApp/Localizable.xcstrings | 33 +++++++++++++++++++ .../Profile/NotificationPreferencesView.swift | 33 +++++++++++++++++-- 7 files changed, 118 insertions(+), 6 deletions(-) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 966eb75..778b2b4 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -390,6 +390,11 @@ Warranty Expiring Reminders for expiring warranties + + Email Notifications + Task Completed Email + Receive email when a task is completed + Save Cancel diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt index bbb6964..cbc1937 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt @@ -40,7 +40,10 @@ data class NotificationPreference( @SerialName("residence_shared") val residenceShared: Boolean = true, @SerialName("warranty_expiring") - val warrantyExpiring: Boolean = true + val warrantyExpiring: Boolean = true, + // Email preferences + @SerialName("email_task_completed") + val emailTaskCompleted: Boolean = true ) @Serializable @@ -56,7 +59,10 @@ data class UpdateNotificationPreferencesRequest( @SerialName("residence_shared") val residenceShared: Boolean? = null, @SerialName("warranty_expiring") - val warrantyExpiring: Boolean? = null + val warrantyExpiring: Boolean? = null, + // Email preferences + @SerialName("email_task_completed") + val emailTaskCompleted: Boolean? = null ) @Serializable diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt index 88580d8..40ea484 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt @@ -35,6 +35,7 @@ fun NotificationPreferencesScreen( var taskAssigned by remember { mutableStateOf(true) } var residenceShared by remember { mutableStateOf(true) } var warrantyExpiring by remember { mutableStateOf(true) } + var emailTaskCompleted by remember { mutableStateOf(true) } // Load preferences on first render LaunchedEffect(Unit) { @@ -51,6 +52,7 @@ fun NotificationPreferencesScreen( taskAssigned = prefs.taskAssigned residenceShared = prefs.residenceShared warrantyExpiring = prefs.warrantyExpiring + emailTaskCompleted = prefs.emailTaskCompleted } } @@ -294,6 +296,36 @@ fun NotificationPreferencesScreen( } } + // Email Notifications Section + Text( + stringResource(Res.string.notifications_email_section), + 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 = stringResource(Res.string.notifications_email_task_completed), + description = stringResource(Res.string.notifications_email_task_completed_desc), + icon = Icons.Default.Email, + iconTint = MaterialTheme.colorScheme.primary, + checked = emailTaskCompleted, + onCheckedChange = { + emailTaskCompleted = it + viewModel.updatePreference(emailTaskCompleted = it) + } + ) + } + } + Spacer(modifier = Modifier.height(AppSpacing.xl)) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt index 4747830..11b005a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt @@ -36,7 +36,8 @@ class NotificationPreferencesViewModel : ViewModel() { taskCompleted: Boolean? = null, taskAssigned: Boolean? = null, residenceShared: Boolean? = null, - warrantyExpiring: Boolean? = null + warrantyExpiring: Boolean? = null, + emailTaskCompleted: Boolean? = null ) { viewModelScope.launch { _updateState.value = ApiResult.Loading @@ -46,7 +47,8 @@ class NotificationPreferencesViewModel : ViewModel() { taskCompleted = taskCompleted, taskAssigned = taskAssigned, residenceShared = residenceShared, - warrantyExpiring = warrantyExpiring + warrantyExpiring = warrantyExpiring, + emailTaskCompleted = emailTaskCompleted ) val result = APILayer.updateNotificationPreferences(request) _updateState.value = when (result) { diff --git a/iosApp/iosApp/Helpers/L10n.swift b/iosApp/iosApp/Helpers/L10n.swift index de92c3d..cea95ec 100644 --- a/iosApp/iosApp/Helpers/L10n.swift +++ b/iosApp/iosApp/Helpers/L10n.swift @@ -540,6 +540,11 @@ enum L10n { static var warrantyExpiring: String { String(localized: "profile_warranty_expiring") } static var warrantyExpiringDescription: String { String(localized: "profile_warranty_expiring_description") } static var otherNotifications: String { String(localized: "profile_other_notifications") } + + // Email Notifications + static var emailNotifications: String { String(localized: "profile_email_notifications") } + static var emailTaskCompleted: String { String(localized: "profile_email_task_completed") } + static var emailTaskCompletedDescription: String { String(localized: "profile_email_task_completed_description") } } // MARK: - Settings diff --git a/iosApp/iosApp/Localizable.xcstrings b/iosApp/iosApp/Localizable.xcstrings index 428fefc..8d1acbb 100644 --- a/iosApp/iosApp/Localizable.xcstrings +++ b/iosApp/iosApp/Localizable.xcstrings @@ -17844,6 +17844,17 @@ } } }, + "profile_email_notifications" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Email Notifications" + } + } + } + }, "profile_email_required_unique" : { "extractionState" : "manual", "localizations" : { @@ -17909,6 +17920,28 @@ } } }, + "profile_email_task_completed" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Task Completed Email" + } + } + } + }, + "profile_email_task_completed_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Receive email when a task is completed" + } + } + } + }, "profile_first_name" : { "extractionState" : "manual", "localizations" : { diff --git a/iosApp/iosApp/Profile/NotificationPreferencesView.swift b/iosApp/iosApp/Profile/NotificationPreferencesView.swift index d3c6c7a..6f87c87 100644 --- a/iosApp/iosApp/Profile/NotificationPreferencesView.swift +++ b/iosApp/iosApp/Profile/NotificationPreferencesView.swift @@ -191,6 +191,31 @@ struct NotificationPreferencesView: View { Text(L10n.Profile.otherNotifications) } .listRowBackground(Color.appBackgroundSecondary) + + // Email Notifications + Section { + Toggle(isOn: $viewModel.emailTaskCompleted) { + Label { + VStack(alignment: .leading, spacing: 2) { + Text(L10n.Profile.emailTaskCompleted) + .foregroundColor(Color.appTextPrimary) + Text(L10n.Profile.emailTaskCompletedDescription) + .font(.caption) + .foregroundColor(Color.appTextSecondary) + } + } icon: { + Image(systemName: "envelope.fill") + .foregroundColor(Color.appPrimary) + } + } + .tint(Color.appPrimary) + .onChange(of: viewModel.emailTaskCompleted) { _, newValue in + viewModel.updatePreference(emailTaskCompleted: newValue) + } + } header: { + Text(L10n.Profile.emailNotifications) + } + .listRowBackground(Color.appBackgroundSecondary) } } .listStyle(.plain) @@ -223,6 +248,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject { @Published var taskAssigned: Bool = true @Published var residenceShared: Bool = true @Published var warrantyExpiring: Bool = true + @Published var emailTaskCompleted: Bool = true @Published var isLoading: Bool = false @Published var errorMessage: String? @@ -243,6 +269,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject { self.taskAssigned = prefs.taskAssigned self.residenceShared = prefs.residenceShared self.warrantyExpiring = prefs.warrantyExpiring + self.emailTaskCompleted = prefs.emailTaskCompleted self.isLoading = false self.errorMessage = nil } else if let error = result as? ApiResultError { @@ -262,7 +289,8 @@ class NotificationPreferencesViewModelWrapper: ObservableObject { taskCompleted: Bool? = nil, taskAssigned: Bool? = nil, residenceShared: Bool? = nil, - warrantyExpiring: Bool? = nil + warrantyExpiring: Bool? = nil, + emailTaskCompleted: Bool? = nil ) { isSaving = true @@ -274,7 +302,8 @@ class NotificationPreferencesViewModelWrapper: ObservableObject { taskCompleted: taskCompleted.map { KotlinBoolean(bool: $0) }, taskAssigned: taskAssigned.map { KotlinBoolean(bool: $0) }, residenceShared: residenceShared.map { KotlinBoolean(bool: $0) }, - warrantyExpiring: warrantyExpiring.map { KotlinBoolean(bool: $0) } + warrantyExpiring: warrantyExpiring.map { KotlinBoolean(bool: $0) }, + emailTaskCompleted: emailTaskCompleted.map { KotlinBoolean(bool: $0) } ) let result = try await APILayer.shared.updateNotificationPreferences(request: request)