mirror of
https://github.com/openMF/mifos-mobile.git
synced 2026-02-06 11:26:51 +00:00
refactor: enhance navigation and screens (#2858)
This commit is contained in:
parent
ca5bb5902b
commit
d3ccfb3678
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.android.app
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mifos.mobile.core.model.DarkThemeConfig
|
||||
|
||||
@ColorInt
|
||||
private val SCRIM_COLOR: Int = Color.TRANSPARENT
|
||||
|
||||
/**
|
||||
* Helper method to handle edge-to-edge logic for dark mode.
|
||||
*
|
||||
* This logic is from the Now-In-Android app found
|
||||
* [here](https://github.com/android/nowinandroid/blob/689ef92e41427ab70f82e2c9fe59755441deae92/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt#L94).
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
fun ComponentActivity.setupEdgeToEdge(
|
||||
appThemeFlow: Flow<DarkThemeConfig>,
|
||||
) {
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
|
||||
combine(
|
||||
isSystemInDarkModeFlow(),
|
||||
appThemeFlow,
|
||||
) { isSystemDarkMode, appTheme ->
|
||||
AppCompatDelegate.setDefaultNightMode(appTheme.osValue)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { isDarkMode ->
|
||||
// This handles all the settings to go edge-to-edge. We are using a transparent
|
||||
// scrim for system bars and switching between "light" and "dark" based on the
|
||||
// system and internal app theme settings.
|
||||
val style = SystemBarStyle.auto(
|
||||
darkScrim = SCRIM_COLOR,
|
||||
lightScrim = SCRIM_COLOR,
|
||||
// Disabling Dark Mode for this app
|
||||
detectDarkMode = { false },
|
||||
)
|
||||
enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a configuration change listener to retrieve whether system is in
|
||||
* dark theme or not. This will emit current status immediately and then
|
||||
* will emit changes as needed.
|
||||
*/
|
||||
private fun ComponentActivity.isSystemInDarkModeFlow(): Flow<Boolean> =
|
||||
callbackFlow {
|
||||
channel.trySend(element = resources.configuration.isSystemInDarkMode)
|
||||
val listener = Consumer<Configuration> {
|
||||
channel.trySend(element = it.isSystemInDarkMode)
|
||||
}
|
||||
addOnConfigurationChangedListener(listener = listener)
|
||||
awaitClose { removeOnConfigurationChangedListener(listener = listener) }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.conflate()
|
||||
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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.android.app
|
||||
|
||||
import android.content.res.Configuration
|
||||
|
||||
val Configuration.isSystemInDarkMode
|
||||
get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
@ -12,13 +12,18 @@ package cmp.android.app
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import cmp.shared.SharedApp
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import io.github.vinceglb.filekit.dialogs.init
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.mifos.mobile.core.datastore.UserPreferencesRepository
|
||||
import org.mifos.mobile.core.ui.utils.ShareUtils
|
||||
import java.util.Locale
|
||||
import kotlin.getValue
|
||||
|
||||
/**
|
||||
* Main activity class.
|
||||
@ -32,13 +37,18 @@ class MainActivity : ComponentActivity() {
|
||||
* Called when the activity is starting.
|
||||
* This is where most initialization should go: calling [setContentView(int)] to inflate the activity's UI,
|
||||
*/
|
||||
|
||||
private val userPreferencesRepository: UserPreferencesRepository by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
|
||||
installSplashScreen()
|
||||
val darkThemeConfigFlow = userPreferencesRepository.observeDarkThemeConfig
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
enableEdgeToEdge()
|
||||
setupEdgeToEdge(darkThemeConfigFlow)
|
||||
ShareUtils.setActivityProvider { return@setActivityProvider this }
|
||||
FileKit.init(this)
|
||||
/**
|
||||
@ -46,7 +56,22 @@ class MainActivity : ComponentActivity() {
|
||||
* @see setContent
|
||||
*/
|
||||
setContent {
|
||||
SharedApp()
|
||||
SharedApp(
|
||||
handleThemeMode = {
|
||||
AppCompatDelegate.setDefaultNightMode(it)
|
||||
},
|
||||
handleAppLocale = {
|
||||
it?.let {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(it),
|
||||
)
|
||||
Locale.setDefault(Locale(it))
|
||||
}
|
||||
},
|
||||
onSplashScreenRemoved = {
|
||||
shouldShowSplashScreen = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,11 @@ fun main() {
|
||||
title = stringResource(Res.string.application_title),
|
||||
) {
|
||||
// Sets the content of the window.
|
||||
SharedApp()
|
||||
SharedApp(
|
||||
handleThemeMode = {},
|
||||
handleAppLocale = {},
|
||||
onSplashScreenRemoved = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ kotlin {
|
||||
implementation(projects.feature.notification)
|
||||
implementation(projects.feature.userProfile)
|
||||
implementation(projects.feature.location)
|
||||
implementation(projects.feature.onboardingLanguage)
|
||||
// Core Modules
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.common)
|
||||
@ -55,6 +56,7 @@ kotlin {
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
}
|
||||
androidMain.dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">CMP App</string>
|
||||
<string name="app_name">Mifos Mobile</string>
|
||||
<string name="home">Home</string>
|
||||
<string name="transfer">Transfer</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="not_connected">⚠️ You aren’t connected to the internet</string>
|
||||
</resources>
|
||||
@ -9,65 +9,41 @@
|
||||
*/
|
||||
package cmp.navigation
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import cmp.navigation.navigation.NavGraphRoute.AUTH_GRAPH
|
||||
import cmp.navigation.navigation.NavGraphRoute.PASSCODE_GRAPH
|
||||
import cmp.navigation.navigation.RootNavGraph
|
||||
import org.koin.compose.koinInject
|
||||
import cmp.navigation.rootnav.RootNavScreen
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.data.util.NetworkMonitor
|
||||
import org.mifos.mobile.core.datastore.model.AppTheme
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
|
||||
@Composable
|
||||
fun ComposeApp(
|
||||
handleThemeMode: (osValue: Int) -> Unit,
|
||||
handleAppLocale: (locale: String?) -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
networkMonitor: NetworkMonitor = koinInject(),
|
||||
viewModel: ComposeAppViewModel = koinViewModel(),
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
val uiState by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val navDestination = when (uiState) {
|
||||
is MainUiState.Loading -> AUTH_GRAPH
|
||||
is MainUiState.Success -> if ((uiState as MainUiState.Success).userData.isAuthenticated) {
|
||||
PASSCODE_GRAPH
|
||||
} else {
|
||||
AUTH_GRAPH
|
||||
EventsEffect(eventFlow = viewModel.eventFlow) { event ->
|
||||
when (event) {
|
||||
is AppEvent.ShowToast -> {}
|
||||
is AppEvent.UpdateAppLocale -> handleAppLocale(event.localeName)
|
||||
is AppEvent.UpdateAppTheme -> handleThemeMode(event.osValue)
|
||||
}
|
||||
|
||||
else -> AUTH_GRAPH
|
||||
}
|
||||
|
||||
val isDarkMode = when (uiState) {
|
||||
is MainUiState.Success -> when ((uiState as MainUiState.Success).appTheme) {
|
||||
AppTheme.SYSTEM -> isSystemInDarkTheme()
|
||||
AppTheme.LIGHT -> false
|
||||
AppTheme.DARK -> true
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
|
||||
MifosMobileTheme(isDarkMode) {
|
||||
RootNavGraph(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
networkMonitor = networkMonitor,
|
||||
navHostController = navController,
|
||||
startDestination = navDestination,
|
||||
onClickLogout = {
|
||||
viewModel.logOut()
|
||||
navController.navigate(AUTH_GRAPH) {
|
||||
popUpTo(navController.graph.id) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
},
|
||||
MifosMobileTheme(
|
||||
darkTheme = uiState.darkTheme,
|
||||
androidTheme = uiState.isAndroidTheme,
|
||||
shouldDisplayDynamicTheming = uiState.isDynamicColorsEnabled,
|
||||
) {
|
||||
RootNavScreen(
|
||||
modifier = modifier,
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,51 +9,113 @@
|
||||
*/
|
||||
package cmp.navigation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
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.data.util.NetworkMonitor
|
||||
import org.mifos.mobile.core.datastore.UserPreferencesRepository
|
||||
import org.mifos.mobile.core.datastore.model.AppTheme
|
||||
import org.mifos.mobile.core.model.UserData
|
||||
import org.mifos.mobile.core.model.DarkThemeConfig
|
||||
import org.mifos.mobile.core.model.LanguageConfig
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
|
||||
class ComposeAppViewModel(
|
||||
private val userDataRepository: UserDataRepository,
|
||||
private val passcodeManager: PasscodeManager,
|
||||
private val userPreferencesRepository: UserPreferencesRepository,
|
||||
) : ViewModel() {
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
) : BaseViewModel<AppState, AppEvent, AppAction>(
|
||||
initialState = AppState(
|
||||
darkTheme = false,
|
||||
isAndroidTheme = false,
|
||||
isDynamicColorsEnabled = false,
|
||||
),
|
||||
) {
|
||||
val networkStatus = networkMonitor.isOnline
|
||||
.map(Boolean::not)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
private val userDataFlow = userDataRepository.userData
|
||||
private val appThemeFlow = userPreferencesRepository.appTheme
|
||||
init {
|
||||
userPreferencesRepository
|
||||
.observeDarkThemeConfig
|
||||
.onEach { trySendAction(AppAction.Internal.ThemeUpdate(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
val uiState: StateFlow<MainUiState> = combine(userDataFlow, appThemeFlow) { dataState, appTheme ->
|
||||
when (dataState) {
|
||||
is DataState.Success -> MainUiState.Success(dataState.data, appTheme)
|
||||
is DataState.Error -> MainUiState.Error(dataState.exception.message ?: "Unknown error")
|
||||
DataState.Loading -> MainUiState.Loading
|
||||
userPreferencesRepository
|
||||
.observeDynamicColorPreference
|
||||
.onEach { trySendAction(AppAction.Internal.DynamicColorsUpdate(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
userPreferencesRepository
|
||||
.observeLanguage
|
||||
.map { AppEvent.UpdateAppLocale(it.localName) }
|
||||
.onEach(::sendEvent)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: AppAction) {
|
||||
when (action) {
|
||||
is AppAction.AppSpecificLanguageUpdate -> handleAppSpecificLanguageUpdate(action)
|
||||
|
||||
is AppAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
|
||||
is AppAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = MainUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
)
|
||||
}
|
||||
|
||||
fun logOut() {
|
||||
private fun handleAppSpecificLanguageUpdate(action: AppAction.AppSpecificLanguageUpdate) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.logOut()
|
||||
passcodeManager.clearPasscode()
|
||||
userPreferencesRepository.setLanguage(action.appLanguage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAppThemeUpdated(action: AppAction.Internal.ThemeUpdate) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(darkTheme = action.theme == DarkThemeConfig.DARK)
|
||||
}
|
||||
sendEvent(AppEvent.UpdateAppTheme(osValue = action.theme.osValue))
|
||||
}
|
||||
|
||||
private fun handleDynamicColorsUpdate(action: AppAction.Internal.DynamicColorsUpdate) {
|
||||
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface MainUiState {
|
||||
data object Loading : MainUiState
|
||||
data class Error(val error: String) : MainUiState
|
||||
data class Success(val userData: UserData, val appTheme: AppTheme) : MainUiState
|
||||
data class AppState(
|
||||
val darkTheme: Boolean,
|
||||
val isAndroidTheme: Boolean,
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
)
|
||||
|
||||
sealed interface AppEvent {
|
||||
data class ShowToast(val message: String) : AppEvent
|
||||
|
||||
data class UpdateAppLocale(
|
||||
val localeName: String?,
|
||||
) : AppEvent
|
||||
|
||||
data class UpdateAppTheme(
|
||||
val osValue: Int,
|
||||
) : AppEvent
|
||||
}
|
||||
|
||||
sealed interface AppAction {
|
||||
data class AppSpecificLanguageUpdate(val appLanguage: LanguageConfig) : AppAction
|
||||
|
||||
sealed class Internal : AppAction {
|
||||
|
||||
data class ThemeUpdate(
|
||||
val theme: DarkThemeConfig,
|
||||
) : Internal()
|
||||
|
||||
data class DynamicColorsUpdate(
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package cmp.navigation.authenticated
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.navigation
|
||||
import cmp.navigation.authenticatednavbar.AuthenticatedNavbarRoute
|
||||
import cmp.navigation.authenticatednavbar.authenticatedNavbarGraph
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data object AuthenticatedGraphRoute
|
||||
|
||||
internal fun NavController.navigateToAuthenticatedGraph(navOptions: NavOptions? = null) {
|
||||
navigate(route = AuthenticatedGraphRoute, navOptions = navOptions)
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
internal fun NavGraphBuilder.authenticatedGraph(
|
||||
navController: NavController,
|
||||
) {
|
||||
navigation<AuthenticatedGraphRoute>(
|
||||
startDestination = AuthenticatedNavbarRoute,
|
||||
) {
|
||||
authenticatedNavbarGraph()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun NavController.navigateUpToAuthenticatedNavbarRoot() {
|
||||
this.popBackStack<AuthenticatedNavbarRoute>(inclusive = false)
|
||||
}
|
||||
|
||||
// User shouldn't navigate back to intermediate screens when reached to this destination
|
||||
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.authenticatednavbar
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import cmp.navigation.utils.toObjectNavigationRoute
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.ui.navigation.NavigationItem
|
||||
import org.mifos.mobile.feature.home.navigation.HomeRoute
|
||||
import org.mifos.mobile.feature.user.profile.navigation.ProfileGraphRoute
|
||||
import org.mifos.mobile.navigation.generated.resources.Res
|
||||
import org.mifos.mobile.navigation.generated.resources.home
|
||||
import org.mifos.mobile.navigation.generated.resources.profile
|
||||
|
||||
sealed class AuthenticatedNavBarTabItem : NavigationItem {
|
||||
|
||||
data object HomeTab : AuthenticatedNavBarTabItem() {
|
||||
override val iconResSelected: ImageVector
|
||||
get() = MifosIcons.HomeTabFilled
|
||||
override val iconRes: ImageVector
|
||||
get() = MifosIcons.HomeTabFilled
|
||||
override val labelRes: StringResource
|
||||
get() = Res.string.home
|
||||
override val contentDescriptionRes: StringResource
|
||||
get() = Res.string.home
|
||||
override val graphRoute: String
|
||||
get() = HomeRoute.toObjectNavigationRoute()
|
||||
override val startDestinationRoute: String
|
||||
get() = HomeRoute.toObjectNavigationRoute()
|
||||
override val testTag: String
|
||||
get() = "HomeTab"
|
||||
}
|
||||
|
||||
// TODO Add Top level destinations here
|
||||
|
||||
// data object TransferTab : AuthenticatedNavBarTabItem() {
|
||||
// override val iconResSelected: ImageVector
|
||||
// get() = MifosIcons.TransferTab
|
||||
// override val iconRes: ImageVector
|
||||
// get() = MifosIcons.TransferTab
|
||||
// override val labelRes: StringResource
|
||||
// get() = Res.string.transfer
|
||||
// override val contentDescriptionRes: StringResource
|
||||
// get() = Res.string.transfer
|
||||
// override val graphRoute: String
|
||||
// get() = transferNavRoute.toObjectNavigationRoute()
|
||||
// override val startDestinationRoute: String
|
||||
// get() = transferNavRoute.toObjectNavigationRoute()
|
||||
// override val testTag: String
|
||||
// get() = "TransferTab"
|
||||
// }
|
||||
|
||||
data object ProfileTab : AuthenticatedNavBarTabItem() {
|
||||
override val iconResSelected: ImageVector
|
||||
get() = MifosIcons.PersonTabFilled
|
||||
override val iconRes: ImageVector
|
||||
get() = MifosIcons.PersonTabFilled
|
||||
override val labelRes: StringResource
|
||||
get() = Res.string.profile
|
||||
override val contentDescriptionRes: StringResource
|
||||
get() = Res.string.profile
|
||||
override val graphRoute: String
|
||||
get() = ProfileGraphRoute.toObjectNavigationRoute()
|
||||
override val startDestinationRoute: String
|
||||
get() = ProfileGraphRoute.toObjectNavigationRoute()
|
||||
override val testTag: String
|
||||
get() = "ProfileTab"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package cmp.navigation.authenticatednavbar
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.mifos.mobile.core.ui.composableWithStayTransitions
|
||||
|
||||
@Serializable
|
||||
data object AuthenticatedNavbarRoute
|
||||
|
||||
internal fun NavController.navigateToAuthenticatedNavBar(navOptions: NavOptions? = null) {
|
||||
navigate(route = AuthenticatedNavbarRoute, navOptions = navOptions)
|
||||
}
|
||||
|
||||
internal fun NavGraphBuilder.authenticatedNavbarGraph() {
|
||||
composableWithStayTransitions<AuthenticatedNavbarRoute> {
|
||||
AuthenticatedNavbarNavigationScreen()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.authenticatednavbar
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.navOptions
|
||||
import cmp.navigation.ui.MifosScaffold
|
||||
import cmp.navigation.ui.ScaffoldNavigationData
|
||||
import cmp.navigation.ui.rememberMifosNavController
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.ui.RootTransitionProviders
|
||||
import org.mifos.mobile.core.ui.navigation.NavigationItem
|
||||
import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
import org.mifos.mobile.feature.home.navigation.HomeRoute
|
||||
import org.mifos.mobile.feature.home.navigation.homeDestination
|
||||
import org.mifos.mobile.feature.home.navigation.navigateToHomeScreen
|
||||
import org.mifos.mobile.feature.user.profile.navigation.navigateToUserProfileGraph
|
||||
import org.mifos.mobile.feature.user.profile.navigation.userprofileNavGraph
|
||||
import org.mifos.mobile.navigation.generated.resources.Res
|
||||
import org.mifos.mobile.navigation.generated.resources.not_connected
|
||||
|
||||
@Composable
|
||||
internal fun AuthenticatedNavbarNavigationScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavHostController = rememberMifosNavController(
|
||||
name = "AuthenticatedNavbarScreen",
|
||||
),
|
||||
viewModel: AuthenticatedNavbarNavigationViewModel = koinViewModel(),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val isOffline by viewModel.isOffline.collectAsStateWithLifecycle()
|
||||
|
||||
EventsEffect(eventFlow = viewModel.eventFlow) { event ->
|
||||
navController.apply {
|
||||
when (event) {
|
||||
// TODO Add navigation to respective screens
|
||||
AuthenticatedNavBarEvent.NavigateToHomeScreen -> {
|
||||
navigateToTabOrRoot(tabToNavigateTo = event.tab) {
|
||||
navigateToHomeScreen(navOptions = it)
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticatedNavBarEvent.NavigateToUserProfileScreen -> {
|
||||
navigateToTabOrRoot(tabToNavigateTo = event.tab) {
|
||||
navigateToUserProfileGraph(navOptions = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val message = stringResource(Res.string.not_connected)
|
||||
LaunchedEffect(isOffline) {
|
||||
if (isOffline) {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticatedNavbarNavigationScreenContent(
|
||||
navController = navController,
|
||||
snackbarHostState = snackbarHostState,
|
||||
modifier = modifier,
|
||||
onAction = remember(viewModel) {
|
||||
{ viewModel.trySendAction(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun AuthenticatedNavbarNavigationScreenContent(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
onAction: (AuthenticatedNavBarAction) -> Unit,
|
||||
) {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val navigationItems = persistentListOf<NavigationItem>(
|
||||
AuthenticatedNavBarTabItem.HomeTab,
|
||||
AuthenticatedNavBarTabItem.ProfileTab,
|
||||
)
|
||||
|
||||
MifosScaffold(
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
navigationData = ScaffoldNavigationData(
|
||||
navigationItems = navigationItems,
|
||||
selectedNavigationItem = navigationItems.find {
|
||||
navBackStackEntry.isCurrentRoute(route = it.graphRoute)
|
||||
},
|
||||
onNavigationClick = { navigationItem ->
|
||||
// TODO navigate to respective screens
|
||||
when (navigationItem) {
|
||||
is AuthenticatedNavBarTabItem.HomeTab -> {
|
||||
onAction(AuthenticatedNavBarAction.HomeTabClick)
|
||||
}
|
||||
|
||||
is AuthenticatedNavBarTabItem.ProfileTab -> {
|
||||
onAction(AuthenticatedNavBarAction.ProfileTabClick)
|
||||
}
|
||||
}
|
||||
},
|
||||
shouldShowNavigation = navigationItems.any {
|
||||
navBackStackEntry.isCurrentRoute(route = it.startDestinationRoute)
|
||||
},
|
||||
),
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
// Because this Scaffold has a bottom navigation bar, the NavHost will:
|
||||
// - consume the vertical navigation bar insets.
|
||||
// - consume the IME insets.
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = HomeRoute,
|
||||
enterTransition = RootTransitionProviders.Enter.fadeIn,
|
||||
exitTransition = RootTransitionProviders.Exit.fadeOut,
|
||||
popEnterTransition = RootTransitionProviders.Enter.fadeIn,
|
||||
popExitTransition = RootTransitionProviders.Exit.fadeOut,
|
||||
) {
|
||||
// TODO Add top level destination screens
|
||||
|
||||
homeDestination(
|
||||
onNavigate = {},
|
||||
callHelpline = {},
|
||||
mailHelpline = {},
|
||||
)
|
||||
|
||||
userprofileNavGraph(navController, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavController.navigateToTabOrRoot(
|
||||
tabToNavigateTo: AuthenticatedNavBarTabItem,
|
||||
navigate: (NavOptions) -> Unit,
|
||||
) {
|
||||
if (tabToNavigateTo.startDestinationRoute == currentDestination?.route) {
|
||||
// We are at the start destination already, so nothing to do.
|
||||
return
|
||||
} else if (currentDestination?.parent?.route == tabToNavigateTo.graphRoute) {
|
||||
// We are not at the start destination but we are in the correct graph,
|
||||
// so lets pop up to the start destination.
|
||||
popBackStack(route = tabToNavigateTo.startDestinationRoute, inclusive = false)
|
||||
} else {
|
||||
// We are not in correct graph at all, so navigate there.
|
||||
navigate(
|
||||
navOptions {
|
||||
popUpTo(graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavBackStackEntry?.isCurrentRoute(route: String): Boolean =
|
||||
this
|
||||
?.destination
|
||||
?.hierarchy
|
||||
?.any { it.route == route } == true
|
||||
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.authenticatednavbar
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.mifos.mobile.core.data.util.NetworkMonitor
|
||||
import org.mifos.mobile.core.datastore.model.AppSettings
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
|
||||
internal class AuthenticatedNavbarNavigationViewModel(
|
||||
networkMonitor: NetworkMonitor,
|
||||
) : BaseViewModel<Unit, AuthenticatedNavBarEvent, AuthenticatedNavBarAction>(
|
||||
initialState = Unit,
|
||||
) {
|
||||
|
||||
val isOffline = networkMonitor.isOnline
|
||||
.map(Boolean::not)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
override fun handleAction(action: AuthenticatedNavBarAction) {
|
||||
when (action) {
|
||||
AuthenticatedNavBarAction.HomeTabClick -> handleVaultTabClicked()
|
||||
|
||||
is AuthenticatedNavBarAction.Internal -> handleInternalAction(action)
|
||||
|
||||
AuthenticatedNavBarAction.ProfileTabClick -> handleProfileTabClicked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVaultTabClicked() {
|
||||
sendEvent(AuthenticatedNavBarEvent.NavigateToHomeScreen)
|
||||
}
|
||||
|
||||
private fun handleProfileTabClicked() {
|
||||
sendEvent(AuthenticatedNavBarEvent.NavigateToUserProfileScreen)
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: AuthenticatedNavBarAction.Internal) {
|
||||
when (action) {
|
||||
is AuthenticatedNavBarAction.Internal.UserStateUpdateReceive -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AuthenticatedNavBarAction {
|
||||
|
||||
// TODO: Add top level destinations here
|
||||
|
||||
data object HomeTabClick : AuthenticatedNavBarAction()
|
||||
|
||||
data object ProfileTabClick : AuthenticatedNavBarAction()
|
||||
|
||||
sealed class Internal : AuthenticatedNavBarAction() {
|
||||
data class UserStateUpdateReceive(val appSettings: AppSettings?) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AuthenticatedNavBarEvent {
|
||||
|
||||
abstract val tab: AuthenticatedNavBarTabItem
|
||||
|
||||
data object NavigateToHomeScreen : AuthenticatedNavBarEvent() {
|
||||
override val tab: AuthenticatedNavBarTabItem = AuthenticatedNavBarTabItem.HomeTab
|
||||
}
|
||||
|
||||
data object NavigateToUserProfileScreen : AuthenticatedNavBarEvent() {
|
||||
override val tab: AuthenticatedNavBarTabItem = AuthenticatedNavBarTabItem.ProfileTab
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,8 @@
|
||||
package cmp.navigation.di
|
||||
|
||||
import cmp.navigation.ComposeAppViewModel
|
||||
import cmp.navigation.authenticatednavbar.AuthenticatedNavbarNavigationViewModel
|
||||
import cmp.navigation.rootnav.RootNavViewModel
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
import org.mifos.library.passcode.di.PasscodeModule
|
||||
@ -27,6 +29,7 @@ import org.mifos.mobile.feature.home.di.HomeModule
|
||||
import org.mifos.mobile.feature.loan.di.LoanModule
|
||||
import org.mifos.mobile.feature.loanaccount.di.loanAccountModule
|
||||
import org.mifos.mobile.feature.notification.di.NotificationModule
|
||||
import org.mifos.mobile.feature.onboarding.language.di.SetOnboardingLanguageModule
|
||||
import org.mifos.mobile.feature.qr.di.QrModule
|
||||
import org.mifos.mobile.feature.recent.transaction.di.recentTransactionModule
|
||||
import org.mifos.mobile.feature.savings.di.SavingsModule
|
||||
@ -53,6 +56,8 @@ object KoinModules {
|
||||
}
|
||||
private val sharedModule = module {
|
||||
viewModelOf(::ComposeAppViewModel)
|
||||
viewModelOf(::AuthenticatedNavbarNavigationViewModel)
|
||||
viewModelOf(::RootNavViewModel)
|
||||
}
|
||||
private val featureModules = module {
|
||||
includes(
|
||||
@ -76,6 +81,7 @@ object KoinModules {
|
||||
GuarantorModule,
|
||||
NotificationModule,
|
||||
ProfileModule,
|
||||
SetOnboardingLanguageModule,
|
||||
)
|
||||
}
|
||||
private val LibraryModule = module {
|
||||
|
||||
@ -1,256 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import cmp.navigation.ui.AppState
|
||||
import org.mifos.mobile.core.model.entity.TransferSuccessDestination
|
||||
import org.mifos.mobile.core.model.enums.AccountType
|
||||
import org.mifos.mobile.core.model.enums.ChargeType
|
||||
import org.mifos.mobile.core.ui.utils.ShareUtils.callHelpline
|
||||
import org.mifos.mobile.core.ui.utils.ShareUtils.mailHelpline
|
||||
import org.mifos.mobile.core.ui.utils.ShareUtils.openAppInfo
|
||||
import org.mifos.mobile.core.ui.utils.ShareUtils.ossLicensesMenuActivity
|
||||
import org.mifos.mobile.core.ui.utils.ShareUtils.shareApp
|
||||
import org.mifos.mobile.feature.about.navigation.aboutUsNavGraph
|
||||
import org.mifos.mobile.feature.about.navigation.navigateToAboutUsScreen
|
||||
import org.mifos.mobile.feature.accounts.navigation.AccountsNavigation
|
||||
import org.mifos.mobile.feature.accounts.navigation.accountsNavGraph
|
||||
import org.mifos.mobile.feature.accounts.navigation.navigateToAccountsScreen
|
||||
import org.mifos.mobile.feature.auth.navigation.navigateToAuthGraph
|
||||
import org.mifos.mobile.feature.beneficiary.navigation.beneficiaryNavGraph
|
||||
import org.mifos.mobile.feature.beneficiary.navigation.navigateToBeneficiaryApplicationScreen
|
||||
import org.mifos.mobile.feature.beneficiary.navigation.navigateToBeneficiaryListScreen
|
||||
import org.mifos.mobile.feature.charge.navigation.clientChargeNavGraph
|
||||
import org.mifos.mobile.feature.charge.navigation.navigateToClientChargeScreen
|
||||
import org.mifos.mobile.feature.guarantor.navigation.guarantorNavGraph
|
||||
import org.mifos.mobile.feature.guarantor.navigation.navigateToGuarantorListScreen
|
||||
import org.mifos.mobile.feature.help.navigation.helpNavGraph
|
||||
import org.mifos.mobile.feature.help.navigation.navigateToHelpScreen
|
||||
import org.mifos.mobile.feature.home.navigation.HomeDestinations
|
||||
import org.mifos.mobile.feature.home.navigation.HomeNavigation
|
||||
import org.mifos.mobile.feature.home.navigation.homeNavGraph
|
||||
import org.mifos.mobile.feature.home.navigation.navigateToHomeScreen
|
||||
import org.mifos.mobile.feature.loan.navigation.loanNavGraph
|
||||
import org.mifos.mobile.feature.loan.navigation.navigateToLoanApplication
|
||||
import org.mifos.mobile.feature.loan.navigation.navigateToLoanDetailScreen
|
||||
import org.mifos.mobile.feature.location.navigation.locationsNavGraph
|
||||
import org.mifos.mobile.feature.location.navigation.navigateToLocationsScreen
|
||||
import org.mifos.mobile.feature.notification.navigation.navigateToNotificationScreen
|
||||
import org.mifos.mobile.feature.notification.navigation.notificationNavGraph
|
||||
import org.mifos.mobile.feature.qr.navigation.navigateToQrDisplayScreen
|
||||
import org.mifos.mobile.feature.qr.navigation.navigateToQrImportScreen
|
||||
import org.mifos.mobile.feature.qr.navigation.navigateToQrReaderScreen
|
||||
import org.mifos.mobile.feature.qr.navigation.qrNavGraph
|
||||
import org.mifos.mobile.feature.recent.transaction.navigation.navigateToRecentTransactionScreen
|
||||
import org.mifos.mobile.feature.recent.transaction.navigation.recentTransactionNavGraph
|
||||
import org.mifos.mobile.feature.savings.navigation.navigateToSavingsApplicationScreen
|
||||
import org.mifos.mobile.feature.savings.navigation.navigateToSavingsDetailScreen
|
||||
import org.mifos.mobile.feature.savings.navigation.navigateToSavingsMakeTransfer
|
||||
import org.mifos.mobile.feature.savings.navigation.savingsNavGraph
|
||||
import org.mifos.mobile.feature.settings.navigation.navigateToSettings
|
||||
import org.mifos.mobile.feature.settings.navigation.settingsNavGraph
|
||||
import org.mifos.mobile.feature.third.party.transfer.navigation.navigateToThirdPartyTransfer
|
||||
import org.mifos.mobile.feature.third.party.transfer.navigation.thirdPartyTransferNavGraph
|
||||
import org.mifos.mobile.feature.transfer.process.navigation.navigateToTransferProcessScreen
|
||||
import org.mifos.mobile.feature.transfer.process.navigation.transferProcessNavGraph
|
||||
import org.mifos.mobile.feature.update.password.navigation.navigateToUpdatePassword
|
||||
import org.mifos.mobile.feature.update.password.navigation.updatePasswordNavGraph
|
||||
import org.mifos.mobile.feature.user.profile.navigation.navigateToUserProfile
|
||||
import org.mifos.mobile.feature.user.profile.navigation.userProfileNavGraph
|
||||
|
||||
@Composable
|
||||
internal fun FeatureNavHost(
|
||||
appState: AppState,
|
||||
onClickLogout: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
NavHost(
|
||||
route = NavGraphRoute.MAIN_GRAPH,
|
||||
startDestination = HomeNavigation.HomeBase.route,
|
||||
navController = appState.navController,
|
||||
modifier = modifier,
|
||||
) {
|
||||
helpNavGraph(
|
||||
findLocations = appState.navController::navigateToLocationsScreen,
|
||||
navigateBack = appState.navController::popBackStack,
|
||||
callHelpline = { callHelpline() },
|
||||
mailHelpline = { mailHelpline() },
|
||||
)
|
||||
homeNavGraph(
|
||||
onNavigate = { handleHomeNavigation(appState.navController, it, onClickLogout) },
|
||||
callHelpline = { callHelpline() },
|
||||
mailHelpline = { mailHelpline() },
|
||||
)
|
||||
|
||||
accountsNavGraph(
|
||||
navController = appState.navController,
|
||||
navigateToLoanApplicationScreen = appState.navController::navigateToLoanApplication,
|
||||
navigateToSavingsApplicationScreen = { appState.navController::navigateToSavingsApplicationScreen },
|
||||
navigateToAccountDetail = { accountType, id ->
|
||||
when (accountType) {
|
||||
AccountType.SAVINGS -> {
|
||||
appState.navController.navigateToSavingsDetailScreen(savingsId = id)
|
||||
}
|
||||
AccountType.LOAN -> {
|
||||
appState.navController.navigateToLoanDetailScreen(loanId = id)
|
||||
}
|
||||
AccountType.SHARE -> { }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
savingsNavGraph(
|
||||
navController = appState.navController,
|
||||
viewCharges = { chargeType, chargeTypeId ->
|
||||
appState.navController.navigateToClientChargeScreen(chargeType, chargeTypeId)
|
||||
},
|
||||
viewQrCode = { appState.navController.navigateToQrDisplayScreen(it) },
|
||||
callHelpline = { callHelpline() },
|
||||
reviewTransfer = { transferPayload, transferType, transferDestination ->
|
||||
appState.navController.navigateToTransferProcessScreen(
|
||||
transferPayload,
|
||||
transferType,
|
||||
transferDestination,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
aboutUsNavGraph(
|
||||
navController = appState.navController,
|
||||
navigateToOssLicense = { ossLicensesMenuActivity() },
|
||||
)
|
||||
|
||||
recentTransactionNavGraph(appState.navController)
|
||||
|
||||
loanNavGraph(
|
||||
navController = appState.navController,
|
||||
viewQr = { appState.navController.navigateToQrDisplayScreen(it) },
|
||||
viewGuarantor = { appState.navController.navigateToGuarantorListScreen(it) },
|
||||
viewCharges = { chargeType, chargeTypeId ->
|
||||
appState.navController.navigateToClientChargeScreen(chargeType, chargeTypeId)
|
||||
},
|
||||
makePayment = { args ->
|
||||
appState.navController.navigateToSavingsMakeTransfer(
|
||||
args,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
clientChargeNavGraph(
|
||||
navigateBack = { appState.navController.popBackStack() },
|
||||
)
|
||||
|
||||
thirdPartyTransferNavGraph(
|
||||
navigateBack = { appState.navController.popBackStack() },
|
||||
addBeneficiary = { },
|
||||
reviewTransfer = { transferPayload, transferType, transferDestination ->
|
||||
appState.navController.navigateToTransferProcessScreen(
|
||||
transferPayload,
|
||||
transferType,
|
||||
transferDestination,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
updatePasswordNavGraph {
|
||||
appState.navController.popBackStack()
|
||||
}
|
||||
|
||||
transferProcessNavGraph(
|
||||
navigateBack = { appState.navController.popBackStack() },
|
||||
onTransferSuccessNavigate = { destination ->
|
||||
when (destination) {
|
||||
TransferSuccessDestination.HOME -> appState.navController.navigateToHomeScreen()
|
||||
TransferSuccessDestination.LOAN_ACCOUNT ->
|
||||
appState.navController.navigateToAccountsScreen(
|
||||
AccountType.LOAN,
|
||||
AccountsNavigation.AccountsBase.route,
|
||||
)
|
||||
|
||||
TransferSuccessDestination.SAVINGS_ACCOUNT -> appState.navController.navigateToAccountsScreen(
|
||||
AccountType.SAVINGS,
|
||||
AccountsNavigation.AccountsBase.route,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
beneficiaryNavGraph(
|
||||
navController = appState.navController,
|
||||
openQrImportScreen = { appState.navController.navigateToQrImportScreen() },
|
||||
openQrReaderScreen = { appState.navController.navigateToQrReaderScreen() },
|
||||
)
|
||||
|
||||
settingsNavGraph(
|
||||
navigateBack = { appState.navController.popBackStack() },
|
||||
navigateToLoginScreen = { appState.navController::navigateToAuthGraph },
|
||||
changePasscode = { },
|
||||
changePassword = { appState.navController::navigateToUpdatePassword.invoke() },
|
||||
languageChanged = { },
|
||||
)
|
||||
|
||||
locationsNavGraph()
|
||||
|
||||
guarantorNavGraph(
|
||||
navController = appState.navController,
|
||||
)
|
||||
|
||||
qrNavGraph(
|
||||
navController = appState.navController,
|
||||
openBeneficiaryApplication = { beneficiary, beneficiaryState ->
|
||||
appState.navController
|
||||
.navigateToBeneficiaryApplicationScreen(beneficiary, beneficiaryState)
|
||||
},
|
||||
)
|
||||
notificationNavGraph(navigateBack = appState.navController::popBackStack)
|
||||
userProfileNavGraph(
|
||||
navigateBack = { appState.navController.popBackStack() },
|
||||
navigateToChangePassword = { appState.navController::navigateToUpdatePassword.invoke() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleHomeNavigation(
|
||||
navController: NavHostController,
|
||||
homeDestinations: HomeDestinations,
|
||||
onClickLogout: () -> Unit,
|
||||
) {
|
||||
when (homeDestinations) {
|
||||
HomeDestinations.LOGOUT -> onClickLogout.invoke()
|
||||
HomeDestinations.HOME -> Unit
|
||||
HomeDestinations.ACCOUNTS -> navController.navigateToAccountsScreen()
|
||||
HomeDestinations.LOAN_ACCOUNT -> navController.navigateToAccountsScreen(accountType = AccountType.LOAN)
|
||||
HomeDestinations.SAVINGS_ACCOUNT -> navController.navigateToAccountsScreen(accountType = AccountType.SAVINGS)
|
||||
HomeDestinations.RECENT_TRANSACTIONS -> navController.navigateToRecentTransactionScreen()
|
||||
HomeDestinations.CHARGES -> navController.navigateToClientChargeScreen(ChargeType.CLIENT, -1L)
|
||||
HomeDestinations.THIRD_PARTY_TRANSFER -> navController.navigateToThirdPartyTransfer()
|
||||
HomeDestinations.SETTINGS -> {
|
||||
navController.navigateToSettings()
|
||||
}
|
||||
|
||||
HomeDestinations.ABOUT_US -> navController.navigateToAboutUsScreen()
|
||||
HomeDestinations.HELP -> navController.navigateToHelpScreen()
|
||||
HomeDestinations.SHARE -> shareApp()
|
||||
HomeDestinations.APP_INFO -> openAppInfo()
|
||||
HomeDestinations.TRANSFER -> {
|
||||
navController.navigateToSavingsMakeTransfer(
|
||||
null,
|
||||
)
|
||||
}
|
||||
HomeDestinations.BENEFICIARIES -> navController.navigateToBeneficiaryListScreen()
|
||||
HomeDestinations.SURVEY -> {}
|
||||
HomeDestinations.NOTIFICATIONS -> navController.navigateToNotificationScreen()
|
||||
HomeDestinations.PROFILE -> navController.navigateToUserProfile()
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ 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
|
||||
|
||||
@ -23,19 +24,19 @@ internal fun NavGraphBuilder.passcodeNavGraph(navController: NavHostController)
|
||||
passcodeRoute(
|
||||
onForgotButton = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(NavGraphRoute.MAIN_GRAPH)
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
},
|
||||
onSkipButton = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(NavGraphRoute.MAIN_GRAPH)
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
},
|
||||
onPasscodeConfirm = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(NavGraphRoute.MAIN_GRAPH)
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
},
|
||||
onPasscodeRejected = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(NavGraphRoute.MAIN_GRAPH)
|
||||
navController.navigate(AuthenticatedGraphRoute)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun RootNavGraph(
|
||||
networkMonitor: NetworkMonitor,
|
||||
navHostController: NavHostController,
|
||||
startDestination: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickLogout: () -> Unit,
|
||||
) {
|
||||
NavHost(
|
||||
navController = navHostController,
|
||||
startDestination = startDestination,
|
||||
route = NavGraphRoute.ROOT_GRAPH,
|
||||
) {
|
||||
authenticationNavGraph(
|
||||
navController = navHostController,
|
||||
navigateToPasscodeScreen = navHostController::navigateToPasscodeScreen,
|
||||
|
||||
)
|
||||
|
||||
passcodeNavGraph(navHostController)
|
||||
|
||||
composable(MAIN_GRAPH) {
|
||||
App(
|
||||
modifier = modifier,
|
||||
networkMonitor = networkMonitor,
|
||||
onClickLogout = onClickLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.rootnav
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
/**
|
||||
* The route for the root navigation screen.
|
||||
*/
|
||||
const val ROOT_ROUTE: String = "root"
|
||||
|
||||
/**
|
||||
* Add the root navigation screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.rootNavDestination(
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
composable(route = ROOT_ROUTE) {
|
||||
RootNavScreen(onSplashScreenRemoved = onSplashScreenRemoved)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.rootnav
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
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.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
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.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.viewmodel.koinViewModel
|
||||
import org.mifos.library.passcode.navigateToPasscodeScreen
|
||||
import org.mifos.mobile.core.ui.NonNullEnterTransitionProvider
|
||||
import org.mifos.mobile.core.ui.NonNullExitTransitionProvider
|
||||
import org.mifos.mobile.core.ui.RootTransitionProviders
|
||||
import org.mifos.mobile.feature.auth.navigation.AuthGraphRoute
|
||||
import org.mifos.mobile.feature.auth.navigation.authenticationNavGraph
|
||||
import org.mifos.mobile.feature.auth.navigation.navigateToAuthGraph
|
||||
import org.mifos.mobile.feature.onboarding.language.navigation.OnboardingLanguageRoute
|
||||
import org.mifos.mobile.feature.onboarding.language.navigation.navigateToOnboardingLanguage
|
||||
import org.mifos.mobile.feature.onboarding.language.navigation.onBoardingLanguageDestination
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun RootNavScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: RootNavViewModel = koinViewModel(),
|
||||
navController: NavHostController = rememberMifosNavController(name = "RootNavScreen"),
|
||||
onSplashScreenRemoved: () -> Unit = {},
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val previousStateReference = remember { mutableStateOf(state) }
|
||||
|
||||
val isNotSplashScreen = state != RootNavState.Splash
|
||||
LaunchedEffect(isNotSplashScreen) {
|
||||
if (isNotSplashScreen) onSplashScreenRemoved()
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = SplashRoute,
|
||||
modifier = modifier,
|
||||
enterTransition = { toEnterTransition()(this) },
|
||||
exitTransition = { toExitTransition()(this) },
|
||||
popEnterTransition = { toEnterTransition()(this) },
|
||||
popExitTransition = { toExitTransition()(this) },
|
||||
) {
|
||||
splashDestination()
|
||||
onBoardingLanguageDestination()
|
||||
authenticationNavGraph(navController, navController::navigateToPasscodeScreen)
|
||||
authenticatedGraph(navController)
|
||||
// userUnlockDestination()
|
||||
passcodeNavGraph(navController)
|
||||
}
|
||||
|
||||
val targetRoute = when (state) {
|
||||
RootNavState.SetLanguage -> OnboardingLanguageRoute
|
||||
RootNavState.Auth -> AuthGraphRoute
|
||||
RootNavState.Splash -> SplashRoute
|
||||
// RootNavState.UserLocked -> UserUnlockRoute.Standard
|
||||
RootNavState.UserLocked -> { }
|
||||
is RootNavState.UserUnlocked -> AuthenticatedGraphRoute
|
||||
}
|
||||
val currentRoute = navController.currentDestination?.rootLevelRoute()
|
||||
|
||||
// Don't navigate if we are already at the correct root. This notably happens during process
|
||||
// death. In this case, the NavHost already restores state, so we don't have to navigate.
|
||||
// However, if the route is correct but the underlying state is different, we should still
|
||||
// proceed in order to get a fresh version of that route.
|
||||
if (currentRoute == targetRoute.toObjectNavigationRoute() &&
|
||||
previousStateReference.value == state
|
||||
) {
|
||||
previousStateReference.value == state
|
||||
return
|
||||
}
|
||||
previousStateReference.value = state
|
||||
|
||||
// When state changes, navigate to different root navigation state
|
||||
val rootNavOptions = navOptions {
|
||||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(navController.graph.id) {
|
||||
inclusive = false
|
||||
saveState = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
|
||||
// Use a LaunchedEffect to ensure we don't navigate too soon when the app first opens. This
|
||||
// avoids a bug that first appeared in Compose Material3 1.2.0-rc01 that causes the initial
|
||||
// transition to appear corrupted.
|
||||
LaunchedEffect(state) {
|
||||
when (state) {
|
||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
|
||||
RootNavState.SetLanguage -> navController.navigateToOnboardingLanguage(rootNavOptions)
|
||||
// RootNavState.UserLocked -> navController.navigateToUserUnlock(rootNavOptions)
|
||||
RootNavState.UserLocked -> { }
|
||||
is RootNavState.UserUnlocked -> navController.navigateToAuthenticatedGraph(
|
||||
navOptions = rootNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavDestination?.rootLevelRoute(): String? = when {
|
||||
this == null -> null
|
||||
parent?.route == null -> route
|
||||
else -> parent.rootLevelRoute()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun AnimatedContentTransitionScope<NavBackStackEntry>.toEnterTransition(): NonNullEnterTransitionProvider =
|
||||
when (targetState.destination.rootLevelRoute()) {
|
||||
SplashRoute.toObjectNavigationRoute() -> RootTransitionProviders.Enter.none
|
||||
else -> RootTransitionProviders.Enter.fadeIn
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun AnimatedContentTransitionScope<NavBackStackEntry>.toExitTransition(): NonNullExitTransitionProvider {
|
||||
return when (initialState.destination.rootLevelRoute()) {
|
||||
// Disable transitions when coming from the splash screen
|
||||
SplashRoute.toObjectNavigationRoute() -> RootTransitionProviders.Exit.none
|
||||
else -> RootTransitionProviders.Exit.fadeOut
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.rootnav
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cmp.navigation.rootnav.RootNavAction.Internal.UserStateUpdateReceive
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.mifos.mobile.core.data.repository.UserDataRepository
|
||||
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,
|
||||
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
|
||||
initialState = RootNavState.Splash,
|
||||
) {
|
||||
|
||||
init {
|
||||
combine(
|
||||
userDataRepository.authState,
|
||||
userDataRepository.settingsState,
|
||||
) { authState, settingsData ->
|
||||
UserStateUpdateReceive(
|
||||
authState = authState,
|
||||
settingsData = settingsData,
|
||||
)
|
||||
}.onEach(::handleAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: RootNavAction) {
|
||||
when (action) {
|
||||
is UserStateUpdateReceive -> handleUserStateUpdateReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserStateUpdateReceive(
|
||||
action: UserStateUpdateReceive,
|
||||
) {
|
||||
val settingsData = action.settingsData
|
||||
println("User data in view model $settingsData")
|
||||
val updatedRootNavState = when {
|
||||
settingsData.firstTimeState -> RootNavState.SetLanguage
|
||||
|
||||
!settingsData.isAuthenticated -> RootNavState.Auth
|
||||
|
||||
// settingsData.passcode.isEmpty() -> RootNavState.UserLocked
|
||||
// TODO: Passcode library needs to update so that it updates passcode in datastore
|
||||
settingsData.passcode.isEmpty() -> {
|
||||
RootNavState.UserUnlocked(settingsData.userId)
|
||||
}
|
||||
|
||||
settingsData.isUnlocked -> {
|
||||
RootNavState.UserUnlocked(settingsData.userId)
|
||||
}
|
||||
|
||||
else -> RootNavState.UserLocked
|
||||
}
|
||||
mutableStateFlow.update { updatedRootNavState }
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RootNavState {
|
||||
data object Auth : RootNavState()
|
||||
|
||||
data object SetLanguage : RootNavState()
|
||||
|
||||
data object Splash : RootNavState()
|
||||
|
||||
data object UserLocked : RootNavState()
|
||||
|
||||
data class UserUnlocked(
|
||||
val activeUserId: String,
|
||||
) : RootNavState()
|
||||
}
|
||||
|
||||
sealed class RootNavAction {
|
||||
|
||||
sealed class Internal {
|
||||
|
||||
data class UserStateUpdateReceive(
|
||||
val authState: AuthState,
|
||||
val settingsData: AppSettings,
|
||||
) : RootNavAction()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package cmp.navigation.splash
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object SplashRoute
|
||||
|
||||
fun NavGraphBuilder.splashDestination() {
|
||||
composable<SplashRoute> { SplashScreen() }
|
||||
}
|
||||
|
||||
fun NavController.navigateToSplash(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
navigate(SplashRoute, navOptions)
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.splash
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
fun SplashScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
color = Color.White,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cmp.navigation.navigation.FeatureNavHost
|
||||
import org.mifos.mobile.core.data.util.NetworkMonitor
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosBackground
|
||||
|
||||
@Composable
|
||||
fun App(
|
||||
networkMonitor: NetworkMonitor,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickLogout: () -> Unit,
|
||||
) {
|
||||
MifosBackground(modifier) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val appState = rememberAppState(
|
||||
networkMonitor = networkMonitor,
|
||||
)
|
||||
|
||||
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
||||
|
||||
// If user is not connected stringResource the internet show a snack bar to inform them.
|
||||
// val notConnectedMessage = (Res.string.not_connected)
|
||||
val notConnectedMessage = "you have lost network connection"
|
||||
LaunchedEffect(isOffline) {
|
||||
if (isOffline) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = notConnectedMessage,
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier,
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Horizontal,
|
||||
),
|
||||
),
|
||||
) {
|
||||
FeatureNavHost(
|
||||
appState = appState,
|
||||
onClickLogout = onClickLogout,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.mifos.mobile.core.data.util.NetworkMonitor
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@Composable
|
||||
internal fun rememberAppState(
|
||||
networkMonitor: NetworkMonitor,
|
||||
windowSizeClass: WindowSizeClass = calculateWindowSizeClass(),
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
): AppState {
|
||||
return remember(
|
||||
navController,
|
||||
coroutineScope,
|
||||
windowSizeClass,
|
||||
networkMonitor,
|
||||
) {
|
||||
AppState(
|
||||
navController = navController,
|
||||
coroutineScope = coroutineScope,
|
||||
windowSizeClass = windowSizeClass,
|
||||
networkMonitor = networkMonitor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
internal class AppState(
|
||||
val navController: NavHostController,
|
||||
coroutineScope: CoroutineScope,
|
||||
val windowSizeClass: WindowSizeClass,
|
||||
networkMonitor: NetworkMonitor,
|
||||
) {
|
||||
val currentDestination: NavDestination?
|
||||
@Composable get() = navController
|
||||
.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
val shouldShowBottomBar: Boolean
|
||||
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact
|
||||
|
||||
val shouldShowNavRail: Boolean
|
||||
get() = !shouldShowBottomBar
|
||||
|
||||
val isOffline = networkMonitor.isOnline
|
||||
.map(Boolean::not)
|
||||
.stateIn(
|
||||
scope = coroutineScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = false,
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import org.mifos.mobile.core.designsystem.component.MifosPullToRefreshState
|
||||
import org.mifos.mobile.core.designsystem.component.rememberMifosPullToRefreshState
|
||||
import org.mifos.mobile.core.designsystem.theme.AppColors
|
||||
import org.mifos.mobile.core.ui.component.MifosPoweredCard
|
||||
import org.mifos.mobile.core.ui.navigation.MifosBottomBar
|
||||
import org.mifos.mobile.core.ui.navigation.MifosNavigationRail
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MifosScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
topBar: @Composable () -> Unit = { },
|
||||
utilityBar: @Composable () -> Unit = { },
|
||||
overlay: @Composable () -> Unit = { },
|
||||
snackbarHost: @Composable () -> Unit = { },
|
||||
floatingActionButton: @Composable () -> Unit = { },
|
||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||
navigationData: ScaffoldNavigationData? = null,
|
||||
pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(),
|
||||
containerColor: Color = AppColors.customWhite,
|
||||
contentColor: Color = AppColors.customBlack,
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
// .union(WindowInsets.displayCutout)
|
||||
.only(WindowInsetsSides.Horizontal),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val windowSize = calculateWindowSizeClass()
|
||||
val isCompact = windowSize.widthSizeClass == WindowWidthSizeClass.Compact
|
||||
val hasNavigationItems = navigationData?.shouldShowNavigation == true
|
||||
val isNavigationRailVisible = !isCompact && hasNavigationItems
|
||||
val isNavigationBarVisible = isCompact && hasNavigationItems
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = topBar,
|
||||
bottomBar = {
|
||||
AnimatedVisibility(
|
||||
visible = isNavigationBarVisible,
|
||||
enter = fadeIn() + slideInVertically(initialOffsetY = { it / 2 }),
|
||||
exit = fadeOut() + slideOutVertically(targetOffsetY = { it / 2 }),
|
||||
) {
|
||||
ScaffoldBottomAppBar(navigationData = requireNotNull(navigationData))
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
Box(modifier = Modifier.imePadding()) {
|
||||
snackbarHost()
|
||||
}
|
||||
},
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
content = { paddingValues ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.consumeWindowInsets(paddingValues = paddingValues)
|
||||
.imePadding(),
|
||||
) {
|
||||
if (isNavigationRailVisible) {
|
||||
ScaffoldNavigationRail(navigationData = navigationData)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.run {
|
||||
if (isNavigationBarVisible) {
|
||||
consumeWindowInsets(
|
||||
insets = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
utilityBar()
|
||||
val internalPullToRefreshState = rememberPullToRefreshState()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(insets = contentWindowInsets)
|
||||
.pullToRefresh(
|
||||
state = internalPullToRefreshState,
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
onRefresh = pullToRefreshState.onRefresh,
|
||||
enabled = pullToRefreshState.isEnabled,
|
||||
),
|
||||
) {
|
||||
content()
|
||||
|
||||
PullToRefreshDefaults.Indicator(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
state = internalPullToRefreshState,
|
||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
overlay()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScaffoldBottomAppBar(
|
||||
navigationData: ScaffoldNavigationData,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
MifosPoweredCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
MifosBottomBar(
|
||||
navigationItems = navigationData.navigationItems,
|
||||
selectedItem = navigationData.selectedNavigationItem,
|
||||
onClick = navigationData.onNavigationClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(tag = "NavigationBarContainer"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScaffoldNavigationRail(
|
||||
navigationData: ScaffoldNavigationData,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// We set the z-index to 1f in order to make sure the content transitions
|
||||
// animate in under the navigation rail.
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.zIndex(zIndex = 1f),
|
||||
) {
|
||||
MifosNavigationRail(
|
||||
navigationItems = navigationData.navigationItems,
|
||||
selectedItem = navigationData.selectedNavigationItem,
|
||||
onClick = navigationData.onNavigationClick,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.testTag(tag = "NavigationBarContainer"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
@Composable
|
||||
fun rememberMifosNavController(
|
||||
name: String,
|
||||
vararg navigators: Navigator<out NavDestination>,
|
||||
): NavHostController =
|
||||
rememberNavController(navigators = navigators).apply {
|
||||
this.addOnDestinationChangedListener { _, destination, _ ->
|
||||
val graph = destination.parent?.route?.let { " in $it" }.orEmpty()
|
||||
Logger.d("$name destination changed: ${destination.route}$graph")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import org.mifos.mobile.core.ui.navigation.NavigationItem
|
||||
|
||||
data class ScaffoldNavigationData(
|
||||
val onNavigationClick: (NavigationItem) -> Unit,
|
||||
val navigationItems: ImmutableList<NavigationItem>,
|
||||
val selectedNavigationItem: NavigationItem?,
|
||||
val shouldShowNavigation: Boolean,
|
||||
)
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.utils
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Gets the route string for an object.
|
||||
*/
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
fun <T : Any> T.toObjectNavigationRoute(): String = this::class.toObjectKClassNavigationRoute()
|
||||
|
||||
/**
|
||||
* Gets the route string for a [KClass] of an object.
|
||||
*/
|
||||
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
fun <T : Any> KClass<T>.toObjectKClassNavigationRoute(): String =
|
||||
this.serializer().descriptor.serialName
|
||||
@ -15,7 +15,15 @@ import cmp.navigation.ComposeApp
|
||||
|
||||
@Composable
|
||||
fun SharedApp(
|
||||
handleThemeMode: (osValue: Int) -> Unit,
|
||||
handleAppLocale: (locale: String?) -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ComposeApp(modifier = modifier)
|
||||
ComposeApp(
|
||||
handleThemeMode = handleThemeMode,
|
||||
handleAppLocale = handleAppLocale,
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@ -20,7 +20,11 @@ fun main() {
|
||||
|
||||
onWasmReady {
|
||||
ComposeViewport(document.body!!) {
|
||||
SharedApp() // Render the root composable of the application.
|
||||
SharedApp(
|
||||
handleThemeMode = {},
|
||||
handleAppLocale = {},
|
||||
onSplashScreenRemoved = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,10 @@ fun main() {
|
||||
* Invokes the root composable of the application.
|
||||
* This function is responsible for setting up the entire UI structure of the app.
|
||||
*/
|
||||
SharedApp()
|
||||
SharedApp(
|
||||
handleThemeMode = {},
|
||||
handleAppLocale = {},
|
||||
onSplashScreenRemoved = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,13 +48,14 @@ import org.mifos.mobile.core.data.repositoryImpl.UserDetailRepositoryImp
|
||||
import org.mifos.mobile.core.data.util.NetworkMonitor
|
||||
|
||||
private val ioDispatcher = named(MifosDispatchers.IO.name)
|
||||
private val unconfinedDispatcher = named(MifosDispatchers.Unconfined.name)
|
||||
|
||||
val RepositoryModule = module {
|
||||
|
||||
single<Json> { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
single<AccountsRepository> { AccountsRepositoryImp(get(), get(ioDispatcher)) }
|
||||
single<UserDataRepository> { AuthenticationUserRepository(get(), get(ioDispatcher)) }
|
||||
single<UserDataRepository> { AuthenticationUserRepository(get(), get(ioDispatcher), get(unconfinedDispatcher)) }
|
||||
single<BeneficiaryRepository> { BeneficiaryRepositoryImp(get(), get(ioDispatcher)) }
|
||||
single<ClientChargeRepository> { ClientChargeRepositoryImp(get(), get(ioDispatcher)) } // TODO
|
||||
single<ClientRepository> { ClientRepositoryImp(get(), get(ioDispatcher)) }
|
||||
|
||||
@ -10,14 +10,25 @@
|
||||
package org.mifos.mobile.core.data.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.datastore.model.AppSettings
|
||||
import org.mifos.mobile.core.model.AuthState
|
||||
import org.mifos.mobile.core.model.UserData
|
||||
|
||||
interface UserDataRepository {
|
||||
|
||||
val activeUserId: Long?
|
||||
|
||||
// TODO
|
||||
/**
|
||||
* Stream of [UserData]
|
||||
*/
|
||||
val userData: Flow<DataState<UserData>>
|
||||
|
||||
val authState: StateFlow<AuthState>
|
||||
|
||||
val settingsState: StateFlow<AppSettings>
|
||||
|
||||
suspend fun logOut(): DataState<String>
|
||||
}
|
||||
|
||||
@ -10,21 +10,37 @@
|
||||
package org.mifos.mobile.core.data.repositoryImpl
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.zip
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.common.Dispatcher
|
||||
import org.mifos.mobile.core.common.MifosDispatchers
|
||||
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.model.UserData
|
||||
|
||||
class AuthenticationUserRepository(
|
||||
private val preferencesHelper: UserPreferencesRepository,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
@Dispatcher(MifosDispatchers.Unconfined)
|
||||
private val unconfinedDispatcher: CoroutineDispatcher,
|
||||
) : UserDataRepository {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(unconfinedDispatcher)
|
||||
|
||||
override val activeUserId: Long?
|
||||
get() = preferencesHelper.userInfo.value.userId
|
||||
|
||||
override val userData: Flow<DataState<UserData>> = flow {
|
||||
try {
|
||||
val userData = UserData(
|
||||
@ -48,4 +64,24 @@ class AuthenticationUserRepository(
|
||||
DataState.Error(e, null)
|
||||
}
|
||||
}
|
||||
|
||||
override val authState: StateFlow<AuthState>
|
||||
get() = preferencesHelper.userInfo.zip(preferencesHelper.settingsInfo) { account, settings ->
|
||||
when {
|
||||
account.isAuthenticated && settings.isAuthenticated &&
|
||||
account.base64EncodedAuthenticationKey != null ->
|
||||
account.base64EncodedAuthenticationKey
|
||||
|
||||
else -> null
|
||||
}
|
||||
}.map {
|
||||
if (it != null) AuthState.Authenticated(it) else AuthState.Unauthenticated
|
||||
}.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = kotlinx.coroutines.flow.SharingStarted.Eagerly,
|
||||
initialValue = AuthState.Unauthenticated,
|
||||
)
|
||||
|
||||
override val settingsState: StateFlow<AppSettings>
|
||||
get() = preferencesHelper.settingsInfo
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import okio.ByteString.Companion.encodeUtf8
|
||||
import org.mifos.mobile.core.datastore.model.AppSettings
|
||||
import org.mifos.mobile.core.datastore.model.AppTheme
|
||||
import org.mifos.mobile.core.datastore.model.UserData
|
||||
import org.mifos.mobile.core.model.DarkThemeConfig
|
||||
import org.mifos.mobile.core.model.LanguageConfig
|
||||
|
||||
private const val USER_DATA = "userData"
|
||||
@ -75,6 +76,12 @@ class UserPreferencesDataSource(
|
||||
val observeLanguage: Flow<LanguageConfig>
|
||||
get() = _settingsInfo.map { it.language }
|
||||
|
||||
val observeDynamicColorPreference: Flow<Boolean>
|
||||
get() = _settingsInfo.map { it.useDynamicColor }
|
||||
|
||||
val observeDarkThemeConfig: Flow<DarkThemeConfig>
|
||||
get() = _settingsInfo.map { it.darkThemeConfig }
|
||||
|
||||
suspend fun updateSettingsInfo(appSettings: AppSettings) {
|
||||
withContext(dispatcher) {
|
||||
settings.putSettingsPreference(appSettings)
|
||||
@ -193,6 +200,20 @@ class UserPreferencesDataSource(
|
||||
_settingsInfo.value = newPreference
|
||||
}
|
||||
|
||||
suspend fun setIsAuthenticated(isAuthenticated: Boolean) =
|
||||
withContext(dispatcher) {
|
||||
val newPreference = settings.getSettingsPreference().copy(isAuthenticated = isAuthenticated)
|
||||
settings.putSettingsPreference(newPreference)
|
||||
_settingsInfo.value = newPreference
|
||||
}
|
||||
|
||||
suspend fun setIsUnlocked(isUnlocked: Boolean) =
|
||||
withContext(dispatcher) {
|
||||
val newPreference = settings.getSettingsPreference().copy(isUnlocked = isUnlocked)
|
||||
settings.putSettingsPreference(newPreference)
|
||||
_settingsInfo.value = newPreference
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PROFILE_IMAGE = "preferences_profile_image"
|
||||
}
|
||||
|
||||
@ -15,12 +15,13 @@ import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.datastore.model.AppSettings
|
||||
import org.mifos.mobile.core.datastore.model.AppTheme
|
||||
import org.mifos.mobile.core.datastore.model.UserData
|
||||
import org.mifos.mobile.core.model.DarkThemeConfig
|
||||
import org.mifos.mobile.core.model.LanguageConfig
|
||||
|
||||
interface UserPreferencesRepository {
|
||||
val userInfo: Flow<UserData>
|
||||
val userInfo: StateFlow<UserData>
|
||||
|
||||
val settingsInfo: Flow<AppSettings>
|
||||
val settingsInfo: StateFlow<AppSettings>
|
||||
|
||||
val token: StateFlow<String?>
|
||||
|
||||
@ -36,6 +37,10 @@ interface UserPreferencesRepository {
|
||||
|
||||
val observeLanguage: Flow<LanguageConfig>
|
||||
|
||||
val observeDarkThemeConfig: Flow<DarkThemeConfig>
|
||||
|
||||
val observeDynamicColorPreference: Flow<Boolean>
|
||||
|
||||
suspend fun updateToken(password: String): DataState<Unit>
|
||||
|
||||
suspend fun updateTheme(theme: AppTheme): DataState<Unit>
|
||||
@ -52,6 +57,10 @@ interface UserPreferencesRepository {
|
||||
|
||||
suspend fun saveGcmToken(token: String?): DataState<Unit>
|
||||
|
||||
suspend fun setIsAuthenticated(isAuthenticated: Boolean)
|
||||
|
||||
suspend fun setIsUnlocked(isUnlocked: Boolean)
|
||||
|
||||
suspend fun setShowOnboarding(showOnboarding: Boolean)
|
||||
|
||||
suspend fun setFirstTimeState(firstTimeState: Boolean)
|
||||
|
||||
@ -20,6 +20,7 @@ import org.mifos.mobile.core.common.DataState
|
||||
import org.mifos.mobile.core.datastore.model.AppSettings
|
||||
import org.mifos.mobile.core.datastore.model.AppTheme
|
||||
import org.mifos.mobile.core.datastore.model.UserData
|
||||
import org.mifos.mobile.core.model.DarkThemeConfig
|
||||
import org.mifos.mobile.core.model.LanguageConfig
|
||||
|
||||
class UserPreferencesRepositoryImpl(
|
||||
@ -29,10 +30,10 @@ class UserPreferencesRepositoryImpl(
|
||||
) : UserPreferencesRepository {
|
||||
private val unconfinedScope = CoroutineScope(unconfinedDispatcher)
|
||||
|
||||
override val userInfo: Flow<UserData>
|
||||
override val userInfo: StateFlow<UserData>
|
||||
get() = preferenceManager.userInfo
|
||||
|
||||
override val settingsInfo: Flow<AppSettings>
|
||||
override val settingsInfo: StateFlow<AppSettings>
|
||||
get() = preferenceManager.settingsInfo
|
||||
|
||||
override val appTheme: StateFlow<AppTheme>
|
||||
@ -69,6 +70,12 @@ class UserPreferencesRepositoryImpl(
|
||||
override val observeLanguage: Flow<LanguageConfig>
|
||||
get() = preferenceManager.observeLanguage
|
||||
|
||||
override val observeDarkThemeConfig: Flow<DarkThemeConfig>
|
||||
get() = preferenceManager.observeDarkThemeConfig
|
||||
|
||||
override val observeDynamicColorPreference: Flow<Boolean>
|
||||
get() = preferenceManager.observeDynamicColorPreference
|
||||
|
||||
override suspend fun updateToken(password: String): DataState<Unit> {
|
||||
return try {
|
||||
val result = preferenceManager.updateToken(password)
|
||||
@ -153,6 +160,14 @@ class UserPreferencesRepositoryImpl(
|
||||
preferenceManager.setLanguage(language)
|
||||
}
|
||||
|
||||
override suspend fun setIsAuthenticated(isAuthenticated: Boolean) {
|
||||
preferenceManager.setIsAuthenticated(isAuthenticated)
|
||||
}
|
||||
|
||||
override suspend fun setIsUnlocked(isUnlocked: Boolean) {
|
||||
preferenceManager.setIsUnlocked(isUnlocked)
|
||||
}
|
||||
|
||||
override suspend fun logOut() {
|
||||
preferenceManager.clearInfo()
|
||||
}
|
||||
|
||||
@ -10,23 +10,29 @@
|
||||
package org.mifos.mobile.core.datastore.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.mifos.mobile.core.model.DarkThemeConfig
|
||||
import org.mifos.mobile.core.model.LanguageConfig
|
||||
|
||||
@Serializable
|
||||
data class AppSettings(
|
||||
val userId: String,
|
||||
val tenant: String,
|
||||
val baseUrl: String,
|
||||
val passcode: String? = null,
|
||||
val appTheme: AppTheme = AppTheme.SYSTEM,
|
||||
val passcode: String,
|
||||
val appTheme: AppTheme,
|
||||
val sentTokenToServer: Boolean = false,
|
||||
val gcmToken: String? = null,
|
||||
|
||||
val useDynamicColor: Boolean,
|
||||
val darkThemeConfig: DarkThemeConfig,
|
||||
val isAuthenticated: Boolean,
|
||||
val isUnlocked: Boolean,
|
||||
val language: LanguageConfig,
|
||||
val showOnboarding: Boolean,
|
||||
val firstTimeState: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT = AppSettings(
|
||||
userId = "",
|
||||
tenant = "default",
|
||||
baseUrl = "https://tt.mifos.community/",
|
||||
appTheme = AppTheme.SYSTEM,
|
||||
@ -35,6 +41,11 @@ data class AppSettings(
|
||||
language = LanguageConfig.DEFAULT,
|
||||
showOnboarding = true,
|
||||
firstTimeState = true,
|
||||
useDynamicColor = false,
|
||||
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
|
||||
isAuthenticated = false,
|
||||
passcode = "",
|
||||
isUnlocked = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ data class UserData(
|
||||
val userId: Long,
|
||||
val userName: String,
|
||||
val clientId: Long,
|
||||
val isAuthenticated: Boolean,
|
||||
val base64EncodedAuthenticationKey: String,
|
||||
val isAuthenticated: Boolean = false,
|
||||
val base64EncodedAuthenticationKey: String? = null,
|
||||
val officeName: String,
|
||||
) {
|
||||
companion object {
|
||||
|
||||
@ -10,16 +10,21 @@
|
||||
package org.mifos.mobile.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
@ -33,25 +38,34 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.mifos.mobile.core.designsystem.theme.AppColors
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MifosScaffold(
|
||||
backPress: () -> Unit,
|
||||
onNavigationIconClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
topBarTitle: String? = null,
|
||||
containerColor: Color = Color.White,
|
||||
floatingActionButtonContent: FloatingActionButtonContent? = null,
|
||||
pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(),
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
.only(WindowInsetsSides.Horizontal),
|
||||
snackbarHost: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (topBarTitle != null) {
|
||||
MifosTopAppBar(
|
||||
MifosTopBar(
|
||||
topBarTitle = topBarTitle,
|
||||
backPress = backPress,
|
||||
onNavigationIconClick = onNavigationIconClick,
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
@ -60,38 +74,205 @@ fun MifosScaffold(
|
||||
floatingActionButtonContent?.let { content ->
|
||||
FloatingActionButton(
|
||||
onClick = content.onClick,
|
||||
// contentColor = content.contentColor,
|
||||
contentColor = content.contentColor,
|
||||
content = content.content,
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = snackbarHost,
|
||||
// containerColor = MaterialTheme.colorScheme.background,
|
||||
containerColor = containerColor,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
content = { paddingValues ->
|
||||
val internalPullToRefreshState = rememberPullToRefreshState()
|
||||
Box(
|
||||
modifier = Modifier.pullToRefresh(
|
||||
state = internalPullToRefreshState,
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
onRefresh = pullToRefreshState.onRefresh,
|
||||
enabled = pullToRefreshState.isEnabled,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.consumeWindowInsets(paddingValues = paddingValues)
|
||||
.imePadding()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
content(paddingValues)
|
||||
|
||||
PullToRefreshDefaults.Indicator(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.align(Alignment.TopCenter),
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
state = internalPullToRefreshState,
|
||||
.windowInsetsPadding(insets = contentWindowInsets)
|
||||
.pullToRefresh(
|
||||
state = internalPullToRefreshState,
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
onRefresh = pullToRefreshState.onRefresh,
|
||||
enabled = pullToRefreshState.isEnabled,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = AppColors.borderColor,
|
||||
)
|
||||
content()
|
||||
}
|
||||
|
||||
PullToRefreshDefaults.Indicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter),
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
state = internalPullToRefreshState,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MifosScaffold(
|
||||
showNavigationIcon: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onNavigationIconClick: () -> Unit = {},
|
||||
topBarTitle: String? = null,
|
||||
containerColor: Color = Color.White,
|
||||
floatingActionButtonContent: FloatingActionButtonContent? = null,
|
||||
pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(),
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
.only(WindowInsetsSides.Horizontal),
|
||||
snackbarHost: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (topBarTitle != null) {
|
||||
MifosTopBar(
|
||||
topBarTitle = topBarTitle,
|
||||
showNavigationIcon = showNavigationIcon,
|
||||
onNavigationIconClick = onNavigationIconClick,
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
floatingActionButton = {
|
||||
floatingActionButtonContent?.let { content ->
|
||||
FloatingActionButton(
|
||||
onClick = content.onClick,
|
||||
contentColor = content.contentColor,
|
||||
content = content.content,
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = snackbarHost,
|
||||
containerColor = containerColor,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
content = { paddingValues ->
|
||||
val internalPullToRefreshState = rememberPullToRefreshState()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.consumeWindowInsets(paddingValues = paddingValues)
|
||||
.imePadding()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(insets = contentWindowInsets)
|
||||
.pullToRefresh(
|
||||
state = internalPullToRefreshState,
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
onRefresh = pullToRefreshState.onRefresh,
|
||||
enabled = pullToRefreshState.isEnabled,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = AppColors.borderColor,
|
||||
)
|
||||
content()
|
||||
}
|
||||
|
||||
PullToRefreshDefaults.Indicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter),
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
state = internalPullToRefreshState,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MifosElevatedScaffold(
|
||||
onNavigateBack: () -> Unit,
|
||||
topBarTitle: String,
|
||||
modifier: Modifier = Modifier,
|
||||
brandIcon: DrawableResource? = null,
|
||||
bottomBar: @Composable () -> Unit = {},
|
||||
floatingActionButtonContent: FloatingActionButtonContent? = null,
|
||||
pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(),
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
.only(WindowInsetsSides.Horizontal),
|
||||
snackbarHost: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MifosRoundedTopAppBar(
|
||||
brandIcon = brandIcon,
|
||||
title = topBarTitle,
|
||||
onNavigateBack = onNavigateBack,
|
||||
actions = actions,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
floatingActionButtonContent?.let { content ->
|
||||
FloatingActionButton(
|
||||
onClick = content.onClick,
|
||||
contentColor = content.contentColor,
|
||||
content = content.content,
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = snackbarHost,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
content = { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.consumeWindowInsets(paddingValues = paddingValues)
|
||||
.imePadding()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
val internalPullToRefreshState = rememberPullToRefreshState()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(insets = contentWindowInsets)
|
||||
.pullToRefresh(
|
||||
state = internalPullToRefreshState,
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
onRefresh = pullToRefreshState.onRefresh,
|
||||
enabled = pullToRefreshState.isEnabled,
|
||||
),
|
||||
) {
|
||||
content()
|
||||
|
||||
PullToRefreshDefaults.Indicator(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
state = internalPullToRefreshState,
|
||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@ -105,16 +286,15 @@ fun MifosScaffold(
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(),
|
||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||
containerColor: Color = MaterialTheme.colorScheme.background,
|
||||
containerColor: Color = Color.White,
|
||||
contentColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
.only(WindowInsetsSides.Horizontal),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
modifier = modifier,
|
||||
topBar = topBar,
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
@ -128,24 +308,34 @@ fun MifosScaffold(
|
||||
contentColor = contentColor,
|
||||
contentWindowInsets = contentWindowInsets,
|
||||
content = { paddingValues ->
|
||||
val internalPullToRefreshState = rememberPullToRefreshState()
|
||||
Box(
|
||||
modifier = Modifier.pullToRefresh(
|
||||
state = internalPullToRefreshState,
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
onRefresh = pullToRefreshState.onRefresh,
|
||||
enabled = pullToRefreshState.isEnabled,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.consumeWindowInsets(paddingValues = paddingValues)
|
||||
.imePadding()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
content(paddingValues)
|
||||
|
||||
PullToRefreshDefaults.Indicator(
|
||||
val internalPullToRefreshState = rememberPullToRefreshState()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.align(Alignment.TopCenter),
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
state = internalPullToRefreshState,
|
||||
)
|
||||
.windowInsetsPadding(insets = contentWindowInsets)
|
||||
.pullToRefresh(
|
||||
state = internalPullToRefreshState,
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
onRefresh = pullToRefreshState.onRefresh,
|
||||
enabled = pullToRefreshState.isEnabled,
|
||||
),
|
||||
) {
|
||||
content()
|
||||
|
||||
PullToRefreshDefaults.Indicator(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
isRefreshing = pullToRefreshState.isRefreshing,
|
||||
state = internalPullToRefreshState,
|
||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -175,3 +365,14 @@ fun rememberMifosPullToRefreshState(
|
||||
onRefresh = onRefresh,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MifosElevated_Preview() {
|
||||
MifosMobileTheme {
|
||||
MifosElevatedScaffold(
|
||||
onNavigateBack = { },
|
||||
topBarTitle = "Mifos Mobile",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,13 @@
|
||||
*/
|
||||
package org.mifos.mobile.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -19,20 +25,24 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.designsystem.theme.AppColors
|
||||
import org.mifos.mobile.core.designsystem.theme.DesignToken
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosTypography
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MifosTopAppBar(
|
||||
fun MifosTopBar(
|
||||
topBarTitle: String,
|
||||
backPress: () -> Unit,
|
||||
onNavigationIconClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector = MifosIcons.ArrowBack,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
@ -40,85 +50,133 @@ fun MifosTopAppBar(
|
||||
Text(
|
||||
text = topBarTitle,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = backPress,
|
||||
onClick = onNavigationIconClick,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = MifosIcons.Chevron,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
containerColor = AppColors.customWhite,
|
||||
),
|
||||
actions = actions,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun MifosTopAppBar(
|
||||
navigateBack: () -> Unit,
|
||||
title: @Composable () -> Unit,
|
||||
@Composable
|
||||
fun MifosTopBar(
|
||||
topBarTitle: String,
|
||||
showNavigationIcon: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector = MifosIcons.ArrowBack,
|
||||
onNavigationIconClick: () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = navigateBack,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = "Back Arrow",
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MifosTopAppBar(
|
||||
topBarTitleResId: StringResource,
|
||||
navigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(topBarTitleResId),
|
||||
text = topBarTitle,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = navigateBack,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MifosIcons.ArrowBack,
|
||||
contentDescription = "Back Arrow",
|
||||
)
|
||||
if (showNavigationIcon) {
|
||||
IconButton(
|
||||
onClick = onNavigationIconClick,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MifosIcons.Chevron,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = AppColors.customWhite,
|
||||
),
|
||||
actions = actions,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MifosRoundedTopAppBar(
|
||||
title: String,
|
||||
onNavigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
brandIcon: DrawableResource? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MifosTypography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
},
|
||||
actions = actions,
|
||||
navigationIcon = {
|
||||
if (brandIcon != null) {
|
||||
Box(
|
||||
modifier = Modifier.padding(DesignToken.padding.medium),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(brandIcon),
|
||||
contentDescription = "Brand Icon",
|
||||
modifier = Modifier
|
||||
.size(344.dp, 40.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(
|
||||
onClick = onNavigateBack,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MifosIcons.Chevron,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = AppColors.customWhite,
|
||||
),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(
|
||||
elevation = DesignToken.elevation.elevation,
|
||||
shape = DesignToken.shapes.topBar,
|
||||
spotColor = Color(0xFF5D5D5D),
|
||||
ambientColor = AppColors.customBlack,
|
||||
)
|
||||
.clip(DesignToken.shapes.topBar)
|
||||
.background(AppColors.customWhite),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewMbsRoundedTopAppBar() {
|
||||
MifosRoundedTopAppBar(
|
||||
title = "TopAppBar",
|
||||
onNavigateBack = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MbsTopBarPreview() {
|
||||
MifosTopBar(
|
||||
topBarTitle = "Title",
|
||||
onNavigationIconClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@ -57,9 +57,13 @@ import fluent.ui.system.icons.FluentIcons
|
||||
import fluent.ui.system.icons.filled.Document
|
||||
import fluent.ui.system.icons.filled.ErrorCircle
|
||||
import fluent.ui.system.icons.filled.Eye
|
||||
import fluent.ui.system.icons.filled.Grid
|
||||
import fluent.ui.system.icons.filled.MoneyHand
|
||||
import fluent.ui.system.icons.filled.Person
|
||||
import fluent.ui.system.icons.regular.Calendar
|
||||
import fluent.ui.system.icons.regular.CardUi
|
||||
import fluent.ui.system.icons.regular.CheckmarkCircle
|
||||
import fluent.ui.system.icons.regular.ChevronLeft
|
||||
import fluent.ui.system.icons.regular.Eye
|
||||
import fluent.ui.system.icons.regular.EyeOff
|
||||
import fluent.ui.system.icons.regular.Image
|
||||
@ -123,4 +127,10 @@ object MifosIcons {
|
||||
|
||||
val DocumentFilled = FluentIcons.Filled.Document
|
||||
val Calendar = FluentIcons.Regular.Calendar
|
||||
|
||||
val HomeTabFilled = FluentIcons.Filled.Grid
|
||||
val TransferTabFilled = FluentIcons.Filled.MoneyHand
|
||||
val PersonTabFilled = FluentIcons.Filled.Person
|
||||
|
||||
val Chevron = FluentIcons.Regular.ChevronLeft
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.model
|
||||
|
||||
/**
|
||||
* Models high level auth state for the application.
|
||||
*/
|
||||
sealed class AuthState {
|
||||
|
||||
/**
|
||||
* Auth state is unknown.
|
||||
*/
|
||||
data object Uninitialized : AuthState()
|
||||
|
||||
/**
|
||||
* User is unauthenticated. Said another way, the app has no access token.
|
||||
*/
|
||||
data object Unauthenticated : AuthState()
|
||||
|
||||
/**
|
||||
* User is authenticated with the given access token.
|
||||
*/
|
||||
data class Authenticated(val accessToken: String) : AuthState()
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.model
|
||||
|
||||
enum class DarkThemeConfig(val configName: String, val osValue: Int) {
|
||||
FOLLOW_SYSTEM("Follow System", -1),
|
||||
LIGHT("Light", 1),
|
||||
DARK("Dark", 2),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): DarkThemeConfig {
|
||||
return entries.find { it.configName.equals(value, ignoreCase = true) } ?: FOLLOW_SYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,6 +47,7 @@ fun MifosPoweredCard(
|
||||
modifier = modifier
|
||||
.shadow(
|
||||
elevation = DesignToken.elevation.elevation,
|
||||
shape = DesignToken.shapes.bottomSheet,
|
||||
ambientColor = AppColors.customBlack,
|
||||
spotColor = AppColors.customBlack,
|
||||
clip = false,
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.BottomAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.mifos.mobile.core.designsystem.theme.AppColors
|
||||
|
||||
@Composable
|
||||
fun MifosBottomBar(
|
||||
navigationItems: List<NavigationItem>,
|
||||
selectedItem: NavigationItem?,
|
||||
onClick: (NavigationItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
|
||||
) {
|
||||
BottomAppBar(
|
||||
containerColor = AppColors.customWhite,
|
||||
contentColor = Color.Unspecified,
|
||||
windowInsets = windowInsets,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
// .shadow(
|
||||
// elevation = DesignToken.elevation.elevation,
|
||||
// spotColor = Color(0xFF5D5D5D),
|
||||
// ambientColor = Color.Black,
|
||||
// )
|
||||
.background(AppColors.customWhite),
|
||||
tonalElevation = 0.dp,
|
||||
) {
|
||||
navigationItems.forEach { navigationItem ->
|
||||
MifosNavigationBarItem(
|
||||
contentDescriptionRes = navigationItem.contentDescriptionRes,
|
||||
selectedIconRes = navigationItem.iconResSelected,
|
||||
unselectedIconRes = navigationItem.iconRes,
|
||||
label = navigationItem.labelRes,
|
||||
isSelected = selectedItem == navigationItem,
|
||||
onClick = { onClick(navigationItem) },
|
||||
modifier = Modifier.testTag(tag = navigationItem.testTag),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.mifos.mobile.core.designsystem.theme.DesignToken
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosTypography
|
||||
|
||||
@Composable
|
||||
fun RowScope.MifosNavigationBarItem(
|
||||
contentDescriptionRes: StringResource,
|
||||
selectedIconRes: ImageVector,
|
||||
label: StringResource,
|
||||
unselectedIconRes: ImageVector,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isSelected) selectedIconRes else unselectedIconRes,
|
||||
contentDescription = stringResource(contentDescriptionRes),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
modifier = Modifier.padding(DesignToken.padding.extraSmall),
|
||||
text = stringResource(label),
|
||||
style = MifosTypography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
},
|
||||
selected = isSelected,
|
||||
alwaysShowLabel = true,
|
||||
onClick = onClick,
|
||||
modifier = modifier.padding(vertical = 6.dp),
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
indicatorColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.NavigationRailDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.mifos.mobile.core.designsystem.theme.DesignToken
|
||||
|
||||
@Composable
|
||||
fun MifosNavigationRail(
|
||||
navigationItems: List<NavigationItem>,
|
||||
selectedItem: NavigationItem?,
|
||||
onClick: (NavigationItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
windowInsets: WindowInsets = NavigationRailDefaults.windowInsets,
|
||||
) {
|
||||
Surface(
|
||||
color = Color.White,
|
||||
contentColor = Color.Unspecified,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.windowInsetsPadding(insets = windowInsets)
|
||||
.widthIn(min = 80.dp)
|
||||
.padding(vertical = DesignToken.padding.extraSmall)
|
||||
.selectableGroup()
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
space = DesignToken.spacing.large,
|
||||
alignment = Alignment.CenterVertically,
|
||||
),
|
||||
) {
|
||||
navigationItems.forEach { navigationItem ->
|
||||
MifosNavigationRailItem(
|
||||
contentDescriptionRes = navigationItem.contentDescriptionRes,
|
||||
selectedIconRes = navigationItem.iconResSelected,
|
||||
unselectedIconRes = navigationItem.iconRes,
|
||||
isSelected = navigationItem == selectedItem,
|
||||
label = navigationItem.labelRes,
|
||||
onClick = { onClick(navigationItem) },
|
||||
modifier = Modifier.testTag(tag = navigationItem.testTag),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.NavigationRailItemDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.mifos.mobile.core.designsystem.theme.DesignToken
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosTypography
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.MifosNavigationRailItem(
|
||||
contentDescriptionRes: StringResource,
|
||||
selectedIconRes: ImageVector,
|
||||
label: StringResource,
|
||||
unselectedIconRes: ImageVector,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
NavigationRailItem(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isSelected) selectedIconRes else unselectedIconRes,
|
||||
contentDescription = stringResource(contentDescriptionRes),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
modifier = Modifier.padding(DesignToken.padding.extraSmall),
|
||||
text = stringResource(label),
|
||||
style = MifosTypography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
},
|
||||
selected = isSelected,
|
||||
alwaysShowLabel = true,
|
||||
onClick = onClick,
|
||||
colors = NavigationRailItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.core.ui.navigation
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
/**
|
||||
* Represents a user-interactable item to navigate a user via the bottom app bar or navigation rail.
|
||||
*/
|
||||
interface NavigationItem {
|
||||
/**
|
||||
* The resource ID for the icon representing the tab when it is selected.
|
||||
*/
|
||||
val iconResSelected: ImageVector
|
||||
|
||||
/**
|
||||
* Resource id for the icon representing the tab.
|
||||
*/
|
||||
val iconRes: ImageVector
|
||||
|
||||
/**
|
||||
* Resource id for the label describing the tab.
|
||||
*/
|
||||
val labelRes: StringResource
|
||||
|
||||
/**
|
||||
* Resource id for the content description describing the tab.
|
||||
*/
|
||||
val contentDescriptionRes: StringResource
|
||||
|
||||
/**
|
||||
* Route of the tab's graph.
|
||||
*/
|
||||
val graphRoute: String
|
||||
|
||||
/**
|
||||
* Route of the tab's start destination.
|
||||
*/
|
||||
val startDestinationRoute: String
|
||||
|
||||
/**
|
||||
* The test tag of the tab.
|
||||
*/
|
||||
val testTag: String
|
||||
}
|
||||
@ -51,7 +51,7 @@ internal fun AboutUsScreen(
|
||||
val aboutUsItems = remember { getAboutUsItems() }
|
||||
|
||||
MifosScaffold(
|
||||
backPress = navigateBack,
|
||||
onNavigationIconClick = navigateBack,
|
||||
topBarTitle = stringResource(Res.string.feature_about_about_us),
|
||||
content = {
|
||||
LazyColumn(
|
||||
|
||||
@ -11,7 +11,6 @@ package org.mifos.mobile.feature.about.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -35,14 +34,14 @@ internal fun PrivacyPolicyScreen(
|
||||
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.feature_about_privacy_policy),
|
||||
backPress = navigateBack,
|
||||
onNavigationIconClick = navigateBack,
|
||||
modifier = modifier,
|
||||
) { paddingValues ->
|
||||
) {
|
||||
WebView(
|
||||
url = stringResource(Res.string.feature_about_policy_url),
|
||||
isLoading = isLoading,
|
||||
onLoadingChange = { isLoading = it },
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
*/
|
||||
package org.mifos.mobile.feature.accounts.screen
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -27,16 +26,13 @@ import mifos_mobile.feature.accounts.generated.resources.Res
|
||||
import mifos_mobile.feature.accounts.generated.resources.feature_account_loan_account
|
||||
import mifos_mobile.feature.accounts.generated.resources.feature_account_savings_account
|
||||
import mifos_mobile.feature.accounts.generated.resources.feature_account_share_account
|
||||
import mifos_mobile.feature.accounts.generated.resources.feature_account_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTabPager
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.model.enums.AccountType
|
||||
import org.mifos.mobile.core.ui.component.MifosTitleSearchCard
|
||||
import org.mifos.mobile.core.ui.utils.DevicePreview
|
||||
import org.mifos.mobile.feature.accounts.component.AccountFilterDialog
|
||||
import org.mifos.mobile.feature.accounts.model.CheckboxStatus
|
||||
@ -102,6 +98,7 @@ fun AccountsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
@Composable
|
||||
private fun AccountsScreenContent(
|
||||
currentPage: Int,
|
||||
@ -138,23 +135,23 @@ private fun AccountsScreenContent(
|
||||
}
|
||||
|
||||
MifosScaffold(
|
||||
topBar = {
|
||||
MifosTopAppBar(
|
||||
navigateBack = navigateBack,
|
||||
title = {
|
||||
MifosTitleSearchCard(
|
||||
actions = {
|
||||
IconButton(onClick = openFilterDialog) {
|
||||
Icon(imageVector = MifosIcons.FilterList, contentDescription = "Filter")
|
||||
}
|
||||
},
|
||||
titleResourceId = Res.string.feature_account_title,
|
||||
searchQuery = onSearchQueryChange,
|
||||
onSearchDismiss = closeSearch,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
// topBar = {
|
||||
// MifosTopAppBar(
|
||||
// navigateBack = navigateBack,
|
||||
// title = {
|
||||
// MifosTitleSearchCard(
|
||||
// actions = {
|
||||
// IconButton(onClick = openFilterDialog) {
|
||||
// Icon(imageVector = MifosIcons.FilterList, contentDescription = "Filter")
|
||||
// }
|
||||
// },
|
||||
// titleResourceId = Res.string.feature_account_title,
|
||||
// searchQuery = onSearchQueryChange,
|
||||
// onSearchDismiss = closeSearch,
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
floatingActionButton = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
@ -171,7 +168,7 @@ private fun AccountsScreenContent(
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
) { paddingValues ->
|
||||
) {
|
||||
ClientAccountsTabRow(
|
||||
tabs = tabs,
|
||||
checkboxOptions = checkboxOptions,
|
||||
@ -179,7 +176,7 @@ private fun AccountsScreenContent(
|
||||
searchQuery = searchQuery,
|
||||
onPageChange = onPageChange,
|
||||
onAccountClicked = onAccountClicked,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,8 +103,10 @@
|
||||
<string name="feature_signup_error_name_contains_digits">Name should not contain digits</string>
|
||||
<string name="feature_signup_error_name_contains_symbols">Name should not contain symbols and spaces</string>
|
||||
<string name="feature_signup_error_name_contains_emoji">Text must not contain emoji or invalid characters</string>
|
||||
|
||||
|
||||
<string name="feature_signup_user_registered_successfully">User Successfully Registered</string>
|
||||
<string name="feature_signup_user_registered_successfully_tip">You can now log in with your username (phone number) and password.</string>
|
||||
<string name="feature_signup_user_registered_failed">Failed To Register The User</string>
|
||||
<string name="feature_signup_user_registered_failed_tip">There is an error to register the user. Please try again.</string>
|
||||
|
||||
|
||||
<string name="feature_upload_id_title">Upload ID</string>
|
||||
@ -132,6 +134,7 @@
|
||||
<string name="feature_upload_id_error_dob_required">Date of birth is required</string>
|
||||
<string name="feature_upload_id_upload_success">File uploaded successfully</string>
|
||||
<string name="feature_upload_id_upload_failed">Failed to upload file. Please try again.</string>
|
||||
<string name="feature_upload_id_error_invalid_dob">Date of Birth cannot be today or a future date</string>
|
||||
<string name="feature_upload_id_generic_error">Something went wrong. Please try again later.</string>
|
||||
|
||||
<string name="feature_otp_title">OTP Authentication</string>
|
||||
@ -177,4 +180,10 @@
|
||||
<string name="feature_recover_now_remember_your_password">Remember your password?</string>
|
||||
<string name="feature_recover_now_log_in">Log in!</string>
|
||||
|
||||
|
||||
<string name="feature_recover_now_recovered_successfully">Password Recovery Successful</string>
|
||||
<string name="feature_recover_now_recovered_successfully_tip">You can now log in with your username(phone number) and password.</string>
|
||||
<string name="feature_recover_now_recovered_failed">Password Recovery Failed</string>
|
||||
<string name="feature_recover_now_registered_failed_tip">There is an error to recover the password of the user. Please try again.</string>
|
||||
|
||||
</resources>
|
||||
@ -13,17 +13,21 @@ package org.mifos.mobile.feature.auth.login
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.mifos.mobile.core.ui.composableWithStayTransitions
|
||||
import org.mifos.mobile.feature.auth.navigation.AuthGraphRoute
|
||||
|
||||
@Serializable
|
||||
@SerialName("login")
|
||||
data object LoginRoute
|
||||
|
||||
fun NavController.navigateToLoginScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(route = LoginRoute, navOptions = navOptions)
|
||||
fun NavController.navigateToLoginScreen() {
|
||||
this.navigate(LoginRoute) {
|
||||
popUpTo(AuthGraphRoute) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.loginDestination(
|
||||
|
||||
@ -13,13 +13,13 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@ -28,6 +28,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -122,9 +123,17 @@ private fun LoginScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onAction: (LoginAction) -> Unit,
|
||||
) {
|
||||
MifosScaffold { paddingValues ->
|
||||
MifosScaffold(
|
||||
bottomBar = {
|
||||
Surface {
|
||||
MifosPoweredCard(
|
||||
modifier = Modifier.fillMaxWidth().navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
LoginScreenContent(
|
||||
modifier = modifier.padding(paddingValues),
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onAction = onAction,
|
||||
)
|
||||
@ -160,35 +169,25 @@ private fun LoginScreenContent(
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
Box(
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 75.dp)
|
||||
.padding(top = 100.dp)
|
||||
.padding(DesignToken.padding.large)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
keyboardController?.hide()
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(DesignToken.padding.large)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
LogoBox()
|
||||
Spacer(modifier = Modifier.height(DesignToken.spacing.medium))
|
||||
InputBox(
|
||||
state = state,
|
||||
onAction = onAction,
|
||||
)
|
||||
}
|
||||
MifosPoweredCard(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
LogoBox()
|
||||
Spacer(modifier = Modifier.height(DesignToken.spacing.medium))
|
||||
InputBox(
|
||||
state = state,
|
||||
onAction = onAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,6 +121,8 @@ class LoginViewModel(
|
||||
)
|
||||
viewModelScope.launch {
|
||||
userPreferencesRepositoryImpl.updateUser(userData)
|
||||
userPreferencesRepositoryImpl.setIsAuthenticated(true)
|
||||
userPreferencesRepositoryImpl.setIsUnlocked(true)
|
||||
}
|
||||
sendEvent(LoginEvent.NavigateToPasscode)
|
||||
}
|
||||
|
||||
@ -66,13 +66,18 @@ fun NavGraphBuilder.authenticationNavGraph(
|
||||
)
|
||||
|
||||
otpAuthenticationDestination(
|
||||
navigateBack = navController::popBackStack,
|
||||
navigateToStatusScreen = navController::navigateToStatusScreen,
|
||||
navigateToSetPasswordScreen = navController::navigateToSetPasswordScreen,
|
||||
)
|
||||
|
||||
statusDestination(
|
||||
navigateToDestination = {
|
||||
navController.navigate(it)
|
||||
if (it == "login") {
|
||||
navController.navigateToLoginScreen()
|
||||
} else {
|
||||
navController.navigate(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -34,11 +34,13 @@ fun NavController.navigateToOtpAuthScreen(
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.otpAuthenticationDestination(
|
||||
navigateToStatusScreen: (EventType, String) -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
navigateToStatusScreen: (EventType, String, String, String, String) -> Unit,
|
||||
navigateToSetPasswordScreen: () -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions<OtpAuthenticationRoute> {
|
||||
OtpAuthenticationScreen(
|
||||
navigateBack = navigateBack,
|
||||
navigateToStatusScreen = navigateToStatusScreen,
|
||||
navigateToSetPasswordScreen = navigateToSetPasswordScreen,
|
||||
)
|
||||
|
||||
@ -17,7 +17,9 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@ -66,7 +68,8 @@ import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
|
||||
@Composable
|
||||
internal fun OtpAuthenticationScreen(
|
||||
navigateToStatusScreen: (EventType, String) -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
navigateToStatusScreen: (EventType, String, String, String, String) -> Unit,
|
||||
navigateToSetPasswordScreen: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: OtpAuthenticationViewModel = koinViewModel(),
|
||||
@ -79,6 +82,9 @@ internal fun OtpAuthenticationScreen(
|
||||
navigateToStatusScreen(
|
||||
event.eventType,
|
||||
event.eventDestination,
|
||||
event.title,
|
||||
event.subtitle,
|
||||
event.buttonText,
|
||||
)
|
||||
}
|
||||
|
||||
@ -87,6 +93,8 @@ internal fun OtpAuthenticationScreen(
|
||||
navigateToSetPasswordScreen.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
is OtpAuthEvent.NavigateBack -> navigateBack.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,16 +145,16 @@ internal fun OptAuthScreenContent(
|
||||
bottomBar = {
|
||||
Surface {
|
||||
MifosPoweredCard(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(DesignToken.padding.large)
|
||||
.padding(top = DesignToken.padding.large)
|
||||
.padding(DesignToken.padding.large),
|
||||
.statusBarsPadding(),
|
||||
|
||||
) {
|
||||
Text(
|
||||
|
||||
@ -20,9 +20,13 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.serializer
|
||||
import mifos_mobile.feature.auth.generated.resources.Res
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_common_next
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_otp_invalid_error
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_otp_required_error
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_signup_user_registered_successfully
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_signup_user_registered_successfully_tip
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
import org.mifos.mobile.feature.auth.login.LoginRoute
|
||||
|
||||
@ -47,7 +51,7 @@ internal class OtpAuthenticationViewModel(
|
||||
|
||||
override fun handleAction(action: OtpAuthAction) {
|
||||
when (action) {
|
||||
is OtpAuthAction.OnCancelClick -> { }
|
||||
is OtpAuthAction.OnCancelClick -> sendEvent(OtpAuthEvent.NavigateBack)
|
||||
|
||||
is OtpAuthAction.OnNextClick -> handleNextClick()
|
||||
|
||||
@ -127,8 +131,11 @@ internal class OtpAuthenticationViewModel(
|
||||
dismissDialog()
|
||||
sendEvent(
|
||||
OtpAuthEvent.NavigateToStatus(
|
||||
EventType.SUCCESS,
|
||||
LoginRoute::class.serializer().descriptor.serialName,
|
||||
eventType = EventType.SUCCESS,
|
||||
eventDestination = LoginRoute::class.serializer().descriptor.serialName,
|
||||
title = getString(Res.string.feature_signup_user_registered_successfully),
|
||||
subtitle = getString(Res.string.feature_signup_user_registered_successfully_tip),
|
||||
buttonText = getString(Res.string.feature_common_next),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -188,8 +195,13 @@ internal sealed interface OtpAuthAction {
|
||||
internal sealed interface OtpAuthEvent {
|
||||
data object NavigateNext : OtpAuthEvent
|
||||
|
||||
data object NavigateBack : OtpAuthEvent
|
||||
|
||||
data class NavigateToStatus(
|
||||
val eventType: EventType,
|
||||
val eventDestination: String,
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val buttonText: String,
|
||||
) : OtpAuthEvent
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ fun NavController.navigateToRecoverPasswordScreen(navOptions: NavOptions? = null
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.recoverPasswordDestination(
|
||||
navigateToOtpAuthenticationScreen: () -> Unit,
|
||||
navigateToOtpAuthenticationScreen: (String) -> Unit,
|
||||
navigateToLoginScreen: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<RecoverPasswordRoute> {
|
||||
|
||||
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
@ -61,7 +62,7 @@ import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
|
||||
@Composable
|
||||
internal fun RecoverPasswordScreen(
|
||||
navigateToOtpAuthenticationScreen: () -> Unit,
|
||||
navigateToOtpAuthenticationScreen: (String) -> Unit,
|
||||
navigateToLoginScreen: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: RecoverPasswordViewModel = koinViewModel(),
|
||||
@ -70,9 +71,13 @@ internal fun RecoverPasswordScreen(
|
||||
|
||||
EventsEffect(viewModel.eventFlow) { event ->
|
||||
when (event) {
|
||||
RecoverPasswordEvent.NavigateToLogin -> navigateToLoginScreen.invoke()
|
||||
is RecoverPasswordEvent.NavigateToLogin -> navigateToLoginScreen.invoke()
|
||||
|
||||
RecoverPasswordEvent.NavigateToOtpAuth -> navigateToOtpAuthenticationScreen.invoke()
|
||||
is RecoverPasswordEvent.NavigateToOtpAuth -> {
|
||||
navigateToOtpAuthenticationScreen(
|
||||
event.nextRoute,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +109,8 @@ internal fun RecoverPasswordScreen(
|
||||
Surface {
|
||||
MifosPoweredCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -164,7 +170,7 @@ internal fun ForgotPasswordInputBox(
|
||||
),
|
||||
config = MifosTextFieldConfig(
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
keyboardType = KeyboardType.Phone,
|
||||
),
|
||||
isError = state.phoneNumberError != null,
|
||||
errorText = state.phoneNumberError?.let { stringResource(it) },
|
||||
|
||||
@ -14,6 +14,9 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.serializer
|
||||
import mifos_mobile.feature.auth.generated.resources.Res
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_recover_now_email_format_error
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_recover_now_email_required
|
||||
@ -23,6 +26,7 @@ import mifos_mobile.feature.auth.generated.resources.feature_recover_now_phone_n
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
import org.mifos.mobile.core.ui.utils.ValidationHelper
|
||||
import org.mifos.mobile.feature.auth.setNewPassword.SetPasswordRoute
|
||||
|
||||
const val PHONE_NUMBER_LENGTH = 10
|
||||
|
||||
@ -131,12 +135,17 @@ internal class RecoverPasswordViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
private fun requestRecoveryCode() {
|
||||
viewModelScope.launch {
|
||||
mutableStateFlow.update { it.copy(dialogState = RecoverPasswordState.DialogState.Loading) }
|
||||
delay(3000)
|
||||
dismissDialog()
|
||||
sendEvent(RecoverPasswordEvent.NavigateToOtpAuth)
|
||||
sendEvent(
|
||||
RecoverPasswordEvent.NavigateToOtpAuth(
|
||||
nextRoute = SetPasswordRoute::class.serializer().descriptor.serialName,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,7 +178,9 @@ data class RecoverPasswordState(
|
||||
}
|
||||
|
||||
internal sealed interface RecoverPasswordEvent {
|
||||
data object NavigateToOtpAuth : RecoverPasswordEvent
|
||||
data class NavigateToOtpAuth(
|
||||
val nextRoute: String,
|
||||
) : RecoverPasswordEvent
|
||||
data object NavigateToLogin : RecoverPasswordEvent
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,9 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@ -158,15 +160,15 @@ private fun RegistrationScreen(
|
||||
bottomBar = {
|
||||
Surface {
|
||||
MifosPoweredCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth().navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
) {
|
||||
RegistrationScreenContent(
|
||||
state = state,
|
||||
onAction = onAction,
|
||||
modifier = modifier.padding(paddingValues),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -193,7 +195,9 @@ private fun RegistrationScreenContent(
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
.padding(DesignToken.padding.large),
|
||||
.padding(DesignToken.padding.large)
|
||||
.padding(top = DesignToken.padding.large)
|
||||
.statusBarsPadding(),
|
||||
verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.medium),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = DesignToken.spacing.extraLarge,
|
||||
@ -385,10 +389,16 @@ fun MifosInputField(
|
||||
trailingIcon = trailingIcon,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = if (config.fieldType == InputFieldType.PASSWORD) {
|
||||
KeyboardType.Password
|
||||
} else {
|
||||
KeyboardType.Text
|
||||
keyboardType = when (config.fieldType) {
|
||||
InputFieldType.PASSWORD -> {
|
||||
KeyboardType.Password
|
||||
}
|
||||
InputFieldType.NUMBER -> {
|
||||
KeyboardType.Number
|
||||
}
|
||||
else -> {
|
||||
KeyboardType.Text
|
||||
}
|
||||
},
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
@ -400,6 +410,7 @@ fun MifosInputField(
|
||||
enum class InputFieldType {
|
||||
TEXT,
|
||||
PASSWORD,
|
||||
NUMBER,
|
||||
}
|
||||
|
||||
data class InputFieldConfig(
|
||||
@ -454,6 +465,7 @@ fun getInputConfigs(
|
||||
errorText = state.customerAccountError,
|
||||
labelRes = Res.string.feature_signup_customer_account_label,
|
||||
onValueChange = { onAction(SignUpAction.OnCustomerAccountChange(it)) },
|
||||
fieldType = InputFieldType.NUMBER,
|
||||
),
|
||||
InputFieldConfig(
|
||||
value = state.password,
|
||||
|
||||
@ -28,7 +28,7 @@ fun NavController.navigateToSetPasswordScreen(navOptions: NavOptions? = null) {
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.setPasswordDestination(
|
||||
navigateToStatusScreen: (EventType, String) -> Unit,
|
||||
navigateToStatusScreen: (EventType, String, String, String, String) -> Unit,
|
||||
navigateToLoginScreen: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<SetPasswordRoute> {
|
||||
|
||||
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
@ -62,7 +63,7 @@ import org.mifos.mobile.feature.auth.otpAuthentication.EventType
|
||||
|
||||
@Composable
|
||||
internal fun SetPasswordScreen(
|
||||
navigateToStatusScreen: (EventType, String) -> Unit,
|
||||
navigateToStatusScreen: (EventType, String, String, String, String) -> Unit,
|
||||
navigateToLoginScreen: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SetPasswordViewModel = koinViewModel(),
|
||||
@ -76,6 +77,9 @@ internal fun SetPasswordScreen(
|
||||
is SetPasswordEvent.NavigateToStatus -> navigateToStatusScreen(
|
||||
event.eventType,
|
||||
event.eventDestination,
|
||||
event.title,
|
||||
event.subtitle,
|
||||
event.buttonText,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -128,7 +132,7 @@ internal fun SetPasswordScreen(
|
||||
bottomBar = {
|
||||
Surface {
|
||||
MifosPoweredCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth().navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -252,7 +256,6 @@ internal fun SetPasswordInputBox(
|
||||
Text(
|
||||
text = stringResource(Res.string.feature_set_new_password_submit),
|
||||
style = MifosTypography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -18,9 +18,13 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.serializer
|
||||
import mifos_mobile.feature.auth.generated.resources.Res
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_common_next
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_recover_now_recovered_successfully
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_recover_now_recovered_successfully_tip
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_mismatch
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_required_error
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.mifos.mobile.core.ui.PasswordStrengthState
|
||||
import org.mifos.mobile.core.ui.utils.BaseViewModel
|
||||
import org.mifos.mobile.core.ui.utils.PasswordChecker
|
||||
@ -182,8 +186,11 @@ internal class SetPasswordViewModel : BaseViewModel<SetPasswordState, SetPasswor
|
||||
dismissDialog()
|
||||
sendEvent(
|
||||
SetPasswordEvent.NavigateToStatus(
|
||||
EventType.SUCCESS,
|
||||
LoginRoute::class.serializer().descriptor.serialName,
|
||||
eventType = EventType.SUCCESS,
|
||||
eventDestination = LoginRoute::class.serializer().descriptor.serialName,
|
||||
title = getString(Res.string.feature_recover_now_recovered_successfully),
|
||||
subtitle = getString(Res.string.feature_recover_now_recovered_successfully_tip),
|
||||
buttonText = getString(Res.string.feature_common_next),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -234,7 +241,7 @@ internal data class SetPasswordState(
|
||||
}
|
||||
|
||||
val isSubmitButtonEnabled: Boolean
|
||||
get() = password.isNotBlank() && password.isNotBlank()
|
||||
get() = password.isNotBlank() && confirmPassword.isNotBlank()
|
||||
}
|
||||
|
||||
internal sealed interface SetPasswordEvent {
|
||||
@ -244,6 +251,9 @@ internal sealed interface SetPasswordEvent {
|
||||
data class NavigateToStatus(
|
||||
val eventType: EventType,
|
||||
val eventDestination: String,
|
||||
val buttonText: String,
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
) : SetPasswordEvent
|
||||
}
|
||||
|
||||
|
||||
@ -30,9 +30,9 @@ import org.mifos.mobile.feature.auth.otpAuthentication.EventType
|
||||
internal fun StatusScreen(
|
||||
eventType: EventType,
|
||||
eventDestination: String,
|
||||
buttonText: String,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
buttonText: String,
|
||||
navigateToDestination: (String) -> Unit,
|
||||
) {
|
||||
MifosScaffold(
|
||||
@ -47,7 +47,6 @@ internal fun StatusScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(DesignToken.padding.large),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
|
||||
@ -16,33 +16,38 @@ import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.toRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.mifos.mobile.core.ui.composableWithStayTransitions
|
||||
import org.mifos.mobile.feature.auth.navigation.AuthGraphRoute
|
||||
import org.mifos.mobile.feature.auth.otpAuthentication.EventType
|
||||
|
||||
@Serializable
|
||||
data class StatusNavigationRoute(
|
||||
val eventType: EventType,
|
||||
val eventDestination: String,
|
||||
val buttonText: String = "Continue",
|
||||
val title: String = "Success",
|
||||
val subtitle: String = "You have completed the action.",
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val buttonText: String,
|
||||
)
|
||||
|
||||
fun NavController.navigateToStatusScreen(
|
||||
eventType: EventType,
|
||||
eventDestination: String,
|
||||
buttonText: String = "Continue",
|
||||
title: String = "Success",
|
||||
subtitle: String = "You have completed the action.",
|
||||
title: String,
|
||||
subtitle: String,
|
||||
buttonText: String,
|
||||
) {
|
||||
this.navigate(
|
||||
StatusNavigationRoute(
|
||||
eventType = eventType,
|
||||
eventDestination = eventDestination,
|
||||
buttonText = buttonText,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
buttonText = buttonText,
|
||||
),
|
||||
)
|
||||
) {
|
||||
popUpTo(AuthGraphRoute) {
|
||||
inclusive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.statusDestination(
|
||||
@ -53,9 +58,9 @@ fun NavGraphBuilder.statusDestination(
|
||||
StatusScreen(
|
||||
eventType = route.eventType,
|
||||
eventDestination = route.eventDestination,
|
||||
buttonText = route.buttonText,
|
||||
title = route.title,
|
||||
subtitle = route.subtitle,
|
||||
buttonText = route.buttonText,
|
||||
navigateToDestination = navigateToDestination,
|
||||
)
|
||||
}
|
||||
|
||||
@ -17,8 +17,11 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DatePicker
|
||||
@ -40,6 +43,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import mifos_mobile.feature.auth.generated.resources.Res
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_common_submit
|
||||
@ -138,18 +143,19 @@ internal fun UploadIdScreenContent(
|
||||
Surface {
|
||||
MifosPoweredCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
|
||||
) {
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(paddingValues)
|
||||
.padding(DesignToken.padding.large),
|
||||
.padding(top = DesignToken.padding.large)
|
||||
.padding(DesignToken.padding.large)
|
||||
.statusBarsPadding(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.feature_upload_id_title),
|
||||
@ -187,7 +193,7 @@ internal fun InputForm(
|
||||
initialSelectedDateMillis = activateDate,
|
||||
selectableDates = object : SelectableDates {
|
||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||
return utcTimeMillis >= Clock.System.now().toEpochMilliseconds()
|
||||
return utcTimeMillis < Clock.System.now().toEpochMilliseconds()
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -208,6 +214,7 @@ internal fun InputForm(
|
||||
isError = state.cellPhoneError != null,
|
||||
errorText = state.cellPhoneError?.let { stringResource(it) },
|
||||
onValueChange = { onAction(UploadIdAction.OnMobileChange(it)) },
|
||||
inputFieldType = InputFieldType.PHONE,
|
||||
)
|
||||
|
||||
MifosTextFieldWithError(
|
||||
@ -216,6 +223,7 @@ internal fun InputForm(
|
||||
isError = state.nationalIdError != null,
|
||||
errorText = state.nationalIdError?.let { stringResource(it) },
|
||||
onValueChange = { onAction(UploadIdAction.OnNationalIdChange(it)) },
|
||||
inputFieldType = InputFieldType.PHONE,
|
||||
)
|
||||
|
||||
MifosTextFieldWithError(
|
||||
@ -307,6 +315,11 @@ internal fun InputForm(
|
||||
}
|
||||
}
|
||||
|
||||
enum class InputFieldType {
|
||||
TEXT,
|
||||
PHONE,
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MifosTextFieldWithError(
|
||||
value: String,
|
||||
@ -316,6 +329,7 @@ private fun MifosTextFieldWithError(
|
||||
errorText: String? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
showClearIcon: Boolean = true,
|
||||
inputFieldType: InputFieldType = InputFieldType.TEXT,
|
||||
) {
|
||||
MifosOutlinedTextField(
|
||||
value = value,
|
||||
@ -333,6 +347,14 @@ private fun MifosTextFieldWithError(
|
||||
errorText = errorText,
|
||||
trailingIcon = trailingIcon,
|
||||
showClearIcon = showClearIcon,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = if (inputFieldType == InputFieldType.PHONE) {
|
||||
KeyboardType.Phone
|
||||
} else {
|
||||
KeyboardType.Text
|
||||
},
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,6 +21,9 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import mifos_mobile.feature.auth.generated.resources.Res
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_recover_now_invalid_phone_number_error
|
||||
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 mifos_mobile.feature.auth.generated.resources.feature_upload_id_error_dob_required
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_upload_id_error_field_empty
|
||||
import mifos_mobile.feature.auth.generated.resources.feature_upload_id_error_id_required
|
||||
@ -30,6 +33,8 @@ import mifos_mobile.feature.auth.generated.resources.feature_upload_id_upload_fa
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
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 :
|
||||
BaseViewModel<UploadIdUiState, UploadIdEvent, UploadIdAction>(
|
||||
@ -142,9 +147,18 @@ internal class UploadIdViewModel :
|
||||
|
||||
private fun validateCellPhone(cellPhone: String): StringResource? {
|
||||
return when {
|
||||
cellPhone.isBlank() -> Res.string.feature_upload_id_error_field_empty
|
||||
// !ValidationHelper.isValidPhoneNumber(cellPhone) -> Res.string.feature_upload_id_error_mobile_not_valid
|
||||
cellPhone.length > 10 -> Res.string.feature_upload_id_error_mobile_not_valid
|
||||
cellPhone.isEmpty() -> Res.string.feature_recover_now_phone_number_required
|
||||
|
||||
cellPhone.any { it.isLetter() || !it.isDigit() } ->
|
||||
Res.string.feature_recover_now_invalid_phone_number_error
|
||||
|
||||
cellPhone.length <= PHONE_NUMBER_LENGTH ->
|
||||
Res.string.feature_recover_now_phone_number_error
|
||||
|
||||
!ValidationHelper.isValidPhoneNumber(cellPhone) ->
|
||||
Res.string
|
||||
.feature_upload_id_error_mobile_not_valid
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@ -192,13 +206,14 @@ internal class UploadIdViewModel :
|
||||
|
||||
private fun uploadDetails() {
|
||||
// TODO call api
|
||||
// viewModelScope.launch {
|
||||
// mutableStateFlow.update {
|
||||
// it.copy(dialogState = UploadIdUiState.DialogState.Loading)
|
||||
// }
|
||||
// delay(3000)
|
||||
// sendEvent(UploadIdEvent.NavigateToOtp)
|
||||
// }
|
||||
viewModelScope.launch {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = UploadIdUiState.DialogState.Loading)
|
||||
}
|
||||
delay(3000)
|
||||
dismissDialog()
|
||||
sendEvent(UploadIdEvent.NavigateToOtp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleDatePicker() {
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
package org.mifos.mobile.feature.beneficiary.beneficiaryApplication
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -93,12 +92,11 @@ private fun BeneficiaryApplicationScreen(
|
||||
MifosScaffold(
|
||||
topBarTitle = state.topBarTitle,
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
backPress = { onAction(BeneficiaryApplicationAction.OnNavigate) },
|
||||
onNavigationIconClick = { onAction(BeneficiaryApplicationAction.OnNavigate) },
|
||||
modifier = modifier,
|
||||
content = { paddingValues ->
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
if (state.template != null && state.beneficiary != null) {
|
||||
BeneficiaryApplicationContent(
|
||||
|
||||
@ -10,13 +10,7 @@
|
||||
package org.mifos.mobile.feature.beneficiary.beneficiaryDetail
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -29,19 +23,15 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import mifos_mobile.feature.beneficiary.generated.resources.Res
|
||||
import mifos_mobile.feature.beneficiary.generated.resources.beneficiary_detail
|
||||
import mifos_mobile.feature.beneficiary.generated.resources.cancel
|
||||
import mifos_mobile.feature.beneficiary.generated.resources.delete
|
||||
import mifos_mobile.feature.beneficiary.generated.resources.delete_beneficiary
|
||||
import mifos_mobile.feature.beneficiary.generated.resources.update_beneficiary
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.BasicDialogState
|
||||
import org.mifos.mobile.core.designsystem.component.MifosBasicDialog
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.model.entity.beneficiary.Beneficiary
|
||||
import org.mifos.mobile.core.ui.component.MifosAlertDialog
|
||||
@ -144,7 +134,7 @@ private fun BeneficiaryDetailScreen(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(it),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
if (state.beneficiary != null) {
|
||||
BeneficiaryDetailContent(
|
||||
@ -160,6 +150,7 @@ private fun BeneficiaryDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
@Composable
|
||||
private fun BeneficiaryDetailTopAppBar(
|
||||
navigateBack: () -> Unit,
|
||||
@ -172,42 +163,41 @@ private fun BeneficiaryDetailTopAppBar(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
MifosTopAppBar(
|
||||
backPress = navigateBack,
|
||||
topBarTitle = stringResource(Res.string.beneficiary_detail),
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { openDropdown = updateDropdownValue.invoke(!openDropdown) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MifosIcons.MoreVert,
|
||||
contentDescription = "More",
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = openDropdown,
|
||||
onDismissRequest = {
|
||||
openDropdown = updateDropdownValue.invoke(!openDropdown)
|
||||
},
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(Res.string.update_beneficiary)) },
|
||||
onClick = {
|
||||
openDropdown = updateDropdownValue.invoke(!openDropdown)
|
||||
updateBeneficiaryClicked.invoke()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(Res.string.delete_beneficiary)) },
|
||||
onClick = {
|
||||
openDropdown = updateDropdownValue.invoke(!openDropdown)
|
||||
showAlert.invoke()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
// MifosTopAppBar(
|
||||
// onNavigationIconClick = navigateBack,
|
||||
// title = stringResource(Res.string.beneficiary_detail),
|
||||
// actions = {
|
||||
// IconButton(
|
||||
// onClick = { openDropdown = updateDropdownValue.invoke(!openDropdown) },
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = MifosIcons.MoreVert,
|
||||
// contentDescription = "More",
|
||||
// )
|
||||
// }
|
||||
// DropdownMenu(
|
||||
// expanded = openDropdown,
|
||||
// onDismissRequest = {
|
||||
// openDropdown = updateDropdownValue.invoke(!openDropdown)
|
||||
// },
|
||||
// ) {
|
||||
// DropdownMenuItem(
|
||||
// text = { Text(text = stringResource(Res.string.update_beneficiary)) },
|
||||
// onClick = {
|
||||
// openDropdown = updateDropdownValue.invoke(!openDropdown)
|
||||
// updateBeneficiaryClicked.invoke()
|
||||
// },
|
||||
// )
|
||||
// DropdownMenuItem(
|
||||
// text = { Text(text = stringResource(Res.string.delete_beneficiary)) },
|
||||
// onClick = {
|
||||
// openDropdown = updateDropdownValue.invoke(!openDropdown)
|
||||
// showAlert.invoke()
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@ -11,7 +11,6 @@ package org.mifos.mobile.feature.beneficiary.beneficiaryList
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
@ -115,7 +114,7 @@ private fun BeneficiaryListScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.beneficiaries),
|
||||
backPress = { onAction(BeneficiaryListAction.OnNavigate) },
|
||||
onNavigationIconClick = { onAction(BeneficiaryListAction.OnNavigate) },
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
modifier = modifier,
|
||||
floatingActionButtonContent = FloatingActionButtonContent(
|
||||
@ -135,8 +134,7 @@ private fun BeneficiaryListScreen(
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
if (state.dialogState == null) {
|
||||
if (state.beneficiaries.isEmpty()) {
|
||||
|
||||
@ -36,12 +36,11 @@ internal fun BeneficiaryScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.add_beneficiary),
|
||||
backPress = topAppbarNavigateBack,
|
||||
onNavigationIconClick = topAppbarNavigateBack,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
|
||||
@ -120,12 +120,11 @@ private fun ClientChargeScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(state.topBarTitleResId),
|
||||
backPress = { onAction(ClientChargeAction.OnNavigate) },
|
||||
onNavigationIconClick = { onAction(ClientChargeAction.OnNavigate) },
|
||||
modifier = modifier,
|
||||
content = { paddingValues ->
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
when {
|
||||
|
||||
@ -107,10 +107,10 @@ private fun AddGuarantorScreen(
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
backPress = { onAction(AddGuarantorAction.NavigateBack) },
|
||||
onNavigationIconClick = { onAction(AddGuarantorAction.NavigateBack) },
|
||||
modifier = modifier,
|
||||
content = {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
AddGuarantorContent(
|
||||
state = state,
|
||||
guarantorItem = state.guarantorItem,
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
package org.mifos.mobile.feature.guarantor.screens.guarantorDetails
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -23,16 +22,12 @@ import mifos_mobile.feature.guarantor.generated.resources.Res
|
||||
import mifos_mobile.feature.guarantor.generated.resources.delete_guarantor
|
||||
import mifos_mobile.feature.guarantor.generated.resources.dialog_are_you_sure_that_you_want_to_string
|
||||
import mifos_mobile.feature.guarantor.generated.resources.dismiss
|
||||
import mifos_mobile.feature.guarantor.generated.resources.guarantor_details
|
||||
import mifos_mobile.feature.guarantor.generated.resources.update_guarantor
|
||||
import mifos_mobile.feature.guarantor.generated.resources.yes
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.BasicDialogState
|
||||
import org.mifos.mobile.core.designsystem.component.MifosBasicDialog
|
||||
import org.mifos.mobile.core.designsystem.component.MifosDropdownMenu
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.ui.component.MifosAlertDialog
|
||||
import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay
|
||||
import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
@ -83,25 +78,25 @@ private fun GuarantorDetailScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
MifosTopAppBar(
|
||||
topBarTitle = stringResource(Res.string.guarantor_details),
|
||||
backPress = { onAction(GuarantorDetailAction.NavigateBack) },
|
||||
actions = {
|
||||
MifosDropdownMenu(
|
||||
menuItems = listOf(
|
||||
stringResource(Res.string.update_guarantor)
|
||||
to { onAction(GuarantorDetailAction.UpdateGuarantor) },
|
||||
stringResource(Res.string.delete_guarantor)
|
||||
to { onAction(GuarantorDetailAction.UpdateMenuDialogValue) },
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
// topBar = {
|
||||
// MifosTopAppBar(
|
||||
// topBarTitle = stringResource(Res.string.guarantor_details),
|
||||
// backPress = { onAction(GuarantorDetailAction.NavigateBack) },
|
||||
// actions = {
|
||||
// MifosDropdownMenu(
|
||||
// menuItems = listOf(
|
||||
// stringResource(Res.string.update_guarantor)
|
||||
// to { onAction(GuarantorDetailAction.UpdateGuarantor) },
|
||||
// stringResource(Res.string.delete_guarantor)
|
||||
// to { onAction(GuarantorDetailAction.UpdateMenuDialogValue) },
|
||||
// ),
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
snackbarHostState = snackbarHostState,
|
||||
content = {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
state.guarantor?.let { it1 -> GuarantorDetailContent(data = it1) }
|
||||
}
|
||||
},
|
||||
|
||||
@ -90,7 +90,7 @@ private fun GuarantorListScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.view_guarantor),
|
||||
backPress = { (onAction(GuarantorListAction.OnNavigateBackClick)) },
|
||||
onNavigationIconClick = { (onAction(GuarantorListAction.OnNavigateBackClick)) },
|
||||
modifier = modifier,
|
||||
floatingActionButtonContent = FloatingActionButtonContent(
|
||||
onClick = { onAction(GuarantorListAction.OnAddGuarantor) },
|
||||
@ -109,7 +109,7 @@ private fun GuarantorListScreen(
|
||||
MifosErrorComponent(isEmptyData = true)
|
||||
} else {
|
||||
GuarantorList(
|
||||
modifier = Modifier.padding(it),
|
||||
modifier = Modifier,
|
||||
guarantorList = state.guarantorList,
|
||||
onAction = onAction,
|
||||
)
|
||||
|
||||
@ -35,13 +35,11 @@ import mifos_mobile.feature.help.generated.resources.no_questions_found
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.model.entity.FAQ
|
||||
import org.mifos.mobile.core.ui.component.EmptyDataView
|
||||
import org.mifos.mobile.core.ui.component.FaqItemHolder
|
||||
import org.mifos.mobile.core.ui.component.MifosTextButtonWithTopDrawable
|
||||
import org.mifos.mobile.core.ui.component.MifosTitleSearchCard
|
||||
import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
|
||||
@Composable
|
||||
@ -81,20 +79,20 @@ private fun HelpScreenContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBar = {
|
||||
MifosTopAppBar(
|
||||
navigateBack = { onAction(HelpAction.NavigateBack) },
|
||||
title = {
|
||||
MifosTitleSearchCard(
|
||||
searchQuery = { query -> onAction(HelpAction.SearchFaq(query)) },
|
||||
titleResourceId = Res.string.help,
|
||||
onSearchDismiss = { onAction(HelpAction.DismissSearch) },
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
// topBar = {
|
||||
// MifosTopAppBar(
|
||||
// navigateBack = { onAction(HelpAction.NavigateBack) },
|
||||
// title = {
|
||||
// MifosTitleSearchCard(
|
||||
// searchQuery = { query -> onAction(HelpAction.SearchFaq(query)) },
|
||||
// titleResourceId = Res.string.help,
|
||||
// onSearchDismiss = { onAction(HelpAction.DismissSearch) },
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
content = {
|
||||
Box(modifier = Modifier) {
|
||||
if (uiState.faqList.isNotEmpty()) {
|
||||
HelpContent(
|
||||
faqArrayList = uiState.faqList,
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
package org.mifos.mobile.feature.home.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import org.mifos.mobile.feature.home.screens.HomeScreen
|
||||
|
||||
fun NavGraphBuilder.homeNavGraph(
|
||||
onNavigate: (HomeDestinations) -> Unit,
|
||||
callHelpline: () -> Unit,
|
||||
mailHelpline: () -> Unit,
|
||||
) {
|
||||
navigation(
|
||||
startDestination = HomeNavigation.HomeScreen.route,
|
||||
route = HomeNavigation.HomeBase.route,
|
||||
) {
|
||||
homeRoute(
|
||||
onNavigate = onNavigate,
|
||||
callHelpline = callHelpline,
|
||||
mailHelpline = mailHelpline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.homeRoute(
|
||||
onNavigate: (HomeDestinations) -> Unit,
|
||||
callHelpline: () -> Unit,
|
||||
mailHelpline: () -> Unit,
|
||||
) {
|
||||
composable(
|
||||
route = HomeNavigation.HomeScreen.route,
|
||||
) {
|
||||
HomeScreen(
|
||||
callHelpline = callHelpline,
|
||||
mailHelpline = mailHelpline,
|
||||
onNavigate = onNavigate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToHomeScreen(navOptions: NavOptions? = null) {
|
||||
return this.navigate(HomeNavigation.HomeScreen.route, navOptions)
|
||||
}
|
||||
@ -9,21 +9,15 @@
|
||||
*/
|
||||
package org.mifos.mobile.feature.home.navigation
|
||||
|
||||
import org.mifos.mobile.feature.home.navigation.HomeRoute.HOME_NAVIGATION_ROUTE_BASE
|
||||
import org.mifos.mobile.feature.home.navigation.HomeRoute.HOME_SCREEN_ROUTE
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.mifos.mobile.core.ui.composableWithStayTransitions
|
||||
import org.mifos.mobile.feature.home.screens.HomeScreen
|
||||
import org.mifos.mobile.feature.home.viewmodel.HomeCardItem
|
||||
import org.mifos.mobile.feature.home.viewmodel.HomeNavigationItems
|
||||
|
||||
sealed class HomeNavigation(val route: String) {
|
||||
data object HomeBase : HomeNavigation(route = HOME_NAVIGATION_ROUTE_BASE)
|
||||
data object HomeScreen : HomeNavigation(route = HOME_SCREEN_ROUTE)
|
||||
}
|
||||
|
||||
object HomeRoute {
|
||||
const val HOME_NAVIGATION_ROUTE_BASE = "home_base_route"
|
||||
const val HOME_SCREEN_ROUTE = "home_screen_route"
|
||||
}
|
||||
|
||||
enum class HomeDestinations {
|
||||
HOME,
|
||||
ACCOUNTS,
|
||||
@ -72,3 +66,23 @@ fun HomeCardItem.toDestination(): HomeDestinations {
|
||||
HomeCardItem.TransferCard -> HomeDestinations.TRANSFER
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object HomeRoute
|
||||
|
||||
fun NavController.navigateToHomeScreen(navOptions: NavOptions? = null) =
|
||||
navigate(HomeRoute, navOptions)
|
||||
|
||||
fun NavGraphBuilder.homeDestination(
|
||||
onNavigate: (HomeDestinations) -> Unit,
|
||||
callHelpline: () -> Unit,
|
||||
mailHelpline: () -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions<HomeRoute> {
|
||||
HomeScreen(
|
||||
callHelpline = callHelpline,
|
||||
mailHelpline = mailHelpline,
|
||||
onNavigate = onNavigate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ package org.mifos.mobile.feature.home.screens
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
@ -22,13 +21,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
@ -41,12 +38,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import mifos_mobile.feature.home.generated.resources.Res
|
||||
import mifos_mobile.feature.home.generated.resources.accounts_overview
|
||||
@ -66,8 +61,6 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.mifos.mobile.core.common.CurrencyFormatter
|
||||
import org.mifos.mobile.core.designsystem.component.MifosCard
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.ui.component.MifosHiddenTextRow
|
||||
import org.mifos.mobile.core.ui.component.MifosLinkText
|
||||
@ -104,49 +97,49 @@ internal fun HomeContent(
|
||||
},
|
||||
content = {
|
||||
MifosScaffold(
|
||||
topBar = {
|
||||
MifosTopAppBar(
|
||||
topBarTitle = stringResource(Res.string.home),
|
||||
icon = MifosIcons.NavigationDrawer,
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onAction(HomeAction.OnNavigate(HomeDestinations.NOTIFICATIONS))
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier,
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MifosIcons.Notifications,
|
||||
contentDescription = null,
|
||||
)
|
||||
if (state.notificationCount > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.padding(2.dp)
|
||||
.size(8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = state.notificationCount.toString(),
|
||||
fontSize = 6.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
backPress = {
|
||||
coroutineScope.launch { drawerState.open() }
|
||||
},
|
||||
)
|
||||
},
|
||||
// topBar = {
|
||||
// MifosTopAppBar(
|
||||
// topBarTitle = stringResource(Res.string.home),
|
||||
// icon = MifosIcons.NavigationDrawer,
|
||||
// actions = {
|
||||
// IconButton(
|
||||
// onClick = {
|
||||
// onAction(HomeAction.OnNavigate(HomeDestinations.NOTIFICATIONS))
|
||||
// },
|
||||
// ) {
|
||||
// Box(
|
||||
// modifier = Modifier,
|
||||
// contentAlignment = Alignment.TopEnd,
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = MifosIcons.Notifications,
|
||||
// contentDescription = null,
|
||||
// )
|
||||
// if (state.notificationCount > 0) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .clip(CircleShape)
|
||||
// .padding(2.dp)
|
||||
// .size(8.dp),
|
||||
// contentAlignment = Alignment.Center,
|
||||
// ) {
|
||||
// Text(
|
||||
// text = state.notificationCount.toString(),
|
||||
// fontSize = 6.sp,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// backPress = {
|
||||
// coroutineScope.launch { drawerState.open() }
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
) {
|
||||
HomeScreenContent(
|
||||
modifier = Modifier.padding(it),
|
||||
modifier = Modifier,
|
||||
onAction = onAction,
|
||||
state = state,
|
||||
)
|
||||
|
||||
@ -11,7 +11,6 @@ package org.mifos.mobile.feature.loan.loanAccount
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -151,7 +150,7 @@ private fun LoanAccountDetailScreen(
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
if (state.loanAccountAssociations != null) {
|
||||
LoanAccountDetailContent(
|
||||
loanWithAssociations = state.loanAccountAssociations,
|
||||
|
||||
@ -7,32 +7,20 @@
|
||||
*
|
||||
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
|
||||
*/
|
||||
@file:Suppress("unusedPrivateProperty", "unusedParameter")
|
||||
|
||||
package org.mifos.mobile.feature.loan.loanAccount
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.compose.ui.unit.dp
|
||||
import mifos_mobile.feature.loan.generated.resources.Res
|
||||
import mifos_mobile.feature.loan.generated.resources.loan_account_details
|
||||
import mifos_mobile.feature.loan.generated.resources.update_loan
|
||||
import mifos_mobile.feature.loan.generated.resources.view_guarantor
|
||||
import mifos_mobile.feature.loan.generated.resources.withdraw_loan
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
|
||||
@Suppress("UnusedParameter", "UnusedPrivateProperty")
|
||||
@Composable
|
||||
internal fun LoanAccountDetailTopBar(
|
||||
navigateBack: () -> Unit,
|
||||
@ -43,43 +31,43 @@ internal fun LoanAccountDetailTopBar(
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
MifosTopAppBar(
|
||||
modifier = modifier,
|
||||
topBarTitle = stringResource(Res.string.loan_account_details),
|
||||
backPress = navigateBack,
|
||||
actions = {
|
||||
IconButton(onClick = { showMenu = !showMenu }) {
|
||||
Icon(
|
||||
imageVector = MifosIcons.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 32.dp),
|
||||
onDismissRequest = { showMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringResource(Res.string.view_guarantor))
|
||||
},
|
||||
onClick = viewGuarantor,
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringResource(Res.string.update_loan))
|
||||
},
|
||||
onClick = updateLoan,
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringResource(Res.string.withdraw_loan))
|
||||
},
|
||||
onClick = withdrawLoan,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
// MifosTopAppBar(
|
||||
// modifier = modifier,
|
||||
// topBarTitle = stringResource(Res.string.loan_account_details),
|
||||
// backPress = navigateBack,
|
||||
// actions = {
|
||||
// IconButton(onClick = { showMenu = !showMenu }) {
|
||||
// Icon(
|
||||
// imageVector = MifosIcons.MoreVert,
|
||||
// contentDescription = "Menu",
|
||||
// )
|
||||
// }
|
||||
// DropdownMenu(
|
||||
// expanded = showMenu,
|
||||
// modifier = Modifier.padding(start = 16.dp, end = 32.dp),
|
||||
// onDismissRequest = { showMenu = false },
|
||||
// ) {
|
||||
// DropdownMenuItem(
|
||||
// text = {
|
||||
// Text(text = stringResource(Res.string.view_guarantor))
|
||||
// },
|
||||
// onClick = viewGuarantor,
|
||||
// )
|
||||
// DropdownMenuItem(
|
||||
// text = {
|
||||
// Text(text = stringResource(Res.string.update_loan))
|
||||
// },
|
||||
// onClick = updateLoan,
|
||||
// )
|
||||
// DropdownMenuItem(
|
||||
// text = {
|
||||
// Text(text = stringResource(Res.string.withdraw_loan))
|
||||
// },
|
||||
// onClick = withdrawLoan,
|
||||
// )
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
@ -12,24 +12,16 @@ package org.mifos.mobile.feature.loan.loanAccountApplication
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import mifos_mobile.feature.loan.generated.resources.Res
|
||||
import mifos_mobile.feature.loan.generated.resources.apply_for_loan
|
||||
import mifos_mobile.feature.loan.generated.resources.update_loan
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.model.enums.LoanState
|
||||
import org.mifos.mobile.core.ui.component.MifosErrorComponent
|
||||
import org.mifos.mobile.core.ui.component.MifosProgressIndicator
|
||||
import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
@ -101,24 +93,23 @@ private fun LoanApplicationScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
MifosTopAppBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backPress = { onAction(LoanApplicationAction.BackPress) },
|
||||
topBarTitle =
|
||||
stringResource(
|
||||
if (state.loanState == LoanState.CREATE) {
|
||||
Res.string.apply_for_loan
|
||||
} else {
|
||||
Res.string.update_loan
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
// topBar = {
|
||||
// MifosTopAppBar(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// backPress = { onAction(LoanApplicationAction.BackPress) },
|
||||
// topBarTitle =
|
||||
// stringResource(
|
||||
// if (state.loanState == LoanState.CREATE) {
|
||||
// Res.string.apply_for_loan
|
||||
// } else {
|
||||
// Res.string.update_loan
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
// },
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
state.loanWithAssociations?.let {
|
||||
|
||||
@ -98,9 +98,9 @@ private fun LoanAccountSummaryScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.loan_summary),
|
||||
backPress = { (onAction(LoanAccountSummaryAction.BackPress)) },
|
||||
onNavigationIconClick = { (onAction(LoanAccountSummaryAction.BackPress)) },
|
||||
) {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
state.loanAccountAssociations?.let {
|
||||
LoanAccountSummaryContent(
|
||||
state = state,
|
||||
|
||||
@ -43,7 +43,6 @@ import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.common.CurrencyFormatter
|
||||
import org.mifos.mobile.core.common.DateHelper
|
||||
import org.mifos.mobile.core.common.Utils.formatTransactionType
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.model.entity.Transaction
|
||||
import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations
|
||||
@ -91,6 +90,7 @@ private fun LoanAccountTransactionDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
@Composable
|
||||
private fun LoanAccountTransactionScreen(
|
||||
state: LoanAccountTransactionState,
|
||||
@ -98,10 +98,10 @@ private fun LoanAccountTransactionScreen(
|
||||
onAction: (LoanAccountTransactionAction) -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
MifosTopAppBar(
|
||||
backPress = { (onAction(LoanAccountTransactionAction.BackPress)) },
|
||||
topBarTitle = stringResource(Res.string.transactions),
|
||||
)
|
||||
// MifosTopAppBar(
|
||||
// backPress = { (onAction(LoanAccountTransactionAction.BackPress)) },
|
||||
// topBarTitle = stringResource(Res.string.transactions),
|
||||
// )
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
state.loanWithAssociations?.let {
|
||||
|
||||
@ -107,11 +107,11 @@ private fun LoanAccountWithdrawScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
MifosScaffold(
|
||||
backPress = { onAction(LoanAccountWithdrawAction.BackPress) },
|
||||
onNavigationIconClick = { onAction(LoanAccountWithdrawAction.BackPress) },
|
||||
topBarTitle = stringResource(Res.string.withdraw_loan),
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
content = {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
Column(modifier = modifier.padding(16.dp)) {
|
||||
LoanAccountWithdrawContent(
|
||||
loanWithAssociations = state.loanWithAssociations,
|
||||
|
||||
@ -112,13 +112,12 @@ private fun LoanRepaymentScheduleScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.loan_repayment_schedule),
|
||||
backPress = { (onAction(LoanRepaymentScheduleAction.BackPress)) },
|
||||
onNavigationIconClick = { (onAction(LoanRepaymentScheduleAction.BackPress)) },
|
||||
modifier = modifier,
|
||||
) { contentPadding ->
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding),
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
state.loanWithAssociations?.let {
|
||||
LoanRepaymentScheduleCard(it)
|
||||
|
||||
@ -27,12 +27,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import mifos_mobile.feature.loan.generated.resources.Res
|
||||
import mifos_mobile.feature.loan.generated.resources.no_internet_connection
|
||||
import mifos_mobile.feature.loan.generated.resources.update_loan
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.model.enums.LoanState
|
||||
import org.mifos.mobile.core.ui.component.MifosErrorComponent
|
||||
@ -105,11 +102,11 @@ private fun ReviewLoanApplicationScreen(
|
||||
snackbarHostState = snackbarHostState,
|
||||
content = {
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
MifosTopAppBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backPress = { onAction(ReviewLoanApplicationAction.NavigateBack(false)) },
|
||||
topBarTitle = stringResource(Res.string.update_loan),
|
||||
)
|
||||
// MifosTopAppBar(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// backPress = { onAction(ReviewLoanApplicationAction.NavigateBack(false)) },
|
||||
// topBarTitle = stringResource(Res.string.update_loan),
|
||||
// )
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
ReviewLoanApplicationContent(
|
||||
data = state.reviewLoanApplicationUiData,
|
||||
|
||||
@ -85,10 +85,10 @@ private fun NotificationScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.notification),
|
||||
backPress = navigateBack,
|
||||
onNavigationIconClick = navigateBack,
|
||||
modifier = modifier,
|
||||
content = {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
when (uiState) {
|
||||
is NotificationUiState.Loading -> MifosProgressIndicatorOverlay()
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.mifos.mobile"
|
||||
namespace = "org.mifos.mobile.feature.onboarding.language"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
||||
@ -11,7 +11,6 @@ package org.mifos.mobile.feature.qr.qr
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
@ -83,7 +82,7 @@ private fun QrCodeReaderContent(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = null,
|
||||
backPress = { onAction(QrCodeReaderAction.OnNavigate) },
|
||||
onNavigationIconClick = { onAction(QrCodeReaderAction.OnNavigate) },
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
@ -92,7 +91,7 @@ private fun QrCodeReaderContent(
|
||||
) {
|
||||
QrScannerWithPermissions(
|
||||
types = listOf(CodeType.QR),
|
||||
modifier = Modifier.padding(it),
|
||||
modifier = Modifier,
|
||||
onScanned = {
|
||||
onAction(QrCodeReaderAction.ScanQrCode(it))
|
||||
true
|
||||
|
||||
@ -18,8 +18,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -36,15 +34,12 @@ import io.github.alexzhirkevich.qrose.rememberQrCodePainter
|
||||
import io.github.alexzhirkevich.qrose.toByteArray
|
||||
import mifos_mobile.feature.qr.generated.resources.Res
|
||||
import mifos_mobile.feature.qr.generated.resources.choose_option
|
||||
import mifos_mobile.feature.qr.generated.resources.qr_code
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.BasicDialogState
|
||||
import org.mifos.mobile.core.designsystem.component.MifosBasicDialog
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.designsystem.icon.MifosIcons
|
||||
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
|
||||
import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay
|
||||
import org.mifos.mobile.core.ui.utils.EventsEffect
|
||||
@ -92,6 +87,7 @@ private fun QrCodeDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
@Composable
|
||||
private fun QrCodeDisplayScreen(
|
||||
state: QrCodeDisplayState,
|
||||
@ -109,34 +105,33 @@ private fun QrCodeDisplayScreen(
|
||||
val option = stringResource(Res.string.choose_option)
|
||||
MifosScaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
MifosTopAppBar(
|
||||
backPress = { onAction(QrCodeDisplayAction.OnNavigate) },
|
||||
topBarTitle = stringResource(Res.string.qr_code),
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onAction(
|
||||
QrCodeDisplayAction.ShareQrCode(
|
||||
bytes,
|
||||
option,
|
||||
),
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Icon(
|
||||
imageVector = MifosIcons.Share,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
// topBar = {
|
||||
// MifosTopAppBar(
|
||||
// backPress = { onAction(QrCodeDisplayAction.OnNavigate) },
|
||||
// topBarTitle = stringResource(Res.string.qr_code),
|
||||
// actions = {
|
||||
// IconButton(
|
||||
// onClick = {
|
||||
// onAction(
|
||||
// QrCodeDisplayAction.ShareQrCode(
|
||||
// bytes,
|
||||
// option,
|
||||
// ),
|
||||
// )
|
||||
// },
|
||||
// content = {
|
||||
// Icon(
|
||||
// imageVector = MifosIcons.Share,
|
||||
// contentDescription = null,
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
QrCodeDisplayContent(painter = painter)
|
||||
|
||||
@ -12,7 +12,6 @@ package org.mifos.mobile.feature.qr.qrCodeImport
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -104,14 +103,13 @@ private fun QrCodeImportScreen(
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.import_qr),
|
||||
backPress = { onAction(QrCodeImportAction.OnNavigate) },
|
||||
onNavigationIconClick = { onAction(QrCodeImportAction.OnNavigate) },
|
||||
modifier = modifier,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
QrCodeImportContent(proceedClicked = { imageBitmap ->
|
||||
|
||||
@ -99,13 +99,12 @@ private fun RecentTransactionScreen(
|
||||
|
||||
MifosScaffold(
|
||||
topBarTitle = stringResource(Res.string.recent_transactions),
|
||||
backPress = navigateBack,
|
||||
onNavigationIconClick = navigateBack,
|
||||
modifier = modifier,
|
||||
content = { paddingValues ->
|
||||
content = {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
PullToRefreshBox(
|
||||
state = pullRefreshState,
|
||||
|
||||
@ -11,7 +11,6 @@ package org.mifos.mobile.feature.savings.savingsAccount
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -19,14 +18,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import mifos_mobile.feature.savings.generated.resources.Res
|
||||
import mifos_mobile.feature.savings.generated.resources.approval_pending
|
||||
import mifos_mobile.feature.savings.generated.resources.ic_assignment_turned_in_black_24dp
|
||||
import mifos_mobile.feature.savings.generated.resources.saving_account_details
|
||||
import mifos_mobile.feature.savings.generated.resources.update_savings_account
|
||||
import mifos_mobile.feature.savings.generated.resources.withdraw_savings_account
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.mifos.mobile.core.designsystem.component.MifosDropdownMenu
|
||||
import org.mifos.mobile.core.designsystem.component.MifosScaffold
|
||||
import org.mifos.mobile.core.designsystem.component.MifosTopAppBar
|
||||
import org.mifos.mobile.core.model.enums.ChargeType
|
||||
import org.mifos.mobile.core.ui.component.EmptyDataView
|
||||
import org.mifos.mobile.core.ui.component.MifosErrorComponent
|
||||
@ -69,6 +62,7 @@ internal fun SavingsAccountDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
@Composable
|
||||
private fun SavingsAccountDetailScreen(
|
||||
uiState: SavingsAccountDetailUiState,
|
||||
@ -84,23 +78,23 @@ private fun SavingsAccountDetailScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
MifosScaffold(
|
||||
topBar = {
|
||||
MifosTopAppBar(
|
||||
topBarTitle = stringResource(Res.string.saving_account_details),
|
||||
backPress = navigateBack,
|
||||
actions = {
|
||||
MifosDropdownMenu(
|
||||
menuItems = listOf(
|
||||
stringResource(Res.string.update_savings_account) to updateSavingsAccount,
|
||||
stringResource(Res.string.withdraw_savings_account) to withdrawSavingsAccount,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
// topBar = {
|
||||
// MifosTopAppBar(
|
||||
// topBarTitle = stringResource(Res.string.saving_account_details),
|
||||
// backPress = navigateBack,
|
||||
// actions = {
|
||||
// MifosDropdownMenu(
|
||||
// menuItems = listOf(
|
||||
// stringResource(Res.string.update_savings_account) to updateSavingsAccount,
|
||||
// stringResource(Res.string.withdraw_savings_account) to withdrawSavingsAccount,
|
||||
// ),
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
when (uiState) {
|
||||
is SavingsAccountDetailUiState.Error -> MifosErrorComponent()
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
package org.mifos.mobile.feature.savings.savingsAccountApplication
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -83,12 +82,12 @@ private fun SavingsAccountApplicationScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
MifosScaffold(
|
||||
backPress = navigateBack,
|
||||
onNavigationIconClick = navigateBack,
|
||||
topBarTitle = topBarTitleText,
|
||||
modifier = modifier,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
content = {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
Box(modifier = Modifier) {
|
||||
when (uiState) {
|
||||
is SavingsAccountApplicationUiState.Error -> {
|
||||
MifosErrorComponent(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user