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,