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:exported="true"
|
||||
android:theme="@style/Theme.MifosSplash"
|
||||
android:windowSoftInputMode="adjustPan|adjustResize">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.navOptions
|
||||
import org.mifospay.core.ui.utility.TabContent
|
||||
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.navigateToBeneficiaryAddEdit
|
||||
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.success.navigateTransferSuccess
|
||||
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.notification.navigateToNotification
|
||||
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.navigateToSendMoneyScreen
|
||||
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.standing.instruction.createOrUpdate.addEditSIScreen
|
||||
import org.mifospay.feature.standing.instruction.details.siDetailsScreen
|
||||
@ -150,7 +157,7 @@ internal fun MifosNavHost(
|
||||
onRequest = {
|
||||
navController.navigateToShowQrScreen()
|
||||
},
|
||||
onPay = navController::navigateToSendMoneyScreen,
|
||||
onPay = navController::navigateToSendMoneyV2Screen,
|
||||
navigateToTransactionDetail = navController::navigateToSpecificTransaction,
|
||||
navigateToAccountDetail = navController::navigateToSavingAccountDetails,
|
||||
navigateToHistory = navController::navigateToHistory,
|
||||
@ -277,6 +284,37 @@ internal fun MifosNavHost(
|
||||
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(
|
||||
navigateBack = navController::popBackStack,
|
||||
onTransferSuccess = {
|
||||
|
||||
@ -32,7 +32,7 @@ interface ClientRepository {
|
||||
|
||||
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>>>
|
||||
|
||||
|
||||
@ -9,14 +9,13 @@
|
||||
*/
|
||||
package org.mifospay.core.data.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.mifospay.core.common.DataState
|
||||
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.AccountOptionsTemplate
|
||||
|
||||
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
|
||||
.getClientAccounts(clientId)
|
||||
.asDataStateFlow().flowOn(ioDispatcher)
|
||||
}
|
||||
|
||||
override suspend fun getAccounts(
|
||||
|
||||
@ -10,29 +10,29 @@
|
||||
package org.mifospay.core.data.repositoryImpl
|
||||
|
||||
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.asDataStateFlow
|
||||
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.payload.TransferPayload
|
||||
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
||||
|
||||
class ThirdPartyTransferRepositoryImpl(
|
||||
private val apiManager: FineractApiManager,
|
||||
private val apiManager: SelfServiceApiManager,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ThirdPartyTransferRepository {
|
||||
override suspend fun getTransferTemplate(): Flow<DataState<AccountOptionsTemplate>> {
|
||||
override suspend fun getTransferTemplate(): AccountOptionsTemplate {
|
||||
return apiManager.thirdPartyTransferApi
|
||||
.accountTransferTemplate()
|
||||
.asDataStateFlow().flowOn(ioDispatcher)
|
||||
}
|
||||
|
||||
override suspend fun makeTransfer(payload: TransferPayload): Flow<DataState<TPTResponse>> {
|
||||
return apiManager.thirdPartyTransferApi
|
||||
.makeTransfer(payload)
|
||||
.asDataStateFlow().flowOn(ioDispatcher)
|
||||
override suspend fun makeTransfer(payload: TransferPayload): DataState<TPTResponse> {
|
||||
return try {
|
||||
val result = apiManager.thirdPartyTransferApi
|
||||
.makeTransfer(payload)
|
||||
DataState.Success(result)
|
||||
} catch (e: Exception) {
|
||||
DataState.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +124,7 @@ fun MifosTextField(
|
||||
errorText: String? = null,
|
||||
onClickClearIcon: () -> Unit = { onValueChange("") },
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
textStyle: TextStyle? = null,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = true,
|
||||
maxLines: Int = if (singleLine) 1 else Int.Companion.MAX_VALUE,
|
||||
@ -177,9 +178,9 @@ fun MifosTextField(
|
||||
)
|
||||
}
|
||||
},
|
||||
textStyle = LocalDensity.current.run {
|
||||
TextStyle(color = KptTheme.colorScheme.onSurface)
|
||||
},
|
||||
textStyle = (textStyle ?: TextStyle()).copy(
|
||||
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.Description
|
||||
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.FlashOn
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
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.Photo
|
||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||
@ -78,6 +81,7 @@ object MifosIcons {
|
||||
val ChevronRight: ImageVector = Icons.Filled.ChevronRight
|
||||
val QrCode: ImageVector = Icons.Filled.QrCode
|
||||
val Close: ImageVector = Icons.Filled.Close
|
||||
val Error: ImageVector = Icons.Filled.Error
|
||||
val AttachMoney: ImageVector = Icons.Filled.AttachMoney
|
||||
val OutlinedVisibilityOff: ImageVector = Icons.Outlined.VisibilityOff
|
||||
val OutlinedVisibility: ImageVector = Icons.Outlined.Visibility
|
||||
@ -85,6 +89,7 @@ object MifosIcons {
|
||||
val Visibility: ImageVector = Icons.Filled.Visibility
|
||||
val Check: ImageVector = Icons.Default.Check
|
||||
val KeyboardArrowDown: ImageVector = Icons.Default.KeyboardArrowDown
|
||||
val KeyboardArrowUp: ImageVector = Icons.Default.KeyboardArrowUp
|
||||
val Home = Icons.Outlined.Home
|
||||
val HomeBoarder = Icons.Rounded.Home
|
||||
val Payment = Icons.Rounded.SwapHoriz
|
||||
@ -129,4 +134,5 @@ object MifosIcons {
|
||||
val Scan = Icons.Outlined.QrCodeScanner
|
||||
val RadioButtonUnchecked = Icons.Default.RadioButtonUnchecked
|
||||
val RadioButtonChecked = Icons.Filled.RadioButtonChecked
|
||||
val History = Icons.Default.History
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import kotlinx.serialization.Serializable
|
||||
data class AccountOption(
|
||||
val accountId: Int? = 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 clientName: String? = null,
|
||||
val officeId: Int? = null,
|
||||
|
||||
@ -53,7 +53,7 @@ interface ClientService {
|
||||
): Unit
|
||||
|
||||
@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")
|
||||
fun getAccounts(
|
||||
|
||||
@ -12,7 +12,6 @@ package org.mifospay.core.network.services
|
||||
import de.jensklingenberg.ktorfit.http.Body
|
||||
import de.jensklingenberg.ktorfit.http.GET
|
||||
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.payload.TransferPayload
|
||||
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
||||
@ -20,8 +19,8 @@ import org.mifospay.core.network.utils.ApiEndPoints
|
||||
|
||||
interface ThirdPartyTransferService {
|
||||
@GET(ApiEndPoints.ACCOUNT_TRANSFER + "/template?type=tpt")
|
||||
suspend fun accountTransferTemplate(): Flow<AccountOptionsTemplate>
|
||||
suspend fun accountTransferTemplate(): AccountOptionsTemplate
|
||||
|
||||
@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_send_money">Send money</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_unable_to_process">Unable to process 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_to_account">To Account</string>
|
||||
<string name="feature_make_transfer_from_account">From Account</string>
|
||||
<string name="feature_make_transfer_description_label">Add a Message</string>
|
||||
<string name="feature_make_transfer_to_account">To %1$s</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_oops_title">Oops!</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_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>
|
||||
|
||||
@ -33,6 +33,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
@ -235,7 +236,7 @@ private fun AccountList(
|
||||
onClick: (Account) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().padding(KptTheme.spacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||
) {
|
||||
Text(
|
||||
@ -289,15 +290,18 @@ private fun AccountItem(
|
||||
trailingContent = {
|
||||
AnimatedContent(
|
||||
targetState = selected,
|
||||
) {
|
||||
label = "radioAnim",
|
||||
) { isSelected ->
|
||||
Icon(
|
||||
imageVector = if (it) {
|
||||
imageVector = if (isSelected) {
|
||||
MifosIcons.RadioButtonChecked
|
||||
} else {
|
||||
MifosIcons.RadioButtonUnchecked
|
||||
},
|
||||
contentDescription = stringResource(Res.string.feature_make_transfer_check_icon_description),
|
||||
tint = if (it) {
|
||||
contentDescription = stringResource(
|
||||
Res.string.feature_make_transfer_check_icon_description,
|
||||
),
|
||||
tint = if (isSelected) {
|
||||
KptTheme.colorScheme.primary
|
||||
} else {
|
||||
KptTheme.colorScheme.outlineVariant
|
||||
@ -313,7 +317,7 @@ private fun AccountItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClientCard(
|
||||
fun ClientCard(
|
||||
client: PaymentQrData,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@ -369,7 +373,7 @@ private fun ClientCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountBadge(
|
||||
fun AccountBadge(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
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.dsl.module
|
||||
import org.mifospay.feature.make.transfer.MakeTransferViewModel
|
||||
import org.mifospay.feature.make.transfer.v2.MakeTransferV2ScreenV2ViewModel
|
||||
|
||||
val MakeTransferModule = module {
|
||||
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.lazy.LazyColumn
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -33,18 +28,18 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
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_title
|
||||
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_unexpected_error_subtitle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
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.rememberMifosPullToRefreshState
|
||||
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||
import org.mifospay.core.model.savingsaccount.Currency
|
||||
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.Timeline
|
||||
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.MerchantViewModel
|
||||
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 -> {
|
||||
MerchantScreenContent(
|
||||
@ -204,48 +204,13 @@ private fun SearchBarScreen(
|
||||
onClearQuery: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SearchBar(
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = onSearch,
|
||||
expanded = false,
|
||||
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 = {},
|
||||
MifosSearchBar(
|
||||
query = query,
|
||||
placeHolder = stringResource(Res.string.feature_merchants_search),
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = onSearch,
|
||||
onClearQuery = onClearQuery,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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_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_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>
|
||||
@ -332,7 +332,7 @@ private fun SelectedAccountCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountBadge(
|
||||
fun AccountBadge(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
borderColor: Color = KptTheme.colorScheme.primary,
|
||||
|
||||
@ -13,8 +13,12 @@ import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
import org.mifospay.feature.send.money.ScannerModule
|
||||
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 {
|
||||
includes(ScannerModule)
|
||||
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