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 androidx.compose.runtime.getValue
|
||||||
import net.codinux.banking.ui.dialogs.ApplicationErrorDialog
|
import net.codinux.banking.ui.dialogs.ApplicationErrorDialog
|
||||||
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
|
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
|
||||||
|
import net.codinux.banking.ui.dialogs.EnterTanDialog
|
||||||
import net.codinux.banking.ui.state.UiState
|
import net.codinux.banking.ui.state.UiState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StateHandler(uiState: UiState) {
|
fun StateHandler(uiState: UiState) {
|
||||||
val applicationError by uiState.applicationErrorOccurred.collectAsState()
|
val applicationError by uiState.applicationErrorOccurred.collectAsState()
|
||||||
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
|
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
|
||||||
|
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
|
||||||
|
|
||||||
applicationError?.let { error ->
|
applicationError?.let { error ->
|
||||||
ApplicationErrorDialog(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
|
package net.codinux.banking.ui.config
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.tan.ActionRequiringTan
|
||||||
|
|
||||||
object Internationalization {
|
object Internationalization {
|
||||||
|
|
||||||
const val ErrorAddAccount = "Konto konnte nicht hinzugefügt werden"
|
const val ErrorAddAccount = "Konto konnte nicht hinzugefügt werden"
|
||||||
|
|
||||||
const val ErrorUpdateAccountTransactions = "Umsätze konnten nicht aktualisiert 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,23 +40,31 @@ fun AddAccountDialog(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
BaseDialog(
|
||||||
RoundedCornersCard {
|
title = "Bank Konto hinzufügen",
|
||||||
Column(Modifier.background(Color.White).padding(8.dp)) {
|
confirmButtonTitle = "Hinzufügen",
|
||||||
|
confirmButtonEnabled = isRequiredDataEntered && isAddingAccount == false,
|
||||||
|
showProgressIndicatorOnConfirmButton = isAddingAccount,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
onConfirm = {
|
||||||
|
selectedBank?.let {
|
||||||
|
isAddingAccount = true
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth()) {
|
coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available
|
||||||
Text(
|
val successful = DI.bankingService.addAccount(selectedBank!!, loginName, password)
|
||||||
"Bank Konto hinzufügen",
|
|
||||||
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)) {
|
withContext(Dispatchers.Main) {
|
||||||
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
|
isAddingAccount = false
|
||||||
|
|
||||||
|
if (successful) {
|
||||||
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
|
||||||
AutocompleteTextField(
|
AutocompleteTextField(
|
||||||
onValueChange = { selectedBank = it },
|
onValueChange = { selectedBank = it },
|
||||||
|
@ -83,7 +91,6 @@ fun AddAccountDialog(
|
||||||
value = loginName,
|
value = loginName,
|
||||||
onValueChange = { loginName = it },
|
onValueChange = { loginName = it },
|
||||||
label = { Text("Login Name") },
|
label = { Text("Login Name") },
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -92,45 +99,6 @@ fun AddAccountDialog(
|
||||||
PasswordTextField(password) { password = it }
|
PasswordTextField(password) { password = it }
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 bankmeister.composeapp.generated.resources.Res
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import net.codinux.banking.client.SimpleBankingClientCallback
|
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.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.options.GetAccountDataOptions
|
||||||
import net.codinux.banking.client.model.response.Response
|
import net.codinux.banking.client.model.options.RetrieveTransactions
|
||||||
import net.codinux.banking.client.model.response.ResponseType
|
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.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.TanChallengeReceived
|
||||||
import net.codinux.banking.ui.model.error.BankingClientAction
|
import net.codinux.banking.ui.model.error.BankingClientAction
|
||||||
import net.codinux.banking.ui.model.error.BankingClientError
|
import net.codinux.banking.ui.model.error.BankingClientError
|
||||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||||
import net.codinux.banking.ui.state.UiState
|
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
|
||||||
|
|
||||||
|
@ -27,6 +28,10 @@ class BankingService(
|
||||||
private val bankFinder: BankFinder
|
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()
|
private val log by logger()
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,12 +46,7 @@ class BankingService(
|
||||||
|
|
||||||
suspend fun addAccount(bank: BankInfo, loginName: String, password: String): Boolean {
|
suspend fun addAccount(bank: BankInfo, loginName: String, password: String): Boolean {
|
||||||
try {
|
try {
|
||||||
val config = FinTsClientConfiguration(FinTsClientOptions(true))
|
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.bankCode, loginName, password, GetAccountDataOptions(retrieveTransactions = RetrieveTransactions.All)))
|
||||||
val client = FinTs4kBankingClientForCustomer(bank.bankCode, loginName, password, config, SimpleBankingClientCallback { tanChallenge, callback ->
|
|
||||||
// TODO: show EnterTanDialog
|
|
||||||
})
|
|
||||||
|
|
||||||
val response = client.getAccountDataAsync()
|
|
||||||
|
|
||||||
if (response.type == ResponseType.Success && response.data != null) {
|
if (response.type == ResponseType.Success && response.data != null) {
|
||||||
handleSuccessfulGetAccountDataResponse(response.data!!)
|
handleSuccessfulGetAccountDataResponse(response.data!!)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package net.codinux.banking.ui.state
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import net.codinux.banking.client.model.AccountTransaction
|
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.ApplicationError
|
||||||
import net.codinux.banking.ui.model.error.BankingClientError
|
import net.codinux.banking.ui.model.error.BankingClientError
|
||||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||||
|
@ -15,6 +16,8 @@ class UiState : ViewModel() {
|
||||||
|
|
||||||
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)
|
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)
|
||||||
|
|
||||||
|
val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null)
|
||||||
|
|
||||||
|
|
||||||
fun applicationErrorOccurred(erroneousAction: ErroneousAction, exception: Throwable, errorMessage: String? = null) {
|
fun applicationErrorOccurred(erroneousAction: ErroneousAction, exception: Throwable, errorMessage: String? = null) {
|
||||||
val message = errorMessage
|
val message = errorMessage
|
||||||
|
|
|
@ -69,6 +69,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
|
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
|
||||||
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
|
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":
|
"@jsonjoy.com/base64@^1.1.1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578"
|
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"
|
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
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:
|
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
|
||||||
version "1.3.8"
|
version "1.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
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"
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
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:
|
eventemitter3@^4.0.0:
|
||||||
version "4.0.7"
|
version "4.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
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"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
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:
|
node-forge@^1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
|
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"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
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:
|
tree-dump@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac"
|
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:
|
dependencies:
|
||||||
minimalistic-assert "^1.0.0"
|
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:
|
webpack-cli@5.1.4:
|
||||||
version "5.1.4"
|
version "5.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b"
|
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"
|
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
|
||||||
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
|
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:
|
which@^1.2.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
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"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
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:
|
ws@^8.16.0:
|
||||||
version "8.18.0"
|
version "8.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
||||||
|
|
Loading…
Reference in New Issue