[MW-225] feat(auth): Implement full Sign In & Sign Up flow with validation (#1872)

This commit is contained in:
Hekmatullah 2025-06-19 10:39:26 +01:00 committed by GitHub
parent 3992e6f696
commit df938aba4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 22975 additions and 419 deletions

View File

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="mifospay-ios" type="KmmRunConfiguration" factoryName="iOS Application" CONFIG_VERSION="1" XCODE_PROJECT="$PROJECT_DIR$/mifospay-ios/iosApp.xcodeproj">
<configuration default="false" name="mifospay-ios" type="KmmRunConfiguration" factoryName="iOS Application" CONFIG_VERSION="1" XCODE_PROJECT="$PROJECT_DIR$/mifospay-ios/iosApp.xcodeproj" XCODE_CONFIGURATION="Debug" XCODE_SCHEME="iosApp">
<method v="2">
<option name="com.jetbrains.kmm.ios.BuildIOSAppTask" enabled="true" />
</method>

View File

@ -10,6 +10,8 @@
plugins {
alias(libs.plugins.mifospay.kmp.library)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrainsCompose)
}
android {
@ -40,6 +42,8 @@ kotlin {
api(libs.squareup.okio)
api(libs.jb.kotlin.stdlib)
api(libs.kotlinx.datetime)
implementation(compose.components.resources)
implementation(libs.jb.composeRuntime)
}
androidMain.dependencies {

View File

@ -0,0 +1,98 @@
/*
* 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-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.common.dialogManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.jetbrains.compose.resources.StringResource
/**
* A singleton for managing dialog state across the app.
* It works with [DialogMessage] to determine what kind of dialog should be shown.
*
* This is typically observed in the UI to display a loading spinner or error message.
*/
object DialogManager {
private val _dialogMessage = MutableStateFlow<DialogMessage>(DialogMessage.None)
/**
* Public read-only dialog message flow.
* UI layers should collect this to react to dialog state changes.
*/
val dialogMessage: StateFlow<DialogMessage> = _dialogMessage.asStateFlow()
/**
* Dismisses any currently shown dialog.
*
* ### Example:
* ```
* DialogManager.dismissDialog()
* ```
*/
fun dismissDialog() {
_dialogMessage.value = DialogMessage.None
}
/**
* Shows a loading dialog.
*
* ### Example:
* ```
* DialogManager.showLoading()
* ```
*/
fun showLoading() {
_dialogMessage.value = DialogMessage.Loading
}
/**
* Shows a plain string message in a dialog.
*
* @param message The text to display.
*
* ### Example:
* ```
* DialogManager.showMessage("Invalid email address.")
* ```
*/
fun showMessage(message: String) {
_dialogMessage.value = DialogMessage.StringMessage(message)
}
/**
* Shows a localized string resource message in a dialog.
*
* @param message The resource ID to display.
*
* ### Example:
* ```
* DialogManager.showMessage(Res.string.feature_auth_error_email_required)
* ```
*/
fun showMessage(message: StringResource) {
_dialogMessage.value = DialogMessage.ResourceMessage(message)
}
/**
* Allows setting any [DialogMessage] manually.
*
* @param message The [DialogMessage] to show.
*
* ### Example:
* ```
* DialogManager.showMessage(DialogMessage.Loading)
* ```
*/
fun showMessage(message: DialogMessage) {
_dialogMessage.value = message
}
}

View File

@ -0,0 +1,68 @@
/*
* 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-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.common.dialogManager
import org.jetbrains.compose.resources.StringResource
/**
* Represents the possible types of dialog messages shown in the application.
* Used in combination with [DialogManager] to control dialog visibility and content.
*/
sealed interface DialogMessage {
/**
* Represents the absence of a dialog message.
* Used to indicate that no dialog should be displayed.
*/
data object None : DialogMessage
/**
* Represents a loading dialog.
* Used to indicate an ongoing operation to the user.
*/
data object Loading : DialogMessage
/**
* Represents a dialog that shows a plain string message.
*
* @property message The message to be displayed in the dialog.
*/
class StringMessage(val message: String) : DialogMessage
/**
* Represents a dialog that shows a localized string resource message.
*
* @property message A [StringResource] ID for localization.
*/
class ResourceMessage(val message: StringResource) : DialogMessage
companion object {
/**
* Converts a [Throwable] into a [DialogMessage].
* Uses [StringMessage] if the exception has a non-blank message,
* otherwise falls back to a generic error.
*
* @return [StringMessage] based on content.
*
* ### Example:
* ```
* try {
* doSomethingRisky()
* } catch (e: Exception) {
* DialogManager.showMessage(e.toDialogMessage())
* }
* ```
*/
fun Throwable.toDialogMessage(): DialogMessage {
return StringMessage(message ?: "Unknown error occurred.")
}
}
}

View File

@ -0,0 +1,43 @@
/*
* 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-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.common.utils
/**
* Formats a list of strings into a bullet-pointed multiline string.
*
* Each non-blank line is trimmed and prefixed with a Unicode bullet () followed by a space.
* Empty or whitespace-only strings are ignored.
*
* @param lines A list of strings to be formatted.
* @return A single string where each line is prefixed with a bullet point and separated by a newline.
*
* ### Example:
* ```
* val feedback = listOf(
* "The password must be at least 12 characters long",
* "Include at least one number"
* )
*
* val result = formatAsBulletPoints(feedback)
* println(result)
* ```
*
* ### Output:
* ```
* The password must be at least 12 characters long
* Include at least one number
* ```
*/
fun formatAsBulletPoints(lines: List<String>): String {
return lines
.map { it.trim() }
.filter { it.isNotEmpty() }
.joinToString(separator = "\n") { "$it" }
}

View File

@ -11,6 +11,8 @@ plugins {
alias(libs.plugins.mifospay.kmp.library)
alias(libs.plugins.kotlin.parcelize)
id("kotlinx-serialization")
alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrainsCompose)
}
android {
@ -32,6 +34,8 @@ kotlin {
implementation(projects.core.network)
implementation(projects.core.analytics)
implementation(libs.kotlinx.serialization.json)
implementation(libs.jb.composeRuntime)
implementation(compose.components.resources)
}
androidMain.dependencies {

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.mifospay.core.common.MifosDispatchers
import org.mifospay.core.data.repository.AccountRepository
import org.mifospay.core.data.repository.AssetRepository
import org.mifospay.core.data.repository.AuthenticationRepository
import org.mifospay.core.data.repository.BeneficiaryRepository
import org.mifospay.core.data.repository.ClientRepository
@ -32,25 +33,26 @@ import org.mifospay.core.data.repository.StandingInstructionRepository
import org.mifospay.core.data.repository.ThirdPartyTransferRepository
import org.mifospay.core.data.repository.TwoFactorAuthRepository
import org.mifospay.core.data.repository.UserRepository
import org.mifospay.core.data.repositoryImp.AccountRepositoryImpl
import org.mifospay.core.data.repositoryImp.AuthenticationRepositoryImpl
import org.mifospay.core.data.repositoryImp.BeneficiaryRepositoryImpl
import org.mifospay.core.data.repositoryImp.ClientRepositoryImpl
import org.mifospay.core.data.repositoryImp.DocumentRepositoryImpl
import org.mifospay.core.data.repositoryImp.InvoiceRepositoryImpl
import org.mifospay.core.data.repositoryImp.KycLevelRepositoryImpl
import org.mifospay.core.data.repositoryImp.LocalAssetRepositoryImpl
import org.mifospay.core.data.repositoryImp.NotificationRepositoryImpl
import org.mifospay.core.data.repositoryImp.RegistrationRepositoryImpl
import org.mifospay.core.data.repositoryImp.RunReportRepositoryImpl
import org.mifospay.core.data.repositoryImp.SavedCardRepositoryImpl
import org.mifospay.core.data.repositoryImp.SavingsAccountRepositoryImpl
import org.mifospay.core.data.repositoryImp.SearchRepositoryImpl
import org.mifospay.core.data.repositoryImp.SelfServiceRepositoryImpl
import org.mifospay.core.data.repositoryImp.StandingInstructionRepositoryImpl
import org.mifospay.core.data.repositoryImp.ThirdPartyTransferRepositoryImpl
import org.mifospay.core.data.repositoryImp.TwoFactorAuthRepositoryImpl
import org.mifospay.core.data.repositoryImp.UserRepositoryImpl
import org.mifospay.core.data.repositoryImpl.AccountRepositoryImpl
import org.mifospay.core.data.repositoryImpl.AssetRepositoryImpl
import org.mifospay.core.data.repositoryImpl.AuthenticationRepositoryImpl
import org.mifospay.core.data.repositoryImpl.BeneficiaryRepositoryImpl
import org.mifospay.core.data.repositoryImpl.ClientRepositoryImpl
import org.mifospay.core.data.repositoryImpl.DocumentRepositoryImpl
import org.mifospay.core.data.repositoryImpl.InvoiceRepositoryImpl
import org.mifospay.core.data.repositoryImpl.KycLevelRepositoryImpl
import org.mifospay.core.data.repositoryImpl.LocalAssetRepositoryImpl
import org.mifospay.core.data.repositoryImpl.NotificationRepositoryImpl
import org.mifospay.core.data.repositoryImpl.RegistrationRepositoryImpl
import org.mifospay.core.data.repositoryImpl.RunReportRepositoryImpl
import org.mifospay.core.data.repositoryImpl.SavedCardRepositoryImpl
import org.mifospay.core.data.repositoryImpl.SavingsAccountRepositoryImpl
import org.mifospay.core.data.repositoryImpl.SearchRepositoryImpl
import org.mifospay.core.data.repositoryImpl.SelfServiceRepositoryImpl
import org.mifospay.core.data.repositoryImpl.StandingInstructionRepositoryImpl
import org.mifospay.core.data.repositoryImpl.ThirdPartyTransferRepositoryImpl
import org.mifospay.core.data.repositoryImpl.TwoFactorAuthRepositoryImpl
import org.mifospay.core.data.repositoryImpl.UserRepositoryImpl
import org.mifospay.core.data.util.NetworkMonitor
import org.mifospay.core.data.util.TimeZoneMonitor
@ -60,6 +62,7 @@ private val unconfined = named(MifosDispatchers.Unconfined.name)
val RepositoryModule = module {
single<Json> { Json { ignoreUnknownKeys = true } }
single<AssetRepository> { AssetRepositoryImpl() }
single<AccountRepository> { AccountRepositoryImpl(get(), get(ioDispatcher)) }
single<AuthenticationRepository> {
AuthenticationRepositoryImpl(get(), get(ioDispatcher))

View File

@ -0,0 +1,16 @@
/*
* 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-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repository
import org.mifospay.core.common.DataState
interface AssetRepository {
suspend fun getCountriesWithStates(): DataState<Map<String, List<String>>>
}

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -0,0 +1,36 @@
/*
* 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-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImpl
import kotlinx.serialization.json.Json
import mobile_wallet.core.data.generated.resources.Res
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.mifospay.core.common.DataState
import org.mifospay.core.data.repository.AssetRepository
import org.mifospay.core.model.utils.Country
class AssetRepositoryImpl : AssetRepository {
@OptIn(ExperimentalResourceApi::class)
override suspend fun getCountriesWithStates(): DataState<Map<String, List<String>>> {
return try {
val json = Json { ignoreUnknownKeys = true }
val bytes = Res.readBytes("files/countries.json")
val jsonString = bytes.decodeToString()
val countries = json.decodeFromString<List<Country>>(jsonString)
DataState.Success(
countries.associate { country ->
country.name to country.states.map { it.name }
},
)
} catch (e: Exception) {
DataState.Error(e)
}
}
}

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineDispatcher

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -7,7 +7,7 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.repositoryImp
package org.mifospay.core.data.repositoryImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

View File

@ -53,7 +53,7 @@ fun MifosOutlinedTextField(
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
isError: Boolean = false,
errorMessage: String = "",
errorMessage: String? = null,
singleLine: Boolean = false,
showClearIcon: Boolean = true,
readOnly: Boolean = false,
@ -74,9 +74,10 @@ fun MifosOutlinedTextField(
value = value,
onValueChange = onValueChange,
label = label,
isError = isError,
readOnly = readOnly,
supportingText = {
if (isError) {
supportingText = errorMessage?.let {
{
Text(text = errorMessage)
}
},

View File

@ -0,0 +1,29 @@
/*
* 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-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.model.utils
import kotlinx.serialization.Serializable
@Serializable
data class Country(
val code2: String,
val code3: String,
val name: String,
val capital: String,
val region: String,
val subregion: String,
val states: List<State>,
)
@Serializable
data class State(
val code: String,
val name: String,
)

View File

@ -12,4 +12,17 @@
<string name="core_ui_retry">Retry</string>
<string name="core_ui_error_occurred">Error Occurred!</string>
<string name="core_ui_try_again">Please check your connection or try again</string>
<string name="core_ui_password_requirements">Password Requirements</string>
<string name="core_ui_error_icon_description">Error</string>
<!-- Password Strength Levels -->
<string-array name="core_ui_password_strength_labels">
<item /> <!-- NONE -->
<item>Weak</item> <!-- WEAK_1 -->
<item>Weak</item> <!-- WEAK_2 -->
<item>Weak</item> <!-- WEAK_3 -->
<item>Good</item> <!-- GOOD -->
<item>Strong</item> <!-- STRONG -->
<item>Very Strong</item> <!-- VERY_STRONG -->
</string-array>
</resources>

View File

@ -9,6 +9,7 @@
*/
package org.mifospay.core.ui
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
@ -49,13 +50,16 @@ fun MifosPasswordField(
readOnly: Boolean = false,
singleLine: Boolean = true,
hint: String? = null,
isError: Boolean = false,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val focusRequester = remember { FocusRequester() }
MifosCustomTextField(
modifier = modifier
.tabNavigation()
@ -74,6 +78,7 @@ fun MifosPasswordField(
keyboardType = keyboardType,
imeAction = imeAction,
),
isError = isError,
keyboardActions = keyboardActions,
supportingText = hint?.let {
{
@ -91,9 +96,9 @@ fun MifosPasswordField(
),
) {
val imageVector = if (showPassword) {
MifosIcons.OutlinedVisibilityOff
} else {
MifosIcons.OutlinedVisibility
} else {
MifosIcons.OutlinedVisibilityOff
}
Icon(
@ -106,6 +111,7 @@ fun MifosPasswordField(
textStyle = TextStyle(
color = MaterialTheme.colorScheme.onSurface,
),
interactionSource = interactionSource,
)
if (autoFocus) {
LaunchedEffect(Unit) { focusRequester.requestFocus() }

View File

@ -10,8 +10,10 @@
package org.mifospay.core.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -21,11 +23,16 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -33,14 +40,224 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import mobile_wallet.core.ui.generated.resources.Res
import mobile_wallet.core.ui.generated.resources.core_ui_error_icon_description
import mobile_wallet.core.ui.generated.resources.core_ui_password_requirements
import mobile_wallet.core.ui.generated.resources.core_ui_password_strength_labels
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringArrayResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.mifospay.core.designsystem.icon.MifosIcons
import org.mifospay.core.designsystem.theme.MifosTheme
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun CombinedPasswordErrorCard(
passwordStrengthState: PasswordStrengthState,
currentCharacterCount: Int,
modifier: Modifier = Modifier,
errorText: StringResource? = null,
errors: List<String> = emptyList(),
minimumCharacterCount: Int? = null,
isPasswordFieldFocused: Boolean = false,
) {
val hasErrors = errorText != null || errors.isNotEmpty()
val widthPercent by animateFloatAsState(
targetValue = when (passwordStrengthState) {
PasswordStrengthState.NONE -> 0f
PasswordStrengthState.WEAK_1 -> .25f
PasswordStrengthState.WEAK_2 -> .5f
PasswordStrengthState.WEAK_3 -> .66f
PasswordStrengthState.GOOD -> .82f
PasswordStrengthState.STRONG -> 1f
PasswordStrengthState.VERY_STRONG -> 1f
},
label = "Width Percent State",
)
val indicatorColor = when (passwordStrengthState) {
PasswordStrengthState.NONE -> MaterialTheme.colorScheme.error
PasswordStrengthState.WEAK_1 -> MaterialTheme.colorScheme.error
PasswordStrengthState.WEAK_2 -> MaterialTheme.colorScheme.error
PasswordStrengthState.WEAK_3 -> weakColor
PasswordStrengthState.GOOD -> MaterialTheme.colorScheme.primary
PasswordStrengthState.STRONG -> strongColor
PasswordStrengthState.VERY_STRONG -> Color.Magenta
}
val animatedIndicatorColor by animateColorAsState(
targetValue = indicatorColor,
label = "Indicator Color State",
)
val strengthLabels = stringArrayResource(resource = Res.array.core_ui_password_strength_labels)
val strengthLabel = strengthLabels[passwordStrengthState.ordinal]
AnimatedVisibility(visible = hasErrors || isPasswordFieldFocused) {
Card(
modifier = modifier
.fillMaxWidth()
.testTag("passwordErrorCard"),
shape = MaterialTheme.shapes.medium,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.05f),
),
border = BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.error.copy(alpha = 0.2f),
),
) {
Column {
// Top border strength indicator
Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.background(MaterialTheme.colorScheme.surfaceContainerHigh),
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
.graphicsLayer {
transformOrigin =
TransformOrigin(pivotFractionX = 0f, pivotFractionY = 0f)
scaleX = widthPercent
}
.drawBehind {
drawRect(animatedIndicatorColor)
},
)
}
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
// Header row with "Password Requirements" and strength label
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(
imageVector = MifosIcons.OutlinedInfo,
contentDescription = stringResource(Res.string.core_ui_error_icon_description),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp),
)
Text(
text = stringResource(Res.string.core_ui_password_requirements),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Medium,
)
}
if (strengthLabel.isNotEmpty()) {
Box(
modifier = Modifier
.background(
color = animatedIndicatorColor,
shape = RoundedCornerShape(4.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = strengthLabel,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Medium,
)
}
}
}
// Minimum character count indicator if provided
minimumCharacterCount?.let { minCount ->
MinimumCharacterCount(
minimumRequirementMet = currentCharacterCount >= minCount,
minimumCharacterCount = minCount,
)
}
// Error text if provided
errorText?.let {
Row(
modifier = Modifier
.fillMaxWidth()
.testTag("passwordError"),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier = Modifier
.size(4.dp)
.background(
color = MaterialTheme.colorScheme.error,
shape = CircleShape,
)
.padding(top = 6.dp),
)
Text(
text = stringResource(it),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.weight(1f),
)
}
}
// Error list
errors.forEachIndexed { index, error ->
Row(
modifier = Modifier
.fillMaxWidth()
.testTag("passwordError_$index"),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.CenterHorizontally,
),
) {
Box(
modifier = Modifier
.size(4.dp)
.background(
color = MaterialTheme.colorScheme.error,
shape = CircleShape,
)
.padding(top = 6.dp),
)
Text(
text = error,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.weight(1f),
)
}
}
}
}
}
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
@Composable
fun PasswordStrengthIndicator(
@ -74,15 +291,10 @@ fun PasswordStrengthIndicator(
targetValue = indicatorColor,
label = "Indicator Color State",
)
val label = when (state) {
PasswordStrengthState.NONE -> ""
PasswordStrengthState.WEAK_1 -> "Weak"
PasswordStrengthState.WEAK_2 -> "Weak"
PasswordStrengthState.WEAK_3 -> "Weak"
PasswordStrengthState.GOOD -> "Good"
PasswordStrengthState.STRONG -> "Strong"
PasswordStrengthState.VERY_STRONG -> "Very Strong"
}
val strengthLabels = stringArrayResource(resource = Res.array.core_ui_password_strength_labels)
val strengthLabel = strengthLabels[state.ordinal]
Column(
modifier = modifier,
) {
@ -118,7 +330,7 @@ fun PasswordStrengthIndicator(
)
}
Text(
text = label,
text = strengthLabel,
style = MaterialTheme.typography.labelSmall,
color = indicatorColor,
)

View File

@ -21,20 +21,7 @@ object PasswordChecker {
private const val MAX_PASSWORD_LENGTH = 50
fun getPasswordStrengthResult(password: String): PasswordStrengthResult {
val errors = buildList {
if (password.isEmpty()) add("Password cannot be empty.")
if (password.length > MAX_PASSWORD_LENGTH) add("Password is too long. Maximum length is $MAX_PASSWORD_LENGTH characters.")
if (password.hasSpaces()) add("Password must not contain spaces.")
if (password.hasConsecutiveRepetitions()) add("Password must not contain consecutive repetitive characters.")
}
if (errors.isNotEmpty()) {
return PasswordStrengthResult.Error(errors.joinToString("\n"))
}
val result = getPasswordStrength(password)
return PasswordStrengthResult.Success(result)
return PasswordStrengthResult.Success(getPasswordStrength(password))
}
private fun getPasswordStrength(password: String): PasswordStrength {
@ -51,9 +38,10 @@ object PasswordChecker {
return when {
length < MIN_PASSWORD_LENGTH -> PasswordStrength.LEVEL_0
numTypesPresent == 1 -> PasswordStrength.LEVEL_1
numTypesPresent == 2 || numTypesPresent == 3 -> PasswordStrength.LEVEL_2
numTypesPresent == 2 -> PasswordStrength.LEVEL_2
numTypesPresent == 4 && length >= STRONG_PASSWORD_LENGTH &&
entropyBits >= MIN_ENTROPY_BITS -> PasswordStrength.LEVEL_5
numTypesPresent == 4 && length >= STRONG_PASSWORD_LENGTH -> PasswordStrength.LEVEL_4
else -> PasswordStrength.LEVEL_3
@ -65,11 +53,16 @@ object PasswordChecker {
return log2(charPool.toDouble().pow(password.length))
}
// TODO: Move password feedback messages to string.xml — currently not possible as SignUpState uses Parcelable
// and cannot hold List<StringResource>; revisit when SavedStateHandle usage is decoupled from state.
fun getPasswordFeedback(password: String): List<String> {
val feedback = mutableListOf<String>()
if (password.length < MIN_PASSWORD_LENGTH) {
feedback.add("Password should be at least $MIN_PASSWORD_LENGTH characters long.")
feedback.add("The password must be at least $MIN_PASSWORD_LENGTH characters long.")
}
if (password.length > MAX_PASSWORD_LENGTH) {
feedback.add("The password must not exceed $MAX_PASSWORD_LENGTH characters.")
}
if (!password.any { it.isUpperCase() }) {
feedback.add("Include at least one uppercase letter.")
@ -83,14 +76,11 @@ object PasswordChecker {
if (!password.any { !it.isLetterOrDigit() }) {
feedback.add("Include at least one special character.")
}
if (password.length < STRONG_PASSWORD_LENGTH) {
feedback.add("For a stronger password, use at least $STRONG_PASSWORD_LENGTH characters.")
}
if (password.hasConsecutiveRepetitions()) {
feedback.add("Remove consecutive repeating characters.")
feedback.add("Avoid using consecutive repeated characters.")
}
if (password.hasSpaces()) {
feedback.add("Remove spaces.")
feedback.add("Do not include spaces in the password.")
}
return feedback

View File

@ -31,6 +31,7 @@ kotlin {
implementation(compose.components.uiToolingPreview)
implementation(libs.jb.kotlin.stdlib)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.serialization.json)
}
androidMain.dependencies {

View File

@ -9,6 +9,7 @@
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources>
<!-- General Auth -->
<string name="feature_auth_login">Login</string>
<string name="feature_auth_welcome_back">Welcome back!</string>
<string name="feature_auth_sign_up">Sign up.</string>
@ -24,6 +25,8 @@
<string name="feature_auth_validate_email">Please enter a valid email address</string>
<string name="feature_auth_all_fields_are_mandatory">All fields are mandatory.</string>
<string name="feature_auth_complete_your_registration">Complete your registration</string>
<!-- Labels -->
<string name="feature_auth_first_name">First Name</string>
<string name="feature_auth_last_name">Last Name</string>
<string name="feature_auth_email">E-mail</string>
@ -35,14 +38,14 @@
<string name="feature_auth_state">State</string>
<string name="feature_auth_country">Country</string>
<string name="feature_auth_mobile_no">Mobile No</string>
<string name="feature_auth_phone_number">Phone Number</string>
<string name="feature_auth_complete">Complete</string>
<string name="feature_auth_please_wait">Please wait…</string>
<string name="feature_auth_mandatory">Mandatory field</string>
<string name="feature_auth_password_cannot_be_empty">Password cannot be empty</string>
<string name="feature_auth_password_must_be_least_6_characters">Password must be at least 6 characters</string>
<string name="feature_auth_confirm_password_cannot_empty">Confirm password cannot be empty</string>
<string name="feature_auth_passwords_do_not_match">Passwords do not match</string>
<!-- Password-->
<string name="feature_auth_password_strength">Password Strength:</string>
<string name="feature_auth_password_requirements">Please ensure password contains :\n- At least one uppercase character\n- At least one lowercase character\n- At least one numeric digit\n- At least one special character\n- With no space or consecutive repeating</string>
<!-- OTP Flow -->
<string name="feature_auth_enter_mobile_number">Enter Mobile Number</string>
<string name="feature_auth_enter_otp">Enter OTP</string>
<string name="feature_auth_enter_mobile_number_description">It should be currently activated on your device
@ -51,5 +54,23 @@
<string name="feature_auth_enter_otp_received">Enter otp received on your registered device</string>
<string name="feature_auth_verify_phone">Verify Phone</string>
<string name="feature_auth_verify_otp">Verify Otp</string>
<string name="feature_auth_phone_number">Phone Number</string>
<!-- Sign-up Validation Errors -->
<string name="feature_auth_error_select_savings_account">Please select a savings account.</string>
<string name="feature_auth_error_first_name_required">Please enter your first name.</string>
<string name="feature_auth_error_last_name_required">Please enter your last name.</string>
<string name="feature_auth_error_username_required">Please enter your username.</string>
<string name="feature_auth_error_email_required">Please enter your email.</string>
<string name="feature_auth_error_email_invalid">Please enter a valid email.</string>
<string name="feature_auth_error_mobile_required">Please enter your mobile number.</string>
<string name="feature_auth_error_mobile_invalid">Mobile number must be 10 digits long.</string>
<string name="feature_auth_error_password_required">The password field cannot be empty.</string>
<string name="feature_auth_error_password_invalid">Your password does not meet requirements.</string>
<string name="feature_auth_error_confirm_password_required">The confirm password field cannot be empty.</string>
<string name="feature_auth_error_passwords_mismatch">Passwords do not match.</string>
<string name="feature_auth_error_address_line1_required">Please enter your address line 1.</string>
<string name="feature_auth_error_address_line2_required">Please enter your address line 2.</string>
<string name="feature_auth_error_pincode_required">Please enter your pin code.</string>
<string name="feature_auth_error_country_required">Please enter your country.</string>
<string name="feature_auth_error_state_required">Please enter your state.</string>
</resources>

View File

@ -77,10 +77,8 @@ class LoginViewModel(
private fun handleLoginResult(action: LoginAction.Internal.ReceiveLoginResult) {
when (action.loginResult) {
is DataState.Error -> {
val message = action.loginResult.exception.message ?: ""
mutableStateFlow.update {
it.copy(dialogState = LoginState.DialogState.Error(message))
it.copy(dialogState = LoginState.DialogState.Error(action.loginResult.message))
}
}

View File

@ -9,6 +9,8 @@
*/
package org.mifospay.feature.auth.signup
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -58,6 +60,8 @@ import mobile_wallet.feature.auth.generated.resources.feature_auth_state
import mobile_wallet.feature.auth.generated.resources.feature_auth_username
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.mifospay.core.common.dialogManager.DialogManager
import org.mifospay.core.common.dialogManager.DialogMessage
import org.mifospay.core.designsystem.component.BasicDialogState
import org.mifospay.core.designsystem.component.LoadingDialogState
import org.mifospay.core.designsystem.component.MifosBasicDialog
@ -67,8 +71,10 @@ import org.mifospay.core.designsystem.component.MifosOutlinedTextField
import org.mifospay.core.designsystem.component.MifosScaffold
import org.mifospay.core.designsystem.component.MifosTopAppBar
import org.mifospay.core.designsystem.icon.MifosIcons
import org.mifospay.core.ui.CombinedPasswordErrorCard
import org.mifospay.core.ui.DropdownBoxItem
import org.mifospay.core.ui.ExposedDropdownBox
import org.mifospay.core.ui.MifosPasswordField
import org.mifospay.core.ui.PasswordStrengthIndicator
import org.mifospay.core.ui.utils.EventsEffect
@Composable
@ -82,6 +88,7 @@ internal fun SignupScreen(
val snackbarHostState = remember { SnackbarHostState() }
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val dialogMessage by DialogManager.dialogMessage.collectAsStateWithLifecycle()
EventsEffect(viewModel) { event ->
when (event) {
@ -96,7 +103,7 @@ internal fun SignupScreen(
}
SignUpDialogs(
dialogState = state.dialogState,
dialogMessage = dialogMessage,
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(SignUpAction.ErrorDialogDismiss) }
},
@ -164,7 +171,6 @@ private fun SignupScreenContent(
value = state.firstNameInput,
label = stringResource(Res.string.feature_auth_first_name),
modifier = Modifier.fillMaxWidth(),
isError = state.firstNameInput.isEmpty(),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
),
@ -179,7 +185,6 @@ private fun SignupScreenContent(
value = state.lastNameInput,
label = stringResource(Res.string.feature_auth_last_name),
modifier = Modifier.fillMaxWidth(),
isError = state.lastNameInput.isEmpty(),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
),
@ -194,7 +199,6 @@ private fun SignupScreenContent(
value = state.userNameInput,
label = stringResource(Res.string.feature_auth_username),
modifier = Modifier.fillMaxWidth(),
isError = state.userNameInput.isEmpty(),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
),
@ -209,7 +213,6 @@ private fun SignupScreenContent(
value = state.emailInput,
label = stringResource(Res.string.feature_auth_email),
modifier = Modifier.fillMaxWidth(),
isError = state.emailInput.isEmpty(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
),
@ -224,7 +227,6 @@ private fun SignupScreenContent(
value = state.mobileNumberInput,
label = stringResource(Res.string.feature_auth_mobile_no),
modifier = Modifier.fillMaxWidth(),
isError = state.mobileNumberInput.isEmpty(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Phone,
),
@ -237,6 +239,8 @@ private fun SignupScreenContent(
item {
Column {
var showPassword by rememberSaveable { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
MifosPasswordField(
value = state.passwordInput,
@ -247,12 +251,15 @@ private fun SignupScreenContent(
},
showPassword = showPassword,
showPasswordChange = { showPassword = !showPassword },
interactionSource = interactionSource,
)
Spacer(modifier = Modifier.height(4.dp))
PasswordStrengthIndicator(
CombinedPasswordErrorCard(
modifier = Modifier.fillMaxWidth(),
state = state.passwordStrengthState,
errors = state.passwordFeedback,
passwordStrengthState = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
isPasswordFieldFocused = isFocused,
)
}
}
@ -277,7 +284,6 @@ private fun SignupScreenContent(
value = state.addressLine1Input,
label = stringResource(Res.string.feature_auth_address_line_1),
modifier = Modifier.fillMaxWidth(),
isError = state.addressLine1Input.isEmpty(),
onValueChange = {
onAction(SignUpAction.AddressLine1InputChange(it))
},
@ -289,7 +295,6 @@ private fun SignupScreenContent(
value = state.addressLine2Input,
modifier = Modifier.fillMaxWidth(),
label = stringResource(Res.string.feature_auth_address_line_2),
isError = state.addressLine2Input.isEmpty(),
onValueChange = {
onAction(SignUpAction.AddressLine2InputChange(it))
},
@ -301,7 +306,6 @@ private fun SignupScreenContent(
value = state.pinCodeInput,
label = stringResource(Res.string.feature_auth_pin_code),
modifier = Modifier.fillMaxWidth(),
isError = state.pinCodeInput.isEmpty(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
@ -317,25 +321,57 @@ private fun SignupScreenContent(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
MifosOutlinedTextField(
value = state.countryInput,
label = stringResource(Res.string.feature_auth_country),
onValueChange = {
onAction(SignUpAction.CountryInputChange(it))
},
modifier = Modifier.weight(1.5f),
isError = state.countryInput.isEmpty(),
)
var isCountryDropdownExpanded by remember { mutableStateOf(false) }
var isStateDropdownExpanded by remember { mutableStateOf(false) }
MifosOutlinedTextField(
value = state.stateInput,
label = stringResource(Res.string.feature_auth_state),
onValueChange = {
onAction(SignUpAction.StateInputChange(it))
},
// Country Dropdown
ExposedDropdownBox(
expanded = isCountryDropdownExpanded,
label = stringResource(Res.string.feature_auth_country),
value = state.countryInput,
onExpandChange = { isCountryDropdownExpanded = it },
modifier = Modifier.weight(1.5f),
isError = state.stateInput.isEmpty(),
)
) {
state.countriesWithStates.keys.forEach { country ->
DropdownBoxItem(
text = country,
onClick = {
onAction(SignUpAction.CountryInputChange(country))
isCountryDropdownExpanded = false
},
)
}
}
// State Dropdown
if (state.statesForSelectedCountry.isNullOrEmpty()) {
MifosOutlinedTextField(
value = state.stateInput,
label = stringResource(Res.string.feature_auth_state),
onValueChange = {
onAction(SignUpAction.StateInputChange(it))
},
modifier = Modifier.weight(1.5f),
)
} else {
ExposedDropdownBox(
expanded = isStateDropdownExpanded,
label = stringResource(Res.string.feature_auth_state),
value = state.stateInput,
onExpandChange = { isStateDropdownExpanded = it },
modifier = Modifier.weight(1.5f),
) {
state.statesForSelectedCountry.forEach { stateName ->
DropdownBoxItem(
text = stateName,
onClick = {
onAction(SignUpAction.StateInputChange(stateName))
isStateDropdownExpanded = false
},
)
}
}
}
}
}
@ -359,21 +395,24 @@ private fun SignupScreenContent(
@Composable
private fun SignUpDialogs(
dialogState: SignUpDialog?,
dialogMessage: DialogMessage,
onDismissRequest: () -> Unit,
) {
when (dialogState) {
is SignUpDialog.Error -> MifosBasicDialog(
visibilityState = BasicDialogState.Shown(
message = dialogState.message,
),
onDismissRequest = onDismissRequest,
)
is SignUpDialog.Loading -> MifosLoadingDialog(
when (dialogMessage) {
is DialogMessage.Loading -> MifosLoadingDialog(
visibilityState = LoadingDialogState.Shown,
)
null -> Unit
is DialogMessage.StringMessage -> MifosBasicDialog(
visibilityState = BasicDialogState.Shown(dialogMessage.message),
onDismissRequest = onDismissRequest,
)
is DialogMessage.ResourceMessage -> MifosBasicDialog(
visibilityState = BasicDialogState.Shown(stringResource(dialogMessage.message)),
onDismissRequest = onDismissRequest,
)
is DialogMessage.None -> Unit
}
}

View File

@ -11,16 +11,41 @@ package org.mifospay.feature.auth.signup
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import mobile_wallet.feature.auth.generated.resources.Res
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_address_line1_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_address_line2_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_confirm_password_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_country_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_email_invalid
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_email_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_first_name_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_last_name_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_mobile_invalid
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_mobile_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_password_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_passwords_mismatch
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_pincode_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_select_savings_account
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_state_required
import mobile_wallet.feature.auth.generated.resources.feature_auth_error_username_required
import org.mifospay.core.common.DataState
import org.mifospay.core.common.IgnoredOnParcel
import org.mifospay.core.common.Parcelable
import org.mifospay.core.common.Parcelize
import org.mifospay.core.common.dialogManager.DialogManager
import org.mifospay.core.common.dialogManager.DialogMessage.Companion.toDialogMessage
import org.mifospay.core.common.utils.formatAsBulletPoints
import org.mifospay.core.common.utils.isValidEmail
import org.mifospay.core.data.repository.AssetRepository
import org.mifospay.core.data.repository.ClientRepository
import org.mifospay.core.data.repository.SearchRepository
import org.mifospay.core.data.repository.UserRepository
@ -36,13 +61,12 @@ import org.mifospay.core.ui.utils.PasswordStrengthResult
import org.mifospay.feature.auth.signup.SignUpAction.Internal.ReceivePasswordStrengthResult
private const val KEY_STATE = "signup_state"
private const val MIN_PASSWORD_LENGTH = 12
private const val MAX_PASSWORD_LENGTH = 50
class SignupViewModel(
private val userRepository: UserRepository,
private val searchRepository: SearchRepository,
private val clientRepository: ClientRepository,
private val assetRepository: AssetRepository,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<SignUpState, SignUpEvent, SignUpAction>(
initialState = savedStateHandle[KEY_STATE] ?: SignUpState(),
@ -71,6 +95,8 @@ class SignupViewModel(
trySendAction(SignUpAction.BusinessNameInputChange(it))
}
}
loadCountriesFromJson()
}
override fun handleAction(action: SignUpAction) {
@ -151,7 +177,11 @@ class SignupViewModel(
is SignUpAction.CountryInputChange -> {
mutableStateFlow.update {
it.copy(countryInput = action.country)
it.copy(
countryInput = action.country,
// reset state when country changes
stateInput = "",
)
}
}
@ -160,9 +190,7 @@ class SignupViewModel(
}
is SignUpAction.ErrorDialogDismiss -> {
mutableStateFlow.update {
it.copy(dialogState = null)
}
DialogManager.dismissDialog()
}
is ReceivePasswordStrengthResult -> handlePasswordStrengthResult(action)
@ -170,12 +198,20 @@ class SignupViewModel(
is SignUpAction.Internal.ReceiveRegisterResult -> handleSignUpResult(action)
is SignUpAction.SubmitClick -> handleSubmitClick()
is SignUpAction.LoadCountries -> loadCountriesFromJson()
}
}
private fun handlePasswordInput(action: SignUpAction.PasswordInputChange) {
// Update input:
mutableStateFlow.update { it.copy(passwordInput = action.password, passwordError = null) }
mutableStateFlow.update {
it.copy(
passwordInput = action.password,
passwordFeedback = PasswordChecker.getPasswordFeedback(action.password)
.toPersistentList(),
)
}
// Update password strength:
passwordStrengthJob.cancel()
if (action.password.isEmpty()) {
@ -206,157 +242,95 @@ class SignupViewModel(
}
}
is PasswordStrengthResult.Error -> {
mutableStateFlow.update {
it.copy(passwordError = result.message.toString(), passwordStrengthState = PasswordStrengthState.NONE)
}
}
is PasswordStrengthResult.Error -> {}
}
}
private fun handleSignUpResult(action: SignUpAction.Internal.ReceiveRegisterResult) {
when (val result = action.registerResult) {
is DataState.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
DialogManager.dismissDialog()
sendEvent(SignUpEvent.NavigateToLogin(result.data))
}
is DataState.Error -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error(result.exception.message.toString()))
}
DialogManager.showMessage(result.exception.toDialogMessage())
}
DataState.Loading -> {
mutableStateFlow.update { it.copy(dialogState = SignUpDialog.Loading) }
DialogManager.showLoading()
}
else -> {}
}
}
// TODO:: move error messages to strings.xml
private fun handleSubmitClick() = when {
state.savingsProductId == 0 -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please select a savings account."))
}
DialogManager.showMessage(Res.string.feature_auth_error_select_savings_account)
}
state.firstNameInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your first name."))
}
DialogManager.showMessage(Res.string.feature_auth_error_first_name_required)
}
state.lastNameInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your last name."))
}
DialogManager.showMessage(Res.string.feature_auth_error_last_name_required)
}
state.userNameInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your username."))
}
DialogManager.showMessage(Res.string.feature_auth_error_username_required)
}
state.emailInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your email."))
}
DialogManager.showMessage(Res.string.feature_auth_error_email_required)
}
!state.emailInput.isValidEmail() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter a valid email."))
}
DialogManager.showMessage(Res.string.feature_auth_error_email_invalid)
}
state.mobileNumberInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your mobile number."))
}
DialogManager.showMessage(Res.string.feature_auth_error_mobile_required)
}
state.mobileNumberInput.length < 10 -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Mobile number must be 10 digits long."))
}
DialogManager.showMessage(Res.string.feature_auth_error_mobile_invalid)
}
state.passwordInput.length < MIN_PASSWORD_LENGTH -> {
mutableStateFlow.update {
it.copy(
dialogState = SignUpDialog.Error(
"Password must be at least $MIN_PASSWORD_LENGTH characters long.",
),
)
}
state.passwordInput.isEmpty() -> {
DialogManager.showMessage(Res.string.feature_auth_error_password_required)
}
state.passwordInput.length > MAX_PASSWORD_LENGTH -> {
mutableStateFlow.update {
it.copy(
dialogState = SignUpDialog.Error(
"Password must be less than $MAX_PASSWORD_LENGTH characters long.",
),
)
}
state.passwordFeedback.isNotEmpty() -> {
val bulletListPasswordFeedback = formatAsBulletPoints(state.passwordFeedback)
DialogManager.showMessage(bulletListPasswordFeedback)
}
!state.isPasswordMatch -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Passwords do not match."))
}
state.confirmPasswordInput.isEmpty() -> {
DialogManager.showMessage(Res.string.feature_auth_error_confirm_password_required)
}
!state.isPasswordStrong -> {
val errorMessage = state.passwordError?.takeIf { it.isNotBlank() }
?: "Please ensure password contains :" +
"\n- At least one uppercase character" +
"\n- At least one lowercase character" +
"\n- At least one numeric digit" +
"\n- At least one special character"
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error(errorMessage.lines().joinToString("\n") { "- $it" }))
}
state.passwordInput != state.confirmPasswordInput -> {
DialogManager.showMessage(Res.string.feature_auth_error_passwords_mismatch)
}
state.addressLine1Input.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your address line 1."))
}
DialogManager.showMessage(Res.string.feature_auth_error_address_line1_required)
}
state.addressLine2Input.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your address line 2."))
}
DialogManager.showMessage(Res.string.feature_auth_error_address_line2_required)
}
state.pinCodeInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your pin code."))
}
}
state.pinCodeInput.length < 6 -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Pin code must be 6 digits long."))
}
DialogManager.showMessage(Res.string.feature_auth_error_pincode_required)
}
state.countryInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your country."))
}
DialogManager.showMessage(Res.string.feature_auth_error_country_required)
}
state.stateInput.isEmpty() -> {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error("Please enter your state."))
}
DialogManager.showMessage(Res.string.feature_auth_error_state_required)
}
else -> initiateSignUp()
@ -366,9 +340,7 @@ class SignupViewModel(
Enhancement: Move the following code in to a Use Case
*/
private fun initiateSignUp() {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Loading)
}
DialogManager.showLoading()
val fieldsToCheck = mapOf(
"Username" to state.userNameInput,
@ -388,20 +360,18 @@ class SignupViewModel(
val errorMessages = results.mapNotNull { (label, result) ->
when (result) {
is DataState.Loading -> {}
is DataState.Success -> {
if (result.data.isNotEmpty()) "$label already exists." else null
}
is DataState.Error ->
result.exception.message
?: "Error checking $label."
else -> null
"Unable to check if $label is unique. Please try again later."
}
}
if (errorMessages.isNotEmpty()) {
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error(errorMessages.joinToString("\n")))
}
DialogManager.showMessage(errorMessages.joinToString("\n"))
} else {
val newUser = NewUser(
state.userNameInput,
@ -419,10 +389,7 @@ class SignupViewModel(
viewModelScope.launch {
when (val result = userRepository.createUser(newUser)) {
is DataState.Error -> {
val message = result.exception.message.toString()
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error(message))
}
DialogManager.showMessage(result.exception.toDialogMessage())
}
is DataState.Success -> {
@ -455,9 +422,7 @@ class SignupViewModel(
is DataState.Error -> {
deleteUser(userId)
val message = result.exception.message.toString()
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error(message))
}
DialogManager.showMessage(message)
}
is DataState.Success -> {
@ -475,16 +440,12 @@ class SignupViewModel(
is DataState.Error -> {
deleteUser(userId)
deleteClient(clientId)
val message = result.exception.message.toString()
mutableStateFlow.update {
it.copy(dialogState = SignUpDialog.Error(message))
}
DialogManager.showMessage(result.exception.toDialogMessage())
}
is DataState.Success -> {
mutableStateFlow.update {
it.copy(dialogState = null)
}
DialogManager.dismissDialog()
sendEvent(SignUpEvent.ShowToast("Registration successful."))
sendAction(
SignUpAction.Internal.ReceiveRegisterResult(
@ -509,6 +470,20 @@ class SignupViewModel(
clientRepository.deleteClient(clientId)
}
}
private fun loadCountriesFromJson() {
viewModelScope.launch {
when (val countriesWithStatesResult = assetRepository.getCountriesWithStates()) {
is DataState.Success ->
mutableStateFlow.update {
it.copy(countriesWithStates = countriesWithStatesResult.data)
}
is DataState.Error -> Logger.d("Failed to load countries.json: ${countriesWithStatesResult.exception.message}")
is DataState.Loading -> {}
}
}
}
}
@Parcelize
@ -527,36 +502,13 @@ data class SignUpState(
val stateInput: String = "",
val countryInput: String = "",
val businessNameInput: String = "",
val dialogState: SignUpDialog? = null,
val passwordStrengthState: PasswordStrengthState = PasswordStrengthState.NONE,
val passwordError: String? = null,
val passwordFeedback: ImmutableList<String> = persistentListOf(),
val countriesWithStates: Map<String, List<String>> = emptyMap(),
) : Parcelable {
@IgnoredOnParcel
val isPasswordStrong: Boolean
get() = when (passwordStrengthState) {
PasswordStrengthState.NONE,
PasswordStrengthState.WEAK_1,
PasswordStrengthState.WEAK_2,
PasswordStrengthState.WEAK_3,
-> false
PasswordStrengthState.GOOD,
PasswordStrengthState.STRONG,
PasswordStrengthState.VERY_STRONG,
-> true
}
@IgnoredOnParcel
val isPasswordMatch: Boolean
get() = passwordInput == confirmPasswordInput
}
sealed interface SignUpDialog : Parcelable {
@Parcelize
data object Loading : SignUpDialog
@Parcelize
data class Error(val message: String) : SignUpDialog
val statesForSelectedCountry =
countriesWithStates[countryInput]
}
sealed interface SignUpEvent {
@ -584,6 +536,7 @@ sealed interface SignUpAction {
data object SubmitClick : SignUpAction
data object CloseClick : SignUpAction
data object ErrorDialogDismiss : SignUpAction
data object LoadCountries : SignUpAction
sealed class Internal : SignUpAction {
data class ReceiveRegisterResult(

View File

@ -914,6 +914,152 @@
| | | | \--- io.insert-koin:koin-annotations-jvm:1.4.0-RC4
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.0 (*)
| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*)
| | | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01
| | | | \--- org.jetbrains.compose.components:components-resources-android:1.7.0-rc01
| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.1.0 (*)
| | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | +--- org.jetbrains.compose.foundation:foundation:1.7.0-rc01
| | | | | +--- androidx.compose.foundation:foundation:1.7.1 -> 1.7.6
| | | | | | \--- androidx.compose.foundation:foundation-android:1.7.6
| | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*)
| | | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | | | +--- androidx.compose.animation:animation:1.7.6
| | | | | | | \--- androidx.compose.animation:animation-android:1.7.6
| | | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*)
| | | | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | | | | +--- androidx.compose.animation:animation-core:1.7.6
| | | | | | | | \--- androidx.compose.animation:animation-core-android:1.7.6
| | | | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | | | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | | | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | | | | | +--- androidx.compose.ui:ui-graphics:1.7.6 (*)
| | | | | | | | +--- androidx.compose.ui:ui-unit:1.7.6 (*)
| | | | | | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.9.0 (*)
| | | | | | | | \--- androidx.compose.animation:animation:1.7.6 (c)
| | | | | | | +--- androidx.compose.foundation:foundation-layout:1.7.6
| | | | | | | | \--- androidx.compose.foundation:foundation-layout-android:1.7.6
| | | | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*)
| | | | | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | | | | | +--- androidx.compose.animation:animation-core:1.2.1 -> 1.7.6 (*)
| | | | | | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | | | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | | | | | +--- androidx.compose.ui:ui-unit:1.7.6 (*)
| | | | | | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | | | | | +--- androidx.core:core:1.7.0 -> 1.15.0 (*)
| | | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | | | | | \--- androidx.compose.foundation:foundation:1.7.6 (c)
| | | | | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | | | | +--- androidx.compose.ui:ui-geometry:1.7.6 (*)
| | | | | | | +--- androidx.compose.ui:ui-graphics:1.7.6 (*)
| | | | | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | | | | \--- androidx.compose.animation:animation-core:1.7.6 (c)
| | | | | | +--- androidx.compose.foundation:foundation-layout:1.7.6 (*)
| | | | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui-text:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | | | +--- androidx.core:core:1.13.1 -> 1.15.0 (*)
| | | | | | +--- androidx.emoji2:emoji2:1.3.0 (*)
| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | | | \--- androidx.compose.foundation:foundation-layout:1.7.6 (c)
| | | | | +--- org.jetbrains.compose.animation:animation:1.7.0-rc01
| | | | | | +--- androidx.compose.animation:animation:1.7.1 -> 1.7.6 (*)
| | | | | | +--- org.jetbrains.compose.animation:animation-core:1.7.0-rc01
| | | | | | | +--- androidx.compose.animation:animation-core:1.7.1 -> 1.7.6 (*)
| | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01
| | | | | | | | +--- androidx.compose.ui:ui:1.7.1 -> 1.7.6 (*)
| | | | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.3-rc01 -> 2.8.3 (*)
| | | | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.3-rc01
| | | | | | | | | +--- androidx.arch.core:core-common:2.2.0 (*)
| | | | | | | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.5 -> 2.8.7 (*)
| | | | | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.3-rc01 -> 2.8.3 (*)
| | | | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.6.11 -> 1.7.0 (*)
| | | | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.0 (*)
| | | | | | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.3-rc01
| | | | | | | | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.5 -> 2.8.7 (*)
| | | | | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.3-rc01 -> 2.8.3 (*)
| | | | | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.3-rc01 (*)
| | | | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.6.11 -> 1.7.0 (*)
| | | | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.6.11 -> 1.7.0 (*)
| | | | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.0 (*)
| | | | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.3-rc01 -> 2.8.3 (*)
| | | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | +--- org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01
| | | | | | | | | +--- androidx.compose.runtime:runtime-saveable:1.7.1 -> 1.7.6 (*)
| | | | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 -> 2.1.0 (*)
| | | | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01
| | | | | | | | | +--- androidx.compose.ui:ui-geometry:1.7.1 -> 1.7.6 (*)
| | | | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01
| | | | | | | | | \--- androidx.compose.ui:ui-util:1.7.1 -> 1.7.6 (*)
| | | | | | | | +--- org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01
| | | | | | | | | +--- androidx.compose.ui:ui-graphics:1.7.1 -> 1.7.6 (*)
| | | | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01
| | | | | | | | | | +--- androidx.compose.ui:ui-unit:1.7.1 -> 1.7.6 (*)
| | | | | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | | | +--- org.jetbrains.compose.ui:ui-text:1.7.0-rc01
| | | | | | | | | +--- androidx.compose.ui:ui-text:1.7.1 -> 1.7.6 (*)
| | | | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | | | +--- org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 (*)
| | | | | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | | | | +--- org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01 (*)
| | | | | | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 (*)
| | | | | | | | | +--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | | | | +--- org.jetbrains.kotlinx:atomicfu:0.23.2 (*)
| | | | | | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 (*)
| | | | | | | | +--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 -> 2.1.0 (*)
| | | | | | | | +--- org.jetbrains.kotlinx:atomicfu:0.23.2 (*)
| | | | | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01
| | | | | | | +--- androidx.compose.foundation:foundation-layout:1.7.1 -> 1.7.6 (*)
| | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.ui:ui-text:1.7.0-rc01 (*)
| | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | +--- org.jetbrains.compose.runtime:runtime:1.7.0 (*)
| | | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0
| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.0 (*)
| | | \--- org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.1.0
@ -1185,6 +1331,8 @@
| | | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*)
| | | \--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 (*)
| | +--- org.jetbrains.compose.runtime:runtime:1.7.0 (*)
| | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01 (*)
| | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0 (*)
| +--- project :core:network (*)
| +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.0 (*)
@ -1206,57 +1354,7 @@
| | +--- androidx.compose.runtime:runtime -> 1.7.6 (*)
| | +--- com.google.accompanist:accompanist-pager:0.34.0
| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.9.0 (*)
| | | +--- androidx.compose.foundation:foundation:1.6.0 -> 1.7.6
| | | | \--- androidx.compose.foundation:foundation-android:1.7.6
| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*)
| | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | +--- androidx.compose.animation:animation:1.7.6
| | | | | \--- androidx.compose.animation:animation-android:1.7.6
| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*)
| | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | | +--- androidx.compose.animation:animation-core:1.7.6
| | | | | | \--- androidx.compose.animation:animation-core-android:1.7.6
| | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui-graphics:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui-unit:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.9.0 (*)
| | | | | | \--- androidx.compose.animation:animation:1.7.6 (c)
| | | | | +--- androidx.compose.foundation:foundation-layout:1.7.6
| | | | | | \--- androidx.compose.foundation:foundation-layout-android:1.7.6
| | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*)
| | | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*)
| | | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.4 (*)
| | | | | | +--- androidx.compose.animation:animation-core:1.2.1 -> 1.7.6 (*)
| | | | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui-unit:1.7.6 (*)
| | | | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | | | +--- androidx.core:core:1.7.0 -> 1.15.0 (*)
| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | | | \--- androidx.compose.foundation:foundation:1.7.6 (c)
| | | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | | +--- androidx.compose.ui:ui-geometry:1.7.6 (*)
| | | | | +--- androidx.compose.ui:ui-graphics:1.7.6 (*)
| | | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | | \--- androidx.compose.animation:animation-core:1.7.6 (c)
| | | | +--- androidx.compose.foundation:foundation-layout:1.7.6 (*)
| | | | +--- androidx.compose.runtime:runtime:1.7.6 (*)
| | | | +--- androidx.compose.ui:ui:1.7.6 (*)
| | | | +--- androidx.compose.ui:ui-text:1.7.6 (*)
| | | | +--- androidx.compose.ui:ui-util:1.7.6 (*)
| | | | +--- androidx.core:core:1.13.1 -> 1.15.0 (*)
| | | | +--- androidx.emoji2:emoji2:1.3.0 (*)
| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (*)
| | | | \--- androidx.compose.foundation:foundation-layout:1.7.6 (c)
| | | +--- androidx.compose.foundation:foundation:1.6.0 -> 1.7.6 (*)
| | | +--- dev.chrisbanes.snapper:snapper:0.2.2
| | | | +--- androidx.compose.foundation:foundation:1.1.1 -> 1.7.6 (*)
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 -> 1.9.20 (*)
@ -1283,65 +1381,7 @@
| | | +--- dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0
| | | | \--- dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0
| | | | +--- androidx.window:window:1.2.0 -> 1.3.0 (*)
| | | | +--- org.jetbrains.compose.ui:ui:1.6.0 -> 1.7.0-rc01
| | | | | +--- androidx.compose.ui:ui:1.7.1 -> 1.7.6 (*)
| | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.3-rc01 -> 2.8.3 (*)
| | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.3-rc01
| | | | | | +--- androidx.arch.core:core-common:2.2.0 (*)
| | | | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.5 -> 2.8.7 (*)
| | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.3-rc01 -> 2.8.3 (*)
| | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.6.11 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.0 (*)
| | | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.3-rc01
| | | | | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.5 -> 2.8.7 (*)
| | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.3-rc01 -> 2.8.3 (*)
| | | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.3-rc01 (*)
| | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.6.11 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime:1.6.11 -> 1.7.0 (*)
| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.0 (*)
| | | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.3-rc01 -> 2.8.3 (*)
| | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01
| | | | | | +--- androidx.compose.runtime:runtime-saveable:1.7.1 -> 1.7.6 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 -> 2.1.0 (*)
| | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01
| | | | | | +--- androidx.compose.ui:ui-geometry:1.7.1 -> 1.7.6 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01
| | | | | | \--- androidx.compose.ui:ui-util:1.7.1 -> 1.7.6 (*)
| | | | | +--- org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01
| | | | | | +--- androidx.compose.ui:ui-graphics:1.7.1 -> 1.7.6 (*)
| | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01
| | | | | | | +--- androidx.compose.ui:ui-unit:1.7.1 -> 1.7.6 (*)
| | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.ui:ui-text:1.7.0-rc01
| | | | | | +--- androidx.compose.ui:ui-text:1.7.1 -> 1.7.6 (*)
| | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.kotlinx:atomicfu:0.23.2 (*)
| | | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 -> 2.1.0 (*)
| | | | | +--- org.jetbrains.kotlinx:atomicfu:0.23.2 (*)
| | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | +--- org.jetbrains.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*)
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.1.0 (*)
| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.0 (*)
| | | +--- io.insert-koin:koin-bom:4.0.1-RC1 (*)
@ -1354,38 +1394,7 @@
| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.9.0 (*)
| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.1.0 (*)
| | | | +--- io.coil-kt.coil3:coil-core:3.0.0-alpha10 (*)
| | | | +--- org.jetbrains.compose.foundation:foundation:1.6.11 -> 1.7.0-rc01
| | | | | +--- androidx.compose.foundation:foundation:1.7.1 -> 1.7.6 (*)
| | | | | +--- org.jetbrains.compose.animation:animation:1.7.0-rc01
| | | | | | +--- androidx.compose.animation:animation:1.7.1 -> 1.7.6 (*)
| | | | | | +--- org.jetbrains.compose.animation:animation-core:1.7.0-rc01
| | | | | | | +--- androidx.compose.animation:animation-core:1.7.1 -> 1.7.6 (*)
| | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01
| | | | | | | +--- androidx.compose.foundation:foundation-layout:1.7.1 -> 1.7.6 (*)
| | | | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | | | | +--- org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 (*)
| | | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | | | +--- org.jetbrains.compose.ui:ui-text:1.7.0-rc01 (*)
| | | | | \--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | | +--- org.jetbrains.compose.foundation:foundation:1.6.11 -> 1.7.0-rc01 (*)
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.10 -> 2.1.0 (*)
| | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | +--- org.jetbrains.compose.foundation:foundation:1.7.0-rc01 (*)
@ -1446,12 +1455,7 @@
| | | | \--- org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01 (*)
| | | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*)
| | | +--- org.jetbrains.compose.ui:ui-util:1.7.0-rc01 (*)
| | | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01
| | | | \--- org.jetbrains.compose.components:components-resources-android:1.7.0-rc01
| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.1.0 (*)
| | | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 -> 1.7.0 (*)
| | | | +--- org.jetbrains.compose.foundation:foundation:1.7.0-rc01 (*)
| | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.9.0 (*)
| | | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01 (*)
| | | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01
| | | \--- org.jetbrains.compose.components:components-ui-tooling-preview-android:1.7.0-rc01
| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.1.0 (*)
@ -1656,6 +1660,7 @@
| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.0 (*)
| | +--- org.jetbrains.kotlin:kotlin-reflect:2.1.0
| | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.0 (*)
| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 (*)
| | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0 (*)
| +--- project :libs:mifos-passcode
| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.7 (*)

View File

@ -1,4 +1,4 @@
package: name='org.mifospay' versionCode='1' versionName='2025.4.4-beta.0.0' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
package: name='org.mifospay' versionCode='1' versionName='2025.6.4-beta.0.10' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
sdkVersion:'26'
targetSdkVersion:'34'
uses-permission: name='android.permission.INTERNET'