From 3f1574126d1866810bf87b41894db6872da161c4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 21 Jul 2026 11:13:51 -0400 Subject: [PATCH 1/5] feat(core): add parameterized FlowStepper Signed-off-by: Brandon McAnsh --- .../flipcash/app/core/ui/flow/FlowStepper.kt | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepper.kt diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepper.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepper.kt new file mode 100644 index 000000000..d59e5be96 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepper.kt @@ -0,0 +1,136 @@ +package com.flipcash.app.core.ui.flow + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import com.getcode.theme.CodeTheme +import com.getcode.theme.extraSmall + +/** + * One entry in a [FlowStepper]. [weight] > 0 makes the row expand and draws a gradient connector to + * the next item when [showConnector] is true. + */ +data class StepperItem( + val icon: Painter, + val title: String, + val description: String, + val weight: Float = 0.6f, + val showConnector: Boolean = weight > 0f, +) + +/** Vertical wizard stepper. Content is fully caller-supplied via [items]. */ +@Composable +fun FlowStepper( + items: List, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + items.forEach { item -> + StepperRow( + icon = item.icon, + title = item.title, + description = item.description, + weight = item.weight, + showConnector = item.showConnector, + ) + } + } +} + +@Composable +private fun ColumnScope.StepperRow( + icon: Painter, + title: String, + description: String, + weight: Float, + showConnector: Boolean, +) { + val hasConnector = weight > 0f + Row( + modifier = Modifier + .fillMaxWidth() + .then(if (hasConnector) Modifier.weight(weight) else Modifier), + horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), + ) { + Column( + modifier = Modifier.then(if (hasConnector) Modifier.fillMaxHeight() else Modifier), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .size(48.dp) + .background( + color = CodeTheme.colors.surfaceVariant, + shape = CodeTheme.shapes.extraSmall, + ) + .border( + width = CodeTheme.dimens.border, + color = CodeTheme.colors.divider, + shape = CodeTheme.shapes.extraSmall, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = icon, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(24.dp), + ) + } + + if (showConnector) { + Box( + modifier = Modifier + .width(CodeTheme.dimens.thickBorder) + .weight(1f) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = 0.2f), + Color.White.copy(alpha = 0.05f), + ), + ), + ), + ) + } else { + Box( + modifier = Modifier + .width(CodeTheme.dimens.thickBorder) + .weight(1f) + ) + } + } + + Column( + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1) + ) { + Text( + text = title, + style = CodeTheme.typography.screenTitle, + color = CodeTheme.colors.textMain, + ) + Text( + text = description, + style = CodeTheme.typography.textSmall, + color = CodeTheme.colors.textSecondary, + ) + } + } +} From 71807b42fca809c723b7b76bdcd5cd241bff3b6b Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 21 Jul 2026 11:21:48 -0400 Subject: [PATCH 2/5] refactor(currency-creator): use shared FlowStepper on InfoScreen Signed-off-by: Brandon McAnsh --- .../internal/components/FlowStepper.kt | 177 ------------------ .../internal/screens/InfoScreen.kt | 78 +++++++- 2 files changed, 74 insertions(+), 181 deletions(-) delete mode 100644 apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/FlowStepper.kt diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/FlowStepper.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/FlowStepper.kt deleted file mode 100644 index 2b9ecdfc9..000000000 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/FlowStepper.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.flipcash.app.currencycreator.internal.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.flipcash.core.R -import com.getcode.opencode.model.financial.Fiat -import com.getcode.opencode.model.financial.minus -import com.getcode.theme.CodeTheme -import com.getcode.theme.extraSmall - -@Composable -internal fun Stepper(modifier: Modifier = Modifier, cost: Fiat, fee: Fiat?) { - val receiving = cost - (fee ?: Fiat.Zero) - val isReceivingAmount = receiving > Fiat.Zero - - Column(modifier = modifier) { - StepperItem( - icon = painterResource(R.drawable.ic_currencycreator_name), - title = stringResource(R.string.title_currencyCreatorStepName), - description = stringResource(R.string.subtitle_currencyCreatorStepName), - weight = 0.6f, - ) - StepperItem( - icon = painterResource(R.drawable.ic_currencycreator_icon), - title = stringResource(R.string.title_currencyCreatorStepIcon), - description = stringResource(R.string.subtitle_currencyCreatorStepIcon), - weight = 0.6f, - ) - StepperItem( - painterResource(R.drawable.ic_currencycreator_description), - title = stringResource(R.string.title_currencyCreatorStepDescription), - description = stringResource(R.string.subtitle_currencyCreatorStepDescription), - weight = 0.6f, - ) - StepperItem( - painterResource(R.drawable.ic_currencycreator_cashbill), - title = stringResource(R.string.title_currencyCreatorStepDesign), - description = stringResource(R.string.subtitle_currencyCreatorStepDesign), - weight = 0.6f, - ) - StepperItem( - painterResource(R.drawable.ic_currencycreator_purchase), - title = stringResource( - R.string.title_currencyCreatorStepPurchase, - cost.formatted( - rule = Fiat.FormattingRule.Truncated, - suffix = stringResource(R.string.subtitle_usdSuffix) - ) - ), - description = stringResource(R.string.subtitle_currencyCreatorStepPurchase), - weight = 0.6f, - showConnector = isReceivingAmount, - ) - - if (isReceivingAmount) { - StepperItem( - painterResource(R.drawable.ic_currencycreator_gift), - title = stringResource( - R.string.title_currencyCreatorStepPurchaseFreeGift, - receiving.formatted(rule = Fiat.FormattingRule.Truncated) - ), - description = stringResource( - R.string.subtitle_currencyCreatorStepPurchaseFreeGift, - receiving.formatted(rule = Fiat.FormattingRule.Truncated) - ), - weight = 0.6f, - showConnector = false - ) - } - } -} - -/** - * A single step in the vertical stepper. When [weight] > 0 the item expands - * proportionally and a gradient connector line fills the space between the - * icon box and the next item. The last item should pass [weight] = 0. - */ -@Composable -private fun ColumnScope.StepperItem( - icon: Painter, - title: String, - description: String, - weight: Float, - showConnector: Boolean = weight > 0f -) { - val hasConnector = weight > 0f - Row( - modifier = Modifier - .fillMaxWidth() - .then(if (hasConnector) Modifier.weight(weight) else Modifier), - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), - ) { - Column( - modifier = Modifier.then(if (hasConnector) Modifier.fillMaxHeight() else Modifier), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = Modifier - .size(48.dp) - .background( - color = CodeTheme.colors.surfaceVariant, - shape = CodeTheme.shapes.extraSmall, - ) - .border( - width = CodeTheme.dimens.border, - color = CodeTheme.colors.divider, - shape = CodeTheme.shapes.extraSmall, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = icon, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(24.dp), - ) - } - - if (showConnector) { - Box( - modifier = Modifier - .width(CodeTheme.dimens.thickBorder) - .weight(1f) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.White.copy(alpha = 0.2f), - Color.White.copy(alpha = 0.05f), - ), - ), - ), - ) - } else { - Box( - modifier = Modifier - .width(CodeTheme.dimens.thickBorder) - .weight(1f) - ) - } - } - - Column( - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1) - ) { - Text( - text = title, - style = CodeTheme.typography.screenTitle, - color = CodeTheme.colors.textMain, - ) - Text( - text = description, - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - ) - } - } -} \ No newline at end of file diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/InfoScreen.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/InfoScreen.kt index b5b42f936..2bcefc69e 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/InfoScreen.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/InfoScreen.kt @@ -15,15 +15,19 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.tokens.CurrencyCreatorResult import com.flipcash.app.core.tokens.CurrencyCreatorStep +import com.flipcash.app.core.ui.flow.FlowStepper +import com.flipcash.app.core.ui.flow.StepperItem import com.flipcash.app.currencycreator.internal.CurrencyCreatorViewModel -import com.flipcash.app.currencycreator.internal.components.Stepper import com.flipcash.core.R +import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.financial.minus import com.getcode.navigation.flow.flowSharedViewModel import com.getcode.navigation.flow.rememberFlowNavigator import kotlinx.coroutines.flow.filterIsInstance @@ -106,12 +110,78 @@ internal fun InfoScreenContent( ) } - Stepper( + FlowStepper( modifier = Modifier .weight(0.5f) .padding(top = CodeTheme.dimens.grid.x12), - cost = state.totalCost, - fee = state.feeAmount + items = currencyCreatorStepperItems(cost = state.totalCost, fee = state.feeAmount), + ) + } + } +} + +@Composable +private fun currencyCreatorStepperItems(cost: Fiat, fee: Fiat?): List { + val receiving = cost - (fee ?: Fiat.Zero) + val isReceivingAmount = receiving > Fiat.Zero + + return buildList { + add( + StepperItem( + icon = painterResource(R.drawable.ic_currencycreator_name), + title = stringResource(R.string.title_currencyCreatorStepName), + description = stringResource(R.string.subtitle_currencyCreatorStepName), + ) + ) + add( + StepperItem( + icon = painterResource(R.drawable.ic_currencycreator_icon), + title = stringResource(R.string.title_currencyCreatorStepIcon), + description = stringResource(R.string.subtitle_currencyCreatorStepIcon), + ) + ) + add( + StepperItem( + icon = painterResource(R.drawable.ic_currencycreator_description), + title = stringResource(R.string.title_currencyCreatorStepDescription), + description = stringResource(R.string.subtitle_currencyCreatorStepDescription), + ) + ) + add( + StepperItem( + icon = painterResource(R.drawable.ic_currencycreator_cashbill), + title = stringResource(R.string.title_currencyCreatorStepDesign), + description = stringResource(R.string.subtitle_currencyCreatorStepDesign), + ) + ) + add( + StepperItem( + icon = painterResource(R.drawable.ic_currencycreator_purchase), + title = stringResource( + R.string.title_currencyCreatorStepPurchase, + cost.formatted( + rule = Fiat.FormattingRule.Truncated, + suffix = stringResource(R.string.subtitle_usdSuffix) + ) + ), + description = stringResource(R.string.subtitle_currencyCreatorStepPurchase), + showConnector = isReceivingAmount, + ) + ) + if (isReceivingAmount) { + add( + StepperItem( + icon = painterResource(R.drawable.ic_currencycreator_gift), + title = stringResource( + R.string.title_currencyCreatorStepPurchaseFreeGift, + receiving.formatted(rule = Fiat.FormattingRule.Truncated) + ), + description = stringResource( + R.string.subtitle_currencyCreatorStepPurchaseFreeGift, + receiving.formatted(rule = Fiat.FormattingRule.Truncated) + ), + showConnector = false, + ) ) } } From 1087689e67edc0800180d6a553acf10af0e7e821 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 21 Jul 2026 11:33:37 -0400 Subject: [PATCH 3/5] feat(core): add stepped-flow type (progress helper, route, scaffold, step bar) Signed-off-by: Brandon McAnsh --- .../flipcash/app/core/ui/flow/FlowProgress.kt | 20 +++++ .../flipcash/app/core/ui/flow/FlowStepBar.kt | 86 +++++++++++++++++++ .../app/core/ui/flow/FlowStepBarController.kt | 21 +++++ .../app/core/ui/flow/SteppedFlowRoute.kt | 16 ++++ .../app/core/ui/flow/SteppedFlowScaffold.kt | 55 ++++++++++++ .../app/core/ui/flow/FlowProgressTest.kt | 40 +++++++++ 6 files changed, 238 insertions(+) create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowProgress.kt create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBar.kt create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBarController.kt create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowRoute.kt create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowScaffold.kt create mode 100644 apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/ui/flow/FlowProgressTest.kt diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowProgress.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowProgress.kt new file mode 100644 index 000000000..4734d0261 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowProgress.kt @@ -0,0 +1,20 @@ +package com.flipcash.app.core.ui.flow + +import com.getcode.navigation.flow.FlowStep +import kotlin.reflect.KClass + +/** + * Fraction (0f..1f) of the stepped flow completed at [step], given the ordered [progressSteps] + * that participate in the progress indicator. Steps outside [progressSteps] (intro, funding, + * processing, etc.) report 0f so the bar hides. Extracted from CurrencyCreatorViewModel.progress + * so every stepped flow shares one definition. + */ +fun flowProgressFor( + step: FlowStep?, + progressSteps: List>, +): Float { + if (step == null || progressSteps.isEmpty()) return 0f + val index = progressSteps.indexOfFirst { it.isInstance(step) } + if (index < 0) return 0f + return (index + 1).toFloat() / progressSteps.size +} diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBar.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBar.kt new file mode 100644 index 000000000..f01f43116 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBar.kt @@ -0,0 +1,86 @@ +package com.flipcash.app.core.ui.flow + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.getcode.theme.CodeTheme +import com.getcode.ui.components.AppBarDefaults +import com.getcode.ui.components.AppBarWithTitle +import com.getcode.ui.utils.rememberKeyboardController + +/** + * Top bar for a [SteppedFlowScaffold]. Shows a centered [LinearProgressIndicator] in the title slot + * while [FlowStepBarController.progress] is strictly between 0 and 1, unless [mainContent] overrides + * the title (used by steps like intro / processing that show a title instead of progress). + */ +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun FlowStepBar( + controller: FlowStepBarController, + mainContent: (@Composable () -> Unit)? = null, + endContent: (@Composable () -> Unit)? = null, +) { + val animatedProgress by animateFloatAsState( + targetValue = controller.progress, + label = "progress", + ) + + val defaultMainContent = @Composable { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + contentAlignment = Alignment.Center + ) { + if (controller.progress in 0.01f..0.999f) { + LinearProgressIndicator( + progress = { animatedProgress }, + modifier = Modifier + .fillMaxWidth(0.8f) + .padding(horizontal = CodeTheme.dimens.grid.x3) + .height(CodeTheme.dimens.grid.x1), + color = Color.White, + trackColor = CodeTheme.colors.divider, + gapSize = 0.dp, + drawStopIndicator = {} + ) + } + } + } + + val defaultEndContent = @Composable { + Spacer(Modifier.width(24.dp)) + } + + val keyboard = rememberKeyboardController() + + AppBarWithTitle( + title = { mainContent?.invoke() ?: defaultMainContent() }, + titleAlignment = Alignment.CenterHorizontally, + leftIcon = { + controller.onBack?.let { onBack -> + AppBarDefaults.UpNavigation { + keyboard.hideIfVisible { + onBack() + } + } + } + }, + rightContents = { + endContent?.invoke() ?: defaultEndContent() + } + ) +} diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBarController.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBarController.kt new file mode 100644 index 000000000..386471e44 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/FlowStepBarController.kt @@ -0,0 +1,21 @@ +package com.flipcash.app.core.ui.flow + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * Mutable chrome state for a [SteppedFlowScaffold]'s top bar. The scaffold updates [progress]; + * each step wires [onBack] / [onEndAction] via the composition local below. + */ +class FlowStepBarController { + var progress: Float by mutableFloatStateOf(0f) + var onBack: (() -> Unit)? by mutableStateOf(null) + var onEndAction: (() -> Unit)? by mutableStateOf(null) +} + +val LocalFlowStepBar = staticCompositionLocalOf { + error("No FlowStepBarController provided") +} diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowRoute.kt new file mode 100644 index 000000000..9de67d591 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowRoute.kt @@ -0,0 +1,16 @@ +package com.flipcash.app.core.ui.flow + +import android.os.Parcelable +import com.getcode.navigation.flow.FlowRouteWithResult +import com.getcode.navigation.flow.FlowStep +import kotlin.reflect.KClass + +/** + * A [FlowRouteWithResult] whose flow presents a stepped-progress wizard. Declaring a route as a + * [SteppedFlowRoute] is how a flow opts into the shared [SteppedFlowScaffold] chrome (progress top + * bar). [progressSteps] is the ordered list of step types that fill the progress bar; steps not in + * this list (intro / processing / funding, etc.) show no progress. + */ +interface SteppedFlowRoute : FlowRouteWithResult { + val progressSteps: List> +} diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowScaffold.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowScaffold.kt new file mode 100644 index 000000000..f54f3f945 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/flow/SteppedFlowScaffold.kt @@ -0,0 +1,55 @@ +package com.flipcash.app.core.ui.flow + +import android.os.Parcelable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import com.getcode.navigation.flow.FlowExitReason +import com.getcode.navigation.flow.FlowHost +import com.getcode.navigation.flow.FlowStep +import com.getcode.navigation.results.NavResultStateRegistry + +/** + * Renders the shared stepped-progress chrome above a [FlowHost]. Owns the progress computation + * (from [SteppedFlowRoute.progressSteps] and the caller-provided [currentStep]) so consumers no + * longer track progress in their ViewModel. Per-step [titleContent] / [endContent] are supplied by + * the consumer (they read their own state); each step wires back / end-action through + * [LocalFlowStepBar]. + * + * @param currentStep the step currently shown, typically read from the shared flow ViewModel's state. + */ +@Composable +fun SteppedFlowScaffold( + route: SteppedFlowRoute, + initialStack: List, + currentStep: FlowStep?, + resultStateRegistry: NavResultStateRegistry, + onExit: (reason: FlowExitReason, isSheetRoot: Boolean) -> Unit, + titleContent: (@Composable () -> Unit)? = null, + endContent: (@Composable () -> Unit)? = null, + entryProvider: (NavKey) -> NavEntry, +) { + val controller = remember { FlowStepBarController() } + controller.progress = flowProgressFor(currentStep, route.progressSteps) + + CompositionLocalProvider(LocalFlowStepBar provides controller) { + Column(modifier = Modifier.fillMaxSize()) { + FlowStepBar( + controller = controller, + mainContent = titleContent, + endContent = endContent, + ) + FlowHost( + initialStack = initialStack, + resultStateRegistry = resultStateRegistry, + onExit = onExit, + entryProvider = entryProvider, + ) + } + } +} diff --git a/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/ui/flow/FlowProgressTest.kt b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/ui/flow/FlowProgressTest.kt new file mode 100644 index 000000000..4b2bc5706 --- /dev/null +++ b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/ui/flow/FlowProgressTest.kt @@ -0,0 +1,40 @@ +package com.flipcash.app.core.ui.flow + +import com.getcode.navigation.flow.FlowStep +import org.junit.Assert.assertEquals +import org.junit.Test + +class FlowProgressTest { + + private object StepA : FlowStep + private object StepB : FlowStep + private object StepC : FlowStep + private object OffListStep : FlowStep + + private val progressSteps = listOf(StepA::class, StepB::class, StepC::class) + + @Test + fun `null step is zero progress`() { + assertEquals(0f, flowProgressFor(null, progressSteps)) + } + + @Test + fun `empty progress steps is zero progress`() { + assertEquals(0f, flowProgressFor(StepA, emptyList())) + } + + @Test + fun `step not in list is zero progress`() { + assertEquals(0f, flowProgressFor(OffListStep, progressSteps)) + } + + @Test + fun `first step is one third`() { + assertEquals(1f / 3f, flowProgressFor(StepA, progressSteps)) + } + + @Test + fun `last step is full`() { + assertEquals(1f, flowProgressFor(StepC, progressSteps)) + } +} From d754945dae7eda3963f7174dc144b067c92c8055 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 21 Jul 2026 11:34:14 -0400 Subject: [PATCH 4/5] refactor(core): CurrencyCreator route is a SteppedFlowRoute Signed-off-by: Brandon McAnsh --- .../main/kotlin/com/flipcash/app/core/AppRoute.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt index d444a4bec..2fac50864 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt @@ -18,8 +18,11 @@ import com.flipcash.app.core.withdrawal.WithdrawalStep import com.flipcash.app.core.chat.ChatStep import com.flipcash.app.core.onboarding.OnboardingStep import com.flipcash.app.core.tokens.FundingSource +import com.flipcash.app.core.ui.flow.SteppedFlowRoute import com.getcode.navigation.flow.FlowRoute import com.getcode.navigation.flow.FlowRouteWithResult +import com.getcode.navigation.flow.FlowStep +import kotlin.reflect.KClass import com.getcode.opencode.model.financial.Fiat import com.getcode.solana.keys.Mint import com.getcode.ui.core.RestrictionType @@ -208,9 +211,18 @@ sealed interface AppRoute : NavKey, Parcelable { data object Discovery: AppRoute @Serializable - data object CurrencyCreator : Token, FlowRouteWithResult { + data object CurrencyCreator : Token, SteppedFlowRoute { override val initialStack: List get() = listOf(CurrencyCreatorStep.Info) + + override val progressSteps: List> + get() = listOf( + CurrencyCreatorStep.NameSelection::class, + CurrencyCreatorStep.IconSelection::class, + CurrencyCreatorStep.DescriptionSelection::class, + CurrencyCreatorStep.BillCustomization::class, + CurrencyCreatorStep.BillReview::class, + ) } } From 0e1044b4a56558a3cd6dad6decf80aa931e7c9b0 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 21 Jul 2026 11:41:59 -0400 Subject: [PATCH 5/5] refactor(currency-creator): use shared SteppedFlowScaffold, delete bespoke chrome Signed-off-by: Brandon McAnsh --- .../CurrencyCreatorFlowScreen.kt | 277 +++++++++--------- .../internal/CurrencyCreatorViewModel.kt | 24 -- .../components/CurrencyCreatorTopBar.kt | 81 ----- .../CurrencyCreatorTopBarController.kt | 19 -- 4 files changed, 140 insertions(+), 261 deletions(-) delete mode 100644 apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBar.kt delete mode 100644 apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBarController.kt diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt index 4f8154abd..bb159132e 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt @@ -1,8 +1,6 @@ package com.flipcash.app.currencycreator import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text @@ -12,16 +10,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color 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.lifecycle.compose.collectAsStateWithLifecycle -import com.getcode.navigation.flow.flowSharedViewModel -import com.getcode.navigation.flow.rememberInitialStack -import com.getcode.navigation.flow.rememberFlowNavigator import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider @@ -29,14 +22,16 @@ import com.flipcash.app.bill.customization.LocalBillPlaygroundController import com.flipcash.app.core.AppRoute import com.flipcash.app.core.tokens.CurrencyCreatorResult import com.flipcash.app.core.tokens.CurrencyCreatorStep -import com.flipcash.app.currencycreator.internal.screens.BillCustomizationContent -import com.flipcash.app.currencycreator.internal.components.CurrencyCreatorTopBar -import com.flipcash.app.currencycreator.internal.components.CurrencyCreatorTopBarController -import com.flipcash.app.currencycreator.internal.components.CurrencyCreatorTopBarController.Companion.LocalCurrencyCreatorTopBar +import com.flipcash.app.core.ui.flow.FlowStepBarController +import com.flipcash.app.core.ui.flow.LocalFlowStepBar +import com.flipcash.app.core.ui.flow.SteppedFlowScaffold import com.flipcash.app.currencycreator.internal.CurrencyCreatorViewModel +import com.flipcash.app.currencycreator.internal.screens.BillCustomizationContent import com.flipcash.app.currencycreator.internal.screens.BillCustomizationScreen +import com.flipcash.app.currencycreator.internal.screens.BillReviewScreen import com.flipcash.app.currencycreator.internal.screens.DescriptionSelectionContent import com.flipcash.app.currencycreator.internal.screens.DescriptionSelectionScreen +import com.flipcash.app.currencycreator.internal.screens.FundingSourceSelectionScreen import com.flipcash.app.currencycreator.internal.screens.IconSelectionContent import com.flipcash.app.currencycreator.internal.screens.IconSelectionScreen import com.flipcash.app.currencycreator.internal.screens.InfoScreen @@ -46,18 +41,18 @@ import com.flipcash.app.currencycreator.internal.screens.NameSelectionScreen import com.flipcash.app.currencycreator.internal.screens.ProcessingContent import com.flipcash.app.currencycreator.internal.screens.ProcessingScreen import com.flipcash.app.currencycreator.internal.screens.ReviewContent -import com.flipcash.app.currencycreator.internal.screens.BillReviewScreen -import com.flipcash.app.currencycreator.internal.screens.FundingSourceSelectionScreen import com.flipcash.app.session.LocalSessionController import com.flipcash.app.theme.FlipcashThemeWrapper import com.flipcash.core.R import com.getcode.navigation.annotatedEntry import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.flow.FlowExitReason -import com.getcode.navigation.flow.FlowHost import com.getcode.navigation.flow.LocalFlowNavigator import com.getcode.navigation.flow.PreviewFlowNavigator import com.getcode.navigation.flow.deliverFlowResult +import com.getcode.navigation.flow.flowSharedViewModel +import com.getcode.navigation.flow.rememberFlowNavigator +import com.getcode.navigation.flow.rememberInitialStack import com.getcode.navigation.results.NavResultOrCanceled import com.getcode.navigation.results.NavResultStateRegistry import com.getcode.opencode.model.financial.Fiat @@ -75,163 +70,171 @@ fun CurrencyCreatorFlowScreen( val initialStack = route.rememberInitialStack() - val topBarController = remember { CurrencyCreatorTopBarController() } - val viewModel = hiltViewModel() val state by viewModel.stateFlow.collectAsStateWithLifecycle() val session = LocalSessionController.current - CompositionLocalProvider(LocalCurrencyCreatorTopBar provides topBarController) { - Column(modifier = Modifier.fillMaxSize()) { - CurrencyCreatorTopBar( - controller = topBarController, - mainContent = when (state.currentStep) { - is CurrencyCreatorStep.Info -> { - { - Text( - text = stringResource(R.string.title_createYourCurrency), - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - ) - } - } + SteppedFlowScaffold( + route = route, + initialStack = initialStack, + currentStep = state.currentStep, + resultStateRegistry = resultStateRegistry, + titleContent = currencyCreatorTitleContent(state), + endContent = currencyCreatorEndContent(state), + onExit = { reason, _ -> + val result: CurrencyCreatorResult = when (reason) { + is FlowExitReason.Completed -> reason.result + FlowExitReason.Canceled, + FlowExitReason.BackedOutOfRoot -> CurrencyCreatorResult.Canceled + } - // TokenSelectScreen's own top bar is hidden on this step (showTopBar = false) - // to avoid stacking two bars; surface its title here instead. Matches the - // LaunchFunding title the select screen would show, and like Bill Review this - // step shows no progress indicator (title replaces the progress slot). - is CurrencyCreatorStep.FundingSourceSelection -> { - { - Text( - text = stringResource(R.string.title_selectPaymentCurrency), - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - ) - } - } + outerNavigator.deliverFlowResult( + route = route, + value = NavResultOrCanceled.ReturnValue(result), + ) - is CurrencyCreatorStep.Processing -> { - { - val text = if (state.processingState.success) { - stringResource(R.string.title_success) - } else if (state.processingState.error) { - stringResource(R.string.title_failed) - } else { - stringResource( - R.string.title_creatingCurrency, - state.nameFieldState.text.toString() - ) - } + if (result is CurrencyCreatorResult.Success) { + // The bill is priced through the new token's bonding curve in the + // ViewModel (see buildLaunchBill); presenting it lets someone grab the + // newly created currency. It's null if pricing failed — just close then. + val bill = state.launchBill + if (state.purchaseAmount > Fiat.Zero && bill != null) { + outerNavigator.hide() + session?.showBill(bill) + } else { + outerNavigator.pop() + } + } else { + outerNavigator.pop() + } + }, + entryProvider = currencyCreatorEntryProvider(), + ) +} - Text( - text = text, - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - ) - } - } +/** + * The step bar title for each step, or null on steps that show the progress indicator instead. + * Returns a composable lambda (deferred) rather than rendering, so the scaffold can distinguish + * "has a title" (override the progress slot) from "no title" (show progress). + */ +private fun currencyCreatorTitleContent( + state: CurrencyCreatorViewModel.State, +): (@Composable () -> Unit)? = when (state.currentStep) { + is CurrencyCreatorStep.Info -> { + { + Text( + text = stringResource(R.string.title_createYourCurrency), + style = CodeTheme.typography.textLarge, + color = CodeTheme.colors.textMain, + ) + } + } - else -> null - }, - endContent = when (state.currentStep) { - is CurrencyCreatorStep.BillCustomization -> { - { - val (accent, onAccent) = rememberDynamicAccent( - fallbackAccent = CodeTheme.colors.secondary, - fallbackOnAccent = CodeTheme.colors.onAction, - ) + // TokenSelectScreen's own top bar is hidden on this step (showTopBar = false) + // to avoid stacking two bars; surface its title here instead. Matches the + // LaunchFunding title the select screen would show, and like Bill Review this + // step shows no progress indicator (title replaces the progress slot). + is CurrencyCreatorStep.FundingSourceSelection -> { + { + Text( + text = stringResource(R.string.title_selectPaymentCurrency), + style = CodeTheme.typography.textLarge, + color = CodeTheme.colors.textMain, + ) + } + } - Text( - text = stringResource(R.string.action_next), - style = CodeTheme.typography.textMedium, - color = onAccent, - modifier = Modifier - .background(accent, shape = CircleShape) - .unboundedClickable { - topBarController.onEndAction?.invoke() - } - .padding( - horizontal = CodeTheme.dimens.grid.x2, - vertical = CodeTheme.dimens.grid.x1 - ), - ) - } - } + is CurrencyCreatorStep.Processing -> { + { + val text = if (state.processingState.success) { + stringResource(R.string.title_success) + } else if (state.processingState.error) { + stringResource(R.string.title_failed) + } else { + stringResource( + R.string.title_creatingCurrency, + state.nameFieldState.text.toString() + ) + } - else -> null - }, + Text( + text = text, + style = CodeTheme.typography.textLarge, + color = CodeTheme.colors.textMain, ) + } + } - FlowHost( - initialStack = initialStack, - resultStateRegistry = resultStateRegistry, - onExit = { reason, _ -> - val result: CurrencyCreatorResult = when (reason) { - is FlowExitReason.Completed -> reason.result - FlowExitReason.Canceled, - FlowExitReason.BackedOutOfRoot -> CurrencyCreatorResult.Canceled - } + else -> null +} - outerNavigator.deliverFlowResult( - route = route, - value = NavResultOrCanceled.ReturnValue(result), - ) +/** + * The step bar end action for each step, or null when the step has none. Bill Customization surfaces + * a "Next" capsule that fires the shared step bar's end action (wired in [SyncStepBar]). + */ +private fun currencyCreatorEndContent( + state: CurrencyCreatorViewModel.State, +): (@Composable () -> Unit)? = when (state.currentStep) { + is CurrencyCreatorStep.BillCustomization -> { + { + val stepBar = LocalFlowStepBar.current + val (accent, onAccent) = rememberDynamicAccent( + fallbackAccent = CodeTheme.colors.secondary, + fallbackOnAccent = CodeTheme.colors.onAction, + ) - if (result is CurrencyCreatorResult.Success) { - // The bill is priced through the new token's bonding curve in the - // ViewModel (see buildLaunchBill); presenting it lets someone grab the - // newly created currency. It's null if pricing failed — just close then. - val bill = state.launchBill - if (state.purchaseAmount > Fiat.Zero && bill != null) { - outerNavigator.hide() - session?.showBill(bill) - } else { - outerNavigator.pop() - } - } else { - outerNavigator.pop() + Text( + text = stringResource(R.string.action_next), + style = CodeTheme.typography.textMedium, + color = onAccent, + modifier = Modifier + .background(accent, shape = CircleShape) + .unboundedClickable { + stepBar.onEndAction?.invoke() } - }, - entryProvider = currencyCreatorEntryProvider(), + .padding( + horizontal = CodeTheme.dimens.grid.x2, + vertical = CodeTheme.dimens.grid.x1 + ), ) } } + + else -> null } private fun currencyCreatorEntryProvider(): (NavKey) -> NavEntry = entryProvider { - annotatedEntry { SyncTopBar(it); InfoScreen() } - annotatedEntry { SyncTopBar(it); NameSelectionScreen() } - annotatedEntry { SyncTopBar(it); IconSelectionScreen() } - annotatedEntry { SyncTopBar(it); DescriptionSelectionScreen() } - annotatedEntry { SyncTopBar(it); BillCustomizationScreen() } - annotatedEntry { SyncTopBar(it); BillReviewScreen() } - annotatedEntry { SyncTopBar(it); FundingSourceSelectionScreen() } - annotatedEntry { SyncTopBar(it); ProcessingScreen() } + annotatedEntry { SyncStepBar(it); InfoScreen() } + annotatedEntry { SyncStepBar(it); NameSelectionScreen() } + annotatedEntry { SyncStepBar(it); IconSelectionScreen() } + annotatedEntry { SyncStepBar(it); DescriptionSelectionScreen() } + annotatedEntry { SyncStepBar(it); BillCustomizationScreen() } + annotatedEntry { SyncStepBar(it); BillReviewScreen() } + annotatedEntry { SyncStepBar(it); FundingSourceSelectionScreen() } + annotatedEntry { SyncStepBar(it); ProcessingScreen() } } /** - * Syncs the current step to the ViewModel, which in turn pushes progress and - * visibility to the top bar controller. Also wires up the back button. + * Wires the shared step bar for the current step: dispatches OnStepChanged (the ViewModel keeps + * currentStep for title switching), and sets the shared controller's back / end-action. Progress is + * computed by SteppedFlowScaffold, not here. */ @Composable -private fun SyncTopBar(step: CurrencyCreatorStep) { - val topBar = LocalCurrencyCreatorTopBar.current +private fun SyncStepBar(step: CurrencyCreatorStep) { + val stepBar = LocalFlowStepBar.current val billPlaygroundController = LocalBillPlaygroundController.current val flowNavigator = rememberFlowNavigator() val viewModel = flowSharedViewModel() - LaunchedEffect(viewModel, topBar) { - viewModel.connectTopBar(topBar) - } LaunchedEffect(step) { viewModel.dispatchEvent(CurrencyCreatorViewModel.Event.OnStepChanged(step)) if (step is CurrencyCreatorStep.Processing) { - topBar.onBack = null + stepBar.onBack = null } else { - topBar.onBack = { flowNavigator.back() } + stepBar.onBack = { flowNavigator.back() } } - topBar.onEndAction = { + stepBar.onEndAction = { val bill = billPlaygroundController.state.value.customizedBill viewModel.dispatchEvent(CurrencyCreatorViewModel.Event.OnBillConfirmed(bill)) flowNavigator.navigateTo(CurrencyCreatorStep.BillReview) @@ -247,7 +250,7 @@ private fun CurrencyCreatorPreview( ) { CompositionLocalProvider( LocalFlowNavigator provides PreviewFlowNavigator(), - LocalCurrencyCreatorTopBar provides remember { CurrencyCreatorTopBarController() }, + LocalFlowStepBar provides remember { FlowStepBarController() }, ) { val state = CurrencyCreatorViewModel.State(feeAmount = feeAmount) content(state) diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt index f6abed44e..f0b7028f6 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt @@ -11,7 +11,6 @@ import com.flipcash.app.core.extensions.onResult import com.flipcash.app.core.tokens.CurrencyCreatorDraft import com.flipcash.app.core.tokens.CurrencyCreatorStep import com.flipcash.app.currencycreator.CurrencyCreatorCoordinator -import com.flipcash.app.currencycreator.internal.components.CurrencyCreatorTopBarController import com.flipcash.app.payments.PurchaseMethodController import com.flipcash.app.tokens.BalancePoller import com.flipcash.app.tokens.TokenCoordinator @@ -134,14 +133,6 @@ internal class CurrencyCreatorViewModel @Inject constructor( get() = descriptionFieldState.text.isNotBlank() && remainingSpaceForDescription >= 0 - val progress: Float - get() { - val step = currentStep ?: return 0f - val index = PROGRESS_STEPS.indexOfFirst { it.isInstance(step) } - if (index < 0) return 0f - return (index + 1).toFloat() / PROGRESS_STEPS.size - } - val totalCost: Fiat get() { val fee = feeAmount.orZero() @@ -658,21 +649,6 @@ internal class CurrencyCreatorViewModel @Inject constructor( ) } - /** - * Connect the ViewModel to the top bar controller. The ViewModel pushes - * [State.progress] to the controller whenever the current step changes. - */ - fun connectTopBar(controller: CurrencyCreatorTopBarController) { - stateFlow - .map { it.currentStep } - .distinctUntilChanged() - .onEach { _ -> - val state = stateFlow.value - controller.progress = state.progress - } - .launchIn(viewModelScope) - } - internal companion object { val updateStateForEvent: (Event) -> (State.() -> State) = { event -> when (event) { diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBar.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBar.kt deleted file mode 100644 index aeb24273e..000000000 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBar.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.flipcash.app.currencycreator.internal.components - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.utils.rememberKeyboardController - -@OptIn(ExperimentalLayoutApi::class) -@Composable -internal fun CurrencyCreatorTopBar( - controller: CurrencyCreatorTopBarController, - mainContent: (@Composable () -> Unit)? = null, - endContent: (@Composable () -> Unit)? = null, -) { - val animatedProgress by animateFloatAsState( - targetValue = controller.progress, - label = "progress", - ) - - val defaultMainContent = @Composable { - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - contentAlignment = Alignment.Center - ) { - if (controller.progress in 0.01f..0.999f) { - LinearProgressIndicator( - progress = { animatedProgress }, - modifier = Modifier - .fillMaxWidth(0.8f) - .padding(horizontal = CodeTheme.dimens.grid.x3) - .height(CodeTheme.dimens.grid.x1), - color = Color.White, - trackColor = CodeTheme.colors.divider, - gapSize = 0.dp, - drawStopIndicator = {} - ) - } - } - } - - val defaultEndContent = @Composable { - Spacer(Modifier.width(24.dp)) - } - - val keyboard = rememberKeyboardController() - - AppBarWithTitle( - title = { mainContent?.invoke() ?: defaultMainContent() }, - titleAlignment = Alignment.CenterHorizontally, - leftIcon = { - controller.onBack?.let { onBack -> - AppBarDefaults.UpNavigation { - keyboard.hideIfVisible { - onBack() - } - } - } - }, - rightContents = { - endContent?.invoke() ?: defaultEndContent() - } - ) -} diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBarController.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBarController.kt deleted file mode 100644 index 3f7920e22..000000000 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/components/CurrencyCreatorTopBarController.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.flipcash.app.currencycreator.internal.components - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf - -internal class CurrencyCreatorTopBarController { - var progress: Float by mutableFloatStateOf(0f) - var onBack: (() -> Unit)? by mutableStateOf(null) - var onEndAction: (() -> Unit)? by mutableStateOf(null) - - companion object { - val LocalCurrencyCreatorTopBar = staticCompositionLocalOf { - error("No CurrencyCreatorTopBarController provided") - } - } -}