From aefe9b31d1b6ec747d62dcfed05884207d8909a4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 21 Aug 2026 15:39:09 -0400 Subject: [PATCH] feat(myaccount): add a Change Display Name row Node 9277:121893 puts Change Display Name at the top of My Account, above Require Biometrics. It was left out of the You-tab rebuild so it could land on its own, with the display name reachable only through the staff-gated User Profile screen in the meantime. The row opens AppRoute.UpdateUserProfile with includePhoto = false, so the flow shows the name step alone and returns to My Account on save or back rather than walking on to photo selection. --- .../flipcash/app/myaccount/MyAccountScreen.kt | 14 +++++++++++ .../internal/myaccount/MyAccountMenuItems.kt | 24 +++++++++++++++---- .../myaccount/MyAccountScreenViewModel.kt | 11 +++++++++ .../MyAccountScreenViewModelStateTest.kt | 23 ++++++++++++++++-- 4 files changed, 65 insertions(+), 7 deletions(-) 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 fbd595f07..89d9a683a 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 @@ -42,6 +42,20 @@ fun MyAccountScreen() { MyAccountScreen(viewModel) } + LaunchedEffect(viewModel) { + viewModel.eventFlow + .filterIsInstance() + .onEach { + navigator.push( + AppRoute.UpdateUserProfile( + origin = AppRoute.Menu.MyAccount, + includeName = true, + includePhoto = false, + ) + ) + }.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 79b55721e..682bdead5 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 @@ -2,12 +2,14 @@ package com.flipcash.app.myaccount.internal.myaccount import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ContactMail +import androidx.compose.material.icons.outlined.Badge import androidx.compose.material.icons.outlined.Block import androidx.compose.runtime.Composable 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 com.flipcash.app.core.AppRoute import com.flipcash.app.menu.FullMenuItem import com.flipcash.app.menu.StaffMenuItem import com.flipcash.core.R as CoreR @@ -18,9 +20,21 @@ 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. * - * Require Biometrics is 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. + * 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. + */ +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) + override val action: MyAccountScreenViewModel.Event = + MyAccountScreenViewModel.Event.OnChangeDisplayNameClicked +} + +/** + * 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. */ internal data object RequireBiometrics : FullMenuItem() { override val icon: Painter @@ -39,8 +53,8 @@ internal data object Blocklist : FullMenuItem() } /** - * Staff/beta only. Promoting this to an always-visible "Change Display Name" row is its own change; - * until then it stays where it was — behind the staff gate. + * Staff/beta only: the whole profile editor — contact methods, photo, name. The name on its own is + * reachable by everyone through [ChangeDisplayName]. */ 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 d1ee107b1..712ec602a 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 @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.onEach import javax.inject.Inject private val FullMenuList = buildList { + add(ChangeDisplayName) add(RequireBiometrics) add(Blocklist) add(UserProfile) @@ -58,6 +59,8 @@ internal class MyAccountScreenViewModel @Inject constructor( ) : Event /** Dispatched only after the screen's biometric prompt succeeds. */ data object OnBiometricsToggled : Event + data object OnChangeDisplayNameClicked : Event + data object OnEditDisplayName : Event data object OnBlocklistClicked: Event data object OnViewBlocklist: Event data object OnContactMethodsClicked : Event @@ -95,6 +98,12 @@ internal class MyAccountScreenViewModel @Inject constructor( ) }.launchIn(viewModelScope) + eventFlow + .filterIsInstance() + .onEach { + dispatchEvent(Event.OnEditDisplayName) + }.launchIn(viewModelScope) + eventFlow .filterIsInstance() .onEach { @@ -120,6 +129,8 @@ internal class MyAccountScreenViewModel @Inject constructor( val updateStateForEvent: (Event) -> ((State) -> State) = { event -> when (event) { Event.OnBiometricsToggled, + Event.OnChangeDisplayNameClicked, + Event.OnEditDisplayName, 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 a4bf19999..fb178e55e 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 @@ -1,6 +1,7 @@ package com.flipcash.app.myaccount.internal import com.flipcash.app.myaccount.internal.myaccount.Blocklist +import com.flipcash.app.myaccount.internal.myaccount.ChangeDisplayName import com.flipcash.app.myaccount.internal.myaccount.MyAccountScreenViewModel import com.flipcash.app.myaccount.internal.myaccount.RequireBiometrics import com.flipcash.app.myaccount.internal.myaccount.UserProfile @@ -14,12 +15,28 @@ class MyAccountScreenViewModelStateTest { private val reduce = MyAccountScreenViewModel.Companion.updateStateForEvent @Test - fun `default state lists biometrics and blocklist`() { + fun `default state lists the display name, biometrics and blocklist`() { val state = MyAccountScreenViewModel.State() - assertEquals(listOf(RequireBiometrics, Blocklist), state.items) + assertEquals(listOf(ChangeDisplayName, RequireBiometrics, Blocklist), state.items) assertFalse(state.biometricsRequired) } + @Test + fun `changing the display name is offered without the beta unlock`() { + val locked = MyAccountScreenViewModel.State() + assertTrue(locked.items.any { it is ChangeDisplayName }) + + val noBiometrics = reduce( + MyAccountScreenViewModel.Event.OnBiometricsSettingChanged( + required = false, + supported = false, + available = false, + ) + )(locked) + + assertTrue(noBiometrics.items.any { it is ChangeDisplayName }) + } + @Test fun `unsupported biometrics hides the row`() { val updated = reduce( @@ -142,6 +159,8 @@ class MyAccountScreenViewModelStateTest { val state = MyAccountScreenViewModel.State(biometricsRequired = true) val noOpEvents = listOf( MyAccountScreenViewModel.Event.OnBiometricsToggled, + MyAccountScreenViewModel.Event.OnChangeDisplayNameClicked, + MyAccountScreenViewModel.Event.OnEditDisplayName, MyAccountScreenViewModel.Event.OnContactMethodsClicked, MyAccountScreenViewModel.Event.OnViewUserProfile, MyAccountScreenViewModel.Event.OnBlocklistClicked,