From 11400010de1c6198ad7eba6eb256bc0bad934021 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Fri, 10 Jan 2025 16:20:01 +0700 Subject: [PATCH] fix: coinbase 2fa send error --- .../coinbase/model/CoinbaseErrorResponse.kt | 7 +- .../coinbase/repository/CoinBaseRepository.kt | 6 +- .../coinbase/ui/EnterTwoFaCodeFragment.kt | 44 +++++----- .../viewmodels/EnterTwoFaCodeViewModel.kt | 87 +++++++------------ .../src/main/res/navigation/nav_coinbase.xml | 2 +- 5 files changed, 63 insertions(+), 83 deletions(-) diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinbaseErrorResponse.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinbaseErrorResponse.kt index 0dcbda96ca..9f074274cf 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinbaseErrorResponse.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinbaseErrorResponse.kt @@ -19,7 +19,9 @@ package org.dash.wallet.integrations.coinbase.model import android.os.Parcelable import com.google.gson.Gson +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize +import org.dash.wallet.integrations.coinbase.CoinbaseConstants enum class CoinbaseErrorType { NONE, @@ -58,4 +60,7 @@ data class CoinbaseErrorResponse( data class Error( val id: String? = null, val message: String? = null -) : Parcelable +) : Parcelable { + @IgnoredOnParcel + val isInvalidRequest = id == CoinbaseConstants.ERROR_ID_INVALID_REQUEST +} diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt index 037a4012b9..50b7558e44 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt @@ -60,7 +60,7 @@ interface CoinBaseRepositoryInt { suspend fun sendFundsToWallet( sendTransactionToWalletParams: SendTransactionToWalletParams, api2FATokenVersion: String? - ): ResponseResource + ): SendTransactionToWalletResponse? suspend fun swapTrade(tradesRequest: TradesRequest): ResponseResource suspend fun commitSwapTrade(buyOrderId: String): ResponseResource suspend fun completeCoinbaseAuthentication(authorizationCode: String): Boolean @@ -211,9 +211,9 @@ class CoinBaseRepository @Inject constructor( override suspend fun sendFundsToWallet( sendTransactionToWalletParams: SendTransactionToWalletParams, api2FATokenVersion: String? - ) = safeApiCall { + ): SendTransactionToWalletResponse? { val userAccountId = config.get(CoinbaseConfig.USER_ACCOUNT_ID) ?: "" - servicesApi.sendCoinsToWallet( + return servicesApi.sendCoinsToWallet( accountId = userAccountId, sendTransactionToWalletParams = sendTransactionToWalletParams, api2FATokenVersion = api2FATokenVersion diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterTwoFaCodeFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterTwoFaCodeFragment.kt index a180a937eb..4af77afa20 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterTwoFaCodeFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterTwoFaCodeFragment.kt @@ -31,9 +31,9 @@ import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import org.dash.wallet.common.ui.LockScreenAware -import org.dash.wallet.common.util.Constants import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.enter_amount.NumericKeyboardView import org.dash.wallet.common.ui.setRoundedBackground @@ -47,8 +47,8 @@ import org.dash.wallet.integrations.coinbase.viewmodels.TransactionState @AndroidEntryPoint class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), LockScreenAware { - private val binding by viewBinding(EnterTwoFaCodeFragmentBinding::bind) + private val args by navArgs() private val viewModel by viewModels() private lateinit var loadingDialog: AdaptiveDialog private var onBackPressedCallback: OnBackPressedCallback? = null @@ -56,11 +56,10 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) handleBackPress() - val params = arguments?.let { EnterTwoFaCodeFragmentArgs.fromBundle(it).transactionParams } - viewModel.sendInitialTransactionToSMSTwoFactorAuth(params?.params) + viewModel.sendInitialTransactionToSMSTwoFactorAuth(args.transactionParams.params) binding.verifyBtn.setOnClickListener { - viewModel.verifyUserAndCompleteTransaction(params?.params, binding.enterCodeField.text.toString()) + viewModel.verifyUserAndCompleteTransaction(binding.enterCodeField.text.toString()) } binding.keyboardView.onKeyboardActionListener = keyboardActionListener @@ -68,11 +67,11 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo setLoadingState(it) } - viewModel.transactionState.observe(viewLifecycleOwner){ state -> - params?.let { setTransactionState(it.type, state) } + viewModel.transactionState.observe(viewLifecycleOwner) { state -> + setTransactionState(args.transactionParams.type, state) } - viewModel.twoFaErrorState.observe(viewLifecycleOwner){ + viewModel.twoFaErrorState.observe(viewLifecycleOwner) { binding.enterCodeField.setRoundedBackground(org.dash.wallet.common.R.style.InputErrorBackground) binding.incorrectCodeGroup.isVisible = true binding.enterCodeDetails.isVisible = false @@ -93,21 +92,21 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo val intent = Intent(ACTION_VIEW) intent.data = Uri.parse(helpUrl) startActivity(intent) - }catch (e: ActivityNotFoundException){ + }catch (e: ActivityNotFoundException) { Toast.makeText(requireActivity(), helpUrl, Toast.LENGTH_SHORT).show() } } private fun setTransactionState(transactionType: TransactionType, state: TransactionState) { - if (state.isTransactionSuccessful){ - when(transactionType){ + if (state.isTransactionSuccessful) { + when(transactionType) { TransactionType.BuyDash -> showTransactionStateDialog(CoinBaseResultDialog.Type.DEPOSIT_SUCCESS) TransactionType.BuySwap -> showTransactionStateDialog(CoinBaseResultDialog.Type.CONVERSION_SUCCESS) TransactionType.TransferDash -> showTransactionStateDialog(CoinBaseResultDialog.Type.TRANSFER_DASH_SUCCESS) else -> {} } } else { - when(transactionType){ + when(transactionType) { TransactionType.BuyDash -> showTransactionStateDialog(CoinBaseResultDialog.Type.DEPOSIT_ERROR, state.responseMessage) TransactionType.BuySwap -> showTransactionStateDialog(CoinBaseResultDialog.Type.TRANSFER_DASH_ERROR, state.responseMessage) TransactionType.TransferDash -> showTransactionStateDialog(CoinBaseResultDialog.Type.TRANSFER_DASH_ERROR, state.responseMessage) @@ -117,7 +116,7 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo } private fun setLoadingState(showLoading: Boolean) { - if (showLoading){ + if (showLoading) { showProgressDialog() } else { hideProgressDialog() @@ -127,7 +126,7 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo private val keyboardActionListener = object : NumericKeyboardView.OnKeyboardActionListener { var value = StringBuilder() - fun refreshValue(){ + fun refreshValue() { value.clear() value.append(binding.enterCodeField.text.toString()) } @@ -140,9 +139,9 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo override fun onBack(longClick: Boolean) { refreshValue() - if (longClick){ + if (longClick) { value.clear() - } else if (value.isNotEmpty()){ + } else if (value.isNotEmpty()) { value.deleteCharAt(value.length - 1) } applyNewValue(value.toString()) @@ -168,16 +167,16 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo } } - private fun showProgressDialog(){ - if (::loadingDialog.isInitialized && loadingDialog.dialog?.isShowing == true){ + private fun showProgressDialog() { + if (::loadingDialog.isInitialized && loadingDialog.dialog?.isShowing == true) { loadingDialog.dismissAllowingStateLoss() } loadingDialog = AdaptiveDialog.progress(getString(R.string.loading)) loadingDialog.show(parentFragmentManager, tag) } - private fun hideProgressDialog(){ - if (loadingDialog.isAdded){ + private fun hideProgressDialog() { + if (loadingDialog.isAdded) { loadingDialog.dismissAllowingStateLoss() } } @@ -189,13 +188,12 @@ class EnterTwoFaCodeFragment : Fragment(R.layout.enter_two_fa_code_fragment), Lo object : CoinBaseResultDialog.CoinBaseResultDialogButtonsClickListener { override fun onPositiveButtonClick(type: CoinBaseResultDialog.Type) { when (type) { - CoinBaseResultDialog.Type.TRANSFER_DASH_ERROR , CoinBaseResultDialog.Type.DEPOSIT_ERROR-> { + CoinBaseResultDialog.Type.TRANSFER_DASH_ERROR, CoinBaseResultDialog.Type.DEPOSIT_ERROR -> { viewModel.logRetry(type) - viewModel.isRetryingTransfer(true) dismiss() binding.enterCodeField.text?.clear() } - CoinBaseResultDialog.Type.CONVERSION_ERROR-> { + CoinBaseResultDialog.Type.CONVERSION_ERROR -> { viewModel.logRetry(type) dismiss() findNavController().popBackStack() diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterTwoFaCodeViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterTwoFaCodeViewModel.kt index 5e756fd046..2269aebc4d 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterTwoFaCodeViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterTwoFaCodeViewModel.kt @@ -22,18 +22,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.dash.wallet.common.data.ResponseResource import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService -import org.dash.wallet.integrations.coinbase.CoinbaseConstants import org.dash.wallet.integrations.coinbase.model.CoinbaseErrorResponse import org.dash.wallet.integrations.coinbase.model.SendTransactionToWalletParams import org.dash.wallet.integrations.coinbase.repository.CoinBaseRepositoryInt import org.dash.wallet.integrations.coinbase.ui.dialogs.CoinBaseResultDialog -import java.io.IOException +import retrofit2.HttpException import java.util.UUID import javax.inject.Inject @@ -42,7 +39,7 @@ class EnterTwoFaCodeViewModel @Inject constructor( private val coinBaseRepository: CoinBaseRepositoryInt, private val analyticsService: AnalyticsService ) : ViewModel() { - + private lateinit var transactionParams: SendTransactionToWalletParams private val _loadingState: MutableLiveData = MutableLiveData() val loadingState: LiveData get() = _loadingState @@ -53,68 +50,48 @@ class EnterTwoFaCodeViewModel @Inject constructor( val twoFaErrorState = SingleLiveEvent() - private var _isRetryingTransfer: Boolean = false + fun sendInitialTransactionToSMSTwoFactorAuth(params: SendTransactionToWalletParams) = viewModelScope.launch { + transactionParams = params.copy(idem = UUID.randomUUID().toString()) - fun isRetryingTransfer(isRetryingTransfer: Boolean) { - _isRetryingTransfer = isRetryingTransfer - } + try { + coinBaseRepository.sendFundsToWallet(transactionParams, null) + } catch (ex: HttpException) { + // Meant to fail with 2fa required error - fun sendInitialTransactionToSMSTwoFactorAuth( - params: SendTransactionToWalletParams? - ) = viewModelScope.launch { - val sendTransactionToWalletParams = params?.copy(idem = UUID.randomUUID().toString()) - sendTransactionToWalletParams?.let { - coinBaseRepository.sendFundsToWallet(it, null) + // TODO: does every account has 2fa? + // iOS does a regular request first and only requires 2fa input in case of failure } } - fun verifyUserAndCompleteTransaction( - params: SendTransactionToWalletParams?, - twoFaCode: String - ) = viewModelScope.launch(Dispatchers.Main) { + fun verifyUserAndCompleteTransaction(twoFaCode: String) = viewModelScope.launch { _loadingState.value = true - val sendTransactionToWalletParams = if (_isRetryingTransfer) { - params?.copy(idem = UUID.randomUUID().toString()) - } else { - params - } - - sendTransactionToWalletParams?.let { - _isRetryingTransfer = false - when (val result = coinBaseRepository.sendFundsToWallet(it, twoFaCode)) { - is ResponseResource.Success -> { - _loadingState.value = false - if (result.value == null) { - _transactionState.value = TransactionState(false) - } else { - _transactionState.value = TransactionState(true) - } - } - - is ResponseResource.Failure -> { - _loadingState.value = false - try { - val error = result.errorBody - if (result.errorCode == 400 || result.errorCode == 402 || result.errorCode == 429) { - error?.let { errorMsg -> - val errorContent = CoinbaseErrorResponse.getErrorMessage(errorMsg) - if (errorContent?.id.equals(CoinbaseConstants.ERROR_ID_INVALID_REQUEST, true) && - errorContent?.message?.contains(CoinbaseConstants.ERROR_MSG_INVALID_REQUEST) == true - ) { - twoFaErrorState.call() - } else { - _transactionState.value = TransactionState(false, errorContent?.message) - } - } + try { + // 2fa request must have same parameters, including idem + val result = coinBaseRepository.sendFundsToWallet(transactionParams, twoFaCode) + _loadingState.value = false + _transactionState.value = TransactionState(result != null) + } catch (ex: Exception) { + _loadingState.value = false + var errorMessage = ex.message ?: ex.toString() + + if (ex is HttpException) { + if (ex.code() == 400 || ex.code() == 402 || ex.code() == 429) { + val error = ex.response()?.errorBody()?.string() + error?.let { errorMsg -> + val errorContent = CoinbaseErrorResponse.getErrorMessage(errorMsg) + + if (errorContent?.isInvalidRequest == true) { + twoFaErrorState.call() + return@launch } else { - _transactionState.value = TransactionState(false, null) + errorContent?.message?.let { errorMessage = it } } - } catch (e: IOException) { - _transactionState.value = TransactionState(false, null) } } } + + _transactionState.value = TransactionState(false, errorMessage) } } diff --git a/integrations/coinbase/src/main/res/navigation/nav_coinbase.xml b/integrations/coinbase/src/main/res/navigation/nav_coinbase.xml index 95c07ce1a1..d126f5807d 100644 --- a/integrations/coinbase/src/main/res/navigation/nav_coinbase.xml +++ b/integrations/coinbase/src/main/res/navigation/nav_coinbase.xml @@ -129,7 +129,7 @@ + app:nullable="false" />