package com.servicebook.ui.reportbug import android.content.Intent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import com.servicebook.R import com.servicebook.ServiceBookApplication import com.servicebook.ui.TestTags import com.servicebook.ui.util.ViewModelFactory @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReportBugScreen( backStack: NavBackStack, viewModel: ReportBugViewModel = viewModel( factory = ViewModelFactory(LocalContext.current.applicationContext as ServiceBookApplication), ), ) { val state by viewModel.state.collectAsStateWithLifecycle() val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app) LaunchedEffect(Unit) { viewModel.effects.collect { effect -> when (effect) { is ReportBugEffect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message) ReportBugEffect.NavigateBack -> backStack.removeLastOrNull() is ReportBugEffect.FallbackToEmail -> { val intent = Intent(Intent.ACTION_SENDTO).apply { data = "mailto:".toUri() putExtra(Intent.EXTRA_EMAIL, arrayOf(effect.recipient)) putExtra(Intent.EXTRA_SUBJECT, effect.subject) putExtra(Intent.EXTRA_TEXT, effect.body) } try { context.startActivity(intent) } catch (_: Exception) { snackbarHostState.showSnackbar(noEmailAppMessage) } } } } } Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { MediumTopAppBar( title = { Text(stringResource(R.string.report_bug_title)) }, navigationIcon = { IconButton(onClick = { backStack.removeLastOrNull() }) { Icon( Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.action_back), ) } }, scrollBehavior = scrollBehavior, ) }, bottomBar = { Surface(tonalElevation = 2.dp) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { TextButton( onClick = { backStack.removeLastOrNull() }, modifier = Modifier.weight(1f), enabled = !state.isSubmitting, ) { Text(stringResource(R.string.action_cancel)) } Button( onClick = { viewModel.onEvent(ReportBugEvent.Submit) }, enabled = state.canSubmit, modifier = Modifier.weight(1f).testTag(TestTags.REPORT_BUG_SUBMIT_BUTTON), shape = MaterialTheme.shapes.extraLarge, ) { Box(contentAlignment = Alignment.Center) { Text( stringResource(R.string.report_bug_submit), modifier = Modifier.alpha(if (state.isSubmitting) 0f else 1f), ) if (state.isSubmitting) { CircularProgressIndicator( modifier = Modifier.size(24.dp).testTag(TestTags.REPORT_BUG_LOADING), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary, ) } } } } } }, ) { padding -> Column( modifier = Modifier .fillMaxSize() .padding(padding) .verticalScroll(rememberScrollState()) .padding(16.dp), ) { // Type Text( text = stringResource(R.string.report_bug_type_section), style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary, ) Row(modifier = Modifier.padding(vertical = 8.dp)) { Button( onClick = { viewModel.onEvent(ReportBugEvent.TypeChanged(ReportType.BUG)) }, modifier = Modifier.weight(1f).testTag(TestTags.REPORT_BUG_TYPE_BUG), enabled = !state.isSubmitting, colors = if (state.type == ReportType.BUG) { androidx.compose.material3.ButtonDefaults.buttonColors() } else { androidx.compose.material3.ButtonDefaults.outlinedButtonColors() }, ) { Text(stringResource(R.string.report_bug_type_bug)) } Button( onClick = { viewModel.onEvent(ReportBugEvent.TypeChanged(ReportType.SUGGESTION)) }, modifier = Modifier.weight(1f).testTag(TestTags.REPORT_BUG_TYPE_SUGGESTION), enabled = !state.isSubmitting, colors = if (state.type == ReportType.SUGGESTION) { androidx.compose.material3.ButtonDefaults.buttonColors() } else { androidx.compose.material3.ButtonDefaults.outlinedButtonColors() }, ) { Text(stringResource(R.string.report_bug_type_suggestion)) } } Spacer(Modifier.height(16.dp)) OutlinedTextField( value = state.summary, onValueChange = { viewModel.onEvent(ReportBugEvent.SummaryChanged(it)) }, label = { Text(stringResource(R.string.report_bug_summary)) }, modifier = Modifier.fillMaxWidth().testTag(TestTags.REPORT_BUG_SUMMARY_FIELD), singleLine = true, enabled = !state.isSubmitting, supportingText = { Text("${state.summary.length}/80", modifier = Modifier.fillMaxWidth()) }, ) Spacer(Modifier.height(16.dp)) OutlinedTextField( value = state.description, onValueChange = { viewModel.onEvent(ReportBugEvent.DescriptionChanged(it)) }, label = { Text(stringResource(R.string.report_bug_description)) }, modifier = Modifier.fillMaxWidth().height(160.dp).testTag(TestTags.REPORT_BUG_DESCRIPTION_FIELD), enabled = !state.isSubmitting, supportingText = { Text("${state.description.length}/2000", modifier = Modifier.fillMaxWidth()) }, ) Spacer(Modifier.height(16.dp)) OutlinedTextField( value = state.contactEmail, onValueChange = { viewModel.onEvent(ReportBugEvent.ContactEmailChanged(it)) }, label = { Text(stringResource(R.string.report_bug_contact_email)) }, modifier = Modifier.fillMaxWidth().testTag(TestTags.REPORT_BUG_EMAIL_FIELD), singleLine = true, enabled = !state.isSubmitting, ) Spacer(Modifier.height(24.dp)) Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow), ) { Column(modifier = Modifier.animateContentSize()) { Row( modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( stringResource(R.string.report_bug_device_info_payload), style = MaterialTheme.typography.titleSmall, modifier = Modifier.weight(1f), ) IconButton( onClick = { viewModel.onEvent(ReportBugEvent.ToggleDeviceInfo) }, modifier = Modifier.testTag(TestTags.REPORT_BUG_DEVICE_INFO_CARD), enabled = !state.isSubmitting, ) { Icon( if (state.showDeviceInfo) Icons.Default.ExpandLess else Icons.Default.ExpandMore, contentDescription = null, ) } } AnimatedVisibility(visible = state.showDeviceInfo) { Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 8.dp)) { Text( stringResource(R.string.report_bug_include_device_info), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), ) Switch( checked = state.includeDeviceInfo, onCheckedChange = { viewModel.onEvent(ReportBugEvent.IncludeDeviceInfoChanged(it)) }, modifier = Modifier.testTag(TestTags.REPORT_BUG_DEVICE_INFO_TOGGLE), enabled = !state.isSubmitting, ) } DeviceInfoItem(stringResource(R.string.report_bug_app_version), state.appVersion) DeviceInfoItem(stringResource(R.string.report_bug_android_version), state.androidVersion) DeviceInfoItem(stringResource(R.string.report_bug_locale), state.locale) DeviceInfoItem(stringResource(R.string.report_bug_device_model), state.deviceModel) DeviceInfoItem(stringResource(R.string.report_bug_vault_configured), state.vaultConfigured.toString()) } } } } } } } @Composable private fun DeviceInfoItem( label: String, value: String, ) { Row(modifier = Modifier.padding(vertical = 2.dp)) { Text(text = "$label: ", style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Bold) Text(text = value, style = MaterialTheme.typography.bodySmall) } }