Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a74cdc6
feat: implement app language selection and onboarding language screen
mena-rizkalla Feb 24, 2026
2e0e217
Merge branch 'development' of https://github.com/mena-rizkalla/mobile…
mena-rizkalla Feb 24, 2026
e4a9cf4
Merge branch 'development' into feat/install-language-selection
mena-rizkalla Feb 24, 2026
252625c
Merge branch 'development' into feat/install-language-selection
mena-rizkalla Feb 25, 2026
254da7b
Merge branch 'development' into feat/install-language-selection
mena-rizkalla Feb 26, 2026
be8ec22
feat: enhance onboarding language selection and persistence
mena-rizkalla Feb 26, 2026
cb369ac
style: cleanup and remove redundant comments
mena-rizkalla Feb 26, 2026
f3a1deb
fix: reorder language update and navigation in LanguageViewModel
mena-rizkalla Feb 26, 2026
754bdde
fix: improve language selection persistence and navigation flow
mena-rizkalla Feb 26, 2026
3e4db00
clean: remove onboarding language strings from settings module
mena-rizkalla Feb 26, 2026
f2fd5da
refactor: use DataState for user preference updates and improve error…
mena-rizkalla Feb 26, 2026
06f2c34
fix: update MainActivity configChanges to handle locale and layoutDir…
mena-rizkalla Feb 26, 2026
d9cb4de
clean: remove language selection feature from settings module
mena-rizkalla Feb 26, 2026
442b726
build: remove datastore and designsystem dependencies from prodReleas…
mena-rizkalla Feb 26, 2026
53c16c9
refactor: update dependencies and refine preference clearing logic
mena-rizkalla Feb 28, 2026
03e33cf
build: remove redundant core:model dependency from prod release class…
mena-rizkalla Feb 28, 2026
f53afbd
Merge branch 'development' into feat/install-language-selection
mena-rizkalla Feb 28, 2026
06c451d
Merge branch 'development' into feat/install-language-selection
mena-rizkalla Mar 2, 2026
4216099
Merge branch 'development' into feat/install-language-selection
mena-rizkalla Mar 4, 2026
1e3b14a
feat: implement MifosRadioButton and enhance language selection UI
mena-rizkalla Mar 5, 2026
56d81a5
Merge development and resolve conflicts
mena-rizkalla Mar 19, 2026
9bfa3ec
build: update and reorganize dependencies for passcode and onboarding…
mena-rizkalla Mar 19, 2026
0f35584
refactor: rename onboarding visibility preference and cleanup design …
mena-rizkalla Mar 21, 2026
d77c000
build: remove redundant passcode feature dependency tree entry
mena-rizkalla Mar 21, 2026
55768da
feat: handle language changes in desktop main
mena-rizkalla Mar 23, 2026
419545a
feat: implement locale persistence and application reload on language…
mena-rizkalla Mar 23, 2026
b5943f8
merge development and solve conflicts
mena-rizkalla Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3430,6 +3430,43 @@
| | +--- org.jetbrains.compose.material3:material3:1.9.0-beta03 -> 1.9.0 (*)
| | +--- org.jetbrains.compose.components:components-resources:1.9.3 -> 1.10.0 (*)
| | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.9.3 (*)
| +--- project :feature:onboarding-language
| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.9.4 (*)
| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4 (*)
| | +--- androidx.tracing:tracing-ktx:1.3.0 (*)
| | +--- io.insert-koin:koin-bom:4.1.1 (*)
| | +--- io.insert-koin:koin-android:4.1.1 (*)
| | +--- io.insert-koin:koin-androidx-compose:4.1.1 (*)
| | +--- io.insert-koin:koin-androidx-navigation:4.1.1 (*)
| | +--- io.insert-koin:koin-core-viewmodel:4.1.1 (*)
| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 (*)
| | +--- io.insert-koin:koin-core:4.1.1 (*)
| | +--- io.insert-koin:koin-annotations:2.1.0 (*)
| | +--- project :core:ui (*)
| | +--- project :core:designsystem (*)
| | +--- project :core:data (*)
| | +--- io.insert-koin:koin-compose:4.1.1 (*)
| | +--- io.insert-koin:koin-compose-viewmodel:4.1.1 (*)
| | +--- org.jetbrains.compose.runtime:runtime:1.9.3 -> 1.10.1 (*)
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6 (*)
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.6 (*)
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6 (*)
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6 (*)
| | +--- org.jetbrains.androidx.savedstate:savedstate:1.4.0 (*)
| | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*)
| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.9.1 (*)
| | +--- org.jetbrains.androidx.navigationevent:navigationevent-compose:1.0.1 (*)
| | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0 (*)
| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0 (*)
| | +--- io.github.openmf:mifos-authenticator-passcode:2.0.6 (*)
| | +--- io.github.openmf:mifos-authenticator-biometrics:2.0.4 (*)
| | +--- org.jetbrains.compose.ui:ui:1.9.3 -> 1.10.1 (*)
| | +--- org.jetbrains.compose.foundation:foundation:1.9.3 -> 1.10.0 (*)
| | +--- org.jetbrains.compose.material3:material3:1.9.0-beta03 -> 1.9.0 (*)
| | +--- org.jetbrains.compose.components:components-resources:1.9.3 -> 1.10.0 (*)
| | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.9.3 (*)
| | +--- project :core:datastore (*)
| | \--- project :core-base:designsystem (*)
| +--- project :feature:passcode (*)
| \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.21 (*)
+--- project :core:data (*)
Expand Down
1 change: 1 addition & 0 deletions cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
:feature:mpay-qr
:feature:mpay-qr-scan
:feature:notification
:feature:onboarding-language
:feature:passcode
:feature:payments
:feature:profile
Expand Down
1 change: 1 addition & 0 deletions cmp-android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="locale|layoutDirection"
android:theme="@style/Theme.MifosSplash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
Expand Down
28 changes: 27 additions & 1 deletion cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
Expand Down Expand Up @@ -57,7 +59,31 @@ class MainActivity : AppCompatActivity() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState
.onEach { uiState = it }
.onEach { state ->
uiState = state
if (state is MainUiState.Success) {
val languageTag = state.language.localName
val currentAppLocales = AppCompatDelegate.getApplicationLocales()

val isRequestedDefault = languageTag.isNullOrBlank()
val isCurrentDefault = currentAppLocales.isEmpty

val shouldUpdate = if (isRequestedDefault) {
!isCurrentDefault
} else {
languageTag != currentAppLocales.toLanguageTags()
}

if (shouldUpdate) {
val appLocale: LocaleListCompat = if (isRequestedDefault) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(languageTag)
}
AppCompatDelegate.setApplicationLocales(appLocale)
}
}
}
.collect()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Expand Down
6 changes: 5 additions & 1 deletion cmp-desktop/src/desktopMain/kotlin/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ fun main() {
state = windowState,
title = "MifosWallet",
) {
MifosPaySharedApp()
MifosPaySharedApp(
onLanguageChange = { languageCode ->
java.util.Locale.setDefault(java.util.Locale(languageCode))
}
)
}
}
}
1 change: 1 addition & 0 deletions cmp-shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ kotlin {
implementation(projects.feature.fastMpay)
implementation(projects.feature.merchants)
implementation(projects.feature.upiSetup)
implementation(projects.feature.onboardingLanguage)
implementation(projects.feature.passcode)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
Expand All @@ -32,6 +33,7 @@ import org.mifospay.core.data.util.NetworkMonitor
import org.mifospay.core.data.util.TimeZoneMonitor
import org.mifospay.core.designsystem.component.MifosDialogBox
import org.mifospay.core.designsystem.theme.MifosTheme
import org.mifospay.feature.onboarding.language.navigation.ONBOARDING_LANGUAGE_ROUTE
import org.mifospay.shared.MainUiState.Success
import org.mifospay.shared.navigation.MifosNavGraph.LOGIN_GRAPH
import org.mifospay.shared.navigation.RootNavGraph
Expand All @@ -43,9 +45,10 @@ fun MifosPaySharedApp(
modifier: Modifier = Modifier,
networkMonitor: NetworkMonitor = koinInject(),
timeZoneMonitor: TimeZoneMonitor = koinInject(),
onLanguageChange: (String) -> Unit = {},
) {
PlatformAuthenticatorLocalCompositionProvider {
MifosPayApp(modifier, networkMonitor, timeZoneMonitor)
MifosPayApp(modifier, networkMonitor, timeZoneMonitor, onLanguageChange = onLanguageChange)
}
}

Expand All @@ -56,6 +59,7 @@ private fun MifosPayApp(
networkMonitor: NetworkMonitor,
timeZoneMonitor: TimeZoneMonitor,
viewModel: MifosPayViewModel = koinViewModel(),
onLanguageChange: (String) -> Unit = {},
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val navController = rememberNavController()
Expand All @@ -73,14 +77,21 @@ private fun MifosPayApp(
}
}

LaunchedEffect(Unit) {
var hasCheckedPasscodeOnStartup by remember { mutableStateOf(false) }

LaunchedEffect(uiState) {
val state = uiState
if (
state is Success &&
state.userData.authenticated &&
!viewModel.isPasscodeCreated()
) {
viewModel.logOut()
if (state is Success) {
val langCode = state.language.localName
if (!langCode.isNullOrEmpty()) {
onLanguageChange(langCode)
}
if (!hasCheckedPasscodeOnStartup) {
hasCheckedPasscodeOnStartup = true
if (state.userData.authenticated && !viewModel.isPasscodeCreated()) {
viewModel.logOut()
}
}
}
}

Expand All @@ -106,7 +117,9 @@ private fun MifosPayApp(

val navDestination = when (uiState) {
is MainUiState.Loading -> LOGIN_GRAPH
is Success -> if ((uiState as Success).userData.authenticated) {
is Success -> if ((uiState as Success).showLanguageScreen) {
ONBOARDING_LANGUAGE_ROUTE
} else if ((uiState as Success).userData.authenticated) {
ROOT_MIFOS_PASSCODE_ROUTE
} else {
LOGIN_GRAPH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.mifos.authenticator.passcode.PasscodeAction
import org.mifos.authenticator.passcode.PasscodeManager
import org.mifos.authenticator.passcode.PasscodeStorageAdapter
import org.mifospay.core.data.repository.AppLockRepository
import org.mifospay.core.datastore.UserPreferencesRepository
import org.mifospay.core.model.LanguageConfig
import org.mifospay.core.model.user.UserInfo

class MifosPayViewModel(
Expand All @@ -29,8 +30,12 @@ class MifosPayViewModel(
private val appLockRepository: AppLockRepository,
private val passcodeStorageAdapter: PasscodeStorageAdapter,
) : ViewModel() {
val uiState: StateFlow<MainUiState> = userDataRepository.userInfo.map {
MainUiState.Success(it)
val uiState: StateFlow<MainUiState> = combine(
userDataRepository.userInfo,
userDataRepository.language,
userDataRepository.showLanguageScreen,
) { userInfo, language, showLanguageScreen ->
MainUiState.Success(userInfo, language, showLanguageScreen)
}.stateIn(
scope = viewModelScope,
initialValue = MainUiState.Loading,
Expand All @@ -55,5 +60,9 @@ class MifosPayViewModel(

sealed interface MainUiState {
data object Loading : MainUiState
data class Success(val userData: UserInfo) : MainUiState
data class Success(
val userData: UserInfo,
val language: LanguageConfig,
val showLanguageScreen: Boolean,
) : MainUiState
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.mifospay.feature.merchants.di.MerchantsModule
import org.mifospay.feature.mpay.qr.di.MpayQrModule
import org.mifospay.feature.mpay.qr.scan.di.MpayQrScanModule
import org.mifospay.feature.notification.di.NotificationModule
import org.mifospay.feature.onboarding.language.di.onboardingLanguageModule
import org.mifospay.feature.payments.di.PaymentsModule
import org.mifospay.feature.profile.di.ProfileModule
import org.mifospay.feature.receipt.di.ReceiptModule
Expand Down Expand Up @@ -96,6 +97,7 @@ object KoinModules {
FastMpayModule,
MerchantsModule,
UpiSetupModule,
onboardingLanguageModule,
MifosAuthenticatorModule,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import org.mifos.feature.passcode.reAuthMifosPasscodeScreen
import org.mifos.feature.passcode.rootMifosPasscodeScreen
import org.mifospay.core.data.util.NetworkMonitor
import org.mifospay.core.data.util.TimeZoneMonitor
import org.mifospay.feature.onboarding.language.navigation.ONBOARDING_LANGUAGE_ROUTE
import org.mifospay.feature.onboarding.language.navigation.onboardingLanguageScreen
import org.mifospay.shared.instance.InstanceSelectorScreen
import org.mifospay.shared.ui.MifosApp

Expand Down Expand Up @@ -58,6 +60,16 @@ internal fun RootNavGraph(
onShowInstanceSelector = { showInstanceSelector = true },
)

onboardingLanguageScreen(
onNavigateToNext = {
navHostController.navigate(MifosNavGraph.LOGIN_GRAPH) {
popUpTo(ONBOARDING_LANGUAGE_ROUTE) {
inclusive = true
}
}
},
)

rootMifosPasscodeScreen(
onForgotButton = onClickLogout,
onAuthenticationSuccess = {
Expand Down
84 changes: 66 additions & 18 deletions cmp-web/src/jsMain/kotlin/Application.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,66 @@

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import kotlinx.browser.document
import org.jetbrains.skiko.wasm.onWasmReady
import org.mifospay.shared.MifosPaySharedApp
import org.mifospay.shared.di.initKoin

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
initKoin()

onWasmReady {
ComposeViewport(document.body!!) {
MifosPaySharedApp()
}
}
}
@file:OptIn(ExperimentalSettingsApi::class)

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.Settings
import com.russhwolf.settings.serialization.decodeValueOrNull
import kotlinx.browser.document
import kotlinx.browser.window
import org.jetbrains.skiko.wasm.onWasmReady
import org.mifospay.core.model.LanguageConfig
import org.mifospay.shared.MifosPaySharedApp
import org.mifospay.shared.di.initKoin

private fun setLocaleOverride(localeCode: String) {
window.localStorage.setItem("__mifos_locale_override", localeCode)
}

private fun applyLocaleOverride() {
js(
"""
(function() {
var loc = localStorage.getItem('__mifos_locale_override');
if (loc) {
Object.defineProperty(navigator, 'language', {
get: function() { return loc; },
configurable: true
});
Object.defineProperty(navigator, 'languages', {
get: function() { return [loc]; },
configurable: true
});
}
})();
"""
)
}

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val settings = Settings()
val languageConfig = settings.decodeValueOrNull(
key = "language",
serializer = LanguageConfig.serializer(),
)
val injectedLocale = languageConfig?.localName

if (injectedLocale != null) {
setLocaleOverride(injectedLocale)
applyLocaleOverride()
}

initKoin()

onWasmReady {
ComposeViewport(document.body!!) {
MifosPaySharedApp(
onLanguageChange = { languageCode ->
if (languageCode != injectedLocale) {
window.location.reload()
}
},
)
}
}
}
Loading
Loading