Showing errors returned from Banking Client

This commit is contained in:
dankito 2024-08-26 18:08:48 +02:00
parent ee4287ada1
commit 9d232dfb43
12 changed files with 190 additions and 21 deletions

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.ui.composables.StateHandler
import net.codinux.banking.ui.composables.TransactionsList import net.codinux.banking.ui.composables.TransactionsList
import net.codinux.banking.ui.dialogs.AddAccountDialog import net.codinux.banking.ui.dialogs.AddAccountDialog
import net.codinux.banking.ui.service.Colors import net.codinux.banking.ui.service.Colors
@ -65,5 +66,7 @@ fun App() {
AddAccountDialog { showAddAccountDialog = false } AddAccountDialog { showAddAccountDialog = false }
} }
} }
StateHandler(DI.uiState)
} }
} }

View File

@ -0,0 +1,19 @@
package net.codinux.banking.ui.composables
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
import net.codinux.banking.ui.state.UiState
@Composable
fun StateHandler(uiState: UiState) {
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
bankingClientError?.let {
BankingClientErrorDialog(it) {
uiState.bankingClientErrorOccurred.value = null
}
}
}

View File

@ -9,9 +9,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,6 +18,7 @@ import net.codinux.banking.ui.forms.*
import net.codinux.banking.ui.model.BankInfo import net.codinux.banking.ui.model.BankInfo
import net.codinux.banking.ui.service.Colors import net.codinux.banking.ui.service.Colors
import net.codinux.banking.ui.service.DI import net.codinux.banking.ui.service.DI
import net.codinux.banking.ui.service.Style
private val bankingService = DI.bankingService private val bankingService = DI.bankingService
@ -48,9 +47,9 @@ fun AddAccountDialog(
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Text( Text(
"Bank Konto hinzufügen", "Bank Konto hinzufügen",
color = Color.Black, color = Style.HeaderTextColor,
fontSize = 20.sp, fontSize = Style.HeaderFontSize,
fontWeight = FontWeight.Bold, fontWeight = Style.HeaderFontWeight,
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f) modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)
) )
@ -95,29 +94,27 @@ fun AddAccountDialog(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss, Modifier.width(Style.DialogButtonWidth)) {
Text("Abbrechen") Text("Abbrechen")
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
TextButton( TextButton(
modifier = Modifier.width(150.dp), modifier = Modifier.width(Style.DialogButtonWidth),
enabled = isRequiredDataEntered && isAddingAccount == false, enabled = isRequiredDataEntered && isAddingAccount == false,
onClick = { onClick = {
selectedBank?.let { selectedBank?.let {
isAddingAccount = true isAddingAccount = true
coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available
val errorMessage = DI.bankingService.addAccount(selectedBank!!, loginName, password) val successful = DI.bankingService.addAccount(selectedBank!!, loginName, password)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
isAddingAccount = false isAddingAccount = false
if (errorMessage == null) { if (successful) {
onDismiss() onDismiss()
} else {
// TODO: show error Message
} }
} }
} }

View File

@ -0,0 +1,25 @@
package net.codinux.banking.ui.dialogs
import androidx.compose.runtime.Composable
import net.codinux.banking.ui.model.BankingClientAction
import net.codinux.banking.ui.model.BankingClientError
import net.codinux.banking.ui.model.Config
@Composable
fun BankingClientErrorDialog(error: BankingClientError, onDismiss: (() -> Unit)? = null) {
val title = when (error.erroneousAction) {
BankingClientAction.AddAccount -> "Konto konnte nicht hinzugefügt werden"
BankingClientAction.UpdateAccountTransactions -> "Umsätze konnten nicht aktualisiert werden"
}
val text = if (error.error.internalError != null) {
error.error.internalError!!
} else if (error.error.errorMessagesFromBank.isNotEmpty()) {
"Fehlermeldung Ihrer Bank:" + Config.NewLine + Config.NewLine + error.error.errorMessagesFromBank.joinToString(Config.NewLine)
} else { // should actually never occur
"${error.error.type}" // TODO: add translations for different error types
}
ErrorDialog(text, title, onDismiss = onDismiss)
}

View File

@ -0,0 +1,42 @@
package net.codinux.banking.ui.dialogs
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material.AlertDialog
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import net.codinux.banking.ui.service.Colors
import net.codinux.banking.ui.service.Style
@Composable
fun ErrorDialog(
text: String,
title: String? = null,
confirmButtonText: String = "OK",
onDismiss: (() -> Unit)? = null
) {
AlertDialog(
text = { Text(text) },
title = { title?.let {
Text(
title,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight
)
} },
onDismissRequest = { onDismiss?.invoke() },
confirmButton = {
TextButton({ onDismiss?.invoke() }, Modifier.width(Style.DialogButtonWidth)) {
Text(confirmButtonText, color = Colors.CodinuxSecondaryColor)
}
}
)
}

View File

@ -0,0 +1,6 @@
package net.codinux.banking.ui.model
enum class BankingClientAction {
AddAccount,
UpdateAccountTransactions
}

View File

@ -0,0 +1,10 @@
package net.codinux.banking.ui.model
import net.codinux.banking.client.model.response.Error
data class BankingClientError(
val erroneousAction: BankingClientAction,
val error: Error
) {
override fun toString() = "$erroneousAction $error"
}

View File

@ -0,0 +1,7 @@
package net.codinux.banking.ui.model
object Config {
const val NewLine = "\r\n"
}

View File

@ -6,15 +6,23 @@ import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForCustomer import net.codinux.banking.client.fints4k.FinTs4kBankingClientForCustomer
import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.response.ResponseType
import net.codinux.banking.fints.config.FinTsClientConfiguration import net.codinux.banking.fints.config.FinTsClientConfiguration
import net.codinux.banking.fints.config.FinTsClientOptions import net.codinux.banking.fints.config.FinTsClientOptions
import net.codinux.banking.ui.model.BankInfo import net.codinux.banking.ui.model.BankInfo
import net.codinux.banking.ui.model.BankingClientAction
import net.codinux.banking.ui.model.BankingClientError
import net.codinux.banking.ui.state.UiState
import net.codinux.csv.reader.CsvReader import net.codinux.csv.reader.CsvReader
import net.codinux.log.Log
import net.codinux.log.logger import net.codinux.log.logger
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
class BankingService( class BankingService(
private val uiState: UiState,
private val bankFinder: BankFinder private val bankFinder: BankFinder
) { ) {
@ -37,7 +45,7 @@ class BankingService(
return transactions return transactions
} }
suspend fun addAccount(bank: BankInfo, loginName: String, password: String): String? { suspend fun addAccount(bank: BankInfo, loginName: String, password: String): Boolean {
try { try {
val config = FinTsClientConfiguration(FinTsClientOptions(true)) val config = FinTsClientConfiguration(FinTsClientOptions(true))
val client = FinTs4kBankingClientForCustomer(bank.bankCode, loginName, password, config, SimpleBankingClientCallback { tanChallenge, callback -> val client = FinTs4kBankingClientForCustomer(bank.bankCode, loginName, password, config, SimpleBankingClientCallback { tanChallenge, callback ->
@ -45,18 +53,35 @@ class BankingService(
}) })
val response = client.getAccountDataAsync() val response = client.getAccountDataAsync()
response.data?.let { accountData ->
if (cachedTransactions == null) { if (response.type == ResponseType.Success && response.data != null) {
cachedTransactions = accountData.bookedTransactions handleSuccessfulGetAccountDataResponse(response.data!!)
} else { } else {
cachedTransactions = (cachedTransactions!! + accountData.bookedTransactions).sortedByDescending { it.valueDate } handleUnsuccessfulBankingClientResponse(BankingClientAction.AddAccount, response)
}
return response.type == ResponseType.Success
} catch (e: Throwable) {
log.error(e) { "Could not add account for ${bank.name} $loginName" }
// TODO: handle internal error
return false
} }
} }
return response.error?.internalError ?: response.error?.errorMessagesFromBank?.joinToString("\n") private fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
} catch (e: Throwable) { // TODO: save customer
log.error(e) { "Could not add account for ${bank.name} $loginName" } if (cachedTransactions == null) {
return e.message cachedTransactions = response.bookedTransactions
} else {
cachedTransactions = (cachedTransactions!! + response.bookedTransactions).sortedByDescending { it.valueDate }
}
}
private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) {
response.error?.let { error ->
uiState.bankingClientErrorOccurred.value = BankingClientError(action, error)
} }
} }

View File

@ -2,14 +2,17 @@ package net.codinux.banking.ui.service
import net.codinux.banking.ui.Platform import net.codinux.banking.ui.Platform
import net.codinux.banking.ui.getPlatform import net.codinux.banking.ui.getPlatform
import net.codinux.banking.ui.state.UiState
object DI { object DI {
val uiState = UiState()
val platform: Platform = getPlatform() val platform: Platform = getPlatform()
val bankFinder = BankFinder() val bankFinder = BankFinder()
val bankingService = BankingService(bankFinder) val bankingService = BankingService(uiState, bankFinder)
val formatUtil = FormatUtil() val formatUtil = FormatUtil()

View File

@ -0,0 +1,18 @@
package net.codinux.banking.ui.service
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
object Style {
val HeaderFontSize = 20.sp
val HeaderTextColor: Color = Color.Black // TODO: find a better one like a dark gray
val HeaderFontWeight: FontWeight = FontWeight.Bold
val DialogButtonWidth = 200.dp
}

View File

@ -0,0 +1,14 @@
package net.codinux.banking.ui.state
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.ui.model.BankingClientError
class UiState : ViewModel() {
val transactions = MutableStateFlow<List<AccountTransaction>>(emptyList())
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)
}