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 6416f35189..83a3b9a93e 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 @@ -265,10 +265,28 @@ sealed interface AppRoute : NavKey, Parcelable { get() = listOf(DepositStep.UsdcInformational(showOtherOptions)) } + /** + * The withdraw flow. + * + * [preselectedMint] picks the entry step: `null` opens the currency picker (the v2 + * "Withdraw Money" tile and the v2 settings entry), Dollars/USDF detours through the + * "Withdraw as USDC" intro, and any other currency lands straight on the amount screen. + * + * [showOtherOptions] is the intro's legacy "Withdraw Other Flipcash Currencies" escape + * hatch — v1 only, since v2 reaches every currency through the picker. + */ @Serializable - data class Withdrawal(val showOtherOptions: Boolean = true) : Transfers, FlowRouteWithResult { + data class Withdrawal( + val showOtherOptions: Boolean = true, + val preselectedMint: Mint? = Mint.usdf, + ) : Transfers, FlowRouteWithResult { override val initialStack: List - get() = listOf(WithdrawalStep.UsdcInformational(showOtherOptions)) + get() = when (preselectedMint) { + null -> listOf(WithdrawalStep.SelectToken) + // The flow models USDF→USDC as a USDC withdrawal, so both mints mean the reserve. + Mint.usdf, Mint.usdc -> listOf(WithdrawalStep.UsdcInformational(showOtherOptions)) + else -> listOf(WithdrawalStep.Amount(preselectedMint)) + } } } diff --git a/apps/flipcash/core/src/main/res/drawable-xxhdpi/ic_coin_dollars.webp b/apps/flipcash/core/src/main/res/drawable-xxhdpi/ic_coin_dollars.webp new file mode 100644 index 0000000000..a6c603a9e8 Binary files /dev/null and b/apps/flipcash/core/src/main/res/drawable-xxhdpi/ic_coin_dollars.webp differ diff --git a/apps/flipcash/core/src/main/res/drawable/ic_arrow_right.xml b/apps/flipcash/core/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000000..cd8ece0038 --- /dev/null +++ b/apps/flipcash/core/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,13 @@ + + + + diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 141425cd1e..d251530f62 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -185,6 +185,8 @@ You should download Flipcash, a new way to send cash\n\n%1$s Enter Solana address + + Enter Address Where would you like to withdraw your %1$s to? Enter up to %1$s You can only give up to %1$s @@ -773,6 +775,8 @@ Withdraw as USDC Your USDF will be converted 1:1 to Solana USDC on withdrawal + + Your Dollars will be converted 1:1 to USDC on Solana Solana USDC USDC USDF diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt index 9b120b233e..9fb8ccc309 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt @@ -224,7 +224,13 @@ internal fun WalletScreenContent( text = stringResource(R.string.action_withdrawMoney), icon = painterResource(R.drawable.ic_menu_withdraw), ) { - dispatchEvent(WalletViewModel.Event.OpenScreen(AppRoute.Transfers.Withdrawal())) + // No preselected mint: the flow opens on the currency picker, which lists + // every balance (Dollars included). + dispatchEvent( + WalletViewModel.Event.OpenScreen( + AppRoute.Transfers.Withdrawal(preselectedMint = null) + ) + ) } } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/CurrencyInfoContentV2.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/CurrencyInfoContentV2.kt index 81e0a438c0..2bc03e946d 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/CurrencyInfoContentV2.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/CurrencyInfoContentV2.kt @@ -400,9 +400,14 @@ private fun CurrencyActionTiles( ) }, onClick = { + // Preselect the currency being viewed: Dollars detours through the + // "Withdraw as USDC" intro, anything else opens straight on the amount screen. dispatch( TokenInfoViewModel.Event.OpenScreen( - AppRoute.Transfers.Withdrawal(showOtherOptions = false) + AppRoute.Transfers.Withdrawal( + showOtherOptions = false, + preselectedMint = tokenMint, + ) ) ) }, diff --git a/apps/flipcash/features/withdrawal/build.gradle.kts b/apps/flipcash/features/withdrawal/build.gradle.kts index bb01cdd496..a2a6ac1bfc 100644 --- a/apps/flipcash/features/withdrawal/build.gradle.kts +++ b/apps/flipcash/features/withdrawal/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { testImplementation(libs.bundles.compose.ui.testing) implementation(project(":apps:flipcash:shared:amount-entry")) + implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:transaction-history")) implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:shared:tokens")) 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 e021550a1f..d757e0565b 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 @@ -20,6 +20,8 @@ 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.featureflags.FeatureFlag +import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.theme.FlipcashThemeWrapper import com.flipcash.app.tokens.ui.SelectTokenViewModel import com.flipcash.app.tokens.ui.TokenList @@ -39,6 +41,7 @@ import com.getcode.navigation.flow.rememberFlowNavigator import com.getcode.navigation.flow.rememberInitialStack 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 @@ -108,6 +111,8 @@ private fun WithdrawalSelectTokenScreen() { val flowNavigator = rememberFlowNavigator() val viewModel = hiltViewModel() val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val isNewUi by LocalFeatureFlags.current.observe(FeatureFlag.NewUi) + .collectAsStateWithLifecycle() Column( modifier = Modifier.fillMaxSize(), @@ -130,13 +135,21 @@ private fun WithdrawalSelectTokenScreen() { viewModel.dispatchEvent(SelectTokenViewModel.Event.OnPurposeChanged(TokenPurpose.Withdraw)) } - LaunchedEffect(viewModel) { + LaunchedEffect(viewModel, isNewUi) { viewModel.eventFlow .filterIsInstance() .filter { it.fromUser } .map { it.mint } .onEach { mint -> - flowNavigator.navigateTo(WithdrawalStep.Amount(mint)) + // v2 makes the picker the flow's entry, so Dollars has to detour through the + // "Withdraw as USDC" intro here — v1 reaches that intro first and only lands on the + // picker via its escape hatch, so a pick there always goes straight to the amount. + val isReserve = mint == Mint.usdf || mint == Mint.usdc + if (isNewUi && isReserve) { + flowNavigator.navigateTo(WithdrawalStep.UsdcInformational(showOtherOptions = false)) + } else { + flowNavigator.navigateTo(WithdrawalStep.Amount(mint)) + } }.launchIn(this) } } 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 016bf18200..4d1dd4b1a6 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 @@ -10,23 +10,24 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.withdrawal.WithdrawalResult import com.flipcash.app.core.withdrawal.WithdrawalStep +import com.flipcash.app.featureflags.FeatureFlag +import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.core.R import com.getcode.navigation.flow.rememberFlowNavigator import com.getcode.solana.keys.Mint import com.getcode.theme.CodeTheme -import com.getcode.theme.White20 import com.getcode.ui.components.AppBarWithTitle import com.getcode.ui.theme.ButtonState import com.getcode.ui.theme.CodeButton @@ -35,11 +36,15 @@ import com.getcode.ui.theme.CodeScaffold @Composable internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { val flowNavigator = rememberFlowNavigator() + val isNewUi by LocalFeatureFlags.current.observe(FeatureFlag.NewUi) + .collectAsStateWithLifecycle() CodeScaffold( topBar = { AppBarWithTitle( - title = stringResource(R.string.title_withdraw), + // v2 reaches this screen mid-flow (picker → Dollars), where the screen's own heading + // already names it; v1 enters here, so it keeps the "Withdraw" title. + title = if (isNewUi) "" else stringResource(R.string.title_withdraw), onBackIconClicked = { flowNavigator.back() }, titleAlignment = Alignment.CenterHorizontally, ) @@ -60,7 +65,9 @@ internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { flowNavigator.navigateTo(WithdrawalStep.Amount(Mint.usdc)) } - if (showOtherOptions) { + // The escape hatch is a v1 affordance: v2 enters the flow on the picker, so every + // other currency is already one step back. + if (showOtherOptions && !isNewUi) { CodeButton( modifier = Modifier .fillMaxWidth(), @@ -88,14 +95,18 @@ internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { - Image( - painter = painterResource(R.drawable.ic_withdraw_usdf_as_usdc), - contentDescription = null, - ) + if (isNewUi) { + ConversionGraphic() + } else { + Image( + painter = painterResource(R.drawable.ic_withdraw_usdf_as_usdc), + contentDescription = null, + ) + } } Column( - modifier = Modifier.fillMaxWidth(0.60f), + modifier = Modifier.fillMaxWidth(if (isNewUi) 0.80f else 0.60f), verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -105,7 +116,13 @@ internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { color = CodeTheme.colors.textMain, ) Text( - text = stringResource(R.string.description_withdrawUsdfAsUsdc), + text = stringResource( + if (isNewUi) { + R.string.description_withdrawDollarsAsUsdc + } else { + R.string.description_withdrawUsdfAsUsdc + } + ), style = CodeTheme.typography.textSmall, color = CodeTheme.colors.textSecondary, textAlign = TextAlign.Center, @@ -114,4 +131,34 @@ internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { } } } -} \ No newline at end of file +} + +/** + * v2 "Dollars → USDC" graphic (Figma node 9216:19798). The v1 art carried the Flipcash "F" mark on + * the left; v2 swaps in the gold Dollars coin, so the two halves are composed here rather than + * shipped as one flattened asset. + */ +@Composable +private fun ConversionGraphic() { + Row( + verticalAlignment = Alignment.CenterVertically, + // 14 dp between each pair, matching the 278x124 Figma frame's coin/arrow gaps. + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Image( + modifier = Modifier.size(100.dp), + painter = painterResource(R.drawable.ic_coin_dollars), + contentDescription = null, + ) + Image( + modifier = Modifier.size(28.dp), + painter = painterResource(R.drawable.ic_arrow_right), + contentDescription = null, + ) + // Natural size (111x112): the 100 dp coin plus the Solana badge overhanging its corner. + Image( + painter = painterResource(R.drawable.ic_usdc_on_solana), + contentDescription = null, + ) + } +} 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 45709489ef..a426673a4d 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 @@ -4,11 +4,15 @@ 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.featureflags.FeatureFlag +import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.withdrawal.WithdrawalViewModel import com.flipcash.app.withdrawal.internal.destination.WithdrawalDestinationScreen import com.flipcash.core.R @@ -23,12 +27,17 @@ import kotlinx.coroutines.flow.onEach internal fun WithdrawalDestinationScreen() { val flowNavigator = rememberFlowNavigator() val viewModel = flowSharedViewModel() + val isNewUi by LocalFeatureFlags.current.observe(FeatureFlag.NewUi) + .collectAsStateWithLifecycle() Column( modifier = Modifier.fillMaxSize(), ) { AppBarWithTitle( - title = stringResource(R.string.title_withdraw), + // v2 names each step; v1 keeps the flow-wide "Withdraw" title on every screen. + title = stringResource( + if (isNewUi) R.string.title_addressEntry else R.string.title_withdraw + ), titleAlignment = Alignment.CenterHorizontally, onBackIconClicked = { flowNavigator.back() }, ) diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalEntryScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalEntryScreen.kt index 4b1c20fa36..7e003587ae 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalEntryScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/WithdrawalEntryScreen.kt @@ -4,12 +4,16 @@ 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.AppRoute import com.flipcash.app.core.withdrawal.WithdrawalResult import com.flipcash.app.core.withdrawal.WithdrawalStep +import com.flipcash.app.featureflags.FeatureFlag +import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.withdrawal.WithdrawalViewModel import com.flipcash.app.withdrawal.internal.entry.WithdrawalEntryScreen import com.flipcash.core.R @@ -29,12 +33,17 @@ internal fun WithdrawalEntryScreen( val codeNavigator = LocalCodeNavigator.current val flowNavigator = rememberFlowNavigator() val viewModel = flowSharedViewModel() + val isNewUi by LocalFeatureFlags.current.observe(FeatureFlag.NewUi) + .collectAsStateWithLifecycle() Column( modifier = Modifier.fillMaxSize(), ) { AppBarWithTitle( - title = stringResource(R.string.title_withdraw), + // v2 names each step; v1 keeps the flow-wide "Withdraw" title on every screen. + title = stringResource( + if (isNewUi) R.string.title_amountToWithdraw else R.string.title_withdraw + ), onBackIconClicked = { flowNavigator.back() }, titleAlignment = Alignment.CenterHorizontally, )