Implemented TransferMoneyDialog
This commit is contained in:
parent
1368ece023
commit
dde54b75d3
|
@ -5,16 +5,16 @@ import androidx.compose.material.SnackbarHostState
|
|||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.launch
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.dialogs.AddAccountDialog
|
||||
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.dialogs.*
|
||||
import net.codinux.banking.ui.screens.ExportScreen
|
||||
import net.codinux.banking.ui.state.UiState
|
||||
|
||||
private val formatUtil = DI.formatUtil
|
||||
|
||||
@Composable
|
||||
fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
||||
val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState()
|
||||
val showTransferMoneyDialog by uiState.showTransferMoneyDialog.collectAsState()
|
||||
val showExportScreen by uiState.showExportScreen.collectAsState()
|
||||
|
||||
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
|
||||
|
@ -28,6 +28,10 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
|||
AddAccountDialog { uiState.showAddAccountDialog.value = false }
|
||||
}
|
||||
|
||||
if (showTransferMoneyDialog) {
|
||||
TransferMoneyDialog { uiState.showTransferMoneyDialog.value = false }
|
||||
}
|
||||
|
||||
if (showExportScreen) {
|
||||
ExportScreen { uiState.showExportScreen.value = false }
|
||||
}
|
||||
|
@ -54,7 +58,7 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
|||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
DI.uiState.transactionsRetrievedEvents.collect { event ->
|
||||
uiState.transactionsRetrievedEvents.collect { event ->
|
||||
val messagePrefix = if (event.newTransactions.isEmpty()) {
|
||||
"Keine neuen Umsätze"
|
||||
} else if (event.newTransactions.size == 1) {
|
||||
|
@ -70,6 +74,16 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
uiState.transferredMoneyEvents.collect { event ->
|
||||
snackbarHostState.showSnackbar(
|
||||
message = "${formatUtil.formatAmount(event.amount, event.currency)} wurden erfolgreich an ${event.recipientName} überwiesen",
|
||||
actionLabel = "Das freut mich",
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -8,6 +8,8 @@ object Internationalization {
|
|||
|
||||
const val ErrorUpdateAccountTransactions = "Umsätze konnten nicht aktualisiert werden"
|
||||
|
||||
const val ErrorTransferMoney = "Überweisung konnte nicht ausgeführt werden"
|
||||
|
||||
|
||||
fun getTextForActionRequiringTan(action: ActionRequiringTan): String = when (action) {
|
||||
ActionRequiringTan.GetAnonymousBankInfo,
|
||||
|
|
|
@ -10,6 +10,7 @@ fun ApplicationErrorDialog(error: ApplicationError, onDismiss: (() -> Unit)? = n
|
|||
val title = when (error.erroneousAction) {
|
||||
ErroneousAction.AddAccount -> Internationalization.ErrorAddAccount
|
||||
ErroneousAction.UpdateAccountTransactions -> Internationalization.ErrorUpdateAccountTransactions
|
||||
ErroneousAction.TransferMoney -> Internationalization.ErrorTransferMoney
|
||||
}
|
||||
|
||||
// add exception stacktrace?
|
||||
|
|
|
@ -11,6 +11,7 @@ fun BankingClientErrorDialog(error: BankingClientError, onDismiss: (() -> Unit)?
|
|||
val title = when (error.erroneousAction) {
|
||||
BankingClientAction.AddAccount -> Internationalization.ErrorAddAccount
|
||||
BankingClientAction.UpdateAccountTransactions -> Internationalization.ErrorUpdateAccountTransactions
|
||||
BankingClientAction.TransferMoney -> Internationalization.ErrorTransferMoney
|
||||
}
|
||||
|
||||
val text = if (error.error.internalError != null) {
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
package net.codinux.banking.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.codinux.banking.client.model.Amount
|
||||
import net.codinux.banking.client.model.BankAccountFeatures
|
||||
import net.codinux.banking.ui.IOorDefault
|
||||
import net.codinux.banking.ui.composables.BankIcon
|
||||
import net.codinux.banking.ui.config.Colors
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.forms.OutlinedTextField
|
||||
import net.codinux.banking.ui.forms.Select
|
||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||
|
||||
private val uiState = DI.uiState
|
||||
|
||||
private val bankingService = DI.bankingService
|
||||
|
||||
@Composable
|
||||
fun TransferMoneyDialog(
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val userAccounts = uiState.userAccounts.value
|
||||
val accountsToUserAccount = userAccounts.sortedBy { it.displayIndex }
|
||||
.flatMap { user -> user.accounts.sortedBy { it.displayIndex }.map { it to user } }.toMap()
|
||||
|
||||
val accountsSupportingTransferringMoney = userAccounts.flatMap { it.accounts }
|
||||
.filter { it.supportsAnyFeature(BankAccountFeatures.TransferMoney, BankAccountFeatures.InstantPayment) }
|
||||
|
||||
if (accountsSupportingTransferringMoney.isEmpty()) {
|
||||
uiState.applicationErrorOccurred(ErroneousAction.TransferMoney, "Keines Ihrer Konten unterstützt das Überweisen von Geld")
|
||||
onDismiss()
|
||||
return
|
||||
}
|
||||
|
||||
var senderAccount by remember { mutableStateOf(accountsSupportingTransferringMoney.first()) }
|
||||
|
||||
var recipientName by remember { mutableStateOf("") }
|
||||
var recipientAccountIdentifier by remember { mutableStateOf("") }
|
||||
var amount by remember { mutableStateOf("") }
|
||||
var paymentReference by remember { mutableStateOf("") }
|
||||
val accountSupportsInstantTransfer by remember(senderAccount) { derivedStateOf { senderAccount.supportsAnyFeature(BankAccountFeatures.InstantPayment) } }
|
||||
var instantTransfer by remember { mutableStateOf(false) }
|
||||
|
||||
val isRequiredDataEntered by remember(recipientName, recipientAccountIdentifier, amount) {
|
||||
// TODO: add check if it's a valid IBAN
|
||||
derivedStateOf { recipientName.length > 2 && recipientAccountIdentifier.length >= 18 && amount.isNotBlank() }
|
||||
}
|
||||
|
||||
var isTransferringMoney by remember { mutableStateOf(false) }
|
||||
|
||||
val verticalSpace = 8.dp
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
||||
fun confirmCalled() {
|
||||
isTransferringMoney = true
|
||||
|
||||
coroutineScope.launch(Dispatchers.IOorDefault) {
|
||||
val successful = bankingService.transferMoney(
|
||||
accountsToUserAccount[senderAccount]!!, senderAccount,
|
||||
recipientName, recipientAccountIdentifier,
|
||||
Amount(amount), "EUR", // TODO: add input field for currency
|
||||
paymentReference, instantTransfer && accountSupportsInstantTransfer
|
||||
// TODO: determine BIC to IBAN
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
isTransferringMoney = false
|
||||
|
||||
if (successful) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BaseDialog(
|
||||
title = "Neue Überweisung ...",
|
||||
confirmButtonTitle = "Überweisen",
|
||||
confirmButtonEnabled = isRequiredDataEntered && isTransferringMoney == false,
|
||||
showProgressIndicatorOnConfirmButton = isTransferringMoney,
|
||||
useMoreThanPlatformDefaultWidthOnMobile = true,
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = { confirmCalled() }
|
||||
) {
|
||||
Select(
|
||||
"Konto", "Alle Konten anzeigen",
|
||||
accountsSupportingTransferringMoney, senderAccount, { senderAccount = it },
|
||||
{ account -> "${accountsToUserAccount[account]?.displayName} ${account.displayName}" },
|
||||
{ BankIcon(accountsToUserAccount[senderAccount]) }
|
||||
) { account ->
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
BankIcon(accountsToUserAccount[account], Modifier.padding(end = 6.dp))
|
||||
|
||||
Text("${accountsToUserAccount[account]?.displayName} ${account.displayName}")
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.padding(top = verticalSpace)) {
|
||||
OutlinedTextField(
|
||||
value = recipientName,
|
||||
onValueChange = { recipientName = it },
|
||||
label = { Text("Name des Empfängers / der Empfängerin") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(verticalSpace))
|
||||
|
||||
OutlinedTextField(
|
||||
value = recipientAccountIdentifier,
|
||||
onValueChange = { recipientAccountIdentifier = it },
|
||||
label = { Text("IBAN") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(verticalSpace))
|
||||
|
||||
OutlinedTextField(
|
||||
value = amount,
|
||||
onValueChange = { amount = it },
|
||||
label = { Text("Betrag") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Next)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(verticalSpace))
|
||||
|
||||
OutlinedTextField(
|
||||
value = paymentReference,
|
||||
onValueChange = { paymentReference = it },
|
||||
label = { Text("Verwendungszweck") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
|
||||
)
|
||||
|
||||
Row(Modifier.padding(top = verticalSpace), verticalAlignment = Alignment.CenterVertically) {
|
||||
Switch(
|
||||
checked = instantTransfer,
|
||||
onCheckedChange = { instantTransfer = it },
|
||||
enabled = accountSupportsInstantTransfer,
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
colors = SwitchDefaults.colors(checkedThumbColor = Colors.CodinuxSecondaryColor)
|
||||
)
|
||||
|
||||
Text("Echtzeitüberweisung (evtl. kostenpflichtig)", Modifier.clickable { instantTransfer = !instantTransfer })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package net.codinux.banking.ui.forms
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowDropUp
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun <T> Select(
|
||||
label: String,
|
||||
contentDescription: String,
|
||||
items: Collection<T>,
|
||||
selectedItem: T,
|
||||
onSelectedItemChanged: (T) -> Unit,
|
||||
getItemDisplayText: (T) -> String,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
dropDownItemContent: @Composable ((T) -> Unit)? = null
|
||||
) {
|
||||
var showDropDownMenu by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(showDropDownMenu, { isExpanded -> showDropDownMenu = isExpanded }, Modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(
|
||||
value = getItemDisplayText(selectedItem),
|
||||
onValueChange = { },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text(label) },
|
||||
readOnly = true,
|
||||
maxLines = 1,
|
||||
trailingIcon = {
|
||||
if (showDropDownMenu) {
|
||||
Icon(Icons.Filled.ArrowDropUp, contentDescription)
|
||||
} else {
|
||||
Icon(Icons.Filled.ArrowDropDown, contentDescription)
|
||||
}
|
||||
},
|
||||
leadingIcon = leadingIcon
|
||||
)
|
||||
|
||||
// due to a bug (still not fixed since 2021) in ExposedDropdownMenu its popup has a maximum width of 800 pixel / 320dp which is too less to fit
|
||||
// TextField's width, see https://issuetracker.google.com/issues/205589613
|
||||
DropdownMenu(showDropDownMenu, { showDropDownMenu = false }, Modifier.exposedDropdownSize(true)) {
|
||||
items.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showDropDownMenu = false
|
||||
onSelectedItemChanged(item)
|
||||
}
|
||||
) {
|
||||
dropDownItemContent?.invoke(item) ?: Text(getItemDisplayText(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,5 +2,8 @@ package net.codinux.banking.ui.model.error
|
|||
|
||||
enum class BankingClientAction {
|
||||
AddAccount,
|
||||
UpdateAccountTransactions
|
||||
|
||||
UpdateAccountTransactions,
|
||||
|
||||
TransferMoney
|
||||
}
|
|
@ -2,5 +2,8 @@ package net.codinux.banking.ui.model.error
|
|||
|
||||
enum class ErroneousAction {
|
||||
AddAccount,
|
||||
UpdateAccountTransactions
|
||||
|
||||
UpdateAccountTransactions,
|
||||
|
||||
TransferMoney
|
||||
}
|
|
@ -4,7 +4,7 @@ import net.codinux.banking.client.model.BankAccount
|
|||
import net.codinux.banking.client.model.UserAccount
|
||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||
|
||||
data class AccountTransactionsRetrieved(
|
||||
data class AccountTransactionsRetrievedEvent(
|
||||
val user: UserAccount,
|
||||
val account: BankAccount,
|
||||
val newTransactions: List<AccountTransactionViewModel>
|
|
@ -0,0 +1,9 @@
|
|||
package net.codinux.banking.ui.model.events
|
||||
|
||||
import net.codinux.banking.client.model.Amount
|
||||
|
||||
data class TransferredMoneyEvent(
|
||||
val recipientName: String,
|
||||
val amount: Amount,
|
||||
val currency: String
|
||||
)
|
|
@ -9,6 +9,7 @@ import net.codinux.banking.client.model.*
|
|||
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.request.TransferMoneyRequestForUser
|
||||
import net.codinux.banking.client.model.response.*
|
||||
import net.codinux.banking.client.service.BankingModelService
|
||||
import net.codinux.banking.dataaccess.BankingRepository
|
||||
|
@ -22,7 +23,8 @@ import net.codinux.banking.ui.model.AccountTransactionViewModel
|
|||
import net.codinux.banking.ui.model.BankInfo
|
||||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
import net.codinux.banking.ui.model.error.*
|
||||
import net.codinux.banking.ui.model.events.AccountTransactionsRetrieved
|
||||
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
|
||||
import net.codinux.banking.ui.model.events.TransferredMoneyEvent
|
||||
import net.codinux.banking.ui.state.UiState
|
||||
import net.codinux.csv.reader.CsvReader
|
||||
import net.codinux.log.logger
|
||||
|
@ -81,7 +83,7 @@ class BankingService(
|
|||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not add account for ${bank.name} $loginName" }
|
||||
|
||||
uiState.applicationErrorOccurred(ErroneousAction.AddAccount, e)
|
||||
uiState.applicationErrorOccurred(ErroneousAction.AddAccount, null, e)
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -157,7 +159,7 @@ class BankingService(
|
|||
}
|
||||
|
||||
val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities)
|
||||
uiState.fireNewTransactionsRetrieved(AccountTransactionsRetrieved(user, account, transactionsViewModel))
|
||||
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(user, account, transactionsViewModel))
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not save updated account transactions for user $user" }
|
||||
|
@ -175,6 +177,29 @@ class BankingService(
|
|||
}
|
||||
|
||||
|
||||
suspend fun transferMoney(user: UserAccountEntity, account: BankAccountEntity,
|
||||
recipientName: String, recipientAccountIdentifier: String, amount: Amount, currency: String,
|
||||
paymentReference: String? = null, instantTransfer: Boolean = false, recipientBankIdentifier: String? = null): Boolean {
|
||||
val response = client.transferMoneyAsync(TransferMoneyRequestForUser(
|
||||
user.bankCode, user.loginName, user.password!!,
|
||||
BankAccountIdentifier(account.identifier, account.subAccountNumber, account.iban), // TODO: use BankingClient's one
|
||||
recipientName, recipientAccountIdentifier, recipientBankIdentifier,
|
||||
amount, "EUR",
|
||||
paymentReference, instantTransfer
|
||||
))
|
||||
|
||||
if (response.error != null) {
|
||||
handleUnsuccessfulBankingClientResponse(BankingClientAction.TransferMoney, response)
|
||||
} else if (response.type == ResponseType.Success) {
|
||||
uiState.dispatchTransferredMoney(TransferredMoneyEvent(recipientName, amount, currency))
|
||||
|
||||
updateAccountTransactions(user, account)
|
||||
}
|
||||
|
||||
return response.type == ResponseType.Success
|
||||
}
|
||||
|
||||
|
||||
private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) {
|
||||
log.error { "$action was not successful: $response" }
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ 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
|
||||
import net.codinux.banking.ui.model.events.AccountTransactionsRetrieved
|
||||
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
|
||||
import net.codinux.banking.ui.model.events.TransferredMoneyEvent
|
||||
|
||||
class UiState : ViewModel() {
|
||||
|
||||
|
@ -22,14 +23,17 @@ class UiState : ViewModel() {
|
|||
|
||||
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())
|
||||
|
||||
val transactionsRetrievedEvents = MutableSharedFlow<AccountTransactionsRetrieved>()
|
||||
val transactionsRetrievedEvents = MutableSharedFlow<AccountTransactionsRetrievedEvent>()
|
||||
|
||||
suspend fun fireNewTransactionsRetrieved(event: AccountTransactionsRetrieved) {
|
||||
coroutineScope {
|
||||
launch {
|
||||
transactionsRetrievedEvents.emit(event)
|
||||
}
|
||||
}
|
||||
suspend fun dispatchNewTransactionsRetrieved(event: AccountTransactionsRetrievedEvent) {
|
||||
transactionsRetrievedEvents.emit(event)
|
||||
}
|
||||
|
||||
|
||||
val transferredMoneyEvents = MutableSharedFlow<TransferredMoneyEvent>()
|
||||
|
||||
suspend fun dispatchTransferredMoney(event: TransferredMoneyEvent) {
|
||||
transferredMoneyEvents.emit(event)
|
||||
}
|
||||
|
||||
|
||||
|
@ -41,8 +45,11 @@ class UiState : ViewModel() {
|
|||
|
||||
val showAddAccountDialog = MutableStateFlow(false)
|
||||
|
||||
val showTransferMoneyDialog = MutableStateFlow(false)
|
||||
|
||||
val showExportScreen = MutableStateFlow(false)
|
||||
|
||||
|
||||
val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null)
|
||||
|
||||
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)
|
||||
|
@ -50,9 +57,10 @@ class UiState : ViewModel() {
|
|||
val applicationErrorOccurred = MutableStateFlow<ApplicationError?>(null)
|
||||
|
||||
|
||||
fun applicationErrorOccurred(erroneousAction: ErroneousAction, exception: Throwable, errorMessage: String? = null) {
|
||||
fun applicationErrorOccurred(erroneousAction: ErroneousAction, errorMessage: String? = null, exception: Throwable? = null) {
|
||||
val message = errorMessage
|
||||
?: exception.message ?: exception::class.simpleName // TODO: find a better way to get error message from exception
|
||||
?: exception?.message // TODO: find a better way to get error message from exception
|
||||
?: exception?.let { it::class.simpleName }
|
||||
|
||||
if (message != null) {
|
||||
applicationErrorOccurred.value = ApplicationError(erroneousAction, message, exception)
|
||||
|
|
Loading…
Reference in New Issue