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 --- .../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 +++++-- 5 files changed, 306 insertions(+), 15 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt (limited to 'wallet/src/main/java/net/taler') 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, + ) } -- cgit v1.2.3