diff --git a/apps/flipcash/core/src/main/res/drawable/ic_profile_picture.xml b/apps/flipcash/core/src/main/res/drawable/ic_profile_picture.xml
new file mode 100644
index 000000000..91ee83feb
--- /dev/null
+++ b/apps/flipcash/core/src/main/res/drawable/ic_profile_picture.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index 8ffe72ef8..573d62f2a 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -941,7 +941,7 @@
Enter username
This is how you\'ll be uniquely identified on the platform
Username
- Change Username
+ Username
Get a custom @username
@@ -1026,6 +1026,9 @@
My Flipcash Code
Couldn\'t Export
We were unable to export your tip code. Please try again
- Change Display Name
+
+ Display Name
+ Profile Picture
diff --git a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/MyAccountScreen.kt b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/MyAccountScreen.kt
index 87b2f4fe2..1c9f63c37 100644
--- a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/MyAccountScreen.kt
+++ b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/MyAccountScreen.kt
@@ -74,6 +74,21 @@ fun MyAccountScreen() {
}.launchIn(this)
}
+ LaunchedEffect(viewModel) {
+ viewModel.eventFlow
+ .filterIsInstance()
+ .onEach {
+ navigator.push(
+ AppRoute.UpdateUserProfile(
+ origin = AppRoute.Menu.MyAccount,
+ nameSource = DisplayNameSource.MyAccount,
+ includeName = false,
+ includePhoto = true,
+ )
+ )
+ }.launchIn(this)
+ }
+
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance()
diff --git a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountMenuItems.kt b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountMenuItems.kt
index e92da8175..ee26df945 100644
--- a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountMenuItems.kt
+++ b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountMenuItems.kt
@@ -21,14 +21,14 @@ import com.flipcash.features.myaccount.R
* Log Out, Delete Account) moved to Advanced, and the standalone App Settings screen folded its one
* surviving toggle (Require Biometrics) in here.
*
- * The row lands straight on the name step: [AppRoute.UpdateUserProfile] walks name then photo, and
- * this is only ever about the name, so it asks for that step alone.
+ * The row lands straight on the name step: [AppRoute.UpdateUserProfile] walks name, username then
+ * photo, and this is only ever about the name, so it asks for that step alone.
*/
internal data object ChangeDisplayName : FullMenuItem() {
override val icon: Painter
@Composable get() = rememberVectorPainter(Icons.Outlined.Badge)
override val name: String
- @Composable get() = stringResource(CoreR.string.title_changeDisplayName)
+ @Composable get() = stringResource(CoreR.string.title_displayName)
override val action: MyAccountScreenViewModel.Event =
MyAccountScreenViewModel.Event.OnChangeDisplayNameClicked
}
@@ -47,11 +47,25 @@ internal data object ChangeUsername : FullMenuItem() {
+ override val icon: Painter
+ @Composable get() = painterResource(CoreR.drawable.ic_profile_picture)
+ override val name: String
+ @Composable get() = stringResource(CoreR.string.title_profilePicture)
+ override val action: MyAccountScreenViewModel.Event =
+ MyAccountScreenViewModel.Event.OnProfilePictureClicked
+}
+
/**
* A toggle, not a destination — the screen renders a switch in its trailing slot and routes the tap
* through a biometric prompt. Its [action] is what a row tap dispatches, same as the switch.
@@ -73,8 +87,9 @@ internal data object Blocklist : FullMenuItem()
}
/**
- * Staff/beta only: the whole profile editor — contact methods, photo, name. The name on its own is
- * reachable by everyone through [ChangeDisplayName].
+ * Staff/beta only: the whole profile editor — contact methods, photo, name. What it adds over the
+ * rows above is the contact methods; the name and the photo are already reachable by everyone
+ * through [ChangeDisplayName] and [ProfilePicture].
*/
internal data object UserProfile : StaffMenuItem() {
override val icon: Painter
diff --git a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountScreenViewModel.kt b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountScreenViewModel.kt
index 96e0761cb..065e28731 100644
--- a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountScreenViewModel.kt
+++ b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/myaccount/MyAccountScreenViewModel.kt
@@ -23,6 +23,7 @@ import javax.inject.Inject
private val FullMenuList = buildList {
add(ChangeDisplayName)
add(ChangeUsername)
+ add(ProfilePicture)
add(RequireBiometrics)
add(Blocklist)
add(UserProfile)
@@ -75,6 +76,8 @@ internal class MyAccountScreenViewModel @Inject constructor(
data class OnUsernameClaimChanged(val claimed: Boolean) : Event
data object OnChangeUsernameClicked : Event
data object OnEditUsername : Event
+ data object OnProfilePictureClicked : Event
+ data object OnEditProfilePicture : Event
data object OnBlocklistClicked: Event
data object OnViewBlocklist: Event
data object OnContactMethodsClicked : Event
@@ -131,6 +134,12 @@ internal class MyAccountScreenViewModel @Inject constructor(
dispatchEvent(Event.OnEditUsername)
}.launchIn(viewModelScope)
+ eventFlow
+ .filterIsInstance()
+ .onEach {
+ dispatchEvent(Event.OnEditProfilePicture)
+ }.launchIn(viewModelScope)
+
eventFlow
.filterIsInstance()
.onEach {
@@ -165,6 +174,8 @@ internal class MyAccountScreenViewModel @Inject constructor(
Event.OnEditDisplayName,
Event.OnChangeUsernameClicked,
Event.OnEditUsername,
+ Event.OnProfilePictureClicked,
+ Event.OnEditProfilePicture,
Event.OnContactMethodsClicked,
Event.OnViewUserProfile,
Event.OnBlocklistClicked,
diff --git a/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/MyAccountScreenViewModelStateTest.kt b/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/MyAccountScreenViewModelStateTest.kt
index e95f15cbc..8b2aa5eca 100644
--- a/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/MyAccountScreenViewModelStateTest.kt
+++ b/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/MyAccountScreenViewModelStateTest.kt
@@ -4,6 +4,7 @@ import com.flipcash.app.myaccount.internal.myaccount.Blocklist
import com.flipcash.app.myaccount.internal.myaccount.ChangeDisplayName
import com.flipcash.app.myaccount.internal.myaccount.ChangeUsername
import com.flipcash.app.myaccount.internal.myaccount.MyAccountScreenViewModel
+import com.flipcash.app.myaccount.internal.myaccount.ProfilePicture
import com.flipcash.app.myaccount.internal.myaccount.RequireBiometrics
import com.flipcash.app.myaccount.internal.myaccount.UserProfile
import kotlin.test.Test
@@ -19,12 +20,29 @@ class MyAccountScreenViewModelStateTest {
reduce(MyAccountScreenViewModel.Event.OnUsernameClaimChanged(claimed = true))(state)
@Test
- fun `default state lists the display name, biometrics and blocklist`() {
+ fun `default state lists the display name, profile picture, biometrics and blocklist`() {
val state = MyAccountScreenViewModel.State()
- assertEquals(listOf(ChangeDisplayName, RequireBiometrics, Blocklist), state.items)
+ assertEquals(
+ listOf(ChangeDisplayName, ProfilePicture, RequireBiometrics, Blocklist),
+ state.items,
+ )
assertFalse(state.biometricsRequired)
}
+ @Test
+ fun `the profile picture row carries no condition`() {
+ val noBiometrics = reduce(
+ MyAccountScreenViewModel.Event.OnBiometricsSettingChanged(
+ required = false,
+ supported = false,
+ available = false,
+ )
+ )(MyAccountScreenViewModel.State())
+
+ assertTrue(noBiometrics.items.any { it is ProfilePicture })
+ assertTrue(claimed(noBiometrics).items.any { it is ProfilePicture })
+ }
+
@Test
fun `changing the username is offered only once a handle is claimed`() {
val unclaimed = MyAccountScreenViewModel.State()
@@ -35,7 +53,7 @@ class MyAccountScreenViewModelStateTest {
assertTrue(withHandle.usernameClaimed)
assertEquals(
- listOf(ChangeDisplayName, ChangeUsername, RequireBiometrics, Blocklist),
+ listOf(ChangeDisplayName, ChangeUsername, ProfilePicture, RequireBiometrics, Blocklist),
withHandle.items,
)
}
@@ -232,6 +250,8 @@ class MyAccountScreenViewModelStateTest {
MyAccountScreenViewModel.Event.OnEditDisplayName,
MyAccountScreenViewModel.Event.OnChangeUsernameClicked,
MyAccountScreenViewModel.Event.OnEditUsername,
+ MyAccountScreenViewModel.Event.OnProfilePictureClicked,
+ MyAccountScreenViewModel.Event.OnEditProfilePicture,
MyAccountScreenViewModel.Event.OnContactMethodsClicked,
MyAccountScreenViewModel.Event.OnViewUserProfile,
MyAccountScreenViewModel.Event.OnBlocklistClicked,