diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index ba284a909..fed44faac 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -740,6 +740,7 @@
Cash Sent Successfully
%1$s is on its way to %2$s
+ your selected recipient
Something Went Wrong
We were unable to send your cash. Please try again
diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt
index 68ff1e143..60986dc11 100644
--- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt
+++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt
@@ -3,15 +3,11 @@ package com.flipcash.app.directsend
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
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.contacts.device.DeviceContact
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.send.SendResult
import com.flipcash.app.core.send.SendStep
@@ -35,7 +31,6 @@ import com.getcode.navigation.flow.rememberFlowNavigator
import com.getcode.navigation.results.NavResultStateRegistry
import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher
import com.getcode.opencode.model.financial.Fiat
-import com.getcode.solana.keys.PublicKey
import com.getcode.util.resources.LocalResources
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
@@ -91,34 +86,28 @@ private fun SendAmountEntryScreen() {
val sharedState by sharedVm.stateFlow.collectAsStateWithLifecycle()
val resources = LocalResources.current
- var contact by remember {
- mutableStateOf(null)
- }
-
- var resolvedAuthority by remember {
- mutableStateOf(null)
- }
-
- LaunchedEffect(Unit) {
- sharedVm.eventFlow
- .filterIsInstance()
- .onEach {
- contact = it.contact
- resolvedAuthority = it.authority
- }
- .launchIn(this)
+ LaunchedEffect(sharedState.resolveState) {
+ if (sharedState.resolveState is SendFlowViewModel.ResolveState.Failed) {
+ BottomBarManager.showAlert(
+ title = resources.getString(R.string.error_title_contactNotOnFlipcash),
+ message = resources.getString(R.string.error_description_contactNotOnFlipcash),
+ onDismiss = { flowNavigator.back() }
+ )
+ }
}
LaunchedEffect(sharedVm) {
sharedVm.eventFlow
.filterIsInstance()
.onEach { event ->
+ val displayName = sharedState.resolveState.contact?.displayName
+ ?: resources.getString(R.string.subtitle_yourSelectedRecipient)
BottomBarManager.showInfo(
title = resources.getString(R.string.prompt_title_fundsSentToContact),
message = resources.getString(
R.string.prompt_description_fundsSentToContact,
event.amount.formatted(rule = Fiat.FormattingRule.Truncated),
- contact?.displayName ?: "your selected recipient"
+ displayName,
),
onDismiss = { flowNavigator.back() }
)
@@ -143,21 +132,20 @@ private fun SendAmountEntryScreen() {
when (result) {
AmountEntryResult.Cancelled -> flowNavigator.back()
is AmountEntryResult.Confirmed -> {
- val destination = resolvedAuthority
- if (destination == null) {
- BottomBarManager.showAlert(
- title = resources.getString(R.string.error_title_contactNotOnFlipcash),
- message = resources.getString(R.string.error_description_contactNotOnFlipcash),
- onDismiss = { flowNavigator.back() }
- )
- } else {
- sharedVm.dispatchEvent(
- SendFlowViewModel.Event.OnSendRequested(
- amount = result.amount,
- token = result.token,
- destinationOwner = destination,
+ when (val resolve = sharedState.resolveState) {
+ is SendFlowViewModel.ResolveState.Resolved -> {
+ sharedVm.dispatchEvent(
+ SendFlowViewModel.Event.OnSendRequested(
+ amount = result.amount,
+ token = result.token,
+ destinationOwner = resolve.authority,
+ )
)
- )
+ }
+ // Resolve still in flight — slide resets, user can retry
+ is SendFlowViewModel.ResolveState.Pending -> Unit
+ // Failed case handled by LaunchedEffect above
+ else -> Unit
}
}
}
diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
index bfc8d6464..1fd1d71df 100644
--- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
+++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
@@ -61,6 +61,14 @@ internal class SendFlowViewModel @Inject constructor(
updateStateForEvent = updateStateForEvent,
) {
+ sealed interface ResolveState {
+ val contact: DeviceContact? get() = null
+ data object Idle : ResolveState
+ data class Pending(override val contact: DeviceContact) : ResolveState
+ data class Resolved(override val contact: DeviceContact, val authority: PublicKey) : ResolveState
+ data class Failed(override val contact: DeviceContact) : ResolveState
+ }
+
data class State @OptIn(ExperimentalMaterial3Api::class) constructor(
val steps: List = listOf(SendStep.ContactList),
val currentStep: SendStep? = null,
@@ -69,6 +77,7 @@ internal class SendFlowViewModel @Inject constructor(
val contactSyncState: LoadingSuccessState = LoadingSuccessState(),
val listItems: List = emptyList(),
val sendProgress: LoadingSuccessState = LoadingSuccessState(),
+ val resolveState: ResolveState = ResolveState.Idle,
)
sealed interface Event {
@@ -108,7 +117,6 @@ internal class SendFlowViewModel @Inject constructor(
val success: Boolean = false,
) : Event
data class SendComplete(val amount: Fiat) : Event
- data object ContactNotResolved : Event
}
init {
@@ -223,11 +231,16 @@ internal class SendFlowViewModel @Inject constructor(
.onEach { event -> contactCoordinator.removeContact(event.e164) }
.launchIn(viewModelScope)
- contactCoordinator.state
- .filter { it.hasDiscoveredFlipcashContacts && it.flipcashE164s.isNotEmpty() }
- .filter { stateFlow.value.currentStep is SendStep.ContactList }
+ combine(
+ contactCoordinator.state,
+ stateFlow.map { it.currentStep },
+ ) { contactState, currentStep ->
+ contactState to currentStep
+ }
+ .filter { (cs, _) -> cs.hasDiscoveredFlipcashContacts && cs.flipcashE164s.isNotEmpty() }
+ .filter { (_, step) -> step is SendStep.ContactList }
.take(1)
- .onEach { contactState ->
+ .onEach { (contactState, _) ->
val count = contactState.flipcashE164s.size
contactCoordinator.consumeContactsDiscovery()
BottomBarManager.showInfo(
@@ -283,15 +296,12 @@ internal class SendFlowViewModel @Inject constructor(
},
onFailure = { Result.failure(it) }
).onSuccess { amount ->
- timber.log.Timber.d("directTransfer success, dispatching checkmark")
dispatchEvent(Event.SendStateUpdated(success = true))
delay(400)
- timber.log.Timber.d("dispatching SendComplete")
dispatchEvent(
Dispatchers.Main,
Event.SendComplete(amount.localFiat.nativeAmount)
)
- timber.log.Timber.d("SendComplete dispatched")
}.onFailure {
dispatchEvent(Event.SendStateUpdated())
BottomBarManager.showError(
@@ -362,10 +372,21 @@ internal class SendFlowViewModel @Inject constructor(
is Event.OnItemsPopulated -> { state -> state.copy(listItems = event.items) }
is Event.OnContactClicked -> { state -> state }
is Event.SendInvite -> { state -> state }
- is Event.SendCashToContact -> { state -> state }
+ is Event.SendCashToContact -> { state ->
+ state.copy(resolveState = ResolveState.Pending(event.contact))
+ }
is Event.NavigateToAmountEntry -> { state -> state.copy(sendProgress = LoadingSuccessState()) }
- is Event.ResolveCompleted -> { state -> state }
- is Event.ResolveFailed -> { state -> state }
+ is Event.ResolveCompleted -> { state ->
+ state.copy(resolveState = ResolveState.Resolved(event.contact, event.authority))
+ }
+ is Event.ResolveFailed -> { state ->
+ val contact = state.resolveState.contact
+ if (contact != null) {
+ state.copy(resolveState = ResolveState.Failed(contact))
+ } else {
+ state
+ }
+ }
is Event.OnSendRequested -> { state -> state }
is Event.SendStateUpdated -> { state ->
state.copy(
@@ -376,7 +397,6 @@ internal class SendFlowViewModel @Inject constructor(
)
}
is Event.SendComplete -> { state -> state }
- Event.ContactNotResolved -> { state -> state }
}
}
}