From 29818f2210283e7ce4c90f8f2016f486c0dc9bef Mon Sep 17 00:00:00 2001 From: Aryan Baglane Date: Sun, 2 Nov 2025 15:33:44 +0530 Subject: [PATCH 1/2] documentation update for the feature/settings --- .../feature/settings/about/AboutNavigation.kt | 10 ++ .../feature/settings/about/AboutScreen.kt | 24 ++- .../settings/appInfo/AppInfoNavigation.kt | 15 ++ .../feature/settings/appInfo/AppInfoScreen.kt | 22 ++- .../settings/componenets/MifosLogoutDilaog.kt | 27 +++ .../settings/componenets/SettingsItems.kt | 25 +++ .../feature/settings/di/SettingsModule.kt | 5 + .../feature/settings/faq/FaqNavigation.kt | 14 ++ .../mobile/feature/settings/faq/FaqScreen.kt | 28 ++++ .../feature/settings/faq/FaqViewmodel.kt | 43 ++++- .../feature/settings/help/HelpNavigation.kt | 14 ++ .../feature/settings/help/HelpScreen.kt | 93 +++++++++++ .../settings/language/LanguageRoute.kt | 12 ++ .../settings/language/LanguageScreen.kt | 27 +++ .../navigation/SettingsNavGraphRoute.kt | 24 +++ .../passcode/UpdatePasscodeNavigation.kt | 12 ++ .../settings/passcode/UpdatePasscodeScreen.kt | 30 ++++ .../passcode/UpdatePasscodeViewModel.kt | 119 +++++++++++++- .../password/ChangePasswordNavigation.kt | 17 ++ .../settings/password/ChangePasswordScreen.kt | 32 ++++ .../password/ChangePasswordViewModel.kt | 155 +++++++++++++++++- .../settings/settings/SettingsRoute.kt | 24 +++ .../settings/settings/SettingsScreen.kt | 45 ++++- .../settings/theme/ChangeThemeRoute.kt | 19 +++ .../settings/theme/ChangeThemeScreen.kt | 27 +++ 25 files changed, 850 insertions(+), 13 deletions(-) diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutNavigation.kt index d31fd2acc9..d3662fa41c 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutNavigation.kt @@ -15,6 +15,11 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Defines the "About Us" screen destination in the navigation graph. + * + * @param onBackClick Lambda to handle back navigation. + */ internal fun NavGraphBuilder.aboutDestination( onBackClick: () -> Unit, ) { @@ -25,5 +30,10 @@ internal fun NavGraphBuilder.aboutDestination( } } +/** + * Navigates to the "About Us" screen. + * + * @param navOptions Optional navigation options. + */ internal fun NavController.navigateToAbout(navOptions: NavOptions? = null) = navigate(SettingsItems.AboutUs, navOptions) diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt index 39f62cffbc..e0025dab4a 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt @@ -49,6 +49,14 @@ import org.mifos.mobile.core.designsystem.theme.DesignToken import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.designsystem.theme.MifosTypography +/** + * The main entry point for the "About Us" screen. This composable function serves as a wrapper + * around the actual content, allowing for separation of concerns and easier testing. + * + * @param modifier The [Modifier] to be applied to the screen. + * @param onBackClick A lambda function to be invoked when the back button is pressed, + * triggering navigation to the previous screen. + */ @Composable fun AboutScreen( modifier: Modifier = Modifier, @@ -60,6 +68,13 @@ fun AboutScreen( ) } +/** + * Renders the UI content for the "About Us" screen. This includes the screen's scaffold, + * top bar, and the informational content about the Mifos Initiative. + * + * @param modifier The [Modifier] to be applied to the content layout. + * @param onBackClick A lambda function to handle the back navigation event from the scaffold. + */ @Composable internal fun AboutScreenContent( modifier: Modifier = Modifier, @@ -99,7 +114,7 @@ internal fun AboutScreenContent( Image( painter = painterResource(Res.drawable.mifos_icon), contentDescription = - stringResource(Res.string.feature_settings_about_logo_content_description), + stringResource(Res.string.feature_settings_about_logo_content_description), modifier = Modifier .size(DesignToken.sizes.iconExtraLarge), ) @@ -145,7 +160,7 @@ internal fun AboutScreenContent( } Image( painter = - painterResource(Res.drawable.ic_icon_money_transfer), + painterResource(Res.drawable.ic_icon_money_transfer), contentDescription = null, modifier = Modifier.fillMaxWidth(), ) @@ -154,6 +169,11 @@ internal fun AboutScreenContent( } } +/** + * A Jetpack Compose preview function for the [AboutScreenContent]. This allows for + * visualizing the UI component in Android Studio's preview pane without needing to + * run the entire application. + */ @Preview @Composable internal fun AboutScreenContentPreview() { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoNavigation.kt index f11817b341..7290877485 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoNavigation.kt @@ -15,9 +15,24 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Navigates to the App Info screen. This is an extension function on [NavController] + * that simplifies the process of navigating to the app info destination. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation, + * allowing for customization of aspects like launch modes and animations. + */ internal fun NavController.navigateToAppInfo(navOptions: NavOptions? = null) = navigate(SettingsItems.AppInfo, navOptions) +/** + * Defines the composable destination for the "App Info" screen within the navigation graph. + * This sets up the route and the content to be displayed, along with screen transitions. + * + * @param onBackClick A lambda function to be invoked when the user initiates a back action. + * @param navigateToPrivacyPolicy A lambda function to navigate to the Privacy Policy screen. + * @param navigateToTermsAndConditions A lambda function to navigate to the Terms and Conditions screen. + */ internal fun NavGraphBuilder.appInfoDestination( onBackClick: () -> Unit, navigateToPrivacyPolicy: () -> Unit, diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt index f8d961f95b..eaeae10c95 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt @@ -48,6 +48,15 @@ import org.mifos.mobile.core.designsystem.theme.DesignToken import org.mifos.mobile.core.designsystem.theme.MifosTypography import org.mifos.mobile.feature.settings.util.appVersion +/** + * The main composable for the App Info screen, which acts as a stateful wrapper + * around the core UI content. + * + * @param onBackClick Lambda to handle back navigation events. + * @param navigateToPrivacyPolicy Lambda to navigate to the Privacy Policy screen. + * @param modifier The [Modifier] to be applied to this screen. + * @param navigateToTermsAndConditions Lambda to navigate to the Terms and Conditions screen. + */ @Composable internal fun AppInfoScreen( onBackClick: () -> Unit, @@ -63,6 +72,15 @@ internal fun AppInfoScreen( ) } +/** + * Renders the stateless UI content for the App Info screen. This includes details + * about the app, version, and links to legal documents. + * + * @param onBackClick Lambda to handle back navigation from the top bar. + * @param navigateToPrivacyPolicy Lambda to navigate to the Privacy Policy screen. + * @param modifier The [Modifier] to be applied to the layout. + * @param navigateToTermsAndConditions Lambda to navigate to the Terms and Conditions screen. + */ @Suppress("UnusedParameter") @Composable internal fun AppInfoContent( @@ -112,7 +130,7 @@ internal fun AppInfoContent( Image( painter = painterResource(Res.drawable.mifos_icon), contentDescription = - stringResource(Res.string.feature_settings_about_logo_content_description), + stringResource(Res.string.feature_settings_about_logo_content_description), modifier = Modifier .size(DesignToken.sizes.iconExtraLarge), ) @@ -164,7 +182,7 @@ internal fun AppInfoContent( Image( painter = painterResource(Res.drawable.mifo_app_info_icon), contentDescription = - stringResource(Res.string.feature_settings_appinfo_logo_content_description), + stringResource(Res.string.feature_settings_appinfo_logo_content_description), modifier = Modifier .size(150.dp) .align(Alignment.BottomEnd).zIndex(0f), diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/MifosLogoutDilaog.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/MifosLogoutDilaog.kt index 613cf3c936..d667db6868 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/MifosLogoutDilaog.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/MifosLogoutDilaog.kt @@ -37,6 +37,13 @@ import org.mifos.mobile.core.designsystem.theme.AppColors import org.mifos.mobile.core.designsystem.theme.DesignToken import org.mifos.mobile.core.designsystem.theme.MifosTypography +/** + * A composable function that displays a confirmation dialog for logging out. + * The dialog's visibility and content are controlled by the [LogoutDialogState]. + * + * @param visibilityState The state that determines whether the dialog is shown or hidden, + * and provides the necessary content and actions. + */ @Composable fun MifosLogoutDialog( visibilityState: LogoutDialogState, @@ -117,10 +124,27 @@ fun MifosLogoutDialog( } } +/** + * Represents the state of the [MifosLogoutDialog]. + */ sealed interface LogoutDialogState { + /** + * The dialog is hidden. + */ data object Hidden : LogoutDialogState + /** + * The dialog is visible. + * + * @param description The main descriptive text of the dialog. + * @param title The title of the dialog. + * @param message A message typically shown below the main action button. + * @param messageActionText Clickable text accompanying the message. + * @param onLogout Lambda to be executed when the logout button is clicked. + * @param onNavigateToHome Lambda to be executed when the message action text is clicked. + * @param onDismiss Lambda to be executed when the dialog is dismissed. + */ data class Shown( val description: StringResource, val title: StringResource, @@ -132,6 +156,9 @@ sealed interface LogoutDialogState { ) : LogoutDialogState } +/** + * A Jetpack Compose preview for the [MifosLogoutDialog]. + */ @Preview @Composable fun MifosLogoutDialogPreview() { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/SettingsItems.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/SettingsItems.kt index 15c42fde20..bfd5e812c7 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/SettingsItems.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/componenets/SettingsItems.kt @@ -37,6 +37,18 @@ import org.jetbrains.compose.resources.StringResource import org.mifos.mobile.core.common.Constants import org.mifos.mobile.core.designsystem.icon.MifosIcons +/** + * A sealed class representing all navigable items available on the settings screen. + * Each object holds metadata for a specific setting, such as its title, subtitle, icon, + * and navigation route, making it easy to generate UI components dynamically. + * + * This class is serializable to support navigation graph persistence. + * + * @property title The string resource for the setting's title. + * @property subTitle The string resource for the setting's descriptive subtitle. + * @property icon The vector graphic icon representing the setting. + * @property route The unique navigation route string for the destination screen. + */ @Serializable sealed class SettingsItems( @Contextual val title: StringResource, @@ -53,6 +65,7 @@ sealed class SettingsItems( // route = Constants.PROFILE, // ) + /** Represents the 'Change Password' setting. */ @Serializable data object Password : SettingsItems( title = Res.string.feature_settings_action_password, @@ -61,6 +74,7 @@ sealed class SettingsItems( route = Constants.PASSWORD, ) + /** Represents the 'Set Passcode' setting for app authentication. */ @Serializable data object AuthPasscode : SettingsItems( title = Res.string.feature_settings_action_auth_passcode, @@ -69,6 +83,7 @@ sealed class SettingsItems( route = Constants.AUTH_PASSCODE, ) + /** Represents the 'Language' selection setting. */ @Serializable data object Language : SettingsItems( title = Res.string.feature_settings_action_language, @@ -78,6 +93,7 @@ sealed class SettingsItems( ) // TODO : uncomment once ui/ux team provide a valid colours for dark theme + /** Represents the 'Display Theme' setting (e.g., light/dark mode). */ @Serializable data object Theme : SettingsItems( title = Res.string.feature_settings_action_theme, @@ -94,6 +110,7 @@ sealed class SettingsItems( // route = Constants.ENDPOINT, // ) + /** Represents the 'About Us' information screen. */ @Serializable data object AboutUs : SettingsItems( title = Res.string.feature_settings_action_about_us, @@ -102,6 +119,7 @@ sealed class SettingsItems( route = Constants.ABOUT_US, ) + /** Represents the 'FAQ' (Frequently Asked Questions) screen. */ @Serializable data object FAQ : SettingsItems( title = Res.string.feature_settings_action_faq, @@ -110,6 +128,7 @@ sealed class SettingsItems( route = Constants.FAQ, ) + /** Represents the 'Help' or support screen. */ @Serializable data object Help : SettingsItems( title = Res.string.feature_settings_action_help, @@ -118,6 +137,7 @@ sealed class SettingsItems( route = Constants.HELP, ) + /** Represents the 'App Info' screen with version and legal details. */ @Serializable data object AppInfo : SettingsItems( title = Res.string.feature_settings_action_app_info, @@ -126,6 +146,7 @@ sealed class SettingsItems( route = Constants.APP_INFO, ) + /** Represents the 'Logout' action. */ @Serializable data object Logout : SettingsItems( title = Res.string.feature_settings_action_logout, @@ -135,6 +156,10 @@ sealed class SettingsItems( ) } +/** + * An immutable list defining the order and content of items displayed on the settings screen. + * This list is used to dynamically render the settings menu. + */ internal val settingsItems: ImmutableList = persistentListOf( // SettingsItems.Profile, SettingsItems.Password, diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/di/SettingsModule.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/di/SettingsModule.kt index d0a82f2696..97bd675c72 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/di/SettingsModule.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/di/SettingsModule.kt @@ -18,6 +18,11 @@ import org.mifos.mobile.feature.settings.password.ChangePasswordViewModel import org.mifos.mobile.feature.settings.settings.SettingsViewModel import org.mifos.mobile.feature.settings.theme.ChangeThemeViewModel +/** + * Koin module for providing dependencies related to the settings feature. + * This module declares all the ViewModels used across the settings screens, + * making them available for dependency injection. + */ val SettingsModule = module { viewModelOf(::SettingsViewModel) viewModelOf(::UpdatePasscodeViewModel) diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqNavigation.kt index 08bdde88c8..6a32bcf538 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqNavigation.kt @@ -15,6 +15,13 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Defines the composable destination for the "FAQ" (Frequently Asked Questions) screen + * within the navigation graph. + * + * @param onBackClick A lambda function to be invoked when the user initiates a back action. + * @param contact A lambda function to handle navigation to a contact or help screen. + */ fun NavGraphBuilder.faqDestination( onBackClick: () -> Unit, contact: () -> Unit, @@ -26,5 +33,12 @@ fun NavGraphBuilder.faqDestination( ) } } + +/** + * Navigates to the "FAQ" screen. This is an extension function on [NavController] + * that simplifies the process of navigating to the FAQ destination. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ fun NavController.navigateToFaq(navOptions: NavOptions? = null) = navigate(SettingsItems.FAQ, navOptions) diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqScreen.kt index 81d2e90962..ce5a6ae17d 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqScreen.kt @@ -48,6 +48,15 @@ import org.mifos.mobile.core.ui.component.FaqItemHolder import org.mifos.mobile.core.ui.utils.DevicePreview import org.mifos.mobile.core.ui.utils.EventsEffect +/** + * A stateful composable that displays the FAQ screen. It collects state and events from + * the [FaqViewModel] and delegates the UI rendering to [FaqScreenContent]. + * + * @param onNavigateBack Callback to handle back navigation. + * @param onClickHelp Callback to navigate to the help/contact screen. + * @param modifier The [Modifier] to be applied to this screen. + * @param viewModel The ViewModel responsible for the screen's logic and state. + */ @Composable internal fun FaqScreen( onNavigateBack: () -> Unit, @@ -71,6 +80,13 @@ internal fun FaqScreen( ) } +/** + * A stateless composable that renders the UI for the FAQ screen based on the provided [uiState]. + * + * @param uiState The current state of the FAQ screen. + * @param onAction Callback to send actions to the ViewModel. + * @param modifier The [Modifier] to be applied to the layout. + */ @Composable private fun FaqScreenContent( uiState: FaqState, @@ -95,6 +111,14 @@ private fun FaqScreenContent( ) } +/** + * Renders the main content of the FAQ screen, including the list of questions and answers + * or an empty state view if no FAQs are available. + * + * @param faqArrayList The list of [FAQ] items to display. + * @param selectedFaqPosition The index of the currently expanded FAQ item. + * @param onAction Callback to send actions to the ViewModel. + */ @Composable private fun FaqContent( faqArrayList: List, @@ -150,6 +174,10 @@ private fun FaqContent( } } +/** + * A Jetpack Compose preview for the [FaqScreenContent]. This allows for + * visualizing the UI component in Android Studio's preview pane. + */ @DevicePreview @Composable fun FaqScreenPreview() { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt index d0d4307e04..95ec996689 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt @@ -16,18 +16,28 @@ import mifos_mobile.feature.settings.generated.resources.Res import mifos_mobile.feature.settings.generated.resources.faq_ans import mifos_mobile.feature.settings.generated.resources.faq_qs import org.jetbrains.compose.resources.getStringArray -import org.mifos.mobile.core.common.DataState.Loading.data import org.mifos.mobile.core.model.entity.FAQ import org.mifos.mobile.core.ui.utils.BaseViewModel +/** + * ViewModel for the FAQ screen. It is responsible for loading the list of frequently + * asked questions, handling user interactions, and managing the UI state. + */ internal class FaqViewModel : BaseViewModel( initialState = FaqState(emptyList()), ) { init { + // Automatically triggers loading the FAQ list when the ViewModel is created. viewModelScope.launch { sendAction(FaqAction.Internal.LoadFaqList) } } + + /** + * Processes incoming [FaqAction]s to update the state or send events. + * + * @param action The action to be handled. + */ override fun handleAction(action: FaqAction) { when (action) { FaqAction.NavigateBack -> { @@ -45,6 +55,11 @@ internal class FaqViewModel : BaseViewModel( } } } + + /** + * Loads the questions and answers from the string array resources, + * pairs them into a list of [FAQ] objects, and updates the state. + */ private fun loadFaqList() { viewModelScope.launch { val questions = getStringArray(Res.array.faq_qs).map { @@ -66,21 +81,47 @@ internal class FaqViewModel : BaseViewModel( } } +/** + * Represents the state of the FAQ screen. + * + * @property faqList The list of frequently asked questions. + * @property selectedFaqPosition The index of the currently expanded FAQ item. + */ internal data class FaqState( val faqList: List = emptyList(), val selectedFaqPosition: Int = 0, ) + +/** + * Defines one-time events that can be sent from the ViewModel to the UI. + */ internal sealed interface FaqEvent { + /** Signals that the UI should navigate back. */ data object OnNavigateBack : FaqEvent + /** Signals that the UI should navigate to the help screen. */ data object OnNavigateToHelp : FaqEvent } + +/** + * Defines all possible actions that can be sent from the UI to the ViewModel. + */ internal sealed interface FaqAction { + /** An action to navigate back. */ data object NavigateBack : FaqAction + /** An action to navigate to the help screen. */ data object NavigateToHelp : FaqAction + /** + * An action to update the index of the currently selected (expanded) FAQ. + * @param position The new position index. + */ data class UpdateFaqPosition(val position: Int) : FaqAction + /** + * Defines internal actions that are not directly triggered by the user. + */ sealed interface Internal : FaqAction { + /** An internal action to trigger loading the FAQ list. */ data object LoadFaqList : Internal } } diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt index 7f95ae1e70..0d62c59634 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt @@ -15,9 +15,22 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Navigates to the "Help" screen. This is an extension function on [NavController] + * that simplifies the process of navigating to the help destination. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ internal fun NavController.navigateToHelp(navOptions: NavOptions? = null) = navigate(SettingsItems.Help, navOptions) +/** + * Defines the composable destination for the "Help" screen within the navigation graph. + * This sets up the route and the content to be displayed, along with screen transitions. + * + * @param onBackClick A lambda function to be invoked when the user initiates a back action. + * @param navigateToFAQ A lambda function to navigate to the "FAQ" screen. + */ internal fun NavGraphBuilder.helpDestination( onBackClick: () -> Unit, navigateToFAQ: () -> Unit, @@ -29,3 +42,4 @@ internal fun NavGraphBuilder.helpDestination( ) } } + diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpScreen.kt index ddde6352eb..59fde7025c 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpScreen.kt @@ -57,6 +57,14 @@ import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.designsystem.theme.MifosTypography import org.mifos.mobile.core.ui.utils.ShareUtils +/** + * A stateful composable that constructs the "Help" screen, including its scaffold and top bar. + * It handles navigation and initiates external actions like calling or mailing. + * + * @param onBackClick Lambda to handle back navigation events. + * @param modifier The [Modifier] to be applied to this screen. + * @param navigateToFAQ Lambda to navigate to the FAQ screen. + */ @Composable internal fun HelpScreen( onBackClick: () -> Unit, @@ -80,6 +88,15 @@ internal fun HelpScreen( } } +/** + * A stateless composable that renders the main content of the "Help" screen, + * which includes various support cards. + * + * @param onCallClick Lambda to be invoked when the call support action is triggered. + * @param onMailClick Lambda to be invoked when the mail support action is triggered. + * @param modifier The [Modifier] to be applied to the layout. + * @param navigateToFAQ Lambda to navigate to the FAQ screen. + */ @Composable internal fun HelpScreenContent( onCallClick: () -> Unit, @@ -104,6 +121,12 @@ internal fun HelpScreenContent( } } +/** + * A specialized [HelpCard] that directs the user to the FAQ screen. + * + * @param modifier The [Modifier] to be applied to the card. + * @param onClick Lambda to be executed when the card is clicked. + */ @Composable private fun FAQCard( modifier: Modifier = Modifier, @@ -132,6 +155,12 @@ private fun FAQCard( } } +/** + * A specialized [SupportCard] for initiating a phone call to the helpline. + * + * @param modifier The [Modifier] to be applied to the card. + * @param onCallClick Lambda to be executed when the call action button is clicked. + */ @Composable private fun PhoneSupportCard( modifier: Modifier = Modifier, @@ -149,6 +178,12 @@ private fun PhoneSupportCard( ) } +/** + * A specialized [SupportCard] for opening a mail client to contact support. + * + * @param modifier The [Modifier] to be applied to the card. + * @param onMailClick Lambda to be executed when the mail action button is clicked. + */ @Composable private fun EmailSupportCard( modifier: Modifier = Modifier, @@ -166,6 +201,14 @@ private fun EmailSupportCard( ) } +/** + * A generic, styled card used as a base for different help options. + * + * @param backgroundColor The background color of the card. + * @param modifier The [Modifier] to be applied to the card. + * @param onClick Optional lambda to handle click events on the card. + * @param content The composable content to be displayed inside the card. + */ @Composable private fun HelpCard( backgroundColor: Color, @@ -182,6 +225,18 @@ private fun HelpCard( } } +/** + * A reusable composable for displaying a support option with text, an action button, and an icon. + * + * @param backgroundColor The background color of the card. + * @param titleRes The string resource for the title. + * @param messageRes The string resource for the descriptive message. + * @param actionRes The string resource for the action button text. + * @param iconRes The drawable resource for the decorative icon. + * @param iconContentDescription The content description for the icon. + * @param modifier The [Modifier] to be applied to the card. + * @param onActionClick Lambda to be executed when the action button is clicked. + */ @Composable private fun SupportCard( backgroundColor: Color, @@ -218,6 +273,15 @@ private fun SupportCard( } } +/** + * Renders the textual content and action button within a [SupportCard]. + * + * @param titleRes The string resource for the title. + * @param messageRes The string resource for the descriptive message. + * @param actionRes The string resource for the action button text. + * @param modifier The [Modifier] to be applied to the content layout. + * @param onActionClick Lambda to be executed when the action button is clicked. + */ @Composable private fun SupportCardContent( titleRes: StringResource, @@ -247,6 +311,13 @@ private fun SupportCardContent( } } +/** + * Displays the decorative icon within a [SupportCard]. + * + * @param iconRes The drawable resource for the icon. + * @param contentDescription The content description for the icon. + * @param modifier The [Modifier] to be applied to the image. + */ @Composable private fun SupportCardIcon( iconRes: DrawableResource, @@ -260,6 +331,13 @@ private fun SupportCardIcon( ) } +/** + * A styled button used for actions within the help cards. + * + * @param textRes The string resource for the button's text. + * @param modifier The [Modifier] to be applied to the button. + * @param onClick Lambda to be executed when the button is clicked. + */ @Composable private fun HelpActionButton( textRes: StringResource, @@ -286,6 +364,9 @@ private fun HelpActionButton( } } +/** + * A Jetpack Compose preview for the [HelpScreenContent]. + */ @Preview @Composable private fun HelpScreenContentPreview() { @@ -298,6 +379,9 @@ private fun HelpScreenContentPreview() { } } +/** + * A Jetpack Compose preview for the [FAQCard]. + */ @Preview @Composable private fun FAQCardPreview() { @@ -306,6 +390,9 @@ private fun FAQCardPreview() { } } +/** + * A Jetpack Compose preview for the [PhoneSupportCard]. + */ @Preview @Composable private fun PhoneSupportCardPreview() { @@ -314,6 +401,9 @@ private fun PhoneSupportCardPreview() { } } +/** + * A Jetpack Compose preview for the [EmailSupportCard]. + */ @Preview @Composable private fun EmailSupportCardPreview() { @@ -322,6 +412,9 @@ private fun EmailSupportCardPreview() { } } +/** + * A Jetpack Compose preview for the [HelpActionButton]. + */ @Preview @Composable private fun HelpActionButtonPreview() { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageRoute.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageRoute.kt index 39a474929f..770d12ab36 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageRoute.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageRoute.kt @@ -15,10 +15,22 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Navigates to the Language selection screen. This is an extension function on [NavController] + * that simplifies the process of navigating to the language destination. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ internal fun NavController.navigateToLanguageScreen(navOptions: NavOptions? = null) { this.navigate(SettingsItems.Language, navOptions) } +/** + * Defines the composable destination for the "Language" screen within the navigation graph. + * This sets up the route and the content to be displayed, along with screen transitions. + * + * @param navigateBack A lambda function to be invoked when the user initiates a back action. + */ internal fun NavGraphBuilder.languageDestination( navigateBack: () -> Unit, ) { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageScreen.kt index c3cc4f50ae..9ec1ad0c04 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/language/LanguageScreen.kt @@ -40,6 +40,14 @@ import org.mifos.mobile.core.designsystem.theme.MifosTypography import org.mifos.mobile.core.model.LanguageConfig import org.mifos.mobile.core.ui.utils.EventsEffect +/** + * A stateful composable that constructs the "Language" screen. It observes state from the + * [LanguageViewModel] and handles navigation events. + * + * @param navigateBack Lambda to handle back navigation events. + * @param modifier The [Modifier] to be applied to this screen. + * @param viewModel The ViewModel responsible for the screen's logic and state management. + */ @Composable internal fun LanguageScreen( navigateBack: () -> Unit, @@ -63,6 +71,14 @@ internal fun LanguageScreen( ) } +/** + * A stateless composable that renders the UI for the language selection screen. + * It includes the scaffold, top bar, content, and a confirmation button. + * + * @param uiState The current state of the language screen. + * @param modifier The [Modifier] to be applied to the layout. + * @param onAction Callback to send actions to the ViewModel. + */ @Composable internal fun LanguageScreenContent( uiState: LanguageState, @@ -95,6 +111,13 @@ internal fun LanguageScreenContent( } } +/** + * Renders the list of available languages as radio buttons, allowing the user to make a selection. + * + * @param selectedLanguage The currently selected [LanguageConfig]. + * @param modifier The [Modifier] to be applied to the list. + * @param onSetLanguage Callback that is invoked when a new language is selected. + */ @Composable internal fun LanguageSelectionContent( selectedLanguage: LanguageConfig, @@ -124,6 +147,10 @@ internal fun LanguageSelectionContent( } } +/** + * A Jetpack Compose preview for the [LanguageScreenContent]. This allows for + * visualizing the UI component in Android Studio's preview pane. + */ @Preview @Composable private fun Language_Screen_Preview() { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt index 22b0d08eee..5b6182c42e 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt @@ -26,12 +26,28 @@ import org.mifos.mobile.feature.settings.settings.SettingsRoute import org.mifos.mobile.feature.settings.settings.settingsDestination import org.mifos.mobile.feature.settings.theme.themeDestination +/** + * A serializable object representing the route for the nested settings navigation graph. + * This serves as the entry point for all settings-related screens. + */ @Serializable data object SettingsNavGraphRoute +/** + * Navigates to the settings navigation graph. This is a convenience extension function + * on [NavController] to encapsulate the navigation logic to the settings feature. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) = navigate(SettingsNavGraphRoute, navOptions) +/** + * Builds the nested navigation graph for the settings feature. This function defines + * all the destinations within the settings module and their corresponding navigation actions. + * + * @param navController The [NavController] used for handling navigation events within the graph. + */ fun NavGraphBuilder.settingsGraph( navController: NavController, ) { @@ -73,7 +89,15 @@ fun NavGraphBuilder.settingsGraph( } } +/** + * A generic internal helper function to navigate to any screen within the settings graph + * using its [SettingsItems] route. + * + * @param route The [SettingsItems] destination to navigate to. + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ internal fun NavController.navigateToScreen( route: SettingsItems, navOptions: NavOptions? = null, ) = navigate(route, navOptions) + diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeNavigation.kt index e6b31bc78c..5dbae8233f 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeNavigation.kt @@ -15,9 +15,21 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Navigates to the "Update Passcode" screen. This is an extension function on [NavController] + * that simplifies the process of navigating to the passcode update destination. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ fun NavController.navigateToUpdatePasscode(navOptions: NavOptions? = null) = navigate(SettingsItems.AuthPasscode, navOptions) +/** + * Defines the composable destination for the "Update Passcode" screen within the navigation graph. + * This sets up the route and the content to be displayed, along with screen transitions. + * + * @param navigateBack A lambda function to be invoked when the user initiates a back action. + */ internal fun NavGraphBuilder.updatePasscodeDestination( navigateBack: () -> Unit, ) { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeScreen.kt index 4ad4d644ce..8f141c97de 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeScreen.kt @@ -43,6 +43,14 @@ import org.mifos.mobile.core.ui.component.MifosSuccessDialog import org.mifos.mobile.core.ui.component.SuccessDialogState import org.mifos.mobile.core.ui.utils.EventsEffect +/** + * A stateful composable that manages the "Update Passcode" screen. + * It observes state and events from the [UpdatePasscodeViewModel] and handles navigation. + * + * @param navigateBack A lambda function to handle back navigation events. + * @param modifier The [Modifier] to be applied to this screen. + * @param viewmodel The ViewModel responsible for the screen's logic and state. + */ @Composable internal fun UpdatePasscodeScreen( navigateBack: () -> Unit, @@ -74,6 +82,14 @@ internal fun UpdatePasscodeScreen( ) } +/** + * A stateless composable that renders the UI for the "Update Passcode" screen. + * It includes the scaffold, top bar, and the main content area. + * + * @param state The current state of the passcode screen. + * @param modifier The [Modifier] to be applied to the layout. + * @param onAction A callback to send actions to the ViewModel. + */ @Composable internal fun UpdatePasscodeScreen( state: PasscodeState, @@ -98,6 +114,13 @@ internal fun UpdatePasscodeScreen( } } +/** + * Renders the form content for updating the passcode, including input fields and a submit button. + * + * @param passcodeData The current state containing the passcode fields' data and errors. + * @param modifier The [Modifier] to be applied to the content layout. + * @param onAction A callback to send user actions to the ViewModel. + */ @Composable internal fun PasscodeScreenContent( passcodeData: PasscodeState, @@ -174,6 +197,13 @@ internal fun PasscodeScreenContent( } } +/** + * A composable that displays a dialog based on the [PasscodeState.DialogState]. + * It can show a loading indicator or a success message. + * + * @param dialogState The current state of the dialog. + * @param onDismiss A lambda to be executed when the success dialog is dismissed. + */ @Composable private fun PasscodeDialog( dialogState: PasscodeState.DialogState?, diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt index 0df1d54fd8..c3e86f6bb6 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt @@ -1,4 +1,4 @@ -/* +/** * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -25,6 +25,17 @@ import org.jetbrains.compose.resources.StringResource import org.mifos.mobile.core.datastore.UserPreferencesRepository import org.mifos.mobile.core.ui.utils.BaseViewModel +/** + * ViewModel for the "Update Passcode" screen. + * + * This class manages the state and business logic for changing the user's passcode. + * It handles input validation, interaction with the [UserPreferencesRepository] to + * fetch the current passcode and set the new one, and communicates with the UI + * through a UDF (Unidirectional Data Flow) pattern. + * + * @param userPreferencesRepository The repository for accessing user preferences, + * including the stored passcode. + */ internal class UpdatePasscodeViewModel( private val userPreferencesRepository: UserPreferencesRepository, ) : @@ -32,6 +43,7 @@ internal class UpdatePasscodeViewModel( initialState = PasscodeState(), ) { init { + // Observes the current passcode from the repository and updates the state. viewModelScope.launch { userPreferencesRepository.passcode.collect { trySendAction(PasscodeAction.Internal.CurrentPasscodeReceived(it)) @@ -39,6 +51,12 @@ internal class UpdatePasscodeViewModel( } } + /** + * Handles all incoming actions from the UI, delegating them to the appropriate + * business logic methods. + * + * @param action The [PasscodeAction] to be processed. + */ override fun handleAction(action: PasscodeAction) { when (action) { is PasscodeAction.OnConfirmPasscodeChange -> onConfirmPasscodeChange(action.confirmPasscode) @@ -94,7 +112,11 @@ internal class UpdatePasscodeViewModel( } } - // Validation functions + /** + * Validates the old passcode against several criteria. + * @param passcode The passcode string to validate. + * @return A [StringResource] with an error message, or null if validation passes. + */ private fun validateOldPasscode(passcode: String): StringResource? = when { passcode.isEmpty() -> Res.string.feature_settings_passcode_empty_error passcode.length != 4 -> Res.string.feature_settings_passcode_length_error @@ -103,6 +125,11 @@ internal class UpdatePasscodeViewModel( else -> null } + /** + * Validates the new passcode against several criteria. + * @param passcode The passcode string to validate. + * @return A [StringResource] with an error message, or null if validation passes. + */ private fun validateNewPasscode(passcode: String): StringResource? = when { passcode.isEmpty() -> Res.string.feature_settings_passcode_empty_error passcode.length != 4 -> Res.string.feature_settings_passcode_length_error @@ -110,6 +137,12 @@ internal class UpdatePasscodeViewModel( else -> null } + /** + * Validates the confirmed passcode against the new passcode and other criteria. + * @param confirmPasscode The confirmed passcode string. + * @param newPasscode The new passcode string to match against. + * @return A [StringResource] with an error message, or null if validation passes. + */ private fun validateConfirmPasscode( confirmPasscode: String, newPasscode: String, @@ -123,6 +156,11 @@ internal class UpdatePasscodeViewModel( private var validationJob: Job? = null + /** + * Handles changes to the old passcode input field with debouncing to avoid + * excessive validation. + * @param newValue The new string value from the input field. + */ private fun onOldPasscodeChange(newValue: String) { // Immediately update the value without validation mutableStateFlow.update { @@ -140,6 +178,11 @@ internal class UpdatePasscodeViewModel( } } + /** + * Handles changes to the new passcode input field with debouncing and also re-validates + * the confirmation field. + * @param newValue The new string value from the input field. + */ private fun onNewPasscodeChange(newValue: String) { mutableStateFlow.update { it.copy(newPasscode = newValue) @@ -164,6 +207,10 @@ internal class UpdatePasscodeViewModel( } } + /** + * Handles changes to the confirm passcode input field with debouncing. + * @param newValue The new string value from the input field. + */ private fun onConfirmPasscodeChange(newValue: String) { mutableStateFlow.update { it.copy(confirmPasscode = newValue) @@ -179,6 +226,10 @@ internal class UpdatePasscodeViewModel( } } + /** + * Validates all fields immediately, typically triggered by the submit button click. + * If all validations pass, it proceeds to [handleSubmitClick]. + */ private fun validateSubmitClick() { val oldPasscodeError = validateOldPasscode(state.oldPasscode) val newPasscodeError = validateNewPasscode(state.newPasscode) @@ -197,6 +248,10 @@ internal class UpdatePasscodeViewModel( } } + /** + * Sets the dialog state to loading and initiates the process of saving the new passcode + * to the repository. + */ private fun handleSubmitClick() { mutableStateFlow.update { it.copy(dialogState = PasscodeState.DialogState.Loading) @@ -213,6 +268,11 @@ internal class UpdatePasscodeViewModel( } } + /** + * Handles the result of the passcode update operation, showing a success dialog + * and clearing sensitive data from the state. + * @param action The result action containing the success message. + */ private fun handleUpdatePasscodeResult(action: PasscodeAction.Internal.UpdatePasscodeResult) { mutableStateFlow.update { it.copy(dialogState = PasscodeState.DialogState.Shown(action.result)) @@ -220,6 +280,9 @@ internal class UpdatePasscodeViewModel( clearSensitiveData() } + /** + * Clears all passcode input fields and their associated errors from the state. + */ private fun clearSensitiveData() { mutableStateFlow.update { it.copy( @@ -232,6 +295,10 @@ internal class UpdatePasscodeViewModel( } } + /** + * Overridden to ensure sensitive data and background jobs are cleared when the + * ViewModel is destroyed. + */ override fun onCleared() { super.onCleared() clearSensitiveData() @@ -239,6 +306,21 @@ internal class UpdatePasscodeViewModel( } } +/** + * Represents the UI state for the "Update Passcode" screen. + * + * @property currentPasscode The user's current passcode fetched from preferences. + * @property oldPasscode The value entered in the "Old Passcode" field. + * @property newPasscode The value entered in the "New Passcode" field. + * @property confirmPasscode The value entered in the "Confirm Passcode" field. + * @property oldPasscodeError An optional error message for the old passcode field. + * @property newPasscodeError An optional error message for the new passcode field. + * @property confirmPasscodeError An optional error message for the confirm passcode field. + * @property isOldPasscodeVisible Toggles the visibility of the old passcode. + * @property isNewPasscodeVisible Toggles the visibility of the new passcode. + * @property isConfirmPasscodeVisible Toggles the visibility of the confirm passcode. + * @property dialogState The current state of the dialog (loading, shown, or hidden). + */ internal data class PasscodeState( internal val currentPasscode: String = "", @@ -255,34 +337,67 @@ internal data class PasscodeState( val isConfirmPasscodeVisible: Boolean = false, val dialogState: DialogState? = null, ) { + /** + * Represents the state of any dialogs shown on the screen, such as a loading + * indicator or a success message. + */ sealed interface DialogState { + /** The dialog is showing a loading indicator. */ data object Loading : DialogState + /** The dialog is showing a message. + * @param message The string resource to display. + */ data class Shown(val message: StringResource) : DialogState } } +/** + * A sealed interface representing one-time events that trigger UI side effects, + * such as navigation. + */ internal sealed interface PasscodeEvent { + /** Event to navigate back from the current screen. */ data object OnNavigateBack : PasscodeEvent + /** Event to navigate to the main passcode screen after a successful update. */ data object OnNavigateToPasscodeScreen : PasscodeEvent } +/** + * A sealed interface representing all possible user actions or internal events that + * the ViewModel can handle. + */ internal sealed interface PasscodeAction { + /** Action triggered when the old passcode input changes. */ data class OnOldPasscodeChange(val oldPasscode: String) : PasscodeAction + /** Action triggered when the new passcode input changes. */ data class OnNewPasscodeChange(val newPasscode: String) : PasscodeAction + /** Action triggered when the confirm passcode input changes. */ data class OnConfirmPasscodeChange(val confirmPasscode: String) : PasscodeAction + /** Action to toggle the visibility of the new passcode. */ data object NewPasscodeVisibleClick : PasscodeAction + /** Action to toggle the visibility of the old passcode. */ data object OldPasscodeVisibleClick : PasscodeAction + /** Action to toggle the visibility of the confirm passcode. */ data object ConfirmPasscodeVisibleClick : PasscodeAction + /** Action triggered when the user clicks the submit button. */ data object SubmitClick : PasscodeAction + /** Action triggered when the user clicks the back navigation button. */ data object NavigateBackClick : PasscodeAction + /** Action to dismiss the current dialog. */ data object DismissDialog : PasscodeAction + /** Action to navigate to the passcode screen. */ data object NavigateToPasscodeScreen : PasscodeAction + /** + * A sealed interface for internal actions not directly triggered by the UI. + */ sealed interface Internal : PasscodeAction { + /** Internal action representing the result of the update operation. */ data class UpdatePasscodeResult(val result: StringResource) : Internal + /** Internal action triggered when the current passcode is fetched from the repository. */ data class CurrentPasscodeReceived(val passcode: String) : Internal } } diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordNavigation.kt index 8826d52537..a154bfc9fa 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordNavigation.kt @@ -15,9 +15,26 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Navigates to the "Change Password" screen. + * + * This is an extension function on [NavController] that simplifies the process + * of navigating to the change password destination using a predefined route. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ fun NavController.navigateToUpdatePassword(navOptions: NavOptions? = null) = navigate(SettingsItems.Password, navOptions) +/** + * Defines the composable destination for the "Change Password" screen within the navigation graph. + * + * This function sets up the route and the content to be displayed, along with screen + * transitions. + * + * @param onBackClick A lambda function to be invoked when the user initiates a back action + * from the [ChangePasswordScreen]. + */ internal fun NavGraphBuilder.changePasswordDestination( onBackClick: () -> Unit, ) { diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordScreen.kt index f193d4f283..fbdee8a7c3 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordScreen.kt @@ -43,6 +43,15 @@ import org.mifos.mobile.core.ui.component.MifosSuccessDialog import org.mifos.mobile.core.ui.component.SuccessDialogState import org.mifos.mobile.core.ui.utils.EventsEffect +/** + * A stateful composable that serves as the entry point for the "Change Password" screen. + * It observes state and events from the [ChangePasswordViewModel], handles navigation, + * and orchestrates the display of the UI content and dialogs. + * + * @param navigateBack A lambda function to handle back navigation events. + * @param modifier The [Modifier] to be applied to this screen. + * @param viewmodel The ViewModel responsible for the screen's logic and state. + */ @Composable internal fun ChangePasswordScreen( navigateBack: () -> Unit, @@ -73,6 +82,14 @@ internal fun ChangePasswordScreen( ) } +/** + * A stateless composable that renders the main scaffold and top bar for the "Change Password" screen. + * It delegates the rendering of the main form content. + * + * @param state The current state of the password screen. + * @param modifier The [Modifier] to be applied to the layout. + * @param onAction A callback to send actions to the ViewModel. + */ @Composable internal fun ChangePasswordScreen( state: PasswordState, @@ -98,6 +115,14 @@ internal fun ChangePasswordScreen( } } +/** + * Renders the core form content for changing the password, including input fields, + * password strength indicators, and a submit button. + * + * @param state The current state containing password fields' data, errors, and visibility. + * @param modifier The [Modifier] to be applied to the content layout. + * @param onAction A callback to send user actions (like input changes) to the ViewModel. + */ @Composable internal fun PasswordScreenContent( state: PasswordState, @@ -183,6 +208,13 @@ internal fun PasswordScreenContent( } } +/** + * A composable that displays a dialog based on the [PasswordState.DialogState]. + * It can show a loading indicator, a success message, or an error dialog. + * + * @param dialogState The current state of the dialog. + * @param onDismiss A lambda to be executed when the error dialog is dismissed. + */ @Composable private fun PasswordDialog( dialogState: PasswordState.DialogState?, diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt index 113ac71c29..7a2c5086d4 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt @@ -37,6 +37,18 @@ import org.mifos.mobile.core.ui.utils.PasswordChecker import org.mifos.mobile.core.ui.utils.PasswordStrength import org.mifos.mobile.core.ui.utils.PasswordStrengthResult +/** + * ViewModel for the "Change Password" screen. + * + * This class manages the state and business logic for changing a user's password. It handles + * real-time input validation, password strength checking, and communication with the + * repositories to update the password. It also implements security features like + * submission attempt throttling and automatic logout upon successful password change. + * It follows a Unidirectional Data Flow (UDF) pattern. + * + * @param repository The repository for handling user authentication and password updates. + * @param userDataRepository The repository for accessing user-specific data, including the current password. + */ @Suppress("CyclomaticComplexMethod", "TooManyFunctions") internal class ChangePasswordViewModel( private val repository: UserAuthRepository, @@ -50,6 +62,7 @@ internal class ChangePasswordViewModel( private val maxFailedAttempts = 5 init { + // Observes the current password from user data and pre-populates the state. userDataRepository.userData.map { it.data?.password ?: "" }.onEach { @@ -57,6 +70,12 @@ internal class ChangePasswordViewModel( }.launchIn(viewModelScope) } + /** + * Central handler for all incoming actions from the UI or internal events. + * It delegates each action to the appropriate business logic function. + * + * @param action The [PasswordAction] to be processed. + */ override fun handleAction(action: PasswordAction) { when (action) { is PasswordAction.OnOldPasswordChange -> { @@ -84,6 +103,11 @@ internal class ChangePasswordViewModel( } } + /** + * Validates the current password input field. + * @param password The password string to validate. + * @return A [ValidationResult] indicating success or failure with an error message. + */ private fun validateCurrentPassword(password: String): ValidationResult = when { password.isEmpty() -> ValidationResult.Error(Res.string.password_empty_error) password.length < 8 -> ValidationResult.Error(Res.string.password_length_error) @@ -91,6 +115,12 @@ internal class ChangePasswordViewModel( else -> ValidationResult.Success } + /** + * Validates the new password against a set of rules, including checks for empty strings, + * repeating characters, and minimum strength requirements. + * @param password The new password string to validate. + * @return A [ValidationResult] indicating success or failure. + */ @Suppress("ReturnCount") private fun validateNewPassword(password: String): ValidationResult { if (password.isEmpty()) { @@ -116,6 +146,12 @@ internal class ChangePasswordViewModel( return ValidationResult.Success } + /** + * Validates the confirmed password to ensure it matches the new password and meets basic requirements. + * @param confirmPassword The password from the confirmation field. + * @param newPassword The password from the new password field. + * @return A [ValidationResult] indicating success or failure. + */ private fun validateConfirmPassword( confirmPassword: String, newPassword: String, @@ -126,6 +162,10 @@ internal class ChangePasswordViewModel( else -> ValidationResult.Success } + /** + * Handles changes to the current password field with debounced validation. + * @param newValue The latest value from the input field. + */ private fun onCurrentPasswordChange(newValue: String) { mutableStateFlow.update { it.copy( @@ -143,6 +183,11 @@ internal class ChangePasswordViewModel( } } + /** + * Handles changes to the new password field, triggering password strength analysis + * and debounced validation for both the new and confirm password fields. + * @param newValue The latest value from the input field. + */ private fun onNewPasswordChange(newValue: String) { mutableStateFlow.update { it.copy( @@ -188,6 +233,10 @@ internal class ChangePasswordViewModel( } } + /** + * Handles changes to the confirm password field with debounced validation. + * @param newValue The latest value from the input field. + */ private fun onConfirmPasswordChange(newValue: String) { mutableStateFlow.update { it.copy( @@ -205,6 +254,10 @@ internal class ChangePasswordViewModel( } } + /** + * Processes the result from the password strength checker and updates the UI state accordingly. + * @param action The internal action containing the [PasswordStrengthResult]. + */ private fun handlePasswordStrengthResult(action: PasswordAction.Internal.ReceivePasswordStrengthResult) { when (val result = action.result) { is PasswordStrengthResult.Success -> { @@ -232,6 +285,11 @@ internal class ChangePasswordViewModel( } } + /** + * Validates all input fields upon submission. If valid, proceeds to update the password. + * If invalid, increments the failed attempt counter. It also blocks submissions + * if the maximum number of attempts has been reached. + */ private fun validateAndSubmit() { if (failedAttempts >= maxFailedAttempts) { mutableStateFlow.update { @@ -266,6 +324,10 @@ internal class ChangePasswordViewModel( } } + /** + * Initiates the password update process by showing a loading dialog and calling + * the repository to update the password. + */ private fun handleSubmitClick() { mutableStateFlow.update { it.copy(dialogState = PasswordState.DialogState.Loading) @@ -292,6 +354,12 @@ internal class ChangePasswordViewModel( } } + /** + * Handles the result of the password update operation from the repository. + * On success, it shows a success dialog, clears sensitive data, and triggers a delayed logout. + * On failure, it shows an error dialog. + * @param action The internal action containing the [DataState] result. + */ private fun handleUpdatePasswordResult(action: PasswordAction.Internal.UpdatePasswordResult) { when (action.result) { is DataState.Error -> { @@ -335,12 +403,21 @@ internal class ChangePasswordViewModel( } } + /** + * Updates the state with the current password received from the data repository. + * @param action The internal action containing the optional password string. + */ private fun handleOldPasswordReceived(action: PasswordAction.Internal.OldPasswordReceived) { action.password?.let { mutableStateFlow.update { it.copy(currentPassword = action.password) } } } + /** + * A utility function to debounce validation logic, preventing excessive processing + * on every keystroke. + * @param validation The suspendable validation block to execute after a delay. + */ private fun debounceValidation(validation: suspend () -> Unit) { validationJob?.cancel() validationJob = viewModelScope.launch { @@ -349,6 +426,11 @@ internal class ChangePasswordViewModel( } } + /** + * Checks if a string contains consecutive repeating characters. + * @param input The string to check. + * @return `true` if consecutive characters are found, `false` otherwise. + */ private fun hasConsecutiveRepeatingChars(input: String): Boolean { for (i in 0 until input.length - 1) { if (input[i] == input[i + 1]) { @@ -358,39 +440,49 @@ internal class ChangePasswordViewModel( return false } + /** Toggles the visibility of the current password field. */ private fun toggleCurrentPasswordVisibility() { mutableStateFlow.update { it.copy(oldPasswordVisible = !it.oldPasswordVisible) } } + /** Toggles the visibility of the new password field. */ private fun toggleNewPasswordVisibility() { mutableStateFlow.update { it.copy(newPasswordVisible = !it.newPasswordVisible) } } + /** Toggles the visibility of the confirm password field. */ private fun toggleConfirmPasswordVisibility() { mutableStateFlow.update { it.copy(confirmPasswordVisible = !it.confirmPasswordVisible) } } + /** Hides any currently displayed dialog. */ private fun dismissDialog() { mutableStateFlow.update { it.copy(dialogState = null) } } + /** Clears sensitive data and sends a navigation back event. */ private fun navigateBack() { clearSensitiveData() sendEvent(PasswordEvent.OnNavigateBack) } + /** Resets the counter for failed submission attempts. */ private fun resetFailedAttempts() { failedAttempts = 0 } + /** + * Clears all sensitive user input and validation state from the ViewModel. + * This is used after a successful update or when navigating away from the screen. + */ private fun clearSensitiveData() { mutableStateFlow.update { it.copy( @@ -406,6 +498,10 @@ internal class ChangePasswordViewModel( } } + /** + * Overridden to ensure all coroutine jobs are cancelled and sensitive data is cleared + * when the ViewModel is no longer in use. + */ override fun onCleared() { super.onCleared() clearSensitiveData() @@ -414,6 +510,24 @@ internal class ChangePasswordViewModel( } } +/** + * Represents the UI state for the "Change Password" screen. + * + * @property currentPassword The user's actual current password (internal use). + * @property oldPassword The value entered in the "Old Password" field. + * @property newPassword The value entered in the "New Password" field. + * @property confirmPassword The value entered in the "Confirm Password" field. + * @property oldPasswordError An optional error message for the old password field. + * @property newPasswordError An optional error message for the new password field. + * @property confirmPasswordError An optional error message for the confirm password field. + * @property oldPasswordVisible Toggles the visibility of the old password. + * @property newPasswordVisible Toggles the visibility of the new password. + * @property confirmPasswordVisible Toggles the visibility of the confirm password. + * @property passwordStrengthState The calculated strength of the new password. + * @property passwordFeedback A list of suggestions to improve password strength. + * @property dialogState The current state of any dialogs (loading, success, error). + * @property isEnabled A computed property to determine if the submit button should be enabled. + */ internal data class PasswordState( internal val currentPassword: String = "", val oldPassword: String = "", @@ -432,45 +546,76 @@ internal data class PasswordState( val passwordFeedback: List = emptyList(), val dialogState: DialogState? = null, ) { + /** Defines the possible states for dialogs on the screen. */ internal sealed interface DialogState { + /** The dialog is showing a loading indicator. */ data object Loading : DialogState + /** The dialog is showing a success message. */ data class Success(val message: StringResource) : DialogState + /** The dialog is showing an error message. */ data class Error(val message: StringResource) : DialogState } + /** Determines if the submit button should be enabled based on input validity and presence. */ internal val isEnabled = oldPasswordError == null && - newPasswordError == null && - confirmPasswordError == null && - oldPassword.isNotEmpty() && - newPassword.isNotEmpty() && - confirmPassword.isNotEmpty() + newPasswordError == null && + confirmPasswordError == null && + oldPassword.isNotEmpty() && + newPassword.isNotEmpty() && + confirmPassword.isNotEmpty() } +/** + * Defines one-time events that can be sent from the ViewModel to the UI, such as navigation. + */ internal sealed interface PasswordEvent { + /** Signals that the UI should navigate back. */ data object OnNavigateBack : PasswordEvent } +/** + * Defines all possible actions that can be sent from the UI to the ViewModel or used internally. + */ internal sealed interface PasswordAction { + /** Action triggered when the old password input changes. */ data class OnOldPasswordChange(val currentPassword: String) : PasswordAction + /** Action triggered when the new password input changes. */ data class OnNewPasswordChange(val newPassword: String) : PasswordAction + /** Action triggered when the confirm password input changes. */ data class OnConfirmPasswordChange(val confirmPassword: String) : PasswordAction + /** Action to toggle the visibility of the old password. */ data object OldPasswordVisibleClick : PasswordAction + /** Action to toggle the visibility of the new password. */ data object NewPasswordVisibleClick : PasswordAction + /** Action to toggle the visibility of the confirm password. */ data object ConfirmPasswordVisibleClick : PasswordAction + /** Action triggered when the user clicks the submit button. */ data object SubmitClick : PasswordAction + /** Action to retry submission after being blocked. */ data object RetrySubmit : PasswordAction + /** Action to navigate back from the screen. */ data object NavigateBack : PasswordAction + /** Action to dismiss the current dialog. */ data object DismissDialog : PasswordAction + /** Defines internal actions that are not directly triggered by the user. */ sealed interface Internal : PasswordAction { + /** Internal action representing the result of the password update operation. */ data class UpdatePasswordResult(val result: DataState) : Internal + /** Internal action carrying the result of a password strength check. */ data class ReceivePasswordStrengthResult(val result: PasswordStrengthResult) : Internal + /** Internal action for when the current password is fetched from the repository. */ data class OldPasswordReceived(val password: String?) : Internal } } +/** + * A sealed class to represent the result of a validation check. + */ sealed class ValidationResult { + /** Represents a successful validation. */ data object Success : ValidationResult() + /** Represents a failed validation with an associated error message. */ data class Error(val message: StringResource) : ValidationResult() } diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsRoute.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsRoute.kt index 2256182386..9cc77de1ba 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsRoute.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsRoute.kt @@ -16,13 +16,37 @@ import kotlinx.serialization.Serializable import org.mifos.mobile.core.ui.composableWithSlideTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * A type-safe, serializable route for the main Settings screen. + * This object serves as the unique identifier for the settings destination + * in the navigation graph. + */ @Serializable data object SettingsRoute +/** + * Navigates to the main Settings screen. + * + * This is an extension function on [NavController] that simplifies the process + * of navigating to the settings destination. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ fun NavController.navigateToSettingsRoute(navOptions: NavOptions? = null) { this.navigate(SettingsRoute, navOptions) } +/** + * Defines the composable destination for the main "Settings" screen within the navigation graph. + * + * This function sets up the route and the content to be displayed, along with screen + * transitions. It provides callbacks for navigating back and to other specific + * settings screens. + * + * @param navigateBack A lambda function to be invoked when the user initiates a back action. + * @param navigateToScreen A lambda function to handle navigation to a specific + * [SettingsItems] destination. + */ fun NavGraphBuilder.settingsDestination( navigateBack: () -> Unit, navigateToScreen: (SettingsItems) -> Unit, diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt index 9fc3a9d29f..faf77b5cc4 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt @@ -57,6 +57,17 @@ import org.mifos.mobile.feature.settings.componenets.LogoutDialogState import org.mifos.mobile.feature.settings.componenets.MifosLogoutDialog import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * A stateful composable that serves as the entry point for the main "Settings" screen. + * + * It connects to the [SettingsViewModel] to observe state and events, and orchestrates + * the display of the UI content and dialogs. It handles navigation events for backing + * out of the screen or navigating to other specific settings pages. + * + * @param navigateBack A lambda function to handle back navigation events. + * @param navigateToScreen A lambda function to handle navigation to a specific [SettingsItems] destination. + * @param viewModel The ViewModel responsible for the screen's logic and state. + */ @Composable internal fun SettingsScreen( navigateBack: () -> Unit, @@ -89,6 +100,13 @@ internal fun SettingsScreen( ) } +/** + * A composable responsible for displaying dialogs based on the current [SettingsState]. + * It currently handles the display of the logout confirmation dialog. + * + * @param state The current state of the settings screen, used to determine which dialog to show. + * @param onAction A callback to send actions to the ViewModel, such as logout confirmation or dismissal. + */ @Composable private fun SettingsDialog( state: SettingsState, @@ -113,6 +131,16 @@ private fun SettingsDialog( } } +/** + * A stateless composable that renders the main UI for the "Settings" screen. + * + * It includes the scaffold, top bar, and conditionally displays content based on the + * [ScreenUiState] (e.g., success, loading, error). + * + * @param state The current state of the settings screen. + * @param onAction A callback to send actions to the ViewModel. + * @param modifier The [Modifier] to be applied to the layout. + */ @Composable internal fun SettingsScreenContent( state: SettingsState, @@ -131,7 +159,7 @@ internal fun SettingsScreenContent( .padding(vertical = DesignToken.padding.large) .verticalScroll(rememberScrollState()), - ) { + ) { when { state.isUserLoading -> { MifosProgressIndicator() @@ -172,6 +200,13 @@ internal fun SettingsScreenContent( } } +/** + * A composable that displays the user's profile information card at the top of the settings screen. + * It includes the user's profile picture, name, and account number. + * + * @param state The current state containing the client's profile data. + * @param modifier The [Modifier] to be applied to the card layout. + */ @Composable internal fun SettingsProfileCard( state: SettingsState, @@ -217,6 +252,13 @@ internal fun SettingsProfileCard( } } +/** + * A composable that renders a list of actionable settings items. + * Each item is displayed as a [MifosActionCard] and triggers a callback when clicked. + * + * @param items An immutable list of [SettingsItems] to be displayed. + * @param onActionClick A lambda function invoked with the [SettingsItems] that was clicked. + */ @Composable internal fun SettingsActions( items: ImmutableList, @@ -245,3 +287,4 @@ internal fun SettingsActions( } } } + diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt index ea5b02f1b6..8936398455 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt @@ -15,6 +15,15 @@ import androidx.navigation.NavOptions import org.mifos.mobile.core.ui.composableWithPushTransitions import org.mifos.mobile.feature.settings.componenets.SettingsItems +/** + * Defines the composable destination for the "Theme" selection screen within the navigation graph. + * + * This function sets up the route and the content to be displayed (`ChangeThemeScreen`), + * along with screen transitions. + * + * @param navigateBack A lambda function to be invoked when the user initiates a back action + * from the [ChangeThemeScreen]. + */ internal fun NavGraphBuilder.themeDestination( navigateBack: () -> Unit, ) { @@ -24,5 +33,15 @@ internal fun NavGraphBuilder.themeDestination( ) } } + +/** + * Navigates to the "Theme" selection screen. + * + * This is an extension function on [NavController] that simplifies the process + * of navigating to the theme destination using a predefined route. + * + * @param navOptions Optional [NavOptions] to apply to this navigation operation. + */ internal fun NavController.navigateToTheme(navOptions: NavOptions? = null) = navigate(SettingsItems.Theme, navOptions) + diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeScreen.kt index f092c61c77..902a67e025 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeScreen.kt @@ -39,6 +39,17 @@ import org.mifos.mobile.core.model.MifosThemeConfig import org.mifos.mobile.core.ui.utils.DevicePreview import org.mifos.mobile.core.ui.utils.EventsEffect +/** + * A stateful composable that constructs the "Change Theme" screen. + * + * It connects to the [ChangeThemeViewModel] to observe the current UI state and + * listen for one-time events, such as navigation. It delegates the rendering + * of the UI to the stateless [ThemeScreenContent] composable. + * + * @param onNavigateBack A lambda function to be invoked when a back navigation event is triggered. + * @param modifier The [Modifier] to be applied to this screen. + * @param viewmodel The ViewModel responsible for the screen's logic and state management. + */ @Composable internal fun ChangeThemeScreen( onNavigateBack: () -> Unit, @@ -60,6 +71,16 @@ internal fun ChangeThemeScreen( ) } +/** + * A stateless composable that renders the UI for the theme selection screen. + * + * It includes the scaffold with a top bar, a list of theme options presented as + * radio buttons, and an "Apply" button to save the selection. + * + * @param uiState The current state of the theme screen, containing the available themes and the selection. + * @param modifier The [Modifier] to be applied to the layout. + * @param onAction A callback to send actions (like theme selection or navigation) to the ViewModel. + */ @Composable internal fun ThemeScreenContent( uiState: ThemeState, @@ -111,6 +132,12 @@ internal fun ThemeScreenContent( } } +/** + * A Jetpack Compose preview for the [ThemeScreenContent]. + * + * This provides a design-time visualization of the theme selection UI in Android Studio, + * configured with a default state. + */ @DevicePreview @Composable fun ThemeScreenPreview() { From 68eb9b006903d5f3eeea5df29677af9d53f53543 Mon Sep 17 00:00:00 2001 From: Aryan Baglane Date: Sun, 2 Nov 2025 15:37:25 +0530 Subject: [PATCH 2/2] documentation update for the feature/settings --- .../feature/settings/about/AboutScreen.kt | 4 ++-- .../feature/settings/appInfo/AppInfoScreen.kt | 4 ++-- .../feature/settings/faq/FaqViewmodel.kt | 2 ++ .../feature/settings/help/HelpNavigation.kt | 1 - .../navigation/SettingsNavGraphRoute.kt | 1 - .../passcode/UpdatePasscodeViewModel.kt | 11 ++++++++- .../password/ChangePasswordViewModel.kt | 23 +++++++++++++++---- .../settings/settings/SettingsScreen.kt | 3 +-- .../settings/theme/ChangeThemeRoute.kt | 1 - 9 files changed, 35 insertions(+), 15 deletions(-) diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt index e0025dab4a..063713c631 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/about/AboutScreen.kt @@ -114,7 +114,7 @@ internal fun AboutScreenContent( Image( painter = painterResource(Res.drawable.mifos_icon), contentDescription = - stringResource(Res.string.feature_settings_about_logo_content_description), + stringResource(Res.string.feature_settings_about_logo_content_description), modifier = Modifier .size(DesignToken.sizes.iconExtraLarge), ) @@ -160,7 +160,7 @@ internal fun AboutScreenContent( } Image( painter = - painterResource(Res.drawable.ic_icon_money_transfer), + painterResource(Res.drawable.ic_icon_money_transfer), contentDescription = null, modifier = Modifier.fillMaxWidth(), ) diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt index eaeae10c95..bdca96dce5 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/appInfo/AppInfoScreen.kt @@ -130,7 +130,7 @@ internal fun AppInfoContent( Image( painter = painterResource(Res.drawable.mifos_icon), contentDescription = - stringResource(Res.string.feature_settings_about_logo_content_description), + stringResource(Res.string.feature_settings_about_logo_content_description), modifier = Modifier .size(DesignToken.sizes.iconExtraLarge), ) @@ -182,7 +182,7 @@ internal fun AppInfoContent( Image( painter = painterResource(Res.drawable.mifo_app_info_icon), contentDescription = - stringResource(Res.string.feature_settings_appinfo_logo_content_description), + stringResource(Res.string.feature_settings_appinfo_logo_content_description), modifier = Modifier .size(150.dp) .align(Alignment.BottomEnd).zIndex(0f), diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt index 95ec996689..82f0c9a86e 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/faq/FaqViewmodel.kt @@ -98,6 +98,7 @@ internal data class FaqState( internal sealed interface FaqEvent { /** Signals that the UI should navigate back. */ data object OnNavigateBack : FaqEvent + /** Signals that the UI should navigate to the help screen. */ data object OnNavigateToHelp : FaqEvent } @@ -108,6 +109,7 @@ internal sealed interface FaqEvent { internal sealed interface FaqAction { /** An action to navigate back. */ data object NavigateBack : FaqAction + /** An action to navigate to the help screen. */ data object NavigateToHelp : FaqAction diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt index 0d62c59634..668dba4519 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/help/HelpNavigation.kt @@ -42,4 +42,3 @@ internal fun NavGraphBuilder.helpDestination( ) } } - diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt index 5b6182c42e..d7ac233951 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/navigation/SettingsNavGraphRoute.kt @@ -100,4 +100,3 @@ internal fun NavController.navigateToScreen( route: SettingsItems, navOptions: NavOptions? = null, ) = navigate(route, navOptions) - diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt index c3e86f6bb6..3fff15b30a 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/passcode/UpdatePasscodeViewModel.kt @@ -1,4 +1,4 @@ -/** +/* * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -344,6 +344,7 @@ internal data class PasscodeState( sealed interface DialogState { /** The dialog is showing a loading indicator. */ data object Loading : DialogState + /** The dialog is showing a message. * @param message The string resource to display. */ @@ -358,6 +359,7 @@ internal data class PasscodeState( internal sealed interface PasscodeEvent { /** Event to navigate back from the current screen. */ data object OnNavigateBack : PasscodeEvent + /** Event to navigate to the main passcode screen after a successful update. */ data object OnNavigateToPasscodeScreen : PasscodeEvent } @@ -369,15 +371,19 @@ internal sealed interface PasscodeEvent { internal sealed interface PasscodeAction { /** Action triggered when the old passcode input changes. */ data class OnOldPasscodeChange(val oldPasscode: String) : PasscodeAction + /** Action triggered when the new passcode input changes. */ data class OnNewPasscodeChange(val newPasscode: String) : PasscodeAction + /** Action triggered when the confirm passcode input changes. */ data class OnConfirmPasscodeChange(val confirmPasscode: String) : PasscodeAction /** Action to toggle the visibility of the new passcode. */ data object NewPasscodeVisibleClick : PasscodeAction + /** Action to toggle the visibility of the old passcode. */ data object OldPasscodeVisibleClick : PasscodeAction + /** Action to toggle the visibility of the confirm passcode. */ data object ConfirmPasscodeVisibleClick : PasscodeAction @@ -386,8 +392,10 @@ internal sealed interface PasscodeAction { /** Action triggered when the user clicks the back navigation button. */ data object NavigateBackClick : PasscodeAction + /** Action to dismiss the current dialog. */ data object DismissDialog : PasscodeAction + /** Action to navigate to the passcode screen. */ data object NavigateToPasscodeScreen : PasscodeAction @@ -397,6 +405,7 @@ internal sealed interface PasscodeAction { sealed interface Internal : PasscodeAction { /** Internal action representing the result of the update operation. */ data class UpdatePasscodeResult(val result: StringResource) : Internal + /** Internal action triggered when the current passcode is fetched from the repository. */ data class CurrentPasscodeReceived(val passcode: String) : Internal } diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt index 7a2c5086d4..13c784860e 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/password/ChangePasswordViewModel.kt @@ -550,18 +550,21 @@ internal data class PasswordState( internal sealed interface DialogState { /** The dialog is showing a loading indicator. */ data object Loading : DialogState + /** The dialog is showing a success message. */ data class Success(val message: StringResource) : DialogState + /** The dialog is showing an error message. */ data class Error(val message: StringResource) : DialogState } + /** Determines if the submit button should be enabled based on input validity and presence. */ internal val isEnabled = oldPasswordError == null && - newPasswordError == null && - confirmPasswordError == null && - oldPassword.isNotEmpty() && - newPassword.isNotEmpty() && - confirmPassword.isNotEmpty() + newPasswordError == null && + confirmPasswordError == null && + oldPassword.isNotEmpty() && + newPassword.isNotEmpty() && + confirmPassword.isNotEmpty() } /** @@ -578,24 +581,31 @@ internal sealed interface PasswordEvent { internal sealed interface PasswordAction { /** Action triggered when the old password input changes. */ data class OnOldPasswordChange(val currentPassword: String) : PasswordAction + /** Action triggered when the new password input changes. */ data class OnNewPasswordChange(val newPassword: String) : PasswordAction + /** Action triggered when the confirm password input changes. */ data class OnConfirmPasswordChange(val confirmPassword: String) : PasswordAction /** Action to toggle the visibility of the old password. */ data object OldPasswordVisibleClick : PasswordAction + /** Action to toggle the visibility of the new password. */ data object NewPasswordVisibleClick : PasswordAction + /** Action to toggle the visibility of the confirm password. */ data object ConfirmPasswordVisibleClick : PasswordAction /** Action triggered when the user clicks the submit button. */ data object SubmitClick : PasswordAction + /** Action to retry submission after being blocked. */ data object RetrySubmit : PasswordAction + /** Action to navigate back from the screen. */ data object NavigateBack : PasswordAction + /** Action to dismiss the current dialog. */ data object DismissDialog : PasswordAction @@ -603,8 +613,10 @@ internal sealed interface PasswordAction { sealed interface Internal : PasswordAction { /** Internal action representing the result of the password update operation. */ data class UpdatePasswordResult(val result: DataState) : Internal + /** Internal action carrying the result of a password strength check. */ data class ReceivePasswordStrengthResult(val result: PasswordStrengthResult) : Internal + /** Internal action for when the current password is fetched from the repository. */ data class OldPasswordReceived(val password: String?) : Internal } @@ -616,6 +628,7 @@ internal sealed interface PasswordAction { sealed class ValidationResult { /** Represents a successful validation. */ data object Success : ValidationResult() + /** Represents a failed validation with an associated error message. */ data class Error(val message: StringResource) : ValidationResult() } diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt index faf77b5cc4..8f3e07d1fd 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/settings/SettingsScreen.kt @@ -159,7 +159,7 @@ internal fun SettingsScreenContent( .padding(vertical = DesignToken.padding.large) .verticalScroll(rememberScrollState()), - ) { + ) { when { state.isUserLoading -> { MifosProgressIndicator() @@ -287,4 +287,3 @@ internal fun SettingsActions( } } } - diff --git a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt index 8936398455..0d2384666e 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifos/mobile/feature/settings/theme/ChangeThemeRoute.kt @@ -44,4 +44,3 @@ internal fun NavGraphBuilder.themeDestination( */ internal fun NavController.navigateToTheme(navOptions: NavOptions? = null) = navigate(SettingsItems.Theme, navOptions) -