Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -39,48 +51,78 @@ 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
@Composable get() = stringResource(R.string.subtitle_scanTipCard)
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 <T : TutorialItem> NewUserTutorial(
title: String,
items: List<TutorialItem>,
items: List<T>,
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,
)

Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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
)
}
}
}

@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 = {},
)
}
21 changes: 21 additions & 0 deletions apps/flipcash/core/src/main/res/drawable/ic_people_circle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!-- IconPeopleCircle — "Finish Your Profile" checklist (node 9544:18148). White stroke, tinted
at the call site. The export carries a 50% group opacity; it is dropped here so the row's
own ColorFilter.tint controls the colour, matching the other tutorial icons. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.25 10C15.25 11.7949 13.7949 13.25 12 13.25C10.2051 13.25 8.75 11.7949 8.75 10C8.75 8.20507 10.2051 6.75 12 6.75C13.7949 6.75 15.25 8.20507 15.25 10Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:strokeLineJoin="round" />
<path
android:pathData="M18.143 18.9157C16.8294 16.9968 14.668 15.75 12 15.75C9.33203 15.75 7.17056 16.9968 5.85697 18.9157M18.143 18.9157C20.0491 17.2214 21.25 14.7509 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 14.7509 3.95086 17.2214 5.85697 18.9157M18.143 18.9157C16.5094 20.3679 14.3577 21.25 12 21.25C9.6423 21.25 7.49061 20.3679 5.85697 18.9157"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:strokeLineJoin="round" />
</vector>
12 changes: 10 additions & 2 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
<string name="title_currencyCreatorNameSelection">What do you want to call your currency?</string>
<string name="hint_currencyName">Currency Name</string>
<string name="action_next">Next</string>
<string name="action_save">Save</string>
<string name="title_currencyCreatorIconSelection">Upload Currency Icon</string>
<string name="subtitle_currencyCreatorIconSelection">Choose an image that represents your currency.\nIt will be displayed as a circular icon.</string>
<string name="subtitle_currencyCreatorIconRecommended">500x500 Recommended</string>
Expand Down Expand Up @@ -540,6 +541,14 @@
<string name="title_scanTipCard">Scan a Tip Card</string>
<string name="subtitle_scanTipCard">Give your first tip</string>

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

<string name="title_addProfilePicture">Add a profile picture</string>
<string name="subtitle_addProfilePicture">Select a photo from your gallery</string>

<string name="title_setMinimumTip">Set your minimum tip amount</string>
<string name="subtitle_setMinimumTip">Decide what size tip matters to you</string>

<string name="title_amountToBuy">Amount to Buy</string>
<string name="title_amountToSell">Amount to Sell</string>
<string name="subtitle_buySellCashHint">Enter up to %1$s</string>
Expand Down Expand Up @@ -968,8 +977,7 @@
<string name="error_description_profilePhotoNotAllowedFlaggedImpersonation">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</string>
<string name="error_description_profilePhotoNotAllowedFlaggedMisleading">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</string>
<string name="error_description_profilePhotoNotAllowedFlaggedSpam">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</string>
<string name="title_profileImageSelection">Upload Your Photo</string>
<string name="subtitle_profileImageSelection">This photo will be shown when receiving tips</string>
<string name="title_setProfilePicture">Set Profile Picture</string>
<string name="placeholder_profileDisplayName">Your Name</string>
<string name="subtitle_profileImageRecommended">500x500 Recommended</string>
<string name="title_myTipCard">My Tip Card</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<TutorialItem>? = null,
val onboardingItems: List<TutorialItem.Wallet>? = null,
/**
* Whether [TutorialItem.ScanTipCard]'s answer is trustworthy.
*
Expand Down Expand Up @@ -111,7 +111,7 @@ internal class WalletViewModel @Inject constructor(

sealed interface Event {
data class OnOnboardingItemsUpdated(
val items: List<TutorialItem>,
val items: List<TutorialItem.Wallet>,
val holdsBalance: Boolean,
val isTipMilestoneResolved: Boolean,
): Event
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading