Skip to content

Commit 6ce8f8d

Browse files
committed
feat(onramp): hoist Coinbase WebView to app level and add Google Pay readiness check
Move CoinbaseOnRampManager to a zero-dep state holder and absorb payment processing into OnRampController, breaking the circular dependency. The invisible WebView and post-payment lifecycle now live at the App level via CoinbaseOnRampHandler, allowing any screen to initiate purchases. Add native Google Pay readiness check (play-services-wallet) before placing orders — surfaces distinct errors for unsupported devices vs missing payment methods. Keep the confirm button loading until the WebView resolves or errors. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 3f0d85a commit 6ce8f8d

14 files changed

Lines changed: 541 additions & 331 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt

Lines changed: 77 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.flipcash.app.core.navigation.DeeplinkAction
4040
import com.flipcash.app.internal.ui.navigation.appEntryProvider
4141
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBlockingOverlayEntryDecorator
4242
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
43+
import com.flipcash.app.onramp.CoinbaseOnRampHandler
4344
import com.flipcash.app.onramp.ExternalWalletOnRampHandler
4445
import com.flipcash.app.onramp.LocalExternalWalletState
4546
import com.flipcash.app.onramp.rememberExternalWalletState
@@ -152,86 +153,92 @@ internal fun App(
152153
lifecycleOwner = LocalLifecycleOwner.current,
153154
navigator = codeNavigator,
154155
) {
155-
AppNavHost(
156+
CoinbaseOnRampHandler(
157+
manager = viewModel.coinbaseOnRampManager,
158+
controller = viewModel.onRampController,
156159
navigator = codeNavigator,
157-
resultStateRegistry = resultStateRegistry,
158-
decorators = listOf(
159-
rememberNavMessagingEntryDecorator(
160-
codeNavigator.backStack,
161-
barManager
160+
) {
161+
AppNavHost(
162+
navigator = codeNavigator,
163+
resultStateRegistry = resultStateRegistry,
164+
decorators = listOf(
165+
rememberNavMessagingEntryDecorator(
166+
codeNavigator.backStack,
167+
barManager
168+
),
169+
rememberNavBlockingOverlayEntryDecorator(),
162170
),
163-
rememberNavBlockingOverlayEntryDecorator(),
164-
),
165-
sceneStrategy = ModalBottomSheetSceneStrategy<NavKey>(
166-
codeNavigator.resultStore
167-
) {
168-
codeNavigator.backStack.getOrNull(
169-
codeNavigator.backStack.lastIndex - 1
170-
)
171-
} then SinglePaneSceneStrategy(),
172-
transitionSpec = {
173-
val shouldCrossfade =
174-
initialState.key == AppRoute.Loading.toString() ||
175-
targetState.key == AppRoute.Loading.toString() ||
176-
targetState.key.toString()
177-
.startsWith("Login")
178-
when {
179-
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
180-
tween(300)
171+
sceneStrategy = ModalBottomSheetSceneStrategy<NavKey>(
172+
codeNavigator.resultStore
173+
) {
174+
codeNavigator.backStack.getOrNull(
175+
codeNavigator.backStack.lastIndex - 1
181176
)
177+
} then SinglePaneSceneStrategy(),
178+
transitionSpec = {
179+
val shouldCrossfade =
180+
initialState.key == AppRoute.Loading.toString() ||
181+
targetState.key == AppRoute.Loading.toString() ||
182+
targetState.key.toString()
183+
.startsWith("Login")
184+
when {
185+
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
186+
tween(300)
187+
)
182188

183-
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
184-
EnterTransition.None togetherWith ExitTransition.None
189+
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
190+
EnterTransition.None togetherWith ExitTransition.None
185191

186-
else -> slideInHorizontally(initialOffsetX = { it }) togetherWith
187-
slideOutHorizontally(targetOffsetX = { -it })
188-
}
189-
},
190-
popTransitionSpec = {
191-
val shouldCrossfade =
192-
initialState.key == AppRoute.Loading.toString() ||
193-
targetState.key == AppRoute.Loading.toString() ||
194-
targetState.key.toString()
195-
.startsWith("Login")
196-
when {
197-
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
198-
tween(300)
199-
)
192+
else -> slideInHorizontally(initialOffsetX = { it }) togetherWith
193+
slideOutHorizontally(targetOffsetX = { -it })
194+
}
195+
},
196+
popTransitionSpec = {
197+
val shouldCrossfade =
198+
initialState.key == AppRoute.Loading.toString() ||
199+
targetState.key == AppRoute.Loading.toString() ||
200+
targetState.key.toString()
201+
.startsWith("Login")
202+
when {
203+
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
204+
tween(300)
205+
)
200206

201-
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
202-
EnterTransition.None togetherWith ExitTransition.None
207+
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
208+
EnterTransition.None togetherWith ExitTransition.None
203209

204-
else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
205-
slideOutHorizontally(targetOffsetX = { it })
206-
}
207-
},
208-
predictivePopTransitionSpec = {
209-
val shouldCrossfade =
210-
initialState.key == AppRoute.Loading.toString() ||
211-
targetState.key == AppRoute.Loading.toString() ||
212-
targetState.key.toString()
213-
.startsWith("Login")
214-
when {
215-
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
216-
tween(300)
217-
)
210+
else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
211+
slideOutHorizontally(targetOffsetX = { it })
212+
}
213+
},
214+
predictivePopTransitionSpec = {
215+
val shouldCrossfade =
216+
initialState.key == AppRoute.Loading.toString() ||
217+
targetState.key == AppRoute.Loading.toString() ||
218+
targetState.key.toString()
219+
.startsWith("Login")
220+
when {
221+
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
222+
tween(300)
223+
)
218224

219-
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
220-
EnterTransition.None togetherWith ExitTransition.None
225+
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
226+
EnterTransition.None togetherWith ExitTransition.None
221227

222-
else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
223-
slideOutHorizontally(targetOffsetX = { it })
224-
}
225-
},
226-
onBack = { codeNavigator.navigateBack() },
227-
entryProvider = appEntryProvider(
228-
resultStateRegistry = resultStateRegistry,
229-
barManager = barManager,
230-
deepLink = { deepLink },
231-
),
232-
)
228+
else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
229+
slideOutHorizontally(targetOffsetX = { it })
230+
}
231+
},
232+
onBack = { codeNavigator.navigateBack() },
233+
entryProvider = appEntryProvider(
234+
resultStateRegistry = resultStateRegistry,
235+
barManager = barManager,
236+
deepLink = { deepLink },
237+
),
238+
)
233239

234-
ScrimOverlay(scrimController)
240+
ScrimOverlay(scrimController)
241+
}
235242
}
236243

237244
val emailCodeChannel = LocalEmailCodeChannel.current

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/HomeViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import com.flipcash.app.android.R
66
import com.flipcash.app.appsettings.AppSettingValue
77
import com.flipcash.app.appsettings.AppSettingsCoordinator
88
import com.flipcash.app.auth.AuthManager
9+
import com.flipcash.app.onramp.CoinbaseOnRampManager
10+
import com.flipcash.app.onramp.OnRampController
911
import com.flipcash.app.shareable.ShareSheetController
1012
import com.flipcash.services.user.UserManager
1113
import com.getcode.manager.BottomBarAction
@@ -26,6 +28,8 @@ internal class HomeViewModel @Inject constructor(
2628
private val resources: ResourceHelper,
2729
private val appSettingsCoordinator: AppSettingsCoordinator,
2830
private val shareSheetController: ShareSheetController,
31+
val coinbaseOnRampManager: CoinbaseOnRampManager,
32+
val onRampController: OnRampController,
2933
) : ViewModel() {
3034

3135
private val _requireBiometrics = MutableStateFlow<Boolean?>(null)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,12 @@
562562
<string name="error_title_onrampInvalidRequest">Something Went Wrong</string>
563563
<string name="error_description_onrampInvalidRequest">We are working with the Coinbase team to resolve the issue. Your card will be refunded in the meantime. Please try again later</string>
564564

565+
<string name="error_title_onrampGooglePayNotSupported">Google Pay Not Available</string>
566+
<string name="error_description_onrampGooglePayNotSupported">Google Pay is not available on this device. Please try on a device with Google Pay support</string>
567+
568+
<string name="error_title_onrampGooglePayNotReady">Google Pay Not Ready</string>
569+
<string name="error_description_onrampGooglePayNotReady">Please add a payment method to Google Pay and try again</string>
570+
565571
<string name="error_title_onrampUnknownFailure">Something Went Wrong</string>
566572
<string name="error_description_onrampUnknownFailure">Please contact support@flipcash.com</string>
567573

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
package com.flipcash.app.onramp
22

3-
import androidx.compose.foundation.layout.Box
43
import androidx.compose.foundation.layout.Column
54
import androidx.compose.foundation.layout.fillMaxSize
65
import androidx.compose.runtime.Composable
76
import androidx.compose.runtime.LaunchedEffect
8-
import androidx.compose.runtime.getValue
9-
import androidx.compose.runtime.mutableStateOf
10-
import androidx.compose.runtime.saveable.rememberSaveable
11-
import androidx.compose.runtime.setValue
127
import androidx.compose.ui.Alignment
138
import androidx.compose.ui.Modifier
149
import androidx.compose.ui.res.stringResource
1510
import com.flipcash.app.core.AppRoute
1611
import com.getcode.solana.keys.Mint
1712
import com.flipcash.app.onramp.internal.OnRampViewModel
18-
import com.flipcash.app.onramp.internal.OnrampOrder
1913
import com.flipcash.app.onramp.internal.screens.OnRampAmountScreen
2014
import com.flipcash.features.onramp.R
2115
import androidx.hilt.navigation.compose.hiltViewModel
@@ -30,7 +24,6 @@ import kotlinx.coroutines.flow.onEach
3024
fun OnRampCustomAmountScreen(mint: Mint) {
3125
val navigator = LocalCodeNavigator.current
3226
val viewModel = hiltViewModel<OnRampViewModel>()
33-
var order by rememberSaveable { mutableStateOf<OnrampOrder?>(null) }
3427

3528
Column(
3629
modifier = Modifier.fillMaxSize(),
@@ -42,27 +35,7 @@ fun OnRampCustomAmountScreen(mint: Mint) {
4235
onBackIconClicked = { navigator.pop() },
4336
titleAlignment = Alignment.CenterHorizontally,
4437
)
45-
Box {
46-
order?.let {
47-
CoinbaseOnRampWebview(
48-
orderId = it.orderId,
49-
paymentLinkUrl = it.paymentLink,
50-
onPaymentSuccess = { orderId ->
51-
viewModel.dispatchEvent(OnRampViewModel.Event.OnPaymentSuccess(orderId))
52-
order = null
53-
},
54-
onPaymentFailure = { error ->
55-
viewModel.dispatchEvent(OnRampViewModel.Event.OnPaymentError(error))
56-
order = null
57-
},
58-
onCancel = {
59-
viewModel.dispatchEvent(OnRampViewModel.Event.OnPaymentCancel)
60-
order = null
61-
},
62-
)
63-
}
64-
OnRampAmountScreen(viewModel)
65-
}
38+
OnRampAmountScreen(viewModel)
6639
}
6740

6841
LaunchedEffect(Unit) {
@@ -71,14 +44,6 @@ fun OnRampCustomAmountScreen(mint: Mint) {
7144
}
7245
}
7346

74-
LaunchedEffect(viewModel) {
75-
viewModel.eventFlow
76-
.filterIsInstance<OnRampViewModel.Event.OnOrderCreated>()
77-
.map { it.order }
78-
.onEach { order = it }
79-
.launchIn(this)
80-
}
81-
8247
val externalWalletOnRamp = LocalExternalWalletState.current
8348
LaunchedEffect(viewModel) {
8449
viewModel.eventFlow
@@ -101,13 +66,4 @@ fun OnRampCustomAmountScreen(mint: Mint) {
10166
)
10267
}.launchIn(this)
10368
}
104-
105-
LaunchedEffect(viewModel) {
106-
viewModel.eventFlow
107-
.filterIsInstance<OnRampViewModel.Event.OnBuySubmitted>()
108-
.map { it.swapId }
109-
.onEach { swapId ->
110-
navigator.push(AppRoute.Token.TxProcessing(swapId))
111-
}.launchIn(this)
112-
}
11369
}

0 commit comments

Comments
 (0)