Skip to content

Commit afeba5f

Browse files
authored
feat(direct-send): wire contact selection -> amount entry -> send flow (#824)
SendFlowViewModel gains directTransfer orchestration to resolve contact, send, and show result. SendFlowScreen registers the AmountEntry step. ContactListScreen listens for NavigateToAmountEntry events. Adds SendStep.AmountEntry, SendResult.Sent, and new string resources. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent a123376 commit afeba5f

5 files changed

Lines changed: 299 additions & 14 deletions

File tree

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/send/SendStep.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.flipcash.app.core.send
22

33
import android.os.Parcelable
44
import com.getcode.navigation.flow.FlowStep
5+
import com.getcode.opencode.model.financial.Fiat
56
import kotlinx.parcelize.Parcelize
67
import kotlinx.serialization.Serializable
78

@@ -22,7 +23,18 @@ sealed interface SendStep : FlowStep, Parcelable {
2223
@Parcelize
2324
@Serializable
2425
data object ContactList : SendStep
26+
27+
@Parcelize
28+
@Serializable
29+
data class AmountEntry(
30+
val e164: String,
31+
val displayName: String,
32+
) : SendStep
2533
}
2634

2735
@Serializable
28-
sealed interface SendResult : Parcelable
36+
sealed interface SendResult : Parcelable {
37+
@Parcelize
38+
@Serializable
39+
data class Sent(val amount: Fiat) : SendResult
40+
}

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
<string name="subtitle_giveCashHint">Enter up to %1$s</string>
5353
<string name="subtitle_giveCashHintLimitExceeded">You can only give up to %1$s</string>
5454

55+
<string name="subtitle_sendHint">Enter up to %1$s</string>
56+
<string name="subtitle_sendHintLimitExceeded">You can only send up to %1$s</string>
57+
5558
<string name="error_title_sendLimitReached">Transaction Limit Reached</string>
5659
<string name="error_description_sendLimitReached">Flipcash is designed for small, every day transactions. Send limits reset daily</string>
5760

@@ -731,4 +734,14 @@
731734
<item quantity="other">%1$s Contacts Already On Flipcash</item>
732735
</plurals>
733736
<string name="prompt_description_contactsAlreadyOnFlipcash">Send them money, or invite other contacts to sign up for Flipcash</string>
737+
738+
<string name="error_title_contactNotOnFlipcash">Something Went Wrong</string>
739+
<string name="error_description_contactNotOnFlipcash">This contact isn\'t on Flipcash. Pick someone else to send cash</string>
740+
741+
<string name="prompt_title_fundsSentToContact">Cash Sent Successfully</string>
742+
<string name="prompt_description_fundsSentToContact">%1$s is on its way to %2$s</string>
743+
744+
<string name="error_title_cashFailedToSend">Something Went Wrong</string>
745+
<string name="error_description_cashFailedToSend">We were unable to send your cash. Please try again</string>
746+
<string name="action_swipeToSend">Swipe to Send</string>
734747
</resources>

apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,43 @@ package com.flipcash.app.directsend
33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.LaunchedEffect
55
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
69
import androidx.hilt.navigation.compose.hiltViewModel
710
import androidx.lifecycle.compose.collectAsStateWithLifecycle
811
import androidx.navigation3.runtime.NavEntry
912
import androidx.navigation3.runtime.NavKey
1013
import androidx.navigation3.runtime.entryProvider
14+
import com.flipcash.app.contacts.device.DeviceContact
15+
import com.flipcash.app.core.AppRoute
1116
import com.flipcash.app.core.send.SendResult
1217
import com.flipcash.app.core.send.SendStep
18+
import com.flipcash.app.core.tokens.TokenPurpose
19+
import com.flipcash.app.core.ui.ConfirmationStyle
20+
import com.flipcash.app.core.ui.TokenSelectionPill
1321
import com.flipcash.app.directsend.internal.SendFlowViewModel
22+
import com.flipcash.app.directsend.internal.screens.AmountEntryResult
23+
import com.flipcash.app.directsend.internal.screens.AmountEntryScreen
1424
import com.flipcash.app.directsend.internal.screens.ContactListScreen
1525
import com.flipcash.app.directsend.internal.screens.ContactsPermissionGateScreen
1626
import com.flipcash.app.directsend.internal.screens.PhoneGateLandingScreen
27+
import com.flipcash.features.directsend.R
28+
import com.getcode.manager.BottomBarManager
1729
import com.getcode.navigation.annotatedEntry
1830
import com.getcode.navigation.flow.FlowExitReason
1931
import com.getcode.navigation.flow.FlowHost
20-
import com.getcode.navigation.results.NavResultStateRegistry
32+
import com.getcode.navigation.flow.LocalOuterCodeNavigator
2133
import com.getcode.navigation.flow.flowSharedViewModel
34+
import com.getcode.navigation.flow.rememberFlowNavigator
35+
import com.getcode.navigation.results.NavResultStateRegistry
2236
import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher
37+
import com.getcode.opencode.model.financial.Fiat
38+
import com.getcode.solana.keys.PublicKey
39+
import com.getcode.util.resources.LocalResources
40+
import kotlinx.coroutines.flow.filterIsInstance
41+
import kotlinx.coroutines.flow.launchIn
42+
import kotlinx.coroutines.flow.onEach
2343

2444
@Composable
2545
fun SendFlowScreen(resultStateRegistry: NavResultStateRegistry) {
@@ -42,19 +62,107 @@ fun SendFlowScreen(resultStateRegistry: NavResultStateRegistry) {
4262
)
4363
}
4464

45-
private fun sendEntryProvider(): (NavKey) -> NavEntry<NavKey> = entryProvider {
46-
annotatedEntry<SendStep.PhoneGate> {
47-
SyncStep(it)
48-
PhoneGateLandingScreen()
65+
@Composable
66+
private fun sendEntryProvider(): (NavKey) -> NavEntry<NavKey> {
67+
return entryProvider {
68+
annotatedEntry<SendStep.PhoneGate> {
69+
SyncStep(it)
70+
PhoneGateLandingScreen()
71+
}
72+
annotatedEntry<SendStep.ContactsGate> {
73+
SyncStep(it)
74+
ContactsPermissionGateScreen()
75+
}
76+
annotatedEntry<SendStep.ContactList> {
77+
SyncStep(it)
78+
ContactListScreen()
79+
}
80+
annotatedEntry<SendStep.AmountEntry> { step ->
81+
SyncStep(step)
82+
SendAmountEntryScreen()
83+
}
4984
}
50-
annotatedEntry<SendStep.ContactsGate> {
51-
SyncStep(it)
52-
ContactsPermissionGateScreen()
85+
}
86+
87+
@Composable
88+
private fun SendAmountEntryScreen() {
89+
val flowNavigator = rememberFlowNavigator<SendStep, SendResult>()
90+
val sharedVm = flowSharedViewModel<SendFlowViewModel>()
91+
val sharedState by sharedVm.stateFlow.collectAsStateWithLifecycle()
92+
val resources = LocalResources.current
93+
94+
var contact by remember {
95+
mutableStateOf<DeviceContact?>(null)
5396
}
54-
annotatedEntry<SendStep.ContactList> {
55-
SyncStep(it)
56-
ContactListScreen()
97+
98+
var resolvedAuthority by remember {
99+
mutableStateOf<PublicKey?>(null)
57100
}
101+
102+
LaunchedEffect(Unit) {
103+
sharedVm.eventFlow
104+
.filterIsInstance<SendFlowViewModel.Event.ResolveCompleted>()
105+
.onEach {
106+
contact = it.contact
107+
resolvedAuthority = it.authority
108+
}
109+
.launchIn(this)
110+
}
111+
112+
LaunchedEffect(sharedVm) {
113+
sharedVm.eventFlow
114+
.filterIsInstance<SendFlowViewModel.Event.SendComplete>()
115+
.onEach { event ->
116+
BottomBarManager.showInfo(
117+
title = resources.getString(R.string.prompt_title_fundsSentToContact),
118+
message = resources.getString(
119+
R.string.prompt_description_fundsSentToContact,
120+
event.amount.formatted(rule = Fiat.FormattingRule.Truncated),
121+
contact?.displayName ?: "your selected recipient"
122+
),
123+
onDismiss = { flowNavigator.back() }
124+
)
125+
}.launchIn(this)
126+
}
127+
128+
AmountEntryScreen(
129+
title = { token ->
130+
TokenSelectionPill(token) {
131+
flowNavigator.navigate(
132+
AppRoute.Sheets.TokenSelection(TokenPurpose.Select)
133+
)
134+
}
135+
},
136+
// region lives outside the flow
137+
// flow replaces LocalCodeNavigator for the flow as the "inner" navigator
138+
navigator = LocalOuterCodeNavigator.current,
139+
canChangeCurrency = true,
140+
confirmationStyle = ConfirmationStyle.Slide,
141+
confirmationState = sharedState.sendProgress,
142+
onResult = { result ->
143+
when (result) {
144+
AmountEntryResult.Cancelled -> flowNavigator.back()
145+
is AmountEntryResult.Confirmed -> {
146+
val destination = resolvedAuthority
147+
if (destination == null) {
148+
BottomBarManager.showAlert(
149+
title = resources.getString(R.string.error_title_contactNotOnFlipcash),
150+
message = resources.getString(R.string.error_description_contactNotOnFlipcash),
151+
onDismiss = { flowNavigator.back() }
152+
)
153+
} else {
154+
sharedVm.dispatchEvent(
155+
SendFlowViewModel.Event.OnSendRequested(
156+
amount = result.amount,
157+
token = result.token,
158+
destinationOwner = destination,
159+
)
160+
)
161+
}
162+
}
163+
}
164+
},
165+
)
58166
}
59167

60168
@Composable

0 commit comments

Comments
 (0)