summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel-Haeberli <haebu@rubigen.ch>2024-04-20 13:28:42 +0200
committerJoel-Haeberli <haebu@rubigen.ch>2024-04-20 13:28:42 +0200
commit39f0572004f1cc8d9ee999eb47630e2e8a7099ca (patch)
treea668e6afc95fe4893f3b80415859fa54f9a3f283
parent76bb4ceeb9799fba19a10b267233571b0de07cab (diff)
downloadcashless2ecash-39f0572004f1cc8d9ee999eb47630e2e8a7099ca.tar.gz
cashless2ecash-39f0572004f1cc8d9ee999eb47630e2e8a7099ca.tar.bz2
cashless2ecash-39f0572004f1cc8d9ee999eb47630e2e8a7099ca.zip
feat(wallee app): navigation and state handling using view model
-rw-r--r--wallee-c2ec/app/build.gradle.kts1
-rw-r--r--wallee-c2ec/app/src/main/AndroidManifest.xml21
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt56
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt17
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt127
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt65
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt102
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt2
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt (renamed from wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt)2
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TerminalConfig.kt (renamed from wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt)2
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt8
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt7
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/QRCodeComposable.kt (renamed from wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt)23
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt234
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt118
-rw-r--r--wallee-c2ec/gradle/libs.versions.toml2
16 files changed, 402 insertions, 385 deletions
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
@@ -13,21 +13,6 @@
android:theme="@style/Theme.Walleec2ec"
tools:targetApi="31">
<activity
- android:name=".PaymentActivity"
- android:exported="false"
- android:label="@string/title_activity_payment"
- android:theme="@style/Theme.Walleec2ec" />
- <activity
- android:name=".WithdrawalCreationActivity"
- android:exported="false"
- android:label="@string/title_activity_withdrawal_creation"
- android:theme="@style/Theme.Walleec2ec" />
- <activity
- android:name=".ExchangeActivity"
- android:exported="false"
- android:label="@string/title_activity_exchange"
- android:theme="@style/Theme.Walleec2ec" />
- <activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
@@ -38,6 +23,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <activity
+ android:name=".withdrawal.WithdrawalActivity"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.Walleec2ec">
+ </activity>
</application>
</manifest> \ 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<Amount> {
-
- 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/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/config/TalerBankIntegrationConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt
index a5dcab9..8ca70f3 100644
--- 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/client/taler/config/TalerBankIntegrationConfig.kt
@@ -1,4 +1,4 @@
-package ch.bfh.habej2.wallee_c2ec.config
+package ch.bfh.habej2.wallee_c2ec.client.taler.config
// TODO how to configure ?? -> implement ativity allowing to choose one of the configured exchanges
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/client/taler/config/TerminalConfig.kt
index e69c74b..0ef0ae6 100644
--- 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/client/taler/config/TerminalConfig.kt
@@ -1,4 +1,4 @@
-package ch.bfh.habej2.wallee_c2ec.config
+package ch.bfh.habej2.wallee_c2ec.client.taler.config
// TODO how to configure ??
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/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/QRCodeComposable.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/QRCodeComposable.kt
index 3fe6004..ed7cbc8 100644
--- 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/withdrawal/QRCodeComposable.kt
@@ -1,4 +1,25 @@
-package ch.bfh.habej2.wallee_c2ec
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+/*
+ * 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
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<Amount> {
+
+ 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<WithdrawalOperationState> = _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" }