Skip to content

Commit 2e0d9c8

Browse files
committed
chore(onramp): lock to USD and add minimum purchase amount
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 1044db5 commit 2e0d9c8

3 files changed

Lines changed: 89 additions & 31 deletions

File tree

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,9 @@
474474
<string name="title_settingsSectionAccount">Account</string>
475475
<string name="title_userFlags">User Flags</string>
476476

477+
<string name="error_title_onrampAmountTooLow">$5 Minimum Purchase</string>
478+
<string name="error_description_onrampAmountTooLow">Please enter an amount of $5 or higher</string>
479+
477480
<string name="error_title_onrampInvalidCard">Debit Cards Only</string>
478481
<string name="error_description_onrampInvalidCard">The transaction was declined. Please make sure you are using a debit card and the billing address is correct</string>
479482

apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/OnRampViewModel.kt

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.getcode.opencode.model.financial.Limits
3030
import com.getcode.opencode.model.financial.LocalFiat
3131
import com.getcode.opencode.model.financial.SendLimit
3232
import com.getcode.opencode.model.financial.Token
33+
import com.getcode.opencode.model.financial.toFiat
3334
import com.getcode.opencode.model.transactions.SwapFundingSource
3435
import com.getcode.solana.keys.Mint
3536
import com.getcode.ui.components.text.AmountAnimatedInputUiModel
@@ -113,32 +114,37 @@ internal class OnRampViewModel @Inject constructor(
113114
val mint: Mint? = null,
114115
val token: Token? = null,
115116
val providers: List<OnRampProviderItem> = DefaultOnRampOptions,
117+
val canChangeCurrency: Boolean = false,
116118
val hasVerifiedPhone: Boolean = false,
117119
val hasVerifiedEmail: Boolean = false,
118120
val selectedProvider: OnRampProvider.ThirdParty? = null,
119121
val amountEntryState: AmountEntryState = AmountEntryState(),
120-
)
122+
) {
123+
val minimumPurchaseAmount = 5.toFiat()
124+
}
121125

122126
sealed interface Event {
123-
data class OnMintChanged(val mint: Mint): Event
124-
data class OnTokenChanged(val token: Token): Event
127+
data class OnMintChanged(val mint: Mint) : Event
128+
data class OnTokenChanged(val token: Token) : Event
125129
data class OnProvidersUpdated(val providers: List<OnRampProviderItem>) : Event
126130

127131
data class OnPhoneVerificationChanged(val verified: Boolean) : Event
128132
data class OnEmailVerificationChanged(val verified: Boolean) : Event
129133

130134
data class OnProviderSelected(val item: OnRampProvider) : Event
131135

132-
data class OnVerificationNeeded(val phone: Boolean = false, val email: Boolean = false): Event
136+
data class OnVerificationNeeded(val phone: Boolean = false, val email: Boolean = false) :
137+
Event
133138

134-
data class OnOrderCreated(val order: OnrampOrder): Event {
135-
constructor(orderId: String, url: String): this(OnrampOrder(orderId, url))
139+
data class OnOrderCreated(val order: OnrampOrder) : Event {
140+
constructor(orderId: String, url: String) : this(OnrampOrder(orderId, url))
136141
}
142+
137143
data class OnBuyUrlGenerated(val url: String) : Event
138144

139145
data class OnPaymentSuccess(val orderId: String) : Event
140146
data class OnPaymentError(val error: CoinbaseOnRampWebError) : Event
141-
data object OnPaymentCancel: Event
147+
data object OnPaymentCancel : Event
142148

143149
// region amount entry events
144150
data class OnMaxDetermined(val max: Double, val currencyCode: CurrencyCode) : Event
@@ -161,7 +167,7 @@ internal class OnRampViewModel @Inject constructor(
161167
data class OnAmountAccepted(val amount: LocalFiat) : Event
162168

163169
data class CreateAndSendTransactionToWallet(val amount: LocalFiat) : Event
164-
data class OnBuySubmitted(val swapId: SwapId): Event
170+
data class OnBuySubmitted(val swapId: SwapId) : Event
165171
// endregion
166172
}
167173

@@ -295,11 +301,23 @@ internal class OnRampViewModel @Inject constructor(
295301
.filter { !(checkFundingAmount()) }
296302
.onEach { data ->
297303
dispatchEvent(Event.UpdateConfirmingAmountState(loading = true))
298-
val rate = exchange.rateFor(stateFlow.value.amountEntryState.currencyModel.code ?: CurrencyCode.USD)
299-
?: exchange.entryRate
304+
val rate = exchange.rateFor(
305+
stateFlow.value.amountEntryState.currencyModel.code ?: CurrencyCode.USD
306+
) ?: exchange.entryRate
300307

301308
val localizedAmount = Fiat(data.amountData.amount, rate.currency)
302309

310+
if (stateFlow.value.selectedProvider is OnRampProvider.Coinbase) {
311+
if (localizedAmount < stateFlow.value.minimumPurchaseAmount) {
312+
BottomBarManager.showAlert(
313+
title = resources.getString(R.string.error_title_onrampAmountTooLow),
314+
message = resources.getString(R.string.error_description_onrampAmountTooLow)
315+
)
316+
dispatchEvent(Event.UpdateConfirmingAmountState())
317+
return@onEach
318+
}
319+
}
320+
303321
val amountFiat = LocalFiat(
304322
usdf = localizedAmount.convertingTo(exchange.rateToUsd(rate.currency)!!),
305323
nativeAmount = localizedAmount,
@@ -401,23 +419,32 @@ internal class OnRampViewModel @Inject constructor(
401419
else -> return@mapNotNull null
402420
}
403421

404-
val destination = if (!(hasVerifiedPhone && hasVerifiedEmail)) {
405-
AppRoute.Verification(
406-
origin = AppRoute.OnRamp.ProviderList(
407-
from = OnRampFlowTracker.source!!
408-
),
409-
target = AppRoute.OnRamp.AmountEntry(mint),
410-
includePhone = !hasVerifiedPhone,
411-
includeEmail = !hasVerifiedEmail,
412-
)
413-
} else {
414-
AppRoute.OnRamp.AmountEntry(mint)
415-
}
422+
val destination =
423+
if (!(hasVerifiedPhone && hasVerifiedEmail)) {
424+
AppRoute.Verification(
425+
origin = AppRoute.OnRamp.ProviderList(
426+
from = OnRampFlowTracker.source!!
427+
),
428+
target = AppRoute.OnRamp.AmountEntry(mint),
429+
includePhone = !hasVerifiedPhone,
430+
includeEmail = !hasVerifiedEmail,
431+
)
432+
} else {
433+
AppRoute.OnRamp.AmountEntry(mint)
434+
}
416435

417436
when (provider.type) {
418-
OnRampType.Virtual -> OnRampProviderDestination.Screen(destination)
419-
OnRampType.PhysicalDebit -> OnRampProviderDestination.Screen(destination)
420-
OnRampType.PhysicalCredit -> OnRampProviderDestination.Screen(destination)
437+
OnRampType.Virtual -> OnRampProviderDestination.Screen(
438+
destination
439+
)
440+
441+
OnRampType.PhysicalDebit -> OnRampProviderDestination.Screen(
442+
destination
443+
)
444+
445+
OnRampType.PhysicalCredit -> OnRampProviderDestination.Screen(
446+
destination
447+
)
421448
}
422449
}
423450

@@ -433,8 +460,8 @@ internal class OnRampViewModel @Inject constructor(
433460
eventFlow
434461
.filterIsInstance<Event.OnProviderSelected>()
435462
.map { it.item }
436-
// we are locking deeplink transfers to USD
437-
.filterIsInstance<OnRampProvider.UsesDeeplinks>()
463+
// we are locking deeplink transfers and onramp buys to USD
464+
.filter { it is OnRampProvider.UsesDeeplinks || it is OnRampProvider.Coinbase }
438465
.mapNotNull { exchange.getCurrency(CurrencyCode.USD.name) }
439466
.onEach { dispatchEvent(Event.OnCurrencyChanged(it)) }
440467
.launchIn(viewModelScope)
@@ -454,14 +481,24 @@ internal class OnRampViewModel @Inject constructor(
454481
.onSuccess {
455482
dispatchEvent(Event.OnOrderCreated(it.first, it.second.url))
456483
}.onFailure { error ->
457-
dispatchEvent(Event.UpdateConfirmingAmountState(loading = false, success = false))
484+
dispatchEvent(
485+
Event.UpdateConfirmingAmountState(
486+
loading = false,
487+
success = false
488+
)
489+
)
458490
when (error) {
459491
is OnRampAuthError.CoinbasePhoneVerificationRequired -> {
460492
dispatchEvent(Event.OnVerificationNeeded(phone = true))
461493
}
462494

463495
is OnRampAuthError.VerificationRequired -> {
464-
dispatchEvent(Event.OnVerificationNeeded(phone = error.phone, email = error.email))
496+
dispatchEvent(
497+
Event.OnVerificationNeeded(
498+
phone = error.phone,
499+
email = error.email
500+
)
501+
)
465502
}
466503

467504
else -> {
@@ -473,6 +510,7 @@ internal class OnRampViewModel @Inject constructor(
473510
}
474511
}
475512
}
513+
476514
else -> Unit
477515
}
478516
}
@@ -492,54 +530,63 @@ internal class OnRampViewModel @Inject constructor(
492530
message = resources.getString(R.string.error_description_onrampUnknownFailure)
493531
)
494532
}
533+
495534
is CoinbaseOnRampWebError.MissingTransactionUuid -> { // TODO:
496535
BottomBarManager.showError(
497536
title = resources.getString(R.string.error_title_onrampUnknownFailure),
498537
message = resources.getString(R.string.error_description_onrampUnknownFailure)
499538
)
500539
}
540+
501541
is CoinbaseOnRampWebError.GuestCardNotDebit -> {
502542
BottomBarManager.showError(
503543
title = resources.getString(R.string.error_title_onrampInvalidCard),
504544
message = resources.getString(R.string.error_description_onrampInvalidCard)
505545
)
506546
}
547+
507548
is CoinbaseOnRampWebError.GuestGooglePayError -> {
508549
BottomBarManager.showError(
509550
title = resources.getString(R.string.error_title_onrampTransactionFailed),
510551
message = resources.getString(R.string.error_description_onrampTransactionFailed)
511552
)
512553
}
554+
513555
is CoinbaseOnRampWebError.GuestTransactionBuyFailed -> {
514556
BottomBarManager.showError(
515557
title = resources.getString(R.string.error_title_onrampTransactionBuyFailed),
516558
message = resources.getString(R.string.error_description_onrampTransactionBuyFailed)
517559
)
518560
}
561+
519562
is CoinbaseOnRampWebError.GuestTransactionSendFailed -> {
520563
BottomBarManager.showError(
521564
title = resources.getString(R.string.error_title_onrampTransactionSendFailed),
522565
message = resources.getString(R.string.error_description_onrampTransactionSendFailed)
523566
)
524567
}
568+
525569
is CoinbaseOnRampWebError.GuestTransactionAvsValidationFailed -> {
526570
BottomBarManager.showError(
527571
title = resources.getString(R.string.error_title_onrampTransactionAvsValidationFailed),
528572
message = resources.getString(R.string.error_description_onrampTransactionAvsValidationFailed)
529573
)
530574
}
575+
531576
is CoinbaseOnRampWebError.GuestTransactionTransactionFailed -> {
532577
BottomBarManager.showError(
533578
title = resources.getString(R.string.error_title_onrampTransactionFailed),
534579
message = resources.getString(R.string.error_description_onrampTransactionFailed)
535580
)
536581
}
582+
537583
is CoinbaseOnRampWebError.Internal -> {
538584
BottomBarManager.showError(
539585
title = resources.getString(R.string.error_title_onrampInternal),
540586
message = resources.getString(R.string.error_description_onrampInternal)
541587
)
542588
}
589+
543590
is CoinbaseOnRampWebError.GooglePayButtonNotFound -> {
544591
BottomBarManager.showError(
545592
title = resources.getString(R.string.error_title_onrampInternal),
@@ -554,7 +601,13 @@ internal class OnRampViewModel @Inject constructor(
554601
when (event) {
555602
is Event.OnMintChanged -> { state -> state.copy(mint = event.mint) }
556603
is Event.OnTokenChanged -> { state -> state.copy(token = event.token) }
557-
is Event.OnProviderSelected -> { state -> state.copy(selectedProvider = event.item as? OnRampProvider.ThirdParty) }
604+
is Event.OnProviderSelected -> { state ->
605+
state.copy(
606+
canChangeCurrency = event.item !is OnRampProvider.Phantom && event.item !is OnRampProvider.Coinbase,
607+
selectedProvider = event.item as? OnRampProvider.ThirdParty
608+
)
609+
}
610+
558611
is Event.OnProvidersUpdated -> { state -> state.copy(providers = event.providers) }
559612

560613
is Event.OnPhoneVerificationChanged -> { state -> state.copy(hasVerifiedPhone = event.verified) }

apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/screens/OnRampAmountScreenContent.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ internal fun OnRampAmountScreen(
3434
OnRampAmountScreenContent(
3535
state = state.amountEntryState,
3636
provider = state.selectedProvider,
37+
canChangeCurrency = state.canChangeCurrency,
3738
dispatchEvent = viewModel::dispatchEvent,
3839
)
3940
}
4041

4142
@Composable
4243
private fun OnRampAmountScreenContent(
4344
state: AmountEntryState,
45+
canChangeCurrency: Boolean,
4446
provider: OnRampProvider.ThirdParty?,
4547
dispatchEvent: (OnRampViewModel.Event) -> Unit,
4648
) {
@@ -63,7 +65,7 @@ private fun OnRampAmountScreenContent(
6365
stringResource(R.string.subtitle_onrampPurchaseHint, state.maxAvailableToAdd)
6466
},
6567
decimalPlaces = state.currencyModel.fractionUnits,
66-
isClickable = provider !is OnRampProvider.Phantom,
68+
isClickable = canChangeCurrency,
6769
onAmountClicked = {
6870
navigator.push(
6971
AppRoute.Main.RegionSelection(

0 commit comments

Comments
 (0)