diff --git a/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repository/UserAuthRepository.kt b/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repository/UserAuthRepository.kt index d696e30c3d..279e0c9c3c 100644 --- a/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repository/UserAuthRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repository/UserAuthRepository.kt @@ -11,18 +11,12 @@ package org.mifos.mobile.core.data.repository import org.mifos.mobile.core.common.DataState import org.mifos.mobile.core.model.entity.User +import org.mifos.mobile.core.model.entity.register.RegisterPayload interface UserAuthRepository { suspend fun registerUser( - accountNumber: String?, - authenticationMode: String?, - email: String?, - firstName: String?, - lastName: String?, - mobileNumber: String?, - password: String?, - username: String?, + registerPayload: RegisterPayload, ): DataState suspend fun login(username: String, password: String): DataState diff --git a/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/UserAuthRepositoryImp.kt b/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/UserAuthRepositoryImp.kt index 028ecc0a2c..96ab45fecc 100644 --- a/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/UserAuthRepositoryImp.kt +++ b/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/UserAuthRepositoryImp.kt @@ -31,25 +31,8 @@ class UserAuthRepositoryImp( ) : UserAuthRepository { override suspend fun registerUser( - accountNumber: String?, - authenticationMode: String?, - email: String?, - firstName: String?, - lastName: String?, - mobileNumber: String?, - password: String?, - username: String?, + registerPayload: RegisterPayload, ): DataState { - val registerPayload = RegisterPayload( - accountNumber = accountNumber, - authenticationMode = authenticationMode, - email = email, - firstName = firstName, - lastName = lastName, - mobileNumber = mobileNumber, - password = password, - username = username, - ) return withContext(ioDispatcher) { try { val response = dataManager.registrationApi.registerUser(registerPayload) diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/icon/MifosIcons.kt index 68c4882965..6ee8da9799 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/icon/MifosIcons.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/icon/MifosIcons.kt @@ -54,7 +54,10 @@ import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.SwapHoriz import androidx.compose.ui.graphics.vector.ImageVector import fluent.ui.system.icons.FluentIcons +import fluent.ui.system.icons.colored.AddCircle import fluent.ui.system.icons.colored.Alert +import fluent.ui.system.icons.colored.CoinMultiple +import fluent.ui.system.icons.colored.Savings import fluent.ui.system.icons.colored.Warning import fluent.ui.system.icons.filled.AppRecent import fluent.ui.system.icons.filled.ArchiveSettings @@ -192,7 +195,9 @@ object MifosIcons { val SearchNew = FluentIcons.Regular.Search val SavingsAccount = FluentIcons.Filled.Wallet + val SavingsAccountColor = FluentIcons.Colored.Savings val LoanAccount = FluentIcons.Filled.CoinMultiple + val LoanAccountColor = FluentIcons.Colored.CoinMultiple val ShareAccount = FluentIcons.Filled.DataWhisker val ApplyForLoan = FluentIcons.Filled.Receipt val ApplyForSavings = FluentIcons.Filled.Savings @@ -255,4 +260,6 @@ object MifosIcons { val Signature = FluentIcons.Regular.DrawShape val Camera = FluentIcons.Regular.Camera val Attach = FluentIcons.Regular.Attach + + val AddColor = FluentIcons.Colored.AddCircle } diff --git a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/register/RegisterPayload.kt b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/register/RegisterPayload.kt index bb5cd8f23e..3f14054893 100644 --- a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/register/RegisterPayload.kt +++ b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/register/RegisterPayload.kt @@ -18,6 +18,8 @@ data class RegisterPayload( val firstName: String? = null, + val middleName: String? = null, + val lastName: String? = null, val email: String? = null, diff --git a/core/ui/src/commonMain/composeResources/values/strings.xml b/core/ui/src/commonMain/composeResources/values/strings.xml index f1e2ffb06f..cee1b579f3 100644 --- a/core/ui/src/commonMain/composeResources/values/strings.xml +++ b/core/ui/src/commonMain/composeResources/values/strings.xml @@ -120,6 +120,11 @@ Pay from Available Balance - %1$s + Welcome back to Mifos + No Accounts Yet + Start your financial journey by opening your first account with us. + Open Account + Filters Reset Apply diff --git a/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosDashboardCard.kt b/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosDashboardCard.kt index cb042dfa5f..3cdf76cb13 100644 --- a/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosDashboardCard.kt +++ b/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosDashboardCard.kt @@ -16,6 +16,8 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image +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 @@ -24,9 +26,13 @@ import androidx.compose.foundation.layout.fillMaxSize 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.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -34,15 +40,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import mifos_mobile.core.ui.generated.resources.Res +import mifos_mobile.core.ui.generated.resources.feature_dashboard_no_accounts_description +import mifos_mobile.core.ui.generated.resources.feature_dashboard_no_accounts_title +import mifos_mobile.core.ui.generated.resources.feature_dashboard_open_account import mifos_mobile.core.ui.generated.resources.ic_icon_dashboard import mifos_mobile.core.ui.generated.resources.powered_by import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.designsystem.component.CardVariant import org.mifos.mobile.core.designsystem.component.MifosButton +import org.mifos.mobile.core.designsystem.component.MifosCustomCard import org.mifos.mobile.core.designsystem.icon.MifosIcons import org.mifos.mobile.core.designsystem.theme.AppColors import org.mifos.mobile.core.designsystem.theme.DesignToken @@ -54,8 +66,6 @@ import org.mifos.mobile.core.designsystem.theme.MifosTypography fun MifosDashboardCard( isVisible: Boolean, modifier: Modifier = Modifier, - isLoanApplied: Boolean = true, - onLoanApplyClick: () -> Unit = {}, isSingleLine: Boolean = false, loanAccount: StringResource? = null, loanAmount: String? = null, @@ -77,95 +87,147 @@ fun MifosDashboardCard( contentDescription = null, contentScale = ContentScale.Crop, ) - if (isLoanApplied) { - Row( + Row( + modifier = Modifier + .fillMaxWidth() + .padding(DesignToken.padding.small), + horizontalArrangement = Arrangement.spacedBy( + DesignToken.spacing.medium, + Alignment.End, + ), + ) { + Column( modifier = Modifier - .fillMaxWidth() - .padding(DesignToken.padding.small), - horizontalArrangement = Arrangement.spacedBy( - DesignToken.spacing.medium, - Alignment.End, - ), + .fillMaxSize() + .padding(DesignToken.padding.medium), + verticalArrangement = Arrangement.SpaceBetween, ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(DesignToken.padding.medium), - verticalArrangement = Arrangement.SpaceBetween, - ) { - if (loanAccount != null) { - Column { - Text( - text = stringResource(loanAccount), - style = MifosTypography.bodySmall, + if (loanAccount != null) { + Column { + Text( + text = stringResource(loanAccount), + style = MifosTypography.bodySmall, // color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.7f), - color = AppColors.customWhite.copy(alpha = 0.5f), + color = AppColors.customWhite.copy(alpha = 0.5f), + ) + AnimatedContent( + targetState = isVisible, + transitionSpec = { + fadeIn(tween(300)) togetherWith fadeOut(tween(300)) + }, + label = "Loan Amount Animation", + ) { visible -> + Text( + text = if (visible) "$loanAmount" else "$currency •••••••••", + style = MifosTypography.titleMediumEmphasized, + color = AppColors.customWhite, ) - AnimatedContent( - targetState = isVisible, - transitionSpec = { - fadeIn(tween(300)) togetherWith fadeOut(tween(300)) - }, - label = "Loan Amount Animation", - ) { visible -> - Text( - text = if (visible) "$loanAmount" else "$currency •••••••••", - style = MifosTypography.titleMediumEmphasized, - color = AppColors.customWhite, - ) - } } } + } - if (savingsAccount != null) { - Column { - Text( - text = stringResource(savingsAccount), - style = MifosTypography.bodySmall, + if (savingsAccount != null) { + Column { + Text( + text = stringResource(savingsAccount), + style = MifosTypography.bodySmall, // color = MaterialTheme.colorScheme.secondary, - color = AppColors.customWhite.copy(alpha = 0.5f), + color = AppColors.customWhite.copy(alpha = 0.5f), + ) + AnimatedContent( + targetState = isVisible, + transitionSpec = { + fadeIn(tween(300)) togetherWith fadeOut(tween(300)) + }, + label = "Savings Amount Animation", + ) { visible -> + Text( + text = if (visible) "$savingsAmount" else "$currency •••••••••", + style = MifosTypography.titleMediumEmphasized, + color = AppColors.customWhite, ) - AnimatedContent( - targetState = isVisible, - transitionSpec = { - fadeIn(tween(300)) togetherWith fadeOut(tween(300)) - }, - label = "Savings Amount Animation", - ) { visible -> - Text( - text = if (visible) "$savingsAmount" else "$currency •••••••••", - style = MifosTypography.titleMediumEmphasized, - color = AppColors.customWhite, - ) - } } } } } + } - IconButton( - onClick = onVisibilityToggle, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(12.dp), - ) { - Icon( - imageVector = if (isVisible) MifosIcons.Eye else MifosIcons.EyeOff, - contentDescription = "Toggle Visibility", - tint = Color.White, + IconButton( + onClick = onVisibilityToggle, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(12.dp), + ) { + Icon( + imageVector = if (isVisible) MifosIcons.Eye else MifosIcons.EyeOff, + contentDescription = "Toggle Visibility", + tint = Color.White, + ) + } + } +} + +@Composable +fun MifosAccountApplyDashboard( + onOpenAccountClick: () -> Unit, +) { + MifosCustomCard( + modifier = Modifier + .padding(horizontal = DesignToken.padding.largeIncreased) + .border( + 0.5.dp, + MaterialTheme.colorScheme.primary, + DesignToken.shapes.medium, + ), + variant = CardVariant.OUTLINED, + enabled = false, + onClick = onOpenAccountClick, + colors = CardDefaults.cardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Column( + modifier = Modifier + .background( + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.01f), ) - } - } else { + .fillMaxWidth() + .padding(DesignToken.padding.extraLarge), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.medium), + ) { + Icon( + imageVector = MifosIcons.AddColor, + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.size(DesignToken.sizes.iconExtraLarge), + ) + + Text( + text = stringResource(Res.string.feature_dashboard_no_accounts_title), + style = MifosTypography.titleMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + + Text( + text = stringResource(Res.string.feature_dashboard_no_accounts_description), + style = MifosTypography.bodySmall, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center, + ) + MifosButton( modifier = Modifier - .fillMaxWidth() - .height(DesignToken.sizes.buttonHeight) - .align(Alignment.Center), - onClick = onLoanApplyClick, - content = { + .wrapContentWidth() + .height(DesignToken.sizes.avatarMedium), + onClick = onOpenAccountClick, + shape = DesignToken.shapes.circle, + text = { Text( - text = "Apply Loan", - style = MifosTypography.titleMedium, + text = stringResource(Res.string.feature_dashboard_open_account), + style = MifosTypography.titleSmallEmphasized, ) }, ) @@ -219,9 +281,8 @@ private fun MifosDashboardCard() { onVisibilityToggle = {}, ) - MifosDashboardCard( - isLoanApplied = false, - isVisible = true, + MifosAccountApplyDashboard( + onOpenAccountClick = {}, ) } } diff --git a/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt b/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt index a58eaec539..827c1ec4a7 100644 --- a/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt +++ b/core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt @@ -39,7 +39,11 @@ fun MifosProgressIndicator( Res.readBytes(LottieConstants.LOADING_ANIMATION).decodeToString(), ) } - val progress by animateLottieCompositionAsState(composition) + + val progress by animateLottieCompositionAsState( + composition, + iterations = Int.MAX_VALUE, + ) Box( modifier = modifier, @@ -64,7 +68,11 @@ fun MifosProgressIndicatorOverlay( Res.readBytes("files/loading_animation.json").decodeToString(), ) } - val progress by animateLottieCompositionAsState(composition) + + val progress by animateLottieCompositionAsState( + composition, + iterations = Int.MAX_VALUE, + ) Box( modifier = modifier diff --git a/feature/auth/src/commonMain/kotlin/org/mifos/mobile/feature/auth/registration/RegistrationViewModel.kt b/feature/auth/src/commonMain/kotlin/org/mifos/mobile/feature/auth/registration/RegistrationViewModel.kt index 1464df18dc..86edabe06c 100644 --- a/feature/auth/src/commonMain/kotlin/org/mifos/mobile/feature/auth/registration/RegistrationViewModel.kt +++ b/feature/auth/src/commonMain/kotlin/org/mifos/mobile/feature/auth/registration/RegistrationViewModel.kt @@ -22,12 +22,14 @@ import mifos_mobile.feature.auth.generated.resources.feature_signup_error_first_ import mifos_mobile.feature.auth.generated.resources.feature_signup_error_invalid_email import mifos_mobile.feature.auth.generated.resources.feature_signup_error_invalid_name import mifos_mobile.feature.auth.generated.resources.feature_signup_error_last_name_empty +import mifos_mobile.feature.auth.generated.resources.feature_signup_error_middle_name_empty import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_mismatch import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_required_error import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_short import org.jetbrains.compose.resources.StringResource import org.mifos.mobile.core.common.DataState import org.mifos.mobile.core.data.repository.UserAuthRepository +import org.mifos.mobile.core.model.entity.register.RegisterPayload import org.mifos.mobile.core.ui.PasswordStrengthState import org.mifos.mobile.core.ui.utils.BaseViewModel import org.mifos.mobile.core.ui.utils.PasswordChecker @@ -186,15 +188,16 @@ class RegistrationViewModel( */ @Suppress("ReturnCount") private fun validateName(name: String, nameType: String): ValidationResult? { - if (name.isEmpty() && nameType != "middle") { + if (name.isEmpty()) { return when (nameType) { "first" -> ValidationResult.Error(Res.string.feature_signup_error_first_name_empty) + "middle" -> ValidationResult.Error(Res.string.feature_signup_error_middle_name_empty) "last" -> ValidationResult.Error(Res.string.feature_signup_error_last_name_empty) else -> ValidationResult.Error(Res.string.feature_signup_error_invalid_name) } } - if (name.isNotEmpty() && !ValidationHelper.isValidName(name)) { + if (!ValidationHelper.isValidName(name)) { return ValidationResult.Error(Res.string.feature_signup_error_invalid_name) } @@ -525,14 +528,17 @@ class RegistrationViewModel( updateState { it.copy(showOverlay = true) } viewModelScope.launch { val response = userAuthRepositoryImpl.registerUser( - accountNumber = state.customerAccount, - authenticationMode = "email", - email = state.email, - firstName = state.firstName, - lastName = state.lastName, - mobileNumber = state.mobileNumber, - password = state.password, - username = state.email, + registerPayload = RegisterPayload( + accountNumber = state.customerAccount, + authenticationMode = "email", + email = state.email, + firstName = state.firstName, + middleName = state.middleName, + lastName = state.lastName, + mobileNumber = state.mobileNumber, + password = state.password, + username = state.email, + ), ) sendAction( SignUpAction.Internal.ReceiveRegisterResult( @@ -626,6 +632,7 @@ data class SignUpState( val isSubmitButtonEnabled: Boolean get() = customerAccount.isNotBlank() && firstName.isNotBlank() && + middleName.isNotBlank() && lastName.isNotBlank() && email.isNotBlank() && password.isNotBlank() && diff --git a/feature/home/src/commonMain/composeResources/values/strings.xml b/feature/home/src/commonMain/composeResources/values/strings.xml index 58e5697a65..2e0d653ef8 100644 --- a/feature/home/src/commonMain/composeResources/values/strings.xml +++ b/feature/home/src/commonMain/composeResources/values/strings.xml @@ -54,6 +54,11 @@ Beneficiary FAQ + Savings Account + Loan Account + Earn interest on your deposits with flexible access to your money anytime. + Get the funds you need for personal, business, or educational purposes. + Total Available Savings Total Available Loan diff --git a/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeScreen.kt b/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeScreen.kt index c80e08c9f1..a32810f8c4 100644 --- a/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeScreen.kt +++ b/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeScreen.kt @@ -15,17 +15,24 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow 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.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -53,10 +60,12 @@ import org.mifos.mobile.core.designsystem.icon.MifosIcons import org.mifos.mobile.core.designsystem.theme.DesignToken import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.designsystem.theme.MifosTypography +import org.mifos.mobile.core.ui.component.MifosAccountApplyDashboard import org.mifos.mobile.core.ui.component.MifosDashboardCard import org.mifos.mobile.core.ui.component.MifosErrorComponent import org.mifos.mobile.core.ui.component.MifosProgressIndicator import org.mifos.mobile.core.ui.utils.EventsEffect +import org.mifos.mobile.feature.home.components.BottomSheetContent import org.mifos.mobile.feature.home.navigation.HomeNavigationDestination import org.mifos.mobile.feature.home.navigation.HomeNavigator @@ -161,10 +170,10 @@ internal fun HomeContent( HomeScreenState.Success -> { Column( modifier = Modifier + .verticalScroll(rememberScrollState()) .padding(DesignToken.padding.large), ) { Spacer(modifier = Modifier.height(DesignToken.spacing.small)) - Text( text = stringResource( Res.string.feature_home_greet, @@ -176,16 +185,21 @@ internal fun HomeContent( Spacer(modifier = Modifier.height(DesignToken.spacing.large)) - MifosDashboardCard( - isLoanApplied = state.isLoanApplied, - savingsAccount = Res.string.feature_home_total_available_savings, - loanAccount = Res.string.feature_home_total_available_loan, - loanAmount = state.loanAmount, - savingsAmount = state.savingsAmount, - isVisible = state.isAmountVisible, - onVisibilityToggle = { onAction(HomeAction.ToggleAmountVisible) }, - currency = state.currency, - ) + if (state.isAccountsPresent) { + MifosDashboardCard( + savingsAccount = Res.string.feature_home_total_available_savings, + loanAccount = Res.string.feature_home_total_available_loan, + loanAmount = state.loanAmount, + savingsAmount = state.savingsAmount, + isVisible = state.isAmountVisible, + onVisibilityToggle = { onAction(HomeAction.ToggleAmountVisible) }, + currency = state.currency, + ) + } else { + MifosAccountApplyDashboard( + onOpenAccountClick = { onAction(HomeAction.BottomBarPicker) }, + ) + } Spacer(modifier = Modifier.height(DesignToken.spacing.extraLarge)) @@ -203,6 +217,7 @@ internal fun HomeContent( ) } } + null -> {} } } @@ -214,21 +229,27 @@ internal fun ServiceBox( onAction: (HomeAction) -> Unit, modifier: Modifier = Modifier, ) { - LazyVerticalGrid( - columns = GridCells.Fixed(4), - verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.medium), + FlowRow( + modifier = modifier + .fillMaxWidth(), + maxItemsInEachRow = 4, horizontalArrangement = Arrangement.spacedBy(DesignToken.spacing.medium), - modifier = modifier, - content = { - items(items) { item -> + verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.medium), + ) { + items.forEach { item -> + Box( + modifier = Modifier + .weight(1f), + contentAlignment = Alignment.Center, + ) { ServiceItemCard( title = item.title, icon = item.icon, onClick = { onAction(HomeAction.OnNavigate(item.route)) }, ) } - }, - ) + } + } } @Composable @@ -273,6 +294,7 @@ internal fun ServiceItemCard( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun HomeScreenDialog( dialogState: HomeState.DialogState?, @@ -287,6 +309,35 @@ private fun HomeScreenDialog( ) } + is HomeState.DialogState.ShowAccountApplyBottomBar -> { + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = dialogState.isVisible, + ) + + LaunchedEffect(dialogState.isVisible) { + if (dialogState.isVisible) { + sheetState.expand() + } + } + + ModalBottomSheet( + onDismissRequest = { + onAction(HomeAction.OnDismissDialog) + }, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + contentWindowInsets = { + BottomSheetDefaults.windowInsets + }, + modifier = Modifier.wrapContentHeight(), + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + BottomSheetContent( + onAction = onAction, + isVisible = dialogState.isVisible, + ) + } + } null -> Unit } } diff --git a/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeViewModel.kt b/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeViewModel.kt index ca2455426b..f5630556ef 100644 --- a/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeViewModel.kt +++ b/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/HomeViewModel.kt @@ -98,6 +98,16 @@ internal class HomeViewModel( action.dataState, ) + is HomeAction.BottomBarPicker -> { + updateState { + it.copy( + dialogState = HomeState.DialogState.ShowAccountApplyBottomBar( + isVisible = true, + ), + ) + } + } + is HomeAction.Internal.ReceiveClientDetails -> handleClientDetails(action.dataState) } } @@ -294,8 +304,7 @@ internal class HomeViewModel( } else { updateState { it.copy( - dialogState = null, - isLoanApplied = false, + isAccountsPresent = false, uiState = HomeScreenState.Success, ) } @@ -374,7 +383,7 @@ internal class HomeViewModel( * @property clientId The ID of the current client. * @property firstName The first name of the client, or an empty string. * @property currency The currency symbol for the client's accounts, or `null`. - * @property isLoanApplied A boolean indicating if the client has any active loans. + * @property isAccountsPresent A boolean indicating if the client has any active loans. * @property username The username of the currently logged-in user. * @property clientAccounts The full account details of the client, or `null`. * @property notificationCount The number of unread notifications. @@ -392,7 +401,7 @@ internal data class HomeState( val firstName: String? = "", val currency: String? = "", val decimals: Int = 2, - val isLoanApplied: Boolean = true, + val isAccountsPresent: Boolean = false, val username: String = "", val clientAccounts: ClientAccounts? = null, val notificationCount: Int = 0, @@ -415,6 +424,12 @@ internal data class HomeState( * @property message The [StringResource] for the error message. */ data class Error(val message: StringResource) : DialogState + + /** + * Represents a modal bottom sheet for Account options. + * @property isVisible A boolean to control the content shown inside the sheet. + */ + data class ShowAccountApplyBottomBar(val isVisible: Boolean) : DialogState } } @@ -482,6 +497,9 @@ sealed interface HomeAction { /** Action to retry fetching data after an error or network issue. */ data object Retry : HomeAction + /** Action to trigger that display Bottom bar for applying to an account */ + data object BottomBarPicker : HomeAction + /** * A sealed interface for internal actions, which are not triggered directly by the UI. */ diff --git a/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/components/BottomSheetContent.kt b/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/components/BottomSheetContent.kt new file mode 100644 index 0000000000..46e0622105 --- /dev/null +++ b/feature/home/src/commonMain/kotlin/org/mifos/mobile/feature/home/components/BottomSheetContent.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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.home.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +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.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +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 mifos_mobile.feature.home.generated.resources.Res +import mifos_mobile.feature.home.generated.resources.feature_home_loan_account +import mifos_mobile.feature.home.generated.resources.feature_home_loan_tip +import mifos_mobile.feature.home.generated.resources.feature_home_saving_account +import mifos_mobile.feature.home.generated.resources.feature_home_savings_tip +import org.jetbrains.compose.resources.stringResource +import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.designsystem.component.CardVariant +import org.mifos.mobile.core.designsystem.component.MifosCustomCard +import org.mifos.mobile.core.designsystem.icon.MifosIcons +import org.mifos.mobile.core.designsystem.theme.DesignToken +import org.mifos.mobile.core.designsystem.theme.MifosTypography +import org.mifos.mobile.feature.home.HomeAction + +@Composable +internal fun BottomSheetContent( + onAction: (HomeAction) -> Unit, + modifier: Modifier = Modifier, + isVisible: Boolean = false, +) { + AnimatedContent( + targetState = isVisible, + transitionSpec = { + slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing), + ) + fadeIn( + animationSpec = tween(durationMillis = 300, delayMillis = 100), + ) togetherWith slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + ) + fadeOut( + animationSpec = tween(durationMillis = 200), + ) + }, + modifier = modifier, + ) { + Column( + modifier = Modifier.padding(horizontal = DesignToken.padding.large), + verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.medium), + ) { + BottomSheetIconContainer( + onClick = { + when (it) { + BottomSheetItemType.LOAN -> onAction(HomeAction.OnNavigate(Constants.APPLY_LOAN)) + BottomSheetItemType.SAVINGS -> onAction(HomeAction.OnNavigate(Constants.APPLY_SAVINGS)) + } + }, + ) + } + } +} + +@Composable +internal fun BottomSheetIconContainer( + onClick: (BottomSheetItemType) -> Unit, +) { + BottomSheetItemType.entries.forEach { it -> + MifosCustomCard( + modifier = Modifier + .fillMaxWidth() + .border( + 1.dp, + MaterialTheme.colorScheme.secondaryContainer, + DesignToken.shapes.medium, + ), + onClick = { onClick(it) }, + variant = CardVariant.OUTLINED, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(DesignToken.padding.large), + ) { + Box( + modifier = Modifier.size(DesignToken.sizes.inputHeight) + .border( + 1.dp, + MaterialTheme.colorScheme.secondaryContainer, + DesignToken.shapes.medium, + ), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.size(DesignToken.sizes.iconMedium), + imageVector = when (it) { + BottomSheetItemType.LOAN -> MifosIcons.LoanAccountColor + BottomSheetItemType.SAVINGS -> MifosIcons.SavingsAccountColor + }, + contentDescription = null, + ) + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.small), + modifier = Modifier.padding(horizontal = DesignToken.padding.large), + ) { + Text( + text = when (it) { + BottomSheetItemType.LOAN -> stringResource(Res.string.feature_home_loan_account) + BottomSheetItemType.SAVINGS -> stringResource(Res.string.feature_home_saving_account) + }, + style = MifosTypography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface, + ) + + Text( + text = when (it) { + BottomSheetItemType.LOAN -> stringResource(Res.string.feature_home_loan_tip) + BottomSheetItemType.SAVINGS -> stringResource(Res.string.feature_home_savings_tip) + }, + style = MifosTypography.bodySmallEmphasized, + color = MaterialTheme.colorScheme.secondary, + ) + } + } + } + } +} + +enum class BottomSheetItemType { + SAVINGS, + LOAN, +} diff --git a/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/loanAccount/LoanAccountScreen.kt b/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/loanAccount/LoanAccountScreen.kt index 25327228e5..8c89b5c9c5 100644 --- a/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/loanAccount/LoanAccountScreen.kt +++ b/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/loanAccount/LoanAccountScreen.kt @@ -178,10 +178,10 @@ internal fun LoanAccountContent( Spacer(modifier = Modifier.height(DesignToken.spacing.large)) MifosDashboardCard( + isVisible = state.isAmountVisible, isSingleLine = true, savingsAccount = Res.string.feature_loan_account_dashboard, savingsAmount = state.totalLoanAmount, - isVisible = state.isAmountVisible, currency = state.currency, onVisibilityToggle = { onAction(LoanAccountsAction.ToggleAmountVisible)