Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: [:libs:passcode] - Migrated to KMP #2753

Merged
merged 9 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmp-navigation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ kotlin {
implementation(projects.core.data)
implementation(projects.core.common)
implementation(projects.core.network)
implementation(projects.libs.mifosPasscode)
//put your multiplatform dependencies here
implementation(compose.material3)
implementation(compose.foundation)
Expand All @@ -41,6 +42,7 @@ android {
namespace = "cmp.navigation"
}


compose.resources {
publicResClass = true
generateResClass = always
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.mifos.library.passcode.data.PasscodeManager
import org.mifos.mobile.core.common.DataState
import org.mifos.mobile.core.data.repository.UserDataRepository
import org.mifos.mobile.core.model.UserData

class ComposeAppViewModel(
private val userDataRepository: UserDataRepository,
// private val passcodeManager: PasscodeManager,
private val passcodeManager: PasscodeManager,
) : ViewModel() {

val uiState: StateFlow<MainUiState> = userDataRepository.userData.map { dataState ->
Expand All @@ -40,7 +41,7 @@ class ComposeAppViewModel(
fun logOut() {
viewModelScope.launch {
userDataRepository.logOut()
// passcodeManager.clearPasscode()
passcodeManager.clearPasscode()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package cmp.navigation.di
import cmp.navigation.ComposeAppViewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import org.mifos.library.passcode.di.PasscodeModule
import org.mifos.mobile.core.common.di.DispatchersModule
import org.mifos.mobile.core.data.di.RepositoryModule
import org.mifos.mobile.core.datastore.di.PreferencesModule
Expand Down Expand Up @@ -39,9 +40,9 @@ object KoinModules {
AuthModule,
)
}
// private val LibraryModule = module {
// includes(PasscodeModule)
// }
private val LibraryModule = module {
includes(PasscodeModule)
}

val allModules = listOf(
commonModules,
Expand All @@ -50,5 +51,6 @@ object KoinModules {
networkModules,
featureModules,
sharedModule,
LibraryModule,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
*/
package cmp.navigation.navigation

import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.navigation
import org.mifos.library.passcode.PASSCODE_SCREEN
import org.mifos.library.passcode.passcodeRoute

internal fun NavGraphBuilder.passcodeNavGraph(navController: NavHostController) {
navigation(
route = NavGraphRoute.PASSCODE_GRAPH,
startDestination = PASSCODE_SCREEN,
) {
passcodeRoute(
onForgotButton = {
navController.popBackStack()
navController.navigate(NavGraphRoute.MAIN_GRAPH)
},
onSkipButton = {
navController.popBackStack()
navController.navigate(NavGraphRoute.MAIN_GRAPH)
},
onPasscodeConfirm = {
navController.popBackStack()
navController.navigate(NavGraphRoute.MAIN_GRAPH)
},
onPasscodeRejected = {
navController.popBackStack()
navController.navigate(NavGraphRoute.MAIN_GRAPH)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
import cmp.navigation.navigation.NavGraphRoute.AUTH_GRAPH
import cmp.navigation.navigation.NavGraphRoute.MAIN_GRAPH
import cmp.navigation.ui.App
import org.mifos.library.passcode.navigateToPasscodeScreen
import org.mifos.mobile.core.data.util.NetworkMonitor
import org.mifos.mobile.feature.auth.navigation.authenticationNavGraph

Expand All @@ -36,9 +37,10 @@ fun RootNavGraph(
authenticationNavGraph(
navController = navHostController,
route = AUTH_GRAPH,
// navigateToPasscodeScreen = navHostController::navigateToPasscodeScreen,
navigateToPasscodeScreen = { },
navigateToPasscodeScreen = navHostController::navigateToPasscodeScreen,

)
passcodeNavGraph(navHostController)
composable(MAIN_GRAPH) {
App(
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ package org.mifos.mobile.core.ui.utils
* navigating to the same view twice, by default, events are ignored if the view is not currently
* resumed. To avoid that restriction, specific events can implement [BackgroundEvent].
*/

interface BackgroundEvent
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 Mifos Initiative
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ class LoginViewModel(
mutableStateFlow.update {
it.copy(dialogState = null)
}
// TODO: Can be removed after integrating passcode and navigate to passcode

sendEvent(LoginEvent.ShowToast("Successfully logged in"))
// sendEvent(LoginEvent.NavigateToPasscodeScreen)
sendEvent(LoginEvent.NavigateToPasscodeScreen)
}
}
}
Expand Down
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,6 @@ mifos-spotless-plugin = { id = "mifos.spotless.plugin", version = "unspecified"

firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" }

roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }
dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" }
Expand Down
45 changes: 30 additions & 15 deletions libs/mifos-passcode/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,43 @@
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
*/
plugins {
alias(libs.plugins.mifos.android.library)
alias(libs.plugins.mifos.android.library.compose)
alias(libs.plugins.mifos.android.hilt)
alias(libs.plugins.mifos.cmp.feature)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
}

android {
namespace = "com.mifos.library.passcode"
}

dependencies {
implementation(libs.androidx.core.ktx)
kotlin{
sourceSets{
commonMain.dependencies {
implementation(compose.ui)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)

implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.foundation.layout)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.ui.util)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.compose)

implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.lifecycle.viewModelCompose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.jb.kotlin.stdlib)
implementation(libs.kotlin.reflect)


implementation(libs.kotlinx.serialization.core)

implementation(libs.multiplatform.settings)
implementation(libs.multiplatform.settings.serialization)
implementation(libs.multiplatform.settings.coroutines)

implementation(libs.kotlinx.coroutines.core)
implementation(projects.core.ui)
}
desktopMain.dependencies {
implementation(libs.kotlinx.coroutines.swing)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,20 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
import org.mifos.library.passcode.component.MifosIcon
import org.mifos.library.passcode.component.PasscodeForgotButton
import org.mifos.library.passcode.component.PasscodeHeader
Expand All @@ -55,10 +55,11 @@ import org.mifos.library.passcode.component.PasscodeSkipButton
import org.mifos.library.passcode.component.PasscodeToolbar
import org.mifos.library.passcode.theme.blueTint
import org.mifos.library.passcode.utility.Constants.PASSCODE_LENGTH
import org.mifos.library.passcode.utility.PreferenceManager
import org.mifos.library.passcode.utility.ShakeAnimation.performShakeAnimation
import org.mifos.library.passcode.utility.VibrationFeedback.vibrateFeedback
import org.mifos.library.passcode.viewmodels.PasscodeAction
import org.mifos.library.passcode.viewmodels.PasscodeEvent
import org.mifos.library.passcode.viewmodels.PasscodeViewModel
import org.mifos.mobile.core.ui.utils.EventsEffect

@Composable
internal fun PasscodeScreen(
Expand All @@ -67,31 +68,27 @@ internal fun PasscodeScreen(
onPasscodeConfirm: (String) -> Unit,
onPasscodeRejected: () -> Unit,
modifier: Modifier = Modifier,
viewModel: PasscodeViewModel = hiltViewModel(),
viewModel: PasscodeViewModel = koinViewModel(),
) {
val context = LocalContext.current
val preferenceManager = remember { PreferenceManager(context) }

val activeStep by viewModel.activeStep.collectAsStateWithLifecycle()
val filledDots by viewModel.filledDots.collectAsStateWithLifecycle()
val passcodeVisible by viewModel.passcodeVisible.collectAsStateWithLifecycle()
val currentPasscode by viewModel.currentPasscodeInput.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
val state by viewModel.stateFlow.collectAsStateWithLifecycle()

val xShake = remember { Animatable(initialValue = 0.0F) }
var passcodeRejectedDialogVisible by remember { mutableStateOf(false) }

LaunchedEffect(key1 = viewModel.onPasscodeConfirmed) {
viewModel.onPasscodeConfirmed.collect {
onPasscodeConfirm(it)
}
}
EventsEffect(viewModel.eventFlow) { event ->
when (event) {
is PasscodeEvent.PasscodeConfirmed -> {
onPasscodeConfirm(event.passcode)
}

LaunchedEffect(key1 = viewModel.onPasscodeRejected) {
viewModel.onPasscodeRejected.collect {
passcodeRejectedDialogVisible = true
vibrateFeedback(context)
performShakeAnimation(xShake)
onPasscodeRejected()
is PasscodeEvent.PasscodeRejected -> {
passcodeRejectedDialogVisible = true
scope.launch {
performShakeAnimation(xShake)
}
onPasscodeRejected()
}
}
}

Expand All @@ -106,10 +103,10 @@ internal fun PasscodeScreen(
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally,
) {
PasscodeToolbar(activeStep = activeStep, preferenceManager.hasPasscode)
PasscodeToolbar(activeStep = state.activeStep, state.hasPasscode)

PasscodeSkipButton(
hasPassCode = preferenceManager.hasPasscode,
hasPassCode = state.hasPasscode,
onSkipButton = onSkipButton,
)

Expand All @@ -122,15 +119,19 @@ internal fun PasscodeScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
PasscodeHeader(
activeStep = activeStep,
isPasscodeAlreadySet = preferenceManager.hasPasscode,
activeStep = state.activeStep,
isPasscodeAlreadySet = state.hasPasscode,
)
PasscodeView(
restart = { viewModel.restart() },
togglePasscodeVisibility = { viewModel.togglePasscodeVisibility() },
filledDots = filledDots,
passcodeVisible = passcodeVisible,
currentPasscode = currentPasscode,
restart = remember(viewModel) {
{ viewModel.trySendAction(PasscodeAction.Restart) }
},
togglePasscodeVisibility = remember(viewModel) {
{ viewModel.trySendAction(PasscodeAction.TogglePasscodeVisibility) }
},
filledDots = state.filledDots,
passcodeVisible = state.passcodeVisible,
currentPasscode = state.currentPasscodeInput,
passcodeRejectedDialogVisible = passcodeRejectedDialogVisible,
onDismissDialog = { passcodeRejectedDialogVisible = false },
xShake = xShake,
Expand All @@ -140,16 +141,21 @@ internal fun PasscodeScreen(
Spacer(modifier = Modifier.height(6.dp))

PasscodeKeys(
enterKey = viewModel::enterKey,
deleteKey = viewModel::deleteKey,
deleteAllKeys = viewModel::deleteAllKeys,
modifier = Modifier.padding(horizontal = 12.dp),
enterKey = remember(viewModel) {
{ viewModel.trySendAction(PasscodeAction.EnterKey(it)) }
},
deleteKey = remember(viewModel) {
{ viewModel.trySendAction(PasscodeAction.DeleteKey) }
},
deleteAllKeys = remember(viewModel) {
{ viewModel.trySendAction(PasscodeAction.DeleteAllKeys) }
},
)

Spacer(modifier = Modifier.height(8.dp))

PasscodeForgotButton(
hasPassCode = preferenceManager.hasPasscode,
hasPassCode = state.hasPasscode,
onForgotButton = onForgotButton,
)
}
Expand Down Expand Up @@ -230,7 +236,7 @@ private fun PasscodeView(
}
}

@Preview(showBackground = true)
@Preview
@Composable
private fun PasscodeScreenPreview() {
PasscodeScreen(
Expand Down
Loading
Loading