Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<WithdrawalResult> {
data class Withdrawal(
val showOtherOptions: Boolean = true,
val preselectedMint: Mint? = Mint.usdf,
) : Transfers, FlowRouteWithResult<WithdrawalResult> {
override val initialStack: List<NavKey>
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))
}
}
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions apps/flipcash/core/src/main/res/drawable/ic_arrow_right.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Figma "IconArrowRight" (node 9216:19801) — the separator between the two coins on the
"Withdraw as USDC" intro. Drawn at 33% white, matching the Figma group opacity. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.33"
android:fillType="evenOdd"
android:pathData="M16.3333 5.22928L25.1041 14L16.3333 22.7708L14.2709 20.7083L19.5209 15.4583H3.5V12.5417H19.5209L14.2709 7.29167L16.3333 5.22928Z" />
</vector>
4 changes: 4 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
<string name="message_invite_contact">You should download Flipcash, a new way to send cash\n\n%1$s</string>

<string name="title_enterAddress">Enter Solana address</string>
<!-- v2 app-bar title for the withdrawal destination step; title_enterAddress stays the field placeholder. -->
<string name="title_addressEntry">Enter Address</string>
<string name="subtitle_whereWithdrawTo">Where would you like to withdraw your %1$s to?</string>
<string name="subtitle_withdrawHint">Enter up to %1$s</string>
<string name="subtitle_withdrawHintLimitExceeded">You can only give up to %1$s</string>
Expand Down Expand Up @@ -773,6 +775,8 @@

<string name="title_withdrawUsdfAsUsdc">Withdraw as USDC</string>
<string name="description_withdrawUsdfAsUsdc">Your USDF will be converted 1:1 to Solana USDC on withdrawal</string>
<!-- v2 wording of the same screen: the reserve is branded "Dollars" and the chain is named last. -->
<string name="description_withdrawDollarsAsUsdc">Your Dollars will be converted 1:1 to USDC on Solana</string>
<string name="displayName_solanaUsdc">Solana USDC</string>
<string name="displayName_usdc">USDC</string>
<string name="displayName_usdf">USDF</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
)
)
},
Expand Down
1 change: 1 addition & 0 deletions apps/flipcash/features/withdrawal/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -108,6 +111,8 @@ private fun WithdrawalSelectTokenScreen() {
val flowNavigator = rememberFlowNavigator<WithdrawalStep, WithdrawalResult>()
val viewModel = hiltViewModel<SelectTokenViewModel>()
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val isNewUi by LocalFeatureFlags.current.observe(FeatureFlag.NewUi)
.collectAsStateWithLifecycle()

Column(
modifier = Modifier.fillMaxSize(),
Expand All @@ -130,13 +135,21 @@ private fun WithdrawalSelectTokenScreen() {
viewModel.dispatchEvent(SelectTokenViewModel.Event.OnPurposeChanged(TokenPurpose.Withdraw))
}

LaunchedEffect(viewModel) {
LaunchedEffect(viewModel, isNewUi) {
viewModel.eventFlow
.filterIsInstance<SelectTokenViewModel.Event.OnTokenSelected>()
.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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,11 +36,15 @@ import com.getcode.ui.theme.CodeScaffold
@Composable
internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) {
val flowNavigator = rememberFlowNavigator<WithdrawalStep, WithdrawalResult>()
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,
)
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
) {
Expand All @@ -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,
Expand All @@ -114,4 +131,34 @@ internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) {
}
}
}
}
}

/**
* 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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,12 +27,17 @@ import kotlinx.coroutines.flow.onEach
internal fun WithdrawalDestinationScreen() {
val flowNavigator = rememberFlowNavigator<WithdrawalStep, WithdrawalResult>()
val viewModel = flowSharedViewModel<WithdrawalViewModel>()
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() },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,12 +33,17 @@ internal fun WithdrawalEntryScreen(
val codeNavigator = LocalCodeNavigator.current
val flowNavigator = rememberFlowNavigator<WithdrawalStep, WithdrawalResult>()
val viewModel = flowSharedViewModel<WithdrawalViewModel>()
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,
)
Expand Down
Loading