diff options
Diffstat (limited to 'wallet/src/main')
9 files changed, 342 insertions, 19 deletions
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" /> </application> + <queries> + <intent> + <data + android:host="iban" + android:scheme="payto" /> + </intent> + </queries> + </manifest> 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 <http://www.gnu.org/licenses/> + */ + +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<String>, +) + 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<Unit>(operation) { + private suspend fun acceptBankIntegratedWithdrawal(status: ReceivedDetails) { + api.request<Unit>("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" /> </com.google.android.material.textfield.TextInputLayout> 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 @@ -82,9 +82,19 @@ </fragment> <fragment + android:id="@+id/nav_exchange_manual_withdrawal_success" + android:name="net.taler.wallet.withdraw.ManualWithdrawSuccessFragment" + android:label="@string/withdraw_title"> + <action + android:id="@+id/action_nav_exchange_manual_withdrawal_success_to_nav_main" + app:destination="@id/nav_main" + app:popUpTo="@id/nav_main" /> + </fragment> + + <fragment android:id="@+id/nav_settings_backup" android:name="net.taler.wallet.settings.BackupSettingsFragment" - android:label="@string/nav_settings_backup"/> + android:label="@string/nav_settings_backup" /> <fragment android:id="@+id/nav_transactions" @@ -96,7 +106,11 @@ android:id="@+id/nav_transactions_detail_withdrawal" android:name="net.taler.wallet.transactions.TransactionWithdrawalFragment" android:label="@string/transactions_detail_title" - tools:layout="@layout/fragment_transaction_withdrawal" /> + tools:layout="@layout/fragment_transaction_withdrawal"> + <action + android:id="@+id/action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success" + app:destination="@id/nav_exchange_manual_withdrawal_success" /> + </fragment> <fragment android:id="@+id/nav_transactions_detail_payment" @@ -138,6 +152,10 @@ app:destination="@id/nav_main" app:popUpTo="@id/nav_main" /> <action + android:id="@+id/action_promptWithdraw_to_nav_exchange_manual_withdrawal_success" + app:destination="@id/nav_exchange_manual_withdrawal_success" + app:popUpTo="@id/nav_main" /> + <action android:id="@+id/action_promptWithdraw_to_errorFragment" app:destination="@id/errorFragment" app:popUpTo="@id/nav_main" /> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 5c27eb4..d2f181c 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -116,6 +116,13 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="withdraw_amount_error">Enter valid amount</string> <string name="withdraw_manual_payment_options">Payment options supported by %1$s:\n\n%2$s</string> <string name="withdraw_manual_check_fees">Check fees</string> + <string name="withdraw_manual_ready_title">Exchange is ready for withdrawal!</string> + <string name="withdraw_manual_ready_intro">To complete the process you need to wire %s to the exchange bank account</string> + <string name="withdraw_manual_ready_details_intro">Bank transfer details</string> + <string name="withdraw_manual_ready_iban">IBAN</string> + <string name="withdraw_manual_ready_subject">Subject</string> + <string name="withdraw_manual_ready_bank_button">Open in banking app</string> + <string name="withdraw_manual_ready_warning">Make sure to use the correct subject, otherwise the money will not arrive in this wallet.</string> <string name="withdraw_error_title">Withdrawal Error</string> <string name="withdraw_error_message">Withdrawing is currently not possible. Please try again later!</string> <string name="withdraw_error_test">Error withdrawing TESTKUDOS</string> |