From 250d24c1a2aee408f31b5edba5c24bf8ec7c94b3 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 19 May 2026 12:22:28 -0400 Subject: [PATCH] fix(onramp): align Coinbase error codes with official docs and group by UI Add 9 missing Coinbase headless onramp error codes and group related errors into sealed subclass hierarchies (CardDeclined, BillingAddressInvalid, InternalFailure, TransactionFailed, UnknownFailure) so the UI handler matches on groups instead of individual variants. Fixes 4 TODO() crashes in showOnRampFailure. Signed-off-by: Brandon McAnsh --- .../core/src/main/res/values/strings.xml | 3 + .../app/onramp/CoinbaseOnRampHandler.kt | 106 ++++++++----- .../app/onramp/CoinbaseOnRampWebview.kt | 4 +- .../internal/CoinbaseOnRampEventHandler.kt | 146 ++++++++++++++---- .../CoinbaseOnRampEventHandlerTest.kt | 99 +++++++++--- 5 files changed, 265 insertions(+), 93 deletions(-) diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 46da99ed8..e93d3261b 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -570,6 +570,9 @@ Billing Address Invalid Please check that your billing address is correct and try again + Billing Name Invalid + Please check that the name on your card matches your billing name and try again + Something Went Wrong The Coinbase team has been notified and is investigating the issue. Your funds will arrive once resolved. We appreciate your patience diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt index a45ed0806..e6537cd52 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt @@ -62,14 +62,45 @@ fun CoinbaseOnRampHandler( private fun showOnRampFailure(resources: Resources, error: CoinbaseOnRampWebError) { when (error) { - is CoinbaseOnRampWebError.Unknown, - is CoinbaseOnRampWebError.MissingTransactionUuid -> { + // --- Grouped errors --- + + is CoinbaseOnRampWebError.UnknownFailure -> { BottomBarManager.showError( title = resources.getString(R.string.error_title_onrampUnknownFailure), message = resources.getString(R.string.error_description_onrampUnknownFailure), ) } + is CoinbaseOnRampWebError.CardDeclined -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampCardSoftDeclined), + message = resources.getString(R.string.error_description_onrampCardSoftDeclined), + ) + } + + is CoinbaseOnRampWebError.BillingAddressInvalid -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampTransactionAvsValidationFailed), + message = resources.getString(R.string.error_description_onrampTransactionAvsValidationFailed), + ) + } + + is CoinbaseOnRampWebError.InternalFailure -> { + BottomBarManager.showError( + title = resources.getString(R.string.error_title_onrampInternal), + message = resources.getString(R.string.error_description_onrampInternal), + ) + } + + is CoinbaseOnRampWebError.TransactionFailed -> { + BottomBarManager.showError( + title = resources.getString(R.string.error_title_onrampTransactionFailed), + message = resources.getString(R.string.error_description_onrampTransactionFailed), + ) + } + + // --- Single-variant errors --- + is CoinbaseOnRampWebError.GuestCardNotDebit -> { BottomBarManager.showAlert( title = resources.getString(R.string.error_title_onrampInvalidCard), @@ -77,68 +108,73 @@ private fun showOnRampFailure(resources: Resources, error: CoinbaseOnRampWebErro ) } - is CoinbaseOnRampWebError.GuestRegionMismatch -> { + is CoinbaseOnRampWebError.GuestCardRiskDeclined -> { BottomBarManager.showAlert( - title = resources.getString(R.string.error_title_onrampRegionMismatch), - message = resources.getString(R.string.error_description_onrampRegionMismatch), + title = resources.getString(R.string.error_title_onrampCardRiskDeclined), + message = resources.getString(R.string.error_description_onrampCardRiskDeclined), + ) + } + + is CoinbaseOnRampWebError.GuestPermissionDenied -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampCardPermissionDenied), + message = resources.getString(R.string.error_description_onrampCardPermissionDenied), ) } - is CoinbaseOnRampWebError.AssetNotTradableInRegion -> { + is CoinbaseOnRampWebError.RegionNotSupported -> { BottomBarManager.showAlert( title = resources.getString(R.string.error_title_onrampRegionMismatch), message = resources.getString(R.string.error_description_onrampRegionMismatch), ) } - is CoinbaseOnRampWebError.GuestGooglePayError -> { - BottomBarManager.showError( - title = resources.getString(R.string.error_title_onrampTransactionFailed), - message = resources.getString(R.string.error_description_onrampTransactionFailed), + is CoinbaseOnRampWebError.GuestWeeklyTransactionLimitReached -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampTransactionLimit), + message = resources.getString(R.string.error_description_onrampTransactionLimit), ) } - is CoinbaseOnRampWebError.GuestGooglePayNotReady -> { + is CoinbaseOnRampWebError.GuestTransactionMaxLimitReached -> { BottomBarManager.showAlert( - title = resources.getString(R.string.error_title_onrampGooglePayNotReady), - message = resources.getString(R.string.error_description_onrampGooglePayNotReady), + title = resources.getString(R.string.error_title_onrampTransactionCount), + message = resources.getString(R.string.error_description_onrampTransactionCount), ) } - is CoinbaseOnRampWebError.GuestTransactionBuyFailed -> { - BottomBarManager.showError( - title = resources.getString(R.string.error_title_onrampTransactionBuyFailed), - message = resources.getString(R.string.error_description_onrampTransactionBuyFailed), + is CoinbaseOnRampWebError.GuestGooglePayNotReady -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampGooglePayNotReady), + message = resources.getString(R.string.error_description_onrampGooglePayNotReady), ) } - is CoinbaseOnRampWebError.GuestTransactionSendFailed -> { - BottomBarManager.showError( - title = resources.getString(R.string.error_title_onrampTransactionSendFailed), - message = resources.getString(R.string.error_description_onrampTransactionSendFailed), + is CoinbaseOnRampWebError.GuestGooglePayNotSupported -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampGooglePayNotSupported), + message = resources.getString(R.string.error_description_onrampGooglePayNotSupported), ) } - is CoinbaseOnRampWebError.GuestTransactionAvsValidationFailed -> { - BottomBarManager.showError( - title = resources.getString(R.string.error_title_onrampTransactionAvsValidationFailed), - message = resources.getString(R.string.error_description_onrampTransactionAvsValidationFailed), + is CoinbaseOnRampWebError.GuestCardInsufficientBalance -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampCardInsufficientBalance), + message = resources.getString(R.string.error_description_onrampCardInsufficientBalance), ) } - is CoinbaseOnRampWebError.GuestTransactionTransactionFailed -> { - BottomBarManager.showError( - title = resources.getString(R.string.error_title_onrampTransactionFailed), - message = resources.getString(R.string.error_description_onrampTransactionFailed), + is CoinbaseOnRampWebError.GuestCardPrepaidDeclined -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampCardPrepaidDeclined), + message = resources.getString(R.string.error_description_onrampCardPrepaidDeclined), ) } - is CoinbaseOnRampWebError.Internal, - is CoinbaseOnRampWebError.GooglePayButtonNotFound, - is CoinbaseOnRampWebError.WebViewTimeout -> { - BottomBarManager.showError( - title = resources.getString(R.string.error_title_onrampInternal), - message = resources.getString(R.string.error_description_onrampInternal), + is CoinbaseOnRampWebError.InvalidBillingName -> { + BottomBarManager.showAlert( + title = resources.getString(R.string.error_title_onrampInvalidBillingName), + message = resources.getString(R.string.error_description_onrampInvalidBillingName), ) } diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampWebview.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampWebview.kt index 673f31509..8275186ab 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampWebview.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampWebview.kt @@ -97,7 +97,7 @@ private fun WebView.configureForCoinbaseOnRamp( "gmsVersion" to gmsVersion }, ) - onPaymentFailure(CoinbaseOnRampWebError.WebViewTimeout()) + onPaymentFailure(CoinbaseOnRampWebError.InternalFailure.WebViewTimeout()) } } @@ -221,7 +221,7 @@ private fun WebView.configureForCoinbaseOnRamp( error: WebResourceError? ) { if (request?.isForMainFrame == true) { - wrappedOnPaymentFailure(CoinbaseOnRampWebError.GuestGooglePayError()) + wrappedOnPaymentFailure(CoinbaseOnRampWebError.TransactionFailed.GooglePayError()) } } } diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandler.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandler.kt index 5a96cc7da..f0c42f1d2 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandler.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandler.kt @@ -187,7 +187,7 @@ internal class CoinbaseOnRampEventHandler( metadata = { "webViewVersion" to webViewVersion "gmsVersion" to gmsVersion - if (errorCode == "ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND" && data != null) { + if (errorCode == CoinbaseOnRampWebError.CODE_GOOGLE_PAY_BUTTON_NOT_FOUND && data != null) { "buttons" to data.optInt("buttons", -1) "gpayElements" to data.optInt("gpayElements", -1) "iframes" to data.optInt("iframes", -1) @@ -265,40 +265,124 @@ internal class CoinbaseOnRampEventHandler( } } -sealed class CoinbaseOnRampWebError(val data: String? = null): Throwable(data) { - class Unknown(data: String? = null) : CoinbaseOnRampWebError(data), NotifiableError - class MissingTransactionUuid(data: String? = null) : CoinbaseOnRampWebError(data), NotifiableError +/** @see [Docs](https://docs.cdp.coinbase.com/onramp/headless-onramp/overview#events-names) */ +sealed class CoinbaseOnRampWebError(val data: String? = null) : Throwable(data) { + + // --- Grouped errors (shared UI) --- + + /** "Something Went Wrong" — unknown / unmapped error codes */ + sealed class UnknownFailure(data: String?) : CoinbaseOnRampWebError(data), NotifiableError { + class Unknown(data: String? = null) : UnknownFailure(data) + class MissingTransactionUuid(data: String? = null) : UnknownFailure(data) + } + + /** "Card Declined" — declined by issuing bank */ + sealed class CardDeclined(data: String?) : CoinbaseOnRampWebError(data) { + class Soft(data: String? = null) : CardDeclined(data) + class Hard(data: String? = null) : CardDeclined(data) + class BuyFailed(data: String? = null) : CardDeclined(data) + } + + /** "Billing Address Invalid" — AVS / zip / address mismatch */ + sealed class BillingAddressInvalid(data: String?) : CoinbaseOnRampWebError(data) { + class AvsValidationFailed(data: String? = null) : BillingAddressInvalid(data) + class InvalidZip(data: String? = null) : BillingAddressInvalid(data) + class InvalidAddress(data: String? = null) : BillingAddressInvalid(data) + } + + /** "Something Went Wrong" — internal / infra failures */ + sealed class InternalFailure(data: String?) : CoinbaseOnRampWebError(data), NotifiableError { + class Internal(data: String? = null) : InternalFailure(data) + class GooglePayButtonNotFound(data: String? = null) : InternalFailure(data) + class WebViewTimeout(data: String? = null) : InternalFailure(data) + class InitError(data: String? = null) : InternalFailure(data) + } + + /** "Something Went Wrong" — transaction processing failure */ + sealed class TransactionFailed(data: String?) : CoinbaseOnRampWebError(data) { + class GooglePayError(data: String? = null) : TransactionFailed(data) + class SendFailed(data: String? = null) : TransactionFailed(data), NotifiableError + class ProcessingFailed(data: String? = null) : TransactionFailed(data), NotifiableError + } + + /** "Your Region Isn't Supported" — region / asset availability */ + sealed class RegionNotSupported(data: String?) : CoinbaseOnRampWebError(data) { + class RegionMismatch(data: String? = null) : RegionNotSupported(data) + class AssetNotTradable(data: String? = null) : RegionNotSupported(data) + } + + // --- Single-variant errors --- + class GuestCardNotDebit(data: String? = null) : CoinbaseOnRampWebError(data) - class GuestGooglePayError(data: String? = null) : CoinbaseOnRampWebError(data) - class GuestTransactionBuyFailed(data: String? = null) : CoinbaseOnRampWebError(data) - class GuestTransactionSendFailed(data: String? = null) : CoinbaseOnRampWebError(data), NotifiableError - class GuestTransactionAvsValidationFailed(data: String? = null) : CoinbaseOnRampWebError(data) - class GuestTransactionTransactionFailed(data: String? = null) : CoinbaseOnRampWebError(data), NotifiableError - class GuestRegionMismatch(data: String? = null) : CoinbaseOnRampWebError(data) - class AssetNotTradableInRegion(data: String? = null): CoinbaseOnRampWebError(data) + class GuestCardRiskDeclined(data: String? = null) : CoinbaseOnRampWebError(data) + class GuestPermissionDenied(data: String? = null) : CoinbaseOnRampWebError(data) + class GuestWeeklyTransactionLimitReached(data: String? = null) : CoinbaseOnRampWebError(data) + class GuestTransactionMaxLimitReached(data: String? = null) : CoinbaseOnRampWebError(data) class GuestGooglePayNotReady(data: String? = null) : CoinbaseOnRampWebError(data) - class Internal(data: String? = null) : CoinbaseOnRampWebError(data), NotifiableError - class GooglePayButtonNotFound(data: String? = null) : CoinbaseOnRampWebError(data), NotifiableError - class WebViewTimeout(data: String? = null) : CoinbaseOnRampWebError(data), NotifiableError + class GuestGooglePayNotSupported(data: String? = null) : CoinbaseOnRampWebError(data) + class GuestCardInsufficientBalance(data: String? = null) : CoinbaseOnRampWebError(data) + class GuestCardPrepaidDeclined(data: String? = null) : CoinbaseOnRampWebError(data) + class InvalidBillingName(data: String? = null) : CoinbaseOnRampWebError(data) class PaymentSheetTimeout(data: String? = null) : CoinbaseOnRampWebError(data) companion object { - fun fromErrorCode(errorCode: String, data: String? = null): CoinbaseOnRampWebError { - return when (errorCode) { - "ERROR_CODE_MISSING_TRANSACTION_UUID" -> MissingTransactionUuid(data) - "ERROR_CODE_ASSET_NOT_TRADABLE" -> AssetNotTradableInRegion(data) - "ERROR_CODE_GUEST_CARD_NOT_DEBIT" -> GuestCardNotDebit(data) - "ERROR_CODE_GUEST_GOOGLE_PAY_ERROR" -> GuestGooglePayError(data) - "ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED" -> GuestTransactionBuyFailed(data) - "ERROR_CODE_GUEST_TRANSACTION_SEND_FAILED" -> GuestTransactionSendFailed(data) - "ERROR_CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED" -> GuestTransactionAvsValidationFailed(data) - "ERROR_CODE_GUEST_TRANSACTION_TRANSACTION_FAILED" -> GuestTransactionTransactionFailed(data) - "ERROR_CODE_GUEST_REGION_MISMATCH" -> GuestRegionMismatch(data) - "ERROR_CODE_GUEST_GOOGLE_PAY_NOT_READY" -> GuestGooglePayNotReady(data) - "ERROR_CODE_INTERNAL" -> Internal(data) - "ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND" -> GooglePayButtonNotFound(data) - else -> Unknown(data) - } - } + const val CODE_INIT = "ERROR_CODE_INIT" + const val CODE_INTERNAL = "ERROR_CODE_INTERNAL" + const val CODE_MISSING_TRANSACTION_UUID = "ERROR_CODE_MISSING_TRANSACTION_UUID" + const val CODE_GOOGLE_PAY_BUTTON_NOT_FOUND = "ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND" + const val CODE_GUEST_INVALID_CARD = "ERROR_CODE_GUEST_INVALID_CARD" + const val CODE_GUEST_CARD_NOT_DEBIT = "ERROR_CODE_GUEST_CARD_NOT_DEBIT" + const val CODE_GUEST_CARD_SOFT_DECLINED = "ERROR_CODE_GUEST_CARD_SOFT_DECLINED" + const val CODE_GUEST_CARD_HARD_DECLINED = "ERROR_CODE_GUEST_CARD_HARD_DECLINED" + const val CODE_GUEST_CARD_RISK_DECLINED = "ERROR_CODE_GUEST_CARD_RISK_DECLINED" + const val CODE_GUEST_CARD_INSUFFICIENT_BALANCE = "ERROR_CODE_GUEST_CARD_INSUFFICIENT_BALANCE" + const val CODE_GUEST_CARD_PREPAID_DECLINED = "ERROR_CODE_GUEST_CARD_PREPAID_DECLINED" + const val CODE_GUEST_PERMISSION_DENIED = "ERROR_CODE_GUEST_PERMISSION_DENIED" + const val CODE_GUEST_REGION_MISMATCH = "ERROR_CODE_GUEST_REGION_MISMATCH" + const val CODE_GUEST_GOOGLE_PAY_ERROR = "ERROR_CODE_GUEST_GOOGLE_PAY_ERROR" + const val CODE_GUEST_GOOGLE_PAY_NOT_READY = "ERROR_CODE_GUEST_GOOGLE_PAY_NOT_READY" + const val CODE_GUEST_GOOGLE_PAY_NOT_SUPPORTED = "ERROR_CODE_GUEST_GOOGLE_PAY_NOT_SUPPORTED" + const val CODE_GUEST_TRANSACTION_LIMIT = "ERROR_CODE_GUEST_TRANSACTION_LIMIT" + const val CODE_GUEST_TRANSACTION_COUNT = "ERROR_CODE_GUEST_TRANSACTION_COUNT" + const val CODE_GUEST_TRANSACTION_BUY_FAILED = "ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED" + const val CODE_GUEST_TRANSACTION_SEND_FAILED = "ERROR_CODE_GUEST_TRANSACTION_SEND_FAILED" + const val CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED = "ERROR_CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED" + const val CODE_GUEST_TRANSACTION_TRANSACTION_FAILED = "ERROR_CODE_GUEST_TRANSACTION_TRANSACTION_FAILED" + const val CODE_ASSET_NOT_TRADABLE = "ERROR_CODE_ASSET_NOT_TRADABLE" + const val CODE_INVALID_BILLING_ZIP = "ERROR_CODE_INVALID_BILLING_ZIP" + const val CODE_INVALID_BILLING_ADDRESS = "ERROR_CODE_INVALID_BILLING_ADDRESS" + const val CODE_INVALID_BILLING_NAME = "ERROR_CODE_INVALID_BILLING_NAME" + + private val codeMap: Map CoinbaseOnRampWebError> = mapOf( + CODE_MISSING_TRANSACTION_UUID to { UnknownFailure.MissingTransactionUuid(it) }, + CODE_GUEST_INVALID_CARD to ::GuestCardNotDebit, + CODE_GUEST_CARD_NOT_DEBIT to ::GuestCardNotDebit, + CODE_GUEST_TRANSACTION_LIMIT to ::GuestWeeklyTransactionLimitReached, + CODE_GUEST_TRANSACTION_COUNT to ::GuestTransactionMaxLimitReached, + CODE_GUEST_CARD_RISK_DECLINED to ::GuestCardRiskDeclined, + CODE_ASSET_NOT_TRADABLE to { RegionNotSupported.AssetNotTradable(it) }, + CODE_GUEST_REGION_MISMATCH to { RegionNotSupported.RegionMismatch(it) }, + CODE_GUEST_GOOGLE_PAY_ERROR to { TransactionFailed.GooglePayError(it) }, + CODE_GUEST_TRANSACTION_BUY_FAILED to { CardDeclined.BuyFailed(it) }, + CODE_GUEST_TRANSACTION_SEND_FAILED to { TransactionFailed.SendFailed(it) }, + CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED to { BillingAddressInvalid.AvsValidationFailed(it) }, + CODE_GUEST_TRANSACTION_TRANSACTION_FAILED to { TransactionFailed.ProcessingFailed(it) }, + CODE_GUEST_PERMISSION_DENIED to ::GuestPermissionDenied, + CODE_GUEST_GOOGLE_PAY_NOT_READY to ::GuestGooglePayNotReady, + CODE_GUEST_GOOGLE_PAY_NOT_SUPPORTED to ::GuestGooglePayNotSupported, + CODE_GUEST_CARD_SOFT_DECLINED to { CardDeclined.Soft(it) }, + CODE_GUEST_CARD_HARD_DECLINED to { CardDeclined.Hard(it) }, + CODE_GUEST_CARD_INSUFFICIENT_BALANCE to ::GuestCardInsufficientBalance, + CODE_GUEST_CARD_PREPAID_DECLINED to ::GuestCardPrepaidDeclined, + CODE_INVALID_BILLING_ZIP to { BillingAddressInvalid.InvalidZip(it) }, + CODE_INVALID_BILLING_ADDRESS to { BillingAddressInvalid.InvalidAddress(it) }, + CODE_INVALID_BILLING_NAME to ::InvalidBillingName, + CODE_INIT to { InternalFailure.InitError(it) }, + CODE_INTERNAL to { InternalFailure.Internal(it) }, + CODE_GOOGLE_PAY_BUTTON_NOT_FOUND to { InternalFailure.GooglePayButtonNotFound(it) }, + ) + + fun fromErrorCode(errorCode: String, data: String? = null): CoinbaseOnRampWebError = + codeMap[errorCode]?.invoke(data) ?: UnknownFailure.Unknown(data) } } diff --git a/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandlerTest.kt b/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandlerTest.kt index 28fb3db6d..a4e55d528 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandlerTest.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandlerTest.kt @@ -6,6 +6,32 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import kotlin.test.assertEquals import kotlin.time.TimeSource +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_ASSET_NOT_TRADABLE +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GOOGLE_PAY_BUTTON_NOT_FOUND +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_CARD_HARD_DECLINED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_CARD_INSUFFICIENT_BALANCE +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_CARD_NOT_DEBIT +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_CARD_PREPAID_DECLINED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_CARD_RISK_DECLINED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_CARD_SOFT_DECLINED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_GOOGLE_PAY_ERROR +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_GOOGLE_PAY_NOT_READY +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_GOOGLE_PAY_NOT_SUPPORTED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_INVALID_CARD +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_PERMISSION_DENIED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_REGION_MISMATCH +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_TRANSACTION_BUY_FAILED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_TRANSACTION_COUNT +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_TRANSACTION_LIMIT +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_TRANSACTION_SEND_FAILED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_GUEST_TRANSACTION_TRANSACTION_FAILED +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_INIT +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_INTERNAL +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_INVALID_BILLING_ADDRESS +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_INVALID_BILLING_NAME +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_INVALID_BILLING_ZIP +import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError.Companion.CODE_MISSING_TRANSACTION_UUID import com.getcode.utils.NotifiableError import kotlin.test.assertFalse import kotlin.test.assertIs @@ -66,19 +92,19 @@ class CoinbaseOnRampEventHandlerTest { @Test fun commitErrorTriggersFailure() { handler.handleEvent("""{"eventName":"onramp_api.commit_error","data":{"errorCode":"ERROR_CODE_INTERNAL"}}""") - assertIs(lastError) + assertIs(lastError) } @Test fun loadErrorTriggersFailure() { handler.handleEvent("""{"eventName":"onramp_api.load_error","data":{"errorCode":"ERROR_CODE_GUEST_GOOGLE_PAY_ERROR"}}""") - assertIs(lastError) + assertIs(lastError) } @Test fun pollingErrorTriggersFailure() { handler.handleEvent("""{"eventName":"onramp_api.polling_error","data":{"errorCode":"ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED"}}""") - assertIs(lastError) + assertIs(lastError) } @Test @@ -90,19 +116,19 @@ class CoinbaseOnRampEventHandlerTest { @Test fun errorWithUnknownCodeFallsBackToUnknown() { handler.handleEvent("""{"eventName":"onramp_api.commit_error","data":{"errorCode":"SOME_NEW_ERROR"}}""") - assertIs(lastError) + assertIs(lastError) } @Test fun errorWithMissingDataFallsBackToUnknown() { handler.handleEvent("""{"eventName":"onramp_api.commit_error"}""") - assertIs(lastError) + assertIs(lastError) } @Test fun errorWithEmptyErrorCodeFallsBackToUnknown() { handler.handleEvent("""{"eventName":"onramp_api.commit_error","data":{"errorCode":""}}""") - assertIs(lastError) + assertIs(lastError) } // --- Data payload --- @@ -111,7 +137,7 @@ class CoinbaseOnRampEventHandlerTest { fun errorCarriesJsonData() { handler.handleEvent("""{"eventName":"onramp_api.commit_error","data":{"errorCode":"ERROR_CODE_INTERNAL","transactionId":"abc-123"}}""") val error = lastError - assertIs(error) + assertIs(error) assertNotNull(error.data) assertTrue(error.data!!.contains("abc-123")) } @@ -203,16 +229,32 @@ class CoinbaseOnRampWebErrorTest { @Test fun fromErrorCodeAllKnownCodes() { val expected = mapOf( - "ERROR_CODE_MISSING_TRANSACTION_UUID" to CoinbaseOnRampWebError.MissingTransactionUuid::class, - "ERROR_CODE_GUEST_CARD_NOT_DEBIT" to CoinbaseOnRampWebError.GuestCardNotDebit::class, - "ERROR_CODE_GUEST_GOOGLE_PAY_ERROR" to CoinbaseOnRampWebError.GuestGooglePayError::class, - "ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED" to CoinbaseOnRampWebError.GuestTransactionBuyFailed::class, - "ERROR_CODE_GUEST_TRANSACTION_SEND_FAILED" to CoinbaseOnRampWebError.GuestTransactionSendFailed::class, - "ERROR_CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED" to CoinbaseOnRampWebError.GuestTransactionAvsValidationFailed::class, - "ERROR_CODE_GUEST_TRANSACTION_TRANSACTION_FAILED" to CoinbaseOnRampWebError.GuestTransactionTransactionFailed::class, - "ERROR_CODE_GUEST_REGION_MISMATCH" to CoinbaseOnRampWebError.GuestRegionMismatch::class, - "ERROR_CODE_INTERNAL" to CoinbaseOnRampWebError.Internal::class, - "ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND" to CoinbaseOnRampWebError.GooglePayButtonNotFound::class, + CODE_MISSING_TRANSACTION_UUID to CoinbaseOnRampWebError.UnknownFailure.MissingTransactionUuid::class, + CODE_GUEST_CARD_NOT_DEBIT to CoinbaseOnRampWebError.GuestCardNotDebit::class, + CODE_GUEST_INVALID_CARD to CoinbaseOnRampWebError.GuestCardNotDebit::class, + CODE_GUEST_GOOGLE_PAY_ERROR to CoinbaseOnRampWebError.TransactionFailed.GooglePayError::class, + CODE_GUEST_TRANSACTION_BUY_FAILED to CoinbaseOnRampWebError.CardDeclined.BuyFailed::class, + CODE_GUEST_TRANSACTION_SEND_FAILED to CoinbaseOnRampWebError.TransactionFailed.SendFailed::class, + CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED to CoinbaseOnRampWebError.BillingAddressInvalid.AvsValidationFailed::class, + CODE_GUEST_TRANSACTION_TRANSACTION_FAILED to CoinbaseOnRampWebError.TransactionFailed.ProcessingFailed::class, + CODE_GUEST_REGION_MISMATCH to CoinbaseOnRampWebError.RegionNotSupported.RegionMismatch::class, + CODE_GUEST_TRANSACTION_LIMIT to CoinbaseOnRampWebError.GuestWeeklyTransactionLimitReached::class, + CODE_GUEST_TRANSACTION_COUNT to CoinbaseOnRampWebError.GuestTransactionMaxLimitReached::class, + CODE_GUEST_CARD_RISK_DECLINED to CoinbaseOnRampWebError.GuestCardRiskDeclined::class, + CODE_GUEST_PERMISSION_DENIED to CoinbaseOnRampWebError.GuestPermissionDenied::class, + CODE_ASSET_NOT_TRADABLE to CoinbaseOnRampWebError.RegionNotSupported.AssetNotTradable::class, + CODE_GUEST_GOOGLE_PAY_NOT_READY to CoinbaseOnRampWebError.GuestGooglePayNotReady::class, + CODE_GUEST_GOOGLE_PAY_NOT_SUPPORTED to CoinbaseOnRampWebError.GuestGooglePayNotSupported::class, + CODE_GUEST_CARD_SOFT_DECLINED to CoinbaseOnRampWebError.CardDeclined.Soft::class, + CODE_GUEST_CARD_HARD_DECLINED to CoinbaseOnRampWebError.CardDeclined.Hard::class, + CODE_GUEST_CARD_INSUFFICIENT_BALANCE to CoinbaseOnRampWebError.GuestCardInsufficientBalance::class, + CODE_GUEST_CARD_PREPAID_DECLINED to CoinbaseOnRampWebError.GuestCardPrepaidDeclined::class, + CODE_INVALID_BILLING_ZIP to CoinbaseOnRampWebError.BillingAddressInvalid.InvalidZip::class, + CODE_INVALID_BILLING_ADDRESS to CoinbaseOnRampWebError.BillingAddressInvalid.InvalidAddress::class, + CODE_INVALID_BILLING_NAME to CoinbaseOnRampWebError.InvalidBillingName::class, + CODE_INIT to CoinbaseOnRampWebError.InternalFailure.InitError::class, + CODE_INTERNAL to CoinbaseOnRampWebError.InternalFailure.Internal::class, + CODE_GOOGLE_PAY_BUTTON_NOT_FOUND to CoinbaseOnRampWebError.InternalFailure.GooglePayButtonNotFound::class, ) for ((code, expectedType) in expected) { @@ -223,29 +265,36 @@ class CoinbaseOnRampWebErrorTest { @Test fun fromErrorCodeUnknownCodeReturnsUnknown() { - assertIs(CoinbaseOnRampWebError.fromErrorCode("SOMETHING_NEW")) + assertIs(CoinbaseOnRampWebError.fromErrorCode("SOMETHING_NEW")) } @Test fun fromErrorCodeEmptyStringReturnsUnknown() { - assertIs(CoinbaseOnRampWebError.fromErrorCode("")) + assertIs(CoinbaseOnRampWebError.fromErrorCode("")) } @Test fun fromErrorCodeCaseSensitive() { - assertIs(CoinbaseOnRampWebError.fromErrorCode("error_code_internal")) + assertIs(CoinbaseOnRampWebError.fromErrorCode("error_code_internal")) } @Test - fun webViewTimeoutImplementsNotifiableError() { - val error = CoinbaseOnRampWebError.WebViewTimeout() - assertIs(error) - assertIs(error) + fun internalFailureGroupImplementsNotifiableError() { + assertIs(CoinbaseOnRampWebError.InternalFailure.InitError()) + assertIs(CoinbaseOnRampWebError.InternalFailure.Internal()) + assertIs(CoinbaseOnRampWebError.InternalFailure.WebViewTimeout()) + assertIs(CoinbaseOnRampWebError.InternalFailure.GooglePayButtonNotFound()) + } + + @Test + fun unknownFailureGroupImplementsNotifiableError() { + assertIs(CoinbaseOnRampWebError.UnknownFailure.Unknown()) + assertIs(CoinbaseOnRampWebError.UnknownFailure.MissingTransactionUuid()) } @Test fun guestRegionMismatchIsNotNotifiable() { - val error = CoinbaseOnRampWebError.GuestRegionMismatch() + val error = CoinbaseOnRampWebError.RegionNotSupported.RegionMismatch() assertFalse(error is NotifiableError) }