Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
98f0b31
feat: Add Firebase Remote Config loader for dynamic backend instance …
claude Nov 25, 2025
c309eb0
feat: Add support for separate main and interbank instance selection
claude Nov 25, 2025
464256e
Fix: gitlive version to 2.4.0
therajanmaurya Nov 25, 2025
7b5377f
fix: Update Firebase Remote Config API usage for GitLive SDK
claude Nov 26, 2025
10ece81
Fix: clean up instance feature
therajanmaurya Nov 26, 2025
df2d3a2
Fix: formatting
therajanmaurya Nov 26, 2025
3293a55
Fix: formatting
therajanmaurya Nov 26, 2025
b76230c
Merge branch 'development' into claude/firebase-remote-config-loader-…
therajanmaurya Nov 26, 2025
9f665f4
Fix: final clean up
therajanmaurya Nov 26, 2025
7ba0e26
Merge remote-tracking branch 'mbs/claude/firebase-remote-config-loade…
therajanmaurya Nov 26, 2025
ba758da
Merge branch 'development' into claude/firebase-remote-config-loader-…
therajanmaurya Feb 6, 2026
c39f3fc
chore(deps): update library versions to match kmp-project-template
therajanmaurya Feb 6, 2026
86ec36a
fix: update compileSdk to 36 and fix Firebase KTX deprecation
therajanmaurya Feb 6, 2026
9af75a7
feat: restructure instance schema with nested interbank servers
therajanmaurya Feb 9, 2026
0e1d16a
chore: update Podfile to match kmp-project-template
therajanmaurya Feb 9, 2026
9e62be6
feat: add Supabase instance config loader and Firebase analytics updates
therajanmaurya Feb 9, 2026
286bdd5
fix: add org.mifospay.debug to google-services.json for prod debug bu…
therajanmaurya Feb 9, 2026
c84be07
feat: add Supabase credentials support to CI workflows
therajanmaurya Feb 9, 2026
87e79b9
chore: update mifos-x-actionhub to v1.0.9
therajanmaurya Feb 9, 2026
f3c6f49
revert: restore original iOS and analytics files
therajanmaurya Feb 9, 2026
e84c5c5
chore: update prodRelease-badging.txt
therajanmaurya Feb 9, 2026
fa6858e
chore: update mifos-x-actionhub to v1.0.10
therajanmaurya Feb 9, 2026
7d60c90
fix: conditionally declare Supabase credentials file as input
therajanmaurya Feb 9, 2026
d5e2e16
chore: update Kotlin JS yarn.lock file
therajanmaurya Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import org.mifospay.feature.settings.di.SettingsModule
import org.mifospay.feature.standing.instruction.di.StandingInstructionModule
import org.mifospay.feature.upi.setup.di.UpiSetupModule
import org.mifospay.shared.MifosPayViewModel
import org.mifospay.shared.instance.InstanceSelectorViewModel

object KoinModules {
private val commonModules = module {
Expand All @@ -65,6 +66,7 @@ object KoinModules {
}
private val sharedModule = module {
viewModelOf(::MifosPayViewModel)
viewModelOf(::InstanceSelectorViewModel)
}
private val featureModules = module {
includes(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.shared.instance

import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput

@Composable
fun Modifier.detectInstanceSelectorGesture(
onGestureDetected: () -> Unit,
): Modifier {
var tapCount by remember { mutableIntStateOf(0) }
var lastTapTime by remember { mutableLongStateOf(0L) }
val tapTimeoutMs = 1000L // Reset tap count after 1 second

return this.pointerInput(Unit) {
detectTapGestures(
onTap = {
val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime > tapTimeoutMs) {
tapCount = 0
}
tapCount++
lastTapTime = currentTime

if (tapCount >= 5) {
onGestureDetected()
tapCount = 0
}
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* 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.shared.instance

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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InstanceSelectorScreen(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
viewModel: InstanceSelectorViewModel = koinViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Scaffold(
topBar = {
TopAppBar(
title = { Text("Select Instance") },
navigationIcon = {
IconButton(onClick = onDismiss) {
Icon(
imageVector = MifosIcons.ArrowBack,
contentDescription = "Close",
)
}
},
)
},
modifier = modifier,
) { paddingValues ->
when (val state = uiState) {
is InstanceSelectorUiState.Loading -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text("Loading instances...")
}
}

is InstanceSelectorUiState.Success -> {
InstancesList(
mainInstances = state.mainInstances,
interbankInstances = state.interbankInstances,
selectedMainInstance = state.selectedMainInstance,
selectedInterbankInstance = state.selectedInterbankInstance,
onInstanceSelected = { instance ->
viewModel.selectInstance(instance)
},
modifier = Modifier.padding(paddingValues),
)
}

is InstanceSelectorUiState.Error -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = "Error: ${state.message}",
color = MaterialTheme.colorScheme.error,
)
}
}
}
}
}

@Composable
private fun InstancesList(
mainInstances: List<ServerInstance>,
interbankInstances: List<ServerInstance>,
selectedMainInstance: ServerInstance?,
selectedInterbankInstance: ServerInstance?,
onInstanceSelected: (ServerInstance) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
item {
Text(
text = "Main Server Instances",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp),
)
}

items(mainInstances) { instance ->
InstanceItem(
instance = instance,
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) },
)
}
}
}

@Composable
private fun InstanceItem(
instance: ServerInstance,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(1f),
) {
Text(
text = instance.label,
style = MaterialTheme.typography.titleMedium,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = instance.fullUrl,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Tenant: ${instance.platformTenantId}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
RadioButton(
selected = isSelected,
onClick = onClick,
)
}
}
}

@Preview
@Composable
private fun InstanceItemPreview() {
KptTheme {
InstanceItem(
instance = ServerInstance(
endpoint = "mifos-bank-2.mifos.community",
protocol = "https://",
path = "/fineract-provider/api/v1/",
platformTenantId = "mifos-bank-2",
label = "Production",
type = InstanceType.MAIN,
isDefault = true,
),
isSelected = true,
onClick = {},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.shared.instance

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
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

sealed interface InstanceSelectorUiState {
data object Loading : InstanceSelectorUiState
data class Success(
val mainInstances: List<ServerInstance>,
val interbankInstances: List<ServerInstance>,
val selectedMainInstance: ServerInstance?,
val selectedInterbankInstance: ServerInstance?,
) : InstanceSelectorUiState
data class Error(val message: String) : InstanceSelectorUiState
}

class InstanceSelectorViewModel(
private val instanceConfigLoader: InstanceConfigLoader,
private val userPreferencesRepository: UserPreferencesRepository,
) : ViewModel() {

private val _uiState = MutableStateFlow<InstanceSelectorUiState>(InstanceSelectorUiState.Loading)
val uiState: StateFlow<InstanceSelectorUiState> = _uiState.asStateFlow()

init {
loadInstances()
}

fun loadInstances() {
viewModelScope.launch {
_uiState.value = InstanceSelectorUiState.Loading
when (val result = instanceConfigLoader.fetchInstancesConfig()) {
is DataState.Success -> {
val config = result.data
val selectedMainInstance = userPreferencesRepository.selectedInstance.value
val selectedInterbankInstance = userPreferencesRepository.selectedInterbankInstance.value
_uiState.value = InstanceSelectorUiState.Success(
mainInstances = config.getMainInstances(),
interbankInstances = config.getInterbankInstances(),
selectedMainInstance = selectedMainInstance ?: config.getDefaultInstance(),
selectedInterbankInstance = selectedInterbankInstance ?: config.getDefaultInterbankInstance(),
)
}
is DataState.Error -> {
_uiState.value = InstanceSelectorUiState.Error(
message = result.exception.message ?: "Failed to load instances",
)
}
is DataState.Loading -> {
_uiState.value = InstanceSelectorUiState.Loading
}
}
}
}

fun selectInstance(instance: ServerInstance) {
viewModelScope.launch {
val currentState = _uiState.value
if (currentState is InstanceSelectorUiState.Success) {
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)
}
}
}
}
}
}
Loading