ErrorComposable.kt
/*
* This file is part of GNU Taler
* (C) 2025 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3, or (at your option) any later version.
*
* GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
package net.taler.wallet.compose
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
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.filled.Close
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import net.taler.wallet.BottomInsetsSpacer
import net.taler.wallet.R
import net.taler.wallet.backend.TalerErrorInfo
@OptIn(ExperimentalSerializationApi::class)
@Composable
fun ErrorComposable(
error: TalerErrorInfo,
modifier: Modifier = Modifier,
devMode: Boolean,
message: String? = null,
scrollable: Boolean = true,
onRetry: (() -> Unit)? = null,
onClose: (() -> Unit)? = null,
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier
.fillMaxWidth()
.then(if (scrollable) Modifier.verticalScroll(scrollState) else Modifier)
.padding(16.dp),
horizontalAlignment = CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
Spacer(Modifier.size(20.dp))
Icon(
Icons.Rounded.ErrorOutline,
modifier = Modifier
.size(85.dp)
.padding(bottom = 5.dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
Text(
modifier = Modifier.padding(bottom = 16.dp),
text = stringResource(R.string.error),
style = MaterialTheme.typography.displaySmall,
)
val jsonError = remember(error) {
val json = Json {
prettyPrint = true
prettyPrintIndent = " "
}
json.encodeToString(error)
}
message?.let {
Text(message,
Modifier.padding(bottom = 16.dp),
style = MaterialTheme.typography.bodyLarge,
)
}
if (devMode) {
Card(Modifier
.fillMaxWidth()) {
Text(
modifier = Modifier.padding(16.dp),
text = jsonError,
style = MaterialTheme.typography.bodyMedium,
color = CardDefaults.cardColors().contentColor,
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.Start,
)
}
} else {
Text(
text = error.userFacingMsg.ifEmpty {
stringResource(R.string.error_code_only, error.rawCode)
},
fontFamily = FontFamily.Default,
textAlign = TextAlign.Center,
)
}
Column(Modifier
.padding(bottom = 16.dp, top = 22.dp)
.fillMaxWidth(),
horizontalAlignment = CenterHorizontally,
) {
Text(stringResource(R.string.error_export),
modifier = Modifier.padding(bottom = 12.dp),
style = MaterialTheme.typography.titleMedium)
Row(Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
CopyToClipboardButton(
label = "Error",
content = jsonError,
)
ShareButton(
content = jsonError,
)
}
}
if (onRetry != null) Button(
modifier = Modifier.padding(bottom = 16.dp),
onClick = onRetry,
) {
Text(text = stringResource(R.string.transactions_retry))
}
if (onClose != null) Button(
modifier = Modifier.padding(bottom = 16.dp),
onClick = onClose,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError,
),
) {
Icon(
Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize),
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(text = stringResource(R.string.close))
}
if (scrollable) {
BottomInsetsSpacer()
}
}
}
@Preview
@Composable
fun ErrorComposablePreview(devMode: Boolean = false) {
TalerSurface {
ErrorComposable(
error = TalerErrorInfo.makeCustomError(
message = "Some random error",
),
message = "Some custom message",
devMode = devMode,
onClose = {},
)
}
}
@Preview
@Composable
fun ErrorComposableDevPreview() {
ErrorComposablePreview(
devMode = true,
)
}