Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -208,9 +211,18 @@ sealed interface AppRoute : NavKey, Parcelable {
data object Discovery: AppRoute

@Serializable
data object CurrencyCreator : Token, FlowRouteWithResult<CurrencyCreatorResult> {
data object CurrencyCreator : Token, SteppedFlowRoute<CurrencyCreatorResult> {
override val initialStack: List<NavKey>
get() = listOf(CurrencyCreatorStep.Info)

override val progressSteps: List<KClass<out FlowStep>>
get() = listOf(
CurrencyCreatorStep.NameSelection::class,
CurrencyCreatorStep.IconSelection::class,
CurrencyCreatorStep.DescriptionSelection::class,
CurrencyCreatorStep.BillCustomization::class,
CurrencyCreatorStep.BillReview::class,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<KClass<out FlowStep>>,
): 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
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.flipcash.app.currencycreator.internal.components
package com.flipcash.app.core.ui.flow

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Box
Expand All @@ -21,10 +21,15 @@ 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
internal fun CurrencyCreatorTopBar(
controller: CurrencyCreatorTopBarController,
fun FlowStepBar(
controller: FlowStepBarController,
mainContent: (@Composable () -> Unit)? = null,
endContent: (@Composable () -> Unit)? = null,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package com.flipcash.app.currencycreator.internal.components
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

internal class CurrencyCreatorTopBarController {
/**
* 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)
}

companion object {
val LocalCurrencyCreatorTopBar = staticCompositionLocalOf<CurrencyCreatorTopBarController> {
error("No CurrencyCreatorTopBarController provided")
}
}
val LocalFlowStepBar = staticCompositionLocalOf<FlowStepBarController> {
error("No FlowStepBarController provided")
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.flipcash.app.currencycreator.internal.components
package com.flipcash.app.core.ui.flow

import androidx.compose.foundation.background
import androidx.compose.foundation.border
Expand All @@ -19,89 +19,48 @@ 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
/**
* 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<StepperItem>,
modifier: Modifier = Modifier,
) {
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
items.forEach { item ->
StepperRow(
icon = item.icon,
title = item.title,
description = item.description,
weight = item.weight,
showConnector = item.showConnector,
)
}
}
}

/**
* 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(
private fun ColumnScope.StepperRow(
icon: Painter,
title: String,
description: String,
weight: Float,
showConnector: Boolean = weight > 0f
showConnector: Boolean,
) {
val hasConnector = weight > 0f
Row(
Expand Down Expand Up @@ -174,4 +133,4 @@ private fun ColumnScope.StepperItem(
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<T : Parcelable> : FlowRouteWithResult<T> {
val progressSteps: List<KClass<out FlowStep>>
}
Original file line number Diff line number Diff line change
@@ -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 <S : FlowStep, R : Parcelable> SteppedFlowScaffold(
route: SteppedFlowRoute<R>,
initialStack: List<S>,
currentStep: FlowStep?,
resultStateRegistry: NavResultStateRegistry,
onExit: (reason: FlowExitReason<R>, isSheetRoot: Boolean) -> Unit,
titleContent: (@Composable () -> Unit)? = null,
endContent: (@Composable () -> Unit)? = null,
entryProvider: (NavKey) -> NavEntry<NavKey>,
) {
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,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
Loading
Loading