From af85eb08a96a75a442bbe96805247430189385f5 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 29 May 2026 15:42:26 -0400 Subject: [PATCH 1/2] refactor(ui): extract CircularIconButton and clean up TitleBar Extract the private CircularIconButton from AppBarDefaults into a shared top-level component with a (Dp) -> Unit content lambda. Update all call sites in TitleBar to use the new size parameter, migrate material2 to material3 imports in SearchInput, and adjust padding in RegionSelectionModalContent after SearchInput lost its built-in horizontal padding. Signed-off-by: Brandon McAnsh --- .../internal/RegionSelectionModalContent.kt | 4 +- .../ui/components/CircularIconButton.kt | 40 +++++++++++ .../com/getcode/ui/components/SearchInput.kt | 7 +- .../com/getcode/ui/components/TitleBar.kt | 67 +++++-------------- 4 files changed, 63 insertions(+), 55 deletions(-) create mode 100644 ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt diff --git a/apps/flipcash/shared/currency-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/RegionSelectionModalContent.kt b/apps/flipcash/shared/currency-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/RegionSelectionModalContent.kt index 299be1886..c3a60287c 100644 --- a/apps/flipcash/shared/currency-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/RegionSelectionModalContent.kt +++ b/apps/flipcash/shared/currency-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/RegionSelectionModalContent.kt @@ -54,7 +54,9 @@ internal fun RegionSelectionModalContent(viewModel: CurrencyViewModel) { .launchIn(this) } SearchInput( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x3), + modifier = Modifier + .padding(horizontal = CodeTheme.dimens.grid.x3) + .padding(top = CodeTheme.dimens.grid.x3), state = state.searchState, contentPadding = PaddingValues(start = CodeTheme.dimens.grid.x1), placeholder = stringResource(R.string.subtitle_searchRegions) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt new file mode 100644 index 000000000..e508552a7 --- /dev/null +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt @@ -0,0 +1,40 @@ +package com.getcode.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.getcode.ui.components.AppBarDefaults.IconSize +import com.getcode.ui.core.rememberedClickable + +private val ButtonSize = 40.dp +private val ButtonBackground = Color.White.copy(alpha = 0.1f) + + +@Composable +fun CircularIconButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + testTag: String? = null, + content: @Composable (Dp) -> Unit, +) { + Box( + modifier = modifier + .size(ButtonSize) + .background(ButtonBackground, CircleShape) + .clip(CircleShape) + .rememberedClickable { onClick() } + .then(if (testTag != null) Modifier.testTag(testTag) else Modifier), + contentAlignment = Alignment.Center, + ) { + content(IconSize) + } +} \ No newline at end of file diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/SearchInput.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/SearchInput.kt index 371d8814f..d2a639449 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/SearchInput.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/SearchInput.kt @@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.Close @@ -24,8 +24,7 @@ fun SearchInput( placeholder: String = stringResource(R.string.action_search), ) { TextInput( - modifier = modifier - .padding(horizontal = CodeTheme.dimens.grid.x3), + modifier = modifier, state = state, contentPadding = contentPadding, leadingIcon = { diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt index f9a7cdc10..bf14ac9cb 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt @@ -1,6 +1,5 @@ package com.getcode.ui.components -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues @@ -8,29 +7,21 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsIgnoringVisibility import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Redo import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.Logout -import androidx.compose.material.icons.filled.RestartAlt -import androidx.compose.material.icons.filled.RestorePage import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material.icons.outlined.Redo import androidx.compose.material.icons.rounded.RestorePage import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow @@ -41,8 +32,6 @@ import com.getcode.navigation.flow.LocalFlowDismissStyle import com.getcode.theme.CodeTheme import com.getcode.theme.DesignSystem import com.getcode.ui.core.addIf -import com.getcode.ui.core.rememberedClickable -import com.getcode.ui.core.unboundedClickable import com.getcode.ui.utils.calculateHorizontalPadding import kotlin.math.max @@ -50,66 +39,64 @@ object AppBarDefaults { val ContentPadding: PaddingValues @Composable get() = PaddingValues(horizontal = CodeTheme.dimens.grid.x2) - private val IconSize = 20.dp - private val ButtonSize = 40.dp - private val ButtonBackground = Color.White.copy(alpha = 0.1f) + internal val IconSize = 20.dp @Composable fun UpNavigation(modifier: Modifier = Modifier, onClick: () -> Unit) { - CircularIconButton(modifier = modifier, onClick = onClick, testTag = "action_back") { + CircularIconButton(modifier = modifier, onClick = onClick, testTag = "action_back") { size -> Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = "", tint = Color.White, - modifier = Modifier.requiredSize(IconSize), + modifier = Modifier.requiredSize(size), ) } } @Composable fun Close(modifier: Modifier = Modifier, onClick: () -> Unit) { - CircularIconButton(modifier = modifier, onClick = onClick, testTag = "action_close") { + CircularIconButton(modifier = modifier, onClick = onClick, testTag = "action_close") { size -> Icon( imageVector = Icons.Outlined.Close, contentDescription = "", tint = Color.White, - modifier = Modifier.requiredSize(IconSize), + modifier = Modifier.requiredSize(size), ) } } @Composable fun Share(modifier: Modifier = Modifier, onClick: () -> Unit) { - CircularIconButton(modifier = modifier, onClick = onClick, testTag = "action_share") { + CircularIconButton(modifier = modifier, onClick = onClick, testTag = "action_share") { size -> Icon( painter = painterResource(R.drawable.ic_remote_send), contentDescription = "", tint = Color.White, - modifier = Modifier.requiredSize(IconSize), + modifier = Modifier.requiredSize(size), ) } } @Composable fun Leave(modifier: Modifier = Modifier, onClick: () -> Unit) { - CircularIconButton(modifier = modifier, onClick = onClick) { + CircularIconButton(modifier = modifier, onClick = onClick) { size -> Icon( imageVector = Icons.AutoMirrored.Outlined.Logout, contentDescription = "", tint = Color.White, - modifier = Modifier.requiredSize(IconSize), + modifier = Modifier.requiredSize(size), ) } } @Composable fun Settings(modifier: Modifier = Modifier, onClick: () -> Unit) { - CircularIconButton(modifier = modifier, onClick = onClick) { + CircularIconButton(modifier = modifier, onClick = onClick) { size -> Icon( painter = painterResource(R.drawable.ic_settings_outline), contentDescription = "", tint = Color.White, - modifier = Modifier.requiredSize(IconSize), + modifier = Modifier.requiredSize(size), ) } } @@ -119,12 +106,12 @@ object AppBarDefaults { modifier: Modifier = Modifier, onClick: () -> Unit ) { - CircularIconButton(modifier = modifier, onClick = onClick) { + CircularIconButton(modifier = modifier, onClick = onClick) { size -> Icon( imageVector = Icons.Outlined.MoreVert, contentDescription = "", tint = Color.White, - modifier = Modifier.requiredSize(IconSize), + modifier = Modifier.requiredSize(size), ) } } @@ -134,12 +121,12 @@ object AppBarDefaults { modifier: Modifier = Modifier, onClick: () -> Unit ) { - CircularIconButton(modifier = modifier, onClick = onClick) { + CircularIconButton(modifier = modifier, onClick = onClick) { size -> Icon( imageVector = Icons.Rounded.RestorePage, contentDescription = "", tint = Color.White, - modifier = Modifier.requiredSize(IconSize), + modifier = Modifier.requiredSize(size), ) } } @@ -159,26 +146,6 @@ object AppBarDefaults { overflow = TextOverflow.Ellipsis ) } - - @Composable - private fun CircularIconButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, - testTag: String? = null, - content: @Composable () -> Unit, - ) { - Box( - modifier = modifier - .size(ButtonSize) - .background(ButtonBackground, CircleShape) - .clip(CircleShape) - .rememberedClickable { onClick() } - .then(if (testTag != null) Modifier.testTag(testTag) else Modifier), - contentAlignment = Alignment.Center, - ) { - content() - } - } } @Composable From 50c45268714f8fa58eebbb23ed3d0dda40a2f119 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 29 May 2026 15:43:48 -0400 Subject: [PATCH 2/2] fix(navigation): handle removed step in FlowHost proceed() When the current step is removed from the steps list (e.g. a gate whose condition is now met), proceed() now navigates to the first remaining step instead of doing nothing. Signed-off-by: Brandon McAnsh --- .../src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt b/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt index e97c0133d..0345cb15d 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/navigation/flow/FlowHost.kt @@ -384,6 +384,10 @@ private class InnerFlowNavigator( val nextIndex = anchorIndex + 1 if (anchorIndex >= 0 && nextIndex <= currentSteps.lastIndex) { navigateTo(currentSteps[nextIndex]) + } else if (anchorIndex < 0 && currentSteps.isNotEmpty()) { + // Current step was removed from the steps list (e.g. a gate whose condition + // is now met). Navigate to the first remaining step. + navigateTo(currentSteps.first(), popCurrent = true) } else { val result = completedResult() if (result != null) exitWithResult(result)