Skip to content

Commit 727983f

Browse files
committed
refactor: auth module with loading animation
1 parent 3d9ad2c commit 727983f

File tree

25 files changed

+359
-255
lines changed

25 files changed

+359
-255
lines changed

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/UserAuthRepositoryImp.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ class UserAuthRepositoryImp(
7373
DataState.Error(Exception("Invalid Credentials"), null)
7474
}
7575
}
76-
} catch (e: Exception) {
77-
DataState.Error(e, null)
76+
} catch (e: ClientRequestException) {
77+
val errorMessage = extractErrorMessage(e.response)
78+
DataState.Error(Exception(errorMessage), null)
7879
}
7980
}
8081

core/ui/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ kotlin{
4444
implementation(libs.jb.composeNavigation)
4545
implementation(libs.filekit.compose)
4646
implementation(libs.filekit.core)
47+
implementation(libs.compottie.resources)
48+
implementation(libs.compottie.lite)
4749
}
4850
}
4951
}

core/ui/src/commonMain/composeResources/files/loading_animation.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,53 +9,94 @@
99
*/
1010
package org.mifos.mobile.core.ui.component
1111

12+
import androidx.compose.foundation.Image
1213
import androidx.compose.foundation.background
1314
import androidx.compose.foundation.clickable
1415
import androidx.compose.foundation.interaction.MutableInteractionSource
15-
import androidx.compose.foundation.layout.Arrangement
16-
import androidx.compose.foundation.layout.Column
16+
import androidx.compose.foundation.layout.Box
1717
import androidx.compose.foundation.layout.fillMaxSize
18-
import androidx.compose.foundation.layout.padding
19-
import androidx.compose.material3.CircularProgressIndicator
2018
import androidx.compose.material3.MaterialTheme
2119
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.getValue
2221
import androidx.compose.runtime.remember
2322
import androidx.compose.ui.Alignment
2423
import androidx.compose.ui.Modifier
25-
import org.mifos.mobile.core.designsystem.theme.DesignToken
26-
import org.mifos.mobile.core.ui.utils.DevicePreview
24+
import io.github.alexzhirkevich.compottie.LottieCompositionSpec
25+
import io.github.alexzhirkevich.compottie.animateLottieCompositionAsState
26+
import io.github.alexzhirkevich.compottie.rememberLottieComposition
27+
import io.github.alexzhirkevich.compottie.rememberLottiePainter
28+
import mifos_mobile.core.ui.generated.resources.Res
29+
import org.jetbrains.compose.ui.tooling.preview.Preview
30+
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
2731

28-
@DevicePreview
2932
@Composable
3033
fun MifosProgressIndicator(
3134
modifier: Modifier = Modifier.fillMaxSize(),
3235
) {
33-
Column(
36+
val composition by rememberLottieComposition {
37+
LottieCompositionSpec.JsonString(
38+
Res.readBytes("files/loading_animation.json").decodeToString(),
39+
)
40+
}
41+
val progress by animateLottieCompositionAsState(composition)
42+
43+
Box(
3444
modifier = modifier,
35-
verticalArrangement = Arrangement.Center,
36-
horizontalAlignment = Alignment.CenterHorizontally,
45+
contentAlignment = Alignment.Center,
3746
) {
38-
CircularProgressIndicator()
47+
Image(
48+
painter = rememberLottiePainter(
49+
composition = composition,
50+
progress = { progress },
51+
),
52+
contentDescription = "Lottie animation",
53+
)
3954
}
4055
}
4156

42-
@DevicePreview
4357
@Composable
4458
fun MifosProgressIndicatorOverlay(
4559
modifier: Modifier = Modifier.fillMaxSize(),
4660
) {
47-
Column(
61+
val composition by rememberLottieComposition {
62+
LottieCompositionSpec.JsonString(
63+
Res.readBytes("files/loading_animation.json").decodeToString(),
64+
)
65+
}
66+
val progress by animateLottieCompositionAsState(composition)
67+
68+
Box(
4869
modifier = modifier
49-
.padding(DesignToken.padding.large)
5070
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f))
5171
.clickable(
5272
enabled = false,
5373
indication = null,
5474
interactionSource = remember { MutableInteractionSource() },
5575
) { },
56-
verticalArrangement = Arrangement.Center,
57-
horizontalAlignment = Alignment.CenterHorizontally,
76+
contentAlignment = Alignment.Center,
5877
) {
59-
CircularProgressIndicator()
78+
Image(
79+
painter = rememberLottiePainter(
80+
composition = composition,
81+
progress = { progress },
82+
),
83+
contentDescription = "Loading animation",
84+
)
85+
}
86+
}
87+
88+
@Preview
89+
@Composable
90+
private fun Loading_Preview() {
91+
MifosMobileTheme {
92+
MifosProgressIndicator()
93+
}
94+
}
95+
96+
@Preview
97+
@Composable
98+
private fun Overlay_Loading_Preview() {
99+
MifosMobileTheme {
100+
MifosProgressIndicatorOverlay()
60101
}
61102
}

core/ui/src/commonMain/kotlin/org/mifos/mobile/core/ui/utils/PresentOrFutureSelectableDates.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@ package org.mifos.mobile.core.ui.utils
1111

1212
import androidx.compose.material3.ExperimentalMaterial3Api
1313
import androidx.compose.material3.SelectableDates
14-
import kotlinx.datetime.Clock
1514
import kotlinx.datetime.TimeZone
1615
import kotlinx.datetime.toLocalDateTime
16+
import kotlin.time.Clock
17+
import kotlin.time.ExperimentalTime
1718

1819
@OptIn(ExperimentalMaterial3Api::class)
1920
object PresentOrFutureSelectableDates : SelectableDates {
2021

22+
@OptIn(ExperimentalTime::class)
2123
@ExperimentalMaterial3Api
2224
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
2325
val currentTimeMillis = Clock.System.now().toEpochMilliseconds()
2426
return utcTimeMillis >= currentTimeMillis
2527
}
2628

29+
@OptIn(ExperimentalTime::class)
2730
override fun isSelectableYear(year: Int): Boolean {
2831
val currentYear = Clock.System.now()
2932
.toLocalDateTime(TimeZone.currentSystemDefault())
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
9+
*/
10+
package org.mifos.mobile.core.ui.utils
11+
12+
import org.jetbrains.compose.resources.StringResource
13+
14+
/**
15+
* A sealed interface representing the different types of full-screen UI states
16+
* that can be used across various screens in an application.
17+
*
18+
* This generic state model provides a consistent way to handle common UI states
19+
* such as loading, errors, and success, which helps to standardize UI logic
20+
* and reduce boilerplate in ViewModels and UI layers.
21+
*/
22+
sealed interface ScreenUiState {
23+
/** * Represents a full-screen loading state.
24+
* * Use this state when the entire screen is busy fetching initial data,
25+
* and a loading indicator (e.g., a progress bar) should be displayed
26+
* over the whole screen.
27+
*/
28+
data object Loading : ScreenUiState
29+
30+
/** * Represents an empty state where no data is available to display.
31+
* * This state is typically used when a data fetch operation is successful
32+
* but returns an empty list or collection. The UI should show a message
33+
* or a graphic indicating that there is no content.
34+
*/
35+
data object Empty : ScreenUiState
36+
37+
/**
38+
* Represents a full-screen error state with a user-facing message.
39+
* * This state should be used when an unrecoverable error occurs, and the
40+
* entire screen needs to show an error message.
41+
*
42+
* @property message The [StringResource] for the error message to be displayed.
43+
*/
44+
data class Error(val message: StringResource) : ScreenUiState
45+
46+
/** * Represents a successful state where content can be displayed.
47+
* * This is the final state after a successful data fetch or operation.
48+
* The UI can now display the main content of the screen.
49+
*/
50+
data object Success : ScreenUiState
51+
52+
/** * Represents a state where there is a network connectivity issue.
53+
* * This state is useful for displaying a dedicated UI screen or a message
54+
* indicating that the device is offline or has lost its connection.
55+
*/
56+
data object Network : ScreenUiState
57+
58+
/** * Represents a state where an overlay loading spinner should be shown.
59+
* * Use this state when a background operation is in progress (e.g., a form
60+
* submission or a data refresh), but the existing content of the screen
61+
* should remain visible underneath a loading indicator.
62+
*/
63+
// data object OverlayLoading : ScreenUiState
64+
}

feature/accounts/src/commonMain/kotlin/org/mifos/mobile/feature/accounts/accountTransactions/TransactionViewModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
1717
import kotlinx.coroutines.flow.map
1818
import kotlinx.coroutines.flow.update
1919
import kotlinx.coroutines.launch
20-
import kotlinx.datetime.Clock
2120
import kotlinx.datetime.DateTimeUnit
2221
import kotlinx.datetime.LocalDate
2322
import kotlinx.datetime.TimeZone
@@ -51,6 +50,9 @@ import org.mifos.mobile.feature.accounts.model.TransactionCheckboxStatus
5150
import org.mifos.mobile.feature.accounts.model.TransactionFilterType
5251
import org.mifos.mobile.feature.accounts.utils.StatusUtils
5352
import kotlin.collections.map
53+
import kotlin.time.Clock
54+
import kotlin.time.ExperimentalTime
55+
5456
/**
5557
* ViewModel for managing the state and logic of the account transactions screen.
5658
*
@@ -451,6 +453,7 @@ internal class AccountsTransactionViewModel(
451453
* @param selectedFilters A list of [TransactionCheckboxStatus] representing the active checkbox filters.
452454
* @return A map of filtered transactions grouped by date.
453455
*/
456+
@OptIn(ExperimentalTime::class)
454457
internal fun applyTransactionFilters(
455458
selectedFilters: List<TransactionCheckboxStatus>,
456459
): Map<String, List<UiTransaction>> {

feature/accounts/src/commonMain/kotlin/org/mifos/mobile/feature/accounts/accounts/AccountsViewModel.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ package org.mifos.mobile.feature.accounts.accounts
1212
import androidx.lifecycle.SavedStateHandle
1313
import androidx.navigation.toRoute
1414
import kotlinx.coroutines.flow.update
15-
import kotlinx.datetime.Clock
1615
import org.jetbrains.compose.resources.StringResource
1716
import org.mifos.mobile.core.common.Constants
1817
import org.mifos.mobile.core.model.enums.AccountType
1918
import org.mifos.mobile.core.ui.utils.BaseViewModel
2019
import org.mifos.mobile.feature.accounts.model.CheckboxStatus
2120
import org.mifos.mobile.feature.accounts.model.FilterType
2221
import org.mifos.mobile.feature.accounts.utils.StatusUtils
22+
import kotlin.time.Clock
23+
import kotlin.time.ExperimentalTime
2324

2425
/**
2526
* ViewModel responsible for managing the account screen state,
@@ -40,6 +41,7 @@ internal class AccountsViewModel(
4041
observeAccountTypeAndInitCheckboxes()
4142
}
4243

44+
@OptIn(ExperimentalTime::class)
4345
override fun handleAction(action: AccountsAction) {
4446
when (action) {
4547
is AccountsAction.SetCheckboxFilterList -> {
@@ -112,6 +114,7 @@ internal class AccountsViewModel(
112114
* Applies the selected checkboxes as filters, sets refresh signal,
113115
* and dismisses the filter dialog.
114116
*/
117+
@OptIn(ExperimentalTime::class)
115118
private fun handleConfirmFilterDialog() {
116119
val selectedFilters = state.checkboxOptions.filter { it.isChecked }
117120

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

208213
/** Current filter checkboxes shown in the dialog */

feature/auth/src/commonMain/kotlin/org/mifos/mobile/feature/auth/login/LoginScreen.kt

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import androidx.compose.foundation.rememberScrollState
2626
import androidx.compose.foundation.verticalScroll
2727
import androidx.compose.material3.Icon
2828
import androidx.compose.material3.MaterialTheme
29-
import androidx.compose.material3.OutlinedTextFieldDefaults
3029
import androidx.compose.material3.SnackbarHostState
3130
import androidx.compose.material3.Surface
3231
import androidx.compose.material3.Text
@@ -56,10 +55,8 @@ import org.jetbrains.compose.resources.stringResource
5655
import org.jetbrains.compose.ui.tooling.preview.Preview
5756
import org.koin.compose.viewmodel.koinViewModel
5857
import org.mifos.mobile.core.designsystem.component.BasicDialogState
59-
import org.mifos.mobile.core.designsystem.component.LoadingDialogState
6058
import org.mifos.mobile.core.designsystem.component.MifosBasicDialog
6159
import org.mifos.mobile.core.designsystem.component.MifosButton
62-
import org.mifos.mobile.core.designsystem.component.MifosLoadingDialog
6360
import org.mifos.mobile.core.designsystem.component.MifosOutlinedTextField
6461
import org.mifos.mobile.core.designsystem.component.MifosPasswordField
6562
import org.mifos.mobile.core.designsystem.component.MifosScaffold
@@ -70,7 +67,9 @@ import org.mifos.mobile.core.designsystem.theme.DesignToken
7067
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
7168
import org.mifos.mobile.core.designsystem.theme.MifosTypography
7269
import org.mifos.mobile.core.ui.component.MifosPoweredCard
70+
import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay
7371
import org.mifos.mobile.core.ui.utils.EventsEffect
72+
import org.mifos.mobile.core.ui.utils.ScreenUiState
7473

7574
@Composable
7675
internal fun LoginScreen(
@@ -132,11 +131,21 @@ private fun LoginScreen(
132131
}
133132
},
134133
) {
135-
LoginScreenContent(
136-
modifier = modifier,
137-
state = state,
138-
onAction = onAction,
139-
)
134+
when (state.uiState) {
135+
ScreenUiState.Success -> {
136+
LoginScreenContent(
137+
modifier = modifier,
138+
state = state,
139+
onAction = onAction,
140+
)
141+
142+
if (state.showOverlay) {
143+
MifosProgressIndicatorOverlay()
144+
}
145+
}
146+
147+
else -> {}
148+
}
140149
}
141150
}
142151

@@ -153,10 +162,6 @@ private fun LoginDialogs(
153162
onDismissRequest = onDismissRequest,
154163
)
155164

156-
is LoginState.DialogState.Loading -> MifosLoadingDialog(
157-
visibilityState = LoadingDialogState.Shown,
158-
)
159-
160165
null -> Unit
161166
}
162167
}
@@ -243,11 +248,6 @@ fun InputBox(
243248
label = stringResource(Res.string.feature_sign_in_username_label),
244249
shape = DesignToken.shapes.medium,
245250
textStyle = MifosTypography.bodyLarge,
246-
colors = OutlinedTextFieldDefaults.colors(
247-
focusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
248-
unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
249-
errorBorderColor = MaterialTheme.colorScheme.error,
250-
),
251251
config = MifosTextFieldConfig(
252252
isError = state.isError,
253253
errorText = state.userNameError.takeIf { state.isError }?.let { stringResource(it) },
@@ -277,11 +277,6 @@ fun InputBox(
277277
showPasswordChange = {
278278
onAction(LoginAction.TogglePasswordVisibility)
279279
},
280-
colors = OutlinedTextFieldDefaults.colors(
281-
focusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
282-
unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
283-
errorBorderColor = MaterialTheme.colorScheme.error,
284-
),
285280
isError = state.isError,
286281
hint = state.passwordError.takeIf { state.isError }?.let { stringResource(it) },
287282
)
@@ -342,7 +337,7 @@ fun InputBox(
342337
private fun LoanScreenPreview() {
343338
MifosMobileTheme {
344339
LoginScreen(
345-
state = LoginState(dialogState = null),
340+
state = LoginState(uiState = ScreenUiState.Success),
346341
onAction = {},
347342
)
348343
}

0 commit comments

Comments
 (0)