From a74cdc64afdbe6ac3600284d6fb7ab2076740fc5 Mon Sep 17 00:00:00 2001 From: mena Date: Tue, 24 Feb 2026 16:49:52 +0200 Subject: [PATCH 01/17] feat: implement app language selection and onboarding language screen - Add `LanguageConfig` model with support for multiple locales. - Implement `OnboardingLanguageScreen` to allow users to select their preferred language during the initial setup. - Add `LanguageScreen` within the settings feature to allow language changes after onboarding. - Update `UserPreferencesRepository` and `UserPreferencesDataSource` to persist language settings and onboarding status. - Integrate language selection into the main navigation flow and `MainActivity` to apply selected locales using `AppCompatDelegate`. - Add localized string resources for English and Arabic. --- .../prodReleaseRuntimeClasspath.tree.txt | 39 +++- .../prodReleaseRuntimeClasspath.txt | 1 + .../main/kotlin/org/mifospay/MainActivity.kt | 19 +- cmp-shared/build.gradle.kts | 1 + .../kotlin/org/mifospay/shared/MifosPayApp.kt | 5 +- .../org/mifospay/shared/MifosPayViewModel.kt | 17 +- .../org/mifospay/shared/di/KoinModules.kt | 2 + .../shared/navigation/MifosNavHost.kt | 7 + .../shared/navigation/RootNavGraph.kt | 12 ++ .../datastore/UserPreferencesDataSource.kt | 47 +++++ .../datastore/UserPreferencesRepository.kt | 9 + .../UserPreferencesRepositoryImpl.kt | 23 +++ .../core/designsystem/icon/MifosIcons.kt | 2 + .../org/mifospay/core/model/LanguageConfig.kt | 107 +++++++++++ feature/onboarding-language/.gitignore | 1 + feature/onboarding-language/build.gradle.kts | 36 ++++ .../src/androidMain/AndroidManifest.xml | 13 ++ .../composeResources/values-ar/strings.xml | 15 ++ .../composeResources/values/strings.xml | 15 ++ .../language/OnboardingLanguageScreen.kt | 168 ++++++++++++++++++ .../language/OnboardingLanguageViewModel.kt | 70 ++++++++ .../language/di/OnboardingLanguageModule.kt | 18 ++ .../OnboardingLanguageNavigation.kt | 32 ++++ feature/settings/build.gradle.kts | 2 + .../composeResources/values-ar/strings.xml | 32 ++++ .../composeResources/values/strings.xml | 5 + .../feature/language/LanguageScreen.kt | 139 +++++++++++++++ .../feature/language/LanguageViewModel.kt | 98 ++++++++++ .../language/navigation/LanguageNavigation.kt | 32 ++++ .../feature/settings/SettingsScreen.kt | 11 ++ .../feature/settings/SettingsViewModel.kt | 6 + .../feature/settings/di/SettingsModule.kt | 2 + .../settings/navigation/SettingsNavigation.kt | 2 + settings.gradle.kts | 1 + 34 files changed, 980 insertions(+), 9 deletions(-) create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt create mode 100644 feature/onboarding-language/.gitignore create mode 100644 feature/onboarding-language/build.gradle.kts create mode 100644 feature/onboarding-language/src/androidMain/AndroidManifest.xml create mode 100644 feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml create mode 100644 feature/onboarding-language/src/commonMain/composeResources/values/strings.xml create mode 100644 feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageScreen.kt create mode 100644 feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageViewModel.kt create mode 100644 feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/di/OnboardingLanguageModule.kt create mode 100644 feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/navigation/OnboardingLanguageNavigation.kt create mode 100644 feature/settings/src/commonMain/composeResources/values-ar/strings.xml create mode 100644 feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageScreen.kt create mode 100644 feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageViewModel.kt create mode 100644 feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/navigation/LanguageNavigation.kt diff --git a/cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt b/cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt index e69a4db19c..19f45f1552 100644 --- a/cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt +++ b/cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt @@ -2390,7 +2390,9 @@ | | +--- org.jetbrains.compose.foundation:foundation:1.9.3 (*) | | +--- org.jetbrains.compose.material3:material3:1.9.0-beta03 (*) | | +--- org.jetbrains.compose.components:components-resources:1.9.3 (*) -| | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.9.3 (*) +| | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.9.3 (*) +| | +--- project :core:datastore (*) +| | \--- project :core-base:designsystem (*) | +--- project :feature:faq | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.9.2 -> 2.9.4 (*) | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2 -> 2.9.4 (*) @@ -3201,6 +3203,41 @@ | | +--- org.jetbrains.compose.material3:material3:1.9.0-beta03 (*) | | +--- org.jetbrains.compose.components:components-resources:1.9.3 (*) | | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.9.3 (*) +| +--- project :feature:onboarding-language +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.9.2 -> 2.9.4 (*) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2 -> 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 (*) +| | +--- 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.kotlinx:kotlinx-collections-immutable:0.4.0 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0 (*) +| | +--- org.jetbrains.compose.ui:ui:1.9.3 (*) +| | +--- org.jetbrains.compose.foundation:foundation:1.9.3 (*) +| | +--- org.jetbrains.compose.material3:material3:1.9.0-beta03 (*) +| | +--- org.jetbrains.compose.components:components-resources:1.9.3 (*) +| | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.9.3 (*) +| | +--- project :core:model (*) +| | +--- project :core:datastore (*) +| | \--- project :core-base:designsystem (*) | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.21 (*) +--- project :core:data (*) +--- project :core:ui (*) diff --git a/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt b/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt index 373034aa28..a8525174bf 100644 --- a/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt +++ b/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt @@ -24,6 +24,7 @@ :feature:make-transfer :feature:merchants :feature:notification +:feature:onboarding-language :feature:payments :feature:profile :feature:qr diff --git a/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt b/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt index bc3e753a6a..aa141e4d20 100644 --- a/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt +++ b/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt @@ -10,12 +10,14 @@ package org.mifospay import android.os.Bundle -import androidx.activity.ComponentActivity 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 @@ -35,7 +37,7 @@ import org.mifospay.shared.MainUiState import org.mifospay.shared.MifosPaySharedApp import org.mifospay.shared.MifosPayViewModel -class MainActivity : ComponentActivity() { +class MainActivity : AppCompatActivity() { private val networkMonitor: NetworkMonitor by inject() private val timeZoneMonitor: TimeZoneMonitor by inject() private val viewModel: MifosPayViewModel by viewModel() @@ -57,7 +59,18 @@ class MainActivity : ComponentActivity() { 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 appLocale: LocaleListCompat = if (languageTag.isNullOrBlank()) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(languageTag) + } + AppCompatDelegate.setApplicationLocales(appLocale) + } + } .collect() } } diff --git a/cmp-shared/build.gradle.kts b/cmp-shared/build.gradle.kts index ccc1468047..3247d6f90c 100644 --- a/cmp-shared/build.gradle.kts +++ b/cmp-shared/build.gradle.kts @@ -57,6 +57,7 @@ kotlin { implementation(projects.feature.qr) implementation(projects.feature.merchants) implementation(projects.feature.upiSetup) + implementation(projects.feature.onboardingLanguage) } desktopMain.dependencies { diff --git a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt index 39ca953e30..7687b299b6 100644 --- a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt +++ b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt @@ -24,6 +24,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.MifosNavGraph.PASSCODE_GRAPH @@ -79,7 +80,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).showOnboarding) { + ONBOARDING_LANGUAGE_ROUTE + } else if ((uiState as Success).userData.authenticated) { PASSCODE_GRAPH } else { LOGIN_GRAPH diff --git a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt index bf04c86cdf..3701d6064a 100644 --- a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt +++ b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt @@ -13,10 +13,11 @@ 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.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.model.LanguageConfig import org.mifospay.core.model.user.UserInfo import proto.org.mifos.library.passcode.data.PasscodeManager @@ -24,8 +25,12 @@ class MifosPayViewModel( private val userDataRepository: UserPreferencesRepository, private val passcodeManager: PasscodeManager, ) : ViewModel() { - val uiState: StateFlow = userDataRepository.userInfo.map { - MainUiState.Success(it) + val uiState: StateFlow = combine( + userDataRepository.userInfo, + userDataRepository.language, + userDataRepository.showOnboarding, + ) { userInfo, language, showOnboarding -> + MainUiState.Success(userInfo, language, showOnboarding) }.stateIn( scope = viewModelScope, initialValue = MainUiState.Loading, @@ -42,5 +47,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 showOnboarding: Boolean, + ) : MainUiState } diff --git a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt index e6a801d58b..556121ccc9 100644 --- a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt +++ b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt @@ -33,6 +33,7 @@ import org.mifospay.feature.kyc.di.KYCModule import org.mifospay.feature.make.transfer.di.MakeTransferModule import org.mifospay.feature.merchants.di.MerchantsModule 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.qr.di.QrModule @@ -92,6 +93,7 @@ object KoinModules { QrModule, MerchantsModule, UpiSetupModule, + onboardingLanguageModule, ) } private val LibraryModule = module { diff --git a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt index d1bfafbf68..a3b82bec94 100644 --- a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt +++ b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt @@ -46,6 +46,8 @@ import org.mifospay.feature.kyc.navigation.kycLevel2Screen import org.mifospay.feature.kyc.navigation.kycLevel3Screen import org.mifospay.feature.kyc.navigation.navigateToKYCLevel2 import org.mifospay.feature.kyc.navigation.navigateToKYCLevel3 +import org.mifospay.feature.language.navigation.languageDestination +import org.mifospay.feature.language.navigation.navigateToLanguage import org.mifospay.feature.make.transfer.navigation.navigateToTransferScreen import org.mifospay.feature.make.transfer.navigation.transferScreen import org.mifospay.feature.make.transfer.success.navigateTransferSuccess @@ -184,6 +186,11 @@ internal fun MifosNavHost( navigateToEditPasswordScreen = navController::navigateToEditPassword, navigateToFaqScreen = navController::navigateToFAQ, navigateToNotificationScreen = navController::navigateToNotification, + navigateToLanguageScreen = navController::navigateToLanguage, + ) + + languageDestination( + onBackPress = navController::navigateUp, ) faqScreen( diff --git a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt index f9d833989f..493138dd1b 100644 --- a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt +++ b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt @@ -20,6 +20,8 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable 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 @@ -45,6 +47,16 @@ internal fun RootNavGraph( onShowInstanceSelector = { showInstanceSelector = true }, ) + onboardingLanguageScreen( + onNavigateToNext = { + navHostController.navigate(MifosNavGraph.LOGIN_GRAPH) { + popUpTo(ONBOARDING_LANGUAGE_ROUTE) { + inclusive = true + } + } + }, + ) + passcodeNavGraph(navHostController) composable(MifosNavGraph.MAIN_GRAPH) { diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt index bac74aeda1..43a1b95d3b 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt @@ -24,6 +24,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import org.mifospay.core.datastore.UserPreferencesDataSource.Companion.DEFAULT_ACCOUNT import org.mifospay.core.datastore.model.ClientPreferences import org.mifospay.core.datastore.model.UserInfoPreferences +import org.mifospay.core.model.LanguageConfig import org.mifospay.core.model.account.DefaultAccount import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.UpdatedClient @@ -35,6 +36,8 @@ private const val USER_INFO_KEY = "userInfo" private const val CLIENT_INFO_KEY = "clientInfo" private const val SELECTED_INSTANCE_KEY = "selectedInstance" private const val SELECTED_INTERBANK_INSTANCE_KEY = "selectedInterbankInstance" +private const val LANGUAGE_KEY = "language" +private const val SHOW_ONBOARDING_KEY = "showOnboarding" @OptIn(ExperimentalSerializationApi::class) class UserPreferencesDataSource( @@ -88,6 +91,24 @@ class UserPreferencesDataSource( ), ) + private val _language = MutableStateFlow( + settings.decodeValue( + key = LANGUAGE_KEY, + serializer = LanguageConfig.serializer(), + defaultValue = settings.decodeValueOrNull( + key = LANGUAGE_KEY, + serializer = LanguageConfig.serializer(), + ) ?: LanguageConfig.DEFAULT, + ), + ) + + private val _showOnboarding = MutableStateFlow( + settings.getBoolean( + key = SHOW_ONBOARDING_KEY, + defaultValue = true, + ), + ) + val token = _userInfo.map { it.base64EncodedAuthenticationKey } @@ -103,6 +124,10 @@ class UserPreferencesDataSource( val selectedInterbankInstance = _selectedInterbankInstance + val language = _language + + val showOnboarding = _showOnboarding + suspend fun updateClientInfo(client: Client) { withContext(dispatcher) { settings.putClientPreference(client.toClientPreferences()) @@ -174,6 +199,20 @@ class UserPreferencesDataSource( } } + suspend fun setLanguage(language: LanguageConfig) { + withContext(dispatcher) { + settings.putLanguage(language) + _language.value = language + } + } + + suspend fun setShowOnboarding(showOnboarding: Boolean) { + withContext(dispatcher) { + settings.putBoolean(SHOW_ONBOARDING_KEY, showOnboarding) + _showOnboarding.value = showOnboarding + } + } + suspend fun clearInfo() { withContext(dispatcher) { settings.clear() @@ -225,3 +264,11 @@ private fun Settings.putSelectedInterbankInstance(instance: InterbankServer) { value = instance, ) } + +private fun Settings.putLanguage(language: LanguageConfig) { + encodeValue( + key = LANGUAGE_KEY, + serializer = LanguageConfig.serializer(), + value = language, + ) +} diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt index d5dd099b23..bf1ebf4079 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt @@ -12,6 +12,7 @@ package org.mifospay.core.datastore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import org.mifospay.core.common.DataState +import org.mifospay.core.model.LanguageConfig import org.mifospay.core.model.account.DefaultAccount import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.UpdatedClient @@ -38,6 +39,10 @@ interface UserPreferencesRepository { val selectedInterbankInstance: StateFlow + val language: StateFlow + + val showOnboarding: StateFlow + suspend fun updateToken(token: String): DataState suspend fun updateUserInfo(user: UserInfo): DataState @@ -52,5 +57,9 @@ interface UserPreferencesRepository { suspend fun updateSelectedInterbankInstance(instance: InterbankServer): DataState + suspend fun setLanguage(language: LanguageConfig) + + suspend fun setShowOnboarding(showOnboarding: Boolean) + suspend fun logOut(): Unit } diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt index 237d5f27dc..128c2203ac 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import org.mifospay.core.common.DataState +import org.mifospay.core.model.LanguageConfig import org.mifospay.core.model.account.DefaultAccount import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.UpdatedClient @@ -88,6 +89,20 @@ class UserPreferencesRepositoryImpl( started = SharingStarted.Eagerly, ) + override val language: StateFlow + get() = preferenceManager.language.stateIn( + scope = unconfinedScope, + initialValue = LanguageConfig.DEFAULT, + started = SharingStarted.Eagerly, + ) + + override val showOnboarding: StateFlow + get() = preferenceManager.showOnboarding.stateIn( + scope = unconfinedScope, + initialValue = true, + started = SharingStarted.Eagerly, + ) + override suspend fun updateDefaultAccount(account: DefaultAccount): DataState { return try { val result = preferenceManager.updateDefaultAccount(account) @@ -155,6 +170,14 @@ class UserPreferencesRepositoryImpl( } } + override suspend fun setLanguage(language: LanguageConfig) { + preferenceManager.setLanguage(language) + } + + override suspend fun setShowOnboarding(showOnboarding: Boolean) { + preferenceManager.setShowOnboarding(showOnboarding) + } + override suspend fun logOut() { preferenceManager.clearInfo() } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt index 231cc45917..0bb5b76ec1 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt @@ -35,6 +35,7 @@ import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Photo @@ -151,4 +152,5 @@ object MifosIcons { val Transfer = Icons.Rounded.Money val CheckCircle = Icons.Outlined.CheckCircle val ArrowRight = Icons.AutoMirrored.Filled.KeyboardArrowRight + val Language = Icons.Default.Language } diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt new file mode 100644 index 0000000000..b55b30f1a0 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt @@ -0,0 +1,107 @@ +/* + * 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-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.model + +import kotlinx.serialization.Serializable + +@Serializable +enum class LanguageConfig( + val localName: String?, + val languageName: String, +) { + DEFAULT( + localName = null, + languageName = "System (Default)", + ), + ENGLISH( + localName = "en", + languageName = "English (English)", + ), + FRENCH( + localName = "fr", + languageName = "French (Français)", + ), + SPANISH( + localName = "es", + languageName = "Spanish (Español)", + ), + GERMAN( + localName = "de", + languageName = "German (Deutsch)", + ), + PORTUGUESE( + localName = "pt", + languageName = "Portuguese (Português)", + ), + RUSSIAN( + localName = "ru", + languageName = "Russian (Русский)", + ), + HINDI( + localName = "hi", + languageName = "Hindi (हिन्दी)", + ), + CHINESE( + localName = "zh", + languageName = "Chinese (中文)", + ), + JAPANESE( + localName = "ja", + languageName = "Japanese (日本語)", + ), + TURKISH( + localName = "tr", + languageName = "Turkish (Türkçe)", + ), + ARABIC( + localName = "ar", + languageName = "Arabic (عربى)", + ), + URDU( + localName = "ur", + languageName = "Urdu (اُردُو)", + ), + BENGALI( + localName = "bn", + languageName = "Bengali (বাঙালি)", + ), + INDONESIAN( + localName = "in", + languageName = "Indonesian (bahasa Indonesia)", + ), + KHMER( + localName = "km", + languageName = "Khmer (ភាសាខ្មែរ)", + ), + KANNADA( + localName = "kn", + languageName = "Kannada (ಕನ್ನಡ)", + ), + TELUGU( + localName = "te", + languageName = "Telugu (తెలుగు)", + ), + BURMESE( + localName = "my", + languageName = "Burmese (မြန်မာ)", + ), + POLISH( + localName = "pl", + languageName = "Polish (Polski)", + ), + SWAHILI( + localName = "sw", + languageName = "Swahili (Kiswahili)", + ), + FARSI( + localName = "fa", + languageName = "Farsi (فارسی)", + ), +} diff --git a/feature/onboarding-language/.gitignore b/feature/onboarding-language/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature/onboarding-language/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/onboarding-language/build.gradle.kts b/feature/onboarding-language/build.gradle.kts new file mode 100644 index 0000000000..a1a2c50f55 --- /dev/null +++ b/feature/onboarding-language/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * 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-wallet/blob/master/LICENSE.md + */ +plugins { + alias(libs.plugins.cmp.feature.convention) +} + +android { + namespace = "org.mifospay.feature.onboarding.language" +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + + implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.compose) + implementation(projects.core.model) + implementation(projects.core.datastore) + implementation(projects.core.designsystem) + implementation(projects.core.ui) + implementation(projects.coreBase.designsystem) + } + } +} diff --git a/feature/onboarding-language/src/androidMain/AndroidManifest.xml b/feature/onboarding-language/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..c9a5b2ec24 --- /dev/null +++ b/feature/onboarding-language/src/androidMain/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml b/feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml new file mode 100644 index 0000000000..e42f5ae8af --- /dev/null +++ b/feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml @@ -0,0 +1,15 @@ + + + + اختر لغة التطبيق + يمكنك تغيير اللغة لاحقاً من الإعدادات + تأكيد + diff --git a/feature/onboarding-language/src/commonMain/composeResources/values/strings.xml b/feature/onboarding-language/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000000..762f3a70fb --- /dev/null +++ b/feature/onboarding-language/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,15 @@ + + + + Choose your app language + Chosen language can be changed later in the settings + Submit + diff --git a/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageScreen.kt b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageScreen.kt new file mode 100644 index 0000000000..59f6a0bf6b --- /dev/null +++ b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageScreen.kt @@ -0,0 +1,168 @@ +/* + * 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-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.onboarding.language + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.RadioButton +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import mobile_wallet.feature.onboarding_language.generated.resources.Res +import mobile_wallet.feature.onboarding_language.generated.resources.feature_onboarding_choose_your_app_language +import mobile_wallet.feature.onboarding_language.generated.resources.feature_onboarding_chosen_language_can_be_changed_later_in_the_settings +import mobile_wallet.feature.onboarding_language.generated.resources.feature_onboarding_submit +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.model.LanguageConfig +import org.mifospay.core.ui.utils.EventsEffect +import template.core.base.designsystem.theme.KptTheme + +@Composable +fun OnboardingLanguageScreenRoute( + onNavigateToNext: () -> Unit, + modifier: Modifier = Modifier, + viewModel: OnboardingLanguageViewModel = koinViewModel(), +) { + val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + OnboardingLanguageEvent.NavigateToNext -> onNavigateToNext() + } + } + + OnboardingLanguageScreen( + uiState = uiState, + modifier = modifier, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} + +@Composable +fun OnboardingLanguageScreen( + uiState: OnboardingLanguageState, + modifier: Modifier = Modifier, + onAction: (OnboardingLanguageAction) -> Unit, +) { + var selectedLanguage by rememberSaveable { mutableStateOf(uiState.currentLanguage) } + + MifosScaffold( + modifier = modifier, + containerColor = KptTheme.colorScheme.background, + bottomBar = { + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(KptTheme.spacing.md), + onClick = { onAction(OnboardingLanguageAction.SetLanguage(selectedLanguage)) }, + text = { Text(text = stringResource(Res.string.feature_onboarding_submit)) }, + ) + }, + ) { contentPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + .padding(horizontal = KptTheme.spacing.md), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(KptTheme.spacing.xl)) + + Text( + text = stringResource(Res.string.feature_onboarding_choose_your_app_language), + style = KptTheme.typography.headlineMedium, + color = KptTheme.colorScheme.onSurface, + ) + + Spacer(modifier = Modifier.height(KptTheme.spacing.md)) + + Text( + text = stringResource(Res.string.feature_onboarding_chosen_language_can_be_changed_later_in_the_settings), + style = KptTheme.typography.bodySmall, + color = KptTheme.colorScheme.secondary, + ) + + Spacer(modifier = Modifier.height(KptTheme.spacing.lg)) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm), + ) { + items(LanguageConfig.entries) { language -> + LanguageSelectionItem( + language = language, + isSelected = selectedLanguage == language, + onSelect = { selectedLanguage = it }, + ) + } + } + } + } +} + +@Composable +fun LanguageSelectionItem( + language: LanguageConfig, + isSelected: Boolean, + onSelect: (LanguageConfig) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(language) } + .padding(vertical = KptTheme.spacing.sm), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = isSelected, + onClick = { onSelect(language) }, + ) + Spacer(modifier = Modifier.width(KptTheme.spacing.sm)) + Text( + text = language.languageName, + style = KptTheme.typography.bodyLarge, + color = KptTheme.colorScheme.onSurface, + ) + } +} + +@Preview +@Composable +private fun OnboardingLanguageScreenPreview() { + MifosTheme { + OnboardingLanguageScreen( + uiState = OnboardingLanguageState(LanguageConfig.DEFAULT), + onAction = {}, + ) + } +} diff --git a/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageViewModel.kt b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageViewModel.kt new file mode 100644 index 0000000000..ce37e32227 --- /dev/null +++ b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/OnboardingLanguageViewModel.kt @@ -0,0 +1,70 @@ +/* + * 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-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.onboarding.language + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.model.LanguageConfig +import org.mifospay.core.ui.utils.BaseViewModel + +class OnboardingLanguageViewModel( + private val userPreferencesRepository: UserPreferencesRepository, +) : BaseViewModel( + OnboardingLanguageState(LanguageConfig.DEFAULT), +) { + init { + userPreferencesRepository.language.map { + OnboardingLanguageAction.Internal.LoadLanguage(it) + }.onEach(::trySendAction) + .launchIn(viewModelScope) + } + + override fun handleAction(action: OnboardingLanguageAction) { + when (action) { + is OnboardingLanguageAction.Internal.LoadLanguage -> handleLoadLanguage(action) + is OnboardingLanguageAction.SetLanguage -> handleSetLanguage(action) + } + } + + private fun handleSetLanguage(action: OnboardingLanguageAction.SetLanguage) { + viewModelScope.launch { + userPreferencesRepository.setShowOnboarding(false) + userPreferencesRepository.setLanguage(action.languageConfig) + sendEvent(OnboardingLanguageEvent.NavigateToNext) + } + } + + private fun handleLoadLanguage(action: OnboardingLanguageAction.Internal.LoadLanguage) { + mutableStateFlow.update { + it.copy(currentLanguage = action.language) + } + } +} + +data class OnboardingLanguageState( + val currentLanguage: LanguageConfig, +) + +sealed interface OnboardingLanguageEvent { + data object NavigateToNext : OnboardingLanguageEvent +} + +sealed interface OnboardingLanguageAction { + data class SetLanguage(val languageConfig: LanguageConfig) : OnboardingLanguageAction + + sealed interface Internal : OnboardingLanguageAction { + data class LoadLanguage(val language: LanguageConfig) : Internal + } +} diff --git a/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/di/OnboardingLanguageModule.kt b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/di/OnboardingLanguageModule.kt new file mode 100644 index 0000000000..b03b007d5f --- /dev/null +++ b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/di/OnboardingLanguageModule.kt @@ -0,0 +1,18 @@ +/* + * 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-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.onboarding.language.di + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module +import org.mifospay.feature.onboarding.language.OnboardingLanguageViewModel + +val onboardingLanguageModule = module { + viewModelOf(::OnboardingLanguageViewModel) +} diff --git a/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/navigation/OnboardingLanguageNavigation.kt b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/navigation/OnboardingLanguageNavigation.kt new file mode 100644 index 0000000000..545b7e15be --- /dev/null +++ b/feature/onboarding-language/src/commonMain/kotlin/org/mifospay/feature/onboarding/language/navigation/OnboardingLanguageNavigation.kt @@ -0,0 +1,32 @@ +/* + * 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-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.onboarding.language.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import org.mifospay.feature.onboarding.language.OnboardingLanguageScreenRoute + +const val ONBOARDING_LANGUAGE_ROUTE = "onboarding_language_route" + +fun NavController.navigateToOnboardingLanguage(navOptions: NavOptions? = null) { + this.navigate(ONBOARDING_LANGUAGE_ROUTE, navOptions) +} + +fun NavGraphBuilder.onboardingLanguageScreen( + onNavigateToNext: () -> Unit, +) { + composable(route = ONBOARDING_LANGUAGE_ROUTE) { + OnboardingLanguageScreenRoute( + onNavigateToNext = onNavigateToNext, + ) + } +} diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 65b3f9f8a1..ec7ca3c88e 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -26,6 +26,8 @@ kotlin { implementation(libs.koin.compose.viewmodel) implementation(libs.koin.compose) + implementation(projects.core.datastore) + implementation(projects.coreBase.designsystem) } } } \ No newline at end of file diff --git a/feature/settings/src/commonMain/composeResources/values-ar/strings.xml b/feature/settings/src/commonMain/composeResources/values-ar/strings.xml new file mode 100644 index 0000000000..71121eacae --- /dev/null +++ b/feature/settings/src/commonMain/composeResources/values-ar/strings.xml @@ -0,0 +1,32 @@ + + + + هل أنت متأكد أنك تريد تعطيل الحساب؟ + إعدادات الإشعارات + الأسئلة الشائعة + الإعدادات + تغيير كلمة المرور + تغيير رمز المرور + تسجيل الخروج + تعطيل الحساب + سيؤدي هذا إلى حذف جميع المعلومات المتعلقة بحسابك + موافق + إلغاء + هل تريد تسجيل الخروج؟ + + نعم + لا + تغيير اللغة + اللغة + اختر لغة التطبيق + يمكن تغيير اللغة المختارة لاحقاً في الإعدادات + تأكيد + diff --git a/feature/settings/src/commonMain/composeResources/values/strings.xml b/feature/settings/src/commonMain/composeResources/values/strings.xml index 1cc87fe9e3..afbab66c3f 100644 --- a/feature/settings/src/commonMain/composeResources/values/strings.xml +++ b/feature/settings/src/commonMain/composeResources/values/strings.xml @@ -24,4 +24,9 @@ Yes No + Change Language + Language + Choose your app language + Chosen language can be changed later in the settings + Submit \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageScreen.kt new file mode 100644 index 0000000000..d78e2846f4 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageScreen.kt @@ -0,0 +1,139 @@ +/* + * 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-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.language + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import mobile_wallet.feature.settings.generated.resources.Res +import mobile_wallet.feature.settings.generated.resources.feature_settings_change_language +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.MifosTopBar +import org.mifospay.core.model.LanguageConfig +import org.mifospay.core.ui.utils.EventsEffect +import template.core.base.designsystem.theme.KptTheme + +@Composable +fun LanguageScreenRoute( + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: LanguageViewModel = koinViewModel(), +) { + val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + LanguageEvent.NavigateBack -> onNavigateBack() + } + } + + LanguageScreen( + uiState = uiState, + modifier = modifier, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} + +@Composable +fun LanguageScreen( + uiState: LanguageState, + onAction: (LanguageAction) -> Unit, + modifier: Modifier = Modifier, +) { + MifosScaffold( + modifier = modifier, + topBar = { + MifosTopBar( + topBarTitle = stringResource(Res.string.feature_settings_change_language), + backPress = { onAction(LanguageAction.OnNavigateBack) }, + ) + }, + bottomBar = { + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(KptTheme.spacing.md), + onClick = { onAction(LanguageAction.SetLanguage(uiState.selectedLanguage)) }, + text = { Text(text = stringResource(Res.string.feature_settings_change_language)) }, + ) + }, + ) { contentPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + .padding(horizontal = KptTheme.spacing.md), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(32.dp)) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm), + ) { + items(LanguageConfig.entries) { language -> + LanguageItem( + language = language, + isSelected = language == uiState.selectedLanguage, + onLanguageSelected = { onAction(LanguageAction.LanguageSelected(language)) }, + ) + } + } + } + } +} + +@Composable +fun LanguageItem( + language: LanguageConfig, + isSelected: Boolean, + onLanguageSelected: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onLanguageSelected) + .padding(vertical = KptTheme.spacing.sm), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = isSelected, + onClick = onLanguageSelected, + ) + Spacer(modifier = Modifier.width(KptTheme.spacing.sm)) + Text( + text = language.languageName, + style = KptTheme.typography.bodyLarge, + color = KptTheme.colorScheme.onSurface, + ) + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageViewModel.kt new file mode 100644 index 0000000000..4eec358e1b --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/LanguageViewModel.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.language + +/* + * 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-wallet/blob/master/LICENSE.md + */ + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.model.LanguageConfig +import org.mifospay.core.ui.utils.BaseViewModel + +class LanguageViewModel( + private val userPreferencesRepository: UserPreferencesRepository, +) : BaseViewModel( + LanguageState( + currentLanguage = LanguageConfig.DEFAULT, + selectedLanguage = LanguageConfig.DEFAULT, + ), +) { + init { + userPreferencesRepository.language.map { + LanguageAction.Internal.LoadLanguage(it) + }.onEach(::trySendAction) + .launchIn(viewModelScope) + } + + override fun handleAction(action: LanguageAction) { + when (action) { + is LanguageAction.OnNavigateBack -> sendEvent(LanguageEvent.NavigateBack) + is LanguageAction.LanguageSelected -> handleLanguageSelection(action.language) + is LanguageAction.SetLanguage -> handleSetLanguage(action) + is LanguageAction.Internal.LoadLanguage -> handleLoadLanguage(action) + } + } + + private fun handleLanguageSelection(language: LanguageConfig) { + viewModelScope.launch { + mutableStateFlow.update { + it.copy(selectedLanguage = language) + } + } + } + + private fun handleSetLanguage(action: LanguageAction.SetLanguage) { + viewModelScope.launch { + userPreferencesRepository.setLanguage(action.languageConfig) + sendEvent(LanguageEvent.NavigateBack) + } + } + + private fun handleLoadLanguage(action: LanguageAction.Internal.LoadLanguage) { + mutableStateFlow.update { + it.copy( + currentLanguage = action.language, + selectedLanguage = action.language, + ) + } + } +} + +data class LanguageState( + val selectedLanguage: LanguageConfig, + val currentLanguage: LanguageConfig, +) + +sealed interface LanguageEvent { + data object NavigateBack : LanguageEvent +} + +sealed interface LanguageAction { + data object OnNavigateBack : LanguageAction + data class LanguageSelected(val language: LanguageConfig) : LanguageAction + data class SetLanguage(val languageConfig: LanguageConfig) : LanguageAction + + sealed interface Internal : LanguageAction { + data class LoadLanguage(val language: LanguageConfig) : Internal + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/navigation/LanguageNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/navigation/LanguageNavigation.kt new file mode 100644 index 0000000000..77fe7bc974 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/language/navigation/LanguageNavigation.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.language.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import org.mifospay.core.ui.composableWithSlideTransitions +import org.mifospay.feature.language.LanguageScreenRoute + +const val LANGUAGE_ROUTE = "language_route" + +fun NavController.navigateToLanguage(navOptions: NavOptions? = null) { + this.navigate(LANGUAGE_ROUTE, navOptions) +} + +fun NavGraphBuilder.languageDestination( + onBackPress: () -> Unit, +) { + composableWithSlideTransitions(route = LANGUAGE_ROUTE) { + LanguageScreenRoute( + onNavigateBack = onBackPress, + ) + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt index 6e1bfcbd33..fb45142542 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.compose.collectAsStateWithLifecycle import mobile_wallet.feature.settings.generated.resources.Res +import mobile_wallet.feature.settings.generated.resources.feature_settings_change_language import mobile_wallet.feature.settings.generated.resources.feature_settings_change_passcode import mobile_wallet.feature.settings.generated.resources.feature_settings_change_password import mobile_wallet.feature.settings.generated.resources.feature_settings_disable_account @@ -59,6 +60,7 @@ internal fun SettingsScreenRoute( onChangePasscode: () -> Unit, navigateToFaqScreen: () -> Unit, navigateToNotificationScreen: () -> Unit, + navigateToLanguageScreen: () -> Unit, modifier: Modifier = Modifier, viewmodel: SettingsViewModel = koinViewModel(), ) { @@ -72,6 +74,7 @@ internal fun SettingsScreenRoute( SettingsEvent.OnNavigateToFaqScreen -> navigateToFaqScreen.invoke() SettingsEvent.OnNavigateToLogout -> onLogout.invoke() SettingsEvent.OnNavigateToNotificationScreen -> navigateToNotificationScreen.invoke() + SettingsEvent.OnNavigateToLanguageScreen -> navigateToLanguageScreen.invoke() } } @@ -142,6 +145,14 @@ private fun SettingsScreenContent( }, ) + SettingsCardItem( + title = stringResource(Res.string.feature_settings_change_language), + icon = MifosIcons.Language, + onClick = { + onAction(SettingsAction.ChangeLanguage) + }, + ) + SettingsCardItem( title = stringResource(Res.string.feature_settings_log_out), icon = vectorResource(Res.drawable.outline_logout), diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt index f216923fcd..51369e4e24 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt @@ -61,6 +61,10 @@ class SettingsViewModel( sendEvent(SettingsEvent.OnNavigateToNotificationScreen) } + is SettingsAction.ChangeLanguage -> { + sendEvent(SettingsEvent.OnNavigateToLanguageScreen) + } + is SettingsAction.DismissDialog -> { mutableStateFlow.update { it.copy(dialogState = null) @@ -170,6 +174,7 @@ sealed interface SettingsEvent { data object OnNavigateToLogout : SettingsEvent data object OnNavigateToFaqScreen : SettingsEvent data object OnNavigateToNotificationScreen : SettingsEvent + data object OnNavigateToLanguageScreen : SettingsEvent } sealed interface SettingsAction { @@ -180,6 +185,7 @@ sealed interface SettingsAction { data object ChangePassword : SettingsAction data object NavigateToFaqScreen : SettingsAction data object NavigateToNotificationSettings : SettingsAction + data object ChangeLanguage : SettingsAction data object DismissDialog : SettingsAction sealed interface Internal : SettingsAction { diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/di/SettingsModule.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/di/SettingsModule.kt index a368a37c3b..d0cc9711eb 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/di/SettingsModule.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/di/SettingsModule.kt @@ -11,8 +11,10 @@ package org.mifospay.feature.settings.di import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module +import org.mifospay.feature.language.LanguageViewModel import org.mifospay.feature.settings.SettingsViewModel val SettingsModule = module { viewModelOf(::SettingsViewModel) + viewModelOf(::LanguageViewModel) } diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt index 281cf082a2..600647e6a9 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt @@ -28,6 +28,7 @@ fun NavGraphBuilder.settingsScreen( navigateToEditPasswordScreen: () -> Unit, navigateToFaqScreen: () -> Unit, navigateToNotificationScreen: () -> Unit, + navigateToLanguageScreen: () -> Unit, ) { composableWithSlideTransitions(route = SETTINGS_ROUTE) { SettingsScreenRoute( @@ -37,6 +38,7 @@ fun NavGraphBuilder.settingsScreen( onChangePasscode = onChangePasscode, navigateToFaqScreen = navigateToFaqScreen, navigateToNotificationScreen = navigateToNotificationScreen, + navigateToLanguageScreen = navigateToLanguageScreen, ) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6254e59dcc..de1951f139 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -86,3 +86,4 @@ include(":feature:upi-setup") include(":feature:qr") include(":libs:mifos-passcode") +include(":feature:onboarding-language") From be8ec220b43c6615355f96f74424661f958ef5b4 Mon Sep 17 00:00:00 2001 From: mena Date: Thu, 26 Feb 2026 12:31:08 +0200 Subject: [PATCH 02/17] feat: enhance onboarding language selection and persistence - Improve `OnboardingLanguageScreen` by adding a snackbar for error handling and migrating selection state to the ViewModel. - Update `OnboardingLanguageViewModel` to handle language selection, error states, and atomic preference updates. - Refactor `UserPreferencesDataSource.clearInfo()` to preserve language, onboarding status, and instance settings while clearing user-specific data. - Optimize `MainActivity` to only update application locales when the requested language differs from the current configuration. - Update `LanguageConfig` to use the correct ISO code (`id`) for Indonesian. - Integrate `composableWithSlideTransitions` for onboarding language navigation. - Add localized error strings and update copyright headers to 2026. --- .../main/kotlin/org/mifospay/MainActivity.kt | 22 +++++++++--- .../datastore/UserPreferencesDataSource.kt | 15 ++++++++ .../org/mifospay/core/model/LanguageConfig.kt | 4 +-- .../composeResources/values-ar/strings.xml | 1 + .../composeResources/values/strings.xml | 3 +- .../language/OnboardingLanguageScreen.kt | 23 ++++++++---- .../language/OnboardingLanguageViewModel.kt | 36 ++++++++++++++++--- .../OnboardingLanguageNavigation.kt | 6 ++-- .../composeResources/values-ar/strings.xml | 2 +- .../composeResources/values/strings.xml | 1 - .../feature/language/LanguageScreen.kt | 5 ++- .../feature/language/LanguageViewModel.kt | 10 ------ 12 files changed, 91 insertions(+), 37 deletions(-) diff --git a/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt b/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt index aa141e4d20..053964900b 100644 --- a/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt +++ b/cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt @@ -63,12 +63,26 @@ class MainActivity : AppCompatActivity() { uiState = state if (state is MainUiState.Success) { val languageTag = state.language.localName - val appLocale: LocaleListCompat = if (languageTag.isNullOrBlank()) { - LocaleListCompat.getEmptyLocaleList() + val currentAppLocales = AppCompatDelegate.getApplicationLocales() + + // Only set locales if the requested language is different from the current app locales + val isRequestedDefault = languageTag.isNullOrBlank() + val isCurrentDefault = currentAppLocales.isEmpty + + val shouldUpdate = if (isRequestedDefault) { + !isCurrentDefault } else { - LocaleListCompat.forLanguageTags(languageTag) + languageTag != currentAppLocales.toLanguageTags() + } + + if (shouldUpdate) { + val appLocale: LocaleListCompat = if (isRequestedDefault) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(languageTag) + } + AppCompatDelegate.setApplicationLocales(appLocale) } - AppCompatDelegate.setApplicationLocales(appLocale) } } .collect() diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt index 5bd7d5ab47..be7e43f129 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt @@ -238,7 +238,22 @@ class UserPreferencesDataSource( suspend fun clearInfo() { withContext(dispatcher) { + val currentLanguage = _language.value + val currentShowOnboarding = _showOnboarding.value + val currentSelectedInstance = _selectedInstance.value + val currentSelectedInterbankInstance = _selectedInterbankInstance.value + settings.clear() + + settings.putLanguage(currentLanguage) + settings.putBoolean(SHOW_ONBOARDING_KEY, currentShowOnboarding) + currentSelectedInstance?.let { settings.putSelectedInstance(it) } + currentSelectedInterbankInstance?.let { settings.putSelectedInterbankInstance(it) } + + _userInfo.value = UserInfoPreferences.DEFAULT + _clientInfo.value = ClientPreferences.DEFAULT + _defaultAccount.value = DefaultAccount.DEFAULT + _accountExternalIds.value = emptyMap() } } diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt index b55b30f1a0..5f62f9c3db 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/LanguageConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2026 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -73,7 +73,7 @@ enum class LanguageConfig( languageName = "Bengali (বাঙালি)", ), INDONESIAN( - localName = "in", + localName = "id", languageName = "Indonesian (bahasa Indonesia)", ), KHMER( diff --git a/feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml b/feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml index e42f5ae8af..33e4ce332a 100644 --- a/feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml +++ b/feature/onboarding-language/src/commonMain/composeResources/values-ar/strings.xml @@ -12,4 +12,5 @@ اختر لغة التطبيق يمكنك تغيير اللغة لاحقاً من الإعدادات تأكيد + فشل حفظ الإعدادات. يرجى المحاولة مرة أخرى. diff --git a/feature/onboarding-language/src/commonMain/composeResources/values/strings.xml b/feature/onboarding-language/src/commonMain/composeResources/values/strings.xml index 762f3a70fb..e483955360 100644 --- a/feature/onboarding-language/src/commonMain/composeResources/values/strings.xml +++ b/feature/onboarding-language/src/commonMain/composeResources/values/strings.xml @@ -1,6 +1,6 @@