cashless2ecash

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

commit 26cf1bb931c46ba5cdfe679a9e402c14be252091
parent 0228c46aed96a22b9742d48d85551cd95586735d
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Sat,  1 Jun 2024 12:14:47 +0200

fix: app taler uri

Diffstat:
Mdocs/content/acknowledgements.tex | 2+-
Mdocs/content/implementation/a-c2ec.tex | 4++--
Mdocs/content/results/discussion.tex | 2+-
Mdocs/thesis.pdf | 0
Mwallee-c2ec/app/release/app-release.apk | 0
Mwallee-c2ec/app/release/baselineProfiles/0/app-release.dm | 0
Mwallee-c2ec/app/release/baselineProfiles/1/app-release.dm | 0
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt | 15++-------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientMock.kt | 12+++++++-----
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/Amount.kt | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/CurrencySpecification.kt | 28++++++++++++++++++++++++++++
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalsApiModel.kt | 7+++----
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt | 10++++------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt | 2+-
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt | 17+++++------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/SummaryActivity.kt | 77+++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/TestTransactionScreen.kt | 6+++---
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt | 135+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mwallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/TerminalApiClientIntegrationTest.kt | 16++++++++++------
Mwallee-c2ec/build.gradle.kts | 2++
20 files changed, 444 insertions(+), 155 deletions(-)

diff --git a/docs/content/acknowledgements.tex b/docs/content/acknowledgements.tex @@ -4,6 +4,6 @@ The GNU Taler team deserves a big thank you to discuss, reflect and sharpen the Also I thank my colleagues from the class who motivated me during the thesis. Especially I would like to thank Jan Fuhrer for the nice friday night coding sessions, Christian Blättler for the valuable discussion about Taler and Andy Bigler for the exchange about Android applications. They were crucial to gain a better understanding of how the components work and how I must do the implementation. -Additionally I would like to thank Meret Staub for her critical thoughts during the proofreading of the thesis. +Additionally, I would like to thank Meret Staub for her critical thoughts during the proofreading of the thesis. Last but not least I thank Flurina from all my heart. You were not mad at me when I had to cancel dinner, because I wanted to write some code. diff --git a/docs/content/implementation/a-c2ec.tex b/docs/content/implementation/a-c2ec.tex @@ -80,4 +80,5 @@ The withdrawal can only be aborted, when it is not yet confirmed by the confirma \newpage \include{content/implementation/a-providers} -\include{content/implementation/a-fees} -\ No newline at end of file +\newpage +\include{content/implementation/a-fees} diff --git a/docs/content/results/discussion.tex b/docs/content/results/discussion.tex @@ -27,7 +27,7 @@ C2EC introduces new ways to access digital cash using GNU Taler. Due to the shor \subsection{Improvements} \begin{enumerate} - \item Wallet integration: the integration of the wallet needs to be further tested + \item Wallet integration: the integration of the wallet needs to be tested \item Run the existing implementation as part of the BFH Taler CHF-Exchange \item Paydroid app: Run a Wallee terminal on behalf of the BFH. \item C2EC: Remove doubled provider structures. Currently providers are saved to the database and must be configured in the configuration. To make the setup and management easier, the providers could only be configured inside the configuration. diff --git a/docs/thesis.pdf b/docs/thesis.pdf Binary files differ. diff --git a/wallee-c2ec/app/release/app-release.apk b/wallee-c2ec/app/release/app-release.apk Binary files differ. diff --git a/wallee-c2ec/app/release/baselineProfiles/0/app-release.dm b/wallee-c2ec/app/release/baselineProfiles/0/app-release.dm Binary files differ. diff --git a/wallee-c2ec/app/release/baselineProfiles/1/app-release.dm b/wallee-c2ec/app/release/baselineProfiles/1/app-release.dm Binary files differ. 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 @@ -1,30 +1,19 @@ package ch.bfh.habej2.wallee_c2ec.client.taler +import ch.bfh.habej2.wallee_c2ec.client.taler.model.Amount import ch.bfh.habej2.wallee_c2ec.client.taler.model.BankWithdrawalOperationStatus import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalApiConfig import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalConfirmationRequest import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetupResponse import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperationStatus -import ch.bfh.habej2.wallee_c2ec.withdrawal.Amount import java.util.Optional interface TerminalClient { companion object { - fun FormatAmount(a: Amount): String { - - if (a.curr == "" && a.value == 0 && a.frac == 0) { - return "" - } - - if (a.frac <= 0) { - return "${a.curr}:${a.value}" - } - - return "${a.curr}:${a.value}.${a.frac}" - } + fun FormatAmount(a: Amount) = a.toJSONString() } fun terminalsConfig( 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 @@ -7,7 +7,6 @@ import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalConfirmati import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetupResponse import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperationStatus -import ch.bfh.habej2.wallee_c2ec.withdrawal.Amount import java.security.SecureRandom import java.util.Optional @@ -20,6 +19,7 @@ class TerminalClientMock: TerminalClient { "0:0:0", "Wallee", "CHF", + "CHF:0", "wallee-transaction" ) ) @@ -56,12 +56,14 @@ class TerminalClientMock: TerminalClient { // "http://mock.com/api", // "http://mock.com/api", // "", + "payto://IBAN/CH1111111111111", + mockWopidOrReservePubKey(), + "http://test.com", Array(0) {""}, mockWopidOrReservePubKey(), -// "payto://IBAN/CH1111111111111", -// aborted = false, -// selectionDone = true, -// transferDone = false + false, + false, + false ) ) ) 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 @@ -0,0 +1,264 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +// https://git.taler.net/taler-android.git/tree/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt + +package ch.bfh.habej2.wallee_c2ec.client.taler.model + +import java.math.BigDecimal +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.text.NumberFormat +import kotlin.math.floor +import kotlin.math.pow +import kotlin.math.roundToInt + +public class AmountParserException(msg: String? = null, cause: Throwable? = null) : + Exception(msg, cause) + +public class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : + Exception(msg, cause) + +public 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. + * ISO 4217 exponents in the name are not supported, + * although the "fraction" is corresponds to an ISO 4217 exponent of 6. + */ + val currency: String, + + /** + * The integer part may be at most 2^52. + * Note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent. + */ + val value: Long, + + /** + * Unsigned 32 bit fractional value to be added to value representing + * an additional currency fraction, in units of one hundred millionth (1e-8) + * of the base currency value. For example, a fraction + * of 50_000_000 would correspond to 50 cents. + */ + val fraction: Int, + + /** + * Currency specification for amount + */ + val spec: CurrencySpecification? = null, +) : Comparable<Amount> { + + public 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() + private const val MAX_FRACTION_LENGTH = 8 + public const val MAX_FRACTION: Int = 99_999_999 + + public fun zero(currency: String): Amount { + return Amount(checkCurrency(currency), 0, 0) + } + + public 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 { + // value + val valueSplit = str.split(".") + val value = checkValue(valueSplit[0].toLongOrNull()) + // fraction + val fraction: Int = if (valueSplit.size > 1) { + val fractionStr = valueSplit[1] + if (fractionStr.length > MAX_FRACTION_LENGTH) + throw AmountParserException("Fraction $fractionStr too long") + checkFraction(fractionStr.getFraction()) + } else 0 + 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)) + throw AmountParserException("Invalid currency: $currency") + return currency + } + + internal fun checkValue(value: Long?): Long { + if (value == null || value > MAX_VALUE) + throw AmountParserException("Value $value greater than $MAX_VALUE") + return value + } + + internal fun checkFraction(fraction: Int?): Int { + if (fraction == null || fraction > MAX_FRACTION) + throw AmountParserException("Fraction $fraction greater than $MAX_FRACTION") + return fraction + } + + } + + fun toBigDecimal(): BigDecimal = BigDecimal("$amountStr") + + public val amountStr: String + get() = if (fraction == 0) "$value" else { + var f = fraction + var fractionStr = "" + while (f > 0) { + fractionStr += f / (FRACTIONAL_BASE / 10) + f = (f * 10) % FRACTIONAL_BASE + } + "$value.$fractionStr" + } + + public 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() + if (resultValue > MAX_VALUE) + throw AmountOverflowException() + val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE + return Amount(currency, resultValue, resultFraction) + } + + public operator fun times(factor: Int): Amount { + // TODO consider replacing with a faster implementation + if (factor == 0) return zero(currency) + var result = this + for (i in 1 until factor) result += this + 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 { + check(currency == other.currency) { "Can only subtract from same currency" } + var resultValue = value + var resultFraction = fraction + if (resultFraction < other.fraction) { + if (resultValue < 1L) + throw AmountOverflowException() + resultValue-- + resultFraction += FRACTIONAL_BASE + } + check(resultFraction >= other.fraction) + resultFraction -= other.fraction + if (resultValue < other.value) + throw AmountOverflowException() + resultValue -= other.value + return Amount(currency, resultValue, resultFraction) + } + + public fun isZero(): Boolean { + return value == 0L && fraction == 0 + } + + public fun toJSONString(): String { + return "$currency:$amountStr" + } + + override fun toString() = toString( + showSymbol = true, + negative = false, + ) + + fun toString( + showSymbol: Boolean = true, + negative: Boolean = false, + symbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols, + ): String { + // We clone the object to safely/cleanly modify it + val s = symbols.clone() as DecimalFormatSymbols + val amount = (if (negative) "-$amountStr" else amountStr).toBigDecimal() + + // No currency spec, so we render normally + if (spec == null) { + val format = NumberFormat.getInstance() + format.maximumFractionDigits = MAX_FRACTION_LENGTH + format.minimumFractionDigits = 0 + s.decimalSeparator = s.monetaryDecimalSeparator + (format as DecimalFormat).decimalFormatSymbols = s + + val fmt = format.format(amount) + return if (showSymbol) "$fmt $currency" else fmt + } + + // There is currency spec, so we can do things right + val format = NumberFormat.getCurrencyInstance() + format.maximumFractionDigits = spec.numFractionalNormalDigits + format.minimumFractionDigits = spec.numFractionalTrailingZeroDigits + s.currencySymbol = spec.symbol ?: "" + (format as DecimalFormat).decimalFormatSymbols = s + + val fmt = format.format(amount) + return if (showSymbol) { + // If no symbol, then we use the currency string + if (spec.symbol != null) fmt else "$fmt $currency" + } else { + // We should do better than manually removing the symbol here + fmt.replace(s.currencySymbol, "").trim() + } + } + + override fun compareTo(other: Amount): Int { + check(currency == other.currency) { "Can only compare amounts with the same currency" } + when { + value == other.value -> { + if (fraction < other.fraction) return -1 + if (fraction > other.fraction) return 1 + return 0 + } + value < other.value -> return -1 + else -> return 1 + } + } + +} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/CurrencySpecification.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/CurrencySpecification.kt @@ -1,2 +1,29 @@ +/* + * This file is part of GNU Taler + * (C) 2024 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +// file copied from https://git.taler.net/taler-android.git/tree/taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt + package ch.bfh.habej2.wallee_c2ec.client.taler.model +data class CurrencySpecification( + val name: String, + val numFractionalInputDigits: Int, + val numFractionalNormalDigits: Int, + val numFractionalTrailingZeroDigits: Int, + val altUnitNames: Map<Int, String>, +) { + val symbol: String? get() = altUnitNames[0] +} +\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalsApiModel.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalsApiModel.kt @@ -1,6 +1,5 @@ package ch.bfh.habej2.wallee_c2ec.client.taler.model -import ch.bfh.habej2.wallee_c2ec.withdrawal.Amount import com.squareup.moshi.Json data class TerminalApiConfig( @@ -46,12 +45,12 @@ data class BankWithdrawalOperationStatus( // @Json(name = "max_amount") val maxAmount: String, @Json(name = "card_fees") val cardFees: String, @Json(name = "sender_wire") val senderWire: String, -// @Json(name = "suggested_exchange") val suggestedExchange: String, -// @Json(name = "required_exchange") val requiredExchange: String, + @Json(name = "suggested_exchange") val suggestedExchange: String, + @Json(name = "required_exchange") val requiredExchange: String, // @Json(name = "confirm_transfer_url") val confirmTransferUrl: String, @Json(name = "wire_types") val wireTypes: Array<String>, @Json(name = "selected_reserve_pub") val selectedReservePub: String, -// @Json(name = "selected_exchange_acount") val selectedExchangeAccount: String, +// @Json(name = "selected_exchange_account") val selectedExchangeAccount: String, @Json(name = "aborted") val aborted: Boolean, @Json(name = "selection_done") val selectionDone: Boolean, @Json(name = "transfer_done") val transferDone: Boolean 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 @@ -1,6 +1,5 @@ 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 @@ -12,16 +11,11 @@ import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable @@ -57,6 +51,10 @@ fun AmountScreen( ) ) + Text(text = "fees: ${uiState.withdrawalFees}") + + Text(text = "total: ${uiState.amount.plus(uiState.withdrawalFees)}") + Button(onClick = { println("clicked 'pay'") model.setupWithdrawal(activity, navigateToWhenAmountEntered) 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 @@ -28,7 +28,7 @@ fun AuthorizePaymentScreen(model: WithdrawalViewModel, activity: Activity, clien val configuration = LocalConfiguration.current val withdrawalAmount = LineItem - .ListBuilder(uiState.encodedWopid, uiState.amount.toBigDecimal()) + .ListBuilder(uiState.encodedWopid, uiState.amount.plus(uiState.withdrawalFees).toBigDecimal()) .build() Column( diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt @@ -21,27 +21,20 @@ var exchanges = listOf( TalerTerminalConfig( "BFH Taler (CHF)", - "http://exchange.chf.taler.net/terminals", - "Wallee-3", - "YHrpzeHUyybGT5gOY9VAiZ7QAV/icOXtHPRKZTXv2n8=" - ), - - TalerTerminalConfig( - "BFH Taler (CHF, https)", - "https://exchange.chf.taler.net/terminals", - "Wallee-3", - "YHrpzeHUyybGT5gOY9VAiZ7QAV/icOXtHPRKZTXv2n8=" + "https://terminals.chf.taler.net", + "Wallee-2", + "YTLYIPPOTNqen//Rl7uc58FUTb8K3Kk0Zp/OSlC0Ufk=" ), TalerTerminalConfig( - "Test System Joel (CHF)", + "Test System (CHF)", "http://taler-c2ec.ti.bfh.ch", "Wallee-3", "YHrpzeHUyybGT5gOY9VAiZ7QAV/icOXtHPRKZTXv2n8=" ), TalerTerminalConfig( - "Test System Joel (CHF, https)", + "Test System (CHF, https)", "https://taler-c2ec.ti.bfh.ch", "Wallee-3", "YHrpzeHUyybGT5gOY9VAiZ7QAV/icOXtHPRKZTXv2n8=" diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/SummaryActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/SummaryActivity.kt @@ -9,12 +9,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.Button import androidx.compose.material3.Text +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 ch.bfh.habej2.wallee_c2ec.MainActivity import ch.bfh.habej2.wallee_c2ec.client.taler.TerminalClientImplementation +import ch.bfh.habej2.wallee_c2ec.client.taler.model.Amount import com.squareup.moshi.Json data class Summary( @@ -31,8 +33,8 @@ class SummaryActivity : ComponentActivity() { var summary: Summary = reset() private fun reset(): Summary = Summary( - Amount(0, 0), - Amount(0, 0), + Amount("", 0, 0), + Amount("", 0, 0), "", "" ) @@ -42,40 +44,51 @@ class SummaryActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { - if (summary.success) { - Text( - text = "Amount authorized", - color = Color.Yellow, - modifier = Modifier.background(color = Color.Black) - ) - Text(text = "Summary") - Text(text = "Withdrawable Amount: ${summary.amount} ${summary.currency}") - Text(text = "Fees: ${summary.fees} ${summary.currency}") - Text(text = "Withdrawal Operation ID (QR Code):") - // QRCode(qrCodeContent = summary.encodedWopid, 2.dp) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Amount authorized", + color = Color.Yellow, + modifier = Modifier.background(color = Color.Black) + ) + Text(text = "Summary") + Text(text = "Amount: ${summary.amount.toString(true)}") + Text(text = "Fees: ${summary.fees.toString(true)}") + // Text(text = "Withdrawal Operation ID (QR Code):") + // QRCode(qrCodeContent = summary.encodedWopid, 2.dp) + + finishButton() + } } else { - Text(text = "Failed authorizing payment.") - Text(text = "Withdrawal aborted.") - } + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Failed authorizing payment.") + Text(text = "Withdrawal aborted.") - Button(onClick = { - this@SummaryActivity.startActivity( - Intent( - baseContext, - MainActivity::class.java - ) - ) - reset() - finish() - }) { - Text(text = "finish") + finishButton() + } } - } + } + } + + @Composable + private fun finishButton() { + Button(onClick = { + this@SummaryActivity.startActivity( + Intent( + baseContext, + MainActivity::class.java + ) + ) + reset() + finish() + }) { + Text(text = "finish") } } } \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/TestTransactionScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/TestTransactionScreen.kt @@ -1,11 +1,9 @@ package ch.bfh.habej2.wallee_c2ec.withdrawal -import android.app.Activity import androidx.compose.foundation.layout.Column import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import ch.bfh.habej2.wallee_c2ec.client.wallee.WalleeResponseHandler import com.wallee.android.till.sdk.ApiClient import com.wallee.android.till.sdk.data.LineItem import com.wallee.android.till.sdk.data.Transaction @@ -14,6 +12,8 @@ import java.math.BigDecimal import java.util.Currency import java.util.UUID +const val TEST_TRANSACTION_PREFIX = "TEST_TRANSACTION" + @Composable fun TestTransactionScreen(walleeClient: ApiClient) { @@ -22,7 +22,7 @@ fun TestTransactionScreen(walleeClient: ApiClient) { val invRef = UUID.randomUUID().toString() val withdrawalAmount = LineItem - .ListBuilder(lineitemuuid, BigDecimal(10.50)) + .ListBuilder(TEST_TRANSACTION_PREFIX+lineitemuuid, BigDecimal(9.0)) .build() val transaction = Transaction.Builder(withdrawalAmount) 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 @@ -13,50 +13,34 @@ import ch.bfh.habej2.wallee_c2ec.client.taler.TerminalClient import ch.bfh.habej2.wallee_c2ec.client.taler.TerminalClientImplementation import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerTerminalConfig import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils +import ch.bfh.habej2.wallee_c2ec.client.taler.model.Amount +import ch.bfh.habej2.wallee_c2ec.client.taler.model.AmountParserException import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalConfirmationRequest import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup -import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperationStatus import ch.bfh.habej2.wallee_c2ec.withdrawal.WithdrawalViewModel.Companion.generateTransactionIdentifier -import com.google.gson.annotations.Until -import com.squareup.moshi.Json import com.wallee.android.till.sdk.ApiClient import com.wallee.android.till.sdk.data.State import com.wallee.android.till.sdk.data.TransactionCompletion import com.wallee.android.till.sdk.data.TransactionCompletionResponse import com.wallee.android.till.sdk.data.TransactionResponse -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.asExecutor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import java.io.Closeable -import java.math.BigDecimal import java.security.SecureRandom import java.util.Optional import java.util.UUID -import java.util.concurrent.Executors -import kotlin.math.sign - -data class Amount( - @Json(name = "value") val value: Int, - @Json(name = "fraction") val frac: Int, - @Json(name = "currency") val curr: String = "" -) { - - fun toBigDecimal(): BigDecimal = BigDecimal("$value.$frac") - - override fun toString(): String { - return "$value.$frac" - } -} object TalerConstants { const val TALER_INTEGRATION = "/taler-integration" const val TALER_INTEGRATION_WITHDRAWAL_OPERATION = "/withdrawal-operation" fun formatTalerUri(terminalsApiBasePath: String, encodedWopid: String) = - "taler://withdraw/$terminalsApiBasePath$TALER_INTEGRATION$TALER_INTEGRATION_WITHDRAWAL_OPERATION/$encodedWopid" + "taler://withdraw/${stripLeadingProtocolIfPresent(terminalsApiBasePath)}/$encodedWopid" // "taler://withdraw/${stripLeadingProtocolIfPresent(terminalsApiBasePath)}$TALER_INTEGRATION_WITHDRAWAL_OPERATION/$encodedWopid" + + private fun stripLeadingProtocolIfPresent(terminalsApiBasePath: String) = terminalsApiBasePath + .removePrefix("https://") + .removePrefix("http://") } @Stable @@ -89,11 +73,11 @@ private class MutableWithdrawalOperationState : WithdrawalOperationState { override val requestUid: String by derivedStateOf { UUID.randomUUID().toString() } override var terminalsApiBasePath: String by mutableStateOf("") override var encodedWopid: String by mutableStateOf("") - override var amount: Amount by mutableStateOf(Amount(0, 0)) + 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 withdrawalFees: Amount by mutableStateOf(Amount("", 0,0)) override var transactionState: TransactionState by mutableStateOf(TransactionState.AUTHORIZATION_PENDING) override var transaction: TransactionResponse? by mutableStateOf(null) override var transactionCompletion: TransactionCompletionResponse? by mutableStateOf(null) @@ -189,6 +173,7 @@ class WithdrawalViewModel( private fun updateCurrency(currency: String) { _uiState.value.currency = currency + _uiState.value.amount = Amount(currency, _uiState.value.amount.value, _uiState.value.amount.fraction) } private fun updateWithdrawalFees(exchangeFees: String): Boolean { @@ -272,17 +257,13 @@ class WithdrawalViewModel( sigs.block() } - fun navToAuthHook(navigateToWhenRegistered: () -> Unit) { - navigateToWhenRegistered() - } - private fun confirmationRequest(activity: Activity) { viewModelScope.launch { terminalClient!!.sendConfirmationRequest( _uiState.value.encodedWopid, TerminalWithdrawalConfirmationRequest( _uiState.value.encodedWopid, - TerminalClient.FormatAmount(Amount(0,0,_uiState.value.currency)) + TerminalClient.FormatAmount(Amount(_uiState.value.currency, 0,0)) ) ) { if (!it) { @@ -292,7 +273,7 @@ class WithdrawalViewModel( SummaryActivity.summary = Summary( _uiState.value.amount, - Amount(0,0), + _uiState.value.withdrawalFees, _uiState.value.currency, _uiState.value.encodedWopid ) @@ -323,7 +304,7 @@ class WithdrawalViewModel( SummaryActivity.summary = Summary( _uiState.value.amount, - Amount(0,0), + _uiState.value.withdrawalFees, _uiState.value.currency, _uiState.value.encodedWopid, success = false @@ -342,48 +323,64 @@ class WithdrawalViewModel( */ private fun parseAmount(inp: String): Optional<Amount> { - var currency = "" - var valueFraction = "" - if (inp.contains(":")) { - val splitted = inp.split(":") - currency = splitted[0] - if (currency != uiState.value.currency) { - println("illegal currency $currency. expected ${uiState.value.currency}") - return Optional.empty() + return if (inp.contains(":")) { + try { + Optional.of(Amount.fromJSONString(inp)) + } catch (ex: AmountParserException) { + println("failed parsing amount with currency prefix: $ex") + Optional.empty() } - valueFraction = splitted[1] } else { - valueFraction = inp - } - - val points = valueFraction.count { it == '.' } - if (points > 1) { - return Optional.empty() - } - - if (points == 1) { - val valueStr = valueFraction.split(".")[0] - val fracStr = valueFraction.split(".")[1] - return try { - val value = valueStr.toInt() - var frac = 0 - if (fracStr.isNotEmpty()) { - frac = fracStr.toInt() - } - Optional.of(Amount(value, frac, uiState.value.currency)) - } catch (ex: NumberFormatException) { - println(ex.message) + try { + Optional.of(Amount.fromString(uiState.value.currency, inp)) + } catch (ex: AmountParserException) { + println("failed parsing plain amount: $ex") Optional.empty() } } - return try { - val value = valueFraction.toInt() - Optional.of(Amount(value, 0, uiState.value.currency)) - } catch (ex: NumberFormatException) { - println(ex.message) - Optional.empty() - } +// var currency = "" +// var valueFraction = "" +// if (inp.contains(":")) { +// val splitted = inp.split(":") +// currency = splitted[0] +// if (currency != uiState.value.currency) { +// println("illegal currency $currency. expected ${uiState.value.currency}") +// return Optional.empty() +// } +// valueFraction = splitted[1] +// } else { +// valueFraction = inp +// } +// +// val points = valueFraction.count { it == '.' } +// if (points > 1) { +// return Optional.empty() +// } +// +// if (points == 1) { +// val valueStr = valueFraction.split(".")[0] +// val fracStr = valueFraction.split(".")[1] +// return try { +// val value = valueStr.toLong() +// var frac = 0 +// if (fracStr.isNotEmpty()) { +// frac = fracStr.toInt() +// } +// Optional.of(Amount(uiState.value.currency, value, frac)) +// } catch (ex: NumberFormatException) { +// println(ex.message) +// Optional.empty() +// } +// } +// +// return try { +// val value = valueFraction.toLong() +// Optional.of(Amount(uiState.value.currency, value, 0)) +// } catch (ex: NumberFormatException) { +// println(ex.message) +// Optional.empty() +// } } fun resetAmountStr() { @@ -450,7 +447,7 @@ class WithdrawalViewModel( // "ciao" // ) // -// _uiState.value.exchangeBankIntegrationApiUrl = "${cfg.terminalApiBaseUrl}${TalerConstants.TALER_INTEGRATION}" +// _uiState.value.exchangeBankIntegrationApiUrl Amount(0,0)= "${cfg.terminalApiBaseUrl}${TalerConstants.TALER_INTEGRATION}" // //chooseExchange(cfg, Activity()) // } // diff --git a/wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/TerminalApiClientIntegrationTest.kt b/wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/TerminalApiClientIntegrationTest.kt @@ -6,13 +6,9 @@ import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.encodeCrock import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtilsTest import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalConfirmationRequest import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup -import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperationStatus -import ch.bfh.habej2.wallee_c2ec.withdrawal.Amount import ch.bfh.habej2.wallee_c2ec.withdrawal.ManageActivity import org.junit.Assert.assertTrue import org.junit.Test -import org.junit.runner.OrderWith -import org.junit.runner.manipulation.Alphanumeric class TerminalApiClientIntegrationTest { @@ -23,7 +19,14 @@ class TerminalApiClientIntegrationTest { "Gofobz8XYEX0H3rts4+w3K7bU68wSP2N5Y36FBe4/f8=" ) - private val client = TerminalClientImplementation(config) + private val config2 = TalerTerminalConfig( + "CHF Exchange", + "https://terminals.chf.taler.net", + "Simulation-1", + "Gofobz8XYEX0H3rts4+w3K7bU68wSP2N5Y36FBe4/f8=" + ) + + private val client = TerminalClientImplementation(config2) private var currency = "" private var wopid = "" @@ -51,6 +54,7 @@ class TerminalApiClientIntegrationTest { wopid = it.get().withdrawalId println("setup withdrawal. wopid=$wopid") } + Thread.sleep(sleepMs) // send check request @@ -74,7 +78,7 @@ class TerminalApiClientIntegrationTest { // ATD2GW6ZNJ5FQ8B6R890C4G13RN3RZTQ1P6VT2ZT5V04AP7DG9K0 wopid = ""; ManageActivity.simulateParameterRegistration( - "http://taler-c2ec.ti.bfh.ch/taler-integration", + "https://taler-c2ec.ti.bfh.ch/taler-integration", wopid ) } diff --git a/wallee-c2ec/build.gradle.kts b/wallee-c2ec/build.gradle.kts @@ -2,4 +2,6 @@ plugins { alias(libs.plugins.androidApplication) apply false alias(libs.plugins.jetbrainsKotlinAndroid) apply false + + kotlin("plugin.serialization") version "1.9.0" } \ No newline at end of file