From 7d39f94271828e7beb1953ff8ff2bcc40bfbcb20 Mon Sep 17 00:00:00 2001 From: dankito Date: Tue, 27 Aug 2024 01:33:38 +0200 Subject: [PATCH] Added EnterTanDialog --- .../composeResources/drawable/zoom_in.png | Bin 0 -> 1672 bytes .../composeResources/drawable/zoom_out.png | Bin 0 -> 1548 bytes .../banking/ui/composables/StateHandler.kt | 8 + .../banking/ui/config/Internationalization.kt | 13 ++ .../banking/ui/dialogs/AddAccountDialog.kt | 136 +++++------ .../codinux/banking/ui/dialogs/BaseDialog.kt | 76 +++++++ .../banking/ui/dialogs/EnterTanDialog.kt | 214 ++++++++++++++++++ .../banking/ui/model/TanChallengeReceived.kt | 11 + .../banking/ui/service/BankingService.kt | 22 +- .../net/codinux/banking/ui/state/UiState.kt | 3 + kotlin-js-store/yarn.lock | 47 ++++ 11 files changed, 435 insertions(+), 95 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/zoom_in.png create mode 100644 composeApp/src/commonMain/composeResources/drawable/zoom_out.png create mode 100644 composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/TanChallengeReceived.kt diff --git a/composeApp/src/commonMain/composeResources/drawable/zoom_in.png b/composeApp/src/commonMain/composeResources/drawable/zoom_in.png new file mode 100644 index 0000000000000000000000000000000000000000..c04269dbae02356829122c248017124ff0597445 GIT binary patch literal 1672 zcmV;326y?1P)Px*LPo{fJ|!7m0I{@*tH^10-d> zKFhc=D1gLDpa2v=VhJRkUM)ZYB$hzp>D2-h zKw=3bo?b0L0VI|{;_1}_ZW+LKyIY@EuyHal2JS0atO3CNS5$v@e->u|xU=vRR!|(i z?cWm1O>FwbEua>knwv=4toi-Br}S9j|xOz`Pw__7C{U(zcuh zfb;YA&byoYX|iMsr!XgYI2`tMAZHIWcSmsmmCJFOpZZrA3(tq|`$Osn8Jk}e!1nWQ zYmL`@k)MF|WFMf#0C>$m!w*JK^+H;$;HRhd9~91YI5+11{u`{}3~OuP&nljj{~!Ic zg)t`E;4A{5^7;RkGU?^)gQBt)zOK%2r26T`I^rvu0YJ7fK?DFX+j%o5c%%xxE9?sT zm-lsmThRGm#AOx7m;EOKv_+au20#(MvG6Dz6#x3X`(g}Wts%P!_ zc}3%cG4PPnVr>$Dxc2*aFstxuKS6+yl`CQca4F~6z-w#(tj2AgcCBh#*8v}2_OYp; zoeYFs_#Cc~8!)0T6&iT+v~yX{sL5l6@8*yI;Ko6&MY6F17J&Xs4yK-mTYQ9Flq`jSS$! z{|9S6WcMSP1vSC;SINC+W<4ua%g5SJ05CjkD90Yta8}v5(`m@+5G(cMr3nsz(G)M_ zS7R<_s#f6uR2J|7?i(h3D+ZpEu}M9%8;4^7Z5Pzh#3fwNkWmcAfM3)Ct~dzyboVNQa2r6^0s=)pyu)0^ zKwARG)PtJej{&ST3g?-yeb_Fjo*;NUvG?+}u-1s*W<>s z`yb2+o~frMx|0tbPmjFFBCe1s%*htuCS*WhU$h>(pwTqCKn9u}1Q7rY2!ZXtQZM>m z$?r&%FsjcGgsv!^0sbJ|0~pJWi&aK9D8&GH2=VeS)k5cLpN%Dm0${qMxr7`U*B0hv zlHVm32M~q8Is`;!LxKWOBNGm2XFyV255J!6r+`BBxCoDuK~hh0BX17ZoXUVzmnxZ(7&Wo zmaNgAAm~Df-6dY4Zq?MTMF9-0W?=>Q)n^FYKi8ittF7en*)nUvf2NFcoIGbsND2V$ zX9ODN3gKn|MAPFwQNQ2NJ)}8jsjo`PO6&%kdh>G90kA>oK!ajASFrWIR67=v#5?! S8xOGn0000Px)#z{m$RCr$Pom+O>HV}pfa`#brCABwFNpT;zi^RLoL+w!ANS&3GyGR~tp$0`d z6ios%0}umM!*g;HCj?0T-&`;tz!=a&Q~@J;b-g$!0AV9g017}@0pZK52Pgnx1%xlJ z9-sh(6%f9>dVm5DRzUdj>H*gbV7sl>#}#ax2FAc$r^Olo+^?ql+5O)B*;x1nD`@IJ z_bx(~vaQLjBkC{*l(K zjI)W)O228W@KZe#VKm0T?$SJBQ2?8V>hHHu(Z_Z=Jzk5K!_)qr*ED~tMF6-sZ*SUg zb0$8~qGQx(%q!g2^}bK!F?}uCHaUPUq6S;@9{^`$vFvtF6RG(*95>? z__8{}ff}a=b;K9K^@`02NdOQ>J1^!Hc2wcVVt3F#zg5Yupu-%^;c0)1IWgxs(Eupo z8w>kY*XQKJB$?^ouFM)7UDcJ-;;@5(B|;%LZ?FMu9x6BL@w$9Y@p}P~wMa2uzyg4L$olZK z-*GJ?rX40UN*Kq~GqC_{9x7MudWV0K4~Ps!Vg@n+U}xVCus z>0!)P$v2< z0A`k-kE5T2m3_4S$>|O85XpWaLtTB4GZbM$Piu7rys?W0Fqw>}!%>!nv6#_W_t3>y zKRX1yd%4l$Q_O~Q?)g0w10g8@W;hziIsZ3 z^TzW|#F3epCoB$NG$xHkV;8rKmP#@j8=}&{Gh@B0000 ApplicationErrorDialog(error) { @@ -24,4 +26,10 @@ fun StateHandler(uiState: UiState) { } } + tanChallengeReceived?.let { tanChallengeReceived -> + EnterTanDialog(tanChallengeReceived) { + uiState.tanChallengeReceived.value = null + } + } + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt index 33cae92..3a2ee9d 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt @@ -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" + } + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/AddAccountDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/AddAccountDialog.kt index 6b687a8..9a5a881 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/AddAccountDialog.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/AddAccountDialog.kt @@ -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)) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt new file mode 100644 index 0000000..d15b0bc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt @@ -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) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt new file mode 100644 index 0000000..ef19c9a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt @@ -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) } ?: "", + 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, { })) { } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/TanChallengeReceived.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/TanChallengeReceived.kt new file mode 100644 index 0000000..c386d84 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/TanChallengeReceived.kt @@ -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" +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt index 1551b1a..9d4dffd 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt @@ -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!!) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt index abda1e7..4105cb0 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt @@ -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(null) + val tanChallengeReceived = MutableStateFlow(null) + fun applicationErrorOccurred(erroneousAction: ErroneousAction, exception: Throwable, errorMessage: String? = null) { val message = errorMessage diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 0e3af6d..939218c 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -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"