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 @@ -247,4 +247,5 @@ object AppColors {
val lightRed = Color(0xFFFF6E6E)
val peach = Color(0xFFFF926E)
val lightPurple = Color(0xFF706EFF)
val stepperColor = Color(0xFF9ECAFC)
}
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ data class AppSizes(
val iconExtraLarge: Dp = 36.dp,
val avatarSmall: Dp = 32.dp,
val avatarMedium: Dp = 48.dp,
val avatarMediumExtra: Dp = 56.dp,
val avatarLarge: Dp = 64.dp,
val avatarLargeLarge: Dp = 128.dp,
val buttonHeight: Dp = 56.dp,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mifos.core.designsystem.theme.AppColors
import com.mifos.core.designsystem.theme.DesignToken
import com.mifos.core.designsystem.theme.MifosTypography
import org.jetbrains.compose.ui.tooling.preview.Preview

data class Step(
val name: String,
val content: @Composable () -> Unit,
)

@Composable
fun MifosStepper(
steps: List<Step>,
currentIndex: Int,
onStepChange: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()

LaunchedEffect(currentIndex) {
listState.animateScrollToItem(currentIndex)
}
Column(
modifier = modifier
.fillMaxWidth()
.padding(
vertical = DesignToken.padding.small,
horizontal = DesignToken.padding.large,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
LazyRow(
state = listState,
modifier = Modifier
.clip(shape = DesignToken.shapes.medium)
.background(MaterialTheme.colorScheme.primary)
.padding(vertical = DesignToken.padding.largeIncreasedExtra)
.padding(start = DesignToken.padding.small)
.fillMaxWidth(),
) {
steps.forEachIndexed { index, step ->
item {
Row(
verticalAlignment = Alignment.Top,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.width(DesignToken.sizes.avatarMediumExtra),
) {
Box(
modifier = Modifier
.size(DesignToken.sizes.iconLarge)
.clip(CircleShape)
.background(
when {
index == currentIndex -> AppColors.customWhite
else -> AppColors.stepperColor
},
)
.clickable(enabled = index < currentIndex) {
if (index < currentIndex) onStepChange(index)
},
contentAlignment = Alignment.Center,
) {
Text(
text = (index + 1).toString(),
color = MaterialTheme.colorScheme.primary,
)
}

Spacer(modifier = Modifier.height(DesignToken.padding.small))
BasicText(
text = step.name,
autoSize = TextAutoSize.StepBased(
minFontSize = 2.sp,
maxFontSize = 11.sp,
),
style = MifosTypography.labelSmall.copy(
color = AppColors.customWhite,
),
)
}
if (index != steps.lastIndex) {
Box(
modifier = Modifier
.padding(vertical = DesignToken.padding.large)
.width(DesignToken.padding.small)
.height(1.dp)
.background(AppColors.stepperColor),
)
} else {
Spacer(Modifier.width(DesignToken.padding.small))
}
}
}
}
}
Spacer(Modifier.height(DesignToken.padding.largeIncreased))
steps[currentIndex].content()
}
}

@Preview
@Composable
fun MifosStepperDemo() {
val steps = listOf(
Step("Details") { Text("Step 1: Details Content") },
Step("Terms") { Text("Step 2: Terms Content") },
Step("Charges") { Text("Step 3: Charges Content") },
Step("Schedule") { Text("Step 4: Schedule Content") },
Step("Preview") { Text("Step 5: Preview Content") },
)

MifosStepper(
steps = steps,
currentIndex = 2,
onStepChange = { },
modifier = Modifier
.fillMaxWidth(),
)
}
1 change: 1 addition & 0 deletions feature/savings/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
plugins {
alias(libs.plugins.mifos.cmp.feature)
alias(libs.plugins.kotlin.serialization)
}

android {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@
<string name="feature_savings_savings_account_id">Savings Account Id</string>
<string name="feature_savings_transaction_view_receipt">View Receipt</string>

<string name="step_details">Details</string>
<string name="step_terms">Terms</string>
<string name="step_charges">Charges</string>
<string name="step_preview">Preview</string>
</resources>


Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.mifos.feature.savings.savingsAccountActivate.SavingsAccountActivateSc
import com.mifos.feature.savings.savingsAccountApproval.SavingsAccountApprovalScreen
import com.mifos.feature.savings.savingsAccountSummary.SavingsAccountSummaryScreen
import com.mifos.feature.savings.savingsAccountTransaction.SavingsAccountTransactionScreen
import com.mifos.feature.savings.savingsAccountv2.savingsAccountDestination
import com.mifos.room.entities.accounts.savings.SavingAccountDepositTypeEntity
import com.mifos.room.entities.accounts.savings.SavingsAccountWithAssociationsEntity

Expand Down Expand Up @@ -83,6 +84,8 @@ fun NavGraphBuilder.savingsNavGraph(
savingsAccountTransactionScreen {
onBackPressed()
}

savingsAccountDestination()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.feature.savings.savingsAccountv2

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import kotlinx.serialization.Serializable

@Serializable
data object SavingsAccountRoute

fun NavGraphBuilder.savingsAccountDestination() {
composable<SavingsAccountRoute> {
SavingsAccountScreen(
onNavigateBack = {},
onFinish = {},
)
}
}

fun NavController.navigateToSavingsAccountRoute() {
this.navigate(
SavingsAccountRoute,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.feature.savings.savingsAccountv2

import androidclient.feature.savings.generated.resources.Res
import androidclient.feature.savings.generated.resources.feature_savings_create_savings_account
import androidclient.feature.savings.generated.resources.step_charges
import androidclient.feature.savings.generated.resources.step_details
import androidclient.feature.savings.generated.resources.step_preview
import androidclient.feature.savings.generated.resources.step_terms
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mifos.core.designsystem.component.MifosScaffold
import com.mifos.core.ui.components.MifosStepper
import com.mifos.core.ui.components.Step
import com.mifos.core.ui.util.EventsEffect
import com.mifos.feature.savings.savingsAccountv2.pages.ChargesPage
import com.mifos.feature.savings.savingsAccountv2.pages.DetailsPage
import com.mifos.feature.savings.savingsAccountv2.pages.PreviewPage
import com.mifos.feature.savings.savingsAccountv2.pages.TermsPage
import org.jetbrains.compose.resources.stringResource

@Composable
internal fun SavingsAccountScreen(
onNavigateBack: () -> Unit,
onFinish: () -> Unit,
modifier: Modifier = Modifier,
viewModel: SavingsAccountViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()

EventsEffect(viewModel.eventFlow) { event ->
when (event) {
SavingsAccountEvent.NavigateBack -> onNavigateBack()
SavingsAccountEvent.Finish -> onFinish()
}
}

SavingsAccountScaffold(
modifier = modifier,
state = state,
onAction = { viewModel.trySendAction(it) },
)
}

@Composable
private fun SavingsAccountScaffold(
state: SavingsAccountState,
modifier: Modifier = Modifier,
onAction: (SavingsAccountAction) -> Unit,
) {
val steps = listOf(
Step(stringResource(Res.string.step_details)) {
DetailsPage {
onAction(SavingsAccountAction.NextStep)
}
},
Step(stringResource(Res.string.step_terms)) {
TermsPage {
onAction(SavingsAccountAction.NextStep)
}
},
Step(stringResource(Res.string.step_charges)) {
ChargesPage {
onAction(SavingsAccountAction.NextStep)
}
},
Step(stringResource(Res.string.step_preview)) {
PreviewPage {
onAction(SavingsAccountAction.NextStep)
}
},
)

MifosScaffold(
title = stringResource(Res.string.feature_savings_create_savings_account),
onBackPressed = { onAction(SavingsAccountAction.NavigateBack) },
modifier = modifier,
) { paddingValues ->
if (state.dialogState == null) {
MifosStepper(
steps = steps,
currentIndex = state.currentStep,
onStepChange = { newIndex ->
onAction(SavingsAccountAction.OnStepChange(newIndex))
},
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues),
)
}
}
}
Loading