Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Migrate :core:datastore to KMP #2306

Open
wants to merge 13 commits into
base: kmp-impl
Choose a base branch
from
30 changes: 17 additions & 13 deletions core/datastore/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
plugins {
alias(libs.plugins.mifos.android.library)
alias(libs.plugins.mifos.android.library.jacoco)
alias(libs.plugins.mifos.android.hilt)
alias(libs.plugins.mifos.kmp.library)
alias(libs.plugins.kotlin.serialization)

}

android {
Expand All @@ -26,15 +26,19 @@ android {
}
}

dependencies {
api(projects.core.model)
api(projects.core.common)

api(libs.converter.gson)
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.multiplatform.settings)
implementation(libs.multiplatform.settings.serialization)
implementation(libs.multiplatform.settings.coroutines)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.core)
// implementation(projects.core.common)
api(projects.core.model)
api(projects.core.common)

// fineract sdk dependencies
api(libs.mifos.android.sdk.arch)

// sdk client
api(libs.fineract.client)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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/android-client/blob/master/LICENSE.md
*/
@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)

package com.mifos.core.datastore

import com.mifos.core.model.objects.ServerConfig
import com.mifos.core.model.objects.UserData
import com.mifos.core.model.objects.users.User
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.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi

private const val USER_DATA = "userData"
private const val AUTH_USER = "user_details"
private const val SERVER_CONFIG_KEY = "server_config"
private const val USER_STATUS = "user_status"
private const val AUTH_USERNAME = "auth_username"
private const val AUTH_PASSWORD = "auth_password"

@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
class UserPreferencesDataSource(
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,
),
)
val userInfo = _userInfo.asStateFlow()

private val _userData = MutableStateFlow(
settings.decodeValue(
key = AUTH_USER,
serializer = User.serializer(),
defaultValue = settings.decodeValueOrNull(
key = AUTH_USER,
serializer = User.serializer(),
) ?: User(),
),
)
val userData = _userData.asStateFlow()

private val _serverConfig = MutableStateFlow(
settings.decodeValue(
key = SERVER_CONFIG_KEY,
serializer = ServerConfig.serializer(),
defaultValue = settings.decodeValueOrNull(
key = SERVER_CONFIG_KEY,
serializer = ServerConfig.serializer(),
) ?: ServerConfig.DEFAULT,
),
)
val serverConfig = _serverConfig.asStateFlow()

val userStatus: Boolean
get() = settings.getBoolean(USER_STATUS, false)

var usernamePassword: Pair<String, String>
get() = Pair(
settings.getString(AUTH_USERNAME, "") ?: "",
settings.getString(AUTH_PASSWORD, "") ?: "",
)
set(value) {
settings.putString(AUTH_USERNAME, value.first)
settings.putString(AUTH_PASSWORD, value.second)
}

val isAuthenticated: Boolean
get() = _userData.value.isAuthenticated == true

val token: String
get() = _userData.value.base64EncodedAuthenticationKey?.let { "Basic $it" } ?: ""

fun updateUserStatus(status: Boolean) {
settings.putBoolean(USER_STATUS, status)
}

suspend fun updateUserInfo(user: UserData) {
withContext(dispatcher) {
settings.putUserPreference(user)
_userInfo.value = user
}
}

suspend fun updateUser(user: User) {
withContext(dispatcher) {
settings.putAuth(user)
_userData.value = user
}
}

suspend fun updateServerConfig(serverConfig: ServerConfig) {
withContext(dispatcher) {
settings.putServerConfig(serverConfig)
_serverConfig.value = serverConfig
}
}

suspend fun clearInfo() {
withContext(dispatcher) {
settings.remove(AUTH_USER)
}
}
}

@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
private fun Settings.putUserPreference(user: UserData) {
encodeValue(
key = USER_DATA,
serializer = UserData.serializer(),
value = user,
)
}

@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
private fun Settings.putAuth(user: User) {
encodeValue(
key = AUTH_USER,
serializer = User.serializer(),
value = user,
)
}

@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
private fun Settings.putServerConfig(serverConfig: ServerConfig) {
encodeValue(
key = SERVER_CONFIG_KEY,
serializer = ServerConfig.serializer(),
value = serverConfig,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.datastore

import com.mifos.core.model.objects.ServerConfig
import com.mifos.core.model.objects.UserData
import com.mifos.core.model.objects.users.User
import kotlinx.coroutines.flow.Flow

interface UserPreferencesRepository {
val userInfo: Flow<UserData>
val userData: Flow<User>
val serverConfig: Flow<ServerConfig>

suspend fun updateUser(user: UserData): Result<Unit>

suspend fun logOut(): Unit

suspend fun updateServerConfig(serverConfig: ServerConfig): Result<Unit>

suspend fun updateUserInfo(user: UserData): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.datastore

import com.mifos.core.model.objects.ServerConfig
import com.mifos.core.model.objects.UserData
import com.mifos.core.model.objects.users.User
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext

class UserPreferencesRepositoryImpl(
private val preferenceManager: UserPreferencesDataSource,
private val ioDispatcher: CoroutineDispatcher,
unconfinedDispatcher: CoroutineDispatcher,
) : UserPreferencesRepository {

private val unconfinedScope = CoroutineScope(unconfinedDispatcher)

override val userInfo: Flow<UserData>
get() = preferenceManager.userInfo

override val userData: Flow<User>
get() = preferenceManager.userData

override val serverConfig: Flow<ServerConfig>
get() = preferenceManager.serverConfig

// Implementing updateUserInfo as required by the interface
override suspend fun updateUserInfo(user: UserData): Result<Unit> {
return withContext(ioDispatcher) {
try {
preferenceManager.updateUserInfo(user)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}

// Implementing updateUser as required by the interface
override suspend fun updateUser(user: UserData): Result<Unit> {
return withContext(ioDispatcher) {
try {
preferenceManager.updateUserInfo(user)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}

// Implementing updateServerConfig correctly
override suspend fun updateServerConfig(serverConfig: ServerConfig): Result<Unit> {
return withContext(ioDispatcher) {
try {
preferenceManager.updateServerConfig(serverConfig)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}

override suspend fun logOut() {
withContext(ioDispatcher) {
preferenceManager.clearInfo()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.datastore.di

import com.mifos.core.datastore.UserPreferencesDataSource
import com.mifos.core.datastore.UserPreferencesRepository
import com.mifos.core.datastore.UserPreferencesRepositoryImpl
import com.russhwolf.settings.Settings
import org.koin.core.qualifier.named
import org.koin.dsl.module

val PreferencesModule = module {
factory<Settings> { Settings() }
factory {
UserPreferencesDataSource(
settings = get(),
dispatcher = get(named(MifosDispatchers.IO.name)),
)
}
single<UserPreferencesRepository> {
UserPreferencesRepositoryImpl(
preferenceManager = get(),
ioDispatcher = get(named(MifosDispatchers.IO.name)),
unconfinedDispatcher = get(named(MifosDispatchers.Unconfined.name)),
)
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment here like this - // TODO: Remove this after :core:common migration

enum class MifosDispatchers {
Default,
IO,
Unconfined,
}
Loading
Loading