Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8b5ee89
feat(messenger): send cash to several members of a private group (#1846)
bmc08gt Oct 26, 2026
dc9590f
feat(payment-requests): model requests in the domain and hold them in…
bmc08gt Oct 25, 2026
656ee1f
feat(payment-requests): pay a request share under the server's intent id
bmc08gt Oct 25, 2026
c26f129
feat(chat): carry the payment request widget through decode, send, st…
bmc08gt Oct 25, 2026
65565e0
feat(chat): seal and send the payment request widget as a pending cha…
bmc08gt Oct 25, 2026
aef9059
feat(messenger): model a new payment request's people, deadline and s…
bmc08gt Oct 25, 2026
a40069c
feat(messenger): offer Send Money or Request Money from a private gro…
bmc08gt Oct 25, 2026
37113c5
feat(messenger): request money from a private group
bmc08gt Oct 25, 2026
ed7fac6
feat(chat): draw the payment request widget as a request card
bmc08gt Oct 25, 2026
ba59a8b
feat(messenger): poll a group chat's payment requests and draw their …
bmc08gt Oct 25, 2026
42da578
feat(messenger): show a group chat's payment requests as a deck in th…
bmc08gt Oct 25, 2026
db550ec
feat(messenger): add Request details with paying a share, closing and…
bmc08gt Oct 25, 2026
7904345
fix(messenger): match request screens to the design after a device pass
bmc08gt Oct 25, 2026
775175a
fix(messenger): match the group request spec on errors, Send, and copy
bmc08gt Oct 25, 2026
ca1dbcc
fix(payments): log a request share's intent id in full only once paid
bmc08gt Oct 25, 2026
8d3719e
fix(messenger): give Request Money's screens their own nav host
bmc08gt Oct 25, 2026
7202e0d
fix(messenger): present Request Money and Request details as full she…
bmc08gt Oct 25, 2026
7162075
fix(messenger): close Request Money and Request details with an X, as…
bmc08gt Oct 25, 2026
0c570ad
fix(messenger): collapse the deck from its scrim, share each card pie…
bmc08gt Oct 25, 2026
4c3fe87
fix(messenger): edit a request's name in the group name editor
bmc08gt Oct 25, 2026
ebb423e
fix(messenger): ask what the request is for on its Name screen
bmc08gt Oct 25, 2026
27a7a95
fix(messenger): space the options and close buttons on request details
bmc08gt Oct 25, 2026
1f4b44e
fix(messenger): fade the deck card's details in late and out first
bmc08gt Oct 25, 2026
fa5f7b3
fix(messenger): take request card and deck spacing from the grid
bmc08gt Oct 25, 2026
5950b1a
fix(messenger): fade the request deck in and out of the app bar
bmc08gt Oct 25, 2026
9837dc1
fix(messenger): put the expanded request deck over the long-press bac…
bmc08gt Oct 26, 2026
960cd03
fix(messenger): keep the request deck expanded under Request details
bmc08gt Oct 26, 2026
dac2a03
fix(messenger): combine Request details' options and close in one cap…
bmc08gt Oct 26, 2026
c02bd6b
fix(messenger): minimize the deck once Request details covers the chat
bmc08gt Oct 26, 2026
d4348fc
fix(messenger): carry the next request up with the swipe through the …
bmc08gt Oct 26, 2026
dc83a3a
fix(chats): preview a payment request in the chat list
bmc08gt Oct 26, 2026
e98fc4e
fix(chats): preview a request by who sent it and its title
bmc08gt Oct 26, 2026
bdafa51
fix(messenger): keep a deck title whole while the pill widens
bmc08gt Oct 26, 2026
b5fc19f
fix(messenger): swipe the deck like a stack of cards
bmc08gt Oct 26, 2026
313b5a9
fix(messenger): spring the deck width from release with the cards
bmc08gt Oct 26, 2026
7425042
feat(messenger): move the deck into a strip when the viewer owes
bmc08gt Oct 26, 2026
33b9d1d
fix(messenger): shrink the owed strip into the grey pill, drop auto-e…
bmc08gt Oct 26, 2026
5672a62
fix(messenger): keep the owed strip in the pill once scrolled away
bmc08gt Oct 26, 2026
329ed3a
feat(messenger): show the viewer's standing on deck cards
bmc08gt Oct 26, 2026
b8f7c98
feat(messenger): redesign the Time To Pay sheet
bmc08gt Oct 26, 2026
a627689
feat(messenger): show the Time To Pay deadline as a range
bmc08gt Oct 26, 2026
4a27b20
feat(messenger): show the Time row for presets and slide the deadline…
bmc08gt Oct 26, 2026
be0b08f
fix(messenger): turn a day black only as the sliding circle settles o…
bmc08gt Oct 26, 2026
2d6b37a
feat(messenger): put the month label inside the day cell on the 1st
bmc08gt Oct 26, 2026
382e015
fix(messenger): keep the day ripple inside the selection circle
bmc08gt Oct 26, 2026
c97836e
fix(messenger): run the deadline band square into the start and end c…
bmc08gt Oct 26, 2026
bce5e16
fix(messenger): animate Request Money's first push and show history a…
bmc08gt Oct 26, 2026
7489add
Merge remote-tracking branch 'origin/code/cash' into feat/group-requests
bmc08gt Oct 26, 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 @@ -3,6 +3,7 @@ package com.flipcash.app.core.chat
import android.os.Parcelable
import com.flipcash.services.models.chat.ChatId
import com.getcode.solana.keys.Mint
import com.getcode.navigation.FullscreenSheet
import com.getcode.navigation.HalfSheet
import com.getcode.navigation.Sheet
import com.getcode.navigation.WrapContentSheet
Expand Down Expand Up @@ -189,6 +190,28 @@ sealed interface ChatStep : FlowStep, Parcelable {
* sheet (node 10330:18091), opened from the pill row's "$". The sheet picks which reaction from
* the viewer's own. Short and fixed, so it wraps its content.
*/
/**
* Frames 03–06 — a private group's Request Money history, and the create flow behind its New
* Request button, on a back stack of its own. Presented as [ShareMessage] is, a full sheet
* with a close at its root; its steps are non-draggable, so the close is the only way out.
*/
@Parcelize
@Serializable
data class RequestMoney(val chatId: ChatId) : ChatStep, Sheet, FullscreenSheet

/**
* Frames 09, 11, 24 and 35 — one payment request in [chatId]. [requestId] is the request's id
* in base58, so the step stays a plain parcelable. A full sheet like [RequestMoney], closed
* only from its app bar; `ChatFlowScreen` turns the drag-to-dismiss off. [pay] opens it
* straight into paying the viewer's share, as the deck's Pay button does.
*/
@Parcelize
@Serializable
data class RequestDetails(val chatId: ChatId, val requestId: String, val pay: Boolean = false) :
ChatStep,
Sheet,
FullscreenSheet

@Parcelize
@Serializable
data class ReactionBoost(
Expand Down
153 changes: 153 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,159 @@
<string name="label_chat_preview_sentCash_bySender">%1$s sent %2$s</string>
<string name="label_chat_preview_tippedCash_bySender">%1$s tipped %2$s</string>
<string name="label_chat_preview_sharedProfile">Shared a profile</string>
<string name="label_paymentRequest_card">Request</string>
<string name="label_chat_preview_sentRequest">You sent request for %1$s</string>
<string name="label_chat_preview_sentRequest_bySender">%1$s sent request for %2$s</string>
<string name="label_chat_preview_sentRequest_untitled">You sent request</string>
<string name="label_chat_preview_sentRequest_untitled_bySender">%1$s sent request</string>
<string name="action_sendMoney">Send Money</string>
<string name="subtitle_sendMoney_group">Send money to someone in the group, or multiple people</string>
<string name="action_requestMoney">Request Money</string>
<string name="subtitle_requestMoney_group">Collect money from people in the group</string>
<!-- Request Money history and the create flow, group requests spec frames 03–06 and 25. -->
<string name="title_requestMoneyHistory">Request Money</string>
<string name="action_newRequest">New Request</string>
<string name="header_openRequests">Open Requests</string>
<string name="header_previousRequests">Previous Requests</string>
<string name="label_requestProgress">%1$s paid, %2$s requested</string>
<string name="label_requestCompleted">✓ %1$s paid</string>
<string name="title_amountPerPerson">Amount Per Person</string>
<string name="title_newRequest">New Request</string>
<string name="label_perPerson">per person</string>
<string name="label_requestName">Name</string>
<string name="title_requestName">What is the request for?</string>
<string name="label_requestPeople">People</string>
<string name="label_requestTiming">Timing</string>
<plurals name="label_requestDaysLeft">
<item quantity="one">%1$d day left to pay</item>
<item quantity="other">%1$d days left to pay</item>
</plurals>
<plurals name="label_requestHoursLeft">
<item quantity="one">%1$d hour left to pay</item>
<item quantity="other">%1$d hours left to pay</item>
</plurals>
<plurals name="label_requestMinutesLeft">
<item quantity="one">%1$d minute left to pay</item>
<item quantity="other">%1$d minutes left to pay</item>
</plurals>
<string name="label_requestPaidInFull">Paid in full</string>
<string name="label_requestClosed">Closed</string>
<string name="label_requestExpired">Expired</string>
<!-- The owed deck's time left: the status line without "to pay". -->
<plurals name="label_requestDaysLeftShort">
<item quantity="one">%1$d day left</item>
<item quantity="other">%1$d days left</item>
</plurals>
<plurals name="label_requestHoursLeftShort">
<item quantity="one">%1$d hour left</item>
<item quantity="other">%1$d hours left</item>
</plurals>
<plurals name="label_requestMinutesLeftShort">
<item quantity="one">%1$d minute left</item>
<item quantity="other">%1$d minutes left</item>
</plurals>
<!-- The owed deck: "You owe $20", then "You owe $20 · 2 days left". -->
<string name="label_requestYouOwe">You owe %1$s</string>
<string name="label_requestYouPaid">✓ You paid %1$s</string>
<string name="label_requestYourRequest">Your request</string>
<string name="cd_requestYouPaid">you paid %1$s</string>
<string name="cd_requestYouOwe">you owe %1$s</string>
<string name="cd_requestYourRequest">your request</string>
<string name="action_requestPay">Pay</string>
<string name="cd_requestPayShare">Pay %1$s</string>
<string name="cd_requestOwed">You owe %1$s for %2$s, %3$s</string>
<!-- The app-bar deck's front card: "$180 of $520". -->
<string name="label_requestDeckProgress">%1$s of %2$s</string>
<!-- Screen reader label for the deck: title, "<paid> of <requested> paid", status line. -->
<string name="cd_requestDeck">%1$s, %2$s of %3$s paid, %4$s</string>
<!-- As cd_requestDeck, with the viewer's standing ("you paid $5") after the title. -->
<string name="cd_requestDeckViewer">%1$s, %2$s, %3$s of %4$s paid, %5$s</string>
<plurals name="cd_requestDeckCount">
<item quantity="one">%1$d request</item>
<item quantity="other">%1$d requests</item>
</plurals>
<string name="action_nextRequest">Next request</string>
<!-- %1$s is the weekday in the device locale: "Saturday Request". -->
<string name="label_defaultRequestTitle">%1$s Request</string>
<string name="value_requestPeople_everyoneIncludingYou">Everyone, Including You</string>
<string name="value_requestPeople_everyoneExceptYou">Everyone Except You</string>
<string name="value_requestPeople_choose">Choose People</string>
<plurals name="value_requestPeople_someIncludingYou">
<item quantity="one">%1$d Person, Including You</item>
<item quantity="other">%1$d People, Including You</item>
</plurals>
<plurals name="value_requestPeople_some">
<item quantity="one">%1$d Person</item>
<item quantity="other">%1$d People</item>
</plurals>
<string name="subtitle_requestMarkedPaid">You\'ll be marked as paid</string>
<plurals name="value_requestDaysToPay">
<item quantity="one">%1$d Day To Pay</item>
<item quantity="other">%1$d Days To Pay</item>
</plurals>
<!-- %1$s is a medium date and short time in the device locale: "Until Nov 2, 6:00 PM". -->
<string name="value_requestUntil">Until %1$s</string>
<string name="subtitle_requestAutoCloses">Request automatically closes after</string>
<plurals name="action_requestFromPeople">
<item quantity="one">Request %2$s from %1$d Person</item>
<item quantity="other">Request %2$s from %1$d People</item>
</plurals>
<string name="label_selectPeople_you">You</string>
<string name="title_requestMaxPeople">100 People Max</string>
<string name="description_requestMaxPeople">A request can include up to 100 people.</string>
<string name="title_timeToPay">Time To Pay</string>
<string name="option_timeToPay_day">1 Day</string>
<string name="option_timeToPay_threeDays">3 Days</string>
<string name="option_timeToPay_week">1 Week</string>
<string name="option_timeToPay_twoWeeks">2 Weeks</string>
<string name="option_timeToPay_fourWeeks">4 Weeks</string>
<string name="option_timeToPay_custom">Custom</string>
<plurals name="description_timeToPayPreset">
<item quantity="one">%2$s · closes %1$d day after you send</item>
<item quantity="other">%2$s · closes %1$d days after you send</item>
</plurals>
<plurals name="description_timeToPayCustom">
<item quantity="one">Custom · %1$d day</item>
<item quantity="other">Custom · %1$d days</item>
</plurals>
<string name="description_timeToPayCustomToday">Custom · today</string>
<string name="label_timeToPayStarts">Starts</string>
<string name="value_timeToPayStarts">Today, on send</string>
<string name="label_timeToPayCloses">Closes</string>
<string name="value_timeToPayClosesAround">≈ %1$s</string>
<string name="label_timeToPayTime">Time</string>
<string name="title_timeToPaySelectTime">Select time</string>
<string name="cd_timeToPayKeyboard">Type a time</string>
<string name="cd_timeToPayDial">Pick a time on the dial</string>
<string name="action_timeToPayOk">OK</string>
<string name="title_sendRequestConfirm">Send Request?</string>
<string name="description_sendRequestConfirm">You will receive and hold the money collected</string>
<string name="action_sendRequest">Send Request</string>
<string name="error_title_cantSendRequest">Can\'t Send Request</string>
<string name="error_description_requestDenied">You can\'t request money in this group.</string>
<string name="error_description_requestEndsAt">Choose a deadline within the next 4 weeks.</string>
<string name="error_description_requestShares">Someone you included is no longer in the group. Check who\'s included and try again.</string>
<string name="error_description_requestCurrency">Requests aren\'t available in this currency yet.</string>
<!-- Request details, group requests spec frames 09–11, 24, 28, 29 and 33–35. -->
<string name="title_requestDetails">Request</string>
<string name="label_requestedBy">Requested by %1$s</string>
<string name="label_requestedByYou">Requested by you</string>
<string name="label_sharePaid">✓ Paid</string>
<string name="label_shareUnpaid">Hasn\'t paid yet</string>
<string name="label_shareUnpaidEnded">Didn\'t pay</string>
<!-- Under the member's name on the Mark as Paid sheet: "$20 · Hasn't paid yet". -->
<string name="label_shareUnpaidAmount">%1$s · Hasn\'t paid yet</string>
<string name="action_sendShare">Send %1$s</string>
<string name="cd_requestOptions">Request options</string>
<string name="cd_close">Close</string>
<string name="action_closeRequest">Close Request</string>
<string name="title_closeRequestConfirm">Close Request?</string>
<string name="description_closeRequestConfirm">This will stop payments. Anyone who hasn\'t paid will be marked Didn\'t pay. Payments already made won\'t be affected.</string>
<string name="action_markAsPaid">Mark as Paid</string>
<string name="title_markPaidConfirm">Mark %1$s as Paid?</string>
<string name="description_markPaidConfirm">Use this if %1$s paid you outside Flipcash. No money will be sent. This action cannot be reversed.</string>
<string name="error_title_requestClosed">Request Closed</string>
<string name="error_description_requestClosed">This request is no longer accepting payments.</string>
<string name="label_chat_preview_noMessages">Nothing yet</string>
<string name="label_isTyping">Is Typing…</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.material3.Text
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -42,6 +44,9 @@ import com.flipcash.app.messenger.internal.screens.ReactionPickerSheet
import com.flipcash.app.messenger.internal.screens.ReactorsSheet
import com.flipcash.app.messenger.internal.screens.ReactorsViewModel
import com.flipcash.app.messenger.internal.screens.ShareToChatsSubject
import com.flipcash.app.messenger.internal.request.ChatRequestsViewModel
import com.flipcash.app.messenger.internal.request.RequestDetailsScreen
import com.flipcash.app.messenger.internal.request.RequestMoneyScreen
import com.flipcash.app.messenger.internal.screens.boost.ReactionBoostAmountViewModel
import com.flipcash.app.messenger.internal.screens.boost.ReactionBoostCurrencySheet
import com.flipcash.app.messenger.internal.screens.boost.ReactionBoostSheet
Expand Down Expand Up @@ -82,11 +87,13 @@ import com.getcode.navigation.scenes.LocalSheetNavigator
import com.getcode.navigation.scenes.ModalBottomSheetSceneStrategy
import com.getcode.ui.components.AppBarDefaults
import com.getcode.ui.components.AppBarWithTitle
import com.getcode.ui.utils.LocalSheetOverhang
import com.getcode.ui.utils.rememberKeyboardController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import com.getcode.utils.base58

@Composable
fun ChatFlowScreen(
Expand Down Expand Up @@ -198,6 +205,13 @@ private fun chatEntryProvider(
val viewModel = flowSharedViewModel<ChatViewModel>()
val dismissSheet = LocalBottomSheetDismissDispatcher.current
val navigator = LocalCodeNavigator.current
// Closed by its X only, as Request details is: no flow host to carry NonDraggableRoute.
val sheetNavigator = LocalSheetNavigator.current
DisposableEffect(sheetNavigator) {
val previous = sheetNavigator?.sheetDragDisabled ?: false
sheetNavigator?.sheetDragDisabled = true
onDispose { sheetNavigator?.sheetDragDisabled = previous }
}
GroupSendDetailsSheet(
tokenName = step.tokenName,
tokenImageUrl = step.tokenImageUrl,
Expand All @@ -220,6 +234,48 @@ private fun chatEntryProvider(
)
}

annotatedEntry<ChatStep.RequestMoney> { step ->
val navigator = LocalCodeNavigator.current
val dismissSheet = LocalBottomSheetDismissDispatcher.current
RequestMoneyScreen(
chatId = step.chatId,
resultStateRegistry = resultStateRegistry,
onClose = { dismissSheet() },
onOpenScreen = { route -> navigator.navigate(route) },
)
}

annotatedEntry<ChatStep.RequestDetails> { step ->
val navigator = LocalCodeNavigator.current
val dismissSheet = LocalBottomSheetDismissDispatcher.current
// No inner flow host to carry a NonDraggableRoute up to the sheet, so turn the drag off
// here, as FlowHost would: the app bar's close is the only way out. The flag is the chat
// flow's, shared with a Request Money sheet underneath, so put back what was there.
val sheetNavigator = LocalSheetNavigator.current
DisposableEffect(sheetNavigator) {
val previous = sheetNavigator?.sheetDragDisabled ?: false
sheetNavigator?.sheetDragDisabled = true
onDispose { sheetNavigator?.sheetDragDisabled = previous }
}
// Opened from the expanded deck, which stays as it was while the sheet rises over it. Once
// the sheet is fully up, snap the deck minimized where nobody sees it, so coming back shows
// the chat with the deck in the app bar and no collapse playing as the sheet leaves.
val requests = flowSharedViewModel<ChatRequestsViewModel>()
val overhang = LocalSheetOverhang.current
LaunchedEffect(requests) {
snapshotFlow { overhang() }.first { it > 1f }
snapshotFlow { overhang() }.first { it <= 1f }
requests.setExpanded(false)
}
RequestDetailsScreen(
chatId = step.chatId,
requestId = step.requestId,
payOnOpen = step.pay,
onBack = { dismissSheet() },
onOpenScreen = { route -> navigator.navigate(route) },
)
}

annotatedEntry<ChatStep.ReactionPicker> { step ->
FlowReactionPickerScreen(step.messageId)
}
Expand Down Expand Up @@ -278,6 +334,15 @@ private fun FlowConversationScreen(
}
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<ChatViewModel.Event.NavigateToRequestMoney>()
.collect {
val chatId = viewModel.stateFlow.value.chatId ?: return@collect
keyboard.hideIfVisible { navigator.push(ChatStep.RequestMoney(chatId)) }
}
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<ChatViewModel.Event.OpenScreen>()
Expand Down
Loading
Loading