Added EnterTanDialog
This commit is contained in:
parent
b6b88d31a1
commit
7d39f94271
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -5,12 +5,14 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import net.codinux.banking.ui.dialogs.ApplicationErrorDialog
|
||||
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
|
||||
import net.codinux.banking.ui.dialogs.EnterTanDialog
|
||||
import net.codinux.banking.ui.state.UiState
|
||||
|
||||
@Composable
|
||||
fun StateHandler(uiState: UiState) {
|
||||
val applicationError by uiState.applicationErrorOccurred.collectAsState()
|
||||
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
|
||||
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
|
||||
|
||||
applicationError?.let { error ->
|
||||
ApplicationErrorDialog(error) {
|
||||
|
@ -24,4 +26,10 @@ fun StateHandler(uiState: UiState) {
|
|||
}
|
||||
}
|
||||
|
||||
tanChallengeReceived?.let { tanChallengeReceived ->
|
||||
EnterTanDialog(tanChallengeReceived) {
|
||||
uiState.tanChallengeReceived.value = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,22 @@
|
|||
package net.codinux.banking.ui.config
|
||||
|
||||
import net.codinux.banking.client.model.tan.ActionRequiringTan
|
||||
|
||||
object Internationalization {
|
||||
|
||||
const val ErrorAddAccount = "Konto konnte nicht hinzugefügt werden"
|
||||
|
||||
const val ErrorUpdateAccountTransactions = "Umsätze konnten nicht aktualisiert werden"
|
||||
|
||||
|
||||
fun getTextForActionRequiringTan(action: ActionRequiringTan): String = when (action) {
|
||||
ActionRequiringTan.GetAnonymousBankInfo,
|
||||
ActionRequiringTan.GetAccountInfo,
|
||||
ActionRequiringTan.GetTanMedia
|
||||
-> "zum Einloggen"
|
||||
ActionRequiringTan.GetTransactions -> "um Kontoumsätze abzuholen"
|
||||
ActionRequiringTan.TransferMoney -> "um Geld zu überweisen"
|
||||
ActionRequiringTan.ChangeTanMedium -> "um das TAN Medium zu ändern"
|
||||
}
|
||||
|
||||
}
|
|
@ -40,97 +40,65 @@ fun AddAccountDialog(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
RoundedCornersCard {
|
||||
Column(Modifier.background(Color.White).padding(8.dp)) {
|
||||
BaseDialog(
|
||||
title = "Bank Konto hinzufügen",
|
||||
confirmButtonTitle = "Hinzufügen",
|
||||
confirmButtonEnabled = isRequiredDataEntered && isAddingAccount == false,
|
||||
showProgressIndicatorOnConfirmButton = isAddingAccount,
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = {
|
||||
selectedBank?.let {
|
||||
isAddingAccount = true
|
||||
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
"Bank Konto hinzufügen",
|
||||
color = Style.HeaderTextColor,
|
||||
fontSize = Style.HeaderFontSize,
|
||||
fontWeight = Style.HeaderFontWeight,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)
|
||||
)
|
||||
coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available
|
||||
val successful = DI.bankingService.addAccount(selectedBank!!, loginName, password)
|
||||
|
||||
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
|
||||
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
isAddingAccount = false
|
||||
|
||||
AutocompleteTextField(
|
||||
onValueChange = { selectedBank = it },
|
||||
label = { Text("Bank (Suche mit Name, Bankleitzahl oder Ort)") },
|
||||
getItemTitle = { bank -> bank.name },
|
||||
fetchSuggestions = { query -> bankingService.findBanks(query) }
|
||||
) { bank ->
|
||||
Text(bank.name)
|
||||
|
||||
Row(Modifier.fillMaxWidth().padding(top = 8.dp)) {
|
||||
Text(bank.bankCode)
|
||||
|
||||
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = Color.Gray)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text("Online-Banking Zugangsdaten", color = Colors.CodinuxSecondaryColor)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = loginName,
|
||||
onValueChange = { loginName = it },
|
||||
label = { Text("Login Name") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
PasswordTextField(password) { password = it }
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = onDismiss, Modifier.width(Style.DialogButtonWidth)) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier.width(Style.DialogButtonWidth),
|
||||
enabled = isRequiredDataEntered && isAddingAccount == false,
|
||||
onClick = {
|
||||
selectedBank?.let {
|
||||
isAddingAccount = true
|
||||
|
||||
coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available
|
||||
val successful = DI.bankingService.addAccount(selectedBank!!, loginName, password)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
isAddingAccount = false
|
||||
|
||||
if (successful) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (isAddingAccount) {
|
||||
CircularProgressIndicator(Modifier.padding(end = 6.dp))
|
||||
}
|
||||
|
||||
Text("Hinzufügen")
|
||||
if (successful) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column {
|
||||
|
||||
AutocompleteTextField(
|
||||
onValueChange = { selectedBank = it },
|
||||
label = { Text("Bank (Suche mit Name, Bankleitzahl oder Ort)") },
|
||||
getItemTitle = { bank -> bank.name },
|
||||
fetchSuggestions = { query -> bankingService.findBanks(query) }
|
||||
) { bank ->
|
||||
Text(bank.name)
|
||||
|
||||
Row(Modifier.fillMaxWidth().padding(top = 8.dp)) {
|
||||
Text(bank.bankCode)
|
||||
|
||||
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = Color.Gray)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text("Online-Banking Zugangsdaten", color = Colors.CodinuxSecondaryColor)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = loginName,
|
||||
onValueChange = { loginName = it },
|
||||
label = { Text("Login Name") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
PasswordTextField(password) { password = it }
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package net.codinux.banking.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import net.codinux.banking.ui.config.Colors
|
||||
import net.codinux.banking.ui.config.Style
|
||||
import net.codinux.banking.ui.forms.*
|
||||
|
||||
@Composable
|
||||
fun BaseDialog(
|
||||
title: String,
|
||||
confirmButtonTitle: String = "OK",
|
||||
confirmButtonEnabled: Boolean = true,
|
||||
showProgressIndicatorOnConfirmButton: Boolean = false,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
|
||||
|
||||
Dialog(onDismissRequest = onDismiss, properties) {
|
||||
RoundedCornersCard {
|
||||
Column(Modifier.background(Color.White).padding(8.dp)) {
|
||||
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
title,
|
||||
color = Style.HeaderTextColor,
|
||||
fontSize = Style.HeaderFontSize,
|
||||
fontWeight = Style.HeaderFontWeight,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)
|
||||
)
|
||||
|
||||
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
|
||||
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
content()
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = onDismiss, Modifier.width(Style.DialogButtonWidth)) {
|
||||
Text("Abbrechen", color = Colors.CodinuxSecondaryColor)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier.width(Style.DialogButtonWidth),
|
||||
enabled = confirmButtonEnabled,
|
||||
onClick = { onConfirm?.invoke() ?: onDismiss() }
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (showProgressIndicatorOnConfirmButton) {
|
||||
CircularProgressIndicator(Modifier.padding(end = 6.dp))
|
||||
}
|
||||
|
||||
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
package net.codinux.banking.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import bankmeister.composeapp.generated.resources.Res
|
||||
import net.codinux.banking.client.model.CustomerAccountViewInfo
|
||||
import net.codinux.banking.client.model.tan.*
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.config.Internationalization
|
||||
import net.codinux.banking.ui.forms.OutlinedTextField
|
||||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
import net.codinux.log.Log
|
||||
import org.jetbrains.compose.resources.imageResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.jetbrains.skia.Image
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import bankmeister.composeapp.generated.resources.zoom_in
|
||||
import bankmeister.composeapp.generated.resources.zoom_out
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () -> Unit) {
|
||||
|
||||
val challenge = tanChallengeReceived.tanChallenge
|
||||
|
||||
var showTanMethodsDropDownMenu by remember { mutableStateOf(false) }
|
||||
var showTanMediaDropDownMenu by remember { mutableStateOf(false) }
|
||||
|
||||
var tanImageHeight by remember { mutableStateOf(250) }
|
||||
val minTanImageHeight = 100
|
||||
val maxTanImageHeight = 500
|
||||
|
||||
val textFieldFocus = remember { FocusRequester() }
|
||||
|
||||
var enteredTan by remember { mutableStateOf("") }
|
||||
|
||||
|
||||
BaseDialog(
|
||||
title = "TAN Eingabe",
|
||||
confirmButtonEnabled = enteredTan.length > 2,
|
||||
onConfirm = {
|
||||
tanChallengeReceived.callback(EnterTanResult(enteredTan))
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = {
|
||||
tanChallengeReceived.callback(EnterTanResult(null))
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row {
|
||||
Text("${challenge.customer.bankName}, Nutzer ${challenge.customer.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}")
|
||||
}
|
||||
Text(
|
||||
"TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}",
|
||||
Modifier.padding(top = 6.dp)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Row(Modifier.padding(top = 16.dp)) {
|
||||
ExposedDropdownMenuBox(showTanMethodsDropDownMenu, { isExpanded -> showTanMethodsDropDownMenu = isExpanded }, Modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(
|
||||
value = challenge.selectedTanMethod.displayName,
|
||||
onValueChange = { Log.info { "TanMethod value changed: $it" }},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text("TAN Verfahren") },
|
||||
readOnly = true,
|
||||
trailingIcon = { Icon(Icons.Filled.ArrowDropDown, "Alle TAN Medien anzeigen") }
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(showTanMethodsDropDownMenu, { showTanMethodsDropDownMenu = false }) {
|
||||
challenge.availableTanMethods.sortedBy { it.identifier }.forEach { tanMethod ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showTanMethodsDropDownMenu = false
|
||||
Log.info { "User selected TanMethod $tanMethod" }
|
||||
// TODO: change TanMethod
|
||||
}
|
||||
) {
|
||||
Text(tanMethod.displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (challenge.availableTanMedia.isNotEmpty()) {
|
||||
Row(Modifier.padding(top = 16.dp)) {
|
||||
ExposedDropdownMenuBox(showTanMediaDropDownMenu, { isExpanded -> showTanMediaDropDownMenu = isExpanded }, Modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(
|
||||
value = challenge.selectedTanMedium?.let { getTanMediumDisplayName(it) } ?: "<Keines ausgewählt>",
|
||||
onValueChange = { Log.info { "TanMedia value changed: $it" }},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text("TAN Medium") },
|
||||
readOnly = true,
|
||||
trailingIcon = { Icon(Icons.Filled.ArrowDropDown, "Alle TAN Verfahren anzeigen") }
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(showTanMediaDropDownMenu, { showTanMediaDropDownMenu = false }) {
|
||||
challenge.availableTanMedia.sortedBy { it.status }.forEach { tanMedium ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showTanMediaDropDownMenu = false
|
||||
Log.info { "User selected TanMedium $tanMedium" }
|
||||
// TODO: change TanMethod
|
||||
}
|
||||
) {
|
||||
Text(getTanMediumDisplayName(tanMedium))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (challenge.tanImage != null || challenge.flickerCode != null) {
|
||||
Column(Modifier.fillMaxWidth().padding(top = 12.dp)) {
|
||||
if (challenge.flickerCode != null) {
|
||||
Text("Es tut uns Leid, für die TAN müsste ein Flickercode angezeigt werden, was wir noch nicht implementiert haben.")
|
||||
Text("Bitte wählen Sie ein anderes TAN Verfahren, z. B. manuelle TAN Eingabe wie chipTAN manuell.", Modifier.padding(top = 6.dp))
|
||||
}
|
||||
|
||||
challenge.tanImage?.let { tanImage ->
|
||||
if (tanImage.decodingSuccessful) {
|
||||
val byteArray = Base64.decode(tanImage.imageBytesBase64)
|
||||
val bitmap = Image.makeFromEncoded(byteArray)
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Größe")
|
||||
TextButton({ tanImageHeight -= 25}, enabled = tanImageHeight > minTanImageHeight, colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
|
||||
Icon(imageResource(Res.drawable.zoom_out), contentDescription = "Bild mit enkodierter TAN verkleiner", Modifier.size(32.dp).padding(horizontal = 6.dp))
|
||||
}
|
||||
TextButton({ tanImageHeight += 25}, enabled = tanImageHeight < maxTanImageHeight, colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
|
||||
Icon(imageResource(Res.drawable.zoom_in), contentDescription = "Bild mit enkodierter TAN vergrößern", Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(bitmap.toComposeImageBitmap(), "Bild mit enkodierter TAN", Modifier.height(tanImageHeight.dp), contentScale = ContentScale.FillHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.padding(top = 16.dp)) {
|
||||
Text("Hinweis Ihrer Bank:", Modifier.padding(bottom = 6.dp))
|
||||
Text(challenge.messageToShowToUser)
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxWidth().padding(top = 16.dp)) {
|
||||
OutlinedTextField(
|
||||
value = enteredTan,
|
||||
onValueChange = { enteredTan = it },
|
||||
label = { Text("TAN eingeben") },
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(textFieldFocus),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = if (challenge.selectedTanMethod.allowedTanFormat == AllowedTanFormat.Numeric) KeyboardType.Number else KeyboardType.Text
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(textFieldFocus) {
|
||||
textFieldFocus.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
fun getTanMediumDisplayName(tanMedium: net.codinux.banking.client.model.tan.TanMedium): String {
|
||||
tanMedium.tanGenerator?.let { tanGenerator ->
|
||||
return "${tanMedium.mediumName} ${tanGenerator.cardNumber}"
|
||||
}
|
||||
|
||||
tanMedium.mobilePhone?.let { mobilePhone ->
|
||||
return "${tanMedium.mediumName} ${mobilePhone.concealedPhoneNumber ?: mobilePhone.phoneNumber}"
|
||||
}
|
||||
|
||||
return tanMedium.mediumName ?: ""
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun EnterTanDialogPreview() {
|
||||
val tanMethod = TanMethod("photoTan", TanMethodType.photoTan, "910", 6, AllowedTanFormat.Numeric)
|
||||
val tanImage = TanImage("image/png", "")
|
||||
|
||||
val customer = CustomerAccountViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank")
|
||||
|
||||
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, customer)
|
||||
|
||||
EnterTanDialog(TanChallengeReceived(tanChallenge, { })) { }
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package net.codinux.banking.ui.model
|
||||
|
||||
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||
import net.codinux.banking.client.model.tan.TanChallenge
|
||||
|
||||
data class TanChallengeReceived(
|
||||
val tanChallenge: TanChallenge,
|
||||
val callback: (EnterTanResult) -> Unit
|
||||
) {
|
||||
override fun toString() = "$tanChallenge"
|
||||
}
|
|
@ -3,21 +3,22 @@ package net.codinux.banking.ui.service
|
|||
import bankmeister.composeapp.generated.resources.Res
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.client.SimpleBankingClientCallback
|
||||
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForCustomer
|
||||
import net.codinux.banking.client.fints4k.FinTs4kBankingClient
|
||||
import net.codinux.banking.client.model.AccountTransaction
|
||||
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.client.model.options.GetAccountDataOptions
|
||||
import net.codinux.banking.client.model.options.RetrieveTransactions
|
||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||
import net.codinux.banking.client.model.response.*
|
||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||
import net.codinux.banking.fints.config.FinTsClientOptions
|
||||
import net.codinux.banking.ui.model.BankInfo
|
||||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
import net.codinux.banking.ui.model.error.BankingClientAction
|
||||
import net.codinux.banking.ui.model.error.BankingClientError
|
||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||
import net.codinux.banking.ui.state.UiState
|
||||
import net.codinux.csv.reader.CsvReader
|
||||
import net.codinux.log.Log
|
||||
import net.codinux.log.logger
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
|
||||
|
@ -27,6 +28,10 @@ class BankingService(
|
|||
private val bankFinder: BankFinder
|
||||
) {
|
||||
|
||||
private val client = FinTs4kBankingClient(FinTsClientConfiguration(FinTsClientOptions(true)), SimpleBankingClientCallback { tanChallenge, callback ->
|
||||
uiState.tanChallengeReceived.value = TanChallengeReceived(tanChallenge, callback)
|
||||
})
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
|
@ -41,12 +46,7 @@ class BankingService(
|
|||
|
||||
suspend fun addAccount(bank: BankInfo, loginName: String, password: String): Boolean {
|
||||
try {
|
||||
val config = FinTsClientConfiguration(FinTsClientOptions(true))
|
||||
val client = FinTs4kBankingClientForCustomer(bank.bankCode, loginName, password, config, SimpleBankingClientCallback { tanChallenge, callback ->
|
||||
// TODO: show EnterTanDialog
|
||||
})
|
||||
|
||||
val response = client.getAccountDataAsync()
|
||||
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.bankCode, loginName, password, GetAccountDataOptions(retrieveTransactions = RetrieveTransactions.All)))
|
||||
|
||||
if (response.type == ResponseType.Success && response.data != null) {
|
||||
handleSuccessfulGetAccountDataResponse(response.data!!)
|
||||
|
|
|
@ -3,6 +3,7 @@ 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.TanChallengeReceived
|
||||
import net.codinux.banking.ui.model.error.ApplicationError
|
||||
import net.codinux.banking.ui.model.error.BankingClientError
|
||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||
|
@ -15,6 +16,8 @@ class UiState : ViewModel() {
|
|||
|
||||
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)
|
||||
|
||||
val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null)
|
||||
|
||||
|
||||
fun applicationErrorOccurred(erroneousAction: ErroneousAction, exception: Throwable, errorMessage: String? = null) {
|
||||
val message = errorMessage
|
||||
|
|
|
@ -69,6 +69,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
|
||||
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
|
||||
|
||||
"@js-joda/timezone@2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.3.0.tgz#72878f6dc8afef20c29906e5d8d946f91618a2c3"
|
||||
integrity sha512-DHXdNs0SydSqC5f0oRJPpTcNfnpRojgBqMCFupQFv6WgeZAjU3DBx+A7JtaGPP3dHrP2Odi2N8Vf+uAm/8ynCQ==
|
||||
|
||||
"@jsonjoy.com/base64@^1.1.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578"
|
||||
|
@ -422,6 +427,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||
|
||||
abort-controller@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||
dependencies:
|
||||
event-target-shim "^5.0.0"
|
||||
|
||||
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||
|
@ -1082,6 +1094,11 @@ etag@~1.8.1:
|
|||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
event-target-shim@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
eventemitter3@^4.0.0:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
|
@ -1991,6 +2008,13 @@ neo-async@^2.6.2:
|
|||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
node-fetch@2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-forge@^1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
|
||||
|
@ -2734,6 +2758,11 @@ toidentifier@1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
tree-dump@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac"
|
||||
|
@ -2832,6 +2861,11 @@ wbuf@^1.1.0, wbuf@^1.7.3:
|
|||
dependencies:
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
webpack-cli@5.1.4:
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b"
|
||||
|
@ -2964,6 +2998,14 @@ websocket-extensions@>=0.1.1:
|
|||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
|
||||
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which@^1.2.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
|
@ -3011,6 +3053,11 @@ wrappy@1:
|
|||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@8.5.0:
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
|
||||
integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
|
||||
|
||||
ws@^8.16.0:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
||||
|
|
Loading…
Reference in New Issue