From 0d35d7762a744f81289273834201e5432dcba183 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 17 Jul 2026 19:44:17 -0400 Subject: [PATCH] fix(add-money): stop pushing Coinbase email verification twice The add-money deposit sheet's Coinbase selection was consumed by two handlers that both navigated to verification: presentDepositOptions (which runs its own email/phone gate) and SwapViewModel's persistent purchaseMethodController.selections collector, whose Coinbase branch ran the email gate and dispatched OnVerificationNeeded before bailing on the missing purchase amount. - SwapViewModel: bail on the missing amount before the email gate, so the collector ignores the amount-less deposit-sheet selection and leaves presentDepositOptions as the sole handler. - InternalPurchaseMethodController: resolve the email the same way as CoinbaseOnRampController.resolveEmail() (verified email only when the flag is on, any entered email otherwise) so the pre-emptive gate agrees with the purchase-time check and doesn't verify an already-verified user. --- .../internal/InternalPurchaseMethodController.kt | 12 ++++++++++-- .../com/flipcash/app/tokens/ui/SwapViewModel.kt | 8 ++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt index 8bd02c72e..795a938dc 100644 --- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt +++ b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt @@ -167,10 +167,18 @@ class InternalPurchaseMethodController @Inject constructor( PurchaseMethod.CoinbaseOnRamp -> { val profile = userManager.profile val needsPhone = profile?.verifiedPhoneNumber == null - val email = profile?.email?.value val needsEmailVerification = userFlags.resolvedFlags.value .requireCoinbaseEmailVerification.effectiveValue - val needsEmail = needsEmailVerification || email == null + // Match CoinbaseOnRampController.resolveEmail(): when verification is required only a + // server-verified email is usable, otherwise any entered email counts. Diverging here + // (forcing verification whenever the flag is on) made this gate disagree with the + // purchase-time check and could send an already-verified user to verification. + val email = if (needsEmailVerification) { + profile?.verifiedEmailAddress + } else { + profile?.email?.value + } + val needsEmail = email == null trace("needsPhone: $needsPhone, needsEmail: $needsEmail, needsEmailVerification: $needsEmailVerification") val swapRoute = AppRoute.Token.Swap( purpose = SwapPurpose.Buy(Mint.usdf, FundingSource.Coinbase), diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt index f4a6029fe..9b46d83a1 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt @@ -1138,6 +1138,12 @@ class SwapViewModel @Inject constructor( .onEach { (method, metadata) -> when (method) { PurchaseMethod.CoinbaseOnRamp -> { + // The add-money deposit sheet (presentDepositOptions) emits a Plain + // selection with no amount and owns its own email/phone gate + navigation. + // Only react to an actual purchase (which carries an amount) here — reacting + // to the deposit selection would push the verification screen a second time. + val amount = metadata.purchaseAmount ?: return@onEach + analytics.buttonTapped(Button.TokenBuyWithCoinbase) dispatchEvent(Event.CoinbaseSelected) @@ -1163,8 +1169,6 @@ class SwapViewModel @Inject constructor( return@onEach } - val amount = metadata.purchaseAmount ?: return@onEach - if (amount < minimumCoinbasePurchaseAmount) { BottomBarManager.showAlert( title = resources.getString(R.string.error_title_onrampAmountTooLow),