diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 2f8cf70e2..e13ae0499 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -1,5 +1,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { `kotlin-dsl` } @@ -29,6 +30,7 @@ dependencies { compileOnly(libs.ktlint.gradlePlugin) compileOnly(libs.spotless.gradle) implementation(libs.truth) + compileOnly(libs.dokka.gradle.plugin) } tasks { diff --git a/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt index 44559cacd..869c4aebb 100644 --- a/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt @@ -2,6 +2,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies +import org.mifospay.configureFeatureDocumentation import org.mifospay.libs class CMPFeatureConventionPlugin : Plugin { @@ -12,7 +13,9 @@ class CMPFeatureConventionPlugin : Plugin { apply("mifospay.kmp.koin") apply("org.jetbrains.kotlin.plugin.compose") apply("org.jetbrains.compose") + apply("org.jetbrains.dokka") } + configureFeatureDocumentation() dependencies { add("commonMainImplementation", project(":core:ui")) diff --git a/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt index 23d0a8913..d37dd84b0 100644 --- a/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt @@ -4,6 +4,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies +import org.mifospay.configureFeatureDocumentation import org.mifospay.configureFlavors import org.mifospay.configureKotlinAndroid import org.mifospay.configureKotlinMultiplatform @@ -18,8 +19,9 @@ class KMPLibraryConventionPlugin: Plugin { apply("mifospay.kmp.koin") apply("mifos.detekt.plugin") apply("mifos.spotless.plugin") + apply("org.jetbrains.dokka") } - + configureFeatureDocumentation() configureKotlinMultiplatform() extensions.configure { diff --git a/build-logic/convention/src/main/kotlin/org/mifospay/Dokka.kt b/build-logic/convention/src/main/kotlin/org/mifospay/Dokka.kt new file mode 100644 index 000000000..f6311d95b --- /dev/null +++ b/build-logic/convention/src/main/kotlin/org/mifospay/Dokka.kt @@ -0,0 +1,26 @@ +package org.mifospay + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.withType + +/** + * Configures Dokka documentation settings for a feature module. + * This extension function replaces the FeatureLibraryPlugin class by directly + * applying the same configuration to the target Project. + */ +// Add this in a shared convention plugin or utilities script +fun Project.configureFeatureDocumentation() { + plugins.withId("org.jetbrains.dokka") { + tasks.withType().configureEach { + outputDirectory.set(layout.buildDirectory.dir("dokka")) + moduleName.set(project.name) + + // Add KMP-specific configuration only for multiplatform modules + if (pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform")) { + dokkaSourceSets.configureEach { + // includes.from("README.md") // Include KMP-specific docs + } + } + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6f2e94a62..1b8a631c1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.dokka.gradle.DokkaMultiModuleTask + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { dependencies { @@ -33,6 +35,7 @@ plugins { alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.wire) apply false alias(libs.plugins.ktorfit) apply false + alias(libs.plugins.dokka) apply false } object DynamicVersion { @@ -57,7 +60,6 @@ tasks.register("printModulePaths") { } } } - // Configuration for CMP module dependency graph moduleGraphAssert { configurations += setOf("commonMainImplementation", "commonMainApi") @@ -66,4 +68,13 @@ moduleGraphAssert { configurations += setOf("jsMainImplementation", "jsMainApi") configurations += setOf("nativeMainImplementation", "nativeMainApi") configurations += setOf("wasmJsMainImplementation", "wasmJsMainApi") -} \ No newline at end of file +} + +// root build.gradle.kts +tasks.register("dokkaHtmlMultiModule") { + outputDirectory.set(buildDir.resolve("docs")) + + // Automatically collect all dokkaHtml tasks from subprojects + addChildTasks(subprojects, "dokkaHtml") +} + diff --git a/feature/accounts/build.gradle.kts b/feature/accounts/build.gradle.kts index d4a4a02b3..33e3e4e55 100644 --- a/feature/accounts/build.gradle.kts +++ b/feature/accounts/build.gradle.kts @@ -27,4 +27,4 @@ kotlin { implementation(libs.kotlinx.serialization.json) } } -} \ No newline at end of file +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt index 285f1a0cf..17925863f 100644 --- a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt @@ -34,7 +34,6 @@ import org.mifospay.feature.accounts.AccountAction.Internal.DeleteBeneficiary import org.mifospay.feature.accounts.AccountEvent.OnAddEditSavingsAccount import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType import org.mifospay.feature.accounts.savingsaccount.SavingsAddEditType - @OptIn(ExperimentalCoroutinesApi::class) class AccountViewModel( private val userRepository: UserPreferencesRepository, @@ -51,6 +50,9 @@ class AccountViewModel( ) }, ) { + /** + * Exposes the account and beneficiary list as a [StateFlow] of [AccountState.ViewState]. + */ val accountState = repository.getAccountAndBeneficiaryList(state.clientId) .mapLatest { when (it) { @@ -67,6 +69,9 @@ class AccountViewModel( initialValue = AccountState.ViewState.Loading, ) + /** + * Handles [AccountAction] and triggers corresponding [AccountEvent] or state updates. + */ override fun handleAction(action: AccountAction) { when (action) { is AccountAction.CreateSavingsAccount -> { @@ -124,6 +129,9 @@ class AccountViewModel( } } + /** + * Handles updating the default account in [userRepository]. + */ private fun handleSetDefaultAccount(action: AccountAction.SetDefaultAccount) { viewModelScope.launch { userRepository.updateDefaultAccount( @@ -138,6 +146,9 @@ class AccountViewModel( sendEvent(AccountEvent.ShowToast("Default account updated")) } + /** + * Sends a delete request for the given beneficiary ID and shows a loading dialog. + */ private fun handleDeleteBeneficiary(action: DeleteBeneficiary) { mutableStateFlow.update { it.copy(dialogState = AccountState.DialogState.Loading) } @@ -148,6 +159,9 @@ class AccountViewModel( } } + /** + * Handles the result of deleting a beneficiary and updates dialog state accordingly. + */ private fun handleBeneficiaryDeleteResult(action: BeneficiaryDeleteResultReceived) { when (action.result) { is DataState.Success -> { @@ -175,14 +189,25 @@ class AccountViewModel( } } +/** + * Represents the state of the account screen. + */ data class AccountState( val clientId: Long, val defaultAccountId: Long? = null, val dialogState: DialogState? = null, ) { + /** + * Defines various dialog UI states. + */ sealed interface DialogState { + /** Represents a loading dialog. */ data object Loading : DialogState + + /** Represents an error dialog with a message. */ data class Error(val message: String) : DialogState + + /** Dialog shown before deleting a beneficiary. */ data class DeleteBeneficiary( val title: StringResource, val message: StringResource, @@ -190,21 +215,26 @@ data class AccountState( ) : DialogState } + /** + * UI state representing the screen's visual content. + */ sealed interface ViewState { val hasFab: Boolean - val isPullToRefreshEnabled: Boolean + /** Loading view state. */ data object Loading : ViewState { override val hasFab: Boolean get() = false override val isPullToRefreshEnabled: Boolean get() = false } + /** Error view state with a message. */ data class Error(val message: String) : ViewState { override val hasFab: Boolean get() = false override val isPullToRefreshEnabled: Boolean get() = false } + /** Content view state with account and beneficiary info. */ data class Content( val accounts: List, val beneficiaries: List, @@ -215,26 +245,32 @@ data class AccountState( } } +/** + * Events that can be sent by [AccountViewModel] to update the UI. + */ sealed interface AccountEvent { data class OnAddEditSavingsAccount(val type: SavingsAddEditType) : AccountEvent data class OnNavigateToAccountDetail(val accountId: Long) : AccountEvent data class OnAddOrEditTPTBeneficiary(val type: BeneficiaryAddEditType) : AccountEvent - data class ShowToast(val message: String) : AccountEvent } +/** + * Actions that can be triggered from the UI to be handled by [AccountViewModel]. + */ sealed interface AccountAction { data object AddTPTBeneficiary : AccountAction data class EditBeneficiary(val beneficiary: Beneficiary) : AccountAction data class DeleteBeneficiary(val beneficiaryId: Long) : AccountAction - data object CreateSavingsAccount : AccountAction data class EditSavingsAccount(val accountId: Long) : AccountAction data class ViewAccountDetails(val accountId: Long) : AccountAction data class SetDefaultAccount(val accountId: Long, val accountNo: String) : AccountAction - data object DismissDialog : AccountAction + /** + * Internal-only actions used within the view model. + */ sealed interface Internal : AccountAction { data class DeleteBeneficiary(val beneficiaryId: Long) : Internal data class BeneficiaryDeleteResultReceived(val result: DataState) : Internal diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 4fa6ca8ef..cd39f88c3 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -45,4 +45,5 @@ kotlin { implementation(libs.play.services.auth) } } -} \ No newline at end of file +} + diff --git a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt index 29c0596fa..f5a7428ab 100644 --- a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt +++ b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt @@ -23,6 +23,12 @@ import org.mifospay.core.ui.utils.BaseViewModel private const val KEY_STATE = "state" +/** + * ViewModel responsible for managing login-related UI state and actions. + * + * @param loginUseCase Use case that performs the login operation. + * @param savedStateHandle Provides access to saved state. + */ class LoginViewModel( private val loginUseCase: LoginUseCase, savedStateHandle: SavedStateHandle, @@ -36,6 +42,11 @@ class LoginViewModel( } } + /** + * Handles incoming actions from the UI. + * + * @param action The [LoginAction] to process. + */ override fun handleAction(action: LoginAction) { when (action) { is LoginAction.UsernameChanged -> { @@ -74,6 +85,11 @@ class LoginViewModel( } } + /** + * Handles the result of the login attempt. + * + * @param action The internal login result action. + */ private fun handleLoginResult(action: LoginAction.Internal.ReceiveLoginResult) { when (action.loginResult) { is DataState.Error -> { @@ -97,6 +113,12 @@ class LoginViewModel( } } + /** + * Initiates the login process. + * + * @param username The user's username. + * @param password The user's password. + */ private fun loginUser( username: String, password: String, @@ -112,6 +134,14 @@ class LoginViewModel( } } +/** + * Represents the UI state of the login screen. + * + * @property username The entered username. + * @property password The entered password (excluded from parceling). + * @property isPasswordVisible Indicates whether the password is visible. + * @property dialogState Represents loading or error dialog state. + */ @Parcelize data class LoginState( val username: String = "", @@ -120,31 +150,96 @@ data class LoginState( val isPasswordVisible: Boolean = false, val dialogState: DialogState?, ) : Parcelable { + + /** + * Represents dialog states shown on the login screen. + */ sealed class DialogState : Parcelable { + /** + * Represents an error dialog. + * @param message The error message to display. + */ @Parcelize data class Error(val message: String) : DialogState() + /** + * Represents a loading dialog. + */ @Parcelize data object Loading : DialogState() } } +/** + * Defines one-time events to be triggered from the login screen. + */ sealed class LoginEvent { + /** + * Event to navigate back from the login screen. + */ data object NavigateBack : LoginEvent() + + /** + * Event to navigate to the signup screen. + */ data object NavigateToSignup : LoginEvent() + + /** + * Event to navigate to the passcode screen after successful login. + */ data object NavigateToPasscodeScreen : LoginEvent() + + /** + * Event to show a toast message. + * + * @param message The message to display. + */ data class ShowToast(val message: String) : LoginEvent() } +/** + * Represents all user interactions and internal actions on the login screen. + */ sealed class LoginAction { + /** + * Action for when the username is changed. + */ data class UsernameChanged(val username: String) : LoginAction() + + /** + * Action for when the password is changed. + */ data class PasswordChanged(val password: String) : LoginAction() + + /** + * Action to toggle password visibility. + */ data object TogglePasswordVisibility : LoginAction() + + /** + * Action to dismiss the error dialog. + */ data object ErrorDialogDismiss : LoginAction() + + /** + * Action for when the login button is clicked. + */ data object LoginClicked : LoginAction() + + /** + * Action for when the signup button is clicked. + */ data object SignupClicked : LoginAction() + /** + * Internal actions used within the ViewModel. + */ sealed class Internal : LoginAction() { + /** + * Result from the login use case. + * + * @param loginResult The result of the login attempt. + */ data class ReceiveLoginResult( val loginResult: DataState, ) : Internal() diff --git a/gradle.properties b/gradle.properties index 41da833cc..56a0b6da4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -53,4 +53,5 @@ RblClientIdProp=a RblClientSecretProp=b kotlin.native.ignoreDisabledTargets=true -kotlin.mpp.androidGradlePluginCompatibility.nowarn=true \ No newline at end of file +kotlin.mpp.androidGradlePluginCompatibility.nowarn=true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c14ff5498..75455503b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ appcompatVersion = "1.7.0" coreKtxVersion = "1.15.0" # KotlinX Dependencies +dokkaGradlePlugin = "2.0.0" lifecycleExtensionsVersion = "2.2.0" lifecycleVersion = "2.8.7" @@ -178,6 +179,7 @@ androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profi androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" } +dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokkaGradlePlugin" } ktlint-gradlePlugin = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktlint" } detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessVersion" } @@ -366,3 +368,4 @@ ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } spotless = { id = "com.diffplug.spotless", version.ref = "spotlessVersion" } version-catalog-linter = { id = "io.github.pemistahl.version-catalog-linter", version.ref = "versionCatalogLinterVersion" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaGradlePlugin" } diff --git a/mifospay-shared/build.gradle.kts b/mifospay-shared/build.gradle.kts index b160d4f23..c1960c17c 100644 --- a/mifospay-shared/build.gradle.kts +++ b/mifospay-shared/build.gradle.kts @@ -102,4 +102,5 @@ android { compose.resources { publicResClass = true generateResClass = always -} \ No newline at end of file +} + diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt index 39ca953e3..f78c07f47 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.rememberNavController import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel @@ -96,7 +97,7 @@ private fun MifosPayApp( onClickLogout = { viewModel.logOut() navController.navigate(LOGIN_GRAPH) { - popUpTo(navController.graph.id) { + popUpTo(navController.graph.findStartDestination().route ?: "") { inclusive = true } } diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt index b65551ece..c92a76747 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt @@ -48,7 +48,7 @@ fun NavController.navigateToMainGraph() { // Pop up to the start destination of the graph to // avoid building up a large stack of destinations // on the back stack as users select items - popUpTo(graph.findStartDestination().id) { + popUpTo(graph.findStartDestination().route ?: "") { saveState = false } // Avoid multiple copies of the same destination when diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt index 55604f92b..e7dd701dc 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt @@ -117,7 +117,7 @@ internal class MifosAppState( // Pop up to the start destination of the graph to // avoid building up a large stack of destinations // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { + popUpTo(navController.graph.findStartDestination().route ?: "") { saveState = true } // Avoid multiple copies of the same destination when