diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt index 9ee8437f9..5f7af609e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt @@ -1,16 +1,25 @@ package to.bitkit.ui.screens.wallets.send +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +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.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -27,75 +36,130 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.theme.Shapes @Composable fun SendErrorScreen( - errorMessage: String, + errorMessage: String?, + stackTrace: String? = null, onRetry: () -> Unit, onClose: () -> Unit, + onSendReportClick: () -> Unit, ) { + var displayDetails by remember { mutableStateOf(false) } + Content( errorMessage = errorMessage, + stackTrace = stackTrace, onRetry = onRetry, onClose = onClose, + displayDetails = displayDetails, + onSendReportClick = onSendReportClick, + onDisplayDetails = { + displayDetails = true + } ) } @Composable private fun Content( - errorMessage: String, + errorMessage: String?, + stackTrace: String?, + displayDetails: Boolean, modifier: Modifier = Modifier, + onDisplayDetails: () -> Unit = {}, + onSendReportClick: () -> Unit = {}, onRetry: () -> Unit = {}, onClose: () -> Unit = {}, ) { - val errorText = errorMessage.ifEmpty { "Unknown error." } + val errorText = errorMessage.orEmpty().ifEmpty { "Unknown error." } Column( modifier = modifier .fillMaxSize() .gradientBackground() + .verticalScroll(rememberScrollState()) .navigationBarsPadding() ) { SheetTopBar(stringResource(R.string.wallet__send_error_tx_failed)) - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) + Box( + modifier = Modifier.weight(1f) ) { - Spacer(modifier = Modifier.height(16.dp)) - - BodyM(text = errorText, color = Colors.White64) - - Spacer(modifier = Modifier.weight(1f)) Image( painter = painterResource(R.drawable.cross), contentDescription = null, modifier = Modifier .fillMaxWidth() .height(256.dp) + .align(Alignment.Center) ) - Spacer(modifier = Modifier.weight(1f)) - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxWidth() + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) ) { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM(text = errorText, color = Colors.White64) + + AnimatedVisibility( + visible = displayDetails && !stackTrace.isNullOrBlank(), + label = "" + ) { + BodyM( + text = stackTrace.orEmpty(), + color = Colors.Red, + modifier = Modifier + .padding(vertical = 16.dp) + .fillMaxWidth() + .background(color = Colors.White10, shape = Shapes.medium) + .padding(16.dp) + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + + } + } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + AnimatedVisibility(visible = !stackTrace.isNullOrBlank() && !displayDetails) { + SecondaryButton( + text = "Technical Details", // TODO Transifex + onClick = onDisplayDetails, + modifier = Modifier + ) + } + AnimatedVisibility(visible = !stackTrace.isNullOrBlank() && displayDetails) { + SecondaryButton( + text = "Send Anonymous Report", // TODO Transifex + onClick = onSendReportClick, + modifier = Modifier + ) + } + if (stackTrace.isNullOrBlank()) { SecondaryButton( text = stringResource(R.string.common__cancel), onClick = onClose, + fullWidth = true, modifier = Modifier - .weight(1f) .testTag("Close") ) - PrimaryButton( - text = stringResource(R.string.common__try_again), - onClick = onRetry, - modifier = Modifier.weight(1f) - ) } - - Spacer(modifier = Modifier.height(16.dp)) + PrimaryButton( + text = stringResource(R.string.common__try_again), + onClick = onRetry, + ) } + + Spacer(modifier = Modifier.height(16.dp)) } } @@ -106,7 +170,83 @@ private fun Preview() { BottomSheetPreview { Content( errorMessage = stringResource(R.string.wallet__send_error_create_tx), + stackTrace = "Test render error\n" + + "\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in WalletsStack (at SceneView.tsx:132)", + modifier = Modifier.sheetHeight(), + displayDetails = true, + onDisplayDetails = {} + ) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewDontDisplayDetails() { + AppThemeSurface { + BottomSheetPreview { + Content( + errorMessage = stringResource(R.string.wallet__send_error_create_tx), + stackTrace = "Test render error\n" + + "\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in Tabbar (at WalletNavigator.tsx:59)\n" + + " in WalletsStack (at SceneView.tsx:132)", modifier = Modifier.sheetHeight(), + displayDetails = false, + onDisplayDetails = {} ) } } @@ -119,7 +259,10 @@ private fun PreviewUnknown() { BottomSheetPreview { Content( errorMessage = "", + stackTrace = null, modifier = Modifier.sheetHeight(), + displayDetails = false, + onDisplayDetails = {} ) } } diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 544d76291..b6855ff46 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -74,11 +74,18 @@ fun SendSheet( appViewModel.clearClipboardForAutoRead() navController.navigate(SendRoute.Success) } + is SendEffect.NavigateToQuickPay -> navController.navigate(SendRoute.QuickPay) is SendEffect.NavigateToWithdrawConfirm -> navController.navigate(SendRoute.WithdrawConfirm) is SendEffect.NavigateToWithdrawError -> navController.navigate(SendRoute.WithdrawError) is SendEffect.NavigateToFee -> navController.navigate(SendRoute.FeeRate) is SendEffect.NavigateToFeeCustom -> navController.navigate(SendRoute.FeeCustom) + is SendEffect.PaymentError -> navController.navigate( + SendRoute.Error( + errorMessage = it.errorMessage, + stackTrace = it.stackTrace + ) + ) } } } @@ -238,6 +245,9 @@ fun SendSheet( } else { navController.navigate(SendRoute.Success) } + }, + onSendReportClick = { + }, onClose = { appViewModel.hideSheet() @@ -298,5 +308,5 @@ sealed interface SendRoute { data object Success : SendRoute @Serializable - data class Error(val errorMessage: String) : SendRoute + data class Error(val errorMessage: String?, val stackTrace: String? = null) : SendRoute } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 0ca94c5fb..dca38c8d8 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -947,12 +947,7 @@ class AppViewModel @Inject constructor( lightningRepo.sync() }.onFailure { e -> Logger.error(msg = "Error sending onchain payment", e = e, context = TAG) - toast( - type = Toast.ToastType.ERROR, - title = "Error Sending", - description = e.message ?: "Unknown error" - ) - hideSheet() + setSendEffect(SendEffect.PaymentError(errorMessage = e.message, stackTrace = e.stackTraceToString())) } } @@ -976,8 +971,7 @@ class AppViewModel @Inject constructor( setSendEffect(SendEffect.PaymentSuccess()) }.onFailure { e -> Logger.error("Error sending lightning payment", e, context = TAG) - toast(e) - hideSheet() + setSendEffect(SendEffect.PaymentError(errorMessage = e.message, stackTrace = e.stackTraceToString())) } } } @@ -1502,6 +1496,8 @@ sealed class SendEffect { data object NavigateToQuickPay : SendEffect() data object NavigateToFee : SendEffect() data object NavigateToFeeCustom : SendEffect() + + data class PaymentError(val errorMessage: String?, val stackTrace: String) : SendEffect() data class PaymentSuccess(val sheet: NewTransactionSheetDetails? = null) : SendEffect() }