cashless2ecash

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

commit be0bcae390ac5f978db5999a367c9c6497d4f10b
parent f902369eb6abef9ac76c7b3ea1a03127f55c7385
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Wed,  1 May 2024 10:54:44 +0200

fix: correct till-api version

Diffstat:
Mwallee-c2ec/app/build.gradle.kts | 2+-
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt | 7+------
Dwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt | 164-------------------------------------------------------------------------------
Awallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalsApiModel.kt | 26++++++++++++++++++++++++++
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt | 15---------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt | 8+++++++-
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt | 1-
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt | 2--
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt | 24++++--------------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt | 11++++++-----
Mwallee-c2ec/gradle/libs.versions.toml | 2+-
12 files changed, 206 insertions(+), 216 deletions(-)

diff --git a/wallee-c2ec/app/build.gradle.kts b/wallee-c2ec/app/build.gradle.kts @@ -10,7 +10,7 @@ android { defaultConfig { applicationId = "ch.bfh.habej2.wallee_c2ec" minSdk = 27 - targetSdk = 28 + targetSdk = 34 versionCode = 1 versionName = "1.0" 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 @@ -4,13 +4,13 @@ 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.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -41,9 +41,4 @@ class MainActivity : ComponentActivity() { } } } - - @Composable - fun SelectExchangeScreen() { - - } } 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,164 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec.client.taler - -import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerTerminalConfig -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 com.squareup.moshi.Moshi -import okhttp3.HttpUrl -import okhttp3.Interceptor -import okhttp3.MediaType -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response -import java.util.Optional - -class TerminalClientException( - val status: Int, - msg: String -): RuntimeException(msg) - -class TerminalClient( - private val config: TalerTerminalConfig -) { - - private val client: OkHttpClient = - OkHttpClient.Builder() - .addInterceptor(C2ECBasicAuthInterceptor(config)) - .build() - - private fun baseUrlBuilder() = HttpUrl.Builder() - .encodedPath(config.terminalApiBaseUrl) - - private fun terminalsConfigUrl() = baseUrlBuilder() - .addPathSegment("config") - .build() - - private fun setupWithdrawalUrl() = baseUrlBuilder() - .addPathSegment("withdrawals") - .build() - - private fun withdrawalsByWopid(encodedWopid: String) = baseUrlBuilder() - .addPathSegment("withdrawals") - .addPathSegment(encodedWopid) - .build() - - private fun <T> serializer(clazz: Class<T>) = Moshi.Builder().build().adapter(clazz) - - private fun withdrawalsConfirm(encodedWopid: String) = withdrawalsByWopid(encodedWopid) - .newBuilder() - .addPathSegment("check") - .build() - - private fun withdrawalsAbort(encodedWopid: String) = withdrawalsByWopid(encodedWopid) - .newBuilder() - .addPathSegment("abort") - .build() - - fun terminalsConfig(): Optional<TerminalApiConfig> { - val req = Request.Builder() - .get() - .url(terminalsConfigUrl()) - .build() - val response = client.newCall(req).execute() - return parseOrEmpty(response) - } - - fun setupWithdrawal(setupReq: TerminalWithdrawalSetup): Optional<TerminalWithdrawalSetupResponse> { - - val reqBody = serializer(TerminalWithdrawalSetup::class.java) - .toJson(setupReq) - .toRequestBody("application/json".toMediaType()) - val req = Request.Builder() - .post(reqBody) - .url(setupWithdrawalUrl()) - .build() - val response = client.newCall(req).execute() - return parseOrEmpty(response) - } - - fun retrieveWithdrawalStatus( - wopid: String, - longPollMs: Int, - oldState: WithdrawalOperationStatus = WithdrawalOperationStatus.PENDING - ): Optional<TerminalWithdrawalSetup> { - - val req = Request.Builder() - .get() - .url(withdrawalsByWopid(wopid) - .newBuilder() - .addQueryParameter("long_poll_ms", longPollMs.toString()) - .addQueryParameter("old_state", oldState.value) - .build() - ) - .build() - val response = client.newCall(req).execute() - return parseOrEmpty(response) - } - - fun sendConfirmationRequest(encodedWopid: String, confirmationRequest: TerminalWithdrawalConfirmationRequest) { - - val reqBody = serializer(TerminalWithdrawalConfirmationRequest::class.java) - .toJson(confirmationRequest) - .toRequestBody("application/json".toMediaType()) - val req = Request.Builder() - .post(reqBody) - .url(withdrawalsConfirm(encodedWopid)) - .build() - val response = client.newCall(req).execute() - if (response.code != 204) { - abortWithdrawal(encodedWopid) - } - } - - fun abortWithdrawal(encodedWopid: String) { - val req = Request.Builder() - .delete() - .url(withdrawalsAbort(encodedWopid)) - .build() - client.newCall(req).execute() - } - - private inline fun <reified T: Any> parseOrEmpty(response: Response): Optional<T> { - - if (response.isSuccessful) { - if (response.body != null) { - val content = serializer(T::class.java).fromJson(response.body!!.source()) - if (content != null) { - return Optional.of(content) - } - return Optional.empty() - } - return Optional.empty() - } - throw TerminalClientException(response.code, "request unsuccessful") - } - - private class C2ECBasicAuthInterceptor( - private val config: TalerTerminalConfig - ) : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - - val base64EncodedCredentials = java.util.Base64 - .getUrlEncoder() - .encode("${config.username}:${config.accessToken}".toByteArray()) - .toString() - - return chain.proceed( - chain.request().newBuilder() - .header("Authorization", base64EncodedCredentials) - .build() - ) - } - } -} - -private fun RequestBody.Companion.create(toMediaType: MediaType): Any { - TODO("Not yet implemented") -} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt @@ -0,0 +1,160 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler + +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerTerminalConfig +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 com.squareup.moshi.Moshi +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import java.util.Optional + +class TerminalClientException( + val status: Int, + msg: String +): RuntimeException(msg) + +class TerminalClientImplementation ( + private val config: TalerTerminalConfig +) { + + private val client: OkHttpClient = + OkHttpClient.Builder() + .addInterceptor(C2ECBasicAuthInterceptor(config)) + .build() + + private fun baseUrlBuilder() = HttpUrl.Builder() + .encodedPath(config.terminalApiBaseUrl) + + private fun terminalsConfigUrl() = baseUrlBuilder() + .addPathSegment("config") + .build() + + private fun setupWithdrawalUrl() = baseUrlBuilder() + .addPathSegment("withdrawals") + .build() + + private fun withdrawalsByWopid(encodedWopid: String) = baseUrlBuilder() + .addPathSegment("withdrawals") + .addPathSegment(encodedWopid) + .build() + + private fun <T> serializer(clazz: Class<T>) = Moshi.Builder().build().adapter(clazz) + + private fun withdrawalsConfirm(encodedWopid: String) = withdrawalsByWopid(encodedWopid) + .newBuilder() + .addPathSegment("check") + .build() + + private fun withdrawalsAbort(encodedWopid: String) = withdrawalsByWopid(encodedWopid) + .newBuilder() + .addPathSegment("abort") + .build() + + fun terminalsConfig(): Optional<TerminalApiConfig> { + val req = Request.Builder() + .get() + .url(terminalsConfigUrl()) + .build() + val response = client.newCall(req).execute() + return parseOrEmpty(response) + } + + fun setupWithdrawal(setupReq: TerminalWithdrawalSetup): Optional<TerminalWithdrawalSetupResponse> { + + val reqBody = serializer(TerminalWithdrawalSetup::class.java) + .toJson(setupReq) + .toRequestBody("application/json".toMediaType()) + val req = Request.Builder() + .post(reqBody) + .url(setupWithdrawalUrl()) + .build() + val response = client.newCall(req).execute() + return parseOrEmpty(response) + } + + fun retrieveWithdrawalStatus( + wopid: String, + longPollMs: Int, + oldState: WithdrawalOperationStatus = WithdrawalOperationStatus.PENDING + ): Optional<TerminalWithdrawalSetup> { + + val req = Request.Builder() + .get() + .url(withdrawalsByWopid(wopid) + .newBuilder() + .addQueryParameter("long_poll_ms", longPollMs.toString()) + .addQueryParameter("old_state", oldState.value) + .build() + ) + .build() + val response = client.newCall(req).execute() + return parseOrEmpty(response) + } + + fun sendConfirmationRequest(encodedWopid: String, confirmationRequest: TerminalWithdrawalConfirmationRequest) { + + val reqBody = serializer(TerminalWithdrawalConfirmationRequest::class.java) + .toJson(confirmationRequest) + .toRequestBody("application/json".toMediaType()) + val req = Request.Builder() + .post(reqBody) + .url(withdrawalsConfirm(encodedWopid)) + .build() + val response = client.newCall(req).execute() + if (response.code != 204) { + abortWithdrawal(encodedWopid) + } + } + + fun abortWithdrawal(encodedWopid: String) { + val req = Request.Builder() + .delete() + .url(withdrawalsAbort(encodedWopid)) + .build() + client.newCall(req).execute() + } + + private inline fun <reified T: Any> parseOrEmpty(response: Response): Optional<T> { + + if (response.isSuccessful) { + if (response.body != null) { + val content = serializer(T::class.java).fromJson(response.body!!.source()) + if (content != null) { + return Optional.of(content) + } + return Optional.empty() + } + return Optional.empty() + } + throw TerminalClientException(response.code, "request unsuccessful") + } + + private class C2ECBasicAuthInterceptor( + private val config: TalerTerminalConfig + ) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + + val base64EncodedCredentials = java.util.Base64 + .getUrlEncoder() + .encode("${config.username}:${config.accessToken}".toByteArray()) + .toString() + + return chain.proceed( + chain.request().newBuilder() + .header("Authorization", base64EncodedCredentials) + .build() + ) + } + } +} 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 @@ -23,3 +23,29 @@ data class TerminalWithdrawalConfirmationRequest( val providerTransactionId: String, val terminalFees: Amount ) + +enum class WithdrawalOperationStatus(val value: String) { + + PENDING("pending"), + SELECTED("selected"), + CONFIRMED("confirmed"), + ABORTED("aborted") +} + +data class BankWitdrawalOperationStatus( + val status: WithdrawalOperationStatus, + val amount: Amount, + val suggestedAmount: Amount, + val maxAmount: Amount, + val cardFees: Amount, + val senderWire: String, + val suggestedExchange: String, + val requiredExchange: String, + val confirmTransferUrl: String, + val wireTypes: Array<String>, + val selectedReservePub: String, + val selectedExchangeAccount: String, + val aborted: Boolean, + val selectionDone: Boolean, + val transferDone: Boolean +) diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt @@ -11,21 +11,6 @@ class WalleeResponseHandler( private val model: WithdrawalViewModel ) : ResponseHandler() { - override fun authorizeTransactionReply(response: TransactionResponse?) { - - // TODO find out how to read the transaction id here.... - - if (response == null) { - // TODO - return - } - - response.transaction.metaData.forEach{ - println("${it.key}=${it.value}") - } - response.transaction.metaData.get("id") - } - override fun completeTransactionReply(response: TransactionCompletionResponse?) { if (response == null) { 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 @@ -25,7 +25,8 @@ fun AmountScreen(model: WithdrawalViewModel, navigateToWhenAmountEntered: () -> TextField( value = "", onValueChange = { - model.updateAmount(it) + println(it) + //model.updateAmount(it) }, label = { Text(text = "Enter amount") }, placeholder = { Text(text = "amount") }, @@ -36,6 +37,11 @@ fun AmountScreen(model: WithdrawalViewModel, navigateToWhenAmountEntered: () -> ) Button(onClick = { + val success = model.setupWithdrawal() + if (!success) { + activity.finish() + } + model.withdrawalOperationFailed() navigateToWhenAmountEntered() }) { Text(text = "pay") 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 @@ -27,7 +27,6 @@ fun ExchangeSelectionScreen( // 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 = { model.exchangeUpdated( activity, diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt @@ -19,8 +19,6 @@ fun RegisterWithdrawalScreen( val uiState by model.uiState.collectAsState() val activity = (LocalContext.current as Activity) - model.setupWithdrawal() - model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) { activity.finish() } 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 @@ -3,6 +3,7 @@ package ch.bfh.habej2.wallee_c2ec.withdrawal import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -17,12 +18,10 @@ class WithdrawalActivity : ComponentActivity() { super.onCreate(savedInstanceState) val model = WithdrawalViewModel() - walleeClient = ApiClient(WalleeResponseHandler(this, model)) - walleeClient.bind(this) - walleeClient.checkApiServiceCompatibility() setContent { + val navController = rememberNavController() NavHost(navController = navController, startDestination = "chooseExchangeScreen") { @@ -46,21 +45,10 @@ class WithdrawalActivity : ComponentActivity() { } } } - } - - override fun onStart() { - super.onStart() - walleeClient.bind(this) - } - override fun onResume() { - super.onResume() + walleeClient = ApiClient(WalleeResponseHandler(this, model)) walleeClient.bind(this) - } - - override fun onStop() { - super.onStop() - walleeClient.unbind(this) + walleeClient.checkApiServiceCompatibility() } override fun onDestroy() { @@ -68,8 +56,4 @@ class WithdrawalActivity : ComponentActivity() { walleeClient.unbind(this) } - override fun onPause() { - super.onPause() - walleeClient.unbind(this) - } } 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 @@ -8,7 +8,7 @@ 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.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.model.TerminalWithdrawalConfirmationRequest import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup @@ -54,13 +54,13 @@ class WithdrawalViewModel( vararg closeables: Closeable ) : ViewModel(*closeables) { - private var terminalClient: TerminalClient? = null + private var terminalClient: TerminalClientImplementation? = null private val _uiState = MutableStateFlow(MutableWithdrawalOperationState()) val uiState: StateFlow<WithdrawalOperationState> = _uiState fun exchangeUpdated(activity: Activity, cfg: TalerTerminalConfig) { - terminalClient = TerminalClient(cfg) + terminalClient = TerminalClientImplementation(cfg) _uiState.value = MutableWithdrawalOperationState() // reset withdrawal operation val optionalApiCfg = terminalClient!!.terminalsConfig() if (!optionalApiCfg.isPresent) { @@ -70,7 +70,7 @@ class WithdrawalViewModel( updateCurrency(optionalApiCfg.get().currency) } - fun setupWithdrawal() { + fun setupWithdrawal(): Boolean { val setupReq = TerminalWithdrawalSetup( _uiState.value.requestUid, @@ -79,10 +79,11 @@ class WithdrawalViewModel( val res = terminalClient!!.setupWithdrawal(setupReq) if (!res.isPresent) { - withdrawalOperationFailed() + return false } _uiState.value.encodedWopid = res.get().withdrawalId + return true } fun updateAmount(amount: String) { diff --git a/wallee-c2ec/gradle/libs.versions.toml b/wallee-c2ec/gradle/libs.versions.toml @@ -13,7 +13,7 @@ activityCompose = "1.7.0" composeBom = "2023.08.00" moshiKotlin = "1.15.1" okhttp = "4.12.0" -sdk = "0.9.12" +sdk = "0.9.20" navigationCompose = "2.7.7" [libraries]