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/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ dependencies {
implementation(project(":apps:flipcash:features:messenger"))
implementation(project(":apps:flipcash:features:invite"))
implementation(project(":apps:flipcash:features:discovery"))
implementation(project(":apps:flipcash:features:user-profile"))
implementation(project(":apps:flipcash:features:userflags"))

implementation(project(":libs:crypto:solana"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.flipcash.app.tokens.TokenSelectScreen

import com.flipcash.app.transactions.TransactionHistoryScreen
import com.flipcash.app.userflags.UserFlagsScreen
import com.flipcash.app.userprofile.UpdateUserProfileFlowScreen
import com.flipcash.app.withdrawal.WithdrawalFlowScreen
import com.getcode.navigation.AppNavHost
import com.getcode.navigation.NonDismissableRoute
Expand Down Expand Up @@ -122,6 +123,12 @@ fun appEntryProvider(
VerificationFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}

// User Profile Management
annotatedEntry<AppRoute.UpdateUserProfile> { key ->
UpdateUserProfileFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}


// Menu
annotatedEntry<AppRoute.Menu.AppSettings> { AppSettingsScreen() }
annotatedEntry<AppRoute.Menu.Lab> { key -> LabsScreen(onboarding = key.onboarding) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,33 @@ package com.flipcash.app.core
import android.os.Parcelable
import androidx.navigation3.runtime.NavKey
import com.flipcash.app.core.chat.ChatIdentifier
import com.flipcash.app.core.chat.ChatStep
import com.flipcash.app.core.deposit.DepositResult
import com.flipcash.app.core.deposit.DepositStep
import com.flipcash.app.core.onboarding.OnboardingStep
import com.flipcash.app.core.tokens.CurrencyCreatorResult
import com.flipcash.app.core.tokens.CurrencyCreatorStep
import com.flipcash.app.core.tokens.FundingSource
import com.flipcash.app.core.tokens.SwapPurpose
import com.flipcash.app.core.tokens.SwapResult
import com.flipcash.app.core.tokens.SwapStep
import com.flipcash.app.core.tokens.TokenPurpose
import com.flipcash.app.core.ui.flow.SteppedFlowRoute
import com.flipcash.app.core.userprofile.UpdateProfileResult
import com.flipcash.app.core.userprofile.UpdateProfileStep
import com.flipcash.app.core.verification.VerificationResult
import com.flipcash.app.core.verification.VerificationStep
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.flipcash.app.core.ui.flow.SteppedFlowRoute
import com.getcode.navigation.flow.FlowRoute
import com.getcode.navigation.flow.FlowRouteWithResult
import com.getcode.navigation.flow.FlowStep
import kotlin.reflect.KClass
import com.getcode.opencode.model.financial.Fiat
import com.getcode.solana.keys.Mint
import com.getcode.ui.core.RestrictionType
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlin.reflect.KClass

@Serializable
@Parcelize
Expand Down Expand Up @@ -138,6 +140,18 @@ sealed interface AppRoute : NavKey, Parcelable {
)
}

@Serializable
@Parcelize
data class UpdateUserProfile(
val origin: AppRoute,
val includeName: Boolean = true,
val includePhoto: Boolean = true,
val target: AppRoute? = null,
): AppRoute, FlowRouteWithResult<UpdateProfileResult> {
override val initialStack: List<NavKey>
get() = buildUpdateUserProfileStack(includeName, includePhoto)
}

@Serializable
@Parcelize
sealed interface Sheets : AppRoute {
Expand All @@ -155,6 +169,7 @@ sealed interface AppRoute : NavKey, Parcelable {
*/
@Serializable
data class Send(val resumed: Boolean = false): Sheets

@Serializable
data object Wallet : Sheets
@Serializable
Expand Down Expand Up @@ -300,3 +315,13 @@ private fun buildVerificationInitialStack(
}
return emptyList()
}

// Ordered list of the steps the flow should walk (via FlowNavigator.proceed()) — name first, then
// photo. In edit mode only the requested step(s) are included.
private fun buildUpdateUserProfileStack(
includeName: Boolean,
includePhoto: Boolean,
): List<NavKey> = buildList {
if (includeName) add(UpdateProfileStep.Name)
if (includePhoto) add(UpdateProfileStep.Photo)
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import com.getcode.ui.utils.heightOrZero
import com.getcode.ui.utils.widthOrZero

data class NavigationBarState(
val notificationUnreadCount: Int = 0,
val contactDmUnreadCount: Int = 0,
val showToast: Boolean = false,
val toastText: String? = null,
val isPaused: Boolean = false,
Expand Down Expand Up @@ -144,7 +144,7 @@ fun NavigationBar(
NavBarButton.Send -> BottomBarAction(
modifier = buttonModifier,
label = stringResource(R.string.action_send),
badgeCount = state.notificationUnreadCount,
badgeCount = state.contactDmUnreadCount,
painter = painterResource(R.drawable.ic_send_outlined),
onClick = { onButtonClick(NavBarButton.Send) }
)
Expand Down Expand Up @@ -301,7 +301,7 @@ private fun BottomBarAction(
@Composable
private fun NavigationBarPreview() {
NavigationBar(
state = NavigationBarState(notificationUnreadCount = 100),
state = NavigationBarState(contactDmUnreadCount = 100),
)
}
@Preview
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.flipcash.app.core.userprofile

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable

/**
* Result returned by the UpdateUserProfile flow to its caller.
*/

@Serializable
sealed interface UpdateProfileResult : Parcelable {
@Parcelize
@Serializable
data object Success : UpdateProfileResult

@Parcelize
@Serializable
data object Canceled : UpdateProfileResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.flipcash.app.core.userprofile

import android.os.Parcelable
import com.getcode.navigation.flow.FlowStep
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable

@Serializable
sealed interface UpdateProfileStep : FlowStep, Parcelable {
@Parcelize
@Serializable
object Name : UpdateProfileStep

@Parcelize
@Serializable
object Photo : UpdateProfileStep


}
9 changes: 9 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -854,4 +854,13 @@

<string name="label_amountToBuy">Amount to buy</string>
<string name="label_exchangeFee">Exchange fee</string>

<string name="title_profileNameSelection">What is your name?</string>
<string name="hint_profileName">Your Name</string>
<string name="error_title_profileNameNotAllowed">This Name is Not Allowed</string>
<string name="error_description_profileNameNotAllowed">Try a different name</string>
<string name="title_profileImageSelection">Upload Your Photo</string>
<string name="subtitle_profileImageSelection">CThis photo will be shown when receiving tips</string>
<string name="placeholder_profileDisplayName">Your Name</string>
<string name="subtitle_profileImageRecommended">500x500 Recommended</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.flipcash.services.models.chat.ChatType
import com.flipcash.services.models.chat.MessageContent
import com.flipcash.services.user.UserManager
import com.flipcash.shared.chat.ChatSummary
import com.flipcash.shared.chat.ui.ConversationReference
import com.getcode.opencode.model.core.ID
import com.getcode.opencode.model.financial.Token
import com.getcode.solana.keys.Mint
Expand Down Expand Up @@ -109,7 +110,7 @@ internal class ContactListBuilder @Inject constructor(
contact = contact,
isOnFlipcash = true,
lastActivity = summary.metadata.lastActivity,
conversation = Conversation(
conversation = ConversationReference(
chatId = chatId,
lastMessagePreview = formatPreview(summary, selfId, tokensByMint),
unreadCount = summary.unreadCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.flipcash.app.directsend.internal

import com.flipcash.app.core.contacts.DeviceContact
import com.flipcash.services.models.chat.ChatId
import com.flipcash.shared.chat.ui.ConversationReference
import kotlin.time.Instant

internal sealed interface ContactListItem {
Expand All @@ -22,14 +23,6 @@ internal sealed interface ContactListItem {
val contact: DeviceContact,
val isOnFlipcash: Boolean,
val lastActivity: Instant? = null,
val conversation: Conversation? = null,
val conversation: ConversationReference? = null,
) : ContactListItem
}

/** Presentation state derived from an existing DM with a contact. */
internal data class Conversation(
val chatId: ChatId,
val lastMessagePreview: String? = null,
val unreadCount: Int = 0,
val isTyping: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.permissions.PickedContact
import com.flipcash.app.tokens.TokenCoordinator
import com.flipcash.features.directsend.R
import com.flipcash.services.models.chat.ChatType
import com.flipcash.services.user.UserManager
import com.flipcash.shared.chat.ChatCoordinator
import com.getcode.manager.BottomBarManager
Expand Down Expand Up @@ -92,7 +93,7 @@ internal class SendFlowViewModel @Inject constructor(
featureFlags.observe(FeatureFlag.PhoneNumberSend),
featureFlags.observe(FeatureFlag.ContactPickerMode),
contactCoordinator.state,
chatCoordinator.feed,
chatCoordinator.feed(ChatType.CONTACT_DM),
) { userState, phoneNumberSendFlag, contactPickerMode, contactState, chats ->
val hasLinkedPhone = userState.userProfile?.verifiedPhoneNumber != null
val phoneNumberSendEnabled = phoneNumberSendFlag ||
Expand All @@ -119,7 +120,7 @@ internal class SendFlowViewModel @Inject constructor(
.map { it.searchState }
.distinctUntilChanged()
.flatMapLatest { snapshotFlow { it.text } },
chatCoordinator.feed,
chatCoordinator.feed(ChatType.CONTACT_DM),
tokenCoordinator.tokens,
) { contactState, searchText, chatFeed, tokens ->
contactListBuilder.build(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Search
Expand All @@ -36,39 +36,33 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.flipcash.app.contacts.ui.ContactAvatar
import com.flipcash.app.core.android.extensions.launchAppSettings
import com.flipcash.app.core.contacts.DeviceContact
import com.flipcash.app.directsend.internal.ContactListItem
import com.flipcash.app.directsend.internal.Conversation
import com.flipcash.app.permissions.ContactAccessHandle
import com.flipcash.app.theme.FlipcashThemeWrapper
import com.flipcash.features.directsend.R
import com.flipcash.services.models.chat.ChatId
import com.flipcash.shared.chat.ui.AnimatedConversationPaymentsPreview
import com.flipcash.shared.chat.ui.ConversationReference
import com.getcode.theme.CodeTheme
import com.getcode.theme.White10
import com.getcode.theme.extraLarge
import com.getcode.theme.extraSmall
import com.getcode.ui.core.verticalScrollStateGradient
import com.getcode.ui.theme.CodeButton
import com.getcode.util.formatLocalized
import com.getcode.util.permissions.PermissionResult
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
Expand Down Expand Up @@ -576,7 +570,7 @@ private fun ContactListPreview() {
contact = flipcashContacts[0].contact,
isOnFlipcash = true,
lastActivity = now - 5.minutes,
conversation = Conversation(
conversation = ConversationReference(
chatId = ChatId(byteArrayOf(1)),
lastMessagePreview = "Sent you $10.00",
unreadCount = 2,
Expand All @@ -587,7 +581,7 @@ private fun ContactListPreview() {
contact = flipcashContacts[1].contact,
isOnFlipcash = true,
lastActivity = now - 1.hours,
conversation = Conversation(
conversation = ConversationReference(
chatId = ChatId(byteArrayOf(2)),
lastMessagePreview = "See you soon",
isTyping = true,
Expand All @@ -598,7 +592,7 @@ private fun ContactListPreview() {
contact = flipcashContacts[2].contact,
isOnFlipcash = true,
lastActivity = now - 26.hours,
conversation = Conversation(
conversation = ConversationReference(
chatId = ChatId(byteArrayOf(3)),
lastMessagePreview = "You: Thanks!",
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,34 @@ fun UserProfileScreen() {
UserProfileScreenContent(state = state, dispatch = viewModel::dispatchEvent)
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<UserProfileViewModel.Event.EditNameClicked>()
.onEach {
navigator.push(
AppRoute.UpdateUserProfile(
origin = AppRoute.Menu.UserProfile,
includeName = true,
includePhoto = false,
)
)
}.launchIn(this)
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<UserProfileViewModel.Event.EditPhotoClicked>()
.onEach {
navigator.push(
AppRoute.UpdateUserProfile(
origin = AppRoute.Menu.UserProfile,
includeName = false,
includePhoto = true,
)
)
}.launchIn(this)
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<UserProfileViewModel.Event.NavigateToPhoneVerification>()
Expand Down
Loading
Loading