commit 009bf7f6077ce602418564ebafb693de742f0ab5 parent 60a1a97a811029e32bc787c58c826e746e8bc144 Author: fsb2 <benjamin.fehrensen@bfh.ch> Date: Sat, 12 Apr 2025 10:39:03 +0200 New Design & Cleanup Diffstat:
12 files changed, 309 insertions(+), 264 deletions(-)
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt @@ -54,7 +54,7 @@ class MainActivity : ComponentActivity() { TalerLogo() TalerButton( - text = "Withdrawal", + text = "Withdraw", onClick = { ctx.startActivity(Intent(this@MainActivity, WithdrawalActivity::class.java)) } ) diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt @@ -60,4 +60,6 @@ interface TerminalClient { encodedWopid: String, callback: (Boolean) -> Unit ) + + fun shutdownDispatcher() } \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt @@ -42,7 +42,8 @@ class TerminalClientImplementation ( private val config: TalerTerminalConfig ): TerminalClient { - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val executor = Executors.newSingleThreadExecutor() + private val dispatcher = executor.asCoroutineDispatcher() private val client: OkHttpClient = OkHttpClient.Builder() @@ -208,6 +209,10 @@ class TerminalClientImplementation ( } } + override fun shutdownDispatcher() { + executor.shutdown() + } + override fun abortWithdrawal( encodedWopid: String, callback: (Boolean) -> Unit diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientMock.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientMock.kt @@ -102,6 +102,10 @@ class TerminalClientMock: TerminalClient { println("aborting withdrawal") } + override fun shutdownDispatcher() { + println("shutting down dispatcher") + } + private fun mockWopidOrReservePubKey(): String { val wopid = ByteArray(32) 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 @@ -135,7 +135,7 @@ data class Amount( } operator fun plus(other: Amount): Amount { - check(currency == other.currency) { "Can only subtract from same currency" } + //check(currency == other.currency) { "Can only subtract from same currency" } // this will crash the load is not fast enough val resultValue = value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong() if (resultValue > MAX_VALUE) diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt @@ -53,10 +53,6 @@ fun AmountScreen( horizontalAlignment = Alignment.CenterHorizontally ) { - if (uiState.encodedWopid.isNotBlank()) { - navigateToWhenAmountEntered() - } - TalerLogo(paddingBottom = 0.dp) Text(uiState.amountError, color = colorResource(id = R.color.bfh_red)) @@ -85,6 +81,7 @@ fun AmountScreen( onClick = { println("clicked 'pay'") model.setupWithdrawal(activity) + navigateToWhenAmountEntered() }, enableExpr = {model.validAmount(uiState.amountStr) && uiState.withdrawalFees.currency != ""} ) 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 @@ -21,7 +21,15 @@ package ch.bfh.habej2.wallee_c2ec.withdrawal import android.app.Activity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DoneAll +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -30,6 +38,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import ch.bfh.habej2.wallee_c2ec.R import com.wallee.android.till.sdk.ApiClient @@ -43,12 +54,10 @@ import java.util.Currency fun AuthorizePaymentScreen( model: WithdrawalViewModel, activity: Activity, - client: ApiClient, - navigateToWhenRegistered: () -> Unit + client: ApiClient ) { val uiState by model.uiState.collectAsState() - val configuration = LocalConfiguration.current val withdrawalAmount = LineItem .ListBuilder( @@ -88,41 +97,37 @@ fun AuthorizePaymentScreen( } } - Column( - Modifier.width(configuration.screenWidthDp.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - - 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) - WithdrawalState.E2EC_TIMEOUT -> model.withdrawalOperationFailed(activity) - 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 -> navigateToWhenRegistered() - WithdrawalState.COMPLETION_FAILED -> navigateToWhenRegistered() - } + when (uiState.withdrawalState) { + WithdrawalState.UNREADY_FOR_AUTHORIZATION -> model.withdrawalOperationFailed(activity) + WithdrawalState.READY_FOR_AUTHORIZATION -> WalleeProcessingTransaction(activity) + WithdrawalState.AUTHORIZATION_TRIGGERED -> WalleeProcessingTransaction(activity) + WithdrawalState.AUTHORIZATION_FAILED -> model.withdrawalOperationFailed(activity) + WithdrawalState.E2EC_TIMEOUT -> model.withdrawalOperationFailed(activity) + WithdrawalState.AUTHORIZED -> WalleeCompletingPayment( + client, + model, + activity + ) // TODO: Maybe following is not needed because completion immediately is specified: "WalleeCompletingPayment(client, model)" + WithdrawalState.COMPLETION_TRIGGERED -> WalleeProcessingTransaction(activity) + WithdrawalState.COMPLETED -> SuccessScreen( + uiState.currency, + uiState.withdrawalFees.toString(true), + uiState.amount.toString(true), + activity + ) + WithdrawalState.COMPLETION_FAILED -> FailedScreen(activity) } } @Composable private fun WalleeCompletingPayment( client: ApiClient, - model: WithdrawalViewModel + model: WithdrawalViewModel, + activity: Activity ) { val uiState by model.uiState.collectAsState() - TalerLogo() - - Text(text = "Completing transaction...") - LaunchedEffect(key1 = Unit) { if (uiState.withdrawalState == WithdrawalState.AUTHORIZED) { @@ -136,12 +141,122 @@ private fun WalleeCompletingPayment( } } + + Spacer(modifier = Modifier.height(32.dp)) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TalerLogo() + + Spacer(modifier = Modifier.height(8.dp)) + + Text(text = "Completing transaction...") + + + Spacer(modifier = Modifier.height(10.dp)) + TalerButton(text = "Done", onClick = { activity.finish() }) + } } @Composable -private fun WalleeProcessingTransaction() { +private fun WalleeProcessingTransaction(activity: Activity) { + + Spacer(modifier = Modifier.height(32.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TalerLogo() - TalerLogo() + Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Processing transaction...") + Text(text = "Processing transaction...") + + Spacer(modifier = Modifier.height(10.dp)) + TalerButton(text = "Done", onClick = { activity.finish() }) + } } + +@Composable +private fun SuccessScreen( + currency: String, + withdrawalFees: String, + amount: String, + activity: Activity +) { + + Spacer(modifier = Modifier.height(32.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TalerLogo() + + Icon( + imageVector = Icons.Filled.DoneAll, + contentDescription = "All steps successful", + tint = colorResource(R.color.bfh_mediumgreen), + modifier = Modifier.size(64.dp) + ) + + Text( + text = "Hurrah! Taler's market capitalization just surged!", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = "Summary", + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold) + ) + Text(text = "Amount: ${currency} ${amount}") + Text(text = "Fees: ${currency} ${withdrawalFees}") + } + Spacer(modifier = Modifier.height(10.dp)) + TalerButton(text = "Done", onClick = { activity.finish() }) + } +} + + +@Composable +private fun FailedScreen( + activity: Activity +) { + Spacer(modifier = Modifier.height(32.dp)) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TalerLogo() + + + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = "Failed authorizing payment", + tint = colorResource(R.color.bfh_red), + modifier = Modifier.size(64.dp) + ) + + Text( + text = "Failed to authorize payment.", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Withdrawal aborted. Please try again.", + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + TalerButton(text = "Done", onClick = { activity.finish() }) + } +} +\ No newline at end of file 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 @@ -18,24 +18,30 @@ package ch.bfh.habej2.wallee_c2ec.withdrawal +import android.annotation.SuppressLint import android.app.Activity import android.os.Handler import android.os.Looper +import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import ch.bfh.habej2.wallee_c2ec.R +import kotlinx.coroutines.flow.distinctUntilChanged @Composable @@ -48,51 +54,46 @@ fun RegisterParametersScreen( val uiState by model.uiState.collectAsState() val configuration = LocalConfiguration.current - LaunchedEffect(uiState.setupWithdrawalRetries) { + // Check if the Taler Wallet is ready to authorize the withdrawal + model.setAuthorizationReadyOrRetry() - if (ManageActivity.SIM_WALLET_ENABLED.value) { - ManageActivity.simulateParameterRegistration( - uiState.terminalsApiBasePath, - uiState.encodedWopid - ) - } - if (model.uiState.value.setupWithdrawalRetries <= MAX_TRIES && - model.uiState.value.withdrawalState == WithdrawalState.UNREADY_FOR_AUTHORIZATION) { - model.setAuthorizationReadyOrRetry() - } - } - - Column( - Modifier.width(configuration.screenWidthDp.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - - if (uiState.withdrawalState == WithdrawalState.READY_FOR_AUTHORIZATION) { + when (uiState.withdrawalState) { + WithdrawalState.READY_FOR_AUTHORIZATION -> { navigateToWhenRegistered() - return } - - if (uiState.withdrawalState == WithdrawalState.E2EC_TIMEOUT) { - println("Operation timed out") - Handler(Looper.getMainLooper()).post { - Toast.makeText(activity.baseContext, activity.getText( - R.string.timeout), Toast.LENGTH_SHORT).show()} + WithdrawalState.E2EC_TIMEOUT -> { + Toast.makeText( + activity, + activity.getString(R.string.timeout), + Toast.LENGTH_LONG + ).show() model.withdrawalOperationFailed(activity) - return } + else -> { + Column( + Modifier.width(configuration.screenWidthDp.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Scan the QR Code with your Taler Wallet to", + modifier = Modifier.padding(top = 15.dp) + ) + Text(text = "register the withdrawal parameters") - Text(text = "Scan the QR Code with your Taler Wallet to", - modifier = Modifier.padding(top = 15.dp)) - Text(text = "register the withdrawal parameters") - QRCode( - TalerConstants.formatTalerUri( - uiState.terminalsApiBasePath, - uiState.encodedWopid - ) - ) + QRCode( + TalerConstants.formatTalerUri( + uiState.terminalsApiBasePath, + uiState.encodedWopid + ) + ) - TalerButton(text = "Exit", onClick = { model.withdrawalOperationFailed(activity) }) + + TalerButton( + text = "Exit", + onClick = { model.withdrawalOperationFailed(activity) }) + } + } } -} -\ No newline at end of file +} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/SummaryScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/SummaryScreen.kt @@ -1,97 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec.withdrawal - -import android.annotation.SuppressLint -import android.app.Activity -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DoneAll -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import ch.bfh.habej2.wallee_c2ec.R - -@SuppressLint("ResourceAsColor") -@Composable -fun SummaryScreen( - model: WithdrawalViewModel, - activity: Activity -) { - val uiState by model.uiState.collectAsState() - - Spacer(modifier = Modifier.height(32.dp)) - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - TalerLogo() - - when (uiState.withdrawalState) { - WithdrawalState.COMPLETED -> { - Icon( - imageVector = Icons.Filled.DoneAll, - contentDescription = "All steps successful", - tint = colorResource(R.color.bfh_mediumgreen), - modifier = Modifier.size(64.dp) - ) - - Text( - text = "Hurrah! Taler's market capitalization just surged!", - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text( - text = "Summary", - style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold) - ) - Text(text = "Amount: ${uiState.currency} ${uiState.amount.toString(true)}") - Text(text = "Fees: ${uiState.currency} ${uiState.withdrawalFees.toString(true)}") - } - } - - WithdrawalState.COMPLETION_FAILED -> { - Icon( - imageVector = Icons.Filled.Warning, - contentDescription = "Failed authorizing payment", - tint = colorResource(R.color.bfh_red), - modifier = Modifier.size(64.dp) - ) - - Text( - text = "Failed to authorize payment.", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Withdrawal aborted. Please try again.", - textAlign = TextAlign.Center - ) - } - - else -> {} - } - TalerButton(text = "Done", onClick = { activity.finish() }) - } -} -\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt @@ -73,12 +73,7 @@ class WithdrawalActivity : ComponentActivity() { } } composable("authorizePaymentScreen") { - AuthorizePaymentScreen(model, this@WithdrawalActivity, walleeClient) { - navController.navigate("summaryScreen") - } - } - composable("summaryScreen") { - SummaryScreen(model, this@WithdrawalActivity) + AuthorizePaymentScreen(model, this@WithdrawalActivity, walleeClient) } } } 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 @@ -23,11 +23,6 @@ import android.os.Handler import android.os.Looper import android.widget.Toast import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import ch.bfh.habej2.wallee_c2ec.R @@ -43,7 +38,6 @@ import com.wallee.android.till.sdk.ApiClient import com.wallee.android.till.sdk.data.State import com.wallee.android.till.sdk.data.TransactionCompletionResponse import com.wallee.android.till.sdk.data.TransactionResponse -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -63,22 +57,6 @@ object TalerConstants { .removePrefix("http://") } -@Stable -interface Withdrawal { - val requestUid: String - val setupWithdrawalRetries: Int - val terminalsApiBasePath: String - val encodedWopid: String - val amount: Amount - val amountStr: String - val amountError: String - val currency: String - val withdrawalFees: Amount - val withdrawalState: WithdrawalState - val transaction: TransactionResponse? - val transactionCompletion: TransactionCompletionResponse? - val transactionId: String -} /** * +--------------------------+ @@ -128,21 +106,22 @@ enum class WithdrawalState { COMPLETED, } -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("") - override var encodedWopid: String by mutableStateOf("") - override var amount: Amount by mutableStateOf(Amount("",0, 0)) - override var amountStr: String by mutableStateOf("") - override var amountError: String by mutableStateOf("") - override var currency: String by mutableStateOf("") - override var withdrawalFees: Amount by mutableStateOf(Amount("", 0,0)) - 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() } -} +@Stable +data class Withdrawal( + val requestUid: String = UUID.randomUUID().toString(), + val withdrawalState: WithdrawalState = WithdrawalState.UNREADY_FOR_AUTHORIZATION, + val setupWithdrawalRetries: Int = 0, + val terminalsApiBasePath: String = "", + val encodedWopid: String = "", + val amount: Amount = Amount("", 0, 0), + val amountStr: String = "", + val amountError: String = "", + val currency: String = "", + val withdrawalFees: Amount = Amount("", 0, 0), + val transaction: TransactionResponse? = null, + val transactionCompletion: TransactionCompletionResponse? = null, + val transactionId: String = generateTransactionIdentifier() +) class WithdrawalViewModel( private val walleeClient: ApiClient, @@ -151,7 +130,7 @@ class WithdrawalViewModel( private var exchangeSelected = false private var terminalClient: TerminalClient? = null - private val _uiState = MutableStateFlow(MutableWithdrawal()) + private val _uiState = MutableStateFlow(Withdrawal()) val uiState: StateFlow<Withdrawal> = _uiState companion object { @@ -171,7 +150,7 @@ class WithdrawalViewModel( terminalClient = TerminalClientImplementation(cfg) //terminalClient = TerminalClientMock() - _uiState.value = MutableWithdrawal() // reset withdrawal operation + _uiState.value = Withdrawal() // reset withdrawal operation terminalClient!!.terminalsConfig { println("terminal config request result present: ${it.isPresent}") @@ -179,8 +158,9 @@ class WithdrawalViewModel( Handler(Looper.getMainLooper()).post {Toast.makeText(activity.baseContext, activity.getText(R.string.not_connected), Toast.LENGTH_SHORT).show()} activity.finish() } else { - _uiState.value.terminalsApiBasePath = - "${cfg.terminalApiBaseUrl}${TalerConstants.TALER_INTEGRATION}" + _uiState.value = _uiState.value.copy( + terminalsApiBasePath = "${cfg.terminalApiBaseUrl}${TalerConstants.TALER_INTEGRATION}" + ) this@WithdrawalViewModel.updateCurrency(it.get().currency) if (!this@WithdrawalViewModel.updateWithdrawalFees(it.get().exchangeFees)) { Handler(Looper.getMainLooper()).post {Toast.makeText(activity.baseContext, activity.getText(R.string.not_fees), Toast.LENGTH_SHORT).show()} @@ -207,29 +187,39 @@ class WithdrawalViewModel( } val wopid = it.get().withdrawalId println("retrieved WOPID from c2ec: $wopid") - _uiState.value.encodedWopid = wopid + updateState { copy(encodedWopid = wopid) } } } } + private fun updateState(update: Withdrawal.() -> Withdrawal) { + val newState = _uiState.value.update() + if (newState != _uiState.value) { + _uiState.value = newState + } + } + + fun validateInput(amount: String): Boolean { //println("validating amount input: $amount") val validAmount = parseAmount(amount) if (validAmount.isPresent) { - _uiState.value.amountError = "" - _uiState.value.amountStr = amount - _uiState.value.amount = validAmount.get() + updateState { copy(amountError = "") } + updateState { copy(amountStr = amount) } + updateState { copy(amount = validAmount.get()) } return true } else { - _uiState.value.amountError = "invalid amount (format: X[.X])" + updateState { copy(amountError = "invalid amount (format: X[.X])")} return false } } private fun updateCurrency(currency: String) { - _uiState.value.currency = currency - _uiState.value.amount = Amount(currency, _uiState.value.amount.value, _uiState.value.amount.fraction) + _uiState.value = _uiState.value.copy( + currency = currency, + amount = Amount(currency, _uiState.value.amount.value, _uiState.value.amount.fraction) + ) } private fun updateWithdrawalFees(exchangeFees: String): Boolean { @@ -238,29 +228,50 @@ class WithdrawalViewModel( if (!optRes.isPresent) { return false } + updateState { copy(withdrawalFees = optRes.get()) } + return true + } - _uiState.value.withdrawalFees = optRes.get() + fun setTransactionResponse( + response: TransactionResponse + ) { + updateState { copy(transaction = response) } + } - return true + + fun setState( + state: WithdrawalState + ) { + if (_uiState.value.withdrawalState != state) { + println("setting state to $state") + updateState { copy(withdrawalState = state) } + } else { + println("state already set to $state") + } + } + + fun incrementCounter() { + _uiState.value = uiState.value.copy( + setupWithdrawalRetries = _uiState.value.setupWithdrawalRetries + 1 + ) } fun setAuthorizing() { - _uiState.value.withdrawalState = WithdrawalState.AUTHORIZATION_TRIGGERED + setState(WithdrawalState.AUTHORIZATION_TRIGGERED) } fun setCompleting() { - _uiState.value.withdrawalState = WithdrawalState.COMPLETION_TRIGGERED + setState(WithdrawalState.COMPLETION_TRIGGERED) } fun updateWalleeTransactionReply(response: TransactionResponse) { - println("updating wallee transaction: $response") - _uiState.value.transaction = response - _uiState.value.withdrawalState = - if (response.state.name == "SUCCESSFUL") - WithdrawalState.AUTHORIZED - else - WithdrawalState.AUTHORIZATION_FAILED + setTransactionResponse(response) + + if (response.state.name == "SUCCESSFUL") + setState(WithdrawalState.AUTHORIZED) + else + setState(WithdrawalState.AUTHORIZATION_FAILED) } fun updateWalleeTransactionCompletion( @@ -269,7 +280,7 @@ class WithdrawalViewModel( if (completion.state == State.FAILED) { println("completion of the transaction failed... aborting") - _uiState.value.withdrawalState = WithdrawalState.COMPLETION_FAILED + setState(WithdrawalState.COMPLETION_FAILED) return } @@ -277,25 +288,32 @@ class WithdrawalViewModel( walleeClient.executeFinalBalance() } - _uiState.value.withdrawalState = WithdrawalState.COMPLETED - _uiState.value.transactionCompletion = completion + updateState { copy(withdrawalState = WithdrawalState.COMPLETED) } + updateState { copy(transactionCompletion = completion) } } - fun setAuthorizationReadyOrRetry() = viewModelScope.launch(Dispatchers.IO) { - terminalClient!!.retrieveWithdrawalStatus(uiState.value.encodedWopid, 10000) { - if (!it.isPresent) { - if (_uiState.value.setupWithdrawalRetries > MAX_TRIES) { - println("setting e2ec timeout") - _uiState.value.withdrawalState = WithdrawalState.E2EC_TIMEOUT + fun setAuthorizationReadyOrRetry() { + println("setting authorization ready or retry") + if (_uiState.value.setupWithdrawalRetries < MAX_TRIES) { + terminalClient!!.retrieveWithdrawalStatus(uiState.value.encodedWopid, 10000) { + if (!it.isPresent) { + if (_uiState.value.setupWithdrawalRetries > MAX_TRIES) { + println("setting e2ec timeout") + terminalClient!!.shutdownDispatcher() + setState(WithdrawalState.E2EC_TIMEOUT) + } else { + println("setting retry because no status was present") + incrementCounter() + } + } else if (_uiState.value.withdrawalState == WithdrawalState.E2EC_TIMEOUT) { + println("Already timed out, ignoring status") + terminalClient!!.shutdownDispatcher() + } else if (_uiState.value.withdrawalState == WithdrawalState.AUTHORIZATION_TRIGGERED) { + println("authorization triggered, waiting for completion") } else { - println("setting retry because no status was present") - _uiState.value.setupWithdrawalRetries += 1 + println("withdrawal status: ${it.get().status}") + setState(WithdrawalState.READY_FOR_AUTHORIZATION) } - } else if (uiState.value.withdrawalState == WithdrawalState.AUTHORIZATION_TRIGGERED) { - println("authorization triggered, waiting for completion") - } else { - println("withdrawal status: ${it.get().status }") - _uiState.value.withdrawalState = WithdrawalState.READY_FOR_AUTHORIZATION } } } @@ -324,7 +342,7 @@ class WithdrawalViewModel( } } else { try { - Optional.of(Amount.fromString(uiState.value.currency, inp)) + Optional.of(Amount.fromString(_uiState.value.currency, inp)) } catch (ex: AmountParserException) { println("failed parsing plain amount: $ex") Optional.empty() @@ -334,6 +352,14 @@ class WithdrawalViewModel( fun resetAmountStr() { println("resetting amountStr. was ${_uiState.value.amountStr}") - _uiState.value.amountStr = "" + _uiState.value = _uiState.value.copy( + amount = Amount( + "", + 0, + 0 + ), + amountError = "", + amountStr = "", + ) } } \ No newline at end of file diff --git a/wallee-c2ec/gradle/libs.versions.toml b/wallee-c2ec/gradle/libs.versions.toml @@ -7,17 +7,17 @@ kotlin = "1.9.0" coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" -espressoCore = "3.5.1" +espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.7.0" composeBom = "2023.08.00" moshiKotlin = "1.15.1" okhttp = "4.12.0" sdk = "0.9.20" -navigationCompose = "2.7.7" +navigationCompose = "2.8.9" materialIconsExtendedAndroid = "1.7.8" composeMaterial = "1.4.1" -material3Android = "1.3.2" + [libraries] androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } @@ -43,8 +43,6 @@ wallee-sdk = { module = "com.wallee.android.till:sdk", version.ref = "sdk" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-material-icons-extended-android = { group = "androidx.compose.material", name = "material-icons-extended-android", version.ref = "materialIconsExtendedAndroid" } androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } -androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" } - [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }