Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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