Successfully created and wrote to new file: /home/Messier82/projects/service-book/app/src/main/java/com/servicebook/ui/reportbug/ReportBugViewModel.kt. Here is the updated code: package com.servicebook.ui.reportbug import android.os.Build import androidx.lifecycle.viewModelScope import com.servicebook.BuildConfig import com.servicebook.R import com.servicebook.ui.util.GatedViewModel import com.servicebook.ui.util.ViewModelDependencies import com.servicebook.util.SentryWrapper import com.servicebook.util.SentryWrapperImpl import io.sentry.SentryFeedback import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.Locale enum class ReportType { BUG, SUGGESTION } data class ReportBugState( val type: ReportType = ReportType.BUG, val summary: String = "", val description: String = "", val contactEmail: String = "", val includeDeviceInfo: Boolean = true, val isSubmitting: Boolean = false, val error: String? = null, val appVersion: String = "", val androidVersion: String = "", val locale: String = "", val deviceModel: String = "", val vaultConfigured: Boolean = false, ) { val canSubmit: Boolean get() = summary.isNotBlank() && description.isNotBlank() && !isSubmitting } sealed class ReportBugEvent { data class TypeChanged(val type: ReportType) : ReportBugEvent() data class SummaryChanged(val summary: String) : ReportBugEvent() data class DescriptionChanged(val description: String) : ReportBugEvent() data class ContactEmailChanged(val email: String) : ReportBugEvent() data class IncludeDeviceInfoChanged(val include: Boolean) : ReportBugEvent() data object Submit : ReportBugEvent() data object DismissError : ReportBugEvent() } sealed class ReportBugEffect { data class ShowSnackbar(val message: String) : ReportBugEffect() data object NavigateBack : ReportBugEffect() data class FallbackToEmail( val recipient: String, val subject: String, val body: String, ) : ReportBugEffect() } class ReportBugViewModel( dependencies: ViewModelDependencies, private val sentryWrapper: SentryWrapper = SentryWrapperImpl(), ) : GatedViewModel(dependencies) { private val _state = MutableStateFlow(ReportBugState()) val state: StateFlow = _state.asStateFlow() private val _effects = Channel(Channel.BUFFERED) val effects = _effects.receiveAsFlow() private var retryCount = 0 init { viewModelScope.launch { val prefs = dependencies.preferences.preferences.first() _state.update { it.copy( appVersion = BuildConfig.VERSION_NAME, androidVersion = Build.VERSION.RELEASE, locale = Locale.getDefault().toString(), deviceModel = Build.MODEL, vaultConfigured = !prefs.vaultUriString.isNullOrEmpty(), ) } } } fun onEvent(event: ReportBugEvent) { when (event) { is ReportBugEvent.TypeChanged -> _state.update { it.copy(type = event.type) } is ReportBugEvent.SummaryChanged -> _state.update { it.copy(summary = event.summary.take(80)) } is ReportBugEvent.DescriptionChanged -> _state.update { it.copy(description = event.description.take(2000)) } is ReportBugEvent.ContactEmailChanged -> _state.update { it.copy(contactEmail = event.email) } is ReportBugEvent.IncludeDeviceInfoChanged -> _state.update { it.copy(includeDeviceInfo = event.include) } ReportBugEvent.Submit -> submitReport() ReportBugEvent.DismissError -> _state.update { it.copy(error = null) } } } private fun submitReport() { if (!state.value.canSubmit) return _state.update { it.copy(isSubmitting = true, error = null) } viewModelScope.launch { try { val s = _state.value val feedback = SentryFeedback().apply { message = "${s.summary}\n\n${s.description}" email = s.contactEmail.ifBlank { null } } val tags = mutableMapOf( "type" to s.type.name.lowercase(), "app_version" to s.appVersion, "android_version" to s.androidVersion, "locale" to s.locale, "device_model" to s.deviceModel, "vault_configured" to s.vaultConfigured.toString(), ) val submissionTags = if (s.includeDeviceInfo) tags else emptyMap() if (sentryWrapper.isEnabled()) { sentryWrapper.captureFeedback(feedback, submissionTags) } else { sentryWrapper.lazyInitAndSendFeedback(app, BuildConfig.SENTRY_DSN, feedback, submissionTags) } _effects.send(ReportBugEffect.ShowSnackbar(app.getString(R.string.report_bug_success))) _effects.send(ReportBugEffect.NavigateBack) } catch (e: Exception) { if (retryCount < 2) { retryCount++ _state.update { it.copy( isSubmitting = false, error = app.getString(R.string.report_bug_retrying, retryCount), ) } submitReport() } else { _state.update { it.copy( isSubmitting = false, error = app.getString(R.string.report_bug_error_failed), ) } fallbackToEmail() } } } } private fun fallbackToEmail() { val s = _state.value val subject = "[${s.type.name}] ${s.summary}" val body = buildString { appendLine(s.description) if (s.includeDeviceInfo) { appendLine() appendLine("--- Device Info ---") appendLine("App Version: ${s.appVersion}") appendLine("Android Version: ${s.androidVersion}") appendLine("Locale: ${s.locale}") appendLine("Device Model: ${s.deviceModel}") appendLine("Vault Configured: ${s.vaultConfigured}") } } viewModelScope.launch { _effects.send(ReportBugEffect.FallbackToEmail("reports@servicebook.app", subject, body)) } } }