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,
    )
}