From 095f67dd25ceeff5df388ef42f73de963dd9348b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 7 Dec 2021 16:23:49 -0300 Subject: Show bank details for manual withdrawal --- build.gradle | 6 +- cashier/build.gradle | 2 +- merchant-terminal/build.gradle | 2 +- taler-kotlin-android/build.gradle | 2 +- .../src/main/res/values/colors.xml | 3 + wallet/build.gradle | 17 +- wallet/src/main/AndroidManifest.xml | 8 + .../transactions/TransactionWithdrawalFragment.kt | 22 ++- .../wallet/withdraw/ManualWithdrawFragment.kt | 2 +- .../withdraw/ManualWithdrawSuccessFragment.kt | 217 +++++++++++++++++++++ .../wallet/withdraw/PromptWithdrawFragment.kt | 6 +- .../net/taler/wallet/withdraw/WithdrawManager.kt | 74 +++++-- .../main/res/layout/fragment_manual_withdraw.xml | 3 +- wallet/src/main/res/navigation/nav_graph.xml | 22 ++- wallet/src/main/res/values/strings.xml | 7 + 15 files changed, 366 insertions(+), 27 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt diff --git a/build.gradle b/build.gradle index 06bd425..8967f1f 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,8 @@ buildscript { ext.ktor_version = "1.6.3" ext.nav_version = "2.3.5" ext.material_version = "1.4.0" - ext.lifecycle_version = "2.3.1" - ext.constraintlayout_version = "2.1.1" + ext.lifecycle_version = "2.4.0" + ext.constraintlayout_version = "2.1.2" ext.junit_version = "4.13.2" // check https://android-rebuilds.beuc.net/ for availability of free build tools ext.build_tools_version = "30.0.3" @@ -15,7 +15,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" diff --git a/cashier/build.gradle b/cashier/build.gradle index 54b2df7..ae7a5a9 100644 --- a/cashier/build.gradle +++ b/cashier/build.gradle @@ -22,7 +22,7 @@ plugins { } android { - compileSdkVersion 30 + compileSdkVersion 31 //noinspection GradleDependency buildToolsVersion "$build_tools_version" diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle index e7a3bcc..5354da5 100644 --- a/merchant-terminal/build.gradle +++ b/merchant-terminal/build.gradle @@ -6,7 +6,7 @@ plugins { } android { - compileSdkVersion 30 + compileSdkVersion 31 //noinspection GradleDependency buildToolsVersion "$build_tools_version" diff --git a/taler-kotlin-android/build.gradle b/taler-kotlin-android/build.gradle index 7d3d8e4..4661b09 100644 --- a/taler-kotlin-android/build.gradle +++ b/taler-kotlin-android/build.gradle @@ -21,7 +21,7 @@ plugins { } android { - compileSdkVersion 30 + compileSdkVersion 31 //noinspection GradleDependency buildToolsVersion "$build_tools_version" diff --git a/taler-kotlin-android/src/main/res/values/colors.xml b/taler-kotlin-android/src/main/res/values/colors.xml index c916442..5eb0587 100644 --- a/taler-kotlin-android/src/main/res/values/colors.xml +++ b/taler-kotlin-android/src/main/res/values/colors.xml @@ -21,4 +21,7 @@ #388E3C #C62828 + #fff3cd + #ffecb5 + #664d03 diff --git a/wallet/build.gradle b/wallet/build.gradle index e5da3a3..27894e9 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -39,7 +39,7 @@ def gitCommit = { -> } android { - compileSdkVersion 30 + compileSdkVersion 31 //noinspection GradleDependency buildToolsVersion "$build_tools_version" @@ -91,8 +91,13 @@ android { jvmTarget = "1.8" } + composeOptions { + kotlinCompilerExtensionVersion '1.0.5' + } + buildFeatures { - viewBinding = true + viewBinding true + compose true } packagingOptions { @@ -119,6 +124,14 @@ dependencies { implementation "com.google.android.material:material:$material_version" implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" + // Compose + implementation 'androidx.activity:activity-compose:1.4.0' + implementation 'androidx.compose.material:material:1.0.5' + implementation 'androidx.compose.animation:animation:1.0.5' + implementation 'androidx.compose.ui:ui-tooling:1.0.5' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0' + implementation "com.google.android.material:compose-theme-adapter:1.1.1" + // Lists and Selection implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview-selection:1.1.0" diff --git a/wallet/src/main/AndroidManifest.xml b/wallet/src/main/AndroidManifest.xml index b011583..963032c 100644 --- a/wallet/src/main/AndroidManifest.xml +++ b/wallet/src/main/AndroidManifest.xml @@ -83,4 +83,12 @@ android:process=":WalletBackendService" /> + + + + + + diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt index 8a45bec..319aa7e 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt @@ -23,21 +23,27 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController import net.taler.common.startActivitySafe import net.taler.common.toAbsoluteTime +import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.cleanExchange import net.taler.wallet.databinding.FragmentTransactionWithdrawalBinding +import net.taler.wallet.withdraw.createManualTransferRequired class TransactionWithdrawalFragment : TransactionDetailFragment() { + private val model: MainViewModel by activityViewModels() + private val withdrawManager by lazy { model.withdrawManager } private lateinit var ui: FragmentTransactionWithdrawalBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { + savedInstanceState: Bundle?, + ): View { ui = FragmentTransactionWithdrawalBinding.inflate(inflater, container, false) return ui.root } @@ -55,6 +61,18 @@ class TransactionWithdrawalFragment : TransactionDetailFragment() { data = Uri.parse(t.withdrawalDetails.bankConfirmationUrl) } ui.confirmWithdrawalButton.setOnClickListener { startActivitySafe(i) } + } else if (t.pending && !t.confirmed && t.withdrawalDetails is WithdrawalDetails.ManualTransfer) { + ui.confirmWithdrawalButton.setText(R.string.withdraw_manual_ready_details_intro) + ui.confirmWithdrawalButton.setOnClickListener { + val status = createManualTransferRequired( + amount = t.amountRaw, + exchangeBaseUrl = t.exchangeBaseUrl, + // TODO what if there's more than one or no URI? + uriStr = t.withdrawalDetails.exchangePaytoUris[0], + ) + withdrawManager.viewManualWithdrawal(status) + findNavController().navigate(R.id.action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success) + } } else ui.confirmWithdrawalButton.visibility = View.GONE ui.chosenAmountLabel.text = getString(R.string.amount_chosen) ui.chosenAmountView.text = diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt index 3acb29f..e78ff44 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt @@ -44,7 +44,7 @@ class ManualWithdrawFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { ui = FragmentManualWithdrawBinding.inflate(inflater, container, false) return ui.root } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt new file mode 100644 index 0000000..1f84278 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt @@ -0,0 +1,217 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.wallet.withdraw + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.composethemeadapter.MdcTheme +import net.taler.common.startActivitySafe +import net.taler.lib.common.Amount +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.cleanExchange + +class ManualWithdrawSuccessFragment : Fragment() { + private val model: MainViewModel by activityViewModels() + private val withdrawManager by lazy { model.withdrawManager } + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = ComposeView(requireContext()).apply { + val status = withdrawManager.withdrawStatus.value as WithdrawStatus.ManualTransferRequired + val intent = Intent().apply { + data = status.uri + } + // TODO test if this works with an actual payto:// handling app + val componentName = intent.resolveActivity(requireContext().packageManager) + val onBankAppClick = if (componentName == null) null else { + { startActivitySafe(intent) } + } + setContent { + MdcTheme { + Surface { + Screen(status, onBankAppClick) + } + } + } + } + + override fun onStart() { + super.onStart() + activity?.setTitle(R.string.withdraw_title) + } +} + +@Composable +private fun Screen( + status: WithdrawStatus.ManualTransferRequired, + bankAppClick: (() -> Unit)?, +) { + Column(modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp) + .wrapContentWidth(CenterHorizontally) + ) { + Text( + text = stringResource(R.string.withdraw_manual_ready_title), + style = MaterialTheme.typography.h5, + ) + Text( + text = stringResource(R.string.withdraw_manual_ready_intro, + status.amountRaw.toString()), + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = 8.dp) + ) + Text( + text = stringResource(R.string.withdraw_manual_ready_details_intro), + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = 8.dp) + ) + Row { + Text( + text = stringResource(R.string.withdraw_manual_ready_iban), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.3f) + ) + Text( + text = status.iban, + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.7f) + ) + } + Row { + Text( + text = stringResource(R.string.withdraw_manual_ready_subject), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.3f) + ) + Text( + text = status.subject, + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.7f) + ) + } + Row { + Text( + text = stringResource(R.string.amount_chosen), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.3f) + ) + Text( + text = status.amountRaw.toString(), + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.7f) + ) + } + Row { + Text( + text = stringResource(R.string.withdraw_exchange), + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.3f) + ) + Text( + text = cleanExchange(status.exchangeBaseUrl), + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.7f) + .alpha(0.7f) + ) + } + Text( + text = stringResource(R.string.withdraw_manual_ready_warning), + style = MaterialTheme.typography.body2, + color = colorResource(R.color.notice_text), + modifier = Modifier + .padding(all = 8.dp) + .background(colorResource(R.color.notice_background)) + .border(BorderStroke(2.dp, colorResource(R.color.notice_border))) + .padding(all = 16.dp) + ) + if (bankAppClick != null) { + Button( + onClick = bankAppClick, + modifier = Modifier + .padding(vertical = 16.dp) + .align(CenterHorizontally), + ) { + Text(text = stringResource(R.string.withdraw_manual_ready_bank_button)) + } + } + } +} + +@Preview +@Composable +fun PreviewScreen() { + Surface { + Screen(WithdrawStatus.ManualTransferRequired( + exchangeBaseUrl = "test.exchange.taler.net", + uri = Uri.parse("https://taler.net"), + iban = "ASDQWEASDZXCASDQWE", + subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", + amountRaw = Amount("KUDOS", 10, 0) + )) {} + } +} diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt index 38e09fa..08cbc2e 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -49,7 +49,7 @@ class PromptWithdrawFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { ui = FragmentPromptWithdrawBinding.inflate(inflater, container, false) return ui.root } @@ -80,6 +80,10 @@ class PromptWithdrawFragment : Fragment() { is TosReviewRequired -> onTosReviewRequired(status) is ReceivedDetails -> onReceivedDetails(status) is Withdrawing -> model.showProgressBar.value = true + is WithdrawStatus.ManualTransferRequired -> { + model.showProgressBar.value = false + findNavController().navigate(R.id.action_promptWithdraw_to_nav_exchange_manual_withdrawal_success) + } is WithdrawStatus.Success -> { model.showProgressBar.value = false withdrawManager.withdrawStatus.value = null diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt index cc4c057..858d63e 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -16,6 +16,7 @@ package net.taler.wallet.withdraw +import android.net.Uri import android.util.Log import androidx.annotation.UiThread import androidx.lifecycle.LiveData @@ -56,6 +57,14 @@ sealed class WithdrawStatus { object Withdrawing : WithdrawStatus() data class Success(val currency: String) : WithdrawStatus() + data class ManualTransferRequired( + val exchangeBaseUrl: String, + val uri: Uri, + val iban: String, + val subject: String, + val amountRaw: Amount, + ) : WithdrawStatus() + data class Error(val message: String?) : WithdrawStatus() } @@ -79,6 +88,11 @@ data class WithdrawalDetails( val amountEffective: Amount, ) +@Serializable +data class AcceptManualWithdrawalResponse( + val exchangePaytoUris: List, +) + data class ExchangeSelection( val amount: Amount, val talerWithdrawUri: String, @@ -197,31 +211,69 @@ class WithdrawManager( @UiThread fun acceptWithdrawal() = scope.launch { val status = withdrawStatus.value as ReceivedDetails - val operation = if (status.talerWithdrawUri == null) { - "acceptManualWithdrawal" + withdrawStatus.value = WithdrawStatus.Withdrawing + if (status.talerWithdrawUri == null) { + acceptManualWithdrawal(status) } else { - "acceptBankIntegratedWithdrawal" + acceptBankIntegratedWithdrawal(status) } - withdrawStatus.value = WithdrawStatus.Withdrawing + } - api.request(operation) { + private suspend fun acceptBankIntegratedWithdrawal(status: ReceivedDetails) { + api.request("acceptBankIntegratedWithdrawal") { put("exchangeBaseUrl", status.exchangeBaseUrl) - if (status.talerWithdrawUri == null) { - put("amount", status.amountRaw.toJSONString()) - } else { - put("talerWithdrawUri", status.talerWithdrawUri) - } + put("talerWithdrawUri", status.talerWithdrawUri) }.onError { - handleError(operation, it) + handleError("acceptBankIntegratedWithdrawal", it) }.onSuccess { withdrawStatus.value = WithdrawStatus.Success(status.amountRaw.currency) } } + private suspend fun acceptManualWithdrawal(status: ReceivedDetails) { + api.request("acceptManualWithdrawal", AcceptManualWithdrawalResponse.serializer()) { + put("exchangeBaseUrl", status.exchangeBaseUrl) + put("amount", status.amountRaw.toJSONString()) + }.onError { + handleError("acceptManualWithdrawal", it) + }.onSuccess { response -> + withdrawStatus.value = createManualTransferRequired( + amount = status.amountRaw, + exchangeBaseUrl = status.exchangeBaseUrl, + // TODO what if there's more than one or no URI? + uriStr = "payto://iban/ASDQWEASDZXCASDQWE?amount=KUDOS%3A10&message=Taler+Withdrawal+P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", // response.exchangePaytoUris[0], + // "payto://x-taler-bank/bank.demo.taler.net/Exchange?amount=KUDOS%3A10&message=Taler+Withdrawal+P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG" + ) + } + } + @UiThread private fun handleError(operation: String, error: TalerErrorInfo) { Log.e(TAG, "Error $operation $error") withdrawStatus.value = WithdrawStatus.Error(error.userFacingMsg) } + /** + * A hack to be able to view bank details for manual withdrawal with the same logic. + * Don't call this from ongoing withdrawal processes as it destroys state. + */ + fun viewManualWithdrawal(status: WithdrawStatus.ManualTransferRequired) { + withdrawStatus.value = status + } + +} + +fun createManualTransferRequired( + amount: Amount, + exchangeBaseUrl: String, + uriStr: String, +): WithdrawStatus.ManualTransferRequired { + val uri = Uri.parse(uriStr) + return WithdrawStatus.ManualTransferRequired( + exchangeBaseUrl = exchangeBaseUrl, + uri = uri, + iban = uri.lastPathSegment!!, + subject = uri.getQueryParameter("message")!!, + amountRaw = amount, + ) } diff --git a/wallet/src/main/res/layout/fragment_manual_withdraw.xml b/wallet/src/main/res/layout/fragment_manual_withdraw.xml index 5b37d2a..724c3e2 100644 --- a/wallet/src/main/res/layout/fragment_manual_withdraw.xml +++ b/wallet/src/main/res/layout/fragment_manual_withdraw.xml @@ -63,7 +63,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" - android:hint="@string/withdraw_amount" + android:minWidth="128dp" app:boxBackgroundMode="outline" app:endIconDrawable="@drawable/ic_cancel" app:endIconMode="clear_text" @@ -76,7 +76,6 @@ android:id="@+id/amountView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:ems="10" android:inputType="number" /> diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index e8929c9..469a399 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -81,10 +81,20 @@ app:destination="@id/promptWithdraw" /> + + + + + android:label="@string/nav_settings_backup" /> + tools:layout="@layout/fragment_transaction_withdrawal"> + + + Enter valid amount Payment options supported by %1$s:\n\n%2$s Check fees + Exchange is ready for withdrawal! + To complete the process you need to wire %s to the exchange bank account + Bank transfer details + IBAN + Subject + Open in banking app + Make sure to use the correct subject, otherwise the money will not arrive in this wallet. Withdrawal Error Withdrawing is currently not possible. Please try again later! Error withdrawing TESTKUDOS -- cgit v1.2.3