From 37ff83bfb5f014b74d1a576e1a2e2f16ab260fd4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 21 May 2026 15:19:13 -0400 Subject: [PATCH 1/2] feat: updated deposit and withdrawal flows with currency selection and USDC info screens Signed-off-by: Brandon McAnsh --- .../kotlin/com/flipcash/app/core/AppRoute.kt | 16 +- .../flipcash/app/core/deposit/DepositStep.kt | 5 +- .../flipcash/app/core/tokens/SwapResult.kt | 4 + .../app/core/withdrawal/WithdrawalStep.kt | 6 +- .../core/src/main/res/values/strings.xml | 3 + .../internal/AdvancedFeatureMenuItems.kt | 20 --- .../features/deposit/build.gradle.kts | 1 + .../flipcash/app/deposit/DepositFlowScreen.kt | 153 +++++++++++------- .../app/deposit/internal/DepositViewModel.kt | 9 +- .../internal/UsdcDepositInformationScreen.kt | 31 +++- .../app/menu/internal/MenuScreenContent.kt | 5 +- .../flipcash/app/tokens/SwapEntryScreen.kt | 7 +- .../com/flipcash/app/tokens/SwapFlowScreen.kt | 31 +++- .../flipcash/app/tokens/TokenSelectScreen.kt | 4 +- .../app/tokens/internal/TokenInfoScreen.kt | 4 +- .../app/withdrawal/WithdrawalFlowScreen.kt | 76 ++++++++- .../app/withdrawal/WithdrawalViewModel.kt | 10 +- .../UsdcWithdrawalInformationScreen.kt | 29 +++- .../screens/WithdrawalConfirmationScreen.kt | 9 +- .../screens/WithdrawalDestinationScreen.kt | 2 +- .../app/tokens/ui/SelectTokenViewModel.kt | 20 ++- .../com/getcode/navigation/flow/FlowHost.kt | 6 + .../getcode/navigation/flow/FlowNavigator.kt | 9 ++ 23 files changed, 320 insertions(+), 140 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt index 73ff3b2e6..5b93dabd7 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt @@ -167,23 +167,15 @@ sealed interface AppRoute : NavKey, Parcelable { @Parcelize sealed interface Transfers : AppRoute { @Serializable - data class Deposit(val mint: Mint): Transfers, FlowRouteWithResult { + data class Deposit(val showOtherOptions: Boolean = true): Transfers, FlowRouteWithResult { override val initialStack: List - get() = if (mint == Mint.usdf) { - listOf(DepositStep.UsdcInformational) - } else { - listOf(DepositStep.Destination(mint)) - } + get() = listOf(DepositStep.UsdcInformational(showOtherOptions)) } @Serializable - data class Withdrawal(val mint: Mint) : Transfers, FlowRouteWithResult { + data class Withdrawal(val showOtherOptions: Boolean = true) : Transfers, FlowRouteWithResult { override val initialStack: List - get() = if (mint == Mint.usdf) { - listOf(WithdrawalStep.UsdcInformational) - } else { - listOf(WithdrawalStep.Amount(mint)) - } + get() = listOf(WithdrawalStep.UsdcInformational(showOtherOptions)) } } diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/deposit/DepositStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/deposit/DepositStep.kt index 560ff27f3..8cb18871f 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/deposit/DepositStep.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/deposit/DepositStep.kt @@ -13,9 +13,12 @@ import kotlinx.serialization.Serializable @Serializable sealed interface DepositStep : FlowStep, Parcelable { @Parcelize - object UsdcInformational : DepositStep + data class UsdcInformational(val showOtherOptions: Boolean) : DepositStep + @Parcelize + @Serializable + data object SelectToken: DepositStep @Parcelize @Serializable data class Destination(val mint: Mint) : DepositStep diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapResult.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapResult.kt index 665f07f94..4b59c21bf 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapResult.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapResult.kt @@ -13,4 +13,8 @@ sealed interface SwapResult : Parcelable { @Parcelize @Serializable data object Canceled : SwapResult + + @Parcelize + @Serializable + data object OpenDeposit : SwapResult } diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/withdrawal/WithdrawalStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/withdrawal/WithdrawalStep.kt index 90b9c8c8c..99662f56d 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/withdrawal/WithdrawalStep.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/withdrawal/WithdrawalStep.kt @@ -14,7 +14,11 @@ import kotlinx.serialization.Serializable @Serializable sealed interface WithdrawalStep : FlowStep, Parcelable { @Parcelize - object UsdcInformational: WithdrawalStep + data class UsdcInformational(val showOtherOptions: Boolean): WithdrawalStep + + @Parcelize + @Serializable + data object SelectToken: WithdrawalStep @Parcelize @Serializable data class Amount(val mint: Mint) : WithdrawalStep diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index ea1791795..0e51936fe 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -669,4 +669,7 @@ Confirm the transaction in Phantom to continue Deposit %1$s + + Deposit Other Flipcash Currencies + Withdraw Other Flipcash Currencies \ No newline at end of file diff --git a/apps/flipcash/features/advanced/src/main/kotlin/com/flipcash/app/advanced/internal/AdvancedFeatureMenuItems.kt b/apps/flipcash/features/advanced/src/main/kotlin/com/flipcash/app/advanced/internal/AdvancedFeatureMenuItems.kt index 61ec97097..cd6cfc8b8 100644 --- a/apps/flipcash/features/advanced/src/main/kotlin/com/flipcash/app/advanced/internal/AdvancedFeatureMenuItems.kt +++ b/apps/flipcash/features/advanced/src/main/kotlin/com/flipcash/app/advanced/internal/AdvancedFeatureMenuItems.kt @@ -14,16 +14,6 @@ import com.flipcash.app.core.tokens.TokenPurpose import com.flipcash.app.menu.FullMenuItem import com.flipcash.features.advanced.R -internal data object CurrencyCreator : FullMenuItem() { - override val icon: Painter - @Composable get() = rememberVectorPainter(Icons.Default.AddCircle) - override val name: String - @Composable get() = stringResource(R.string.title_createYourCurrency) - override val action: AdvancedFeaturesScreenViewModel.Event = AdvancedFeaturesScreenViewModel.Event.OpenScreen( - AppRoute.Token.CurrencyCreator - ) -} - internal data object BillCustomizer : FullMenuItem() { override val icon: Painter @Composable get() = rememberVectorPainter(Icons.Outlined.Palette) @@ -32,16 +22,6 @@ internal data object BillCustomizer : FullMenuItem() { - override val icon: Painter - @Composable get() = painterResource(R.drawable.ic_menu_deposit) - override val name: String - @Composable get() = stringResource(R.string.title_depositFunds) - override val action: AdvancedFeaturesScreenViewModel.Event = AdvancedFeaturesScreenViewModel.Event.OpenScreen( - AppRoute.Sheets.TokenSelection(purpose = TokenPurpose.Deposit) - ) -} - internal data object DeviceLogs : FullMenuItem() { override val icon: Painter @Composable get() = rememberVectorPainter(Icons.Outlined.Description) diff --git a/apps/flipcash/features/deposit/build.gradle.kts b/apps/flipcash/features/deposit/build.gradle.kts index f0ccbbe22..ac6970387 100644 --- a/apps/flipcash/features/deposit/build.gradle.kts +++ b/apps/flipcash/features/deposit/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(project(":libs:messaging")) implementation(project(":apps:flipcash:shared:featureflags")) + implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":services:flipcash")) } diff --git a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt index e32eee7b7..752e204b2 100644 --- a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt +++ b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt @@ -1,12 +1,17 @@ package com.flipcash.app.deposit +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey @@ -14,11 +19,13 @@ import androidx.navigation3.runtime.entryProvider import com.flipcash.app.core.AppRoute import com.flipcash.app.core.deposit.DepositResult import com.flipcash.app.core.deposit.DepositStep -import com.flipcash.app.featureflags.FeatureFlag -import com.flipcash.app.featureflags.LocalFeatureFlags +import com.flipcash.app.core.tokens.TokenPurpose import com.flipcash.app.deposit.internal.DepositViewModel import com.flipcash.app.deposit.internal.UsdcDepositInformationScreen import com.flipcash.app.theme.FlipcashThemeWrapper +import com.flipcash.app.tokens.ui.SelectTokenViewModel +import com.flipcash.app.tokens.ui.TokenList +import com.flipcash.core.R import com.getcode.navigation.annotatedEntry import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.flow.FlowExitReason @@ -26,9 +33,17 @@ import com.getcode.navigation.flow.FlowHost import com.getcode.navigation.flow.LocalFlowNavigator import com.getcode.navigation.flow.PreviewFlowNavigator import com.getcode.navigation.flow.deliverFlowResult +import com.getcode.navigation.flow.rememberFlowNavigator +import com.getcode.navigation.flow.rememberInitialStack +import com.getcode.navigation.flowAnnotatedEntry import com.getcode.navigation.results.NavResultOrCanceled import com.getcode.navigation.results.NavResultStateRegistry -import com.getcode.solana.keys.Mint +import com.getcode.ui.components.AppBarWithTitle +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach @Composable fun DepositFlowScreen( @@ -36,63 +51,91 @@ fun DepositFlowScreen( resultStateRegistry: NavResultStateRegistry, ) { val outerNavigator = LocalCodeNavigator.current - val featureFlags = LocalFeatureFlags.current + val initialStack = route.rememberInitialStack() - val directDeposit by featureFlags - .observe(FeatureFlag.DepositUsdc) - .collectAsStateWithLifecycle() - - val initialStack = remember(route, directDeposit) { - @Suppress("UNCHECKED_CAST") - val steps = route.initialStack as List - println("direct deposit = $directDeposit, isUsdf=${route.mint == Mint.usdf}") - if (!directDeposit && route.mint == Mint.usdf) { - listOf(DepositStep.Destination(route.mint)) - } else { - steps - } - } - - key(directDeposit) { - FlowHost( - initialStack = initialStack, - resultStateRegistry = resultStateRegistry, - onExit = { reason, isSheetRoot -> - val result: DepositResult = when (reason) { - is FlowExitReason.Completed -> reason.result - FlowExitReason.Canceled, - FlowExitReason.BackedOutOfRoot -> DepositResult.Canceled - } - if (isSheetRoot) { - outerNavigator.pop() - } else { - outerNavigator.deliverFlowResult( - route = route, - value = NavResultOrCanceled.ReturnValue(result), - ) - when (result) { - DepositResult.Success -> { - outerNavigator.popUntil { it == AppRoute.Sheets.Menu } - } - DepositResult.Canceled -> { - outerNavigator.pop() - } + FlowHost( + initialStack = initialStack, + resultStateRegistry = resultStateRegistry, + onExit = { reason, isSheetRoot -> + val result: DepositResult = when (reason) { + is FlowExitReason.Completed -> reason.result + FlowExitReason.Canceled, + FlowExitReason.BackedOutOfRoot -> DepositResult.Canceled + } + if (isSheetRoot) { + outerNavigator.pop() + } else { + outerNavigator.deliverFlowResult( + route = route, + value = NavResultOrCanceled.ReturnValue(result), + ) + when (result) { + DepositResult.Success -> { + outerNavigator.popUntil { it == AppRoute.Sheets.Menu } + } + DepositResult.Canceled -> { + outerNavigator.pop() } } - }, - entryProvider = depositEntryProvider(route.mint), - ) - } + } + }, + entryProvider = depositEntryProvider(route.showOtherOptions), + ) } private fun depositEntryProvider( - mint: Mint, + showOtherOptions: Boolean, ): (NavKey) -> NavEntry = entryProvider { - annotatedEntry { - UsdcDepositInformationScreen() + flowAnnotatedEntry { + UsdcDepositInformationScreen(showOtherOptions) + } + annotatedEntry { + DepositSelectTokenScreen() + } + annotatedEntry { key -> + DepositDestinationScreen(key.mint) + } +} + +@Composable +private fun DepositSelectTokenScreen() { + val flowNavigator = rememberFlowNavigator() + val viewModel = hiltViewModel() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithTitle( + isInModal = true, + title = stringResource(R.string.title_selectCurrency), + backButton = true, + onBackIconClicked = { flowNavigator.back() }, + titleAlignment = Alignment.CenterHorizontally, + ) + TokenList( + modifier = Modifier.fillMaxSize(), + tokens = state.tokens, + selectedToken = state.selectedToken, + showFlags = true, + includeReserves = true, + onTokenSelected = { viewModel.dispatchEvent(SelectTokenViewModel.Event.OnTokenSelected(it.address)) }, + ) } - annotatedEntry { - DepositDestinationScreen(mint) + + LaunchedEffect(viewModel) { + viewModel.dispatchEvent(SelectTokenViewModel.Event.OnPurposeChanged(TokenPurpose.Deposit)) + } + + LaunchedEffect(viewModel) { + viewModel.eventFlow + .filterIsInstance() + .filter { it.fromUser } + .map { it.mint } + .onEach { mint -> + flowNavigator.navigateTo(DepositStep.Destination(mint)) + }.launchIn(this) } } @@ -112,5 +155,5 @@ private fun DepositFlowPreview( @PreviewWrapper(FlipcashThemeWrapper::class) @Composable private fun Preview_UsdcInformational() { - DepositFlowPreview { UsdcDepositInformationScreen() } + DepositFlowPreview { UsdcDepositInformationScreen(true) } } diff --git a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt index 5e4b74dff..aa4656e8a 100644 --- a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt +++ b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt @@ -61,8 +61,7 @@ internal class DepositViewModel @Inject constructor( .onResult( onSuccess = { result -> viewModelScope.launch { - val directDeposit = featureFlags.get(FeatureFlag.DepositUsdc) - val address = if (result.token.address == Mint.usdf && directDeposit) { + val address = if (result.token.address == Mint.usdc) { userManager.accountCluster?.authorityPublicKey?.base58() } else { userManager.accountCluster?.depositAddressFor(result.token)?.base58() @@ -77,11 +76,11 @@ internal class DepositViewModel @Inject constructor( } return@launch } - val tokenName = when { - directDeposit && result.token.address == Mint.usdf -> { + val tokenName = when (result.token.address) { + Mint.usdc -> { resources.getString(R.string.displayName_usdc) } - result.token.address == Mint.usdf -> { + Mint.usdf -> { resources.getString(R.string.displayName_usdf) } else -> result.token.name diff --git a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt index b90044278..8a2acc2c0 100644 --- a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt +++ b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt @@ -27,9 +27,8 @@ import com.getcode.ui.theme.CodeButton import com.getcode.ui.theme.CodeScaffold @Composable -internal fun UsdcDepositInformationScreen() { +internal fun UsdcDepositInformationScreen(showOtherOptions: Boolean) { val flowNavigator = rememberFlowNavigator() - CodeScaffold( topBar = { AppBarWithTitle( @@ -41,17 +40,33 @@ internal fun UsdcDepositInformationScreen() { ) }, bottomBar = { - CodeButton( - modifier = Modifier - .fillMaxWidth() + Column( + modifier = Modifier.fillMaxWidth() .padding(horizontal = CodeTheme.dimens.inset) .padding(bottom = CodeTheme.dimens.grid.x2) .navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_next), ) { - flowNavigator.navigateTo(DepositStep.Destination(Mint.usdf)) + CodeButton( + modifier = Modifier + .fillMaxWidth(), + buttonState = ButtonState.Filled, + text = stringResource(R.string.action_next), + ) { + flowNavigator.navigateTo(DepositStep.Destination(Mint.usdc)) + } + + if (showOtherOptions) { + CodeButton( + modifier = Modifier + .fillMaxWidth(), + buttonState = ButtonState.Subtle, + text = stringResource(R.string.action_depositOtherCurrencies), + ) { + flowNavigator.navigateTo(DepositStep.SelectToken) + } + } } + } ) { padding -> Box( diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt index e7c56ce61..66291ab87 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.AppRoute -import com.flipcash.app.core.tokens.TokenPurpose import com.flipcash.app.core.ui.TileButton import com.flipcash.app.menu.MenuList import com.flipcash.app.menu.internal.MenuScreenViewModel.Event @@ -105,7 +104,7 @@ internal fun MenuScreenContent(viewModel: MenuScreenViewModel) { text = stringResource(R.string.action_depositFunds), icon = painterResource(R.drawable.ic_menu_deposit) ) { - navigator.push(AppRoute.Sheets.TokenSelection(purpose = TokenPurpose.Deposit)) + navigator.push(AppRoute.Transfers.Deposit()) } TileButton( @@ -113,7 +112,7 @@ internal fun MenuScreenContent(viewModel: MenuScreenViewModel) { text = stringResource(R.string.action_withdraw), icon = painterResource(R.drawable.ic_menu_withdraw) ) { - navigator.push(AppRoute.Sheets.TokenSelection(TokenPurpose.Withdraw)) + navigator.push(AppRoute.Transfers.Withdrawal()) } } }, diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt index 9967560c7..aa8ebcf39 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt @@ -17,11 +17,9 @@ import com.flipcash.app.onramp.LocalCoinbaseOnRampController import com.flipcash.app.tokens.internal.SwapEntryScreenContent import com.flipcash.app.tokens.ui.SwapViewModel import com.flipcash.features.tokens.R -import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.flow.flowSharedViewModel import com.getcode.navigation.flow.rememberFlowNavigator import com.getcode.opencode.model.financial.Fiat -import com.getcode.solana.keys.Mint import com.getcode.ui.components.AppBarWithTitle import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn @@ -36,7 +34,6 @@ internal fun SwapEntryScreen( val flowNavigator = rememberFlowNavigator() val viewModel = flowSharedViewModel() val state by viewModel.stateFlow.collectAsStateWithLifecycle() - val navigator = LocalCodeNavigator.current val coinbaseOnRampController = LocalCoinbaseOnRampController.current Column( @@ -99,7 +96,7 @@ internal fun SwapEntryScreen( .filterIsInstance() .onEach { (phone, email) -> val mint = (viewModel.stateFlow.value.purpose as? SwapPurpose.Buy)?.mint ?: return@onEach - navigator.push( + flowNavigator.navigate( AppRoute.Verification( origin = AppRoute.Token.Swap(SwapPurpose.Buy(mint)), includePhone = phone, @@ -121,7 +118,7 @@ internal fun SwapEntryScreen( viewModel.eventFlow .filterIsInstance() .onEach { - navigator.push(AppRoute.Transfers.Deposit(Mint.usdf)) + flowNavigator.exitWithResult(SwapResult.OpenDeposit) }.launchIn(this) } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt index 0645cb027..92e122d77 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt @@ -1,10 +1,12 @@ package com.flipcash.app.tokens import androidx.compose.runtime.Composable +import androidx.compose.runtime.snapshots.Snapshot import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import com.flipcash.app.core.AppRoute +import com.flipcash.app.core.extensions.openAsSheet import com.flipcash.app.core.tokens.SwapResult import com.flipcash.app.core.tokens.SwapStep import com.getcode.navigation.annotatedEntry @@ -16,6 +18,7 @@ import com.getcode.navigation.flow.FlowHost import com.getcode.navigation.flow.deliverFlowResult import com.getcode.navigation.results.NavResultOrCanceled import com.getcode.navigation.results.NavResultStateRegistry +import com.getcode.navigation.scenes.LocalSheetNavigator @Composable fun SwapFlowScreen( @@ -23,6 +26,7 @@ fun SwapFlowScreen( resultStateRegistry: NavResultStateRegistry, ) { val outerNavigator = LocalCodeNavigator.current + val rootNavigator = LocalSheetNavigator.current val initialStack = route.rememberInitialStack() FlowHost( @@ -34,16 +38,27 @@ fun SwapFlowScreen( FlowExitReason.Canceled, FlowExitReason.BackedOutOfRoot -> SwapResult.Canceled } - outerNavigator.deliverFlowResult( - route = route, - value = NavResultOrCanceled.ReturnValue(result), - ) when (result) { - SwapResult.Success -> { - if (route.shortfall != null) outerNavigator.popAll() - else outerNavigator.popUntil { it is AppRoute.Token.Info } + is SwapResult.OpenDeposit -> { + Snapshot.withMutableSnapshot { + rootNavigator?.hide() + // can only deposit USDC from this flow + rootNavigator?.openAsSheet(AppRoute.Transfers.Deposit(showOtherOptions = false)) + } + } + else -> { + outerNavigator.deliverFlowResult( + route = route, + value = NavResultOrCanceled.ReturnValue(result), + ) + when (result) { + SwapResult.Success -> { + if (route.shortfall != null) outerNavigator.popAll() + else outerNavigator.popUntil { it is AppRoute.Token.Info } + } + SwapResult.Canceled -> outerNavigator.pop() + } } - SwapResult.Canceled -> outerNavigator.pop() } }, entryProvider = swapEntryProvider(), diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSelectScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSelectScreen.kt index eeb2b28de..6a306b981 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSelectScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSelectScreen.kt @@ -63,10 +63,10 @@ fun TokenSelectScreen(purpose: TokenPurpose) { TokenPurpose.Balance -> Unit TokenPurpose.Select -> Unit TokenPurpose.Withdraw -> { - navigator.push(Withdrawal(token)) + navigator.push(Withdrawal()) } TokenPurpose.Deposit -> { - navigator.push(Deposit(token)) + navigator.push(Deposit()) } } }.launchIn(this) diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt index 86e2f7b9b..e76e83d8a 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt @@ -330,7 +330,7 @@ private fun RowScope.ReserveButtonOptions( ) { dispatch( TokenInfoViewModel.Event.OpenScreen( - AppRoute.Transfers.Deposit(mint = mint) + AppRoute.Transfers.Deposit() ) ) } @@ -345,7 +345,7 @@ private fun RowScope.ReserveButtonOptions( ) { dispatch( TokenInfoViewModel.Event.OpenScreen( - AppRoute.Transfers.Withdrawal(mint = mint) + AppRoute.Transfers.Withdrawal(showOtherOptions = false) ) ) } diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt index 63f97f5be..272eabecd 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt @@ -1,20 +1,33 @@ package com.flipcash.app.withdrawal +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import com.flipcash.app.core.AppRoute +import com.flipcash.app.core.tokens.TokenPurpose import com.flipcash.app.core.withdrawal.WithdrawalResult import com.flipcash.app.core.withdrawal.WithdrawalStep import com.flipcash.app.theme.FlipcashThemeWrapper +import com.flipcash.app.tokens.ui.SelectTokenViewModel +import com.flipcash.app.tokens.ui.TokenList import com.flipcash.app.withdrawal.internal.screens.UsdcWithdrawalInformationScreen import com.flipcash.app.withdrawal.internal.screens.WithdrawalConfirmationScreen import com.flipcash.app.withdrawal.internal.screens.WithdrawalDestinationScreen import com.flipcash.app.withdrawal.internal.screens.WithdrawalEntryScreen +import com.flipcash.core.R import com.getcode.navigation.annotatedEntry import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.flow.FlowExitReason @@ -22,11 +35,17 @@ import com.getcode.navigation.flow.FlowHost import com.getcode.navigation.flow.LocalFlowNavigator import com.getcode.navigation.flow.PreviewFlowNavigator import com.getcode.navigation.flow.deliverFlowResult +import com.getcode.navigation.flow.rememberFlowNavigator import com.getcode.navigation.flow.rememberInitialStack import com.getcode.navigation.flowAnnotatedEntry import com.getcode.navigation.results.NavResultOrCanceled import com.getcode.navigation.results.NavResultStateRegistry -import com.getcode.solana.keys.Mint +import com.getcode.ui.components.AppBarWithTitle +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach @Composable fun WithdrawalFlowScreen( @@ -59,15 +78,18 @@ fun WithdrawalFlowScreen( } } }, - entryProvider = withdrawalEntryProvider(route.mint), + entryProvider = withdrawalEntryProvider(route.showOtherOptions), ) } private fun withdrawalEntryProvider( - mint: Mint, + showOtherOptions: Boolean, ): (NavKey) -> NavEntry = entryProvider { annotatedEntry { - UsdcWithdrawalInformationScreen() + UsdcWithdrawalInformationScreen(showOtherOptions) + } + annotatedEntry { + WithdrawalSelectTokenScreen() } flowAnnotatedEntry { step -> WithdrawalEntryScreen(step.mint) @@ -76,7 +98,49 @@ private fun withdrawalEntryProvider( WithdrawalDestinationScreen() } annotatedEntry { - WithdrawalConfirmationScreen(mint = mint) + WithdrawalConfirmationScreen() + } +} + +@Composable +private fun WithdrawalSelectTokenScreen() { + val flowNavigator = rememberFlowNavigator() + val viewModel = hiltViewModel() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithTitle( + isInModal = true, + title = stringResource(R.string.title_selectCurrency), + backButton = true, + onBackIconClicked = { flowNavigator.back() }, + titleAlignment = Alignment.CenterHorizontally, + ) + TokenList( + modifier = Modifier.fillMaxSize(), + tokens = state.tokens, + selectedToken = state.selectedToken, + showFlags = true, + includeReserves = true, + onTokenSelected = { viewModel.dispatchEvent(SelectTokenViewModel.Event.OnTokenSelected(it.address)) }, + ) + } + + LaunchedEffect(viewModel) { + viewModel.dispatchEvent(SelectTokenViewModel.Event.OnPurposeChanged(TokenPurpose.Withdraw)) + } + + LaunchedEffect(viewModel) { + viewModel.eventFlow + .filterIsInstance() + .filter { it.fromUser } + .map { it.mint } + .onEach { mint -> + flowNavigator.navigateTo(WithdrawalStep.Amount(mint)) + }.launchIn(this) } } @@ -96,6 +160,6 @@ private fun WithdrawalFlowPreview( @PreviewWrapper(FlipcashThemeWrapper::class) @Composable private fun Preview_UsdcInformational() { - WithdrawalFlowPreview { UsdcWithdrawalInformationScreen() } + WithdrawalFlowPreview { UsdcWithdrawalInformationScreen(showOtherOptions = true) } } diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalViewModel.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalViewModel.kt index 82fbe2ba4..f21c1a6b2 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalViewModel.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalViewModel.kt @@ -654,7 +654,15 @@ internal class WithdrawalViewModel @Inject constructor( internal companion object { val updateStateForEvent: (Event) -> ((State) -> State) = { event -> when (event) { - is Event.OnMintSelected -> { state -> state.copy(selectedTokenAddress = event.mint) } + is Event.OnMintSelected -> { state -> + // if mint is USDC, store it as USDF + val mint = if (event.mint == Mint.usdc) { + Mint.usdf + } else { + event.mint + } + state.copy(selectedTokenAddress = mint) + } is Event.OnTokenUpdated -> { state -> state.copy(token = event.token) } is Event.OnAmountChanged -> { state -> diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt index e979c32c4..5efe0d176 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt @@ -33,7 +33,7 @@ import com.getcode.ui.theme.CodeButton import com.getcode.ui.theme.CodeScaffold @Composable -internal fun UsdcWithdrawalInformationScreen() { +internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { val flowNavigator = rememberFlowNavigator() CodeScaffold( @@ -47,16 +47,31 @@ internal fun UsdcWithdrawalInformationScreen() { ) }, bottomBar = { - CodeButton( - modifier = Modifier - .fillMaxWidth() + Column( + modifier = Modifier.fillMaxWidth() .padding(horizontal = CodeTheme.dimens.inset) .padding(bottom = CodeTheme.dimens.grid.x2) .navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_next), ) { - flowNavigator.navigateTo(WithdrawalStep.Amount(Mint.usdf)) + CodeButton( + modifier = Modifier + .fillMaxWidth(), + buttonState = ButtonState.Filled, + text = stringResource(R.string.action_next), + ) { + flowNavigator.navigateTo(WithdrawalStep.Amount(Mint.usdc)) + } + + if (showOtherOptions) { + CodeButton( + modifier = Modifier + .fillMaxWidth(), + buttonState = ButtonState.Subtle, + text = stringResource(R.string.action_withdrawOtherCurrencies), + ) { + flowNavigator.navigateTo(WithdrawalStep.SelectToken) + } + } } } ) { padding -> diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalConfirmationScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalConfirmationScreen.kt index 703a12a9d..e970c470d 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalConfirmationScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalConfirmationScreen.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.withdrawal.WithdrawalResult import com.flipcash.app.core.withdrawal.WithdrawalStep import com.flipcash.app.withdrawal.WithdrawalViewModel @@ -21,13 +23,15 @@ import com.getcode.ui.components.AppBarWithTitle import com.getcode.util.resources.LocalResources import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach @Composable -internal fun WithdrawalConfirmationScreen(mint: Mint) { +internal fun WithdrawalConfirmationScreen() { val flowNavigator = rememberFlowNavigator() val viewModel = flowSharedViewModel() val resources = LocalResources.current + val state by viewModel.stateFlow.collectAsStateWithLifecycle() Column( modifier = Modifier.fillMaxSize(), @@ -61,7 +65,8 @@ internal fun WithdrawalConfirmationScreen(mint: Mint) { LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() - .onEach { + .mapNotNull { state.selectedTokenAddress } + .onEach { mint -> BottomBarManager.showAlert( title = resources.getString(WR.string.error_title_withdrawalTooSmall), message = resources.getString(WR.string.error_description_withdrawalTooSmall), diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalDestinationScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalDestinationScreen.kt index fc0f81f96..5266e3979 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalDestinationScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalDestinationScreen.kt @@ -40,7 +40,7 @@ internal fun WithdrawalDestinationScreen() { LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() - .onEach { + .onEach { mint -> flowNavigator.navigateTo(WithdrawalStep.Confirmation) }.launchIn(this) } diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt index 31bae0cd5..2712523ac 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt @@ -133,7 +133,25 @@ class SelectTokenViewModel @Inject constructor( TokenWithLocalizedBalance( token = it.token, balance = balance, - appreciation = appreciation + appreciation = appreciation, + displayName = when (purpose) { + TokenPurpose.Balance -> it.token.name + TokenPurpose.Deposit -> { + if (it.token.address == Mint.usdf) { + resources.getString(R.string.displayName_usdf) + } else { + it.token.name + } + } + TokenPurpose.Select -> it.token.name + TokenPurpose.Withdraw -> { + if (it.token.address == Mint.usdf) { + resources.getString(R.string.displayName_usdf) + } else { + it.token.name + } + } + } ) } .sortedWith(compareByDescending { item -> item.balance.nativeAmount }) diff --git a/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt b/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt index 8932a3294..24597972e 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt @@ -175,6 +175,7 @@ fun FlowHost( val flowNavigator = remember(innerNavigator) { InnerFlowNavigator( navigator = innerNavigator, + outerNavigator = outerNavigator, onExit = { reason -> currentOnExit.value(reason, isSheetRoot) }, ) } @@ -227,6 +228,7 @@ fun FlowHost( private class InnerFlowNavigator( private val navigator: CodeNavigator, + private val outerNavigator: CodeNavigator, private val onExit: (FlowExitReason) -> Unit, ) : FlowNavigator { @@ -272,6 +274,10 @@ private class InnerFlowNavigator( override fun exitCanceled() { onExit(FlowExitReason.Canceled) } + + override fun navigate(route: NavKey) { + outerNavigator.push(route) + } } /** diff --git a/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowNavigator.kt b/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowNavigator.kt index 85675f99f..ba367bf9b 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowNavigator.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowNavigator.kt @@ -3,6 +3,7 @@ package com.getcode.navigation.flow import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf +import androidx.navigation3.runtime.NavKey import com.getcode.navigation.core.CodeNavigator /** @@ -39,6 +40,13 @@ interface FlowNavigator { /** Exit the flow without delivering a result (caller sees it as a cancellation). */ fun exitCanceled() + + /** + * Push [route] onto the *outer* (app-level) navigator. + * Use this when a flow step needs to open a screen outside the flow + * (e.g. region selection, token selection) without referencing the outer navigator directly. + */ + fun navigate(route: NavKey) } val LocalFlowNavigator = @@ -66,6 +74,7 @@ class PreviewFlowNavigator : FlowNavigator { override fun back(): Boolean = false override fun exitWithResult(result: R) {} override fun exitCanceled() {} + override fun navigate(route: NavKey) {} } /** From e4f7870aaf05ba2850963b8a859a5373716cec96 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 21 May 2026 15:48:14 -0400 Subject: [PATCH 2/2] feat: refactor TokenBalanceStyle into sealed interface with per-style textStyle and selection Signed-off-by: Brandon McAnsh --- .../flipcash/app/core/ui/TokenBalanceRow.kt | 147 +++++++++++++----- .../balance/internal/BalanceScreenContent.kt | 6 +- .../flipcash/app/deposit/DepositFlowScreen.kt | 1 - .../app/tokens/internal/TokenSelectScreen.kt | 9 +- .../tokens/internal/TokenSellReceiptScreen.kt | 9 +- .../app/withdrawal/WithdrawalFlowScreen.kt | 1 - .../internal/components/TransactionReceipt.kt | 9 +- .../com/flipcash/app/tokens/ui/TokenList.kt | 13 +- 8 files changed, 144 insertions(+), 51 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt index ba6388616..85b9c829f 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt @@ -1,6 +1,7 @@ package com.flipcash.app.core.ui import androidx.compose.foundation.Image +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -11,7 +12,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,6 +22,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.flipcash.core.R import com.getcode.opencode.compose.LocalExchange import com.getcode.opencode.model.financial.Fiat @@ -27,23 +30,48 @@ import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.TokenWithBalance import com.getcode.opencode.model.financial.TokenWithLocalizedBalance import com.getcode.theme.CodeTheme +import com.getcode.theme.White20 +import com.getcode.theme.extraSmall import com.getcode.ui.components.text.AnimatedNumberText import com.getcode.ui.core.addIf -data class TokenBalanceRowSizing( +sealed interface TokenBalanceStyle { + val textStyle: TextStyle + + data class Large(override val textStyle: TextStyle = TextStyle.Default) : TokenBalanceStyle + data class Pill(override val textStyle: TextStyle = TextStyle.Default) : TokenBalanceStyle +} + +enum class TokenSelectionStyle { + None, + Chevron, + Checkbox, + ; +} + +data class TokenBalanceRowStyling( val nameTextStyle: TextStyle, - val balanceTextStyle: TextStyle, + val balanceDisplayStyle: TokenBalanceStyle, val iconSize: Dp, val flagSize: Dp, + val selectionStyle: TokenSelectionStyle, ) @Composable -fun rememberTokenBalanceRowSizing( +fun rememberTokenBalanceRowStyling( nameTextStyle: TextStyle = CodeTheme.typography.screenTitle, - balanceTextStyle: TextStyle = CodeTheme.typography.screenTitle, + balanceDisplayStyle: TokenBalanceStyle = TokenBalanceStyle.Large(), iconSize: Dp = CodeTheme.dimens.staticGrid.x6, flagSize: Dp = CodeTheme.dimens.staticGrid.x3, -): TokenBalanceRowSizing = TokenBalanceRowSizing(nameTextStyle, balanceTextStyle, iconSize, flagSize) + selectionStyle: TokenSelectionStyle = TokenSelectionStyle.None, +): TokenBalanceRowStyling = + TokenBalanceRowStyling( + nameTextStyle = nameTextStyle, + balanceDisplayStyle = balanceDisplayStyle, + iconSize = iconSize, + flagSize = flagSize, + selectionStyle = selectionStyle + ) @Composable fun TokenBalanceRow( @@ -56,7 +84,7 @@ fun TokenBalanceRow( iconOverride: @Composable ((Any?) -> Any?) = { it }, formattedBalance: (Fiat) -> String = { it.formatted() }, horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween, - sizing: TokenBalanceRowSizing = rememberTokenBalanceRowSizing(), + styling: TokenBalanceRowStyling = rememberTokenBalanceRowStyling(), contentPadding: PaddingValues = PaddingValues(vertical = CodeTheme.dimens.inset), onClick: (() -> Unit)? = null, ) { @@ -72,7 +100,7 @@ fun TokenBalanceRow( showName = showName, showFlag = showFlag, showLogo = showLogo, - sizing = sizing, + styling = styling, horizontalArrangement = horizontalArrangement, contentPadding = contentPadding, onClick = onClick @@ -90,7 +118,7 @@ fun TokenBalanceRow( iconOverride: @Composable ((Any?) -> Any?) = { it }, formattedBalance: (Fiat) -> String = { it.formatted() }, horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween, - sizing: TokenBalanceRowSizing = rememberTokenBalanceRowSizing(), + styling: TokenBalanceRowStyling = rememberTokenBalanceRowStyling(), contentPadding: PaddingValues = PaddingValues(vertical = CodeTheme.dimens.inset), onClick: (() -> Unit)? = null, ) { @@ -104,7 +132,7 @@ fun TokenBalanceRow( showFlag = showFlag, isSelected = isSelected, modifier = modifier, - sizing = sizing, + styling = styling, iconOverride = iconOverride, formattedBalance = formattedBalance, horizontalArrangement = horizontalArrangement, @@ -127,7 +155,7 @@ fun TokenBalanceRow( iconOverride: @Composable ((Any?) -> Any?) = { it }, formattedBalance: (Fiat) -> String = { it.formatted() }, horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween, - sizing: TokenBalanceRowSizing = rememberTokenBalanceRowSizing(), + styling: TokenBalanceRowStyling = rememberTokenBalanceRowStyling(), contentPadding: PaddingValues = PaddingValues(vertical = CodeTheme.dimens.inset), onClick: (() -> Unit)? = null, ) { @@ -150,8 +178,8 @@ fun TokenBalanceRow( token = token, iconOverride = iconOverride, displayName = { displayName }, - imageSize = sizing.iconSize, - textStyle = sizing.nameTextStyle, + imageSize = styling.iconSize, + textStyle = styling.nameTextStyle, textColor = CodeTheme.colors.textMain, spacing = CodeTheme.dimens.grid.x2, ) @@ -162,11 +190,12 @@ fun TokenBalanceRow( is Painter -> Image( painter = image, contentDescription = null, - modifier = Modifier.size(sizing.iconSize), + modifier = Modifier.size(styling.iconSize), ) + else -> TokenIcon( image = image, - modifier = Modifier.size(sizing.iconSize) + modifier = Modifier.size(styling.iconSize) ) } } @@ -174,7 +203,7 @@ fun TokenBalanceRow( showName -> { Text( text = displayName, - style = sizing.nameTextStyle, + style = styling.nameTextStyle, color = CodeTheme.colors.textMain, ) } @@ -190,8 +219,8 @@ fun TokenBalanceRow( flag?.let { Image( modifier = Modifier - .height(sizing.flagSize) - .width(sizing.flagSize) + .height(styling.flagSize) + .width(styling.flagSize) .clip(CircleShape), painter = painterResource(it), contentDescription = "" @@ -199,24 +228,70 @@ fun TokenBalanceRow( } } - AnimatedNumberText( - value = formattedBalance(balance), - style = sizing.balanceTextStyle, - color = CodeTheme.colors.textMain, - ) - - if (isSelected != null) { - Image( - modifier = Modifier - .wrapContentWidth() - .padding(start = CodeTheme.dimens.grid.x3), - painter = painterResource( - if (isSelected) - R.drawable.ic_checked else R.drawable.ic_unchecked - ), - contentDescription = "" - ) + when (val displayStyle = styling.balanceDisplayStyle) { + is TokenBalanceStyle.Large -> { + val resolvedTextStyle = displayStyle.textStyle + .takeUnless { it == TextStyle.Default } + ?: CodeTheme.typography.screenTitle + + AnimatedNumberText( + value = formattedBalance(balance), + style = resolvedTextStyle, + color = CodeTheme.colors.textMain, + ) + } + + is TokenBalanceStyle.Pill -> { + val resolvedTextStyle = displayStyle.textStyle + .takeUnless { it == TextStyle.Default } + ?: CodeTheme.typography.caption + + Text( + modifier = Modifier + .padding(start = CodeTheme.dimens.grid.x1) + .border( + width = CodeTheme.dimens.border, + color = White20, + shape = CodeTheme.shapes.extraSmall, + ) + .padding( + horizontal = 4.dp, + vertical = 3.dp + ), + text = formattedBalance(balance), + color = CodeTheme.colors.textSecondary, + style = resolvedTextStyle, + ) + } + } + + when (styling.selectionStyle) { + TokenSelectionStyle.Chevron -> { + Icon( + modifier = Modifier.padding(start = CodeTheme.dimens.grid.x1), + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + tint = CodeTheme.colors.secondary, + ) + } + + TokenSelectionStyle.Checkbox -> { + if (isSelected != null) { + Image( + modifier = Modifier + .wrapContentWidth() + .padding(start = CodeTheme.dimens.grid.x3), + painter = painterResource( + if (isSelected) + R.drawable.ic_checked else R.drawable.ic_unchecked + ), + contentDescription = "" + ) + } + } + + TokenSelectionStyle.None -> Unit } } } -} \ No newline at end of file +} diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt index 1a2b148f5..a9b3fad8a 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt @@ -1,6 +1,5 @@ package com.flipcash.app.balance.internal -import androidx.compose.animation.animateBounds import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,11 +23,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.balance.internal.components.BalanceHeader -import com.flipcash.app.balance.internal.components.CashReservesRow import com.flipcash.app.core.AppRoute import com.flipcash.app.core.tokens.TokenPurpose import com.flipcash.app.theme.FlipcashPreview import com.flipcash.app.tokens.ui.SelectTokenViewModel +import com.flipcash.app.core.ui.rememberTokenBalanceRowStyling import com.flipcash.app.tokens.ui.TokenList import com.flipcash.features.balance.R import com.getcode.opencode.compose.ExchangeStub @@ -36,7 +35,6 @@ import com.getcode.opencode.compose.LocalExchange import com.getcode.opencode.model.financial.CurrencyCode import com.getcode.opencode.model.financial.Rate import com.getcode.theme.CodeTheme -import com.getcode.ui.core.addIf import com.getcode.ui.theme.ButtonState import com.getcode.ui.theme.CodeButton @@ -63,7 +61,7 @@ private fun BalanceScreenContent( TokenList( modifier = Modifier.weight(1f), itemModifier = { Modifier.animateItem(fadeInSpec = null) }, - includeReserves = true, + styling = rememberTokenBalanceRowStyling(), header = { BalanceHeader( modifier = Modifier diff --git a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt index 752e204b2..26249adc8 100644 --- a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt +++ b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt @@ -119,7 +119,6 @@ private fun DepositSelectTokenScreen() { tokens = state.tokens, selectedToken = state.selectedToken, showFlags = true, - includeReserves = true, onTokenSelected = { viewModel.dispatchEvent(SelectTokenViewModel.Event.OnTokenSelected(it.address)) }, ) } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt index 6d1d13070..ca0913470 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt @@ -17,6 +17,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.tokens.TokenPurpose +import com.flipcash.app.core.ui.TokenBalanceStyle +import com.flipcash.app.core.ui.TokenSelectionStyle +import com.flipcash.app.core.ui.rememberTokenBalanceRowStyling import com.flipcash.app.theme.FlipcashPreview import com.flipcash.app.tokens.ui.SelectTokenViewModel import com.flipcash.app.tokens.ui.TokenList @@ -43,9 +46,13 @@ private fun SelectTokenScreenContent( modifier = Modifier.fillMaxSize(), tokens = tokens, selectedToken = state.selectedToken, + styling = rememberTokenBalanceRowStyling( + balanceDisplayStyle = TokenBalanceStyle.Pill(), + selectionStyle = if (state.purpose is TokenPurpose.Select) TokenSelectionStyle.Checkbox else TokenSelectionStyle.Chevron, + ), showSelections = state.purpose is TokenPurpose.Select, showFlags = state.purpose !is TokenPurpose.Select, - includeReserves = state.purpose is TokenPurpose.Deposit || state.purpose is TokenPurpose.Withdraw, + includeReserves = state.purpose !is TokenPurpose.Select, emptyState = { Box( modifier = Modifier diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt index a70dc9dd9..9edff7496 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt @@ -21,7 +21,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.ui.ReceiptLineItem import com.flipcash.app.core.ui.TokenBalanceRow -import com.flipcash.app.core.ui.rememberTokenBalanceRowSizing +import com.flipcash.app.core.ui.TokenBalanceStyle +import com.flipcash.app.core.ui.rememberTokenBalanceRowStyling import com.flipcash.app.tokens.ui.SwapViewModel import com.flipcash.features.tokens.R import com.getcode.opencode.model.financial.Fiat @@ -166,8 +167,10 @@ private fun SellReceipt( showLogo = false, showFlag = true, horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - sizing = rememberTokenBalanceRowSizing( - balanceTextStyle = CodeTheme.typography.displaySmall.bolded(), + styling = rememberTokenBalanceRowStyling( + balanceDisplayStyle = TokenBalanceStyle.Large( + textStyle = CodeTheme.typography.displaySmall.bolded() + ), flagSize = CodeTheme.dimens.grid.x4, ), contentPadding = PaddingValues(0.dp), diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt index 272eabecd..dfbfcd8a7 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt @@ -124,7 +124,6 @@ private fun WithdrawalSelectTokenScreen() { tokens = state.tokens, selectedToken = state.selectedToken, showFlags = true, - includeReserves = true, onTokenSelected = { viewModel.dispatchEvent(SelectTokenViewModel.Event.OnTokenSelected(it.address)) }, ) } diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt index c52e9e099..8844df27b 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt @@ -23,7 +23,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.flipcash.app.core.ui.ReceiptLineItem import com.flipcash.app.core.ui.TokenBalanceRow -import com.flipcash.app.core.ui.rememberTokenBalanceRowSizing +import com.flipcash.app.core.ui.TokenBalanceStyle +import com.flipcash.app.core.ui.rememberTokenBalanceRowStyling import com.flipcash.app.theme.FlipcashPreview import com.flipcash.features.withdrawal.R import com.getcode.opencode.compose.ExchangeStub @@ -104,8 +105,10 @@ internal fun TransactionReceipt( amount.convertingToUsdIfNeeded(exchange.entryRate) .estimatedTokenAmountIn(tokenWithBalance.token, fractionDigits = 2) }, - sizing = rememberTokenBalanceRowSizing( - balanceTextStyle = CodeTheme.typography.displayMedium.bolded(), + styling = rememberTokenBalanceRowStyling( + balanceDisplayStyle = TokenBalanceStyle.Large( + textStyle = CodeTheme.typography.displayMedium.bolded() + ), ), contentPadding = PaddingValues(0.dp), ) diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenList.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenList.kt index 6203b9eb3..4c79db781 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenList.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenList.kt @@ -20,6 +20,10 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import com.flipcash.app.core.ui.TokenBalanceRow +import com.flipcash.app.core.ui.TokenBalanceRowStyling +import com.flipcash.app.core.ui.TokenBalanceStyle +import com.flipcash.app.core.ui.TokenSelectionStyle +import com.flipcash.app.core.ui.rememberTokenBalanceRowStyling import com.getcode.opencode.model.financial.Fiat import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token @@ -37,9 +41,13 @@ fun TokenList( modifier: Modifier = Modifier, itemModifier: LazyItemScope.() -> Modifier = { Modifier }, showFlags: Boolean = false, + styling: TokenBalanceRowStyling = rememberTokenBalanceRowStyling( + balanceDisplayStyle = TokenBalanceStyle.Pill(), + selectionStyle = TokenSelectionStyle.Chevron, + ), selectedToken: Mint? = null, showSelections: Boolean = false, - includeReserves: Boolean = false, + includeReserves: Boolean = true, pinFooter: Boolean = false, emptyState: (@Composable LazyItemScope.() -> Unit)? = null, reserves: (@Composable LazyItemScope.(mint: Mint, cashReserves: LocalFiat) -> Unit)? = null, @@ -97,6 +105,7 @@ fun TokenList( .then(itemModifier()), tokenWithBalance = item, showFlag = showFlags, + styling = styling, isSelected = (selectedToken == item.token.address).takeIf { showSelections }, ) { onTokenSelected(item.token) } @@ -149,4 +158,4 @@ fun TokenList( } } } -} \ No newline at end of file +}