mirror of
https://github.com/openMF/mobile-wallet.git
synced 2026-02-06 11:36:57 +00:00
feat : send money flow change (#1926)
This commit is contained in:
parent
9b4ee9de23
commit
e3061d29cd
@ -45,7 +45,7 @@
|
|||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.MifosSplash"
|
android:theme="@style/Theme.MifosSplash"
|
||||||
android:windowSoftInputMode="adjustPan|adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.navOptions
|
import androidx.navigation.navOptions
|
||||||
import org.mifospay.core.ui.utility.TabContent
|
import org.mifospay.core.ui.utility.TabContent
|
||||||
import org.mifospay.feature.accounts.AccountsScreen
|
import org.mifospay.feature.accounts.AccountsScreen
|
||||||
|
import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType
|
||||||
import org.mifospay.feature.accounts.beneficiary.addEditBeneficiaryScreen
|
import org.mifospay.feature.accounts.beneficiary.addEditBeneficiaryScreen
|
||||||
import org.mifospay.feature.accounts.beneficiary.navigateToBeneficiaryAddEdit
|
import org.mifospay.feature.accounts.beneficiary.navigateToBeneficiaryAddEdit
|
||||||
import org.mifospay.feature.accounts.savingsaccount.SavingsAddEditType
|
import org.mifospay.feature.accounts.savingsaccount.SavingsAddEditType
|
||||||
@ -47,6 +48,8 @@ import org.mifospay.feature.make.transfer.navigation.navigateToTransferScreen
|
|||||||
import org.mifospay.feature.make.transfer.navigation.transferScreen
|
import org.mifospay.feature.make.transfer.navigation.transferScreen
|
||||||
import org.mifospay.feature.make.transfer.success.navigateTransferSuccess
|
import org.mifospay.feature.make.transfer.success.navigateTransferSuccess
|
||||||
import org.mifospay.feature.make.transfer.success.transferSuccessScreen
|
import org.mifospay.feature.make.transfer.success.transferSuccessScreen
|
||||||
|
import org.mifospay.feature.make.transfer.v2.makeTransferScreenV2
|
||||||
|
import org.mifospay.feature.make.transfer.v2.navigateToMakeTransferScreenV2
|
||||||
import org.mifospay.feature.merchants.navigation.merchantTransferScreen
|
import org.mifospay.feature.merchants.navigation.merchantTransferScreen
|
||||||
import org.mifospay.feature.notification.navigateToNotification
|
import org.mifospay.feature.notification.navigateToNotification
|
||||||
import org.mifospay.feature.notification.notificationScreen
|
import org.mifospay.feature.notification.notificationScreen
|
||||||
@ -65,6 +68,10 @@ import org.mifospay.feature.savedcards.details.cardDetailRoute
|
|||||||
import org.mifospay.feature.send.money.navigation.SEND_MONEY_BASE_ROUTE
|
import org.mifospay.feature.send.money.navigation.SEND_MONEY_BASE_ROUTE
|
||||||
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen
|
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen
|
||||||
import org.mifospay.feature.send.money.navigation.sendMoneyScreen
|
import org.mifospay.feature.send.money.navigation.sendMoneyScreen
|
||||||
|
import org.mifospay.feature.send.money.selectScreen.navigateToSelectAccountScreen
|
||||||
|
import org.mifospay.feature.send.money.selectScreen.selectAccountScreenDestination
|
||||||
|
import org.mifospay.feature.send.money.v2.navigateToSendMoneyV2Screen
|
||||||
|
import org.mifospay.feature.send.money.v2.sendMoneyScreenDestination
|
||||||
import org.mifospay.feature.settings.navigation.settingsScreen
|
import org.mifospay.feature.settings.navigation.settingsScreen
|
||||||
import org.mifospay.feature.standing.instruction.createOrUpdate.addEditSIScreen
|
import org.mifospay.feature.standing.instruction.createOrUpdate.addEditSIScreen
|
||||||
import org.mifospay.feature.standing.instruction.details.siDetailsScreen
|
import org.mifospay.feature.standing.instruction.details.siDetailsScreen
|
||||||
@ -150,7 +157,7 @@ internal fun MifosNavHost(
|
|||||||
onRequest = {
|
onRequest = {
|
||||||
navController.navigateToShowQrScreen()
|
navController.navigateToShowQrScreen()
|
||||||
},
|
},
|
||||||
onPay = navController::navigateToSendMoneyScreen,
|
onPay = navController::navigateToSendMoneyV2Screen,
|
||||||
navigateToTransactionDetail = navController::navigateToSpecificTransaction,
|
navigateToTransactionDetail = navController::navigateToSpecificTransaction,
|
||||||
navigateToAccountDetail = navController::navigateToSavingAccountDetails,
|
navigateToAccountDetail = navController::navigateToSavingAccountDetails,
|
||||||
navigateToHistory = navController::navigateToHistory,
|
navigateToHistory = navController::navigateToHistory,
|
||||||
@ -277,6 +284,37 @@ internal fun MifosNavHost(
|
|||||||
navigateToScanQrScreen = navController::navigateToScanQr,
|
navigateToScanQrScreen = navController::navigateToScanQr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
selectAccountScreenDestination(
|
||||||
|
navigateToMakeTransferV2Screen = navController::navigateToMakeTransferScreenV2,
|
||||||
|
navigateBack = navController::popBackStack,
|
||||||
|
)
|
||||||
|
|
||||||
|
makeTransferScreenV2(
|
||||||
|
navigateBack = navController::popBackStack,
|
||||||
|
onTransferSuccess = {
|
||||||
|
navController.navigateTransferSuccess(
|
||||||
|
navOptions {
|
||||||
|
popUpTo(SEND_MONEY_BASE_ROUTE) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
sendMoneyScreenDestination(
|
||||||
|
navigateToSelectAccountScreen = {
|
||||||
|
navController.navigateToSelectAccountScreen()
|
||||||
|
},
|
||||||
|
navigateToBeneficiary = {
|
||||||
|
navController.navigateToBeneficiaryAddEdit(
|
||||||
|
BeneficiaryAddEditType.AddItem,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigateBack = navController::popBackStack,
|
||||||
|
)
|
||||||
|
|
||||||
transferScreen(
|
transferScreen(
|
||||||
navigateBack = navController::popBackStack,
|
navigateBack = navController::popBackStack,
|
||||||
onTransferSuccess = {
|
onTransferSuccess = {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ interface ClientRepository {
|
|||||||
|
|
||||||
suspend fun updateClientImage(clientId: Long, image: String): DataState<String>
|
suspend fun updateClientImage(clientId: Long, image: String): DataState<String>
|
||||||
|
|
||||||
suspend fun getClientAccounts(clientId: Long): Flow<DataState<ClientAccountsEntity>>
|
suspend fun getClientAccounts(clientId: Long): ClientAccountsEntity
|
||||||
|
|
||||||
suspend fun getAccounts(clientId: Long, accountType: String): Flow<DataState<List<Account>>>
|
suspend fun getAccounts(clientId: Long, accountType: String): Flow<DataState<List<Account>>>
|
||||||
|
|
||||||
|
|||||||
@ -9,14 +9,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.mifospay.core.data.repository
|
package org.mifospay.core.data.repository
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import org.mifospay.core.common.DataState
|
import org.mifospay.core.common.DataState
|
||||||
import org.mifospay.core.network.model.entity.TPTResponse
|
import org.mifospay.core.network.model.entity.TPTResponse
|
||||||
import org.mifospay.core.network.model.entity.payload.TransferPayload
|
import org.mifospay.core.network.model.entity.payload.TransferPayload
|
||||||
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
||||||
|
|
||||||
interface ThirdPartyTransferRepository {
|
interface ThirdPartyTransferRepository {
|
||||||
suspend fun getTransferTemplate(): Flow<DataState<AccountOptionsTemplate>>
|
suspend fun getTransferTemplate(): AccountOptionsTemplate
|
||||||
|
|
||||||
suspend fun makeTransfer(payload: TransferPayload): Flow<DataState<TPTResponse>>
|
suspend fun makeTransfer(payload: TransferPayload): DataState<TPTResponse>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,10 +90,9 @@ class ClientRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getClientAccounts(clientId: Long): Flow<DataState<ClientAccountsEntity>> {
|
override suspend fun getClientAccounts(clientId: Long): ClientAccountsEntity {
|
||||||
return apiManager.clientsApi
|
return apiManager.clientsApi
|
||||||
.getClientAccounts(clientId)
|
.getClientAccounts(clientId)
|
||||||
.asDataStateFlow().flowOn(ioDispatcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAccounts(
|
override suspend fun getAccounts(
|
||||||
|
|||||||
@ -10,29 +10,29 @@
|
|||||||
package org.mifospay.core.data.repositoryImpl
|
package org.mifospay.core.data.repositoryImpl
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import org.mifospay.core.common.DataState
|
import org.mifospay.core.common.DataState
|
||||||
import org.mifospay.core.common.asDataStateFlow
|
|
||||||
import org.mifospay.core.data.repository.ThirdPartyTransferRepository
|
import org.mifospay.core.data.repository.ThirdPartyTransferRepository
|
||||||
import org.mifospay.core.network.FineractApiManager
|
import org.mifospay.core.network.SelfServiceApiManager
|
||||||
import org.mifospay.core.network.model.entity.TPTResponse
|
import org.mifospay.core.network.model.entity.TPTResponse
|
||||||
import org.mifospay.core.network.model.entity.payload.TransferPayload
|
import org.mifospay.core.network.model.entity.payload.TransferPayload
|
||||||
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
||||||
|
|
||||||
class ThirdPartyTransferRepositoryImpl(
|
class ThirdPartyTransferRepositoryImpl(
|
||||||
private val apiManager: FineractApiManager,
|
private val apiManager: SelfServiceApiManager,
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : ThirdPartyTransferRepository {
|
) : ThirdPartyTransferRepository {
|
||||||
override suspend fun getTransferTemplate(): Flow<DataState<AccountOptionsTemplate>> {
|
override suspend fun getTransferTemplate(): AccountOptionsTemplate {
|
||||||
return apiManager.thirdPartyTransferApi
|
return apiManager.thirdPartyTransferApi
|
||||||
.accountTransferTemplate()
|
.accountTransferTemplate()
|
||||||
.asDataStateFlow().flowOn(ioDispatcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun makeTransfer(payload: TransferPayload): Flow<DataState<TPTResponse>> {
|
override suspend fun makeTransfer(payload: TransferPayload): DataState<TPTResponse> {
|
||||||
return apiManager.thirdPartyTransferApi
|
return try {
|
||||||
.makeTransfer(payload)
|
val result = apiManager.thirdPartyTransferApi
|
||||||
.asDataStateFlow().flowOn(ioDispatcher)
|
.makeTransfer(payload)
|
||||||
|
DataState.Success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DataState.Error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,6 +124,7 @@ fun MifosTextField(
|
|||||||
errorText: String? = null,
|
errorText: String? = null,
|
||||||
onClickClearIcon: () -> Unit = { onValueChange("") },
|
onClickClearIcon: () -> Unit = { onValueChange("") },
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
|
textStyle: TextStyle? = null,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
singleLine: Boolean = true,
|
singleLine: Boolean = true,
|
||||||
maxLines: Int = if (singleLine) 1 else Int.Companion.MAX_VALUE,
|
maxLines: Int = if (singleLine) 1 else Int.Companion.MAX_VALUE,
|
||||||
@ -177,9 +178,9 @@ fun MifosTextField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
textStyle = LocalDensity.current.run {
|
textStyle = (textStyle ?: TextStyle()).copy(
|
||||||
TextStyle(color = KptTheme.colorScheme.onSurface)
|
color = KptTheme.colorScheme.onSurface,
|
||||||
},
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,10 +25,13 @@ import androidx.compose.material.icons.filled.ContentCopy
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Description
|
import androidx.compose.material.icons.filled.Description
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
import androidx.compose.material.icons.filled.FlashOff
|
import androidx.compose.material.icons.filled.FlashOff
|
||||||
import androidx.compose.material.icons.filled.FlashOn
|
import androidx.compose.material.icons.filled.FlashOn
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.icons.filled.Photo
|
import androidx.compose.material.icons.filled.Photo
|
||||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||||
@ -78,6 +81,7 @@ object MifosIcons {
|
|||||||
val ChevronRight: ImageVector = Icons.Filled.ChevronRight
|
val ChevronRight: ImageVector = Icons.Filled.ChevronRight
|
||||||
val QrCode: ImageVector = Icons.Filled.QrCode
|
val QrCode: ImageVector = Icons.Filled.QrCode
|
||||||
val Close: ImageVector = Icons.Filled.Close
|
val Close: ImageVector = Icons.Filled.Close
|
||||||
|
val Error: ImageVector = Icons.Filled.Error
|
||||||
val AttachMoney: ImageVector = Icons.Filled.AttachMoney
|
val AttachMoney: ImageVector = Icons.Filled.AttachMoney
|
||||||
val OutlinedVisibilityOff: ImageVector = Icons.Outlined.VisibilityOff
|
val OutlinedVisibilityOff: ImageVector = Icons.Outlined.VisibilityOff
|
||||||
val OutlinedVisibility: ImageVector = Icons.Outlined.Visibility
|
val OutlinedVisibility: ImageVector = Icons.Outlined.Visibility
|
||||||
@ -85,6 +89,7 @@ object MifosIcons {
|
|||||||
val Visibility: ImageVector = Icons.Filled.Visibility
|
val Visibility: ImageVector = Icons.Filled.Visibility
|
||||||
val Check: ImageVector = Icons.Default.Check
|
val Check: ImageVector = Icons.Default.Check
|
||||||
val KeyboardArrowDown: ImageVector = Icons.Default.KeyboardArrowDown
|
val KeyboardArrowDown: ImageVector = Icons.Default.KeyboardArrowDown
|
||||||
|
val KeyboardArrowUp: ImageVector = Icons.Default.KeyboardArrowUp
|
||||||
val Home = Icons.Outlined.Home
|
val Home = Icons.Outlined.Home
|
||||||
val HomeBoarder = Icons.Rounded.Home
|
val HomeBoarder = Icons.Rounded.Home
|
||||||
val Payment = Icons.Rounded.SwapHoriz
|
val Payment = Icons.Rounded.SwapHoriz
|
||||||
@ -129,4 +134,5 @@ object MifosIcons {
|
|||||||
val Scan = Icons.Outlined.QrCodeScanner
|
val Scan = Icons.Outlined.QrCodeScanner
|
||||||
val RadioButtonUnchecked = Icons.Default.RadioButtonUnchecked
|
val RadioButtonUnchecked = Icons.Default.RadioButtonUnchecked
|
||||||
val RadioButtonChecked = Icons.Filled.RadioButtonChecked
|
val RadioButtonChecked = Icons.Filled.RadioButtonChecked
|
||||||
|
val History = Icons.Default.History
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import kotlinx.serialization.Serializable
|
|||||||
data class AccountOption(
|
data class AccountOption(
|
||||||
val accountId: Int? = null,
|
val accountId: Int? = null,
|
||||||
val accountNo: String? = null,
|
val accountNo: String? = null,
|
||||||
val accountType: org.mifospay.core.network.model.entity.templates.account.AccountType? = null,
|
val accountType: AccountType? = null,
|
||||||
val clientId: Long? = null,
|
val clientId: Long? = null,
|
||||||
val clientName: String? = null,
|
val clientName: String? = null,
|
||||||
val officeId: Int? = null,
|
val officeId: Int? = null,
|
||||||
|
|||||||
@ -53,7 +53,7 @@ interface ClientService {
|
|||||||
): Unit
|
): Unit
|
||||||
|
|
||||||
@GET(ApiEndPoints.CLIENTS + "/{clientId}/accounts")
|
@GET(ApiEndPoints.CLIENTS + "/{clientId}/accounts")
|
||||||
suspend fun getClientAccounts(@Path("clientId") clientId: Long): Flow<ClientAccountsEntity>
|
suspend fun getClientAccounts(@Path("clientId") clientId: Long): ClientAccountsEntity
|
||||||
|
|
||||||
@GET(ApiEndPoints.CLIENTS + "/{clientId}/accounts")
|
@GET(ApiEndPoints.CLIENTS + "/{clientId}/accounts")
|
||||||
fun getAccounts(
|
fun getAccounts(
|
||||||
|
|||||||
@ -12,7 +12,6 @@ package org.mifospay.core.network.services
|
|||||||
import de.jensklingenberg.ktorfit.http.Body
|
import de.jensklingenberg.ktorfit.http.Body
|
||||||
import de.jensklingenberg.ktorfit.http.GET
|
import de.jensklingenberg.ktorfit.http.GET
|
||||||
import de.jensklingenberg.ktorfit.http.POST
|
import de.jensklingenberg.ktorfit.http.POST
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import org.mifospay.core.network.model.entity.TPTResponse
|
import org.mifospay.core.network.model.entity.TPTResponse
|
||||||
import org.mifospay.core.network.model.entity.payload.TransferPayload
|
import org.mifospay.core.network.model.entity.payload.TransferPayload
|
||||||
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
||||||
@ -20,8 +19,8 @@ import org.mifospay.core.network.utils.ApiEndPoints
|
|||||||
|
|
||||||
interface ThirdPartyTransferService {
|
interface ThirdPartyTransferService {
|
||||||
@GET(ApiEndPoints.ACCOUNT_TRANSFER + "/template?type=tpt")
|
@GET(ApiEndPoints.ACCOUNT_TRANSFER + "/template?type=tpt")
|
||||||
suspend fun accountTransferTemplate(): Flow<AccountOptionsTemplate>
|
suspend fun accountTransferTemplate(): AccountOptionsTemplate
|
||||||
|
|
||||||
@POST(ApiEndPoints.ACCOUNT_TRANSFER + "?type=tpt")
|
@POST(ApiEndPoints.ACCOUNT_TRANSFER + "?type=tpt")
|
||||||
suspend fun makeTransfer(@Body transferPayload: TransferPayload): Flow<TPTResponse>
|
suspend fun makeTransfer(@Body transferPayload: TransferPayload): TPTResponse
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* 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.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import template.core.base.designsystem.KptTheme
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MifosSearchBar(
|
||||||
|
query: String,
|
||||||
|
placeHolder: String,
|
||||||
|
showClearButton: Boolean = true,
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
|
onClearQuery: () -> Unit = {},
|
||||||
|
enabled: Boolean = true,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
SearchBar(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
inputField = {
|
||||||
|
SearchBarDefaults.InputField(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onQueryChange,
|
||||||
|
onSearch = onSearch,
|
||||||
|
expanded = false,
|
||||||
|
onExpandedChange = {},
|
||||||
|
enabled = enabled,
|
||||||
|
placeholder = { Text(text = placeHolder) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (showClearButton && query.isNotEmpty()) {
|
||||||
|
IconButton(onClick = onClearQuery) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interactionSource = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expanded = false,
|
||||||
|
onExpandedChange = {},
|
||||||
|
shape = SearchBarDefaults.inputFieldShape,
|
||||||
|
colors = SearchBarDefaults.colors(),
|
||||||
|
tonalElevation = SearchBarDefaults.TonalElevation,
|
||||||
|
shadowElevation = SearchBarDefaults.ShadowElevation,
|
||||||
|
windowInsets = SearchBarDefaults.windowInsets,
|
||||||
|
content = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleSearchBar(
|
||||||
|
query: String,
|
||||||
|
placeHolder: String,
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClearQuery: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = onQueryChange,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp).clickable {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = placeHolder,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Search,
|
||||||
|
contentDescription = "Search",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
IconButton(onClick = onClearQuery) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Close,
|
||||||
|
contentDescription = "Clear",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = KptTheme.shapes.small,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
errorIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@DevicePreviews
|
||||||
|
fun PreviewMifosSearchBar() {
|
||||||
|
KptTheme {
|
||||||
|
MifosSearchBar(
|
||||||
|
query = "Hello",
|
||||||
|
placeHolder = "Search...",
|
||||||
|
onQueryChange = {},
|
||||||
|
onSearch = {},
|
||||||
|
onClearQuery = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@DevicePreviews
|
||||||
|
fun PreviewSimpleSearchBar() {
|
||||||
|
KptTheme {
|
||||||
|
SimpleSearchBar(
|
||||||
|
query = "",
|
||||||
|
placeHolder = "Search here...",
|
||||||
|
onQueryChange = {},
|
||||||
|
onClearQuery = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,14 +12,15 @@
|
|||||||
<string name="feature_make_transfer_loading">Loading</string>
|
<string name="feature_make_transfer_loading">Loading</string>
|
||||||
<string name="feature_make_transfer_send_money">Send money</string>
|
<string name="feature_make_transfer_send_money">Send money</string>
|
||||||
<string name="feature_make_transfer_sending_to">Sending to</string>
|
<string name="feature_make_transfer_sending_to">Sending to</string>
|
||||||
<string name="feature_make_transfer_amount">Amount</string>
|
<string name="feature_make_transfer_amount">Enter Amount</string>
|
||||||
<string name="feature_make_transfer_transaction_success">Transaction successful</string>
|
<string name="feature_make_transfer_transaction_success">Transaction successful</string>
|
||||||
<string name="feature_make_transfer_transaction_unable_to_process">Unable to process transfer</string>
|
<string name="feature_make_transfer_transaction_unable_to_process">Unable to process transfer</string>
|
||||||
|
|
||||||
<string name="feature_make_transfer_review_title">Review Transfer</string>
|
<string name="feature_make_transfer_review_title">Review Transfer</string>
|
||||||
<string name="feature_make_transfer_description_label">Description</string>
|
<string name="feature_make_transfer_description_label">Add a Message</string>
|
||||||
<string name="feature_make_transfer_to_account">To Account</string>
|
<string name="feature_make_transfer_to_account">To %1$s</string>
|
||||||
<string name="feature_make_transfer_from_account">From Account</string>
|
<string name="feature_make_transfer_from_account">From %1$s</string>
|
||||||
|
<string name="feature_make_transfer_from_account_title">From Account</string>
|
||||||
<string name="feature_make_transfer_continue_button">Continue</string>
|
<string name="feature_make_transfer_continue_button">Continue</string>
|
||||||
<string name="feature_make_transfer_oops_title">Oops!</string>
|
<string name="feature_make_transfer_oops_title">Oops!</string>
|
||||||
<string name="feature_make_transfer_no_accounts_found">No accounts found!</string>
|
<string name="feature_make_transfer_no_accounts_found">No accounts found!</string>
|
||||||
@ -43,4 +44,10 @@
|
|||||||
<string name="feature_make_transfer_error_same_account">Cannot transfer to the same account</string>
|
<string name="feature_make_transfer_error_same_account">Cannot transfer to the same account</string>
|
||||||
<string name="feature_make_transfer_error_insufficient_balance">Insufficient balance</string>
|
<string name="feature_make_transfer_error_insufficient_balance">Insufficient balance</string>
|
||||||
|
|
||||||
|
<string name="feature_make_transfer_available_balance">Available Balance: %1$s</string>
|
||||||
|
<string name="feature_make_transfer_show_balance">Show Balance</string>
|
||||||
|
<string name="feature_make_transfer_hide_balance">Hide Balance</string>
|
||||||
|
<string name="feature_make_transfer_amount_error">Your account has insufficient balance</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@ -235,7 +236,7 @@ private fun AccountList(
|
|||||||
onClick: (Account) -> Unit,
|
onClick: (Account) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth().padding(KptTheme.spacing.md),
|
||||||
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@ -289,15 +290,18 @@ private fun AccountItem(
|
|||||||
trailingContent = {
|
trailingContent = {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = selected,
|
targetState = selected,
|
||||||
) {
|
label = "radioAnim",
|
||||||
|
) { isSelected ->
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (it) {
|
imageVector = if (isSelected) {
|
||||||
MifosIcons.RadioButtonChecked
|
MifosIcons.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
MifosIcons.RadioButtonUnchecked
|
MifosIcons.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = stringResource(Res.string.feature_make_transfer_check_icon_description),
|
contentDescription = stringResource(
|
||||||
tint = if (it) {
|
Res.string.feature_make_transfer_check_icon_description,
|
||||||
|
),
|
||||||
|
tint = if (isSelected) {
|
||||||
KptTheme.colorScheme.primary
|
KptTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
KptTheme.colorScheme.outlineVariant
|
KptTheme.colorScheme.outlineVariant
|
||||||
@ -313,7 +317,7 @@ private fun AccountItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ClientCard(
|
fun ClientCard(
|
||||||
client: PaymentQrData,
|
client: PaymentQrData,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@ -369,7 +373,7 @@ private fun ClientCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AccountBadge(
|
fun AccountBadge(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
borderColor: Color = KptTheme.colorScheme.primary,
|
borderColor: Color = KptTheme.colorScheme.primary,
|
||||||
|
|||||||
@ -12,7 +12,9 @@ package org.mifospay.feature.make.transfer.di
|
|||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.mifospay.feature.make.transfer.MakeTransferViewModel
|
import org.mifospay.feature.make.transfer.MakeTransferViewModel
|
||||||
|
import org.mifospay.feature.make.transfer.v2.MakeTransferV2ScreenV2ViewModel
|
||||||
|
|
||||||
val MakeTransferModule = module {
|
val MakeTransferModule = module {
|
||||||
viewModelOf(::MakeTransferViewModel)
|
viewModelOf(::MakeTransferViewModel)
|
||||||
|
viewModelOf(::MakeTransferV2ScreenV2ViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.feature.make.transfer.v2
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MakeTransferScreenV2Route(
|
||||||
|
val amount: Int,
|
||||||
|
val accountId: Long,
|
||||||
|
val toOfficeId: Int? = null,
|
||||||
|
val toClientId: Long? = null,
|
||||||
|
val toAccountTypeId: Int? = null,
|
||||||
|
val toAccountName: String = "",
|
||||||
|
val toAccountNo: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun NavController.navigateToMakeTransferScreenV2(
|
||||||
|
toOfficeId: Int?,
|
||||||
|
toClientId: Long?,
|
||||||
|
toAccountTypeId: Int?,
|
||||||
|
toAccountId: Int,
|
||||||
|
amount: Int,
|
||||||
|
toAccountName: String,
|
||||||
|
toAccountNo: String,
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
this.navigate(
|
||||||
|
MakeTransferScreenV2Route(
|
||||||
|
toOfficeId = toOfficeId,
|
||||||
|
toClientId = toClientId,
|
||||||
|
toAccountTypeId = toAccountTypeId,
|
||||||
|
|
||||||
|
accountId = toAccountId.toLong(),
|
||||||
|
amount = amount,
|
||||||
|
toAccountName = toAccountName,
|
||||||
|
toAccountNo = toAccountNo,
|
||||||
|
),
|
||||||
|
navOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.makeTransferScreenV2(
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
onTransferSuccess: () -> Unit,
|
||||||
|
) {
|
||||||
|
composable<MakeTransferScreenV2Route> {
|
||||||
|
MakeTransferScreenV2(
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
onTransferSuccess = onTransferSuccess,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,332 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.make.transfer.v2
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import io.ktor.client.request.invoke
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_empty_amount
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_empty_description
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_insufficient_balance
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_invalid_amount
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_same_account
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_select_account
|
||||||
|
import org.jetbrains.compose.resources.StringResource
|
||||||
|
import org.mifospay.core.common.DataState
|
||||||
|
import org.mifospay.core.common.DateHelper
|
||||||
|
import org.mifospay.core.common.StringResourceSerializer
|
||||||
|
import org.mifospay.core.common.utils.capitalizeWords
|
||||||
|
import org.mifospay.core.data.repository.ClientRepository
|
||||||
|
import org.mifospay.core.data.repository.ThirdPartyTransferRepository
|
||||||
|
import org.mifospay.core.network.model.entity.TPTResponse
|
||||||
|
import org.mifospay.core.network.model.entity.payload.TransferPayload
|
||||||
|
import org.mifospay.core.network.model.entity.templates.account.AccountOption
|
||||||
|
import org.mifospay.core.ui.utils.BaseViewModel
|
||||||
|
import kotlin.Int
|
||||||
|
|
||||||
|
internal class MakeTransferV2ScreenV2ViewModel(
|
||||||
|
private val repository: ThirdPartyTransferRepository,
|
||||||
|
private val clientRepo: ClientRepository,
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : BaseViewModel<MakeTransferV2State, MakeTransferV2Event, MakeTransferV2Action>(
|
||||||
|
initialState = run {
|
||||||
|
val route = savedStateHandle.toRoute<MakeTransferScreenV2Route>()
|
||||||
|
MakeTransferV2State(
|
||||||
|
toOfficeId = route.toOfficeId,
|
||||||
|
toClientId = route.toClientId,
|
||||||
|
toAccountType = route.toAccountTypeId,
|
||||||
|
toAccountId = route.accountId.toInt(),
|
||||||
|
toAccountName = route.toAccountName,
|
||||||
|
toAccountNo = route.toAccountNo,
|
||||||
|
amount = if (route.amount == 0) "" else route.amount.toString(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
getFromAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun handleAction(action: MakeTransferV2Action) {
|
||||||
|
when (action) {
|
||||||
|
MakeTransferV2Action.NavigateBack -> {
|
||||||
|
sendEvent(MakeTransferV2Event.OnNavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MakeTransferV2Action.AmountChanged -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(amount = action.amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MakeTransferV2Action.DescriptionChanged -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(description = action.desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MakeTransferV2Action.SelectAccount -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
selectedAccount = action.account,
|
||||||
|
selectedAccountBalance = state.balanceMap.getOrElse(
|
||||||
|
action.account?.accountNo ?: "",
|
||||||
|
) { 0.0 },
|
||||||
|
showBottomSheet = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MakeTransferV2Action.DismissDialog -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MakeTransferV2Action.InitiateTransfer -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = MakeTransferV2State.DialogState.Loading)
|
||||||
|
}
|
||||||
|
validateTransfer()
|
||||||
|
}
|
||||||
|
|
||||||
|
is MakeTransferV2Action.Internal.HandleTransferResult -> handleTransferResult(action)
|
||||||
|
|
||||||
|
MakeTransferV2Action.CloseBottomSheet -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(showBottomSheet = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MakeTransferV2Action.OpenBottomSheet -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(showBottomSheet = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getFromAccounts() {
|
||||||
|
try {
|
||||||
|
val res = repository.getTransferTemplate()
|
||||||
|
if (res.fromAccountOptions.isNullOrEmpty()) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
state = MakeTransferV2State.State.NoAccounts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
fromAccountOptions = res.fromAccountOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getBalanceOfAccounts()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
state = MakeTransferV2State.State.Error(e.message ?: ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getBalanceOfAccounts() {
|
||||||
|
try {
|
||||||
|
val clientId = state.fromAccountOptions?.first()?.clientId ?: -1
|
||||||
|
val result = clientRepo.getClientAccounts(clientId)
|
||||||
|
val balanceMap: Map<String, Double> =
|
||||||
|
result.savingsAccounts.associate { account ->
|
||||||
|
account.accountNo to account.accountBalance
|
||||||
|
}
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
state = MakeTransferV2State.State.Success,
|
||||||
|
balanceMap = balanceMap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val firstAccount = state.fromAccountOptions?.first()
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
selectedAccount = firstAccount,
|
||||||
|
selectedAccountBalance = balanceMap.getOrElse(
|
||||||
|
firstAccount?.accountNo ?: "",
|
||||||
|
{ 0.0 },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
state = MakeTransferV2State.State.Error(e.message ?: ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateTransfer() = when {
|
||||||
|
state.amount.isBlank() -> updateErrorState(Res.string.feature_make_transfer_error_empty_amount)
|
||||||
|
|
||||||
|
state.amount.toDoubleOrNull() == null -> updateErrorState(Res.string.feature_make_transfer_error_invalid_amount)
|
||||||
|
|
||||||
|
state.description.isBlank() -> updateErrorState(Res.string.feature_make_transfer_error_empty_description)
|
||||||
|
|
||||||
|
state.selectedAccount == null -> updateErrorState(Res.string.feature_make_transfer_error_select_account)
|
||||||
|
|
||||||
|
state.selectedAccount?.accountId == state.toAccountId -> {
|
||||||
|
updateErrorState(Res.string.feature_make_transfer_error_same_account)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.amount.toDouble() > state.selectedAccountBalance -> {
|
||||||
|
updateErrorState(Res.string.feature_make_transfer_error_insufficient_balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> initiateTransfer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initiateTransfer() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = repository.makeTransfer(state.transferPayload)
|
||||||
|
|
||||||
|
sendAction(MakeTransferV2Action.Internal.HandleTransferResult(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleTransferResult(action: MakeTransferV2Action.Internal.HandleTransferResult) {
|
||||||
|
when (action.result) {
|
||||||
|
is DataState.Loading -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = MakeTransferV2State.DialogState.Loading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Error -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = MakeTransferV2State.DialogState.Error.StringMessage(action.result.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Success -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(MakeTransferV2Event.OnTransferSuccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateErrorState(message: StringResource) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = MakeTransferV2State.DialogState.Error.ResourceMessage(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class MakeTransferV2State(
|
||||||
|
val toOfficeId: Int? = null,
|
||||||
|
val toClientId: Long? = null,
|
||||||
|
val toAccountType: Int? = null,
|
||||||
|
val toAccountId: Int? = null,
|
||||||
|
val toAccountName: String = "",
|
||||||
|
val toAccountNo: String = "",
|
||||||
|
val amount: String = "",
|
||||||
|
|
||||||
|
val showBottomSheet: Boolean = false,
|
||||||
|
val state: State = State.Loading,
|
||||||
|
val description: String = "",
|
||||||
|
val selectedAccount: AccountOption? = null,
|
||||||
|
val selectedAccountBalance: Double = 0.0,
|
||||||
|
val dialogState: DialogState? = null,
|
||||||
|
val fromAccountOptions: List<AccountOption>? = emptyList(),
|
||||||
|
val balanceMap: Map<String, Double> = emptyMap(),
|
||||||
|
) {
|
||||||
|
val amountIsValid: Boolean
|
||||||
|
get() = amount.isNotEmpty() && amount.toDoubleOrNull() != null && amount.toDouble() <= selectedAccountBalance
|
||||||
|
|
||||||
|
val descriptionIsValid: Boolean
|
||||||
|
get() = description.isNotEmpty()
|
||||||
|
|
||||||
|
val transferPayload: TransferPayload
|
||||||
|
get() = TransferPayload(
|
||||||
|
fromOfficeId = selectedAccount?.officeId,
|
||||||
|
fromClientId = selectedAccount?.clientId,
|
||||||
|
fromAccountType = selectedAccount?.accountType?.id,
|
||||||
|
fromAccountId = selectedAccount?.accountId,
|
||||||
|
toOfficeId = toOfficeId,
|
||||||
|
toClientId = toClientId,
|
||||||
|
toAccountType = toAccountType,
|
||||||
|
toAccountId = toAccountId,
|
||||||
|
transferDate = DateHelper.formattedShortDate,
|
||||||
|
transferAmount = amount.toDoubleOrNull() ?: 0.0,
|
||||||
|
transferDescription = description.capitalizeWords(),
|
||||||
|
locale = "en_IN",
|
||||||
|
dateFormat = DateHelper.SHORT_MONTH,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface DialogState {
|
||||||
|
@Serializable
|
||||||
|
data object Loading : DialogState
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class Error : DialogState {
|
||||||
|
@Serializable
|
||||||
|
data class StringMessage(val message: String) : Error()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ResourceMessage(
|
||||||
|
@Serializable(with = StringResourceSerializer::class)
|
||||||
|
val message: StringResource,
|
||||||
|
) : Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
data object Loading : State
|
||||||
|
data object NoAccounts : State
|
||||||
|
data object Success : State
|
||||||
|
data class Error(val message: String) : State
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed interface MakeTransferV2Event {
|
||||||
|
data object OnNavigateBack : MakeTransferV2Event
|
||||||
|
data object OnTransferSuccess : MakeTransferV2Event
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed interface MakeTransferV2Action {
|
||||||
|
data object NavigateBack : MakeTransferV2Action
|
||||||
|
|
||||||
|
data object DismissDialog : MakeTransferV2Action
|
||||||
|
|
||||||
|
data object InitiateTransfer : MakeTransferV2Action
|
||||||
|
|
||||||
|
data class AmountChanged(val amount: String) : MakeTransferV2Action
|
||||||
|
|
||||||
|
data class DescriptionChanged(val desc: String) : MakeTransferV2Action
|
||||||
|
|
||||||
|
data class SelectAccount(val account: AccountOption?) : MakeTransferV2Action
|
||||||
|
|
||||||
|
sealed interface Internal : MakeTransferV2Action {
|
||||||
|
data class HandleTransferResult(val result: DataState<TPTResponse>) : Internal
|
||||||
|
}
|
||||||
|
|
||||||
|
data object OpenBottomSheet : MakeTransferV2Action
|
||||||
|
data object CloseBottomSheet : MakeTransferV2Action
|
||||||
|
}
|
||||||
@ -0,0 +1,571 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.make.transfer.v2
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.TextAutoSize
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_amount
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_amount_error
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_available_balance
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_check_icon_description
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_continue_button
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_description_label
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_from_account
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_from_account_title
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_no_accounts_found
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_oops_title
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_review_title
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_show_balance
|
||||||
|
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_to_account
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.mifospay.core.designsystem.component.BasicDialogState
|
||||||
|
import org.mifospay.core.designsystem.component.MifosBasicDialog
|
||||||
|
import org.mifospay.core.designsystem.component.MifosBottomSheetScaffold
|
||||||
|
import org.mifospay.core.designsystem.component.MifosButton
|
||||||
|
import org.mifospay.core.designsystem.component.MifosTextField
|
||||||
|
import org.mifospay.core.designsystem.component.MifosTopBar
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.network.model.entity.templates.account.AccountOption
|
||||||
|
import org.mifospay.core.ui.AvatarBox
|
||||||
|
import org.mifospay.core.ui.EmptyContentScreen
|
||||||
|
import org.mifospay.core.ui.MifosProgressIndicator
|
||||||
|
import org.mifospay.core.ui.MifosProgressIndicatorOverlay
|
||||||
|
import org.mifospay.core.ui.utils.EventsEffect
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MakeTransferScreenV2(
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
onTransferSuccess: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: MakeTransferV2ScreenV2ViewModel = koinViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
EventsEffect(viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
MakeTransferV2Event.OnNavigateBack -> navigateBack.invoke()
|
||||||
|
MakeTransferV2Event.OnTransferSuccess -> onTransferSuccess.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MakeTransferDialogsV2(
|
||||||
|
dialogState = state.dialogState,
|
||||||
|
onDismissRequest = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(MakeTransferV2Action.DismissDialog) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
MakeTransferScreenV2(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onAction = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(it) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MakeTransferDialogsV2(
|
||||||
|
dialogState: MakeTransferV2State.DialogState?,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
when (dialogState) {
|
||||||
|
is MakeTransferV2State.DialogState.Error -> {
|
||||||
|
val message = when (dialogState) {
|
||||||
|
is MakeTransferV2State.DialogState.Error.StringMessage -> dialogState.message
|
||||||
|
is MakeTransferV2State.DialogState.Error.ResourceMessage -> stringResource(dialogState.message)
|
||||||
|
}
|
||||||
|
MifosBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
message = message,
|
||||||
|
),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MakeTransferV2State.DialogState.Loading -> MifosProgressIndicatorOverlay()
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun MakeTransferScreenV2(
|
||||||
|
state: MakeTransferV2State,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
|
onAction: (MakeTransferV2Action) -> Unit,
|
||||||
|
) {
|
||||||
|
MifosBottomSheetScaffold(
|
||||||
|
topBar = {
|
||||||
|
MifosTopBar(
|
||||||
|
topBarTitle = stringResource(Res.string.feature_make_transfer_review_title),
|
||||||
|
backPress = {
|
||||||
|
onAction(MakeTransferV2Action.NavigateBack)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
sheetPeekHeight = if (state.showBottomSheet) 200.dp else 0.dp,
|
||||||
|
sheetContent = {
|
||||||
|
if (state.showBottomSheet) {
|
||||||
|
AccountList(
|
||||||
|
accounts = state.fromAccountOptions ?: emptyList(),
|
||||||
|
selected = remember(state) {
|
||||||
|
{ state.selectedAccount == it }
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onAction(MakeTransferV2Action.SelectAccount(it))
|
||||||
|
},
|
||||||
|
balanceMap = state.balanceMap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
) { paddingValues ->
|
||||||
|
when (state.state) {
|
||||||
|
is MakeTransferV2State.State.Error -> {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_make_transfer_oops_title),
|
||||||
|
subTitle = stringResource(Res.string.feature_make_transfer_no_accounts_found),
|
||||||
|
iconTint = KptTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MakeTransferV2State.State.Loading -> {
|
||||||
|
MifosProgressIndicator()
|
||||||
|
}
|
||||||
|
MakeTransferV2State.State.NoAccounts -> {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_make_transfer_oops_title),
|
||||||
|
subTitle = stringResource(Res.string.feature_make_transfer_no_accounts_found),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MakeTransferV2State.State.Success -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(KptTheme.spacing.md)
|
||||||
|
.imePadding(),
|
||||||
|
state = lazyListState,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
ClientCard(
|
||||||
|
name = state.toAccountName,
|
||||||
|
account = state.toAccountNo,
|
||||||
|
onEdit = { onAction(MakeTransferV2Action.NavigateBack) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.selectedAccount != null) {
|
||||||
|
item {
|
||||||
|
FromAccountCard(
|
||||||
|
account = state.selectedAccount,
|
||||||
|
onAction = onAction,
|
||||||
|
isOpened = state.showBottomSheet,
|
||||||
|
balance = state.selectedAccountBalance.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
EnterAmountCard(
|
||||||
|
state = state,
|
||||||
|
onAction = onAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
MifosTextField(
|
||||||
|
label = stringResource(Res.string.feature_make_transfer_description_label),
|
||||||
|
value = state.description,
|
||||||
|
isError = !state.descriptionIsValid,
|
||||||
|
onValueChange = { onAction(MakeTransferV2Action.DescriptionChanged(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
MifosButton(
|
||||||
|
onClick = { onAction(MakeTransferV2Action.InitiateTransfer) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = state.amountIsValid && state.descriptionIsValid,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(Res.string.feature_make_transfer_continue_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FromAccountCard(
|
||||||
|
account: AccountOption,
|
||||||
|
balance: String,
|
||||||
|
isOpened: Boolean,
|
||||||
|
onAction: (MakeTransferV2Action) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
if (isOpened) {
|
||||||
|
onAction(MakeTransferV2Action.CloseBottomSheet)
|
||||||
|
} else {
|
||||||
|
onAction(MakeTransferV2Action.OpenBottomSheet)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
BasicText(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_make_transfer_from_account,
|
||||||
|
account.clientName ?: "",
|
||||||
|
),
|
||||||
|
autoSize = TextAutoSize.StepBased(
|
||||||
|
minFontSize = 6.sp,
|
||||||
|
maxFontSize = 16.sp,
|
||||||
|
stepSize = 1.sp,
|
||||||
|
),
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
color = KptTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(text = account.accountNo ?: "")
|
||||||
|
Text(text = stringResource(Res.string.feature_make_transfer_available_balance, balance))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(
|
||||||
|
icon = MifosIcons.Bank,
|
||||||
|
backgroundColor = KptTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (isOpened) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.KeyboardArrowUp,
|
||||||
|
contentDescription = stringResource(Res.string.feature_make_transfer_check_icon_description),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.KeyboardArrowDown,
|
||||||
|
contentDescription = stringResource(Res.string.feature_make_transfer_check_icon_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EnterAmountCard(
|
||||||
|
state: MakeTransferV2State,
|
||||||
|
onAction: (MakeTransferV2Action) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = modifier.fillMaxWidth().then(
|
||||||
|
if (!state.amountIsValid) {
|
||||||
|
Modifier.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_make_transfer_amount),
|
||||||
|
style = KptTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
leadingIcon = {
|
||||||
|
Text(
|
||||||
|
text = "$",
|
||||||
|
style = KptTheme.typography.headlineMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
value = state.amount,
|
||||||
|
onValueChange = {
|
||||||
|
onAction(MakeTransferV2Action.AmountChanged(it))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Decimal,
|
||||||
|
),
|
||||||
|
isError = !state.amountIsValid,
|
||||||
|
textStyle = KptTheme.typography.headlineMedium,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
errorContainerColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = KptTheme.colorScheme.primary,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ((state.amount.toDoubleOrNull() ?: 0.0) > state.selectedAccountBalance) {
|
||||||
|
Spacer(modifier = Modifier.height(KptTheme.spacing.sm))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_make_transfer_amount_error),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountList(
|
||||||
|
accounts: List<AccountOption?>,
|
||||||
|
balanceMap: Map<String, Double>,
|
||||||
|
selected: (AccountOption?) -> Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (AccountOption?) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth().padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_make_transfer_from_account_title),
|
||||||
|
style = KptTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
items(items = accounts) { account ->
|
||||||
|
AccountItem(
|
||||||
|
account = account,
|
||||||
|
selected = selected(account),
|
||||||
|
onClick = remember(account) {
|
||||||
|
{ onClick(account) }
|
||||||
|
},
|
||||||
|
balance = balanceMap[account?.accountNo ?: ""].toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountItem(
|
||||||
|
account: AccountOption?,
|
||||||
|
balance: String,
|
||||||
|
selected: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
var revealBalance by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = account?.clientName ?: "")
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(text = account?.accountNo ?: "")
|
||||||
|
|
||||||
|
if (revealBalance) {
|
||||||
|
Text(text = stringResource(Res.string.feature_make_transfer_available_balance, balance))
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_make_transfer_show_balance),
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.clickable { revealBalance = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(
|
||||||
|
icon = MifosIcons.Bank,
|
||||||
|
backgroundColor = KptTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = selected,
|
||||||
|
label = "radioAnim",
|
||||||
|
) { isSelected ->
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isSelected) {
|
||||||
|
MifosIcons.RadioButtonChecked
|
||||||
|
} else {
|
||||||
|
MifosIcons.RadioButtonUnchecked
|
||||||
|
},
|
||||||
|
contentDescription = stringResource(
|
||||||
|
Res.string.feature_make_transfer_check_icon_description,
|
||||||
|
),
|
||||||
|
tint = if (isSelected) {
|
||||||
|
KptTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
KptTheme.colorScheme.outlineVariant
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClientCard(
|
||||||
|
name: String,
|
||||||
|
account: String,
|
||||||
|
onEdit: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
BasicText(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_make_transfer_to_account,
|
||||||
|
name,
|
||||||
|
),
|
||||||
|
autoSize = TextAutoSize.StepBased(
|
||||||
|
minFontSize = 6.sp,
|
||||||
|
maxFontSize = 16.sp,
|
||||||
|
stepSize = 1.sp,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
color = KptTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = account)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(
|
||||||
|
icon = MifosIcons.Bank,
|
||||||
|
backgroundColor = KptTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Edit,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
Res.string.feature_make_transfer_check_icon_description,
|
||||||
|
),
|
||||||
|
modifier = Modifier.clickable { onEdit() },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,11 +17,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.SearchBar
|
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -33,18 +28,18 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import mobile_wallet.feature.merchants.generated.resources.Res
|
import mobile_wallet.feature.merchants.generated.resources.Res
|
||||||
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_close
|
|
||||||
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_empty_no_merchants_subtitle
|
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_empty_no_merchants_subtitle
|
||||||
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_empty_no_merchants_title
|
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_empty_no_merchants_title
|
||||||
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_error_oops
|
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_error_oops
|
||||||
|
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_loading
|
||||||
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_search
|
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_search
|
||||||
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_unexpected_error_subtitle
|
import mobile_wallet.feature.merchants.generated.resources.feature_merchants_unexpected_error_subtitle
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.mifospay.core.designsystem.component.MfLoadingWheel
|
||||||
import org.mifospay.core.designsystem.component.MifosScaffold
|
import org.mifospay.core.designsystem.component.MifosScaffold
|
||||||
import org.mifospay.core.designsystem.component.rememberMifosPullToRefreshState
|
import org.mifospay.core.designsystem.component.rememberMifosPullToRefreshState
|
||||||
import org.mifospay.core.designsystem.icon.MifosIcons
|
|
||||||
import org.mifospay.core.designsystem.theme.MifosTheme
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
import org.mifospay.core.model.savingsaccount.Currency
|
import org.mifospay.core.model.savingsaccount.Currency
|
||||||
import org.mifospay.core.model.savingsaccount.DepositType
|
import org.mifospay.core.model.savingsaccount.DepositType
|
||||||
@ -55,7 +50,7 @@ import org.mifospay.core.model.savingsaccount.SubStatus
|
|||||||
import org.mifospay.core.model.savingsaccount.Summary
|
import org.mifospay.core.model.savingsaccount.Summary
|
||||||
import org.mifospay.core.model.savingsaccount.Timeline
|
import org.mifospay.core.model.savingsaccount.Timeline
|
||||||
import org.mifospay.core.ui.EmptyContentScreen
|
import org.mifospay.core.ui.EmptyContentScreen
|
||||||
import org.mifospay.core.ui.MifosProgressIndicator
|
import org.mifospay.core.ui.MifosSearchBar
|
||||||
import org.mifospay.feature.merchants.MerchantUiState
|
import org.mifospay.feature.merchants.MerchantUiState
|
||||||
import org.mifospay.feature.merchants.MerchantViewModel
|
import org.mifospay.feature.merchants.MerchantViewModel
|
||||||
import org.mifospay.feature.merchants.navigation.navigateToMerchantTransferScreen
|
import org.mifospay.feature.merchants.navigation.navigateToMerchantTransferScreen
|
||||||
@ -123,7 +118,12 @@ internal fun MerchantScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MerchantUiState.Loading -> MifosProgressIndicator()
|
MerchantUiState.Loading -> {
|
||||||
|
MfLoadingWheel(
|
||||||
|
contentDesc = stringResource(Res.string.feature_merchants_loading),
|
||||||
|
backgroundColor = KptTheme.colorScheme.surface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is MerchantUiState.ShowMerchants -> {
|
is MerchantUiState.ShowMerchants -> {
|
||||||
MerchantScreenContent(
|
MerchantScreenContent(
|
||||||
@ -204,48 +204,13 @@ private fun SearchBarScreen(
|
|||||||
onClearQuery: () -> Unit,
|
onClearQuery: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
SearchBar(
|
MifosSearchBar(
|
||||||
inputField = {
|
query = query,
|
||||||
SearchBarDefaults.InputField(
|
placeHolder = stringResource(Res.string.feature_merchants_search),
|
||||||
query = query,
|
onQueryChange = onQueryChange,
|
||||||
onQueryChange = onQueryChange,
|
onSearch = onSearch,
|
||||||
onSearch = onSearch,
|
onClearQuery = onClearQuery,
|
||||||
expanded = false,
|
modifier = modifier,
|
||||||
onExpandedChange = {},
|
|
||||||
enabled = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(text = stringResource(Res.string.feature_merchants_search))
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = MifosIcons.Search,
|
|
||||||
contentDescription = stringResource(Res.string.feature_merchants_search),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onClearQuery,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = MifosIcons.Close,
|
|
||||||
contentDescription = stringResource(Res.string.feature_merchants_close),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
interactionSource = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expanded = false,
|
|
||||||
onExpandedChange = {},
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = KptTheme.spacing.md, horizontal = KptTheme.spacing.md),
|
|
||||||
shape = SearchBarDefaults.inputFieldShape,
|
|
||||||
colors = SearchBarDefaults.colors(),
|
|
||||||
tonalElevation = SearchBarDefaults.TonalElevation,
|
|
||||||
shadowElevation = SearchBarDefaults.ShadowElevation,
|
|
||||||
windowInsets = SearchBarDefaults.windowInsets,
|
|
||||||
content = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,4 +38,15 @@
|
|||||||
<string name="feature_send_money_error_account_cannot_be_empty">Account cannot be empty</string>
|
<string name="feature_send_money_error_account_cannot_be_empty">Account cannot be empty</string>
|
||||||
<string name="feature_send_money_error_requesting_payment_qr_but_found">Requesting payment QR but found - %1$s</string>
|
<string name="feature_send_money_error_requesting_payment_qr_but_found">Requesting payment QR but found - %1$s</string>
|
||||||
<string name="feature_send_money_error_requesting_payment_qr_data_missing">Failed to request payment QR: required data is missing</string>
|
<string name="feature_send_money_error_requesting_payment_qr_data_missing">Failed to request payment QR: required data is missing</string>
|
||||||
|
|
||||||
|
<string name="feature_select_payment_title">Select Payee</string>
|
||||||
|
<string name="feature_select_account_placeholder">Search VPA/Mobile/Account Number</string>
|
||||||
|
<string name="feature_send_money_add_payee_title">Add Payee</string>
|
||||||
|
<string name="feature_send_money_add_payee_subtitle">Add Payee to transfer money quickly</string>
|
||||||
|
<string name="feature_send_money_add_icon_desc">Add</string>
|
||||||
|
<string name="feature_send_money_recents_title">Recents</string>
|
||||||
|
<string name="feature_send_money_recents_icon_desc">Recents</string>
|
||||||
|
<string name="feature_send_money_pay_button">Pay</string>
|
||||||
|
<string name="feature_send_money_no_accounts_found_for_search">No accounts found for searched query</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@ -332,7 +332,7 @@ private fun SelectedAccountCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AccountBadge(
|
fun AccountBadge(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
borderColor: Color = KptTheme.colorScheme.primary,
|
borderColor: Color = KptTheme.colorScheme.primary,
|
||||||
|
|||||||
@ -13,8 +13,12 @@ import org.koin.core.module.dsl.viewModelOf
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.mifospay.feature.send.money.ScannerModule
|
import org.mifospay.feature.send.money.ScannerModule
|
||||||
import org.mifospay.feature.send.money.SendMoneyViewModel
|
import org.mifospay.feature.send.money.SendMoneyViewModel
|
||||||
|
import org.mifospay.feature.send.money.selectScreen.SelectScreenViewModel
|
||||||
|
import org.mifospay.feature.send.money.v2.SendMoneyV2ViewModel
|
||||||
|
|
||||||
val SendMoneyModule = module {
|
val SendMoneyModule = module {
|
||||||
includes(ScannerModule)
|
includes(ScannerModule)
|
||||||
viewModelOf(::SendMoneyViewModel)
|
viewModelOf(::SendMoneyViewModel)
|
||||||
|
viewModelOf(::SelectScreenViewModel)
|
||||||
|
viewModelOf(::SendMoneyV2ViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,399 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.send.money.selectScreen
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_select_account_placeholder
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_select_payment_title
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_bottom_bar
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_close
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_no_accounts_found
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_no_accounts_found_for_search
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_oops
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_proceed
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_selected
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_something_went_wrong
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_to_account
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.mifospay.core.designsystem.component.MifosButton
|
||||||
|
import org.mifospay.core.designsystem.component.MifosGradientBackground
|
||||||
|
import org.mifospay.core.designsystem.component.MifosScaffold
|
||||||
|
import org.mifospay.core.designsystem.component.MifosTopBar
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.designsystem.theme.toRoundedCornerShape
|
||||||
|
import org.mifospay.core.network.model.entity.templates.account.AccountOption
|
||||||
|
import org.mifospay.core.ui.AvatarBox
|
||||||
|
import org.mifospay.core.ui.EmptyContentScreen
|
||||||
|
import org.mifospay.core.ui.MifosProgressIndicator
|
||||||
|
import org.mifospay.core.ui.SimpleSearchBar
|
||||||
|
import org.mifospay.core.ui.utils.EventsEffect
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectPayeeScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
navigateToMakeTransferV2Screen: (
|
||||||
|
toOfficeId: Int?,
|
||||||
|
toClientId: Long?,
|
||||||
|
toAccountTypeId: Int?,
|
||||||
|
toAccountId: Int,
|
||||||
|
amount: Int,
|
||||||
|
accountName: String,
|
||||||
|
accountNo: String,
|
||||||
|
) -> Unit,
|
||||||
|
viewModel: SelectScreenViewModel = koinViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
EventsEffect(viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
is SelectScreenEvent.NavigateToTransferScreen -> {
|
||||||
|
navigateToMakeTransferV2Screen(
|
||||||
|
state.selectedAccount?.officeId,
|
||||||
|
state.selectedAccount?.clientId,
|
||||||
|
state.selectedAccount?.accountType?.id ?: 0,
|
||||||
|
state.selectedAccount?.accountId ?: 0,
|
||||||
|
state.amount.toIntOrNull() ?: 0,
|
||||||
|
state.selectedAccount?.clientName ?: "",
|
||||||
|
state.selectedAccount?.accountNo ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectScreenEvent.NavigateBack -> navigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectAccountScreen(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onAction = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(it) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun SelectAccountScreen(
|
||||||
|
state: SelectScreenState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onAction: (SelectScreenAction) -> Unit,
|
||||||
|
) {
|
||||||
|
MifosGradientBackground {
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
MifosTopBar(
|
||||||
|
topBarTitle = stringResource(Res.string.feature_select_payment_title),
|
||||||
|
backPress = {
|
||||||
|
onAction(SelectScreenAction.NavigateBack)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
SendMoneyBottomBar(
|
||||||
|
showDetails = state.isProceedEnabled,
|
||||||
|
selectedAccount = state.selectedAccount,
|
||||||
|
onDeselect = {
|
||||||
|
onAction(SelectScreenAction.DeselectAccount)
|
||||||
|
},
|
||||||
|
onClickProceed = {
|
||||||
|
onAction(SelectScreenAction.OnProceedClicked)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
SelectAccountContent(
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
onAction = onAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectAccountContent(
|
||||||
|
state: SelectScreenState,
|
||||||
|
onAction: (SelectScreenAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
when (state.state) {
|
||||||
|
is SelectScreenState.State.Error -> {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_send_money_oops),
|
||||||
|
subTitle = stringResource(Res.string.feature_send_money_something_went_wrong),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
iconTint = KptTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SelectScreenState.State.Loading -> {
|
||||||
|
MifosProgressIndicator()
|
||||||
|
}
|
||||||
|
SelectScreenState.State.NoAccounts -> {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_send_money_oops),
|
||||||
|
subTitle = stringResource(Res.string.feature_send_money_no_accounts_found),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SelectScreenState.State.Success -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.padding(horizontal = KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
SimpleSearchBar(
|
||||||
|
query = state.accountNumber,
|
||||||
|
placeHolder = stringResource(Res.string.feature_select_account_placeholder),
|
||||||
|
onQueryChange = {
|
||||||
|
onAction(SelectScreenAction.AccountNumberChanged(it))
|
||||||
|
},
|
||||||
|
onClearQuery = {
|
||||||
|
onAction(SelectScreenAction.AccountNumberChanged(""))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
accountListContent(
|
||||||
|
state = state,
|
||||||
|
onAction = {
|
||||||
|
onAction(it)
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
selected = { state.selectedAccount == it },
|
||||||
|
)
|
||||||
|
if (state.filteredToAccounts?.isEmpty() == true) {
|
||||||
|
item {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_send_money_oops),
|
||||||
|
subTitle = stringResource(Res.string.feature_send_money_no_accounts_found_for_search),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.accountListContent(
|
||||||
|
state: SelectScreenState,
|
||||||
|
selected: (AccountOption?) -> Boolean,
|
||||||
|
onAction: (SelectScreenAction.SelectAccount) -> Unit,
|
||||||
|
) {
|
||||||
|
items(state.filteredToAccounts?.size ?: 0) { it ->
|
||||||
|
AccountCard(
|
||||||
|
account = state.filteredToAccounts?.get(it),
|
||||||
|
selected = selected,
|
||||||
|
onClick = remember(state.filteredToAccounts?.get(it)) {
|
||||||
|
{
|
||||||
|
onAction(SelectScreenAction.SelectAccount(it))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountCard(
|
||||||
|
account: AccountOption?,
|
||||||
|
selected: (AccountOption?) -> Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (AccountOption?) -> Unit,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = account?.clientName ?: "")
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = account?.accountNo ?: "")
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(
|
||||||
|
icon = MifosIcons.Bank,
|
||||||
|
backgroundColor = KptTheme.colorScheme.tertiary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
trailingContent = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = selected(account),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Check,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_money_selected),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onClick(account)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SendMoneyBottomBar(
|
||||||
|
showDetails: Boolean,
|
||||||
|
selectedAccount: AccountOption?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClickProceed: () -> Unit,
|
||||||
|
onDeselect: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
shape = KptTheme.shapes.toRoundedCornerShape(
|
||||||
|
topStart = KptTheme.spacing.sm,
|
||||||
|
topEnd = KptTheme.spacing.sm,
|
||||||
|
),
|
||||||
|
color = KptTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showDetails && selectedAccount != null,
|
||||||
|
label = stringResource(Res.string.feature_send_money_bottom_bar),
|
||||||
|
enter = fadeIn() + slideInVertically(
|
||||||
|
initialOffsetY = { fullHeight ->
|
||||||
|
fullHeight / 4
|
||||||
|
},
|
||||||
|
),
|
||||||
|
exit = fadeOut(tween(300)) + slideOutVertically(
|
||||||
|
targetOffsetY = { fullHeight ->
|
||||||
|
fullHeight / 4
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
selectedAccount?.let {
|
||||||
|
SelectedAccountCard(
|
||||||
|
account = selectedAccount,
|
||||||
|
onDeselect = onDeselect,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MifosButton(
|
||||||
|
onClick = onClickProceed,
|
||||||
|
enabled = showDetails,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(Res.string.feature_send_money_proceed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectedAccountCard(
|
||||||
|
account: AccountOption,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onDeselect: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_money_to_account),
|
||||||
|
style = KptTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
border = BorderStroke(1.dp, KptTheme.colorScheme.primary),
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = account.clientName ?: "",
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = account.accountNo ?: "")
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(icon = MifosIcons.Bank)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onDeselect,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_money_close),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.feature.send.money.selectScreen
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SelectAccountRoute
|
||||||
|
|
||||||
|
fun NavController.navigateToSelectAccountScreen(navOptions: NavOptions? = null) {
|
||||||
|
this.navigate(SelectAccountRoute, navOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.selectAccountScreenDestination(
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
navigateToMakeTransferV2Screen: (
|
||||||
|
toOfficeId: Int?,
|
||||||
|
toClientId: Long?,
|
||||||
|
toAccountTypeId: Int?,
|
||||||
|
toAccountId: Int,
|
||||||
|
amount: Int,
|
||||||
|
accountName: String,
|
||||||
|
accountNo: String,
|
||||||
|
) -> Unit,
|
||||||
|
) {
|
||||||
|
composable<SelectAccountRoute> {
|
||||||
|
SelectPayeeScreen(
|
||||||
|
navigateToMakeTransferV2Screen = navigateToMakeTransferV2Screen,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.send.money.selectScreen
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.mifospay.core.data.repository.ThirdPartyTransferRepository
|
||||||
|
import org.mifospay.core.network.model.entity.templates.account.AccountOption
|
||||||
|
import org.mifospay.core.ui.utils.BaseViewModel
|
||||||
|
|
||||||
|
class SelectScreenViewModel(
|
||||||
|
private val repository: ThirdPartyTransferRepository,
|
||||||
|
) : BaseViewModel<SelectScreenState, SelectScreenEvent, SelectScreenAction>(
|
||||||
|
initialState = SelectScreenState(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
getToAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: SelectScreenAction) {
|
||||||
|
when (action) {
|
||||||
|
is SelectScreenAction.AccountNumberChanged -> {
|
||||||
|
val filteredAccounts = state.toAccountOptions?.filter { account ->
|
||||||
|
account.accountNo?.contains(action.accountNumber) == true
|
||||||
|
}
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
accountNumber = action.accountNumber,
|
||||||
|
filteredToAccounts = filteredAccounts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectScreenAction.NavigateBack -> {
|
||||||
|
sendEvent(SelectScreenEvent.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SelectScreenAction.SelectAccount -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(selectedAccount = action.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectScreenAction.DeselectAccount -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(selectedAccount = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectScreenAction.OnProceedClicked -> {
|
||||||
|
sendEvent(SelectScreenEvent.NavigateToTransferScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getToAccounts() {
|
||||||
|
try {
|
||||||
|
val res = repository.getTransferTemplate()
|
||||||
|
if (res.toAccountOptions.isNullOrEmpty()) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
state = SelectScreenState.State.NoAccounts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
state = SelectScreenState.State.Success,
|
||||||
|
toAccountOptions = res.toAccountOptions,
|
||||||
|
filteredToAccounts = res.toAccountOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
state = SelectScreenState.State.Error(e.message ?: ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SelectScreenState(
|
||||||
|
val amount: String = "",
|
||||||
|
val accountNumber: String = "",
|
||||||
|
val selectedAccount: AccountOption? = null,
|
||||||
|
val state: State = State.Loading,
|
||||||
|
val toAccountOptions: List<AccountOption>? = emptyList(),
|
||||||
|
val filteredToAccounts: List<AccountOption>? = emptyList(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
val isProceedEnabled: Boolean
|
||||||
|
get() = selectedAccount != null
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
data object Loading : State
|
||||||
|
data object NoAccounts : State
|
||||||
|
data object Success : State
|
||||||
|
data class Error(val message: String) : State
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SelectScreenEvent {
|
||||||
|
data object NavigateToTransferScreen : SelectScreenEvent
|
||||||
|
|
||||||
|
data object NavigateBack : SelectScreenEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SelectScreenAction {
|
||||||
|
data object NavigateBack : SelectScreenAction
|
||||||
|
|
||||||
|
data class AccountNumberChanged(val accountNumber: String) : SelectScreenAction
|
||||||
|
|
||||||
|
data class SelectAccount(val account: AccountOption?) : SelectScreenAction
|
||||||
|
|
||||||
|
data object DeselectAccount : SelectScreenAction
|
||||||
|
|
||||||
|
data object OnProceedClicked : SelectScreenAction
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.send.money.v2
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SendMoneyRoute
|
||||||
|
|
||||||
|
fun NavController.navigateToSendMoneyV2Screen(navOptions: NavOptions? = null) {
|
||||||
|
this.navigate(SendMoneyRoute, navOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.sendMoneyScreenDestination(
|
||||||
|
navigateToSelectAccountScreen: () -> Unit,
|
||||||
|
navigateToBeneficiary: () -> Unit,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
composable<SendMoneyRoute> {
|
||||||
|
SendMoneyv2Screen(
|
||||||
|
navigateToSelectAccountScreen = navigateToSelectAccountScreen,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToBeneficiary = navigateToBeneficiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,272 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.send.money.v2
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_select_account_placeholder
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_add_icon_desc
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_add_payee_subtitle
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_add_payee_title
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_pay_button
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_recents_title
|
||||||
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_send
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.mifospay.core.designsystem.component.MifosBottomSheetScaffold
|
||||||
|
import org.mifospay.core.designsystem.component.MifosOutlinedButton
|
||||||
|
import org.mifospay.core.designsystem.component.MifosTextUserImage
|
||||||
|
import org.mifospay.core.designsystem.component.MifosTopBar
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.ui.SimpleSearchBar
|
||||||
|
import org.mifospay.core.ui.utils.EventsEffect
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SendMoneyv2Screen(
|
||||||
|
navigateToSelectAccountScreen: () -> Unit,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
navigateToBeneficiary: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: SendMoneyV2ViewModel = koinViewModel(),
|
||||||
|
) {
|
||||||
|
EventsEffect(viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
SendMoneyV2Event.NavigateToSearchAccountSelection -> navigateToSelectAccountScreen()
|
||||||
|
|
||||||
|
SendMoneyV2Event.NavigateBack -> navigateBack()
|
||||||
|
|
||||||
|
SendMoneyV2Event.NavigateToBeneficiary -> navigateToBeneficiary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SendMoneyScreen(
|
||||||
|
modifier = modifier,
|
||||||
|
onAction = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(it) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SendMoneyScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onAction: (SendMoneyV2Action) -> Unit,
|
||||||
|
) {
|
||||||
|
MifosBottomSheetScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
MifosTopBar(
|
||||||
|
topBarTitle = stringResource(Res.string.feature_send_money_send),
|
||||||
|
backPress = {
|
||||||
|
onAction(SendMoneyV2Action.NavigateBack)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
sheetContent = {
|
||||||
|
// TODO : If we can get recent payment details in self with toAccount number and amount
|
||||||
|
// show those list with pay button here and on click it should navigate to that id and price
|
||||||
|
// RecentBottomSheet()
|
||||||
|
},
|
||||||
|
sheetPeekHeight = 0.dp,
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxSize().padding(paddingValues).padding(horizontal = KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
SimpleSearchBar(
|
||||||
|
query = "",
|
||||||
|
placeHolder = stringResource(Res.string.feature_select_account_placeholder),
|
||||||
|
onQueryChange = {},
|
||||||
|
onClick = {
|
||||||
|
onAction(SendMoneyV2Action.OnSearchBarClicked)
|
||||||
|
},
|
||||||
|
enabled = false,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(KptTheme.spacing.md))
|
||||||
|
AddPayeeCard(
|
||||||
|
onClick = {
|
||||||
|
onAction(SendMoneyV2Action.OnAddPayeeClicked)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddPayeeCard(
|
||||||
|
title: String = stringResource(Res.string.feature_send_money_add_payee_title),
|
||||||
|
subtitle: String = stringResource(Res.string.feature_send_money_add_payee_subtitle),
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onClick() },
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary.copy(0.1f),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f),
|
||||||
|
shape = CircleShape,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Add,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_money_add_icon_desc),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(KptTheme.spacing.md))
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = KptTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove it once we get data from self api
|
||||||
|
data class RecentTransaction(
|
||||||
|
val name: String,
|
||||||
|
val toAccount: String,
|
||||||
|
val amount: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecentBottomSheet() {
|
||||||
|
// TODO : when we get api pass data from api.
|
||||||
|
val recents = listOf(
|
||||||
|
RecentTransaction("Alex Doe", "**** **** 1234", "₹1500"),
|
||||||
|
RecentTransaction("Jane Smith", "**** **** 5678", "₹2200"),
|
||||||
|
RecentTransaction("Mike Johnson", "**** **** 9012", "₹800"),
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.History,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_money_recents_title),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_money_recents_title),
|
||||||
|
style = KptTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(KptTheme.spacing.md))
|
||||||
|
|
||||||
|
recents.forEach { transaction ->
|
||||||
|
RecentTransactionItem(transaction = transaction)
|
||||||
|
Spacer(modifier = Modifier.height(KptTheme.spacing.sm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecentTransactionItem(transaction: RecentTransaction) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(KptTheme.shapes.medium)
|
||||||
|
.padding(horizontal = KptTheme.spacing.md, vertical = KptTheme.spacing.sm),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
MifosTextUserImage(
|
||||||
|
text = transaction.name.first().toString(),
|
||||||
|
size = 40.dp,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = transaction.name,
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = transaction.toAccount,
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MifosOutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
// TODO pass the id and amount from here
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = KptTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_money_pay_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.send.money.v2
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.mifospay.core.ui.utils.BaseViewModel
|
||||||
|
|
||||||
|
class SendMoneyV2ViewModel() : BaseViewModel<SendMoneyV2State, SendMoneyV2Event, SendMoneyV2Action>(
|
||||||
|
initialState = SendMoneyV2State,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun handleAction(action: SendMoneyV2Action) {
|
||||||
|
when (action) {
|
||||||
|
SendMoneyV2Action.NavigateBack -> {
|
||||||
|
sendEvent(SendMoneyV2Event.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
SendMoneyV2Action.OnSearchBarClicked -> {
|
||||||
|
sendEvent(SendMoneyV2Event.NavigateToSearchAccountSelection)
|
||||||
|
}
|
||||||
|
|
||||||
|
SendMoneyV2Action.OnAddPayeeClicked -> {
|
||||||
|
sendEvent(SendMoneyV2Event.NavigateToBeneficiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SendMoneyV2State
|
||||||
|
|
||||||
|
sealed interface SendMoneyV2Event {
|
||||||
|
data object NavigateToSearchAccountSelection : SendMoneyV2Event
|
||||||
|
data object NavigateBack : SendMoneyV2Event
|
||||||
|
data object NavigateToBeneficiary : SendMoneyV2Event
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SendMoneyV2Action {
|
||||||
|
data object NavigateBack : SendMoneyV2Action
|
||||||
|
|
||||||
|
data object OnSearchBarClicked : SendMoneyV2Action
|
||||||
|
|
||||||
|
data object OnAddPayeeClicked : SendMoneyV2Action
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user