diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/NewUserTutorial.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/onboarding/NewUserTutorial.kt similarity index 52% rename from apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/NewUserTutorial.kt rename to apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/onboarding/NewUserTutorial.kt index c8a9f930cc..9e9eeeb807 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/NewUserTutorial.kt +++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/onboarding/NewUserTutorial.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.balance.internal.components +package com.flipcash.app.core.ui.onboarding import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -25,11 +25,23 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +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.util.fastForEach -import com.flipcash.features.balance.R +import com.flipcash.app.theme.FlipcashThemeWrapper +import com.flipcash.core.R import com.getcode.theme.CodeTheme +import com.getcode.theme.extraSmall +/** + * A row in a "what's left to do" checklist. + * + * Split by the screen that owns it: [Wallet] and [Profile] are drawn by different tabs and share + * nothing but the row layout, so each call site's `when` stays exhaustive over its own family and + * cannot be handed an item it has no branch for. + */ sealed interface TutorialItem { val title: String @Composable get @@ -39,17 +51,22 @@ sealed interface TutorialItem { @Composable get val isCompleted: Boolean - class AddMoney(override val isCompleted: Boolean) : TutorialItem { + /** The wallet tab's new-user milestones. */ + sealed interface Wallet : TutorialItem + + /** The "You" tab's profile-completion steps (node 9544:18140). */ + sealed interface Profile : TutorialItem + + class AddMoney(override val isCompleted: Boolean) : Wallet { override val title: String @Composable get() = stringResource(R.string.title_addMoney) override val description: String @Composable get() = stringResource(R.string.subtitle_addMoney) override val icon: Painter @Composable get() = rememberVectorPainter(Icons.Outlined.AddCircleOutline) - } - class ScanTipCard(override val isCompleted: Boolean) : TutorialItem { + class ScanTipCard(override val isCompleted: Boolean) : Wallet { override val title: String @Composable get() = stringResource(R.string.title_scanTipCard) override val description: String @@ -57,30 +74,55 @@ sealed interface TutorialItem { override val icon: Painter @Composable get() = painterResource(R.drawable.ic_nav_scan) } + + class ProfilePicture(override val isCompleted: Boolean) : Profile { + override val title: String + @Composable get() = stringResource(R.string.title_addProfilePicture) + override val description: String + @Composable get() = stringResource(R.string.subtitle_addProfilePicture) + override val icon: Painter + @Composable get() = painterResource(R.drawable.ic_people_circle) + } + + /** + * Drawn but inert. Nothing backs a user-set minimum tip yet: the amount comes from + * server-supplied regional presets, no field for it exists on the profile or the tip-card + * customization message, and iOS has no implementation either. The row is in the design, so + * it is drawn — and it never completes, which is the state node 9641:17019 shows. + */ + class MinimumTip(override val isCompleted: Boolean = false) : Profile { + override val title: String + @Composable get() = stringResource(R.string.title_setMinimumTip) + override val description: String + @Composable get() = stringResource(R.string.subtitle_setMinimumTip) + override val icon: Painter + @Composable get() = painterResource(R.drawable.ic_coins) + } } @Composable -fun NewUserTutorial( +fun NewUserTutorial( title: String, - items: List, + items: List, modifier: Modifier = Modifier, - onItemClicked: (TutorialItem) -> Unit, + onItemClicked: (T) -> Unit, ) { val completedCount = remember(items) { items.count { it.isCompleted } } - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset) - ) { + Column(modifier = modifier) { + // Node 9641:17024 pads the header on all four sides rather than putting a gap under it, so + // the space between the title and the box belongs to the header. Row( modifier = Modifier.fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), + .padding(CodeTheme.dimens.grid.x3), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = title, - style = CodeTheme.typography.screenTitle, + // Not `screenTitle`: FlipcashDesignSystem overrides it to 20sp/W500 app-wide, and + // node 9641:17026 asks for Avenir Demi at 18. + style = CodeTheme.typography.textMedium.copy(fontSize = 18.sp), color = CodeTheme.colors.textMain, ) @@ -91,10 +133,14 @@ fun NewUserTutorial( ) } + // Node 9641:17028: the box owns the padding and the rows sit flush inside it, spaced by + // the same step. Column( modifier = Modifier - .clip(CodeTheme.shapes.medium) - .background(color = Color.White.copy(0.05f)), + .clip(CodeTheme.shapes.extraSmall) + .background(color = Color.White.copy(0.05f)) + .padding(CodeTheme.dimens.grid.x3), + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), ) { items.fastForEach { item -> OnboardingItemRow( @@ -114,9 +160,8 @@ private fun OnboardingItemRow( modifier: Modifier = Modifier, onClick: () -> Unit, ) { - Row(modifier = modifier - .clickable(enabled = !item.isCompleted, onClick = onClick) - .padding(CodeTheme.dimens.inset), + Row( + modifier = modifier.clickable(enabled = !item.isCompleted, onClick = onClick), horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), ) { Image( @@ -146,10 +191,38 @@ private fun OnboardingItemRow( ) } Icon( - modifier = Modifier.align(Alignment.CenterVertically), + modifier = Modifier.align(Alignment.CenterVertically).size(16.dp), painter = painterResource(R.drawable.ic_chevron_right), tint = CodeTheme.colors.textSecondary, contentDescription = null ) } -} \ No newline at end of file +} + +@Preview(name = "Finish Your Profile — nothing done") +@PreviewWrapper(FlipcashThemeWrapper::class) +@Composable +private fun PreviewFinishProfileEmpty() { + NewUserTutorial( + title = stringResource(R.string.title_finishYourProfile), + items = listOf( + TutorialItem.ProfilePicture(isCompleted = false), + TutorialItem.MinimumTip(), + ), + onItemClicked = {}, + ) +} + +@Preview(name = "Finish Your Profile — photo set") +@PreviewWrapper(FlipcashThemeWrapper::class) +@Composable +private fun PreviewFinishProfilePhotoSet() { + NewUserTutorial( + title = stringResource(R.string.title_finishYourProfile), + items = listOf( + TutorialItem.ProfilePicture(isCompleted = true), + TutorialItem.MinimumTip(), + ), + onItemClicked = {}, + ) +} diff --git a/apps/flipcash/core/src/main/res/drawable/ic_people_circle.xml b/apps/flipcash/core/src/main/res/drawable/ic_people_circle.xml new file mode 100644 index 0000000000..ef9b32f04d --- /dev/null +++ b/apps/flipcash/core/src/main/res/drawable/ic_people_circle.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index dc07f4d0b1..8ffe72ef81 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -488,6 +488,7 @@ What do you want to call your currency? Currency Name Next + Save Upload Currency Icon Choose an image that represents your currency.\nIt will be displayed as a circular icon. 500x500 Recommended @@ -540,6 +541,14 @@ Scan a Tip Card Give your first tip + Finish Your Profile + + Add a profile picture + Select a photo from your gallery + + Set your minimum tip amount + Decide what size tip matters to you + Amount to Buy Amount to Sell Enter up to %1$s @@ -968,8 +977,7 @@ AI flagged this photo for impersonation. Please try a different photo. If you think the photo was rejected in error please DM @flipcash on X AI flagged this photo as misleading. Please try a different photo. If you think the photo was rejected in error please DM @flipcash on X AI flagged this photo as spam. Please try a different photo. If you think the photo was rejected in error please DM @flipcash on X - Upload Your Photo - This photo will be shown when receiving tips + Set Profile Picture Your Name 500x500 Recommended My Tip Card diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt index 9d774a1dd1..880cd53105 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt @@ -38,8 +38,8 @@ import kotlinx.coroutines.launch import com.flipcash.app.core.ui.AppreciationStyle import com.flipcash.app.core.ui.TokenCardStack import com.flipcash.app.balance.internal.components.BalanceHeader -import com.flipcash.app.balance.internal.components.NewUserTutorial -import com.flipcash.app.balance.internal.components.TutorialItem +import com.flipcash.app.core.ui.onboarding.NewUserTutorial +import com.flipcash.app.core.ui.onboarding.TutorialItem import com.flipcash.app.core.navigation.LocalTabBarPadding import com.flipcash.app.core.ui.TileButton import com.flipcash.app.core.ui.TileButtonStyle diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt index f416bd7f67..640dd2feca 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt @@ -3,7 +3,7 @@ package com.flipcash.app.balance.internal import androidx.lifecycle.viewModelScope import com.flipcash.app.analytics.Analytics import com.flipcash.app.analytics.FlipcashAnalyticsService -import com.flipcash.app.balance.internal.components.TutorialItem +import com.flipcash.app.core.ui.onboarding.TutorialItem import com.flipcash.app.core.AppRoute import com.flipcash.shared.transactionhistory.ActivityFeedCoordinator import com.flipcash.shared.transactionhistory.FeedSyncState @@ -53,7 +53,7 @@ internal class WalletViewModel @Inject constructor( * without waiting on the network. The tip milestone inside it is the one that needs a * server round-trip; [isTipMilestoneResolved] says whether it can be believed yet. */ - val onboardingItems: List? = null, + val onboardingItems: List? = null, /** * Whether [TutorialItem.ScanTipCard]'s answer is trustworthy. * @@ -111,7 +111,7 @@ internal class WalletViewModel @Inject constructor( sealed interface Event { data class OnOnboardingItemsUpdated( - val items: List, + val items: List, val holdsBalance: Boolean, val isTipMilestoneResolved: Boolean, ): Event diff --git a/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletLoadingStateTest.kt b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletLoadingStateTest.kt index 0a750bdbb2..c6a52c41d4 100644 --- a/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletLoadingStateTest.kt +++ b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletLoadingStateTest.kt @@ -1,6 +1,6 @@ package com.flipcash.app.balance.internal -import com.flipcash.app.balance.internal.components.TutorialItem +import com.flipcash.app.core.ui.onboarding.TutorialItem import com.flipcash.shared.transactionhistory.FeedSyncState import com.flipcash.shared.transactionhistory.TransactionAvatar import com.flipcash.shared.transactionhistory.TransactionListItem diff --git a/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletMilestoneGatingTest.kt b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletMilestoneGatingTest.kt index 2c10bdcc01..0a2461499a 100644 --- a/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletMilestoneGatingTest.kt +++ b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletMilestoneGatingTest.kt @@ -2,7 +2,7 @@ package com.flipcash.app.balance.internal import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.flipcash.app.analytics.StubFlipcashAnalytics -import com.flipcash.app.balance.internal.components.TutorialItem +import com.flipcash.app.core.ui.onboarding.TutorialItem import com.flipcash.app.core.MainCoroutineRule import com.flipcash.app.core.dispatchers.TestDispatchers import com.flipcash.app.funding.PurchaseMethodController diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt index f7e0557dbe..3862a6d6dd 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -77,6 +78,8 @@ import com.flipcash.app.core.navigation.LocalTabBarPadding import com.flipcash.app.menu.MenuList import com.flipcash.app.menu.internal.MenuScreenViewModel.Event import com.flipcash.app.menu.internal.MenuScreenViewModel.TipCardState +import com.flipcash.app.core.ui.onboarding.NewUserTutorial +import com.flipcash.app.core.ui.onboarding.TutorialItem import com.flipcash.app.menu.internal.components.UsernameProgress import com.flipcash.app.menu.internal.components.UsernameProgressCard import com.flipcash.app.theme.FlipcashThemeWrapper @@ -280,6 +283,10 @@ internal fun MenuScreenContent(viewModel: MenuScreenViewModel) { usernameProgress = state.usernameProgress, usernameMinimumBalance = state.usernameMinimumBalance, onClaimUsername = { viewModel.dispatchEvent(Event.ClaimUsername) }, + profileTutorial = state.profileTutorial, + onSetProfilePicture = { + viewModel.dispatchEvent(Event.SetProfilePicture) + }, ) }, footer = { @@ -407,6 +414,8 @@ private fun YouHeader( usernameProgress: UsernameProgress?, usernameMinimumBalance: String, onClaimUsername: () -> Unit, + profileTutorial: List?, + onSetProfilePicture: () -> Unit, ) { when (tipCardState) { TipCardState.Unknown -> Unit @@ -433,6 +442,8 @@ private fun YouHeader( usernameProgress = usernameProgress, usernameMinimumBalance = usernameMinimumBalance, onClaimUsername = onClaimUsername, + profileTutorial = profileTutorial, + onSetProfilePicture = onSetProfilePicture, ) } } @@ -466,6 +477,8 @@ private fun ClaimedTipCard( usernameProgress: UsernameProgress?, usernameMinimumBalance: String, onClaimUsername: () -> Unit, + profileTutorial: List?, + onSetProfilePicture: () -> Unit, ) { Column( modifier = Modifier.fillMaxWidth(), @@ -532,7 +545,7 @@ private fun ClaimedTipCard( modifier = slideAway.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(Modifier.height(CodeTheme.dimens.grid.x13)) + Spacer(Modifier.height(CodeTheme.dimens.grid.x6)) Column( modifier = Modifier @@ -540,6 +553,33 @@ private fun ClaimedTipCard( .padding(horizontal = CodeTheme.dimens.grid.x5), verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), ) { + // Node 9641:17031 puts the checklist directly under the caption, above the + // link row. Null while the profile is unresolved so it never draws against a + // guess. + if (profileTutorial != null) { + NewUserTutorial( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.title_finishYourProfile), + items = profileTutorial, + ) { item -> + when (item) { + is TutorialItem.ProfilePicture -> onSetProfilePicture() + // Inert: nothing backs a user-set minimum tip yet. + is TutorialItem.MinimumTip -> Unit + } + } + + + // Node 9641:17048 separates the checklist from the link row. The column + // already spaces siblings by 10dp; the rest of the 20dp gap on each side is + // the divider's own padding. + HorizontalDivider( + modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x2), + color = CodeTheme.colors.divider, + thickness = CodeTheme.dimens.border, + ) + } + if (link != null) { TipLinkRow(link = link, enabled = enabled, onCopy = onCopyLink) } diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt index c06d4902f0..979ed23017 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt @@ -12,6 +12,7 @@ import com.flipcash.app.core.bill.Scannable import com.flipcash.app.core.extensions.setText import com.flipcash.app.core.share.TipCodeExportFormat import com.flipcash.app.core.share.TipCodeExporter +import com.flipcash.app.core.ui.onboarding.TutorialItem import com.flipcash.app.core.util.Linkify import com.flipcash.app.featureflags.BetaFeature import com.flipcash.app.core.toast.SystemToastController @@ -106,6 +107,9 @@ internal class MenuScreenViewModel @Inject constructor( // The gate, formatted (e.g. `$100 USD`). Carried next to [usernameProgress] because both the // card's locked subtitle and the sheet behind its tap quote it. val usernameMinimumBalance: String = "", + // The "Finish Your Profile" checklist, or null while the profile is unresolved. Only ever + // drawn under a claimed card — see [ClaimedTipCard]. + val profileTutorial: List? = null, ) { /** The card to share, export or expand — only a claimed one qualifies. */ val tipCard: Scannable.TipCard? @@ -159,6 +163,10 @@ internal class MenuScreenViewModel @Inject constructor( val progress: UsernameProgress?, val minimumBalance: String, ) : Event + data class OnProfileTutorialChanged(val items: List?) : Event + + /** The checklist's photo row — opens the photo step of the profile flow on its own. */ + data object SetProfilePicture : Event /** The progress card's tap — claim a handle, or explain why it can't be claimed yet. */ data object ClaimUsername : Event @@ -272,6 +280,17 @@ internal class MenuScreenViewModel @Inject constructor( } .launchIn(viewModelScope) + // Gated on Ready for the same reason as the tip card: a named account restores its cached + // profile before auth completes, so the checklist would otherwise flash an outstanding + // photo step at someone who already has one. + userManager.state + .filter { it.authState is AuthState.Ready } + .map { it.userProfile } + .distinctUntilChanged() + .map { profileTutorialItems(it) } + .onEach { dispatchEvent(Event.OnProfileTutorialChanged(it)) } + .launchIn(viewModelScope) + // The username nudge. Gated on Ready for the same reason as the tip card: a named account // restores its cached profile before auth completes, so the card would otherwise flash for // someone who already holds a handle. @@ -365,6 +384,25 @@ internal class MenuScreenViewModel @Inject constructor( } .launchIn(viewModelScope) + eventFlow + .filterIsInstance() + .onEach { + dispatchEvent( + Event.OpenScreen( + AppRoute.UpdateUserProfile( + origin = AppRoute.Sheets.Menu, + nameSource = DisplayNameSource.MyAccount, + // Photo only: the account already has a name and a card by the time + // this checklist is drawn, so the flow reduces to the one step. + includeName = false, + includePhoto = true, + includeUsername = false, + ) + ) + ) + } + .launchIn(viewModelScope) + eventFlow .filterIsInstance() .mapNotNull { stateFlow.value.tipLink } @@ -520,10 +558,15 @@ internal class MenuScreenViewModel @Inject constructor( ) } + is Event.OnProfileTutorialChanged -> { state -> + state.copy(profileTutorial = event.items) + } + is Event.PresentDepositOptions, Event.CheckForUpdate, Event.ClaimTipCard, Event.ClaimUsername, + Event.SetProfilePicture, Event.ShareTipCard, Event.CopyTipLink, Event.DownloadTipCard, diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/ProfileTutorial.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/ProfileTutorial.kt new file mode 100644 index 0000000000..ae5925adf6 --- /dev/null +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/ProfileTutorial.kt @@ -0,0 +1,20 @@ +package com.flipcash.app.menu.internal + +import com.flipcash.app.core.ui.onboarding.TutorialItem +import com.flipcash.services.models.UserProfile + +/** + * The "Finish Your Profile" checklist for the "You" tab (node 9544:18140). + * + * Null while the profile is unresolved, so the card is never drawn against a guess — an account + * that already has a photo would otherwise flash an outstanding step on the way in. + * + * The minimum-tip step is always outstanding; see [TutorialItem.MinimumTip]. + */ +internal fun profileTutorialItems(profile: UserProfile?): List? { + profile ?: return null + return listOf( + TutorialItem.ProfilePicture(isCompleted = profile.profilePicture != null), + TutorialItem.MinimumTip(), + ) +} diff --git a/apps/flipcash/features/menu/src/test/kotlin/com/flipcash/app/menu/internal/ProfileTutorialTest.kt b/apps/flipcash/features/menu/src/test/kotlin/com/flipcash/app/menu/internal/ProfileTutorialTest.kt new file mode 100644 index 0000000000..f0000ca28b --- /dev/null +++ b/apps/flipcash/features/menu/src/test/kotlin/com/flipcash/app/menu/internal/ProfileTutorialTest.kt @@ -0,0 +1,49 @@ +package com.flipcash.app.menu.internal + +import com.flipcash.app.core.ui.onboarding.TutorialItem +import com.flipcash.services.models.UserProfile +import com.flipcash.services.models.chat.MediaItem +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ProfileTutorialTest { + + // MediaItem is a plain data class over a rendition list, so an empty one stands in for + // "a picture is set" without needing a mocking library in this module. + private val anyPicture = MediaItem(renditions = emptyList()) + + private fun profile(picture: MediaItem?) = UserProfile( + displayName = "Brandon", + socialAccounts = emptyList(), + phoneNumber = null, + email = null, + profilePicture = picture, + ) + + @Test + fun `an unresolved profile has no checklist`() { + assertNull(profileTutorialItems(profile = null)) + } + + @Test + fun `a profile without a picture leaves both steps outstanding`() { + val items = profileTutorialItems(profile(picture = null)) + assertEquals(2, items?.size) + assertTrue(items!!.none { it.isCompleted }) + } + + @Test + fun `a profile with a picture completes only the picture step`() { + val items = profileTutorialItems(profile(picture = anyPicture)) + assertEquals(1, items?.count { it.isCompleted }) + assertTrue(items!!.first { it is TutorialItem.ProfilePicture }.isCompleted) + } + + @Test + fun `the minimum tip step never completes`() { + val items = profileTutorialItems(profile(picture = anyPicture)) + assertTrue(items!!.none { it is TutorialItem.MinimumTip && it.isCompleted }) + } +} diff --git a/apps/flipcash/features/user-profile/src/main/kotlin/com/flipcash/app/userprofile/internal/photo/PhotoSelectionScreen.kt b/apps/flipcash/features/user-profile/src/main/kotlin/com/flipcash/app/userprofile/internal/photo/PhotoSelectionScreen.kt index 4cbbd69568..665d4b68e1 100644 --- a/apps/flipcash/features/user-profile/src/main/kotlin/com/flipcash/app/userprofile/internal/photo/PhotoSelectionScreen.kt +++ b/apps/flipcash/features/user-profile/src/main/kotlin/com/flipcash/app/userprofile/internal/photo/PhotoSelectionScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -67,6 +66,8 @@ internal fun PhotoSelectionScreen() { Column { AppBarWithTitle( + title = stringResource(R.string.title_setProfilePicture), + titleAlignment = Alignment.CenterHorizontally, onBackIconClicked = { keyboard.hideIfVisible { flowNavigator.back() @@ -101,29 +102,8 @@ private fun PhotoSelectionScreenContent( CodeScaffold( modifier = Modifier .padding(horizontal = CodeTheme.dimens.inset), - topBar = { - Column( - modifier = Modifier.fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x8), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), - ) { - Text( - text = stringResource(R.string.title_profileImageSelection), - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - ) - - Text( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset), - text = stringResource(R.string.subtitle_profileImageSelection), - style = CodeTheme.typography.textSmall, - textAlign = TextAlign.Center, - color = CodeTheme.colors.textSecondary, - ) - } - }, + // The app bar carries the title now; the body is just the photo and the name. + topBar = {}, bottomBar = { Column( modifier = Modifier.fillMaxWidth(), @@ -135,7 +115,7 @@ private fun PhotoSelectionScreenContent( .fillMaxWidth() .navigationBarsPadding() .padding(bottom = CodeTheme.dimens.grid.x3), - text = stringResource(R.string.action_next), + text = stringResource(R.string.action_save), enabled = state.image.isLoaded() && state.processingState.isIdle, isLoading = state.processingState.loading, isSuccess = state.processingState.success,