From 39f0572004f1cc8d9ee999eb47630e2e8a7099ca Mon Sep 17 00:00:00 2001 From: Joel-Haeberli Date: Sat, 20 Apr 2024 13:28:42 +0200 Subject: feat(wallee app): navigation and state handling using view model --- wallee-c2ec/app/build.gradle.kts | 1 + wallee-c2ec/app/src/main/AndroidManifest.xml | 21 +- .../ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt | 56 ----- .../java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt | 17 +- .../ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt | 127 ----------- .../ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt | 56 ----- .../wallee_c2ec/WithdrawalCreationActivity.kt | 65 ------ .../bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt | 102 --------- .../client/taler/BankIntegrationClient.kt | 2 +- .../taler/config/TalerBankIntegrationConfig.kt | 10 + .../client/taler/config/TerminalConfig.kt | 8 + .../client/taler/encoding/TalerBase32Codec.kt | 8 + .../config/TalerBankIntegrationConfig.kt | 10 - .../habej2/wallee_c2ec/config/TerminalConfig.kt | 8 - .../wallee_c2ec/encoding/TalerBase32Codec.kt | 7 - .../wallee_c2ec/withdrawal/QRCodeComposable.kt | 77 +++++++ .../wallee_c2ec/withdrawal/WithdrawalActivity.kt | 234 +++++++++++++++++++++ .../wallee_c2ec/withdrawal/WithdrawalViewModel.kt | 118 +++++++++++ wallee-c2ec/gradle/libs.versions.toml | 2 + 19 files changed, 473 insertions(+), 456 deletions(-) delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt create mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt create mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TerminalConfig.kt create mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt delete mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt create mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/QRCodeComposable.kt create mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt create mode 100644 wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt diff --git a/wallee-c2ec/app/build.gradle.kts b/wallee-c2ec/app/build.gradle.kts index 44d0084..bc8cb03 100644 --- a/wallee-c2ec/app/build.gradle.kts +++ b/wallee-c2ec/app/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/wallee-c2ec/app/src/main/AndroidManifest.xml b/wallee-c2ec/app/src/main/AndroidManifest.xml index 86771a7..f276936 100644 --- a/wallee-c2ec/app/src/main/AndroidManifest.xml +++ b/wallee-c2ec/app/src/main/AndroidManifest.xml @@ -12,21 +12,6 @@ android:supportsRtl="true" android:theme="@style/Theme.Walleec2ec" tools:targetApi="31"> - - - + + \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt deleted file mode 100644 index 8bdc310..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt +++ /dev/null @@ -1,56 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import ch.bfh.habej2.wallee_c2ec.config.EXCHANGES -import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme - -class ExchangeActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - Walleec2ecTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Text(text = "Choose the exchange to withdraw from") - - // TODO let user select exchanges from config here - // config must contain display name, credentials (generated by cli) - // and the base url of the c2ec bank-integration api - EXCHANGES.forEach { Text(text = it.displayName) } - - val ctx = LocalContext.current - Button(onClick = { ctx.startActivity(Intent(this@ExchangeActivity.parent, WithdrawalCreationActivity::class.java)) }) { - Text(text = "withdraw") - } - - Button(onClick = { finish() }) { - Text(text = "back") - } - } - } - } - } - } -} \ No newline at end of file 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 index 13d84fc..be02bd8 100644 --- 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 @@ -4,9 +4,7 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme @@ -16,11 +14,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import ch.bfh.habej2.wallee_c2ec.config.loadConfiguredExchanges import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme +import ch.bfh.habej2.wallee_c2ec.withdrawal.WithdrawalActivity -class MainActivity : AppCompatActivity() { +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,15 +33,17 @@ class MainActivity : AppCompatActivity() { horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "Withdraw Taler using Wallee") - Button(onClick = { ctx.startActivity(Intent(this@MainActivity, WithdrawalCreationActivity::class.java)) }) { + Button(onClick = { ctx.startActivity(Intent(this@MainActivity, WithdrawalActivity::class.java)) }) { Text(text = "Start Withdrawal") } - Button(onClick = { ctx.startActivity(Intent(this@MainActivity, ExchangeActivity::class.java)) }) { - Text(text = "Choose Exchange") - } } } } } } + + @Composable + fun SelectExchangeScreen() { + + } } diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt deleted file mode 100644 index e63552b..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt +++ /dev/null @@ -1,127 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import ch.bfh.habej2.wallee_c2ec.client.taler.BankIntegrationClient -import ch.bfh.habej2.wallee_c2ec.client.wallee.WalleeResponseHandler -import ch.bfh.habej2.wallee_c2ec.config.TalerBankIntegrationConfig -import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme -import com.wallee.android.till.sdk.ApiClient -import com.wallee.android.till.sdk.data.LineItem -import com.wallee.android.till.sdk.data.Transaction -import com.wallee.android.till.sdk.data.TransactionProcessingBehavior -import java.math.BigDecimal -import java.util.Currency -import java.util.Optional - -class PaymentActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // TODO retrieve WithdrawalViewModel here - // maybe something like savedStateRegistry.getSavedStateProvider("current-withdrawal") - val model = WithdrawalViewModel(BankIntegrationClient( - TalerBankIntegrationConfig("TestExchange", "http://localhost:8082/c2ec", "Wallee-1", "secret"))) - val client = ApiClient(WalleeResponseHandler()) - - setContent { - Walleec2ecTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Text(text = "present card, trigger payment") - - TextField( - value = "", - onValueChange = { - val optAmount = parseAmount(it) - if (optAmount.isPresent) { - model.updateAmount(optAmount.get()) - } - }, - label = { Text(text = "Enter amount") }, - placeholder = { Text(text = "amount") } - ) - - Button(enabled = false, onClick = { - val withdrawalAmount = LineItem - .ListBuilder( - model.uiState.encodedWopid, - BigDecimal("${model.uiState.amount.value}.${model.uiState.amount.frac}") - ) - .build() - - val transaction = Transaction.Builder(withdrawalAmount) - .setCurrency(Currency.getInstance(model.uiState.currency)) - .setInvoiceReference(model.uiState.encodedWopid) - .setMerchantReference(model.uiState.encodedWopid) - .setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY) - .build() - - try { - client.authorizeTransaction(transaction) - } catch (e: Exception) { - e.printStackTrace() - } - }) { - Text(text = "") - } - - Button(onClick = { - model.withdrawalOperationFailed(applicationContext) - finish() - }) { - Text(text = "abort") - } - } - } - } - } - } - - /** - * Format expected X[.X], X an integer - */ - private fun parseAmount(inp: String): Optional { - - val points = inp.count { it == '.' } - if (points > 1) { - return Optional.empty() - } - - if (points == 1) { - val valueStr = inp.split(".")[0] - val fracStr = inp.split(".")[1] - return try { - val value = valueStr.toInt() - val frac = fracStr.toInt() - Optional.of(Amount(value, frac)) - } catch (ex: NumberFormatException) { - Optional.empty() - } - } - - return try { - val value = inp.toInt() - Optional.of(Amount(value, 0)) - } catch (ex: NumberFormatException) { - Optional.empty() - } - } -} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt deleted file mode 100644 index 3fe6004..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt +++ /dev/null @@ -1,56 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec - -import android.graphics.Bitmap -import android.graphics.Color -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min -import com.google.zxing.BarcodeFormat.QR_CODE -import com.google.zxing.qrcode.QRCodeWriter - -@Composable -fun QRCode(qrCodeContent: String) { - - Column { - - val qrCodeSize = getQrCodeSize() - val btmp = makeQrCode(qrCodeContent).asImageBitmap() - - Image( - modifier = androidx.compose.ui.Modifier - .size(qrCodeSize) - .padding(vertical = 8.dp), - bitmap = btmp, - contentDescription = "Scan the QR Code to start withdrawal", - ) - } -} - -@Composable -fun getQrCodeSize(): Dp { - val configuration = LocalConfiguration.current - val screenHeight = configuration.screenHeightDp.dp - val screenWidth = configuration.screenWidthDp.dp - return min(screenHeight, screenWidth) -} - -private fun makeQrCode(text: String, size: Int = 256): Bitmap { - val qrCodeWriter = QRCodeWriter() - val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size) - val height = bitMatrix.height - val width = bitMatrix.width - val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) - for (x in 0 until width) { - for (y in 0 until height) { - bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) - } - } - return bmp -} \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt deleted file mode 100644 index 229f911..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt +++ /dev/null @@ -1,65 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import ch.bfh.habej2.wallee_c2ec.client.taler.BankIntegrationClient -import ch.bfh.habej2.wallee_c2ec.config.TalerBankIntegrationConfig -import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme -import java.util.concurrent.Executors - -class WithdrawalCreationActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // TODO Initialize model properly and put in location where everyone involved has access - val model = WithdrawalViewModel(BankIntegrationClient(TalerBankIntegrationConfig( - "TestExchange", "http://localhost:8082/c2ec", "Wallee-1", "secret" - ))) - model.initialize() - - setContent { - - // start long polling activity for the created wopid and start authorization - model.startAuthorizationWhenReadyOrAbort(LocalContext.current) - - Walleec2ecTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Text(text = "Generated Random WOPID=${model.uiState.encodedWopid}") - - Text(text = "QR-Code content: ${formatTalerUri(model.uiState.encodedWopid)}") - - QRCode(model.uiState.encodedWopid) - - Button(onClick = { - Executors.newSingleThreadExecutor().submit { model.withdrawalOperationFailed(applicationContext) } - finish() - }) { - Text(text = "abort") - } - } - } - } - } - } - - private fun formatTalerUri(encodedWopid: String) = "taler://withdraw/$encodedWopid" -} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt deleted file mode 100644 index f23fa7d..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt +++ /dev/null @@ -1,102 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec - -import android.content.Context -import android.content.Intent -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import ch.bfh.habej2.wallee_c2ec.client.taler.BankIntegrationClient -import ch.bfh.habej2.wallee_c2ec.client.taler.model.PaymentNotification -import ch.bfh.habej2.wallee_c2ec.client.wallee.WalleeResponseHandler -import ch.bfh.habej2.wallee_c2ec.encoding.Base32Encode -import com.wallee.android.till.sdk.ApiClient -import com.wallee.android.till.sdk.data.Transaction -import kotlinx.coroutines.launch -import java.io.Closeable -import java.math.BigDecimal -import java.security.SecureRandom - -data class Amount( - val value: Int, - val frac: Int -) { - fun toBigDecimal(): BigDecimal = BigDecimal("$value.$frac") -} - -@Stable -interface WithdrawalOperationState{ - val exchangeBankIntegrationApiUrl: String - val encodedWopid: String - val amount: Amount - val currency: String - val payed: Boolean - val transaction: Transaction? -} - -private class MutableWithdrawalOperationState: WithdrawalOperationState { - override var exchangeBankIntegrationApiUrl: String by mutableStateOf("") - override var encodedWopid: String by mutableStateOf("") - override var amount: Amount by mutableStateOf(Amount(0,0)) - override var currency: String by mutableStateOf("") - override var payed: Boolean by mutableStateOf(false) - override var transaction: Transaction? by mutableStateOf(null) -} - -class WithdrawalViewModel( - private val bankIntegrationClient: BankIntegrationClient, - vararg closeables: Closeable -) : ViewModel(*closeables) { - - private val _uiState = MutableWithdrawalOperationState() - val uiState: WithdrawalOperationState = _uiState - - fun initialize() { - _uiState.encodedWopid = Base32Encode(wopid()) - } - - fun updateAmount(amount: Amount) { - _uiState.amount = amount - } - - fun updateCurrency(currency: String) { - _uiState.currency = currency - } - - fun updateWalleeTransaction(transaction: Transaction) { - _uiState.transaction = transaction - } - - fun startAuthorizationWhenReadyOrAbort(ctx: Context) { - viewModelScope.launch { - val result = bankIntegrationClient.retrieveWithdrawalStatus(uiState.encodedWopid, 30000) - if (result.isPresent) { - ctx.startActivity(Intent(ctx, PaymentActivity::class.java)) - } else { - withdrawalOperationFailed(ctx) - } - } - } - - fun withdrawalOperationFailed(ctx: Context? = null) { - viewModelScope.launch { - bankIntegrationClient.abortWithdrawal(uiState.encodedWopid) - ctx?.startActivity(Intent(ctx, MainActivity::class.java)) - } - } - - fun confirmPayment() { - viewModelScope.launch{ - bankIntegrationClient.sendPaymentNotification(PaymentNotification()) - } - } - - private fun wopid(): ByteArray { - val wopid = ByteArray(32) - val rand = SecureRandom() - rand.nextBytes(wopid) // will seed automatically - return wopid - } -} \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt index 55b9b8d..28b3914 100644 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt +++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt @@ -1,10 +1,10 @@ package ch.bfh.habej2.wallee_c2ec.client.taler +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig import ch.bfh.habej2.wallee_c2ec.client.taler.model.BankIntegrationConfig import ch.bfh.habej2.wallee_c2ec.client.taler.model.PaymentNotification import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperation import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperationStatus -import ch.bfh.habej2.wallee_c2ec.config.TalerBankIntegrationConfig import com.squareup.moshi.Moshi import okhttp3.HttpUrl import okhttp3.Interceptor diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt new file mode 100644 index 0000000..8ca70f3 --- /dev/null +++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt @@ -0,0 +1,10 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler.config + +// TODO how to configure ?? -> implement ativity allowing to choose one of the configured exchanges + +data class TalerBankIntegrationConfig( + val displayName: String, + val bankIntegrationBaseUrl: String, + val terminalId: String, + val accessToken: String +) \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TerminalConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TerminalConfig.kt new file mode 100644 index 0000000..0ef0ae6 --- /dev/null +++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TerminalConfig.kt @@ -0,0 +1,8 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler.config + +// TODO how to configure ?? + +data class TerminalConfig( + val terminalId: String, + val accessToken: String +) diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt new file mode 100644 index 0000000..5464858 --- /dev/null +++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt @@ -0,0 +1,8 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler.encoding + +import android.util.Base64 +import org.apache.commons.codec.binary.Base32 + +fun Base32Encode(byts: ByteArray): String = Base64.encodeToString(byts, 0) // Base32().encodeAsString(byts) + +fun Base32Decode(enc: String) = Base32().decode(enc) \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt deleted file mode 100644 index a5dcab9..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec.config - -// TODO how to configure ?? -> implement ativity allowing to choose one of the configured exchanges - -data class TalerBankIntegrationConfig( - val displayName: String, - val bankIntegrationBaseUrl: String, - val terminalId: String, - val accessToken: String -) \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt deleted file mode 100644 index e69c74b..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec.config - -// TODO how to configure ?? - -data class TerminalConfig( - val terminalId: String, - val accessToken: String -) diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt deleted file mode 100644 index 15c1805..0000000 --- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec.encoding - -import org.apache.commons.codec.binary.Base32 - -fun Base32Encode(byts: ByteArray): String = Base32().encodeAsString(byts) - -fun Base32Decode(enc: String) = Base32().decode(enc) \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/QRCodeComposable.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/QRCodeComposable.kt new file mode 100644 index 0000000..ed7cbc8 --- /dev/null +++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/QRCodeComposable.kt @@ -0,0 +1,77 @@ +/* + * This file is part of GNU Taler + * (C) 2022 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 + */ + +/* + * The code in this file was copied from the Taler Wallet App + * source: https://git.taler.net/taler-android.git/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt + */ + +package ch.bfh.habej2.wallee_c2ec.withdrawal + +import android.graphics.Bitmap +import android.graphics.Color +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min +import com.google.zxing.BarcodeFormat.QR_CODE +import com.google.zxing.qrcode.QRCodeWriter + +@Composable +fun QRCode(qrCodeContent: String) { + + Column { + + val qrCodeSize = getQrCodeSize() + val btmp = makeQrCode(qrCodeContent).asImageBitmap() + + Image( + modifier = androidx.compose.ui.Modifier + .size(qrCodeSize) + .padding(vertical = 8.dp), + bitmap = btmp, + contentDescription = "Scan the QR Code to start withdrawal", + ) + } +} + +@Composable +fun getQrCodeSize(): Dp { + val configuration = LocalConfiguration.current + val screenHeight = configuration.screenHeightDp.dp + val screenWidth = configuration.screenWidthDp.dp + return min(screenHeight, screenWidth) +} + +private fun makeQrCode(text: String, size: Int = 256): Bitmap { + val qrCodeWriter = QRCodeWriter() + val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size) + val height = bitMatrix.height + val width = bitMatrix.width + val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) + for (x in 0 until width) { + for (y in 0 until height) { + bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) + } + } + return bmp +} \ 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 new file mode 100644 index 0000000..94238e7 --- /dev/null +++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt @@ -0,0 +1,234 @@ +package ch.bfh.habej2.wallee_c2ec.withdrawal + +import android.app.Activity +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig +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 +import com.wallee.android.till.sdk.data.TransactionProcessingBehavior +import java.math.BigDecimal +import java.util.Currency +import java.util.Optional + +class WithdrawalActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + + val model = WithdrawalViewModel() + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = "chooseExchangeScreen") { + composable("chooseExchangeScreen") { + ExchangeSelectionScreen(model) { + navController.navigate("registerWithdrawalScreen") + } + } + composable("registerWithdrawalScreen") { + RegisterWithdrawalScreen(model) { + navController.navigate("paymentScreen") + } + } + composable("paymentScreen") { PaymentScreen(model) } + } + } + } +} + +@Composable +fun RegisterWithdrawalScreen( + model: WithdrawalViewModel, + navigateToWhenRegistered: () -> Unit +) { + + val uiState by model.uiState.collectAsState() + val activity = (LocalContext.current as Activity) + + model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) { + activity.finish() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "QR-Code content: ${formatTalerUri(uiState.encodedWopid)}") + + QRCode(formatTalerUri(uiState.encodedWopid)) + + Button(onClick = { + model.withdrawalOperationFailed() + activity.finish() + }) { + Text(text = "abort") + } + } +} + +@Composable +fun PaymentScreen(model: WithdrawalViewModel) { + + val activity = LocalContext.current as Activity + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "present card, trigger payment") + + TextField( + value = "", + onValueChange = { + val optAmount = parseAmount(it) + if (optAmount.isPresent) { + model.updateAmount(optAmount.get()) + } + }, + label = { Text(text = "Enter amount") }, + placeholder = { Text(text = "amount") } + ) + + AuthorizePaymentButton(model = model) + + Button(onClick = { + model.withdrawalOperationFailed() + activity.finish() + }) { + Text(text = "abort") + } + } +} + +@Composable +fun AuthorizePaymentButton(model: WithdrawalViewModel) { + + val uiState by model.uiState.collectAsState() + val activity = LocalContext.current as Activity + val client = ApiClient(WalleeResponseHandler()) + + client.bind(activity) + + Button(enabled = false, onClick = { + val withdrawalAmount = LineItem + .ListBuilder( + uiState.encodedWopid, + BigDecimal("${uiState.amount.value}.${uiState.amount.frac}") + ) + .build() + + val transaction = Transaction.Builder(withdrawalAmount) + .setCurrency(Currency.getInstance(uiState.currency)) + .setInvoiceReference(uiState.encodedWopid) + .setMerchantReference(uiState.encodedWopid) + .setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY) + .build() + + try { + client.authorizeTransaction(transaction) + } catch (e: Exception) { + e.printStackTrace() + } + }) { + Text(text = "") + } +} + +@Composable +fun ExchangeSelectionScreen( + model: WithdrawalViewModel, + onNavigateToWithdrawal: () -> Unit +) { + + val activity = LocalContext.current as Activity + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "Choose the exchange to withdraw from") + + // TODO let user select exchanges from config here + // config must contain display name, credentials (generated by cli) + // and the base url of the c2ec bank-integration api + + val ctx = LocalContext.current + Button(onClick = { + // TODO trigger model.exchangeUpdated(...) + model.exchangeUpdated(TalerBankIntegrationConfig("","","","")) + onNavigateToWithdrawal() + }) { + Text(text = "withdraw") + } + + Button(onClick = { activity.finish() }) { + Text(text = "abort") + } + } +} + +@Composable +fun SummaryScreen(model: WithdrawalViewModel) { + + val activity = LocalContext.current as Activity + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "Transaction Executed") + + Button(onClick = { activity.finish() }) { + Text(text = "finish") + } + } +} + +/** + * Format expected X[.X], X an integer + */ +private fun parseAmount(inp: String): Optional { + + val points = inp.count { it == '.' } + if (points > 1) { + return Optional.empty() + } + + if (points == 1) { + val valueStr = inp.split(".")[0] + val fracStr = inp.split(".")[1] + return try { + val value = valueStr.toInt() + val frac = fracStr.toInt() + Optional.of(Amount(value, frac)) + } catch (ex: NumberFormatException) { + Optional.empty() + } + } + + return try { + val value = inp.toInt() + Optional.of(Amount(value, 0)) + } catch (ex: NumberFormatException) { + Optional.empty() + } +} + +private fun formatTalerUri(encodedWopid: String) = "taler://withdraw/$encodedWopid" 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 new file mode 100644 index 0000000..46490e0 --- /dev/null +++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt @@ -0,0 +1,118 @@ +package ch.bfh.habej2.wallee_c2ec.withdrawal + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ch.bfh.habej2.wallee_c2ec.client.taler.BankIntegrationClient +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig +import ch.bfh.habej2.wallee_c2ec.client.taler.model.PaymentNotification +import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.Base32Encode +import com.wallee.android.till.sdk.data.TransactionCompletionResponse +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 + +data class Amount( + val value: Int, + val frac: Int +) { + fun toBigDecimal(): BigDecimal = BigDecimal("$value.$frac") +} + +@Stable +interface WithdrawalOperationState{ + val exchangeBankIntegrationApiUrl: String + val encodedWopid: String + val amount: Amount + val currency: String + val payed: Boolean + val transaction: TransactionCompletionResponse? +} + +private class MutableWithdrawalOperationState: WithdrawalOperationState { + override var exchangeBankIntegrationApiUrl: String by mutableStateOf("") + override var encodedWopid: String by mutableStateOf("") + override var amount: Amount by mutableStateOf(Amount(0,0)) + override var currency: String by mutableStateOf("") + override var payed: Boolean by mutableStateOf(false) + override var transaction: TransactionCompletionResponse? by mutableStateOf(null) +} + +class WithdrawalViewModel( + vararg closeables: Closeable +) : ViewModel(*closeables) { + + private var bankIntegrationClient: BankIntegrationClient? = null + private val _uiState = MutableStateFlow(MutableWithdrawalOperationState()) + val uiState: StateFlow = _uiState + + init { + initializeWopid() + } + + fun exchangeUpdated(cfg: TalerBankIntegrationConfig) { + bankIntegrationClient = BankIntegrationClient(cfg) + _uiState.value = MutableWithdrawalOperationState() // reset withdrawal operation + initializeWopid() // initialize new withdrawal operation identifier + } + + fun initializeWopid() { + _uiState.value.encodedWopid = Base32Encode(wopid()) + } + + fun updateAmount(amount: Amount) { + _uiState.value.amount = amount + } + + fun updateCurrency(currency: String) { + _uiState.value.currency = currency + } + + fun updateWalleeTransaction(completion: TransactionCompletionResponse) { + _uiState.value.transaction = completion + } + + fun startAuthorizationWhenReadyOrAbort( + onSuccess: () -> Unit, + onFailure: () -> Unit + ) { + + return + + viewModelScope.launch { + onSuccess() // TODO +// val result = bankIntegrationClient!!.retrieveWithdrawalStatus(uiState.value.encodedWopid, 30000) +// if (result.isPresent) { +// onSuccess() +// } else { +// withdrawalOperationFailed() +// onFailure() +// } + } + } + + fun withdrawalOperationFailed() { + viewModelScope.launch { + bankIntegrationClient!!.abortWithdrawal(uiState.value.encodedWopid) + } + } + + fun confirmPayment() { + viewModelScope.launch{ + bankIntegrationClient!!.sendPaymentNotification(PaymentNotification()) + } + } + + private fun wopid(): ByteArray { + val wopid = ByteArray(32) + val rand = SecureRandom() + rand.nextBytes(wopid) // will seed automatically + return wopid + } +} \ No newline at end of file diff --git a/wallee-c2ec/gradle/libs.versions.toml b/wallee-c2ec/gradle/libs.versions.toml index 16e0f1a..b14cea4 100644 --- a/wallee-c2ec/gradle/libs.versions.toml +++ b/wallee-c2ec/gradle/libs.versions.toml @@ -14,6 +14,7 @@ composeBom = "2023.08.00" moshiKotlin = "1.15.1" okhttp = "4.12.0" sdk = "0.9.12" +navigationCompose = "2.7.7" [libraries] androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } @@ -36,6 +37,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3" moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } wallee-sdk = { module = "com.wallee.android.till:sdk", version.ref = "sdk" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } -- cgit v1.2.3