mirror of
https://github.com/openMF/mobile-wallet.git
synced 2026-02-06 11:56:48 +00:00
Feature interbank Transfer (#1941)
This commit is contained in:
parent
9a127fd724
commit
ee2f78bd77
@ -2809,6 +2809,40 @@
|
|||||||
| | +--- org.jetbrains.compose.material:material-icons-extended:1.7.3 (*)
|
| | +--- org.jetbrains.compose.material:material-icons-extended:1.7.3 (*)
|
||||||
| | +--- org.jetbrains.compose.components:components-resources:1.8.2 (*)
|
| | +--- org.jetbrains.compose.components:components-resources:1.8.2 (*)
|
||||||
| | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.8.2 (*)
|
| | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.8.2 (*)
|
||||||
|
| +--- project :feature:send-interbank
|
||||||
|
| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.9.2 (*)
|
||||||
|
| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2 (*)
|
||||||
|
| | +--- androidx.tracing:tracing-ktx:1.3.0 (*)
|
||||||
|
| | +--- io.insert-koin:koin-bom:4.1.0 (*)
|
||||||
|
| | +--- io.insert-koin:koin-android:4.1.0 (*)
|
||||||
|
| | +--- io.insert-koin:koin-androidx-compose:4.1.0 (*)
|
||||||
|
| | +--- io.insert-koin:koin-androidx-navigation:4.1.0 (*)
|
||||||
|
| | +--- io.insert-koin:koin-core-viewmodel:4.1.0 (*)
|
||||||
|
| | +--- com.google.android.gms:play-services-code-scanner:16.1.0 (*)
|
||||||
|
| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.20 -> 2.1.21 (*)
|
||||||
|
| | +--- io.insert-koin:koin-core:4.1.0 (*)
|
||||||
|
| | +--- io.insert-koin:koin-annotations:2.1.0 (*)
|
||||||
|
| | +--- project :core:ui (*)
|
||||||
|
| | +--- project :core:designsystem (*)
|
||||||
|
| | +--- project :core:data (*)
|
||||||
|
| | +--- io.insert-koin:koin-compose:4.1.0 (*)
|
||||||
|
| | +--- io.insert-koin:koin-compose-viewmodel:4.1.0 (*)
|
||||||
|
| | +--- org.jetbrains.compose.runtime:runtime:1.8.2 (*)
|
||||||
|
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.1 (*)
|
||||||
|
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1 (*)
|
||||||
|
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.1 (*)
|
||||||
|
| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.1 (*)
|
||||||
|
| | +--- org.jetbrains.androidx.savedstate:savedstate:1.3.1 (*)
|
||||||
|
| | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*)
|
||||||
|
| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.9.0-beta03 (*)
|
||||||
|
| | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 (*)
|
||||||
|
| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1 (*)
|
||||||
|
| | +--- org.jetbrains.compose.ui:ui:1.8.2 (*)
|
||||||
|
| | +--- org.jetbrains.compose.foundation:foundation:1.8.2 (*)
|
||||||
|
| | +--- org.jetbrains.compose.material3:material3:1.8.2 (*)
|
||||||
|
| | +--- org.jetbrains.compose.material:material-icons-extended:1.7.3 (*)
|
||||||
|
| | +--- org.jetbrains.compose.components:components-resources:1.8.2 (*)
|
||||||
|
| | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.8.2 (*)
|
||||||
| +--- project :feature:make-transfer
|
| +--- project :feature:make-transfer
|
||||||
| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.9.2 (*)
|
| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.9.2 (*)
|
||||||
| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2 (*)
|
| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2 (*)
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
:feature:receipt
|
:feature:receipt
|
||||||
:feature:request-money
|
:feature:request-money
|
||||||
:feature:savedcards
|
:feature:savedcards
|
||||||
|
:feature:send-interbank
|
||||||
:feature:send-money
|
:feature:send-money
|
||||||
:feature:settings
|
:feature:settings
|
||||||
:feature:standing-instruction
|
:feature:standing-instruction
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package: name='org.mifospay' versionCode='1' versionName='2025.8.2-beta.0.6' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
|
package: name='org.mifospay' versionCode='1' versionName='2025.10.5-beta.0.16' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
|
||||||
minSdkVersion:'26'
|
minSdkVersion:'26'
|
||||||
targetSdkVersion:'34'
|
targetSdkVersion:'34'
|
||||||
uses-permission: name='android.permission.INTERNET'
|
uses-permission: name='android.permission.INTERNET'
|
||||||
|
|||||||
@ -51,6 +51,7 @@ kotlin {
|
|||||||
implementation(projects.feature.standingInstruction)
|
implementation(projects.feature.standingInstruction)
|
||||||
implementation(projects.feature.requestMoney)
|
implementation(projects.feature.requestMoney)
|
||||||
implementation(projects.feature.sendMoney)
|
implementation(projects.feature.sendMoney)
|
||||||
|
implementation(projects.feature.sendInterbank)
|
||||||
implementation(projects.feature.makeTransfer)
|
implementation(projects.feature.makeTransfer)
|
||||||
implementation(projects.feature.qr)
|
implementation(projects.feature.qr)
|
||||||
implementation(projects.feature.merchants)
|
implementation(projects.feature.merchants)
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import org.mifospay.feature.qr.di.QrModule
|
|||||||
import org.mifospay.feature.receipt.di.ReceiptModule
|
import org.mifospay.feature.receipt.di.ReceiptModule
|
||||||
import org.mifospay.feature.request.money.di.RequestMoneyModule
|
import org.mifospay.feature.request.money.di.RequestMoneyModule
|
||||||
import org.mifospay.feature.savedcards.di.SavedCardsModule
|
import org.mifospay.feature.savedcards.di.SavedCardsModule
|
||||||
|
import org.mifospay.feature.send.interbank.di.interbankTransferModule
|
||||||
import org.mifospay.feature.send.money.di.SendMoneyModule
|
import org.mifospay.feature.send.money.di.SendMoneyModule
|
||||||
import org.mifospay.feature.settings.di.SettingsModule
|
import org.mifospay.feature.settings.di.SettingsModule
|
||||||
import org.mifospay.feature.standing.instruction.di.StandingInstructionModule
|
import org.mifospay.feature.standing.instruction.di.StandingInstructionModule
|
||||||
@ -84,6 +85,7 @@ object KoinModules {
|
|||||||
StandingInstructionModule,
|
StandingInstructionModule,
|
||||||
RequestMoneyModule,
|
RequestMoneyModule,
|
||||||
SendMoneyModule,
|
SendMoneyModule,
|
||||||
|
interbankTransferModule,
|
||||||
MakeTransferModule,
|
MakeTransferModule,
|
||||||
QrModule,
|
QrModule,
|
||||||
MerchantsModule,
|
MerchantsModule,
|
||||||
|
|||||||
@ -59,6 +59,7 @@ import org.mifospay.feature.payments.PAYMENTS_ROUTE
|
|||||||
import org.mifospay.feature.payments.PaymentsScreenContents
|
import org.mifospay.feature.payments.PaymentsScreenContents
|
||||||
import org.mifospay.feature.payments.RequestScreen
|
import org.mifospay.feature.payments.RequestScreen
|
||||||
import org.mifospay.feature.payments.paymentsScreen
|
import org.mifospay.feature.payments.paymentsScreen
|
||||||
|
import org.mifospay.feature.payments.selectTransferType.SelectTransferTypeScreen
|
||||||
import org.mifospay.feature.profile.navigation.profileNavGraph
|
import org.mifospay.feature.profile.navigation.profileNavGraph
|
||||||
import org.mifospay.feature.qr.navigation.SCAN_QR_ROUTE
|
import org.mifospay.feature.qr.navigation.SCAN_QR_ROUTE
|
||||||
import org.mifospay.feature.qr.navigation.navigateToScanQr
|
import org.mifospay.feature.qr.navigation.navigateToScanQr
|
||||||
@ -68,12 +69,13 @@ import org.mifospay.feature.request.money.navigation.navigateToShowQrScreen
|
|||||||
import org.mifospay.feature.request.money.navigation.showQrScreen
|
import org.mifospay.feature.request.money.navigation.showQrScreen
|
||||||
import org.mifospay.feature.savedcards.createOrUpdate.addEditCardScreen
|
import org.mifospay.feature.savedcards.createOrUpdate.addEditCardScreen
|
||||||
import org.mifospay.feature.savedcards.details.cardDetailRoute
|
import org.mifospay.feature.savedcards.details.cardDetailRoute
|
||||||
|
import org.mifospay.feature.send.interbank.navigation.interbankTransferScreen
|
||||||
|
import org.mifospay.feature.send.interbank.navigation.navigateToInterbankTransfer
|
||||||
import org.mifospay.feature.send.money.navigation.SEND_MONEY_BASE_ROUTE
|
import org.mifospay.feature.send.money.navigation.SEND_MONEY_BASE_ROUTE
|
||||||
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen
|
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen
|
||||||
import org.mifospay.feature.send.money.navigation.sendMoneyScreen
|
import org.mifospay.feature.send.money.navigation.sendMoneyScreen
|
||||||
import org.mifospay.feature.send.money.selectScreen.navigateToSelectAccountScreen
|
import org.mifospay.feature.send.money.selectScreen.navigateToSelectAccountScreen
|
||||||
import org.mifospay.feature.send.money.selectScreen.selectAccountScreenDestination
|
import org.mifospay.feature.send.money.selectScreen.selectAccountScreenDestination
|
||||||
import org.mifospay.feature.send.money.v2.SendMoneyv2Screen
|
|
||||||
import org.mifospay.feature.send.money.v2.navigateToSendMoneyV2Screen
|
import org.mifospay.feature.send.money.v2.navigateToSendMoneyV2Screen
|
||||||
import org.mifospay.feature.send.money.v2.sendMoneyScreenDestination
|
import org.mifospay.feature.send.money.v2.sendMoneyScreenDestination
|
||||||
import org.mifospay.feature.settings.navigation.settingsScreen
|
import org.mifospay.feature.settings.navigation.settingsScreen
|
||||||
@ -92,17 +94,13 @@ internal fun MifosNavHost(
|
|||||||
|
|
||||||
val paymentsTabContents = listOf(
|
val paymentsTabContents = listOf(
|
||||||
TabContent(PaymentsScreenContents.SEND.name) {
|
TabContent(PaymentsScreenContents.SEND.name) {
|
||||||
SendMoneyv2Screen(
|
SelectTransferTypeScreen(
|
||||||
navigateToSelectAccountScreen = {
|
onIntraBankTransferClick = {
|
||||||
navController.navigateToSelectAccountScreen(returnDestination = "payments")
|
navController.navigateToSendMoneyV2Screen()
|
||||||
},
|
},
|
||||||
navigateBack = {
|
onInterBankTransferClick = {
|
||||||
navController.navigateUp()
|
navController.navigateToInterbankTransfer()
|
||||||
},
|
},
|
||||||
navigateToBeneficiary = {
|
|
||||||
navController.navigateToBeneficiaryAddEdit(BeneficiaryAddEditType.AddItem)
|
|
||||||
},
|
|
||||||
showTopBar = false,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
TabContent(PaymentsScreenContents.REQUEST.name) {
|
TabContent(PaymentsScreenContents.REQUEST.name) {
|
||||||
@ -173,7 +171,7 @@ internal fun MifosNavHost(
|
|||||||
onRequest = {
|
onRequest = {
|
||||||
navController.navigateToShowQrScreen()
|
navController.navigateToShowQrScreen()
|
||||||
},
|
},
|
||||||
onPay = navController::navigateToSendMoneyV2Screen,
|
onPay = navController::navigateToTransferOptions,
|
||||||
navigateToTransactionDetail = navController::navigateToSpecificTransaction,
|
navigateToTransactionDetail = navController::navigateToSpecificTransaction,
|
||||||
navigateToAccountDetail = navController::navigateToSavingAccountDetails,
|
navigateToAccountDetail = navController::navigateToSavingAccountDetails,
|
||||||
navigateToHistory = navController::navigateToHistory,
|
navigateToHistory = navController::navigateToHistory,
|
||||||
@ -360,6 +358,7 @@ internal fun MifosNavHost(
|
|||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
navController.navigate(HOME_ROUTE) {
|
navController.navigate(HOME_ROUTE) {
|
||||||
popUpTo(HOME_ROUTE) {
|
popUpTo(HOME_ROUTE) {
|
||||||
@ -405,5 +404,25 @@ internal fun MifosNavHost(
|
|||||||
setupUpiPinScreen(
|
setupUpiPinScreen(
|
||||||
navigateBack = navController::navigateUp,
|
navigateBack = navController::navigateUp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
transferOptionsDialog(
|
||||||
|
onIntraBankTransferClick = navController::navigateToSendMoneyV2Screen,
|
||||||
|
onInterBankTransferClick = navController::navigateToInterbankTransfer,
|
||||||
|
onDismiss = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interbankTransferScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onTransferSuccess = {
|
||||||
|
navController.navigate(HOME_ROUTE) {
|
||||||
|
popUpTo(HOME_ROUTE) {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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.shared.navigation
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.compose.dialog
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.mifospay.shared.ui.components.TransferOptionsBottomSheet
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object TransferOptionsRoute
|
||||||
|
|
||||||
|
fun NavController.navigateToTransferOptions() {
|
||||||
|
this.navigate(TransferOptionsRoute)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.transferOptionsDialog(
|
||||||
|
onIntraBankTransferClick: () -> Unit,
|
||||||
|
onInterBankTransferClick: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
dialog<TransferOptionsRoute> {
|
||||||
|
TransferOptionsBottomSheet(
|
||||||
|
onIntraBankTransferClick = {
|
||||||
|
onIntraBankTransferClick()
|
||||||
|
},
|
||||||
|
onInterBankTransferClick = {
|
||||||
|
onInterBankTransferClick()
|
||||||
|
},
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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.shared.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_inter_bank_transfer_description
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_inter_bank_transfer_title
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_intra_bank_transfer_description
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_intra_bank_transfer_title
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_transfer_options_title
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.designsystem.component.MifosBottomSheet
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TransferOptionsBottomSheet(
|
||||||
|
onIntraBankTransferClick: () -> Unit,
|
||||||
|
onInterBankTransferClick: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MifosBottomSheet(
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
modifier = modifier,
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_payments_transfer_options_title),
|
||||||
|
modifier = Modifier.padding(horizontal = KptTheme.spacing.md),
|
||||||
|
style = KptTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
// Intra-Bank Transfer Option
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_payments_intra_bank_transfer_title),
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_payments_intra_bank_transfer_description),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onIntraBankTransferClick()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = KptTheme.spacing.md),
|
||||||
|
color = KptTheme.colorScheme.outlineVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inter-Bank Transfer Option
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_payments_inter_bank_transfer_title),
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_payments_inter_bank_transfer_description),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onInterBankTransferClick()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TransferOptionsBottomSheetPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
TransferOptionsBottomSheet(
|
||||||
|
onIntraBankTransferClick = {},
|
||||||
|
onInterBankTransferClick = {},
|
||||||
|
onDismiss = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ import org.mifospay.core.data.repository.AuthenticationRepository
|
|||||||
import org.mifospay.core.data.repository.BeneficiaryRepository
|
import org.mifospay.core.data.repository.BeneficiaryRepository
|
||||||
import org.mifospay.core.data.repository.ClientRepository
|
import org.mifospay.core.data.repository.ClientRepository
|
||||||
import org.mifospay.core.data.repository.DocumentRepository
|
import org.mifospay.core.data.repository.DocumentRepository
|
||||||
|
import org.mifospay.core.data.repository.InterBankRepository
|
||||||
import org.mifospay.core.data.repository.InvoiceRepository
|
import org.mifospay.core.data.repository.InvoiceRepository
|
||||||
import org.mifospay.core.data.repository.KycLevelRepository
|
import org.mifospay.core.data.repository.KycLevelRepository
|
||||||
import org.mifospay.core.data.repository.LocalAssetRepository
|
import org.mifospay.core.data.repository.LocalAssetRepository
|
||||||
@ -39,6 +40,7 @@ import org.mifospay.core.data.repositoryImpl.AuthenticationRepositoryImpl
|
|||||||
import org.mifospay.core.data.repositoryImpl.BeneficiaryRepositoryImpl
|
import org.mifospay.core.data.repositoryImpl.BeneficiaryRepositoryImpl
|
||||||
import org.mifospay.core.data.repositoryImpl.ClientRepositoryImpl
|
import org.mifospay.core.data.repositoryImpl.ClientRepositoryImpl
|
||||||
import org.mifospay.core.data.repositoryImpl.DocumentRepositoryImpl
|
import org.mifospay.core.data.repositoryImpl.DocumentRepositoryImpl
|
||||||
|
import org.mifospay.core.data.repositoryImpl.InterBankRepositoryImpl
|
||||||
import org.mifospay.core.data.repositoryImpl.InvoiceRepositoryImpl
|
import org.mifospay.core.data.repositoryImpl.InvoiceRepositoryImpl
|
||||||
import org.mifospay.core.data.repositoryImpl.KycLevelRepositoryImpl
|
import org.mifospay.core.data.repositoryImpl.KycLevelRepositoryImpl
|
||||||
import org.mifospay.core.data.repositoryImpl.LocalAssetRepositoryImpl
|
import org.mifospay.core.data.repositoryImpl.LocalAssetRepositoryImpl
|
||||||
@ -77,6 +79,7 @@ val RepositoryModule = module {
|
|||||||
}
|
}
|
||||||
single<DocumentRepository> { DocumentRepositoryImpl(get(), get(ioDispatcher)) }
|
single<DocumentRepository> { DocumentRepositoryImpl(get(), get(ioDispatcher)) }
|
||||||
single<InvoiceRepository> { InvoiceRepositoryImpl(get(), get(ioDispatcher)) }
|
single<InvoiceRepository> { InvoiceRepositoryImpl(get(), get(ioDispatcher)) }
|
||||||
|
single<InterBankRepository> { InterBankRepositoryImpl(get(), get(ioDispatcher)) }
|
||||||
single<KycLevelRepository> { KycLevelRepositoryImpl(get(), get(ioDispatcher)) }
|
single<KycLevelRepository> { KycLevelRepositoryImpl(get(), get(ioDispatcher)) }
|
||||||
single<NotificationRepository> { NotificationRepositoryImpl(get(), get(ioDispatcher)) }
|
single<NotificationRepository> { NotificationRepositoryImpl(get(), get(ioDispatcher)) }
|
||||||
single<RegistrationRepository> { RegistrationRepositoryImpl(get(), get(ioDispatcher)) }
|
single<RegistrationRepository> { RegistrationRepositoryImpl(get(), get(ioDispatcher)) }
|
||||||
|
|||||||
@ -10,8 +10,10 @@
|
|||||||
package org.mifospay.core.data.mapper
|
package org.mifospay.core.data.mapper
|
||||||
|
|
||||||
import org.mifospay.core.model.account.Account
|
import org.mifospay.core.model.account.Account
|
||||||
|
import org.mifospay.core.model.savingsaccount.AccountType
|
||||||
import org.mifospay.core.model.savingsaccount.SavingAccountEntity
|
import org.mifospay.core.model.savingsaccount.SavingAccountEntity
|
||||||
import org.mifospay.core.network.model.entity.client.ClientAccountsEntity
|
import org.mifospay.core.network.model.entity.client.ClientAccountsEntity
|
||||||
|
import org.mifospay.core.network.model.entity.templates.account.AccountType as NetworkAccountType
|
||||||
|
|
||||||
fun ClientAccountsEntity.toAccount(): List<Account> {
|
fun ClientAccountsEntity.toAccount(): List<Account> {
|
||||||
return this.savingsAccounts.toAccount()
|
return this.savingsAccounts.toAccount()
|
||||||
@ -23,10 +25,21 @@ fun List<SavingAccountEntity>.toAccount(): List<Account> {
|
|||||||
name = it.productName,
|
name = it.productName,
|
||||||
number = it.accountNo,
|
number = it.accountNo,
|
||||||
id = it.id,
|
id = it.id,
|
||||||
|
externalId = it.externalId,
|
||||||
balance = it.accountBalance,
|
balance = it.accountBalance,
|
||||||
currency = it.currency,
|
currency = it.currency,
|
||||||
productId = it.productId,
|
productId = it.productId,
|
||||||
|
productName = it.productName,
|
||||||
status = it.status,
|
status = it.status,
|
||||||
|
accountType = it.accountType,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NetworkAccountType.toModelAccountType(): AccountType {
|
||||||
|
return AccountType(
|
||||||
|
id = this.id?.toLong() ?: 0L,
|
||||||
|
code = this.code ?: "",
|
||||||
|
value = this.value ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.core.data.repository
|
||||||
|
|
||||||
|
import org.mifospay.core.common.DataState
|
||||||
|
import org.mifospay.core.model.interbank.InterBankParticipantResponse
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoResponse
|
||||||
|
import org.mifospay.core.model.interbank.InterBankTransferRequest
|
||||||
|
import org.mifospay.core.model.interbank.InterBankTransferResponse
|
||||||
|
|
||||||
|
interface InterBankRepository {
|
||||||
|
suspend fun fetchParticipant(
|
||||||
|
partyId: String,
|
||||||
|
currencyCode: String,
|
||||||
|
): DataState<InterBankParticipantResponse>
|
||||||
|
|
||||||
|
suspend fun fetchPartyInfo(
|
||||||
|
partyId: String,
|
||||||
|
currencyCode: String,
|
||||||
|
ownerFspId: String,
|
||||||
|
): DataState<InterBankPartyInfoResponse>
|
||||||
|
|
||||||
|
suspend fun findParticipant(
|
||||||
|
partyId: String,
|
||||||
|
currencyCode: String,
|
||||||
|
): DataState<InterBankPartyInfoResponse>
|
||||||
|
|
||||||
|
suspend fun interBankMakeTransfer(
|
||||||
|
request: InterBankTransferRequest,
|
||||||
|
): DataState<InterBankTransferResponse>
|
||||||
|
}
|
||||||
@ -57,6 +57,10 @@ interface SelfServiceRepository {
|
|||||||
clientId: Long,
|
clientId: Long,
|
||||||
): Flow<DataState<List<Account>>>
|
): Flow<DataState<List<Account>>>
|
||||||
|
|
||||||
|
fun getActiveAccountsWithAccountTransferTemplate(
|
||||||
|
clientId: Long,
|
||||||
|
): Flow<DataState<List<Account>>>
|
||||||
|
|
||||||
fun getAccountsTransactions(clientId: Long): Flow<DataState<List<Transaction>>>
|
fun getAccountsTransactions(clientId: Long): Flow<DataState<List<Transaction>>>
|
||||||
|
|
||||||
fun getTransactions(accountId: List<Long>, limit: Int?): Flow<List<Transaction>>
|
fun getTransactions(accountId: List<Long>, limit: Int?): Flow<List<Transaction>>
|
||||||
|
|||||||
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.core.data.repositoryImpl
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.mifospay.core.common.DataState
|
||||||
|
import org.mifospay.core.data.repository.InterBankRepository
|
||||||
|
import org.mifospay.core.model.interbank.InterBankParticipantRequest
|
||||||
|
import org.mifospay.core.model.interbank.InterBankParticipantResponse
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoRequest
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoResponse
|
||||||
|
import org.mifospay.core.model.interbank.InterBankTransferRequest
|
||||||
|
import org.mifospay.core.model.interbank.InterBankTransferResponse
|
||||||
|
import org.mifospay.core.network.InterBankApiManager
|
||||||
|
|
||||||
|
class InterBankRepositoryImpl(
|
||||||
|
private val apiManager: InterBankApiManager,
|
||||||
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
) : InterBankRepository {
|
||||||
|
|
||||||
|
override suspend fun fetchParticipant(
|
||||||
|
partyId: String,
|
||||||
|
currencyCode: String,
|
||||||
|
): DataState<InterBankParticipantResponse> {
|
||||||
|
return try {
|
||||||
|
val request = InterBankParticipantRequest(
|
||||||
|
partyId = partyId.trim(),
|
||||||
|
currencyCode = currencyCode,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
)
|
||||||
|
val result = withContext(ioDispatcher) {
|
||||||
|
apiManager.interBankApi.fetchParticipant(request)
|
||||||
|
}
|
||||||
|
DataState.Success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DataState.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchPartyInfo(
|
||||||
|
partyId: String,
|
||||||
|
currencyCode: String,
|
||||||
|
ownerFspId: String,
|
||||||
|
): DataState<InterBankPartyInfoResponse> {
|
||||||
|
return try {
|
||||||
|
val request = InterBankPartyInfoRequest(
|
||||||
|
partyId = partyId.trim(),
|
||||||
|
currencyCode = currencyCode,
|
||||||
|
ownerFspId = ownerFspId,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
)
|
||||||
|
val result = withContext(ioDispatcher) {
|
||||||
|
apiManager.interBankApi.fetchPartyInfo(request)
|
||||||
|
}
|
||||||
|
DataState.Success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DataState.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findParticipant(
|
||||||
|
partyId: String,
|
||||||
|
currencyCode: String,
|
||||||
|
): DataState<InterBankPartyInfoResponse> {
|
||||||
|
return try {
|
||||||
|
// First, fetch participant to get the FSP ID
|
||||||
|
val participantResult = fetchParticipant(partyId, currencyCode)
|
||||||
|
|
||||||
|
if (participantResult !is DataState.Success) {
|
||||||
|
return DataState.Error(
|
||||||
|
Exception("Failed to fetch participant"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val participant = participantResult.data
|
||||||
|
|
||||||
|
// Then, fetch party info using the FSP ID
|
||||||
|
val partyInfoResult = fetchPartyInfo(
|
||||||
|
partyId = partyId,
|
||||||
|
currencyCode = currencyCode,
|
||||||
|
ownerFspId = participant.fspId,
|
||||||
|
)
|
||||||
|
|
||||||
|
partyInfoResult
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DataState.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun interBankMakeTransfer(
|
||||||
|
request: InterBankTransferRequest,
|
||||||
|
): DataState<InterBankTransferResponse> {
|
||||||
|
return try {
|
||||||
|
val result = withContext(ioDispatcher) {
|
||||||
|
apiManager.interBankApi.interBankMakeTransfer(request)
|
||||||
|
}
|
||||||
|
DataState.Success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DataState.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ import org.mifospay.core.common.asDataStateFlow
|
|||||||
import org.mifospay.core.common.combineResultsWith
|
import org.mifospay.core.common.combineResultsWith
|
||||||
import org.mifospay.core.data.mapper.toAccount
|
import org.mifospay.core.data.mapper.toAccount
|
||||||
import org.mifospay.core.data.mapper.toModel
|
import org.mifospay.core.data.mapper.toModel
|
||||||
|
import org.mifospay.core.data.mapper.toModelAccountType
|
||||||
import org.mifospay.core.data.mapper.toTransactionList
|
import org.mifospay.core.data.mapper.toTransactionList
|
||||||
import org.mifospay.core.data.repository.SelfServiceRepository
|
import org.mifospay.core.data.repository.SelfServiceRepository
|
||||||
import org.mifospay.core.data.util.Constants
|
import org.mifospay.core.data.util.Constants
|
||||||
@ -188,6 +189,39 @@ class SelfServiceRepositoryImpl(
|
|||||||
.asDataStateFlow()
|
.asDataStateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getActiveAccountsWithAccountTransferTemplate(
|
||||||
|
clientId: Long,
|
||||||
|
): Flow<DataState<List<Account>>> {
|
||||||
|
val accountsFlow = apiManager.clientsApi
|
||||||
|
.getAccounts(clientId, Constants.SAVINGS)
|
||||||
|
.map { entity -> entity.savingsAccounts.filter { it.status.active } }
|
||||||
|
.flowOn(dispatcher)
|
||||||
|
|
||||||
|
val templateFlow = apiManager.accountTransfersApi
|
||||||
|
.getAccountTransferTemplate()
|
||||||
|
.flowOn(dispatcher)
|
||||||
|
|
||||||
|
return accountsFlow.zip(templateFlow) { accounts, template ->
|
||||||
|
accounts
|
||||||
|
.toAccount()
|
||||||
|
.map { account ->
|
||||||
|
val templateAccount = template.fromAccountOptions?.firstOrNull {
|
||||||
|
it.accountNo == account.number
|
||||||
|
}
|
||||||
|
if (templateAccount == null) {
|
||||||
|
account
|
||||||
|
} else {
|
||||||
|
account.copy(
|
||||||
|
clientName = templateAccount.clientName ?: "",
|
||||||
|
accountType = templateAccount.accountType?.toModelAccountType(),
|
||||||
|
officeName = templateAccount.officeName,
|
||||||
|
officeId = templateAccount.officeId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.asDataStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getTransactions(accountId: List<Long>, limit: Int?): Flow<List<Transaction>> {
|
override fun getTransactions(accountId: List<Long>, limit: Int?): Flow<List<Transaction>> {
|
||||||
return accountId.asFlow().flatMapMerge { clientId ->
|
return accountId.asFlow().flatMapMerge { clientId ->
|
||||||
getSelfAccountTransactions(clientId)
|
getSelfAccountTransactions(clientId)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ package org.mifospay.core.designsystem.icon
|
|||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.ArrowOutward
|
import androidx.compose.material.icons.filled.ArrowOutward
|
||||||
@ -34,7 +35,7 @@ import androidx.compose.material.icons.filled.History
|
|||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
import androidx.compose.material.icons.filled.OpenInNew
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.icons.filled.Photo
|
import androidx.compose.material.icons.filled.Photo
|
||||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||||
@ -48,6 +49,7 @@ import androidx.compose.material.icons.filled.VisibilityOff
|
|||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material.icons.outlined.AccountCircle
|
import androidx.compose.material.icons.outlined.AccountCircle
|
||||||
import androidx.compose.material.icons.outlined.Cancel
|
import androidx.compose.material.icons.outlined.Cancel
|
||||||
|
import androidx.compose.material.icons.outlined.CheckCircle
|
||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
import androidx.compose.material.icons.outlined.DoneAll
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
@ -62,11 +64,13 @@ import androidx.compose.material.icons.outlined.Visibility
|
|||||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.material.icons.outlined.Wallet
|
import androidx.compose.material.icons.outlined.Wallet
|
||||||
import androidx.compose.material.icons.rounded.AccountBalance
|
import androidx.compose.material.icons.rounded.AccountBalance
|
||||||
|
import androidx.compose.material.icons.rounded.AccountBalanceWallet
|
||||||
import androidx.compose.material.icons.rounded.AccountCircle
|
import androidx.compose.material.icons.rounded.AccountCircle
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Contacts
|
import androidx.compose.material.icons.rounded.Contacts
|
||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
import androidx.compose.material.icons.rounded.Money
|
||||||
import androidx.compose.material.icons.rounded.MoreVert
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
import androidx.compose.material.icons.rounded.QrCode
|
import androidx.compose.material.icons.rounded.QrCode
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
@ -142,4 +146,9 @@ object MifosIcons {
|
|||||||
val Filter = Icons.Default.FilterList
|
val Filter = Icons.Default.FilterList
|
||||||
val OpenInNew = Icons.AutoMirrored.Filled.OpenInNew
|
val OpenInNew = Icons.AutoMirrored.Filled.OpenInNew
|
||||||
val Warning = Icons.Default.Warning
|
val Warning = Icons.Default.Warning
|
||||||
|
val Location = Icons.Filled.LocationOn
|
||||||
|
val Savings = Icons.Rounded.AccountBalanceWallet
|
||||||
|
val Transfer = Icons.Rounded.Money
|
||||||
|
val CheckCircle = Icons.Outlined.CheckCircle
|
||||||
|
val ArrowRight = Icons.AutoMirrored.Filled.KeyboardArrowRight
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
package org.mifospay.core.model.account
|
package org.mifospay.core.model.account
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.mifospay.core.model.savingsaccount.AccountType
|
||||||
import org.mifospay.core.model.savingsaccount.Currency
|
import org.mifospay.core.model.savingsaccount.Currency
|
||||||
import org.mifospay.core.model.savingsaccount.Status
|
import org.mifospay.core.model.savingsaccount.Status
|
||||||
|
|
||||||
@ -20,7 +21,13 @@ data class Account(
|
|||||||
val number: String,
|
val number: String,
|
||||||
val balance: Double = 0.0,
|
val balance: Double = 0.0,
|
||||||
val id: Long = 0L,
|
val id: Long = 0L,
|
||||||
|
val externalId: String? = null,
|
||||||
|
val productName: String? = null,
|
||||||
val productId: Long = 0L,
|
val productId: Long = 0L,
|
||||||
val currency: Currency,
|
val currency: Currency,
|
||||||
val status: Status,
|
val status: Status,
|
||||||
|
val clientName: String = "",
|
||||||
|
val accountType: AccountType? = null,
|
||||||
|
val officeName: String? = null,
|
||||||
|
val officeId: Int? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.core.model.interbank
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InterBankParticipantRequest(
|
||||||
|
@SerialName("partyId")
|
||||||
|
val partyId: String,
|
||||||
|
@SerialName("partyIdType")
|
||||||
|
val partyIdType: String,
|
||||||
|
@SerialName("currencyCode")
|
||||||
|
val currencyCode: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InterBankParticipantResponse(
|
||||||
|
@SerialName("partyId")
|
||||||
|
val partyId: String,
|
||||||
|
@SerialName("fspId")
|
||||||
|
val fspId: String,
|
||||||
|
@SerialName("executionStatus")
|
||||||
|
val executionStatus: Boolean,
|
||||||
|
@SerialName("systemMessage")
|
||||||
|
val systemMessage: String,
|
||||||
|
)
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.core.model.interbank
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InterBankPartyInfoRequest(
|
||||||
|
@SerialName("partyId")
|
||||||
|
val partyId: String,
|
||||||
|
@SerialName("partyIdType")
|
||||||
|
val partyIdType: String,
|
||||||
|
@SerialName("currencyCode")
|
||||||
|
val currencyCode: String,
|
||||||
|
@SerialName("ownerFspId")
|
||||||
|
val ownerFspId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InterBankPartyInfoResponse(
|
||||||
|
@SerialName("sourceFspId")
|
||||||
|
val sourceFspId: String,
|
||||||
|
|
||||||
|
@SerialName("destinationFspId")
|
||||||
|
val destinationFspId: String,
|
||||||
|
|
||||||
|
@SerialName("requestId")
|
||||||
|
val requestId: String,
|
||||||
|
|
||||||
|
@SerialName("partyId")
|
||||||
|
val partyId: String,
|
||||||
|
|
||||||
|
@SerialName("partySubIdOrType")
|
||||||
|
val partySubIdOrType: String? = null,
|
||||||
|
|
||||||
|
@SerialName("currencyCode")
|
||||||
|
val currencyCode: String,
|
||||||
|
|
||||||
|
@SerialName("firsName")
|
||||||
|
val firstName: String,
|
||||||
|
|
||||||
|
@SerialName("middleName")
|
||||||
|
val middleName: String? = null,
|
||||||
|
|
||||||
|
@SerialName("lastName")
|
||||||
|
val lastName: String,
|
||||||
|
|
||||||
|
@SerialName("dateOfBirth")
|
||||||
|
val dateOfBirth: String? = null,
|
||||||
|
|
||||||
|
@SerialName("systemMessage")
|
||||||
|
val systemMessage: String,
|
||||||
|
|
||||||
|
@SerialName("executionStatus")
|
||||||
|
val executionStatus: Boolean,
|
||||||
|
|
||||||
|
@SerialName("errorCode")
|
||||||
|
val errorCode: String? = null,
|
||||||
|
|
||||||
|
@SerialName("errorMessage")
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
|
||||||
|
@SerialName("partyIdType")
|
||||||
|
val partyIdType: String,
|
||||||
|
)
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.core.model.interbank
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InterBankTransferRequest(
|
||||||
|
@SerialName("homeTransactionId")
|
||||||
|
val homeTransactionId: String,
|
||||||
|
@SerialName("from")
|
||||||
|
val from: Party,
|
||||||
|
@SerialName("to")
|
||||||
|
val to: Party,
|
||||||
|
@SerialName("amountType")
|
||||||
|
val amountType: String,
|
||||||
|
@SerialName("amount")
|
||||||
|
val amount: Amount,
|
||||||
|
@SerialName("transactionType")
|
||||||
|
val transactionType: TransactionType,
|
||||||
|
@SerialName("note")
|
||||||
|
val note: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Party(
|
||||||
|
@SerialName("fspId")
|
||||||
|
val fspId: String,
|
||||||
|
@SerialName("idType")
|
||||||
|
val idType: String,
|
||||||
|
@SerialName("idValue")
|
||||||
|
val idValue: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Amount(
|
||||||
|
@SerialName("currencyCode")
|
||||||
|
val currencyCode: String,
|
||||||
|
@SerialName("amount")
|
||||||
|
val amount: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TransactionType(
|
||||||
|
@SerialName("scenario")
|
||||||
|
val scenario: String,
|
||||||
|
@SerialName("subScenario")
|
||||||
|
val subScenario: String,
|
||||||
|
@SerialName("initiator")
|
||||||
|
val initiator: String,
|
||||||
|
@SerialName("initiatorType")
|
||||||
|
val initiatorType: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InterBankTransferResponse(
|
||||||
|
@SerialName("homeTransactionId")
|
||||||
|
val homeTransactionId: String,
|
||||||
|
@SerialName("transactionId")
|
||||||
|
val transactionId: String,
|
||||||
|
@SerialName("systemMessage")
|
||||||
|
val systemMessage: String,
|
||||||
|
@SerialName("executionStatus")
|
||||||
|
val executionStatus: Boolean,
|
||||||
|
)
|
||||||
@ -46,5 +46,6 @@ fun SavingAccountDetail.toAccount(): Account {
|
|||||||
productId = savingsProductId,
|
productId = savingsProductId,
|
||||||
currency = currency,
|
currency = currency,
|
||||||
status = status,
|
status = status,
|
||||||
|
clientName = clientName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,10 +17,10 @@ data class Summary(
|
|||||||
val currency: Currency,
|
val currency: Currency,
|
||||||
val totalDeposits: Double = 0.0,
|
val totalDeposits: Double = 0.0,
|
||||||
val totalWithdrawals: Double = 0.0,
|
val totalWithdrawals: Double = 0.0,
|
||||||
val totalInterestPosted: Long = 0,
|
val totalInterestPosted: Double = 0.0,
|
||||||
val accountBalance: Double = 0.0,
|
val accountBalance: Double = 0.0,
|
||||||
val totalOverdraftInterestDerived: Long = 0,
|
val totalOverdraftInterestDerived: Double = 0.0,
|
||||||
val interestNotPosted: Long = 0,
|
val interestNotPosted: Double = 0.0,
|
||||||
val availableBalance: Double = 0.0,
|
val availableBalance: Double = 0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.core.network
|
||||||
|
|
||||||
|
class InterBankApiManager(
|
||||||
|
private val ktorfitClient: KtorfitClient,
|
||||||
|
) {
|
||||||
|
val interBankApi by lazy { ktorfitClient.interBankApi }
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import org.mifospay.core.network.services.createAuthenticationService
|
|||||||
import org.mifospay.core.network.services.createBeneficiaryService
|
import org.mifospay.core.network.services.createBeneficiaryService
|
||||||
import org.mifospay.core.network.services.createClientService
|
import org.mifospay.core.network.services.createClientService
|
||||||
import org.mifospay.core.network.services.createDocumentService
|
import org.mifospay.core.network.services.createDocumentService
|
||||||
|
import org.mifospay.core.network.services.createInterBankService
|
||||||
import org.mifospay.core.network.services.createInvoiceService
|
import org.mifospay.core.network.services.createInvoiceService
|
||||||
import org.mifospay.core.network.services.createKYCLevel1Service
|
import org.mifospay.core.network.services.createKYCLevel1Service
|
||||||
import org.mifospay.core.network.services.createNotificationService
|
import org.mifospay.core.network.services.createNotificationService
|
||||||
@ -64,4 +65,6 @@ class KtorfitClient(
|
|||||||
internal val standingInstructionApi by lazy { ktorfit.createStandingInstructionService() }
|
internal val standingInstructionApi by lazy { ktorfit.createStandingInstructionService() }
|
||||||
|
|
||||||
internal val beneficiaryApi by lazy { ktorfit.createBeneficiaryService() }
|
internal val beneficiaryApi by lazy { ktorfit.createBeneficiaryService() }
|
||||||
|
|
||||||
|
internal val interBankApi by lazy { ktorfit.createInterBankService() }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import org.mifos.corebase.network.httpClient
|
|||||||
import org.mifos.corebase.network.setupDefaultHttpClient
|
import org.mifos.corebase.network.setupDefaultHttpClient
|
||||||
import org.mifospay.core.datastore.UserPreferencesRepository
|
import org.mifospay.core.datastore.UserPreferencesRepository
|
||||||
import org.mifospay.core.network.FineractApiManager
|
import org.mifospay.core.network.FineractApiManager
|
||||||
|
import org.mifospay.core.network.InterBankApiManager
|
||||||
import org.mifospay.core.network.KtorfitClient
|
import org.mifospay.core.network.KtorfitClient
|
||||||
import org.mifospay.core.network.SelfServiceApiManager
|
import org.mifospay.core.network.SelfServiceApiManager
|
||||||
import org.mifospay.core.network.utils.BaseURL
|
import org.mifospay.core.network.utils.BaseURL
|
||||||
@ -33,7 +34,7 @@ val NetworkModule = module {
|
|||||||
client = httpClient(
|
client = httpClient(
|
||||||
config = setupDefaultHttpClient(
|
config = setupDefaultHttpClient(
|
||||||
baseUrl = BaseURL.selfServiceUrl,
|
baseUrl = BaseURL.selfServiceUrl,
|
||||||
loggableHosts = listOf("tt.mifos.community"),
|
loggableHosts = listOf("mifos-bank-1.mifos.community"),
|
||||||
),
|
),
|
||||||
).config {
|
).config {
|
||||||
install(KtorInterceptor) {
|
install(KtorInterceptor) {
|
||||||
@ -60,11 +61,11 @@ val NetworkModule = module {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
defaultHeaders = mapOf(
|
defaultHeaders = mapOf(
|
||||||
"Fineract-Platform-TenantId" to "default",
|
"Fineract-Platform-TenantId" to "mifos-bank-1",
|
||||||
"Content-Type" to "application/json",
|
"Content-Type" to "application/json",
|
||||||
"Accept" to "application/json",
|
"Accept" to "application/json",
|
||||||
),
|
),
|
||||||
loggableHosts = listOf("tt.mifos.community"),
|
loggableHosts = listOf("mifos-bank-1.mifos.community", "apis.flexcore.mx"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -75,6 +76,27 @@ val NetworkModule = module {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single<KtorfitClient>(qualifier = InterBankClient) {
|
||||||
|
KtorfitClient(
|
||||||
|
Ktorfit.Builder()
|
||||||
|
.httpClient(
|
||||||
|
client = httpClient(
|
||||||
|
config = setupDefaultHttpClient(
|
||||||
|
baseUrl = BaseURL.interBankUrl,
|
||||||
|
defaultHeaders = mapOf(
|
||||||
|
"Fineract-Platform-TenantId" to BaseURL.FINERACT_PLATFORM_TENANT_ID,
|
||||||
|
"Content-Type" to "application/json",
|
||||||
|
"Accept" to "application/json",
|
||||||
|
),
|
||||||
|
loggableHosts = listOf("apis.flexcore.mx"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.converterFactories(FlowConverterFactory())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
FineractApiManager(ktorfitClient = get(BaseClient))
|
FineractApiManager(ktorfitClient = get(BaseClient))
|
||||||
}
|
}
|
||||||
@ -82,4 +104,8 @@ val NetworkModule = module {
|
|||||||
single {
|
single {
|
||||||
SelfServiceApiManager(ktorfitClient = get(SelfClient))
|
SelfServiceApiManager(ktorfitClient = get(SelfClient))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
InterBankApiManager(ktorfitClient = get(InterBankClient))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,3 +13,4 @@ import org.koin.core.qualifier.named
|
|||||||
|
|
||||||
val SelfClient = named("SelfClient")
|
val SelfClient = named("SelfClient")
|
||||||
val BaseClient = named("BaseClient")
|
val BaseClient = named("BaseClient")
|
||||||
|
val InterBankClient = named("InterBankClient")
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import org.mifospay.core.model.account.AccountTransferPayload
|
|||||||
import org.mifospay.core.model.savingsaccount.TransactionsEntity
|
import org.mifospay.core.model.savingsaccount.TransactionsEntity
|
||||||
import org.mifospay.core.model.savingsaccount.TransferDetail
|
import org.mifospay.core.model.savingsaccount.TransferDetail
|
||||||
import org.mifospay.core.model.search.AccountResult
|
import org.mifospay.core.model.search.AccountResult
|
||||||
|
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
|
||||||
import org.mifospay.core.network.utils.ApiEndPoints
|
import org.mifospay.core.network.utils.ApiEndPoints
|
||||||
|
|
||||||
interface AccountTransfersService {
|
interface AccountTransfersService {
|
||||||
@ -42,4 +43,7 @@ interface AccountTransfersService {
|
|||||||
suspend fun makeTransfer(
|
suspend fun makeTransfer(
|
||||||
@Body payload: AccountTransferPayload,
|
@Body payload: AccountTransferPayload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@GET(ApiEndPoints.ACCOUNT_TRANSFER + "/template")
|
||||||
|
fun getAccountTransferTemplate(): Flow<AccountOptionsTemplate>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
package org.mifospay.core.network.services
|
||||||
|
|
||||||
|
import de.jensklingenberg.ktorfit.http.Body
|
||||||
|
import de.jensklingenberg.ktorfit.http.POST
|
||||||
|
import org.mifospay.core.model.interbank.InterBankParticipantRequest
|
||||||
|
import org.mifospay.core.model.interbank.InterBankParticipantResponse
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoRequest
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoResponse
|
||||||
|
import org.mifospay.core.model.interbank.InterBankTransferRequest
|
||||||
|
import org.mifospay.core.model.interbank.InterBankTransferResponse
|
||||||
|
|
||||||
|
interface InterBankService {
|
||||||
|
@POST("participant")
|
||||||
|
suspend fun fetchParticipant(
|
||||||
|
@Body request: InterBankParticipantRequest,
|
||||||
|
): InterBankParticipantResponse
|
||||||
|
|
||||||
|
@POST("partyinfo")
|
||||||
|
suspend fun fetchPartyInfo(
|
||||||
|
@Body request: InterBankPartyInfoRequest,
|
||||||
|
): InterBankPartyInfoResponse
|
||||||
|
|
||||||
|
@POST("executetransfer")
|
||||||
|
suspend fun interBankMakeTransfer(
|
||||||
|
@Body request: InterBankTransferRequest,
|
||||||
|
): InterBankTransferResponse
|
||||||
|
}
|
||||||
@ -11,20 +11,28 @@ package org.mifospay.core.network.utils
|
|||||||
|
|
||||||
object BaseURL {
|
object BaseURL {
|
||||||
private const val PROTOCOL_HTTPS = "https://"
|
private const val PROTOCOL_HTTPS = "https://"
|
||||||
private const val API_ENDPOINT = "tt.mifos.community"
|
private const val API_ENDPOINT = "mifos-bank-1.mifos.community"
|
||||||
private const val API_PATH = "/fineract-provider/api/v1/"
|
private const val API_PATH = "/fineract-provider/api/v1/"
|
||||||
|
|
||||||
// self service url
|
// self service url
|
||||||
private const val API_ENDPOINT_SELF = "tt.mifos.community"
|
private const val API_ENDPOINT_SELF = "mifos-bank-1.mifos.community"
|
||||||
private const val API_PATH_SELF = "/fineract-provider/api/v1/self/"
|
private const val API_PATH_SELF = "/fineract-provider/api/v1/self/"
|
||||||
|
|
||||||
const val HEADER_TENANT = "Fineract-Platform-TenantId"
|
const val HEADER_TENANT = "Fineract-Platform-TenantId"
|
||||||
const val HEADER_AUTH = "Authorization"
|
const val HEADER_AUTH = "Authorization"
|
||||||
const val DEFAULT = "default"
|
const val DEFAULT = "default"
|
||||||
|
|
||||||
|
const val API_ENDPOINT_INTERBANK = "apis.flexcore.mx"
|
||||||
|
const val API_PATH_INTERBANK = "/v1.0/vnext1/"
|
||||||
|
|
||||||
|
const val FINERACT_PLATFORM_TENANT_ID = "mifos-bank-1"
|
||||||
|
|
||||||
val url: String
|
val url: String
|
||||||
get() = PROTOCOL_HTTPS + API_ENDPOINT + API_PATH
|
get() = PROTOCOL_HTTPS + API_ENDPOINT + API_PATH
|
||||||
|
|
||||||
val selfServiceUrl: String
|
val selfServiceUrl: String
|
||||||
get() = PROTOCOL_HTTPS + API_ENDPOINT_SELF + API_PATH_SELF
|
get() = PROTOCOL_HTTPS + API_ENDPOINT_SELF + API_PATH_SELF
|
||||||
|
|
||||||
|
val interBankUrl: String
|
||||||
|
get() = PROTOCOL_HTTPS + API_ENDPOINT_INTERBANK + API_PATH_INTERBANK
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class KtorInterceptor(
|
|||||||
companion object Plugin : HttpClientPlugin<Config, KtorInterceptor> {
|
companion object Plugin : HttpClientPlugin<Config, KtorInterceptor> {
|
||||||
private const val HEADER_TENANT = "Fineract-Platform-TenantId"
|
private const val HEADER_TENANT = "Fineract-Platform-TenantId"
|
||||||
private const val HEADER_AUTH = "Authorization"
|
private const val HEADER_AUTH = "Authorization"
|
||||||
private const val DEFAULT = "default"
|
private const val DEFAULT = "mifos-bank-1"
|
||||||
|
|
||||||
override val key: AttributeKey<KtorInterceptor> = AttributeKey("KtorInterceptor")
|
override val key: AttributeKey<KtorInterceptor> = AttributeKey("KtorInterceptor")
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,382 @@
|
|||||||
|
/*
|
||||||
|
* 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.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
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.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
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.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AmountEditText - Generic amount input component with validation
|
||||||
|
*
|
||||||
|
* @param value Current amount value as String (formatted display)
|
||||||
|
* @param onValueChange Callback when amount changes, receives unformatted Double value
|
||||||
|
* @param modifier Modifier for the component
|
||||||
|
* @param currencyCode Currency code to display (e.g., "MXN")
|
||||||
|
* @param availableBalance Available balance to display
|
||||||
|
* @param maxAmount Maximum allowed amount (for validation)
|
||||||
|
* @param errorMessage Error message to display if validation fails
|
||||||
|
* @param onAmountValidation Callback for amount validation with error message
|
||||||
|
* @param enabled Whether the input is enabled
|
||||||
|
* @param isError Whether the field is in error state
|
||||||
|
* @param backgroundColor Background color of the input box
|
||||||
|
* @param errorBorderColor Border color when in error state
|
||||||
|
* @param successBorderColor Border color when valid
|
||||||
|
* @param borderWidth Border width
|
||||||
|
* @param cornerRadius Corner radius of the input box
|
||||||
|
* @param contentPadding Padding inside the input box
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AmountEditText(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (Double) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
currencyCode: String = "MXN",
|
||||||
|
availableBalance: Double? = null,
|
||||||
|
maxAmount: Double? = null,
|
||||||
|
errorMessage: String? = null,
|
||||||
|
onAmountValidation: ((Double, String?) -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
isError: Boolean = false,
|
||||||
|
backgroundColor: Color = KptTheme.colorScheme.background,
|
||||||
|
errorBorderColor: Color = KptTheme.colorScheme.error,
|
||||||
|
successBorderColor: Color = Color(0xFFE0E0E0),
|
||||||
|
borderWidth: Dp = 2.dp,
|
||||||
|
cornerRadius: Dp = 12.dp,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
currencyTextStyle: TextStyle = TextStyle(
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
amountTextStyle: TextStyle = TextStyle(
|
||||||
|
fontSize = 40.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
balanceTextStyle: TextStyle = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF388E3C),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// Amount Input Container
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = backgroundColor,
|
||||||
|
shape = RoundedCornerShape(cornerRadius),
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = borderWidth,
|
||||||
|
color = when {
|
||||||
|
isError -> errorBorderColor
|
||||||
|
isFocused -> MaterialTheme.colorScheme.primary
|
||||||
|
else -> successBorderColor
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(cornerRadius),
|
||||||
|
)
|
||||||
|
.padding(contentPadding),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
// Currency Symbol
|
||||||
|
Text(
|
||||||
|
text = currencyCode,
|
||||||
|
style = currencyTextStyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
.height(48.dp),
|
||||||
|
thickness = 2.dp,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// Amount Input
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
val formattedValue = formatAmount(newValue)
|
||||||
|
val doubleValue = formattedValue.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||||
|
|
||||||
|
// Validate amount
|
||||||
|
val validationError = validateAmount(doubleValue, maxAmount)
|
||||||
|
onAmountValidation?.invoke(doubleValue, validationError)
|
||||||
|
|
||||||
|
// Always call onValueChange with Double value
|
||||||
|
onValueChange(doubleValue)
|
||||||
|
},
|
||||||
|
textStyle = amountTextStyle,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Decimal,
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
isFocused = focusState.isFocused
|
||||||
|
},
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "0.00",
|
||||||
|
style = amountTextStyle.copy(
|
||||||
|
color = amountTextStyle.color.copy(alpha = 0.3f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error Message Display
|
||||||
|
if (isError && errorMessage != null) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.background(
|
||||||
|
color = Color(0xFFFFEBEE),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.padding(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "⚠️",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = Color(0xFFD32F2F),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available Balance
|
||||||
|
if (availableBalance != null && availableBalance > 0) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = balanceTextStyle.color,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Available Balance: $availableBalance $currencyCode",
|
||||||
|
style = balanceTextStyle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the amount based on constraints
|
||||||
|
* @return Error message if validation fails, null if valid
|
||||||
|
*/
|
||||||
|
private fun validateAmount(amount: Double, maxAmount: Double?): String? {
|
||||||
|
return when {
|
||||||
|
amount <= 0 -> "Amount must be greater than 0"
|
||||||
|
maxAmount != null && amount > maxAmount -> "Amount exceeds available balance"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the amount input with thousand separators
|
||||||
|
*/
|
||||||
|
private fun formatAmount(input: String): String {
|
||||||
|
// Remove non-digit characters except decimal point
|
||||||
|
val cleaned = input.replace(Regex("[^\\d.]"), "")
|
||||||
|
|
||||||
|
// Split by decimal point
|
||||||
|
val parts = cleaned.split(".")
|
||||||
|
|
||||||
|
// Format integer part with thousand separators
|
||||||
|
val integerPart = parts.getOrNull(0)?.let { part ->
|
||||||
|
if (part.isNotEmpty()) {
|
||||||
|
part.reversed()
|
||||||
|
.chunked(3)
|
||||||
|
.joinToString(",")
|
||||||
|
.reversed()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
// Combine with decimal part if exists (limit to 2 decimal places)
|
||||||
|
return if (parts.size > 1) {
|
||||||
|
val decimalPart = parts[1].take(2)
|
||||||
|
"$integerPart.$decimalPart"
|
||||||
|
} else if (cleaned.endsWith(".")) {
|
||||||
|
"$integerPart."
|
||||||
|
} else {
|
||||||
|
integerPart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AmountEditTextPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
var amount by remember { mutableStateOf("1234.50") }
|
||||||
|
var errorMsg by remember { mutableStateOf<String?>(null) }
|
||||||
|
var isError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
AmountEditText(
|
||||||
|
value = amount,
|
||||||
|
onValueChange = { doubleAmount ->
|
||||||
|
amount = doubleAmount.toString()
|
||||||
|
},
|
||||||
|
currencyCode = "MXN",
|
||||||
|
availableBalance = 5000.0,
|
||||||
|
maxAmount = 5000.0,
|
||||||
|
errorMessage = errorMsg,
|
||||||
|
onAmountValidation = { doubleAmount, error ->
|
||||||
|
errorMsg = error
|
||||||
|
isError = error != null
|
||||||
|
},
|
||||||
|
isError = isError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AmountEditTextEmptyPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
var amount by remember { mutableStateOf("") }
|
||||||
|
var errorMsg by remember { mutableStateOf<String?>(null) }
|
||||||
|
var isError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
AmountEditText(
|
||||||
|
value = amount,
|
||||||
|
onValueChange = { doubleAmount ->
|
||||||
|
amount = doubleAmount.toString()
|
||||||
|
},
|
||||||
|
currencyCode = "MXN",
|
||||||
|
availableBalance = 5000.0,
|
||||||
|
maxAmount = 5000.0,
|
||||||
|
errorMessage = errorMsg,
|
||||||
|
onAmountValidation = { doubleAmount, error ->
|
||||||
|
errorMsg = error
|
||||||
|
isError = error != null
|
||||||
|
},
|
||||||
|
isError = isError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AmountEditTextErrorPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
var amount by remember { mutableStateOf("6000.00") }
|
||||||
|
var errorMsg by remember { mutableStateOf("Amount exceeds available balance") }
|
||||||
|
var isError by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
AmountEditText(
|
||||||
|
value = amount,
|
||||||
|
onValueChange = { doubleAmount ->
|
||||||
|
amount = doubleAmount.toString()
|
||||||
|
},
|
||||||
|
currencyCode = "MXN",
|
||||||
|
availableBalance = 5000.0,
|
||||||
|
maxAmount = 5000.0,
|
||||||
|
errorMessage = errorMsg,
|
||||||
|
onAmountValidation = { doubleAmount, error ->
|
||||||
|
errorMsg = error ?: ""
|
||||||
|
isError = error != null
|
||||||
|
},
|
||||||
|
isError = isError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,7 +28,7 @@ module FastlaneConfig
|
|||||||
key_id: "7V3ABCDEFG",
|
key_id: "7V3ABCDEFG",
|
||||||
issuer_id: "7ab9e231-9603-4c3e-a147-be3b0f123456",
|
issuer_id: "7ab9e231-9603-4c3e-a147-be3b0f123456",
|
||||||
key_filepath: "./secrets/Auth_key.p8",
|
key_filepath: "./secrets/Auth_key.p8",
|
||||||
version_number: "1.0.0",
|
version_number: "1.1.0",
|
||||||
metadata_path: "./fastlane/metadata/ios",
|
metadata_path: "./fastlane/metadata/ios",
|
||||||
app_rating_config_path: "./fastlane/age_rating.json",
|
app_rating_config_path: "./fastlane/age_rating.json",
|
||||||
screenshots_ios_path: "./fastlane/screenshots_ios",
|
screenshots_ios_path: "./fastlane/screenshots_ios",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
https://github.com/openMF/mobile-wallet
|
https://mifos.org/resources/support/
|
||||||
|
|||||||
@ -344,10 +344,10 @@ val sampleMerchantList = List(10) {
|
|||||||
),
|
),
|
||||||
totalDeposits = 18.19,
|
totalDeposits = 18.19,
|
||||||
totalWithdrawals = 20.21,
|
totalWithdrawals = 20.21,
|
||||||
totalInterestPosted = 6052,
|
totalInterestPosted = 6052.0,
|
||||||
accountBalance = 22.23,
|
accountBalance = 22.23,
|
||||||
totalOverdraftInterestDerived = 2232,
|
totalOverdraftInterestDerived = 2232.0,
|
||||||
interestNotPosted = 5113,
|
interestNotPosted = 5113.0,
|
||||||
availableBalance = 24.25,
|
availableBalance = 24.25,
|
||||||
),
|
),
|
||||||
transactions = listOf(),
|
transactions = listOf(),
|
||||||
|
|||||||
@ -26,3 +26,8 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compose.resources {
|
||||||
|
publicResClass = true
|
||||||
|
generateResClass = always
|
||||||
|
}
|
||||||
@ -14,4 +14,15 @@
|
|||||||
<string name="feature_payments_receive">Receive</string>
|
<string name="feature_payments_receive">Receive</string>
|
||||||
<string name="feature_payments_show_code">Show code</string>
|
<string name="feature_payments_show_code">Show code</string>
|
||||||
|
|
||||||
|
<!-- Select Transfer Type Screen -->
|
||||||
|
<string name="feature_payments_select_transfer_type_header">How would you like to make\nthis transfer?</string>
|
||||||
|
<string name="feature_payments_select_transfer_type_subtitle">Choose a transfer method below</string>
|
||||||
|
<string name="feature_payments_intra_bank_transfer_title">Intra-Bank Transfer</string>
|
||||||
|
<string name="feature_payments_intra_bank_transfer_description">Move funds between accounts within this bank.</string>
|
||||||
|
<string name="feature_payments_inter_bank_transfer_title">Inter-Bank Transfer</string>
|
||||||
|
<string name="feature_payments_inter_bank_transfer_description">Send funds to accounts in different banks.</string>
|
||||||
|
|
||||||
|
<!-- Transfer Options Bottom Sheet -->
|
||||||
|
<string name="feature_payments_transfer_options_title">Transfer Options</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.payments.selectTransferType
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SelectTransferTypeRoute
|
||||||
|
|
||||||
|
fun NavController.navigateToSelectTransferType(navOptions: NavOptions? = null) =
|
||||||
|
navigate(SelectTransferTypeRoute, navOptions)
|
||||||
|
|
||||||
|
fun NavGraphBuilder.selectTransferTypeScreen(
|
||||||
|
onIntraBankTransferClick: () -> Unit,
|
||||||
|
onInterBankTransferClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
composable<SelectTransferTypeRoute> {
|
||||||
|
SelectTransferTypeScreen(
|
||||||
|
onIntraBankTransferClick = onIntraBankTransferClick,
|
||||||
|
onInterBankTransferClick = onInterBankTransferClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* 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.payments.selectTransferType
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_inter_bank_transfer_description
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_inter_bank_transfer_title
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_intra_bank_transfer_description
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_intra_bank_transfer_title
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_select_transfer_type_header
|
||||||
|
import mobile_wallet.feature.payments.generated.resources.feature_payments_select_transfer_type_subtitle
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.designsystem.component.MifosCard
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
|
import org.mifospay.core.ui.AvatarBox
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectTransferTypeScreen(
|
||||||
|
onIntraBankTransferClick: () -> Unit,
|
||||||
|
onInterBankTransferClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(KptTheme.colorScheme.background)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(KptTheme.spacing.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
// Header Section
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = KptTheme.spacing.md),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_payments_select_transfer_type_header),
|
||||||
|
style = KptTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = KptTheme.colorScheme.onBackground,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_payments_select_transfer_type_subtitle),
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.onBackground,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intra-Bank Transfer Card
|
||||||
|
TransferTypeCard(
|
||||||
|
title = stringResource(Res.string.feature_payments_intra_bank_transfer_title),
|
||||||
|
description = stringResource(Res.string.feature_payments_intra_bank_transfer_description),
|
||||||
|
icon = MifosIcons.Transfer,
|
||||||
|
onClick = onIntraBankTransferClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inter-Bank Transfer Card
|
||||||
|
TransferTypeCard(
|
||||||
|
title = stringResource(Res.string.feature_payments_inter_bank_transfer_title),
|
||||||
|
description = stringResource(Res.string.feature_payments_inter_bank_transfer_description),
|
||||||
|
icon = MifosIcons.Bank,
|
||||||
|
onClick = onInterBankTransferClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spacer
|
||||||
|
Spacer(modifier = Modifier.height(KptTheme.spacing.lg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TransferTypeCard(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MifosCard(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(KptTheme.spacing.lg),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.sm),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// Icon
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.size(60.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
AvatarBox(
|
||||||
|
modifier = Modifier.size(50.dp),
|
||||||
|
icon = icon,
|
||||||
|
size = 50,
|
||||||
|
backgroundColor = KptTheme.colorScheme.primaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(vertical = KptTheme.spacing.sm),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow Icon
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.ArrowRight,
|
||||||
|
contentDescription = "Navigate to $title",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = KptTheme.spacing.sm)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SelectTransferTypeScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SelectTransferTypeScreen(
|
||||||
|
onIntraBankTransferClick = {},
|
||||||
|
onInterBankTransferClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
feature/send-interbank/.gitignore
vendored
Normal file
1
feature/send-interbank/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
451
feature/send-interbank/IMPLEMENTATION_SUMMARY.md
Normal file
451
feature/send-interbank/IMPLEMENTATION_SUMMARY.md
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
# Interbank Transfer Implementation Summary
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A complete interbank transfer flow has been implemented in the `feature/send-interbank` module following the 6-screen design specification. The implementation provides a seamless user experience for transferring money between bank accounts with comprehensive state management, error handling, and validation.
|
||||||
|
|
||||||
|
## Implemented Components
|
||||||
|
|
||||||
|
### 1. Core Files
|
||||||
|
|
||||||
|
#### `InterbankTransferViewModel.kt`
|
||||||
|
- **Purpose**: Central state management for the entire transfer flow
|
||||||
|
- **Key Features**:
|
||||||
|
- Manages 6-step transfer process
|
||||||
|
- Handles all user actions and state transitions
|
||||||
|
- Validates transfer details
|
||||||
|
- Communicates with repository for API calls
|
||||||
|
- Emits events for navigation
|
||||||
|
|
||||||
|
- **State Management**:
|
||||||
|
- `InterbankTransferState`: Holds all transfer data
|
||||||
|
- `LoadingState`: Tracks account loading status
|
||||||
|
- `Step`: Enum for current screen in flow
|
||||||
|
|
||||||
|
- **Actions Handled**:
|
||||||
|
- Navigation between steps
|
||||||
|
- Amount, date, description updates
|
||||||
|
- Transfer confirmation and retry
|
||||||
|
- Error dismissal
|
||||||
|
|
||||||
|
#### `InterbankTransferFlowScreen.kt`
|
||||||
|
- **Purpose**: Orchestrates the entire transfer flow
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Routes to correct screen based on current step
|
||||||
|
- Handles event callbacks
|
||||||
|
- Manages search results state
|
||||||
|
- Provides callbacks for all user interactions
|
||||||
|
|
||||||
|
### 2. Screen Components
|
||||||
|
|
||||||
|
#### `SelectAccountScreen.kt` (Step 1)
|
||||||
|
- Displays available sender accounts
|
||||||
|
- Shows account holder name, number, and balance
|
||||||
|
- Loading and error states
|
||||||
|
- Account selection with visual feedback
|
||||||
|
|
||||||
|
#### `SearchRecipientScreen.kt` (Step 2)
|
||||||
|
- Phone number search field
|
||||||
|
- Real-time search results display
|
||||||
|
- Recipient information cards
|
||||||
|
- Empty state handling
|
||||||
|
|
||||||
|
#### `TransferDetailsScreen.kt` (Step 3)
|
||||||
|
- Amount input with decimal validation
|
||||||
|
- Date input field
|
||||||
|
- Description input (multi-line)
|
||||||
|
- Display of selected accounts
|
||||||
|
- Continue button with validation
|
||||||
|
|
||||||
|
#### `PreviewTransferScreen.kt` (Step 4)
|
||||||
|
- Complete transfer review
|
||||||
|
- Sender and recipient account cards
|
||||||
|
- Amount, date, and description display
|
||||||
|
- Confirm and Edit buttons
|
||||||
|
- Processing state indication
|
||||||
|
|
||||||
|
#### `TransferResultScreens.kt` (Steps 5 & 6)
|
||||||
|
- **TransferSuccessScreen**:
|
||||||
|
- Success confirmation with icon
|
||||||
|
- Recipient name and amount display
|
||||||
|
- Download receipt button
|
||||||
|
- Back to home button
|
||||||
|
|
||||||
|
- **TransferFailedScreen**:
|
||||||
|
- Error message and details
|
||||||
|
- Retry button
|
||||||
|
- Contact support button
|
||||||
|
- Back to home button
|
||||||
|
|
||||||
|
### 3. Navigation
|
||||||
|
|
||||||
|
#### `InterbankTransferNavigation.kt`
|
||||||
|
- `InterbankTransferRoute`: Serializable route with return destination
|
||||||
|
- `navigateToInterbankTransfer()`: Navigation function
|
||||||
|
- `interbankTransferScreen()`: NavGraphBuilder extension
|
||||||
|
- Handles return destination for post-transfer navigation
|
||||||
|
|
||||||
|
### 4. Dependency Injection
|
||||||
|
|
||||||
|
#### `InterbankTransferModule.kt`
|
||||||
|
- Provides `InterbankTransferViewModel` via Koin
|
||||||
|
- Injects `ThirdPartyTransferRepository`
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### InterbankTransferState
|
||||||
|
```kotlin
|
||||||
|
data class InterbankTransferState(
|
||||||
|
val currentStep: Step, // Current screen
|
||||||
|
val loadingState: LoadingState, // Loading/Error state
|
||||||
|
val fromAccounts: List<AccountOption>, // Available accounts
|
||||||
|
val selectedFromAccount: AccountOption?, // Selected sender
|
||||||
|
val selectedRecipient: RecipientInfo?, // Selected recipient
|
||||||
|
val transferAmount: String, // Amount
|
||||||
|
val transferDate: String, // Date
|
||||||
|
val transferDescription: String, // Description
|
||||||
|
val isProcessing: Boolean, // Processing flag
|
||||||
|
val errorMessage: String?, // Error message
|
||||||
|
val transferResponse: Any?, // API response
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RecipientInfo
|
||||||
|
```kotlin
|
||||||
|
data class RecipientInfo(
|
||||||
|
val clientId: Long,
|
||||||
|
val officeId: Int,
|
||||||
|
val accountId: Int,
|
||||||
|
val accountType: Int,
|
||||||
|
val clientName: String,
|
||||||
|
val accountNo: String,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flow Architecture
|
||||||
|
|
||||||
|
### State Transitions
|
||||||
|
```
|
||||||
|
SelectAccount → SearchRecipient → TransferDetails → PreviewTransfer
|
||||||
|
↓
|
||||||
|
(Confirm)
|
||||||
|
↓
|
||||||
|
TransferSuccess
|
||||||
|
or
|
||||||
|
TransferFailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backward Navigation
|
||||||
|
- Each screen can navigate back to the previous step
|
||||||
|
- Edit button on Preview returns to Transfer Details
|
||||||
|
- Retry on failure returns to Preview
|
||||||
|
|
||||||
|
### Data Persistence
|
||||||
|
- Transfer payload is built incrementally as user progresses
|
||||||
|
- All data stored in ViewModel state
|
||||||
|
- Automatic reconstruction of payload for API call
|
||||||
|
|
||||||
|
## Validation Strategy
|
||||||
|
|
||||||
|
### Account Selection
|
||||||
|
- Accounts loaded from repository
|
||||||
|
- Empty state if no accounts available
|
||||||
|
- Error state with retry option
|
||||||
|
|
||||||
|
### Recipient Search
|
||||||
|
- Search query stored in state
|
||||||
|
- Results displayed in real-time
|
||||||
|
- Empty state when no results
|
||||||
|
|
||||||
|
### Transfer Details
|
||||||
|
- Amount: Must be decimal, > 0
|
||||||
|
- Date: Format validation
|
||||||
|
- Description: Must not be empty
|
||||||
|
- Continue button disabled until all valid
|
||||||
|
|
||||||
|
### Preview
|
||||||
|
- All data reviewed before confirmation
|
||||||
|
- Processing state during API call
|
||||||
|
- Error handling with retry option
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Loading Errors
|
||||||
|
- Display error message
|
||||||
|
- Show retry option
|
||||||
|
- Graceful degradation
|
||||||
|
|
||||||
|
### Validation Errors
|
||||||
|
- Field-level validation
|
||||||
|
- Clear error messages
|
||||||
|
- Disable actions until valid
|
||||||
|
|
||||||
|
### Transfer Errors
|
||||||
|
- API error messages displayed
|
||||||
|
- Retry mechanism available
|
||||||
|
- Support contact option
|
||||||
|
- Detailed error logging
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
### Repository Methods Used
|
||||||
|
```kotlin
|
||||||
|
// Load available accounts
|
||||||
|
suspend fun getTransferTemplate(): AccountOptionsTemplate
|
||||||
|
|
||||||
|
// Process transfer
|
||||||
|
suspend fun makeTransfer(payload: TransferPayload): DataState<TPTResponse>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transfer Payload
|
||||||
|
```kotlin
|
||||||
|
TransferPayload(
|
||||||
|
fromOfficeId = selectedFromAccount?.officeId,
|
||||||
|
fromClientId = selectedFromAccount?.clientId,
|
||||||
|
fromAccountType = selectedFromAccount?.accountType?.id,
|
||||||
|
fromAccountId = selectedFromAccount?.accountId,
|
||||||
|
toOfficeId = selectedRecipient?.officeId,
|
||||||
|
toClientId = selectedRecipient?.clientId,
|
||||||
|
toAccountType = selectedRecipient?.accountType,
|
||||||
|
toAccountId = selectedRecipient?.accountId,
|
||||||
|
transferDate = transferDate,
|
||||||
|
transferAmount = transferAmount.toDoubleOrNull() ?: 0.0,
|
||||||
|
transferDescription = transferDescription,
|
||||||
|
locale = "en_IN",
|
||||||
|
dateFormat = "dd MMMM yyyy",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI/UX Features
|
||||||
|
|
||||||
|
### Visual Design
|
||||||
|
- Consistent with design system (KptTheme)
|
||||||
|
- Avatar boxes for account identification
|
||||||
|
- Color-coded sections (primary, secondary, error)
|
||||||
|
- Proper spacing and typography
|
||||||
|
|
||||||
|
### User Feedback
|
||||||
|
- Loading indicators during async operations
|
||||||
|
- Progress indication through step numbers
|
||||||
|
- Success/failure visual feedback
|
||||||
|
- Error messages with actionable solutions
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
- Proper content descriptions
|
||||||
|
- Keyboard navigation support
|
||||||
|
- Screen reader compatibility
|
||||||
|
- Color contrast compliance
|
||||||
|
|
||||||
|
## Integration Guide
|
||||||
|
|
||||||
|
### Adding to App Navigation
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// In your main navigation graph
|
||||||
|
interbankTransferScreen(
|
||||||
|
onBackClick = { navController.popBackStack() },
|
||||||
|
onTransferSuccess = { destination ->
|
||||||
|
navController.navigate(destination) {
|
||||||
|
popUpTo(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onContactSupport = { openSupportChat() },
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigating to Interbank Transfer
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
navController.navigateToInterbankTransfer(
|
||||||
|
returnDestination = "home",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Injection Setup
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// In your Koin module
|
||||||
|
includes(interbankTransferModule)
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/send-interbank/
|
||||||
|
├── src/commonMain/kotlin/org/mifospay/feature/send/interbank/
|
||||||
|
│ ├── InterbankTransferScreen.kt
|
||||||
|
│ ├── InterbankTransferViewModel.kt
|
||||||
|
│ ├── InterbankTransferFlowScreen.kt
|
||||||
|
│ ├── screens/
|
||||||
|
│ │ ├── SelectAccountScreen.kt
|
||||||
|
│ │ ├── SearchRecipientScreen.kt
|
||||||
|
│ │ ├── TransferDetailsScreen.kt
|
||||||
|
│ │ ├── PreviewTransferScreen.kt
|
||||||
|
│ │ └── TransferResultScreens.kt
|
||||||
|
│ ├── navigation/
|
||||||
|
│ │ └── InterbankTransferNavigation.kt
|
||||||
|
│ └── di/
|
||||||
|
│ └── InterbankTransferModule.kt
|
||||||
|
├── README.md
|
||||||
|
├── FLOW_DOCUMENTATION.md
|
||||||
|
└── IMPLEMENTATION_SUMMARY.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Implementation Details
|
||||||
|
|
||||||
|
### State Management Pattern
|
||||||
|
- Single source of truth in ViewModel
|
||||||
|
- Immutable state updates using `copy()`
|
||||||
|
- Event-driven navigation
|
||||||
|
- Action-based user interactions
|
||||||
|
|
||||||
|
### Coroutine Usage
|
||||||
|
- `viewModelScope` for lifecycle management
|
||||||
|
- Proper exception handling
|
||||||
|
- Flow-based state updates
|
||||||
|
- Async API calls with proper error handling
|
||||||
|
|
||||||
|
### Compose Best Practices
|
||||||
|
- Composable functions are pure
|
||||||
|
- State hoisting to ViewModel
|
||||||
|
- Proper recomposition optimization
|
||||||
|
- Remember for expensive operations
|
||||||
|
|
||||||
|
### Navigation Pattern
|
||||||
|
- Type-safe navigation with serialization
|
||||||
|
- Return destination support
|
||||||
|
- Proper back stack management
|
||||||
|
- Event-based navigation triggers
|
||||||
|
|
||||||
|
## Testing Considerations
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- ViewModel action handling
|
||||||
|
- State transitions
|
||||||
|
- Validation logic
|
||||||
|
- Error scenarios
|
||||||
|
|
||||||
|
### UI Tests
|
||||||
|
- Screen rendering
|
||||||
|
- User interactions
|
||||||
|
- Navigation flow
|
||||||
|
- Input validation
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- End-to-end transfer flow
|
||||||
|
- API integration
|
||||||
|
- Error handling
|
||||||
|
- Edge cases
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
- Lazy loading of accounts
|
||||||
|
- Debounced search input
|
||||||
|
- Efficient state updates
|
||||||
|
- Minimal recompositions
|
||||||
|
- Proper resource cleanup
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Sensitive data in state (consider encryption)
|
||||||
|
- API call validation
|
||||||
|
- Input sanitization
|
||||||
|
- Error message sanitization
|
||||||
|
- Transaction logging
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Recipient Management**
|
||||||
|
- Save favorite recipients
|
||||||
|
- Recent recipients list
|
||||||
|
- Recipient groups/categories
|
||||||
|
|
||||||
|
2. **Advanced Features**
|
||||||
|
- Scheduled transfers
|
||||||
|
- Recurring transfers
|
||||||
|
- Transfer templates
|
||||||
|
- Batch transfers
|
||||||
|
|
||||||
|
3. **Security Features**
|
||||||
|
- Biometric confirmation
|
||||||
|
- OTP verification
|
||||||
|
- Transaction limits
|
||||||
|
- Fraud detection
|
||||||
|
|
||||||
|
4. **Analytics**
|
||||||
|
- Transfer tracking
|
||||||
|
- Success rate monitoring
|
||||||
|
- User behavior analysis
|
||||||
|
- Error tracking
|
||||||
|
|
||||||
|
5. **Localization**
|
||||||
|
- Multi-language support
|
||||||
|
- Currency conversion
|
||||||
|
- Regional date formats
|
||||||
|
- Local payment methods
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `core:data` - Repository interfaces
|
||||||
|
- `core:network` - API models
|
||||||
|
- `core:designsystem` - UI components
|
||||||
|
- `core:ui` - Common utilities
|
||||||
|
- Compose - UI framework
|
||||||
|
- Koin - Dependency injection
|
||||||
|
- Kotlinx Serialization - Data serialization
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
- Follows Kotlin conventions
|
||||||
|
- Proper error handling
|
||||||
|
- Comprehensive documentation
|
||||||
|
- Type-safe implementation
|
||||||
|
- SOLID principles applied
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] All screens implemented
|
||||||
|
- [ ] Navigation working correctly
|
||||||
|
- [ ] State management tested
|
||||||
|
- [ ] Error handling verified
|
||||||
|
- [ ] API integration tested
|
||||||
|
- [ ] UI/UX reviewed
|
||||||
|
- [ ] Accessibility checked
|
||||||
|
- [ ] Performance optimized
|
||||||
|
- [ ] Documentation complete
|
||||||
|
- [ ] Unit tests written
|
||||||
|
- [ ] UI tests written
|
||||||
|
- [ ] Integration tests written
|
||||||
|
- [ ] Code reviewed
|
||||||
|
- [ ] Ready for production
|
||||||
|
|
||||||
|
## Support and Maintenance
|
||||||
|
|
||||||
|
### Common Issues and Solutions
|
||||||
|
|
||||||
|
1. **Accounts not loading**
|
||||||
|
- Check repository implementation
|
||||||
|
- Verify API endpoint
|
||||||
|
- Check error handling
|
||||||
|
|
||||||
|
2. **Navigation not working**
|
||||||
|
- Verify route serialization
|
||||||
|
- Check NavGraphBuilder setup
|
||||||
|
- Verify navigation callbacks
|
||||||
|
|
||||||
|
3. **State not updating**
|
||||||
|
- Check action handling
|
||||||
|
- Verify state updates
|
||||||
|
- Check recomposition
|
||||||
|
|
||||||
|
### Debugging Tips
|
||||||
|
|
||||||
|
- Enable Compose layout inspector
|
||||||
|
- Use ViewModel state logging
|
||||||
|
- Check Logcat for errors
|
||||||
|
- Use Android Studio debugger
|
||||||
|
- Monitor network calls
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The interbank transfer flow implementation provides a complete, production-ready solution for transferring money between bank accounts. It follows best practices for state management, error handling, and user experience, with comprehensive documentation for future maintenance and enhancement.
|
||||||
322
feature/send-interbank/README.md
Normal file
322
feature/send-interbank/README.md
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
# Interbank Transfer Module
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `send-interbank` module implements a complete interbank transfer flow for the Mobile Wallet application. It provides a multi-step user interface for transferring money between different bank accounts.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Flow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Interbank Transfer Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
1. SELECT ACCOUNT
|
||||||
|
├─ Load user's available accounts
|
||||||
|
├─ Display account list
|
||||||
|
└─ User selects sender account
|
||||||
|
↓
|
||||||
|
2. SEARCH RECIPIENT
|
||||||
|
├─ Search by phone number or account number
|
||||||
|
├─ Display search results
|
||||||
|
└─ User selects recipient
|
||||||
|
↓
|
||||||
|
3. TRANSFER DETAILS
|
||||||
|
├─ Enter amount
|
||||||
|
├─ Enter date
|
||||||
|
├─ Enter description
|
||||||
|
└─ Validate inputs
|
||||||
|
↓
|
||||||
|
4. PREVIEW TRANSFER
|
||||||
|
├─ Display all transfer details
|
||||||
|
├─ Show sender and recipient info
|
||||||
|
├─ Show amount and date
|
||||||
|
└─ User confirms or edits
|
||||||
|
↓
|
||||||
|
5. PROCESS TRANSFER
|
||||||
|
├─ Validate transfer payload
|
||||||
|
├─ Call API to initiate transfer
|
||||||
|
└─ Handle response
|
||||||
|
↓
|
||||||
|
6. RESULT SCREEN
|
||||||
|
├─ SUCCESS: Show confirmation with receipt download option
|
||||||
|
└─ FAILED: Show error with retry/support options
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/send-interbank/
|
||||||
|
├── src/
|
||||||
|
│ └── commonMain/
|
||||||
|
│ └── kotlin/org/mifospay/feature/send/interbank/
|
||||||
|
│ ├── InterbankTransferScreen.kt # Main entry point
|
||||||
|
│ ├── InterbankTransferViewModel.kt # State management
|
||||||
|
│ ├── InterbankTransferFlowScreen.kt # Flow orchestrator
|
||||||
|
│ ├── screens/
|
||||||
|
│ │ ├── SelectAccountScreen.kt # Step 1: Account selection
|
||||||
|
│ │ ├── SearchRecipientScreen.kt # Step 2: Recipient search
|
||||||
|
│ │ ├── TransferDetailsScreen.kt # Step 3: Transfer details
|
||||||
|
│ │ ├── PreviewTransferScreen.kt # Step 4: Preview
|
||||||
|
│ │ └── TransferResultScreens.kt # Step 5 & 6: Success/Failed
|
||||||
|
│ ├── navigation/
|
||||||
|
│ │ └── InterbankTransferNavigation.kt # Navigation setup
|
||||||
|
│ └── di/
|
||||||
|
│ └── InterbankTransferModule.kt # Dependency injection
|
||||||
|
└── build.gradle.kts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screen Details
|
||||||
|
|
||||||
|
### 1. Select Account Screen
|
||||||
|
**Purpose**: Allow user to choose the sender account
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Displays list of available accounts
|
||||||
|
- Shows account holder name and account number
|
||||||
|
- Shows account type (e.g., Wallet, Savings)
|
||||||
|
- Loading state while fetching accounts
|
||||||
|
- Error handling for account loading failures
|
||||||
|
|
||||||
|
**User Actions**:
|
||||||
|
- Select an account → Navigate to Search Recipient
|
||||||
|
- Back → Exit flow
|
||||||
|
|
||||||
|
### 2. Search Recipient Screen
|
||||||
|
**Purpose**: Find and select the recipient
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Search field for phone number or account number
|
||||||
|
- Real-time search results
|
||||||
|
- Display recipient name and account details
|
||||||
|
- Empty state when no results found
|
||||||
|
|
||||||
|
**User Actions**:
|
||||||
|
- Enter search query → Display results
|
||||||
|
- Select recipient → Navigate to Transfer Details
|
||||||
|
- Back → Return to Select Account
|
||||||
|
|
||||||
|
### 3. Transfer Details Screen
|
||||||
|
**Purpose**: Enter transfer amount, date, and description
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Display selected sender and recipient accounts
|
||||||
|
- Amount input field (decimal validation)
|
||||||
|
- Date input field
|
||||||
|
- Description input field (multi-line)
|
||||||
|
- Continue button enabled only when all fields are valid
|
||||||
|
|
||||||
|
**Validations**:
|
||||||
|
- Amount must be a valid decimal number
|
||||||
|
- Amount must be greater than 0
|
||||||
|
- Description must not be empty
|
||||||
|
- Date format validation
|
||||||
|
|
||||||
|
**User Actions**:
|
||||||
|
- Fill details → Continue to Preview
|
||||||
|
- Back → Return to Search Recipient
|
||||||
|
|
||||||
|
### 4. Preview Transfer Screen
|
||||||
|
**Purpose**: Review all transfer details before confirmation
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Display sender account with avatar
|
||||||
|
- Display recipient account with avatar
|
||||||
|
- Show transfer amount (highlighted)
|
||||||
|
- Show transfer date
|
||||||
|
- Show transfer description
|
||||||
|
- Edit button to go back and modify details
|
||||||
|
- Confirm button to proceed with transfer
|
||||||
|
|
||||||
|
**User Actions**:
|
||||||
|
- Confirm → Process transfer
|
||||||
|
- Edit → Go back to Transfer Details
|
||||||
|
- Back → Return to Transfer Details
|
||||||
|
|
||||||
|
### 5. Transfer Success Screen
|
||||||
|
**Purpose**: Confirm successful transfer
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Success icon and message
|
||||||
|
- Display recipient name and transfer amount
|
||||||
|
- Download receipt button
|
||||||
|
- Back to home button
|
||||||
|
|
||||||
|
**User Actions**:
|
||||||
|
- Download Receipt → Generate and download receipt
|
||||||
|
- Back to Home → Return to home screen
|
||||||
|
|
||||||
|
### 6. Transfer Failed Screen
|
||||||
|
**Purpose**: Handle transfer failures
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Error icon and message
|
||||||
|
- Display error details
|
||||||
|
- Retry button to attempt transfer again
|
||||||
|
- Contact support button
|
||||||
|
- Back to home button
|
||||||
|
|
||||||
|
**User Actions**:
|
||||||
|
- Retry → Go back to Preview and retry
|
||||||
|
- Contact Support → Open support contact
|
||||||
|
- Back to Home → Return to home screen
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### InterbankTransferState
|
||||||
|
```kotlin
|
||||||
|
data class InterbankTransferState(
|
||||||
|
val currentStep: Step, // Current screen in flow
|
||||||
|
val loadingState: LoadingState, // Loading/Error state
|
||||||
|
val fromAccounts: List<AccountOption>, // Available sender accounts
|
||||||
|
val selectedFromAccount: AccountOption?, // Selected sender
|
||||||
|
val selectedRecipient: RecipientInfo?, // Selected recipient
|
||||||
|
val transferAmount: String, // Amount to transfer
|
||||||
|
val transferDate: String, // Transfer date
|
||||||
|
val transferDescription: String, // Transfer description
|
||||||
|
val isProcessing: Boolean, // Processing transfer
|
||||||
|
val errorMessage: String?, // Error message if any
|
||||||
|
val transferResponse: Any?, // API response
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RecipientInfo
|
||||||
|
```kotlin
|
||||||
|
data class RecipientInfo(
|
||||||
|
val clientId: Long,
|
||||||
|
val officeId: Int,
|
||||||
|
val accountId: Int,
|
||||||
|
val accountType: Int,
|
||||||
|
val clientName: String,
|
||||||
|
val accountNo: String,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actions and Events
|
||||||
|
|
||||||
|
### InterbankTransferAction
|
||||||
|
- `NavigateToRecipientSearch(account)` - Move to search step
|
||||||
|
- `NavigateToTransferDetails(recipient)` - Move to details step
|
||||||
|
- `NavigateToPreview` - Move to preview step
|
||||||
|
- `NavigateBack` - Go to previous step
|
||||||
|
- `UpdateAmount(amount)` - Update transfer amount
|
||||||
|
- `UpdateDate(date)` - Update transfer date
|
||||||
|
- `UpdateDescription(description)` - Update description
|
||||||
|
- `ConfirmTransfer` - Initiate transfer
|
||||||
|
- `RetryTransfer` - Retry failed transfer
|
||||||
|
- `DismissError` - Dismiss error message
|
||||||
|
|
||||||
|
### InterbankTransferEvent
|
||||||
|
- `OnNavigateBack` - User exited flow
|
||||||
|
- `OnTransferSuccess` - Transfer completed successfully
|
||||||
|
- `OnTransferFailed(message)` - Transfer failed
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
### Adding to Navigation Graph
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
interbankTransferScreen(
|
||||||
|
onBackClick = { /* Handle back */ },
|
||||||
|
onTransferSuccess = { destination -> /* Navigate to destination */ },
|
||||||
|
onContactSupport = { /* Open support */ },
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation to Interbank Transfer
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
navController.navigateToInterbankTransfer(
|
||||||
|
returnDestination = "home",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
|
||||||
|
Add to your Koin module:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
includes(interbankTransferModule)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
### ThirdPartyTransferRepository
|
||||||
|
The module uses `ThirdPartyTransferRepository` for API calls:
|
||||||
|
|
||||||
|
- `getTransferTemplate()` - Fetch available accounts
|
||||||
|
- `makeTransfer(payload)` - Initiate transfer
|
||||||
|
|
||||||
|
### TransferPayload
|
||||||
|
```kotlin
|
||||||
|
data class TransferPayload(
|
||||||
|
val fromOfficeId: Int?,
|
||||||
|
val fromClientId: Long?,
|
||||||
|
val fromAccountType: Int?,
|
||||||
|
val fromAccountId: Int?,
|
||||||
|
val toOfficeId: Int?,
|
||||||
|
val toClientId: Long?,
|
||||||
|
val toAccountType: Int?,
|
||||||
|
val toAccountId: Int?,
|
||||||
|
val transferDate: String?,
|
||||||
|
val transferAmount: Double?,
|
||||||
|
val transferDescription: String?,
|
||||||
|
val dateFormat: String? = "dd MMMM yyyy",
|
||||||
|
val locale: String? = "en",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The module implements comprehensive error handling:
|
||||||
|
|
||||||
|
1. **Account Loading Errors**: Display error message and retry option
|
||||||
|
2. **Validation Errors**: Show validation messages for each field
|
||||||
|
3. **Transfer Errors**: Display error details with retry option
|
||||||
|
4. **Network Errors**: Handle gracefully with retry mechanism
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Implement actual recipient search from API
|
||||||
|
- [ ] Add receipt generation and download
|
||||||
|
- [ ] Implement support contact integration
|
||||||
|
- [ ] Add transfer history
|
||||||
|
- [ ] Implement favorite recipients
|
||||||
|
- [ ] Add scheduled transfers
|
||||||
|
- [ ] Implement transfer templates
|
||||||
|
- [ ] Add biometric authentication for confirmation
|
||||||
|
- [ ] Implement transaction tracking
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- ViewModel state management
|
||||||
|
- Action handling
|
||||||
|
- Validation logic
|
||||||
|
|
||||||
|
### UI Tests
|
||||||
|
- Screen navigation flow
|
||||||
|
- Input validation
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- End-to-end transfer flow
|
||||||
|
- API integration
|
||||||
|
- Error scenarios
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `core:data` - Repository interfaces
|
||||||
|
- `core:network` - API models and responses
|
||||||
|
- `core:designsystem` - UI components and theme
|
||||||
|
- `core:ui` - Common UI utilities
|
||||||
|
- Compose - UI framework
|
||||||
|
- Koin - Dependency injection
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
||||||
34
feature/send-interbank/build.gradle.kts
Normal file
34
feature/send-interbank/build.gradle.kts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.cmp.feature.convention)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "org.mifospay.feature.send.interbank"
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain.dependencies {
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
|
implementation(compose.components.resources)
|
||||||
|
implementation(compose.components.uiToolingPreview)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidMain.dependencies {
|
||||||
|
implementation(libs.google.play.services.code.scanner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2024 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
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<!-- Select Account Screen -->
|
||||||
|
<string name="feature_send_interbank_select_account">Select Account</string>
|
||||||
|
<string name="feature_send_interbank_select_your_account">Select Your Account</string>
|
||||||
|
<string name="feature_send_interbank_choose_account_to_send">Choose the account you want to send money from</string>
|
||||||
|
<string name="feature_send_interbank_no_accounts">No Accounts</string>
|
||||||
|
<string name="feature_send_interbank_no_accounts_available">You don\'t have any accounts available for transfer</string>
|
||||||
|
<string name="feature_send_interbank_oops">Oops!</string>
|
||||||
|
|
||||||
|
<!-- Search Recipient Screen -->
|
||||||
|
<string name="feature_send_interbank_search_recipient">Search Recipient</string>
|
||||||
|
<string name="feature_send_interbank_enter_phone_number">Enter Phone Number</string>
|
||||||
|
<string name="feature_send_interbank_no_results">No Results</string>
|
||||||
|
<string name="feature_send_interbank_no_recipients_found">No recipients found matching your search</string>
|
||||||
|
<string name="feature_send_interbank_found_recipients">Found %1$d recipient(s)</string>
|
||||||
|
<string name="feature_send_interbank_enter_phone_to_search">Enter a phone number to search for recipients</string>
|
||||||
|
<string name="feature_send_interbank_account">Account: %1$d -# %2$d</string>
|
||||||
|
<string name="feature_send_interbank_to_account_interbank">Account: %1$d</string>
|
||||||
|
<string name="feature_send_interbank_office">Office: %1$d</string>
|
||||||
|
<string name="feature_send_interbank_account_type">Account Type: %1$d</string>
|
||||||
|
<string name="feature_send_interbank_id">ID</string>
|
||||||
|
<string name="feature_send_interbank_bank">Bank: %1$d</string>
|
||||||
|
<string name="feature_send_interbank_balance">Balance: %1$d</string>
|
||||||
|
|
||||||
|
<!-- Transfer Details Screen -->
|
||||||
|
<string name="feature_send_interbank_transfer_details">Transfer Details</string>
|
||||||
|
<string name="feature_send_interbank_from_account">FROM Account</string>
|
||||||
|
<string name="feature_send_interbank_to_account">TO Account</string>
|
||||||
|
<string name="feature_send_interbank_amount">Amount</string>
|
||||||
|
<string name="feature_send_interbank_date">Transaction Date</string>
|
||||||
|
<string name="feature_send_interbank_description">Description (Optional)</string>
|
||||||
|
<string name="feature_send_interbank_continue">Continue</string>
|
||||||
|
<string name="feature_send_interbank_amount_exceeds_balance">Amount exceeds available balance</string>
|
||||||
|
<string name="feature_send_interbank_available_balance">Available Balance: %1$d</string>
|
||||||
|
<string name="feature_send_interbank_enter_amount">Enter amount</string>
|
||||||
|
<string name="feature_send_interbank_today">Today</string>
|
||||||
|
<string name="feature_send_interbank_verified">Verified</string>
|
||||||
|
<string name="feature_send_interbank_exchange_rate_info">Exchange rate info or fees</string>
|
||||||
|
|
||||||
|
<!-- Preview Transfer Screen -->
|
||||||
|
<string name="feature_send_interbank_preview_transfer">Preview Transfer</string>
|
||||||
|
<string name="feature_send_interbank_review_transfer">Review Your Transfer</string>
|
||||||
|
<string name="feature_send_interbank_confirm_pay">Confirm & Pay</string>
|
||||||
|
<string name="feature_send_interbank_edit">Edit</string>
|
||||||
|
<string name="feature_send_interbank_review_transfer_button">Review Transfer</string>
|
||||||
|
|
||||||
|
<!-- Transfer Result Screens -->
|
||||||
|
<string name="feature_send_interbank_transfer_successful">Transfer Successful</string>
|
||||||
|
<string name="feature_send_interbank_transfer_completed">Your transfer to %1$s has been completed successfully.</string>
|
||||||
|
<string name="feature_send_interbank_amount_label">Amount: %1$s</string>
|
||||||
|
<string name="feature_send_interbank_download_receipt">Download Receipt</string>
|
||||||
|
<string name="feature_send_interbank_back_to_home">Back to Home</string>
|
||||||
|
|
||||||
|
<string name="feature_send_interbank_transfer_failed">Transfer Failed</string>
|
||||||
|
<string name="feature_send_interbank_transaction_failed">The transaction could not be completed. Please check your details and try again.</string>
|
||||||
|
<string name="feature_send_interbank_error">Error: %1$s</string>
|
||||||
|
<string name="feature_send_interbank_retry_transfer">Retry Transfer</string>
|
||||||
|
<string name="feature_send_interbank_contact_support">Contact Support</string>
|
||||||
|
|
||||||
|
<!-- Success Icon Description -->
|
||||||
|
<string name="feature_send_interbank_success">Success</string>
|
||||||
|
<!-- Failed Icon Description -->
|
||||||
|
<string name="feature_send_interbank_failed">Failed</string>
|
||||||
|
|
||||||
|
<!-- Preview Transfer Screen - Additional Strings -->
|
||||||
|
<string name="feature_send_interbank_acknowledgement_section">Acknowledgement Section</string>
|
||||||
|
<string name="feature_send_interbank_transfer_amount">Transfer Amount</string>
|
||||||
|
<string name="feature_send_interbank_verified_recipient">✓ Verified Recipient</string>
|
||||||
|
|
||||||
|
<!-- Transfer Result Screens - Additional Strings -->
|
||||||
|
<string name="feature_send_interbank_transaction_reference">Transaction Reference</string>
|
||||||
|
<string name="feature_send_interbank_amount_transferred">Amount Transferred</string>
|
||||||
|
<string name="feature_send_interbank_from_account_label">From Account</string>
|
||||||
|
<string name="feature_send_interbank_to_account_label">To Account</string>
|
||||||
|
<string name="feature_send_interbank_transaction_date_label">Transaction Date</string>
|
||||||
|
<string name="feature_send_interbank_attempted_amount">Attempted Amount</string>
|
||||||
|
<string name="feature_send_interbank_available_balance_label">Available Balance</string>
|
||||||
|
|
||||||
|
<!-- Validation Error Messages -->
|
||||||
|
<string name="feature_send_interbank_error_select_sender_account">Please select a sender account</string>
|
||||||
|
<string name="feature_send_interbank_error_select_recipient">Please select a recipient</string>
|
||||||
|
<string name="feature_send_interbank_error_enter_amount">Please enter an amount</string>
|
||||||
|
<string name="feature_send_interbank_error_invalid_amount">Invalid amount</string>
|
||||||
|
<string name="feature_send_interbank_error_amount_greater_than_zero">Amount must be greater than 0</string>
|
||||||
|
<string name="feature_send_interbank_error_enter_description">Please enter a description</string>
|
||||||
|
<string name="feature_send_interbank_error_phone_number_digits">Phone number must be at least 10 digits</string>
|
||||||
|
<string name="feature_send_interbank_error_failed_to_search_recipient">Failed to search recipient</string>
|
||||||
|
<string name="feature_send_interbank_terms_acknowledged">Terms Acknowledged</string>
|
||||||
|
<string name="feature_send_interbank_i_acknowledged">I Acknowledge</string>
|
||||||
|
<string name="feature_send_interbank_terms_acknowledged_description">By completing this final payment, you acknowledge that the transaction is irreversible. Please ensure all details are correct before submission.</string>
|
||||||
|
<string name="feature_send_interbank_cancel">Cancel</string>
|
||||||
|
<string name="feature_send_interbank_ok">Ok</string>
|
||||||
|
<string name="feature_send_interbank_edit_transfer">Edit Transfer</string>
|
||||||
|
</resources>
|
||||||
@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank
|
||||||
|
|
||||||
|
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.Modifier
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.mifospay.core.common.CurrencyFormatter
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
|
import org.mifospay.core.ui.utils.EventsEffect
|
||||||
|
import org.mifospay.feature.send.interbank.screens.PreviewTransferScreen
|
||||||
|
import org.mifospay.feature.send.interbank.screens.SearchRecipientScreen
|
||||||
|
import org.mifospay.feature.send.interbank.screens.SelectAccountScreen
|
||||||
|
import org.mifospay.feature.send.interbank.screens.TransferDetailsScreen
|
||||||
|
import org.mifospay.feature.send.interbank.screens.TransferFailedScreen
|
||||||
|
import org.mifospay.feature.send.interbank.screens.TransferSuccessScreen
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main orchestrator screen for the interbank transfer flow
|
||||||
|
* Manages navigation between all steps of the transfer process
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun InterbankTransferFlowScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onTransferSuccess: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: InterbankTransferViewModel = koinViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
EventsEffect(viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
InterbankTransferEvent.OnNavigateBack -> onBackClick()
|
||||||
|
else -> {
|
||||||
|
// Other steps don't require navigation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state.currentStep) {
|
||||||
|
InterbankTransferState.Step.SelectAccount -> {
|
||||||
|
SelectAccountScreen(
|
||||||
|
accounts = state.fromAccounts,
|
||||||
|
isLoading = state.loadingState is InterbankTransferState.LoadingState.Loading,
|
||||||
|
error = (state.loadingState as? InterbankTransferState.LoadingState.Error)?.message,
|
||||||
|
onAccountSelected = { account ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
InterbankTransferAction.NavigateToRecipientSearch(account),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onBackClick = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.NavigateBack)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferState.Step.SearchRecipient -> {
|
||||||
|
val searchState = state.searchRecipientState
|
||||||
|
val isSearching = searchState is InterbankTransferState.SearchRecipientState.Loading
|
||||||
|
val searchError = (searchState as? InterbankTransferState.SearchRecipientState.Error)?.message
|
||||||
|
|
||||||
|
SearchRecipientScreen(
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
onSearchQueryChanged = { query ->
|
||||||
|
searchQuery = query
|
||||||
|
},
|
||||||
|
recipients = state.searchResults,
|
||||||
|
onRecipientSelected = { participantInfo ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
InterbankTransferAction.NavigateToTransferDetails(participantInfo),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSearchClick = { phoneNumber ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
InterbankTransferAction.SearchRecipient(phoneNumber),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isSearching = isSearching,
|
||||||
|
searchError = searchError,
|
||||||
|
onBackClick = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.NavigateBack)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferState.Step.TransferDetails -> {
|
||||||
|
TransferDetailsScreen(
|
||||||
|
fromAccount = state.selectedFromAccount,
|
||||||
|
recipient = state.selectedParticipantInfo,
|
||||||
|
amount = state.transferAmount,
|
||||||
|
onAmountChanged = { amount ->
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.UpdateAmount(amount))
|
||||||
|
},
|
||||||
|
date = state.transferDate,
|
||||||
|
initialDate = state.initialDate,
|
||||||
|
onDateChanged = { date ->
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.UpdateDate(date))
|
||||||
|
},
|
||||||
|
description = state.transferDescription,
|
||||||
|
onDescriptionChanged = { desc ->
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.UpdateDescription(desc))
|
||||||
|
},
|
||||||
|
onContinueClick = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.NavigateToPreview)
|
||||||
|
},
|
||||||
|
onBackClick = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.NavigateBack)
|
||||||
|
},
|
||||||
|
onEditFromAccount = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.EditFromAccount)
|
||||||
|
},
|
||||||
|
onEditRecipient = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.EditRecipient)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferState.Step.PreviewTransfer -> {
|
||||||
|
PreviewTransferScreen(
|
||||||
|
amount = state.transferAmount,
|
||||||
|
transferDate = state.transferDate,
|
||||||
|
transferDescription = state.transferDescription,
|
||||||
|
fromAccountName = state.selectedFromAccount?.name ?: "Unknown",
|
||||||
|
fromAccountNo = state.selectedFromAccount?.number ?: "N/A",
|
||||||
|
fromAccountBalance = state.selectedFromAccount?.balance ?: 0.0,
|
||||||
|
fromAccountType = "${state.selectedFromAccount?.accountType?.value ?: ""} | ${state.selectedFromAccount?.currency?.name ?: ""}".trim(),
|
||||||
|
recipientInfo = state.selectedParticipantInfo,
|
||||||
|
isProcessing = state.isProcessing,
|
||||||
|
currencyCode = state.selectedFromAccount?.currency?.code ?: "MXN",
|
||||||
|
onEditClick = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.NavigateBack)
|
||||||
|
},
|
||||||
|
onConfirmClick = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.ConfirmTransfer)
|
||||||
|
},
|
||||||
|
onBackClick = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.NavigateBack)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferState.Step.TransferSuccess -> {
|
||||||
|
TransferSuccessScreen(
|
||||||
|
recipientName = "${state.selectedParticipantInfo?.firstName ?: ""} ${state.selectedParticipantInfo?.lastName ?: ""}".trim().ifEmpty { "Recipient" },
|
||||||
|
amount = state.transferAmount,
|
||||||
|
transactionReference = state.transferResponse ?: "N/A",
|
||||||
|
fromAccount = state.selectedFromAccount?.number ?: "N/A",
|
||||||
|
fromAccountName = state.selectedFromAccount?.name ?: "Unknown",
|
||||||
|
toAccount = "${state.selectedParticipantInfo?.firstName ?: ""} ${state.selectedParticipantInfo?.lastName ?: ""}".trim(),
|
||||||
|
toAccountNumber = "Account: ${state.selectedParticipantInfo?.partyId ?: "N/A"}",
|
||||||
|
transactionDate = state.transferDate,
|
||||||
|
description = state.transferDescription,
|
||||||
|
currencyCode = state.selectedFromAccount?.currency?.code ?: "MXN",
|
||||||
|
onBackToHome = onTransferSuccess,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferState.Step.TransferFailed -> {
|
||||||
|
TransferFailedScreen(
|
||||||
|
errorMessage = state.errorMessage ?: "Unknown error occurred",
|
||||||
|
errorTitle = "Transfer Failed",
|
||||||
|
attemptedAmount = CurrencyFormatter.format(
|
||||||
|
state.transferAmount.toDoubleOrNull() ?: 0.0,
|
||||||
|
state.selectedFromAccount?.currency?.code ?: "MXN",
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
availableBalance = CurrencyFormatter.format(
|
||||||
|
state.selectedFromAccount?.balance ?: 0.0,
|
||||||
|
state.selectedFromAccount?.currency?.code ?: "MXN",
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
fromAccount = state.selectedFromAccount?.number ?: "N/A",
|
||||||
|
fromAccountName = state.selectedFromAccount?.name ?: "Unknown",
|
||||||
|
toAccount = "${state.selectedParticipantInfo?.firstName ?: ""} ${state.selectedParticipantInfo?.lastName ?: ""}".trim(),
|
||||||
|
toAccountNumber = "Account: ${state.selectedParticipantInfo?.partyId ?: "N/A"}",
|
||||||
|
transactionDate = state.transferDate,
|
||||||
|
description = state.transferDescription,
|
||||||
|
onRetry = {
|
||||||
|
viewModel.trySendAction(InterbankTransferAction.RetryTransfer)
|
||||||
|
},
|
||||||
|
onBackToHome = onBackClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun InterbankTransferFlowScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
InterbankTransferFlowScreen(
|
||||||
|
onBackClick = {},
|
||||||
|
onTransferSuccess = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,485 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import org.mifospay.core.common.DataState
|
||||||
|
import org.mifospay.core.common.DateHelper
|
||||||
|
import org.mifospay.core.data.repository.InterBankRepository
|
||||||
|
import org.mifospay.core.data.repository.SelfServiceRepository
|
||||||
|
import org.mifospay.core.datastore.UserPreferencesRepository
|
||||||
|
import org.mifospay.core.model.account.Account
|
||||||
|
import org.mifospay.core.model.client.Client
|
||||||
|
import org.mifospay.core.model.interbank.Amount
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoResponse
|
||||||
|
import org.mifospay.core.model.interbank.InterBankTransferRequest
|
||||||
|
import org.mifospay.core.model.interbank.Party
|
||||||
|
import org.mifospay.core.model.interbank.TransactionType
|
||||||
|
import org.mifospay.core.ui.utils.BaseViewModel
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for managing interbank transfer flow
|
||||||
|
* Handles all stages: account selection, recipient search, transfer details, preview, and confirmation
|
||||||
|
*/
|
||||||
|
class InterbankTransferViewModel(
|
||||||
|
private val selfServiceRepository: SelfServiceRepository,
|
||||||
|
private val interBankRepository: InterBankRepository,
|
||||||
|
private val preferencesRepository: UserPreferencesRepository,
|
||||||
|
) : BaseViewModel<InterbankTransferState, InterbankTransferEvent, InterbankTransferAction>(
|
||||||
|
initialState = run {
|
||||||
|
val client = requireNotNull(preferencesRepository.client.value)
|
||||||
|
InterbankTransferState(client = client)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadFromAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: InterbankTransferAction) {
|
||||||
|
when (action) {
|
||||||
|
// Navigation actions
|
||||||
|
is InterbankTransferAction.NavigateToRecipientSearch -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
currentStep = InterbankTransferState.Step.SearchRecipient,
|
||||||
|
selectedFromAccount = action.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is InterbankTransferAction.NavigateToTransferDetails -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
currentStep = InterbankTransferState.Step.TransferDetails,
|
||||||
|
selectedParticipantInfo = action.participantInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is InterbankTransferAction.NavigateToPreview -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(currentStep = InterbankTransferState.Step.PreviewTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferAction.NavigateBack -> {
|
||||||
|
val previousStep = when (state.currentStep) {
|
||||||
|
InterbankTransferState.Step.SelectAccount -> {
|
||||||
|
sendEvent(InterbankTransferEvent.OnNavigateBack)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferState.Step.SearchRecipient -> InterbankTransferState.Step.SelectAccount
|
||||||
|
InterbankTransferState.Step.TransferDetails -> InterbankTransferState.Step.SearchRecipient
|
||||||
|
InterbankTransferState.Step.PreviewTransfer -> InterbankTransferState.Step.TransferDetails
|
||||||
|
InterbankTransferState.Step.TransferSuccess -> InterbankTransferState.Step.PreviewTransfer
|
||||||
|
InterbankTransferState.Step.TransferFailed -> InterbankTransferState.Step.PreviewTransfer
|
||||||
|
}
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(currentStep = previousStep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferAction.EditFromAccount -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(currentStep = InterbankTransferState.Step.SelectAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferAction.EditRecipient -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(currentStep = InterbankTransferState.Step.SearchRecipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer details actions
|
||||||
|
is InterbankTransferAction.UpdateAmount -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(transferAmount = action.amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is InterbankTransferAction.UpdateDate -> {
|
||||||
|
val date = DateHelper.getDateAsStringFromLong(action.date)
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(transferDate = date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is InterbankTransferAction.UpdateDescription -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(transferDescription = action.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer confirmation
|
||||||
|
InterbankTransferAction.ConfirmTransfer -> {
|
||||||
|
validateAndInitiateTransfer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
InterbankTransferAction.RetryTransfer -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(currentStep = InterbankTransferState.Step.PreviewTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InterbankTransferAction.DismissError -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(errorMessage = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is InterbankTransferAction.Internal.HandleTransferResult -> {
|
||||||
|
handleTransferResult(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is InterbankTransferAction.SearchRecipient -> {
|
||||||
|
searchRecipient(action.phoneNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadFromAccounts() {
|
||||||
|
try {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(loadingState = InterbankTransferState.LoadingState.Loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
selfServiceRepository.getActiveAccountsWithAccountTransferTemplate(state.client.id)
|
||||||
|
.collect { result ->
|
||||||
|
when (result) {
|
||||||
|
is DataState.Error -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
loadingState = InterbankTransferState.LoadingState.Error(
|
||||||
|
result.message ?: "Failed to load accounts",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Loading -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(loadingState = InterbankTransferState.LoadingState.Loading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Success -> {
|
||||||
|
val accounts = result.data
|
||||||
|
if (accounts.isEmpty()) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
loadingState = InterbankTransferState.LoadingState.Error(
|
||||||
|
"No accounts available",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
loadingState = InterbankTransferState.LoadingState.Success,
|
||||||
|
fromAccounts = accounts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
loadingState = InterbankTransferState.LoadingState.Error(
|
||||||
|
e.message ?: "Failed to load accounts",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
private fun validateAndInitiateTransfer() {
|
||||||
|
val validationError = validateTransferDetails()
|
||||||
|
if (validationError != null) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(errorMessage = validationError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(isProcessing = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build InterBank transfer request from participantInfo and selected account
|
||||||
|
val participantInfo = state.selectedParticipantInfo ?: return@launch
|
||||||
|
val transferRequest = InterBankTransferRequest(
|
||||||
|
homeTransactionId = Uuid.random().toString(),
|
||||||
|
from = Party(
|
||||||
|
fspId = participantInfo.sourceFspId,
|
||||||
|
idType = participantInfo.partyIdType,
|
||||||
|
idValue = state.selectedFromAccount?.externalId
|
||||||
|
?: state.selectedFromAccount?.number ?: "",
|
||||||
|
),
|
||||||
|
to = Party(
|
||||||
|
fspId = participantInfo.destinationFspId,
|
||||||
|
idType = participantInfo.partyIdType,
|
||||||
|
idValue = participantInfo.partyId,
|
||||||
|
),
|
||||||
|
amountType = "SEND",
|
||||||
|
amount = Amount(
|
||||||
|
currencyCode = state.selectedFromAccount?.currency?.code
|
||||||
|
?: participantInfo.currencyCode,
|
||||||
|
amount = state.transferAmount.toDoubleOrNull() ?: 0.0,
|
||||||
|
),
|
||||||
|
transactionType = TransactionType(
|
||||||
|
scenario = "TRANSFER",
|
||||||
|
subScenario = "DOMESTIC",
|
||||||
|
initiator = "PAYER",
|
||||||
|
initiatorType = "CUSTOMER",
|
||||||
|
),
|
||||||
|
note = state.transferDescription,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = interBankRepository.interBankMakeTransfer(transferRequest)
|
||||||
|
sendAction(InterbankTransferAction.Internal.HandleTransferResult(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateTransferDetails(): String? {
|
||||||
|
return when {
|
||||||
|
state.selectedFromAccount == null -> "Please select a sender account"
|
||||||
|
state.selectedParticipantInfo == null -> "Please select a recipient"
|
||||||
|
state.transferAmount.isBlank() -> "Please enter an amount"
|
||||||
|
state.transferAmount.toDoubleOrNull() == null -> "Invalid amount"
|
||||||
|
state.transferAmount.toDouble() <= 0 -> "Amount must be greater than 0"
|
||||||
|
state.transferDescription.isBlank() -> "Please enter a description"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchRecipient(phoneNumber: String) {
|
||||||
|
if (phoneNumber.length < 10) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
searchRecipientState = InterbankTransferState.SearchRecipientState.Error(
|
||||||
|
"Phone number must be at least 10 digits",
|
||||||
|
),
|
||||||
|
searchResults = emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(searchRecipientState = InterbankTransferState.SearchRecipientState.Loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = interBankRepository.findParticipant(
|
||||||
|
partyId = phoneNumber,
|
||||||
|
currencyCode = mutableStateFlow.value.selectedFromAccount?.currency?.code ?: "MXN",
|
||||||
|
)
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is DataState.Success -> {
|
||||||
|
val partyInfo = result.data
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
searchRecipientState = InterbankTransferState.SearchRecipientState.Success,
|
||||||
|
searchResults = listOf(partyInfo),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Error -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
searchRecipientState = InterbankTransferState.SearchRecipientState.Error(
|
||||||
|
result.message ?: "Failed to search recipient",
|
||||||
|
),
|
||||||
|
searchResults = emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Loading -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(searchRecipientState = InterbankTransferState.SearchRecipientState.Loading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleTransferResult(action: InterbankTransferAction.Internal.HandleTransferResult) {
|
||||||
|
when (action.result) {
|
||||||
|
is DataState.Loading -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(isProcessing = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Success -> {
|
||||||
|
val transferResponse = action.result.data
|
||||||
|
val responseMessage = when (transferResponse) {
|
||||||
|
is org.mifospay.core.model.interbank.InterBankTransferResponse -> {
|
||||||
|
"Transfer ID: ${transferResponse.transactionId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> "Transfer completed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
isProcessing = false,
|
||||||
|
currentStep = InterbankTransferState.Step.TransferSuccess,
|
||||||
|
transferResponse = responseMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sendEvent(InterbankTransferEvent.OnTransferSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Error -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
isProcessing = false,
|
||||||
|
currentStep = InterbankTransferState.Step.TransferFailed,
|
||||||
|
errorMessage = action.result.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sendEvent(InterbankTransferEvent.OnTransferFailed(action.result.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
@Serializable
|
||||||
|
data class InterbankTransferState(
|
||||||
|
val client: Client,
|
||||||
|
val currentStep: Step = Step.SelectAccount,
|
||||||
|
val loadingState: LoadingState = LoadingState.Loading,
|
||||||
|
val fromAccounts: List<Account> = emptyList(),
|
||||||
|
val selectedFromAccount: Account? = null,
|
||||||
|
val selectedParticipantInfo: InterBankPartyInfoResponse? = null,
|
||||||
|
val transferAmount: String = "1.0",
|
||||||
|
val transferDate: String = DateHelper.getDateAsString(
|
||||||
|
Clock.System.todayIn(TimeZone.currentSystemDefault()).toString(),
|
||||||
|
),
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val initialDate: Long = Clock.System.now().toEpochMilliseconds(),
|
||||||
|
|
||||||
|
val transferDescription: String = "Interbank Transfer",
|
||||||
|
val isProcessing: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val transferResponse: String? = null,
|
||||||
|
val searchRecipientState: SearchRecipientState = SearchRecipientState.Idle,
|
||||||
|
val searchResults: List<InterBankPartyInfoResponse> = emptyList(),
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
sealed interface Step {
|
||||||
|
@Serializable
|
||||||
|
data object SelectAccount : Step
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SearchRecipient : Step
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object TransferDetails : Step
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object PreviewTransfer : Step
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object TransferSuccess : Step
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object TransferFailed : Step
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface LoadingState {
|
||||||
|
@Serializable
|
||||||
|
data object Loading : LoadingState
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Success : LoadingState
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Error(val message: String) : LoadingState
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface SearchRecipientState {
|
||||||
|
@Serializable
|
||||||
|
data object Idle : SearchRecipientState
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Loading : SearchRecipientState
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Success : SearchRecipientState
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Error(val message: String) : SearchRecipientState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface InterbankTransferEvent {
|
||||||
|
data object OnNavigateBack : InterbankTransferEvent
|
||||||
|
data object OnTransferSuccess : InterbankTransferEvent
|
||||||
|
data class OnTransferFailed(val message: String) : InterbankTransferEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface InterbankTransferAction {
|
||||||
|
// Navigation
|
||||||
|
data class NavigateToRecipientSearch(val account: Account) : InterbankTransferAction
|
||||||
|
data class NavigateToTransferDetails(val participantInfo: InterBankPartyInfoResponse) :
|
||||||
|
InterbankTransferAction
|
||||||
|
|
||||||
|
data object NavigateToPreview : InterbankTransferAction
|
||||||
|
data object NavigateBack : InterbankTransferAction
|
||||||
|
data object EditFromAccount : InterbankTransferAction
|
||||||
|
data object EditRecipient : InterbankTransferAction
|
||||||
|
|
||||||
|
// Transfer details
|
||||||
|
data class UpdateAmount(val amount: String) : InterbankTransferAction
|
||||||
|
data class UpdateDate(val date: Long) : InterbankTransferAction
|
||||||
|
data class UpdateDescription(val description: String) : InterbankTransferAction
|
||||||
|
|
||||||
|
// Recipient search
|
||||||
|
data class SearchRecipient(val phoneNumber: String) : InterbankTransferAction
|
||||||
|
|
||||||
|
// Transfer confirmation
|
||||||
|
data object ConfirmTransfer : InterbankTransferAction
|
||||||
|
data object RetryTransfer : InterbankTransferAction
|
||||||
|
data object DismissError : InterbankTransferAction
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
sealed interface Internal : InterbankTransferAction {
|
||||||
|
data class HandleTransferResult(val result: DataState<Any>) : Internal
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank.di
|
||||||
|
|
||||||
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import org.mifospay.feature.send.interbank.InterbankTransferViewModel
|
||||||
|
|
||||||
|
val interbankTransferModule = module {
|
||||||
|
viewModelOf(::InterbankTransferViewModel)
|
||||||
|
}
|
||||||
@ -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.interbank.navigation
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.mifospay.feature.send.interbank.InterbankTransferFlowScreen
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InterbankTransferRoute(
|
||||||
|
val returnDestination: String = "home",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun NavController.navigateToInterbankTransfer(
|
||||||
|
returnDestination: String = "home",
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
this.navigate(InterbankTransferRoute(returnDestination = returnDestination), navOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.interbankTransferScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onTransferSuccess: () -> Unit,
|
||||||
|
) {
|
||||||
|
composable<InterbankTransferRoute> { backStackEntry ->
|
||||||
|
val route = backStackEntry.toRoute<InterbankTransferRoute>()
|
||||||
|
InterbankTransferFlowScreen(
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onTransferSuccess = onTransferSuccess,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,565 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_acknowledgement_section
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_amount
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_available_balance
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_confirm_pay
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_date
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_description
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_edit
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_from_account
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_i_acknowledged
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_preview_transfer
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_terms_acknowledged
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_terms_acknowledged_description
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_to_account
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transfer_amount
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_verified_recipient
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.common.CurrencyFormatter
|
||||||
|
import org.mifospay.core.designsystem.component.MifosButton
|
||||||
|
import org.mifospay.core.designsystem.component.MifosCard
|
||||||
|
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.MifosTheme
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoResponse
|
||||||
|
import org.mifospay.core.ui.AvatarBox
|
||||||
|
import org.mifospay.core.ui.MifosProgressIndicatorOverlay
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PreviewTransferScreen(
|
||||||
|
amount: String,
|
||||||
|
transferDate: String,
|
||||||
|
transferDescription: String,
|
||||||
|
fromAccountName: String,
|
||||||
|
fromAccountNo: String,
|
||||||
|
fromAccountBalance: Double = 0.0,
|
||||||
|
fromAccountType: String = "",
|
||||||
|
recipientInfo: InterBankPartyInfoResponse?,
|
||||||
|
isProcessing: Boolean,
|
||||||
|
onEditClick: () -> Unit,
|
||||||
|
onConfirmClick: () -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
currencyCode: String = "MXN",
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var disclaimerAccepted by remember { mutableStateOf(false) }
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
MifosTopBar(
|
||||||
|
topBarTitle = stringResource(Res.string.feature_send_interbank_preview_transfer),
|
||||||
|
backPress = onBackClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
MifosButton(
|
||||||
|
onClick = onConfirmClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isProcessing && disclaimerAccepted,
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_interbank_confirm_pay))
|
||||||
|
}
|
||||||
|
|
||||||
|
MifosButton(
|
||||||
|
onClick = onEditClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isProcessing,
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_interbank_edit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = KptTheme.colorScheme.background,
|
||||||
|
) { paddingValues ->
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(horizontal = KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.lg),
|
||||||
|
) {
|
||||||
|
// From Account Section
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_from_account),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
TransferPreviewCard(
|
||||||
|
name = fromAccountName,
|
||||||
|
accountNo = fromAccountNo,
|
||||||
|
accountType = fromAccountType,
|
||||||
|
balance = fromAccountBalance,
|
||||||
|
currencyCode = currencyCode,
|
||||||
|
icon = MifosIcons.Bank,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To Account Section
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_to_account),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
TransferPreviewCard(
|
||||||
|
name = "${recipientInfo?.firstName ?: ""} ${recipientInfo?.lastName ?: ""}".trim(),
|
||||||
|
accountNo = recipientInfo?.partyId ?: "N/A",
|
||||||
|
accountType = recipientInfo?.destinationFspId ?: "",
|
||||||
|
icon = MifosIcons.Person,
|
||||||
|
isVerified = recipientInfo?.executionStatus == true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_amount),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.lg),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = currencyCode,
|
||||||
|
style = KptTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = amount,
|
||||||
|
style = KptTheme.typography.displaySmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transfer_amount),
|
||||||
|
style = KptTheme.typography.labelSmall,
|
||||||
|
color = KptTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_date),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
MifosCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.CalenderMonth,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = transferDate.ifEmpty { "N/A" },
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "Today",
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_description),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MifosCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = transferDescription.ifEmpty { "N/A" },
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(KptTheme.spacing.md),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acknowledgement Section
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_acknowledgement_section),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
DisclaimerCheckboxCard(
|
||||||
|
isChecked = disclaimerAccepted,
|
||||||
|
onCheckedChange = { disclaimerAccepted = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Box(modifier = Modifier.padding(vertical = KptTheme.spacing.lg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProcessing) {
|
||||||
|
MifosProgressIndicatorOverlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TransferPreviewCard(
|
||||||
|
name: String,
|
||||||
|
accountNo: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
accountType: String = "",
|
||||||
|
balance: Double = 0.0,
|
||||||
|
currencyCode: String = "",
|
||||||
|
isVerified: Boolean = false,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.primary.copy(alpha = 0.08f),
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(KptTheme.spacing.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
AvatarBox(
|
||||||
|
icon = icon,
|
||||||
|
backgroundColor = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
if (accountType.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = accountType,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = accountNo,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
if (isVerified) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_verified_recipient),
|
||||||
|
style = KptTheme.typography.labelSmall,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (balance > 0.0 && currencyCode.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_available_balance,
|
||||||
|
CurrencyFormatter.format(balance, currencyCode, null),
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisclaimerCheckboxCard(
|
||||||
|
isChecked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val containerColor = if (isChecked) {
|
||||||
|
KptTheme.colorScheme.primaryContainer
|
||||||
|
} else {
|
||||||
|
KptTheme.colorScheme.errorContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
val textColor = if (isChecked) {
|
||||||
|
KptTheme.colorScheme.onPrimaryContainer
|
||||||
|
} else {
|
||||||
|
KptTheme.colorScheme.onErrorContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
val checkboxColor = if (isChecked) {
|
||||||
|
KptTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
KptTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = containerColor,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!isChecked) }
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = isChecked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = checkboxColor,
|
||||||
|
uncheckedColor = checkboxColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isChecked) {
|
||||||
|
stringResource(Res.string.feature_send_interbank_terms_acknowledged)
|
||||||
|
} else {
|
||||||
|
stringResource(Res.string.feature_send_interbank_i_acknowledged)
|
||||||
|
},
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_terms_acknowledged_description),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewTransferScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
val mockRecipient = InterBankPartyInfoResponse(
|
||||||
|
sourceFspId = "mifos-bank-1",
|
||||||
|
destinationFspId = "blackbank-test",
|
||||||
|
requestId = "req-001",
|
||||||
|
partyId = "9880000020",
|
||||||
|
currencyCode = "MXN",
|
||||||
|
firstName = "Pedro",
|
||||||
|
lastName = "Barreto",
|
||||||
|
systemMessage = "Success",
|
||||||
|
executionStatus = true,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
)
|
||||||
|
|
||||||
|
PreviewTransferScreen(
|
||||||
|
amount = "100.00",
|
||||||
|
transferDate = "11/09/25",
|
||||||
|
transferDescription = "Dinner share",
|
||||||
|
fromAccountName = "ALEJANDRO ESCUTIA",
|
||||||
|
fromAccountNo = "00000002",
|
||||||
|
fromAccountBalance = 90760.0,
|
||||||
|
fromAccountType = "Savings Account | Mexican Peso",
|
||||||
|
recipientInfo = mockRecipient,
|
||||||
|
isProcessing = false,
|
||||||
|
onEditClick = {},
|
||||||
|
onConfirmClick = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewTransferScreenProcessingPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
val mockRecipient = InterBankPartyInfoResponse(
|
||||||
|
sourceFspId = "mifos-bank-1",
|
||||||
|
destinationFspId = "blackbank-test",
|
||||||
|
requestId = "req-001",
|
||||||
|
partyId = "9880000020",
|
||||||
|
currencyCode = "MXN",
|
||||||
|
firstName = "Pedro",
|
||||||
|
lastName = "Barreto",
|
||||||
|
systemMessage = "Success",
|
||||||
|
executionStatus = true,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
)
|
||||||
|
|
||||||
|
PreviewTransferScreen(
|
||||||
|
amount = "100.00",
|
||||||
|
transferDate = "11/09/25",
|
||||||
|
transferDescription = "Dinner share",
|
||||||
|
fromAccountName = "ALEJANDRO ESCUTIA",
|
||||||
|
fromAccountNo = "00000002",
|
||||||
|
fromAccountBalance = 90760.0,
|
||||||
|
fromAccountType = "Savings Account | Mexican Peso",
|
||||||
|
recipientInfo = mockRecipient,
|
||||||
|
isProcessing = true,
|
||||||
|
onEditClick = {},
|
||||||
|
onConfirmClick = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,308 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank.screens
|
||||||
|
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_bank
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_enter_phone_number
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_enter_phone_to_search
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_found_recipients
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_no_recipients_found
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_no_results
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_search_recipient
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_to_account_interbank
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.designsystem.component.MifosButton
|
||||||
|
import org.mifospay.core.designsystem.component.MifosCard
|
||||||
|
import org.mifospay.core.designsystem.component.MifosScaffold
|
||||||
|
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.designsystem.theme.MifosTheme
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoResponse
|
||||||
|
import org.mifospay.core.ui.AvatarBox
|
||||||
|
import org.mifospay.core.ui.EmptyContentScreen
|
||||||
|
import org.mifospay.core.ui.MifosProgressIndicator
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchRecipientScreen(
|
||||||
|
searchQuery: String,
|
||||||
|
onSearchQueryChanged: (String) -> Unit,
|
||||||
|
recipients: List<InterBankPartyInfoResponse>,
|
||||||
|
onRecipientSelected: (InterBankPartyInfoResponse) -> Unit,
|
||||||
|
onSearchClick: (String) -> Unit,
|
||||||
|
isSearching: Boolean = false,
|
||||||
|
searchError: String? = null,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
MifosTopBar(
|
||||||
|
topBarTitle = stringResource(Res.string.feature_send_interbank_search_recipient),
|
||||||
|
backPress = onBackClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = KptTheme.colorScheme.background,
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
MifosTextField(
|
||||||
|
label = stringResource(Res.string.feature_send_interbank_enter_phone_number),
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = onSearchQueryChanged,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Phone,
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
MifosButton(
|
||||||
|
onClick = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
onSearchClick(searchQuery)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = searchQuery.length >= 10 && !isSearching,
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_interbank_search_recipient))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSearching) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = KptTheme.spacing.lg),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
MifosProgressIndicator()
|
||||||
|
}
|
||||||
|
} else if (searchError != null) {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_send_interbank_no_results),
|
||||||
|
subTitle = searchError,
|
||||||
|
)
|
||||||
|
} else if (recipients.isEmpty() && searchQuery.isNotEmpty()) {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_send_interbank_no_results),
|
||||||
|
subTitle = stringResource(Res.string.feature_send_interbank_no_recipients_found),
|
||||||
|
)
|
||||||
|
} else if (recipients.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_found_recipients,
|
||||||
|
recipients.size,
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
items(recipients) { recipient ->
|
||||||
|
RecipientSelectionCard(
|
||||||
|
recipient = recipient,
|
||||||
|
onClick = { onRecipientSelected(recipient) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = KptTheme.spacing.lg),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_enter_phone_to_search),
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecipientSelectionCard(
|
||||||
|
recipient: InterBankPartyInfoResponse,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MifosCard(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.xs)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
colors = CardDefaults.cardColors(KptTheme.colorScheme.surface),
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = "${recipient.firstName} ${recipient.lastName}",
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_to_account_interbank,
|
||||||
|
recipient.partyId,
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_bank,
|
||||||
|
recipient.destinationFspId,
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(
|
||||||
|
icon = MifosIcons.Person,
|
||||||
|
backgroundColor = KptTheme.colorScheme.secondaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SearchRecipientScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
val mockRecipients = listOf(
|
||||||
|
InterBankPartyInfoResponse(
|
||||||
|
sourceFspId = "mifos-bank-1",
|
||||||
|
destinationFspId = "blackbank-test",
|
||||||
|
requestId = "req-001",
|
||||||
|
partyId = "9880000020",
|
||||||
|
currencyCode = "MXN",
|
||||||
|
firstName = "Pedro",
|
||||||
|
lastName = "Barreto",
|
||||||
|
systemMessage = "Success",
|
||||||
|
executionStatus = true,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
),
|
||||||
|
InterBankPartyInfoResponse(
|
||||||
|
sourceFspId = "mifos-bank-1",
|
||||||
|
destinationFspId = "blackbank-test",
|
||||||
|
requestId = "req-002",
|
||||||
|
partyId = "9880000021",
|
||||||
|
currencyCode = "MXN",
|
||||||
|
firstName = "Maria",
|
||||||
|
lastName = "Garcia",
|
||||||
|
systemMessage = "Success",
|
||||||
|
executionStatus = true,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SearchRecipientScreen(
|
||||||
|
searchQuery = "988",
|
||||||
|
onSearchQueryChanged = {},
|
||||||
|
recipients = mockRecipients,
|
||||||
|
onRecipientSelected = {},
|
||||||
|
onSearchClick = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SearchRecipientScreenEmptyPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SearchRecipientScreen(
|
||||||
|
searchQuery = "",
|
||||||
|
onSearchQueryChanged = {},
|
||||||
|
recipients = emptyList(),
|
||||||
|
onRecipientSelected = {},
|
||||||
|
onSearchClick = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SearchRecipientScreenNoResultsPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SearchRecipientScreen(
|
||||||
|
searchQuery = "999999",
|
||||||
|
onSearchQueryChanged = {},
|
||||||
|
recipients = emptyList(),
|
||||||
|
onRecipientSelected = {},
|
||||||
|
onSearchClick = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SearchRecipientScreenLoadingPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SearchRecipientScreen(
|
||||||
|
searchQuery = "9388006020",
|
||||||
|
onSearchQueryChanged = {},
|
||||||
|
recipients = emptyList(),
|
||||||
|
onRecipientSelected = {},
|
||||||
|
onSearchClick = {},
|
||||||
|
isSearching = true,
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank.screens
|
||||||
|
|
||||||
|
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.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.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.unit.dp
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_account
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_account_type
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_balance
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_choose_account_to_send
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_no_accounts
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_no_accounts_available
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_office
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_oops
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_select_account
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_select_your_account
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.common.CurrencyFormatter
|
||||||
|
import org.mifospay.core.designsystem.component.MifosCard
|
||||||
|
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.MifosTheme
|
||||||
|
import org.mifospay.core.model.account.Account
|
||||||
|
import org.mifospay.core.model.savingsaccount.Currency
|
||||||
|
import org.mifospay.core.model.savingsaccount.Status
|
||||||
|
import org.mifospay.core.network.model.entity.templates.account.AccountType
|
||||||
|
import org.mifospay.core.ui.AvatarBox
|
||||||
|
import org.mifospay.core.ui.EmptyContentScreen
|
||||||
|
import org.mifospay.core.ui.MifosProgressIndicator
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectAccountScreen(
|
||||||
|
accounts: List<Account>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
error: String?,
|
||||||
|
onAccountSelected: (Account) -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
MifosTopBar(
|
||||||
|
topBarTitle = stringResource(Res.string.feature_send_interbank_select_account),
|
||||||
|
backPress = onBackClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = KptTheme.colorScheme.background,
|
||||||
|
) { paddingValues ->
|
||||||
|
when {
|
||||||
|
isLoading -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
MifosProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error != null -> {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_send_interbank_oops),
|
||||||
|
subTitle = error,
|
||||||
|
iconTint = KptTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts.isEmpty() -> {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_send_interbank_no_accounts),
|
||||||
|
subTitle = stringResource(Res.string.feature_send_interbank_no_accounts_available),
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_select_your_account),
|
||||||
|
style = KptTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_choose_account_to_send),
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = KptTheme.spacing.xs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(accounts) { account ->
|
||||||
|
AccountSelectionCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
account = account,
|
||||||
|
onClick = { onAccountSelected(account) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Box(modifier = Modifier.padding(vertical = KptTheme.spacing.md))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountSelectionCard(
|
||||||
|
account: Account,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MifosCard(
|
||||||
|
colors = CardDefaults.cardColors(KptTheme.colorScheme.background),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
modifier = modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
val accountBalance = CurrencyFormatter.format(
|
||||||
|
balance = account.balance,
|
||||||
|
currencyCode = account.currency.code,
|
||||||
|
maximumFractionDigits = null,
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = account.clientName,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_balance,
|
||||||
|
accountBalance,
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_account,
|
||||||
|
account.name,
|
||||||
|
account.number,
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_office,
|
||||||
|
account.officeName ?: "",
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_account_type,
|
||||||
|
account.accountType?.value ?: "",
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(KptTheme.spacing.xs))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(
|
||||||
|
icon = MifosIcons.Bank,
|
||||||
|
backgroundColor = KptTheme.colorScheme.primaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SelectAccountScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
val mockAccounts = listOf(
|
||||||
|
Account(
|
||||||
|
id = 1L,
|
||||||
|
name = "WALLET",
|
||||||
|
clientName = "ALEJANDRO ESCUTIA",
|
||||||
|
number = "00000002",
|
||||||
|
balance = 5000.50,
|
||||||
|
productId = 1L,
|
||||||
|
officeName = "SAN JUAN OZOLOTEPEC",
|
||||||
|
accountType = org.mifospay.core.model.savingsaccount.AccountType(
|
||||||
|
id = 1L,
|
||||||
|
code = "savingsAccountType.savings",
|
||||||
|
value = "Savings",
|
||||||
|
),
|
||||||
|
currency = Currency(
|
||||||
|
code = "USD",
|
||||||
|
name = "US Dollar",
|
||||||
|
displaySymbol = "$",
|
||||||
|
displayLabel = "US Dollar ($)",
|
||||||
|
decimalPlaces = 2,
|
||||||
|
inMultiplesOf = 10,
|
||||||
|
nameCode = "USD",
|
||||||
|
),
|
||||||
|
status = Status(
|
||||||
|
id = 300,
|
||||||
|
code = "savingsAccountStatusType.active",
|
||||||
|
value = "Active",
|
||||||
|
submittedAndPendingApproval = false,
|
||||||
|
approved = true,
|
||||||
|
rejected = false,
|
||||||
|
withdrawnByApplicant = false,
|
||||||
|
active = true,
|
||||||
|
closed = false,
|
||||||
|
prematureClosed = false,
|
||||||
|
transferInProgress = false,
|
||||||
|
transferOnHold = false,
|
||||||
|
matured = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Account(
|
||||||
|
id = 2L,
|
||||||
|
name = "WALLET",
|
||||||
|
number = "00000003",
|
||||||
|
clientName = "JUAN PEREZ",
|
||||||
|
balance = 3200.75,
|
||||||
|
productId = 1L,
|
||||||
|
officeName = "SAN JUAN OZOLOTEPEC",
|
||||||
|
accountType = org.mifospay.core.model.savingsaccount.AccountType(
|
||||||
|
id = 1L,
|
||||||
|
code = "savingsAccountType.savings",
|
||||||
|
value = "Savings",
|
||||||
|
),
|
||||||
|
currency = Currency(
|
||||||
|
code = "USD",
|
||||||
|
name = "US Dollar",
|
||||||
|
displaySymbol = "$",
|
||||||
|
displayLabel = "US Dollar ($)",
|
||||||
|
decimalPlaces = 2,
|
||||||
|
inMultiplesOf = 10,
|
||||||
|
nameCode = "USD",
|
||||||
|
),
|
||||||
|
status = Status(
|
||||||
|
id = 300,
|
||||||
|
code = "savingsAccountStatusType.active",
|
||||||
|
value = "Active",
|
||||||
|
submittedAndPendingApproval = false,
|
||||||
|
approved = true,
|
||||||
|
rejected = false,
|
||||||
|
withdrawnByApplicant = false,
|
||||||
|
active = true,
|
||||||
|
closed = false,
|
||||||
|
prematureClosed = false,
|
||||||
|
transferInProgress = false,
|
||||||
|
transferOnHold = false,
|
||||||
|
matured = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SelectAccountScreen(
|
||||||
|
accounts = mockAccounts,
|
||||||
|
isLoading = false,
|
||||||
|
error = null,
|
||||||
|
onAccountSelected = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SelectAccountScreenLoadingPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SelectAccountScreen(
|
||||||
|
accounts = emptyList(),
|
||||||
|
isLoading = true,
|
||||||
|
error = null,
|
||||||
|
onAccountSelected = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SelectAccountScreenErrorPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SelectAccountScreen(
|
||||||
|
accounts = emptyList(),
|
||||||
|
isLoading = false,
|
||||||
|
error = "Failed to load accounts. Please try again.",
|
||||||
|
onAccountSelected = {},
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,605 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank.screens
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.DatePicker
|
||||||
|
import androidx.compose.material3.DatePickerDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.SelectableDates
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_amount
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_available_balance
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_cancel
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_continue
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_date
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_description
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_edit
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_from_account
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_ok
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_to_account
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_to_account_interbank
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transfer_details
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_verified
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.common.CurrencyFormatter
|
||||||
|
import org.mifospay.core.designsystem.component.MifosButton
|
||||||
|
import org.mifospay.core.designsystem.component.MifosCard
|
||||||
|
import org.mifospay.core.designsystem.component.MifosScaffold
|
||||||
|
import org.mifospay.core.designsystem.component.MifosTextField
|
||||||
|
import org.mifospay.core.designsystem.component.MifosTopBar
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
|
import org.mifospay.core.model.account.Account
|
||||||
|
import org.mifospay.core.model.interbank.InterBankPartyInfoResponse
|
||||||
|
import org.mifospay.core.model.savingsaccount.Currency
|
||||||
|
import org.mifospay.core.model.savingsaccount.Status
|
||||||
|
import org.mifospay.core.ui.AmountEditText
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
|
||||||
|
@Composable
|
||||||
|
fun TransferDetailsScreen(
|
||||||
|
fromAccount: Account?,
|
||||||
|
recipient: InterBankPartyInfoResponse?,
|
||||||
|
amount: String,
|
||||||
|
onAmountChanged: (String) -> Unit,
|
||||||
|
initialDate: Long,
|
||||||
|
date: String,
|
||||||
|
onDateChanged: (Long) -> Unit,
|
||||||
|
description: String,
|
||||||
|
onDescriptionChanged: (String) -> Unit,
|
||||||
|
onContinueClick: () -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onEditFromAccount: () -> Unit = {},
|
||||||
|
onEditRecipient: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
MifosTopBar(
|
||||||
|
topBarTitle = stringResource(Res.string.feature_send_interbank_transfer_details),
|
||||||
|
backPress = onBackClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
MifosButton(
|
||||||
|
onClick = onContinueClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = amount.isNotEmpty() &&
|
||||||
|
amount.toDoubleOrNull()?.let {
|
||||||
|
it <= (fromAccount?.balance ?: 0.0)
|
||||||
|
} ?: false &&
|
||||||
|
description.isNotEmpty(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_interbank_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = KptTheme.colorScheme.background,
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(KptTheme.spacing.md)
|
||||||
|
.imePadding(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
// From Account Card
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_from_account),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
AccountDetailCard(
|
||||||
|
account = fromAccount,
|
||||||
|
showVerified = false,
|
||||||
|
onEditClick = onEditFromAccount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To Account Card
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_to_account),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
RecipientDetailCard(
|
||||||
|
recipient = recipient,
|
||||||
|
onEditClick = onEditRecipient,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amount Input
|
||||||
|
item {
|
||||||
|
var amountLocal by remember { mutableStateOf(amount) }
|
||||||
|
var errorMsg by remember { mutableStateOf<String?>(null) }
|
||||||
|
var isError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_amount),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
AmountEditText(
|
||||||
|
value = amountLocal,
|
||||||
|
onValueChange = { doubleAmount ->
|
||||||
|
onAmountChanged.invoke(doubleAmount.toString())
|
||||||
|
amountLocal = doubleAmount.toString()
|
||||||
|
},
|
||||||
|
currencyCode = fromAccount?.currency?.code ?: "MXN",
|
||||||
|
availableBalance = fromAccount?.balance ?: 0.0,
|
||||||
|
maxAmount = fromAccount?.balance ?: 0.0,
|
||||||
|
errorMessage = errorMsg,
|
||||||
|
onAmountValidation = { _, error ->
|
||||||
|
errorMsg = error ?: ""
|
||||||
|
isError = error != null
|
||||||
|
},
|
||||||
|
isError = isError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date Input with Picker
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_date),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
MifosCard(
|
||||||
|
colors = CardDefaults.cardColors(KptTheme.colorScheme.background),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
// Hide the onclick show transaction date picker for now as
|
||||||
|
// we are not allowing
|
||||||
|
// showDatePicker = true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = date,
|
||||||
|
style = KptTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CalendarMonth,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_interbank_edit),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description Input
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_description),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
MifosTextField(
|
||||||
|
label = "",
|
||||||
|
value = description,
|
||||||
|
onValueChange = onDescriptionChanged,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
minLines = 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Box(modifier = Modifier.padding(vertical = KptTheme.spacing.md))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = initialDate,
|
||||||
|
selectableDates = object : SelectableDates {
|
||||||
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||||
|
return utcTimeMillis <= initialDate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val confirmEnabled = remember {
|
||||||
|
derivedStateOf { dateState.selectedDateMillis != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date Picker Dialog
|
||||||
|
AnimatedVisibility(showDatePicker) {
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showDatePicker = false
|
||||||
|
onDateChanged(dateState.selectedDateMillis ?: initialDate)
|
||||||
|
},
|
||||||
|
enabled = confirmEnabled.value,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(Res.string.feature_send_interbank_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDatePicker = false }) {
|
||||||
|
Text(text = stringResource(Res.string.feature_send_interbank_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
DatePicker(state = dateState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountDetailCard(
|
||||||
|
account: Account?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showVerified: Boolean = false,
|
||||||
|
onEditClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
MifosCard(
|
||||||
|
colors = CardDefaults.cardColors(KptTheme.colorScheme.background),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = account?.clientName ?: "",
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${account?.name ?: ""} - #${account?.number ?: ""}",
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${account?.accountType?.value ?: ""} | ${account?.currency?.name ?: ""}",
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_available_balance,
|
||||||
|
CurrencyFormatter.format(
|
||||||
|
account?.balance ?: 0.0,
|
||||||
|
account?.currency?.code ?: "",
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onEditClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_interbank_edit),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecipientDetailCard(
|
||||||
|
recipient: InterBankPartyInfoResponse?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onEditClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
MifosCard(
|
||||||
|
colors = CardDefaults.cardColors(KptTheme.colorScheme.background),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${recipient?.firstName ?: ""} ${recipient?.lastName ?: ""}".trim(),
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${recipient?.destinationFspId ?: ""} | ${recipient?.partyIdType ?: ""}",
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.feature_send_interbank_to_account_interbank,
|
||||||
|
recipient?.partyId ?: "",
|
||||||
|
),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
if (recipient?.executionStatus == true) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(top = KptTheme.spacing.xs),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "✓",
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_verified),
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = onEditClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_interbank_edit),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TransferDetailsScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
val mockFromAccount = Account(
|
||||||
|
name = "WALLET",
|
||||||
|
number = "00000002",
|
||||||
|
balance = 5000.0,
|
||||||
|
id = 101L,
|
||||||
|
productId = 202L,
|
||||||
|
clientName = "ALEJANDRO ESCUTIA",
|
||||||
|
accountType = org.mifospay.core.model.savingsaccount.AccountType(
|
||||||
|
id = 1L,
|
||||||
|
code = "savingsAccountType.savings",
|
||||||
|
value = "Savings Account",
|
||||||
|
),
|
||||||
|
currency = Currency(
|
||||||
|
code = "MXN",
|
||||||
|
name = "Mexican Peso",
|
||||||
|
displaySymbol = "MX$",
|
||||||
|
displayLabel = "Mexican Peso (MX$)",
|
||||||
|
decimalPlaces = 2,
|
||||||
|
inMultiplesOf = 10,
|
||||||
|
nameCode = "MXN",
|
||||||
|
),
|
||||||
|
status = Status(
|
||||||
|
id = 300,
|
||||||
|
code = "savingsAccountStatusType.active",
|
||||||
|
value = "Active",
|
||||||
|
submittedAndPendingApproval = false,
|
||||||
|
approved = true,
|
||||||
|
rejected = false,
|
||||||
|
withdrawnByApplicant = false,
|
||||||
|
active = true,
|
||||||
|
closed = false,
|
||||||
|
prematureClosed = false,
|
||||||
|
transferInProgress = false,
|
||||||
|
transferOnHold = false,
|
||||||
|
matured = false,
|
||||||
|
),
|
||||||
|
image = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val mockRecipient = InterBankPartyInfoResponse(
|
||||||
|
sourceFspId = "mifos-bank-1",
|
||||||
|
destinationFspId = "blackbank-test",
|
||||||
|
requestId = "req-001",
|
||||||
|
partyId = "9880000020",
|
||||||
|
currencyCode = "MXN",
|
||||||
|
firstName = "Pedro",
|
||||||
|
lastName = "Barreto",
|
||||||
|
systemMessage = "Success",
|
||||||
|
executionStatus = true,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
)
|
||||||
|
|
||||||
|
TransferDetailsScreen(
|
||||||
|
fromAccount = mockFromAccount,
|
||||||
|
recipient = mockRecipient,
|
||||||
|
amount = "100.00",
|
||||||
|
onAmountChanged = {},
|
||||||
|
date = "11/09/25",
|
||||||
|
onDateChanged = {},
|
||||||
|
description = "Dinner share",
|
||||||
|
onDescriptionChanged = {},
|
||||||
|
onContinueClick = {},
|
||||||
|
onBackClick = {},
|
||||||
|
initialDate = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TransferDetailsScreenEmptyPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
val mockFromAccount = Account(
|
||||||
|
name = "WALLET",
|
||||||
|
number = "00000002",
|
||||||
|
balance = 5000.0,
|
||||||
|
id = 101L,
|
||||||
|
productId = 202L,
|
||||||
|
clientName = "ALEJANDRO ESCUTIA",
|
||||||
|
accountType = org.mifospay.core.model.savingsaccount.AccountType(
|
||||||
|
id = 1L,
|
||||||
|
code = "savingsAccountType.savings",
|
||||||
|
value = "Savings Account",
|
||||||
|
),
|
||||||
|
currency = Currency(
|
||||||
|
code = "MXN",
|
||||||
|
name = "Mexican Peso",
|
||||||
|
displaySymbol = "MX$",
|
||||||
|
displayLabel = "Mexican Peso (MX$)",
|
||||||
|
decimalPlaces = 2,
|
||||||
|
inMultiplesOf = 10,
|
||||||
|
nameCode = "MXN",
|
||||||
|
),
|
||||||
|
status = Status(
|
||||||
|
id = 300,
|
||||||
|
code = "savingsAccountStatusType.active",
|
||||||
|
value = "Active",
|
||||||
|
submittedAndPendingApproval = false,
|
||||||
|
approved = true,
|
||||||
|
rejected = false,
|
||||||
|
withdrawnByApplicant = false,
|
||||||
|
active = true,
|
||||||
|
closed = false,
|
||||||
|
prematureClosed = false,
|
||||||
|
transferInProgress = false,
|
||||||
|
transferOnHold = false,
|
||||||
|
matured = false,
|
||||||
|
),
|
||||||
|
image = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val mockRecipient = InterBankPartyInfoResponse(
|
||||||
|
sourceFspId = "mifos-bank-1",
|
||||||
|
destinationFspId = "blackbank-test",
|
||||||
|
requestId = "req-001",
|
||||||
|
partyId = "9880000020",
|
||||||
|
currencyCode = "MXN",
|
||||||
|
firstName = "Pedro",
|
||||||
|
lastName = "Barreto",
|
||||||
|
systemMessage = "Success",
|
||||||
|
executionStatus = true,
|
||||||
|
partyIdType = "MSISDN",
|
||||||
|
)
|
||||||
|
|
||||||
|
TransferDetailsScreen(
|
||||||
|
fromAccount = mockFromAccount,
|
||||||
|
recipient = mockRecipient,
|
||||||
|
amount = "",
|
||||||
|
onAmountChanged = {},
|
||||||
|
date = "",
|
||||||
|
onDateChanged = {},
|
||||||
|
description = "",
|
||||||
|
onDescriptionChanged = {},
|
||||||
|
onContinueClick = {},
|
||||||
|
onBackClick = {},
|
||||||
|
initialDate = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,677 @@
|
|||||||
|
/*
|
||||||
|
* 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.interbank.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_amount_transferred
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_attempted_amount
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_available_balance_label
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_back_to_home
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_description
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_edit_transfer
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_failed
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_from_account_label
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_success
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_to_account_label
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transaction_date_label
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transaction_failed
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transaction_reference
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transfer_completed
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transfer_failed
|
||||||
|
import mobile_wallet.feature.send_interbank.generated.resources.feature_send_interbank_transfer_successful
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mifospay.core.designsystem.component.MifosButton
|
||||||
|
import org.mifospay.core.designsystem.component.MifosCard
|
||||||
|
import org.mifospay.core.designsystem.component.MifosScaffold
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TransferSuccessScreen(
|
||||||
|
recipientName: String,
|
||||||
|
amount: String,
|
||||||
|
transactionReference: String = "TXN-20250911-0001",
|
||||||
|
fromAccount: String = "WALLET - #0000000001",
|
||||||
|
fromAccountName: String = "TOMAS ASCENCIO ASCENCIO",
|
||||||
|
toAccount: String = "Pedro Barreto",
|
||||||
|
toAccountNumber: String = "Account: 9388006020",
|
||||||
|
transactionDate: String = "11/09/25 at 09:41 AM",
|
||||||
|
description: String = "Interbank Transfer",
|
||||||
|
currencyCode: String = "MXN",
|
||||||
|
onBackToHome: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
var copied by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
MifosButton(
|
||||||
|
onClick = onBackToHome,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_interbank_back_to_home))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = KptTheme.colorScheme.background,
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(KptTheme.spacing.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.lg),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
// Success Icon
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.background(
|
||||||
|
color = KptTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||||
|
shape = KptTheme.shapes.large,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Check,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_interbank_success),
|
||||||
|
modifier = Modifier.size(56.dp),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title and Subtitle
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transfer_successful),
|
||||||
|
style = KptTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transfer_completed, recipientName),
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction Reference
|
||||||
|
item {
|
||||||
|
MifosCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transaction_reference),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f).padding(end = KptTheme.spacing.xs),
|
||||||
|
text = transactionReference,
|
||||||
|
style = KptTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(transactionReference))
|
||||||
|
copied = true
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Copy,
|
||||||
|
contentDescription = "Copy transaction reference",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction Details
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
// Amount
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_amount_transferred),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "$currencyCode $amount",
|
||||||
|
style = KptTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From Account
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_from_account_label),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = fromAccount,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = fromAccountName,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To Account
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_to_account_label),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = toAccount,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = toAccountNumber,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transaction_date_label),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = transactionDate,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_description),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TransferFailedScreen(
|
||||||
|
errorMessage: String,
|
||||||
|
errorTitle: String = "Insufficient Balance",
|
||||||
|
attemptedAmount: String = "MXN 1.00",
|
||||||
|
availableBalance: String = "MXN 5,000.00",
|
||||||
|
fromAccount: String = "WALLET - #0000000001",
|
||||||
|
fromAccountName: String = "TOMAS ASCENCIO ASCENCIO",
|
||||||
|
toAccount: String = "Pedro Barreto",
|
||||||
|
toAccountNumber: String = "Account: 9388006020",
|
||||||
|
transactionDate: String = "11/09/25 at 09:41 AM",
|
||||||
|
description: String = "Interbank Transfer",
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onBackToHome: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
MifosButton(
|
||||||
|
onClick = onRetry,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_interbank_edit_transfer))
|
||||||
|
}
|
||||||
|
|
||||||
|
MifosButton(
|
||||||
|
onClick = onBackToHome,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.feature_send_interbank_back_to_home))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = KptTheme.colorScheme.background,
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(KptTheme.spacing.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.lg),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
// Error Icon
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.background(
|
||||||
|
color = KptTheme.colorScheme.error.copy(alpha = 0.1f),
|
||||||
|
shape = KptTheme.shapes.large,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Error,
|
||||||
|
contentDescription = stringResource(Res.string.feature_send_interbank_failed),
|
||||||
|
modifier = Modifier.size(56.dp),
|
||||||
|
tint = KptTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title and Subtitle
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transfer_failed),
|
||||||
|
style = KptTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transaction_failed),
|
||||||
|
style = KptTheme.typography.bodyMedium,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error Card
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.error.copy(alpha = 0.1f),
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = KptTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = errorTitle,
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
// Amount
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_attempted_amount),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = attemptedAmount,
|
||||||
|
style = KptTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = KptTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available Balance
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_available_balance_label),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = availableBalance,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From Account
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_from_account_label),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = fromAccount,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = fromAccountName,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To Account
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_to_account_label),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = toAccount,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = toAccountNumber,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_transaction_date_label),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = transactionDate,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_send_interbank_description),
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Transaction Details
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
// Attempted Amount
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Attempted Amount",
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = attemptedAmount,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction Date
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Transaction Date",
|
||||||
|
style = KptTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = transactionDate,
|
||||||
|
style = KptTheme.typography.bodySmall,
|
||||||
|
color = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TransferSuccessScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
TransferSuccessScreen(
|
||||||
|
recipientName = "Pedro Barreto",
|
||||||
|
amount = "1.00",
|
||||||
|
transactionReference = "TXN-20250911-0001TXN-20250911-0001TXN-20250911-0001",
|
||||||
|
fromAccount = "WALLET - #0000000001",
|
||||||
|
fromAccountName = "TOMAS ASCENCIO ASCENCIO",
|
||||||
|
toAccount = "Pedro Barreto",
|
||||||
|
toAccountNumber = "Account: 9388006020",
|
||||||
|
transactionDate = "11/09/25 at 09:41 AM",
|
||||||
|
currencyCode = "MXN",
|
||||||
|
onBackToHome = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TransferFailedScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
TransferFailedScreen(
|
||||||
|
errorMessage = "Your available balance is not enough to complete this transfer. Please update the amount and try again.",
|
||||||
|
errorTitle = "Insufficient Balance",
|
||||||
|
attemptedAmount = "MXN 1.00",
|
||||||
|
availableBalance = "MXN 5,000.00",
|
||||||
|
transactionDate = "11/09/25 at 09:41 AM",
|
||||||
|
onRetry = {},
|
||||||
|
onBackToHome = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -67,6 +67,7 @@ import mobile_wallet.feature.send_money.generated.resources.feature_send_money_s
|
|||||||
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_to_account
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_to_account
|
||||||
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_vpa_mobile_account_number
|
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_vpa_mobile_account_number
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.mifospay.core.common.utils.maskString
|
import org.mifospay.core.common.utils.maskString
|
||||||
import org.mifospay.core.designsystem.component.BasicDialogState.Shown
|
import org.mifospay.core.designsystem.component.BasicDialogState.Shown
|
||||||
@ -80,6 +81,7 @@ import org.mifospay.core.designsystem.component.MifosScaffold
|
|||||||
import org.mifospay.core.designsystem.component.MifosTextField
|
import org.mifospay.core.designsystem.component.MifosTextField
|
||||||
import org.mifospay.core.designsystem.component.MifosTopBar
|
import org.mifospay.core.designsystem.component.MifosTopBar
|
||||||
import org.mifospay.core.designsystem.icon.MifosIcons
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
import org.mifospay.core.designsystem.theme.toRoundedCornerShape
|
import org.mifospay.core.designsystem.theme.toRoundedCornerShape
|
||||||
import org.mifospay.core.model.search.AccountResult
|
import org.mifospay.core.model.search.AccountResult
|
||||||
import org.mifospay.core.ui.AvatarBox
|
import org.mifospay.core.ui.AvatarBox
|
||||||
@ -530,3 +532,148 @@ private fun SendMoneyDialogs(
|
|||||||
null -> Unit
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun SendMoneyScreenPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SendMoneyScreen(
|
||||||
|
state = SendMoneyState(
|
||||||
|
amount = "100",
|
||||||
|
accountNumber = "1234567890",
|
||||||
|
selectedAccount = null,
|
||||||
|
dialogState = null,
|
||||||
|
),
|
||||||
|
accountState = ViewState.Empty,
|
||||||
|
showTopBar = true,
|
||||||
|
onAction = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun SendMoneyScreenWithAccountsPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SendMoneyScreen(
|
||||||
|
state = SendMoneyState(
|
||||||
|
amount = "500",
|
||||||
|
accountNumber = "9876543210",
|
||||||
|
selectedAccount = AccountResult(
|
||||||
|
entityId = 1,
|
||||||
|
entityName = "Savings",
|
||||||
|
entityType = "SAVINGS",
|
||||||
|
parentName = "John Doe",
|
||||||
|
entityAccountNo = "1234567890",
|
||||||
|
entityExternalId = "1234567890",
|
||||||
|
parentId = 1,
|
||||||
|
subEntityType = "SAVINGS",
|
||||||
|
parentType = "SAVINGS",
|
||||||
|
),
|
||||||
|
dialogState = null,
|
||||||
|
),
|
||||||
|
accountState = ViewState.Content(
|
||||||
|
data = listOf(
|
||||||
|
AccountResult(
|
||||||
|
entityId = 1,
|
||||||
|
entityName = "Savings",
|
||||||
|
entityType = "SAVINGS",
|
||||||
|
parentName = "John Doe",
|
||||||
|
entityAccountNo = "1234567890",
|
||||||
|
entityExternalId = "1234567890",
|
||||||
|
parentId = 1,
|
||||||
|
subEntityType = "SAVINGS",
|
||||||
|
parentType = "SAVINGS",
|
||||||
|
),
|
||||||
|
AccountResult(
|
||||||
|
entityId = 2,
|
||||||
|
entityName = "Checking",
|
||||||
|
entityType = "CHECKING",
|
||||||
|
parentName = "Jane Smith",
|
||||||
|
entityAccountNo = "1234567890",
|
||||||
|
entityExternalId = "1234567890",
|
||||||
|
parentId = 1,
|
||||||
|
subEntityType = "SAVINGS",
|
||||||
|
parentType = "SAVINGS",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
showTopBar = true,
|
||||||
|
onAction = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun SendMoneyBottomBarPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SendMoneyBottomBar(
|
||||||
|
showDetails = true,
|
||||||
|
selectedAccount = AccountResult(
|
||||||
|
entityId = 1,
|
||||||
|
entityName = "Savings",
|
||||||
|
entityType = "SAVINGS",
|
||||||
|
parentName = "John Doe",
|
||||||
|
entityAccountNo = "1234567890",
|
||||||
|
entityExternalId = "1234567890",
|
||||||
|
parentId = 1,
|
||||||
|
subEntityType = "SAVINGS",
|
||||||
|
parentType = "SAVINGS",
|
||||||
|
),
|
||||||
|
onClickProceed = {},
|
||||||
|
onDeselect = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun SelectedAccountCardPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
SelectedAccountCard(
|
||||||
|
account = AccountResult(
|
||||||
|
entityId = 1,
|
||||||
|
entityName = "Savings",
|
||||||
|
entityType = "SAVINGS",
|
||||||
|
parentName = "John Doe",
|
||||||
|
entityAccountNo = "1234567890",
|
||||||
|
entityExternalId = "1234567890",
|
||||||
|
parentId = 1,
|
||||||
|
subEntityType = "SAVINGS",
|
||||||
|
parentType = "SAVINGS",
|
||||||
|
),
|
||||||
|
onDeselect = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AccountCardPreview() {
|
||||||
|
MifosTheme {
|
||||||
|
AccountCard(
|
||||||
|
account = AccountResult(
|
||||||
|
entityId = 1,
|
||||||
|
entityName = "Savings",
|
||||||
|
entityType = "SAVINGS",
|
||||||
|
parentName = "John Doe",
|
||||||
|
entityAccountNo = "1234567890",
|
||||||
|
entityExternalId = "1234567890",
|
||||||
|
parentId = 1,
|
||||||
|
subEntityType = "SAVINGS",
|
||||||
|
parentType = "SAVINGS",
|
||||||
|
),
|
||||||
|
selected = { true },
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AccountBadgePreview() {
|
||||||
|
MifosTheme {
|
||||||
|
AccountBadge(text = "SAVINGS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -67,6 +67,7 @@ include(":feature:faq")
|
|||||||
include(":feature:auth")
|
include(":feature:auth")
|
||||||
include(":feature:make-transfer")
|
include(":feature:make-transfer")
|
||||||
include(":feature:send-money")
|
include(":feature:send-money")
|
||||||
|
include(":feature:send-interbank")
|
||||||
include(":feature:notification")
|
include(":feature:notification")
|
||||||
include(":feature:editpassword")
|
include(":feature:editpassword")
|
||||||
include(":feature:kyc")
|
include(":feature:kyc")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user