diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/DataState.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/DataState.kt new file mode 100644 index 000000000..474ac98d4 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/DataState.kt @@ -0,0 +1,30 @@ +/* + * 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.datastore + +// Should be deleted once common module is migrated +sealed class DataState { + abstract val data: T? + + data object Loading : DataState() { + override val data: Nothing? get() = null + } + + data class Success( + override val data: T, + ) : DataState() + + data class Error( + val exception: Throwable, + override val data: T? = null, + ) : DataState() { + val message = exception.message.toString() + } +} diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/PreferenceHelper.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/PreferenceHelper.kt new file mode 100644 index 000000000..a658c9d93 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/PreferenceHelper.kt @@ -0,0 +1,105 @@ +/* + * 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.datastore + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import org.mifos.mobile.core.datastore.model.AppSettings +import org.mifos.mobile.core.datastore.model.AppTheme +import org.mifos.mobile.core.datastore.model.UserData + +class PreferenceHelper( + private val preferenceManager: UserPreferenceDataSource, + unconfinedDispatcher: CoroutineDispatcher, +) : UserPreferencesRepository { + private val unconfinedScope = CoroutineScope(unconfinedDispatcher) + + override val userInfo: Flow + get() = preferenceManager.userInfo + + override val settingsInfo: Flow + get() = preferenceManager.settingsInfo + + override val appTheme: StateFlow + get() = preferenceManager.appTheme.stateIn( + scope = unconfinedScope, + initialValue = null, + started = SharingStarted.Eagerly, + ) + override val token: StateFlow + get() = preferenceManager.token.stateIn( + scope = unconfinedScope, + initialValue = null, + started = SharingStarted.Eagerly, + ) + + override val clientId: StateFlow + get() = preferenceManager.clientId.stateIn( + scope = unconfinedScope, + initialValue = null, + started = SharingStarted.Eagerly, + ) + + override val profileImage: String? + get() = preferenceManager.getProfileImage() + + override suspend fun updateToken(token: String): DataState { + return try { + val result = preferenceManager.updateToken(token) + DataState.Success(result) + } catch (e: Exception) { + DataState.Error(e) + } + } + + override suspend fun updateTheme(theme: AppTheme): DataState { + return try { + val result = preferenceManager.updateTheme(theme) + DataState.Success(result) + } catch (e: Exception) { + DataState.Error(e) + } + } + + override suspend fun updateUser(user: UserData): DataState { + return try { + val result = preferenceManager.updateUserInfo(user) + DataState.Success(result) + } catch (e: Exception) { + DataState.Error(e) + } + } + + override suspend fun updateSettings(appSettings: AppSettings): DataState { + return try { + val result = preferenceManager.updateSettingsInfo(appSettings) + DataState.Success(result) + } catch (e: Exception) { + DataState.Error(e) + } + } + + override suspend fun updateProfileImage(image: String): DataState { + return try { + val result = preferenceManager.updateProfileImage(image) + DataState.Success(result) + } catch (e: Exception) { + DataState.Error(e) + } + } + + override suspend fun logOut() { + preferenceManager.clearInfo() + } +} diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/PreferencesHelper.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/PreferencesHelper.kt deleted file mode 100644 index e9234b22b..000000000 --- a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/PreferencesHelper.kt +++ /dev/null @@ -1,220 +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 org.mifos.mobile.core.datastore - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.Settings -import com.russhwolf.settings.get -import com.russhwolf.settings.set -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import org.mifos.mobile.core.datastore.model.AppSettings -import org.mifos.mobile.core.datastore.model.AppTheme -import org.mifos.mobile.core.datastore.model.MifosAppLanguage -import org.mifos.mobile.core.datastore.model.UserData - -class PreferencesHelper(private val settings: Settings) { - - fun clear() { - val keysToPreserve = setOf(BASE_URL, TENANT) - settings.keys.filter { it !in keysToPreserve } - .forEach { key -> - settings.remove(key) - } - } - - fun saveToken(token: String) { - settings.putString(TOKEN, token) - } - - fun clearToken() { - settings.putString(TOKEN, "") - } - - private val token: String? - get() = settings.getString(TOKEN, "") - - var isAuthenticated: Boolean = false - get() = !token.isNullOrEmpty() - - var userId: Long - get() = settings.getLong(USER_ID, -1) - set(value) { - settings.putLong(USER_ID, value) - } - - var userName: String - get() = settings.getString(USER_NAME, "") - set(value) { - settings.putString(USER_NAME, value) - } - - var tenant: String - get() = settings.getString(TENANT, DEFAULT_TENANT) - set(value) { - settings.putString(TENANT, value) - } - - var passcode: String - get() = settings.getString(PASSCODE, "") - set(value) { - settings.putString(PASSCODE, value) - } - - var clientId: Long - get() = settings.getLong(CLIENT_ID, -1) - set(value) { - settings.putLong(CLIENT_ID, value) - } - - var clientName: String - get() = settings.getString(CLIENT_NAME, "") - set(value) { - settings.putString(CLIENT_NAME, value) - } - - var officeName: String - get() = settings.getString(OFFICE_NAME, "") - set(value) { - settings.putString(OFFICE_NAME, value) - } - - fun setOverviewState(state: Boolean) { - settings.putBoolean(OVERVIEW_STATE, state) - } - - fun overviewState(): Boolean = settings.getBoolean(OVERVIEW_STATE, true) - - fun saveGcmToken(token: String) { - settings.putString(GCM_TOKEN, token) - } - - var userProfileImage: String - get() = settings.getString(PROFILE_IMAGE, null.toString()) - set(value) { - settings.putString(PROFILE_IMAGE, value) - } - - val gcmToken: String - get() = settings.getString(GCM_TOKEN, "") - - fun setSentTokenToServer(state: Boolean) { - settings.putBoolean(SENT_TOKEN_TO_SERVER, state) - } - - fun sentTokenToServerState(): Boolean = settings.getBoolean(SENT_TOKEN_TO_SERVER, false) - - fun updateConfiguration(baseUrl: String, tenant: String) { - settings.apply { - putString(BASE_URL, baseUrl) - putString(TENANT, tenant) - } - } - - val baseUrl: String - get() = settings.getString(BASE_URL, DEFAULT_BASE_URL) - - var appTheme: AppTheme - get() = AppTheme.fromOrdinal(settings.getInt(APPLICATION_THEME, AppTheme.SYSTEM.ordinal)) - set(value) { - settings.putInt(APPLICATION_THEME, value.ordinal) - } - - var language: String - get() = settings.getString(LANGUAGE_TYPE, MifosAppLanguage.ENGLISH.code) - ?: MifosAppLanguage.SYSTEM_LANGUAGE.code - set(value) { - settings.putString(LANGUAGE_TYPE, value) - } - - var isDefaultSystemLanguage: Boolean - get() = settings.getBoolean(DEFAULT_SYSTEM_LANGUAGE, false) - set(value) { - settings.putBoolean(DEFAULT_SYSTEM_LANGUAGE, value) - } - - fun getUserData(): UserData { - return UserData( - clientId = clientId, - userName = userName, - isAuthenticated = isAuthenticated, - userId = userId, - ) - } - - fun saveUserData(userData: UserData) { - clientId = userData.clientId - userName = userData.userName - isAuthenticated = userData.isAuthenticated - } - - fun getAppSettings(): AppSettings { - return AppSettings( - tenant = tenant, - baseUrl = baseUrl, - passcode = passcode, - appTheme = appTheme, - ) - } - - fun saveAppSettings(appSettings: AppSettings) { - settings.putString(TENANT, appSettings.tenant) - settings.putString(BASE_URL, appSettings.baseUrl) - appSettings.passcode?.let { settings.putString(PASSCODE, it) } - settings.putInt(APPLICATION_THEME, appSettings.appTheme.ordinal) - } - - companion object { - private const val USER_ID = "preferences_user_id" - private const val TOKEN = "preferences_token" - private const val CLIENT_ID = "preferences_client" - private const val OFFICE_NAME = "preferences_office_name" - private const val USER_NAME = "preferences_user_name" - const val PASSCODE = "preferences_passcode" - private const val OVERVIEW_STATE = "preferences_overview_state" - private const val SENT_TOKEN_TO_SERVER = "sentTokenToServer" - private const val GCM_TOKEN = "gcm_token" - const val TENANT = "preferences_base_tenant" - const val BASE_URL = "preferences_base_url_key" - private const val PROFILE_IMAGE = "preferences_profile_image" - const val CLIENT_NAME = "client_name" - const val APPLICATION_THEME = "application_theme" - const val LANGUAGE_TYPE = "language_type" - const val DEFAULT_SYSTEM_LANGUAGE = "default_system_language" - - private const val DEFAULT_TENANT = "default" - private const val DEFAULT_BASE_URL = "https://demo.mifos.community" - } - - fun getStringFlowForKey(keyForString: String, settings: ObservableSettings): Flow = callbackFlow { - trySend(settings.getStringOrNull(keyForString)) - - val listener = settings.addStringOrNullListener(keyForString) { newValue -> - trySend(newValue) - } - - awaitClose { - listener.deactivate() - } - } - - fun getIntFlowForKey(keyForInt: String, settings: ObservableSettings): Flow = callbackFlow { - trySend(settings.getIntOrNull(keyForInt)) - - val listener = settings.addIntOrNullListener(keyForInt) { newValue -> - trySend(newValue) - } - - awaitClose { - listener.deactivate() - } - } -} diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/UserPreferenceDataSource.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/UserPreferenceDataSource.kt new file mode 100644 index 000000000..d696ec112 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/UserPreferenceDataSource.kt @@ -0,0 +1,144 @@ +/* + * 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:OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) + +package org.mifos.mobile.core.datastore + +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.Settings +import com.russhwolf.settings.serialization.decodeValue +import com.russhwolf.settings.serialization.decodeValueOrNull +import com.russhwolf.settings.serialization.encodeValue +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import org.mifos.mobile.core.datastore.model.AppSettings +import org.mifos.mobile.core.datastore.model.AppTheme +import org.mifos.mobile.core.datastore.model.UserData + +private const val USER_DATA = "userData" +private const val APP_SETTINGS = "appSettings" + +class UserPreferenceDataSource( + private val settings: Settings, + private val dispatcher: CoroutineDispatcher, +) { + + private val _userInfo = MutableStateFlow( + settings.decodeValue( + key = USER_DATA, + serializer = UserData.serializer(), + defaultValue = settings.decodeValueOrNull( + key = USER_DATA, + serializer = UserData.serializer(), + ) ?: UserData.DEFAULT, + ), + ) + + private val _settingsInfo = MutableStateFlow( + settings.decodeValue( + key = APP_SETTINGS, + serializer = AppSettings.serializer(), + defaultValue = settings.decodeValueOrNull( + key = APP_SETTINGS, + serializer = AppSettings.serializer(), + ) ?: AppSettings.DEFAULT, + ), + ) + + val token = _userInfo.map { + it.base64EncodedAuthenticationKey + } + + val userInfo = _userInfo + + val settingsInfo = _settingsInfo + + val clientId = _userInfo.map { it.clientId } + + val appTheme = _settingsInfo.map { it.appTheme } + + suspend fun updateSettingsInfo(appSettings: AppSettings) { + withContext(dispatcher) { + settings.putSettingsPreference(appSettings) + _settingsInfo.value = appSettings + } + } + + suspend fun updateUserInfo(user: UserData) { + withContext(dispatcher) { + settings.putUserPreference(user) + _userInfo.value = user + } + } + + suspend fun updateToken(token: String) { + withContext(dispatcher) { + settings.putUserPreference( + UserData.DEFAULT.copy( + base64EncodedAuthenticationKey = token, + ), + ) + _userInfo.value = UserData.DEFAULT.copy( + base64EncodedAuthenticationKey = token, + ) + } + } + + suspend fun updateTheme(theme: AppTheme) { + withContext(dispatcher) { + settings.putSettingsPreference( + AppSettings.DEFAULT.copy( + appTheme = theme, + ), + ) + _settingsInfo.value = AppSettings.DEFAULT.copy( + appTheme = theme, + ) + } + } + + fun updateProfileImage(image: String) { + settings.putString(PROFILE_IMAGE, image) + } + + fun getProfileImage(): String? { + return settings.getString(PROFILE_IMAGE, "").ifEmpty { null } + } + + suspend fun clearInfo() { + withContext(dispatcher) { + settings.clear() + } + } + + companion object { + private const val PROFILE_IMAGE = "preferences_profile_image" + } +} + +@OptIn(ExperimentalSerializationApi::class) +private fun Settings.putUserPreference(user: UserData) { + encodeValue( + key = USER_DATA, + serializer = UserData.serializer(), + value = user, + ) +} + +private fun Settings.putSettingsPreference(settings: AppSettings) { + encodeValue( + key = APP_SETTINGS, + serializer = AppSettings.serializer(), + value = settings, + ) +} diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/UserPreferencesRepository.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/UserPreferencesRepository.kt new file mode 100644 index 000000000..af03136e2 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/UserPreferencesRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.datastore + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import org.mifos.mobile.core.datastore.model.AppSettings +import org.mifos.mobile.core.datastore.model.AppTheme +import org.mifos.mobile.core.datastore.model.UserData + +interface UserPreferencesRepository { + val userInfo: Flow + + val settingsInfo: Flow + + val token: StateFlow + + val clientId: StateFlow + + val appTheme: StateFlow + + val profileImage: String? + + suspend fun updateToken(token: String): DataState + + suspend fun updateTheme(theme: AppTheme): DataState + + suspend fun updateUser(user: UserData): DataState + + suspend fun updateSettings(appSettings: AppSettings): DataState + + suspend fun updateProfileImage(image: String): DataState + + suspend fun logOut(): Unit +} diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppSettings.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppSettings.kt index dba970599..737ed9e11 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppSettings.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppSettings.kt @@ -9,6 +9,9 @@ */ package org.mifos.mobile.core.datastore.model +import kotlinx.serialization.Serializable + +@Serializable data class AppSettings( val tenant: String, val baseUrl: String, @@ -16,9 +19,9 @@ data class AppSettings( val appTheme: AppTheme = AppTheme.SYSTEM, ) { companion object { - fun default() = AppSettings( - tenant = "default_tenant", - baseUrl = "https://default.url", + val DEFAULT = AppSettings( + tenant = "default", + baseUrl = "https://demo.mifos.community", appTheme = AppTheme.SYSTEM, ) } diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppTheme.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppTheme.kt index b65b7fe47..c2e1ee93f 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppTheme.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/AppTheme.kt @@ -15,11 +15,4 @@ enum class AppTheme( SYSTEM(themeName = "System Theme"), LIGHT(themeName = "Light Theme"), DARK(themeName = "Dark Theme"), - ; - - companion object { - fun fromOrdinal(ordinal: Int): AppTheme { - return entries.firstOrNull { it.ordinal == ordinal } ?: SYSTEM - } - } } diff --git a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/UserData.kt b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/UserData.kt index acb73bd1c..59123f47d 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/UserData.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifos/mobile/core/datastore/model/UserData.kt @@ -9,9 +9,23 @@ */ package org.mifos.mobile.core.datastore.model +import kotlinx.serialization.Serializable + +@Serializable data class UserData( - val isAuthenticated: Boolean, + val userId: Long, val userName: String, val clientId: Long, - val userId: Long, -) + val isAuthenticated: Boolean, + val base64EncodedAuthenticationKey: String, +) { + companion object { + val DEFAULT = UserData( + userId = -1, + userName = "", + clientId = -1, + isAuthenticated = false, + base64EncodedAuthenticationKey = "", + ) + } +}