mirror of
https://github.com/openMF/mobile-wallet.git
synced 2026-02-06 11:56:48 +00:00
feat(beneficiary): Add beneficiary list screen (#1934)
This commit is contained in:
parent
ebb59713c2
commit
9a127fd724
@ -97,7 +97,7 @@ fun Project.configureBadgingTasks(
|
|||||||
// Registers a callback to be called, when a new variant is configured
|
// Registers a callback to be called, when a new variant is configured
|
||||||
componentsExtension.onVariants { variant ->
|
componentsExtension.onVariants { variant ->
|
||||||
// Registers a new task to verify the app bundle.
|
// Registers a new task to verify the app bundle.
|
||||||
val capitalizedVariantName = variant.name.capitalized()
|
val capitalizedVariantName = variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
|
||||||
val generateBadgingTaskName = "generate${capitalizedVariantName}Badging"
|
val generateBadgingTaskName = "generate${capitalizedVariantName}Badging"
|
||||||
val generateBadging =
|
val generateBadging =
|
||||||
tasks.register<GenerateBadgingTask>(generateBadgingTaskName) {
|
tasks.register<GenerateBadgingTask>(generateBadgingTaskName) {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import org.mifospay.feature.accounts.AccountsScreen
|
|||||||
import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType
|
import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType
|
||||||
import org.mifospay.feature.accounts.beneficiary.addEditBeneficiaryScreen
|
import org.mifospay.feature.accounts.beneficiary.addEditBeneficiaryScreen
|
||||||
import org.mifospay.feature.accounts.beneficiary.navigateToBeneficiaryAddEdit
|
import org.mifospay.feature.accounts.beneficiary.navigateToBeneficiaryAddEdit
|
||||||
|
import org.mifospay.feature.accounts.benficiaryList.BeneficiaryListScreen
|
||||||
import org.mifospay.feature.accounts.savingsaccount.SavingsAddEditType
|
import org.mifospay.feature.accounts.savingsaccount.SavingsAddEditType
|
||||||
import org.mifospay.feature.accounts.savingsaccount.addEditSavingAccountScreen
|
import org.mifospay.feature.accounts.savingsaccount.addEditSavingAccountScreen
|
||||||
import org.mifospay.feature.accounts.savingsaccount.details.navigateToSavingAccountDetails
|
import org.mifospay.feature.accounts.savingsaccount.details.navigateToSavingAccountDetails
|
||||||
@ -137,6 +138,12 @@ internal fun MifosNavHost(
|
|||||||
onAddOrEditBeneficiary = navController::navigateToBeneficiaryAddEdit,
|
onAddOrEditBeneficiary = navController::navigateToBeneficiaryAddEdit,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
TabContent(FinanceScreenContents.BENEFICIARIES.name) {
|
||||||
|
BeneficiaryListScreen(
|
||||||
|
onAddOrEditBeneficiary = navController::navigateToBeneficiaryAddEdit,
|
||||||
|
)
|
||||||
|
},
|
||||||
// TabContent(FinanceScreenContents.CARDS.name) {
|
// TabContent(FinanceScreenContents.CARDS.name) {
|
||||||
// CardsScreen(
|
// CardsScreen(
|
||||||
// navigateToViewDetail = navController::navigateToCardDetails,
|
// navigateToViewDetail = navController::navigateToCardDetails,
|
||||||
@ -218,6 +225,7 @@ internal fun MifosNavHost(
|
|||||||
|
|
||||||
addEditBeneficiaryScreen(
|
addEditBeneficiaryScreen(
|
||||||
navigateBack = navController::navigateUp,
|
navigateBack = navController::navigateUp,
|
||||||
|
navigateToQrReaderScreen = navController::navigateToScanQr,
|
||||||
)
|
)
|
||||||
|
|
||||||
savingAccountDetailRoute(
|
savingAccountDetailRoute(
|
||||||
@ -376,6 +384,17 @@ internal fun MifosNavHost(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
navigateToAddBeneficiaryScreen = {
|
||||||
|
navController.navigateToBeneficiaryAddEdit(
|
||||||
|
BeneficiaryAddEditType.EditItem(it),
|
||||||
|
navOptions = navOptions {
|
||||||
|
popUpTo(SCAN_QR_ROUTE) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
merchantTransferScreen(
|
merchantTransferScreen(
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
|||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
|
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.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
@ -140,4 +141,5 @@ object MifosIcons {
|
|||||||
val History = Icons.Default.History
|
val History = Icons.Default.History
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import org.mifospay.core.common.Parcelize
|
|||||||
@Serializable
|
@Serializable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Beneficiary(
|
data class Beneficiary(
|
||||||
val id: Long,
|
val id: Long = 0L,
|
||||||
val name: String,
|
val name: String = "",
|
||||||
val officeName: String,
|
val officeName: String,
|
||||||
val clientName: String,
|
val clientName: String,
|
||||||
val accountType: AccountType,
|
val accountType: AccountType,
|
||||||
|
|||||||
@ -160,4 +160,8 @@
|
|||||||
<string name="feature_accounts_saving_button_update">Update</string>
|
<string name="feature_accounts_saving_button_update">Update</string>
|
||||||
<string name="feature_accounts_saving_title_create">Create Saving Account</string>
|
<string name="feature_accounts_saving_title_create">Create Saving Account</string>
|
||||||
<string name="feature_accounts_saving_title_update">Update Saving Account</string>
|
<string name="feature_accounts_saving_title_update">Update Saving Account</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="skip_the_form">Skip the form </string>
|
||||||
|
<string name="scan_qr_code">Scan QR Code</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -9,10 +9,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.mifospay.feature.accounts.beneficiary
|
package org.mifospay.feature.accounts.beneficiary
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@ -52,6 +56,8 @@ import mobile_wallet.feature.accounts.generated.resources.feature_accounts_benef
|
|||||||
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_beneficiary_nickname
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_beneficiary_nickname
|
||||||
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_beneficiary_office_name
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_beneficiary_office_name
|
||||||
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_beneficiary_transfer_limit
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_beneficiary_transfer_limit
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.scan_qr_code
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.skip_the_form
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.mifospay.core.designsystem.component.BasicDialogState
|
import org.mifospay.core.designsystem.component.BasicDialogState
|
||||||
@ -70,6 +76,7 @@ import template.core.base.designsystem.theme.KptTheme
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun AddEditBeneficiaryScreen(
|
internal fun AddEditBeneficiaryScreen(
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
|
navigateToQrReaderScreen: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: AddEditBeneficiaryViewModel = koinViewModel(),
|
viewModel: AddEditBeneficiaryViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
@ -82,6 +89,9 @@ internal fun AddEditBeneficiaryScreen(
|
|||||||
EventsEffect(viewModel) { event ->
|
EventsEffect(viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is AEBEvent.NavigateBack -> navigateBack.invoke()
|
is AEBEvent.NavigateBack -> navigateBack.invoke()
|
||||||
|
|
||||||
|
is AEBEvent.NavigateToQr -> navigateToQrReaderScreen.invoke()
|
||||||
|
|
||||||
is AEBEvent.ShowToast -> {
|
is AEBEvent.ShowToast -> {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(event.message)
|
snackbarHostState.showSnackbar(event.message)
|
||||||
@ -280,6 +290,22 @@ internal fun AddEditBeneficiaryScreenContent(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(KptTheme.spacing.md))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.skip_the_form),
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.scan_qr_code),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { onAction(AEBAction.OnQrScanClicked) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,6 +148,8 @@ internal class AddEditBeneficiaryViewModel(
|
|||||||
|
|
||||||
AEBAction.SaveBeneficiary -> initiateSaveBeneficiary()
|
AEBAction.SaveBeneficiary -> initiateSaveBeneficiary()
|
||||||
|
|
||||||
|
AEBAction.OnQrScanClicked -> sendEvent(AEBEvent.NavigateToQr)
|
||||||
|
|
||||||
is HandleBeneficiaryAddEditResult -> handleBeneficiaryAddEditResult(action)
|
is HandleBeneficiaryAddEditResult -> handleBeneficiaryAddEditResult(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,6 +313,7 @@ internal data class AEBState(
|
|||||||
|
|
||||||
internal sealed interface AEBEvent {
|
internal sealed interface AEBEvent {
|
||||||
data object NavigateBack : AEBEvent
|
data object NavigateBack : AEBEvent
|
||||||
|
data object NavigateToQr : AEBEvent
|
||||||
data class ShowToast(val message: String) : AEBEvent
|
data class ShowToast(val message: String) : AEBEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,6 +324,7 @@ internal sealed interface AEBAction {
|
|||||||
data class ChangeAccountNumber(val accountNumber: String) : AEBAction
|
data class ChangeAccountNumber(val accountNumber: String) : AEBAction
|
||||||
data class ChangeAccountType(val accountType: Int) : AEBAction
|
data class ChangeAccountType(val accountType: Int) : AEBAction
|
||||||
data class ChangeTransferLimit(val transferLimit: String) : AEBAction
|
data class ChangeTransferLimit(val transferLimit: String) : AEBAction
|
||||||
|
data object OnQrScanClicked : AEBAction
|
||||||
|
|
||||||
data object DismissDialog : AEBAction
|
data object DismissDialog : AEBAction
|
||||||
data object NavigateBack : AEBAction
|
data object NavigateBack : AEBAction
|
||||||
|
|||||||
@ -43,6 +43,7 @@ data class BeneficiaryAddEditArgs(
|
|||||||
|
|
||||||
fun NavGraphBuilder.addEditBeneficiaryScreen(
|
fun NavGraphBuilder.addEditBeneficiaryScreen(
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
|
navigateToQrReaderScreen: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
route = ADD_EDIT_ITEM_ROUTE,
|
route = ADD_EDIT_ITEM_ROUTE,
|
||||||
@ -52,6 +53,7 @@ fun NavGraphBuilder.addEditBeneficiaryScreen(
|
|||||||
) {
|
) {
|
||||||
AddEditBeneficiaryScreen(
|
AddEditBeneficiaryScreen(
|
||||||
navigateBack = navigateBack,
|
navigateBack = navigateBack,
|
||||||
|
navigateToQrReaderScreen = navigateToQrReaderScreen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* 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.accounts.benficiaryList
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_add
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_delete_beneficiary
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_edit_beneficiary
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_error_oops
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_unexpected_error_subtitle
|
||||||
|
import org.jetbrains.compose.resources.getString
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.mifospay.core.designsystem.component.BasicDialogState
|
||||||
|
import org.mifospay.core.designsystem.component.MifosBasicDialog
|
||||||
|
import org.mifospay.core.designsystem.component.MifosScaffold
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
|
import org.mifospay.core.designsystem.theme.MifosTheme
|
||||||
|
import org.mifospay.core.model.beneficiary.Beneficiary
|
||||||
|
import org.mifospay.core.ui.AvatarBox
|
||||||
|
import org.mifospay.core.ui.EmptyContentScreen
|
||||||
|
import org.mifospay.core.ui.MifosProgressIndicator
|
||||||
|
import org.mifospay.core.ui.utils.EventsEffect
|
||||||
|
import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType
|
||||||
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BeneficiaryListScreen(
|
||||||
|
onAddOrEditBeneficiary: (BeneficiaryAddEditType) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: BeneficiaryListViewModel = koinViewModel(),
|
||||||
|
) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
val accountState by viewModel.accountState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
EventsEffect(viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
is BeneficiaryListEvent.OnAddOrEditTPTBeneficiary -> onAddOrEditBeneficiary.invoke(event.type)
|
||||||
|
|
||||||
|
is BeneficiaryListEvent.ShowToast -> {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(getString(event.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BeneficiaryListDialog(
|
||||||
|
dialogState = state.dialogState,
|
||||||
|
onAction = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(BeneficiaryListAction.DismissDialog) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BeneficiaryListScreenContent(
|
||||||
|
state = accountState,
|
||||||
|
onAction = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(it) }
|
||||||
|
},
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BeneficiaryListDialog(
|
||||||
|
dialogState: BeneficiaryListState.DialogState?,
|
||||||
|
onAction: (BeneficiaryListAction) -> Unit,
|
||||||
|
) {
|
||||||
|
when (dialogState) {
|
||||||
|
is BeneficiaryListState.DialogState.DeleteBeneficiary -> MifosBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = stringResource(dialogState.title),
|
||||||
|
message = stringResource(dialogState.message),
|
||||||
|
),
|
||||||
|
onConfirm = dialogState.onConfirm,
|
||||||
|
onDismissRequest = { onAction(BeneficiaryListAction.DismissDialog) },
|
||||||
|
)
|
||||||
|
|
||||||
|
is BeneficiaryListState.DialogState.Error -> MifosBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
message = dialogState.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = { onAction(BeneficiaryListAction.DismissDialog) },
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BeneficiaryListScreenContent(
|
||||||
|
state: BeneficiaryListState.ViewState,
|
||||||
|
onAction: (BeneficiaryListAction) -> Unit,
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MifosScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
floatingActionButtonPosition = FabPosition.EndOverlay,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
floatingActionButton = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.hasFab,
|
||||||
|
enter = scaleIn(),
|
||||||
|
exit = scaleOut(),
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
onAction(BeneficiaryListAction.AddTPTBeneficiary)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Add,
|
||||||
|
stringResource(Res.string.feature_accounts_add),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
BeneficiariesList(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(it),
|
||||||
|
state = state,
|
||||||
|
onAction = onAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BeneficiariesList(
|
||||||
|
state: BeneficiaryListState.ViewState,
|
||||||
|
onAction: (BeneficiaryListAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = KptTheme.spacing.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is BeneficiaryListState.ViewState.Loading -> MifosProgressIndicator()
|
||||||
|
|
||||||
|
is BeneficiaryListState.ViewState.Content -> {
|
||||||
|
LazyColumn {
|
||||||
|
items(
|
||||||
|
items = state.beneficiaries,
|
||||||
|
key = { it.id },
|
||||||
|
) { beneficiary ->
|
||||||
|
BeneficiaryItem(
|
||||||
|
beneficiary = beneficiary,
|
||||||
|
onClickEdit = { onAction(BeneficiaryListAction.EditBeneficiary(it)) },
|
||||||
|
onClickDelete = { onAction(BeneficiaryListAction.DeleteBeneficiary(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BeneficiaryListState.ViewState.Error -> {
|
||||||
|
EmptyContentScreen(
|
||||||
|
title = stringResource(Res.string.feature_accounts_error_oops),
|
||||||
|
subTitle = stringResource(Res.string.feature_accounts_unexpected_error_subtitle),
|
||||||
|
modifier = Modifier,
|
||||||
|
iconTint = KptTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BeneficiaryItem(
|
||||||
|
beneficiary: Beneficiary,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClickEdit: (Beneficiary) -> Unit,
|
||||||
|
onClickDelete: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
shape = KptTheme.shapes.small,
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = KptTheme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = beneficiary.name)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = beneficiary.accountNumber)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AvatarBox(
|
||||||
|
icon = MifosIcons.AccountCircle,
|
||||||
|
backgroundColor = KptTheme.colorScheme.tertiaryContainer,
|
||||||
|
contentColor = KptTheme.colorScheme.tertiary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = {
|
||||||
|
onClickEdit(beneficiary)
|
||||||
|
},
|
||||||
|
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||||
|
containerColor = KptTheme.colorScheme.surfaceContainerHighest,
|
||||||
|
contentColor = KptTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.Edit2,
|
||||||
|
contentDescription = stringResource(Res.string.feature_accounts_edit_beneficiary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = {
|
||||||
|
onClickDelete(beneficiary.id)
|
||||||
|
},
|
||||||
|
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||||
|
containerColor = KptTheme.colorScheme.errorContainer,
|
||||||
|
contentColor = KptTheme.colorScheme.error,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MifosIcons.OutlinedDelete,
|
||||||
|
contentDescription = stringResource(Res.string.feature_accounts_delete_beneficiary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewBeneficiaryListScreen() {
|
||||||
|
MifosTheme {
|
||||||
|
BeneficiaryListScreenContent(
|
||||||
|
state = BeneficiaryListState.ViewState.Loading,
|
||||||
|
onAction = { },
|
||||||
|
modifier = Modifier,
|
||||||
|
snackbarHostState = remember { SnackbarHostState() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* 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.accounts.benficiaryList
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.delete_beneficiary_subtitle
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.delete_beneficiary_title
|
||||||
|
import mobile_wallet.feature.accounts.generated.resources.feature_accounts_beneficiary_deleted
|
||||||
|
import org.jetbrains.compose.resources.StringResource
|
||||||
|
import org.mifospay.core.common.DataState
|
||||||
|
import org.mifospay.core.data.repository.SelfServiceRepository
|
||||||
|
import org.mifospay.core.datastore.UserPreferencesRepository
|
||||||
|
import org.mifospay.core.model.beneficiary.Beneficiary
|
||||||
|
import org.mifospay.core.ui.utils.BaseViewModel
|
||||||
|
import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class BeneficiaryListViewModel(
|
||||||
|
private val userRepository: UserPreferencesRepository,
|
||||||
|
private val repository: SelfServiceRepository,
|
||||||
|
private val json: Json,
|
||||||
|
) : BaseViewModel<BeneficiaryListState, BeneficiaryListEvent, BeneficiaryListAction>(
|
||||||
|
initialState = run {
|
||||||
|
val clientId = requireNotNull(userRepository.clientId.value)
|
||||||
|
val defaultAccount = userRepository.defaultAccountId.value
|
||||||
|
|
||||||
|
BeneficiaryListState(
|
||||||
|
clientId = clientId,
|
||||||
|
defaultAccountId = defaultAccount,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val accountState = mutableStateFlow
|
||||||
|
.flatMapLatest {
|
||||||
|
repository.getBeneficiaryList()
|
||||||
|
}
|
||||||
|
.mapLatest {
|
||||||
|
when (it) {
|
||||||
|
is DataState.Loading -> BeneficiaryListState.ViewState.Loading
|
||||||
|
is DataState.Error -> BeneficiaryListState.ViewState.Error(it.exception.message.toString())
|
||||||
|
is DataState.Success -> {
|
||||||
|
BeneficiaryListState.ViewState.Content(
|
||||||
|
beneficiaries = it.data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
|
initialValue = BeneficiaryListState.ViewState.Loading,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun handleAction(action: BeneficiaryListAction) {
|
||||||
|
when (action) {
|
||||||
|
is BeneficiaryListAction.AddTPTBeneficiary -> {
|
||||||
|
sendEvent(BeneficiaryListEvent.OnAddOrEditTPTBeneficiary(BeneficiaryAddEditType.AddItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
is BeneficiaryListAction.EditBeneficiary -> {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val beneficiary = json.encodeToString<Beneficiary>(action.beneficiary)
|
||||||
|
sendEvent(
|
||||||
|
BeneficiaryListEvent.OnAddOrEditTPTBeneficiary(
|
||||||
|
BeneficiaryAddEditType.EditItem(beneficiary),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BeneficiaryListAction.DeleteBeneficiary -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = BeneficiaryListState.DialogState.DeleteBeneficiary(
|
||||||
|
title = Res.string.delete_beneficiary_title,
|
||||||
|
message = Res.string.delete_beneficiary_subtitle,
|
||||||
|
onConfirm = {
|
||||||
|
trySendAction(BeneficiaryListAction.DeleteBeneficiary(action.beneficiaryId))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BeneficiaryListAction.DismissDialog -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BeneficiaryListAction.Internal.BeneficiaryDeleteResultReceived ->
|
||||||
|
handleBeneficiaryDeleteResult(action)
|
||||||
|
|
||||||
|
is BeneficiaryListAction.Internal.DeleteBeneficiary -> handleDeleteBeneficiary(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeleteBeneficiary(action: BeneficiaryListAction.Internal.DeleteBeneficiary) {
|
||||||
|
mutableStateFlow.update { it.copy(dialogState = BeneficiaryListState.DialogState.Loading) }
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = repository.deleteBeneficiary(action.beneficiaryId)
|
||||||
|
|
||||||
|
sendAction(BeneficiaryListAction.Internal.BeneficiaryDeleteResultReceived(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBeneficiaryDeleteResult(action: BeneficiaryListAction.Internal.BeneficiaryDeleteResultReceived) {
|
||||||
|
when (action.result) {
|
||||||
|
is DataState.Success -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(BeneficiaryListEvent.ShowToast(Res.string.feature_accounts_beneficiary_deleted))
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataState.Error -> {
|
||||||
|
val message = action.result.exception.message.toString()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = BeneficiaryListState.DialogState.Error(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DataState.Loading -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = BeneficiaryListState.DialogState.Loading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BeneficiaryListState(
|
||||||
|
val clientId: Long,
|
||||||
|
val defaultAccountId: Long? = null,
|
||||||
|
val dialogState: DialogState? = null,
|
||||||
|
) {
|
||||||
|
sealed interface DialogState {
|
||||||
|
data object Loading : DialogState
|
||||||
|
data class Error(val message: String) : DialogState
|
||||||
|
data class DeleteBeneficiary(
|
||||||
|
val title: StringResource,
|
||||||
|
val message: StringResource,
|
||||||
|
val onConfirm: () -> Unit,
|
||||||
|
) : DialogState
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ViewState {
|
||||||
|
val hasFab: Boolean
|
||||||
|
|
||||||
|
val isPullToRefreshEnabled: Boolean
|
||||||
|
|
||||||
|
data object Loading : ViewState {
|
||||||
|
override val hasFab: Boolean get() = false
|
||||||
|
override val isPullToRefreshEnabled: Boolean get() = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Error(val message: String) : ViewState {
|
||||||
|
override val hasFab: Boolean get() = false
|
||||||
|
override val isPullToRefreshEnabled: Boolean get() = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Content(
|
||||||
|
val beneficiaries: List<Beneficiary>,
|
||||||
|
) : ViewState {
|
||||||
|
override val hasFab: Boolean get() = true
|
||||||
|
override val isPullToRefreshEnabled: Boolean get() = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface BeneficiaryListEvent {
|
||||||
|
data class OnAddOrEditTPTBeneficiary(val type: BeneficiaryAddEditType) : BeneficiaryListEvent
|
||||||
|
|
||||||
|
data class ShowToast(val message: StringResource) : BeneficiaryListEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface BeneficiaryListAction {
|
||||||
|
data object AddTPTBeneficiary : BeneficiaryListAction
|
||||||
|
data class EditBeneficiary(val beneficiary: Beneficiary) : BeneficiaryListAction
|
||||||
|
data class DeleteBeneficiary(val beneficiaryId: Long) : BeneficiaryListAction
|
||||||
|
|
||||||
|
data object DismissDialog : BeneficiaryListAction
|
||||||
|
|
||||||
|
sealed interface Internal : BeneficiaryListAction {
|
||||||
|
data class DeleteBeneficiary(val beneficiaryId: Long) : Internal
|
||||||
|
data class BeneficiaryDeleteResultReceived(val result: DataState<String>) : Internal
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import org.koin.core.module.dsl.viewModelOf
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.mifospay.feature.accounts.AccountViewModel
|
import org.mifospay.feature.accounts.AccountViewModel
|
||||||
import org.mifospay.feature.accounts.beneficiary.AddEditBeneficiaryViewModel
|
import org.mifospay.feature.accounts.beneficiary.AddEditBeneficiaryViewModel
|
||||||
|
import org.mifospay.feature.accounts.benficiaryList.BeneficiaryListViewModel
|
||||||
import org.mifospay.feature.accounts.savingsaccount.AddEditSavingViewModel
|
import org.mifospay.feature.accounts.savingsaccount.AddEditSavingViewModel
|
||||||
import org.mifospay.feature.accounts.savingsaccount.details.SavingAccountDetailViewModel
|
import org.mifospay.feature.accounts.savingsaccount.details.SavingAccountDetailViewModel
|
||||||
|
|
||||||
@ -23,4 +24,5 @@ val AccountsModule = module {
|
|||||||
viewModelOf(::AddEditBeneficiaryViewModel)
|
viewModelOf(::AddEditBeneficiaryViewModel)
|
||||||
viewModelOf(::SavingAccountDetailViewModel)
|
viewModelOf(::SavingAccountDetailViewModel)
|
||||||
viewModelOf(::AddEditSavingViewModel)
|
viewModelOf(::AddEditSavingViewModel)
|
||||||
|
viewModelOf(::BeneficiaryListViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ internal fun FinanceRoute(
|
|||||||
// https://venus.mifos.community/fineract-provider/api/v1/datatables/kyc_level1_details/2
|
// https://venus.mifos.community/fineract-provider/api/v1/datatables/kyc_level1_details/2
|
||||||
enum class FinanceScreenContents {
|
enum class FinanceScreenContents {
|
||||||
ACCOUNTS,
|
ACCOUNTS,
|
||||||
|
BENEFICIARIES,
|
||||||
// CARDS,
|
// CARDS,
|
||||||
// MERCHANTS,
|
// MERCHANTS,
|
||||||
// KYC,
|
// KYC,
|
||||||
|
|||||||
@ -17,5 +17,7 @@
|
|||||||
<string name="feature_qr_loading">Loading</string>
|
<string name="feature_qr_loading">Loading</string>
|
||||||
<string name="feature_qr_oops">Oops</string>
|
<string name="feature_qr_oops">Oops</string>
|
||||||
<string name="feature_qr_unexpected_error_subtitle">An unexpected error occurred. Please try again.</string>
|
<string name="feature_qr_unexpected_error_subtitle">An unexpected error occurred. Please try again.</string>
|
||||||
|
<string name="feature_qr_instruction">Please, align QR Code within the frame to make scanning easily detectable.</string>
|
||||||
|
<string name="feature_qr_warning_title">Potential Warning</string>
|
||||||
|
<string name="feature_qr_warning_message">If you scan the QR code, it could take you to a phishing website that steals your personal information, like credit card numbers or usernames and passwords. It could also download malware onto your phone and give hackers access to your device.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -9,15 +9,38 @@
|
|||||||
*/
|
*/
|
||||||
package org.mifospay.feature.qr
|
package org.mifospay.feature.qr
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mobile_wallet.feature.qr.generated.resources.Res
|
||||||
|
import mobile_wallet.feature.qr.generated.resources.feature_qr_instruction
|
||||||
|
import mobile_wallet.feature.qr.generated.resources.feature_qr_warning_message
|
||||||
|
import mobile_wallet.feature.qr.generated.resources.feature_qr_warning_title
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.mifospay.core.designsystem.component.MifosCard
|
||||||
|
import org.mifospay.core.designsystem.icon.MifosIcons
|
||||||
import template.core.base.designsystem.theme.KptTheme
|
import template.core.base.designsystem.theme.KptTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -35,6 +58,35 @@ fun QrScannerWithPermissions(
|
|||||||
openSettingsLabel: String = "Open Settings",
|
openSettingsLabel: String = "Open Settings",
|
||||||
onScanned: (String) -> Boolean,
|
onScanned: (String) -> Boolean,
|
||||||
) {
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(KptTheme.spacing.lg)
|
||||||
|
.padding(top = KptTheme.spacing.lg),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 56.dp + 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_qr_instruction),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = Color.Black,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(KptTheme.spacing.md))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(KptTheme.shapes.medium)
|
||||||
|
.drawQrCorners()
|
||||||
|
.border(1.dp, Color.Transparent, KptTheme.shapes.medium),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
QrScannerWithPermissions(
|
QrScannerWithPermissions(
|
||||||
types = types,
|
types = types,
|
||||||
modifier = modifier.clipToBounds(),
|
modifier = modifier.clipToBounds(),
|
||||||
@ -56,6 +108,44 @@ fun QrScannerWithPermissions(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MifosCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KptTheme.spacing.sm),
|
||||||
|
shape = KptTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(KptTheme.spacing.lg),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = MifosIcons.Warning,
|
||||||
|
contentDescription = "Warning",
|
||||||
|
tint = Color.Unspecified,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_qr_warning_title),
|
||||||
|
color = Color.Black,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.feature_qr_warning_message),
|
||||||
|
color = Color.Black,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -79,3 +169,72 @@ fun QrScannerWithPermissions(
|
|||||||
permissionDeniedContent(permissionState)
|
permissionDeniedContent(permissionState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Modifier.drawQrCorners(): Modifier = drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
|
||||||
|
val strokeWidth = 5.dp.toPx()
|
||||||
|
val lineLength = 40.dp.toPx()
|
||||||
|
|
||||||
|
val horizontalPadding = 50.dp.toPx() // for left & right
|
||||||
|
val verticalPaddingTop = 50.dp.toPx() // for top corners
|
||||||
|
val verticalPaddingBottom = 300.dp.toPx() // more padding for bottom corners
|
||||||
|
|
||||||
|
val color = Color.White
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(horizontalPadding, verticalPaddingTop),
|
||||||
|
Offset(horizontalPadding + lineLength, verticalPaddingTop),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(horizontalPadding, verticalPaddingTop),
|
||||||
|
Offset(horizontalPadding, verticalPaddingTop + lineLength),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Top-right
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(size.width - horizontalPadding, verticalPaddingTop),
|
||||||
|
Offset(size.width - horizontalPadding - lineLength, verticalPaddingTop),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(size.width - horizontalPadding, verticalPaddingTop),
|
||||||
|
Offset(size.width - horizontalPadding, verticalPaddingTop + lineLength),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bottom-left
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(horizontalPadding, size.height - verticalPaddingBottom),
|
||||||
|
Offset(horizontalPadding + lineLength, size.height - verticalPaddingBottom),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(horizontalPadding, size.height - verticalPaddingBottom),
|
||||||
|
Offset(horizontalPadding, size.height - verticalPaddingBottom - lineLength),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bottom-right
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(size.width - horizontalPadding, size.height - verticalPaddingBottom),
|
||||||
|
Offset(size.width - horizontalPadding - lineLength, size.height - verticalPaddingBottom),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color,
|
||||||
|
Offset(size.width - horizontalPadding, size.height - verticalPaddingBottom),
|
||||||
|
Offset(size.width - horizontalPadding, size.height - verticalPaddingBottom - lineLength),
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import org.mifospay.core.designsystem.component.MifosScaffold
|
|||||||
internal fun ScanQrCodeScreen(
|
internal fun ScanQrCodeScreen(
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
navigateToSendScreen: (String) -> Unit,
|
navigateToSendScreen: (String) -> Unit,
|
||||||
|
navigateToAddBeneficiaryScreen: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: ScanQrViewModel = koinViewModel(),
|
viewModel: ScanQrViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
@ -50,6 +51,10 @@ internal fun ScanQrCodeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ScanQrEvent.OnNavigateToAddBeneficiary -> navigateToAddBeneficiaryScreen.invoke(
|
||||||
|
(eventFlow as ScanQrEvent.OnNavigateToAddBeneficiary).beneficiary,
|
||||||
|
)
|
||||||
|
|
||||||
null -> Unit
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,9 @@ import androidx.lifecycle.ViewModel
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.mifospay.core.data.util.UpiQrCodeProcessor
|
import org.mifospay.core.data.util.UpiQrCodeProcessor
|
||||||
|
import org.mifospay.core.model.beneficiary.Beneficiary
|
||||||
|
|
||||||
class ScanQrViewModel : ViewModel() {
|
class ScanQrViewModel : ViewModel() {
|
||||||
|
|
||||||
@ -30,15 +32,54 @@ class ScanQrViewModel : ViewModel() {
|
|||||||
|
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
getQrCodeResult(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getQrCodeResult(data: String): Boolean {
|
||||||
|
val trimmedData = data.trim()
|
||||||
|
println("scanned $data")
|
||||||
|
|
||||||
|
if (!trimmedData.startsWith("{") || !trimmedData.endsWith("}")) {
|
||||||
_eventFlow.update {
|
_eventFlow.update {
|
||||||
ScanQrEvent.ShowToast("Scan a Valid Payment QR Code")
|
ScanQrEvent.ShowToast("Scan a Valid QR Code")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val beneficiary = parseBeneficiaryFromJson(trimmedData)
|
||||||
|
if (beneficiary != null) {
|
||||||
|
val beneficiaryString = Json.encodeToString<Beneficiary>(beneficiary)
|
||||||
|
_eventFlow.update {
|
||||||
|
ScanQrEvent.OnNavigateToAddBeneficiary(beneficiaryString)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
_eventFlow.update {
|
||||||
|
ScanQrEvent.ShowToast("Scan a Valid QR Code")
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
_eventFlow.update {
|
||||||
|
ScanQrEvent.ShowToast("Scan a Valid QR Code")
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseBeneficiaryFromJson(jsonString: String): Beneficiary? {
|
||||||
|
return try {
|
||||||
|
Json.decodeFromString<Beneficiary>(jsonString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ScanQrEvent {
|
sealed interface ScanQrEvent {
|
||||||
data class OnNavigateToSendScreen(val data: String) : ScanQrEvent
|
data class OnNavigateToSendScreen(val data: String) : ScanQrEvent
|
||||||
|
data class OnNavigateToAddBeneficiary(val beneficiary: String) : ScanQrEvent
|
||||||
data class ShowToast(val message: String) : ScanQrEvent
|
data class ShowToast(val message: String) : ScanQrEvent
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,11 +23,13 @@ fun NavController.navigateToScanQr(navOptions: NavOptions? = null) =
|
|||||||
fun NavGraphBuilder.scanQrScreen(
|
fun NavGraphBuilder.scanQrScreen(
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
navigateToSendScreen: (String) -> Unit,
|
navigateToSendScreen: (String) -> Unit,
|
||||||
|
navigateToAddBeneficiaryScreen: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(route = SCAN_QR_ROUTE) {
|
composableWithSlideTransitions(route = SCAN_QR_ROUTE) {
|
||||||
ScanQrCodeScreen(
|
ScanQrCodeScreen(
|
||||||
navigateBack = navigateBack,
|
navigateBack = navigateBack,
|
||||||
navigateToSendScreen = navigateToSendScreen,
|
navigateToSendScreen = navigateToSendScreen,
|
||||||
|
navigateToAddBeneficiaryScreen = navigateToAddBeneficiaryScreen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user