cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

commit 75d46bd03d2f083deaf75cdf83ab9efb3fb3c7f3
parent a74af4ab88f39800cd02df7fa52efb4398123032
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Tue, 18 Mar 2025 09:44:28 +0100

improve: state handling

Diffstat:
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/Amount.kt | 61+++++++++++++++----------------------------------------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt | 25+++++++++++++------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt | 16+++++++---------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
4 files changed, 89 insertions(+), 88 deletions(-)

diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/Amount.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/Amount.kt @@ -26,13 +26,13 @@ import kotlin.math.floor import kotlin.math.pow import kotlin.math.roundToInt -public class AmountParserException(msg: String? = null, cause: Throwable? = null) : +class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) -public class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : +class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) -public data class Amount( +data class Amount( /** * name of the currency using either a three-character ISO 4217 currency code, * or a regional currency identifier starting with a "*" followed by at most 10 characters. @@ -61,26 +61,26 @@ public data class Amount( val spec: CurrencySpecification? = null, ) : Comparable<Amount> { - public companion object { + companion object { private const val FRACTIONAL_BASE: Int = 100000000 // 1e8 private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""") - public val MAX_VALUE: Long = 2.0.pow(52).toLong() + val MAX_VALUE: Long = 2.0.pow(52).toLong() private const val MAX_FRACTION_LENGTH = 8 - public const val MAX_FRACTION: Int = 99_999_999 + private const val MAX_FRACTION: Int = 99_999_999 - public fun zero(currency: String): Amount { + fun zero(currency: String): Amount { return Amount(checkCurrency(currency), 0, 0) } - public fun fromJSONString(str: String): Amount { + fun fromJSONString(str: String): Amount { val split = str.split(":") if (split.size != 2) throw AmountParserException("Invalid Amount Format") return fromString(split[0], split[1]) } - public fun fromString(currency: String, str: String): Amount { + fun fromString(currency: String, str: String): Amount { // value val valueSplit = str.split(".") val value = checkValue(valueSplit[0].toLongOrNull()) @@ -94,33 +94,12 @@ public data class Amount( return Amount(checkCurrency(currency), value, fraction) } - public fun isValidAmountStr(str: String): Boolean { - if (str.count { it == '.' } > 1) return false - val split = str.split(".") - try { - checkValue(split[0].toLongOrNull()) - } catch (e: AmountParserException) { - return false - } - // also check fraction, if it exists - if (split.size > 1) { - val fractionStr = split[1] - if (fractionStr.length > MAX_FRACTION_LENGTH) return false - val fraction = fractionStr.getFraction() ?: return false - return fraction <= MAX_FRACTION - } - return true - } - private fun String.getFraction(): Int? { return "0.$this".toDoubleOrNull() ?.times(FRACTIONAL_BASE) ?.roundToInt() } - public fun min(currency: String): Amount = Amount(currency, 0, 1) - public fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION) - internal fun checkCurrency(currency: String): String { if (!REGEX_CURRENCY.matches(currency)) @@ -142,9 +121,9 @@ public data class Amount( } - fun toBigDecimal(): BigDecimal = BigDecimal("$amountStr") + fun toBigDecimal(): BigDecimal = BigDecimal(amountStr) - public val amountStr: String + private val amountStr: String get() = if (fraction == 0) "$value" else { var f = fraction var fractionStr = "" @@ -155,7 +134,7 @@ public data class Amount( "$value.$fractionStr" } - public operator fun plus(other: Amount): Amount { + operator fun plus(other: Amount): Amount { check(currency == other.currency) { "Can only subtract from same currency" } val resultValue = value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong() @@ -165,7 +144,7 @@ public data class Amount( return Amount(currency, resultValue, resultFraction) } - public operator fun times(factor: Int): Amount { + operator fun times(factor: Int): Amount { // TODO consider replacing with a faster implementation if (factor == 0) return zero(currency) var result = this @@ -173,13 +152,7 @@ public data class Amount( return result } - public fun withCurrency(currency: String): Amount { - return Amount(checkCurrency(currency), this.value, this.fraction) - } - - fun withSpec(spec: CurrencySpecification?) = copy(spec = spec) - - public operator fun minus(other: Amount): Amount { + operator fun minus(other: Amount): Amount { check(currency == other.currency) { "Can only subtract from same currency" } var resultValue = value var resultFraction = fraction @@ -197,11 +170,7 @@ public data class Amount( return Amount(currency, resultValue, resultFraction) } - public fun isZero(): Boolean { - return value == 0L && fraction == 0 - } - - public fun toJSONString(): String { + fun toJSONString(): String { return "$currency:$amountStr" } diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt @@ -52,7 +52,7 @@ fun AuthorizePaymentScreen(model: WithdrawalViewModel, activity: Activity, clien println("LaunchedEffect: starting authorization") - if (uiState.transactionState == TransactionState.READY_FOR_AUTHORIZATION) { + if (uiState.withdrawalState == WithdrawalState.READY_FOR_AUTHORIZATION) { val transaction = Transaction.Builder(withdrawalAmount) .setCurrency(Currency.getInstance(uiState.currency)) .setInvoiceReference(uiState.encodedWopid) @@ -61,7 +61,7 @@ fun AuthorizePaymentScreen(model: WithdrawalViewModel, activity: Activity, clien .build() try { - client.authorizeTransaction(transaction) + client.authorizeTransaction(transaction) // STOPPED model.setAuthorizing() } catch (e: Exception) { println("FAILED authorizing transaction ${e.message}") @@ -77,16 +77,17 @@ fun AuthorizePaymentScreen(model: WithdrawalViewModel, activity: Activity, clien horizontalAlignment = Alignment.CenterHorizontally ) { - when (uiState.transactionState) { - TransactionState.UNREADY_FOR_AUTHORIZATION -> model.withdrawalOperationFailed(activity) - TransactionState.READY_FOR_AUTHORIZATION -> WalleeProcessingTransaction() - TransactionState.AUTHORIZATION_TRIGGERED -> WalleeProcessingTransaction() - TransactionState.AUTHORIZATION_FAILED -> model.withdrawalOperationFailed(activity) + when (uiState.withdrawalState) { + WithdrawalState.UNREADY_FOR_AUTHORIZATION -> model.withdrawalOperationFailed(activity) + WithdrawalState.READY_FOR_AUTHORIZATION -> WalleeProcessingTransaction() + WithdrawalState.AUTHORIZATION_TRIGGERED -> WalleeProcessingTransaction() + WithdrawalState.AUTHORIZATION_FAILED -> model.withdrawalOperationFailed(activity) // TODO : Coordination of Wallee SDK components and this app must be looked at - TransactionState.AUTHORIZED -> WalleeCompletingPayment(client, model) // TODO: Maybe following is not needed because completion immediately is specified... WalleeCompletingPayment(client, model) - TransactionState.COMPLETION_TRIGGERED -> WalleeProcessingTransaction() - TransactionState.COMPLETED -> model.talerCheckWalleeRequest(activity) - TransactionState.COMPLETION_FAILED -> model.withdrawalOperationFailed(activity) + // -> Input Ben: Broadcast Messages abfangen (welche Broadcast sendet die SDK?) + WithdrawalState.AUTHORIZED -> WalleeCompletingPayment(client, model) // TODO: Maybe following is not needed because completion immediately is specified: "WalleeCompletingPayment(client, model)" + WithdrawalState.COMPLETION_TRIGGERED -> WalleeProcessingTransaction() + WithdrawalState.COMPLETED -> model.talerCheckWalleeRequest(activity) + WithdrawalState.COMPLETION_FAILED -> model.withdrawalOperationFailed(activity) } } } @@ -103,7 +104,7 @@ private fun WalleeCompletingPayment( LaunchedEffect(key1 = Unit) { - if (uiState.transactionState == TransactionState.AUTHORIZED) { + if (uiState.withdrawalState == WithdrawalState.AUTHORIZED) { client.completeTransaction( TransactionCompletion.Builder(uiState.transaction!!.transaction.lineItems) .setCurrency(uiState.transaction!!.transaction.currency) diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt @@ -22,7 +22,6 @@ import android.app.Activity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -33,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp -private const val MAX_RETRIES = 6 // one retry runs 10sec (long-poll) -> 6x10sec = 1min +private const val MAX_RETRIES = 4 @Composable fun RegisterParametersScreen( @@ -46,7 +45,7 @@ fun RegisterParametersScreen( val configuration = LocalConfiguration.current LaunchedEffect(uiState.setupWithdrawalRetries) { - // this will run exactly once -> Unit as parameter to LaunchedEffect. + if (ManageActivity.SIM_WALLET_ENABLED.value) { ManageActivity.simulateParameterRegistration( uiState.terminalsApiBasePath, @@ -54,9 +53,8 @@ fun RegisterParametersScreen( ) } if (model.uiState.value.setupWithdrawalRetries < 1 && - model.uiState.value.transactionState == TransactionState.UNREADY_FOR_AUTHORIZATION) { - println("starting authorization") - model.startAuthorizationWhenReadyOrSetRetry() + model.uiState.value.withdrawalState == WithdrawalState.UNREADY_FOR_AUTHORIZATION) { + model.setAuthorizationReadyOrRetry() } } @@ -66,13 +64,13 @@ fun RegisterParametersScreen( horizontalAlignment = Alignment.CenterHorizontally ) { - if (uiState.transactionState == TransactionState.READY_FOR_AUTHORIZATION) { + if (uiState.withdrawalState == WithdrawalState.READY_FOR_AUTHORIZATION) { navigateToWhenRegistered() return } - if (uiState.setupWithdrawalRetries > MAX_RETRIES && - uiState.transactionState == TransactionState.UNREADY_FOR_AUTHORIZATION) { + if (uiState.setupWithdrawalRetries > MAX_RETRIES-1 && + uiState.withdrawalState == WithdrawalState.UNREADY_FOR_AUTHORIZATION) { println("maximal retries exceeded") model.withdrawalOperationFailed(activity) return diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt @@ -61,7 +61,7 @@ object TalerConstants { } @Stable -interface WithdrawalOperationState { +interface Withdrawal { val requestUid: String val setupWithdrawalRetries: Int val terminalsApiBasePath: String @@ -71,13 +71,46 @@ interface WithdrawalOperationState { val amountError: String val currency: String val withdrawalFees: Amount - val transactionState: TransactionState + val withdrawalState: WithdrawalState val transaction: TransactionResponse? val transactionCompletion: TransactionCompletionResponse? val transactionId: String } -enum class TransactionState { +/** + * +--------------------------+ + * | UNREADY_FOR_AUTHORIZATION | + * +--------------------------+ + * | + * v + * +--------------------------+ + * | READY_FOR_AUTHORIZATION | + * +--------------------------+ + * | + * +-------------------------+ + * | AUTHORIZATION_TRIGGERED | + * +-------------------------+ + * | + * +-----------+-----------+ + * | | + * v v + * +-------------------+ +-------------------------+ + * | AUTHORIZED | | AUTHORIZATION_FAILED | + * +-------------------+ +-------------------------+ + * | + * v + * +----------------------+ + * | COMPLETION_TRIGGERED | + * +----------------------+ + * | + * +-----------+-----------+ + * | | + * v v + * +-------------------+ +-------------------------+ + * | COMPLETED | | COMPLETION_FAILED | + * +-------------------+ +-------------------------+ + */ +enum class WithdrawalState { UNREADY_FOR_AUTHORIZATION, READY_FOR_AUTHORIZATION, AUTHORIZATION_TRIGGERED, @@ -88,7 +121,7 @@ enum class TransactionState { COMPLETED, } -private class MutableWithdrawalOperationState : WithdrawalOperationState { +private class MutableWithdrawal : Withdrawal { override val requestUid: String by derivedStateOf { UUID.randomUUID().toString() } override var setupWithdrawalRetries: Int by mutableIntStateOf(0) override var terminalsApiBasePath: String by mutableStateOf("") @@ -98,7 +131,7 @@ private class MutableWithdrawalOperationState : WithdrawalOperationState { override var amountError: String by mutableStateOf("") override var currency: String by mutableStateOf("") override var withdrawalFees: Amount by mutableStateOf(Amount("", 0,0)) - override var transactionState: TransactionState by mutableStateOf(TransactionState.UNREADY_FOR_AUTHORIZATION) + override var withdrawalState: WithdrawalState by mutableStateOf(WithdrawalState.UNREADY_FOR_AUTHORIZATION) override var transaction: TransactionResponse? by mutableStateOf(null) override var transactionCompletion: TransactionCompletionResponse? by mutableStateOf(null) override val transactionId: String by derivedStateOf { generateTransactionIdentifier() } @@ -111,8 +144,8 @@ class WithdrawalViewModel( private var exchangeSelected = false private var terminalClient: TerminalClient? = null - private val _uiState = MutableStateFlow(MutableWithdrawalOperationState()) - val uiState: StateFlow<WithdrawalOperationState> = _uiState + private val _uiState = MutableStateFlow(MutableWithdrawal()) + val uiState: StateFlow<Withdrawal> = _uiState companion object { fun generateTransactionIdentifier(): String { @@ -131,7 +164,7 @@ class WithdrawalViewModel( terminalClient = TerminalClientImplementation(cfg) //terminalClient = TerminalClientMock() - _uiState.value = MutableWithdrawalOperationState() // reset withdrawal operation + _uiState.value = MutableWithdrawal() // reset withdrawal operation terminalClient!!.terminalsConfig { println("terminal config request result present: ${it.isPresent}") @@ -173,7 +206,7 @@ class WithdrawalViewModel( fun validateInput(amount: String): Boolean { - println("validating amount input: $amount") + //println("validating amount input: $amount") val validAmount = parseAmount(amount) if (validAmount.isPresent) { _uiState.value.amountError = "" @@ -204,22 +237,22 @@ class WithdrawalViewModel( } fun setAuthorizing() { - _uiState.value.transactionState = TransactionState.AUTHORIZATION_TRIGGERED + _uiState.value.withdrawalState = WithdrawalState.AUTHORIZATION_TRIGGERED } fun setCompleting() { - _uiState.value.transactionState = TransactionState.COMPLETION_TRIGGERED + _uiState.value.withdrawalState = WithdrawalState.COMPLETION_TRIGGERED } fun updateWalleeTransactionReply(response: TransactionResponse) { println("updating wallee transaction: $response") _uiState.value.transaction = response - _uiState.value.transactionState = + _uiState.value.withdrawalState = if (response.state.name == "SUCCESSFUL") - TransactionState.AUTHORIZED + WithdrawalState.AUTHORIZED else - TransactionState.AUTHORIZATION_FAILED + WithdrawalState.AUTHORIZATION_FAILED } fun updateWalleeTransactionCompletion( @@ -228,7 +261,7 @@ class WithdrawalViewModel( if (completion.state == State.FAILED) { println("completion of the transaction failed... aborting") - _uiState.value.transactionState = TransactionState.COMPLETION_FAILED + _uiState.value.withdrawalState = WithdrawalState.COMPLETION_FAILED return } @@ -236,18 +269,18 @@ class WithdrawalViewModel( walleeClient.executeFinalBalance() } - _uiState.value.transactionState = TransactionState.COMPLETED + _uiState.value.withdrawalState = WithdrawalState.COMPLETED _uiState.value.transactionCompletion = completion } - fun startAuthorizationWhenReadyOrSetRetry() = viewModelScope.launch { + fun setAuthorizationReadyOrRetry() = viewModelScope.launch { terminalClient!!.retrieveWithdrawalStatus(uiState.value.encodedWopid, 10000) { if (!it.isPresent) { println("setting retry because no status was present") _uiState.value.setupWithdrawalRetries += 1 } else { println("withdrawal status: ${it.get().status }") - _uiState.value.transactionState = TransactionState.READY_FOR_AUTHORIZATION + _uiState.value.withdrawalState = WithdrawalState.READY_FOR_AUTHORIZATION } } } @@ -280,9 +313,9 @@ class WithdrawalViewModel( fun withdrawalOperationFailed(activity: Activity) { viewModelScope.launch { - if (_uiState.value.transactionState == TransactionState.UNREADY_FOR_AUTHORIZATION || - _uiState.value.transactionState == TransactionState.AUTHORIZATION_FAILED || - _uiState.value.transactionState == TransactionState.COMPLETION_FAILED) { + if (_uiState.value.withdrawalState == WithdrawalState.UNREADY_FOR_AUTHORIZATION || + _uiState.value.withdrawalState == WithdrawalState.AUTHORIZATION_FAILED || + _uiState.value.withdrawalState == WithdrawalState.COMPLETION_FAILED) { terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) { SummaryActivity.summary = Summary(