Skip to content

Commit 51f553c

Browse files
authored
Merge branch 'code/cash' into dependabot/gradle/code/cash/androidx.paging-paging-compose-3.5.0
2 parents 5f9949d + 8deac98 commit 51f553c

15 files changed

Lines changed: 701 additions & 235 deletions

File tree

.well-known/release-manifest.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
2-
"updated": "2026-05-15T00:43:09Z",
2+
"updated": "2026-05-15T18:05:45Z",
33
"tracks": {
44
"production": {
5-
"versionCode": 3586,
6-
"versionName": "2026.5.3"
5+
"versionCode": 3599,
6+
"versionName": "2026.5.4"
77
},
88
"beta": null,
99
"alpha": null,
1010
"internal": {
11-
"versionCode": 3596,
11+
"versionCode": 3599,
1212
"versionName": "2026.5.4"
1313
}
1414
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,10 @@
587587
<string name="error_title_onrampGooglePayNotReady">Google Pay Not Ready</string>
588588
<string name="error_description_onrampGooglePayNotReady">Please add a payment method to Google Pay and try again</string>
589589

590+
<string name="error_title_onrampNonStableWebView">Unsupported WebView</string>
591+
<string name="error_description_onrampNonStableWebView">Your device is using a %1$s version of Android System WebView, which may cause issues with purchases. Google recommends switching to the stable channel before continuing with your purchase</string>
592+
<string name="action_continueAnyway">Continue Anyway</string>
593+
590594
<string name="error_title_onrampUnknownFailure">Something Went Wrong</string>
591595
<string name="error_description_onrampUnknownFailure">Please contact support@flipcash.com</string>
592596

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

Lines changed: 116 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import com.flipcash.app.core.ui.CurrencyHolder
99
import com.flipcash.app.onramp.CoinbaseOnRampState
1010
import com.flipcash.app.onramp.OnRampAuthError
1111
import com.flipcash.app.onramp.CoinbaseOnRampController
12-
import com.flipcash.app.onramp.OnRampPaymentError
12+
import com.flipcash.app.onramp.PurchaseGate
1313
import com.flipcash.features.onramp.R
1414
import com.flipcash.libs.coroutines.DispatcherProvider
1515
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
1616
import com.flipcash.services.internal.model.thirdparty.OnRampType
17+
import com.getcode.manager.BottomBarAction
1718
import com.getcode.manager.BottomBarManager
1819
import com.getcode.opencode.controllers.TokenController
1920
import com.getcode.opencode.controllers.TransactionOperations
@@ -39,6 +40,7 @@ import com.getcode.view.LoadingSuccessState
3940
import dagger.hilt.android.lifecycle.HiltViewModel
4041
import kotlinx.coroutines.flow.filter
4142
import kotlinx.coroutines.flow.filterIsInstance
43+
import kotlinx.coroutines.launch
4244
import kotlinx.coroutines.flow.launchIn
4345
import kotlinx.coroutines.flow.map
4446
import kotlinx.coroutines.flow.mapNotNull
@@ -83,7 +85,7 @@ internal class OnRampViewModel @Inject constructor(
8385
tokenController: TokenController,
8486
transactionController: TransactionOperations,
8587
dispatchers: DispatcherProvider,
86-
analytics: FlipcashAnalyticsService,
88+
private val analytics: FlipcashAnalyticsService,
8789
) : BaseViewModel2<OnRampViewModel.State, OnRampViewModel.Event>(
8890
initialState = State(),
8991
updateStateForEvent = updateStateForEvent,
@@ -139,7 +141,7 @@ internal class OnRampViewModel @Inject constructor(
139141
data class UpdateOrderLookupState(
140142
val loading: Boolean = false,
141143
val success: Boolean = false
142-
): Event
144+
) : Event
143145

144146
data class OnAmountAccepted(val amount: VerifiedFiat) : Event
145147

@@ -169,16 +171,21 @@ internal class OnRampViewModel @Inject constructor(
169171
onRampController.state
170172
.onEach { s ->
171173
when (s) {
172-
is CoinbaseOnRampState.Completed -> dispatchEvent(Event.UpdateOrderLookupState(success = true))
174+
is CoinbaseOnRampState.Completed ->
175+
dispatchEvent(Event.UpdateOrderLookupState(success = true))
176+
173177
is CoinbaseOnRampState.Failed -> {
174178
dispatchEvent(Event.UpdateConfirmingAmountState())
175179
dispatchEvent(Event.UpdateOrderLookupState())
176180
}
181+
177182
CoinbaseOnRampState.Idle -> {
178183
dispatchEvent(Event.UpdateConfirmingAmountState())
179184
dispatchEvent(Event.UpdateOrderLookupState())
180185
}
181-
is CoinbaseOnRampState.Paying -> dispatchEvent(Event.UpdateOrderLookupState(loading = true))
186+
187+
is CoinbaseOnRampState.Paying ->
188+
dispatchEvent(Event.UpdateOrderLookupState(loading = true))
182189
}
183190
}
184191
.launchIn(viewModelScope)
@@ -343,62 +350,7 @@ internal class OnRampViewModel @Inject constructor(
343350
return@onEach
344351
}
345352

346-
onRampController.placeOrderAndStartPayment(
347-
amount = selectedAmount.localFiat.underlyingTokenAmount,
348-
token = token,
349-
verifiedFiat = selectedAmount,
350-
).onSuccess {
351-
analytics.buy(
352-
method = Analytics.PurchaseMethod.Coinbase,
353-
amount = selectedAmount.localFiat.nativeAmount,
354-
mint = token.address,
355-
)
356-
}.onFailure { error ->
357-
dispatchEvent(Event.UpdateConfirmingAmountState())
358-
359-
when (error) {
360-
is OnRampAuthError.CoinbasePhoneVerificationRequired -> {
361-
dispatchEvent(Event.OnVerificationNeeded(phone = true))
362-
}
363-
364-
is OnRampAuthError.VerificationRequired -> {
365-
dispatchEvent(
366-
Event.OnVerificationNeeded(
367-
phone = error.phone,
368-
email = error.email
369-
)
370-
)
371-
}
372-
373-
is OnRampPaymentError.GooglePayNotSupported -> {
374-
BottomBarManager.showAlert(
375-
title = resources.getString(R.string.error_title_onrampGooglePayNotSupported),
376-
message = resources.getString(R.string.error_description_onrampGooglePayNotSupported),
377-
)
378-
}
379-
380-
is OnRampPaymentError.GooglePayNoPaymentMethod -> {
381-
BottomBarManager.showAlert(
382-
title = resources.getString(R.string.error_title_onrampGooglePayNotReady),
383-
message = resources.getString(R.string.error_description_onrampGooglePayNotReady),
384-
)
385-
}
386-
387-
else -> {
388-
analytics.buy(
389-
method = Analytics.PurchaseMethod.Coinbase,
390-
amount = selectedAmount.localFiat.nativeAmount,
391-
mint = token.address,
392-
error = error
393-
)
394-
395-
BottomBarManager.showError(
396-
title = "Something Went Wrong",
397-
message = error.message ?: "Please try again",
398-
)
399-
}
400-
}
401-
}
353+
executeCoinbasePurchase(selectedAmount, token)
402354
}
403355

404356
else -> Unit
@@ -412,6 +364,109 @@ internal class OnRampViewModel @Inject constructor(
412364
}.launchIn(viewModelScope)
413365
}
414366

367+
private suspend fun executeCoinbasePurchase(
368+
selectedAmount: VerifiedFiat,
369+
token: Token,
370+
) {
371+
onRampController.checkPurchaseGates()
372+
.fold(
373+
onSuccess = { proceedWithCoinbasePurchase(selectedAmount, token) },
374+
onFailure = { gate ->
375+
when (gate) {
376+
is PurchaseGate.GooglePayNotSupported -> {
377+
dispatchEvent(Event.UpdateConfirmingAmountState())
378+
BottomBarManager.showAlert(
379+
title = resources.getString(R.string.error_title_onrampGooglePayNotSupported),
380+
message = resources.getString(R.string.error_description_onrampGooglePayNotSupported),
381+
)
382+
}
383+
384+
is PurchaseGate.GooglePayNoPaymentMethod -> {
385+
dispatchEvent(Event.UpdateConfirmingAmountState())
386+
BottomBarManager.showAlert(
387+
title = resources.getString(R.string.error_title_onrampGooglePayNotReady),
388+
message = resources.getString(R.string.error_description_onrampGooglePayNotReady),
389+
)
390+
}
391+
392+
is PurchaseGate.WebViewWarning -> {
393+
BottomBarManager.showAlert(
394+
title = resources.getString(R.string.error_title_onrampNonStableWebView),
395+
message = resources.getString(
396+
R.string.error_description_onrampNonStableWebView,
397+
gate.channel.name,
398+
),
399+
actions = listOf(
400+
BottomBarAction(
401+
text = resources.getString(R.string.action_cancel),
402+
style = BottomBarManager.BottomBarButtonStyle.Filled,
403+
) {
404+
dispatchEvent(Event.UpdateConfirmingAmountState())
405+
},
406+
BottomBarAction(
407+
text = resources.getString(R.string.action_continueAnyway),
408+
style = BottomBarManager.BottomBarButtonStyle.Text,
409+
) {
410+
viewModelScope.launch {
411+
proceedWithCoinbasePurchase(selectedAmount, token)
412+
}
413+
},
414+
),
415+
)
416+
}
417+
}
418+
},
419+
)
420+
}
421+
422+
private suspend fun proceedWithCoinbasePurchase(
423+
selectedAmount: VerifiedFiat,
424+
token: Token,
425+
) {
426+
onRampController.placeOrderAndStartPayment(
427+
amount = selectedAmount.localFiat.underlyingTokenAmount,
428+
token = token,
429+
verifiedFiat = selectedAmount,
430+
).onSuccess {
431+
analytics.buy(
432+
method = Analytics.PurchaseMethod.Coinbase,
433+
amount = selectedAmount.localFiat.nativeAmount,
434+
mint = token.address,
435+
)
436+
}.onFailure { error ->
437+
dispatchEvent(Event.UpdateConfirmingAmountState())
438+
439+
when (error) {
440+
is OnRampAuthError.CoinbasePhoneVerificationRequired -> {
441+
dispatchEvent(Event.OnVerificationNeeded(phone = true))
442+
}
443+
444+
is OnRampAuthError.VerificationRequired -> {
445+
dispatchEvent(
446+
Event.OnVerificationNeeded(
447+
phone = error.phone,
448+
email = error.email
449+
)
450+
)
451+
}
452+
453+
else -> {
454+
analytics.buy(
455+
method = Analytics.PurchaseMethod.Coinbase,
456+
amount = selectedAmount.localFiat.nativeAmount,
457+
mint = token.address,
458+
error = error
459+
)
460+
461+
BottomBarManager.showError(
462+
title = "Something Went Wrong",
463+
message = error.message ?: "Please try again",
464+
)
465+
}
466+
}
467+
}
468+
}
469+
415470
override fun onCleared() {
416471
exchange.resetEntryToBalance()
417472
}

apps/flipcash/shared/onramp/coinbase/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies {
1414
testImplementation(libs.bundles.unit.testing)
1515
testImplementation(libs.robolectric)
1616

17+
implementation(libs.androidx.webkit)
1718
implementation(libs.androidx.localbroadcastmanager)
1819
implementation(libs.bundles.kotlinx.serialization)
1920

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampController.kt

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ import kotlinx.serialization.json.JsonIgnoreUnknownKeys
4646
import retrofit2.HttpException
4747
import javax.inject.Inject
4848

49+
sealed class PurchaseGate : Throwable() {
50+
data class WebViewWarning(val channel: WebViewChannel) : PurchaseGate()
51+
data object GooglePayNotSupported : PurchaseGate()
52+
data object GooglePayNoPaymentMethod : PurchaseGate()
53+
}
54+
4955
typealias OrderWithPaymentLink = Pair<String, OnRampPurchaseResponse.PaymentLink>
5056

5157
private val json = Json {
@@ -63,6 +69,7 @@ class CoinbaseOnRampController @Inject constructor(
6369
private val featureFlags: FeatureFlagController,
6470
private val transactionController: TransactionOperations,
6571
private val googlePayReadiness: GooglePayReadiness,
72+
private val webViewChannelDetector: WebViewChannelDetector,
6673
) {
6774

6875
private val _state = MutableStateFlow<CoinbaseOnRampState>(CoinbaseOnRampState.Idle)
@@ -100,19 +107,30 @@ class CoinbaseOnRampController @Inject constructor(
100107
_state.update { CoinbaseOnRampState.Idle }
101108
}
102109

110+
suspend fun checkPurchaseGates(): Result<Unit> {
111+
when (googlePayReadiness.check()) {
112+
GooglePayReadiness.Status.NotSupported -> return Result.failure(PurchaseGate.GooglePayNotSupported)
113+
GooglePayReadiness.Status.NoPaymentMethod -> return Result.failure(PurchaseGate.GooglePayNoPaymentMethod)
114+
GooglePayReadiness.Status.Ready -> Unit
115+
}
116+
117+
webViewChannelDetector.detect()?.let { channel ->
118+
trace(
119+
tag = "CoinbaseOnRamp",
120+
message = "Non-stable WebView detected at purchase gate",
121+
metadata = { "channel" to channel.name },
122+
)
123+
return Result.failure(PurchaseGate.WebViewWarning(channel))
124+
}
125+
126+
return Result.success(Unit)
127+
}
128+
103129
suspend fun placeOrderAndStartPayment(
104130
amount: Fiat,
105131
token: Token,
106132
verifiedFiat: VerifiedFiat,
107133
): Result<Unit> {
108-
when (googlePayReadiness.check()) {
109-
GooglePayReadiness.Status.NotSupported ->
110-
return Result.failure(OnRampPaymentError.GooglePayNotSupported())
111-
GooglePayReadiness.Status.NoPaymentMethod ->
112-
return Result.failure(OnRampPaymentError.GooglePayNoPaymentMethod())
113-
GooglePayReadiness.Status.Ready -> Unit
114-
}
115-
116134
return placeOrderInclusiveOfFees(amount)
117135
.mapCatching { (orderId, paymentLink) ->
118136
val owner = userManager.accountCluster
@@ -376,6 +394,8 @@ sealed class OnRampPaymentError(
376394
) : CodeServerError(message) {
377395
class GooglePayNotSupported : OnRampPaymentError("Google Pay is not available on this device")
378396
class GooglePayNoPaymentMethod : OnRampPaymentError("No payment method enrolled in Google Pay")
397+
class NonStableWebViewChannel(val channel: WebViewChannel) :
398+
OnRampPaymentError("Non-stable WebView channel detected: ${channel.name}")
379399
}
380400

381401
sealed class OnRampAuthError(

0 commit comments

Comments
 (0)