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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddCircleOutline
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -75,6 +76,19 @@ sealed interface TutorialItem {
@Composable get() = painterResource(R.drawable.ic_nav_tipcard)
}

/**
* Accounts created before onboarding asked for a name have none, and chat shows them as
* "Flipcash User" until they set one.
*/
class DisplayName(override val isCompleted: Boolean) : Profile {
override val title: String
@Composable get() = stringResource(R.string.title_addDisplayName)
override val description: String
@Composable get() = stringResource(R.string.subtitle_addDisplayName)
override val icon: Painter
@Composable get() = rememberVectorPainter(Icons.Outlined.Edit)
}

class ProfilePicture(override val isCompleted: Boolean) : Profile {
override val title: String
@Composable get() = stringResource(R.string.title_addProfilePicture)
Expand Down Expand Up @@ -204,6 +218,7 @@ private fun PreviewFinishProfileEmpty() {
NewUserTutorial(
title = stringResource(R.string.title_finishYourProfile),
items = listOf(
TutorialItem.DisplayName(isCompleted = false),
TutorialItem.ProfilePicture(isCompleted = false),
TutorialItem.MinimumTip(),
),
Expand All @@ -218,6 +233,7 @@ private fun PreviewFinishProfilePhotoSet() {
NewUserTutorial(
title = stringResource(R.string.title_finishYourProfile),
items = listOf(
TutorialItem.DisplayName(isCompleted = true),
TutorialItem.ProfilePicture(isCompleted = true),
TutorialItem.MinimumTip(),
),
Expand Down
3 changes: 3 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,9 @@

<string name="title_finishYourProfile">Finish Your Profile</string>

<!-- Copy matches iOS's "Finish Your Profile" display-name step. -->
<string name="title_addDisplayName">Add your display name</string>
<string name="subtitle_addDisplayName">Choose the name people see in chat</string>
<string name="title_addProfilePicture">Add a profile picture</string>
<string name="subtitle_addProfilePicture">Select a photo from your gallery</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,9 @@ private fun YouHeader(
cardWidth = YouCardWidth,
enabled = enabled,
onClaim = onClaim,
profileTutorial = profileTutorial,
onSetProfilePicture = onSetProfilePicture,
onSetMinimumTip = onSetMinimumTip,
)
is TipCardState.Claimed -> ClaimedTipCard(
card = tipCardState.card,
Expand All @@ -448,6 +451,7 @@ private fun YouHeader(
usernameMinimumBalance = usernameMinimumBalance,
onClaimUsername = onClaimUsername,
profileTutorial = profileTutorial,
onSetDisplayName = onClaim,
onSetProfilePicture = onSetProfilePicture,
onSetMinimumTip = onSetMinimumTip,
)
Expand Down Expand Up @@ -484,6 +488,7 @@ private fun ClaimedTipCard(
usernameMinimumBalance: String,
onClaimUsername: () -> Unit,
profileTutorial: List<TutorialItem.Profile>?,
onSetDisplayName: () -> Unit,
onSetProfilePicture: () -> Unit,
onSetMinimumTip: () -> Unit,
) {
Expand Down Expand Up @@ -564,17 +569,12 @@ private fun ClaimedTipCard(
// 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),
ProfileTutorial(
items = profileTutorial,
) { item ->
when (item) {
is TutorialItem.ProfilePicture -> onSetProfilePicture()
is TutorialItem.MinimumTip -> onSetMinimumTip()
}
}

onSetDisplayName = onSetDisplayName,
onSetProfilePicture = onSetProfilePicture,
onSetMinimumTip = onSetMinimumTip,
)

// 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
Expand Down Expand Up @@ -648,6 +648,9 @@ private fun UnclaimedTipCardPrompt(
cardWidth: Dp,
enabled: Boolean,
onClaim: () -> Unit,
profileTutorial: List<TutorialItem.Profile>?,
onSetProfilePicture: () -> Unit,
onSetMinimumTip: () -> Unit,
blurEnabled: Boolean = HazeBlurDefaults.isBlurEnabledByDefault(),
) {
val shape = RoundedCornerShape(cardWidth * TipCardCornerFraction)
Expand Down Expand Up @@ -760,10 +763,47 @@ private fun UnclaimedTipCardPrompt(
}
}

// No link row or Share / Download to sit above here, so the checklist goes straight under
// the stand-in, with the display-name step first.
if (profileTutorial != null) {
ProfileTutorial(
modifier = Modifier
.padding(top = CodeTheme.dimens.grid.x6)
.padding(horizontal = CodeTheme.dimens.grid.x5),
items = profileTutorial,
// The same destination as "Start Receiving Tips": the name is what claims the card.
onSetDisplayName = onClaim,
onSetProfilePicture = onSetProfilePicture,
onSetMinimumTip = onSetMinimumTip,
)
}

Spacer(Modifier.height(UnclaimedRowsGap))
}
}

/** The "Finish Your Profile" checklist, drawn under whichever card the tab is showing. */
@Composable
private fun ProfileTutorial(
items: List<TutorialItem.Profile>,
onSetDisplayName: () -> Unit,
onSetProfilePicture: () -> Unit,
onSetMinimumTip: () -> Unit,
modifier: Modifier = Modifier,
) {
NewUserTutorial(
modifier = modifier.fillMaxWidth(),
title = stringResource(R.string.title_finishYourProfile),
items = items,
) { item ->
when (item) {
is TutorialItem.DisplayName -> onSetDisplayName()
is TutorialItem.ProfilePicture -> onSetProfilePicture()
is TutorialItem.MinimumTip -> onSetMinimumTip()
}
}
}

/** The tip card's height-to-width proportion and corner radius, mirrored from `TipCard`. */
private const val TipCardAspectRatio = 333f / 269f
private const val TipCardCornerFraction = 0.08f
Expand Down Expand Up @@ -978,6 +1018,10 @@ private fun Preview_UnclaimedTipCardPrompt() {
cardWidth = YouCardWidth,
enabled = true,
onClaim = {},
// What a nameless account sees: the display-name step first, nothing done yet.
profileTutorial = profileTutorialItems(UserProfile.Empty),
onSetProfilePicture = {},
onSetMinimumTip = {},
)
}

Expand All @@ -995,6 +1039,9 @@ private fun Preview_UnclaimedTipCardPrompt_NoBlur() {
cardWidth = YouCardWidth,
enabled = true,
onClaim = {},
profileTutorial = null,
onSetProfilePicture = {},
onSetMinimumTip = {},
blurEnabled = false,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import com.flipcash.services.models.UserProfile
* that already has a photo would otherwise flash an outstanding step on the way in. Null again once
* every step is done: a checklist with nothing left to do is just a row of ticks.
*
* Both steps read straight off the profile, so a step completed elsewhere — My Account's own
* Minimum Tip row, say — closes here too.
* Every step reads straight off the profile, so a step completed elsewhere — My Account's own
* Minimum Tip row, say — closes here too. An account that already has a name sees that step ticked
* rather than dropped, so the list keeps its length until the whole thing goes away.
*/
internal fun profileTutorialItems(profile: UserProfile?): List<TutorialItem.Profile>? {
profile ?: return null
val items = listOf(
// The same test the tab uses to choose the claimed card over the blurred stand-in.
TutorialItem.DisplayName(isCompleted = profile.displayName.isNotEmpty()),
TutorialItem.ProfilePicture(isCompleted = profile.profilePicture != null),
TutorialItem.MinimumTip(isCompleted = profile.minDmChatInitFee != null),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.flipcash.services.models.chat.MediaItem
import com.getcode.opencode.model.financial.Fiat
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

Expand All @@ -15,8 +16,12 @@ class ProfileTutorialTest {
// "a picture is set" without needing a mocking library in this module.
private val anyPicture = MediaItem(renditions = emptyList())

private fun profile(picture: MediaItem? = null, minimumTip: Fiat? = null) = UserProfile(
displayName = "Brandon",
private fun profile(
name: String = "Brandon",
picture: MediaItem? = null,
minimumTip: Fiat? = null,
) = UserProfile(
displayName = name,
socialAccounts = emptyList(),
phoneNumber = null,
email = null,
Expand All @@ -30,28 +35,56 @@ class ProfileTutorialTest {
}

@Test
fun `a bare profile leaves both steps outstanding`() {
val items = profileTutorialItems(profile())
assertEquals(2, items?.size)
fun `the steps run display name, picture, minimum tip`() {
val items = profileTutorialItems(profile(name = ""))
assertEquals(
listOf(
TutorialItem.DisplayName::class,
TutorialItem.ProfilePicture::class,
TutorialItem.MinimumTip::class,
),
items?.map { it::class },
)
}

@Test
fun `a nameless profile leaves every step outstanding`() {
val items = profileTutorialItems(profile(name = ""))
assertTrue(items!!.none { it.isCompleted })
}

@Test
fun `a profile with a picture completes only the picture step`() {
fun `a named profile shows the display name step ticked rather than dropping it`() {
val items = profileTutorialItems(profile())
assertEquals(3, items?.size)
assertEquals(1, items!!.count { it.isCompleted })
assertTrue(items.first { it is TutorialItem.DisplayName }.isCompleted)
}

@Test
fun `a picture completes the picture step`() {
val items = profileTutorialItems(profile(picture = anyPicture))
assertEquals(1, items?.count { it.isCompleted })
assertTrue(items!!.first { it is TutorialItem.ProfilePicture }.isCompleted)
assertFalse(items.first { it is TutorialItem.MinimumTip }.isCompleted)
}

@Test
fun `a saved minimum tip completes only the minimum tip step`() {
fun `a saved minimum tip completes the minimum tip step`() {
val items = profileTutorialItems(profile(minimumTip = Fiat(1.0)))
assertEquals(1, items?.count { it.isCompleted })
assertTrue(items!!.first { it is TutorialItem.MinimumTip }.isCompleted)
assertFalse(items.first { it is TutorialItem.ProfilePicture }.isCompleted)
}

@Test
fun `a picture and a minimum tip take the checklist away entirely`() {
fun `a name, a picture and a minimum tip take the checklist away entirely`() {
assertNull(profileTutorialItems(profile(picture = anyPicture, minimumTip = Fiat(1.0))))
}

@Test
fun `a nameless profile keeps the checklist even with the other two steps done`() {
val items = profileTutorialItems(
profile(name = "", picture = anyPicture, minimumTip = Fiat(1.0))
)
assertEquals(listOf(false, true, true), items?.map { it.isCompleted })
}
}
Loading