mirror of
https://github.com/openMF/mifos-mobile.git
synced 2026-02-06 11:26:51 +00:00
Merge 20d821e565 into 1600b41ad2
This commit is contained in:
commit
b2370ce40b
@ -16,18 +16,29 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import cmp.navigation.rootnav.RootNavScreen
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.model.MifosThemeConfig
|
||||
import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
import org.mifos.mobile.core.ui.utils.NetworkBanner
|
||||
import org.mifos.mobile.core.ui.utils.SessionHandler
|
||||
import template.core.base.designsystem.theme.KptTheme
|
||||
|
||||
@Composable
|
||||
fun ComposeApp(
|
||||
@ -35,10 +46,35 @@ fun ComposeApp(
|
||||
handleAppLocale: (locale: String?) -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sessionManager: SessionManager = koinInject(),
|
||||
viewModel: ComposeAppViewModel = koinViewModel(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val uiState by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
var wasBackgrounded by remember { mutableStateOf(false) }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_STOP) {
|
||||
wasBackgrounded = true
|
||||
viewModel.trySendAction(AppAction.LockApp)
|
||||
} else if (event == Lifecycle.Event.ON_START) {
|
||||
if (wasBackgrounded) {
|
||||
viewModel.trySendAction(AppAction.LockApp)
|
||||
wasBackgrounded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
val isSessionExpired by sessionManager.isExpired.collectAsStateWithLifecycle()
|
||||
EventsEffect(eventFlow = viewModel.eventFlow) { event ->
|
||||
when (event) {
|
||||
is AppEvent.ShowToast -> {}
|
||||
@ -59,25 +95,36 @@ fun ComposeApp(
|
||||
androidTheme = uiState.isAndroidTheme,
|
||||
shouldDisplayDynamicTheming = uiState.isDynamicColorsEnabled,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding(),
|
||||
) {
|
||||
NetworkBanner(
|
||||
bannerState = uiState.networkBanner,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
LaunchedEffect(isSessionExpired) {
|
||||
if (isSessionExpired) {
|
||||
viewModel.trySendAction(AppAction.SessionExpired)
|
||||
}
|
||||
}
|
||||
|
||||
RootNavScreen(
|
||||
modifier = Modifier,
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
)
|
||||
SessionHandler(
|
||||
sessionManager = sessionManager,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(KptTheme.colorScheme.surface),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding(),
|
||||
) {
|
||||
NetworkBanner(
|
||||
bannerState = uiState.networkBanner,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
RootNavScreen(
|
||||
navController = navController,
|
||||
modifier = Modifier,
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,6 +146,16 @@ class ComposeAppViewModel(
|
||||
is AppAction.Internal.SystemThemeUpdate -> handleSystemThemeUpdate(action)
|
||||
|
||||
is AppAction.Internal.TimeBasedThemeUpdate -> handleTimeBasedThemeUpdate(action)
|
||||
|
||||
is AppAction.SessionExpired -> handleLockApp()
|
||||
|
||||
is AppAction.LockApp -> handleLockApp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLockApp() {
|
||||
viewModelScope.launch {
|
||||
userPreferencesRepository.setIsUnlocked(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,4 +271,7 @@ sealed interface AppAction {
|
||||
val timeBasedTheme: TimeBasedTheme,
|
||||
) : Internal()
|
||||
}
|
||||
|
||||
data object LockApp : AppAction
|
||||
data object SessionExpired : AppAction
|
||||
}
|
||||
|
||||
@ -10,33 +10,21 @@
|
||||
package cmp.navigation.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.navigation
|
||||
import cmp.navigation.authenticated.AuthenticatedGraphRoute
|
||||
import org.mifos.library.passcode.PASSCODE_SCREEN
|
||||
import org.mifos.library.passcode.passcodeRoute
|
||||
import cmp.navigation.utils.toObjectNavigationRoute
|
||||
import org.mifos.mobile.feature.passcode.navigation.PasscodeRoute
|
||||
import org.mifos.mobile.feature.passcode.navigation.passcodeDestination
|
||||
|
||||
internal fun NavGraphBuilder.passcodeNavGraph(navController: NavHostController) {
|
||||
internal fun NavGraphBuilder.passcodeNavGraph(
|
||||
onPasscodeVerified: () -> Unit,
|
||||
) {
|
||||
navigation(
|
||||
route = NavGraphRoute.PASSCODE_GRAPH,
|
||||
startDestination = PASSCODE_SCREEN,
|
||||
startDestination = PasscodeRoute.Standard.toObjectNavigationRoute(),
|
||||
) {
|
||||
passcodeRoute(
|
||||
onForgotButton = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
},
|
||||
onSkipButton = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
},
|
||||
onPasscodeConfirm = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
},
|
||||
onPasscodeRejected = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
passcodeDestination(
|
||||
onPasscodeConfirm = { _, _, _, _, _ ->
|
||||
onPasscodeVerified()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -24,15 +24,16 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.navOptions
|
||||
import cmp.navigation.authenticated.AuthenticatedGraphRoute
|
||||
import cmp.navigation.authenticated.authenticatedGraph
|
||||
import cmp.navigation.authenticated.navigateToAuthenticatedGraph
|
||||
import cmp.navigation.authenticated.navigateToStatusScreenLoginFlow
|
||||
import cmp.navigation.authenticated.navigateToStatusScreenPasscodeFlow
|
||||
import cmp.navigation.navigation.passcodeNavGraph
|
||||
import cmp.navigation.splash.SplashRoute
|
||||
import cmp.navigation.splash.navigateToSplash
|
||||
import cmp.navigation.splash.splashDestination
|
||||
import cmp.navigation.ui.rememberMifosNavController
|
||||
import cmp.navigation.utils.toObjectNavigationRoute
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.ui.NonNullEnterTransitionProvider
|
||||
import org.mifos.mobile.core.ui.NonNullExitTransitionProvider
|
||||
import org.mifos.mobile.core.ui.RootTransitionProviders
|
||||
@ -44,13 +45,13 @@ import org.mifos.mobile.feature.onboarding.language.navigation.navigateToOnboard
|
||||
import org.mifos.mobile.feature.onboarding.language.navigation.onBoardingLanguageDestination
|
||||
import org.mifos.mobile.feature.passcode.navigation.PasscodeRoute
|
||||
import org.mifos.mobile.feature.passcode.navigation.navigateToPasscodeScreen
|
||||
import org.mifos.mobile.feature.passcode.navigation.passcodeDestination
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun RootNavScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: RootNavViewModel = koinViewModel(),
|
||||
sessionManager: SessionManager = koinInject(),
|
||||
navController: NavHostController = rememberMifosNavController(name = "RootNavScreen"),
|
||||
onSplashScreenRemoved: () -> Unit = {},
|
||||
) {
|
||||
@ -79,7 +80,14 @@ fun RootNavScreen(
|
||||
navController::navigateToStatusScreenLoginFlow,
|
||||
)
|
||||
authenticatedGraph(navController)
|
||||
passcodeDestination(navController::navigateToStatusScreenPasscodeFlow)
|
||||
passcodeNavGraph(
|
||||
onPasscodeVerified = {
|
||||
sessionManager.startSession()
|
||||
navController.navigate(AuthenticatedGraphRoute) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val targetRoute = when (state) {
|
||||
@ -122,10 +130,20 @@ fun RootNavScreen(
|
||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
|
||||
RootNavState.SetLanguage -> navController.navigateToOnboardingLanguage(rootNavOptions)
|
||||
RootNavState.UserLocked -> navController.navigateToPasscodeScreen(rootNavOptions)
|
||||
is RootNavState.UserUnlocked -> navController.navigateToAuthenticatedGraph(
|
||||
navOptions = rootNavOptions,
|
||||
RootNavState.UserLocked -> navController.navigateToPasscodeScreen(
|
||||
navOptions {
|
||||
popUpTo(navController.graph.id) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
},
|
||||
)
|
||||
is RootNavState.UserUnlocked -> {
|
||||
navController.navigate(AuthenticatedGraphRoute) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,17 +17,24 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mifos.mobile.core.data.repository.UserDataRepository
|
||||
import org.mifos.mobile.core.datastore.UserPreferencesRepository
|
||||
import org.mifos.mobile.core.datastore.model.AppSettings
|
||||
import org.mifos.mobile.core.model.AuthState
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
|
||||
class RootNavViewModel(
|
||||
userDataRepository: UserDataRepository,
|
||||
private val userPreferencesRepository: UserPreferencesRepository,
|
||||
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
|
||||
initialState = RootNavState.Splash,
|
||||
) {
|
||||
|
||||
init {
|
||||
|
||||
viewModelScope.launch {
|
||||
userPreferencesRepository.setIsUnlocked(false)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
userDataRepository.authState
|
||||
.collect { authState ->
|
||||
|
||||
@ -95,7 +95,9 @@ object Constants {
|
||||
const val MAKE_PAYMENT = "make_payments"
|
||||
const val LOAN_SUMMARY = "loan_summary"
|
||||
|
||||
// Settings constants
|
||||
const val TIMEOUT_SESSION_MS = 5 * 60 * 1000L
|
||||
|
||||
// Settings constants
|
||||
const val PROFILE = "profile"
|
||||
const val PASSWORD = "password"
|
||||
const val AUTH_PASSCODE = "auth_passcode"
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2026 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.common
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.AtomicLong
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class SessionManager {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
private val checkLock = Mutex()
|
||||
|
||||
private var heartbeatJob: Job? = null
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private val lastInteractionTime = AtomicLong(0L)
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private val isMonitoring = AtomicBoolean(false)
|
||||
private val timeoutMs = Constants.TIMEOUT_SESSION_MS
|
||||
|
||||
private val _isExpired = MutableStateFlow(false)
|
||||
val isExpired = _isExpired.asStateFlow()
|
||||
|
||||
@OptIn(ExperimentalTime::class, ExperimentalAtomicApi::class)
|
||||
fun startSession() {
|
||||
if (isMonitoring.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
_isExpired.value = false
|
||||
scope.launch {
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
lastInteractionTime.store(now)
|
||||
if (!_isExpired.value) {
|
||||
heartbeatJob = startHeartbeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class, ExperimentalAtomicApi::class)
|
||||
fun userInteracted() {
|
||||
if (_isExpired.value) return
|
||||
if (isMonitoring.load()) {
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
lastInteractionTime.store(now)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
fun stopSession() {
|
||||
isMonitoring.store(false)
|
||||
_isExpired.value = false
|
||||
heartbeatJob?.cancel()
|
||||
heartbeatJob = null
|
||||
lastInteractionTime.store(0L)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class, ExperimentalTime::class)
|
||||
private suspend fun checkExpirationInternal() {
|
||||
checkLock.withLock {
|
||||
if (!isMonitoring.load() || _isExpired.value) return
|
||||
|
||||
val ramTime = lastInteractionTime.load()
|
||||
|
||||
if (ramTime == 0L) return
|
||||
|
||||
val currentTime = Clock.System.now().toEpochMilliseconds()
|
||||
|
||||
if (currentTime - ramTime >= timeoutMs) {
|
||||
_isExpired.value = true
|
||||
isMonitoring.store(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class, ExperimentalAtomicApi::class)
|
||||
private fun startHeartbeat(): Job {
|
||||
return scope.launch {
|
||||
while (isMonitoring.load()) {
|
||||
checkExpirationInternal()
|
||||
delay(30_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.mifos.mobile.core.common.MifosDispatchers
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
|
||||
val DispatchersModule = module {
|
||||
includes(ioDispatcherModule)
|
||||
@ -25,6 +26,7 @@ val DispatchersModule = module {
|
||||
single<CoroutineScope>(named("ApplicationScope")) {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
}
|
||||
single { SessionManager() }
|
||||
}
|
||||
|
||||
expect val ioDispatcherModule: Module
|
||||
|
||||
@ -32,6 +32,7 @@ kotlin{
|
||||
commonMain.dependencies {
|
||||
api(projects.core.designsystem)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.common)
|
||||
api(libs.kotlinx.datetime)
|
||||
implementation(libs.jb.lifecycle.compose)
|
||||
implementation(libs.jb.composeViewmodel)
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2026 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.ui.utils
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.changedToDown
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
|
||||
@Composable
|
||||
fun SessionHandler(
|
||||
sessionManager: SessionManager,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val isExpired by sessionManager.isExpired.collectAsStateWithLifecycle()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(isExpired) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||
|
||||
if (isExpired) {
|
||||
event.changes.forEach { it.consume() }
|
||||
} else if (event.changes.any { it.changedToDown() }) {
|
||||
sessionManager.userInteracted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onPreviewKeyEvent { event ->
|
||||
if (event.type == KeyEventType.KeyUp) {
|
||||
if (isExpired) {
|
||||
return@onPreviewKeyEvent true
|
||||
} else {
|
||||
sessionManager.userInteracted()
|
||||
}
|
||||
}
|
||||
false
|
||||
},
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@ import mifos_mobile.feature.auth.generated.resources.feature_sign_in_password_er
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_sign_in_username_error
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.data.repository.UserAuthRepository
|
||||
import org.mifos.mobile.core.datastore.UserPreferencesRepository
|
||||
import org.mifos.mobile.core.datastore.model.UserData
|
||||
@ -30,6 +31,7 @@ import org.mifos.mobile.core.ui.utils.ScreenUiState
|
||||
class LoginViewModel(
|
||||
private val userAuthRepositoryImpl: UserAuthRepository,
|
||||
private val userPreferencesRepositoryImpl: UserPreferencesRepository,
|
||||
sessionManager: SessionManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LoginState, LoginEvent, LoginAction>(
|
||||
initialState = LoginState(uiState = ScreenUiState.Success),
|
||||
@ -38,6 +40,7 @@ class LoginViewModel(
|
||||
private var loginJob: Job? = null
|
||||
|
||||
init {
|
||||
sessionManager.stopSession()
|
||||
savedStateHandle.get<String>("username")?.let {
|
||||
trySendAction(LoginAction.UsernameChanged(it))
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.mifos.mobile.core.common.Constants
|
||||
import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.data.repository.UserAuthRepository
|
||||
import org.mifos.mobile.core.model.EventType
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
@ -39,11 +40,13 @@ import org.mifos.mobile.feature.auth.login.LoginRoute
|
||||
|
||||
internal class OtpAuthenticationViewModel(
|
||||
private val userAuthRepositoryImpl: UserAuthRepository,
|
||||
sessionManager: SessionManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<OtpAuthState, OtpAuthEvent, OtpAuthAction>(
|
||||
initialState = OtpAuthState(dialogState = null),
|
||||
) {
|
||||
init {
|
||||
sessionManager.stopSession()
|
||||
val nextRoute = savedStateHandle.toRoute<OtpAuthenticationRoute>()
|
||||
|
||||
mutableStateFlow.update {
|
||||
|
||||
@ -24,6 +24,7 @@ import mifos_mobile.feature.auth.generated.resources.feature_recover_now_invalid
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_recover_now_phone_number_error
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_recover_now_phone_number_required
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
import org.mifos.mobile.core.ui.utils.ScreenUiState
|
||||
import org.mifos.mobile.core.ui.utils.ValidationHelper
|
||||
@ -31,11 +32,17 @@ import org.mifos.mobile.feature.auth.setNewPassword.SetPasswordRoute
|
||||
|
||||
const val PHONE_NUMBER_LENGTH = 10
|
||||
|
||||
internal class RecoverPasswordViewModel :
|
||||
internal class RecoverPasswordViewModel(
|
||||
sessionManager: SessionManager,
|
||||
) :
|
||||
BaseViewModel<RecoverPasswordState, RecoverPasswordEvent, RecoverPasswordAction>(
|
||||
initialState = RecoverPasswordState(),
|
||||
) {
|
||||
|
||||
init {
|
||||
sessionManager.stopSession()
|
||||
}
|
||||
|
||||
private var validationJob: Job? = null
|
||||
|
||||
override fun handleAction(action: RecoverPasswordAction) {
|
||||
|
||||
@ -27,6 +27,7 @@ import mifos_mobile.feature.auth.generated.resources.feature_signup_error_passwo
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_short
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.data.repository.UserAuthRepository
|
||||
import org.mifos.mobile.core.model.entity.register.RegisterPayload
|
||||
import org.mifos.mobile.core.ui.PasswordStrengthState
|
||||
@ -48,10 +49,15 @@ import org.mifos.mobile.core.ui.utils.ValidationHelper
|
||||
@Suppress("TooManyFunctions")
|
||||
class RegistrationViewModel(
|
||||
private val userAuthRepositoryImpl: UserAuthRepository,
|
||||
sessionManager: SessionManager,
|
||||
) : BaseViewModel<SignUpState, SignUpEvent, SignUpAction>(
|
||||
initialState = SignUpState(),
|
||||
) {
|
||||
|
||||
init {
|
||||
sessionManager.stopSession()
|
||||
}
|
||||
|
||||
private var validationJob: Job? = null
|
||||
private var passwordStrengthJob: Job = Job()
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ import mifos_mobile.feature.auth.generated.resources.feature_signup_error_passwo
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_required_error
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.model.EventType
|
||||
import org.mifos.mobile.core.ui.PasswordStrengthState
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
@ -34,10 +35,16 @@ import org.mifos.mobile.core.ui.utils.PasswordStrengthResult
|
||||
import org.mifos.mobile.core.ui.utils.ScreenUiState
|
||||
import org.mifos.mobile.feature.auth.login.LoginRoute
|
||||
|
||||
internal class SetPasswordViewModel : BaseViewModel<SetPasswordState, SetPasswordEvent, SetPasswordAction>(
|
||||
internal class SetPasswordViewModel(
|
||||
sessionManager: SessionManager,
|
||||
) : BaseViewModel<SetPasswordState, SetPasswordEvent, SetPasswordAction>(
|
||||
initialState = SetPasswordState(dialogState = null),
|
||||
) {
|
||||
|
||||
init {
|
||||
sessionManager.stopSession()
|
||||
}
|
||||
|
||||
private var validationJob: Job? = null
|
||||
private var passwordStrengthJob: Job = Job()
|
||||
|
||||
|
||||
@ -31,15 +31,22 @@ import mifos_mobile.feature.auth.generated.resources.feature_upload_id_error_mob
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_upload_id_error_photo_required
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_upload_id_upload_failed
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.common.toBase64DataUri
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
import org.mifos.mobile.core.ui.utils.ValidationHelper
|
||||
import org.mifos.mobile.feature.auth.recoverPassword.PHONE_NUMBER_LENGTH
|
||||
|
||||
internal class UploadIdViewModel :
|
||||
internal class UploadIdViewModel(
|
||||
sessionManager: SessionManager,
|
||||
) :
|
||||
BaseViewModel<UploadIdUiState, UploadIdEvent, UploadIdAction>(
|
||||
initialState = UploadIdUiState(dialogState = null),
|
||||
) {
|
||||
|
||||
init {
|
||||
sessionManager.stopSession()
|
||||
}
|
||||
override fun handleAction(action: UploadIdAction) {
|
||||
when (action) {
|
||||
UploadIdAction.OnBackClick -> sendEvent(UploadIdEvent.BackClick)
|
||||
|
||||
@ -21,6 +21,7 @@ import mifos_mobile.feature.home.generated.resources.feature_server_error
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.mifos.mobile.core.common.CurrencyFormatter
|
||||
import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.data.repository.HomeRepository
|
||||
import org.mifos.mobile.core.data.util.NetworkMonitor
|
||||
import org.mifos.mobile.core.datastore.UserPreferencesRepository
|
||||
@ -45,6 +46,7 @@ import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
internal class HomeViewModel(
|
||||
private val homeRepositoryImpl: HomeRepository,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val sessionManager: SessionManager,
|
||||
userPreferencesRepositoryImpl: UserPreferencesRepository,
|
||||
) : BaseViewModel<HomeState, HomeEvent, HomeAction>(
|
||||
initialState = HomeState(
|
||||
@ -58,6 +60,7 @@ internal class HomeViewModel(
|
||||
private var isHandlingNetworkChange = false
|
||||
|
||||
init {
|
||||
sessionManager.startSession()
|
||||
observeNetworkStatus()
|
||||
}
|
||||
|
||||
|
||||
@ -23,4 +23,6 @@
|
||||
|
||||
<string name="feature_passcode_authenticate">Ingresa el PIN de autenticación</string>
|
||||
<string name="feature_passcode_authenticate_tip">Confirma que eres tú ingresando tu PIN de autenticación de 4 dígitos para completar tu acción</string>
|
||||
<string name="feature_passcode_unlock_success">¡Desbloqueado!</string>
|
||||
<string name="feature_passcode_unlock_success_msg">Has verificado tu identidad correctamente.</string>
|
||||
</resources>
|
||||
|
||||
@ -23,4 +23,7 @@
|
||||
|
||||
<string name="feature_passcode_authenticate">Enter Authentication Code</string>
|
||||
<string name="feature_passcode_authenticate_tip">Confirm it’s you by entering your 4-digit authentication code in order to complete your action</string>
|
||||
|
||||
<string name="feature_passcode_unlock_success">Unlocked!</string>
|
||||
<string name="feature_passcode_unlock_success_msg">You have successfully verified your identity.</string>
|
||||
</resources>
|
||||
|
||||
@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import mifos_mobile.feature.passcode.generated.resources.Res
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_authenticate
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_confirm
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_setup
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_tip
|
||||
@ -121,6 +122,7 @@ private fun PasscodeScreenContent(
|
||||
text = when (state.mode) {
|
||||
PasscodeMode.Set -> stringResource(Res.string.feature_passcode_setup)
|
||||
PasscodeMode.Confirm -> stringResource(Res.string.feature_passcode_confirm)
|
||||
PasscodeMode.Verify -> stringResource(Res.string.feature_passcode_authenticate)
|
||||
},
|
||||
style = MifosTypography.titleMedium,
|
||||
color = KptTheme.colorScheme.onBackground,
|
||||
|
||||
@ -10,24 +10,45 @@
|
||||
package org.mifos.mobile.feature.passcode
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import mifos_mobile.feature.passcode.generated.resources.Res
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_common_continue
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_setup_successful
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_setup_successful_msg
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_unlock_success
|
||||
import mifos_mobile.feature.passcode.generated.resources.feature_passcode_unlock_success_msg
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.mifos.mobile.core.common.Constants
|
||||
import org.mifos.mobile.core.common.SessionManager
|
||||
import org.mifos.mobile.core.datastore.UserPreferencesRepository
|
||||
import org.mifos.mobile.core.model.EventType
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
|
||||
internal class PasscodeViewModel(
|
||||
private val userPreferencesRepository: UserPreferencesRepository,
|
||||
sessionManager: SessionManager,
|
||||
) : BaseViewModel<PasscodeState, PasscodeEvent, PasscodeAction>(
|
||||
initialState = PasscodeState(),
|
||||
) {
|
||||
|
||||
init {
|
||||
sessionManager.stopSession()
|
||||
|
||||
viewModelScope.launch {
|
||||
val storedPasscode = userPreferencesRepository.passcode.firstOrNull()
|
||||
if (!storedPasscode.isNullOrEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
mode = PasscodeMode.Verify,
|
||||
storedPasscode = storedPasscode,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var passcodeBuilder: StringBuilder = StringBuilder()
|
||||
|
||||
override fun handleAction(action: PasscodeAction) {
|
||||
@ -77,6 +98,7 @@ internal class PasscodeViewModel(
|
||||
if (confirm == state.firstPasscode) {
|
||||
viewModelScope.launch {
|
||||
userPreferencesRepository.setPasscode(confirm)
|
||||
userPreferencesRepository.setIsUnlocked(true)
|
||||
sendEvent(
|
||||
PasscodeEvent.OnPasscodeConfirm(
|
||||
eventType = EventType.SUCCESS.name,
|
||||
@ -100,10 +122,42 @@ internal class PasscodeViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PasscodeMode.Verify -> viewModelScope.launch {
|
||||
val storedPasscode = userPreferencesRepository.passcode.firstOrNull()
|
||||
val entered = passcodeBuilder.toString()
|
||||
if (entered == storedPasscode) {
|
||||
userPreferencesRepository.setIsUnlocked(true)
|
||||
sendEvent(
|
||||
PasscodeEvent.OnPasscodeConfirm(
|
||||
eventType = EventType.SUCCESS.name,
|
||||
eventDestination = "UNLOCK_ACTION",
|
||||
title = getString(Res.string.feature_passcode_unlock_success),
|
||||
subtitle = getString(Res.string.feature_passcode_unlock_success_msg),
|
||||
buttonText = getString(Res.string.feature_passcode_common_continue),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
handleWrongPasscode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWrongPasscode() {
|
||||
passcodeBuilder.clear()
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
firstPasscode = if (it.mode == PasscodeMode.Confirm) "" else it.firstPasscode,
|
||||
mode = if (it.mode == PasscodeMode.Confirm) PasscodeMode.Set else it.mode,
|
||||
passcode = "",
|
||||
filledDots = 0,
|
||||
passcodeError = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePasscodeState() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
@ -144,4 +198,5 @@ internal sealed interface PasscodeAction {
|
||||
enum class PasscodeMode {
|
||||
Set,
|
||||
Confirm,
|
||||
Verify,
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user