From 20ba4eebbab798e842aedf2928977ccf8554ee7b Mon Sep 17 00:00:00 2001 From: Sk Niyaj Ali Date: Sat, 19 Oct 2024 08:38:56 +0530 Subject: [PATCH] Feat: Migrated Accounts Module to KMP (#1793) * Feat: Migrated Accounts Module to KMP * Updated README.md * Update README.md --- README.md | 86 ++- .../org/mifospay/core/common/DataState.kt | 37 + .../core/common/DataStateExtensions.kt | 105 +++ .../org/mifospay/core/common/DateHelper.kt | 20 + .../kotlin/org/mifospay/core/common/Result.kt | 25 - core/data/build.gradle.kts | 2 +- .../mifospay/core/data/di/RepositoryModule.kt | 13 +- .../core/data/mapper/AccountMapper.kt | 5 +- .../core/data/mapper/CurrencyMapper.kt | 21 - .../core/data/mapper/SavingAccountMapper.kt | 60 ++ .../core/data/mapper/TrannsferDetailMapper.kt | 64 -- .../core/data/mapper/TransactionMapper.kt | 6 +- .../core/data/repository/AccountRepository.kt | 6 +- .../repository/AuthenticationRepository.kt | 4 +- .../data/repository/BeneficiaryRepository.kt | 19 +- .../core/data/repository/ClientRepository.kt | 23 +- .../data/repository/DocumentRepository.kt | 12 +- .../core/data/repository/InvoiceRepository.kt | 12 +- .../data/repository/KycLevelRepository.kt | 8 +- .../data/repository/LocalAssetRepository.kt | 17 + .../data/repository/NotificationRepository.kt | 4 +- .../data/repository/RegistrationRepository.kt | 6 +- .../data/repository/RunReportRepository.kt | 4 +- .../data/repository/SavedCardRepository.kt | 10 +- .../repository/SavingsAccountRepository.kt | 33 +- .../core/data/repository/SearchRepository.kt | 4 +- .../data/repository/SelfServiceRepository.kt | 40 +- .../StandingInstructionRepository.kt | 12 +- .../ThirdPartyTransferRepository.kt | 6 +- .../repository/TwoFactorAuthRepository.kt | 8 +- .../core/data/repository/UserRepository.kt | 16 +- .../repositoryImp/AccountRepositoryImpl.kt | 12 +- .../AuthenticationRepositoryImpl.kt | 8 +- .../BeneficiaryRepositoryImpl.kt | 63 +- .../repositoryImp/ClientRepositoryImpl.kt | 82 +- .../repositoryImp/DocumentRepositoryImpl.kt | 24 +- .../repositoryImp/InvoiceRepositoryImpl.kt | 26 +- .../repositoryImp/KycLevelRepositoryImpl.kt | 16 +- .../repositoryImp/LocalAssetRepositoryImpl.kt | 47 ++ .../NotificationRepositoryImpl.kt | 8 +- .../RegistrationRepositoryImpl.kt | 14 +- .../repositoryImp/RunReportRepositoryImpl.kt | 8 +- .../repositoryImp/SavedCardRepositoryImpl.kt | 20 +- .../SavingsAccountRepositoryImpl.kt | 105 ++- .../repositoryImp/SearchRepositoryImpl.kt | 8 +- .../SelfServiceRepositoryImpl.kt | 198 +++-- .../StandingInstructionRepositoryImpl.kt | 24 +- .../ThirdPartyTransferRepositoryImpl.kt | 12 +- .../TwoFactorAuthRepositoryImpl.kt | 16 +- .../data/repositoryImp/UserRepositoryImpl.kt | 40 +- .../mifospay/core/data/util/SampleLocale.kt | 700 ++++++++++++++++++ .../datastore/UserPreferencesDataSource.kt | 17 +- .../datastore/UserPreferencesRepository.kt | 14 +- .../UserPreferencesRepositoryImpl.kt | 39 +- .../core/designsystem/component/Button.kt | 6 +- .../designsystem/component/MifosScaffold.kt | 87 ++- .../component/OutlineTextField.kt | 63 -- .../core/designsystem/component/TextField.kt | 330 ++++----- .../core/designsystem/icon/MifosIcons.kt | 2 + .../org/mifospay/core/domain/LoginUseCase.kt | 32 +- .../mifospay/core/model/account/Account.kt | 2 + .../AccountContent.kt} | 11 +- .../model/account/AccountsWithTransactions.kt | 17 + .../core/model/bank/BankAccountDetails.kt | 1 + .../core/model/beneficiary/Beneficiary.kt | 35 + .../model/beneficiary}/BeneficiaryPayload.kt | 2 +- .../beneficiary}/BeneficiaryUpdatePayload.kt | 2 +- .../core/model/savingsaccount/AccountType.kt} | 14 +- .../BlockUnblockResponseEntity.kt | 2 +- .../savingsaccount/CreateNewSavingEntity.kt | 32 + .../core/model/savingsaccount/Currency.kt | 4 + .../core/model/savingsaccount/DepositType.kt | 43 +- .../model/savingsaccount/InterestPeriod.kt | 22 + .../model/savingsaccount/PaymentDetailData.kt | 26 + .../core/model/savingsaccount}/PaymentType.kt | 7 +- .../model/savingsaccount/SavingAccount.kt | 20 +- .../savingsaccount/SavingAccountDetail.kt | 51 ++ .../savingsaccount/SavingAccountEntity.kt | 30 + .../savingsaccount/SavingAccountTemplate.kt | 30 + .../model/savingsaccount/SavingCharge.kt} | 14 +- .../savingsaccount/SavingProductOption.kt | 26 + .../SavingsWithAssociationsEntity.kt | 42 ++ .../core/model/savingsaccount/Status.kt | 28 +- .../core/model/savingsaccount/SubStatus.kt | 29 + .../core/model/savingsaccount/Summary.kt | 36 + .../core/model/savingsaccount/Timeline.kt | 31 + .../core/model/savingsaccount/Transaction.kt | 30 +- .../savingsaccount}/TransactionsEntity.kt | 43 +- .../core/model/savingsaccount/Transfer.kt | 25 + .../UpdateSavingAccountEntity.kt | 21 + .../org/mifospay/core/model/utils/Locale.kt | 34 + .../entity/accounts/savings/CurrencyEntity.kt | 33 - .../accounts/savings/PaymentDetailData.kt | 33 - .../accounts/savings/SavingAccountEntity.kt | 106 --- .../savings/SavingsWithAssociationsEntity.kt | 50 -- .../entity/accounts/savings/StatusEntity.kt | 45 -- .../model/entity/accounts/savings/Summary.kt | 25 - .../model/entity/accounts/savings/TimeLine.kt | 43 -- .../accounts/savings/TransactionType.kt | 35 - .../model/entity/beneficary/Beneficiary.kt | 24 - .../entity/client/ClientAccountsEntity.kt | 4 +- .../model/entity/client/DepositTypeEntity.kt | 35 +- .../model/entity/payload/ClientPayload.kt | 2 +- .../StandingInstruction.kt | 13 +- .../account/AccountOptionsTemplate.kt | 4 +- .../beneficiary/BeneficiaryTemplate.kt | 2 +- .../services/AccountTransfersService.kt | 2 +- .../network/services/BeneficiaryService.kt | 15 +- .../core/network/services/ClientService.kt | 4 +- .../core/network/services/RunReportService.kt | 2 +- .../services/SavingsAccountsService.kt | 26 +- core/ui/build.gradle.kts | 1 + .../drawable/arrow_outward.xml | 6 +- .../kotlin/org/mifospay/core/ui}/AvatarBox.kt | 27 +- .../org/mifospay/core/ui/MifosDivider.kt | 33 + .../org/mifospay/core/ui/MifosSmallChip.kt | 47 ++ .../core/ui/NavGraphBuilderExtensions.kt | 91 +++ .../org/mifospay/core/ui/RevealSwipe.kt | 588 +++++++++++++++ .../core/ui/TransactionHistoryCard.kt | 105 +++ ...onItemScreen.kt => TransactionItemCard.kt} | 121 ++- .../org/mifospay/core/ui/utils/Transition.kt | 379 ++++++++++ feature/accounts/build.gradle.kts | 22 +- feature/accounts/consumer-rules.pro | 0 feature/accounts/proguard-rules.pro | 21 - .../{main => androidMain}/AndroidManifest.xml | 0 .../drawable/baseline_check.xml | 15 + .../drawable/baseline_unchecked.xml | 15 + .../drawable/feature_accounts_ic_bank.xml | 0 .../drawable/feature_accounts_logo_axis.png | Bin .../drawable/feature_accounts_logo_hdfc.png | Bin .../drawable/feature_accounts_logo_icici.png | Bin .../drawable/feature_accounts_logo_pnb.png | Bin .../drawable/feature_accounts_logo_rbl.png | Bin .../drawable/feature_accounts_logo_sbi.png | Bin .../feature_accounts_sim_card_selected.xml | 2 +- .../feature_accounts_sim_card_unselected.xml | 2 +- .../composeResources}/values/strings.xml | 2 + .../feature/accounts/AccountViewModel.kt | 236 ++++++ .../feature/accounts/AccountsScreen.kt | 614 +++++++++++++++ .../beneficiary/AddEditBeneficiaryScreen.kt | 296 ++++++++ .../AddEditBeneficiaryViewModel.kt | 299 ++++++++ .../beneficiary/BeneficiaryAddEditType.kt | 29 + .../beneficiary/BeneficiaryNavigation.kt | 80 ++ .../feature/accounts/di/AccountsModule.kt | 26 + .../AddEditSavingAccountScreen.kt | 544 ++++++++++++++ .../savingsaccount/AddEditSavingNavigation.kt | 85 +++ .../savingsaccount/AddEditSavingViewModel.kt | 431 +++++++++++ .../savingsaccount/SavingsAddEditType.kt | 29 + .../details/SavingAccountDetailNavigation.kt | 42 ++ .../details/SavingAccountDetailScreen.kt | 503 +++++++++++++ .../details/SavingAccountDetailViewModel.kt | 113 +++ .../feature/bank/accounts/AccountViewModel.kt | 141 ---- .../feature/bank/accounts/AccountsItem.kt | 92 --- .../feature/bank/accounts/AccountsScreen.kt | 230 ------ .../choose/sim/ChooseSimDialogSheet.kt | 210 ------ .../details/BankAccountDetailScreen.kt | 251 ------- .../bank/accounts/di/AccountsModule.kt | 26 - .../accounts/link/LinkBankAccountScreen.kt | 347 --------- .../accounts/link/LinkBankAccountViewModel.kt | 106 --- ...LinkBankUiStatePreviewParameterProvider.kt | 34 - .../navigation/BankAccountDetailNavigation.kt | 70 -- .../navigation/LinkBankAccountNavigation.kt | 31 - .../accounts/src/main/res/values/colors.xml | 15 - .../feature/auth/login/LoginScreen.kt | 27 +- .../feature/auth/login/LoginViewModel.kt | 10 +- .../MobileVerificationViewModel.kt | 18 +- .../feature/auth/signup/SignupScreen.kt | 22 +- .../feature/auth/signup/SignupViewModel.kt | 36 +- .../editpassword/EditPasswordViewModel.kt | 10 +- .../feature/history/HistoryViewModel.kt | 90 +-- .../history/components/TransactionDetail.kt | 1 + .../history/components/TransactionList.kt | 32 +- .../detail/TransactionDetailViewModel.kt | 10 +- .../SpecificTransactionsViewModel.kt | 18 +- .../org/mifospay/feature/home/HomeScreen.kt | 529 +++++++------ .../mifospay/feature/home/HomeViewModel.kt | 173 ++--- .../feature/home/navigation/HomeNavigation.kt | 6 +- .../merchants/ui/MerchantTransferScreen.kt | 4 +- .../mifospay/feature/profile/ProfileScreen.kt | 111 ++- .../feature/profile/ProfileViewModel.kt | 85 +-- .../feature/profile/edit/EditProfileScreen.kt | 5 +- .../profile/edit/EditProfileViewModel.kt | 28 +- .../navigation/EditProfileNavigation.kt | 4 +- .../feature/request/money/SetAmountDialog.kt | 6 +- .../feature/send/money/SendScreenRoute.kt | 6 +- .../feature/settings/SettingsViewModel.kt | 10 +- libs/pullrefresh/build.gradle.kts | 17 +- .../mifos/library/pullrefresh}/PullRefresh.kt | 0 .../pullrefresh}/PullRefreshIndicator.kt | 0 .../PullRefreshIndicatorTransform.kt | 0 .../library/pullrefresh}/PullRefreshState.kt | 0 .../prodReleaseRuntimeClasspath.tree.txt | 160 ++-- .../prodReleaseRuntimeClasspath.txt | 1 + mifospay-shared/build.gradle.kts | 1 + .../org/mifospay/shared/di/KoinModules.kt | 2 + .../shared/navigation/MifosNavHost.kt | 31 +- 196 files changed, 7947 insertions(+), 3785 deletions(-) create mode 100644 core/common/src/commonMain/kotlin/org/mifospay/core/common/DataState.kt create mode 100644 core/common/src/commonMain/kotlin/org/mifospay/core/common/DataStateExtensions.kt delete mode 100644 core/common/src/commonMain/kotlin/org/mifospay/core/common/Result.kt delete mode 100644 core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/CurrencyMapper.kt create mode 100644 core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/SavingAccountMapper.kt delete mode 100644 core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TrannsferDetailMapper.kt create mode 100644 core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/LocalAssetRepository.kt create mode 100644 core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/LocalAssetRepositoryImpl.kt create mode 100644 core/data/src/commonMain/kotlin/org/mifospay/core/data/util/SampleLocale.kt delete mode 100644 core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/OutlineTextField.kt rename core/model/src/commonMain/kotlin/org/mifospay/core/model/{client/AccountWithTransactions.kt => account/AccountContent.kt} (63%) create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/account/AccountsWithTransactions.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/Beneficiary.kt rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary => model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary}/BeneficiaryPayload.kt (93%) rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary => model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary}/BeneficiaryUpdatePayload.kt (89%) rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/SavingAccountsListResponse.kt => model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/AccountType.kt} (60%) rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings => model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount}/BlockUnblockResponseEntity.kt (90%) create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/CreateNewSavingEntity.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/InterestPeriod.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/PaymentDetailData.kt rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings => model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount}/PaymentType.kt (75%) create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountDetail.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountEntity.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountTemplate.kt rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Transfer.kt => model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingCharge.kt} (62%) create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingProductOption.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingsWithAssociationsEntity.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SubStatus.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Summary.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Timeline.kt rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings => model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount}/TransactionsEntity.kt (62%) create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transfer.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/UpdateSavingAccountEntity.kt create mode 100644 core/model/src/commonMain/kotlin/org/mifospay/core/model/utils/Locale.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/CurrencyEntity.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/PaymentDetailData.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingAccountEntity.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingsWithAssociationsEntity.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/StatusEntity.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Summary.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TimeLine.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TransactionType.kt delete mode 100644 core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/Beneficiary.kt rename libs/pullrefresh/src/main/AndroidManifest.xml => core/ui/src/commonMain/composeResources/drawable/arrow_outward.xml (52%) rename {feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components => core/ui/src/commonMain/kotlin/org/mifospay/core/ui}/AvatarBox.kt (71%) create mode 100644 core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosDivider.kt create mode 100644 core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosSmallChip.kt create mode 100644 core/ui/src/commonMain/kotlin/org/mifospay/core/ui/NavGraphBuilderExtensions.kt create mode 100644 core/ui/src/commonMain/kotlin/org/mifospay/core/ui/RevealSwipe.kt create mode 100644 core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionHistoryCard.kt rename core/ui/src/commonMain/kotlin/org/mifospay/core/ui/{TransactionItemScreen.kt => TransactionItemCard.kt} (50%) create mode 100644 core/ui/src/commonMain/kotlin/org/mifospay/core/ui/utils/Transition.kt delete mode 100644 feature/accounts/consumer-rules.pro delete mode 100644 feature/accounts/proguard-rules.pro rename feature/accounts/src/{main => androidMain}/AndroidManifest.xml (100%) create mode 100644 feature/accounts/src/commonMain/composeResources/drawable/baseline_check.xml create mode 100644 feature/accounts/src/commonMain/composeResources/drawable/baseline_unchecked.xml rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_ic_bank.xml (100%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_logo_axis.png (100%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_logo_hdfc.png (100%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_logo_icici.png (100%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_logo_pnb.png (100%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_logo_rbl.png (100%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_logo_sbi.png (100%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_sim_card_selected.xml (97%) rename feature/accounts/src/{main/res => commonMain/composeResources}/drawable/feature_accounts_sim_card_unselected.xml (96%) rename feature/accounts/src/{main/res => commonMain/composeResources}/values/strings.xml (93%) create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountsScreen.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryScreen.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryViewModel.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryAddEditType.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryNavigation.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/di/AccountsModule.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingAccountScreen.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingNavigation.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingViewModel.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/SavingsAddEditType.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailNavigation.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailScreen.kt create mode 100644 feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailViewModel.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountViewModel.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsItem.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsScreen.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/choose/sim/ChooseSimDialogSheet.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/details/BankAccountDetailScreen.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/di/AccountsModule.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountScreen.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountViewModel.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankUiStatePreviewParameterProvider.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/BankAccountDetailNavigation.kt delete mode 100644 feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/LinkBankAccountNavigation.kt delete mode 100644 feature/accounts/src/main/res/values/colors.xml rename libs/pullrefresh/src/{main/kotlin/com.mifos.library.pullrefresh => commonMain/kotlin/com/mifos/library/pullrefresh}/PullRefresh.kt (100%) rename libs/pullrefresh/src/{main/kotlin/com.mifos.library.pullrefresh => commonMain/kotlin/com/mifos/library/pullrefresh}/PullRefreshIndicator.kt (100%) rename libs/pullrefresh/src/{main/kotlin/com.mifos.library.pullrefresh => commonMain/kotlin/com/mifos/library/pullrefresh}/PullRefreshIndicatorTransform.kt (100%) rename libs/pullrefresh/src/{main/kotlin/com.mifos.library.pullrefresh => commonMain/kotlin/com/mifos/library/pullrefresh}/PullRefreshState.kt (100%) diff --git a/README.md b/README.md index a13883916..25725cf98 100644 --- a/README.md +++ b/README.md @@ -6,48 +6,54 @@ Mobile Wallet is an Android-based framework for mobile wallets based on top of < clean architecture and contains a core library module that can be used as a dependency in any other wallet based project. It is developed at MIFOS together with a global community. +# Run the project +![Screenshot 2024-10-19 005524](https://github.com/user-attachments/assets/8023c529-1215-4c4b-b212-630f0233223f) +- To run the android-app select the `mifospay-android` run configuration and click run. +- To run the desktop-app select the `mifospay-desktop` run configuration and click run. +- To run the web-app-js select the `mifospay-web-js` run configuration and click run. + ## KMP Status for modules -| Module | Progress | Desktop supported | Android supported | iOS supported | Web supported(JS) | Web supported(WASM-JS) | -|-------------------------------|-------------------|-------------------|--------------------|-------------------|-------------------|------------------------| -| mifospay-android | In progress | ✔️ | ✔️ | ✔️ | ✔️ | ❔ | -| mifospay-desktop | In progress | ✔️ | ✔️ | ✔️ | ✔️ | ❔ | -| mifospay-web | In progress | ✔️ | ✔️ | ✔️ | ✔️ | ❔ | -| mifospay-ios | No implementation | No implementation | No implementation | No implementation | No implementation | No implementation | -| :core:analytics | Done | ❌ | ✔️ | ❌ | ❌ | ❌ | -| :core:common | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:data | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:datastore | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:datastore-proto | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:designsystem | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:domain | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:model | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:network | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :core:ui | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:auth | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:editpassword | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:faq | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:history | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:home | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:profile | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:settings | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:payments | Done | ✅ | ✅ | ✅ | ✅ | ❔ | -| :feature:finance | Done | ✅ | ✅ | ✅ | ✅ | ❔ | -| :feature:account | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:invoices | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:kyc | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:make-transfer | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:merchants | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:notification | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:qr | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:receipt | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:request-money | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:saved-cards | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:search | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:send-money | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:standing-instruction | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| :feature:upi-setup | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | -| lint | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| Module | Progress | Desktop supported | Android supported | iOS supported | Web supported(JS) | Web supported(WASM-JS) | +|-------------------------------|-------------------|-------------------|-------------------|-------------------|-------------------|------------------------| +| mifospay-android | In progress | ✔️ | ✔️ | ✔️ | ✔️ | ❔ | +| mifospay-desktop | In progress | ✔️ | ✔️ | ✔️ | ✔️ | ❔ | +| mifospay-web | In progress | ✔️ | ✔️ | ✔️ | ✔️ | ❔ | +| mifospay-ios | No implementation | No implementation | No implementation | No implementation | No implementation | No implementation | +| :core:analytics | Done | ❌ | ✔️ | ❌ | ❌ | ❌ | +| :core:common | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:data | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:datastore | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:datastore-proto | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:designsystem | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:domain | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:model | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:network | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :core:ui | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:auth | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:editpassword | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:faq | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:history | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:home | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:profile | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:settings | Done | ✅ | ✅ | ❔ | ✅ | ❔ | +| :feature:payments | Done | ✅ | ✅ | ✅ | ✅ | ❔ | +| :feature:finance | Done | ✅ | ✅ | ✅ | ✅ | ❔ | +| :feature:account | Done | ✅ | ✅ | ✅ | ✅ | ❔ | +| :feature:invoices | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:kyc | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:make-transfer | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:merchants | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:notification | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:qr | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:receipt | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:request-money | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:saved-cards | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:search | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:send-money | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:standing-instruction | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:upi-setup | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| lint | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | ✅: Functioning properly ❔: Not yet tested, but expected to work diff --git a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DataState.kt b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DataState.kt new file mode 100644 index 000000000..31dedff8c --- /dev/null +++ b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DataState.kt @@ -0,0 +1,37 @@ +/* + * 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.core.common + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +sealed class DataState { + abstract val data: T? + + data object Loading : DataState() { + override val data: Nothing? get() = null + } + + data class Success( + override val data: T, + ) : DataState() + + data class Error( + val exception: Throwable, + override val data: T? = null, + ) : DataState() +} + +fun Flow.asDataStateFlow(): Flow> = + map> { DataState.Success(it) } + .onStart { emit(DataState.Loading) } + .catch { emit(DataState.Error(it, null)) } diff --git a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DataStateExtensions.kt b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DataStateExtensions.kt new file mode 100644 index 000000000..9e7baea60 --- /dev/null +++ b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DataStateExtensions.kt @@ -0,0 +1,105 @@ +/* + * 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.core.common + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.transformWhile + +inline fun DataState.map( + transform: (T) -> R, +): DataState = when (this) { + is DataState.Success -> DataState.Success(transform(data)) + is DataState.Loading -> DataState.Loading + is DataState.Error -> DataState.Error(exception, data?.let(transform)) +} + +inline fun DataState.mapNullable( + transform: (T?) -> R, +): DataState = when (this) { + is DataState.Success -> DataState.Success(data = transform(data)) + is DataState.Loading -> DataState.Loading + is DataState.Error -> DataState.Error(exception = exception, data = transform(data)) +} + +fun Flow>.takeUntilResultSuccess(): Flow> = transformWhile { + emit(it) + it !is DataState.Success +} + +fun combineResults( + dataState1: DataState, + dataState2: DataState, + transform: (t1: T1, t2: T2) -> R, +): DataState { + val nullableTransform: (T1?, T2?) -> R? = { t1, t2 -> + if (t1 != null && t2 != null) transform(t1, t2) else null + } + + return when { + // Error states have highest priority, fail fast. + dataState1 is DataState.Error -> { + DataState.Error( + exception = dataState1.exception, + data = nullableTransform(dataState1.data, dataState2.data), + ) + } + + dataState2 is DataState.Error -> { + DataState.Error( + exception = dataState2.exception, + data = nullableTransform(dataState1.data, dataState2.data), + ) + } + + // Something is still loading, we will wait for all the data. + dataState1 is DataState.Loading || dataState2 is DataState.Loading -> DataState.Loading + + // Pending state for everything while any one piece of data is updating. + // Both states are _root_ide_package_.org.mifospay.core.common.Result.Success and have data + else -> { + @Suppress("UNCHECKED_CAST") + DataState.Success(transform(dataState1.data as T1, dataState2.data as T2)) + } + } +} + +fun combineResults( + dataState1: DataState, + dataState2: DataState, + dataState3: DataState, + transform: (t1: T1, t2: T2, t3: T3) -> R, +): DataState = + dataState1 + .combineResultsWith(dataState2) { t1, t2 -> t1 to t2 } + .combineResultsWith(dataState3) { t1t2Pair, t3 -> + transform(t1t2Pair.first, t1t2Pair.second, t3) + } + +fun combineResults( + dataState1: DataState, + dataState2: DataState, + dataState3: DataState, + dataState4: DataState, + transform: (t1: T1, t2: T2, t3: T3, t4: T4) -> R, +): DataState = + dataState1 + .combineResultsWith(dataState2) { t1, t2 -> t1 to t2 } + .combineResultsWith(dataState3) { t1t2Pair, t3 -> + Triple(t1t2Pair.first, t1t2Pair.second, t3) + } + .combineResultsWith(dataState4) { t1t2t3Triple, t3 -> + transform(t1t2t3Triple.first, t1t2t3Triple.second, t1t2t3Triple.third, t3) + } + +fun DataState.combineResultsWith( + dataState2: DataState, + transform: (t1: T1, t2: T2) -> R, +): DataState = + combineResults(this, dataState2, transform) diff --git a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt index be6ea7374..6b4fee1be 100644 --- a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt +++ b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt @@ -27,7 +27,17 @@ import kotlin.time.Duration.Companion.days @OptIn(FormatStringsInDatetimeFormats::class) object DateHelper { private const val LOG_TAG = "DateHelper" + + /* + * This is the full month format for the date picker. + * "dd MM yyyy" is the format of the date picker. + */ const val FULL_MONTH = "dd MM yyyy" + + /* + * This is the short month format for the date picker. + * "dd-MM-yyyy" is the format of the date picker. + */ const val SHORT_MONTH = "dd-MM-yyyy" private val fullMonthFormat = LocalDateTime.Format { @@ -168,6 +178,16 @@ object DateHelper { } val currentDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + + /* + * This is the full date format for the date picker. + * "dd MM yyyy" is the format of the date picker. + */ val formattedFullDate = currentDate.format(fullMonthFormat) + + /* + * This is the short date format for the date picker. + * "dd-MM-yyyy" is the format of the date picker. + */ val formattedShortDate = currentDate.format(shortMonthFormat) } diff --git a/core/common/src/commonMain/kotlin/org/mifospay/core/common/Result.kt b/core/common/src/commonMain/kotlin/org/mifospay/core/common/Result.kt deleted file mode 100644 index fbc2e14ef..000000000 --- a/core/common/src/commonMain/kotlin/org/mifospay/core/common/Result.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.core.common - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart - -sealed interface Result { - data class Success(val data: T) : Result - data class Error(val exception: Throwable) : Result - data object Loading : Result -} - -fun Flow.asResult(): Flow> = map> { Result.Success(it) } - .onStart { emit(Result.Loading) } - .catch { emit(Result.Error(it)) } diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 80c987522..23a406caa 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -31,12 +31,12 @@ kotlin { api(projects.core.model) implementation(projects.core.network) implementation(projects.core.analytics) + implementation(libs.kotlinx.serialization.json) } commonTest.dependencies { implementation(libs.multiplatform.settings) implementation(libs.multiplatform.settings.test) - implementation(libs.kotlinx.serialization.json) } androidMain.dependencies { diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt index aa48af381..442a19387 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt @@ -9,6 +9,7 @@ */ package org.mifospay.core.data.di +import kotlinx.serialization.json.Json import org.koin.core.qualifier.named import org.koin.dsl.module import org.mifospay.core.common.MifosDispatchers @@ -19,6 +20,7 @@ import org.mifospay.core.data.repository.ClientRepository import org.mifospay.core.data.repository.DocumentRepository import org.mifospay.core.data.repository.InvoiceRepository import org.mifospay.core.data.repository.KycLevelRepository +import org.mifospay.core.data.repository.LocalAssetRepository import org.mifospay.core.data.repository.NotificationRepository import org.mifospay.core.data.repository.RegistrationRepository import org.mifospay.core.data.repository.RunReportRepository @@ -37,6 +39,7 @@ import org.mifospay.core.data.repositoryImp.ClientRepositoryImpl import org.mifospay.core.data.repositoryImp.DocumentRepositoryImpl import org.mifospay.core.data.repositoryImp.InvoiceRepositoryImpl import org.mifospay.core.data.repositoryImp.KycLevelRepositoryImpl +import org.mifospay.core.data.repositoryImp.LocalAssetRepositoryImpl import org.mifospay.core.data.repositoryImp.NotificationRepositoryImpl import org.mifospay.core.data.repositoryImp.RegistrationRepositoryImpl import org.mifospay.core.data.repositoryImp.RunReportRepositoryImpl @@ -55,6 +58,8 @@ private val ioDispatcher = named(MifosDispatchers.IO.name) private val unconfined = named(MifosDispatchers.Unconfined.name) val RepositoryModule = module { + single { Json { ignoreUnknownKeys = true } } + single { AccountRepositoryImpl(get(), get(ioDispatcher)) } single { AuthenticationRepositoryImpl(get(), get(ioDispatcher)) @@ -65,7 +70,6 @@ val RepositoryModule = module { apiManager = get(), fineractApiManager = get(), ioDispatcher = get(ioDispatcher), - unconfinedDispatcher = get(unconfined), ) } single { DocumentRepositoryImpl(get(), get(ioDispatcher)) } @@ -91,4 +95,11 @@ val RepositoryModule = module { single { getPlatformDataModule } single { getPlatformDataModule.networkMonitor } single { getPlatformDataModule.timeZoneMonitor } + single { + LocalAssetRepositoryImpl( + ioDispatcher = get(qualifier = ioDispatcher), + unconfinedDispatcher = get(unconfined), + networkJson = get(), + ) + } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/AccountMapper.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/AccountMapper.kt index 3e5045e89..ac16801d8 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/AccountMapper.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/AccountMapper.kt @@ -19,8 +19,9 @@ fun ClientAccountsEntity.toAccount(): List { number = it.accountNo, id = it.id, balance = it.accountBalance, - currency = it.currency.toModel(), - productId = it.productId.toLong(), + currency = it.currency, + productId = it.productId, + status = it.status, ) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/CurrencyMapper.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/CurrencyMapper.kt deleted file mode 100644 index 0517a7bc2..000000000 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/CurrencyMapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.core.data.mapper - -import org.mifospay.core.network.model.entity.accounts.savings.CurrencyEntity -import org.mifospay.core.model.savingsaccount.Currency as DomainCurrency - -fun CurrencyEntity.toModel(): DomainCurrency { - return DomainCurrency( - code = this.code, - displayLabel = this.displayLabel, - displaySymbol = this.displaySymbol, - ) -} diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/SavingAccountMapper.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/SavingAccountMapper.kt new file mode 100644 index 000000000..c45a00dc9 --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/SavingAccountMapper.kt @@ -0,0 +1,60 @@ +/* + * 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.core.data.mapper + +import org.mifospay.core.model.savingsaccount.SavingAccount +import org.mifospay.core.model.savingsaccount.SavingAccountDetail +import org.mifospay.core.model.savingsaccount.SavingAccountEntity +import org.mifospay.core.model.savingsaccount.SavingsWithAssociationsEntity + +fun SavingAccountEntity.toModel(): SavingAccount { + return SavingAccount( + id = id, + accountNo = accountNo, + productId = productId, + productName = productName, + shortProductName = shortProductName, + status = status, + currency = currency, + accountBalance = accountBalance, + accountType = accountType, + timeline = timeline, + subStatus = subStatus, + lastActiveTransactionDate = lastActiveTransactionDate, + depositType = depositType, + externalId = externalId, + ) +} + +fun SavingsWithAssociationsEntity.toSavingDetail(): SavingAccountDetail { + return SavingAccountDetail( + id = id, + accountNo = accountNo, + depositType = depositType, + clientId = clientId, + clientName = clientName, + savingsProductId = savingsProductId, + savingsProductName = savingsProductName, + fieldOfficerId = fieldOfficerId, + status = status, + timeline = timeline, + currency = currency, + nominalAnnualInterestRate = nominalAnnualInterestRate, + withdrawalFeeForTransfers = withdrawalFeeForTransfers, + allowOverdraft = allowOverdraft, + enforceMinRequiredBalance = enforceMinRequiredBalance, + lienAllowed = lienAllowed, + withHoldTax = withHoldTax, + lastActiveTransactionDate = lastActiveTransactionDate, + isDormancyTrackingActive = isDormancyTrackingActive, + summary = summary, + transactions = transactions.map { it.toModel() }, + ) +} diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TrannsferDetailMapper.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TrannsferDetailMapper.kt deleted file mode 100644 index 017cf7138..000000000 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TrannsferDetailMapper.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.core.data.mapper - -import org.mifospay.core.model.savingsaccount.DepositType -import org.mifospay.core.model.savingsaccount.SavingAccount -import org.mifospay.core.model.savingsaccount.Status -import org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity -import org.mifospay.core.network.model.entity.accounts.savings.StatusEntity -import org.mifospay.core.network.model.entity.client.DepositTypeEntity - -fun SavingAccountEntity.toModel(): SavingAccount { - return SavingAccount( - id = id, - accountNo = accountNo, - productName = productName, - productId = productId, - overdraftLimit = overdraftLimit, - minRequiredBalance = minRequiredBalance, - accountBalance = accountBalance, - totalDeposits = totalDeposits, - savingsProductName = savingsProductName, - clientName = clientName, - savingsProductId = savingsProductId, - nominalAnnualInterestRate = nominalAnnualInterestRate, - status = status?.toModel(), - currency = currency.toModel(), - depositType = depositType?.toModel(), - isRecurring = this.isRecurring(), - ) -} - -fun StatusEntity.toModel(): Status { - return Status( - id = id, - code = code, - value = value, - submittedAndPendingApproval = submittedAndPendingApproval, - approved = approved, - rejected = rejected, - withdrawnByApplicant = withdrawnByApplicant, - active = active, - closed = closed, - prematureClosed = prematureClosed, - transferInProgress = transferInProgress, - transferOnHold = transferOnHold, - matured = matured, - ) -} - -fun DepositTypeEntity.toModel(): DepositType { - return DepositType( - id = id, - code = code, - value = value, - ) -} diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TransactionMapper.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TransactionMapper.kt index 1f7c76c0d..c932b9d15 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TransactionMapper.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/TransactionMapper.kt @@ -10,10 +10,10 @@ package org.mifospay.core.data.mapper import org.mifospay.core.common.DateHelper +import org.mifospay.core.model.savingsaccount.SavingsWithAssociationsEntity import org.mifospay.core.model.savingsaccount.Transaction import org.mifospay.core.model.savingsaccount.TransactionType -import org.mifospay.core.network.model.entity.accounts.savings.SavingsWithAssociationsEntity -import org.mifospay.core.network.model.entity.accounts.savings.TransactionsEntity +import org.mifospay.core.model.savingsaccount.TransactionsEntity fun SavingsWithAssociationsEntity.toTransactionList(): List { return this.transactions.map { it.toModel() } @@ -24,7 +24,7 @@ fun TransactionsEntity.toModel(): Transaction { transactionId = this.id, amount = this.amount, date = DateHelper.getDateAsString(this.submittedOnDate), - currency = this.currency.toModel(), + currency = this.currency, transactionType = when { this.transactionType.deposit -> TransactionType.CREDIT this.transactionType.withdrawal -> TransactionType.DEBIT diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AccountRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AccountRepository.kt index 3e2209985..8e318772e 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AccountRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AccountRepository.kt @@ -10,12 +10,12 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.savingsaccount.Transaction import org.mifospay.core.model.savingsaccount.TransferDetail interface AccountRepository { - fun getTransaction(accountId: Long, transactionId: Long): Flow> + fun getTransaction(accountId: Long, transactionId: Long): Flow> - fun getAccountTransfer(transferId: Long): Flow> + fun getAccountTransfer(transferId: Long): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AuthenticationRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AuthenticationRepository.kt index a68c02fe8..e4d65eb89 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AuthenticationRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/AuthenticationRepository.kt @@ -9,9 +9,9 @@ */ package org.mifospay.core.data.repository -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.user.UserInfo interface AuthenticationRepository { - suspend fun authenticate(username: String, password: String): Result + suspend fun authenticate(username: String, password: String): DataState } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/BeneficiaryRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/BeneficiaryRepository.kt index 8cd329f11..31d421686 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/BeneficiaryRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/BeneficiaryRepository.kt @@ -10,24 +10,23 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result -import org.mifospay.core.network.model.CommonResponse -import org.mifospay.core.network.model.entity.beneficary.Beneficiary -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryPayload -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryUpdatePayload +import org.mifospay.core.common.DataState +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.model.beneficiary.BeneficiaryPayload +import org.mifospay.core.model.beneficiary.BeneficiaryUpdatePayload import org.mifospay.core.network.model.entity.templates.beneficiary.BeneficiaryTemplate interface BeneficiaryRepository { - suspend fun getBeneficiaryList(): Flow>> + suspend fun getBeneficiaryList(): Flow>> - suspend fun getBeneficiaryTemplate(): Flow> + suspend fun getBeneficiaryTemplate(): Flow> - suspend fun createBeneficiary(beneficiaryPayload: BeneficiaryPayload): Flow> + suspend fun createBeneficiary(beneficiaryPayload: BeneficiaryPayload): DataState suspend fun updateBeneficiary( beneficiaryId: Long, payload: BeneficiaryUpdatePayload, - ): Flow> + ): DataState - suspend fun deleteBeneficiary(beneficiaryId: Long): Flow> + suspend fun deleteBeneficiary(beneficiaryId: Long): DataState } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt index 829e928ea..656ece770 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt @@ -10,8 +10,7 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.account.Account import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.NewClient @@ -21,23 +20,23 @@ import org.mifospay.core.network.model.entity.client.ClientAccountsEntity interface ClientRepository { - fun getClientInfo(clientId: Long): StateFlow> + fun getClientInfo(clientId: Long): Flow> - suspend fun getClients(): Flow>> + suspend fun getClients(): Flow>> - suspend fun getClient(clientId: Long): Result + suspend fun getClient(clientId: Long): DataState - suspend fun updateClient(clientId: Long, client: UpdatedClient): Result + suspend fun updateClient(clientId: Long, client: UpdatedClient): DataState - fun getClientImage(clientId: Long): Flow> + fun getClientImage(clientId: Long): Flow> - suspend fun updateClientImage(clientId: Long, image: String): Result + suspend fun updateClientImage(clientId: Long, image: String): DataState - suspend fun getClientAccounts(clientId: Long): Flow> + suspend fun getClientAccounts(clientId: Long): Flow> - suspend fun getAccounts(clientId: Long, accountType: String): Result> + suspend fun getAccounts(clientId: Long, accountType: String): Flow>> - suspend fun createClient(newClient: NewClient): Result + suspend fun createClient(newClient: NewClient): DataState - suspend fun deleteClient(clientId: Int): Result + suspend fun deleteClient(clientId: Int): DataState } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt index 3efeec010..d19b8c874 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt @@ -11,11 +11,11 @@ package org.mifospay.core.data.repository import io.ktor.http.content.PartData import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.entity.noncore.Document interface DocumentRepository { - suspend fun getDocuments(entityType: String, entityId: Int): Flow>> + suspend fun getDocuments(entityType: String, entityId: Int): Flow>> suspend fun createDocument( entityType: String, @@ -23,11 +23,11 @@ interface DocumentRepository { name: String, description: String, fileName: PartData.FileItem, - ): Flow> + ): Flow> - suspend fun downloadDocument(entityType: String, entityId: Int, documentId: Int): Flow> + suspend fun downloadDocument(entityType: String, entityId: Int, documentId: Int): Flow> - suspend fun deleteDocument(entityType: String, entityId: Int, documentId: Int): Flow> + suspend fun deleteDocument(entityType: String, entityId: Int, documentId: Int): Flow> suspend fun updateDocument( entityType: String, @@ -36,5 +36,5 @@ interface DocumentRepository { name: String, description: String, fileName: PartData.FileItem, - ): Flow> + ): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/InvoiceRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/InvoiceRepository.kt index 4bc53f800..7b8587618 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/InvoiceRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/InvoiceRepository.kt @@ -10,22 +10,22 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.GenericResponse import org.mifospay.core.network.model.entity.Invoice interface InvoiceRepository { - suspend fun getInvoice(clientId: Int, invoiceId: Int): Flow> + suspend fun getInvoice(clientId: Int, invoiceId: Int): Flow> - suspend fun getInvoices(clientId: Int): Flow>> + suspend fun getInvoices(clientId: Int): Flow>> - suspend fun createInvoice(clientId: Int, invoice: Invoice): Result + suspend fun createInvoice(clientId: Int, invoice: Invoice): DataState suspend fun updateInvoice( clientId: Int, invoiceId: Int, invoice: Invoice, - ): Flow> + ): Flow> - suspend fun deleteInvoice(clientId: Int, invoiceId: Int): Flow> + suspend fun deleteInvoice(clientId: Int, invoiceId: Int): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt index a5954daa4..537f8498a 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt @@ -10,20 +10,20 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.GenericResponse import org.mifospay.core.network.model.entity.kyc.KYCLevel1Details interface KycLevelRepository { - suspend fun fetchKYCLevel1Details(clientId: Int): Flow>> + suspend fun fetchKYCLevel1Details(clientId: Int): Flow>> suspend fun addKYCLevel1Details( clientId: Int, kycLevel1Details: KYCLevel1Details, - ): Flow> + ): Flow> suspend fun updateKYCLevel1Details( clientId: Int, kycLevel1Details: KYCLevel1Details, - ): Flow> + ): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/LocalAssetRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/LocalAssetRepository.kt new file mode 100644 index 000000000..f5ea27879 --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/LocalAssetRepository.kt @@ -0,0 +1,17 @@ +/* + * 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.core.data.repository + +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.model.utils.Locale + +interface LocalAssetRepository { + val localeList: StateFlow> +} diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/NotificationRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/NotificationRepository.kt index 22ae67eb3..183394ccb 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/NotificationRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/NotificationRepository.kt @@ -10,9 +10,9 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.NotificationPayload interface NotificationRepository { - suspend fun fetchNotifications(clientId: Long): Flow>> + suspend fun fetchNotifications(clientId: Long): Flow>> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RegistrationRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RegistrationRepository.kt index 288eb7543..c0f715f6b 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RegistrationRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RegistrationRepository.kt @@ -9,12 +9,12 @@ */ package org.mifospay.core.data.repository -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.entity.register.RegisterPayload import org.mifospay.core.network.model.entity.register.UserVerify interface RegistrationRepository { - suspend fun registerUser(registerPayload: RegisterPayload): Result + suspend fun registerUser(registerPayload: RegisterPayload): DataState - suspend fun verifyUser(userVerify: UserVerify): Result + suspend fun verifyUser(userVerify: UserVerify): DataState } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RunReportRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RunReportRepository.kt index eda8c5ecc..2e8dc8c6c 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RunReportRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/RunReportRepository.kt @@ -10,12 +10,12 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.savingsaccount.Transaction interface RunReportRepository { suspend fun getTransactionReceipt( outputType: String, transactionId: String, - ): Flow> + ): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavedCardRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavedCardRepository.kt index 819759e87..2c3bf7c59 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavedCardRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavedCardRepository.kt @@ -10,16 +10,16 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.GenericResponse import org.mifospay.core.network.model.entity.savedcards.Card interface SavedCardRepository { - suspend fun getSavedCards(clientId: Int): Flow>> + suspend fun getSavedCards(clientId: Int): Flow>> - suspend fun addSavedCard(clientId: Int, card: Card): Flow> + suspend fun addSavedCard(clientId: Int, card: Card): Flow> - suspend fun deleteCard(clientId: Int, cardId: Int): Flow> + suspend fun deleteCard(clientId: Int, cardId: Int): Flow> - suspend fun updateCard(clientId: Int, cardId: Int, card: Card): Flow> + suspend fun updateCard(clientId: Int, cardId: Int, card: Card): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavingsAccountRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavingsAccountRepository.kt index 16c724bc3..3e97f2fd4 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavingsAccountRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavingsAccountRepository.kt @@ -10,35 +10,46 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState +import org.mifospay.core.model.savingsaccount.CreateNewSavingEntity +import org.mifospay.core.model.savingsaccount.SavingAccountDetail +import org.mifospay.core.model.savingsaccount.SavingAccountTemplate +import org.mifospay.core.model.savingsaccount.SavingsWithAssociationsEntity import org.mifospay.core.model.savingsaccount.Transaction -import org.mifospay.core.network.model.GenericResponse +import org.mifospay.core.model.savingsaccount.UpdateSavingAccountEntity import org.mifospay.core.network.model.entity.Page -import org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity -import org.mifospay.core.network.model.entity.accounts.savings.SavingsWithAssociationsEntity interface SavingsAccountRepository { - suspend fun getSavingsAccounts(limit: Int): Flow>> + suspend fun getSavingsAccounts(limit: Int): Flow>> suspend fun getSavingsWithAssociations( accountId: Long, associationType: String, - ): Flow> + ): Flow> - suspend fun createSavingsAccount(savingAccount: SavingAccountEntity): Flow> + fun getAccountDetail(accountId: Long): Flow> + + suspend fun createSavingsAccount(savingAccount: CreateNewSavingEntity): DataState + + suspend fun updateSavingsAccount( + accountId: Long, + savingAccount: UpdateSavingAccountEntity, + ): DataState suspend fun unblockAccount( accountId: Long, - ): Result + ): DataState suspend fun blockAccount( accountId: Long, - ): Result + ): DataState suspend fun getSavingAccountTransaction( accountId: Long, transactionId: Long, - ): Flow> + ): Flow> + + suspend fun payViaMobile(accountId: Long): Flow> - suspend fun payViaMobile(accountId: Long): Flow> + fun getSavingAccountTemplate(clientId: Long): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SearchRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SearchRepository.kt index c3649a378..6c0d78f8e 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SearchRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SearchRepository.kt @@ -9,7 +9,7 @@ */ package org.mifospay.core.data.repository -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.search.SearchResult interface SearchRepository { @@ -17,5 +17,5 @@ interface SearchRepository { query: String, resources: String, exactMatch: Boolean, - ): Result> + ): DataState> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SelfServiceRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SelfServiceRepository.kt index 7d248413c..f4c4cb695 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SelfServiceRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SelfServiceRepository.kt @@ -10,24 +10,25 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.account.Account +import org.mifospay.core.model.account.AccountContent +import org.mifospay.core.model.account.AccountsWithTransactions +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.model.beneficiary.BeneficiaryPayload +import org.mifospay.core.model.beneficiary.BeneficiaryUpdatePayload import org.mifospay.core.model.client.Client import org.mifospay.core.model.savingsaccount.Transaction -import org.mifospay.core.network.model.CommonResponse import org.mifospay.core.network.model.entity.Page import org.mifospay.core.network.model.entity.authentication.AuthenticationPayload -import org.mifospay.core.network.model.entity.beneficary.Beneficiary -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryPayload -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryUpdatePayload import org.mifospay.core.network.model.entity.user.User interface SelfServiceRepository { - suspend fun loginSelf(payload: AuthenticationPayload): Result + suspend fun loginSelf(payload: AuthenticationPayload): DataState - suspend fun getSelfClientDetails(clientId: Long): Result + fun getSelfClientDetails(clientId: Long): Flow> - suspend fun getSelfClientDetails(): Flow>> + suspend fun getSelfClientDetails(): Flow>> fun getSelfAccountTransactions( accountId: Long, @@ -36,16 +37,29 @@ interface SelfServiceRepository { suspend fun getSelfAccountTransactionFromId( accountId: Long, transactionId: Long, - ): Result> + ): DataState> - suspend fun getSelfAccounts(clientId: Long): Result> + fun getSelfAccounts(clientId: Long): Flow>> - suspend fun getBeneficiaryList(): Flow>> + fun getBeneficiaryList(): Flow>> - suspend fun createBeneficiary(beneficiaryPayload: BeneficiaryPayload): Flow> + fun getActiveAccountsWithTransactions( + clientId: Long, + limit: Int, + ): Flow> + + fun getAccountsTransactions(clientId: Long): Flow>> + + fun getTransactions(accountId: List, limit: Int?): Flow> + + fun getAccountAndBeneficiaryList(clientId: Long): Flow> + + suspend fun createBeneficiary(beneficiaryPayload: BeneficiaryPayload): DataState suspend fun updateBeneficiary( beneficiaryId: Long, payload: BeneficiaryUpdatePayload, - ): Flow> + ): DataState + + suspend fun deleteBeneficiary(beneficiaryId: Long): DataState } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/StandingInstructionRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/StandingInstructionRepository.kt index cf927559c..7e570a715 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/StandingInstructionRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/StandingInstructionRepository.kt @@ -10,7 +10,7 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.GenericResponse import org.mifospay.core.network.model.entity.Page import org.mifospay.core.network.model.entity.payload.StandingInstructionPayload @@ -20,18 +20,18 @@ import org.mifospay.core.network.model.entity.standinginstruction.StandingInstru interface StandingInstructionRepository { suspend fun getAllStandingInstructions( clientId: Long, - ): Flow>> + ): Flow>> - suspend fun getStandingInstruction(instructionId: Long): Flow> + suspend fun getStandingInstruction(instructionId: Long): Flow> suspend fun createStandingInstruction( payload: StandingInstructionPayload, - ): Flow> + ): Flow> suspend fun updateStandingInstruction( instructionId: Long, payload: StandingInstructionPayload, - ): Flow> + ): Flow> - suspend fun deleteStandingInstruction(instructionId: Long): Flow> + suspend fun deleteStandingInstruction(instructionId: Long): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ThirdPartyTransferRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ThirdPartyTransferRepository.kt index 13b6a7493..80598f2e3 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ThirdPartyTransferRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ThirdPartyTransferRepository.kt @@ -10,13 +10,13 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.entity.TPTResponse import org.mifospay.core.network.model.entity.payload.TransferPayload import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate interface ThirdPartyTransferRepository { - suspend fun getTransferTemplate(): Flow> + suspend fun getTransferTemplate(): Flow> - suspend fun makeTransfer(payload: TransferPayload): Flow> + suspend fun makeTransfer(payload: TransferPayload): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/TwoFactorAuthRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/TwoFactorAuthRepository.kt index 37bc17ddb..890b6751a 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/TwoFactorAuthRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/TwoFactorAuthRepository.kt @@ -10,14 +10,14 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.network.model.twofactor.AccessToken import org.mifospay.core.network.model.twofactor.DeliveryMethod interface TwoFactorAuthRepository { - suspend fun deliveryMethods(): Flow>> + suspend fun deliveryMethods(): Flow>> - suspend fun requestOTP(deliveryMethod: String): Flow> + suspend fun requestOTP(deliveryMethod: String): Flow> - suspend fun validateToken(token: String): Flow> + suspend fun validateToken(token: String): Flow> } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/UserRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/UserRepository.kt index 2ad38fea8..5d9be9d9f 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/UserRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/UserRepository.kt @@ -10,24 +10,24 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.user.NewUser import org.mifospay.core.network.model.CommonResponse import org.mifospay.core.network.model.GenericResponse import org.mifospay.core.network.model.entity.UserWithRole interface UserRepository { - suspend fun getUsers(): Flow>> + suspend fun getUsers(): Flow>> - suspend fun getUser(): Flow> + suspend fun getUser(): Flow> - suspend fun createUser(newUser: NewUser): Result + suspend fun createUser(newUser: NewUser): DataState - suspend fun updateUser(userId: Int, updatedUser: NewUser): Flow> + suspend fun updateUser(userId: Int, updatedUser: NewUser): Flow> - suspend fun updateUserPassword(userId: Long, password: String): Result + suspend fun updateUserPassword(userId: Long, password: String): DataState - suspend fun deleteUser(userId: Int): Result + suspend fun deleteUser(userId: Int): DataState - suspend fun assignClientToUser(userId: Int, clientId: Int): Result + suspend fun assignClientToUser(userId: Int, clientId: Int): DataState } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AccountRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AccountRepositoryImpl.kt index 6775dcea4..3e020367d 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AccountRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AccountRepositoryImpl.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.mapper.toModel import org.mifospay.core.data.repository.AccountRepository import org.mifospay.core.model.savingsaccount.Transaction @@ -29,16 +29,16 @@ class AccountRepositoryImpl( override fun getTransaction( accountId: Long, transactionId: Long, - ): Flow> { + ): Flow> { return apiManager.accountTransfersApi .getTransaction(accountId, transactionId) .map { it.toModel() } - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } - override fun getAccountTransfer(transferId: Long): Flow> { + override fun getAccountTransfer(transferId: Long): Flow> { return apiManager.accountTransfersApi .getAccountTransfer(transferId.toInt()) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AuthenticationRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AuthenticationRepositoryImpl.kt index 18fd2d1fb..397e540f8 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AuthenticationRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/AuthenticationRepositoryImpl.kt @@ -11,7 +11,7 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.mapper.toUserInfo import org.mifospay.core.data.repository.AuthenticationRepository import org.mifospay.core.model.user.UserInfo @@ -22,7 +22,7 @@ class AuthenticationRepositoryImpl( private val apiManager: SelfServiceApiManager, private val ioDispatcher: CoroutineDispatcher, ) : AuthenticationRepository { - override suspend fun authenticate(username: String, password: String): Result { + override suspend fun authenticate(username: String, password: String): DataState { return try { val payload = AuthenticationPayload(username, password) @@ -30,9 +30,9 @@ class AuthenticationRepositoryImpl( apiManager.authenticationApi.authenticate(payload) } - Result.Success(result.toUserInfo()) + DataState.Success(result.toUserInfo()) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/BeneficiaryRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/BeneficiaryRepositoryImpl.kt index e9abdcf8b..466f4884a 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/BeneficiaryRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/BeneficiaryRepositoryImpl.kt @@ -12,49 +12,66 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import kotlinx.coroutines.withContext +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.BeneficiaryRepository +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.model.beneficiary.BeneficiaryPayload +import org.mifospay.core.model.beneficiary.BeneficiaryUpdatePayload import org.mifospay.core.network.SelfServiceApiManager -import org.mifospay.core.network.model.CommonResponse -import org.mifospay.core.network.model.entity.beneficary.Beneficiary -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryPayload -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryUpdatePayload import org.mifospay.core.network.model.entity.templates.beneficiary.BeneficiaryTemplate class BeneficiaryRepositoryImpl( private val apiManager: SelfServiceApiManager, private val ioDispatcher: CoroutineDispatcher, ) : BeneficiaryRepository { - override suspend fun getBeneficiaryList(): Flow>> { - return apiManager.beneficiaryApi.beneficiaryList().asResult().flowOn(ioDispatcher) + override suspend fun getBeneficiaryList(): Flow>> { + return apiManager.beneficiaryApi.beneficiaryList().asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun getBeneficiaryTemplate(): Flow> { - return apiManager.beneficiaryApi.beneficiaryTemplate().asResult().flowOn(ioDispatcher) + override suspend fun getBeneficiaryTemplate(): Flow> { + return apiManager.beneficiaryApi.beneficiaryTemplate().asDataStateFlow().flowOn(ioDispatcher) } override suspend fun createBeneficiary( beneficiaryPayload: BeneficiaryPayload, - ): Flow> { - return apiManager - .beneficiaryApi - .createBeneficiary(beneficiaryPayload) - .asResult().flowOn(ioDispatcher) + ): DataState { + return try { + withContext(ioDispatcher) { + apiManager.beneficiaryApi.createBeneficiary(beneficiaryPayload) + } + + DataState.Success("Beneficiary created successfully") + } catch (e: Exception) { + DataState.Error(e) + } } override suspend fun updateBeneficiary( beneficiaryId: Long, payload: BeneficiaryUpdatePayload, - ): Flow> { - return apiManager.beneficiaryApi - .updateBeneficiary(beneficiaryId, payload) - .asResult().flowOn(ioDispatcher) + ): DataState { + return try { + withContext(ioDispatcher) { + apiManager.beneficiaryApi.updateBeneficiary(beneficiaryId, payload) + } + + DataState.Success("Beneficiary updated successfully") + } catch (e: Exception) { + DataState.Error(e) + } } - override suspend fun deleteBeneficiary(beneficiaryId: Long): Flow> { - return apiManager.beneficiaryApi - .deleteBeneficiary(beneficiaryId) - .asResult().flowOn(ioDispatcher) + override suspend fun deleteBeneficiary(beneficiaryId: Long): DataState { + return try { + withContext(ioDispatcher) { + apiManager.beneficiaryApi.deleteBeneficiary(beneficiaryId) + } + + DataState.Success("Beneficiary deleted successfully") + } catch (e: Exception) { + DataState.Error(e) + } } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt index 8ef867da7..8048b57a3 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt @@ -10,17 +10,13 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.mapper.toAccount import org.mifospay.core.data.mapper.toEntity import org.mifospay.core.data.mapper.toModel @@ -38,55 +34,48 @@ class ClientRepositoryImpl( private val apiManager: SelfServiceApiManager, private val fineractApiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, - unconfinedDispatcher: CoroutineDispatcher, ) : ClientRepository { - private val coroutineScope = CoroutineScope(unconfinedDispatcher) - - override suspend fun getClients(): Flow>> { - return apiManager.clientsApi.clients().map { it.toModel() }.asResult().flowOn(ioDispatcher) + override suspend fun getClients(): Flow>> { + return apiManager.clientsApi.clients().map { it.toModel() }.asDataStateFlow().flowOn(ioDispatcher) } - override fun getClientInfo(clientId: Long): StateFlow> { + override fun getClientInfo(clientId: Long): Flow> { return fineractApiManager.clientsApi .getClient(clientId) - .catch { Result.Error(it) } - .map { Result.Success(it.toModel()) } - .stateIn( - scope = coroutineScope, - started = SharingStarted.Eagerly, - initialValue = Result.Loading, - ) + .catch { DataState.Error(it, null) } + .map { DataState.Success(it.toModel()) } + .flowOn(ioDispatcher) } - override suspend fun getClient(clientId: Long): Result { + override suspend fun getClient(clientId: Long): DataState { return try { val result = fineractApiManager.clientsApi.getClientForId(clientId) - Result.Success(result.toModel()) + DataState.Success(result.toModel()) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun updateClient(clientId: Long, client: UpdatedClient): Result { + override suspend fun updateClient(clientId: Long, client: UpdatedClient): DataState { return try { withContext(ioDispatcher) { fineractApiManager.clientsApi.updateClient(clientId, client.toEntity()) } - Result.Success("Client updated successfully") + DataState.Success("Client updated successfully") } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override fun getClientImage(clientId: Long): Flow> { + override fun getClientImage(clientId: Long): Flow> { return fineractApiManager.clientsApi .getClientImage(clientId) - .catch { Result.Error(it) } - .map { Result.Success(it) } + .catch { DataState.Error(it, null) } + .map { DataState.Success(it) } } - override suspend fun updateClientImage(clientId: Long, image: String): Result { + override suspend fun updateClientImage(clientId: Long, image: String): DataState { return try { withContext(ioDispatcher) { fineractApiManager.clientsApi.updateClientImage( @@ -95,54 +84,49 @@ class ClientRepositoryImpl( ) } - Result.Success("Client image updated successfully") + DataState.Success("Client image updated successfully") } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun getClientAccounts(clientId: Long): Flow> { + override suspend fun getClientAccounts(clientId: Long): Flow> { return apiManager.clientsApi .getClientAccounts(clientId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun getAccounts( clientId: Long, accountType: String, - ): Result> { - return try { - val result = withContext(ioDispatcher) { - apiManager.clientsApi.getAccounts(clientId, accountType) - } - - Result.Success(result.toAccount()) - } catch (e: Exception) { - Result.Error(e) - } + ): Flow>> { + return apiManager.clientsApi + .getAccounts(clientId, accountType) + .map { it.toAccount() } + .asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun createClient(newClient: NewClient): Result { + override suspend fun createClient(newClient: NewClient): DataState { return try { val result = withContext(ioDispatcher) { fineractApiManager.clientsApi.createClient(newClient.toEntity()) } - Result.Success(result.clientId) + DataState.Success(result.clientId) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun deleteClient(clientId: Int): Result { + override suspend fun deleteClient(clientId: Int): DataState { return try { val result = withContext(ioDispatcher) { fineractApiManager.clientsApi.deleteClient(clientId) } - Result.Success(result.clientId) + DataState.Success(result.clientId) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt index e9bfe21cf..2e8532176 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt @@ -13,8 +13,8 @@ import io.ktor.http.content.PartData import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.DocumentRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.entity.noncore.Document @@ -23,10 +23,10 @@ class DocumentRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : DocumentRepository { - override suspend fun getDocuments(entityType: String, entityId: Int): Flow>> { + override suspend fun getDocuments(entityType: String, entityId: Int): Flow>> { return apiManager.documentApi .getDocuments(entityType, entityId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun createDocument( @@ -35,30 +35,30 @@ class DocumentRepositoryImpl( name: String, description: String, fileName: PartData.FileItem, - ): Flow> { + ): Flow> { return apiManager.documentApi .createDocument(entityType, entityId, name, description, fileName) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun downloadDocument( entityType: String, entityId: Int, documentId: Int, - ): Flow> { + ): Flow> { return apiManager.documentApi .downloadDocument(entityType, entityId, documentId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun deleteDocument( entityType: String, entityId: Int, documentId: Int, - ): Flow> { + ): Flow> { return apiManager.documentApi .removeDocument(entityType, entityId, documentId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun updateDocument( @@ -68,10 +68,10 @@ class DocumentRepositoryImpl( name: String, description: String, fileName: PartData.FileItem, - ): Flow> { + ): Flow> { return apiManager.documentApi .updateDocument(entityType, entityId, documentId, name, description, fileName) - .asResult() + .asDataStateFlow() .flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/InvoiceRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/InvoiceRepositoryImpl.kt index 6c1487125..467e67921 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/InvoiceRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/InvoiceRepositoryImpl.kt @@ -12,8 +12,8 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.InvoiceRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.GenericResponse @@ -23,19 +23,19 @@ class InvoiceRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : InvoiceRepository { - override suspend fun getInvoice(clientId: Int, invoiceId: Int): Flow> { - return apiManager.invoiceApi.getInvoice(clientId, invoiceId).asResult().flowOn(ioDispatcher) + override suspend fun getInvoice(clientId: Int, invoiceId: Int): Flow> { + return apiManager.invoiceApi.getInvoice(clientId, invoiceId).asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun getInvoices(clientId: Int): Flow>> { - return apiManager.invoiceApi.getInvoices(clientId).asResult().flowOn(ioDispatcher) + override suspend fun getInvoices(clientId: Int): Flow>> { + return apiManager.invoiceApi.getInvoices(clientId).asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun createInvoice(clientId: Int, invoice: Invoice): Result { + override suspend fun createInvoice(clientId: Int, invoice: Invoice): DataState { return try { - Result.Success(apiManager.invoiceApi.addInvoice(clientId, invoice)) + DataState.Success(apiManager.invoiceApi.addInvoice(clientId, invoice)) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } @@ -43,18 +43,18 @@ class InvoiceRepositoryImpl( clientId: Int, invoiceId: Int, invoice: Invoice, - ): Flow> { + ): Flow> { return apiManager.invoiceApi - .updateInvoice(clientId, invoiceId, invoice).asResult() + .updateInvoice(clientId, invoiceId, invoice).asDataStateFlow() .flowOn(ioDispatcher) } override suspend fun deleteInvoice( clientId: Int, invoiceId: Int, - ): Flow> { + ): Flow> { return apiManager.invoiceApi - .deleteInvoice(clientId, invoiceId).asResult() + .deleteInvoice(clientId, invoiceId).asDataStateFlow() .flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt index af256b987..4d4ad45f4 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt @@ -12,8 +12,8 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.KycLevelRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.GenericResponse @@ -25,27 +25,27 @@ class KycLevelRepositoryImpl( ) : KycLevelRepository { override suspend fun fetchKYCLevel1Details( clientId: Int, - ): Flow>> { + ): Flow>> { return apiManager.kycLevel1Api .fetchKYCLevel1Details(clientId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun addKYCLevel1Details( clientId: Int, kycLevel1Details: KYCLevel1Details, - ): Flow> { + ): Flow> { return apiManager.kycLevel1Api .addKYCLevel1Details(clientId, kycLevel1Details) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun updateKYCLevel1Details( clientId: Int, kycLevel1Details: KYCLevel1Details, - ): Flow> { + ): Flow> { return apiManager.kycLevel1Api .updateKYCLevel1Details(clientId, kycLevel1Details) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/LocalAssetRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/LocalAssetRepositoryImpl.kt new file mode 100644 index 000000000..c8baa5a13 --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/LocalAssetRepositoryImpl.kt @@ -0,0 +1,47 @@ +/* + * 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.core.data.repositoryImp + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import org.mifospay.core.data.repository.LocalAssetRepository +import org.mifospay.core.data.util.SAMPLE_LOCALE +import org.mifospay.core.model.utils.Locale + +class LocalAssetRepositoryImpl( + private val ioDispatcher: CoroutineDispatcher, + unconfinedDispatcher: CoroutineDispatcher, + private val networkJson: Json, +) : LocalAssetRepository { + private val coroutineScope = CoroutineScope(unconfinedDispatcher) + + override val localeList: StateFlow> + get() = flow { + val data = withContext(ioDispatcher) { + networkJson.decodeFromString>(SAMPLE_LOCALE) + } + + emit(data) + }.catch { + Logger.e(it) { "Error while fetching locale list" } + }.stateIn( + scope = coroutineScope, + initialValue = emptyList(), + started = SharingStarted.Eagerly, + ) +} diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/NotificationRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/NotificationRepositoryImpl.kt index d6eff667c..eec131c7d 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/NotificationRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/NotificationRepositoryImpl.kt @@ -12,8 +12,8 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.NotificationRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.NotificationPayload @@ -24,9 +24,9 @@ class NotificationRepositoryImpl( ) : NotificationRepository { override suspend fun fetchNotifications( clientId: Long, - ): Flow>> { + ): Flow>> { return apiManager.notificationApi .fetchNotifications(clientId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RegistrationRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RegistrationRepositoryImpl.kt index bcb2e5d93..e40c98fc1 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RegistrationRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RegistrationRepositoryImpl.kt @@ -11,7 +11,7 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.repository.RegistrationRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.entity.register.RegisterPayload @@ -21,25 +21,25 @@ class RegistrationRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : RegistrationRepository { - override suspend fun registerUser(registerPayload: RegisterPayload): Result { + override suspend fun registerUser(registerPayload: RegisterPayload): DataState { return try { val result = withContext(ioDispatcher) { apiManager.registrationAPi.registerUser(registerPayload) } - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun verifyUser(userVerify: UserVerify): Result { + override suspend fun verifyUser(userVerify: UserVerify): DataState { return try { val result = withContext(ioDispatcher) { apiManager.registrationAPi.verifyUser(userVerify) } - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RunReportRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RunReportRepositoryImpl.kt index 7c989c45b..708859703 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RunReportRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/RunReportRepositoryImpl.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.mapper.toModel import org.mifospay.core.data.repository.RunReportRepository import org.mifospay.core.model.savingsaccount.Transaction @@ -27,11 +27,11 @@ class RunReportRepositoryImpl( override suspend fun getTransactionReceipt( outputType: String, transactionId: String, - ): Flow> { + ): Flow> { return apiManager.runReportApi .getTransactionReceipt(outputType, transactionId) .map { it.toModel() } - .asResult() + .asDataStateFlow() .flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavedCardRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavedCardRepositoryImpl.kt index 6aebd3457..1caf25fca 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavedCardRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavedCardRepositoryImpl.kt @@ -12,8 +12,8 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.SavedCardRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.GenericResponse @@ -23,25 +23,25 @@ class SavedCardRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : SavedCardRepository { - override suspend fun getSavedCards(clientId: Int): Flow>> { - return apiManager.savedCardApi.getSavedCards(clientId).asResult().flowOn(ioDispatcher) + override suspend fun getSavedCards(clientId: Int): Flow>> { + return apiManager.savedCardApi.getSavedCards(clientId).asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun addSavedCard(clientId: Int, card: Card): Flow> { - return apiManager.savedCardApi.addSavedCard(clientId, card).asResult().flowOn(ioDispatcher) + override suspend fun addSavedCard(clientId: Int, card: Card): Flow> { + return apiManager.savedCardApi.addSavedCard(clientId, card).asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun deleteCard(clientId: Int, cardId: Int): Flow> { - return apiManager.savedCardApi.deleteCard(clientId, cardId).asResult().flowOn(ioDispatcher) + override suspend fun deleteCard(clientId: Int, cardId: Int): Flow> { + return apiManager.savedCardApi.deleteCard(clientId, cardId).asDataStateFlow().flowOn(ioDispatcher) } override suspend fun updateCard( clientId: Int, cardId: Int, card: Card, - ): Flow> { + ): Flow> { return apiManager.savedCardApi .updateCard(clientId, cardId, card) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavingsAccountRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavingsAccountRepositoryImpl.kt index 588e52240..505b2aff4 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavingsAccountRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavingsAccountRepositoryImpl.kt @@ -11,21 +11,25 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.Constants +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.mapper.toModel +import org.mifospay.core.data.mapper.toSavingDetail import org.mifospay.core.data.repository.SavingsAccountRepository +import org.mifospay.core.model.savingsaccount.CreateNewSavingEntity +import org.mifospay.core.model.savingsaccount.SavingAccountDetail +import org.mifospay.core.model.savingsaccount.SavingAccountTemplate +import org.mifospay.core.model.savingsaccount.SavingsWithAssociationsEntity import org.mifospay.core.model.savingsaccount.Transaction +import org.mifospay.core.model.savingsaccount.TransactionsEntity +import org.mifospay.core.model.savingsaccount.UpdateSavingAccountEntity import org.mifospay.core.network.FineractApiManager -import org.mifospay.core.network.model.GenericResponse import org.mifospay.core.network.model.entity.Page -import org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity -import org.mifospay.core.network.model.entity.accounts.savings.SavingsWithAssociationsEntity -import org.mifospay.core.network.model.entity.accounts.savings.TransactionsEntity class SavingsAccountRepositoryImpl( private val apiManager: FineractApiManager, @@ -33,79 +37,110 @@ class SavingsAccountRepositoryImpl( ) : SavingsAccountRepository { override suspend fun getSavingsAccounts( limit: Int, - ): Flow>> { + ): Flow>> { return apiManager.savingsAccountsApi .getSavingsAccounts(limit) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun getSavingsWithAssociations( accountId: Long, associationType: String, - ): Flow> { - return flow { - try { - val result = withContext(ioDispatcher) { - apiManager.savingsAccountsApi - .getSavingsWithAssociations(accountId, associationType) - } + ): Flow> { + return apiManager.savingsAccountsApi + .getSavingsWithAssociations(accountId, associationType) + .catch { DataState.Error(it, null) } + .asDataStateFlow() + .flowOn(ioDispatcher) + } + + override fun getAccountDetail(accountId: Long): Flow> { + return apiManager.savingsAccountsApi + .getSavingsWithAssociations(accountId, Constants.TRANSACTIONS) + .catch { DataState.Error(it, null) } + .map(SavingsWithAssociationsEntity::toSavingDetail) + .asDataStateFlow() + .flowOn(ioDispatcher) + } - emit(Result.Success(result)) - } catch (e: Exception) { - emit(Result.Error(e)) + override suspend fun createSavingsAccount( + savingAccount: CreateNewSavingEntity, + ): DataState { + return try { + withContext(ioDispatcher) { + apiManager.savingsAccountsApi.createSavingsAccount(savingAccount) } + + DataState.Success("Savings Account Created Successfully") + } catch (e: Exception) { + DataState.Error(e) } } - override suspend fun createSavingsAccount( - savingAccount: SavingAccountEntity, - ): Flow> { - return apiManager.savingsAccountsApi - .createSavingsAccount(savingAccount) - .asResult().flowOn(ioDispatcher) + override suspend fun updateSavingsAccount( + accountId: Long, + savingAccount: UpdateSavingAccountEntity, + ): DataState { + return try { + withContext(ioDispatcher) { + apiManager.savingsAccountsApi.updateSavingsAccount(accountId, savingAccount) + } + + DataState.Success("Savings Account Updated Successfully") + } catch (e: Exception) { + DataState.Error(e) + } } override suspend fun unblockAccount( accountId: Long, - ): Result { + ): DataState { return try { withContext(ioDispatcher) { apiManager.savingsAccountsApi.blockUnblockAccount(accountId, "unblock") } - Result.Success("Account unblocked successfully") + DataState.Success("Account unblocked successfully") } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun blockAccount(accountId: Long): Result { + override suspend fun blockAccount(accountId: Long): DataState { return try { withContext(ioDispatcher) { apiManager.savingsAccountsApi.blockUnblockAccount(accountId, "block") } - Result.Success("Account blocked successfully") + DataState.Success("Account blocked successfully") } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } override suspend fun getSavingAccountTransaction( accountId: Long, transactionId: Long, - ): Flow> { + ): Flow> { return apiManager.savingsAccountsApi .getSavingAccountTransaction(accountId, transactionId) .map(TransactionsEntity::toModel) - .asResult() + .asDataStateFlow() .flowOn(ioDispatcher) } - override suspend fun payViaMobile(accountId: Long): Flow> { + override suspend fun payViaMobile(accountId: Long): Flow> { return apiManager.savingsAccountsApi .payViaMobile(accountId) .map(TransactionsEntity::toModel) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) + } + + override fun getSavingAccountTemplate(clientId: Long): Flow> { + return apiManager.savingsAccountsApi + .getSavingAccountTemplate(clientId) + .catch { DataState.Error(it, null) } + .asDataStateFlow() + .flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SearchRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SearchRepositoryImpl.kt index 8f7c7f098..c4bda89ed 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SearchRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SearchRepositoryImpl.kt @@ -11,7 +11,7 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.mapper.toSearchResult import org.mifospay.core.data.repository.SearchRepository import org.mifospay.core.model.search.SearchResult @@ -25,15 +25,15 @@ class SearchRepositoryImpl( query: String, resources: String, exactMatch: Boolean, - ): Result> { + ): DataState> { return try { val result = withContext(ioDispatcher) { apiManager.searchApi.searchResources(query, resources, exactMatch) } - Result.Success(result.toSearchResult()) + DataState.Success(result.toSearchResult()) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SelfServiceRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SelfServiceRepositoryImpl.kt index 52ca37a20..f38042078 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SelfServiceRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SelfServiceRepositoryImpl.kt @@ -10,79 +10,82 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.zip import kotlinx.coroutines.withContext -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow +import org.mifospay.core.common.combineResultsWith import org.mifospay.core.data.mapper.toAccount import org.mifospay.core.data.mapper.toModel import org.mifospay.core.data.mapper.toTransactionList import org.mifospay.core.data.repository.SelfServiceRepository import org.mifospay.core.data.util.Constants import org.mifospay.core.model.account.Account +import org.mifospay.core.model.account.AccountContent +import org.mifospay.core.model.account.AccountsWithTransactions +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.model.beneficiary.BeneficiaryPayload +import org.mifospay.core.model.beneficiary.BeneficiaryUpdatePayload import org.mifospay.core.model.client.Client import org.mifospay.core.model.savingsaccount.Transaction import org.mifospay.core.network.SelfServiceApiManager -import org.mifospay.core.network.model.CommonResponse import org.mifospay.core.network.model.entity.Page import org.mifospay.core.network.model.entity.authentication.AuthenticationPayload -import org.mifospay.core.network.model.entity.beneficary.Beneficiary -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryPayload -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryUpdatePayload import org.mifospay.core.network.model.entity.user.User +@OptIn(ExperimentalCoroutinesApi::class) class SelfServiceRepositoryImpl( private val apiManager: SelfServiceApiManager, private val dispatcher: CoroutineDispatcher, ) : SelfServiceRepository { - - override suspend fun loginSelf(payload: AuthenticationPayload): Result { + override suspend fun loginSelf(payload: AuthenticationPayload): DataState { return try { val result = apiManager.authenticationApi.authenticate(payload) - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun getSelfClientDetails(clientId: Long): Result { - return try { - val result = apiManager.clientsApi.getClientForId(clientId).toModel() - Result.Success(result) - } catch (e: Exception) { - Result.Error(e) - } + override fun getSelfClientDetails(clientId: Long): Flow> { + return apiManager.clientsApi + .getClient(clientId) + .onStart { DataState.Loading } + .catch { DataState.Error(it, null) } + .map { it.toModel() } + .asDataStateFlow().flowOn(dispatcher) } - override suspend fun getSelfClientDetails(): Flow>> { - return apiManager.clientsApi.clients().map { it.toModel() }.asResult().flowOn(dispatcher) + override suspend fun getSelfClientDetails(): Flow>> { + return apiManager.clientsApi.clients().map { it.toModel() }.asDataStateFlow() + .flowOn(dispatcher) } override fun getSelfAccountTransactions( accountId: Long, ): Flow> { - return flow { - try { - val result = withContext(dispatcher) { - apiManager.savingAccountsListApi - .getSavingsWithAssociations(accountId, Constants.TRANSACTIONS) - } - - emit(result.toTransactionList()) - } catch (e: Exception) { - throw e - } - } + return apiManager.savingAccountsListApi + .getSavingsWithAssociations(accountId, Constants.TRANSACTIONS) + .map { it.toTransactionList() } + .flowOn(dispatcher) } override suspend fun getSelfAccountTransactionFromId( accountId: Long, transactionId: Long, - ): Result> { + ): DataState> { return try { val result = withContext(dispatcher) { apiManager.savingAccountsListApi.getSavingAccountTransaction( @@ -91,42 +94,133 @@ class SelfServiceRepositoryImpl( ) } - Result.Success(result.map { it.toModel() }) + DataState.Success(result.map { it.toModel() }) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun getSelfAccounts(clientId: Long): Result> { - return try { - val result = withContext(dispatcher) { - apiManager.clientsApi.getAccounts(clientId, Constants.SAVINGS) + override fun getSelfAccounts(clientId: Long): Flow>> { + return apiManager.clientsApi + .getAccounts(clientId, Constants.SAVINGS) + .map { it.toAccount() } + .asDataStateFlow().flowOn(dispatcher) + } + + override fun getAccountAndBeneficiaryList(clientId: Long): Flow> { + val accountList = apiManager.clientsApi + .getAccounts(clientId, Constants.SAVINGS) + .onStart { DataState.Loading } + .catch { DataState.Error(it, null) } + .map { DataState.Success(it.toAccount()) } + .flowOn(dispatcher) + + val beneficiaryList = apiManager.beneficiaryApi + .beneficiaryList() + .onStart { DataState.Loading } + .catch { DataState.Error(it, null) } + .map { DataState.Success(it) } + .flowOn(dispatcher) + + return accountList.zip(beneficiaryList) { accounts, beneficiaries -> + accounts.combineResultsWith(beneficiaries) { accData, bccData -> + AccountContent(accData, bccData) } + }.flowOn(dispatcher) + } - Result.Success(result.toAccount()) - } catch (e: Exception) { - Result.Error(e) + @OptIn(ExperimentalCoroutinesApi::class) + override fun getActiveAccountsWithTransactions( + clientId: Long, + limit: Int, + ): Flow> { + val accounts = apiManager.clientsApi + .getAccounts(clientId, Constants.SAVINGS) + .map { it.toAccount() } + .map { list -> list.filter { it.status.active } } + .flowOn(dispatcher) + + val transactions = accounts + .map { list -> list.map { it.id } } + .flatMapLatest { + getTransactions(it, limit) + } + + return accounts.combine(transactions) { accountList, transaction -> + AccountsWithTransactions(accountList, transaction) + }.map { DataState.Success(it) } + } + + override fun getTransactions(accountId: List, limit: Int?): Flow> { + return accountId.asFlow().flatMapMerge { clientId -> + getSelfAccountTransactions(clientId) + }.runningFold(emptyList()) { acc, transactions -> + acc + transactions.sortedByDescending { it.date }.let { sortedList -> + limit?.let { sortedList.take(it) } ?: sortedList + } } } - override suspend fun getBeneficiaryList(): Flow>> { - return apiManager.beneficiaryApi.beneficiaryList().asResult().flowOn(dispatcher) + override fun getAccountsTransactions( + clientId: Long, + ): Flow>> { + return apiManager.clientsApi + .getAccounts(clientId, Constants.SAVINGS) + .onStart { DataState.Loading } + .catch { DataState.Error(it, null) } + .map { it.toAccount() } + .map { list -> list.filter { it.status.active } } + .map { list -> list.map { it.id } } + .flatMapLatest { + getTransactions(accountId = it, null) + }.map { + DataState.Success(it) + } + .flowOn(dispatcher) + } + + override fun getBeneficiaryList(): Flow>> { + return apiManager.beneficiaryApi.beneficiaryList().asDataStateFlow().flowOn(dispatcher) } override suspend fun createBeneficiary( beneficiaryPayload: BeneficiaryPayload, - ): Flow> { - return apiManager.beneficiaryApi - .createBeneficiary(beneficiaryPayload) - .asResult().flowOn(dispatcher) + ): DataState { + return try { + withContext(dispatcher) { + apiManager.beneficiaryApi.createBeneficiary(beneficiaryPayload) + } + + DataState.Success("Beneficiary created successfully") + } catch (e: Exception) { + DataState.Error(e) + } } override suspend fun updateBeneficiary( beneficiaryId: Long, payload: BeneficiaryUpdatePayload, - ): Flow> { - return apiManager.beneficiaryApi - .updateBeneficiary(beneficiaryId, payload) - .asResult().flowOn(dispatcher) + ): DataState { + return try { + withContext(dispatcher) { + apiManager.beneficiaryApi.updateBeneficiary(beneficiaryId, payload) + } + + DataState.Success("Beneficiary updated successfully") + } catch (e: Exception) { + DataState.Error(e) + } + } + + override suspend fun deleteBeneficiary(beneficiaryId: Long): DataState { + return try { + withContext(dispatcher) { + apiManager.beneficiaryApi.deleteBeneficiary(beneficiaryId) + } + + DataState.Success("Beneficiary deleted successfully") + } catch (e: Exception) { + DataState.Error(e) + } } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/StandingInstructionRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/StandingInstructionRepositoryImpl.kt index 4777a6339..67f7f0011 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/StandingInstructionRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/StandingInstructionRepositoryImpl.kt @@ -12,8 +12,8 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.StandingInstructionRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.GenericResponse @@ -28,42 +28,42 @@ class StandingInstructionRepositoryImpl( ) : StandingInstructionRepository { override suspend fun getAllStandingInstructions( clientId: Long, - ): Flow>> { + ): Flow>> { return apiManager.standingInstructionApi .getAllStandingInstructions(clientId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun getStandingInstruction( instructionId: Long, - ): Flow> { + ): Flow> { return apiManager.standingInstructionApi .getStandingInstruction(instructionId) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun createStandingInstruction( payload: StandingInstructionPayload, - ): Flow> { + ): Flow> { return apiManager.standingInstructionApi .createStandingInstruction(payload) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun updateStandingInstruction( instructionId: Long, payload: StandingInstructionPayload, - ): Flow> { + ): Flow> { return apiManager.standingInstructionApi .updateStandingInstruction(instructionId, payload, "update") - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun deleteStandingInstruction( instructionId: Long, - ): Flow> { + ): Flow> { return apiManager.standingInstructionApi .deleteStandingInstruction(instructionId, "delete") - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ThirdPartyTransferRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ThirdPartyTransferRepositoryImpl.kt index e06b4ae17..0db69798d 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ThirdPartyTransferRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ThirdPartyTransferRepositoryImpl.kt @@ -12,8 +12,8 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.ThirdPartyTransferRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.entity.TPTResponse @@ -24,15 +24,15 @@ class ThirdPartyTransferRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : ThirdPartyTransferRepository { - override suspend fun getTransferTemplate(): Flow> { + override suspend fun getTransferTemplate(): Flow> { return apiManager.thirdPartyTransferApi .accountTransferTemplate() - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun makeTransfer(payload: TransferPayload): Flow> { + override suspend fun makeTransfer(payload: TransferPayload): Flow> { return apiManager.thirdPartyTransferApi .makeTransfer(payload) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/TwoFactorAuthRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/TwoFactorAuthRepositoryImpl.kt index 1d4aadad3..c4390b86a 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/TwoFactorAuthRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/TwoFactorAuthRepositoryImpl.kt @@ -12,8 +12,8 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.TwoFactorAuthRepository import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.model.twofactor.AccessToken @@ -23,17 +23,17 @@ class TwoFactorAuthRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : TwoFactorAuthRepository { - override suspend fun deliveryMethods(): Flow>> { - return apiManager.twoFactorAuthApi.deliveryMethods().asResult().flowOn(ioDispatcher) + override suspend fun deliveryMethods(): Flow>> { + return apiManager.twoFactorAuthApi.deliveryMethods().asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun requestOTP(deliveryMethod: String): Flow> { + override suspend fun requestOTP(deliveryMethod: String): Flow> { return apiManager.twoFactorAuthApi .requestOTP(deliveryMethod) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun validateToken(token: String): Flow> { - return apiManager.twoFactorAuthApi.validateToken(token).asResult().flowOn(ioDispatcher) + override suspend fun validateToken(token: String): Flow> { + return apiManager.twoFactorAuthApi.validateToken(token).asDataStateFlow().flowOn(ioDispatcher) } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/UserRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/UserRepositoryImpl.kt index 23726d7ef..4911802c4 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/UserRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/UserRepositoryImpl.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext -import org.mifospay.core.common.Result -import org.mifospay.core.common.asResult +import org.mifospay.core.common.DataState +import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.mapper.toEntity import org.mifospay.core.data.repository.UserRepository import org.mifospay.core.model.user.NewUser @@ -28,69 +28,69 @@ class UserRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : UserRepository { - override suspend fun getUsers(): Flow>> { - return apiManager.userApi.users().asResult().flowOn(ioDispatcher) + override suspend fun getUsers(): Flow>> { + return apiManager.userApi.users().asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun getUser(): Flow> { - return apiManager.userApi.getUser().asResult().flowOn(ioDispatcher) + override suspend fun getUser(): Flow> { + return apiManager.userApi.getUser().asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun createUser(newUser: NewUser): Result { + override suspend fun createUser(newUser: NewUser): DataState { return try { val result = withContext(ioDispatcher) { apiManager.userApi.createUser(newUser.toEntity()) } - Result.Success(result.resourceId) + DataState.Success(result.resourceId) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } override suspend fun updateUser( userId: Int, updatedUser: NewUser, - ): Flow> { + ): Flow> { return apiManager.userApi .updateUser(userId, updatedUser.toEntity()) - .asResult().flowOn(ioDispatcher) + .asDataStateFlow().flowOn(ioDispatcher) } - override suspend fun updateUserPassword(userId: Long, password: String): Result { + override suspend fun updateUserPassword(userId: Long, password: String): DataState { return try { apiManager.userApi.updateUserPassword( userId = userId, updateUserEntity = UpdateUserEntityPassword(password, password), ) - Result.Success("Password updated successfully") + DataState.Success("Password updated successfully") } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun deleteUser(userId: Int): Result { + override suspend fun deleteUser(userId: Int): DataState { return try { val result = withContext(ioDispatcher) { apiManager.userApi.deleteUser(userId) } - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun assignClientToUser(userId: Int, clientId: Int): Result { + override suspend fun assignClientToUser(userId: Int, clientId: Int): DataState { return try { val result = withContext(ioDispatcher) { apiManager.userApi.assignClientToUser(userId, mapOf("clients" to listOf(clientId))) } - Result.Success(Unit) + DataState.Success(Unit) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/util/SampleLocale.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/util/SampleLocale.kt new file mode 100644 index 000000000..6956465fb --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/util/SampleLocale.kt @@ -0,0 +1,700 @@ +/* + * 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.core.data.util + +internal val SAMPLE_LOCALE = """ + [ + { + "countryName": "Afrikaans (South Africa)", + "localName": "af_ZA", + "dominantName": "af_ZA" + }, + { + "countryName": "Albanian (Albania)", + "localName": "sq_AL", + "dominantName": "sq_AL" + }, + { + "countryName": "Arabic (Algeria)", + "localName": "ar_DZ", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Bahrain)", + "localName": "ar_BH", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Egypt)", + "localName": "ar_EG", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Iraq)", + "localName": "ar_IQ", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Jordan)", + "localName": "ar_JO", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Kuwait)", + "localName": "ar_KW", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Lebanon)", + "localName": "ar_LB", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Libya)", + "localName": "ar_LY", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Morocco)", + "localName": "ar_MA", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Oman)", + "localName": "ar_OM", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Qatar)", + "localName": "ar_QA", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Saudi Arabia)", + "localName": "ar_SA", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Syria)", + "localName": "ar_SY", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Tunisia)", + "localName": "ar_TN", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (United Arab Emirates)", + "localName": "ar_AE", + "dominantName": "ar_SA" + }, + { + "countryName": "Arabic (Yemen)", + "localName": "ar_YE", + "dominantName": "ar_SA" + }, + { + "countryName": "Armenian (Armenia)", + "localName": "hy_AM", + "dominantName": "hy_AM" + }, + { + "countryName": "Azerbaijani (Azerbaijan)", + "localName": "az_AZ", + "dominantName": "az_AZ" + }, + { + "countryName": "Basque (Spain)", + "localName": "eu_ES", + "dominantName": "eu_ES" + }, + { + "countryName": "Belarusian (Belarus)", + "localName": "be_BY", + "dominantName": "be_BY" + }, + { + "countryName": "Bengali (India)", + "localName": "bn_IN", + "dominantName": "bn_IN" + }, + { + "countryName": "Bosnian (Bosnia and Herzegovina)", + "localName": "bs_BA", + "dominantName": "bs_BA" + }, + { + "countryName": "Bulgarian (Bulgaria)", + "localName": "bg_BG", + "dominantName": "bg_BG" + }, + { + "countryName": "Catalan (Spain)", + "localName": "ca_ES", + "dominantName": "ca_ES" + }, + { + "countryName": "Chinese (China)", + "localName": "zh_CN", + "dominantName": "zh_CN" + }, + { + "countryName": "Chinese (Hong Kong SAR China)", + "localName": "zh_HK", + "dominantName": "zh_TW" + }, + { + "countryName": "Chinese (Macao SAR China)", + "localName": "zh_MO", + "dominantName": "zh_TW" + }, + { + "countryName": "Chinese (Singapore)", + "localName": "zh_SG", + "dominantName": "zh_CN" + }, + { + "countryName": "Chinese (Taiwan)", + "localName": "zh_TW", + "dominantName": "zh_TW" + }, + { + "countryName": "Croatian (Croatia)", + "localName": "hr_HR", + "dominantName": "hr_HR" + }, + { + "countryName": "Czech (Czech Republic)", + "localName": "cs_CZ", + "dominantName": "cs_CZ" + }, + { + "countryName": "Danish (Denmark)", + "localName": "da_DK", + "dominantName": "da_DK" + }, + { + "countryName": "Dutch (Belgium)", + "localName": "nl_BE", + "dominantName": "nl_NL" + }, + { + "countryName": "Dutch (Netherlands)", + "localName": "nl_NL", + "dominantName": "nl_NL" + }, + { + "countryName": "English (Australia)", + "localName": "en_AU", + "dominantName": "en_US" + }, + { + "countryName": "English (Belize)", + "localName": "en_BZ", + "dominantName": "en_US" + }, + { + "countryName": "English (Canada)", + "localName": "en_CA", + "dominantName": "en_US" + }, + { + "countryName": "English (Ireland)", + "localName": "en_IE", + "dominantName": "en_US" + }, + { + "countryName": "English (Jamaica)", + "localName": "en_JM", + "dominantName": "en_US" + }, + { + "countryName": "English (New Zealand)", + "localName": "en_NZ", + "dominantName": "en_US" + }, + { + "countryName": "English (Philippines)", + "localName": "en_PH", + "dominantName": "en_US" + }, + { + "countryName": "English (South Africa)", + "localName": "en_ZA", + "dominantName": "en_US" + }, + { + "countryName": "English (Trinidad and Tobago)", + "localName": "en_TT", + "dominantName": "en_US" + }, + { + "countryName": "English (U.S. Virgin Islands)", + "localName": "en_VI", + "dominantName": "en_US" + }, + { + "countryName": "English (India)", + "localName": "en_IN", + "dominantName": "en_IN" + }, + { + "countryName": "English (United Kingdom)", + "localName": "en_GB", + "dominantName": "en_US" + }, + { + "countryName": "English (United States)", + "localName": "en_US", + "dominantName": "en_US" + }, + { + "countryName": "English (Zimbabwe)", + "localName": "en_ZW", + "dominantName": "en_US" + }, + { + "countryName": "Estonian (Estonia)", + "localName": "et_EE", + "dominantName": "et_EE" + }, + { + "countryName": "Faroese (Faroe Islands)", + "localName": "fo_FO", + "dominantName": "fo_FO" + }, + { + "countryName": "Finnish (Finland)", + "localName": "fi_FI", + "dominantName": "fi_FI" + }, + { + "countryName": "French (Belgium)", + "localName": "fr_BE", + "dominantName": "fr_FR" + }, + { + "countryName": "French (Canada)", + "localName": "fr_CA", + "dominantName": "fr_FR" + }, + { + "countryName": "French (France)", + "localName": "fr_FR", + "dominantName": "fr_FR" + }, + { + "countryName": "French (Luxembourg)", + "localName": "fr_LU", + "dominantName": "fr_FR" + }, + { + "countryName": "French (Monaco)", + "localName": "fr_MC", + "dominantName": "fr_FR" + }, + { + "countryName": "French (Switzerland)", + "localName": "fr_CH", + "dominantName": "fr_FR" + }, + { + "countryName": "Galician (Spain)", + "localName": "gl_ES", + "dominantName": "gl_ES" + }, + { + "countryName": "Georgian (Georgia)", + "localName": "ka_GE", + "dominantName": "ka_GE" + }, + { + "countryName": "German (Austria)", + "localName": "de_AT", + "dominantName": "de_DE" + }, + { + "countryName": "German (Germany)", + "localName": "de_DE", + "dominantName": "de_DE" + }, + { + "countryName": "German (Liechtenstein)", + "localName": "de_LI", + "dominantName": "de_DE" + }, + { + "countryName": "German (Luxembourg)", + "localName": "de_LU", + "dominantName": "de_DE" + }, + { + "countryName": "German (Switzerland)", + "localName": "de_CH", + "dominantName": "de_DE" + }, + { + "countryName": "Greek (Greece)", + "localName": "el_GR", + "dominantName": "el_GR" + }, + { + "countryName": "Gujarati (India)", + "localName": "gu_IN", + "dominantName": "gu_IN" + }, + { + "countryName": "Hebrew (Israel)", + "localName": "he_IL", + "dominantName": "he_IL" + }, + { + "countryName": "Hindi (India)", + "localName": "hi_IN", + "dominantName": "hi_IN" + }, + { + "countryName": "Hungarian (Hungary)", + "localName": "hu_HU", + "dominantName": "hu_HU" + }, + { + "countryName": "Icelandic (Iceland)", + "localName": "is_IS", + "dominantName": "is_IS" + }, + { + "countryName": "Indonesian (Indonesia)", + "localName": "id_ID", + "dominantName": "id_ID" + }, + { + "countryName": "Italian (Italy)", + "localName": "it_IT", + "dominantName": "it_IT" + }, + { + "countryName": "Italian (Switzerland)", + "localName": "it_CH", + "dominantName": "it_IT" + }, + { + "countryName": "Japanese (Japan)", + "localName": "ja_JP", + "dominantName": "ja_JP" + }, + { + "countryName": "Kannada (India)", + "localName": "kn_IN", + "dominantName": "kn_IN" + }, + { + "countryName": "Kazakh (Kazakhstan)", + "localName": "kk_KZ", + "dominantName": "kk_KZ" + }, + { + "countryName": "Konkani (India)", + "localName": "kok_IN", + "dominantName": "kok_IN" + }, + { + "countryName": "Korean (South Korea)", + "localName": "ko_KR", + "dominantName": "ko_KR" + }, + { + "countryName": "Latvian (Latvia)", + "localName": "lv_LV", + "dominantName": "lv_LV" + }, + { + "countryName": "Lithuanian (Lithuania)", + "localName": "lt_LT", + "dominantName": "lt_LT" + }, + { + "countryName": "Macedonian (Macedonia)", + "localName": "mk_MK", + "dominantName": "mk_MK" + }, + { + "countryName": "Malay (Brunei)", + "localName": "ms_BN", + "dominantName": "ms_MY" + }, + { + "countryName": "Malay (Malaysia)", + "localName": "ms_MY", + "dominantName": "ms_MY" + }, + { + "countryName": "Malayalam (India)", + "localName": "ml_IN", + "dominantName": "ml_IN" + }, + { + "countryName": "Maltese (Malta)", + "localName": "mt_MT", + "dominantName": "mt_MT" + }, + { + "countryName": "Marathi (India)", + "localName": "mr_IN", + "dominantName": "mr_IN" + }, + { + "countryName": "Mongolian (Mongolia)", + "localName": "mn_MN", + "dominantName": "mn_MN" + }, + { + "countryName": "Northern Sami (Norway)", + "localName": "se_NO", + "dominantName": "se_NO" + }, + { + "countryName": "Norwegian Bokmål (Norway)", + "localName": "nb_NO", + "dominantName": "nb_NO" + }, + { + "countryName": "Norwegian Nynorsk (Norway)", + "localName": "nn_NO", + "dominantName": "nn_NO" + }, + { + "countryName": "Persian (Iran)", + "localName": "fa_IR", + "dominantName": "fa_IR" + }, + { + "countryName": "Polish (Poland)", + "localName": "pl_PL", + "dominantName": "pl_PL" + }, + { + "countryName": "Portuguese (Brazil)", + "localName": "pt_BR", + "dominantName": "pt_BR" + }, + { + "countryName": "Portuguese (Portugal)", + "localName": "pt_PT", + "dominantName": "pt_BR" + }, + { + "countryName": "Punjabi (India)", + "localName": "pa_IN", + "dominantName": "pa_IN" + }, + { + "countryName": "Romanian (Romania)", + "localName": "ro_RO", + "dominantName": "ro_RO" + }, + { + "countryName": "Russian (Russia)", + "localName": "ru_RU", + "dominantName": "ru_RU" + }, + { + "countryName": "Serbian (Bosnia and Herzegovina)", + "localName": "sr_BA", + "dominantName": "sr_BA" + }, + { + "countryName": "Serbian (Serbia And Montenegro)", + "localName": "sr_CS", + "dominantName": "sr_BA" + }, + { + "countryName": "Slovak (Slovakia)", + "localName": "sk_SK", + "dominantName": "sk_SK" + }, + { + "countryName": "Slovenian (Slovenia)", + "localName": "sl_SI", + "dominantName": "sk_SK" + }, + { + "countryName": "Spanish (Argentina)", + "localName": "es_AR", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Bolivia)", + "localName": "es_BO", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Chile)", + "localName": "es_CL", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Colombia)", + "localName": "es_CO", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Costa Rica)", + "localName": "es_CR", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Dominican Republic)", + "localName": "es_DO", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Ecuador)", + "localName": "es_EC", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (El Salvador)", + "localName": "es_SV", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Guatemala)", + "localName": "es_GT", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Honduras)", + "localName": "es_HN", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Mexico)", + "localName": "es_MX", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Nicaragua)", + "localName": "es_NI", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Panama)", + "localName": "es_PA", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Paraguay)", + "localName": "es_PY", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Peru)", + "localName": "es_PE", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Puerto Rico)", + "localName": "es_PR", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Spain)", + "localName": "es_ES", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Uruguay)", + "localName": "es_UY", + "dominantName": "es_ES" + }, + { + "countryName": "Spanish (Venezuela)", + "localName": "es_VE", + "dominantName": "es_ES" + }, + { + "countryName": "Swahili (Kenya)", + "localName": "sw_KE", + "dominantName": "sw_KE" + }, + { + "countryName": "Swedish (Finland)", + "localName": "sv_FI", + "dominantName": "sv_SE" + }, + { + "countryName": "Swedish (Sweden)", + "localName": "sv_SE", + "dominantName": "sv_SE" + }, + { + "countryName": "Syriac (Syria)", + "localName": "syr_SY", + "dominantName": "syr_SY" + }, + { + "countryName": "Tamil (India)", + "localName": "ta_IN", + "dominantName": "ta_IN" + }, + { + "countryName": "Telugu (India)", + "localName": "te_IN", + "dominantName": "te_IN" + }, + { + "countryName": "Thai (Thailand)", + "localName": "th_TH", + "dominantName": "th_TH" + }, + { + "countryName": "Tswana (South Africa)", + "localName": "tn_ZA", + "dominantName": "tn_ZA" + }, + { + "countryName": "Turkish (Turkey)", + "localName": "tr_TR", + "dominantName": "tr_TR" + }, + { + "countryName": "Ukrainian (Ukraine)", + "localName": "uk_UA", + "dominantName": "uk_UA" + }, + { + "countryName": "Uzbek (Uzbekistan)", + "localName": "uz_UZ", + "dominantName": "uz_UZ" + }, + { + "countryName": "Vietnamese (Vietnam)", + "localName": "vi_VN", + "dominantName": "vi_VN" + }, + { + "countryName": "Welsh (United Kingdom)", + "localName": "cy_GB", + "dominantName": "cy_GB" + }, + { + "countryName": "Xhosa (South Africa)", + "localName": "xh_ZA", + "dominantName": "xh_ZA" + }, + { + "countryName": "Zulu (South Africa)", + "localName": "zu_ZA", + "dominantName": "zu_ZA" + } + ] +""".trimIndent() diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt index ee53419fd..82b4e6951 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt @@ -110,11 +110,19 @@ class UserPreferencesDataSource( } fun updateAuthToken(token: String) { - settings.putString("authToken", token) + settings.putString(AUTH_TOKEN, token) } fun getAuthToken(): String? { - return settings.getString("authToken", "").ifEmpty { null } + return settings.getString(AUTH_TOKEN, "").ifEmpty { null } + } + + fun getDefaultAccount(): Long? { + return settings.getLong(DEFAULT_ACCOUNT, 0).takeIf { it != 0L } + } + + fun updateDefaultAccount(accountId: Long) { + settings.putLong(DEFAULT_ACCOUNT, accountId) } suspend fun clearInfo() { @@ -122,6 +130,11 @@ class UserPreferencesDataSource( settings.clear() } } + + companion object { + const val AUTH_TOKEN = "authToken" + const val DEFAULT_ACCOUNT = "default_account" + } } private fun Settings.putClientPreference(preference: ClientPreferences) { diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt index d9ec3c51f..4dd55ddfc 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt @@ -11,7 +11,7 @@ package org.mifospay.core.datastore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.model.user.UserInfo @@ -27,13 +27,17 @@ interface UserPreferencesRepository { val authToken: String? - suspend fun updateToken(token: String): Result + val defaultAccount: Long? - suspend fun updateUserInfo(user: UserInfo): Result + suspend fun updateToken(token: String): DataState - suspend fun updateClientInfo(client: Client): Result + suspend fun updateUserInfo(user: UserInfo): DataState - suspend fun updateClientProfile(client: UpdatedClient): Result + suspend fun updateClientInfo(client: Client): DataState + + suspend fun updateClientProfile(client: UpdatedClient): DataState suspend fun logOut(): Unit + + suspend fun updateDefaultAccount(accountId: Long): DataState } diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt index ccf476904..36c973941 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.model.user.UserInfo @@ -55,42 +55,55 @@ class UserPreferencesRepositoryImpl( override val authToken: String? get() = preferenceManager.getAuthToken() - override suspend fun updateToken(token: String): Result { + override val defaultAccount: Long? + get() = preferenceManager.getDefaultAccount() + + override suspend fun updateDefaultAccount(accountId: Long): DataState { + return try { + val result = preferenceManager.updateDefaultAccount(accountId) + + DataState.Success(result) + } catch (e: Exception) { + DataState.Error(e) + } + } + + override suspend fun updateToken(token: String): DataState { return try { val result = preferenceManager.updateAuthToken(token) - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun updateClientInfo(client: Client): Result { + override suspend fun updateClientInfo(client: Client): DataState { return try { val result = preferenceManager.updateClientInfo(client) - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun updateClientProfile(client: UpdatedClient): Result { + override suspend fun updateClientProfile(client: UpdatedClient): DataState { return try { val result = preferenceManager.updateClientProfile(client) - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } - override suspend fun updateUserInfo(user: UserInfo): Result { + override suspend fun updateUserInfo(user: UserInfo): DataState { return try { val result = preferenceManager.updateUserInfo(user) - Result.Success(result) + DataState.Success(result) } catch (e: Exception) { - Result.Error(e) + DataState.Error(e) } } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Button.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Button.kt index b52403ecc..98d35a003 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Button.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Button.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn @@ -57,7 +58,8 @@ fun MifosButton( ) { Button( onClick = onClick, - modifier = modifier, + modifier = modifier + .height(48.dp), enabled = enabled, colors = ButtonDefaults.buttonColors(containerColor = color), contentPadding = contentPadding, @@ -104,7 +106,7 @@ fun MifosButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - color: Color = MaterialTheme.colorScheme.onBackground, + color: Color = MaterialTheme.colorScheme.primary, leadingIcon: @Composable (() -> Unit)? = null, ) { MifosButton( diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt index 6a1356a74..a6865f5d3 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt @@ -9,12 +9,13 @@ */ package org.mifospay.core.designsystem.component +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.MaterialTheme @@ -22,17 +23,23 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MifosScaffold( backPress: () -> Unit, modifier: Modifier = Modifier, topBarTitle: String? = null, floatingActionButtonContent: FloatingActionButtonContent? = null, + pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(), snackbarHost: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, content: @Composable (PaddingValues) -> Unit = {}, @@ -58,11 +65,32 @@ fun MifosScaffold( }, snackbarHost = snackbarHost, containerColor = Color.Transparent, - content = content, + content = { paddingValues -> + val internalPullToRefreshState = rememberPullToRefreshState() + Box( + modifier = Modifier.pullToRefresh( + state = internalPullToRefreshState, + isRefreshing = pullToRefreshState.isRefreshing, + onRefresh = pullToRefreshState.onRefresh, + enabled = pullToRefreshState.isEnabled, + ), + ) { + content(paddingValues) + + PullToRefreshDefaults.Indicator( + modifier = Modifier + .padding(paddingValues) + .align(Alignment.TopCenter), + isRefreshing = pullToRefreshState.isRefreshing, + state = internalPullToRefreshState, + ) + } + }, modifier = modifier, ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MifosScaffold( modifier: Modifier = Modifier, @@ -70,6 +98,7 @@ fun MifosScaffold( bottomBar: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(), floatingActionButtonPosition: FabPosition = FabPosition.End, containerColor: Color = Color.Transparent, contentColor: Color = MaterialTheme.colorScheme.onSurface, @@ -77,19 +106,40 @@ fun MifosScaffold( content: @Composable (PaddingValues) -> Unit, ) { Scaffold( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding(), + modifier = modifier, topBar = topBar, bottomBar = bottomBar, snackbarHost = { SnackbarHost(snackbarHostState) }, - floatingActionButton = floatingActionButton, + floatingActionButton = { + Box(modifier = Modifier.navigationBarsPadding()) { + floatingActionButton() + } + }, floatingActionButtonPosition = floatingActionButtonPosition, containerColor = containerColor, contentColor = contentColor, contentWindowInsets = contentWindowInsets, - content = content, + content = { paddingValues -> + val internalPullToRefreshState = rememberPullToRefreshState() + Box( + modifier = Modifier.pullToRefresh( + state = internalPullToRefreshState, + isRefreshing = pullToRefreshState.isRefreshing, + onRefresh = pullToRefreshState.onRefresh, + enabled = pullToRefreshState.isEnabled, + ), + ) { + content(paddingValues) + + PullToRefreshDefaults.Indicator( + modifier = Modifier + .padding(paddingValues) + .align(Alignment.TopCenter), + isRefreshing = pullToRefreshState.isRefreshing, + state = internalPullToRefreshState, + ) + } + }, ) } @@ -98,3 +148,22 @@ data class FloatingActionButtonContent( val contentColor: Color, val content: (@Composable () -> Unit), ) + +data class MifosPullToRefreshState( + val isEnabled: Boolean, + val isRefreshing: Boolean, + val onRefresh: () -> Unit, +) + +@Composable +fun rememberMifosPullToRefreshState( + isEnabled: Boolean = false, + isRefreshing: Boolean = false, + onRefresh: () -> Unit = { }, +): MifosPullToRefreshState = remember(isEnabled, isRefreshing, onRefresh) { + MifosPullToRefreshState( + isEnabled = isEnabled, + isRefreshing = isRefreshing, + onRefresh = onRefresh, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/OutlineTextField.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/OutlineTextField.kt deleted file mode 100644 index fe9e6d5db..000000000 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/OutlineTextField.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.core.designsystem.component - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.sp - -@Composable -fun MifosOutlinedTextField( - label: String, - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - maxLines: Int = 1, - singleLine: Boolean = true, - icon: ImageVector? = null, - error: Boolean = false, - visualTransformation: VisualTransformation = VisualTransformation.None, - trailingIcon: @Composable (() -> Unit)? = null, - keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), -) { - MifosCustomTextField( - value = value, - onValueChange = onValueChange, - label = label, - modifier = modifier, - leadingIcon = if (icon != null) { - { - Icon( - imageVector = icon, - contentDescription = icon.name, - ) - } - } else { - null - }, - trailingIcon = trailingIcon, - maxLines = maxLines, - singleLine = singleLine, - textStyle = LocalDensity.current.run { - TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface) - }, - keyboardOptions = keyboardOptions, - visualTransformation = visualTransformation, - isError = error, - ) -} diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TextField.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TextField.kt index 28d60e83b..9af717543 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TextField.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TextField.kt @@ -9,60 +9,44 @@ */ package org.mifospay.core.designsystem.component -import androidx.compose.foundation.background +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.interaction.collectIsFocusedAsState 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.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -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.graphics.RectangleShape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation 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.theme.MifosTheme +import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.NewUi @Composable -fun MfOutlinedTextField( +fun MifosOutlinedTextField( value: String, label: String, onValueChange: (String) -> Unit, @@ -70,26 +54,47 @@ fun MfOutlinedTextField( isError: Boolean = false, errorMessage: String = "", singleLine: Boolean = false, + showClearIcon: Boolean = true, + readOnly: Boolean = false, + clearIcon: ImageVector = MifosIcons.Close, + onClickClearIcon: () -> Unit = {}, onKeyboardActions: (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { + val isFocused by interactionSource.collectIsFocusedAsState() + MifosCustomTextField( - modifier = modifier, + modifier = modifier.fillMaxWidth(), value = value, onValueChange = onValueChange, label = label, + readOnly = readOnly, supportingText = { if (isError) { Text(text = errorMessage) } }, singleLine = singleLine, - trailingIcon = trailingIcon, + leadingIcon = leadingIcon, + trailingIcon = @Composable { + if (showClearIcon && isFocused) { + ClearIconButton( + showClearIcon = true, + clearIcon = clearIcon, + onClickClearIcon = onClickClearIcon, + ) + } else { + trailingIcon?.invoke() + } + }, keyboardActions = KeyboardActions { onKeyboardActions?.invoke() }, keyboardOptions = keyboardOptions, + interactionSource = interactionSource, textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface) }, @@ -97,89 +102,62 @@ fun MfOutlinedTextField( } @Composable -fun MfPasswordTextField( - password: String, - label: String, - isError: Boolean, - isPasswordVisible: Boolean, - onTogglePasswordVisibility: () -> Unit, - onPasswordChange: (String) -> Unit, - modifier: Modifier = Modifier, - errorMessage: String? = null, -) { - OutlinedTextField( - modifier = modifier, - value = password, - onValueChange = onPasswordChange, - label = { Text(label) }, - isError = isError, - visualTransformation = if (isPasswordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - supportingText = { - errorMessage?.let { Text(text = it) } - }, - trailingIcon = { - IconButton(onClick = onTogglePasswordVisibility) { - Icon( - if (isPasswordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, - contentDescription = "Show password", - ) - } - }, - ) -} - -@Composable -fun MifosOutlinedTextField( +fun MifosTextField( + value: String, + onValueChange: (String) -> Unit, label: String, - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, - maxLines: Int = 1, - singleLine: Boolean = true, - icon: ImageVector? = null, + enabled: Boolean = true, + showClearIcon: Boolean = true, + readOnly: Boolean = false, + clearIcon: ImageVector = MifosIcons.Close, + onClickClearIcon: () -> Unit = {}, + textStyle: TextStyle = LocalTextStyle.current, visualTransformation: VisualTransformation = VisualTransformation.None, - trailingIcon: @Composable (() -> Unit)? = null, keyboardActions: KeyboardActions = KeyboardActions.Default, - error: Boolean = false, + singleLine: Boolean = true, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, ) { - OutlinedTextField( + val isFocused by interactionSource.collectIsFocusedAsState() + + MifosCustomTextField( value = value, + label = label, onValueChange = onValueChange, - label = { Text(label) }, - modifier = modifier, - leadingIcon = if (icon != null) { - { - Icon( - imageVector = icon, - contentDescription = icon.name, + textStyle = textStyle, + modifier = modifier.fillMaxWidth(), + enabled = enabled, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + trailingIcon = @Composable { + if (showClearIcon && isFocused) { + ClearIconButton( + showClearIcon = true, + clearIcon = clearIcon, + onClickClearIcon = onClickClearIcon, ) + } else { + trailingIcon?.invoke() } - } else { - null }, - trailingIcon = trailingIcon, - maxLines = maxLines, - singleLine = singleLine, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.onSurface, - focusedLabelColor = MaterialTheme.colorScheme.onSurface, - ), - textStyle = LocalDensity.current.run { - TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface) - }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - keyboardActions = keyboardActions, - visualTransformation = visualTransformation, - isError = error, + leadingIcon = leadingIcon, ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun MifosTextField( +fun MifosCustomTextField( value: String, onValueChange: (String) -> Unit, label: String, @@ -194,91 +172,80 @@ fun MifosTextField( singleLine: Boolean = true, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, + isError: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), trailingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, - indicatorColor: Color? = null, + supportingText: @Composable (() -> Unit)? = null, ) { - var isFocused by rememberSaveable { mutableStateOf(false) } - + val colors = TextFieldDefaults.colors().copy( + cursorColor = MaterialTheme.colorScheme.primary, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + errorContainerColor = Color.Transparent, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f), + focusedTrailingIconColor = NewUi.onSurface.copy(0.15f), + unfocusedTrailingIconColor = NewUi.onSurface.copy(0.15f), + ) BasicTextField( value = value, onValueChange = onValueChange, - textStyle = textStyle, - modifier = modifier - .fillMaxWidth() - .padding(top = 10.dp) - .onFocusChanged { focusState -> - isFocused = focusState.isFocused - } - .semantics(mergeDescendants = true) {}, + modifier = modifier, + interactionSource = interactionSource, enabled = enabled, + singleLine = singleLine, readOnly = readOnly, + textStyle = textStyle, visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, - interactionSource = interactionSource, - singleLine = singleLine, maxLines = maxLines, minLines = minLines, + keyboardOptions = keyboardOptions, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { innerTextField -> - Column { + ) { + TextFieldDefaults.DecorationBox( + value = value, + visualTransformation = VisualTransformation.None, + innerTextField = it, + singleLine = singleLine, + enabled = enabled, + interactionSource = interactionSource, + label = { Text( text = label, color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelLarge, - modifier = Modifier.align(alignment = Alignment.Start), + modifier = Modifier.padding(bottom = 10.dp), ) - - Spacer(modifier = Modifier.height(5.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - if (leadingIcon != null) { - leadingIcon() - } - - Box(modifier = Modifier.weight(1f)) { - innerTextField() - } - - if (trailingIcon != null) { - trailingIcon() - } - } - indicatorColor?.let { color -> - HorizontalDivider( - thickness = 1.dp, - color = if (isFocused) { - color - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f) - }, - ) - } ?: run { - HorizontalDivider( - thickness = 1.dp, - color = if (isFocused) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f) - }, - ) - } - } - }, - ) + }, + trailingIcon = trailingIcon, + leadingIcon = leadingIcon, + supportingText = supportingText, + colors = colors, + isError = isError, + contentPadding = PaddingValues(bottom = 10.dp), + container = { + TextFieldDefaults.Container( + enabled = enabled, + isError = isError, + colors = colors, + interactionSource = interactionSource, + shape = RectangleShape, + focusedIndicatorLineThickness = 1.dp, + unfocusedIndicatorLineThickness = 1.dp, + ) + }, + ) + } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MifosCustomTextField( - value: String, - onValueChange: (String) -> Unit, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, label: String, modifier: Modifier = Modifier .fillMaxWidth() @@ -296,9 +263,7 @@ fun MifosCustomTextField( keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), trailingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, - supportingText: - @Composable() - (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, ) { val colors = TextFieldDefaults.colors().copy( cursorColor = MaterialTheme.colorScheme.primary, @@ -327,7 +292,7 @@ fun MifosCustomTextField( cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), ) { TextFieldDefaults.DecorationBox( - value = value, + value = value.text, visualTransformation = VisualTransformation.None, innerTextField = it, singleLine = singleLine, @@ -362,43 +327,26 @@ fun MifosCustomTextField( } } -@Preview @Composable -fun MfOutlinedTextFieldPreview() { - MifosTheme { - Box( - modifier = Modifier.background(color = MaterialTheme.colorScheme.surface), - ) { - MfOutlinedTextField( - value = "Text Field Value", - label = "Text Field", - onValueChange = { }, - modifier = Modifier, - isError = true, - errorMessage = "Error Message", - onKeyboardActions = { }, - ) - } - } -} - -@Preview -@Composable -fun MfPasswordTextFieldPreview() { - MifosTheme { - val password = " " - Box( - modifier = Modifier.background(color = Color.White), +private fun ClearIconButton( + showClearIcon: Boolean, + clearIcon: ImageVector, + onClickClearIcon: () -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = showClearIcon, + modifier = modifier, + ) { + IconButton( + onClick = onClickClearIcon, + modifier = Modifier.semantics { + contentDescription = "clearIcon" + }, ) { - MfPasswordTextField( - password = password, - label = "Password", - isError = true, - isPasswordVisible = true, - onTogglePasswordVisibility = { }, - onPasswordChange = { }, - modifier = Modifier.fillMaxWidth(), - errorMessage = "Password must be at least 6 characters", + Icon( + imageVector = clearIcon, + contentDescription = "trailingIcon", ) } } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt index 6c40a12c8..af2304740 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt @@ -35,6 +35,7 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Info @@ -99,6 +100,7 @@ object MifosIcons { val Camera = Icons.Filled.Camera val PhotoLibrary = Icons.Filled.PhotoLibrary val Delete = Icons.Filled.Delete + val OutlinedDelete = Icons.Outlined.DeleteOutline val RoundedInfo = Icons.Rounded.Info val Contact = Icons.Rounded.Contacts val Settings = Icons.Rounded.Settings diff --git a/core/domain/src/commonMain/kotlin/org/mifospay/core/domain/LoginUseCase.kt b/core/domain/src/commonMain/kotlin/org/mifospay/core/domain/LoginUseCase.kt index 95fc90a4d..888df68d8 100644 --- a/core/domain/src/commonMain/kotlin/org/mifospay/core/domain/LoginUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/mifospay/core/domain/LoginUseCase.kt @@ -10,7 +10,7 @@ package org.mifospay.core.domain import kotlinx.coroutines.CoroutineDispatcher -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.repository.AuthenticationRepository import org.mifospay.core.data.repository.ClientRepository import org.mifospay.core.datastore.UserPreferencesRepository @@ -22,44 +22,44 @@ class LoginUseCase( private val userPreferencesRepository: UserPreferencesRepository, private val ioDispatcher: CoroutineDispatcher, ) { - suspend operator fun invoke(username: String, password: String): Result { + suspend operator fun invoke(username: String, password: String): DataState { return when (val result = repository.authenticate(username, password)) { - is Result.Loading -> Result.Loading - is Result.Error -> Result.Error(Exception("Invalid credentials")) - is Result.Success -> { + is DataState.Loading -> DataState.Loading + is DataState.Error -> DataState.Error(Exception("Invalid credentials")) + is DataState.Success -> { if (result.data.clients.isEmpty()) { - return Result.Error(Exception("No clients found")) + return DataState.Error(Exception("No clients found")) } updateUserInfo(result.data) } } } - private suspend fun updateUserInfo(userInfo: UserInfo): Result { + private suspend fun updateUserInfo(userInfo: UserInfo): DataState { val updateResult = userPreferencesRepository.updateToken(userInfo.base64EncodedAuthenticationKey) return when (updateResult) { - is Result.Success -> updateClientInfo(userInfo) - is Result.Error -> Result.Error(Exception("Something went wrong")) - is Result.Loading -> Result.Loading + is DataState.Success -> updateClientInfo(userInfo) + is DataState.Error -> DataState.Error(Exception("Something went wrong")) + is DataState.Loading -> DataState.Loading } } - private suspend fun updateClientInfo(userInfo: UserInfo): Result { + private suspend fun updateClientInfo(userInfo: UserInfo): DataState { return when (val clientInfo = clientRepository.getClient(userInfo.clients.first())) { - is Result.Success -> { + is DataState.Success -> { userPreferencesRepository.updateClientInfo(clientInfo.data) userPreferencesRepository.updateUserInfo(userInfo) - Result.Success(userInfo) + DataState.Success(userInfo) } - is Result.Error -> { + is DataState.Error -> { userPreferencesRepository.logOut() - Result.Error(Exception("No client found")) + DataState.Error(Exception("No client found")) } - is Result.Loading -> Result.Loading + is DataState.Loading -> DataState.Loading } } } diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/Account.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/Account.kt index 7970d27da..e81dcef65 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/Account.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/Account.kt @@ -12,6 +12,7 @@ package org.mifospay.core.model.account import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize import org.mifospay.core.model.savingsaccount.Currency +import org.mifospay.core.model.savingsaccount.Status @Parcelize data class Account( @@ -22,4 +23,5 @@ data class Account( val id: Long = 0L, val productId: Long = 0L, val currency: Currency, + val status: Status, ) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/AccountWithTransactions.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/AccountContent.kt similarity index 63% rename from core/model/src/commonMain/kotlin/org/mifospay/core/model/client/AccountWithTransactions.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/account/AccountContent.kt index b7d08db38..5073b7d44 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/AccountWithTransactions.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/AccountContent.kt @@ -7,15 +7,14 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.model.client +package org.mifospay.core.model.account import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize -import org.mifospay.core.model.account.Account -import org.mifospay.core.model.savingsaccount.Transaction +import org.mifospay.core.model.beneficiary.Beneficiary @Parcelize -data class AccountWithTransactions( - val account: Account, - val transactions: List, +data class AccountContent( + val accounts: List, + val beneficiaries: List, ) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/AccountsWithTransactions.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/AccountsWithTransactions.kt new file mode 100644 index 000000000..e332db448 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/account/AccountsWithTransactions.kt @@ -0,0 +1,17 @@ +/* + * 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.core.model.account + +import org.mifospay.core.model.savingsaccount.Transaction + +data class AccountsWithTransactions( + val accounts: List, + val transactions: List, +) diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/bank/BankAccountDetails.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/bank/BankAccountDetails.kt index 9d57090ca..52c204642 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/bank/BankAccountDetails.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/bank/BankAccountDetails.kt @@ -14,6 +14,7 @@ import org.mifospay.core.common.Parcelize @Parcelize data class BankAccountDetails( + val accountNo: String, val bankName: String?, val accountHolderName: String?, val branch: String?, diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/Beneficiary.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/Beneficiary.kt new file mode 100644 index 000000000..7f09f88d1 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/Beneficiary.kt @@ -0,0 +1,35 @@ +/* + * 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.core.model.beneficiary + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class Beneficiary( + val id: Long, + val name: String, + val officeName: String, + val clientName: String, + val accountType: AccountType, + val accountNumber: String, + val transferLimit: Int = 0, +) : Parcelable { + + @Serializable + @Parcelize + data class AccountType( + val id: Int, + val code: String, + val value: String, + ) : Parcelable +} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/BeneficiaryPayload.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/BeneficiaryPayload.kt similarity index 93% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/BeneficiaryPayload.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/BeneficiaryPayload.kt index 970ddef75..0bdcadb4b 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/BeneficiaryPayload.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/BeneficiaryPayload.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.beneficary +package org.mifospay.core.model.beneficiary import kotlinx.serialization.Serializable diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/BeneficiaryUpdatePayload.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/BeneficiaryUpdatePayload.kt similarity index 89% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/BeneficiaryUpdatePayload.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/BeneficiaryUpdatePayload.kt index 155ac3643..0213b6d6f 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/BeneficiaryUpdatePayload.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/beneficiary/BeneficiaryUpdatePayload.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.beneficary +package org.mifospay.core.model.beneficiary import kotlinx.serialization.Serializable diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/SavingAccountsListResponse.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/AccountType.kt similarity index 60% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/SavingAccountsListResponse.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/AccountType.kt index 989b4651c..fe4b12805 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/SavingAccountsListResponse.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/AccountType.kt @@ -7,12 +7,16 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.accounts +package org.mifospay.core.model.savingsaccount import kotlinx.serialization.Serializable -import org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize @Serializable -data class SavingAccountsListResponse( - var savingsAccounts: List = ArrayList(), -) +@Parcelize +data class AccountType( + val id: Long, + val code: String, + val value: String, +) : Parcelable diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/BlockUnblockResponseEntity.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/BlockUnblockResponseEntity.kt similarity index 90% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/BlockUnblockResponseEntity.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/BlockUnblockResponseEntity.kt index 6c3b82790..0ef5bcc0d 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/BlockUnblockResponseEntity.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/BlockUnblockResponseEntity.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.accounts.savings +package org.mifospay.core.model.savingsaccount import kotlinx.serialization.Serializable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/CreateNewSavingEntity.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/CreateNewSavingEntity.kt new file mode 100644 index 000000000..5443239c0 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/CreateNewSavingEntity.kt @@ -0,0 +1,32 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class CreateNewSavingEntity( + val clientId: String, + val productId: Long, + val nominalAnnualInterestRate: Double, + val minRequiredOpeningBalance: Long, + val withdrawalFeeForTransfers: Boolean, + val allowOverdraft: Boolean, + val overdraftLimit: String, + val enforceMinRequiredBalance: Boolean, + val withHoldTax: Boolean, + val externalId: String, + val submittedOnDate: String, + val locale: String, + val dateFormat: String, +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Currency.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Currency.kt index 430776817..2b4889bd7 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Currency.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Currency.kt @@ -17,6 +17,10 @@ import org.mifospay.core.common.Parcelize @Serializable data class Currency( val code: String, + val name: String, + val decimalPlaces: Int, + val inMultiplesOf: Int, val displaySymbol: String, + val nameCode: String, val displayLabel: String, ) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/DepositType.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/DepositType.kt index d78b7aa2c..43b79a3c9 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/DepositType.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/DepositType.kt @@ -9,12 +9,47 @@ */ package org.mifospay.core.model.savingsaccount +import kotlinx.serialization.Serializable import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize @Parcelize +@Serializable data class DepositType( - val id: Int?, - val code: String?, - val value: String?, -) : Parcelable + val id: Int, + val code: String, + val value: String, +) : Parcelable { + val isRecurring: Boolean + get() = ServerTypes.RECURRING.id == id + val endpoint: String + get() = ServerTypes.fromId( + id!!, + ).endpoint + val serverType: ServerTypes + get() = ServerTypes.fromId( + id!!, + ) + + enum class ServerTypes(val id: Int, val code: String, val endpoint: String) { + SAVINGS(id = 100, code = "depositAccountType.savingsDeposit", endpoint = "savingsaccounts"), + FIXED(id = 200, code = "depositAccountType.fixedDeposit", endpoint = "savingsaccounts"), + RECURRING( + id = 300, + code = "depositAccountType.recurringDeposit", + endpoint = "recurringdepositaccounts", + ), + ; + + companion object { + fun fromId(id: Int): ServerTypes { + for (type in entries) { + if (type.id == id) { + return type + } + } + return ServerTypes.SAVINGS + } + } + } +} diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/InterestPeriod.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/InterestPeriod.kt new file mode 100644 index 000000000..d7ab4cd0e --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/InterestPeriod.kt @@ -0,0 +1,22 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class InterestPeriod( + val id: Long = 0, + val code: String = "", + val value: String = "", +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/PaymentDetailData.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/PaymentDetailData.kt new file mode 100644 index 000000000..b6293486f --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/PaymentDetailData.kt @@ -0,0 +1,26 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class PaymentDetailData( + val id: Long, + val paymentType: PaymentType, + val accountNumber: String, + val checkNumber: String, + val routingCode: String, + val receiptNumber: String, + val bankNumber: String, +) : Parcelable diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/PaymentType.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/PaymentType.kt similarity index 75% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/PaymentType.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/PaymentType.kt index 718708d0f..51e07674a 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/PaymentType.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/PaymentType.kt @@ -7,13 +7,16 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.accounts.savings +package org.mifospay.core.model.savingsaccount import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize @Serializable +@Parcelize data class PaymentType( val id: Int? = null, val name: String? = null, val isSystemDefined: Boolean, -) +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccount.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccount.kt index 61a1d802e..bfd7deba4 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccount.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccount.kt @@ -16,18 +16,16 @@ import org.mifospay.core.common.Parcelize data class SavingAccount( val id: Long, val accountNo: String, + val productId: Long, val productName: String, - val productId: Int, - val overdraftLimit: Long, - val minRequiredBalance: Long, - val accountBalance: Double, - val totalDeposits: Double, - val savingsProductName: String?, - val clientName: String?, - val savingsProductId: String?, - val nominalAnnualInterestRate: Double, - val status: Status?, + val shortProductName: String, + val status: Status, val currency: Currency, + val accountBalance: Double, + val accountType: AccountType, + val timeline: Timeline, + val subStatus: SubStatus, + val lastActiveTransactionDate: List, val depositType: DepositType?, - val isRecurring: Boolean, + val externalId: String?, ) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountDetail.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountDetail.kt new file mode 100644 index 000000000..c5276e122 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountDetail.kt @@ -0,0 +1,51 @@ +/* + * 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.core.model.savingsaccount + +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.model.account.Account + +@Parcelize +data class SavingAccountDetail( + val id: Long, + val accountNo: String, + val depositType: DepositType, + val clientId: Long, + val clientName: String, + val savingsProductId: Long, + val savingsProductName: String, + val fieldOfficerId: Long, + val status: Status, + val timeline: Timeline, + val currency: Currency, + val nominalAnnualInterestRate: Double, + val withdrawalFeeForTransfers: Boolean, + val allowOverdraft: Boolean, + val enforceMinRequiredBalance: Boolean, + val lienAllowed: Boolean, + val withHoldTax: Boolean, + val lastActiveTransactionDate: List, + val isDormancyTrackingActive: Boolean, + val summary: Summary, + val transactions: List, +) : Parcelable + +fun SavingAccountDetail.toAccount(): Account { + return Account( + name = savingsProductName, + number = accountNo, + balance = summary.accountBalance, + id = id, + productId = savingsProductId, + currency = currency, + status = status, + ) +} diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountEntity.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountEntity.kt new file mode 100644 index 000000000..734589faa --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountEntity.kt @@ -0,0 +1,30 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable + +@Serializable +data class SavingAccountEntity( + val id: Long, + val accountNo: String, + val productId: Long, + val productName: String, + val shortProductName: String, + val status: Status, + val currency: Currency, + val accountBalance: Double = 0.0, + val accountType: AccountType, + val timeline: Timeline, + val subStatus: SubStatus, + val lastActiveTransactionDate: List = emptyList(), + val depositType: DepositType?, + val externalId: String?, +) diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountTemplate.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountTemplate.kt new file mode 100644 index 000000000..d95149c96 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingAccountTemplate.kt @@ -0,0 +1,30 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Parcelize +@Serializable +data class SavingAccountTemplate( + val clientId: String, + val clientName: String, + val nominalAnnualInterestRate: Double, + val withdrawalFeeForTransfers: Boolean, + val allowOverdraft: Boolean, + val enforceMinRequiredBalance: Boolean, + val lienAllowed: Boolean, + val withHoldTax: Boolean, + val isDormancyTrackingActive: Boolean, + val productOptions: List, + val chargeOptions: List = emptyList(), +) : Parcelable diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Transfer.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingCharge.kt similarity index 62% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Transfer.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingCharge.kt index e00e68989..9fb84c14d 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Transfer.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingCharge.kt @@ -7,13 +7,15 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.accounts.savings +package org.mifospay.core.model.savingsaccount import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +@Parcelize @Serializable -data class Transfer( - val id: Long = 0L, -) { - constructor() : this(id = 0L) -} +data class SavingCharge( + val chargeId: Long, + val amount: Long, +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingProductOption.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingProductOption.kt new file mode 100644 index 000000000..f11628ba4 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingProductOption.kt @@ -0,0 +1,26 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Parcelize +@Serializable +data class SavingProductOption( + val id: Long, + val name: String, + val withdrawalFeeForTransfers: Boolean, + val allowOverdraft: Boolean, + val enforceMinRequiredBalance: Boolean, + val lienAllowed: Boolean, + val withHoldTax: Boolean, +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingsWithAssociationsEntity.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingsWithAssociationsEntity.kt new file mode 100644 index 000000000..a63d016ae --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SavingsWithAssociationsEntity.kt @@ -0,0 +1,42 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable + +@Serializable +data class SavingsWithAssociationsEntity( + val id: Long, + val accountNo: String, + val depositType: DepositType, + val clientId: Long, + val clientName: String, + val savingsProductId: Long, + val savingsProductName: String, + val fieldOfficerId: Long, + val status: Status, + val subStatus: SubStatus, + val timeline: Timeline, + val currency: Currency, + val nominalAnnualInterestRate: Double, + val interestCompoundingPeriodType: InterestPeriod = InterestPeriod(), + val interestPostingPeriodType: InterestPeriod = InterestPeriod(), + val interestCalculationType: InterestPeriod = InterestPeriod(), + val interestCalculationDaysInYearType: InterestPeriod = InterestPeriod(), + val withdrawalFeeForTransfers: Boolean, + val allowOverdraft: Boolean, + val enforceMinRequiredBalance: Boolean, + val lienAllowed: Boolean, + val withHoldTax: Boolean, + val lastActiveTransactionDate: List = emptyList(), + val isDormancyTrackingActive: Boolean, + val summary: Summary, + val transactions: List = emptyList(), +) diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Status.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Status.kt index ac00755b5..787e10074 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Status.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Status.kt @@ -9,22 +9,24 @@ */ package org.mifospay.core.model.savingsaccount +import kotlinx.serialization.Serializable import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize @Parcelize +@Serializable data class Status( - val id: Int?, - val code: String?, - val value: String?, - val submittedAndPendingApproval: Boolean?, - val approved: Boolean?, - val rejected: Boolean?, - val withdrawnByApplicant: Boolean?, - val active: Boolean?, - val closed: Boolean?, - val prematureClosed: Boolean?, - val transferInProgress: Boolean?, - val transferOnHold: Boolean?, - val matured: Boolean?, + val id: Int, + val code: String, + val value: String, + val submittedAndPendingApproval: Boolean, + val approved: Boolean, + val rejected: Boolean, + val withdrawnByApplicant: Boolean, + val active: Boolean, + val closed: Boolean, + val prematureClosed: Boolean, + val transferInProgress: Boolean, + val transferOnHold: Boolean, + val matured: Boolean, ) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SubStatus.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SubStatus.kt new file mode 100644 index 000000000..ecb796aa1 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/SubStatus.kt @@ -0,0 +1,29 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class SubStatus( + val id: Long, + val code: String, + val value: String, + val none: Boolean, + val inactive: Boolean, + val dormant: Boolean, + val escheat: Boolean, + val block: Boolean, + val blockCredit: Boolean, + val blockDebit: Boolean, +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Summary.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Summary.kt new file mode 100644 index 000000000..542d2ce44 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Summary.kt @@ -0,0 +1,36 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.CurrencyFormatter +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class Summary( + val currency: Currency, + val totalDeposits: Double = 0.0, + val totalWithdrawals: Double = 0.0, + val totalInterestPosted: Long = 0, + val accountBalance: Double = 0.0, + val totalOverdraftInterestDerived: Long = 0, + val interestNotPosted: Long = 0, + val availableBalance: Double = 0.0, +) : Parcelable + +fun Summary.formatAmount(amount: Double): String { + return CurrencyFormatter.format( + balance = amount, + currencyCode = currency.code, + maximumFractionDigits = null, + ) +} diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Timeline.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Timeline.kt new file mode 100644 index 000000000..18a50c200 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Timeline.kt @@ -0,0 +1,31 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class Timeline( + val submittedOnDate: List = emptyList(), + val submittedByUsername: String = "", + val submittedByFirstname: String = "", + val submittedByLastname: String = "", + val approvedOnDate: List = emptyList(), + val approvedByUsername: String = "", + val approvedByFirstname: String = "", + val approvedByLastname: String = "", + val activatedOnDate: List = emptyList(), + val activatedByUsername: String = "", + val activatedByFirstname: String = "", + val activatedByLastname: String = "", +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transaction.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transaction.kt index 2ae5f6085..032ef6fa4 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transaction.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transaction.kt @@ -25,24 +25,12 @@ data class Transaction( val transferId: Long?, val originalTransactionId: Long, val paymentDetailId: Long?, -) : Parcelable - -@Serializable -data class TransactionInfo( - val id: Long, - val officeId: Long, - val officeName: String, - val type: Type, - val date: List, - val currency: Currency, - val amount: Double, - val submittedOnDate: List, - val reversed: Boolean, -) - -@Serializable -data class Type( - val id: Long, - val code: String, - val value: String, -) +) : Parcelable { + @Serializable + @Parcelize + data class Type( + val id: Long, + val code: String, + val value: String, + ) : Parcelable +} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TransactionsEntity.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/TransactionsEntity.kt similarity index 62% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TransactionsEntity.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/TransactionsEntity.kt index 2f3219282..481b360c5 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TransactionsEntity.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/TransactionsEntity.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.accounts.savings +package org.mifospay.core.model.savingsaccount import kotlinx.serialization.Serializable @@ -19,7 +19,7 @@ data class TransactionsEntity( val accountId: Long, val accountNo: String, val date: List, - val currency: CurrencyEntity, + val currency: Currency, val amount: Double, val runningBalance: Double, val reversed: Boolean, @@ -34,17 +34,6 @@ data class TransactionsEntity( val releaseTransactionId: Long, val paymentDetailData: PaymentDetailData? = null, ) { - @Serializable - data class Currency( - val code: String, - val name: String, - val decimalPlaces: Long, - val inMultiplesOf: Long, - val displaySymbol: String, - val nameCode: String, - val displayLabel: String, - ) - @Serializable data class TransactionType( val id: Long, @@ -67,32 +56,4 @@ data class TransactionsEntity( val amountHold: Boolean, val amountRelease: Boolean, ) - - @Serializable - data class Transfer( - val id: Long, - val reversed: Boolean, - val currency: Currency, - val transferAmount: Double, - val transferDate: List, - val transferDescription: String, - ) - - @Serializable - data class PaymentDetailData( - val id: Long, - val paymentType: PaymentType, - val accountNumber: String, - val checkNumber: String, - val routingCode: String, - val receiptNumber: String, - val bankNumber: String, - ) - - @Serializable - data class PaymentType( - val id: Long, - val name: String, - val isSystemDefined: Boolean, - ) } diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transfer.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transfer.kt new file mode 100644 index 000000000..6984f81c9 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/Transfer.kt @@ -0,0 +1,25 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Parcelize +@Serializable +data class Transfer( + val id: Long, + val reversed: Boolean, + val currency: Currency, + val transferAmount: Double, + val transferDate: List, + val transferDescription: String, +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/UpdateSavingAccountEntity.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/UpdateSavingAccountEntity.kt new file mode 100644 index 000000000..5a71e04d6 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/savingsaccount/UpdateSavingAccountEntity.kt @@ -0,0 +1,21 @@ +/* + * 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.core.model.savingsaccount + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Serializable +@Parcelize +data class UpdateSavingAccountEntity( + val clientId: String, + val productId: Long, +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/utils/Locale.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/utils/Locale.kt new file mode 100644 index 000000000..9f03f8f46 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/utils/Locale.kt @@ -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 + */ +package org.mifospay.core.model.utils + +import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Parcelize +@Serializable +data class Locale( + val countryName: String, + val localName: String, + val dominantName: String, +) : Parcelable + +fun List.filterLocales(filterText: String): List { + return if (filterText.isNotEmpty()) { + this.filter { + it.countryName.contains(filterText, true) || + it.localName.contains(filterText, true) || + it.dominantName.contains(filterText, true) + } + } else { + this + } +} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/CurrencyEntity.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/CurrencyEntity.kt deleted file mode 100644 index d15b53f95..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/CurrencyEntity.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable - -@Serializable -data class CurrencyEntity( - val code: String = "", - val name: String = "", - val decimalPlaces: Int? = null, - val inMultiplesOf: Int? = null, - val displaySymbol: String = "", - val nameCode: String = "", - val displayLabel: String = "", -) { - constructor() : this( - code = "", - name = "", - decimalPlaces = 0, - inMultiplesOf = 0, - displaySymbol = "", - nameCode = "", - displayLabel = "", - ) -} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/PaymentDetailData.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/PaymentDetailData.kt deleted file mode 100644 index e5531fe70..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/PaymentDetailData.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable - -@Serializable -data class PaymentDetailData( - val id: Int? = null, - val paymentType: org.mifospay.core.network.model.entity.accounts.savings.PaymentType? = null, - val accountNumber: String? = null, - val checkNumber: String? = null, - val routingCode: String? = null, - val receiptNumber: String? = null, - val bankNumber: String? = null, -) { - constructor() : this( - id = null, - paymentType = null, - accountNumber = null, - checkNumber = null, - routingCode = null, - receiptNumber = null, - bankNumber = null, - ) -} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingAccountEntity.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingAccountEntity.kt deleted file mode 100644 index 018a4eca9..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingAccountEntity.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable -import org.mifospay.core.network.model.entity.client.DepositTypeEntity -import org.mifospay.core.network.model.entity.templates.account.AccountType - -@Serializable -data class SavingAccountEntity( - val id: Long = 0L, - val accountNo: String = "", - val productName: String = "", - val productId: Int = 0, - val overdraftLimit: Long = 0L, - val minRequiredBalance: Long = 0L, - val accountBalance: Double = 0.0, - val totalDeposits: Double = 0.0, - val savingsProductName: String? = null, - val clientName: String? = null, - val savingsProductId: String? = null, - val nominalAnnualInterestRate: Double = 0.0, - val status: StatusEntity? = null, - val currency: CurrencyEntity = CurrencyEntity(), - val depositType: DepositTypeEntity? = null, - val shortProductName: String? = null, - val accountType: AccountType = AccountType(), - val timeline: TimelineEntity = TimelineEntity(), - val subStatus: SubStatus = SubStatus(), - val lastActiveTransactionDate: List = emptyList(), - val externalId: String? = null, -) { - fun isRecurring(): Boolean { - return this.depositType != null && this.depositType.isRecurring - } - - constructor() : this( - id = 0L, - accountNo = "", - productName = "", - productId = 0, - overdraftLimit = 0L, - minRequiredBalance = 0L, - accountBalance = 0.0, - totalDeposits = 0.0, - savingsProductName = "", - clientName = "", - savingsProductId = "", - nominalAnnualInterestRate = 0.0, - status = StatusEntity(), - currency = CurrencyEntity(), - depositType = DepositTypeEntity(), - shortProductName = "", - accountType = AccountType(), - timeline = TimelineEntity(), - subStatus = SubStatus(), - lastActiveTransactionDate = emptyList(), - ) -} - -@Serializable -data class SubStatus( - val id: Long, - val code: String, - val value: String, - val none: Boolean, - val inactive: Boolean, - val dormant: Boolean, - val escheat: Boolean, - val block: Boolean, - val blockCredit: Boolean, - val blockDebit: Boolean, -) { - constructor() : this( - id = 0L, - code = "", - value = "", - none = false, - inactive = false, - dormant = false, - escheat = false, - block = false, - blockCredit = false, - blockDebit = false, - ) -} - -@Serializable -data class TimelineEntity( - val submittedOnDate: List = emptyList(), - val submittedByUsername: String? = null, - val submittedByFirstname: String? = null, - val submittedByLastname: String? = null, - val approvedOnDate: List = emptyList(), - val approvedByUsername: String? = null, - val approvedByFirstname: String? = null, - val approvedByLastname: String? = null, - val activatedOnDate: List = emptyList(), -) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingsWithAssociationsEntity.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingsWithAssociationsEntity.kt deleted file mode 100644 index 4e60200f6..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/SavingsWithAssociationsEntity.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable -import org.mifospay.core.network.model.entity.client.DepositTypeEntity - -@Serializable -data class SavingsWithAssociationsEntity( - val id: Long = 0L, - val accountNo: String? = null, - val depositType: DepositTypeEntity? = null, - val clientId: Int = 0, - val clientName: String = "", - val savingsProductId: Int? = null, - val savingsProductName: String? = null, - val fieldOfficerId: Int? = null, - val status: StatusEntity? = null, - val timeline: TimeLine? = null, - val currency: CurrencyEntity? = null, - val nominalAnnualInterestRate: Double? = null, - val withdrawalFeeForTransfers: Boolean? = null, - val allowOverdraft: Boolean? = null, - val enforceMinRequiredBalance: Boolean? = null, - val withHoldTax: Boolean? = null, - val lastActiveTransactionDate: List? = null, - val summary: Summary? = null, - val transactions: List = ArrayList(), - val subStatus: SubStatus = SubStatus(), - val interestCompoundingPeriodType: InterestPeriod = InterestPeriod(), - val interestPostingPeriodType: InterestPeriod = InterestPeriod(), - val interestCalculationType: InterestPeriod = InterestPeriod(), - val interestCalculationDaysInYearType: InterestPeriod = InterestPeriod(), - val lienAllowed: Boolean = false, - val isDormancyTrackingActive: Boolean = false, -) - -@Serializable -data class InterestPeriod( - val id: Long = 0, - val code: String = "", - val value: String = "", -) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/StatusEntity.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/StatusEntity.kt deleted file mode 100644 index eacfa7ca7..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/StatusEntity.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable - -@Serializable -data class StatusEntity( - val id: Int? = null, - val code: String? = null, - val value: String? = null, - val submittedAndPendingApproval: Boolean? = null, - val approved: Boolean? = null, - val rejected: Boolean? = null, - val withdrawnByApplicant: Boolean? = null, - val active: Boolean? = null, - val closed: Boolean? = null, - val prematureClosed: Boolean? = null, - val transferInProgress: Boolean? = null, - val transferOnHold: Boolean? = null, - val matured: Boolean? = null, -) { - constructor() : this( - id = null, - code = null, - value = null, - submittedAndPendingApproval = null, - approved = null, - rejected = null, - withdrawnByApplicant = null, - active = null, - closed = null, - prematureClosed = null, - transferInProgress = null, - transferOnHold = null, - matured = null, - ) -} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Summary.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Summary.kt deleted file mode 100644 index 194f2e391..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/Summary.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable - -@Serializable -data class Summary( - val currency: org.mifospay.core.network.model.entity.accounts.savings.CurrencyEntity? = null, - val totalDeposits: Double? = null, - val totalWithdrawals: Double? = null, - val totalInterestEarned: Double? = null, - val totalInterestPosted: Double? = null, - val accountBalance: Double? = null, - val totalOverdraftInterestDerived: Double? = null, - val interestNotPosted: Double? = null, - val lastInterestCalculationDate: List? = null, -) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TimeLine.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TimeLine.kt deleted file mode 100644 index 22c94a461..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TimeLine.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable - -@Serializable -data class TimeLine( - val submittedOnDate: List = ArrayList(), - val submittedByUsername: String? = null, - val submittedByFirstname: String? = null, - val submittedByLastname: String? = null, - val approvedOnDate: List = ArrayList(), - val approvedByUsername: String? = null, - val approvedByFirstname: String? = null, - val approvedByLastname: String? = null, - val activatedOnDate: List? = null, - val activatedByUsername: String? = null, - val activatedByFirstname: String? = null, - val activatedByLastname: String? = null, -) { - constructor() : this( - submittedOnDate = ArrayList(), - submittedByUsername = null, - submittedByFirstname = null, - submittedByLastname = null, - approvedOnDate = ArrayList(), - approvedByUsername = null, - approvedByFirstname = null, - approvedByLastname = null, - activatedOnDate = null, - activatedByUsername = null, - activatedByFirstname = null, - activatedByLastname = null, - ) -} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TransactionType.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TransactionType.kt deleted file mode 100644 index 81c623fa0..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/accounts/savings/TransactionType.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.core.network.model.entity.accounts.savings - -import kotlinx.serialization.Serializable - -@Serializable -data class TransactionType( - val id: Int? = null, - val code: String? = null, - val value: String? = null, - val deposit: Boolean = false, - val dividendPayout: Boolean = false, - val withdrawal: Boolean = false, - val interestPosting: Boolean = false, - val feeDeduction: Boolean = false, - val initiateTransfer: Boolean = false, - val approveTransfer: Boolean = false, - val withdrawTransfer: Boolean = false, - val rejectTransfer: Boolean = false, - val overdraftInterest: Boolean = false, - val writtenoff: Boolean = false, - val overdraftFee: Boolean = false, - val withholdTax: Boolean = false, - val escheat: Boolean? = null, - val amountHold: Boolean = false, - val amountRelease: Boolean = false, -) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/Beneficiary.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/Beneficiary.kt deleted file mode 100644 index da8b1f57f..000000000 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/beneficary/Beneficiary.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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.core.network.model.entity.beneficary - -import kotlinx.serialization.Serializable -import org.mifospay.core.network.model.entity.templates.account.AccountType - -@Serializable -data class Beneficiary( - val id: Int? = null, - val name: String? = null, - val officeName: String? = null, - val clientName: String? = null, - val accountType: org.mifospay.core.network.model.entity.templates.account.AccountType? = null, - val accountNumber: String? = null, - val transferLimit: Int = 0, -) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientAccountsEntity.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientAccountsEntity.kt index db6ce275d..62460d0cd 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientAccountsEntity.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientAccountsEntity.kt @@ -10,7 +10,7 @@ package org.mifospay.core.network.model.entity.client import kotlinx.serialization.Serializable -import org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity +import org.mifospay.core.model.savingsaccount.SavingAccountEntity @Serializable data class ClientAccountsEntity( @@ -32,7 +32,7 @@ data class ClientAccountsEntity( private fun getSavingsAccounts(wantRecurring: Boolean): List { val result: MutableList = ArrayList() for (account in savingsAccounts) { - if (account.isRecurring() == wantRecurring) { + if (account.depositType?.isRecurring == wantRecurring) { result.add(account) } } diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/DepositTypeEntity.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/DepositTypeEntity.kt index bab9669bf..b51f4f95b 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/DepositTypeEntity.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/DepositTypeEntity.kt @@ -16,37 +16,4 @@ data class DepositTypeEntity( val id: Int? = null, val code: String? = null, val value: String? = null, -) { - val isRecurring: Boolean - get() = ServerTypes.RECURRING.id == id - val endpoint: String - get() = ServerTypes.fromId( - id!!, - ).endpoint - val serverType: ServerTypes - get() = ServerTypes.fromId( - id!!, - ) - - enum class ServerTypes(val id: Int, val code: String, val endpoint: String) { - SAVINGS(id = 100, code = "depositAccountType.savingsDeposit", endpoint = "savingsaccounts"), - FIXED(id = 200, code = "depositAccountType.fixedDeposit", endpoint = "savingsaccounts"), - RECURRING( - id = 300, - code = "depositAccountType.recurringDeposit", - endpoint = "recurringdepositaccounts", - ), - ; - - companion object { - fun fromId(id: Int): ServerTypes { - for (type in entries) { - if (type.id == id) { - return type - } - } - return ServerTypes.SAVINGS - } - } - } -} +) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/payload/ClientPayload.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/payload/ClientPayload.kt index f67b18b27..0e3ff7d3f 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/payload/ClientPayload.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/payload/ClientPayload.kt @@ -29,5 +29,5 @@ data class ClientPayload( val clientClassificationId: Int? = null, val dateFormat: String? = "DD_MMMM_YYYY", val locale: String? = "en", - val datatables: List = ArrayList(), + val datatables: List = ArrayList(), ) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/standinginstruction/StandingInstruction.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/standinginstruction/StandingInstruction.kt index 5db037f92..95dc21beb 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/standinginstruction/StandingInstruction.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/standinginstruction/StandingInstruction.kt @@ -10,16 +10,19 @@ package org.mifospay.core.network.model.entity.standinginstruction import kotlinx.serialization.Serializable +import org.mifospay.core.model.savingsaccount.SavingAccountEntity +import org.mifospay.core.network.model.entity.client.ClientEntity +import org.mifospay.core.network.model.entity.client.Status @Serializable data class StandingInstruction( val id: Long, val name: String, - val fromClient: org.mifospay.core.network.model.entity.client.ClientEntity, - val fromAccount: org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity, - val toClient: org.mifospay.core.network.model.entity.client.ClientEntity, - val toAccount: org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity, - val status: org.mifospay.core.network.model.entity.client.Status, + val fromClient: ClientEntity, + val fromAccount: SavingAccountEntity, + val toClient: ClientEntity, + val toAccount: SavingAccountEntity, + val status: Status, val amount: Double, val validFrom: List, val validTill: List?, diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/account/AccountOptionsTemplate.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/account/AccountOptionsTemplate.kt index 65b4456a8..e05ce28b7 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/account/AccountOptionsTemplate.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/account/AccountOptionsTemplate.kt @@ -13,6 +13,6 @@ import kotlinx.serialization.Serializable @Serializable data class AccountOptionsTemplate( - val fromAccountOptions: List? = listOf(), - val toAccountOptions: List? = listOf(), + val fromAccountOptions: List? = listOf(), + val toAccountOptions: List? = listOf(), ) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/beneficiary/BeneficiaryTemplate.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/beneficiary/BeneficiaryTemplate.kt index 9d50bda1f..d76c26b9d 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/beneficiary/BeneficiaryTemplate.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/templates/beneficiary/BeneficiaryTemplate.kt @@ -13,5 +13,5 @@ import kotlinx.serialization.Serializable @Serializable data class BeneficiaryTemplate( - val accountTypeOptions: List? = null, + val accountTypeOptions: List? = null, ) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/AccountTransfersService.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/AccountTransfersService.kt index f0aeb30ce..c50e08463 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/AccountTransfersService.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/AccountTransfersService.kt @@ -12,8 +12,8 @@ package org.mifospay.core.network.services import de.jensklingenberg.ktorfit.http.GET import de.jensklingenberg.ktorfit.http.Path import kotlinx.coroutines.flow.Flow +import org.mifospay.core.model.savingsaccount.TransactionsEntity import org.mifospay.core.model.savingsaccount.TransferDetail -import org.mifospay.core.network.model.entity.accounts.savings.TransactionsEntity import org.mifospay.core.network.utils.ApiEndPoints interface AccountTransfersService { diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/BeneficiaryService.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/BeneficiaryService.kt index 8723689ff..144b49856 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/BeneficiaryService.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/BeneficiaryService.kt @@ -16,29 +16,28 @@ import de.jensklingenberg.ktorfit.http.POST import de.jensklingenberg.ktorfit.http.PUT import de.jensklingenberg.ktorfit.http.Path import kotlinx.coroutines.flow.Flow -import org.mifospay.core.network.model.CommonResponse -import org.mifospay.core.network.model.entity.beneficary.Beneficiary -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryPayload -import org.mifospay.core.network.model.entity.beneficary.BeneficiaryUpdatePayload +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.model.beneficiary.BeneficiaryPayload +import org.mifospay.core.model.beneficiary.BeneficiaryUpdatePayload import org.mifospay.core.network.model.entity.templates.beneficiary.BeneficiaryTemplate import org.mifospay.core.network.utils.ApiEndPoints interface BeneficiaryService { @GET(ApiEndPoints.BENEFICIARIES + "/tpt") - suspend fun beneficiaryList(): Flow> + fun beneficiaryList(): Flow> @GET(ApiEndPoints.BENEFICIARIES + "/tpt/template") suspend fun beneficiaryTemplate(): Flow @POST(ApiEndPoints.BENEFICIARIES + "/tpt") - suspend fun createBeneficiary(@Body beneficiaryPayload: BeneficiaryPayload): Flow + suspend fun createBeneficiary(@Body beneficiaryPayload: BeneficiaryPayload) @PUT(ApiEndPoints.BENEFICIARIES + "/tpt/{beneficiaryId}") suspend fun updateBeneficiary( @Path("beneficiaryId") beneficiaryId: Long, @Body payload: BeneficiaryUpdatePayload, - ): Flow + ) @DELETE(ApiEndPoints.BENEFICIARIES + "/tpt/{beneficiaryId}") - suspend fun deleteBeneficiary(@Path("beneficiaryId") beneficiaryId: Long): Flow + suspend fun deleteBeneficiary(@Path("beneficiaryId") beneficiaryId: Long): Unit } diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt index 0ad7e090a..b618063c4 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt @@ -56,10 +56,10 @@ interface ClientService { suspend fun getClientAccounts(@Path("clientId") clientId: Long): Flow @GET(ApiEndPoints.CLIENTS + "/{clientId}/accounts") - suspend fun getAccounts( + fun getAccounts( @Path("clientId") clientId: Long, @Query("fields") accountType: String, - ): ClientAccountsEntity + ): Flow @POST(ApiEndPoints.CLIENTS) suspend fun createClient(@Body newClient: NewClientEntity): ClientResponseEntity diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/RunReportService.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/RunReportService.kt index 9444a8312..573c3d839 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/RunReportService.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/RunReportService.kt @@ -12,7 +12,7 @@ package org.mifospay.core.network.services import de.jensklingenberg.ktorfit.http.GET import de.jensklingenberg.ktorfit.http.Query import kotlinx.coroutines.flow.Flow -import org.mifospay.core.network.model.entity.accounts.savings.TransactionsEntity +import org.mifospay.core.model.savingsaccount.TransactionsEntity import org.mifospay.core.network.utils.ApiEndPoints interface RunReportService { diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/SavingsAccountsService.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/SavingsAccountsService.kt index 567665af0..5018fd13a 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/SavingsAccountsService.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/SavingsAccountsService.kt @@ -15,20 +15,21 @@ import de.jensklingenberg.ktorfit.http.POST import de.jensklingenberg.ktorfit.http.Path import de.jensklingenberg.ktorfit.http.Query import kotlinx.coroutines.flow.Flow -import org.mifospay.core.network.model.GenericResponse +import org.mifospay.core.model.savingsaccount.BlockUnblockResponseEntity +import org.mifospay.core.model.savingsaccount.CreateNewSavingEntity +import org.mifospay.core.model.savingsaccount.SavingAccountTemplate +import org.mifospay.core.model.savingsaccount.SavingsWithAssociationsEntity +import org.mifospay.core.model.savingsaccount.TransactionsEntity +import org.mifospay.core.model.savingsaccount.UpdateSavingAccountEntity import org.mifospay.core.network.model.entity.Page -import org.mifospay.core.network.model.entity.accounts.savings.BlockUnblockResponseEntity -import org.mifospay.core.network.model.entity.accounts.savings.SavingAccountEntity -import org.mifospay.core.network.model.entity.accounts.savings.SavingsWithAssociationsEntity -import org.mifospay.core.network.model.entity.accounts.savings.TransactionsEntity import org.mifospay.core.network.utils.ApiEndPoints interface SavingsAccountsService { @GET(ApiEndPoints.SAVINGS_ACCOUNTS + "/{accountId}") - suspend fun getSavingsWithAssociations( + fun getSavingsWithAssociations( @Path("accountId") accountId: Long, @Query("associations") associationType: String, - ): SavingsWithAssociationsEntity + ): Flow @GET(ApiEndPoints.SAVINGS_ACCOUNTS) suspend fun getSavingsAccounts( @@ -36,7 +37,13 @@ interface SavingsAccountsService { ): Flow> @POST(ApiEndPoints.SAVINGS_ACCOUNTS) - suspend fun createSavingsAccount(@Body savingAccount: SavingAccountEntity): Flow + suspend fun createSavingsAccount(@Body savingAccount: CreateNewSavingEntity) + + @POST(ApiEndPoints.SAVINGS_ACCOUNTS + "/{accountId}") + suspend fun updateSavingsAccount( + @Path("accountId") accountId: Long, + @Body savingAccount: UpdateSavingAccountEntity, + ) @POST(ApiEndPoints.SAVINGS_ACCOUNTS + "/{accountId}") suspend fun blockUnblockAccount( @@ -58,4 +65,7 @@ interface SavingsAccountsService { "/{accountId}/" + ApiEndPoints.TRANSACTIONS + "?command=deposit", ) suspend fun payViaMobile(@Path("accountId") accountId: Long): Flow + + @GET(ApiEndPoints.SAVINGS_ACCOUNTS + "/template") + fun getSavingAccountTemplate(@Query("clientId") clientId: Long): Flow } diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 0bb0546c4..6d03d8984 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -38,6 +38,7 @@ kotlin { implementation(compose.material3) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) + implementation(libs.jb.composeNavigation) } androidInstrumentedTest.dependencies { implementation(libs.bundles.androidx.compose.ui.test) diff --git a/libs/pullrefresh/src/main/AndroidManifest.xml b/core/ui/src/commonMain/composeResources/drawable/arrow_outward.xml similarity index 52% rename from libs/pullrefresh/src/main/AndroidManifest.xml rename to core/ui/src/commonMain/composeResources/drawable/arrow_outward.xml index 367affcc3..95e05cbfa 100644 --- a/libs/pullrefresh/src/main/AndroidManifest.xml +++ b/core/ui/src/commonMain/composeResources/drawable/arrow_outward.xml @@ -8,4 +8,8 @@ See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md --> - \ No newline at end of file + + + + + diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/AvatarBox.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt similarity index 71% rename from feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/AvatarBox.kt rename to core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt index 4e117e37b..5d037f1f7 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/AvatarBox.kt +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt @@ -7,12 +7,13 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.feature.history.components +package org.mifospay.core.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor @@ -21,11 +22,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @Composable -internal fun AvatarBox( +fun AvatarBox( name: String, size: Int = 40, backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer, @@ -51,3 +53,24 @@ internal fun AvatarBox( ) } } + +@Composable +fun AvatarBox( + icon: ImageVector, + size: Int = 40, + backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer, +) { + Box( + modifier = Modifier + .size(size.dp) + .clip(CircleShape) + .background(backgroundColor), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = "Avatar", + tint = contentColorFor(backgroundColor), + ) + } +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosDivider.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosDivider.kt new file mode 100644 index 000000000..a46ecbae3 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosDivider.kt @@ -0,0 +1,33 @@ +/* + * 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.core.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.mifospay.core.designsystem.theme.NewUi + +@Composable +fun MifosDivider( + modifier: Modifier = Modifier, + thickness: Dp = 1.dp, + color: Color = NewUi.onSurface.copy(alpha = 0.05f), +) { + HorizontalDivider( + modifier = modifier + .fillMaxWidth(), + thickness = thickness, + color = color, + ) +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosSmallChip.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosSmallChip.kt new file mode 100644 index 000000000..51f33d7b9 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosSmallChip.kt @@ -0,0 +1,47 @@ +/* + * 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.core.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun MifosSmallChip( + label: String, + modifier: Modifier = Modifier, + containerColor: Color = MaterialTheme.colorScheme.primaryContainer, + contentColor: Color = contentColorFor(containerColor), + onClick: () -> Unit = {}, +) { + OutlinedCard( + onClick = onClick, + modifier = modifier, + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = containerColor, + contentColor = contentColor, + ), + ) { + Text( + text = label, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(4.dp), + ) + } +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/NavGraphBuilderExtensions.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/NavGraphBuilderExtensions.kt new file mode 100644 index 000000000..d07ec3a1c --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/NavGraphBuilderExtensions.kt @@ -0,0 +1,91 @@ +/* + * 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.core.ui + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.runtime.Composable +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDeepLink +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.mifospay.core.ui.utils.TransitionProviders + +fun NavGraphBuilder.composableWithSlideTransitions( + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, +) { + this.composable( + route = route, + arguments = arguments, + deepLinks = deepLinks, + enterTransition = TransitionProviders.Enter.slideUp, + exitTransition = TransitionProviders.Exit.stay, + popEnterTransition = TransitionProviders.Enter.stay, + popExitTransition = TransitionProviders.Exit.slideDown, + content = content, + ) +} + +fun NavGraphBuilder.composableWithStayTransitions( + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, +) { + this.composable( + route = route, + arguments = arguments, + deepLinks = deepLinks, + enterTransition = TransitionProviders.Enter.stay, + exitTransition = TransitionProviders.Exit.stay, + popEnterTransition = TransitionProviders.Enter.stay, + popExitTransition = TransitionProviders.Exit.stay, + content = content, + ) +} + +fun NavGraphBuilder.composableWithPushTransitions( + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, +) { + this.composable( + route = route, + arguments = arguments, + deepLinks = deepLinks, + enterTransition = TransitionProviders.Enter.pushLeft, + exitTransition = TransitionProviders.Exit.stay, + popEnterTransition = TransitionProviders.Enter.stay, + popExitTransition = TransitionProviders.Exit.pushRight, + content = content, + ) +} + +fun NavGraphBuilder.composableWithRootPushTransitions( + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, +) { + this.composable( + route = route, + arguments = arguments, + deepLinks = deepLinks, + enterTransition = TransitionProviders.Enter.stay, + exitTransition = TransitionProviders.Exit.pushLeft, + popEnterTransition = TransitionProviders.Enter.pushRight, + popExitTransition = TransitionProviders.Exit.fadeOut, + content = content, + ) +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/RevealSwipe.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/RevealSwipe.kt new file mode 100644 index 000000000..4466e93e4 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/RevealSwipe.kt @@ -0,0 +1,588 @@ +/* + * 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.core.ui + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.exponentialDecay +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.gestures.animateTo +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.snapTo +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.math.absoluteValue +import kotlin.math.roundToInt + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RevealSwipe( + modifier: Modifier = Modifier, + enableSwipe: Boolean = true, + onContentClick: (() -> Unit)? = null, + onContentLongClick: ((DpOffset) -> Unit)? = null, + backgroundStartActionLabel: String?, + onBackgroundStartClick: () -> Boolean = { true }, + backgroundEndActionLabel: String?, + onBackgroundEndClick: () -> Boolean = { true }, + closeOnContentClick: Boolean = true, + closeOnBackgroundClick: Boolean = true, + shape: CornerBasedShape, + alphaEasing: Easing = CubicBezierEasing(0.4f, 0.4f, 0.17f, 0.9f), + backgroundCardStartColor: Color, + backgroundCardEndColor: Color, + card: @Composable BoxScope.( + shape: Shape, + content: @Composable ColumnScope.() -> Unit, + ) -> Unit, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + state: RevealState = rememberRevealState( + maxRevealDp = 75.dp, + directions = setOf( + RevealDirection.StartToEnd, + RevealDirection.EndToStart, + ), + ), + hiddenContentEnd: @Composable BoxScope.() -> Unit = {}, + hiddenContentStart: @Composable BoxScope.() -> Unit = {}, + content: @Composable (Shape) -> Unit, +) { + val closeOnContentClickHandler: () -> Unit = remember(coroutineScope, state) { + { + coroutineScope.launch { + state.reset() + } + } + } + + val backgroundStartClick = remember(coroutineScope, state, onBackgroundStartClick) { + { + if (closeOnBackgroundClick) { + coroutineScope.launch { + state.reset() + } + } + onBackgroundStartClick() + } + } + + val backgroundEndClick = remember(coroutineScope, state, onBackgroundEndClick) { + { + if (closeOnBackgroundClick) { + coroutineScope.launch { + state.reset() + } + } + onBackgroundEndClick() + } + } + + val hapticFeedback = LocalHapticFeedback.current + var pressOffset by remember { mutableStateOf(DpOffset.Zero) } + + BaseRevealSwipe( + modifier = modifier.semantics { + customActions = buildList { + backgroundStartActionLabel?.let { + add( + CustomAccessibilityAction( + it, + onBackgroundStartClick, + ), + ) + } + backgroundEndActionLabel?.let { + add( + CustomAccessibilityAction( + it, + onBackgroundEndClick, + ), + ) + } + } + }, + enableSwipe = enableSwipe, + animateBackgroundCardColor = enableSwipe, + shape = shape, + alphaEasing = alphaEasing, + backgroundCardStartColor = backgroundCardStartColor, + backgroundCardEndColor = backgroundCardEndColor, + card = card, + state = state, + hiddenContentEnd = { + Box( + modifier = Modifier + .fillMaxSize() + .clickable { + backgroundEndClick() + }, + contentAlignment = Alignment.Center, + ) { + hiddenContentEnd() + } + }, + hiddenContentStart = { + Box( + modifier = Modifier + .fillMaxSize() + .clickable { + backgroundStartClick() + }, + contentAlignment = Alignment.Center, + ) { + hiddenContentStart() + } + }, + content = { + val clickableModifier = when { + onContentClick != null && !closeOnContentClick -> { + Modifier.combinedClickable( + onClick = onContentClick, + onLongClick = { + onContentLongClick?.let { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + it.invoke(pressOffset) + } + }, + ) + } + + onContentClick == null && closeOnContentClick -> { + // if no onContentClick handler passed, add click handler with no indication to enable close on content click + Modifier.combinedClickable( + onClick = closeOnContentClickHandler, + onLongClick = { + onContentLongClick?.let { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + it.invoke(pressOffset) + } + }, + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) + } + + onContentClick != null && closeOnContentClick -> { + // decide based on state: + // 1. if open, just close without indication + // 2. if closed, call click handler + Modifier.combinedClickable( + onClick = + { + val isOpen = + state.anchoredDraggableState.targetValue != RevealValue.Default + // if open, just close. No click event. + if (isOpen) { + closeOnContentClickHandler() + } else { + onContentClick() + } + }, + onLongClick = { + onContentLongClick?.let { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + it.invoke(pressOffset) + } + }, + // no indication if just closing + indication = if (state.anchoredDraggableState.targetValue != RevealValue.Default) null else LocalIndication.current, + interactionSource = remember { MutableInteractionSource() }, + ) + } + + else -> Modifier + } + + Box( + modifier = clickableModifier.pointerInput(true) { + kotlinx.coroutines.coroutineScope { + awaitEachGesture { + val down = awaitFirstDown() + pressOffset = DpOffset( + down.position.x.toDp(), + down.position.y.toDp(), + ) + } + } + }, + ) { + content(it) + } + }, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun BaseRevealSwipe( + modifier: Modifier = Modifier, + enableSwipe: Boolean = true, + animateBackgroundCardColor: Boolean = true, + shape: CornerBasedShape, + alphaEasing: Easing = CubicBezierEasing(0.4f, 0.4f, 0.17f, 0.9f), + backgroundCardStartColor: Color, + backgroundCardEndColor: Color, + card: @Composable BoxScope.( + shape: Shape, + content: @Composable ColumnScope.() -> Unit, + ) -> Unit, + state: RevealState = rememberRevealState( + maxRevealDp = 75.dp, + directions = setOf( + RevealDirection.StartToEnd, + RevealDirection.EndToStart, + ), + ), + hiddenContentEnd: @Composable BoxScope.() -> Unit = {}, + hiddenContentStart: @Composable BoxScope.() -> Unit = {}, + content: @Composable (Shape) -> Unit, +) { + Box( + modifier = modifier, + ) { + var shapeSize: Size by remember { mutableStateOf(Size(0f, 0f)) } + + val density = LocalDensity.current + + val cornerRadiusBottomEnd = remember(shapeSize, density) { + shape.bottomEnd.toPx( + shapeSize = shapeSize, + density = density, + ) + } + val cornerRadiusTopEnd = remember(shapeSize, density) { + shape.topEnd.toPx( + shapeSize = shapeSize, + density = density, + ) + } + + val cornerRadiusBottomStart = remember(shapeSize, density) { + shape.bottomStart.toPx( + shapeSize = shapeSize, + density = density, + ) + } + val cornerRadiusTopStart = remember(shapeSize, density) { + shape.topStart.toPx( + shapeSize = shapeSize, + density = density, + ) + } + + val minDragAmountForStraightCorner = + kotlin.math.max(cornerRadiusTopEnd, cornerRadiusBottomEnd) + + val cornerFactorEnd = + (-state.anchoredDraggableState.offset / minDragAmountForStraightCorner).nonNaNorZero() + .coerceIn(0f, 1f).or(0f) { + state.directions.contains(RevealDirection.EndToStart).not() + } + + val cornerFactorStart = + (state.anchoredDraggableState.offset / minDragAmountForStraightCorner).nonNaNorZero() + .coerceIn(0f, 1f).or(0f) { + state.directions.contains(RevealDirection.StartToEnd).not() + } + + val animatedCornerRadiusTopEnd: Float = lerp(cornerRadiusTopEnd, 0f, cornerFactorEnd) + val animatedCornerRadiusBottomEnd: Float = lerp(cornerRadiusBottomEnd, 0f, cornerFactorEnd) + + val animatedCornerRadiusTopStart: Float = lerp(cornerRadiusTopStart, 0f, cornerFactorStart) + val animatedCornerRadiusBottomStart: Float = + lerp(cornerRadiusBottomStart, 0f, cornerFactorStart) + + val animatedShape = shape.copy( + bottomStart = CornerSize(animatedCornerRadiusBottomStart), + bottomEnd = CornerSize(animatedCornerRadiusBottomEnd), + topStart = CornerSize(animatedCornerRadiusTopStart), + topEnd = CornerSize(animatedCornerRadiusTopEnd), + ) + + // alpha for background + val maxRevealPx = with(LocalDensity.current) { state.maxRevealDp.toPx() } + val draggedRatio = + (state.anchoredDraggableState.offset.absoluteValue / maxRevealPx.absoluteValue).coerceIn( + 0f, + 1f, + ) + + // cubic parameters can be evaluated here https://cubic-bezier.com/ + val alpha = alphaEasing.transform(draggedRatio) + + val animatedBackgroundEndColor = + if (alpha in 0f..1f && animateBackgroundCardColor) { + backgroundCardEndColor.copy( + alpha = alpha, + ) + } else { + backgroundCardEndColor + } + + val animatedBackgroundStartColor = + if (alpha in 0f..1f && animateBackgroundCardColor) { + backgroundCardStartColor.copy( + alpha = alpha, + ) + } else { + backgroundCardStartColor + } + + // non swipeable with hidden content + card(shape) { + Box( + modifier = Modifier + .fillMaxSize() + .alpha(alpha), + ) { + val hasStartContent = state.directions.contains(RevealDirection.StartToEnd) + val hasEndContent = state.directions.contains(RevealDirection.EndToStart) + if (hasStartContent) { + Box( + modifier = Modifier + .width(state.maxRevealDp) + .align(Alignment.CenterStart) + .fillMaxHeight() + .background(animatedBackgroundStartColor), + content = hiddenContentStart, + ) + } + if (hasEndContent) { + Box( + modifier = Modifier + .width(state.maxRevealDp) + .align(Alignment.CenterEnd) + .fillMaxHeight() + .background(animatedBackgroundEndColor), + content = hiddenContentEnd, + ) + } + } + } + + Box( + modifier = Modifier + .then( + if (enableSwipe) { + Modifier + .offset { + IntOffset( + x = state.anchoredDraggableState + .requireOffset() + .roundToInt(), + y = 0, + ) + } + .anchoredDraggable( + state = state.anchoredDraggableState, + orientation = Orientation.Horizontal, + enabled = true, + reverseDirection = LocalLayoutDirection.current == LayoutDirection.Rtl, + ) + } else { + Modifier + }, + ), + ) { + content(animatedShape) + } + + // This box is used to determine shape size. + // The box is sized to match it's parent, which in turn is sized according to its first child - the card. + BoxWithConstraints( + modifier = Modifier.matchParentSize(), + ) { + shapeSize = Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat()) + } + } +} + +/** + * Return an alternative value if whenClosure is true. Replaces if/else + */ +private fun T.or(orValue: T, whenClosure: T.() -> Boolean): T { + return if (whenClosure()) orValue else this +} + +private fun Float.nonNaNorZero() = if (isNaN()) 0f else this + +enum class RevealDirection { + /** + * Can be dismissed by swiping in the reading direction. + */ + StartToEnd, + + /** + * Can be dismissed by swiping in the reverse of the reading direction. + */ + EndToStart, +} + +/** + * Possible values of [RevealState]. + */ +enum class RevealValue { + /** + * Indicates the component has not been revealed yet. + */ + Default, + + /** + * Fully revealed to end + */ + FullyRevealedEnd, + + /** + * Fully revealed to start + */ + FullyRevealedStart, +} + +/** + * Create and [remember] a [RevealState] with the default animation clock. + * + * @param initialValue The initial value of the state. + * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. + */ +@Composable +fun rememberRevealState( + maxRevealDp: Dp = 75.dp, + directions: Set = setOf( + RevealDirection.StartToEnd, + RevealDirection.EndToStart, + ), + initialValue: RevealValue = RevealValue.Default, + positionalThreshold: (totalDistance: Float) -> Float = { distance: Float -> distance * 0.5f }, + velocityThreshold: (() -> Float)? = null, + snapAnimationSpec: AnimationSpec = tween(), + decayAnimationSpec: DecayAnimationSpec = exponentialDecay(), + confirmValueChange: (newValue: RevealValue) -> Boolean = { true }, +): RevealState { + val density = LocalDensity.current + return remember { + RevealState( + maxRevealDp = maxRevealDp, + directions = directions, + density = density, + initialValue = initialValue, + positionalThreshold = positionalThreshold, + velocityThreshold = velocityThreshold ?: { with(density) { 100.dp.toPx() } }, + snapAnimationSpec = snapAnimationSpec, + decayAnimationSpec = decayAnimationSpec, + confirmValueChange = confirmValueChange, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +data class RevealState( + val maxRevealDp: Dp = 75.dp, + val directions: Set, + private val density: Density, + private val initialValue: RevealValue = RevealValue.Default, + private val positionalThreshold: (totalDistance: Float) -> Float = { distance: Float -> distance * 0.5f }, + private val velocityThreshold: (() -> Float)? = null, + private val snapAnimationSpec: AnimationSpec = tween(), + private val decayAnimationSpec: DecayAnimationSpec, + private val confirmValueChange: (newValue: RevealValue) -> Boolean = { true }, +) { + @OptIn(ExperimentalFoundationApi::class) + val anchoredDraggableState: AnchoredDraggableState = AnchoredDraggableState( + initialValue = initialValue, + anchors = DraggableAnchors { + RevealValue.Default at 0f + if (RevealDirection.StartToEnd in directions) { + RevealValue.FullyRevealedEnd at with( + density, + ) { maxRevealDp.toPx() } + } + if (RevealDirection.EndToStart in directions) { + RevealValue.FullyRevealedStart at -with( + density, + ) { maxRevealDp.toPx() } + } + }, + positionalThreshold = positionalThreshold, + velocityThreshold = velocityThreshold ?: { with(density) { 10.dp.toPx() } }, + snapAnimationSpec = snapAnimationSpec, + decayAnimationSpec = decayAnimationSpec, + confirmValueChange = confirmValueChange, + ) +} + +/** + * Reset the component to the default position, with an animation. + */ +@OptIn(ExperimentalFoundationApi::class) +suspend fun RevealState.reset() { + anchoredDraggableState.animateTo( + targetValue = RevealValue.Default, + ) +} + +/** + * Reset the component to the default position, with an animation. + */ +@OptIn(ExperimentalFoundationApi::class) +suspend fun RevealState.resetFast() { + anchoredDraggableState.snapTo( + targetValue = RevealValue.Default, + ) +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionHistoryCard.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionHistoryCard.kt new file mode 100644 index 000000000..e31d7dd01 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionHistoryCard.kt @@ -0,0 +1,105 @@ +/* + * 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.core.ui + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +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 org.mifospay.core.designsystem.theme.NewUi +import org.mifospay.core.model.savingsaccount.Transaction + +@Composable +fun TransactionHistoryCard( + transactions: List, + modifier: Modifier = Modifier, + showLeadingIcon: Boolean = false, + showViewAll: Boolean = true, + onViewTransaction: (Long, Long) -> Unit, + onClickViewAll: () -> Unit = {}, +) { + Card( + modifier = modifier + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color.White, + ), + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Transaction History", + color = NewUi.primaryColor, + fontWeight = FontWeight(500), + ) + + AnimatedVisibility(showViewAll) { + Box( + modifier = Modifier.clickable( + onClick = onClickViewAll, + ), + ) { + Text( + text = "See All", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight(300), + ) + } + } + } + + transactions.forEachIndexed { i, transaction -> + TransactionItem( + transaction = transaction, + onClick = onViewTransaction, + showLeadingIcon = showLeadingIcon, + ) + + if (i != transactions.size - 1) { + MifosDivider(modifier = Modifier.padding(horizontal = 6.dp)) + } + } + + if (transactions.isEmpty()) { + Text( + text = "No transactions found", + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight(300), + ) + } + } + } +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionItemScreen.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionItemCard.kt similarity index 50% rename from core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionItemScreen.kt rename to core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionItemCard.kt index 4368dae0b..087f1bba3 100644 --- a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionItemScreen.kt +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/TransactionItemCard.kt @@ -9,6 +9,7 @@ */ package org.mifospay.core.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,6 +21,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface 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.graphics.ColorFilter @@ -39,7 +41,7 @@ import org.mifospay.core.model.savingsaccount.Transaction import org.mifospay.core.model.savingsaccount.TransactionType @Composable -fun TransactionItemScreen( +fun TransactionItemCard( transaction: Transaction, onClick: (Long, Long) -> Unit, modifier: Modifier = Modifier, @@ -54,40 +56,22 @@ fun TransactionItemScreen( ) { Row( modifier = modifier - .fillMaxWidth() - .padding(16.dp), + .fillMaxWidth(), horizontalArrangement = Arrangement.Absolute.SpaceBetween, ) { - Image( - modifier = Modifier - .size(20.dp) - .padding(top = 2.dp), - painter = painterResource( - resource = when (transaction.transactionType) { - TransactionType.DEBIT -> Res.drawable.core_ui_money_out - TransactionType.CREDIT -> Res.drawable.core_ui_money_in - else -> Res.drawable.core_ui_money_in - }, - ), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), - ) Column( - modifier = Modifier - .padding(start = 32.dp) - .weight(.3f), + modifier = Modifier, ) { Text( text = transaction.transactionType.toString(), style = TextStyle( fontSize = 14.sp, - fontWeight = FontWeight(400), + fontWeight = FontWeight(500), color = MaterialTheme.colorScheme.onSurface, - ), ) Text( - text = transaction.date.toString(), + text = transaction.date, style = TextStyle( fontSize = 10.sp, fontWeight = FontWeight(400), @@ -106,7 +90,7 @@ fun TransactionItemScreen( else -> formattedAmount } Text( - modifier = Modifier.weight(.3f), + modifier = Modifier, text = amount, style = TextStyle( fontSize = 16.sp, @@ -122,3 +106,92 @@ fun TransactionItemScreen( } } } + +@Composable +fun TransactionItem( + transaction: Transaction, + modifier: Modifier = Modifier, + showLeadingIcon: Boolean = true, + onClick: (Long, Long) -> Unit, +) { + Surface( + modifier = modifier, + onClick = { + onClick(transaction.accountId, transaction.transactionId) + }, + color = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + AnimatedVisibility(showLeadingIcon) { + Image( + modifier = Modifier + .size(20.dp) + .padding(top = 2.dp), + painter = painterResource( + resource = when (transaction.transactionType) { + TransactionType.DEBIT -> Res.drawable.core_ui_money_out + TransactionType.CREDIT -> Res.drawable.core_ui_money_in + else -> Res.drawable.core_ui_money_in + }, + ), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), + ) + } + + Column { + Text( + text = transaction.transactionType.name.uppercase(), + fontSize = 14.sp, + fontWeight = FontWeight(500), + style = MaterialTheme.typography.bodySmall, + ) + Text( + text = transaction.date, + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + ) + } + } + + Row(verticalAlignment = Alignment.CenterVertically) { + val formattedAmount = CurrencyFormatter.format( + balance = transaction.amount, + currencyCode = transaction.currency.code, + maximumFractionDigits = 2, + ) + val amount = when (transaction.transactionType) { + TransactionType.DEBIT -> "- $formattedAmount" + TransactionType.CREDIT -> "+ $formattedAmount" + else -> formattedAmount + } + + Text( + modifier = Modifier, + text = amount, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = when (transaction.transactionType) { + TransactionType.DEBIT -> red + TransactionType.CREDIT -> green + else -> Color.Black + }, + textAlign = TextAlign.End, + ), + ) + } + } + } +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/utils/Transition.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/utils/Transition.kt new file mode 100644 index 000000000..d2df41176 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/utils/Transition.kt @@ -0,0 +1,379 @@ +/* + * 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.core.ui.utils + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.navigation.NavBackStackEntry +import kotlin.jvm.JvmSuppressWildcards + +typealias EnterTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?) + +typealias ExitTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?) + +typealias NonNullEnterTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) + +typealias NonNullExitTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) + +/** + * The default transition time (in milliseconds) for all fade transitions in the + * [TransitionProviders]. + */ +const val DEFAULT_FADE_TRANSITION_TIME_MS: Int = 300 + +/** + * The default transition time (in milliseconds) for all slide transitions in the + * [TransitionProviders]. + */ +const val DEFAULT_SLIDE_TRANSITION_TIME_MS: Int = 450 + +/** + * The default transition time (in milliseconds) for all slide transitions in the + * [TransitionProviders]. + */ +const val DEFAULT_PUSH_TRANSITION_TIME_MS: Int = 350 + +/** + * The default transition time (in milliseconds) for all "stay"/no-op transitions in the + * [TransitionProviders]. + * + * This should be at least as large as any other transition that might also be happening during a + * navigation. + */ +val DEFAULT_STAY_TRANSITION_TIME_MS: Int = + maxOf( + DEFAULT_FADE_TRANSITION_TIME_MS, + DEFAULT_SLIDE_TRANSITION_TIME_MS, + DEFAULT_PUSH_TRANSITION_TIME_MS, + ) + +/** + * Checks if the parent of the destination before and after the navigation is the same. This is + * useful to ignore certain enter/exit transitions when navigating between distinct, nested flows. + */ +val AnimatedContentTransitionScope.isSameGraphNavigation: Boolean + get() = initialState.destination.parent == targetState.destination.parent + +/** + * Contains standard "transition providers" that may be used to specify the [EnterTransition] and + * [ExitTransition] used when building a typical composable destination. These may return `null` + * values in order to allow transitions between nested navigation graphs to be specified by + * components higher up in the graph. + */ +object TransitionProviders { + /** + * The standard set of "enter" transition providers. + */ + object Enter { + /** + * Fades the new screen in. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val fadeIn: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .fadeIn(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the new screen in from the left of the screen. + */ + val pushLeft: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .pushLeft(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the new screen in from the right of the screen. + */ + val pushRight: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .pushRight(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the new screen in from the bottom of the screen. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val slideUp: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .slideUp(this) + .takeIf { isSameGraphNavigation } + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately appear while the other screen transitions away. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val stay: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .stay(this) + .takeIf { isSameGraphNavigation } + } + } + + /** + * The standard set of "exit" transition providers. + */ + object Exit { + /** + * Fades the current screen out. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val fadeOut: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .fadeOut(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the current screen out to the left of the screen. + */ + val pushLeft: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .pushLeft(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the current screen out to the right of the screen. + */ + val pushRight: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .pushRight(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the current screen down to the bottom of the screen. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val slideDown: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .slideDown(this) + .takeIf { isSameGraphNavigation } + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately disappear while the other screen transitions into place. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val stay: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .stay(this) + .takeIf { isSameGraphNavigation } + } + } +} + +/** + * Contains standard "transition providers" that may be used to specify the [EnterTransition] and + * [ExitTransition] used when building a root [NavHost], which requires a non-null value. + */ +object RootTransitionProviders { + /** + * The standard set of "enter" transition providers. + */ + object Enter { + /** + * Fades the new screen in. + */ + val fadeIn: NonNullEnterTransitionProvider = { + fadeIn(tween(DEFAULT_FADE_TRANSITION_TIME_MS)) + } + + /** + * There is no transition for the entering screen. + */ + val none: NonNullEnterTransitionProvider = { + EnterTransition.None + } + + /** + * Slides the new screen in from the left of the screen. + */ + val pushLeft: NonNullEnterTransitionProvider = { + val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS + slideInHorizontally( + animationSpec = tween(durationMillis = totalTransitionDurationMs), + initialOffsetX = { fullWidth -> fullWidth / 2 }, + ) + fadeIn( + animationSpec = tween( + durationMillis = totalTransitionDurationMs / 2, + delayMillis = totalTransitionDurationMs / 2, + ), + ) + } + + /** + * Slides the new screen in from the right of the screen. + */ + val pushRight: NonNullEnterTransitionProvider = { + val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS + slideInHorizontally( + animationSpec = tween(durationMillis = totalTransitionDurationMs), + initialOffsetX = { fullWidth -> -fullWidth / 2 }, + ) + fadeIn( + animationSpec = tween( + durationMillis = totalTransitionDurationMs / 2, + delayMillis = totalTransitionDurationMs / 2, + ), + ) + } + + /** + * Slides the new screen in from the bottom of the screen. + */ + val slideUp: NonNullEnterTransitionProvider = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Up, + animationSpec = tween(DEFAULT_SLIDE_TRANSITION_TIME_MS), + ) + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately appear while the other screen transitions away. + */ + val stay: NonNullEnterTransitionProvider = { + fadeIn( + animationSpec = tween(DEFAULT_STAY_TRANSITION_TIME_MS), + initialAlpha = 1f, + ) + } + } + + /** + * The standard set of "exit" transition providers. + */ + object Exit { + /** + * Fades the current screen out. + */ + val fadeOut: NonNullExitTransitionProvider = { + fadeOut(tween(DEFAULT_FADE_TRANSITION_TIME_MS)) + } + + /** + * There is no transition for the exiting screen. + * + * Unlike the [stay] transition, this will immediately remove the outgoing screen even if + * there is an ongoing enter transition happening for the new screen. + */ + val none: NonNullExitTransitionProvider = { + ExitTransition.None + } + + /** + * Slides the current screen out to the left of the screen. + */ + @Suppress("MagicNumber") + val pushLeft: NonNullExitTransitionProvider = { + val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS + val delayMs = totalTransitionDurationMs / 7 + val slideWithoutDelayMs = totalTransitionDurationMs - delayMs + slideOutHorizontally( + animationSpec = tween( + durationMillis = slideWithoutDelayMs, + delayMillis = delayMs, + ), + targetOffsetX = { fullWidth -> -fullWidth / 2 }, + ) + fadeOut( + animationSpec = tween( + durationMillis = totalTransitionDurationMs / 2, + delayMillis = delayMs, + ), + ) + } + + /** + * Slides the current screen out to the right of the screen. + */ + @Suppress("MagicNumber") + val pushRight: NonNullExitTransitionProvider = { + val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS + val delayMs = totalTransitionDurationMs / 7 + val slideWithoutDelayMs = totalTransitionDurationMs - delayMs + slideOutHorizontally( + animationSpec = tween( + durationMillis = slideWithoutDelayMs, + delayMillis = delayMs, + ), + targetOffsetX = { fullWidth -> fullWidth / 2 }, + ) + fadeOut( + animationSpec = tween( + durationMillis = totalTransitionDurationMs / 2, + delayMillis = delayMs, + ), + ) + } + + /** + * Slides the current screen down to the bottom of the screen. + */ + val slideDown: NonNullExitTransitionProvider = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Down, + animationSpec = tween(DEFAULT_SLIDE_TRANSITION_TIME_MS), + ) + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately disappear while the other screen transitions into place. + */ + val stay: NonNullExitTransitionProvider = { + fadeOut( + animationSpec = tween(DEFAULT_STAY_TRANSITION_TIME_MS), + targetAlpha = 0.99f, + ) + } + } +} diff --git a/feature/accounts/build.gradle.kts b/feature/accounts/build.gradle.kts index a0ddb8c38..d4a4a02b3 100644 --- a/feature/accounts/build.gradle.kts +++ b/feature/accounts/build.gradle.kts @@ -8,17 +8,23 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.android.feature) - alias(libs.plugins.mifospay.android.library.compose) + alias(libs.plugins.mifospay.cmp.feature) + alias(libs.plugins.kotlin.parcelize) } android { - namespace = "org.mifospay.feature.bank.accounts" + namespace = "org.mifospay.feature.accounts" } -dependencies { - implementation(projects.core.data) - - implementation(projects.libs.pullrefresh) - implementation(libs.play.services.auth) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.kotlinx.serialization.json) + } + } } \ No newline at end of file diff --git a/feature/accounts/consumer-rules.pro b/feature/accounts/consumer-rules.pro deleted file mode 100644 index e69de29bb..000000000 diff --git a/feature/accounts/proguard-rules.pro b/feature/accounts/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/feature/accounts/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/accounts/src/main/AndroidManifest.xml b/feature/accounts/src/androidMain/AndroidManifest.xml similarity index 100% rename from feature/accounts/src/main/AndroidManifest.xml rename to feature/accounts/src/androidMain/AndroidManifest.xml diff --git a/feature/accounts/src/commonMain/composeResources/drawable/baseline_check.xml b/feature/accounts/src/commonMain/composeResources/drawable/baseline_check.xml new file mode 100644 index 000000000..9a2af107e --- /dev/null +++ b/feature/accounts/src/commonMain/composeResources/drawable/baseline_check.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/feature/accounts/src/commonMain/composeResources/drawable/baseline_unchecked.xml b/feature/accounts/src/commonMain/composeResources/drawable/baseline_unchecked.xml new file mode 100644 index 000000000..a619bdb3e --- /dev/null +++ b/feature/accounts/src/commonMain/composeResources/drawable/baseline_unchecked.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_ic_bank.xml b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_ic_bank.xml similarity index 100% rename from feature/accounts/src/main/res/drawable/feature_accounts_ic_bank.xml rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_ic_bank.xml diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_logo_axis.png b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_axis.png similarity index 100% rename from feature/accounts/src/main/res/drawable/feature_accounts_logo_axis.png rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_axis.png diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_logo_hdfc.png b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_hdfc.png similarity index 100% rename from feature/accounts/src/main/res/drawable/feature_accounts_logo_hdfc.png rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_hdfc.png diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_logo_icici.png b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_icici.png similarity index 100% rename from feature/accounts/src/main/res/drawable/feature_accounts_logo_icici.png rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_icici.png diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_logo_pnb.png b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_pnb.png similarity index 100% rename from feature/accounts/src/main/res/drawable/feature_accounts_logo_pnb.png rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_pnb.png diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_logo_rbl.png b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_rbl.png similarity index 100% rename from feature/accounts/src/main/res/drawable/feature_accounts_logo_rbl.png rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_rbl.png diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_logo_sbi.png b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_sbi.png similarity index 100% rename from feature/accounts/src/main/res/drawable/feature_accounts_logo_sbi.png rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_logo_sbi.png diff --git a/feature/accounts/src/main/res/drawable/feature_accounts_sim_card_selected.xml b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_sim_card_selected.xml similarity index 97% rename from feature/accounts/src/main/res/drawable/feature_accounts_sim_card_selected.xml rename to feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_sim_card_selected.xml index 61e66a4e1..b81efcaa2 100644 --- a/feature/accounts/src/main/res/drawable/feature_accounts_sim_card_selected.xml +++ b/feature/accounts/src/commonMain/composeResources/drawable/feature_accounts_sim_card_selected.xml @@ -15,7 +15,7 @@ android:viewportWidth="512"> Delete Beneficiary + Are you sure you want to delete this beneficiary? \ No newline at end of file diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt new file mode 100644 index 000000000..8ec6841ea --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountViewModel.kt @@ -0,0 +1,236 @@ +/* + * 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.feature.accounts + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +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 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.account.Account +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.accounts.AccountAction.Internal.BeneficiaryDeleteResultReceived +import org.mifospay.feature.accounts.AccountAction.Internal.DeleteBeneficiary +import org.mifospay.feature.accounts.AccountEvent.OnAddEditSavingsAccount +import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType +import org.mifospay.feature.accounts.savingsaccount.SavingsAddEditType + +@OptIn(ExperimentalCoroutinesApi::class) +class AccountViewModel( + private val userRepository: UserPreferencesRepository, + private val repository: SelfServiceRepository, + private val json: Json, +) : BaseViewModel( + initialState = run { + val clientId = requireNotNull(userRepository.clientId.value) + val defaultAccount = userRepository.defaultAccount + + AccountState( + clientId = clientId, + defaultAccountId = defaultAccount, + ) + }, +) { + val accountState = repository.getAccountAndBeneficiaryList(state.clientId) + .mapLatest { + when (it) { + is DataState.Loading -> AccountState.ViewState.Loading + is DataState.Error -> AccountState.ViewState.Error(it.exception.message.toString()) + is DataState.Success -> { + AccountState.ViewState.Content(it.data.accounts, it.data.beneficiaries) + } + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = AccountState.ViewState.Loading, + ) + + override fun handleAction(action: AccountAction) { + when (action) { + is AccountAction.CreateSavingsAccount -> { + sendEvent(OnAddEditSavingsAccount(SavingsAddEditType.AddItem)) + } + + is AccountAction.EditSavingsAccount -> { + sendEvent(OnAddEditSavingsAccount(SavingsAddEditType.EditItem(action.accountId))) + } + + is AccountAction.ViewAccountDetails -> { + sendEvent(AccountEvent.OnNavigateToAccountDetail(action.accountId)) + } + + is AccountAction.AddTPTBeneficiary -> { + sendEvent(AccountEvent.OnAddOrEditTPTBeneficiary(BeneficiaryAddEditType.AddItem)) + } + + is AccountAction.EditBeneficiary -> { + viewModelScope.launch { + val beneficiary = json.encodeToString(action.beneficiary) + sendEvent( + AccountEvent.OnAddOrEditTPTBeneficiary( + BeneficiaryAddEditType.EditItem(beneficiary), + ), + ) + } + } + + is AccountAction.DeleteBeneficiary -> { + mutableStateFlow.update { + it.copy( + dialogState = AccountState.DialogState.DeleteBeneficiary( + title = Res.string.delete_beneficiary_title, + message = Res.string.delete_beneficiary_subtitle, + onConfirm = { + trySendAction(DeleteBeneficiary(action.beneficiaryId)) + }, + ), + ) + } + } + + is AccountAction.DismissDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + is AccountAction.SetDefaultAccount -> handleSetDefaultAccount(action) + + is DeleteBeneficiary -> handleDeleteBeneficiary(action) + + is BeneficiaryDeleteResultReceived -> handleBeneficiaryDeleteResult(action) + } + } + + private fun handleSetDefaultAccount(action: AccountAction.SetDefaultAccount) { + viewModelScope.launch { + userRepository.updateDefaultAccount(action.accountId) + } + + mutableStateFlow.update { it.copy(defaultAccountId = action.accountId) } + sendEvent(AccountEvent.ShowToast("Default account updated")) + } + + private fun handleDeleteBeneficiary(action: DeleteBeneficiary) { + mutableStateFlow.update { it.copy(dialogState = AccountState.DialogState.Loading) } + + viewModelScope.launch { + val result = repository.deleteBeneficiary(action.beneficiaryId) + + sendAction(BeneficiaryDeleteResultReceived(result)) + } + } + + private fun handleBeneficiaryDeleteResult(action: BeneficiaryDeleteResultReceived) { + when (action.result) { + is DataState.Success -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + + sendEvent(AccountEvent.ShowToast("Beneficiary deleted")) + } + + is DataState.Error -> { + val message = action.result.exception.message.toString() + + mutableStateFlow.update { + it.copy(dialogState = AccountState.DialogState.Error(message)) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = AccountState.DialogState.Loading) + } + } + } + } +} + +data class AccountState( + 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 accounts: List, + val beneficiaries: List, + ) : ViewState { + override val hasFab: Boolean get() = true + override val isPullToRefreshEnabled: Boolean get() = true + } + } +} + +sealed interface AccountEvent { + data class OnAddEditSavingsAccount(val type: SavingsAddEditType) : AccountEvent + data class OnNavigateToAccountDetail(val accountId: Long) : AccountEvent + data class OnAddOrEditTPTBeneficiary(val type: BeneficiaryAddEditType) : AccountEvent + + data class ShowToast(val message: String) : AccountEvent +} + +sealed interface AccountAction { + data object AddTPTBeneficiary : AccountAction + data class EditBeneficiary(val beneficiary: Beneficiary) : AccountAction + data class DeleteBeneficiary(val beneficiaryId: Long) : AccountAction + + data object CreateSavingsAccount : AccountAction + data class EditSavingsAccount(val accountId: Long) : AccountAction + data class ViewAccountDetails(val accountId: Long) : AccountAction + data class SetDefaultAccount(val accountId: Long) : AccountAction + + data object DismissDialog : AccountAction + + sealed interface Internal : AccountAction { + data class DeleteBeneficiary(val beneficiaryId: Long) : Internal + data class BeneficiaryDeleteResultReceived(val result: DataState) : Internal + } +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountsScreen.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountsScreen.kt new file mode 100644 index 000000000..7b6ab663d --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/AccountsScreen.kt @@ -0,0 +1,614 @@ +/* + * 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.feature.accounts + +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.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +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.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +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.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import mobile_wallet.feature.accounts.generated.resources.Res +import mobile_wallet.feature.accounts.generated.resources.baseline_check +import mobile_wallet.feature.accounts.generated.resources.baseline_unchecked +import mobile_wallet.feature.accounts.generated.resources.feature_accounts_error_oops +import mobile_wallet.feature.accounts.generated.resources.feature_accounts_loading +import mobile_wallet.feature.accounts.generated.resources.feature_accounts_unexpected_error_subtitle +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.resources.vectorResource +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.BasicDialogState +import org.mifospay.core.designsystem.component.LoadingDialogState +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosBasicDialog +import org.mifospay.core.designsystem.component.MifosLoadingDialog +import org.mifospay.core.designsystem.component.MifosOutlinedButton +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.NewUi +import org.mifospay.core.model.account.Account +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.model.savingsaccount.Status +import org.mifospay.core.ui.AvatarBox +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.MifosSmallChip +import org.mifospay.core.ui.RevealDirection +import org.mifospay.core.ui.RevealSwipe +import org.mifospay.core.ui.rememberRevealState +import org.mifospay.core.ui.utils.EventsEffect +import org.mifospay.feature.accounts.beneficiary.BeneficiaryAddEditType +import org.mifospay.feature.accounts.savingsaccount.SavingsAddEditType + +@Composable +fun AccountsScreen( + onViewSavingAccountDetails: (Long) -> Unit, + onAddEditSavingsAccount: (SavingsAddEditType) -> Unit, + onAddOrEditBeneficiary: (BeneficiaryAddEditType) -> Unit, + modifier: Modifier = Modifier, + viewModel: AccountViewModel = 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 AccountEvent.OnAddEditSavingsAccount -> { + onAddEditSavingsAccount(event.type) + } + + is AccountEvent.OnNavigateToAccountDetail -> { + onViewSavingAccountDetails(event.accountId) + } + + is AccountEvent.OnAddOrEditTPTBeneficiary -> { + onAddOrEditBeneficiary(event.type) + } + + is AccountEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + AccountDialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(AccountAction.DismissDialog) } + }, + ) + + AccountsScreenContent( + defaultAccountId = state.defaultAccountId, + state = accountState, + onAction = viewModel::trySendAction, + snackbarHostState = snackbarHostState, + modifier = modifier, + ) +} + +@Composable +internal fun AccountsScreenContent( + defaultAccountId: Long?, + state: AccountState.ViewState, + onAction: (AccountAction) -> Unit, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, +) { + MifosScaffold( + modifier = modifier, + snackbarHostState = snackbarHostState, + floatingActionButtonPosition = FabPosition.EndOverlay, + floatingActionButton = { + AnimatedVisibility( + visible = state.hasFab, + enter = scaleIn(), + exit = scaleOut(), + ) { + FloatingActionButton( + onClick = { + onAction(AccountAction.CreateSavingsAccount) + }, + ) { + Icon(imageVector = MifosIcons.Add, "Add") + } + } + }, + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center, + ) { + when (state) { + is AccountState.ViewState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(Res.string.feature_accounts_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is AccountState.ViewState.Error -> { + EmptyContentScreen( + title = stringResource(Res.string.feature_accounts_error_oops), + subTitle = stringResource(Res.string.feature_accounts_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } + + is AccountState.ViewState.Content -> { + AccountsScreenContent( + state = state, + defaultAccountId = defaultAccountId, + onAction = onAction, + ) + } + } + } + } +} + +@Composable +internal fun AccountsScreenContent( + defaultAccountId: Long?, + state: AccountState.ViewState.Content, + onAction: (AccountAction) -> Unit, + modifier: Modifier = Modifier, +) { + AccountsList( + modifier = modifier, + defaultAccountId = defaultAccountId, + accounts = state.accounts, + beneficiaryList = state.beneficiaries, + onAddTPTBeneficiary = { + onAction(AccountAction.AddTPTBeneficiary) + }, + onClickEditBeneficiary = { + onAction(AccountAction.EditBeneficiary(it)) + }, + onClickDeleteBeneficiary = { + onAction(AccountAction.DeleteBeneficiary(it)) + }, + onAccountClicked = { + onAction(AccountAction.SetDefaultAccount(it)) + }, + onClickEditAccount = { + onAction(AccountAction.EditSavingsAccount(it)) + }, + onClickViewAccount = { + onAction(AccountAction.ViewAccountDetails(it)) + }, + ) +} + +@Composable +private fun AccountsList( + defaultAccountId: Long?, + accounts: List, + beneficiaryList: List, + onAccountClicked: (Long) -> Unit, + onAddTPTBeneficiary: () -> Unit, + onClickEditBeneficiary: (Beneficiary) -> Unit, + onClickDeleteBeneficiary: (Long) -> Unit, + onClickEditAccount: (Long) -> Unit, + onClickViewAccount: (Long) -> Unit, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), +) { + LazyColumn( + modifier = modifier + .fillMaxSize(), + state = lazyListState, + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + Text( + text = "Savings Account", + style = MaterialTheme.typography.labelLarge, + ) + } + + items( + items = accounts, + key = { it.id }, + ) { account -> + AccountItem( + account = account, + isDefault = defaultAccountId == account.id, + onClick = onAccountClicked, + onClickEditAccount = onClickEditAccount, + onClickViewAccount = onClickViewAccount, + ) + } + + item { + HorizontalDivider( + modifier = Modifier + .fillMaxWidth(), + thickness = 1.dp, + color = NewUi.onSurface.copy(alpha = 0.05f), + ) + } + + item { + Text( + text = "Beneficiaries", + style = MaterialTheme.typography.labelLarge, + ) + } + + items( + items = beneficiaryList, + key = { it.accountNumber }, + ) { beneficiary -> + BeneficiaryItem( + beneficiary = beneficiary, + onClickEdit = onClickEditBeneficiary, + onClickDelete = onClickDeleteBeneficiary, + ) + } + + item { + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + thickness = 1.dp, + color = NewUi.onSurface.copy(alpha = 0.05f), + ) + } + + item { + Box( + modifier = modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + MifosOutlinedButton( + text = { + Text(text = "Add Beneficiary") + }, + leadingIcon = { + Icon( + imageVector = MifosIcons.Add, + contentDescription = "add", + ) + }, + onClick = onAddTPTBeneficiary, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} + +@Composable +private fun AccountItem( + account: Account, + isDefault: Boolean, + modifier: Modifier = Modifier, + onClick: (Long) -> Unit, + onClickEditAccount: (Long) -> Unit, + onClickViewAccount: (Long) -> Unit, +) { + val state = rememberRevealState( + maxRevealDp = if (account.status.submittedAndPendingApproval) 105.dp else 75.dp, + directions = setOf(RevealDirection.EndToStart), + ) + RevealSwipe( + modifier = modifier, + state = state, + shape = RoundedCornerShape(8.dp), + backgroundCardStartColor = MaterialTheme.colorScheme.surfaceContainerHigh, + backgroundCardEndColor = MaterialTheme.colorScheme.primary, + backgroundStartActionLabel = null, + backgroundEndActionLabel = "Edit", + card = { shape, content -> + Card( + modifier = Modifier.matchParentSize(), + colors = CardDefaults.cardColors( + contentColor = MaterialTheme.colorScheme.onSecondary, + containerColor = Color.Transparent, + ), + shape = shape, + content = content, + ) + }, + hiddenContentEnd = { + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility( + visible = account.status.submittedAndPendingApproval, + ) { + IconButton( + onClick = { onClickEditAccount(account.id) }, + ) { + Icon( + imageVector = MifosIcons.Edit2, + contentDescription = "Edit", + ) + } + } + + IconButton( + onClick = { onClickViewAccount(account.id) }, + ) { + Icon( + imageVector = MifosIcons.Info, + contentDescription = "Info", + ) + } + } + }, + onContentClick = if (account.status.active) { + { onClick(account.id) } + } else { + null + }, + ) { + OutlinedCard( + modifier = modifier.fillMaxWidth(), + shape = it, + colors = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + ), + ) { + ListItem( + headlineContent = { + Text(text = account.name) + }, + supportingContent = { + Text(text = account.number) + }, + leadingContent = { + AvatarBox( + icon = MifosIcons.Bank, + backgroundColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ) + }, + trailingContent = { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SavingAccountStatusCard(account.status) + + AnimatedVisibility(isDefault) { + OutlinedCard( + onClick = {}, + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + ), + ) { + Text( + text = "Default", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(4.dp), + ) + } + } + + Icon( + imageVector = if (isDefault) { + vectorResource(Res.drawable.baseline_check) + } else { + vectorResource(Res.drawable.baseline_unchecked) + }, + contentDescription = "check", + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + ), + ) + } + } +} + +@Composable +private fun BeneficiaryItem( + beneficiary: Beneficiary, + modifier: Modifier = Modifier, + onClickEdit: (Beneficiary) -> Unit, + onClickDelete: (Long) -> Unit, +) { + OutlinedCard( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + ), + ) { + ListItem( + headlineContent = { + Text(text = beneficiary.name) + }, + supportingContent = { + Text(text = beneficiary.accountNumber) + }, + leadingContent = { + AvatarBox( + icon = MifosIcons.AccountCircle, + ) + }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + FilledTonalIconButton( + onClick = { + onClickEdit(beneficiary) + }, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), + ) { + Icon( + imageVector = MifosIcons.Edit2, + contentDescription = "Edit Beneficiary", + ) + } + + FilledTonalIconButton( + onClick = { + onClickDelete(beneficiary.id) + }, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + ), + ) { + Icon( + imageVector = MifosIcons.OutlinedDelete, + contentDescription = "Delete Beneficiary", + ) + } + } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + ), + ) + } +} + +@Composable +private fun AccountDialogs( + dialogState: AccountState.DialogState?, + onDismissRequest: () -> Unit, +) { + when (dialogState) { + is AccountState.DialogState.DeleteBeneficiary -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + title = stringResource(dialogState.title), + message = stringResource(dialogState.message), + ), + onConfirm = dialogState.onConfirm, + onDismissRequest = onDismissRequest, + ) + + is AccountState.DialogState.Error -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + message = dialogState.message, + ), + onDismissRequest = onDismissRequest, + ) + + is AccountState.DialogState.Loading -> MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + + null -> Unit + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SavingAccountStatusCard( + status: Status, + modifier: Modifier = Modifier, +) { + val statusChips = listOf( + "Pending Approval" to status.submittedAndPendingApproval, + "Approved" to status.approved, + "Rejected" to status.rejected, + "Withdrawn" to status.withdrawnByApplicant, + "Closed" to status.closed, + "Prematurely Closed" to status.prematureClosed, + "Transfer in Progress" to status.transferInProgress, + "Transfer on Hold" to status.transferOnHold, + "Matured" to status.matured, + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + statusChips.forEach { (label, isActive) -> + if (isActive) { + StatusChip(label) + } + } + } +} + +@Composable +private fun StatusChip(label: String) { + val color = when (label) { + "Pending Approval" -> Color(0xFFFFF9C4) + "Approved" -> Color(0xFFC8E6C9) + "Rejected" -> Color(0xFFFFCDD2) + "Withdrawn" -> Color(0xFFE1BEE7) + "Active" -> Color(0xFFBBDEFB) + "Closed" -> Color(0xFFCFD8DC) + "Prematurely Closed" -> Color(0xFFD7CCC8) + "Transfer in Progress" -> Color(0xFFFFE0B2) + "Transfer on Hold" -> Color(0xFFF0F4C3) + "Matured" -> Color(0xFFB2DFDB) + else -> Color(0xFFEFEFEF) + } + + MifosSmallChip( + label = label, + containerColor = color, + ) +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryScreen.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryScreen.kt new file mode 100644 index 000000000..ce683b2f8 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryScreen.kt @@ -0,0 +1,296 @@ +/* + * 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.feature.accounts.beneficiary + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.window.PopupProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.BasicDialogState +import org.mifospay.core.designsystem.component.LoadingDialogState +import org.mifospay.core.designsystem.component.MifosBasicDialog +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosLoadingDialog +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.MifosTextField +import org.mifospay.core.model.utils.Locale +import org.mifospay.core.model.utils.filterLocales +import org.mifospay.core.ui.MifosDivider +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +internal fun AddEditBeneficiaryScreen( + navigateBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: AddEditBeneficiaryViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val localeList by viewModel.filteredLocalList.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + is AEBEvent.NavigateBack -> navigateBack.invoke() + is AEBEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + BeneficiaryDialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(AEBAction.DismissDialog) } + }, + ) + + AddEditBeneficiaryScreenContent( + state = state, + localeList = localeList, + snackbarHostState = snackbarHostState, + modifier = modifier, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AddEditBeneficiaryScreenContent( + state: AEBState, + localeList: List, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + onAction: (AEBAction) -> Unit, +) { + MifosScaffold( + topBarTitle = state.title, + backPress = { onAction(AEBAction.NavigateBack) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { paddingValues -> + LazyColumn( + modifier = modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + MifosTextField( + label = "Nickname", + value = state.name, + onValueChange = { + onAction(AEBAction.ChangeName(it)) + }, + ) + } + + item { + MifosTextField( + label = "Account No", + value = state.accountNumber, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + onValueChange = { + onAction(AEBAction.ChangeAccountNumber(it)) + }, + ) + } + + item { + MifosTextField( + label = "Transfer Limit", + value = state.transferLimit.toString(), + onValueChange = { + onAction(AEBAction.ChangeTransferLimit(it)) + }, + onClickClearIcon = { + onAction(AEBAction.ChangeTransferLimit("")) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + ) + } + + item { + val filteredLocalList by remember(localeList, state.locale) { + derivedStateOf { + localeList.filterLocales(state.locale) + } + } + + var textFieldSize by remember { mutableStateOf(Size.Zero) } + var localeToggled by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = localeToggled && localeList.isNotEmpty(), + onExpandedChange = { + localeToggled = !localeToggled + }, + ) { + MifosTextField( + label = "Locale", + value = state.locale, + onValueChange = { + localeToggled = true + onAction(AEBAction.ChangeLocale(it)) + }, + onClickClearIcon = { + onAction(AEBAction.ChangeLocale("")) + }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = localeToggled, + ) + }, + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + textFieldSize = coordinates.size.toSize() + } + .menuAnchor(MenuAnchorType.PrimaryNotEditable), + ) + + DropdownMenu( + expanded = localeToggled, + onDismissRequest = { + localeToggled = false + }, + properties = PopupProperties( + focusable = false, + dismissOnBackPress = true, + dismissOnClickOutside = true, + clippingEnabled = true, + ), + modifier = Modifier + .width(with(LocalDensity.current) { textFieldSize.width.toDp() }) + .heightIn(max = 200.dp), + ) { + filteredLocalList.forEachIndexed { index, locale -> + DropdownMenuItem( + modifier = Modifier + .fillMaxWidth(), + onClick = { + onAction(AEBAction.ChangeLocale(locale.localName)) + localeToggled = false + }, + text = { + Text(text = locale.countryName) + }, + ) + + if (index != filteredLocalList.size - 1) { + MifosDivider() + } + } + } + } + } + + item { + MifosTextField( + label = "Office Name", + value = state.officeName, + showClearIcon = false, + readOnly = true, + onValueChange = { + onAction(AEBAction.ChangeOfficeName(it)) + }, + ) + } + + item { + MifosTextField( + label = "Account Type", + value = state.accountTypeName, + readOnly = true, + showClearIcon = false, + onValueChange = { + onAction(AEBAction.ChangeAccountType(it.toInt())) + }, + ) + } + + item { + MifosButton( + text = { + Text(text = state.btnText) + }, + onClick = { + onAction(AEBAction.SaveBeneficiary) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun BeneficiaryDialogs( + dialogState: AEBState.DialogState?, + onDismissRequest: () -> Unit, +) { + when (dialogState) { + is AEBState.DialogState.Error -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + message = dialogState.message, + ), + onDismissRequest = onDismissRequest, + ) + + is AEBState.DialogState.Loading -> MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + + null -> Unit + } +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryViewModel.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryViewModel.kt new file mode 100644 index 000000000..d0c49976b --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/AddEditBeneficiaryViewModel.kt @@ -0,0 +1,299 @@ +/* + * 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.feature.accounts.beneficiary + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import org.mifospay.core.common.DataState +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.data.repository.LocalAssetRepository +import org.mifospay.core.data.repository.SelfServiceRepository +import org.mifospay.core.model.beneficiary.Beneficiary +import org.mifospay.core.model.beneficiary.BeneficiaryPayload +import org.mifospay.core.model.beneficiary.BeneficiaryUpdatePayload +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.accounts.beneficiary.AEBAction.Internal.HandleBeneficiaryAddEditResult +import org.mifospay.feature.accounts.beneficiary.AEBState.DialogState.Error + +private const val KEY = "AddEditBeneficiaryViewModel" + +internal class AddEditBeneficiaryViewModel( + private val localAssetRepository: LocalAssetRepository, + private val repository: SelfServiceRepository, + private val json: Json, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY] ?: run { + when (val addEditType = BeneficiaryAddEditArgs(savedStateHandle).addEditType) { + is BeneficiaryAddEditType.AddItem -> { + AEBState( + name = "", + accountNumber = "", + transferLimit = 0, + addEditType = addEditType, + ) + } + + is BeneficiaryAddEditType.EditItem -> { + val beneficiary = json.decodeFromString( + Beneficiary.serializer(), + addEditType.beneficiary, + ) + + AEBState( + name = beneficiary.name, + accountNumber = beneficiary.accountNumber, + transferLimit = beneficiary.transferLimit, + officeName = beneficiary.officeName, + beneficiaryId = beneficiary.id, + addEditType = addEditType, + ) + } + } + }, +) { + val filteredLocalList = localAssetRepository.localeList.stateIn( + scope = viewModelScope, + initialValue = emptyList(), + started = SharingStarted.WhileSubscribed(5_000), + ) + + init { + stateFlow + .onEach { savedStateHandle[KEY] = it } + .launchIn(viewModelScope) + } + + override fun handleAction(action: AEBAction) { + when (action) { + is AEBAction.ChangeName -> { + mutableStateFlow.update { + it.copy(name = action.name) + } + } + + is AEBAction.ChangeTransferLimit -> { + mutableStateFlow.update { + it.copy(transferLimit = action.transferLimit.toIntOrNull() ?: 0) + } + } + + is AEBAction.ChangeAccountNumber -> { + mutableStateFlow.update { + it.copy(accountNumber = action.accountNumber) + } + } + + is AEBAction.ChangeAccountType -> { + mutableStateFlow.update { + it.copy(accountType = action.accountType) + } + } + + is AEBAction.ChangeLocale -> { + mutableStateFlow.update { + it.copy(locale = action.locale) + } + } + + is AEBAction.ChangeOfficeName -> { + mutableStateFlow.update { + it.copy(officeName = action.officeName) + } + } + + AEBAction.NavigateBack -> { + sendEvent(AEBEvent.NavigateBack) + } + + AEBAction.DismissDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + AEBAction.SaveBeneficiary -> initiateSaveBeneficiary() + + is HandleBeneficiaryAddEditResult -> handleBeneficiaryAddEditResult(action) + } + } + + private fun initiateSaveBeneficiary() = when { + state.name.isBlank() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Beneficiary Name cannot be empty")) + } + } + + state.accountNumber.isBlank() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Account number cannot be empty")) + } + } + + state.transferLimit <= 0 -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Transfer limit should be greater than 0")) + } + } + + state.locale.isBlank() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Select a locale")) + } + } + + state.officeName.isBlank() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Office name cannot be empty")) + } + } + + state.accountType == 0 -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Select an account type")) + } + } + + else -> handleSaveBeneficiary() + } + + private fun handleSaveBeneficiary() { + mutableStateFlow.update { + it.copy(dialogState = AEBState.DialogState.Loading) + } + + when (state.addEditType) { + is BeneficiaryAddEditType.AddItem -> { + val payload = BeneficiaryPayload( + name = state.name, + accountNumber = state.accountNumber, + transferLimit = state.transferLimit, + locale = state.locale, + officeName = state.officeName, + accountType = state.accountType, + ) + + viewModelScope.launch { + val result = repository.createBeneficiary(payload) + + sendAction(HandleBeneficiaryAddEditResult(result)) + } + } + + is BeneficiaryAddEditType.EditItem -> { + val payload = BeneficiaryUpdatePayload( + name = state.name, + transferLimit = state.transferLimit, + ) + + viewModelScope.launch { + val beneficiaryId = state.beneficiaryId ?: return@launch + + val result = repository.updateBeneficiary(beneficiaryId, payload) + + sendAction(HandleBeneficiaryAddEditResult(result)) + } + } + } + } + + private fun handleBeneficiaryAddEditResult(action: HandleBeneficiaryAddEditResult) { + when (action.result) { + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = AEBState.DialogState.Loading) + } + } + + is DataState.Error -> { + mutableStateFlow.update { + it.copy(dialogState = Error(action.result.exception.toString())) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + sendEvent(AEBEvent.ShowToast(action.result.data)) + sendEvent(AEBEvent.NavigateBack) + } + } + } +} + +@Parcelize +internal data class AEBState( + val addEditType: BeneficiaryAddEditType, + val name: String, + val accountNumber: String, + val transferLimit: Int, + val locale: String = "en_US", + val officeName: String = OFFICE_NAME, + val accountType: Int = SAVINGS_ACC_ID, + val beneficiaryId: Long? = null, + val dialogState: DialogState? = null, +) : Parcelable { + private val isAddItemMode: Boolean + get() = addEditType is BeneficiaryAddEditType.AddItem + + val btnText: String + get() = if (isAddItemMode) "Save" else "Update" + + val title: String + get() = if (isAddItemMode) "Add Beneficiary" else "Update Beneficiary" + + val accountTypeName: String + get() = if (accountType == SAVINGS_ACC_ID) "WALLET" else "Other" + + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } + + companion object { + const val SAVINGS_ACC_ID = 2 + const val OFFICE_NAME = "Head Office" + } +} + +internal sealed interface AEBEvent { + data object NavigateBack : AEBEvent + data class ShowToast(val message: String) : AEBEvent +} + +internal sealed interface AEBAction { + data class ChangeLocale(val locale: String) : AEBAction + data class ChangeName(val name: String) : AEBAction + data class ChangeOfficeName(val officeName: String) : AEBAction + data class ChangeAccountNumber(val accountNumber: String) : AEBAction + data class ChangeAccountType(val accountType: Int) : AEBAction + data class ChangeTransferLimit(val transferLimit: String) : AEBAction + + data object DismissDialog : AEBAction + data object NavigateBack : AEBAction + data object SaveBeneficiary : AEBAction + + sealed interface Internal : AEBAction { + data class HandleBeneficiaryAddEditResult(val result: DataState) : Internal + } +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryAddEditType.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryAddEditType.kt new file mode 100644 index 000000000..c9985cb97 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryAddEditType.kt @@ -0,0 +1,29 @@ +/* + * 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.feature.accounts.beneficiary + +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +sealed class BeneficiaryAddEditType : Parcelable { + + abstract val beneficiary: String? + + @Parcelize + data object AddItem : BeneficiaryAddEditType() { + override val beneficiary: String? + get() = null + } + + @Parcelize + data class EditItem( + override val beneficiary: String, + ) : BeneficiaryAddEditType() +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryNavigation.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryNavigation.kt new file mode 100644 index 000000000..0d1cc6812 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/beneficiary/BeneficiaryNavigation.kt @@ -0,0 +1,80 @@ +/* + * 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.feature.accounts.beneficiary + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.navArgument +import org.mifospay.core.ui.composableWithSlideTransitions + +private const val ADD_TYPE: String = "add_beneficiary" +private const val EDIT_TYPE: String = "edit_beneficiary" +private const val EDIT_ITEM_ID: String = "beneficiary_edit_id" + +private const val ADD_EDIT_ITEM_PREFIX: String = "beneficiary_add_edit_item" +private const val ADD_EDIT_ITEM_TYPE: String = "beneficiary_add_edit_type" + +private const val ADD_EDIT_ITEM_ROUTE: String = + ADD_EDIT_ITEM_PREFIX + + "/{$ADD_EDIT_ITEM_TYPE}" + + "?$EDIT_ITEM_ID={$EDIT_ITEM_ID}" + +data class BeneficiaryAddEditArgs( + val addEditType: BeneficiaryAddEditType, +) { + constructor(savedStateHandle: SavedStateHandle) : this( + addEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) { + ADD_TYPE -> BeneficiaryAddEditType.AddItem + EDIT_TYPE -> BeneficiaryAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID])) + else -> throw IllegalStateException("Unknown BeneficiaryAddEditType.") + }, + ) +} + +fun NavGraphBuilder.addEditBeneficiaryScreen( + navigateBack: () -> Unit, +) { + composableWithSlideTransitions( + route = ADD_EDIT_ITEM_ROUTE, + arguments = listOf( + navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType }, + ), + ) { + AddEditBeneficiaryScreen( + navigateBack = navigateBack, + ) + } +} + +fun NavController.navigateToBeneficiaryAddEdit( + addEditType: BeneficiaryAddEditType, + navOptions: NavOptions? = null, +) { + navigate( + route = "$ADD_EDIT_ITEM_PREFIX/${addEditType.toTypeString()}" + + "?$EDIT_ITEM_ID=${addEditType.toIdOrNull()}", + navOptions = navOptions, + ) +} + +private fun BeneficiaryAddEditType.toTypeString(): String = + when (this) { + is BeneficiaryAddEditType.AddItem -> ADD_TYPE + is BeneficiaryAddEditType.EditItem -> EDIT_TYPE + } + +private fun BeneficiaryAddEditType.toIdOrNull(): String? = + when (this) { + is BeneficiaryAddEditType.AddItem -> null + is BeneficiaryAddEditType.EditItem -> beneficiary + } diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/di/AccountsModule.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/di/AccountsModule.kt new file mode 100644 index 000000000..744414853 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/di/AccountsModule.kt @@ -0,0 +1,26 @@ +/* + * 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.feature.accounts.di + +import kotlinx.serialization.json.Json +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module +import org.mifospay.feature.accounts.AccountViewModel +import org.mifospay.feature.accounts.beneficiary.AddEditBeneficiaryViewModel +import org.mifospay.feature.accounts.savingsaccount.AddEditSavingViewModel +import org.mifospay.feature.accounts.savingsaccount.details.SavingAccountDetailViewModel + +val AccountsModule = module { + single { Json { ignoreUnknownKeys = true } } + viewModelOf(::AccountViewModel) + viewModelOf(::AddEditBeneficiaryViewModel) + viewModelOf(::SavingAccountDetailViewModel) + viewModelOf(::AddEditSavingViewModel) +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingAccountScreen.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingAccountScreen.kt new file mode 100644 index 000000000..ac0bd9a5e --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingAccountScreen.kt @@ -0,0 +1,544 @@ +/* + * 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.feature.accounts.savingsaccount + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.window.PopupProperties +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_error_oops +import mobile_wallet.feature.accounts.generated.resources.feature_accounts_loading +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.BasicDialogState +import org.mifospay.core.designsystem.component.LoadingDialogState +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosBasicDialog +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosLoadingDialog +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.MifosTextField +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.model.utils.Locale +import org.mifospay.core.model.utils.filterLocales +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.MifosDivider +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +internal fun AddEditSavingAccountScreen( + navigateBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: AddEditSavingViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val localeList by viewModel.localeList.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + is AESEvent.OnNavigateBack -> navigateBack.invoke() + is AESEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + SavingAccountDialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(AESAction.DismissDialog) } + }, + ) + + AddEditSavingAccountScreenContent( + state = state, + localeList = localeList, + snackbarHostState = snackbarHostState, + modifier = modifier, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} + +@Composable +private fun SavingAccountDialogs( + dialogState: AESState.DialogState?, + onDismissRequest: () -> Unit, +) { + when (dialogState) { + is AESState.DialogState.Error -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + message = dialogState.message, + ), + onDismissRequest = onDismissRequest, + ) + + is AESState.DialogState.Loading -> MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + + null -> Unit + } +} + +@Composable +internal fun AddEditSavingAccountScreenContent( + state: AESState, + localeList: List, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + onAction: (AESAction) -> Unit, +) { + MifosScaffold( + topBarTitle = state.title, + backPress = { onAction(AESAction.NavigateBack) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentAlignment = Alignment.Center, + ) { + when (state.viewState) { + is AESState.ViewState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(Res.string.feature_accounts_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is AESState.ViewState.Error -> { + EmptyContentScreen( + title = stringResource(Res.string.feature_accounts_error_oops), + subTitle = state.viewState.message, + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } + + is AESState.ViewState.Content -> { + AddEditSavingAccountScreenContent( + btnText = state.btnText, + isInEditMode = state.isInEditMode, + state = state.viewState, + localeList = localeList, + onAction = onAction, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AddEditSavingAccountScreenContent( + btnText: String, + isInEditMode: Boolean, + state: AESState.ViewState.Content, + localeList: List, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), + onAction: (AESAction) -> Unit, +) { + val filteredLocalList by remember(localeList, state.locale) { + derivedStateOf { + localeList.filterLocales(state.locale) + } + } + + LazyColumn( + modifier = modifier + .fillMaxSize(), + state = lazyListState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item("Client Name") { + MifosTextField( + value = state.template.clientName, + label = "Client Name", + onValueChange = {}, + readOnly = true, + ) + } + + item("Saving Product") { + var fieldSize by remember { mutableStateOf(Size.Zero) } + var productToggled by remember { mutableStateOf(false) } + val productName = remember { + requireNotNull(state.template.productOptions.find { it.id == state.productId }).name + } + + ExposedDropdownMenuBox( + expanded = productToggled, + onExpandedChange = { + productToggled = !productToggled + }, + ) { + MifosTextField( + label = "Saving Product", + value = productName, + showClearIcon = false, + readOnly = true, + onValueChange = {}, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = productToggled, + ) + }, + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + fieldSize = coordinates.size.toSize() + } + .menuAnchor(MenuAnchorType.PrimaryNotEditable), + ) + + DropdownMenu( + expanded = productToggled, + onDismissRequest = { + productToggled = false + }, + properties = PopupProperties( + focusable = false, + dismissOnBackPress = true, + dismissOnClickOutside = true, + clippingEnabled = true, + ), + modifier = Modifier + .width(with(LocalDensity.current) { fieldSize.width.toDp() }) + .heightIn(max = 200.dp), + ) { + state.template.productOptions.forEachIndexed { index, product -> + DropdownMenuItem( + modifier = Modifier + .fillMaxWidth(), + onClick = { + onAction(AESAction.ProductChanged(product.id)) + productToggled = false + }, + text = { + Text(text = product.name) + }, + ) + + if (index != state.template.productOptions.size - 1) { + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 44.dp), + ) + } + } + } + } + } + + if (!isInEditMode) { + item("External Id") { + MifosTextField( + value = state.externalId, + label = "External Id", + onValueChange = { + onAction(AESAction.ExternalIdChanged(it)) + }, + onClickClearIcon = { + onAction(AESAction.ExternalIdChanged("")) + }, + modifier = Modifier, + ) + } + + item("Submitted Date & Date Format") { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MifosTextField( + label = "Submitted Date", + value = state.submittedOnDate, + onValueChange = {}, + readOnly = true, + showClearIcon = false, + modifier = Modifier.weight(1.5f), + ) + + MifosTextField( + label = "Date Format", + value = state.dateFormat, + onValueChange = {}, + readOnly = true, + showClearIcon = false, + modifier = Modifier.weight(1.5f), + ) + } + } + + item("Locale") { + var textFieldSize by remember { mutableStateOf(Size.Zero) } + var localeToggled by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = localeToggled && filteredLocalList.isNotEmpty(), + onExpandedChange = { + localeToggled = !localeToggled + }, + ) { + MifosTextField( + label = "Locale", + value = state.locale, + onValueChange = { + onAction(AESAction.LocaleChanged(it)) + }, + onClickClearIcon = { + onAction(AESAction.LocaleChanged("")) + }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = localeToggled, + ) + }, + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + textFieldSize = coordinates.size.toSize() + } + .menuAnchor(MenuAnchorType.PrimaryNotEditable), + ) + + DropdownMenu( + expanded = localeToggled, + onDismissRequest = { + localeToggled = false + }, + properties = PopupProperties( + focusable = false, + dismissOnBackPress = true, + dismissOnClickOutside = true, + clippingEnabled = true, + ), + modifier = Modifier + .width(with(LocalDensity.current) { textFieldSize.width.toDp() }) + .heightIn(max = 200.dp), + ) { + filteredLocalList.forEachIndexed { index, locale -> + DropdownMenuItem( + modifier = Modifier + .fillMaxWidth(), + onClick = { + onAction(AESAction.LocaleChanged(locale.localName)) + localeToggled = false + }, + text = { + Text(text = locale.countryName) + }, + ) + + if (index != filteredLocalList.size - 1) { + MifosDivider() + } + } + } + } + } + + item("Nominal Annual Interest Rate") { + MifosTextField( + value = state.nominalAnnualInterestRate.toString(), + label = "Nominal Annual Interest Rate", + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { + onAction(AESAction.NominalAnnualInterestRateChanged(it)) + }, + onClickClearIcon = { + onAction(AESAction.NominalAnnualInterestRateChanged("")) + }, + ) + } + + item("Allow Overdraft") { + CustomCheckbox( + text = "Allow Overdraft", + checked = state.allowOverdraft, + onCheckedChange = { + onAction(AESAction.AllowOverdraftChanged) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + + if (state.allowOverdraft) { + item("Overdraft Limit") { + MifosTextField( + label = "Overdraft Limit", + value = state.overdraftLimit, + onValueChange = { + onAction(AESAction.OverdraftLimitChanged(it)) + }, + onClickClearIcon = { + onAction(AESAction.OverdraftLimitChanged("")) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + } + } + + item("Min Required Balance") { + CustomCheckbox( + text = "Min Required Balance", + checked = state.enforceMinRequiredBalance, + onCheckedChange = { + onAction(AESAction.EnforceMinRequiredBalanceChanged) + }, + modifier = Modifier, + ) + } + + if (state.enforceMinRequiredBalance) { + item("Required Balance") { + MifosTextField( + label = "Opening Balance", + value = state.minRequiredOpeningBalance.toString(), + onValueChange = { + onAction(AESAction.MinRequiredOpeningBalanceChanged(it)) + }, + onClickClearIcon = { + onAction(AESAction.MinRequiredOpeningBalanceChanged("")) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + } + } + + item("Withdrawal Fee For Transfers") { + CustomCheckbox( + text = "Withdrawal Fee For Transfer", + checked = state.withdrawalFeeForTransfers, + onCheckedChange = { + onAction(AESAction.WithdrawalFeeForTransfersChanged) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + + item("With Hold Tax") { + CustomCheckbox( + text = "With Hold Tax", + checked = state.withHoldTax, + onCheckedChange = { + onAction(AESAction.WithHoldTaxChanged) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("CreateOrUpdateSavingAccount") { + MifosButton( + text = { + Text(text = btnText) + }, + onClick = { + onAction(AESAction.CreateOrUpdateSavingAccount) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@Composable +private fun CustomCheckbox( + modifier: Modifier = Modifier, + text: String, + checked: Boolean, + onCheckedChange: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .clickable { onCheckedChange() }, + contentAlignment = Alignment.CenterStart, + ) { + Row( + modifier = Modifier + .fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Checkbox( + checked = checked, + onCheckedChange = { onCheckedChange() }, + ) + + Text( + text = text, + maxLines = 2, + overflow = TextOverflow.Clip, + ) + } + } +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingNavigation.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingNavigation.kt new file mode 100644 index 000000000..716f124c0 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingNavigation.kt @@ -0,0 +1,85 @@ +/* + * 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.feature.accounts.savingsaccount + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.navArgument +import org.mifospay.core.ui.composableWithSlideTransitions + +private const val ADD_TYPE: String = "add_saving" +private const val EDIT_TYPE: String = "edit_saving" +private const val EDIT_ITEM_ID: String = "saving_edit_id" + +private const val ADD_EDIT_ITEM_PREFIX: String = "saving_add_edit_item" +private const val ADD_EDIT_ITEM_TYPE: String = "saving_add_edit_type" + +private const val ADD_EDIT_ITEM_ROUTE: String = + ADD_EDIT_ITEM_PREFIX + + "/{$ADD_EDIT_ITEM_TYPE}" + + "?$EDIT_ITEM_ID={$EDIT_ITEM_ID}" + +data class SavingAccountAddEditArgs( + val savingsAddEditType: SavingsAddEditType, +) { + constructor(savedStateHandle: SavedStateHandle) : this( + savingsAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) { + ADD_TYPE -> SavingsAddEditType.AddItem + + EDIT_TYPE -> { + val accountId = requireNotNull(savedStateHandle.get(EDIT_ITEM_ID)).toLong() + SavingsAddEditType.EditItem(savingsAccountId = accountId) + } + + else -> throw IllegalStateException("Unknown SavingsAddEditType.") + }, + ) +} + +fun NavGraphBuilder.addEditSavingAccountScreen( + navigateBack: () -> Unit, +) { + composableWithSlideTransitions( + route = ADD_EDIT_ITEM_ROUTE, + arguments = listOf( + navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType }, + ), + ) { + AddEditSavingAccountScreen( + navigateBack = navigateBack, + ) + } +} + +fun NavController.navigateToSavingAccountAddEdit( + addEditType: SavingsAddEditType, + navOptions: NavOptions? = null, +) { + navigate( + route = "$ADD_EDIT_ITEM_PREFIX/${addEditType.toTypeString()}" + + "?$EDIT_ITEM_ID=${addEditType.toIdOrNull()}", + navOptions = navOptions, + ) +} + +private fun SavingsAddEditType.toTypeString(): String = + when (this) { + is SavingsAddEditType.AddItem -> ADD_TYPE + is SavingsAddEditType.EditItem -> EDIT_TYPE + } + +private fun SavingsAddEditType.toIdOrNull(): String? = + when (this) { + is SavingsAddEditType.AddItem -> null + is SavingsAddEditType.EditItem -> savingsAccountId.toString() + } diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingViewModel.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingViewModel.kt new file mode 100644 index 000000000..a86278487 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/AddEditSavingViewModel.kt @@ -0,0 +1,431 @@ +/* + * 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.feature.accounts.savingsaccount + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState +import org.mifospay.core.common.DateHelper +import org.mifospay.core.common.IgnoredOnParcel +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.data.repository.LocalAssetRepository +import org.mifospay.core.data.repository.SavingsAccountRepository +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.model.savingsaccount.CreateNewSavingEntity +import org.mifospay.core.model.savingsaccount.SavingAccountTemplate +import org.mifospay.core.model.savingsaccount.UpdateSavingAccountEntity +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.accounts.savingsaccount.AESAction.CreateOrUpdateSavingAccount +import org.mifospay.feature.accounts.savingsaccount.AESAction.Internal.HandleSavingAddEditResult +import org.mifospay.feature.accounts.savingsaccount.AESAction.Internal.HandleSavingTemplateResult +import org.mifospay.feature.accounts.savingsaccount.AESState.ViewState.Error +import org.mifospay.feature.accounts.savingsaccount.AESState.DialogState.Error as DialogStateError + +private const val KEY = "add_edit_saving_state" + +internal class AddEditSavingViewModel( + private val repository: SavingsAccountRepository, + private val userRepository: UserPreferencesRepository, + localAssetRepository: LocalAssetRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY] ?: run { + val clientId = requireNotNull(userRepository.clientId.value) + val type = SavingAccountAddEditArgs(savedStateHandle).savingsAddEditType + + AESState( + clientId = clientId, + type = type, + viewState = AESState.ViewState.Loading, + dialogState = null, + ) + }, +) { + val localeList = localAssetRepository.localeList.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + init { + stateFlow + .onEach { savedStateHandle[KEY] = it } + .launchIn(viewModelScope) + + repository.getSavingAccountTemplate(state.clientId).onEach { + sendAction(HandleSavingTemplateResult(it)) + }.launchIn(viewModelScope) + } + + override fun handleAction(action: AESAction) { + when (action) { + is AESAction.ExternalIdChanged -> { + updateContent { content -> + content.copy(externalId = action.externalId) + } + } + + is AESAction.ProductChanged -> { + updateContent { content -> + content.copy(productId = action.productId) + } + } + + is AESAction.LocaleChanged -> { + updateContent { content -> + content.copy(locale = action.locale) + } + } + + is AESAction.AllowOverdraftChanged -> { + updateContent { content -> + content.copy(allowOverdraft = !content.allowOverdraft) + } + } + + is AESAction.EnforceMinRequiredBalanceChanged -> { + updateContent { content -> + content.copy(enforceMinRequiredBalance = !content.enforceMinRequiredBalance) + } + } + + is AESAction.MinRequiredOpeningBalanceChanged -> { + updateContent { content -> + content.copy(minRequiredOpeningBalance = action.balance.toLong()) + } + } + + is AESAction.NominalAnnualInterestRateChanged -> { + updateContent { content -> + content.copy(nominalAnnualInterestRate = action.rate.toDouble()) + } + } + + is AESAction.OverdraftLimitChanged -> { + updateContent { content -> + content.copy(overdraftLimit = action.overdraftLimit) + } + } + + is AESAction.WithHoldTaxChanged -> { + updateContent { content -> + content.copy(withHoldTax = !content.withHoldTax) + } + } + + is AESAction.WithdrawalFeeForTransfersChanged -> { + updateContent { content -> + content.copy(withdrawalFeeForTransfers = !content.withdrawalFeeForTransfers) + } + } + + is AESAction.NavigateBack -> { + sendEvent(AESEvent.OnNavigateBack) + } + + is AESAction.DismissDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + is CreateOrUpdateSavingAccount -> initiateCreateOrUpdateSavingAccount() + + is HandleSavingAddEditResult -> handleSavingAddEditResult(action) + + is HandleSavingTemplateResult -> handleSavingTemplateResult(action) + } + } + + private fun initiateCreateOrUpdateSavingAccount() { + onContent { content -> + when (state.type) { + is SavingsAddEditType.AddItem -> when { + content.productId == 0L -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Select a Saving Product")) + } + } + + content.clientId.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Client ID is required")) + } + } + + content.externalId.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("External ID is required")) + } + } + + content.externalId.length < 8 -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("External ID must be at least 8 characters")) + } + } + + content.enforceMinRequiredBalance && content.minRequiredOpeningBalance == 0L -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Min Required Opening Balance is required")) + } + } + + content.allowOverdraft && content.overdraftLimit.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Overdraft Limit is required")) + } + } + + content.locale.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Locale is required")) + } + } + + content.submittedOnDate.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Submitted On Date is required")) + } + } + + content.dateFormat.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Date Format is required")) + } + } + + else -> initiateCreateSavingAccount() + } + + is SavingsAddEditType.EditItem -> when { + content.productId == 0L -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Select a Saving Product")) + } + } + + content.clientId.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = DialogStateError("Client ID is required")) + } + } + + else -> initiateUpdateSavingAccount() + } + } + } + } + + private fun initiateCreateSavingAccount() { + onContent { content -> + viewModelScope.launch { + val result = repository.createSavingsAccount(content.createSavingEntity) + sendAction(HandleSavingAddEditResult(result)) + } + } + } + + private fun initiateUpdateSavingAccount() { + onContent { content -> + val accountId = requireNotNull(state.type.savingsAccountId) { + "Account ID is required for updating saving account" + } + + viewModelScope.launch { + val result = repository.updateSavingsAccount(accountId, content.updateSavingEntity) + sendAction(HandleSavingAddEditResult(result)) + } + } + } + + private fun handleSavingTemplateResult(action: HandleSavingTemplateResult) { + when (action.result) { + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(viewState = AESState.ViewState.Loading) + } + } + + is DataState.Error -> { + mutableStateFlow.update { + it.copy(viewState = Error(action.result.exception.message.toString())) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy(viewState = AESState.ViewState.Content(action.result.data)) + } + } + } + } + + private fun handleSavingAddEditResult(action: HandleSavingAddEditResult) { + when (action.result) { + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = AESState.DialogState.Loading) + } + } + + is DataState.Error -> { + val message = action.result.exception.message.toString() + mutableStateFlow.update { + it.copy(dialogState = DialogStateError(message)) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + sendEvent(AESEvent.ShowToast(action.result.data)) + sendEvent(AESEvent.OnNavigateBack) + } + } + } + + private inline fun onContent( + crossinline block: (AESState.ViewState.Content) -> Unit, + ) { + (state.viewState as? AESState.ViewState.Content)?.let(block) + } + + private inline fun updateContent( + crossinline block: ( + AESState.ViewState.Content, + ) -> AESState.ViewState.Content?, + ) { + val currentViewState = state.viewState + val updatedContent = (currentViewState as? AESState.ViewState.Content) + ?.let(block) + ?: return + mutableStateFlow.update { it.copy(viewState = updatedContent) } + } +} + +@Parcelize +internal data class AESState( + val clientId: Long, + val type: SavingsAddEditType, + val viewState: ViewState, + val dialogState: DialogState?, +) : Parcelable { + + val isInEditMode: Boolean + get() = type is SavingsAddEditType.EditItem + + val btnText: String + get() = if (!isInEditMode) "Save" else "Update" + + val title: String + get() = if (!isInEditMode) "Create Saving Account" else "Update Saving Account" + + sealed interface ViewState : Parcelable { + @Parcelize + data object Loading : ViewState + + @Parcelize + data class Error(val message: String) : ViewState + + @Parcelize + data class Content( + val template: SavingAccountTemplate, + val minRequiredOpeningBalance: Long = 0, + val overdraftLimit: String = "", + val externalId: String = "", + val locale: String = "en_IN", + val submittedOnDate: String = DateHelper.formattedShortDate, + val dateFormat: String = DateHelper.SHORT_MONTH, + val clientId: String = template.clientId, + val productId: Long = template.productOptions.first().id, + val nominalAnnualInterestRate: Double = template.nominalAnnualInterestRate, + val withdrawalFeeForTransfers: Boolean = template.withdrawalFeeForTransfers, + val allowOverdraft: Boolean = template.allowOverdraft, + val enforceMinRequiredBalance: Boolean = template.enforceMinRequiredBalance, + val withHoldTax: Boolean = template.withHoldTax, + ) : ViewState { + + @IgnoredOnParcel + val createSavingEntity: CreateNewSavingEntity + get() = CreateNewSavingEntity( + clientId = clientId, + productId = productId, + nominalAnnualInterestRate = nominalAnnualInterestRate, + minRequiredOpeningBalance = minRequiredOpeningBalance, + withdrawalFeeForTransfers = withdrawalFeeForTransfers, + allowOverdraft = allowOverdraft, + overdraftLimit = overdraftLimit, + enforceMinRequiredBalance = enforceMinRequiredBalance, + withHoldTax = withHoldTax, + externalId = externalId, + submittedOnDate = submittedOnDate, + locale = locale, + dateFormat = dateFormat, + ) + + @IgnoredOnParcel + val updateSavingEntity: UpdateSavingAccountEntity + get() = UpdateSavingAccountEntity( + clientId = clientId, + productId = productId, + ) + } + } + + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +internal sealed interface AESEvent { + data object OnNavigateBack : AESEvent + data class ShowToast(val message: String) : AESEvent +} + +internal sealed interface AESAction { + data class ProductChanged(val productId: Long) : AESAction + data class ExternalIdChanged(val externalId: String) : AESAction + data class LocaleChanged(val locale: String) : AESAction + + data class NominalAnnualInterestRateChanged(val rate: String) : AESAction + + data class MinRequiredOpeningBalanceChanged(val balance: String) : AESAction + data object EnforceMinRequiredBalanceChanged : AESAction + + data object AllowOverdraftChanged : AESAction + data class OverdraftLimitChanged(val overdraftLimit: String) : AESAction + + data object WithdrawalFeeForTransfersChanged : AESAction + data object WithHoldTaxChanged : AESAction + + data object DismissDialog : AESAction + data object NavigateBack : AESAction + + data object CreateOrUpdateSavingAccount : AESAction + + sealed interface Internal : AESAction { + data class HandleSavingAddEditResult(val result: DataState) : Internal + data class HandleSavingTemplateResult(val result: DataState) : + Internal + } +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/SavingsAddEditType.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/SavingsAddEditType.kt new file mode 100644 index 000000000..57b1f2023 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/SavingsAddEditType.kt @@ -0,0 +1,29 @@ +/* + * 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.feature.accounts.savingsaccount + +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +sealed class SavingsAddEditType : Parcelable { + + abstract val savingsAccountId: Long? + + @Parcelize + data object AddItem : SavingsAddEditType() { + override val savingsAccountId: Long? + get() = null + } + + @Parcelize + data class EditItem( + override val savingsAccountId: Long, + ) : SavingsAddEditType() +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailNavigation.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailNavigation.kt new file mode 100644 index 000000000..929dca77d --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailNavigation.kt @@ -0,0 +1,42 @@ +/* + * 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.feature.accounts.savingsaccount.details + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.navArgument +import org.mifospay.core.ui.composableWithSlideTransitions + +private const val ROUTE = "saving_account_detail" +private const val ACCOUNT_ID = "accountId" + +private const val BASE_ROUTE = "$ROUTE?$ACCOUNT_ID={$ACCOUNT_ID}" + +fun NavGraphBuilder.savingAccountDetailRoute( + navigateBack: () -> Unit, + onViewTransaction: (Long, Long) -> Unit, +) { + composableWithSlideTransitions( + route = BASE_ROUTE, + arguments = listOf( + navArgument(ACCOUNT_ID) { type = NavType.LongType }, + ), + ) { + SavingAccountDetailScreen( + navigateBack = navigateBack, + onViewTransaction = onViewTransaction, + ) + } +} + +fun NavController.navigateToSavingAccountDetails(accountId: Long) { + navigate("$ROUTE?$ACCOUNT_ID=$accountId") +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailScreen.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailScreen.kt new file mode 100644 index 000000000..3ad306e36 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailScreen.kt @@ -0,0 +1,503 @@ +/* + * 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.feature.accounts.savingsaccount.details + +import androidx.annotation.VisibleForTesting +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.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +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.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.SuggestionChipDefaults +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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +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_error_oops +import mobile_wallet.feature.accounts.generated.resources.feature_accounts_loading +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.common.CurrencyFormatter +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.NewUi +import org.mifospay.core.model.account.Account +import org.mifospay.core.model.savingsaccount.SavingAccountDetail +import org.mifospay.core.model.savingsaccount.Status +import org.mifospay.core.model.savingsaccount.Summary +import org.mifospay.core.model.savingsaccount.formatAmount +import org.mifospay.core.model.savingsaccount.toAccount +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.MifosDivider +import org.mifospay.core.ui.TransactionHistoryCard +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +internal fun SavingAccountDetailScreen( + navigateBack: () -> Unit, + onViewTransaction: (Long, Long) -> Unit, + modifier: Modifier = Modifier, + viewModel: SavingAccountDetailViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + is SADEvent.NavigateBack -> navigateBack.invoke() + + is SADEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + + is SADEvent.OnViewTransaction -> { + onViewTransaction(event.clientId, event.accountId) + } + } + } + + SavingAccountDetailScreen( + state = state.viewState, + snackbarHostState = snackbarHostState, + modifier = modifier, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} + +@Composable +@VisibleForTesting +internal fun SavingAccountDetailScreen( + state: SADState.ViewState, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + onAction: (SADAction) -> Unit, +) { + MifosScaffold( + backPress = { + onAction(SADAction.NavigateBack) + }, + topBarTitle = "Account Details", + snackbarHost = { SnackbarHost(snackbarHostState) }, + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentAlignment = Alignment.Center, + ) { + when (state) { + is SADState.ViewState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(Res.string.feature_accounts_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is SADState.ViewState.Error -> { + EmptyContentScreen( + title = stringResource(Res.string.feature_accounts_error_oops), + subTitle = state.message, + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } + + is SADState.ViewState.Content -> { + SavingAccountDetailScreenContent( + state = state, + onAction = onAction, + ) + } + } + } + } +} + +@Composable +private fun SavingAccountDetailScreenContent( + state: SADState.ViewState.Content, + modifier: Modifier = Modifier, + onAction: (SADAction) -> Unit, +) { + SavingAccountDetails( + savingAccountDetail = state.data, + modifier = modifier, + onViewTransaction = { clientId, accountId -> + onAction(SADAction.ViewTransaction(clientId, accountId)) + }, + ) +} + +@Composable +private fun SavingAccountDetails( + savingAccountDetail: SavingAccountDetail, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), + onViewTransaction: (Long, Long) -> Unit, +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + contentPadding = PaddingValues(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + SavingAccountCard( + account = savingAccountDetail.toAccount(), + status = savingAccountDetail.status, + ) + } + + item { + SavingAccountSummaryCard( + summary = savingAccountDetail.summary, + ) + } + + item { + TransactionHistoryCard( + transactions = savingAccountDetail.transactions, + onViewTransaction = onViewTransaction, + showLeadingIcon = true, + showViewAll = false, + ) + } + } +} + +@Composable +private fun SavingAccountSummaryCard( + summary: Summary, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color.White, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "Account Summary", + color = NewUi.primaryColor, + fontWeight = FontWeight(500), + modifier = Modifier.padding(8.dp), + ) + + MifosDivider() + + RowBlock { + Text(text = "Account Balance") + Text( + text = summary.formatAmount(summary.accountBalance), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } + + MifosDivider() + + RowBlock { + Text(text = "Total Deposits") + Text( + text = summary.formatAmount(summary.totalDeposits), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } + + MifosDivider() + + RowBlock { + Text(text = "Total Withdrawals") + Text( + text = summary.formatAmount(summary.totalWithdrawals), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } + + MifosDivider() + + RowBlock { + Text(text = "Available Balance") + Text( + text = summary.formatAmount(summary.availableBalance), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } + + MifosDivider() + + RowBlock { + Text(text = "Total Interest Posted") + Text( + text = summary.totalInterestPosted.toString(), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } + + MifosDivider() + + RowBlock { + Text(text = "Total Overdraft") + Text( + text = summary.totalOverdraftInterestDerived.toString(), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } + + MifosDivider() + + RowBlock { + Text(text = "Interest Not Posted") + Text( + text = summary.interestNotPosted.toString(), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } + } + } +} + +@Composable +private inline fun RowBlock( + crossinline content: + @Composable() + (RowScope.() -> Unit), +) { + Box { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + content() + } + } +} + +@Composable +private fun SavingAccountCard( + account: Account, + status: Status, + modifier: Modifier = Modifier, +) { + val brush = remember { + Brush.linearGradient( + colors = listOf(NewUi.walletColor1, NewUi.walletColor2), + ) + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + .background( + brush = brush, + shape = RoundedCornerShape(16.dp), + ), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(12.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text( + text = "Product Name", + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.surface, + ) + + Text( + text = account.name, + fontWeight = FontWeight(400), + color = MaterialTheme.colorScheme.surface, + ) + } + + SavingAccountStatusCard(status) + } + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Text( + text = account.number, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.headlineMedium, + letterSpacing = 0.50.sp, + ) + } + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + Column { + Text( + text = "Wallet Balance", + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.surface, + ) + + val accountBalance = CurrencyFormatter.format( + balance = account.balance, + currencyCode = account.currency.code, + maximumFractionDigits = null, + ) + + Text( + text = accountBalance, + color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.headlineLarge, + ) + } + + Icon( + modifier = Modifier + .graphicsLayer(rotationZ = 90f) + .padding(4.dp), + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = "arrow", + tint = MaterialTheme.colorScheme.surface, + ) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SavingAccountStatusCard( + status: Status, + modifier: Modifier = Modifier, +) { + val statusChips = listOf( + "Pending Approval" to status.submittedAndPendingApproval, + "Approved" to status.approved, + "Rejected" to status.rejected, + "Withdrawn" to status.withdrawnByApplicant, + "Active" to status.active, + "Closed" to status.closed, + "Prematurely Closed" to status.prematureClosed, + "Transfer in Progress" to status.transferInProgress, + "Transfer on Hold" to status.transferOnHold, + "Matured" to status.matured, + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + statusChips.forEach { (label, isActive) -> + if (isActive) { + StatusChip(label) + } + } + } +} + +@Composable +private fun StatusChip(label: String) { + val color = when (label) { + "Pending Approval" -> Color(0xFFFFF9C4) + "Approved" -> Color(0xFFC8E6C9) + "Rejected" -> Color(0xFFFFCDD2) + "Withdrawn" -> Color(0xFFE1BEE7) + "Active" -> Color(0xFFBBDEFB) + "Closed" -> Color(0xFFCFD8DC) + "Prematurely Closed" -> Color(0xFFD7CCC8) + "Transfer in Progress" -> Color(0xFFFFE0B2) + "Transfer on Hold" -> Color(0xFFF0F4C3) + "Matured" -> Color(0xFFB2DFDB) + else -> Color(0xFFEFEFEF) + } + + SuggestionChip( + onClick = { /* Handle click if needed */ }, + label = { Text(label) }, + border = SuggestionChipDefaults.suggestionChipBorder( + enabled = true, + borderColor = color, + ), + colors = SuggestionChipDefaults.suggestionChipColors( + labelColor = color, + ), + modifier = Modifier, + ) +} diff --git a/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailViewModel.kt b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailViewModel.kt new file mode 100644 index 000000000..88c136541 --- /dev/null +++ b/feature/accounts/src/commonMain/kotlin/org/mifospay/feature/accounts/savingsaccount/details/SavingAccountDetailViewModel.kt @@ -0,0 +1,113 @@ +/* + * 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.feature.accounts.savingsaccount.details + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import org.mifospay.core.common.DataState +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.data.repository.SavingsAccountRepository +import org.mifospay.core.model.savingsaccount.SavingAccountDetail +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.accounts.savingsaccount.details.SADAction.Internal.SavingAccountDetailResultReceived +import org.mifospay.feature.accounts.savingsaccount.details.SADState.ViewState.Error + +private const val KEY = "saving_account_detail" + +internal class SavingAccountDetailViewModel( + savedStateHandle: SavedStateHandle, + repository: SavingsAccountRepository, +) : BaseViewModel( + initialState = savedStateHandle[KEY] ?: SADState( + accountId = requireNotNull(savedStateHandle["accountId"]), + viewState = SADState.ViewState.Loading, + ), +) { + + init { + repository.getAccountDetail(state.accountId).onEach { + sendAction(SavingAccountDetailResultReceived(it)) + }.launchIn(viewModelScope) + } + + override fun handleAction(action: SADAction) { + when (action) { + is SADAction.NavigateBack -> { + sendEvent(SADEvent.NavigateBack) + } + + is SADAction.ViewTransaction -> { + sendEvent(SADEvent.OnViewTransaction(action.clientId, action.accountId)) + } + + is SavingAccountDetailResultReceived -> handleSavingAccountDetailResult(action) + } + } + + private fun handleSavingAccountDetailResult(action: SavingAccountDetailResultReceived) { + when (action.result) { + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(viewState = SADState.ViewState.Loading) + } + } + + is DataState.Error -> { + mutableStateFlow.update { + it.copy(viewState = Error(action.result.exception.message.toString())) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy(viewState = SADState.ViewState.Content(action.result.data)) + } + } + } + } +} + +@Parcelize +internal data class SADState( + val accountId: Long, + val viewState: ViewState, +) : Parcelable { + sealed interface ViewState : Parcelable { + @Parcelize + data object Loading : ViewState + + @Parcelize + data class Error(val message: String) : ViewState + + @Parcelize + data class Content(val data: SavingAccountDetail) : ViewState + } +} + +internal sealed interface SADEvent { + data class ShowToast(val message: String) : SADEvent + data class OnViewTransaction(val clientId: Long, val accountId: Long) : SADEvent + data object NavigateBack : SADEvent +} + +internal sealed interface SADAction { + data object NavigateBack : SADAction + data class ViewTransaction(val clientId: Long, val accountId: Long) : SADAction + + sealed interface Internal : SADAction { + data class SavingAccountDetailResultReceived( + val result: DataState, + ) : Internal + } +} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountViewModel.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountViewModel.kt deleted file mode 100644 index 1512449d8..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountViewModel.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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.feature.bank.accounts - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mifospay.core.model.domain.BankAccountDetails -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import java.util.Random - -class AccountViewModel : ViewModel() { - - private val _bankAccountDetailsList = MutableStateFlow>(emptyList()) - val bankAccountDetailsList: StateFlow> = _bankAccountDetailsList - - private val _accountsUiState = MutableStateFlow(AccountsUiState.Loading) - val accountsUiState: StateFlow = _accountsUiState - - init { - fetchLinkedAccount() - } - - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() - - fun refresh() { - viewModelScope.launch { - _isRefreshing.emit(true) - fetchLinkedAccount() - _isRefreshing.emit(false) - } - } - - private val mRandom = Random() - - private fun fetchLinkedAccount() { - viewModelScope.launch { - _accountsUiState.value = AccountsUiState.Loading - delay(2000) - val linkedAccounts = fetchSampleLinkedAccounts() - _bankAccountDetailsList.value = linkedAccounts - _accountsUiState.value = if (linkedAccounts.isEmpty()) { - AccountsUiState.Empty - } else { - AccountsUiState.LinkedAccounts(linkedAccounts) - } - } - } - - private fun fetchSampleLinkedAccounts(): List { - return listOf( - BankAccountDetails( - "SBI", - "Ankur Sharma", - "New Delhi", - mRandom.nextInt().toString() + " ", - "Savings", - ), - BankAccountDetails( - "HDFC", - "Mandeep Singh", - "Uttar Pradesh", - mRandom.nextInt().toString() + " ", - "Savings", - ), - BankAccountDetails( - "ANDHRA", - "Rakesh anna", - "Telegana", - mRandom.nextInt().toString() + " ", - "Savings", - ), - BankAccountDetails( - "PNB", - "luv Pro", - "Gujrat", - mRandom.nextInt().toString() + " ", - "Savings", - ), - BankAccountDetails( - "HDF", - "Harry potter", - "Hogwarts", - mRandom.nextInt().toString() + " ", - "Savings", - ), - BankAccountDetails( - "GCI", - "JIGME", - "JAMMU", - mRandom.nextInt().toString() + " ", - "Savings", - ), - BankAccountDetails( - "FCI", - "NISHU BOII", - "ASSAM", - mRandom.nextInt().toString() + " ", - "Savings", - ), - ) - } - - fun addBankAccount(bankAccountDetails: BankAccountDetails) { - viewModelScope.launch { - val updatedList = _bankAccountDetailsList.value.toMutableList().apply { - add(bankAccountDetails) - } - _bankAccountDetailsList.value = updatedList - _accountsUiState.value = AccountsUiState.LinkedAccounts(updatedList) - } - } - - fun updateBankAccount(index: Int, bankAccountDetails: BankAccountDetails) { - viewModelScope.launch { - val updatedList = _bankAccountDetailsList.value.toMutableList().apply { - this[index] = bankAccountDetails - } - _bankAccountDetailsList.value = updatedList - _accountsUiState.value = AccountsUiState.LinkedAccounts(updatedList) - } - } -} - -sealed class AccountsUiState { - data object Loading : AccountsUiState() - data object Empty : AccountsUiState() - data object Error : AccountsUiState() - data class LinkedAccounts(val linkedAccounts: List) : AccountsUiState() -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsItem.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsItem.kt deleted file mode 100644 index fdc951c2f..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsItem.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.feature.bank.accounts - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.mifospay.core.model.domain.BankAccountDetails -import org.mifospay.core.designsystem.component.MifosCard - -@Composable -internal fun AccountsItem( - bankAccountDetails: BankAccountDetails, - onAccountClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - MifosCard( - modifier = modifier, - onClick = { onAccountClicked.invoke() }, - colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface), - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - ) { - Icon( - painter = painterResource(id = R.drawable.feature_accounts_ic_bank), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 16.dp, end = 16.dp) - .size(39.dp), - ) - - Column { - Text( - text = bankAccountDetails.accountholderName.toString(), - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = bankAccountDetails.bankName.toString(), - modifier = Modifier.padding(top = 4.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - Column( - horizontalAlignment = Alignment.End, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = bankAccountDetails.branch.toString(), - modifier = Modifier.padding(16.dp), - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface, - ) - } - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun AccountsItemPreview() { - AccountsItem( - bankAccountDetails = BankAccountDetails("A", "B", "C"), - onAccountClicked = {}, - ) -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsScreen.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsScreen.kt deleted file mode 100644 index cfa68243f..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsScreen.kt +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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.feature.bank.accounts - -import androidx.compose.foundation.background -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.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mifos.library.pullrefresh.PullRefreshIndicator -import com.mifos.library.pullrefresh.pullRefresh -import com.mifos.library.pullrefresh.rememberPullRefreshState -import com.mifospay.core.model.domain.BankAccountDetails -import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfLoadingWheel -import org.mifospay.core.designsystem.icon.MifosIcons -import org.mifospay.core.ui.EmptyContentScreen -import org.mifospay.core.ui.utility.AddCardChip - -@Composable -fun AccountsScreen( - navigateToBankAccountDetailScreen: (BankAccountDetails, Int) -> Unit, - navigateToLinkBankAccountScreen: () -> Unit, - modifier: Modifier = Modifier, - viewModel: AccountViewModel = koinViewModel(), -) { - val accountsUiState by viewModel.accountsUiState.collectAsStateWithLifecycle() - val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() - val bankAccountDetailsList by viewModel.bankAccountDetailsList.collectAsStateWithLifecycle() - - AccountScreen( - modifier = modifier, - accountsUiState = accountsUiState, - onAddAccount = { - navigateToLinkBankAccountScreen.invoke() - }, - bankAccountDetailsList = bankAccountDetailsList, - isRefreshing = isRefreshing, - onRefresh = { - viewModel.refresh() - }, - onUpdateAccount = { bankAccountDetails, index -> - viewModel.updateBankAccount(index, bankAccountDetails) - navigateToBankAccountDetailScreen.invoke(bankAccountDetails, index) - }, - ) -} - -@Composable -private fun AccountScreen( - accountsUiState: AccountsUiState, - onAddAccount: () -> Unit, - bankAccountDetailsList: List, - isRefreshing: Boolean, - onRefresh: () -> Unit, - onUpdateAccount: (BankAccountDetails, Int) -> Unit, - modifier: Modifier = Modifier, -) { - val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) - Box(modifier.pullRefresh(pullRefreshState)) { - Column(modifier = Modifier.fillMaxSize()) { - when (accountsUiState) { - AccountsUiState.Empty -> { - NoLinkedAccountsScreen( - onAddBtn = onAddAccount, - ) - } - - AccountsUiState.Error -> { - EmptyContentScreen( - title = stringResource(id = R.string.feature_accounts_error_oops), - subTitle = stringResource(id = R.string.feature_accounts_unexpected_error_subtitle), - modifier = Modifier, - iconTint = MaterialTheme.colorScheme.onSurface, - iconImageVector = MifosIcons.Info, - ) - } - - is AccountsUiState.LinkedAccounts -> { - LazyColumn( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - ) { - item { - Text( - text = stringResource(id = R.string.feature_accounts_linked_bank_account), - fontSize = 16.sp, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(top = 48.dp, start = 24.dp), - ) - } - items(bankAccountDetailsList) { bankAccountDetails -> - val index = bankAccountDetailsList.indexOf(bankAccountDetails) - AccountsItem( - bankAccountDetails = bankAccountDetails, - onAccountClicked = { - onUpdateAccount(bankAccountDetails, index) - }, - ) - HorizontalDivider( - modifier = Modifier.padding(8.dp), - ) - } - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .background(MaterialTheme.colorScheme.surface), - ) { - AddCardChip( - text = R.string.feature_accounts_add_account, - btnText = R.string.feature_accounts_add_cards, - onAddBtn = onAddAccount, - modifier = Modifier.align(Alignment.Center), - ) - } - } - } - } - - AccountsUiState.Loading -> { - MfLoadingWheel( - contentDesc = stringResource(R.string.feature_accounts_loading), - backgroundColor = MaterialTheme.colorScheme.surface, - ) - } - } - } - PullRefreshIndicator( - refreshing = isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) - } -} - -@Composable -private fun NoLinkedAccountsScreen( - onAddBtn: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = stringResource(R.string.feature_accounts_no_linked_bank_accounts)) - AddCardChip( - text = R.string.feature_accounts_add_account, - btnText = R.string.feature_accounts_add_cards, - onAddBtn = onAddBtn, - modifier = Modifier, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun AccountScreenLoadingPreview() { - AccountScreen( - accountsUiState = AccountsUiState.Loading, - {}, - emptyList(), - false, - {}, - { _, _ -> }, - ) -} - -@Preview(showBackground = true) -@Composable -private fun AccountEmptyScreenPreview() { - AccountScreen(accountsUiState = AccountsUiState.Empty, {}, emptyList(), false, {}, { _, _ -> }) -} - -@Preview(showBackground = true) -@Composable -private fun AccountListScreenPreview() { - AccountScreen( - accountsUiState = AccountsUiState.LinkedAccounts(sampleLinkedAccount), - {}, - sampleLinkedAccount, - false, - {}, - { _, _ -> }, - ) -} - -@Preview(showBackground = true) -@Composable -private fun AccountErrorScreenPreview() { - AccountScreen(accountsUiState = AccountsUiState.Error, {}, emptyList(), false, {}, { _, _ -> }) -} - -val sampleLinkedAccount = List(10) { - BankAccountDetails( - "SBI", - "Ankur Sharma", - "New Delhi", - "XXXXXXXX9990XXX " + " ", - "Savings", - ) -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/choose/sim/ChooseSimDialogSheet.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/choose/sim/ChooseSimDialogSheet.kt deleted file mode 100644 index 97bb1679b..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/choose/sim/ChooseSimDialogSheet.kt +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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.feature.bank.accounts.choose.sim - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -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.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay -import org.mifospay.core.designsystem.component.MifosBottomSheet -import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.theme.MifosTheme -import org.mifospay.feature.bank.accounts.R - -@Composable -internal fun ChooseSimDialogSheet( - onSimSelected: (Int) -> Unit, - modifier: Modifier = Modifier, -) { - MifosBottomSheet( - content = { - ChooseSimDialogSheetContent( - onSimSelected = onSimSelected, - ) - }, - onDismiss = { - onSimSelected.invoke(-1) - }, - modifier = modifier, - ) -} - -/** - * TODO Read Device SIM and show numbers accordingly If one sim exist then show only one otherwise - * show both of them and implement send SMS after select and confirm. - */ -@Composable -@Suppress("LongMethod") -private fun ChooseSimDialogSheetContent( - onSimSelected: (Int) -> Unit, - modifier: Modifier = Modifier, -) { - var selectedSim by rememberSaveable { mutableIntStateOf(-1) } - var showMessage by remember { mutableStateOf(false) } - val message = stringResource(id = R.string.feature_accounts_choose_a_sim) - - LaunchedEffect(key1 = showMessage) { - if (showMessage) { - delay(5000) - showMessage = false - } - } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .padding(8.dp), - ) { - Text( - text = stringResource(id = R.string.feature_accounts_verify_mobile_number), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.feature_accounts_confirm_mobile_number_message), - style = MaterialTheme.typography.bodySmall.copy( - textAlign = TextAlign.Center, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = stringResource(id = R.string.feature_accounts_bank_account_mobile_verification_conditions), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.height(20.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - SimCard( - simNumber = 1, - isSelected = selectedSim == 1, - onSimSelected = { selectedSim = 1 }, - ) - - Spacer(modifier = Modifier.width(24.dp)) - Text( - text = stringResource(id = R.string.feature_accounts_or), - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.width(24.dp)) - SimCard( - simNumber = 2, - isSelected = selectedSim == 2, - onSimSelected = { selectedSim = 2 }, - ) - } - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.feature_accounts_regular_charges_will_apply), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodySmall, - ) - - AnimatedVisibility( - visible = showMessage, - ) { - Text( - text = message, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(vertical = 4.dp), - ) - } - - MifosButton( - modifier = Modifier - .width(200.dp) - .padding(top = 16.dp), - onClick = { - if (selectedSim == -1) { - showMessage = true - } else { - onSimSelected(selectedSim) - } - }, - ) { - Text(text = stringResource(id = R.string.feature_accounts_confirm)) - } - Spacer(modifier = Modifier.height(24.dp)) - } -} - -@Composable -private fun SimCard( - simNumber: Int, - isSelected: Boolean, - onSimSelected: () -> Unit, - modifier: Modifier = Modifier, -) { - val drawable: Painter = painterResource( - id = if (isSelected) { - R.drawable.feature_accounts_sim_card_selected - } else { - R.drawable.feature_accounts_sim_card_unselected - }, - ) - Image( - painter = drawable, - contentDescription = "SIM Card $simNumber", - contentScale = ContentScale.Fit, - modifier = modifier - .size(50.dp) - .clickable { onSimSelected() }, - ) -} - -@Preview -@Composable -private fun SimSelectionPreview() { - MifosTheme { - Surface { - ChooseSimDialogSheetContent( - onSimSelected = {}, - ) - } - } -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/details/BankAccountDetailScreen.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/details/BankAccountDetailScreen.kt deleted file mode 100644 index 4e99dae28..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/details/BankAccountDetailScreen.kt +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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.feature.bank.accounts.details - -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -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.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.mifospay.core.model.domain.BankAccountDetails -import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.component.MifosTopBar -import org.mifospay.core.designsystem.icon.MifosIcons -import org.mifospay.feature.bank.accounts.R - -@Composable -internal fun BankAccountDetailScreen( - bankAccountDetails: BankAccountDetails, - onSetupUpiPin: () -> Unit, - onChangeUpiPin: () -> Unit, - onForgotUpiPin: () -> Unit, - navigateBack: () -> Unit, -) { - BankAccountDetailScreen( - bankName = bankAccountDetails.bankName.toString(), - accountHolderName = bankAccountDetails.accountholderName.toString(), - branchName = bankAccountDetails.branch.toString(), - ifsc = bankAccountDetails.ifsc.toString(), - type = bankAccountDetails.type.toString(), - isUpiEnabled = bankAccountDetails.isUpiEnabled, - onSetupUpiPin = onSetupUpiPin, - onChangeUpiPin = onChangeUpiPin, - onForgotUpiPin = onForgotUpiPin, - navigateBack = navigateBack, - ) -} - -@Composable -private fun BankAccountDetailScreen( - bankName: String, - accountHolderName: String, - branchName: String, - ifsc: String, - type: String, - isUpiEnabled: Boolean, - onSetupUpiPin: () -> Unit, - onChangeUpiPin: () -> Unit, - onForgotUpiPin: () -> Unit, - navigateBack: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxSize(), - ) { - MifosTopBar( - topBarTitle = R.string.feature_accounts_bank_account_details, - backPress = navigateBack, - ) - - Column( - modifier = Modifier - .padding(20.dp) - .border(2.dp, MaterialTheme.colorScheme.onSurface) - .padding(20.dp), - ) { - BankAccountDetailRows( - modifier = Modifier.fillMaxWidth(), - detail = R.string.feature_accounts_bank_name, - detailValue = bankName, - ) - BankAccountDetailRows( - modifier = Modifier - .fillMaxWidth() - .padding(top = 10.dp), - detail = R.string.feature_accounts_ac_holder_name, - detailValue = accountHolderName, - ) - BankAccountDetailRows( - modifier = Modifier - .fillMaxWidth() - .padding(top = 10.dp), - detail = R.string.feature_accounts_branch_name, - detailValue = branchName, - ) - BankAccountDetailRows( - modifier = Modifier - .fillMaxWidth() - .padding(top = 10.dp), - detail = R.string.feature_accounts_ifsc, - detailValue = ifsc, - ) - BankAccountDetailRows( - modifier = Modifier - .fillMaxWidth() - .padding(top = 10.dp), - detail = R.string.feature_accounts_type, - detailValue = type, - ) - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - BankAccountDetailButton( - btnText = R.string.feature_accounts_setup_upi, - onClick = { onSetupUpiPin.invoke() }, - isUpiEnabled = !isUpiEnabled, - hasTrailingIcon = false, - ) - - BankAccountDetailButton( - btnText = R.string.feature_accounts_delete_bank, - onClick = {}, - isUpiEnabled = !isUpiEnabled, - ) - } - - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(20.dp), - ) { - BankAccountDetailButton( - btnText = R.string.feature_accounts_change_upi_pin, - onClick = { onChangeUpiPin.invoke() }, - isUpiEnabled = isUpiEnabled, - modifier = Modifier.fillMaxWidth(), - ) - BankAccountDetailButton( - btnText = R.string.feature_accounts_forgot_upi_pin, - onClick = { onForgotUpiPin.invoke() }, - isUpiEnabled = isUpiEnabled, - modifier = Modifier.fillMaxWidth(), - ) - } - } -} - -@Composable -private fun BankAccountDetailRows( - detail: Int, - detailValue: String, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = detail), - modifier = Modifier.padding(end = 10.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = detailValue, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } -} - -@Composable -private fun BankAccountDetailButton( - btnText: Int, - onClick: () -> Unit, - isUpiEnabled: Boolean, - modifier: Modifier = Modifier, - hasTrailingIcon: Boolean = false, -) { - if (isUpiEnabled) { - MifosButton( - onClick = { onClick.invoke() }, - colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary), - modifier = modifier - .padding(start = 20.dp, end = 20.dp), - contentPadding = PaddingValues(20.dp), - ) { - Row( - modifier = Modifier, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = btnText), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onPrimary, - ) - if (hasTrailingIcon) { - Icon( - imageVector = MifosIcons.ChevronRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun BankAccountDetailUpiDisabledPreview() { - BankAccountDetailScreen( - "Mifos Bank", - "Mifos Account Holder", - "Mifos Branch", - "IFSC", - "type", - false, - {}, {}, {}, {}, - ) -} - -@Preview(showBackground = true) -@Composable -private fun BankAccountDetailUpiEnabledPreview() { - BankAccountDetailScreen( - "Mifos Bank", - "Mifos Account Holder", - "Mifos Branch", - "IFSC", - "type", - true, - {}, {}, {}, {}, - ) -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/di/AccountsModule.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/di/AccountsModule.kt deleted file mode 100644 index db21bb51c..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/di/AccountsModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.feature.bank.accounts.di - -import org.koin.core.module.dsl.viewModel -import org.koin.dsl.module -import org.mifospay.core.data.di.DataModule -import org.mifospay.feature.bank.accounts.AccountViewModel -import org.mifospay.feature.bank.accounts.link.LinkBankAccountViewModel - -val AccountsModule = module { - includes(DataModule) - viewModel { - LinkBankAccountViewModel(localAssetRepository = get()) - } - viewModel { - AccountViewModel() - } -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountScreen.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountScreen.kt deleted file mode 100644 index d51bfce27..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountScreen.kt +++ /dev/null @@ -1,347 +0,0 @@ -/* - * 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.feature.bank.accounts.link - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowColumn -import androidx.compose.foundation.layout.FlowRow -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.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mifospay.core.model.domain.Bank -import com.mifospay.core.model.domain.BankType -import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfLoadingWheel -import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel -import org.mifospay.core.designsystem.component.MifosCard -import org.mifospay.core.designsystem.component.MifosOutlinedTextField -import org.mifospay.core.designsystem.component.MifosTopAppBar -import org.mifospay.core.designsystem.icon.MifosIcons -import org.mifospay.core.designsystem.theme.MifosTheme -import org.mifospay.core.ui.DevicePreviews -import org.mifospay.feature.bank.accounts.R -import org.mifospay.feature.bank.accounts.choose.sim.ChooseSimDialogSheet - -@Composable -internal fun LinkBankAccountRoute( - viewModel: LinkBankAccountViewModel = koinViewModel(), - onBackClick: () -> Unit, -) { - val bankUiState by viewModel.bankListUiState.collectAsStateWithLifecycle() - var showSimBottomSheet by rememberSaveable { mutableStateOf(false) } - var showOverlyProgressBar by rememberSaveable { mutableStateOf(false) } - - if (showSimBottomSheet) { - ChooseSimDialogSheet( - onSimSelected = { selectedSim -> - showSimBottomSheet = false - if (selectedSim != -1) { - showOverlyProgressBar = true - viewModel.fetchBankAccountDetails { - showOverlyProgressBar = false - onBackClick() - } - } - }, - ) - } - - LinkBankAccountScreen( - bankUiState = bankUiState, - showOverlyProgressBar = showOverlyProgressBar, - onBankSearch = { query -> - viewModel.updateSearchQuery(query) - }, - onBankSelected = { - viewModel.updateSelectedBank(it) - showSimBottomSheet = true - }, - onBackClick = onBackClick, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun LinkBankAccountScreen( - bankUiState: BankUiState, - showOverlyProgressBar: Boolean, - onBankSearch: (String) -> Unit, - onBankSelected: (Bank) -> Unit, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Scaffold( - modifier = modifier - .background(color = MaterialTheme.colorScheme.surface), - topBar = { - MifosTopAppBar( - titleRes = R.string.feature_accounts_link_bank_account, - navigationIcon = MifosIcons.Back, - navigationIconContentDescription = "Back icon", - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - onNavigationClick = onBackClick, - ) - }, - ) { paddingValues -> - Box( - modifier = Modifier - .padding(paddingValues), - ) { - when (bankUiState) { - is BankUiState.Loading -> { - MfLoadingWheel( - contentDesc = stringResource(R.string.feature_accounts_loading), - backgroundColor = MaterialTheme.colorScheme.surface, - ) - } - - is BankUiState.Success -> { - BankListScreenContent( - banks = bankUiState.banks, - onBankSearch = onBankSearch, - onBankSelected = onBankSelected, - ) - } - } - - if (showOverlyProgressBar) { - MfOverlayLoadingWheel() - } - } - } -} - -@Composable -private fun BankListScreenContent( - banks: List, - onBankSearch: (String) -> Unit, - onBankSelected: (Bank) -> Unit, - modifier: Modifier = Modifier, -) { - var searchQuery by rememberSaveable { mutableStateOf("") } - Column( - modifier = modifier - .fillMaxSize() - .background(color = MaterialTheme.colorScheme.surface) - .verticalScroll(rememberScrollState()), - ) { - MifosOutlinedTextField( - label = R.string.feature_accounts_search, - value = searchQuery, - onValueChange = { - searchQuery = it - onBankSearch(it) - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - trailingIcon = { - Icon(imageVector = MifosIcons.Search, contentDescription = null) - }, - ) - - if (searchQuery.isBlank()) { - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = stringResource(id = R.string.feature_accounts_popular_banks), - style = TextStyle( - MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Medium, - ), - modifier = Modifier.padding(start = 16.dp), - ) - Spacer(modifier = Modifier.height(12.dp)) - PopularBankGridBody( - banks = banks.filter { it.bankType == BankType.POPULAR }, - onBankSelected = onBankSelected, - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = stringResource(id = R.string.feature_accounts_other_banks), - style = TextStyle( - MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Medium, - ), - modifier = Modifier.padding(start = 16.dp), - ) - Spacer(modifier = Modifier.height(12.dp)) - } - - BankListBody( - banks = if (searchQuery.isBlank()) { - banks.filter { it.bankType == BankType.OTHER } - } else { - banks - }, - onBankSelected = onBankSelected, - ) - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun PopularBankGridBody( - banks: List, - onBankSelected: (Bank) -> Unit, - modifier: Modifier = Modifier, -) { - MifosCard( - modifier = modifier, - shape = RoundedCornerShape(0.dp), - elevation = 2.dp, - colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface), - ) { - FlowRow( - modifier = Modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), - maxItemsInEachRow = 3, - ) { - banks.forEach { - PopularBankItemBody( - modifier = Modifier.weight(1f), - bank = it, - onBankSelected = onBankSelected, - ) - } - } - } -} - -@Composable -private fun PopularBankItemBody( - bank: Bank, - onBankSelected: (Bank) -> Unit, - modifier: Modifier = Modifier, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = modifier - .fillMaxSize() - .clickable { - onBankSelected(bank) - }, - ) { - Image( - modifier = Modifier - .size(58.dp) - .padding(bottom = 4.dp, top = 16.dp), - painter = painterResource(id = bank.image), - contentDescription = bank.name, - ) - Text( - text = bank.name, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), - ) - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun BankListBody( - banks: List, - onBankSelected: (Bank) -> Unit, - modifier: Modifier = Modifier, -) { - FlowColumn(modifier) { - banks.forEach { bank -> - BankListItemBody(bank = bank, onBankSelected = onBankSelected) - } - } -} - -@Composable -private fun BankListItemBody( - bank: Bank, - onBankSelected: (Bank) -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxSize() - .clickable { onBankSelected(bank) }, - ) { - HorizontalDivider() - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, top = 8.dp, bottom = 8.dp), - ) { - Image( - modifier = Modifier.size(32.dp), - painter = painterResource(id = bank.image), - contentDescription = bank.name, - ) - Text( - modifier = Modifier.padding(start = 16.dp, end = 16.dp), - text = bank.name, - style = TextStyle(fontSize = 14.sp), - ) - } - } -} - -@DevicePreviews -@Composable -private fun LinkBankAccountScreenPreview( - @PreviewParameter(LinkBankUiStatePreviewParameterProvider::class) - bankUiState: BankUiState, -) { - MifosTheme { - LinkBankAccountScreen( - bankUiState = bankUiState, - showOverlyProgressBar = false, - onBankSelected = { }, - onBankSearch = { }, - onBackClick = { }, - ) - } -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountViewModel.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountViewModel.kt deleted file mode 100644 index 2d23eb372..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountViewModel.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.feature.bank.accounts.link - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mifospay.core.model.domain.Bank -import com.mifospay.core.model.domain.BankAccountDetails -import com.mifospay.core.model.domain.BankType -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import org.mifospay.core.data.repository.local.LocalAssetRepository -import org.mifospay.feature.bank.accounts.R -import java.util.Random - -class LinkBankAccountViewModel( - localAssetRepository: LocalAssetRepository, -) : ViewModel() { - - private val searchQuery = MutableStateFlow("") - private var selectedBank by mutableStateOf(null) - - private val accountDetails: MutableStateFlow = MutableStateFlow(null) - val bankAccountDetails: StateFlow = accountDetails.asStateFlow() - - fun updateSearchQuery(query: String) { - searchQuery.update { query } - } - - fun updateSelectedBank(bank: Bank) { - selectedBank = bank - } - - val bankListUiState: StateFlow = combine( - searchQuery, - localAssetRepository.getBanks(), - ::Pair, - ).map { searchQueryAndBanks -> - val searchQuery = searchQueryAndBanks.first - val localBanks = searchQueryAndBanks.second.map { - Bank(it, R.drawable.feature_accounts_ic_bank, BankType.OTHER) - } - val banks = ArrayList().apply { - addAll(popularBankList()) - addAll(localBanks) - }.distinctBy { it.name } - BankUiState.Success( - banks.filter { it.name.contains(searchQuery.lowercase(), ignoreCase = true) }, - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = BankUiState.Loading, - ) - - private fun popularBankList(): List { - return listOf( - Bank("RBL Bank", R.drawable.feature_accounts_logo_rbl, BankType.POPULAR), - Bank("SBI Bank", R.drawable.feature_accounts_logo_sbi, BankType.POPULAR), - Bank("PNB Bank", R.drawable.feature_accounts_logo_pnb, BankType.POPULAR), - Bank("HDFC Bank", R.drawable.feature_accounts_logo_hdfc, BankType.POPULAR), - Bank("ICICI Bank", R.drawable.feature_accounts_logo_icici, BankType.POPULAR), - Bank("AXIS Bank", R.drawable.feature_accounts_logo_axis, BankType.POPULAR), - ) - } - - fun fetchBankAccountDetails(onBankDetailsSuccess: () -> Unit) { - // TODO:: UPI API implement, Implement with real API, - // It revert back to Account Screen after successful BankAccount Add - accountDetails.update { - BankAccountDetails( - selectedBank?.name, - "Ankur Sharma", - "New Delhi", - mRandom.nextInt().toString() + " ", - "Savings", - ) - } - onBankDetailsSuccess.invoke() - } - - companion object { - private val mRandom = Random() - } -} - -sealed interface BankUiState { - data class Success(val banks: List = emptyList()) : BankUiState - data object Loading : BankUiState -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankUiStatePreviewParameterProvider.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankUiStatePreviewParameterProvider.kt deleted file mode 100644 index dc723e98a..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankUiStatePreviewParameterProvider.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.feature.bank.accounts.link - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.mifospay.core.model.domain.Bank -import com.mifospay.core.model.domain.BankType -import org.mifospay.feature.bank.accounts.R - -class LinkBankUiStatePreviewParameterProvider : PreviewParameterProvider { - - val banks = ArrayList().apply { - add(Bank("RBL Bank", R.drawable.feature_accounts_logo_rbl, BankType.POPULAR)) - add(Bank("SBI Bank", R.drawable.feature_accounts_logo_sbi, BankType.POPULAR)) - add(Bank("PNB Bank", R.drawable.feature_accounts_logo_pnb, BankType.POPULAR)) - add(Bank("HDFC Bank", R.drawable.feature_accounts_logo_hdfc, BankType.POPULAR)) - add(Bank("ICICI Bank", R.drawable.feature_accounts_logo_icici, BankType.POPULAR)) - add(Bank("AXIS Bank", R.drawable.feature_accounts_logo_axis, BankType.POPULAR)) - add(Bank("HDFC Bank", R.drawable.feature_accounts_ic_bank, BankType.OTHER)) - add(Bank("ICICI Bank", R.drawable.feature_accounts_ic_bank, BankType.OTHER)) - add(Bank("AXIS Bank", R.drawable.feature_accounts_ic_bank, BankType.OTHER)) - } - - override val values: Sequence = sequenceOf( - BankUiState.Success(banks = banks), - ) -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/BankAccountDetailNavigation.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/BankAccountDetailNavigation.kt deleted file mode 100644 index 43c2f723b..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/BankAccountDetailNavigation.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.feature.bank.accounts.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.mifospay.core.model.domain.BankAccountDetails -import org.mifospay.core.common.Constants -import org.mifospay.feature.bank.accounts.details.BankAccountDetailScreen - -const val BANK_ACCOUNT_DETAIL_ROUTE = "bank_account_detail_route" - -fun NavGraphBuilder.bankAccountDetailScreen( - onSetupUpiPin: (BankAccountDetails, Int) -> Unit, - onChangeUpiPin: (BankAccountDetails, Int) -> Unit, - onForgotUpiPin: (BankAccountDetails, Int) -> Unit, - onBackClick: (BankAccountDetails, Int) -> Unit, -) { - composable( - route = "$BANK_ACCOUNT_DETAIL_ROUTE/{${Constants.BANK_ACCOUNT_DETAILS}}/{${Constants.INDEX}}", - arguments = listOf( - navArgument(Constants.BANK_ACCOUNT_DETAILS) { type = NavType.StringType }, - navArgument(Constants.INDEX) { type = NavType.IntType }, - ), - ) { backStackEntry -> - val bankAccountDetails = - backStackEntry.arguments?.getParcelable(Constants.BANK_ACCOUNT_DETAILS) - ?: BankAccountDetails("", "", "", "", "") - val index = backStackEntry.arguments?.getInt(Constants.INDEX) ?: 0 - - BankAccountDetailScreen( - bankAccountDetails = bankAccountDetails, - onSetupUpiPin = { onSetupUpiPin(bankAccountDetails, index) }, - onChangeUpiPin = { - if (bankAccountDetails.isUpiEnabled) { - onChangeUpiPin(bankAccountDetails, index) - } else { - // TODO: Use global snackbar - } - }, - onForgotUpiPin = { - if (bankAccountDetails.isUpiEnabled) { - onForgotUpiPin(bankAccountDetails, index) - } else { - // TODO: Use global snackbar - } - }, - navigateBack = { onBackClick(bankAccountDetails, index) }, - ) - } -} - -fun NavController.navigateToBankAccountDetail( - bankAccountDetails: BankAccountDetails, - index: Int, - navOptions: NavOptions? = null, -) { - this.navigate("$BANK_ACCOUNT_DETAIL_ROUTE/$bankAccountDetails/$index", navOptions) -} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/LinkBankAccountNavigation.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/LinkBankAccountNavigation.kt deleted file mode 100644 index d2062eb87..000000000 --- a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/navigation/LinkBankAccountNavigation.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.feature.bank.accounts.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import org.mifospay.feature.bank.accounts.link.LinkBankAccountRoute - -const val LINK_BANK_ACCOUNT_ROUTE = "link_bank_account_route" - -fun NavController.navigateToLinkBankAccount(navOptions: NavOptions? = null) = - navigate(LINK_BANK_ACCOUNT_ROUTE, navOptions) - -fun NavGraphBuilder.linkBankAccountScreen( - onBackClick: () -> Unit, -) { - composable(route = LINK_BANK_ACCOUNT_ROUTE) { - LinkBankAccountRoute( - onBackClick = onBackClick, - ) - } -} diff --git a/feature/accounts/src/main/res/values/colors.xml b/feature/accounts/src/main/res/values/colors.xml deleted file mode 100644 index 31b420f82..000000000 --- a/feature/accounts/src/main/res/values/colors.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - #DE000000 - @color/feature_accounts_colorBlack87 - #303F9F - \ No newline at end of file diff --git a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt index f8aa21734..2e6b105ef 100644 --- a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt +++ b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt @@ -20,8 +20,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -33,8 +31,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -54,10 +50,10 @@ import org.mifospay.core.designsystem.component.MifosBasicDialog import org.mifospay.core.designsystem.component.MifosButton import org.mifospay.core.designsystem.component.MifosLoadingDialog import org.mifospay.core.designsystem.component.MifosOutlinedTextField -import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.MifosTheme import org.mifospay.core.designsystem.theme.grey import org.mifospay.core.designsystem.theme.styleNormal18sp +import org.mifospay.core.ui.MifosPasswordField import org.mifospay.core.ui.utils.EventsEffect @Composable @@ -165,29 +161,16 @@ private fun LoginScreenContent( modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.padding(top = 16.dp)) - MifosOutlinedTextField( + MifosPasswordField( label = stringResource(Res.string.feature_auth_password), value = state.password, onValueChange = { onEvent(LoginAction.PasswordChanged(it)) }, modifier = Modifier.fillMaxWidth(), - visualTransformation = if (state.isPasswordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - trailingIcon = { - val icon = if (state.isPasswordVisible) { - MifosIcons.Visibility - } else { - MifosIcons.VisibilityOff - } - IconButton( - onClick = { onEvent(LoginAction.TogglePasswordVisibility) }, - ) { - Icon(imageVector = icon, null) - } + showPassword = state.isPasswordVisible, + showPasswordChange = { + onEvent(LoginAction.TogglePasswordVisibility) }, ) val isLoginButtonEnabled = state.username.isNotEmpty() && state.password.isNotEmpty() diff --git a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt index f2b8aaca8..5db33e8e6 100644 --- a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt +++ b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt @@ -13,10 +13,10 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState import org.mifospay.core.common.IgnoredOnParcel import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize -import org.mifospay.core.common.Result import org.mifospay.core.domain.LoginUseCase import org.mifospay.core.model.user.UserInfo import org.mifospay.core.ui.utils.BaseViewModel @@ -76,7 +76,7 @@ class LoginViewModel( private fun handleLoginResult(action: LoginAction.Internal.ReceiveLoginResult) { when (action.loginResult) { - is Result.Error -> { + is DataState.Error -> { val message = action.loginResult.exception.message ?: "" mutableStateFlow.update { @@ -84,13 +84,13 @@ class LoginViewModel( } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = LoginState.DialogState.Loading) } } - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(dialogState = null) } @@ -148,7 +148,7 @@ sealed class LoginAction { sealed class Internal : LoginAction() { data class ReceiveLoginResult( - val loginResult: Result, + val loginResult: DataState, ) : Internal() } } diff --git a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationViewModel.kt b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationViewModel.kt index c90595ad5..925cb7751 100644 --- a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationViewModel.kt +++ b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationViewModel.kt @@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize -import org.mifospay.core.common.Result import org.mifospay.core.data.repository.SearchRepository import org.mifospay.core.data.util.Constants import org.mifospay.core.ui.utils.BaseViewModel @@ -102,7 +102,7 @@ class MobileVerificationViewModel( ) when (result) { - is Result.Error -> { + is DataState.Error -> { val message = result.exception.message ?: "Something Went Wrong!" mutableStateFlow.update { currentState.copy( @@ -111,13 +111,13 @@ class MobileVerificationViewModel( } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { currentState.copy(dialogState = MobileVerificationState.DialogState.Loading) } } - is Result.Success -> { + is DataState.Success -> { if (result.data.isEmpty()) { requestAnOtpToPhoneNo(phoneNo) } else { @@ -153,7 +153,7 @@ class MobileVerificationViewModel( ) } - sendAction(ReceiveOtpVerifyResult(Result.Success(state.phoneNo))) + sendAction(ReceiveOtpVerifyResult(DataState.Success(state.phoneNo))) } else { mutableStateFlow.update { state.copy( @@ -166,7 +166,7 @@ class MobileVerificationViewModel( private fun handleOtpVerifyResult(action: ReceiveOtpVerifyResult) { when (action.loginResult) { - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { (state as? MobileVerificationState.VerifyOtpState)?.copy( dialogState = MobileVerificationState.DialogState.Error("Otp Verification Failed"), @@ -174,7 +174,7 @@ class MobileVerificationViewModel( } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { (state as? MobileVerificationState.VerifyOtpState)?.copy( dialogState = MobileVerificationState.DialogState.Loading, @@ -182,7 +182,7 @@ class MobileVerificationViewModel( } } - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { (state as? MobileVerificationState.VerifyOtpState)?.copy( dialogState = null, @@ -281,7 +281,7 @@ sealed interface MobileVerificationAction { sealed class Internal : MobileVerificationAction { data class ReceiveOtpVerifyResult( - val loginResult: Result, + val loginResult: DataState, ) : Internal() } } diff --git a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupScreen.kt b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupScreen.kt index 29455000f..106ff660e 100644 --- a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupScreen.kt +++ b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupScreen.kt @@ -56,10 +56,10 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.mifospay.core.designsystem.component.BasicDialogState import org.mifospay.core.designsystem.component.LoadingDialogState -import org.mifospay.core.designsystem.component.MfOutlinedTextField import org.mifospay.core.designsystem.component.MifosBasicDialog import org.mifospay.core.designsystem.component.MifosButton import org.mifospay.core.designsystem.component.MifosLoadingDialog +import org.mifospay.core.designsystem.component.MifosOutlinedTextField import org.mifospay.core.designsystem.component.MifosScaffold import org.mifospay.core.designsystem.component.MifosTopAppBar import org.mifospay.core.designsystem.icon.MifosIcons @@ -157,7 +157,7 @@ private fun SignupScreenContent( .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - MfOutlinedTextField( + MifosOutlinedTextField( value = state.firstNameInput, label = stringResource(Res.string.feature_auth_first_name), modifier = Modifier.fillMaxWidth(), @@ -167,7 +167,7 @@ private fun SignupScreenContent( isError = state.firstNameInput.isEmpty(), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.lastNameInput, label = stringResource(Res.string.feature_auth_last_name), modifier = Modifier.fillMaxWidth(), @@ -177,7 +177,7 @@ private fun SignupScreenContent( isError = state.lastNameInput.isEmpty(), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.userNameInput, label = stringResource(Res.string.feature_auth_username), modifier = Modifier.fillMaxWidth(), @@ -187,7 +187,7 @@ private fun SignupScreenContent( isError = state.userNameInput.isEmpty(), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.emailInput, label = stringResource(Res.string.feature_auth_email), modifier = Modifier.fillMaxWidth(), @@ -197,7 +197,7 @@ private fun SignupScreenContent( isError = state.emailInput.isEmpty(), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.mobileNumberInput, label = stringResource(Res.string.feature_auth_mobile_no), modifier = Modifier.fillMaxWidth(), @@ -235,7 +235,7 @@ private fun SignupScreenContent( showPasswordChange = { showPassword = !showPassword }, ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.addressLine1Input, label = stringResource(Res.string.feature_auth_address_line_1), modifier = Modifier.fillMaxWidth(), @@ -245,7 +245,7 @@ private fun SignupScreenContent( isError = state.addressLine1Input.isEmpty(), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.addressLine2Input, modifier = Modifier.fillMaxWidth(), label = stringResource(Res.string.feature_auth_address_line_2), @@ -255,7 +255,7 @@ private fun SignupScreenContent( isError = state.addressLine2Input.isEmpty(), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.pinCodeInput, label = stringResource(Res.string.feature_auth_pin_code), modifier = Modifier.fillMaxWidth(), @@ -270,7 +270,7 @@ private fun SignupScreenContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - MfOutlinedTextField( + MifosOutlinedTextField( value = state.countryInput, label = stringResource(Res.string.feature_auth_country), onValueChange = { @@ -280,7 +280,7 @@ private fun SignupScreenContent( isError = state.countryInput.isEmpty(), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = state.stateInput, label = stringResource(Res.string.feature_auth_state), onValueChange = { diff --git a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt index acf08f7e2..82b7cd68d 100644 --- a/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt +++ b/feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt @@ -16,9 +16,9 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize -import org.mifospay.core.common.Result import org.mifospay.core.common.utils.isValidEmail import org.mifospay.core.data.repository.ClientRepository import org.mifospay.core.data.repository.SearchRepository @@ -210,18 +210,18 @@ class SignupViewModel( private fun handleSignUpResult(action: SignUpAction.Internal.ReceiveRegisterResult) { when (val result = action.registerResult) { - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(dialogState = null) } sendEvent(SignUpEvent.NavigateToLogin(result.data)) } - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy(dialogState = SignUpDialog.Error(result.exception.message.toString())) } } - Result.Loading -> { + DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = SignUpDialog.Loading) } } @@ -365,14 +365,14 @@ class SignupViewModel( ) when (result) { - is Result.Error -> { + is DataState.Error -> { val message = result.exception.message.toString() mutableStateFlow.update { it.copy(dialogState = SignUpDialog.Error(message)) } } - is Result.Success -> { + is DataState.Success -> { if (result.data.isEmpty()) { // Username is unique val newUser = NewUser( @@ -391,7 +391,7 @@ class SignupViewModel( } } - is Result.Loading -> Unit + is DataState.Loading -> Unit } } } @@ -399,18 +399,18 @@ class SignupViewModel( private fun createUser(newUser: NewUser) { viewModelScope.launch { when (val result = userRepository.createUser(newUser)) { - is Result.Error -> { + is DataState.Error -> { val message = result.exception.message.toString() mutableStateFlow.update { it.copy(dialogState = SignUpDialog.Error(message)) } } - is Result.Success -> { + is DataState.Success -> { createClient(result.data) } - is Result.Loading -> Unit + is DataState.Loading -> Unit } } } @@ -433,7 +433,7 @@ class SignupViewModel( ) when (val result = clientRepository.createClient(newClient)) { - is Result.Error -> { + is DataState.Error -> { deleteUser(userId) val message = result.exception.message.toString() mutableStateFlow.update { @@ -441,11 +441,11 @@ class SignupViewModel( } } - is Result.Success -> { + is DataState.Success -> { assignClientToUser(result.data, userId) } - is Result.Loading -> Unit + is DataState.Loading -> Unit } } } @@ -453,7 +453,7 @@ class SignupViewModel( private fun assignClientToUser(clientId: Int, userId: Int) { viewModelScope.launch { when (val result = userRepository.assignClientToUser(userId, clientId)) { - is Result.Error -> { + is DataState.Error -> { deleteUser(userId) deleteClient(clientId) val message = result.exception.message.toString() @@ -462,19 +462,19 @@ class SignupViewModel( } } - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(dialogState = null) } sendEvent(SignUpEvent.ShowToast("Registration successful.")) sendAction( SignUpAction.Internal.ReceiveRegisterResult( - Result.Success(state.userNameInput), + DataState.Success(state.userNameInput), ), ) } - is Result.Loading -> Unit + is DataState.Loading -> Unit } } } @@ -582,7 +582,7 @@ sealed interface SignUpAction { sealed class Internal : SignUpAction { data class ReceiveRegisterResult( - val registerResult: Result, + val registerResult: DataState, ) : Internal() data class ReceivePasswordStrengthResult( diff --git a/feature/editpassword/src/commonMain/kotlin/org/mifospay/feature/editpassword/EditPasswordViewModel.kt b/feature/editpassword/src/commonMain/kotlin/org/mifospay/feature/editpassword/EditPasswordViewModel.kt index c6adfbe03..2ef978ea5 100644 --- a/feature/editpassword/src/commonMain/kotlin/org/mifospay/feature/editpassword/EditPasswordViewModel.kt +++ b/feature/editpassword/src/commonMain/kotlin/org/mifospay/feature/editpassword/EditPasswordViewModel.kt @@ -18,9 +18,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize -import org.mifospay.core.common.Result import org.mifospay.core.data.repository.UserRepository import org.mifospay.core.datastore.UserPreferencesRepository import org.mifospay.core.ui.PasswordStrengthState @@ -129,19 +129,19 @@ internal class EditPasswordViewModel( private fun handleResult(action: ReceiveUpdatePasswordResult) { when (val result = action.result) { - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(dialogState = null) } sendEvent(EditPasswordEvent.ShowToast(result.data)) sendEvent(EditPasswordEvent.OnLogoutUser) } - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy(dialogState = Error(result.exception.message.toString())) } } - Result.Loading -> { + DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = EditPasswordDialog.Loading) } } } @@ -257,7 +257,7 @@ internal sealed interface EditPasswordAction { sealed class Internal : EditPasswordAction { data class ReceiveUpdatePasswordResult( - val result: Result, + val result: DataState, ) : Internal() data class ReceivePasswordStrengthResult( diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryViewModel.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryViewModel.kt index a6c7c4d6a..591223b1f 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryViewModel.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryViewModel.kt @@ -10,21 +10,20 @@ package org.mifospay.feature.history import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.repository.SelfServiceRepository import org.mifospay.core.datastore.UserPreferencesRepository import org.mifospay.core.model.savingsaccount.Transaction import org.mifospay.core.model.savingsaccount.TransactionType import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.history.HistoryAction.Internal.TransactionsLoaded class HistoryViewModel( private val preferencesRepository: UserPreferencesRepository, - private val repository: SelfServiceRepository, + repository: SelfServiceRepository, ) : BaseViewModel( initialState = run { val clientId = requireNotNull(preferencesRepository.clientId.value) @@ -35,9 +34,10 @@ class HistoryViewModel( ) }, ) { - init { - trySendAction(HistoryAction.LoadTransactions) + repository.getAccountsTransactions(state.clientId).onEach { + sendAction(TransactionsLoaded(it)) + }.launchIn(viewModelScope) } override fun handleAction(action: HistoryAction) { @@ -48,50 +48,10 @@ class HistoryViewModel( sendEvent(HistoryEvent.OnTransactionDetail(action.transferId)) } - HistoryAction.LoadTransactions -> loadAccounts(state.clientId) + is TransactionsLoaded -> handleTransactionLoaded(action) } } - private fun loadAccounts(clientId: Long) { - viewModelScope.launch { - mutableStateFlow.update { it.copy(viewState = HistoryState.ViewState.Loading) } - when (val result = repository.getSelfAccounts(clientId)) { - is Result.Error -> { - mutableStateFlow.update { - it.copy(viewState = HistoryState.ViewState.Error("No accounts found")) - } - } - - is Result.Loading -> { - mutableStateFlow.update { it.copy(viewState = HistoryState.ViewState.Loading) } - } - - is Result.Success -> { - loadTransactions(result.data.first().id) - } - } - } - } - - private fun loadTransactions(accountId: Long) { - repository.getSelfAccountTransactions(accountId).onEach { result -> - mutableStateFlow.update { currentState -> - currentState.copy( - transactions = result, - viewState = if (result.isEmpty()) { - HistoryState.ViewState.Empty - } else { - HistoryState.ViewState.Content(result) - }, - ) - } - }.catch { - mutableStateFlow.update { - it.copy(viewState = HistoryState.ViewState.Error("Failed to load transactions")) - } - }.launchIn(viewModelScope) - } - private fun applyFilter(filter: TransactionType) { val filteredTransactions = state.transactions.filter { if (filter == TransactionType.OTHER) { @@ -112,6 +72,36 @@ class HistoryViewModel( ) } } + + private fun handleTransactionLoaded(action: TransactionsLoaded) { + when (action.result) { + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(viewState = HistoryState.ViewState.Loading) + } + } + + is DataState.Error -> { + val message = action.result.exception.message.toString() + mutableStateFlow.update { + it.copy(viewState = HistoryState.ViewState.Error(message)) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + transactions = action.result.data, + viewState = if (action.result.data.isEmpty()) { + HistoryState.ViewState.Empty + } else { + HistoryState.ViewState.Content(action.result.data) + }, + ) + } + } + } + } } data class HistoryState( @@ -133,8 +123,10 @@ sealed interface HistoryEvent { } sealed interface HistoryAction { - data object LoadTransactions : HistoryAction - data class SetFilter(val filter: TransactionType) : HistoryAction data class ViewTransaction(val transferId: Long) : HistoryAction + + sealed interface Internal : HistoryAction { + data class TransactionsLoaded(val result: DataState>) : Internal + } } diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionDetail.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionDetail.kt index e6458342f..66ea74ee1 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionDetail.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionDetail.kt @@ -33,6 +33,7 @@ import org.mifospay.core.common.CurrencyFormatter import org.mifospay.core.common.DateHelper import org.mifospay.core.designsystem.theme.NewUi import org.mifospay.core.model.savingsaccount.TransferDetail +import org.mifospay.core.ui.AvatarBox @Composable internal fun TransactionDetail( diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionList.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionList.kt index 2eb695d32..df86f9cc0 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionList.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/components/TransactionList.kt @@ -9,6 +9,7 @@ */ package org.mifospay.feature.history.components +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -90,6 +91,7 @@ internal fun TransactionList( internal fun TransactionItem( transaction: Transaction, modifier: Modifier = Modifier, + showLeadingIcon: Boolean = true, onClick: (Long) -> Unit, ) { Surface( @@ -112,20 +114,22 @@ internal fun TransactionItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Image( - modifier = Modifier - .size(20.dp) - .padding(top = 2.dp), - painter = painterResource( - resource = when (transaction.transactionType) { - TransactionType.DEBIT -> Res.drawable.core_ui_money_out - TransactionType.CREDIT -> Res.drawable.core_ui_money_in - else -> Res.drawable.core_ui_money_in - }, - ), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), - ) + AnimatedVisibility(showLeadingIcon) { + Image( + modifier = Modifier + .size(20.dp) + .padding(top = 2.dp), + painter = painterResource( + resource = when (transaction.transactionType) { + TransactionType.DEBIT -> Res.drawable.core_ui_money_out + TransactionType.CREDIT -> Res.drawable.core_ui_money_in + else -> Res.drawable.core_ui_money_in + }, + ), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), + ) + } Column { Text( diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/detail/TransactionDetailViewModel.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/detail/TransactionDetailViewModel.kt index cdd5d2d34..060a5b446 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/detail/TransactionDetailViewModel.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/detail/TransactionDetailViewModel.kt @@ -14,7 +14,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.repository.AccountRepository import org.mifospay.core.model.savingsaccount.TransferDetail import org.mifospay.core.ui.utils.BaseViewModel @@ -53,19 +53,19 @@ internal class TransactionDetailViewModel( private fun handleTransferDetailReceive(action: TransferDetailReceive) { when (action.result) { - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy(viewState = Error(action.result.exception.message ?: "Error")) } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(viewState = TransactionDetailState.ViewState.Loading) } } - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(viewState = Content(action.result.data)) } @@ -92,5 +92,5 @@ internal sealed interface TransactionDetailEvent { internal sealed interface TransactionDetailAction { data object NavigateBack : TransactionDetailAction data object ShareTransaction : TransactionDetailAction - data class TransferDetailReceive(val result: Result) : TransactionDetailAction + data class TransferDetailReceive(val result: DataState) : TransactionDetailAction } diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/transactions/SpecificTransactionsViewModel.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/transactions/SpecificTransactionsViewModel.kt index 6369a8ecc..59a17afda 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/transactions/SpecificTransactionsViewModel.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/transactions/SpecificTransactionsViewModel.kt @@ -14,7 +14,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.repository.AccountRepository import org.mifospay.core.model.savingsaccount.Transaction import org.mifospay.core.model.savingsaccount.TransferDetail @@ -55,19 +55,19 @@ internal class SpecificTransactionsViewModel( private fun handleTransactionReceive(action: STAction.Internal.TransactionReceive) { when (action.result) { - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(viewState = STState.ViewState.Loading) } } - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy(viewState = Error(action.result.exception.message.toString())) } } - is Result.Success -> { + is DataState.Success -> { handleTransferDetailReceive(action.result.data) } } @@ -76,21 +76,21 @@ internal class SpecificTransactionsViewModel( private fun handleTransferDetailReceive(transaction: Transaction) { transaction.transferId?.let { transferId -> accountRepository.getAccountTransfer(transferId) - .onEach { result: Result -> + .onEach { result: DataState -> when (result) { - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy(viewState = Error(result.exception.message.toString())) } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(viewState = STState.ViewState.Loading) } } - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(viewState = Content(transaction, result.data)) } @@ -128,6 +128,6 @@ internal sealed interface STAction { data class ViewTransaction(val transferId: Long) : STAction sealed interface Internal : STAction { - data class TransactionReceive(val result: Result) : Internal + data class TransactionReceive(val result: DataState) : Internal } } diff --git a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt index 29cabd5bc..a61c78577 100644 --- a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt +++ b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt @@ -9,6 +9,7 @@ */ package org.mifospay.feature.home +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -31,17 +32,20 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -50,16 +54,19 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import mobile_wallet.feature.home.generated.resources.Res @@ -79,19 +86,16 @@ import org.mifospay.core.designsystem.component.LoadingDialogState import org.mifospay.core.designsystem.component.MfLoadingWheel import org.mifospay.core.designsystem.component.MifosBasicDialog import org.mifospay.core.designsystem.component.MifosLoadingDialog +import org.mifospay.core.designsystem.component.MifosScaffold import org.mifospay.core.designsystem.component.scrollbar.DraggableScrollbar import org.mifospay.core.designsystem.component.scrollbar.rememberDraggableScroller import org.mifospay.core.designsystem.component.scrollbar.scrollbarState +import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.NewUi import org.mifospay.core.model.account.Account -import org.mifospay.core.model.client.Client -import org.mifospay.core.model.client.ClientStatus -import org.mifospay.core.model.client.ClientTimeline -import org.mifospay.core.model.savingsaccount.Currency -import org.mifospay.core.model.savingsaccount.Transaction -import org.mifospay.core.model.savingsaccount.TransactionType import org.mifospay.core.ui.ErrorScreenContent -import org.mifospay.core.ui.TransactionItemScreen +import org.mifospay.core.ui.MifosSmallChip +import org.mifospay.core.ui.TransactionHistoryCard import org.mifospay.core.ui.utils.EventsEffect /* @@ -100,19 +104,22 @@ import org.mifospay.core.ui.utils.EventsEffect * Show transaction history of selected account */ @Composable -internal fun HomeRoute( +internal fun HomeScreen( onNavigateBack: () -> Unit, onRequest: (String) -> Unit, onPay: () -> Unit, navigateToTransactionDetail: (Long, Long) -> Unit, + navigateToAccountDetail: (Long) -> Unit, modifier: Modifier = Modifier, - homeViewModel: HomeViewModel = koinViewModel(), + viewModel: HomeViewModel = koinViewModel(), ) { val snackbarState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() - val homeUIState by homeViewModel.stateFlow.collectAsStateWithLifecycle() - EventsEffect(homeViewModel) { event -> + val homeUIState by viewModel.stateFlow.collectAsStateWithLifecycle() + val accountState by viewModel.accountState.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> when (event) { is HomeEvent.NavigateBack -> onNavigateBack.invoke() is HomeEvent.NavigateToRequestScreen -> onRequest(event.vpa) @@ -121,50 +128,76 @@ internal fun HomeRoute( is HomeEvent.NavigateToTransactionDetail -> { navigateToTransactionDetail(event.accountId, event.transactionId) } + is HomeEvent.NavigateToTransactionScreen -> {} is HomeEvent.ShowToast -> { scope.launch { snackbarState.showSnackbar(event.message) } } + + is HomeEvent.NavigateToAccountDetail -> { + navigateToAccountDetail(event.accountId) + } } } - Box(modifier = modifier.fillMaxSize()) { - HomeDialogs( - dialogState = homeUIState.dialogState, - onDismissRequest = remember(homeViewModel) { - { homeViewModel.trySendAction(HomeAction.OnDismissDialog) } - }, - ) + HomeScreenDialog( + dialogState = homeUIState.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(HomeAction.OnDismissDialog) } + }, + ) - when (homeUIState.viewState) { - is HomeState.ViewState.Loading -> { - MfLoadingWheel( - contentDesc = stringResource(Res.string.feature_home_loading), - backgroundColor = MaterialTheme.colorScheme.surface, - ) - } + HomeScreenContent( + viewState = accountState, + defaultAccountId = homeUIState.defaultAccountId, + snackbarHostState = snackbarState, + modifier = modifier, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} - is HomeState.ViewState.Content -> { - val successState = homeUIState.viewState as HomeState.ViewState.Content +@Composable +fun HomeScreenContent( + viewState: ViewState, + defaultAccountId: Long?, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + onAction: (HomeAction) -> Unit, +) { + MifosScaffold( + modifier = modifier, + snackbarHostState = snackbarHostState, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentAlignment = Alignment.Center, + ) { + when (viewState) { + is ViewState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(Res.string.feature_home_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } - HomeScreen( - client = homeUIState.client, - viewState = successState, - onAction = remember(homeViewModel) { - { homeViewModel.trySendAction(it) } - }, - modifier = Modifier, - ) - } + is ViewState.Content -> { + HomeScreenContent( + viewState = viewState, + defaultAccountId = defaultAccountId, + onAction = onAction, + modifier = Modifier, + ) + } - is HomeState.ViewState.Error -> { - ErrorScreenContent( - onClickRetry = remember(homeViewModel) { - { homeViewModel.trySendAction(HomeAction.Internal.LoadAccounts) } - }, - ) + is ViewState.Error -> { + ErrorScreenContent() + } } } } @@ -172,9 +205,9 @@ internal fun HomeRoute( @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable -private fun HomeScreen( - client: Client, - viewState: HomeState.ViewState.Content, +private fun HomeScreenContent( + viewState: ViewState.Content, + defaultAccountId: Long?, onAction: (HomeAction) -> Unit, modifier: Modifier = Modifier, ) { @@ -192,12 +225,18 @@ private fun HomeScreen( modifier = modifier .fillMaxSize(), state = state, - contentPadding = PaddingValues(8.dp), + contentPadding = PaddingValues(12.dp), ) { item { - MifosWalletCard( - account = viewState.account, - clientName = client.displayName, + AccountList( + accounts = viewState.accounts, + defaultAccountId = defaultAccountId, + onClick = { + onAction(HomeAction.AccountDetailsClicked(it)) + }, + onMarkAsDefault = { + onAction(HomeAction.MarkAsDefault(it)) + }, ) } @@ -249,110 +288,190 @@ private fun HomeScreen( } @Composable -private fun MifosWalletCard( - clientName: String, +private fun AccountList( + accounts: List, + defaultAccountId: Long?, + modifier: Modifier = Modifier, + onMarkAsDefault: (Long) -> Unit, + onClick: (Long) -> Unit, +) { + val pagerState = rememberPagerState { accounts.size } + + HorizontalPager( + state = pagerState, + modifier = modifier, + ) { + AccountCard( + account = accounts[it], + defaultAccountId = defaultAccountId, + onMarkAsDefault = onMarkAsDefault, + onClick = onClick, + ) + } +} + +@Composable +private fun AccountCard( account: Account, + defaultAccountId: Long?, + onMarkAsDefault: (Long) -> Unit, modifier: Modifier = Modifier, + onClick: (Long) -> Unit, ) { + val brush = remember { + Brush.linearGradient( + colors = listOf(NewUi.walletColor1, NewUi.walletColor2), + ) + } + Box( modifier = modifier .fillMaxWidth() .height(200.dp) .background( - brush = Brush.linearGradient( - colors = listOf( - NewUi.walletColor1, - NewUi.walletColor2, - ), - ), + brush = brush, shape = RoundedCornerShape(16.dp), - ), + ) + .clip(RoundedCornerShape(16.dp)) + .clickable { + onClick(account.id) + }, ) { - Card( + Column( modifier = Modifier - .fillMaxSize(), - colors = CardDefaults.cardColors( - containerColor = Color.Transparent, - ), + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.SpaceBetween, ) { - Column( - modifier = Modifier.fillMaxSize(), + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.padding(20.dp)) { - Text( - text = "Client Name", - fontWeight = FontWeight(300), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.surface, - ) + Column { + Text( + text = "Account Type", + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.surface, + ) - Text( - text = clientName, - fontWeight = FontWeight(400), - color = MaterialTheme.colorScheme.surface, - ) - } + Text( + text = account.name, + fontWeight = FontWeight(400), + color = MaterialTheme.colorScheme.surface, + ) + } - IconButton( - onClick = { }, - modifier = Modifier.padding(end = 12.dp), - ) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = "more", - tint = MaterialTheme.colorScheme.surface, + AnimatedContent( + targetState = account.id == defaultAccountId, + ) { + if (it) { + MifosSmallChip( + label = "Default", + containerColor = MaterialTheme.colorScheme.primary, + ) + } else { + CardDropdownBox( + onClickDefault = { + onMarkAsDefault(account.id) + }, ) } } + } - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, - ) { - Column { - Text( - text = "Wallet Balance", - fontWeight = FontWeight(300), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.surface, - ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Text( + text = account.number, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.headlineMedium, + letterSpacing = 0.50.sp, + ) + } - val accountBalance = CurrencyFormatter.format( - balance = account.balance, - currencyCode = account.currency.code, - maximumFractionDigits = null, - ) + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + Column { + Text( + text = "Wallet Balance", + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.surface, + ) - Text( - text = accountBalance, - color = MaterialTheme.colorScheme.surface, - style = MaterialTheme.typography.headlineLarge, - ) - } + val accountBalance = CurrencyFormatter.format( + balance = account.balance, + currencyCode = account.currency.code, + maximumFractionDigits = null, + ) - Icon( - modifier = Modifier - .graphicsLayer(rotationZ = 90f) - .padding(4.dp), - imageVector = Icons.Filled.KeyboardArrowUp, - contentDescription = "arrow", - tint = MaterialTheme.colorScheme.surface, + Text( + text = accountBalance, + color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.headlineLarge, ) } + + Icon( + modifier = Modifier + .graphicsLayer(rotationZ = 90f) + .padding(4.dp), + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = "arrow", + tint = MaterialTheme.colorScheme.surface, + ) } } } } +@Composable +fun CardDropdownBox( + onClickDefault: () -> Unit, + modifier: Modifier = Modifier, +) { + var showDropdown by remember { mutableStateOf(false) } + + Box(modifier = modifier) { + IconButton( + onClick = { + showDropdown = !showDropdown + }, + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.surface, + ), + ) { + Icon( + imageVector = MifosIcons.MoreVert, + contentDescription = "View More", + ) + } + + DropdownMenu( + expanded = showDropdown, + onDismissRequest = { showDropdown = false }, + ) { + DropdownMenuItem( + text = { Text("Mark as Default") }, + onClick = { + onClickDefault() + showDropdown = false + }, + ) + } + } +} + @Composable private fun PayRequestScreen( onRequest: () -> Unit, @@ -402,7 +521,9 @@ private fun PayRequestScreen( @Composable @Preview -fun MifosSendMoneyFreeCard(modifier: Modifier = Modifier) { +private fun MifosSendMoneyFreeCard( + modifier: Modifier = Modifier, +) { Card( modifier = modifier, colors = CardDefaults.cardColors( @@ -442,72 +563,6 @@ fun MifosSendMoneyFreeCard(modifier: Modifier = Modifier) { } } -@Composable -fun TransactionHistoryCard( - transactions: List, - modifier: Modifier = Modifier, - onClickViewAll: () -> Unit, - onViewTransaction: (Long, Long) -> Unit, -) { - Card( - modifier = modifier - .fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = Color.White, - ), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp, bottom = 10.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "Transaction History", - color = NewUi.primaryColor, - fontWeight = FontWeight(500), - ) - - Box( - modifier = Modifier.clickable( - onClick = onClickViewAll, - ), - ) { - Text( - text = "See All", - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight(300), - ) - } - } - - transactions.forEachIndexed { i, transaction -> - TransactionItemScreen( - transaction = transaction, - onClick = onViewTransaction, - ) - - if (i != transactions.size - 1) { - HorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - thickness = 1.dp, - color = NewUi.onSurface.copy(alpha = 0.05f), - ) - } - } - } - } -} - @Composable private fun PaymentButton( text: String, @@ -535,7 +590,7 @@ private fun PaymentButton( } @Composable -private fun HomeDialogs( +private fun HomeScreenDialog( dialogState: HomeState.DialogState?, onDismissRequest: () -> Unit, ) { @@ -554,87 +609,3 @@ private fun HomeDialogs( null -> Unit } } - -@Preview -@Composable -private fun HomeScreenPreview() { - HomeScreen( - viewState = HomeState.ViewState.Content( - account = Account( - image = "", - name = "Mifos", - number = "1234567890", - balance = 10000.0, - id = 1L, - currency = Currency( - code = "USD", - displayLabel = "$", - displaySymbol = "$", - ), - productId = 1223, - ), - transactions = List(25) { index -> - Transaction( - accountId = 5505, - amount = 2.3, - date = "eum", - currency = Currency( - code = "molestie", - displaySymbol = "viris", - displayLabel = "metus", - ), - transactionType = TransactionType.DEBIT, - transactionId = index.toLong(), - accountNo = "placerat", - transferId = 9075, - originalTransactionId = 1692, - paymentDetailId = 8149, - ) - }, - ), - client = Client( - id = 8858, - accountNo = "dignissim", - externalId = "sonet", - active = false, - activationDate = listOf(), - firstname = "Hollis Tyler", - lastname = "Lindsay Salazar", - displayName = "Janell Howell", - mobileNo = "principes", - emailAddress = "vicky.dominguez@example.com", - dateOfBirth = listOf(), - isStaff = false, - officeId = 2628, - officeName = "Enrique Dickson", - savingsProductName = "Lamont Brady", - timeline = ClientTimeline( - submittedOnDate = listOf(), - activatedOnDate = listOf(), - activatedByUsername = null, - activatedByFirstname = null, - activatedByLastname = null, - ), - status = ClientStatus( - id = 6242, - code = "possim", - value = "accommodare", - ), - legalForm = ClientStatus( - id = 2235, - code = "unum", - value = "laudem", - ), - ), - onAction = {}, - ) -} - -@Preview -@Composable -private fun PayRequestScreenPreview() { - PayRequestScreen( - onRequest = {}, - onSend = {}, - ) -} diff --git a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt index 4a7acd3d5..6bf7a70d1 100644 --- a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt +++ b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt @@ -10,50 +10,59 @@ package org.mifospay.feature.home import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize -import org.mifospay.core.common.Result 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.savingsaccount.Transaction import org.mifospay.core.ui.utils.BaseViewModel -import org.mifospay.feature.home.HomeAction.Internal.SelectAccount -/* - * Feature Enhancement - * Show all saving accounts as stacked card - * Show transaction history of selected account - */ +private const val TRANSACTION_LIMIT = 5 + +@OptIn(ExperimentalCoroutinesApi::class) class HomeViewModel( private val preferencesRepository: UserPreferencesRepository, - private val repository: SelfServiceRepository, + repository: SelfServiceRepository, ) : BaseViewModel( initialState = run { val client = requireNotNull(preferencesRepository.client.value) + val defaultAccountId = preferencesRepository.defaultAccount HomeState( client = client, - viewState = HomeState.ViewState.Loading, + defaultAccountId = defaultAccountId, ) }, ) { - init { - trySendAction(HomeAction.Internal.LoadAccounts) - - preferencesRepository.client.onEach { data -> - data?.let { client -> - mutableStateFlow.update { - it.copy(client = client) - } + val accountState = repository.getActiveAccountsWithTransactions( + clientId = state.client.id, + limit = TRANSACTION_LIMIT, + ).mapLatest { result -> + when (result) { + is DataState.Error -> ViewState.Error("No accounts found") + + is DataState.Loading -> ViewState.Loading + + is DataState.Success -> { + ViewState.Content( + accounts = result.data.accounts, + transactions = result.data.transactions, + ) } - }.launchIn(viewModelScope) - } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = ViewState.Loading, + ) override fun handleAction(action: HomeAction) { when (action) { @@ -93,98 +102,40 @@ class HomeViewModel( sendEvent(HomeEvent.NavigateBack) } - is HomeAction.Internal.LoadAccounts -> loadAccounts(state.client.id) - - is SelectAccount -> selectAccount(action.accountId) - } - } - - private fun loadAccounts(clientId: Long) { - viewModelScope.launch { - mutableStateFlow.update { it.copy(viewState = HomeState.ViewState.Loading) } - when (val result = repository.getSelfAccounts(clientId)) { - is Result.Error -> { - mutableStateFlow.update { - it.copy(viewState = HomeState.ViewState.Error("No accounts found")) - } - } - - is Result.Loading -> { - mutableStateFlow.update { it.copy(viewState = HomeState.ViewState.Loading) } - } - - is Result.Success -> { - mutableStateFlow.update { - it.copy( - viewState = HomeState.ViewState.Content( - account = result.data.first(), - selectedAccountId = result.data.firstOrNull()?.id, - ), - ) - } - sendAction(SelectAccount(result.data.firstOrNull()?.id ?: 0)) - } + is HomeAction.AccountDetailsClicked -> { + sendEvent(HomeEvent.NavigateToAccountDetail(action.accountId)) } - } - } - private fun selectAccount(accountId: Long) { - viewModelScope.launch { - mutableStateFlow.update { currentState -> - when (val viewState = currentState.viewState) { - is HomeState.ViewState.Content -> { - currentState.copy( - viewState = viewState.copy(selectedAccountId = accountId), - ) + is HomeAction.MarkAsDefault -> { + viewModelScope.launch { + val result = preferencesRepository.updateDefaultAccount(action.accountId) + + when (result) { + is DataState.Loading -> {} + is DataState.Error -> { + sendEvent(HomeEvent.ShowToast("Error marking account as default")) + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy(defaultAccountId = action.accountId) + } + sendEvent(HomeEvent.ShowToast("Account marked as default")) + } } - - else -> currentState } } } - loadTransactions(accountId) - } - - private fun loadTransactions(accountId: Long) { - repository.getSelfAccountTransactions(accountId).onEach { result -> - mutableStateFlow.update { currentState -> - val state = currentState.viewState as HomeState.ViewState.Content - currentState.copy( - viewState = HomeState.ViewState.Content( - account = state.account, - transactions = result, - selectedAccountId = accountId, - ), - dialogState = null, - ) - } - }.catch { - mutableStateFlow.update { - it.copy(dialogState = HomeState.DialogState.Error("Failed to load transactions")) - } - }.launchIn(viewModelScope) } } @Parcelize data class HomeState( val client: Client, - val viewState: ViewState, + val defaultAccountId: Long?, val dialogState: DialogState? = null, ) : Parcelable { - @Parcelize - sealed class ViewState : Parcelable { - data object Loading : ViewState() - data class Error(val message: String) : ViewState() - data class Content( - // val accounts: List, - val account: Account, - val transactions: List = emptyList(), - val selectedAccountId: Long? = null, - ) : ViewState() - } - @Parcelize sealed class DialogState : Parcelable { @@ -196,13 +147,26 @@ data class HomeState( } } +sealed interface ViewState { + data object Loading : ViewState + + data class Error(val message: String) : ViewState + + data class Content( + val accounts: List, + val transactions: List, + ) : ViewState +} + sealed interface HomeEvent { data object NavigateBack : HomeEvent - data class NavigateToRequestScreen(val vpa: String) : HomeEvent data object NavigateToSendScreen : HomeEvent data object NavigateToTransactionScreen : HomeEvent - data class NavigateToTransactionDetail(val accountId: Long, val transactionId: Long) : HomeEvent data object NavigateToClientDetailScreen : HomeEvent + data class NavigateToRequestScreen(val vpa: String) : HomeEvent + data class NavigateToTransactionDetail(val accountId: Long, val transactionId: Long) : HomeEvent + data class NavigateToAccountDetail(val accountId: Long) : HomeEvent + data class ShowToast(val message: String) : HomeEvent } @@ -214,10 +178,7 @@ sealed interface HomeAction { data object OnDismissDialog : HomeAction data object OnNavigateBack : HomeAction + data class MarkAsDefault(val accountId: Long) : HomeAction + data class AccountDetailsClicked(val accountId: Long) : HomeAction data class TransactionClicked(val accountId: Long, val transactionId: Long) : HomeAction - - sealed interface Internal : HomeAction { - data object LoadAccounts : HomeAction - data class SelectAccount(val accountId: Long) : HomeAction - } } diff --git a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt index d00f6afa5..5ea8e9776 100644 --- a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt +++ b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt @@ -13,7 +13,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import org.mifospay.feature.home.HomeRoute +import org.mifospay.feature.home.HomeScreen const val HOME_ROUTE = "home_route" @@ -24,13 +24,15 @@ fun NavGraphBuilder.homeScreen( onRequest: (String) -> Unit, onPay: () -> Unit, navigateToTransactionDetail: (Long, Long) -> Unit, + navigateToAccountDetail: (Long) -> Unit, ) { composable(route = HOME_ROUTE) { - HomeRoute( + HomeScreen( onRequest = onRequest, onPay = onPay, onNavigateBack = onNavigateBack, navigateToTransactionDetail = navigateToTransactionDetail, + navigateToAccountDetail = navigateToAccountDetail, ) } } diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt index 077f64682..225631688 100644 --- a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt +++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt @@ -57,9 +57,9 @@ import com.mifospay.core.model.domain.client.Client import com.mifospay.core.model.entity.accounts.savings.SavingAccount import org.koin.androidx.compose.koinViewModel import org.mifospay.core.designsystem.component.MfLoadingWheel -import org.mifospay.core.designsystem.component.MfOutlinedTextField import org.mifospay.core.designsystem.component.MifosBottomSheet import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOutlinedTextField import org.mifospay.core.designsystem.component.MifosScaffold import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.ElectricViolet @@ -227,7 +227,7 @@ private fun MerchantBottomSheet( ) Spacer(modifier = Modifier.height(24.dp)) - MfOutlinedTextField( + MifosOutlinedTextField( value = amount, label = stringResource(id = R.string.feature_merchants_amount), onValueChange = onAmountChange, diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileScreen.kt index 1d3f0ef19..3d3147f16 100644 --- a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileScreen.kt +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileScreen.kt @@ -19,13 +19,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults -import androidx.compose.material3.pulltorefresh.pullToRefresh -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -44,13 +40,13 @@ import org.mifospay.core.designsystem.component.MifosBasicDialog import org.mifospay.core.designsystem.component.MifosButton import org.mifospay.core.designsystem.component.MifosLoadingDialog import org.mifospay.core.designsystem.component.MifosOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.ui.ErrorScreenContent import org.mifospay.core.ui.utils.EventsEffect import org.mifospay.feature.profile.components.ProfileDetailsCard import org.mifospay.feature.profile.components.ProfileImage -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ProfileScreen( onEditProfile: () -> Unit, @@ -58,8 +54,8 @@ internal fun ProfileScreen( modifier: Modifier = Modifier, viewModel: ProfileViewModel = koinViewModel(), ) { - val pullToRefreshState = rememberPullToRefreshState() val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val clientState by viewModel.clientState.collectAsStateWithLifecycle() EventsEffect(viewModel) { event -> when (event) { @@ -71,70 +67,61 @@ internal fun ProfileScreen( } } - Box( - modifier = modifier - .fillMaxSize() - .pullToRefresh( - isRefreshing = state.viewState is ProfileState.ViewState.Loading, - state = pullToRefreshState, - onRefresh = remember(viewModel) { - { viewModel.trySendAction(ProfileAction.RefreshProfile) } - }, - ), - contentAlignment = Alignment.Center, - ) { - ProfileScreenContent( - state = state, - onAction = remember(viewModel) { - { action -> viewModel.trySendAction(action) } - }, - modifier = Modifier.align(Alignment.Center), - ) - - ProfileDialogs( - dialogState = state.dialogState, - onDismissRequest = remember(viewModel) { - { viewModel.trySendAction(ProfileAction.DismissErrorDialog) } - }, - ) - - PullToRefreshDefaults.Indicator( - modifier = Modifier - .align(Alignment.TopCenter), - isRefreshing = state.viewState is ProfileState.ViewState.Loading, - state = pullToRefreshState, - ) - } + ProfileDialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(ProfileAction.DismissErrorDialog) } + }, + ) + + ProfileScreenContent( + state = state, + clientState = clientState, + onAction = remember(viewModel) { + { action -> viewModel.trySendAction(action) } + }, + modifier = modifier.fillMaxSize(), + ) } @Composable internal fun ProfileScreenContent( state: ProfileState, + clientState: ProfileState.ViewState, onAction: (ProfileAction) -> Unit, modifier: Modifier = Modifier, ) { - when (state.viewState) { - is ProfileState.ViewState.Loading -> { - MifosOverlayLoadingWheel( - contentDesc = "ProfileLoading", - modifier = modifier, - ) - } - - is ProfileState.ViewState.Error -> { - ErrorScreenContent( - onClickRetry = { onAction(ProfileAction.RefreshProfile) }, - modifier = modifier, - ) - } - - is ProfileState.ViewState.Success -> { - ProfileScreenContent( - state = state.viewState, - clientImage = state.clientImage, - onAction = onAction, - modifier = modifier, - ) + MifosScaffold( + modifier = modifier, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + when (clientState) { + is ProfileState.ViewState.Loading -> { + MifosOverlayLoadingWheel( + contentDesc = "ProfileLoading", + modifier = Modifier.align(Alignment.Center), + ) + } + + is ProfileState.ViewState.Error -> { + ErrorScreenContent( + onClickRetry = { }, + modifier = Modifier.align(Alignment.Center), + ) + } + + is ProfileState.ViewState.Success -> { + ProfileScreenContent( + state = clientState, + clientImage = state.clientImage, + onAction = onAction, + modifier = Modifier, + ) + } + } } } } diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt index 8d1829a4b..b3690efbb 100644 --- a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt @@ -10,18 +10,19 @@ package org.mifospay.feature.profile import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.repository.ClientRepository import org.mifospay.core.datastore.UserPreferencesRepository import org.mifospay.core.model.client.Client import org.mifospay.core.ui.utils.BaseViewModel import org.mifospay.feature.profile.ProfileAction.Internal.HandleLoadClientImageResult -import org.mifospay.feature.profile.ProfileAction.Internal.HandleLoadClientResult -import org.mifospay.feature.profile.ProfileAction.Internal.LoadClient import org.mifospay.feature.profile.ProfileAction.Internal.LoadClientImage internal class ProfileViewModel( @@ -30,15 +31,23 @@ internal class ProfileViewModel( ) : BaseViewModel( initialState = run { val clientId = requireNotNull(preferencesRepository.clientId.value) - ProfileState( - clientId = clientId, - viewState = ProfileState.ViewState.Loading, - ) + ProfileState(clientId = clientId) }, ) { + val clientState = clientRepository.getClientInfo(state.clientId).map { + when (it) { + is DataState.Loading -> ProfileState.ViewState.Loading + is DataState.Error -> ProfileState.ViewState.Error(it.exception.message.toString()) + is DataState.Success -> ProfileState.ViewState.Success(it.data) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = ProfileState.ViewState.Loading, + ) + init { viewModelScope.launch { - sendAction(LoadClient(state.clientId)) sendAction(LoadClientImage(state.clientId)) } } @@ -66,34 +75,24 @@ internal class ProfileViewModel( is HandleLoadClientImageResult -> handleLoadClientImageResult(action) is LoadClientImage -> loadClientImage(action) - - is LoadClient -> loadClient(action) - - is HandleLoadClientResult -> handleClientResult(action) - - is ProfileAction.RefreshProfile -> { - viewModelScope.launch { - sendAction(LoadClient(state.clientId)) - } - } } } private fun handleLoadClientImageResult(action: HandleLoadClientImageResult) { when (action.result) { - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(clientImage = action.result.data) } } - is Result.Error -> { + is DataState.Error -> { // mutableStateFlow.update { // it.copy(dialogState = Error(action.result.exception.message ?: "")) // } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = ProfileState.DialogState.Loading) } @@ -106,48 +105,10 @@ internal class ProfileViewModel( sendAction(HandleLoadClientImageResult(it)) }.launchIn(viewModelScope) } - - private fun loadClient(action: LoadClient) { - clientRepository - .getClientInfo(action.clientId) - .onEach { sendAction(HandleLoadClientResult(it)) } - .launchIn(viewModelScope) - } - - private fun handleClientResult(action: HandleLoadClientResult) { - when (action.result) { - is Result.Error -> { - mutableStateFlow.update { - it.copy( - viewState = ProfileState.ViewState.Error( - action.result.exception.message ?: "", - ), - ) - } - } - - is Result.Loading -> { - mutableStateFlow.update { - it.copy(viewState = ProfileState.ViewState.Loading) - } - } - - is Result.Success -> { - mutableStateFlow.update { - it.copy(viewState = ProfileState.ViewState.Success(action.result.data)) - } - - viewModelScope.launch { - preferencesRepository.updateClientInfo(action.result.data) - } - } - } - } } internal data class ProfileState( val clientId: Long, - val viewState: ViewState = ViewState.Loading, val clientImage: String? = null, val dialogState: DialogState? = null, ) { @@ -176,13 +137,9 @@ internal sealed interface ProfileAction { data object ShowPersonalQRCode : ProfileAction data object DismissErrorDialog : ProfileAction - data object RefreshProfile : ProfileAction sealed interface Internal : ProfileAction { data class LoadClientImage(val clientId: Long) : Internal - data class HandleLoadClientImageResult(val result: Result) : Internal - - data class LoadClient(val clientId: Long) : Internal - data class HandleLoadClientResult(val result: Result) : Internal + data class HandleLoadClientImageResult(val result: DataState) : Internal } } diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt index 9a6971063..bfb04339a 100644 --- a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt @@ -109,8 +109,8 @@ private fun EditProfileScreenContent( modifier = modifier .padding(paddingValues) .fillMaxSize(), - contentPadding = PaddingValues(top = 30.dp, bottom = 10.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { item { @@ -178,7 +178,6 @@ private fun EditProfileScreenContent( MifosButton( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp) .height(54.dp), color = MifosBlue, text = { Text(text = stringResource(Res.string.feature_profile_save)) }, diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt index 62689eb58..a74e1e851 100644 --- a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt @@ -15,10 +15,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState import org.mifospay.core.common.IgnoredOnParcel import org.mifospay.core.common.Parcelable import org.mifospay.core.common.Parcelize -import org.mifospay.core.common.Result import org.mifospay.core.common.utils.isValidEmail import org.mifospay.core.data.repository.ClientRepository import org.mifospay.core.datastore.UserPreferencesRepository @@ -116,19 +116,19 @@ internal class EditProfileViewModel( private fun handleLoadClientImageResult(action: HandleLoadClientImageResult) { when (action.result) { - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(imageInput = action.result.data) } } - is Result.Error -> { + is DataState.Error -> { // mutableStateFlow.update { // it.copy(dialogState = Error(action.result.exception.message ?: "")) // } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = EditProfileState.DialogState.Loading) } @@ -198,19 +198,19 @@ internal class EditProfileViewModel( private fun handleUpdateProfileResult(action: OnUpdateProfileResult) { when (action.result) { - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy(dialogState = Error(action.result.exception.message ?: "")) } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = EditProfileState.DialogState.Loading) } } - is Result.Success -> { + is DataState.Success -> { viewModelScope.launch { if (state.imageInput != null) { val result = clientRepository.updateClientImage( @@ -223,7 +223,7 @@ internal class EditProfileViewModel( val result = preferencesRepository.updateClientProfile(state.updatedClient) when (result) { - is Result.Success -> { + is DataState.Success -> { sendEvent(EditProfileEvent.ShowToast("Profile updated successfully")) trySendAction(EditProfileAction.NavigateBack) } @@ -237,19 +237,19 @@ internal class EditProfileViewModel( private fun handleUpdateClientImageResult(action: HandleUpdateClientImageResult) { when (action.result) { - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy(dialogState = Error(action.result.exception.message ?: "")) } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = EditProfileState.DialogState.Loading) } } - is Result.Success -> { + is DataState.Success -> { sendEvent(EditProfileEvent.ShowToast("Profile image updated successfully")) } } @@ -306,10 +306,10 @@ sealed interface EditProfileAction { sealed interface Internal : EditProfileAction { data class LoadClientImage(val clientId: Long) : Internal - data class HandleLoadClientImageResult(val result: Result) : Internal + data class HandleLoadClientImageResult(val result: DataState) : Internal - data class OnUpdateProfileResult(val result: Result) : Internal + data class OnUpdateProfileResult(val result: DataState) : Internal - data class HandleUpdateClientImageResult(val result: Result) : Internal + data class HandleUpdateClientImageResult(val result: DataState) : Internal } } diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt index aa94c7f5a..9f312e480 100644 --- a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt @@ -12,7 +12,7 @@ package org.mifospay.feature.profile.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.compose.composable +import org.mifospay.core.ui.composableWithPushTransitions import org.mifospay.feature.profile.edit.EditProfileScreen const val EDIT_PROFILE_ROUTE = "edit_profile_route" @@ -22,7 +22,7 @@ fun NavController.navigateToEditProfile(navOptions: NavOptions? = null) = internal fun NavGraphBuilder.editProfileScreen( onBackPress: () -> Unit, ) { - composable(route = EDIT_PROFILE_ROUTE) { + composableWithPushTransitions(route = EDIT_PROFILE_ROUTE) { EditProfileScreen( onBackClick = onBackPress, ) diff --git a/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/SetAmountDialog.kt b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/SetAmountDialog.kt index eab98646c..96d4e9db7 100644 --- a/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/SetAmountDialog.kt +++ b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/SetAmountDialog.kt @@ -37,10 +37,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.mifospay.core.designsystem.component.MfOutlinedTextField import org.mifospay.core.designsystem.component.MifosButton import org.mifospay.core.designsystem.component.MifosCustomDialog import org.mifospay.core.designsystem.component.MifosOutlinedButton +import org.mifospay.core.designsystem.component.MifosOutlinedTextField import org.mifospay.core.designsystem.icon.MifosIcons @Suppress("MaxLineLength", "ReturnCount") @@ -117,7 +117,7 @@ internal fun SetAmountDialog( Spacer(modifier = Modifier.height(8.dp)) - MfOutlinedTextField( + MifosOutlinedTextField( value = amount, label = stringResource(id = R.string.feature_request_money_set_amount), onValueChange = { amount = it }, @@ -140,7 +140,7 @@ internal fun SetAmountDialog( ), ) - MfOutlinedTextField( + MifosOutlinedTextField( value = currency, label = stringResource(id = R.string.feature_request_money_currency), onValueChange = { currency = it }, diff --git a/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendScreenRoute.kt b/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendScreenRoute.kt index 4452bd258..37b7c21fc 100644 --- a/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendScreenRoute.kt +++ b/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendScreenRoute.kt @@ -57,10 +57,10 @@ import com.mifos.library.countrycodepicker.CountryCodePicker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfOutlinedTextField import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel import org.mifospay.core.designsystem.component.MifosButton import org.mifospay.core.designsystem.component.MifosNavigationTopAppBar +import org.mifospay.core.designsystem.component.MifosOutlinedTextField import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.styleMedium16sp import org.mifospay.core.designsystem.theme.styleNormal18sp @@ -215,7 +215,7 @@ internal fun SendMoneyScreen( onClick = { sendMethodType = SendMethodType.MOBILE }, ) } - MfOutlinedTextField( + MifosOutlinedTextField( value = amount, label = stringResource(id = R.string.feature_send_money_amount), onValueChange = { @@ -228,7 +228,7 @@ internal fun SendMoneyScreen( ) when (sendMethodType) { SendMethodType.VPA -> { - MfOutlinedTextField( + MifosOutlinedTextField( value = vpa, label = stringResource(id = R.string.feature_send_money_virtual_payment_address), onValueChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt index 1515b6441..f216923fc 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt @@ -18,7 +18,7 @@ import mobile_wallet.feature.settings.generated.resources.feature_settings_alert import mobile_wallet.feature.settings.generated.resources.feature_settings_empty import mobile_wallet.feature.settings.generated.resources.feature_settings_log_out_title import org.jetbrains.compose.resources.StringResource -import org.mifospay.core.common.Result +import org.mifospay.core.common.DataState import org.mifospay.core.data.repository.SavingsAccountRepository import org.mifospay.core.datastore.UserPreferencesRepository import org.mifospay.core.model.client.Client @@ -116,7 +116,7 @@ class SettingsViewModel( private fun handleDisableAccountResult(action: DisableAccountResult) { when (action.result) { - is Result.Error -> { + is DataState.Error -> { mutableStateFlow.update { it.copy( dialogState = DialogState.Error( @@ -126,13 +126,13 @@ class SettingsViewModel( } } - is Result.Loading -> { + is DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = DialogState.Loading) } } - is Result.Success -> { + is DataState.Success -> { mutableStateFlow.update { it.copy(dialogState = null) } @@ -184,6 +184,6 @@ sealed interface SettingsAction { sealed interface Internal : SettingsAction { data object DisableAccount : Internal - data class DisableAccountResult(val result: Result) : Internal + data class DisableAccountResult(val result: DataState) : Internal } } diff --git a/libs/pullrefresh/build.gradle.kts b/libs/pullrefresh/build.gradle.kts index dd1890e5e..699d68771 100644 --- a/libs/pullrefresh/build.gradle.kts +++ b/libs/pullrefresh/build.gradle.kts @@ -8,17 +8,20 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.android.library) - alias(libs.plugins.mifospay.android.library.compose) + alias(libs.plugins.mifospay.cmp.feature) } android { namespace = "com.mifos.library.pullrefresh" } -dependencies { - implementation(libs.androidx.compose.animation) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.runtime) - implementation(libs.androidx.compose.ui.util) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.animation) + } + } } diff --git a/libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefresh.kt b/libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefresh.kt similarity index 100% rename from libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefresh.kt rename to libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefresh.kt diff --git a/libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefreshIndicator.kt b/libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefreshIndicator.kt similarity index 100% rename from libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefreshIndicator.kt rename to libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefreshIndicator.kt diff --git a/libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefreshIndicatorTransform.kt b/libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefreshIndicatorTransform.kt similarity index 100% rename from libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefreshIndicatorTransform.kt rename to libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefreshIndicatorTransform.kt diff --git a/libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefreshState.kt b/libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefreshState.kt similarity index 100% rename from libs/pullrefresh/src/main/kotlin/com.mifos.library.pullrefresh/PullRefreshState.kt rename to libs/pullrefresh/src/commonMain/kotlin/com/mifos/library/pullrefresh/PullRefreshState.kt diff --git a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt index 5280d0236..ef8f6898f 100644 --- a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt +++ b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt @@ -1429,6 +1429,7 @@ | | | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) | | | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) | | | \--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2 (*) | | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) | +--- project :core:network (*) | +--- org.jetbrains.compose.material3:material3:1.7.0-rc01 @@ -1800,7 +1801,69 @@ | | | +--- io.coil-kt.coil3:coil-compose-core:3.0.0-alpha10 (*) | | | +--- org.jetbrains.compose.material3:material3:1.7.0-rc01 (*) | | | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01 (*) -| | | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01 (*) +| | | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01 (*) +| | | \--- org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 +| | | +--- androidx.activity:activity-compose:1.8.0 -> 1.9.2 (*) +| | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 -> 2.8.2 +| | | | +--- androidx.activity:activity-compose:1.8.0 -> 1.9.2 (*) +| | | | +--- androidx.compose.animation:animation:1.7.2 (*) +| | | | +--- androidx.compose.foundation:foundation-layout:1.7.2 (*) +| | | | +--- androidx.compose.runtime:runtime:1.7.2 (*) +| | | | +--- androidx.compose.runtime:runtime-saveable:1.7.2 (*) +| | | | +--- androidx.compose.ui:ui:1.7.2 (*) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -> 2.8.6 (*) +| | | | +--- androidx.navigation:navigation-runtime-ktx:2.8.2 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.2 (*) +| | | | +--- androidx.navigation:navigation-runtime-ktx:2.8.2 (c) +| | | | +--- androidx.navigation:navigation-fragment-ktx:2.8.2 (c) +| | | | +--- androidx.navigation:navigation-common-ktx:2.8.2 (c) +| | | | +--- androidx.navigation:navigation-runtime:2.8.2 (c) +| | | | +--- androidx.navigation:navigation-fragment:2.8.2 (c) +| | | | \--- androidx.navigation:navigation-common:2.8.2 (c) +| | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.2 -> 2.8.3-rc01 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.2 -> 2.8.3-rc01 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.2 -> 2.8.3-rc01 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) +| | | +--- org.jetbrains.androidx.navigation:navigation-common:2.8.0-alpha10 +| | | | +--- androidx.core:core-ktx:1.1.0 -> 1.13.1 (*) +| | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 -> 2.8.2 (*) +| | | | +--- androidx.profileinstaller:profileinstaller:1.3.0 -> 1.4.0 (*) +| | | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.2 -> 2.8.3-rc01 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.2 -> 2.8.3-rc01 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) +| | | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) +| | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) +| | | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2 -> 1.7.2 (*) +| | | +--- org.jetbrains.androidx.navigation:navigation-runtime:2.8.0-alpha10 +| | | | +--- androidx.activity:activity-ktx:1.7.1 -> 1.9.2 (*) +| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 -> 2.8.2 (*) +| | | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.2 -> 2.8.3-rc01 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.2 -> 2.8.3-rc01 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) +| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) +| | | | +--- org.jetbrains.androidx.navigation:navigation-common:2.8.0-alpha10 (*) +| | | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) +| | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) +| | | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2 -> 1.7.2 (*) +| | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) +| | | +--- org.jetbrains.compose.animation:animation:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.compose.animation:animation-core:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.compose.foundation:foundation-layout:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.compose.runtime:runtime-saveable:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.compose.ui:ui:1.7.0-beta02 -> 1.7.0-rc01 (*) +| | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2 -> 1.7.2 (*) | | +--- project :core:designsystem (*) | | +--- project :core:data (*) | | +--- io.insert-koin:koin-compose:1.2.0-Beta4 -> 4.0.0-RC2 (*) @@ -1811,68 +1874,7 @@ | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) -| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 -| | | +--- androidx.activity:activity-compose:1.8.0 -> 1.9.2 (*) -| | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 -> 2.8.2 -| | | | +--- androidx.activity:activity-compose:1.8.0 -> 1.9.2 (*) -| | | | +--- androidx.compose.animation:animation:1.7.2 (*) -| | | | +--- androidx.compose.foundation:foundation-layout:1.7.2 (*) -| | | | +--- androidx.compose.runtime:runtime:1.7.2 (*) -| | | | +--- androidx.compose.runtime:runtime-saveable:1.7.2 (*) -| | | | +--- androidx.compose.ui:ui:1.7.2 (*) -| | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -> 2.8.6 (*) -| | | | +--- androidx.navigation:navigation-runtime-ktx:2.8.2 (*) -| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) -| | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.2 (*) -| | | | +--- androidx.navigation:navigation-runtime-ktx:2.8.2 (c) -| | | | +--- androidx.navigation:navigation-fragment-ktx:2.8.2 (c) -| | | | +--- androidx.navigation:navigation-common-ktx:2.8.2 (c) -| | | | +--- androidx.navigation:navigation-runtime:2.8.2 (c) -| | | | +--- androidx.navigation:navigation-fragment:2.8.2 (c) -| | | | \--- androidx.navigation:navigation-common:2.8.2 (c) -| | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) -| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.2 -> 2.8.3-rc01 (*) -| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.2 -> 2.8.3-rc01 (*) -| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.2 -> 2.8.3-rc01 (*) -| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) -| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2 (*) -| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) -| | | +--- org.jetbrains.androidx.navigation:navigation-common:2.8.0-alpha10 -| | | | +--- androidx.core:core-ktx:1.1.0 -> 1.13.1 (*) -| | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 -> 2.8.2 (*) -| | | | +--- androidx.profileinstaller:profileinstaller:1.3.0 -> 1.4.0 (*) -| | | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.2 -> 2.8.3-rc01 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.2 -> 2.8.3-rc01 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) -| | | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) -| | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) -| | | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2 -> 1.7.2 (*) -| | | +--- org.jetbrains.androidx.navigation:navigation-runtime:2.8.0-alpha10 -| | | | +--- androidx.activity:activity-ktx:1.7.1 -> 1.9.2 (*) -| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 -> 2.8.2 (*) -| | | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.2 -> 2.8.3-rc01 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.2 -> 2.8.3-rc01 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) -| | | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) -| | | | +--- org.jetbrains.androidx.navigation:navigation-common:2.8.0-alpha10 (*) -| | | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) -| | | | +--- org.jetbrains.compose.annotation-internal:annotation:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | | +--- org.jetbrains.compose.collection-internal:collection:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) -| | | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2 -> 1.7.2 (*) -| | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) -| | | +--- org.jetbrains.compose.animation:animation:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | +--- org.jetbrains.compose.animation:animation-core:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | +--- org.jetbrains.compose.foundation:foundation-layout:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | +--- org.jetbrains.compose.runtime:runtime:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | +--- org.jetbrains.compose.runtime:runtime-saveable:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | +--- org.jetbrains.compose.ui:ui:1.7.0-beta02 -> 1.7.0-rc01 (*) -| | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2 -> 1.7.2 (*) +| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 (*) | | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 (*) | | +--- project :core:domain | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) @@ -2192,6 +2194,38 @@ | | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01 (*) | | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01 (*) | | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) +| +--- project :feature:accounts +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.6 (*) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- project :core:ui (*) +| | +--- project :core:designsystem (*) +| | +--- project :core:data (*) +| | +--- io.insert-koin:koin-compose:1.2.0-Beta4 -> 4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-compose-viewmodel:1.2.0-Beta4 -> 4.0.0-RC2 (*) +| | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 (*) +| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2 (*) +| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) +| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) +| | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) +| | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) +| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 (*) +| | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.foundation:foundation:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.material3:material3:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2 (*) +| | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) | +--- project :core:ui (*) diff --git a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt index 1fe044e71..5b54783fe 100644 --- a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt +++ b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt @@ -8,6 +8,7 @@ :core:model :core:network :core:ui +:feature:accounts :feature:auth :feature:editpassword :feature:faq diff --git a/mifospay-shared/build.gradle.kts b/mifospay-shared/build.gradle.kts index 270638cb0..a0a7c6a23 100644 --- a/mifospay-shared/build.gradle.kts +++ b/mifospay-shared/build.gradle.kts @@ -44,6 +44,7 @@ kotlin { api(projects.feature.history) api(projects.feature.payments) api(projects.feature.finance) + api(projects.feature.accounts) } desktopMain.dependencies { diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt index de11c05b3..9c44d8eb5 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt @@ -21,6 +21,7 @@ import org.mifospay.core.datastore.di.PreferencesModule import org.mifospay.core.domain.di.DomainModule import org.mifospay.core.network.di.LocalModule import org.mifospay.core.network.di.NetworkModule +import org.mifospay.feature.accounts.di.AccountsModule import org.mifospay.feature.auth.di.AuthModule import org.mifospay.feature.editpassword.di.EditPasswordModule import org.mifospay.feature.faq.di.FaqModule @@ -60,6 +61,7 @@ object KoinModules { ProfileModule, HistoryModule, PaymentsModule, + AccountsModule, ) } private val LibraryModule = module { diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt index 7a4e9d8cb..e844263e4 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt @@ -17,6 +17,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import org.mifospay.core.ui.utility.TabContent +import org.mifospay.feature.accounts.AccountsScreen +import org.mifospay.feature.accounts.beneficiary.addEditBeneficiaryScreen +import org.mifospay.feature.accounts.beneficiary.navigateToBeneficiaryAddEdit +import org.mifospay.feature.accounts.savingsaccount.addEditSavingAccountScreen +import org.mifospay.feature.accounts.savingsaccount.details.navigateToSavingAccountDetails +import org.mifospay.feature.accounts.savingsaccount.details.savingAccountDetailRoute +import org.mifospay.feature.accounts.savingsaccount.navigateToSavingAccountAddEdit import org.mifospay.feature.editpassword.navigation.editPasswordScreen import org.mifospay.feature.editpassword.navigation.navigateToEditPassword import org.mifospay.feature.faq.navigation.faqScreen @@ -65,9 +72,11 @@ internal fun MifosNavHost( val tabContents = listOf( TabContent(FinanceScreenContents.ACCOUNTS.name) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Accounts Screen || TODO", modifier = Modifier.align(Alignment.Center)) - } + AccountsScreen( + onAddEditSavingsAccount = navController::navigateToSavingAccountAddEdit, + onViewSavingAccountDetails = navController::navigateToSavingAccountDetails, + onAddOrEditBeneficiary = navController::navigateToBeneficiaryAddEdit, + ) }, TabContent(FinanceScreenContents.CARDS.name) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -85,7 +94,7 @@ internal fun MifosNavHost( } }, ) - + NavHost( route = MifosNavGraph.MAIN_GRAPH, startDestination = HOME_ROUTE, @@ -97,6 +106,7 @@ internal fun MifosNavHost( onRequest = {}, onPay = {}, navigateToTransactionDetail = navController::navigateToSpecificTransaction, + navigateToAccountDetail = navController::navigateToSavingAccountDetails, ) settingsScreen( @@ -138,5 +148,18 @@ internal fun MifosNavHost( transactionDetailNavigation( navigateBack = navController::navigateUp, ) + + addEditBeneficiaryScreen( + navigateBack = navController::navigateUp, + ) + + savingAccountDetailRoute( + navigateBack = navController::navigateUp, + onViewTransaction = navController::navigateToSpecificTransaction, + ) + + addEditSavingAccountScreen( + navigateBack = navController::navigateUp, + ) } }