feat: Add support for separate main and interbank instance selection

- Add InstanceType enum (MAIN, INTERBANK) to ServerInstance model
- Update InstancesConfig with helper methods to filter by instance type
- Extend datastore to store both selectedInstance and selectedInterbankInstance
- Update InstanceConfigManager to handle both main and interbank instances
- Add DEFAULT_INTERBANK_INSTANCE with apis.flexcore.mx configuration
- Update BaseURL.interBankUrl to use dynamic configuration from InstanceConfigManager
- Update FirebaseInstanceConfigLoader with default instances for both types
- Redesign InstanceSelectorScreen to display main and interbank instances separately
- Update InstanceSelectorViewModel to handle selection of both instance types
- Users can now independently select main server and interbank server instances

Firebase Remote Config JSON format now supports:
{
  "instances": [
    {
      "endpoint": "mifos-bank-2.mifos.community",
      "protocol": "https://",
      "path": "/fineract-provider/api/v1/",
      "platformTenantId": "mifos-bank-2",
      "label": "Production Main",
      "type": "MAIN",
      "isDefault": true
    },
    {
      "endpoint": "apis.flexcore.mx",
      "protocol": "https://",
      "path": "/v1.0/vnext1/",
      "platformTenantId": "mifos-bank-2",
      "label": "Production Interbank",
      "type": "INTERBANK",
      "isDefault": true
    }
  ]
}
This commit is contained in:
Claude 2025-11-25 19:28:14 +00:00
parent 98f0b318b9
commit c309eb0d2e
No known key found for this signature in database
10 changed files with 162 additions and 24 deletions

View File

@ -37,9 +37,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.material3.HorizontalDivider
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
import org.mifospay.core.designsystem.icon.MifosIcons
import org.mifospay.core.model.instance.InstanceType
import org.mifospay.core.model.instance.ServerInstance
import template.core.base.designsystem.theme.KptTheme
@ -85,11 +87,12 @@ fun InstanceSelectorScreen(
is InstanceSelectorUiState.Success -> {
InstancesList(
instances = state.instances,
selectedInstance = state.selectedInstance,
mainInstances = state.mainInstances,
interbankInstances = state.interbankInstances,
selectedMainInstance = state.selectedMainInstance,
selectedInterbankInstance = state.selectedInterbankInstance,
onInstanceSelected = { instance ->
viewModel.selectInstance(instance)
onDismiss()
},
modifier = Modifier.padding(paddingValues),
)
@ -115,8 +118,10 @@ fun InstanceSelectorScreen(
@Composable
private fun InstancesList(
instances: List<ServerInstance>,
selectedInstance: ServerInstance?,
mainInstances: List<ServerInstance>,
interbankInstances: List<ServerInstance>,
selectedMainInstance: ServerInstance?,
selectedInterbankInstance: ServerInstance?,
onInstanceSelected: (ServerInstance) -> Unit,
modifier: Modifier = Modifier,
) {
@ -126,10 +131,40 @@ private fun InstancesList(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(instances) { instance ->
item {
Text(
text = "Main Server Instances",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp),
)
}
items(mainInstances) { instance ->
InstanceItem(
instance = instance,
isSelected = instance == selectedInstance,
isSelected = instance == selectedMainInstance,
onClick = { onInstanceSelected(instance) },
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(16.dp))
}
item {
Text(
text = "Interbank Server Instances",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp),
)
}
items(interbankInstances) { instance ->
InstanceItem(
instance = instance,
isSelected = instance == selectedInterbankInstance,
onClick = { onInstanceSelected(instance) },
)
}
@ -194,6 +229,7 @@ private fun InstanceItemPreview() {
path = "/fineract-provider/api/v1/",
platformTenantId = "mifos-bank-2",
label = "Production",
type = InstanceType.MAIN,
isDefault = true,
),
isSelected = true,

View File

@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.mifospay.core.common.DataState
import org.mifospay.core.datastore.UserPreferencesRepository
import org.mifospay.core.model.instance.InstanceType
import org.mifospay.core.model.instance.InstancesConfig
import org.mifospay.core.model.instance.ServerInstance
import org.mifospay.core.network.config.InstanceConfigLoader
@ -24,8 +25,10 @@ import org.mifospay.core.network.config.InstanceConfigLoader
sealed interface InstanceSelectorUiState {
data object Loading : InstanceSelectorUiState
data class Success(
val instances: List<ServerInstance>,
val selectedInstance: ServerInstance?,
val mainInstances: List<ServerInstance>,
val interbankInstances: List<ServerInstance>,
val selectedMainInstance: ServerInstance?,
val selectedInterbankInstance: ServerInstance?,
) : InstanceSelectorUiState
data class Error(val message: String) : InstanceSelectorUiState
}
@ -48,10 +51,13 @@ class InstanceSelectorViewModel(
when (val result = instanceConfigLoader.fetchInstancesConfig()) {
is DataState.Success -> {
val config = result.data
val selectedInstance = userPreferencesRepository.selectedInstance.value
val selectedMainInstance = userPreferencesRepository.selectedInstance.value
val selectedInterbankInstance = userPreferencesRepository.selectedInterbankInstance.value
_uiState.value = InstanceSelectorUiState.Success(
instances = config.instances,
selectedInstance = selectedInstance ?: config.getDefaultInstance(),
mainInstances = config.getMainInstances(),
interbankInstances = config.getInterbankInstances(),
selectedMainInstance = selectedMainInstance ?: config.getDefaultInstance(),
selectedInterbankInstance = selectedInterbankInstance ?: config.getDefaultInterbankInstance(),
)
}
is DataState.Error -> {
@ -68,10 +74,18 @@ class InstanceSelectorViewModel(
fun selectInstance(instance: ServerInstance) {
viewModelScope.launch {
userPreferencesRepository.updateSelectedInstance(instance)
val currentState = _uiState.value
if (currentState is InstanceSelectorUiState.Success) {
_uiState.value = currentState.copy(selectedInstance = instance)
when (instance.type) {
InstanceType.MAIN -> {
userPreferencesRepository.updateSelectedInstance(instance)
_uiState.value = currentState.copy(selectedMainInstance = instance)
}
InstanceType.INTERBANK -> {
userPreferencesRepository.updateSelectedInterbankInstance(instance)
_uiState.value = currentState.copy(selectedInterbankInstance = instance)
}
}
}
}
}

View File

@ -33,6 +33,7 @@ import org.mifospay.core.model.user.UserInfo
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"
@OptIn(ExperimentalSerializationApi::class)
class UserPreferencesDataSource(
@ -79,6 +80,13 @@ class UserPreferencesDataSource(
),
)
private val _selectedInterbankInstance = MutableStateFlow(
settings.decodeValueOrNull(
key = SELECTED_INTERBANK_INSTANCE_KEY,
serializer = ServerInstance.serializer(),
),
)
val token = _userInfo.map {
it.base64EncodedAuthenticationKey
}
@ -92,6 +100,8 @@ class UserPreferencesDataSource(
val selectedInstance = _selectedInstance
val selectedInterbankInstance = _selectedInterbankInstance
suspend fun updateClientInfo(client: Client) {
withContext(dispatcher) {
settings.putClientPreference(client.toClientPreferences())
@ -156,6 +166,13 @@ class UserPreferencesDataSource(
}
}
suspend fun updateSelectedInterbankInstance(instance: ServerInstance) {
withContext(dispatcher) {
settings.putSelectedInterbankInstance(instance)
_selectedInterbankInstance.value = instance
}
}
suspend fun clearInfo() {
withContext(dispatcher) {
settings.clear()
@ -199,3 +216,11 @@ private fun Settings.putSelectedInstance(instance: ServerInstance) {
value = instance,
)
}
private fun Settings.putSelectedInterbankInstance(instance: ServerInstance) {
encodeValue(
key = SELECTED_INTERBANK_INSTANCE_KEY,
serializer = ServerInstance.serializer(),
value = instance,
)
}

View File

@ -35,6 +35,8 @@ interface UserPreferencesRepository {
val selectedInstance: StateFlow<ServerInstance?>
val selectedInterbankInstance: StateFlow<ServerInstance?>
suspend fun updateToken(token: String): DataState<Unit>
suspend fun updateUserInfo(user: UserInfo): DataState<Unit>
@ -47,5 +49,7 @@ interface UserPreferencesRepository {
suspend fun updateSelectedInstance(instance: ServerInstance): DataState<Unit>
suspend fun updateSelectedInterbankInstance(instance: ServerInstance): DataState<Unit>
suspend fun logOut(): Unit
}

View File

@ -80,6 +80,13 @@ class UserPreferencesRepositoryImpl(
started = SharingStarted.Eagerly,
)
override val selectedInterbankInstance: StateFlow<ServerInstance?>
get() = preferenceManager.selectedInterbankInstance.stateIn(
scope = unconfinedScope,
initialValue = null,
started = SharingStarted.Eagerly,
)
override suspend fun updateDefaultAccount(account: DefaultAccount): DataState<Unit> {
return try {
val result = preferenceManager.updateDefaultAccount(account)
@ -99,6 +106,15 @@ class UserPreferencesRepositoryImpl(
}
}
override suspend fun updateSelectedInterbankInstance(instance: ServerInstance): DataState<Unit> {
return try {
preferenceManager.updateSelectedInterbankInstance(instance)
DataState.Success(Unit)
} catch (e: Exception) {
DataState.Error(e)
}
}
override suspend fun updateToken(token: String): DataState<Unit> {
return try {
val result = preferenceManager.updateAuthToken(token)

View File

@ -15,5 +15,15 @@ import kotlinx.serialization.Serializable
data class InstancesConfig(
val instances: List<ServerInstance> = emptyList(),
) {
fun getDefaultInstance(): ServerInstance? = instances.firstOrNull { it.isDefault }
fun getDefaultInstance(): ServerInstance? =
instances.firstOrNull { it.isDefault && it.type == InstanceType.MAIN }
fun getDefaultInterbankInstance(): ServerInstance? =
instances.firstOrNull { it.isDefault && it.type == InstanceType.INTERBANK }
fun getMainInstances(): List<ServerInstance> =
instances.filter { it.type == InstanceType.MAIN }
fun getInterbankInstances(): List<ServerInstance> =
instances.filter { it.type == InstanceType.INTERBANK }
}

View File

@ -11,6 +11,11 @@ package org.mifospay.core.model.instance
import kotlinx.serialization.Serializable
enum class InstanceType {
MAIN,
INTERBANK,
}
@Serializable
data class ServerInstance(
val endpoint: String,
@ -18,6 +23,7 @@ data class ServerInstance(
val path: String,
val platformTenantId: String,
val label: String,
val type: InstanceType = InstanceType.MAIN,
val isDefault: Boolean = false,
) {
val fullUrl: String

View File

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.json.Json
import org.mifospay.core.common.DataState
import org.mifospay.core.model.instance.InstanceType
import org.mifospay.core.model.instance.InstancesConfig
import org.mifospay.core.model.instance.ServerInstance
@ -43,7 +44,17 @@ class FirebaseInstanceConfigLoader : InstanceConfigLoader {
protocol = "https://",
path = "/fineract-provider/api/v1/",
platformTenantId = "mifos-bank-2",
label = "Default Instance",
label = "Default Main Instance",
type = InstanceType.MAIN,
isDefault = true,
),
ServerInstance(
endpoint = "apis.flexcore.mx",
protocol = "https://",
path = "/v1.0/vnext1/",
platformTenantId = "mifos-bank-2",
label = "Default Interbank Instance",
type = InstanceType.INTERBANK,
isDefault = true,
),
),

View File

@ -11,27 +11,45 @@ package org.mifospay.core.network.config
import kotlinx.coroutines.flow.StateFlow
import org.mifospay.core.datastore.UserPreferencesRepository
import org.mifospay.core.model.instance.InstanceType
import org.mifospay.core.model.instance.ServerInstance
class InstanceConfigManager(
private val userPreferencesRepository: UserPreferencesRepository,
) {
companion object {
// Default instance configuration
private val DEFAULT_INSTANCE = ServerInstance(
// Default main instance configuration
private val DEFAULT_MAIN_INSTANCE = ServerInstance(
endpoint = "mifos-bank-2.mifos.community",
protocol = "https://",
path = "/fineract-provider/api/v1/",
platformTenantId = "mifos-bank-2",
label = "Default Instance",
type = InstanceType.MAIN,
isDefault = true,
)
// Default interbank instance configuration
private val DEFAULT_INTERBANK_INSTANCE = ServerInstance(
endpoint = "apis.flexcore.mx",
protocol = "https://",
path = "/v1.0/vnext1/",
platformTenantId = "mifos-bank-2",
label = "Default Interbank",
type = InstanceType.INTERBANK,
isDefault = true,
)
}
val selectedInstance: StateFlow<ServerInstance?> = userPreferencesRepository.selectedInstance
val selectedInterbankInstance: StateFlow<ServerInstance?> = userPreferencesRepository.selectedInterbankInstance
fun getCurrentInstance(): ServerInstance {
return selectedInstance.value ?: DEFAULT_INSTANCE
return selectedInstance.value ?: DEFAULT_MAIN_INSTANCE
}
fun getCurrentInterbankInstance(): ServerInstance {
return selectedInterbankInstance.value ?: DEFAULT_INTERBANK_INSTANCE
}
fun getEndpoint(): String = getCurrentInstance().endpoint
@ -48,4 +66,6 @@ class InstanceConfigManager(
val instance = getCurrentInstance()
return "${instance.protocol}${instance.endpoint}${instance.path}self/"
}
fun getInterbankUrl(): String = getCurrentInterbankInstance().fullUrl
}

View File

@ -18,10 +18,6 @@ class BaseURL(
const val HEADER_TENANT = "Fineract-Platform-TenantId"
const val HEADER_AUTH = "Authorization"
const val DEFAULT = "default"
const val API_ENDPOINT_INTERBANK = "apis.flexcore.mx"
const val API_PATH_INTERBANK = "/v1.0/vnext1/"
private const val PROTOCOL_HTTPS = "https://"
}
val url: String
@ -31,7 +27,7 @@ class BaseURL(
get() = configManager.getSelfServiceUrl()
val interBankUrl: String
get() = PROTOCOL_HTTPS + API_ENDPOINT_INTERBANK + API_PATH_INTERBANK
get() = configManager.getInterbankUrl()
val fineractPlatformTenantId: String
get() = configManager.getPlatformTenantId()