Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5b05128
feat(payments): add PaymentAction.Plain and Google Pay deposit-sheet …
bmc08gt Jul 7, 2026
3306652
refactor(amount-entry): model action label as AmountEntryLabel sealed…
bmc08gt Jul 7, 2026
bbf6d6c
feat(deposit): deposit-first "Add Money" UX groundwork
bmc08gt Jul 7, 2026
e6974f3
fix(swap): guard reserves branches against add-money purposes
bmc08gt Jul 7, 2026
1ee9919
feat(swap): add deposit-options navigation events
bmc08gt Jul 7, 2026
110ec03
feat(swap): route over-reserve token buys to add-money
bmc08gt Jul 7, 2026
54b8806
feat(currency-creator): add reserves-check and deposit-options events…
bmc08gt Jul 7, 2026
e91aa85
feat(currency-creator): gate token creation intro on USDF reserves
bmc08gt Jul 7, 2026
0baf897
feat(swap): route phantom deposits through amount entry
bmc08gt Jul 7, 2026
afb2250
feat(menu): flag-gate add-money and withdraw labels
bmc08gt Jul 7, 2026
ae38b90
fix(tokens): exclude USDF from giveable balance when GiveUsdf is off
bmc08gt Jul 7, 2026
9c4db30
feat(onramp): buy USDC via Coinbase for add-money deposits
bmc08gt Jul 7, 2026
61b4789
feat(onramp): make Coinbase email verification optional
bmc08gt Jul 7, 2026
049e4ee
fix(profile): properly handle NotFound error for GetProfile
bmc08gt Jul 8, 2026
751422d
feat: present contextual modal for chat/send for the user to purchase…
bmc08gt Jul 8, 2026
8126474
fix(chat): prevent duplicate event streams from concurrent open
bmc08gt Jul 8, 2026
0e82f81
chore(swap): await coinbase order processing before awaiting a balanc…
bmc08gt Jul 8, 2026
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 @@ -17,6 +17,7 @@ import com.flipcash.app.core.withdrawal.WithdrawalResult
import com.flipcash.app.core.withdrawal.WithdrawalStep
import com.flipcash.app.core.chat.ChatStep
import com.flipcash.app.core.onboarding.OnboardingStep
import com.flipcash.app.core.tokens.FundingSource
import com.getcode.navigation.flow.FlowRoute
import com.getcode.navigation.flow.FlowRouteWithResult
import com.getcode.opencode.model.financial.Fiat
Expand Down Expand Up @@ -181,7 +182,20 @@ sealed interface AppRoute : NavKey, Parcelable {
val popToRoot: Boolean = false,
) : Token, FlowRouteWithResult<SwapResult> {
override val initialStack: List<NavKey>
get() = listOf(SwapStep.Entry(purpose, initialAmount = shortfall))
get() = when (purpose) {
is SwapPurpose.Buy -> {
if (purpose.fundingSource == FundingSource.Phantom) {
// adding money (deposit) via phantom
listOf(SwapStep.PhantomConnect)
} else {
listOf(SwapStep.Entry(purpose, initialAmount = shortfall))
}
}
is SwapPurpose.Sell -> listOf(SwapStep.Entry(purpose, initialAmount = shortfall))
}



}

@Serializable
Expand Down
27 changes: 27 additions & 0 deletions apps/flipcash/core/src/main/res/drawable/ic_google_pay.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="62dp"
android:height="24dp"
android:viewportWidth="62"
android:viewportHeight="24">
<path
android:pathData="M28.743,11.673V18.749H26.482V1.275H32.476C33.994,1.275 35.29,1.778 36.35,2.784C37.433,3.79 37.974,5.018 37.974,6.468C37.974,7.953 37.433,9.181 36.35,10.175C35.302,11.17 34.006,11.661 32.476,11.661H28.743V11.673ZM28.743,3.427V9.521H32.523C33.418,9.521 34.171,9.216 34.76,8.62C35.361,8.023 35.667,7.298 35.667,6.48C35.667,5.673 35.361,4.959 34.76,4.363C34.171,3.743 33.429,3.439 32.523,3.439H28.743V3.427Z"
android:fillColor="#5F6368"/>
<path
android:pathData="M43.886,6.398C45.558,6.398 46.876,6.842 47.842,7.731C48.808,8.62 49.29,9.836 49.29,11.38V18.749H47.136V17.088H47.041C46.111,18.456 44.863,19.135 43.309,19.135C41.978,19.135 40.871,18.749 39.976,17.965C39.081,17.181 38.634,16.211 38.634,15.041C38.634,13.801 39.105,12.819 40.047,12.094C40.989,11.357 42.249,10.994 43.815,10.994C45.157,10.994 46.264,11.24 47.124,11.731V11.216C47.124,10.433 46.818,9.778 46.194,9.228C45.569,8.678 44.839,8.409 44.003,8.409C42.743,8.409 41.743,8.936 41.013,10L39.022,8.76C40.118,7.181 41.743,6.398 43.886,6.398ZM40.965,15.076C40.965,15.661 41.213,16.152 41.719,16.538C42.214,16.924 42.802,17.123 43.473,17.123C44.427,17.123 45.275,16.772 46.017,16.07C46.759,15.368 47.136,14.55 47.136,13.602C46.429,13.053 45.452,12.772 44.192,12.772C43.273,12.772 42.508,12.994 41.896,13.427C41.272,13.883 40.965,14.433 40.965,15.076Z"
android:fillColor="#5F6368"/>
<path
android:pathData="M61.584,6.784L54.048,24H51.716L54.519,17.977L49.549,6.784H52.011L55.59,15.368H55.637L59.123,6.784H61.584Z"
android:fillColor="#5F6368"/>
<path
android:pathData="M19.75,10.246C19.75,9.513 19.684,8.813 19.562,8.139H10.085V11.999L15.543,12C15.322,13.284 14.609,14.379 13.518,15.109V17.613H16.767C18.663,15.869 19.75,13.291 19.75,10.246Z"
android:fillColor="#4285F4"/>
<path
android:pathData="M13.519,15.109C12.615,15.715 11.45,16.069 10.088,16.069C7.456,16.069 5.223,14.308 4.424,11.933H1.073V14.516C2.733,17.788 6.145,20.034 10.088,20.034C12.813,20.034 15.102,19.144 16.768,17.612L13.519,15.109Z"
android:fillColor="#34A853"/>
<path
android:pathData="M4.108,10.018C4.108,9.351 4.22,8.706 4.424,8.101V5.518H1.073C0.386,6.871 0,8.399 0,10.018C0,11.636 0.387,13.164 1.073,14.517L4.424,11.935C4.22,11.329 4.108,10.684 4.108,10.018Z"
android:fillColor="#FABB05"/>
<path
android:pathData="M10.088,3.965C11.575,3.965 12.907,4.474 13.958,5.468L16.837,2.611C15.089,0.993 12.809,0 10.088,0C6.147,0 2.733,2.246 1.073,5.518L4.424,8.101C5.223,5.726 7.456,3.965 10.088,3.965Z"
android:fillColor="#E94235"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M160,320h640v-80L160,240v80ZM80,240q0,-33 23.5,-56.5T160,160h640q33,0 56.5,23.5T880,240v240L160,480v240h164v80L160,800q-33,0 -56.5,-23.5T80,720v-480ZM598,880 L428,710l56,-56 114,112 226,-226 56,58L598,880ZM160,240v480,-180 113,-413Z"
android:fillColor="#e8eaed"/>
</vector>
30 changes: 24 additions & 6 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@
<string name="action_deposit">Deposit</string>
<string name="title_deposit">Deposit</string>
<string name="title_depositFunds">Deposit Funds</string>
<string name="action_depositFunds">Deposit Funds</string>


<string name="action_addMoney">Add Money</string>
<string name="action_addMoreMoney">Add More Money</string>
<string name="title_withdraw">Withdraw</string>
<string name="title_withdrawFunds">Withdraw Funds</string>

Expand Down Expand Up @@ -187,6 +186,7 @@
<string name="error_title_invalidAddress">Invalid address</string>

<string name="action_withdraw">Withdraw</string>
<string name="action_withdrawMoney">Withdraw Money</string>
<string name="action_confirmWithdraw">Yes, Withdraw</string>
<string name="prompt_title_confirmWithdrawal">Are You Sure?</string>

Expand Down Expand Up @@ -241,6 +241,7 @@

<string name="action_addCash">Add Cash</string>
<string name="title_selectMethod">Select Method</string>
<string name="title_amountToAdd">Amount to Add</string>
<string name="title_amountToDeposit">Amount to Deposit</string>
<string name="title_addCashWithDebitCard">Add Cash with Debit Card</string>

Expand All @@ -254,7 +255,16 @@
<string name="title_tapAboveToAddCashToWallet">Tap above to Add Cash to your wallet</string>
<string name="title_tapBelowToAddCashWallet">You don\'t have any cash yet.\nTap below to add cash to your wallet</string>
<string name="title_noBalanceYet">No Balance Yet</string>
<string name="description_noBalanceYet">Deposit funds to get started</string>
<string name="title_noCommunityCurrenciesYet">No Community Currencies Yet</string>
<string name="description_noCommunityCurrenciesYet">Discover and buy a currency, or create your own</string>
<string name="title_insufficientBalance">Insufficient Balance</string>
<string name="description_insufficientBalanceToCreate">Add more money to create a currency</string>
<string name="description_insufficientBalanceToUse">Add more money, or enter a smaller amount</string>
<string name="description_noBalanceYetToGive">Add money to give cash</string>
<string name="description_noBalanceYetToBuy">Add money to buy currencies</string>
<string name="description_noBalanceYetToSend">Add money to send cash</string>
<string name="description_noBalanceYetForBalance">Add money to get started</string>
<string name="description_noBalanceYetToCreate">Add money to create a currency</string>
<string name="description_noBalanceYetDiscover">Buy your first currency to get started</string>
<string name="action_dismiss">Dismiss</string>
<string name="title_success">Success</string>
Expand Down Expand Up @@ -352,6 +362,8 @@
<string name="action_addPhoneNumber">Add Phone Number</string>
<string name="action_addEmailAddress">Add Email Address</string>
<string name="subtitle_linkedForPayments">Linked for payments</string>
<string name="label_verified">Verified</string>
<string name="label_unverified">Unverified</string>
<string name="prompt_title_unlinkPhone">Unlink Phone Number?</string>
<string name="prompt_description_unlinkPhone">Your phone number will be removed from your profile.</string>
<string name="prompt_title_unlinkEmail">Unlink Email Address?</string>
Expand Down Expand Up @@ -386,7 +398,7 @@
<string name="title_onrampProviderCoinbaseVirtual">Debit Card with Google Pay</string>
<string name="title_onrampProviderCoinbaseDebit">Debit Card</string>
<string name="title_onrampProviderCoinbaseCredit">Credit Card</string>
<string name="title_onrampProviderManualDeposit">Manual Deposit</string>
<string name="title_onrampProviderManualDeposit">Manual USDC Deposit</string>
<string name="title_onrampProviderBackpack">Backpack Wallet</string>
<string name="title_onrampProviderPhantom">Phantom Wallet</string>
<string name="title_onrampProviderSolflare">Solflare Wallet</string>
Expand Down Expand Up @@ -497,6 +509,8 @@
<string name="title_cashReserves">USDF</string>
<string name="action_buyMore">Buy More</string>

<string name="title_addMoney">Add Money</string>

<string name="title_amountToBuy">Amount to Buy</string>
<string name="title_amountToSell">Amount to Sell</string>
<string name="subtitle_buySellCashHint">Enter up to %1$s</string>
Expand All @@ -509,7 +523,7 @@
<string name="label_solanaUsdc">Solana USDC with</string>
<string name="title_sellToken">Sell %1$s</string>
<string name="title_purchasingToken">Purchasing %1$s</string>
<string name="title_depositingToken">Depositing %1$s</string>
<string name="title_addingMoney">Adding Money</string>
<string name="title_sellingToken">Selling %1$s</string>
<string name="label_sellWarning">Review the above before confirming.\nOnce made, your transaction is irreversible.</string>

Expand Down Expand Up @@ -730,6 +744,10 @@
<string name="title_buyWithPhantom">Buy With Phantom</string>
<string name="description_buyWithPhantom">Purchase using Solana USDC in Phantom. Simply connect your wallet and confirm the transaction</string>


<string name="title_addMoneyWithPhantom">Add Money With Phantom</string>
<string name="description_addMoneyWithPhantom">Add money using Solana USDC in Phantom. Your USDC will be automatically converted to USDF 1:1</string>

<string name="title_confirmation">Confirmation</string>
<string name="title_buyWithPhantomConnected">Connected</string>
<string name="description_buyWithPhantomConnected">Confirm the transaction in Phantom to continue</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ internal fun BalanceScreen(
val balanceState by viewModel.stateFlow.collectAsStateWithLifecycle()
val tokenState by tokenViewModel.stateFlow.collectAsStateWithLifecycle()
BalanceScreenContent(
depositFirstUx = balanceState.depositFirstUx,
addMoneyUx = balanceState.depositFirstUx,
tokenState = tokenState,
dispatchEvent = viewModel::dispatchEvent
)
}

@Composable
private fun BalanceScreenContent(
depositFirstUx: Boolean = false,
addMoneyUx: Boolean = false,
tokenState: SelectTokenViewModel.State,
dispatchEvent: (BalanceViewModel.Event) -> Unit
) {
Expand Down Expand Up @@ -100,8 +100,8 @@ private fun BalanceScreenContent(

Text(
modifier = Modifier.fillMaxWidth(0.6f),
text = if (depositFirstUx) {
stringResource(R.string.description_noBalanceYet)
text = if (addMoneyUx) {
stringResource(R.string.description_noBalanceYetForBalance)
} else {
stringResource(R.string.description_noBalanceYetDiscover)
},
Expand All @@ -118,8 +118,8 @@ private fun BalanceScreenContent(
.padding(top = CodeTheme.dimens.grid.x2)
.align(Alignment.CenterHorizontally),
contentPadding = PaddingValues(),
text = if (depositFirstUx) {
stringResource(R.string.action_depositFunds)
text = if (addMoneyUx) {
stringResource(R.string.action_addMoney)
} else {
stringResource(R.string.action_discoverCurrencies)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal class BalanceViewModel @Inject constructor(
}

init {
featureFlags.observe(FeatureFlag.DepositFirstUX)
featureFlags.observe(FeatureFlag.AddMoneyUX)
.onEach { dispatchEvent(Event.DepositFirstUxEnabled(it)) }
.launchIn(viewModelScope)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.flipcash.app.tokens.TokenCoordinator
import com.flipcash.features.cash.R
import com.flipcash.libs.coroutines.DispatcherProvider
import com.flipcash.shared.amountentry.AmountEntryDelegate
import com.flipcash.shared.amountentry.AmountEntryLabel
import com.flipcash.shared.amountentry.AmountEntryStyle
import com.getcode.manager.BottomBarAction
import com.getcode.manager.BottomBarManager
Expand Down Expand Up @@ -75,7 +76,7 @@ internal class CashScreenViewModel @Inject constructor(
exchange = exchange,
scope = viewModelScope,
style = AmountEntryStyle(
actionLabel = resources.getString(R.string.action_next),
actionLabel = AmountEntryLabel.Plain(resources.getString(R.string.action_next)),
infoHint = { resources.getString(R.string.subtitle_giveCashHint, it) },
overMaxHint = { resources.getString(R.string.subtitle_giveCashHintLimitExceeded, it) },
),
Expand Down
2 changes: 2 additions & 0 deletions apps/flipcash/features/contact-verification/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ dependencies {
implementation(project(":apps:flipcash:shared:analytics"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:phone"))
implementation(project(":apps:flipcash:shared:userflags"))

implementation(project(":libs:messaging"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,14 @@ fun EmailVerificationContent(
}
}.launchIn(this)
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<EmailVerificationViewModel.Event.OnEntrySaved>()
.onEach {
keyboard.hideIfVisible {
flowNavigator.exitWithResult(VerificationResult.Success)
}
}.launchIn(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.flipcash.app.core.verification.email.EmailCodeChannel
import com.flipcash.app.core.verification.email.EmailDeeplinkOrigin
import com.flipcash.app.core.extensions.onResult
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.getcode.opencode.utils.base64
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -45,6 +46,7 @@ class EmailVerificationViewModel @Inject constructor(
private val resources: ResourceHelper,
private val dispatchers: DispatcherProvider,
private val emailCodeChannel: EmailCodeChannel,
private val userFlags: UserFlagsCoordinator,
) : BaseViewModel<EmailVerificationViewModel.State, EmailVerificationViewModel.Event>(
initialState = State(),
updateStateForEvent = updateStateForEvent,
Expand All @@ -66,6 +68,8 @@ class EmailVerificationViewModel @Inject constructor(
data class OnOriginSet(val origin: EmailDeeplinkOrigin?) : Event
data class OnDataProvided(val email: String?, val code: String?) : Event
data object OnSendCodeClicked : Event
/** Emitted in skip-verification mode once the entered email is persisted locally. */
data object OnEntrySaved : Event
data object OnResendCodeClicked: Event
data class OnSendingCodeChanged(
val loading: Boolean = false,
Expand Down Expand Up @@ -111,11 +115,26 @@ class EmailVerificationViewModel @Inject constructor(

eventFlow
.filterIsInstance<Event.OnSendCodeClicked>()
.map {
.onEach {
val emailAddress = stateFlow.value.email.text.toString()
ContactMethod.Email(emailAddress, computeClientData())
}.onEach { handleSendVerificationCode(it) }
.launchIn(viewModelScope)
val requiresVerification = userFlags.resolvedFlags.value
.requireCoinbaseEmailVerification.effectiveValue

if (!requiresVerification) {
dispatchEvent(Event.OnSendingCodeChanged(loading = true))
viewModelScope.launch {
delay(1.seconds)
dispatchEvent(Event.OnSendingCodeChanged(success = true))
delay(1.seconds)
// Skip server verification: record the email locally as unverified
// and complete. The profile write is persisted by ProfileCoordinator.
verificationController.setLocalUnverified(ContactMethod.Email(emailAddress))
dispatchEvent(Event.OnEntrySaved)
}
} else {
handleSendVerificationCode(ContactMethod.Email(emailAddress, computeClientData()))
}
}.launchIn(viewModelScope)

eventFlow
.filterIsInstance<Event.OnResendCodeClicked>()
Expand Down Expand Up @@ -270,6 +289,7 @@ class EmailVerificationViewModel @Inject constructor(
val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
when (event) {
is Event.OnOriginSet -> { state -> state }
Event.OnEntrySaved -> { state -> state }
is Event.OnDataProvided -> { state ->
state.copy(
email = TextFieldState(event.email ?: state.email.text.toString()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import com.getcode.manager.BottomBarManager
import com.getcode.util.resources.FakeResourceHelper
import com.flipcash.app.core.MainCoroutineRule
import com.flipcash.app.core.dispatchers.TestDispatchers
import com.flipcash.app.userflags.FieldOverride
import com.flipcash.app.userflags.ResolvedFlag
import com.flipcash.app.userflags.ResolvedUserFlags
import com.flipcash.app.userflags.UserFlagsCoordinator
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
Expand All @@ -32,12 +38,21 @@ class EmailVerificationViewModelErrorTest {
private val verificationController: ContactVerificationController = mock()
private val profileController = mockk<ProfileController>(relaxed = true)
private val resources = FakeResourceHelper()
private val userFlags = mockk<UserFlagsCoordinator>(relaxed = true)

private lateinit var dispatchers: TestDispatchers

@Before
fun setUp() {
BottomBarManager.clear()
// Verification required → OnSendCodeClicked takes the server send path.
val resolvedFlags = mockk<ResolvedUserFlags>(relaxed = true) {
every { requireCoinbaseEmailVerification } returns ResolvedFlag(
serverValue = true,
override = FieldOverride.None,
)
}
every { userFlags.resolvedFlags } returns MutableStateFlow(resolvedFlags)
}

@After
Expand All @@ -52,6 +67,7 @@ class EmailVerificationViewModelErrorTest {
resources = resources,
dispatchers = dispatchers,
emailCodeChannel = EmailCodeChannel(),
userFlags = userFlags,
)
}

Expand Down
1 change: 1 addition & 0 deletions apps/flipcash/features/currency-creator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ android {
dependencies {
implementation(project(":apps:flipcash:features:bill-customization"))
implementation(project(":apps:flipcash:shared:bills"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:currency-creator"))
implementation(project(":apps:flipcash:shared:onramp:deeplinks"))
implementation(project(":apps:flipcash:shared:payments"))
Expand Down
Loading
Loading