diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Color.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Color.kt index 1dbef938154..43035909dc7 100644 --- a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Color.kt @@ -247,4 +247,5 @@ object AppColors { val lightRed = Color(0xFFFF6E6E) val peach = Color(0xFFFF926E) val lightPurple = Color(0xFF706EFF) + val stepperColor = Color(0xFF9ECAFC) } diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt index 738be3516e7..eab37f2051a 100644 --- a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt @@ -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, diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosStepper.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosStepper.kt new file mode 100644 index 00000000000..79b1a1cf9c5 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosStepper.kt @@ -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, + 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(), + ) +} diff --git a/feature/savings/build.gradle.kts b/feature/savings/build.gradle.kts index e1b16d1537a..53f95809bf9 100644 --- a/feature/savings/build.gradle.kts +++ b/feature/savings/build.gradle.kts @@ -9,6 +9,7 @@ */ plugins { alias(libs.plugins.mifos.cmp.feature) + alias(libs.plugins.kotlin.serialization) } android { diff --git a/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml b/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml index c7cd6ea52f4..aae0d047649 100644 --- a/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml +++ b/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml @@ -95,6 +95,10 @@ Savings Account Id View Receipt + Details + Terms + Charges + Preview diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt index eefad9bdee9..5c5e9997bb3 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt @@ -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 @@ -83,6 +84,8 @@ fun NavGraphBuilder.savingsNavGraph( savingsAccountTransactionScreen { onBackPressed() } + + savingsAccountDestination() } } diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountRoute.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountRoute.kt new file mode 100644 index 00000000000..453e0d2282e --- /dev/null +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountRoute.kt @@ -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 { + SavingsAccountScreen( + onNavigateBack = {}, + onFinish = {}, + ) + } +} + +fun NavController.navigateToSavingsAccountRoute() { + this.navigate( + SavingsAccountRoute, + ) +} diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt new file mode 100644 index 00000000000..d321d74c123 --- /dev/null +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt @@ -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), + ) + } + } +} diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountViewModel.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountViewModel.kt new file mode 100644 index 00000000000..2c36356e21c --- /dev/null +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountViewModel.kt @@ -0,0 +1,66 @@ +/* + * 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 com.mifos.core.ui.util.BaseViewModel +import kotlinx.coroutines.flow.update + +internal class SavingsAccountViewModel : + BaseViewModel( + initialState = SavingsAccountState(), + ) { + + override fun handleAction(action: SavingsAccountAction) { + when (action) { + SavingsAccountAction.NavigateBack -> sendEvent(SavingsAccountEvent.NavigateBack) + SavingsAccountAction.NextStep -> moveToNextStep() + SavingsAccountAction.Finish -> sendEvent(SavingsAccountEvent.Finish) + is SavingsAccountAction.OnStepChange -> + mutableStateFlow.value = + mutableStateFlow.value.copy(currentStep = action.newIndex) + } + } + + private fun moveToNextStep() { + val current = state.currentStep + if (current < state.totalSteps) { + mutableStateFlow.update { + it.copy( + currentStep = current + 1, + ) + } + } else { + sendEvent(SavingsAccountEvent.Finish) + } + } +} + +data class SavingsAccountState( + val currentStep: Int = 0, + val totalSteps: Int = 4, + val dialogState: DialogState? = null, +) { + sealed interface DialogState { + data class Error(val message: String) : DialogState + data object Loading : DialogState + } +} + +sealed interface SavingsAccountEvent { + data object NavigateBack : SavingsAccountEvent + data object Finish : SavingsAccountEvent +} + +sealed interface SavingsAccountAction { + data object NavigateBack : SavingsAccountAction + data object NextStep : SavingsAccountAction + data object Finish : SavingsAccountAction + data class OnStepChange(val newIndex: Int) : SavingsAccountAction +} diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/ChargesPage.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/ChargesPage.kt new file mode 100644 index 00000000000..377c7ba2775 --- /dev/null +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/ChargesPage.kt @@ -0,0 +1,35 @@ +/* + * 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.pages + +import androidclient.feature.savings.generated.resources.Res +import androidclient.feature.savings.generated.resources.feature_savings_submit +import androidclient.feature.savings.generated.resources.step_charges +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource + +@Composable +fun ChargesPage(onNext: () -> Unit) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(stringResource(Res.string.step_charges)) + Spacer(Modifier.height(8.dp)) + Button(onClick = onNext) { + Text(stringResource(Res.string.feature_savings_submit)) + } + } +} diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/DetailsPage.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/DetailsPage.kt new file mode 100644 index 00000000000..317df67dad3 --- /dev/null +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/DetailsPage.kt @@ -0,0 +1,35 @@ +/* + * 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.pages + +import androidclient.feature.savings.generated.resources.Res +import androidclient.feature.savings.generated.resources.feature_savings_submit +import androidclient.feature.savings.generated.resources.step_details +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource + +@Composable +fun DetailsPage(onNext: () -> Unit) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(stringResource(Res.string.step_details)) + Spacer(Modifier.height(8.dp)) + Button(onClick = onNext) { + Text(stringResource(Res.string.feature_savings_submit)) + } + } +} diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/PreviewPage.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/PreviewPage.kt new file mode 100644 index 00000000000..8c6b124c746 --- /dev/null +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/PreviewPage.kt @@ -0,0 +1,35 @@ +/* + * 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.pages + +import androidclient.feature.savings.generated.resources.Res +import androidclient.feature.savings.generated.resources.feature_savings_submit +import androidclient.feature.savings.generated.resources.step_preview +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource + +@Composable +fun PreviewPage(onNext: () -> Unit) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(stringResource(Res.string.step_preview)) + Spacer(Modifier.height(8.dp)) + Button(onClick = onNext) { + Text(stringResource(Res.string.feature_savings_submit)) + } + } +} diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/TermsPage.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/TermsPage.kt new file mode 100644 index 00000000000..e335bee6bef --- /dev/null +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/TermsPage.kt @@ -0,0 +1,35 @@ +/* + * 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.pages + +import androidclient.feature.savings.generated.resources.Res +import androidclient.feature.savings.generated.resources.feature_savings_submit +import androidclient.feature.savings.generated.resources.step_terms +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource + +@Composable +fun TermsPage(onNext: () -> Unit) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(stringResource(Res.string.step_terms)) + Spacer(Modifier.height(8.dp)) + Button(onClick = onNext) { + Text(stringResource(Res.string.feature_savings_submit)) + } + } +}