Skip to content

Commit

Permalink
fix: coinbase 2fa send error for transfers (#1341)
Browse files Browse the repository at this point in the history
* fix: coinbase 2fa send error

* fix: coinbase proguard rules
  • Loading branch information
Syn-McJ authored Jan 13, 2025
1 parent 6f24a49 commit 447e319
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ interface CoinBaseRepositoryInt {
suspend fun sendFundsToWallet(
sendTransactionToWalletParams: SendTransactionToWalletParams,
api2FATokenVersion: String?
): ResponseResource<SendTransactionToWalletResponse?>
): SendTransactionToWalletResponse?
suspend fun swapTrade(tradesRequest: TradesRequest): ResponseResource<SwapTradeUIModel>
suspend fun commitSwapTrade(buyOrderId: String): ResponseResource<SwapTradeUIModel>
suspend fun completeCoinbaseAuthentication(authorizationCode: String): Boolean
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,32 +47,31 @@ 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<EnterTwoFaCodeFragmentArgs>()
private val viewModel by viewModels<EnterTwoFaCodeViewModel>()
private lateinit var loadingDialog: AdaptiveDialog
private var onBackPressedCallback: OnBackPressedCallback? = null

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

viewModel.loadingState.observe(viewLifecycleOwner) {
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
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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())
}
Expand All @@ -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())
Expand All @@ -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()
}
}
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<Boolean> = MutableLiveData()
val loadingState: LiveData<Boolean>
get() = _loadingState
Expand All @@ -53,68 +50,48 @@ class EnterTwoFaCodeViewModel @Inject constructor(

val twoFaErrorState = SingleLiveEvent<Unit>()

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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
<argument
android:name="transactionParams"
app:argType="org.dash.wallet.integrations.coinbase.model.CoinbaseTransactionParams"
/>
app:nullable="false" />
<action
android:id="@+id/conversionPreviewToTwoFaCode"
app:destination="@id/enterTwoFaCodeFragment"
Expand Down
7 changes: 7 additions & 0 deletions wallet/proguard.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,11 @@

-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}

# Coinbase
-keep class org.dash.wallet.integrations.coinbase.** { *; }
-keepclassmembers class org.dash.wallet.integrations.coinbase.** { *; }
-keep class org.dash.wallet.integrations.coinbase.service.CoinBaseAuthApi {
*;
}

0 comments on commit 447e319

Please sign in to comment.