Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -73,8 +73,9 @@ class UserAuthRepositoryImp(
DataState.Error(Exception("Invalid Credentials"), null)
}
}
} catch (e: Exception) {
DataState.Error(e, null)
} catch (e: ClientRequestException) {
val errorMessage = extractErrorMessage(e.response)
DataState.Error(Exception(errorMessage), null)
}
}

Expand Down
2 changes: 2 additions & 0 deletions core/ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ kotlin{
implementation(libs.jb.composeNavigation)
implementation(libs.filekit.compose)
implementation(libs.filekit.core)
implementation(libs.compottie.resources)
implementation(libs.compottie.lite)
}
}
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,94 @@
*/
package org.mifos.mobile.core.ui.component

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.mifos.mobile.core.designsystem.theme.DesignToken
import org.mifos.mobile.core.ui.utils.DevicePreview
import io.github.alexzhirkevich.compottie.LottieCompositionSpec
import io.github.alexzhirkevich.compottie.animateLottieCompositionAsState
import io.github.alexzhirkevich.compottie.rememberLottieComposition
import io.github.alexzhirkevich.compottie.rememberLottiePainter
import mifos_mobile.core.ui.generated.resources.Res
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme

@DevicePreview
@Composable
fun MifosProgressIndicator(
modifier: Modifier = Modifier.fillMaxSize(),
) {
Column(
val composition by rememberLottieComposition {
LottieCompositionSpec.JsonString(
Res.readBytes("files/loading_animation.json").decodeToString(),
)
}
val progress by animateLottieCompositionAsState(composition)

Box(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
Image(
painter = rememberLottiePainter(
composition = composition,
progress = { progress },
),
contentDescription = "Lottie animation",
)
}
}

@DevicePreview
@Composable
fun MifosProgressIndicatorOverlay(
modifier: Modifier = Modifier.fillMaxSize(),
) {
Column(
val composition by rememberLottieComposition {
LottieCompositionSpec.JsonString(
Res.readBytes("files/loading_animation.json").decodeToString(),
)
}
val progress by animateLottieCompositionAsState(composition)

Box(
modifier = modifier
.padding(DesignToken.padding.large)
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f))
.clickable(
enabled = false,
indication = null,
interactionSource = remember { MutableInteractionSource() },
) { },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
Image(
painter = rememberLottiePainter(
composition = composition,
progress = { progress },
),
contentDescription = "Loading animation",
)
}
}

@Preview
@Composable
private fun Loading_Preview() {
MifosMobileTheme {
MifosProgressIndicator()
}
}

@Preview
@Composable
private fun Overlay_Loading_Preview() {
MifosMobileTheme {
MifosProgressIndicatorOverlay()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ package org.mifos.mobile.core.ui.utils

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SelectableDates
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

@OptIn(ExperimentalMaterial3Api::class)
object PresentOrFutureSelectableDates : SelectableDates {

@OptIn(ExperimentalTime::class)
@ExperimentalMaterial3Api
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
val currentTimeMillis = Clock.System.now().toEpochMilliseconds()
return utcTimeMillis >= currentTimeMillis
}

@OptIn(ExperimentalTime::class)
override fun isSelectableYear(year: Int): Boolean {
val currentYear = Clock.System.now()
.toLocalDateTime(TimeZone.currentSystemDefault())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.ui.utils

import org.jetbrains.compose.resources.StringResource

/**
* A sealed interface representing the different types of full-screen UI states
* that can be used across various screens in an application.
*
* This generic state model provides a consistent way to handle common UI states
* such as loading, errors, and success, which helps to standardize UI logic
* and reduce boilerplate in ViewModels and UI layers.
*/
sealed interface ScreenUiState {
/** * Represents a full-screen loading state.
* * Use this state when the entire screen is busy fetching initial data,
* and a loading indicator (e.g., a progress bar) should be displayed
* over the whole screen.
*/
data object Loading : ScreenUiState

/** * Represents an empty state where no data is available to display.
* * This state is typically used when a data fetch operation is successful
* but returns an empty list or collection. The UI should show a message
* or a graphic indicating that there is no content.
*/
data object Empty : ScreenUiState

/**
* Represents a full-screen error state with a user-facing message.
* * This state should be used when an unrecoverable error occurs, and the
* entire screen needs to show an error message.
*
* @property message The [StringResource] for the error message to be displayed.
*/
data class Error(val message: StringResource) : ScreenUiState

/** * Represents a successful state where content can be displayed.
* * This is the final state after a successful data fetch or operation.
* The UI can now display the main content of the screen.
*/
data object Success : ScreenUiState

/** * Represents a state where there is a network connectivity issue.
* * This state is useful for displaying a dedicated UI screen or a message
* indicating that the device is offline or has lost its connection.
*/
data object Network : ScreenUiState

/** * Represents a state where an overlay loading spinner should be shown.
* * Use this state when a background operation is in progress (e.g., a form
* submission or a data refresh), but the existing content of the screen
* should remain visible underneath a loading indicator.
*/
// data object OverlayLoading : ScreenUiState
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
Expand Down Expand Up @@ -51,6 +50,9 @@ import org.mifos.mobile.feature.accounts.model.TransactionCheckboxStatus
import org.mifos.mobile.feature.accounts.model.TransactionFilterType
import org.mifos.mobile.feature.accounts.utils.StatusUtils
import kotlin.collections.map
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

/**
* ViewModel for managing the state and logic of the account transactions screen.
*
Expand Down Expand Up @@ -451,6 +453,7 @@ internal class AccountsTransactionViewModel(
* @param selectedFilters A list of [TransactionCheckboxStatus] representing the active checkbox filters.
* @return A map of filtered transactions grouped by date.
*/
@OptIn(ExperimentalTime::class)
internal fun applyTransactionFilters(
selectedFilters: List<TransactionCheckboxStatus>,
): Map<String, List<UiTransaction>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ package org.mifos.mobile.feature.accounts.accounts
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.update
import kotlinx.datetime.Clock
import org.jetbrains.compose.resources.StringResource
import org.mifos.mobile.core.common.Constants
import org.mifos.mobile.core.model.enums.AccountType
import org.mifos.mobile.core.ui.utils.BaseViewModel
import org.mifos.mobile.feature.accounts.model.CheckboxStatus
import org.mifos.mobile.feature.accounts.model.FilterType
import org.mifos.mobile.feature.accounts.utils.StatusUtils
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

/**
* ViewModel responsible for managing the account screen state,
Expand All @@ -40,6 +41,7 @@ internal class AccountsViewModel(
observeAccountTypeAndInitCheckboxes()
}

@OptIn(ExperimentalTime::class)
override fun handleAction(action: AccountsAction) {
when (action) {
is AccountsAction.SetCheckboxFilterList -> {
Expand Down Expand Up @@ -112,6 +114,7 @@ internal class AccountsViewModel(
* Applies the selected checkboxes as filters, sets refresh signal,
* and dismisses the filter dialog.
*/
@OptIn(ExperimentalTime::class)
private fun handleConfirmFilterDialog() {
val selectedFilters = state.checkboxOptions.filter { it.isChecked }

Expand Down Expand Up @@ -202,7 +205,9 @@ internal class AccountsViewModel(
* UI state for the Accounts screen, containing filter options, dialog visibility,
* current account type, and refresh signals.
*/
internal data class AccountsState(
internal data class AccountsState
@OptIn(ExperimentalTime::class)
constructor(
val isRefreshing: Boolean = false,

/** Current filter checkboxes shown in the dialog */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
Expand Down Expand Up @@ -56,10 +55,8 @@ import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
import org.mifos.mobile.core.designsystem.component.BasicDialogState
import org.mifos.mobile.core.designsystem.component.LoadingDialogState
import org.mifos.mobile.core.designsystem.component.MifosBasicDialog
import org.mifos.mobile.core.designsystem.component.MifosButton
import org.mifos.mobile.core.designsystem.component.MifosLoadingDialog
import org.mifos.mobile.core.designsystem.component.MifosOutlinedTextField
import org.mifos.mobile.core.designsystem.component.MifosPasswordField
import org.mifos.mobile.core.designsystem.component.MifosScaffold
Expand All @@ -70,7 +67,9 @@ import org.mifos.mobile.core.designsystem.theme.DesignToken
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
import org.mifos.mobile.core.designsystem.theme.MifosTypography
import org.mifos.mobile.core.ui.component.MifosPoweredCard
import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay
import org.mifos.mobile.core.ui.utils.EventsEffect
import org.mifos.mobile.core.ui.utils.ScreenUiState

@Composable
internal fun LoginScreen(
Expand Down Expand Up @@ -132,11 +131,21 @@ private fun LoginScreen(
}
},
) {
LoginScreenContent(
modifier = modifier,
state = state,
onAction = onAction,
)
when (state.uiState) {
ScreenUiState.Success -> {
LoginScreenContent(
modifier = modifier,
state = state,
onAction = onAction,
)

if (state.showOverlay) {
MifosProgressIndicatorOverlay()
}
}

else -> {}
}
}
}

Expand All @@ -153,10 +162,6 @@ private fun LoginDialogs(
onDismissRequest = onDismissRequest,
)

is LoginState.DialogState.Loading -> MifosLoadingDialog(
visibilityState = LoadingDialogState.Shown,
)

null -> Unit
}
}
Expand Down Expand Up @@ -243,11 +248,6 @@ fun InputBox(
label = stringResource(Res.string.feature_sign_in_username_label),
shape = DesignToken.shapes.medium,
textStyle = MifosTypography.bodyLarge,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
errorBorderColor = MaterialTheme.colorScheme.error,
),
config = MifosTextFieldConfig(
isError = state.isError,
errorText = state.userNameError.takeIf { state.isError }?.let { stringResource(it) },
Expand Down Expand Up @@ -277,11 +277,6 @@ fun InputBox(
showPasswordChange = {
onAction(LoginAction.TogglePasswordVisibility)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
errorBorderColor = MaterialTheme.colorScheme.error,
),
isError = state.isError,
hint = state.passwordError.takeIf { state.isError }?.let { stringResource(it) },
)
Expand Down Expand Up @@ -342,7 +337,7 @@ fun InputBox(
private fun LoanScreenPreview() {
MifosMobileTheme {
LoginScreen(
state = LoginState(dialogState = null),
state = LoginState(uiState = ScreenUiState.Success),
onAction = {},
)
}
Expand Down
Loading
Loading