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
1 change: 1 addition & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@

<string name="prompt_title_fundsSentToContact">Cash Sent Successfully</string>
<string name="prompt_description_fundsSentToContact">%1$s is on its way to %2$s</string>
<string name="subtitle_yourSelectedRecipient">your selected recipient</string>

<string name="error_title_cashFailedToSend">Something Went Wrong</string>
<string name="error_description_cashFailedToSend">We were unable to send your cash. Please try again</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -91,34 +86,28 @@ private fun SendAmountEntryScreen() {
val sharedState by sharedVm.stateFlow.collectAsStateWithLifecycle()
val resources = LocalResources.current

var contact by remember {
mutableStateOf<DeviceContact?>(null)
}

var resolvedAuthority by remember {
mutableStateOf<PublicKey?>(null)
}

LaunchedEffect(Unit) {
sharedVm.eventFlow
.filterIsInstance<SendFlowViewModel.Event.ResolveCompleted>()
.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<SendFlowViewModel.Event.SendComplete>()
.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() }
)
Expand All @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SendStep> = listOf(SendStep.ContactList),
val currentStep: SendStep? = null,
Expand All @@ -69,6 +77,7 @@ internal class SendFlowViewModel @Inject constructor(
val contactSyncState: LoadingSuccessState = LoadingSuccessState(),
val listItems: List<ContactListItem> = emptyList(),
val sendProgress: LoadingSuccessState = LoadingSuccessState(),
val resolveState: ResolveState = ResolveState.Idle,
)

sealed interface Event {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -376,7 +397,6 @@ internal class SendFlowViewModel @Inject constructor(
)
}
is Event.SendComplete -> { state -> state }
Event.ContactNotResolved -> { state -> state }
}
}
}
Expand Down
Loading