diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeContentFrame.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeContentFrame.kt index e65767e050..70d4f136b9 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeContentFrame.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeContentFrame.kt @@ -49,6 +49,7 @@ import com.naviapp.screenOverlay.nudge.initializer.InitScreenOverlayComponents import com.naviapp.screenOverlay.popup.ui.PopupRenderer import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM import com.naviapp.utils.navigateTo +import com.naviapp.utils.navigateToGlobalSendMoney @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -198,6 +199,21 @@ fun HomeContentFrame( onNavigation = { sendMoneyScreenSource, url, _, _ -> navigateTo(activity, sendMoneyScreenSource, url) }, + onGlobalQrNavigation = { + qrContent, + payeeEntity, + upiGlobalSendMoneyScreenSource, + upiGlobalMainScreenSource, + url -> + navigateToGlobalSendMoney( + activity, + qrContent, + payeeEntity, + upiGlobalSendMoneyScreenSource, + upiGlobalMainScreenSource, + url, + ) + }, qrScreenVisible = qrScreenVisible, ) } diff --git a/android/app/src/main/java/com/naviapp/utils/Utility.kt b/android/app/src/main/java/com/naviapp/utils/Utility.kt index eaa47dfa12..5e72b15b14 100644 --- a/android/app/src/main/java/com/naviapp/utils/Utility.kt +++ b/android/app/src/main/java/com/naviapp/utils/Utility.kt @@ -47,6 +47,7 @@ import com.navi.base.utils.orZero import com.navi.common.model.ModuleNameV2 import com.navi.common.ui.activity.NaviCoreActivity import com.navi.common.upi.PAYEE_ENTITY +import com.navi.common.upi.QR_CONTENT import com.navi.common.upi.SEND_MONEY_SCREEN_SOURCE import com.navi.common.upi.TRANSACTION_TYPE import com.navi.common.utils.CommonNaviAnalytics @@ -55,14 +56,18 @@ import com.navi.common.utils.TemporaryStorageHelper import com.navi.common.utils.log import com.navi.insurance.navigator.NaviInsuranceDeeplinkNavigator import com.navi.insurance.sharedpref.NaviPreferenceManager +import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity import com.navi.pay.management.common.sendmoney.model.view.SendMoneyScreenSource import com.navi.pay.management.common.sendmoney.model.view.UpiTransactionType +import com.navi.pay.management.global.models.view.UpiGlobalMainScreenSource +import com.navi.pay.management.global.models.view.UpiGlobalSendMoneyScreenSource import com.navi.payment.juspay.HyperServicesHolder import com.naviapp.R import com.naviapp.analytics.utils.NaviAnalytics import com.naviapp.analytics.utils.NaviSDKHelper import com.naviapp.app.NaviApplication import com.naviapp.common.navigator.NaviDeepLinkNavigator +import com.naviapp.home.compose.activity.HomePageActivity import com.naviapp.home.dashboard.models.response.DashboardContentResponse import com.naviapp.manager.usecase.UserDataUploadWorkerUseCase import com.naviapp.models.ColorData @@ -497,3 +502,24 @@ fun navigateTo( }, ) } + +fun navigateToGlobalSendMoney( + activity: HomePageActivity, + qrContent: String?, + payeeEntity: PayeeEntity?, + upiGlobalSendMoneyScreenSource: UpiGlobalSendMoneyScreenSource?, + upiGlobalMainScreenSource: UpiGlobalMainScreenSource.QrScannerScreen?, + url: String?, +) { + NaviDeepLinkNavigator.navigate( + activity = activity, + ctaData = CtaData(url = url), + bundle = + Bundle().apply { + putString(QR_CONTENT, qrContent) + putParcelable(PAYEE_ENTITY, payeeEntity) + putString("upiGlobalSendMoneyScreenSource", upiGlobalSendMoneyScreenSource?.name) + putParcelable("upiGlobalMainScreenSource", upiGlobalMainScreenSource) + }, + ) +} diff --git a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt index a63024cca7..bc1dc7e745 100644 --- a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt +++ b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt @@ -139,6 +139,7 @@ object FirebaseRemoteConfigHelper { const val NAVI_PAY_LOCATION_PERMISSION_ENABLED = "NAVI_PAY_LOCATION_PERMISSION_ENABLED" const val NAVI_PAY_DATA_REFRESH_MIN_TIMESTAMP = "NAVI_PAY_DATA_REFRESH_MIN_TIMESTAMP" const val NAVI_PAY_ENABLE_DARK_KNIGHT = "NAVI_PAY_ENABLE_DARK_KNIGHT" + const val NAVI_PAY_UPI_GLOBAL_ENABLED = "NAVI_PAY_UPI_GLOBAL_ENABLED" // COMMON const val LITMUS_EXPERIMENTS_CACHE_DURATION_IN_MILLIS = diff --git a/android/navi-common/src/main/res/drawable-hdpi-v4/vkyc_info_background.webp b/android/navi-common/src/main/res/drawable-hdpi-v4/ic_info_solid_blue.webp similarity index 100% rename from android/navi-common/src/main/res/drawable-hdpi-v4/vkyc_info_background.webp rename to android/navi-common/src/main/res/drawable-hdpi-v4/ic_info_solid_blue.webp diff --git a/android/navi-common/src/main/res/drawable-ldpi-v4/vkyc_info_background.webp b/android/navi-common/src/main/res/drawable-ldpi-v4/ic_info_solid_blue.webp similarity index 100% rename from android/navi-common/src/main/res/drawable-ldpi-v4/vkyc_info_background.webp rename to android/navi-common/src/main/res/drawable-ldpi-v4/ic_info_solid_blue.webp diff --git a/android/navi-common/src/main/res/drawable-mdpi-v4/vkyc_info_background.webp b/android/navi-common/src/main/res/drawable-mdpi-v4/ic_info_solid_blue.webp similarity index 100% rename from android/navi-common/src/main/res/drawable-mdpi-v4/vkyc_info_background.webp rename to android/navi-common/src/main/res/drawable-mdpi-v4/ic_info_solid_blue.webp diff --git a/android/navi-common/src/main/res/drawable-xhdpi-v4/vkyc_info_background.webp b/android/navi-common/src/main/res/drawable-xhdpi-v4/ic_info_solid_blue.webp similarity index 100% rename from android/navi-common/src/main/res/drawable-xhdpi-v4/vkyc_info_background.webp rename to android/navi-common/src/main/res/drawable-xhdpi-v4/ic_info_solid_blue.webp diff --git a/android/navi-common/src/main/res/drawable-xxhdpi-v4/vkyc_info_background.webp b/android/navi-common/src/main/res/drawable-xxhdpi-v4/ic_info_solid_blue.webp similarity index 100% rename from android/navi-common/src/main/res/drawable-xxhdpi-v4/vkyc_info_background.webp rename to android/navi-common/src/main/res/drawable-xxhdpi-v4/ic_info_solid_blue.webp diff --git a/android/navi-common/src/main/res/drawable-xxxhdpi-v4/vkyc_info_background.webp b/android/navi-common/src/main/res/drawable-xxxhdpi-v4/ic_info_solid_blue.webp similarity index 100% rename from android/navi-common/src/main/res/drawable-xxxhdpi-v4/vkyc_info_background.webp rename to android/navi-common/src/main/res/drawable-xxxhdpi-v4/ic_info_solid_blue.webp diff --git a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/utils/NaviPayExtTest.kt b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/utils/NaviPayExtTest.kt index 67b9da5edf..52f4eececf 100644 --- a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/utils/NaviPayExtTest.kt +++ b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/utils/NaviPayExtTest.kt @@ -8,6 +8,7 @@ package com.navi.pay.utils import com.navi.common.utils.CommonUtils.isAmountValid +import com.navi.pay.common.utils.getBankCharges import kotlin.system.measureTimeMillis import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -78,6 +79,18 @@ class AmountUtilityUnitTest { assertEquals("11223456.10", "11223456.10".getFormattedAmountWithDecimal()) assertEquals("0.10", "0.1".getFormattedAmountWithDecimal()) } + + @Test + fun isCorrectGlobalFormattedCurrency() { + assertEquals("1", "1".globalFormattedCurrency()) + assertEquals("50", "50".globalFormattedCurrency()) + assertEquals("-50", "-50".globalFormattedCurrency()) + assertEquals("-5,000", "-5000".globalFormattedCurrency()) + assertEquals("-5,000,000", "-5000000".globalFormattedCurrency()) + assertEquals("1,000", "1000".globalFormattedCurrency()) + assertEquals("55,973", "55973".globalFormattedCurrency()) + assertEquals("12,345,678", "12345678".globalFormattedCurrency()) + } } /** Unit test to check if UPI allowed amount is valid or not */ @@ -394,3 +407,29 @@ class ParallelFilterUnitTest { assertEquals(emptyList(), result) } } + +class GlobalBankChargesUnitTest { + + @Test + fun checkBankForexCharges_isValidatedCorrectly() { + assertEquals(12.0, getBankCharges(forex = "1.2", markUp = "10", baseAmount = "100"), 0.0) + assertEquals(0.0, getBankCharges(forex = "0", markUp = "10", baseAmount = "100"), 0.0) + assertEquals(50.0, getBankCharges(forex = "2.5", markUp = "20", baseAmount = "100"), 0.0) + assertEquals(250.0, getBankCharges(forex = "5", markUp = "50", baseAmount = "100"), 0.0) + assertEquals( + 0.0, + getBankCharges(forex = null, markUp = "10", baseAmount = "100"), + 0.0, + ) // Handling null inputs + assertEquals( + 0.0, + getBankCharges(forex = "abc", markUp = "10", baseAmount = "100"), + 0.0, + ) // Handling invalid inputs + assertEquals( + 0.01, + getBankCharges(forex = "0.01", markUp = "1", baseAmount = "100"), + 0.0, + ) // Edge cases + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt index 5c4c2c7540..eeea02f247 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt @@ -35,6 +35,8 @@ import com.navi.pay.management.common.sendmoney.model.view.SendMoneyRegexValidat import com.navi.pay.management.common.sendmoney.model.view.SendMoneyScreenSource import com.navi.pay.management.common.sendmoney.model.view.UpiTransactionType import com.navi.pay.management.common.utils.PspEvaluationResult +import com.navi.pay.management.global.models.view.UpiGlobalMainScreenSource +import com.navi.pay.management.global.models.view.UpiGlobalSendMoneyScreenSource import com.navi.pay.management.mandate.model.view.MandateCategory import com.navi.pay.management.mandate.model.view.MandateEntity import com.navi.pay.management.moneytransfer.scanpay.model.view.QrError @@ -1603,6 +1605,22 @@ class NaviPayAnalytics private constructor() { ), ) } + + fun onUpiGlobalButtonClicked( + bankName: String, + bankAccountUniqueId: String, + currentStatus: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + eventName = "NaviPay_AccountDetails_UPIGlobal_StatusToggle_Clicked", + eventValues = + mapOf( + "bankName" to bankName, + "bankAccountUniqueId" to bankAccountUniqueId, + "currentStatus" to currentStatus, + ), + ) + } } inner class NaviPayAccountVerify { @@ -3249,6 +3267,50 @@ class NaviPayAnalytics private constructor() { ), ) } + + fun onNfcStatusReceived(isNfcEnabled: Boolean, isNfcAvailable: Boolean) { + NaviTrackEvent.trackEventOnClickStream( + eventName = "NaviPay_Dev_NFC_Status", + eventValues = + mapOf( + "isNfcEnabled" to isNfcEnabled.toString(), + "isNfcAvailable" to isNfcAvailable.toString(), + ), + ) + } + + fun onConversionDetailsClicked( + source: String, + transactionType: String, + upiRequestId: String, + accountAdded: Boolean, + currency: String, + status: String, + amountInRupees: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + eventName = "NaviPay_TransactionDetails_ConversionDetails_Clicked", + eventValues = + mapOf( + "upiRequestId" to upiRequestId, + "status" to status, + "source" to source, + "accountAdded" to accountAdded.toString(), + "transactionType" to transactionType, + "international" to "y", + "currency" to currency, + "in_INR" to amountInRupees, + ), + ) + } + + fun onConversionDetailsBottomSheetLanded(convertedAmount: String, bankMarkUpRate: String) { + NaviTrackEvent.trackEventOnClickStream( + eventName = "NaviPay_TransactionDetails_ViewConversionDetails_Conversion_Landed", + eventValues = + mapOf("convertedAmount" to convertedAmount, "bank_forex" to bankMarkUpRate), + ) + } } inner class NaviPayBlockedUsers { @@ -6173,6 +6235,39 @@ class NaviPayAnalytics private constructor() { ), ) } + + fun onConversionDetailsClicked( + source: String, + transactionType: String, + upiRequestId: String, + accountAdded: Boolean, + currency: String, + status: String, + amountInRupees: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + eventName = "NaviPay_OrderDetails_ConversionDetails_Clicked", + eventValues = + mapOf( + "upiRequestId" to upiRequestId, + "status" to status, + "source" to source, + "accountAdded" to accountAdded.toString(), + "transactionType" to transactionType, + "international" to "y", + "currency" to currency, + "in_INR" to amountInRupees, + ), + ) + } + + fun onConversionDetailsBottomSheetLanded(convertedAmount: String, bankMarkUpRate: String) { + NaviTrackEvent.trackEventOnClickStream( + eventName = "NaviPay_OrderDetails_ViewConversionDetails_Conversion_Landed", + eventValues = + mapOf("convertedAmount" to convertedAmount, "bank_forex" to bankMarkUpRate), + ) + } } inner class NaviPayLinkUpiNumber { @@ -6526,6 +6621,201 @@ class NaviPayAnalytics private constructor() { } } + inner class NaviPayUpiGlobal { + fun onUpiGlobalLanded(source: UpiGlobalMainScreenSource) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_UPIGlobal_Page_Landed", + mapOf("source" to (source::class.simpleName ?: "")), + ) + } + + fun onToggleClicked(bankName: String, bankAccountUniqueId: String) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_UPIGlobal_Toggle_Clicked", + mapOf("bankName" to bankName, "bankAccountUniqueId" to bankAccountUniqueId), + ) + } + + fun onAddBankClicked() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_UPIGlobal_AddBankAccount_Clicked") + } + + fun onHelpClicked() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_UPIGlobal_HelpButton_Clicked") + } + + fun onAccountActivationSuccess(bankName: String, bankAccountUniqueId: String) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_UPIGlobal_Activation_Success", + mapOf("bankName" to bankName, "bankAccountUniqueId" to bankAccountUniqueId), + ) + } + + fun onActivateUpiGlobalClicked( + bankName: String, + bankAccountUniqueId: String, + endDate: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_UPIGlobal_ActivationPeriod_Landed", + mapOf( + "bankName" to bankName, + "bankAccountUniqueId" to bankAccountUniqueId, + "endDate" to endDate.toString(), + ), + ) + } + + fun onActivationPending() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_UPIGlobal_Activation_Pending") + } + + fun onActivationFailed( + bankName: String, + bankAccountUniqueId: String, + errorCode: String, + errorMessage: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_UPIGlobal_Activation_Failed", + mapOf( + "bankName" to bankName, + "bankAccountUniqueId" to bankAccountUniqueId, + "errorCode" to errorCode, + "errorMessage" to errorMessage, + ), + ) + } + } + + inner class NaviPayGlobalSendMoney { + fun onGlobalSendMoneyLanded(naviPaySessionAttributes: Map) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SendMoney_Landed_International", + mapOf("naviPaySessionId" to naviPaySessionAttributes["naviPaySessionId"].orEmpty()), + ) + } + + fun onSendMoneyPayClicked( + upiRequestId: String, + purposeCode: String, + source: UpiGlobalSendMoneyScreenSource, + naviPaySessionAttributes: Map, + amount: String, + currency: String, + tncApproval: Boolean, + isDynamicQr: Boolean, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SendMoney_PayClicked_International", + mapOf( + "upiRequestId" to upiRequestId, + "purposeCode" to purposeCode, + "source" to (source::class.simpleName ?: ""), + "amount" to amount, + "currency" to currency, + "tnc_approval" to tncApproval.toString(), + "naviPaySessionId" to naviPaySessionAttributes["naviPaySessionId"].orEmpty(), + "qr_type" to if (isDynamicQr) "dynamic" else "static", + ), + ) + } + + fun onViewConversionDetailClicked( + upiRequestId: String, + source: UpiGlobalSendMoneyScreenSource, + currency: String, + amountInINR: String, + currencyAmount: String, + bankForex: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SendMoney_ViewConversionDetails_Clicked_International", + mapOf( + "upiRequestId" to upiRequestId, + "source" to (source::class.simpleName ?: ""), + "currencyAmount" to currencyAmount, + "currency" to currency, + "converted_amount" to amountInINR, + "bank_forex" to bankForex, + ), + ) + } + + fun onHelpClicked( + bankName: String, + bankAccountId: String, + naviPaySessionAttributes: Map, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SendMoney_Help_International", + mapOf( + "bankName" to bankName, + "bankAccountId" to bankAccountId, + "naviPaySessionId" to naviPaySessionAttributes["naviPaySessionId"].orEmpty(), + ), + ) + } + + fun onViewHistoryClicked( + upiRequestId: String, + source: UpiGlobalSendMoneyScreenSource, + naviPaySessionAttributes: Map, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SendMoney_ViewHistory_Clicked_International", + mapOf( + "upiRequestId" to upiRequestId, + "source" to (source::class.simpleName ?: ""), + "naviPaySessionId" to naviPaySessionAttributes["naviPaySessionId"].orEmpty(), + ), + ) + } + + fun onBankDropDownClicked( + naviPaySessionAttributes: Map, + bankUptimeStatus: String, + bankAccountUniqueId: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SendMoney_BankDropDown_International", + mapOf( + "bankUptimeStatus" to bankUptimeStatus, + "bankAccountUniqueId" to bankAccountUniqueId, + "naviPaySessionId" to naviPaySessionAttributes["naviPaySessionId"].orEmpty(), + ), + ) + } + + fun onBankSelected( + naviPaySessionAttributes: Map, + bankUptimeStatus: String, + bankAccountUniqueId: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SendMoney_BankSelectClicked_International", + mapOf( + "bankUptimeStatus" to bankUptimeStatus, + "bankAccountUniqueId" to bankAccountUniqueId, + "naviPaySessionId" to naviPaySessionAttributes["naviPaySessionId"].orEmpty(), + ), + ) + } + + fun onAddAccountClicked(naviPaySessionAttributes: Map) { + NaviTrackEvent.trackEventOnClickStream( + eventName = "NaviPay_SendMoney_AddAccountClicked", + eventValues = + mapOf( + "naviPaySessionId" to + naviPaySessionAttributes["naviPaySessionId"].orEmpty(), + "naviPayCustomerStatus" to + naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(), + ), + ) + } + } + companion object { val INSTANCE = NaviPayAnalytics() const val NAVI_PAY_ACTIVITY = "navipay_activity" @@ -6570,5 +6860,7 @@ class NaviPayAnalytics private constructor() { const val NAVI_PAY_ARC_HOME_SCREEN = "NaviPay_ArcHomeScreen" const val NAVI_PAY_MANAGE_UPI_IDS = "NaviPay_ManageUpiIds" const val NAVI_PAY_ACTIVATE_VPA = "NaviPay_ActivateVpa" + const val NAVI_PAY_UPI_GLOBAL = "NaviPay_UPIGlobal" + const val NAVI_PAY_UPI_GLOBAL_SEND_MONEY_SCREEN = "NaviPay_UpiGlobal_SendMoneyScreen" } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingNonOnboardingConfig.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingNonOnboardingConfig.kt index 19ce81f8dd..ca84612aef 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingNonOnboardingConfig.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingNonOnboardingConfig.kt @@ -160,6 +160,32 @@ data class NaviPaySettingNonOnboardingConfig( title = "Payment settings", items = listOf( + ProfileItem( + id = "UPI_GLOBAL", + tag = "New", + title = "UPI\nGlobal", + iconUrl = + "https://public-assets.prod.navi-sa.in/navi-pay/png/upi-profile-icons/UPI_international.png", + actionData = + ActionData( + url = "naviPay/NAVI_PAY_GLOBAL_MAIN_SCREEN", + parameters = + listOf( + LineItem( + key = "isDelayedOnboardingEnabled", + value = "true", + ) + ), + metaData = + GenericAnalytics( + clickedData = + GenericAnalyticsData( + eventName = + "NaviPay_UPI_International_Clicked" + ) + ), + ), + ), ProfileItem( id = "PENDING_REQUEST", title = "Payment\nrequest", diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingOnboardingConfig.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingOnboardingConfig.kt index 2b9b199bc9..9367a47c1c 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingOnboardingConfig.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPaySettingOnboardingConfig.kt @@ -160,6 +160,32 @@ data class NaviPaySettingOnboardingConfig( title = "Payment settings", items = listOf( + ProfileItem( + id = "UPI_GLOBAL", + tag = "New", + title = "UPI\nGlobal", + iconUrl = + "https://public-assets.prod.navi-sa.in/navi-pay/png/upi-profile-icons/UPI_international.png", + actionData = + ActionData( + url = "naviPay/NAVI_PAY_GLOBAL_MAIN_SCREEN", + parameters = + listOf( + LineItem( + key = "isDelayedOnboardingEnabled", + value = "true", + ) + ), + metaData = + GenericAnalytics( + clickedData = + GenericAnalyticsData( + eventName = + "NaviPay_UPI_International_Clicked" + ) + ), + ), + ), ProfileItem( id = "PENDING_REQUEST", title = "Payment\nrequest", diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayFlowType.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayFlowType.kt index e6e65cb354..20635de3bf 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayFlowType.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayFlowType.kt @@ -33,7 +33,7 @@ enum class NaviPayFlowType { MANDATE, UPI_LITE, LITE_AUTO_TOP_UP, - UPI_INTERNATIONAL, + UPI_GLOBAL, RCC_BILL_PAYMENTS, EMI_CONVERSION, EMI_FORECLOSURE, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayScreenType.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayScreenType.kt index 2ef2a7fe6f..8e3780acef 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayScreenType.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPayScreenType.kt @@ -41,4 +41,6 @@ enum class NaviPayScreenType { NAVI_PAY_MANAGE_UPI_IDS_SCREEN, NAVI_PAY_ACTIVATE_VPA_SCREEN, NAVI_PAY_LEDGER_SCREEN, + NAVI_PAY_GLOBAL_MAIN_SCREEN, + NAVI_PAY_GLOBAL_SEND_MONEY, } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayRouter.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayRouter.kt index 44a56c1323..6ac689892f 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayRouter.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayRouter.kt @@ -42,8 +42,10 @@ import com.navi.pay.destinations.PayToContactsScreenDestination import com.navi.pay.destinations.QrScannerScreenDestination import com.navi.pay.destinations.SavedBeneficiaryScreenDestination import com.navi.pay.destinations.SendMoneyScreenDestination +import com.navi.pay.destinations.UPIGlobalSendMoneyScreenDestination import com.navi.pay.destinations.UPIIdInputScreenDestination import com.navi.pay.destinations.UPILiteScreenDestination +import com.navi.pay.destinations.UpiGlobalScreenDestination import com.navi.pay.destinations.UpiNumberScreenDestination import com.navi.pay.entry.NaviPayActivity import com.navi.pay.entry.NaviPayActivityDataProvider @@ -182,6 +184,9 @@ object NaviPayRouter { MandateDetailScreenOfPendingCategoryDestination( payeeEntity = bundle.getParcelable(PAYEE_ENTITY) ) + NaviPayScreenType.NAVI_PAY_GLOBAL_MAIN_SCREEN.name -> UpiGlobalScreenDestination() + NaviPayScreenType.NAVI_PAY_GLOBAL_SEND_MONEY.name -> + UPIGlobalSendMoneyScreenDestination() else -> null } } @@ -238,6 +243,8 @@ object NaviPayRouter { else null } NaviPayScreenType.NAVI_PAY_ACTIVATE_VPA_SCREEN.name -> ActivateVpaScreenDestination + NaviPayScreenType.NAVI_PAY_GLOBAL_MAIN_SCREEN.name -> UpiGlobalScreenDestination + NaviPayScreenType.NAVI_PAY_GLOBAL_SEND_MONEY.name -> UPIGlobalSendMoneyScreenDestination else -> null } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/ui/NaviPayCommonView.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/ui/NaviPayCommonView.kt index 309c8bef19..2e8c7f05c8 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/ui/NaviPayCommonView.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/ui/NaviPayCommonView.kt @@ -7,8 +7,10 @@ package com.navi.pay.common.ui +import android.app.DatePickerDialog import android.graphics.Rect import android.view.ViewTreeObserver +import android.widget.DatePicker import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent @@ -135,6 +137,7 @@ import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.navi.base.utils.DateUtils import com.navi.base.utils.isNull import com.navi.common.R as CommonR import com.navi.common.model.NaviLottieCompositionSpecType @@ -145,6 +148,7 @@ import com.navi.design.font.getFontWeight import com.navi.design.font.naviFontFamily import com.navi.design.theme.ABABAB import com.navi.design.theme.D1D9E6_30 +import com.navi.guarddog.utils.clickableDebounce import com.navi.naviwidgets.R as WidgetsR import com.navi.naviwidgets.extensions.NaviText import com.navi.pay.R @@ -169,6 +173,8 @@ import com.navi.pay.utils.BOTTOM_SHEET_HEIGHT_PERCENTAGE_85 import com.navi.pay.utils.BULLET import com.navi.pay.utils.CHECK_BALANCE_ERROR_ANIMATION_DURATION import com.navi.pay.utils.CHECK_BALANCE_ERROR_PAUSE_DURATION +import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_YEAR_WITH_SLASH_SEPARATOR +import com.navi.pay.utils.DATE_TIME_FORMAT_YEAR_MONTH_DATE_WITH_SLASH_SEPARATOR import com.navi.pay.utils.MORE_THAN_HUNDRED_PERCENT_AS_FLOAT import com.navi.pay.utils.NAVI_PAY_LOADER import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE @@ -177,6 +183,7 @@ import com.navi.pay.utils.NAVI_PAY_UPI_LITE_LOGO_URL import com.navi.pay.utils.NAVI_PAY_UPI_NUMBER_LINKING_IN_PROGRESS_LOTTIE import com.navi.pay.utils.NAVI_PAY_UPI_NUMBER_LINK_SUCCESSFUL_LOTTIE import com.navi.pay.utils.RESOURCE_DEFAULT_ID +import com.navi.pay.utils.UPI_GLOBAL_MAX_ALLOWED_ACTIVATION_DAYS import com.navi.pay.utils.ZERO_STRING import com.navi.pay.utils.clickableDebounce import com.navi.pay.utils.conditional @@ -194,10 +201,13 @@ import com.navi.rr.common.ui.OfferBottomSheetWidget import com.navi.rr.utils.constants.Constants.DETAILS import com.navi.uitron.utils.alfredMaskSensitiveComposable import com.navi.uitron.utils.orValue +import java.util.Calendar +import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.joda.time.DateTime @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -1903,14 +1913,40 @@ fun EmptyDataScreen( } @Composable -fun BottomSheetLoadingScreen(header: String, description: String) { +fun SuccessBottomSheetWithIconHeader(headerText: String) { Column( modifier = - Modifier.navigationBarsPadding() - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp) + Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp) ) { - NaviPayLottieAnimation(modifier = Modifier.size(24.dp), lottieFileName = NAVI_PAY_LOADER) + Image( + painter = painterResource(id = R.drawable.navi_pay_ic_checked_circle_green), + contentDescription = EMPTY, + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + NaviText( + text = headerText, + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + ) + } +} + +@Composable +fun BottomSheetLoadingScreen( + header: String, + description: String, + lottieFileName: String = NAVI_PAY_LOADER, +) { + Column( + modifier = + Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp) + ) { + NaviPayLottieAnimation(modifier = Modifier.size(24.dp), lottieFileName = lottieFileName) Spacer(modifier = Modifier.height(8.dp)) @@ -1937,10 +1973,15 @@ fun BottomSheetLoadingScreen(header: String, description: String) { } @Composable -fun BottomSheetLoadingScreen(@StringRes headerTextId: Int, @StringRes descriptionTextId: Int) { +fun BottomSheetLoadingScreen( + @StringRes headerTextId: Int, + @StringRes descriptionTextId: Int, + lottieFileName: String = NAVI_PAY_LOADER, +) { BottomSheetLoadingScreen( header = stringResource(id = headerTextId), description = stringResource(id = descriptionTextId), + lottieFileName = lottieFileName, ) } @@ -3883,3 +3924,391 @@ fun KeyValueTextSection( ) } } + +@Composable +fun BottomSheetContentWithDateSelectionCalendarAndRadioButtons( + bankName: String, + deactivationDate: DateTime?, + updateDate: (DateTime) -> Unit, + onPrimaryButtonClicked: () -> Unit, + showCtaLoader: Boolean, +) { + val calendar = Calendar.getInstance(Locale.getDefault()) + + calendar.add(Calendar.DAY_OF_YEAR, 1) + + Column( + modifier = + Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp) + ) { + NaviText( + text = stringResource(id = R.string.np_select_activation_period, bankName), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + fontSize = 16.sp, + color = NaviPayColor.textPrimary, + lineHeight = 24.sp, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + NaviText( + text = stringResource(id = R.string.np_upi_global_activation_maximum_time_description), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 14.sp, + color = textTertiary, + lineHeight = 22.sp, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + DateSelectionItemList( + calendar = calendar, + updateDate = updateDate, + deactivationDate = deactivationDate, + showCtaLoader = showCtaLoader, + ) + } + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + LoaderRoundedButton( + text = stringResource(id = R.string.np_activate_upi_global), + onClick = onPrimaryButtonClicked, + modifier = Modifier.weight(1f), + showLoader = showCtaLoader, + enabled = !showCtaLoader && deactivationDate != null, + lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE, + ) + } + } +} + +@Composable +fun DateSelectionItemList( + calendar: Calendar, + updateDate: (DateTime) -> Unit, + deactivationDate: DateTime?, + showCtaLoader: Boolean, +) { + val options = remember { listOf("1 week", "2 weeks", "1 month", "3 months", "Select dates") } + val endDates = remember { listOf(6, 13, 29, 88) } // Corresponding days to add for each option + var selectedOptionIndex by remember { + mutableIntStateOf(0) + } // Default selection to the custom date option + + LaunchedEffect(selectedOptionIndex) { + if (selectedOptionIndex != options.size - 1) { + val endDate = endDates[selectedOptionIndex] + calendar.add(Calendar.DAY_OF_YEAR, endDate - 1) + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + 1 // months are 0-based + val day = calendar.get(Calendar.DAY_OF_MONTH) + updateDate( + DateUtils.getDateTimeObjectFromDateTimeString( + dateTime = "$year/$month/$day", + format = DATE_TIME_FORMAT_YEAR_MONTH_DATE_WITH_SLASH_SEPARATOR, + ) + ) + } + } + + RadioButtonGroupItem( + options = options, + selectedOptionIndex = selectedOptionIndex, + onOptionSelected = { selectedOptionIndex = it }, + deactivationDate = deactivationDate, + calendar = calendar, + updateDate = updateDate, + showCtaLoader = showCtaLoader, + ) +} + +@Composable +fun RadioButtonGroupItem( + options: List, + selectedOptionIndex: Int, + onOptionSelected: (Int) -> Unit, + deactivationDate: DateTime?, + updateDate: (DateTime) -> Unit, + calendar: Calendar, + showCtaLoader: Boolean, +) { + var isCustomDateSelected by remember { mutableStateOf(false) } + + Column { + options.forEachIndexed { index, option -> + Row( + modifier = + Modifier.fillMaxWidth().clickableDebounce { + isCustomDateSelected = index == options.size - 1 + onOptionSelected(index) + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + NaviText( + text = option, + fontFamily = naviFontFamily, + fontWeight = + if (isCustomDateSelected && index == options.size - 1) + getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR) + else getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 14.sp, + color = NaviPayColor.textPrimary, + ) + NaviPayDottedRadioButton( + isSelected = index == selectedOptionIndex, + onCheckedChange = { + isCustomDateSelected = index == options.size - 1 + onOptionSelected(index) + }, + ) + } + Spacer(modifier = Modifier.height(if (index != options.size - 1) 24.dp else 8.dp)) + } + CustomDatePickerView( + deactivationDate = deactivationDate, + updateDate = updateDate, + calendar = calendar, + showCtaLoader = showCtaLoader, + isCustomDateSelected = isCustomDateSelected, + ) + Spacer(modifier = Modifier.height(24.dp)) + } +} + +@Composable +fun CustomDatePickerView( + deactivationDate: DateTime?, + updateDate: (DateTime) -> Unit, + calendar: Calendar, + showCtaLoader: Boolean, + isCustomDateSelected: Boolean, +) { + val context = LocalContext.current + val minDate = calendar.timeInMillis + val datePickerDialog = remember { + val maxDate = + Calendar.getInstance(Locale.getDefault()) + .apply { add(Calendar.DAY_OF_YEAR, UPI_GLOBAL_MAX_ALLOWED_ACTIVATION_DAYS) } + .timeInMillis + DatePickerDialog( + context, + R.style.DatePickerDialogTheme, + { _: DatePicker, year: Int, month: Int, day: Int -> + updateDate( + DateUtils.getDateTimeObjectFromDateTimeString( + dateTime = "$year/${month + 1}/$day", + DATE_TIME_FORMAT_YEAR_MONTH_DATE_WITH_SLASH_SEPARATOR, + ) + ) + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH), + ) + .apply { + datePicker.minDate = minDate + datePicker.maxDate = maxDate + } + } + if (isCustomDateSelected) { + Row(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.weight(1f)) { + NaviText( + text = stringResource(id = R.string.np_start), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 14.sp, + color = NaviPayColor.textPrimary, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + InputTextFieldWithDescriptionHeader( + value = + DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = DateTime.now(), + format = DATE_TIME_FORMAT_DATE_MONTH_YEAR_WITH_SLASH_SEPARATOR, + ), + modifier = Modifier.fillMaxWidth(), + headerString = null, + placeHolderString = EMPTY, + enabled = false, + backgroundColor = bgDefault, + textStyle = + TextStyle( + fontSize = 14.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.inputFieldDefault, + ), + ) + } + + Spacer(modifier = Modifier.width(40.dp)) + + Column(modifier = Modifier.weight(1f)) { + NaviText( + text = stringResource(id = R.string.np_end), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + fontSize = 14.sp, + color = NaviPayColor.textPrimary, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + InputTextFieldWithDescriptionHeader( + value = + if (deactivationDate != null) { + DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = deactivationDate, + format = DATE_TIME_FORMAT_DATE_MONTH_YEAR_WITH_SLASH_SEPARATOR, + ) + } else stringResource(id = R.string.np_date_format), + modifier = + Modifier.fillMaxWidth().clickableDebounce { + if (!showCtaLoader) datePickerDialog.show() + }, + headerString = null, + placeHolderString = EMPTY, + isTrailingIconEnabled = true, + trailingIconId = R.drawable.ic_calendar_mandate, + onTrailingIconClicked = { if (!showCtaLoader) datePickerDialog.show() }, + enabled = false, + backgroundColor = bgDefault, + textStyle = + TextStyle( + fontSize = 14.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + color = NaviPayColor.inputFieldFilled, + ), + ) + } + } + } +} + +@Composable +fun LoadingBottomSheetView(text: String) { + val spec = LottieCompositionSpec.Asset(NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE) + val composition by rememberLottieComposition(spec) + Column( + modifier = + Modifier.navigationBarsPadding() + .fillMaxWidth() + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp) + ) { + LottieAnimation( + composition = composition, + iterations = LottieConstants.IterateForever, + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + NaviText( + text = text, + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + ) + } +} + +@Composable +fun DeactivateUpiGlobalView( + onPrimaryButtonClicked: () -> Unit, + onSecondaryButtonClicked: () -> Unit, + showCtaLoader: Boolean, +) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Image( + painter = painterResource(id = CommonR.drawable.ic_info_solid_blue), + contentDescription = EMPTY, + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + NaviText( + text = stringResource(R.string.np_deactivate_upi_global), + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + ) + Spacer(modifier = Modifier.height(8.dp)) + NaviText( + text = stringResource(R.string.np_upi_global_deactivate_description), + fontSize = 14.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = textTertiary, + lineHeight = 22.sp, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + LoaderRoundedButton( + text = stringResource(R.string.np_deactivate_upi_global), + onClick = onPrimaryButtonClicked, + enabled = !showCtaLoader, + showLoader = showCtaLoader, + modifier = Modifier.fillMaxWidth(), + lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SecondaryRoundedButton( + text = stringResource(R.string.cancel), + onClick = { + if (!showCtaLoader) { + onSecondaryButtonClicked() + } + }, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +fun ConversionDetailSection(title: String, description: String, value: String) { + Column(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + NaviText( + text = title, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 14.sp, + lineHeight = 22.sp, + color = NaviPayColor.textPrimary, + ) + Spacer(modifier = Modifier.weight(1f)) + NaviText( + text = value, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + fontSize = 14.sp, + lineHeight = 22.sp, + color = NaviPayColor.textPrimary, + ) + } + + Spacer(modifier = Modifier.height(2.dp)) + + NaviText( + text = description, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 12.sp, + lineHeight = 18.sp, + color = textTertiary, + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/ActivateUpiGlobalUseCase.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/ActivateUpiGlobalUseCase.kt new file mode 100644 index 0000000000..667e40308f --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/ActivateUpiGlobalUseCase.kt @@ -0,0 +1,162 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.common.usecase + +import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity +import com.navi.pay.common.model.view.NaviPayFlowType +import com.navi.pay.common.model.view.SimInfo +import com.navi.pay.common.utils.DeviceInfoProvider +import com.navi.pay.common.utils.NaviPayCommonUtils +import com.navi.pay.common.utils.getMetricInfo +import com.navi.pay.management.common.utils.NaviPayPspManager +import com.navi.pay.npcicl.CredDataProvider +import com.navi.pay.npcicl.NpciCredData +import com.navi.pay.npcicl.NpciRepository +import com.navi.pay.npcicl.NpciResult +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.navi.pay.onboarding.binding.model.view.NaviPayCustomerOnboardingEntity +import javax.inject.Inject +import org.joda.time.DateTime + +class ActivateUpiGlobalUseCase +@Inject +constructor( + private val naviPayPspManager: NaviPayPspManager, + private val deviceInfoProvider: DeviceInfoProvider, + private val credDataProvider: CredDataProvider, + private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity, + private val upiRequestIdUseCase: UpiRequestIdUseCase, + private val npciRepository: NpciRepository, +) { + + private lateinit var screenName: String + + suspend fun execute( + screenName: String, + selectedBankAccount: LinkedAccountEntity, + onOnboardingTriggered: suspend () -> Unit = {}, + onOnboardingSuccess: suspend () -> Unit = {}, + onActivateUpiGlobalClicked: () -> Unit, + onNpciResult: + suspend ( + npciResult: NpciResult, + upiRequestId: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) -> Unit, + onNoInternetError: () -> Unit, + onAirplaneModeError: () -> Unit, + onSimFailureError: (isNoSimPresent: Boolean) -> Unit, + ) { + this.screenName = screenName + val vpaEntityList = selectedBankAccount.vpaEntityList + naviPayPspManager.evaluateAndOnboardPspForFlow( + naviPayFlowType = NaviPayFlowType.UPI_GLOBAL, + vpaEntityList = vpaEntityList, + screenName = screenName, + onOnboardingTriggered = { onOnboardingTriggered() }, + onPspEvaluated = { pspEvaluationResult -> + if (pspEvaluationResult.onboardingDataEntity == null) { + return@evaluateAndOnboardPspForFlow + } + if (!checkIsInternetAvailableOrShowError(onNoInternetError)) + return@evaluateAndOnboardPspForFlow + if (checkIsAirplaneModeOnOrShowError(onAirplaneModeError)) + return@evaluateAndOnboardPspForFlow + val currentSimInfoList = naviPayNetworkConnectivity.getCurrentSimInfoList() + if ( + !validateSimInfoOrShowError( + currentSimInfoList = currentSimInfoList, + onSimFailureError = onSimFailureError, + ) + ) + return@evaluateAndOnboardPspForFlow + if (pspEvaluationResult.isOnboardingTriggered) { + onOnboardingSuccess() + } + activateUpiGlobalAccount( + selectedBankAccount = selectedBankAccount, + customerOnboardingEntity = pspEvaluationResult.onboardingDataEntity, + onActivateUpiGlobalClicked = onActivateUpiGlobalClicked, + onNpciResult = onNpciResult, + ) + }, + ) + } + + private suspend fun activateUpiGlobalAccount( + selectedBankAccount: LinkedAccountEntity, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + onActivateUpiGlobalClicked: () -> Unit, + onNpciResult: + suspend ( + npciResult: NpciResult, + upiRequestId: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) -> Unit, + ) { + onActivateUpiGlobalClicked() + val upiRequestId = upiRequestIdUseCase.execute(customerOnboardingEntity.pspType) + val upiInternationalActivationCredData = + getUpiGlobalActivationCredData( + upiRequestId = upiRequestId, + linkedAccountEntity = selectedBankAccount, + ) + val npciResult = + npciRepository.fetchCredentials( + npciCredData = upiInternationalActivationCredData, + metricInfo = getMetricInfo(screenName = screenName), + customerOnboardingEntity = customerOnboardingEntity, + ) + onNpciResult(npciResult, upiRequestId, customerOnboardingEntity) + } + + private fun getUpiGlobalActivationCredData( + upiRequestId: String, + linkedAccountEntity: LinkedAccountEntity, + ): NpciCredData { + + val txnTimeStamp = DateTime.now() + return credDataProvider.upiGlobalCred( + linkedAccountEntity = linkedAccountEntity, + upiRequestId = upiRequestId, + txnTimeStamp = txnTimeStamp.toString(), + ) + } + + private fun checkIsInternetAvailableOrShowError(onNoInternetError: () -> Unit): Boolean { + if (!naviPayNetworkConnectivity.isInternetConnected()) { + onNoInternetError() + return false + } + return true + } + + private fun checkIsAirplaneModeOnOrShowError(onAirplaneModeError: () -> Unit): Boolean { + if (naviPayNetworkConnectivity.isAirplaneModeOn()) { + onAirplaneModeError() + return true + } + return false + } + + private suspend fun validateSimInfoOrShowError( + currentSimInfoList: List, + onSimFailureError: (isNoSimPresent: Boolean) -> Unit, + ): Boolean { + val isSimInfoValid = + NaviPayCommonUtils.isSimInfoValid( + currentSimInfoList = currentSimInfoList, + deviceInfoProvider = deviceInfoProvider, + ) + if (!isSimInfoValid) { + onSimFailureError(currentSimInfoList.isEmpty()) + return false + } + return true + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/DeactivateUpiGlobalUseCase.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/DeactivateUpiGlobalUseCase.kt new file mode 100644 index 0000000000..a9b643281f --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/DeactivateUpiGlobalUseCase.kt @@ -0,0 +1,165 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.common.usecase + +import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity +import com.navi.pay.common.model.view.NaviPayFlowType +import com.navi.pay.common.model.view.SimInfo +import com.navi.pay.common.utils.DeviceInfoProvider +import com.navi.pay.common.utils.NaviPayCommonUtils +import com.navi.pay.common.utils.getMetricInfo +import com.navi.pay.management.common.utils.NaviPayPspManager +import com.navi.pay.npcicl.CredDataProvider +import com.navi.pay.npcicl.NpciCredData +import com.navi.pay.npcicl.NpciRepository +import com.navi.pay.npcicl.NpciResult +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.navi.pay.onboarding.binding.model.view.NaviPayCustomerOnboardingEntity +import javax.inject.Inject +import org.joda.time.DateTime + +class DeactivateUpiGlobalUseCase +@Inject +constructor( + private val naviPayPspManager: NaviPayPspManager, + private val deviceInfoProvider: DeviceInfoProvider, + private val credDataProvider: CredDataProvider, + private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity, + private val upiRequestIdUseCase: UpiRequestIdUseCase, + private val npciRepository: NpciRepository, +) { + + private lateinit var screenName: String + + suspend fun execute( + screenName: String, + selectedBankAccount: LinkedAccountEntity, + onOnboardingTriggered: suspend () -> Unit, + onOnboardingSuccess: suspend () -> Unit, + onDeactivateUpiGlobalClicked: () -> Unit, + onNpciResult: + suspend ( + npciResult: NpciResult, + upiRequestId: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) -> Unit, + onNoInternetError: () -> Unit, + onAirplaneModeError: () -> Unit, + onSimFailureError: (isNoSimPresent: Boolean) -> Unit, + ) { + this.screenName = screenName + val vpaEntity = + selectedBankAccount.getVpaEntityByPsp( + selectedBankAccount.upiGlobalInfo?.pspType ?: return + ) + naviPayPspManager.evaluateAndOnboardPspForVpa( + vpaEntity = vpaEntity, + screenName = screenName, + onOnboardingTriggered = { onOnboardingTriggered() }, + onPspEvaluated = { pspEvaluationResult -> + if (pspEvaluationResult.onboardingDataEntity == null) { + return@evaluateAndOnboardPspForVpa + } + if (!checkIsInternetAvailableOrShowError(onNoInternetError)) + return@evaluateAndOnboardPspForVpa + if (checkIsAirplaneModeOnOrShowError(onAirplaneModeError)) + return@evaluateAndOnboardPspForVpa + val currentSimInfoList = naviPayNetworkConnectivity.getCurrentSimInfoList() + if ( + !validateSimInfoOrShowError( + currentSimInfoList = currentSimInfoList, + onSimFailureError = onSimFailureError, + ) + ) + return@evaluateAndOnboardPspForVpa + if (pspEvaluationResult.isOnboardingTriggered) { + onOnboardingSuccess() + } + deactivateUPIGlobalAccount( + selectedBankAccount = selectedBankAccount, + customerOnboardingEntity = pspEvaluationResult.onboardingDataEntity, + onDeactivateUpiGlobalClicked = onDeactivateUpiGlobalClicked, + onNpciResult = onNpciResult, + ) + }, + naviPayFlowType = NaviPayFlowType.UPI_GLOBAL, + ) + } + + private suspend fun deactivateUPIGlobalAccount( + selectedBankAccount: LinkedAccountEntity, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + onDeactivateUpiGlobalClicked: () -> Unit, + onNpciResult: + suspend ( + npciResult: NpciResult, + upiRequestId: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) -> Unit, + ) { + onDeactivateUpiGlobalClicked() + val upiRequestId = upiRequestIdUseCase.execute(customerOnboardingEntity.pspType) + val upiInternationalDeactivationCredData = + getUpiGlobalDeactivationCredData( + upiRequestId = upiRequestId, + linkedAccountEntity = selectedBankAccount, + ) + val npciResult = + npciRepository.fetchCredentials( + npciCredData = upiInternationalDeactivationCredData, + metricInfo = getMetricInfo(screenName = screenName), + customerOnboardingEntity = customerOnboardingEntity, + ) + onNpciResult(npciResult, upiRequestId, customerOnboardingEntity) + } + + private fun getUpiGlobalDeactivationCredData( + upiRequestId: String, + linkedAccountEntity: LinkedAccountEntity, + ): NpciCredData { + + val txnTimeStamp = DateTime.now() + return credDataProvider.upiGlobalCred( + linkedAccountEntity = linkedAccountEntity, + upiRequestId = upiRequestId, + txnTimeStamp = txnTimeStamp.toString(), + ) + } + + private fun checkIsInternetAvailableOrShowError(onNoInternetError: () -> Unit): Boolean { + if (!naviPayNetworkConnectivity.isInternetConnected()) { + onNoInternetError() + return false + } + return true + } + + private fun checkIsAirplaneModeOnOrShowError(onAirplaneModeError: () -> Unit): Boolean { + if (naviPayNetworkConnectivity.isAirplaneModeOn()) { + onAirplaneModeError() + return true + } + return false + } + + private suspend fun validateSimInfoOrShowError( + currentSimInfoList: List, + onSimFailureError: (isNoSimPresent: Boolean) -> Unit, + ): Boolean { + val isSimInfoValid = + NaviPayCommonUtils.isSimInfoValid( + currentSimInfoList = currentSimInfoList, + deviceInfoProvider = deviceInfoProvider, + ) + if (!isSimInfoValid) { + onSimFailureError(currentSimInfoList.isEmpty()) + return false + } + return true + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt index b391cc628f..1f196a7ca8 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt @@ -90,6 +90,7 @@ import com.navi.pay.common.settingscreen.model.CardType import com.navi.pay.common.settingscreen.model.QrDetails import com.navi.pay.common.theme.color.NaviPayColor import com.navi.pay.common.utils.NaviPayCommonUtils.getNormalisedPhoneNumber +import com.navi.pay.common.utils.NaviPayCommonUtils.roundOffToTwoDecimalPlaces import com.navi.pay.common.viewmodel.NaviPayBaseVM.Companion.ERROR_DEFAULT_TAG import com.navi.pay.common.viewmodel.NaviPayBaseVM.Companion.UNKNOWN_ERROR_CODE import com.navi.pay.management.common.sendmoney.model.network.TransactionInitiationType @@ -137,6 +138,7 @@ import com.navi.pay.utils.X import com.navi.pay.utils.Z import com.navi.pay.utils.customHide import com.navi.pay.utils.getMaskedAccountNumber +import com.navi.pay.utils.safeConvertStringToDouble import java.security.MessageDigest import java.text.SimpleDateFormat import java.util.Date @@ -231,6 +233,10 @@ object NaviPayCommonUtils { ) != 0 } + fun roundOffToTwoDecimalPlaces(value: Double): Double { + return String.format(Locale.US, "%.2f", value).toDoubleOrNull() ?: 0.0 + } + private fun getSSIDFromSubscriptionId(subscriptionId: Int): String { return subscriptionId.toString() } @@ -1060,3 +1066,29 @@ fun getTimeInMillisTillMidnight(): Long { return min(24, hoursLeftTillMidnight).hours.inWholeMilliseconds } + +fun getBankCharges(forex: String?, markUp: String?, baseAmount: String?): Double { + return roundOffToTwoDecimalPlaces( + baseAmount.safeConvertStringToDouble() * + forex.safeConvertStringToDouble() * + markUp.safeConvertStringToDouble() / 100.0 + ) +} + +fun getBankNameTruncatedAccountNumberText(bankName: String, maskedAccountNumber: String): String { + return if (bankName.length > 15) { + val truncated = bankName.substring(startIndex = 0, endIndex = 15) + "$truncated... - ${maskedAccountNumber.getMaskedAccountNumber()}".trim() + } else { + getBankNameAccountNumberText(bankName = bankName, maskedAccountNumber = maskedAccountNumber) + } +} + +fun getDayEndDateTime(date: DateTime?): String? { + return date + ?.withHourOfDay(23) + ?.withMinuteOfHour(59) + ?.withSecondOfMinute(59) + ?.withZone(DateTimeZone.forTimeZone(TimeZone.getTimeZone("Asia/Kolkata"))) + .toString() +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/db/NaviPayAppDatabase.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/db/NaviPayAppDatabase.kt index dc0268954f..03336a2819 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/db/NaviPayAppDatabase.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/db/NaviPayAppDatabase.kt @@ -43,6 +43,7 @@ import com.navi.pay.onboarding.account.common.dao.VpaDao import com.navi.pay.onboarding.account.common.model.view.AccountEntity import com.navi.pay.onboarding.account.common.model.view.VpaEntity import com.navi.pay.onboarding.account.common.util.UPILiteInfoConverter +import com.navi.pay.onboarding.account.common.util.UpiGlobalInfoConverter import com.navi.pay.onboarding.binding.db.dao.NaviPayCustomerOnboardingDao import com.navi.pay.onboarding.binding.model.view.NaviPayCustomerOnboardingEntity import com.navi.pay.tstore.list.db.converter.OrderDetailConverter @@ -102,7 +103,7 @@ import com.navi.pay.utils.NAVI_PAY_SYNC_TABLE_TRANSACTION_HISTORY_KEY OrderErrorEntity::class, ], views = [SelfTransferView::class], - version = 17, + version = 18, exportSchema = false, ) @TypeConverters( @@ -113,6 +114,7 @@ import com.navi.pay.utils.NAVI_PAY_SYNC_TABLE_TRANSACTION_HISTORY_KEY OrderStatusOfViewConverter::class, OrderDetailConverter::class, MessageContentConverter::class, + UpiGlobalInfoConverter::class, ) abstract class NaviPayAppDatabase : RoomDatabase() { abstract fun bankDao(): BankDao @@ -525,3 +527,24 @@ val NAVI_PAY_APP_DATABASE_MIGRATION_16_17 = ) } } + +val NAVI_PAY_APP_DATABASE_MIGRATION_17_18 = + object : Migration(17, 18) { + override fun migrate(db: SupportSQLiteDatabase) { + + // This migration is for adding new column in account table for global info required for + // upi global and adding new column in transactionHistory for upiGlobalAmount + db.execSQL( + "ALTER TABLE `$NAVI_PAY_DATABASE_ACCOUNTS_TABLE_NAME` ADD COLUMN `upiGlobalInfo` TEXT NOT NULL DEFAULT '[]'" + ) + db.execSQL( + "ALTER TABLE `$NAVI_PAY_DATABASE_TRANSACTION_HISTORY_TABLE_NAME` ADD COLUMN `upiGlobalCurrencyAndAmount` TEXT NOT NULL DEFAULT ''" + ) + + // This migration is for adding new column in order history list table to display upi + // global amount in history list + db.execSQL( + "ALTER TABLE `$NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME` ADD COLUMN `upiGlobalCurrencyAndAmount` TEXT NOT NULL DEFAULT ''" + ) + } + } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/model/view/ShareReceiptEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/model/view/ShareReceiptEntity.kt index 4fca414e33..04c1dbd457 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/model/view/ShareReceiptEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/model/view/ShareReceiptEntity.kt @@ -31,8 +31,11 @@ data class ShareReceiptEntity( val payerBankNameFormatted: String, val isCollectRequestExpired: Boolean, val isCollectRequestDeclined: Boolean, - val isCredited: Boolean, val transactionCompletionTime: Long = 10000L, val shareReceiptCallOutText: String, val payeeMcc: String, + val isCredited: Boolean, + val currencySymbol: String, + val isUpiGlobalTransaction: Boolean, + val baseCurrencyAmount: String?, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt index 2a6ab378dc..5f67582d45 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt @@ -94,6 +94,7 @@ import com.navi.pay.management.common.paymentsummary.model.view.PaymentSummarySc import com.navi.pay.management.common.paymentsummary.model.view.ShareReceiptEntity import com.navi.pay.management.common.paymentsummary.viewmodel.PaymentSummaryViewModel import com.navi.pay.management.common.transaction.model.view.TransactionStatusOfView +import com.navi.pay.management.common.transaction.util.isUpiGlobalTransaction import com.navi.pay.utils.NAVI_PAY_GREEN_TICK_LOTTIE import com.navi.pay.utils.clickableDebounce import com.navi.pay.utils.shareReceipt @@ -240,6 +241,11 @@ fun PaymentSummaryScreen( transactionCompletionTime = transactionCompletionTime, shareReceiptCallOutText = paymentSummaryViewModel.shareReceiptCallOutText, payeeMcc = transactionEntity?.transactionDetailEntity?.payeeInfo?.mcc.orEmpty(), + currencySymbol = + transactionEntity?.transactionDetailEntity?.metaData?.baseCurr.orEmpty(), + isUpiGlobalTransaction = isUpiGlobalTransaction(transactionEntity), + baseCurrencyAmount = + transactionEntity?.transactionDetailEntity?.metaData?.baseAmount, ), upiAppLogoBaseUrl = upiAppLogoBaseUrl, upiAppsIconList = upiAppsIconList, @@ -458,6 +464,13 @@ fun PaymentSummaryScreen( isArcProtected = isArcProtected, transactionProcessedCalloutType = paymentSummaryViewModel.transactionProcessedCalloutType, + currencySymbol = + transactionEntity + ?.transactionDetailEntity + ?.metaData + ?.baseCurr + .orEmpty(), + isUpiGlobalTransaction = isUpiGlobalTransaction(transactionEntity), ) } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt index 9a76794214..ca46c1afde 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt @@ -97,6 +97,7 @@ import com.navi.pay.management.common.paymentsummary.model.view.PaymentSummaryRe import com.navi.pay.management.common.paymentsummary.model.view.TransactionProcessedCalloutType import com.navi.pay.management.common.paymentsummary.model.view.UpiLiteInfoBannerData import com.navi.pay.management.common.ui.TransactionInfoCard +import com.navi.pay.management.common.ui.UpiGlobalConvertedAmountSection import com.navi.pay.management.transactionhistory.model.view.TransactionDetailItemProperty import com.navi.pay.management.transactionhistory.model.view.TransactionEntity import com.navi.pay.utils.ACTIVATE @@ -154,6 +155,8 @@ fun SharedTransitionScope.PaymentSummaryTransactionDetailSection( playerCardDataProvider: () -> ScratchCardModel?, isArcProtected: Boolean, transactionProcessedCalloutType: TransactionProcessedCalloutType, + currencySymbol: String, + isUpiGlobalTransaction: Boolean, ) { val onViewDetailCtaClicked = { @@ -217,6 +220,8 @@ fun SharedTransitionScope.PaymentSummaryTransactionDetailSection( payeeInfoText = transactionEntity?.transactionDetailEntity?.payeeInfo?.name, ctaText = stringResource(id = R.string.np_view_details), onCtaClicked = onViewDetailCtaClicked, + currencySymbol = currencySymbol, + isUpiGlobalTransaction = isUpiGlobalTransaction, ) if (isArcProtected) { @@ -226,6 +231,11 @@ fun SharedTransitionScope.PaymentSummaryTransactionDetailSection( ) } + if (isUpiGlobalTransaction) { + UpiGlobalConvertedAmountSection( + convertedAmount = transactionEntity?.formattedAmount.orEmpty() + ) + } Spacer( modifier = Modifier.height( diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/SendMoneyRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/SendMoneyRequest.kt index ca4713e161..bf56a3bf25 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/SendMoneyRequest.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/SendMoneyRequest.kt @@ -67,6 +67,15 @@ data class GatewayTxnInfo( @SerializedName("txnRequestType") val txnRequestType: String? = null, @SerializedName("isArcProtected") val isArcProtected: Boolean? = null, @SerializedName("geocode") val geocode: String? = null, + @SerializedName("upiGlobalInfo") val upiGlobalInfo: UpiGlobalInfo? = null, +) + +data class UpiGlobalInfo( + @SerializedName("baseAmount") val baseAmount: String, + @SerializedName("baseCurr") val baseCurr: String, + @SerializedName("validateQrUpiRequestId") val validateQrUpiRequestId: String, + @SerializedName("forex") val forex: String, + @SerializedName("markUp") val markUp: String, ) @Parcelize diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/viewmodel/SendMoneyViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/viewmodel/SendMoneyViewModel.kt index b8a75b9e82..b2286eb946 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/viewmodel/SendMoneyViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/viewmodel/SendMoneyViewModel.kt @@ -2213,7 +2213,6 @@ constructor( updateTriggerDismissBottomSheet() return } - val currentSimInfoList = naviPayNetworkConnectivity.getCurrentSimInfoList() if ( !validateSimInfoOrShowError( diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/model/view/TransactionCategoryTags.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/model/view/TransactionCategoryTags.kt index 8915a35f58..640132fac9 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/model/view/TransactionCategoryTags.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/model/view/TransactionCategoryTags.kt @@ -17,4 +17,5 @@ enum class TransactionCategoryTags(val value: String) { AUTO_PAY("A6"), LOANS("A7"), GOLD("A8"), + UPI_GLOBAL("A9"), } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/OrderEntityMapperUtil.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/OrderEntityMapperUtil.kt index 84fe21bb10..86abbbfdfa 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/OrderEntityMapperUtil.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/OrderEntityMapperUtil.kt @@ -8,6 +8,7 @@ package com.navi.pay.management.common.transaction.util import com.navi.base.utils.EMPTY +import com.navi.pay.common.utils.NaviPayCommonUtils.getTagStringWithSeparator import com.navi.pay.management.common.transaction.model.network.TransactionInstrumentType import com.navi.pay.management.common.transaction.model.network.TransactionStatus import com.navi.pay.management.common.transaction.model.view.TransactionStatusOfView @@ -17,6 +18,7 @@ import com.navi.pay.management.transactionhistory.model.view.TransactionEntity import com.navi.pay.tstore.details.model.view.OrderPaymentStatus import com.navi.pay.tstore.details.model.view.OrderProductType import com.navi.pay.tstore.details.ui.upi.NaviPayTransactionDetailsMetadata +import com.navi.pay.tstore.details.ui.upi.UpiGlobalInfo import com.navi.pay.tstore.details.ui.upi.UpiInfo import com.navi.pay.tstore.details.ui.upi.UserTxnInfo import com.navi.pay.tstore.list.model.network.NaviUpiAccInfo @@ -89,6 +91,12 @@ internal fun TransactionEntity.toOrderEntity(tstoreOrderId: String): OrderEntity paymentStatus = TransactionStatus.toTransactionStatus(transactionDetailEntity.metaData.txnStatus) .toPaymentStatus(), + upiGlobalCurrencyAndAmount = + getTagStringWithSeparator( + transactionDetailEntity.metaData.baseCurr, + transactionDetailEntity.metaData.baseAmount, + shouldSort = false, + ), ) private fun TransactionStatusOfView.toOrderStatusOfView(): OrderStatusOfView = @@ -120,6 +128,13 @@ private fun TransactionEntity.getNaviPayTransactionDetailsMetadata(): gwRefId = transactionDetailEntity.metaData.gwRefId, gwTxnId = transactionDetailEntity.metaData.gwTxnId, ), + upiGlobalInfo = + UpiGlobalInfo( + baseCurr = transactionDetailEntity.metaData.baseCurr, + baseAmount = transactionDetailEntity.metaData.baseAmount, + forex = transactionDetailEntity.metaData.forex, + markUp = transactionDetailEntity.metaData.markUp, + ), upiReqId = transactionDetailEntity.metaData.upiReqId, splitDetails = transactionDetailEntity.metaData.splitDetails, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/TransactionEntityMapperUtil.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/TransactionEntityMapperUtil.kt index ac1ae9d71f..eddcc8dde3 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/TransactionEntityMapperUtil.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/transaction/util/TransactionEntityMapperUtil.kt @@ -9,6 +9,7 @@ package com.navi.pay.management.common.transaction.util import com.google.firebase.crashlytics.FirebaseCrashlytics import com.navi.base.utils.getOrDefaultCompat +import com.navi.base.utils.orFalse import com.navi.common.utils.EMPTY import com.navi.pay.common.utils.NaviPayCommonUtils import com.navi.pay.common.utils.NaviPayCommonUtils.getTagStringWithSeparator @@ -25,6 +26,7 @@ import com.navi.pay.management.common.transaction.model.view.getViewTransactionS import com.navi.pay.management.mandate.model.view.LiteMandatePurpose import com.navi.pay.management.transactionhistory.model.view.TransactionDetailEntity import com.navi.pay.management.transactionhistory.model.view.TransactionDetailMetaData +import com.navi.pay.management.transactionhistory.model.view.TransactionEntity import com.navi.pay.onboarding.account.add.model.view.AccountType import com.navi.pay.utils.BBPS_UPI_PURPOSE import com.navi.pay.utils.NAVI_LOGO_TRANSACTION_HISTORY_PLACEHOLDER_URL @@ -40,6 +42,7 @@ import com.navi.pay.utils.NAVI_PAY_UPI_LITE_INITIAL_TOP_UP_PURPOSE_CODE import com.navi.pay.utils.NAVI_PAY_UPI_LITE_LOGO_URL import com.navi.pay.utils.NAVI_PAY_UPI_LITE_SEND_MONEY_PURPOSE_CODE import com.navi.pay.utils.NAVI_PAY_UPI_LITE_SUBSEQUENT_TOP_UP_PURPOSE_CODE +import com.navi.pay.utils.UPI_GLOBAL_PURPOSE import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.json.JSONObject @@ -266,6 +269,10 @@ fun TransactionDetailEntity.getOtherUserInfoWithSeparator(): String { return getTagStringWithSeparator(name, formattedVpa, vpa, imageUrl, shouldSort = false) } +fun TransactionDetailEntity.getUpiGlobalCurrencyAndAmountWithSeparator(): String { + return getTagStringWithSeparator(metaData.baseCurr, metaData.baseAmount, shouldSort = false) +} + fun TransactionDetailEntity.getTransactionCategoryTagsWithSeparator(): String { val category = getTransactionCategory() @@ -311,6 +318,10 @@ fun TransactionDetailEntity.getTransactionCategoryTagsWithSeparator(): String { eligibleTags.add(TransactionCategoryTags.BBPS.value) } + if (metaData.purposeCode == UPI_GLOBAL_PURPOSE) { + eligibleTags.add(TransactionCategoryTags.UPI_GLOBAL.value) + } + // Check for BBPS case & add tag for BBPS return getTagStringWithSeparator(*eligibleTags.toTypedArray()) @@ -395,6 +406,9 @@ fun TransactionDetailEntity.isTransactionOfTypeSelfTransfer() = metaData.role == TransactionRole.PAYER.name && metaData.txnType == UpiTransactionType.SELF_PAY.name +fun isUpiGlobalTransaction(transactionEntity: TransactionEntity?) = + transactionEntity?.categoryTags?.contains(TransactionCategoryTags.UPI_GLOBAL.value).orFalse() + fun getOwnBankIconUrlFromOwnBankInfo(ownBankInfo: String) = ownBankInfo.split(NAVI_PAY_TRANSACTION_HISTORY_TAG_SEPARATOR).getOrElse(2) { "" } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/ui/TransactionDetailSectionCommonViews.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/ui/TransactionDetailSectionCommonViews.kt index 34056b9628..40c1be31bc 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/ui/TransactionDetailSectionCommonViews.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/ui/TransactionDetailSectionCommonViews.kt @@ -27,7 +27,10 @@ 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.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.navi.base.utils.EMPTY @@ -54,6 +57,8 @@ fun TransactionInfoCard( onCtaClicked: () -> Unit, payeeInfoText: String? = null, onHelpCtaClicked: () -> Unit, + currencySymbol: String, + isUpiGlobalTransaction: Boolean = false, ) { Column( modifier = @@ -79,10 +84,16 @@ fun TransactionInfoCard( Column { NaviText( text = - stringResource( - id = R.string.rupee_symbol_x, - transactionEntity?.formattedAmount ?: "", - ), + if (isUpiGlobalTransaction) + "$currencySymbol ${ + transactionEntity?.transactionDetailEntity?.metaData?.baseAmount + ?: "" + }" + else + stringResource( + id = R.string.rupee_symbol_x, + transactionEntity?.formattedAmount.orEmpty(), + ), fontFamily = naviFontFamily, fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), fontSize = 24.sp, @@ -184,3 +195,36 @@ fun TransactionInfoCard( Spacer(modifier = Modifier.height(24.dp)) } } + +@Composable +fun UpiGlobalConvertedAmountSection(convertedAmount: String) { + Row(modifier = Modifier.fillMaxWidth().background(color = NaviPayColor.bgAlt2).padding(16.dp)) { + NaviText( + text = + buildAnnotatedString { + withStyle( + style = + SpanStyle( + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 14.sp, + color = NaviPayColor.textPrimary, + ) + ) { + append(stringResource(R.string.np_converted_amount_with_colon)) + } + withStyle( + style = + SpanStyle( + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + fontSize = 14.sp, + color = NaviPayColor.textPrimary, + ) + ) { + append(stringResource(id = R.string.rupee_symbol_x, convertedAmount)) + } + } + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/utils/NaviPayPspManager.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/utils/NaviPayPspManager.kt index b482bb7442..3378452f5d 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/utils/NaviPayPspManager.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/utils/NaviPayPspManager.kt @@ -794,7 +794,7 @@ constructor( resourceProvider.getString(resId = R.string.np_activate_upi_id_to_add_balance_lite) NaviPayFlowType.LITE_AUTO_TOP_UP -> resourceProvider.getString(resId = R.string.np_activate_upi_id_to_set_auto_top_up) - NaviPayFlowType.UPI_INTERNATIONAL -> + NaviPayFlowType.UPI_GLOBAL -> resourceProvider.getString(resId = R.string.np_activate_upi_id_to_activate_global) NaviPayFlowType.EMI_CONVERSION -> resourceProvider.getString(resId = R.string.np_activate_upi_id_to_view_and_set_emi) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalAccountStatusRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalAccountStatusRequest.kt new file mode 100644 index 0000000000..d3d30b88f3 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalAccountStatusRequest.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName +import com.navi.pay.common.model.view.DeviceData + +data class GlobalAccountStatusRequest( + @SerializedName("deviceData") val deviceData: DeviceData, + @SerializedName("merchantCustomerId") val merchantCustomerId: String, + @SerializedName("baccId") val baccId: String, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalAccountStatusResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalAccountStatusResponse.kt new file mode 100644 index 0000000000..1dcb37828b --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalAccountStatusResponse.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName +import org.joda.time.DateTime + +data class GlobalAccountStatusResponse( + @SerializedName("status") val status: String, + @SerializedName("startDate") val startDate: DateTime?, + @SerializedName("endDate") val endDate: DateTime?, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalActivationRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalActivationRequest.kt new file mode 100644 index 0000000000..ddd9ac3585 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalActivationRequest.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName +import com.navi.pay.common.model.view.DeviceData + +data class GlobalActivationRequest( + @SerializedName("deviceData") val deviceData: DeviceData, + @SerializedName("merchantCustomerId") val merchantCustomerId: String, + @SerializedName("credBlock") val credBlock: String, + @SerializedName("baccId") val baccId: String, + @SerializedName("upiRequestId") val upiRequestId: String, + @SerializedName("endDate") val endDate: String? = null, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalActivationResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalActivationResponse.kt new file mode 100644 index 0000000000..067a9c68d0 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalActivationResponse.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName +import org.joda.time.DateTime + +data class GlobalActivationResponse( + @SerializedName("status") val status: String, + @SerializedName("startDate") val startDate: DateTime?, + @SerializedName("endDate") val endDate: DateTime?, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalDeactivationRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalDeactivationRequest.kt new file mode 100644 index 0000000000..c56c404c0e --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalDeactivationRequest.kt @@ -0,0 +1,19 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName +import com.navi.pay.common.model.view.DeviceData + +data class GlobalDeactivationRequest( + @SerializedName("deviceData") val deviceData: DeviceData, + @SerializedName("merchantCustomerId") val merchantCustomerId: String, + @SerializedName("credBlock") val credBlock: String, + @SerializedName("baccId") val baccId: String, + @SerializedName("upiRequestId") val upiRequestId: String, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalDeactivationResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalDeactivationResponse.kt new file mode 100644 index 0000000000..c73a6aebcb --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/GlobalDeactivationResponse.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName + +data class GlobalDeactivationResponse(@SerializedName("status") val status: String) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/ValidateGlobalQrRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/ValidateGlobalQrRequest.kt new file mode 100644 index 0000000000..af41410e54 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/ValidateGlobalQrRequest.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName +import com.navi.pay.common.model.view.DeviceData + +data class ValidateGlobalQrRequest( + @SerializedName("deviceData") val deviceData: DeviceData, + @SerializedName("merchantCustomerId") val merchantCustomerId: String, + @SerializedName("initiationMode") val initiationMode: String, + @SerializedName("qrPayLoad") val qrPayLoad: String, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/ValidateGlobalQrResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/ValidateGlobalQrResponse.kt new file mode 100644 index 0000000000..62e9eff195 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/network/ValidateGlobalQrResponse.kt @@ -0,0 +1,31 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.network + +import com.google.gson.annotations.SerializedName + +data class ValidateGlobalQrResponse( + @SerializedName("upiRequestId") val upiRequestId: String, + @SerializedName("paymentDetails") val paymentDetails: PaymentDetails, +) + +data class PaymentDetails( + @SerializedName("payeeName") val payeeName: String, + @SerializedName("payeeMcc") val payeeMcc: String, + @SerializedName("payeeType") val payeeType: String, + @SerializedName("payeeVpa") val payeeVpa: String, + @SerializedName("forexList") val forexList: List, +) + +data class ForexDetail( + @SerializedName("forex") val forex: String, + @SerializedName("markUp") val markUp: String, + @SerializedName("baseAmount") val baseAmount: String, + @SerializedName("baseCurr") val baseCurr: String, + @SerializedName("convertedAmount") val convertedAmount: String, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/ToggleState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/ToggleState.kt new file mode 100644 index 0000000000..45f011d8fc --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/ToggleState.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.view + +import org.joda.time.DateTime + +sealed class ToggleState { + data class On(val startDate: DateTime, val endDate: DateTime) : ToggleState() + + data object Off : ToggleState() + + data object Loading : ToggleState() + + data object Disabled : ToggleState() + + data object ActivationPending : ToggleState() + + data object DeactivationPending : ToggleState() + + data object PinNotSet : ToggleState() +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalAccountStatus.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalAccountStatus.kt new file mode 100644 index 0000000000..69a44e885e --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalAccountStatus.kt @@ -0,0 +1,33 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.view + +import com.navi.base.utils.TrustedTimeAccessor +import org.joda.time.DateTime + +enum class UpiGlobalAccountStatus { + INIT_ACTIVE, + ACTIVE, + INIT_DEACTIVE, + DEACTIVE; + + companion object { + fun toUpiGlobalAccountStatus(status: String): UpiGlobalAccountStatus { + return entries.singleOrNull { it.name == status } ?: INIT_ACTIVE + } + + fun isStatusPending(status: UpiGlobalAccountStatus): Boolean { + return status == INIT_DEACTIVE || status == INIT_ACTIVE + } + + fun isStatusActive(status: UpiGlobalAccountStatus, activeDateTime: DateTime): Boolean { + return status == ACTIVE && + TrustedTimeAccessor.getCurrentTimeMillis() < activeDateTime.millis + } + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalMainScreenSource.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalMainScreenSource.kt new file mode 100644 index 0000000000..460ea3abde --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalMainScreenSource.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.view + +import android.os.Parcelable +import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity +import kotlinx.parcelize.Parcelize + +sealed class UpiGlobalMainScreenSource : Parcelable { + @Parcelize + data class QrScannerScreen(val qrContent: String, val payeeEntity: PayeeEntity) : + UpiGlobalMainScreenSource() + + @Parcelize data object Profile : UpiGlobalMainScreenSource() +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenBottomSheetStateHolder.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenBottomSheetStateHolder.kt new file mode 100644 index 0000000000..4159badd03 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenBottomSheetStateHolder.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.view + +data class UpiGlobalScreenBottomSheetStateHolder( + val showBottomSheet: Boolean, + val bottomSheetStateChange: Boolean, + val bottomSheetUIState: UpiGlobalScreenBottomSheetUiState, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenBottomSheetUIState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenBottomSheetUIState.kt new file mode 100644 index 0000000000..a23d0ad949 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenBottomSheetUIState.kt @@ -0,0 +1,29 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.view + +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity + +sealed class UpiGlobalScreenBottomSheetUiState { + data class UpiGlobalDateSelection( + val linkedAccountIndex: Int, + val selectedBankAccount: LinkedAccountEntity?, + ) : UpiGlobalScreenBottomSheetUiState() + + data class UpiGlobalStatusSuccess(val title: String) : UpiGlobalScreenBottomSheetUiState() + + data class UpiGlobalStatusPending(val descriptionId: Int) : UpiGlobalScreenBottomSheetUiState() + + data class UpiGlobalStatusActivationFailed(val descriptionId: Int) : + UpiGlobalScreenBottomSheetUiState() + + data class Loading(val title: String) : UpiGlobalScreenBottomSheetUiState() + + data class UpiGlobalDeactivationConfirmation(val linkedAccountIndex: Int) : + UpiGlobalScreenBottomSheetUiState() +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenUIState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenUIState.kt new file mode 100644 index 0000000000..6abe157fe8 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalScreenUIState.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.view + +enum class UpiGlobalScreenUiState { + Loading, + Loaded, + Empty, +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalSendMoneyScreenSource.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalSendMoneyScreenSource.kt new file mode 100644 index 0000000000..bd14b817d8 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/models/view/UpiGlobalSendMoneyScreenSource.kt @@ -0,0 +1,24 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.models.view + +enum class UpiGlobalSendMoneyScreenSource { + QrScannerScreen, + UpiGlobalScreen; + + companion object { + fun getUpiGlobalSendMoneyScreenSource( + type: String, + defaultType: UpiGlobalSendMoneyScreenSource = QrScannerScreen, + ): UpiGlobalSendMoneyScreenSource { + return UpiGlobalSendMoneyScreenSource.entries.singleOrNull { + it.name.equals(type, ignoreCase = true) + } ?: defaultType + } + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/repository/UpiGlobalRepository.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/repository/UpiGlobalRepository.kt new file mode 100644 index 0000000000..cb0f43b259 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/repository/UpiGlobalRepository.kt @@ -0,0 +1,70 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.repository + +import com.navi.common.checkmate.model.MetricInfo +import com.navi.common.network.models.RepoResult +import com.navi.common.network.retrofit.ResponseCallback +import com.navi.pay.management.global.models.network.GlobalAccountStatusRequest +import com.navi.pay.management.global.models.network.GlobalAccountStatusResponse +import com.navi.pay.management.global.models.network.GlobalActivationRequest +import com.navi.pay.management.global.models.network.GlobalActivationResponse +import com.navi.pay.management.global.models.network.GlobalDeactivationRequest +import com.navi.pay.management.global.models.network.GlobalDeactivationResponse +import com.navi.pay.management.global.models.network.ValidateGlobalQrRequest +import com.navi.pay.management.global.models.network.ValidateGlobalQrResponse +import com.navi.pay.network.retrofit.NaviPayRetrofitService +import javax.inject.Inject + +class UpiGlobalRepository +@Inject +constructor(private val naviPayRetrofitService: NaviPayRetrofitService) : ResponseCallback() { + suspend fun activateGlobalAccount( + globalActivationRequest: GlobalActivationRequest, + metricInfo: MetricInfo>, + ) = + apiResponseCallback( + naviPayRetrofitService.activateInternationalAccount( + globalActivationRequest = globalActivationRequest + ), + metricInfo = metricInfo, + ) + + suspend fun deactivateGlobalAccount( + globalDeactivationRequest: GlobalDeactivationRequest, + metricInfo: MetricInfo>, + ) = + apiResponseCallback( + naviPayRetrofitService.deactivateInternationalAccount( + globalDeactivationRequest = globalDeactivationRequest + ), + metricInfo = metricInfo, + ) + + suspend fun globalAccountStatusCheck( + globalAccountStatusRequest: GlobalAccountStatusRequest, + metricInfo: MetricInfo>, + ) = + apiResponseCallback( + naviPayRetrofitService.internationalAccountStatusCheck( + globalAccountStatusRequest = globalAccountStatusRequest + ), + metricInfo = metricInfo, + ) + + suspend fun validateGlobalQr( + validateGlobalQrRequest: ValidateGlobalQrRequest, + metricInfo: MetricInfo>, + ) = + apiResponseCallback( + naviPayRetrofitService.validateInternationalQr( + validateGlobalQrRequest = validateGlobalQrRequest + ), + metricInfo = metricInfo, + ) +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/ui/UpiGlobalScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/ui/UpiGlobalScreen.kt new file mode 100644 index 0000000000..83ffd6700b --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/ui/UpiGlobalScreen.kt @@ -0,0 +1,783 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.Scaffold +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.utils.DateUtils +import com.navi.base.utils.SPACE +import com.navi.common.R as CommonR +import com.navi.common.utils.navigateUp +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.design.snackbar.SnackBarConfig +import com.navi.design.snackbar.SuccessSnackBar +import com.navi.naviwidgets.extensions.NaviText +import com.navi.naviwidgets.utils.EMPTY +import com.navi.pay.R +import com.navi.pay.common.setup.NaviPayRouter +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.ui.BottomSheetContentWithDateSelectionCalendarAndRadioButtons +import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader +import com.navi.pay.common.ui.DeactivateUpiGlobalView +import com.navi.pay.common.ui.ImageWithCircularBackground +import com.navi.pay.common.ui.LoadingBottomSheetView +import com.navi.pay.common.ui.LoadingScreen +import com.navi.pay.common.ui.NaviPayCard +import com.navi.pay.common.ui.NaviPayHeader +import com.navi.pay.common.ui.NaviPayModalBottomSheet +import com.navi.pay.common.ui.NaviPaySponsorView +import com.navi.pay.common.ui.SuccessBottomSheetWithIconHeader +import com.navi.pay.common.ui.ThemeRoundedButton +import com.navi.pay.common.utils.ErrorEventHandler +import com.navi.pay.common.utils.NaviPayEventBus +import com.navi.pay.entry.NaviPayActivity +import com.navi.pay.management.global.models.view.ToggleState +import com.navi.pay.management.global.models.view.UpiGlobalMainScreenSource +import com.navi.pay.management.global.models.view.UpiGlobalScreenBottomSheetUiState +import com.navi.pay.management.global.models.view.UpiGlobalScreenUiState +import com.navi.pay.management.global.viewmodel.UpiGlobalScreenContract +import com.navi.pay.management.global.viewmodel.UpiGlobalViewModel +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR +import com.navi.pay.utils.clearBackStackUpToAndNavigate +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.joda.time.DateTime + +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun UpiGlobalScreen( + naviPayActivity: NaviPayActivity, + navigator: DestinationsNavigator, + upiGlobalMainScreenSource: UpiGlobalMainScreenSource? = UpiGlobalMainScreenSource.Profile, + upiGlobalViewModel: UpiGlobalViewModel = hiltViewModel(), +) { + val state by upiGlobalViewModel.state.collectAsStateWithLifecycle() + + val eventDispatcher: (UpiGlobalScreenContract.Event) -> Unit = { event -> + upiGlobalViewModel.onEvent(event = event) + } + + val scope = rememberCoroutineScope() + + val bottomSheetState = + rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { state.bottomSheetStateHolder.bottomSheetStateChange }, + ) + + val onBackClick = { eventDispatcher(UpiGlobalScreenContract.Event.OnBackClicked) } + + BackHandler { + if ( + state.bottomSheetStateHolder.bottomSheetUIState is + UpiGlobalScreenBottomSheetUiState.Loading && + state.bottomSheetStateHolder.showBottomSheet + ) { + // Back press is disabled during Loading state - (no-op) + } else if (state.bottomSheetStateHolder.showBottomSheet) { + eventDispatcher( + UpiGlobalScreenContract.Event.OnBottomSheetStateChanged(showBottomSheet = false) + ) + } else { + onBackClick() + } + } + + val onDismissBottomSheet: () -> Unit = { + scope + .launch { bottomSheetState.hide() } + .invokeOnCompletion { + if ( + !bottomSheetState.isVisible || + !state.bottomSheetStateHolder.bottomSheetStateChange + ) { + eventDispatcher( + UpiGlobalScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + } + } + } + + LaunchedEffect(Unit) { + ErrorEventHandler.errorCtaClickEvent.collectLatest { NaviPayEventBus.resetEventBus() } + } + + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(key1 = lifecycleOwner, key2 = state) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME, + Lifecycle.Event.ON_CREATE -> { + naviPayActivity.window.statusBarColor = + ContextCompat.getColor( + naviPayActivity, + if (state.upiGlobalScreenUIState == UpiGlobalScreenUiState.Loaded) + R.color.navi_pay_status_bar_green + else R.color.navi_pay_status_bar_default_color, + ) + } + else -> Unit + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + naviPayActivity.window.statusBarColor = + ContextCompat.getColor(naviPayActivity, R.color.navi_pay_status_bar_default_color) + } + } + + LaunchedEffect(key1 = upiGlobalViewModel.effect) { + upiGlobalViewModel.effect.collectLatest { + when (it) { + is UpiGlobalScreenContract.Effect.Navigate -> { + if (it.shouldClearBackStack) { + navigator.clearBackStackUpToAndNavigate( + destination = it.direction, + popUpTo = it.popUpToDestination, + inclusive = false, + ) + } else { + navigator.navigate(it.direction) + } + } + is UpiGlobalScreenContract.Effect.NavigateToHelpScreen -> { + NaviPayRouter.onCtaClick( + naviPayActivity = naviPayActivity, + ctaData = it.ctaData, + ) + } + UpiGlobalScreenContract.Effect.GoBack -> { + navigator.navigateUp(naviPayActivity) + } + } + } + } + + LaunchedEffect(state.isGlobalHeaderVisible) { + naviPayActivity.window.statusBarColor = + ContextCompat.getColor( + naviPayActivity, + if (state.isGlobalHeaderVisible) R.color.navi_pay_status_bar_default_color + else R.color.navi_pay_status_bar_green, + ) + } + + if (state.bottomSheetStateHolder.showBottomSheet) { + NaviPayModalBottomSheet( + modifier = Modifier.fillMaxWidth(), + bottomSheetState = bottomSheetState, + onDismissRequest = onDismissBottomSheet, + shouldDismissOnBackPress = true, + bottomSheetContent = { + RenderUpiGlobalScreenBottomSheet(state = state, eventDispatcher = eventDispatcher) + }, + ) + } + + when (state.upiGlobalScreenUIState) { + UpiGlobalScreenUiState.Loading -> { + LoadingScreen() + } + UpiGlobalScreenUiState.Loaded -> { + UPIGlobalMainScreen( + state = state, + eventDispatcher = eventDispatcher, + onBackClick = onBackClick, + ) + } + UpiGlobalScreenUiState.Empty -> { + eventDispatcher(UpiGlobalScreenContract.Event.NoAccountLinked) + UPIGlobalEmptyScreen( + state = state, + eventDispatcher = eventDispatcher, + onBackClick = onBackClick, + ) + } + } +} + +@Composable +fun RenderUpiGlobalScreenBottomSheet( + state: UpiGlobalScreenContract.State, + eventDispatcher: (UpiGlobalScreenContract.Event) -> Unit, +) { + when (state.bottomSheetStateHolder.bottomSheetUIState) { + is UpiGlobalScreenBottomSheetUiState.UpiGlobalDateSelection -> { + BottomSheetContentWithDateSelectionCalendarAndRadioButtons( + bankName = + state.bottomSheetStateHolder.bottomSheetUIState.selectedBankAccount + ?.bankNameTruncatedWithAccountNumber + .orEmpty(), + updateDate = { + eventDispatcher( + UpiGlobalScreenContract.Event.OnUpiGlobalEndDateSelected( + selectedUpiGlobalEndDate = it + ) + ) + }, + onPrimaryButtonClicked = { + eventDispatcher( + UpiGlobalScreenContract.Event.OnActivateUpiGlobalClicked( + linkedAccountIndex = + state.bottomSheetStateHolder.bottomSheetUIState.linkedAccountIndex + ) + ) + }, + showCtaLoader = state.isActivationBottomSheetLoaderRunning, + deactivationDate = state.selectedUpiGlobalEndDate, + ) + } + is UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusPending -> { + BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( + iconId = CommonR.drawable.ic_info_solid_blue, + iconSize = 24.dp, + header = stringResource(id = R.string.np_your_request_is_under_process), + description = + stringResource( + id = state.bottomSheetStateHolder.bottomSheetUIState.descriptionId + ), + primaryButton = stringResource(id = R.string.np_okay_got_it), + onPrimaryButtonClicked = { + eventDispatcher( + UpiGlobalScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + }, + onSecondaryButtonClicked = {}, + secondaryButton = null, + ) + } + is UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusActivationFailed -> { + BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( + iconId = CommonR.drawable.ic_exclamation_red_border, + iconSize = 24.dp, + header = stringResource(id = R.string.np_activate_upi_global_failed), + description = + stringResource( + id = state.bottomSheetStateHolder.bottomSheetUIState.descriptionId + ), + primaryButton = stringResource(id = R.string.np_okay_got_it), + onPrimaryButtonClicked = { + eventDispatcher( + UpiGlobalScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + }, + onSecondaryButtonClicked = {}, + secondaryButton = null, + ) + } + is UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusSuccess -> { + SuccessBottomSheetWithIconHeader( + headerText = state.bottomSheetStateHolder.bottomSheetUIState.title + ) + } + is UpiGlobalScreenBottomSheetUiState.Loading -> { + LoadingBottomSheetView(text = state.bottomSheetStateHolder.bottomSheetUIState.title) + } + is UpiGlobalScreenBottomSheetUiState.UpiGlobalDeactivationConfirmation -> { + DeactivateUpiGlobalView( + onPrimaryButtonClicked = { + eventDispatcher( + UpiGlobalScreenContract.Event.OnDeactivateUpiGlobalClicked( + linkedAccountIndex = + state.bottomSheetStateHolder.bottomSheetUIState.linkedAccountIndex + ) + ) + }, + onSecondaryButtonClicked = { + eventDispatcher( + UpiGlobalScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + }, + showCtaLoader = state.isDeactivationBottomSheetLoaderRunning, + ) + } + } +} + +@Composable +fun UPIGlobalEmptyScreen( + state: UpiGlobalScreenContract.State, + eventDispatcher: (UpiGlobalScreenContract.Event) -> Unit, + onBackClick: () -> Unit, +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { paddingValues -> + Column(modifier = Modifier.fillMaxSize().padding(paddingValues)) { + NaviPayHeader( + title = stringResource(R.string.np_upi_global), + onNavigationIconClick = onBackClick, + actionIconText = stringResource(R.string.help), + onActionClick = { + eventDispatcher(UpiGlobalScreenContract.Event.OnHelpCtaClicked) + }, + modifier = Modifier.fillMaxWidth(), + ) + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_np_global_icon_big), + contentDescription = EMPTY, + ) + + NaviText( + text = stringResource(R.string.np_no_bank_accounts_added), + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textSecondary, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + NaviText( + text = stringResource(R.string.np_add_bank_account), + fontSize = 14.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.textTertiary, + textAlign = TextAlign.Center, + lineHeight = 22.sp, + ) + } + } + }, + bottomBar = { UpiGlobalBottomBar(state = state, eventDispatcher = eventDispatcher) }, + ) +} + +@Composable +fun UPIGlobalMainScreen( + state: UpiGlobalScreenContract.State, + eventDispatcher: (UpiGlobalScreenContract.Event) -> Unit, + onBackClick: () -> Unit, +) { + + val scrollState = rememberScrollState() + var headerHeight by remember { mutableIntStateOf(0) } + val headerScrolledToTop by remember { derivedStateOf { scrollState.value > headerHeight } } + + LaunchedEffect(headerScrolledToTop) { + eventDispatcher(UpiGlobalScreenContract.Event.OnScroll(headerScrolledToTop)) + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + NaviPayHeader( + title = + if (headerScrolledToTop) { + stringResource(R.string.np_upi_global) + } else { + "" + }, + onNavigationIconClick = onBackClick, + actionIconText = stringResource(R.string.help), + onActionClick = { eventDispatcher(UpiGlobalScreenContract.Event.OnHelpCtaClicked) }, + backgroundColor = + if (headerScrolledToTop) { + NaviPayColor.ctaWhite + } else { + NaviPayColor.brandSupportingLightGreen + }, + modifier = Modifier.fillMaxWidth(), + ) + }, + content = { paddingValues -> + Column( + modifier = + Modifier.fillMaxSize() + .padding(paddingValues) + .verticalScroll(scrollState) + .background(NaviPayColor.bgDefault) + ) { + UpiGlobalHeader { measuredHeight -> headerHeight = measuredHeight } + + Spacer(modifier = Modifier.height(24.dp)) + + state.linkedAccounts.forEachIndexed { index, linkedAccount -> + AccountCard( + account = linkedAccount, + index = index, + eventDispatcher = eventDispatcher, + toggleState = state.toggleStateList[index], + ) + if (index != state.linkedAccounts.size - 1) + Spacer(modifier = Modifier.height(16.dp)) + } + } + }, + bottomBar = { UpiGlobalBottomBar(state = state, eventDispatcher = eventDispatcher) }, + snackbarHost = { + if (state.snackBarState.isVisible) { + SuccessSnackBar( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 32.dp), + show = true, + snackBarConfig = + SnackBarConfig( + leadingIconResId = R.drawable.navi_pay_ic_checked_circle_green, + title = stringResource(R.string.np_activate_upi_global_now), + ), + ) + } + }, + ) +} + +@Composable +fun UpiGlobalHeader(onHeightMeasured: (Int) -> Unit) { + Row( + modifier = + Modifier.fillMaxWidth() + .background(color = NaviPayColor.brandSupportingLightGreen) + .padding(start = 16.dp) + .onGloballyPositioned { layoutCoordinates -> + onHeightMeasured(layoutCoordinates.size.height) + } + ) { + Column(modifier = Modifier.weight(0.6f)) { + NaviText( + text = stringResource(R.string.np_upi_global), + fontSize = 24.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + ) + Spacer(modifier = Modifier.height(6.dp)) + NaviText( + text = stringResource(R.string.np_manage_international_payment), + fontSize = 12.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.textCopySecondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + lineHeight = 18.sp, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Image( + painter = painterResource(id = R.drawable.ic_np_global), + modifier = Modifier.weight(0.4f), + contentDescription = EMPTY, + ) + } +} + +@Composable +fun AccountCard( + account: LinkedAccountEntity, + index: Int, + eventDispatcher: (UpiGlobalScreenContract.Event) -> Unit, + toggleState: ToggleState, +) { + NaviPayCard( + modifier = + Modifier.fillMaxWidth() + .background(color = NaviPayColor.bgDefault, shape = RoundedCornerShape(size = 4.dp)) + .padding(horizontal = 16.dp), + borderStroke = BorderStroke(1.dp, color = NaviPayColor.borderAlt), + ambientColor = NaviPayColor.bgDefault, + spotColor = NaviPayColor.bgDefault, + backgroundColor = NaviPayColor.bgDefault, + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ImageWithCircularBackground( + boxSize = 40.dp, + imageUrl = account.bankIconImageUrl, + imageSize = 28.dp, + ) + + Spacer(modifier = Modifier.width(14.dp)) + + NaviText( + text = account.bankNameAccountNumber, + fontSize = 14.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + color = getBankTextColor(toggleState = toggleState), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + + Spacer(modifier = Modifier.width(16.dp)) + + SwitchView( + toggleState = toggleState, + eventDispatcher = eventDispatcher, + index = index, + ) + } + GlobalActivationStatusView(toggleState = toggleState) + } + } +} + +@Composable +private fun SwitchView( + toggleState: ToggleState, + eventDispatcher: (UpiGlobalScreenContract.Event) -> Unit, + index: Int, +) { + when (toggleState) { + ToggleState.Disabled, + ToggleState.ActivationPending, + ToggleState.DeactivationPending, + ToggleState.Loading -> { + // Nothing to show + } + ToggleState.Off, + is ToggleState.On, + ToggleState.PinNotSet -> { + Switch( + modifier = Modifier.width(48.dp).height(28.dp), + checked = toggleState is ToggleState.On, + onCheckedChange = { + eventDispatcher( + UpiGlobalScreenContract.Event.OnToggleClicked( + index = index, + allowBottomSheetStateChange = true, + ) + ) + }, + colors = + SwitchDefaults.colors( + checkedThumbColor = NaviPayColor.bgDefault, + checkedTrackColor = NaviPayColor.onSurfaceHighlight, + uncheckedThumbColor = NaviPayColor.bgDefault, + uncheckedTrackColor = NaviPayColor.bgAlt, + uncheckedBorderColor = Color.Transparent, + ), + thumbContent = { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + }, + ) + } + } +} + +@Composable +private fun GlobalActivationStatusView(toggleState: ToggleState) { + if ( + toggleState is ToggleState.On || + toggleState == ToggleState.ActivationPending || + toggleState == ToggleState.DeactivationPending || + toggleState == ToggleState.Disabled || + toggleState == ToggleState.PinNotSet + ) { + Row( + modifier = + Modifier.fillMaxWidth() + .background(color = getAccountCardColor(toggleState = toggleState)) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.ic_np_exclamation_dark_grey), + contentDescription = EMPTY, + modifier = Modifier.size(16.dp), + ) + + Spacer(modifier = Modifier.width(12.dp)) + + if (toggleState is ToggleState.On) { + NaviText(text = getActiveCardInfoText(endDate = toggleState.endDate)) + } else { + NaviText( + text = getAccountCardInfoText(toggleState = toggleState), + fontSize = 12.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.textTertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +fun getActiveCardInfoText(endDate: DateTime): AnnotatedString { + val descriptionString = buildAnnotatedString { + withStyle( + style = + SpanStyle( + fontSize = 12.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.textTertiary, + ) + ) { + append(stringResource(R.string.np_upi_global_active_till)) + } + append(SPACE) + val dateTime = + DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = endDate, + format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, + ) + withStyle( + style = + SpanStyle( + fontSize = 12.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + color = NaviPayColor.textTertiary, + ) + ) { + append(dateTime) + } + } + return descriptionString +} + +@Composable +fun UpiGlobalBottomBar( + state: UpiGlobalScreenContract.State, + eventDispatcher: (UpiGlobalScreenContract.Event) -> Unit, +) { + Column( + modifier = + Modifier.fillMaxWidth().padding(horizontal = 16.dp).background(NaviPayColor.bgDefault), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + + if (state.isSponsorVisible) { + NaviPaySponsorView(modifier = Modifier.fillMaxWidth()) + } + + Spacer(modifier = Modifier.height(12.dp)) + + ThemeRoundedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.add_bank_account), + onClick = { eventDispatcher(UpiGlobalScreenContract.Event.OnAddAccountClicked) }, + ) + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun getAccountCardInfoText(toggleState: ToggleState) = + when (toggleState) { + ToggleState.ActivationPending -> stringResource(R.string.np_upi_global_activation_progress) + ToggleState.DeactivationPending -> + stringResource(R.string.np_upi_global_deactivation_progress) + ToggleState.PinNotSet -> stringResource(R.string.np_upi_pin_not_set) + else -> stringResource(R.string.np_upi_global_not_eligible) + } + +@Composable +private fun getAccountCardColor(toggleState: ToggleState) = + when (toggleState) { + is ToggleState.On -> NaviPayColor.brandSupportingLightGreen + ToggleState.ActivationPending, + ToggleState.DeactivationPending -> NaviPayColor.bgNotice + ToggleState.PinNotSet -> NaviPayColor.bgAlt + else -> NaviPayColor.bgAlt + } + +@Composable +private fun getBankTextColor(toggleState: ToggleState): Color { + return if ( + !(toggleState is ToggleState.ActivationPending || + toggleState is ToggleState.DeactivationPending || + toggleState is ToggleState.On || + toggleState is ToggleState.Off || + toggleState is ToggleState.PinNotSet) + ) { + NaviPayColor.inputFieldDefault + } else { + NaviPayColor.inputFieldFilled + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/viewmodel/UpiGlobalViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/viewmodel/UpiGlobalViewModel.kt new file mode 100644 index 0000000000..bfcd832dc8 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/global/viewmodel/UpiGlobalViewModel.kt @@ -0,0 +1,1001 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.global.viewmodel + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.navi.base.model.CtaData +import com.navi.base.utils.DateUtils +import com.navi.base.utils.ResourceProvider +import com.navi.common.network.models.RepoResult +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.NaviApiPoller +import com.navi.pay.R +import com.navi.pay.analytics.NaviPayAnalytics +import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_UPI_GLOBAL +import com.navi.pay.common.model.view.NaviPayFlowType +import com.navi.pay.common.model.view.NaviPayScreenType +import com.navi.pay.common.model.view.PspType +import com.navi.pay.common.model.view.SnackBarState +import com.navi.pay.common.usecase.ActivateUpiGlobalUseCase +import com.navi.pay.common.usecase.DeactivateUpiGlobalUseCase +import com.navi.pay.common.usecase.LinkedAccountsUseCase +import com.navi.pay.common.usecase.RefreshLinkedAccountsUseCase +import com.navi.pay.common.utils.DeviceInfoProvider +import com.navi.pay.common.utils.NaviPayCommonUtils.getHelpCtaData +import com.navi.pay.common.utils.getDayEndDateTime +import com.navi.pay.common.utils.getMetricInfo +import com.navi.pay.common.viewmodel.NaviPayBaseVM +import com.navi.pay.common.viewmodel.NaviPayViewModelContract +import com.navi.pay.destinations.DirectionDestination +import com.navi.pay.destinations.LinkedAccountVerifyScreenDestination +import com.navi.pay.destinations.NaviPayLauncherScreenDestination +import com.navi.pay.destinations.QrScannerScreenDestination +import com.navi.pay.destinations.UPIGlobalSendMoneyScreenDestination +import com.navi.pay.entry.NaviPayActivityDataProvider +import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity +import com.navi.pay.management.common.utils.NaviPayPspManager +import com.navi.pay.management.global.models.network.GlobalAccountStatusRequest +import com.navi.pay.management.global.models.network.GlobalAccountStatusResponse +import com.navi.pay.management.global.models.network.GlobalActivationRequest +import com.navi.pay.management.global.models.network.GlobalDeactivationRequest +import com.navi.pay.management.global.models.view.ToggleState +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus.Companion.isStatusPending +import com.navi.pay.management.global.models.view.UpiGlobalMainScreenSource +import com.navi.pay.management.global.models.view.UpiGlobalScreenBottomSheetStateHolder +import com.navi.pay.management.global.models.view.UpiGlobalScreenBottomSheetUiState +import com.navi.pay.management.global.models.view.UpiGlobalScreenUiState +import com.navi.pay.management.global.models.view.UpiGlobalSendMoneyScreenSource +import com.navi.pay.management.global.repository.UpiGlobalRepository +import com.navi.pay.npcicl.NpciResult +import com.navi.pay.onboarding.account.add.model.view.AccountType +import com.navi.pay.onboarding.account.add.repository.BankRepository +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.navi.pay.onboarding.binding.model.view.NaviPayCustomerOnboardingEntity +import com.navi.pay.utils.ACTION_PIN_SET +import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR +import com.navi.pay.utils.SAVINGS_ONLY_ENABLED_ACCOUNTS +import com.ramcosta.composedestinations.spec.Direction +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.joda.time.DateTime + +@HiltViewModel +class UpiGlobalViewModel +@Inject +constructor( + savedStateHandle: SavedStateHandle, + private val deviceInfoProvider: DeviceInfoProvider, + private val linkedAccountsUseCase: LinkedAccountsUseCase, + private val upiGlobalRepository: UpiGlobalRepository, + private val refreshLinkedAccountsUseCase: RefreshLinkedAccountsUseCase, + private val bankRepository: BankRepository, + private val resourceProvider: ResourceProvider, + private val naviPayPspManager: NaviPayPspManager, + private val activateUpiGlobalUseCase: ActivateUpiGlobalUseCase, + private val deactivateUpiGlobalUseCase: DeactivateUpiGlobalUseCase, + private val naviPayActivityDataProvider: NaviPayActivityDataProvider, +) : NaviPayBaseVM(), UpiGlobalScreenContract { + companion object { + private val POLL_INTERVAL = 3.seconds + private const val POLL_MAX_ITERATIONS = 3 + } + + private val _state = MutableStateFlow(UpiGlobalScreenContract.State()) + override val state: StateFlow = _state.asStateFlow() + + private val _effect = MutableSharedFlow() + override val effect: SharedFlow = _effect.asSharedFlow() + + private val upiUPIGlobalMainScreenSource: UpiGlobalMainScreenSource = + savedStateHandle["upiGlobalMainScreenSource"] + ?: naviPayActivityDataProvider + .getIntentData() + ?.getParcelable("upiGlobalMainScreenSource")!! + + private val helpCtaData = getHelpCtaData(NaviPayScreenType.NAVI_PAY_GLOBAL_MAIN_SCREEN.name) + + private val naviApiPoller by lazy { + NaviApiPoller(repeatInterval = POLL_INTERVAL, numberOfIterations = POLL_MAX_ITERATIONS) + } + + private val naviPayAnalytics: NaviPayAnalytics.NaviPayUpiGlobal = + NaviPayAnalytics.INSTANCE.NaviPayUpiGlobal() + + init { + onLand() + linkedAccountsChangeListener() + } + + private fun onLand() { + naviPayAnalytics.onUpiGlobalLanded(source = upiUPIGlobalMainScreenSource) + } + + override fun onEvent(event: UpiGlobalScreenContract.Event) { + when (event) { + is UpiGlobalScreenContract.Event.OnToggleClicked -> { + onToggleClicked( + index = event.index, + allowBottomSheetStateChange = event.allowBottomSheetStateChange, + ) + } + is UpiGlobalScreenContract.Event.OnBottomSheetStateChanged -> { + updateBottomSheetUIState( + showBottomSheet = event.showBottomSheet, + allowBottomSheetStateChange = event.bottomSheetStateChange, + bottomSheetUIState = event.bottomSheetUIState, + ) + _state.update { it.copy(isActivationBottomSheetLoaderRunning = false) } + } + is UpiGlobalScreenContract.Event.OnUpiGlobalEndDateSelected -> { + onUpiGlobalEndDateSelected( + selectedUpiGlobalEndDate = event.selectedUpiGlobalEndDate + ) + } + is UpiGlobalScreenContract.Event.OnActivateUpiGlobalClicked -> { + onActivateUpiGlobalClicked(linkedAccountIndex = event.linkedAccountIndex) + } + is UpiGlobalScreenContract.Event.OnDeactivateUpiGlobalClicked -> { + onDeactivateUpiGlobalClicked(linkedAccountIndex = event.linkedAccountIndex) + } + UpiGlobalScreenContract.Event.OnHelpCtaClicked -> { + onHelpClicked() + } + UpiGlobalScreenContract.Event.OnBackClicked -> { + onBackClicked() + } + is UpiGlobalScreenContract.Event.OnScroll -> { + _state.update { it.copy(isGlobalHeaderVisible = event.isGlobalHeaderVisible) } + } + UpiGlobalScreenContract.Event.NoAccountLinked -> { + _state.update { it.copy(isSponsorVisible = false) } + } + UpiGlobalScreenContract.Event.OnAddAccountClicked -> { + onAddBankAccountClicked() + } + } + } + + private fun onActivateUpiGlobalClicked(linkedAccountIndex: Int) { + if (state.value.selectedUpiGlobalEndDate == null) return + val selectedBankAccount = state.value.linkedAccounts[linkedAccountIndex] + viewModelScope.launch(Dispatchers.IO) { + activateUpiGlobalUseCase.execute( + screenName = screenName, + selectedBankAccount = selectedBankAccount, + onOnboardingSuccess = { + showRedirectingBottomSheet( + resourceProvider.getString(R.string.np_redirect_upi_global_activation) + ) + }, + onOnboardingTriggered = { updateBottomSheetUIState(showBottomSheet = false) }, + onActivateUpiGlobalClicked = { + _state.update { it.copy(isActivationBottomSheetLoaderRunning = true) } + updateBottomSheetUIState(allowBottomSheetStateChange = false) + naviPayAnalytics.onActivateUpiGlobalClicked( + bankName = selectedBankAccount.bankName, + bankAccountUniqueId = selectedBankAccount.accountId, + endDate = selectedBankAccount.upiGlobalInfo?.endDate.toString(), + ) + }, + onNpciResult = { npciResult, upiRequestId, customerOnboardingEntity -> + when (npciResult) { + is NpciResult.Success -> { + updateBottomSheetUIState(showBottomSheet = false) + activateUpiGlobalPostCL( + upiRequestId = upiRequestId, + linkedAccountEntity = selectedBankAccount, + linkedAccountIndex = linkedAccountIndex, + credBlock = npciResult.data, + customerOnboardingEntity = customerOnboardingEntity, + ) + } + + is NpciResult.Error -> { + updateBottomSheetUIState(showBottomSheet = false) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = ToggleState.Off, + ) + _state.update { it.copy(isActivationBottomSheetLoaderRunning = false) } + handleNpciError(npciResult = npciResult) + } + + else -> Unit + } + }, + onNoInternetError = { notifyError(getNoInternetErrorConfig()) }, + onAirplaneModeError = { notifyError(getAirplaneModeOnErrorConfig()) }, + onSimFailureError = { isNoSimPresent -> + notifyError(getSimFailureErrorConfig(isNoSimPresent)) + }, + ) + } + } + + private fun onDeactivateUpiGlobalClicked(linkedAccountIndex: Int) { + viewModelScope.launch(Dispatchers.IO) { + val selectedBankAccount = state.value.linkedAccounts[linkedAccountIndex] + deactivateUpiGlobalUseCase.execute( + screenName = screenName, + selectedBankAccount = selectedBankAccount, + onOnboardingTriggered = { updateBottomSheetUIState(showBottomSheet = false) }, + onOnboardingSuccess = { + showRedirectingBottomSheet( + resourceProvider.getString(R.string.np_redirect_upi_global_deactivation) + ) + }, + onDeactivateUpiGlobalClicked = { + _state.update { it.copy(isDeactivationBottomSheetLoaderRunning = true) } + updateBottomSheetUIState(allowBottomSheetStateChange = false) + }, + onNpciResult = { npciResult, upiRequestId, customerOnboardingEntity -> + val currentStartDate = + state.value.linkedAccounts[linkedAccountIndex].upiGlobalInfo?.startDate + ?: DateTime.now() + val currentEndDate = + state.value.linkedAccounts[linkedAccountIndex].upiGlobalInfo?.endDate + ?: DateTime.now() + when (npciResult) { + is NpciResult.Success -> { + _state.update { + it.copy(isDeactivationBottomSheetLoaderRunning = false) + } + updateBottomSheetUIState(showBottomSheet = false) + deactivateUpiGlobalPostCL( + upiRequestId = upiRequestId, + linkedAccountEntity = selectedBankAccount, + linkedAccountIndex = linkedAccountIndex, + credBlock = npciResult.data, + currentStartDate = currentStartDate, + currentEndDate = currentEndDate, + customerOnboardingEntity = customerOnboardingEntity, + ) + } + + is NpciResult.Error -> { + _state.update { + it.copy(isDeactivationBottomSheetLoaderRunning = false) + } + updateBottomSheetUIState(showBottomSheet = false) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = + ToggleState.On( + startDate = currentStartDate, + endDate = currentEndDate, + ), + ) + handleNpciError(npciResult = npciResult) + } + else -> Unit + } + }, + onNoInternetError = { notifyError(getNoInternetErrorConfig()) }, + onAirplaneModeError = { notifyError(getAirplaneModeOnErrorConfig()) }, + onSimFailureError = { isNoSimPresent -> + notifyError(getSimFailureErrorConfig(isNoSimPresent)) + }, + ) + } + } + + private fun onToggleClicked(index: Int, allowBottomSheetStateChange: Boolean = false) { + + val toggleState = state.value.toggleStateList[index] + + naviPayAnalytics.onToggleClicked( + bankName = state.value.linkedAccounts[index].bankName, + bankAccountUniqueId = state.value.linkedAccounts[index].accountId, + ) + + when (toggleState) { + is ToggleState.On -> { + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = allowBottomSheetStateChange, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.UpiGlobalDeactivationConfirmation( + linkedAccountIndex = index + ), + ) + } + ToggleState.Off -> { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.UpiGlobalDateSelection( + linkedAccountIndex = index, + selectedBankAccount = state.value.linkedAccounts.getOrNull(index), + ), + allowBottomSheetStateChange = allowBottomSheetStateChange, + ) + } + ToggleState.PinNotSet -> { + handleSetPinClicked(index) + } + else -> {} + } + } + + private fun handleSetPinClicked(index: Int) { + viewModelScope.launch(Dispatchers.IO) { + naviPayPspManager.evaluateAndOnboardPspForFlow( + naviPayFlowType = NaviPayFlowType.SET_PIN, + vpaEntityList = state.value.linkedAccounts[index].vpaEntityList, + screenName = screenName, + onPspEvaluated = { pspEvaluationResult -> + if (pspEvaluationResult.onboardingDataEntity == null) { + return@evaluateAndOnboardPspForFlow + } + updateNavigationToNextScreen( + direction = + LinkedAccountVerifyScreenDestination( + accountId = state.value.linkedAccounts[index].accountId, + actionName = ACTION_PIN_SET, + pspType = pspEvaluationResult.onboardingDataEntity.pspType, + ) + ) + }, + ) + } + } + + private fun updateNavigationToNextScreen( + direction: Direction, + shouldClearBackStack: Boolean = false, + popUpToDestination: DirectionDestination = NaviPayLauncherScreenDestination, + ) { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit( + UpiGlobalScreenContract.Effect.Navigate( + direction = direction, + shouldClearBackStack = shouldClearBackStack, + popUpToDestination = popUpToDestination, + ) + ) + } + } + + private fun activateUpiGlobalPostCL( + upiRequestId: String, + linkedAccountEntity: LinkedAccountEntity, + linkedAccountIndex: Int, + credBlock: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) { + viewModelScope.launch(Dispatchers.IO) { + _state.update { it.copy(isActivationBottomSheetLoaderRunning = false) } + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = false, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.Loading( + title = + resourceProvider.getString( + R.string.np_upi_global_activation_description + ) + ), + ) + val globalActivationRequest = + GlobalActivationRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = customerOnboardingEntity.merchantCustomerId, + upiRequestId = upiRequestId, + credBlock = credBlock, + baccId = linkedAccountEntity.getBuidByPsp(psp = PspType.JUSPAY_AXIS).orEmpty(), + endDate = getDayEndDateTime(state.value.selectedUpiGlobalEndDate), + ) + val globalActivationResponse = + upiGlobalRepository.activateGlobalAccount( + globalActivationRequest = globalActivationRequest, + metricInfo = getMetricInfo(screenName = screenName), + ) + + if (globalActivationResponse.isSuccessWithData()) { + val globalActivationResponseData = globalActivationResponse.data!! + val startDate = globalActivationResponseData.startDate ?: DateTime.now() + val endDate = globalActivationResponseData.endDate ?: DateTime.now() + val status = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus( + globalActivationResponseData.status + ) + + updateLinkedAccountStatus() + + when (status) { + UpiGlobalAccountStatus.ACTIVE -> { + naviPayAnalytics.onAccountActivationSuccess( + bankName = linkedAccountEntity.bankName, + bankAccountUniqueId = linkedAccountEntity.accountId, + ) + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = true, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusSuccess( + title = + resourceProvider.getString( + R.string.np_upi_global_activated_successfully, + DateUtils + .getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = endDate, + format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, + ), + ) + ), + ) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = ToggleState.On(startDate = startDate, endDate = endDate), + ) + checkAndRedirectToSendMoneyScreenIfRequired() + // To show bottom sheet with activation details + delay(1000) + updateBottomSheetUIState(showBottomSheet = false) + } + UpiGlobalAccountStatus.INIT_ACTIVE -> { + naviPayAnalytics.onActivationPending() + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = true, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusPending( + descriptionId = R.string.np_upi_global_activation_pending + ), + ) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = ToggleState.ActivationPending, + ) + pollForStatus( + linkedAccount = linkedAccountEntity, + index = linkedAccountIndex, + ) + } + else -> Unit + } + } else { + naviPayAnalytics.onActivationFailed( + bankName = linkedAccountEntity.bankName, + bankAccountUniqueId = linkedAccountEntity.accountId, + errorCode = globalActivationResponse.errors?.get(0)?.code.orEmpty(), + errorMessage = globalActivationResponse.errors?.get(0)?.message.orEmpty(), + ) + updateBottomSheetUIState(showBottomSheet = false) + notifyError(response = globalActivationResponse) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = ToggleState.Off, + ) + } + } + } + + private suspend fun checkAndRedirectToSendMoneyScreenIfRequired() { + when (upiUPIGlobalMainScreenSource) { + is UpiGlobalMainScreenSource.QrScannerScreen -> { + redirectToSendMoneyScreenAfterActivation( + payeeEntity = upiUPIGlobalMainScreenSource.payeeEntity, + qrContent = upiUPIGlobalMainScreenSource.qrContent, + ) + } + UpiGlobalMainScreenSource.Profile -> Unit + } + } + + private suspend fun redirectToSendMoneyScreenAfterActivation( + payeeEntity: PayeeEntity, + qrContent: String, + ) { + // To show activation details before navigating to SM + delay(500) + + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = false, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.Loading( + title = resourceProvider.getString(R.string.np_redirecting_make_payment) + ), + ) + + // delay to avoid jerk while redirecting to SM + delay(500) + + updateNavigationToNextScreen( + direction = + UPIGlobalSendMoneyScreenDestination( + payeeEntity = payeeEntity, + qrContent = qrContent, + upiGlobalSendMoneyScreenSource = UpiGlobalSendMoneyScreenSource.UpiGlobalScreen, + ), + shouldClearBackStack = true, + popUpToDestination = QrScannerScreenDestination, + ) + } + + private suspend fun deactivateUpiGlobalPostCL( + upiRequestId: String, + linkedAccountEntity: LinkedAccountEntity, + linkedAccountIndex: Int, + credBlock: String, + currentStartDate: DateTime, + currentEndDate: DateTime, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) { + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = false, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.Loading( + title = + resourceProvider.getString(R.string.np_upi_global_deactivation_description) + ), + ) + val globalDeactivationRequest = + GlobalDeactivationRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = customerOnboardingEntity.merchantCustomerId, + upiRequestId = upiRequestId, + credBlock = credBlock, + baccId = linkedAccountEntity.getBuidByPsp(psp = PspType.JUSPAY_AXIS).orEmpty(), + ) + + val globalDeactivationResponse = + upiGlobalRepository.deactivateGlobalAccount( + globalDeactivationRequest = globalDeactivationRequest, + metricInfo = getMetricInfo(screenName = screenName), + ) + + if (globalDeactivationResponse.isSuccessWithData()) { + val globalDeactivationResponseData = globalDeactivationResponse.data!! + val status = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus( + globalDeactivationResponseData.status + ) + updateLinkedAccountStatus() + when (status) { + UpiGlobalAccountStatus.DEACTIVE -> { + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = true, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusSuccess( + title = + resourceProvider.getString( + R.string.np_upi_global_deactivated_successfully + ) + ), + ) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = ToggleState.Off, + ) + // To show deactivation success bottom sheet + delay(1000) + updateBottomSheetUIState(showBottomSheet = false) + } + UpiGlobalAccountStatus.INIT_DEACTIVE -> { + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = true, + bottomSheetUIState = + UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusPending( + descriptionId = R.string.np_upi_global_deactivation_pending + ), + ) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = ToggleState.DeactivationPending, + ) + pollForStatus(linkedAccount = linkedAccountEntity, index = linkedAccountIndex) + } + else -> Unit + } + } else { + notifyError(response = globalDeactivationResponse) + updateBottomSheetUIState(showBottomSheet = false) + setToggleState( + linkedAccountIndex = linkedAccountIndex, + toggleState = ToggleState.On(startDate = currentStartDate, endDate = currentEndDate), + ) + } + } + + private fun handleNpciError(npciResult: NpciResult.Error) { + if (!npciResult.isUserAborted) { + updateBottomSheetUIState(showBottomSheet = false) + notifyError() + } + } + + private fun linkedAccountsChangeListener() { + viewModelScope.launch(Dispatchers.IO) { + linkedAccountsUseCase.execute().collect { linkedAccounts -> + checkAndShowToastOnAccountUpdateIfRequired(updatedLinkedAccounts = linkedAccounts) + _state.update { + it.copy( + linkedAccounts = linkedAccounts, + upiGlobalScreenUIState = + if (linkedAccounts.isEmpty()) UpiGlobalScreenUiState.Empty + else UpiGlobalScreenUiState.Loaded, + toggleStateList = getToggleStateList(linkedAccounts), + ) + } + } + } + } + + private suspend fun checkAndShowToastOnAccountUpdateIfRequired( + updatedLinkedAccounts: List + ) { + if (state.value.upiGlobalScreenUIState == UpiGlobalScreenUiState.Loading) return + + // Showing snack bar once any account is added that supports UPI Global + if ( + isFirstAccountAddedThatSupportsUpiGlobal(updatedLinkedAccounts = updatedLinkedAccounts) + ) { + updateSnackBarState(shouldUpdateSnackBarStateWithDelay = true) + } + + // Showing snack bar once pin is setup for any account + if (isPinSetSuccessForAnyLinkedAccount(updatedLinkedAccounts = updatedLinkedAccounts)) { + updateSnackBarState() + } + } + + private fun updateSnackBarState(shouldUpdateSnackBarStateWithDelay: Boolean = false) { + viewModelScope.launch(Dispatchers.IO) { + if (shouldUpdateSnackBarStateWithDelay) + delay(2.seconds) // Added here to wait for SDK bottom sheet to disappear + _state.update { it.copy(snackBarState = SnackBarState(isVisible = true)) } + // Remove snack bar + delay(5.seconds) + _state.update { it.copy(snackBarState = SnackBarState(isVisible = false)) } + } + } + + private fun isPinSetSuccessForAnyLinkedAccount( + updatedLinkedAccounts: List + ): Boolean { + // Checking with the previously linked account to see if the updated account PIN has been + // set. + state.value.linkedAccounts.forEach { + if ( + !it.isMPinSet && + updatedLinkedAccounts + .find { linkedAccount -> linkedAccount.accountId == it.accountId } + ?.isMPinSet == true + ) { + return true + } + } + return false + } + + private suspend fun isFirstAccountAddedThatSupportsUpiGlobal( + updatedLinkedAccounts: List + ): Boolean { + + return state.value.linkedAccounts.isEmpty() && + updatedLinkedAccounts.size == 1 && + isUpiGlobalSupported( + bankCode = updatedLinkedAccounts.firstOrNull()?.bankCode.orEmpty(), + accountType = updatedLinkedAccounts.firstOrNull()?.accountType.orEmpty(), + ) && + updatedLinkedAccounts.firstOrNull()?.upiGlobalInfo?.status != + UpiGlobalAccountStatus.ACTIVE + } + + private suspend fun showRedirectingBottomSheet(titleText: String) { + updateBottomSheetUIState( + showBottomSheet = true, + allowBottomSheetStateChange = false, + bottomSheetUIState = UpiGlobalScreenBottomSheetUiState.Loading(title = titleText), + ) + // Redirecting bottomsheet before navigation + delay(2.seconds) + } + + private fun onAddBankAccountClicked() { + naviPayAnalytics.onAddBankClicked() + viewModelScope.launch(Dispatchers.IO) { + naviPayPspManager.handleAccountAdditionFlow( + screenName = screenName, + enabledAccountTypes = SAVINGS_ONLY_ENABLED_ACCOUNTS, + ) + } + } + + private suspend fun getToggleStateBasedOnLinkedAccount( + linkedAccount: LinkedAccountEntity, + index: Int, + ): ToggleState { + if ( + !isUpiGlobalSupported( + bankCode = linkedAccount.bankCode, + accountType = linkedAccount.accountType, + ) + ) + return ToggleState.Disabled + + if (!linkedAccount.isMPinSet) return ToggleState.PinNotSet + + val upiGlobalInfo = linkedAccount.upiGlobalInfo ?: return ToggleState.Disabled + + return when (upiGlobalInfo.status) { + UpiGlobalAccountStatus.ACTIVE -> { + if ( + UpiGlobalAccountStatus.isStatusActive( + status = upiGlobalInfo.status, + activeDateTime = upiGlobalInfo.endDate ?: DateTime.now(), + ) + ) { + ToggleState.On( + startDate = upiGlobalInfo.startDate ?: DateTime.now(), + endDate = upiGlobalInfo.endDate ?: DateTime.now(), + ) + } else { + ToggleState.Off + } + } + UpiGlobalAccountStatus.DEACTIVE -> { + ToggleState.Off + } + UpiGlobalAccountStatus.INIT_ACTIVE -> { + pollForStatus(linkedAccount = linkedAccount, index = index) + ToggleState.ActivationPending + } + UpiGlobalAccountStatus.INIT_DEACTIVE -> { + pollForStatus(linkedAccount = linkedAccount, index = index) + ToggleState.DeactivationPending + } + else -> { + ToggleState.Off + } + } + } + + private suspend fun getToggleStateList( + linkedAccounts: List + ): SnapshotStateList { + return linkedAccounts + .mapIndexed { index, linkedAccount -> + getToggleStateBasedOnLinkedAccount(linkedAccount = linkedAccount, index = index) + } + .toMutableStateList() + } + + private fun pollForStatus(linkedAccount: LinkedAccountEntity, index: Int) { + viewModelScope.launch(Dispatchers.IO) { + val upiGlobalInfo = linkedAccount.upiGlobalInfo + + if (upiGlobalInfo == null) { + setToggleState(linkedAccountIndex = index, toggleState = ToggleState.Disabled) + return@launch + } + + if (!isStatusPending(linkedAccount.upiGlobalInfo.status)) { + return@launch + } + + naviApiPoller + .startPolling { + upiGlobalRepository.globalAccountStatusCheck( + globalAccountStatusRequest = + GlobalAccountStatusRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = deviceInfoProvider.getMerchantCustomerId(), + baccId = + linkedAccount.getBuidByPsp(psp = PspType.JUSPAY_AXIS).orEmpty(), + ), + metricInfo = getMetricInfo(screenName = screenName), + ) + } + .collect { + try { + val activationStatusAPIResponse = + it as RepoResult + handlePollingResponse( + activationStatusAPIResponse = activationStatusAPIResponse, + index = index, + ) + } catch (exception: Exception) { + naviApiPoller.stopPolling() + } + } + } + } + + private suspend fun handlePollingResponse( + activationStatusAPIResponse: RepoResult, + index: Int, + ) { + if (!activationStatusAPIResponse.isSuccessWithData()) { + naviApiPoller.stopPolling() + return + } + + val activationStatusData = activationStatusAPIResponse.data!! + val activationStatus = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus(activationStatusData.status) + + updateLinkedAccountStatus() + + when (activationStatus) { + UpiGlobalAccountStatus.ACTIVE -> { + setToggleState( + linkedAccountIndex = index, + toggleState = + ToggleState.On( + startDate = activationStatusData.startDate ?: DateTime.now(), + endDate = activationStatusData.endDate ?: DateTime.now(), + ), + ) + if ( + state.value.bottomSheetStateHolder.bottomSheetUIState + is UpiGlobalScreenBottomSheetUiState.UpiGlobalStatusPending + ) { + updateBottomSheetUIState(showBottomSheet = false) + } + naviApiPoller.stopPolling() + } + UpiGlobalAccountStatus.DEACTIVE -> { + setToggleState(linkedAccountIndex = index, toggleState = ToggleState.Off) + naviApiPoller.stopPolling() + } + UpiGlobalAccountStatus.INIT_ACTIVE, + UpiGlobalAccountStatus.INIT_DEACTIVE -> {} + } + } + + private suspend fun updateLinkedAccountStatus() { + refreshLinkedAccountsUseCase.execute(screenName = screenName) + } + + private suspend fun isUpiGlobalSupported(bankCode: String, accountType: String): Boolean { + if (AccountType.isAccountOfTypeCreditCardOrCreditLine(type = accountType)) return false + val bankEntity = + bankRepository.getBankUiModelListFromBankCodes(listOf(bankCode)).firstOrNull() + return bankEntity?.isUpiInternationalSupported ?: false + } + + private fun onBackClicked() { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit(UpiGlobalScreenContract.Effect.GoBack) + } + } + + private fun onHelpClicked() { + naviPayAnalytics.onHelpClicked() + viewModelScope.launch(Dispatchers.IO) { + _effect.emit(UpiGlobalScreenContract.Effect.NavigateToHelpScreen(ctaData = helpCtaData)) + } + } + + private fun setToggleState(linkedAccountIndex: Int, toggleState: ToggleState) { + _state.update { + it.copy( + toggleStateList = + it.toggleStateList.apply { + set(index = linkedAccountIndex, element = toggleState) + } + ) + } + } + + fun updateBottomSheetUIState( + showBottomSheet: Boolean = true, + allowBottomSheetStateChange: Boolean? = true, + bottomSheetUIState: UpiGlobalScreenBottomSheetUiState? = null, + ) { + _state.update { + it.copy( + bottomSheetStateHolder = + UpiGlobalScreenBottomSheetStateHolder( + showBottomSheet = showBottomSheet, + bottomSheetStateChange = + allowBottomSheetStateChange + ?: it.bottomSheetStateHolder.bottomSheetStateChange, + bottomSheetUIState = + bottomSheetUIState ?: it.bottomSheetStateHolder.bottomSheetUIState, + ) + ) + } + } + + private fun onUpiGlobalEndDateSelected(selectedUpiGlobalEndDate: DateTime) { + _state.update { it.copy(selectedUpiGlobalEndDate = selectedUpiGlobalEndDate) } + } + + override val screenName: String + get() = NAVI_PAY_UPI_GLOBAL +} + +interface UpiGlobalScreenContract : + NaviPayViewModelContract< + UpiGlobalScreenContract.State, + UpiGlobalScreenContract.Effect, + UpiGlobalScreenContract.Event, + > { + + data class State( + val upiGlobalScreenUIState: UpiGlobalScreenUiState = UpiGlobalScreenUiState.Loading, + val linkedAccounts: List = emptyList(), + val toggleStateList: SnapshotStateList = mutableStateListOf(), + val bottomSheetStateHolder: UpiGlobalScreenBottomSheetStateHolder = + UpiGlobalScreenBottomSheetStateHolder( + showBottomSheet = false, + bottomSheetStateChange = true, + bottomSheetUIState = UpiGlobalScreenBottomSheetUiState.Loading(title = ""), + ), + val isSponsorVisible: Boolean = true, + val isActivationBottomSheetLoaderRunning: Boolean = false, + val selectedUpiGlobalEndDate: DateTime? = null, + val isDeactivationBottomSheetLoaderRunning: Boolean = false, + val snackBarState: SnackBarState = SnackBarState(isVisible = false), + val isGlobalHeaderVisible: Boolean = true, + ) + + sealed class Effect { + data class Navigate( + val direction: Direction, + val shouldClearBackStack: Boolean = false, + val popUpToDestination: DirectionDestination = NaviPayLauncherScreenDestination, + ) : Effect() + + data class NavigateToHelpScreen(val ctaData: CtaData?) : Effect() + + data object GoBack : Effect() + } + + sealed class Event { + data class OnToggleClicked( + val index: Int, + val allowBottomSheetStateChange: Boolean = false, + ) : Event() + + data class OnBottomSheetStateChanged( + val showBottomSheet: Boolean, + val bottomSheetStateChange: Boolean? = null, + val bottomSheetUIState: UpiGlobalScreenBottomSheetUiState? = null, + ) : Event() + + data class OnUpiGlobalEndDateSelected(val selectedUpiGlobalEndDate: DateTime) : Event() + + data class OnActivateUpiGlobalClicked(val linkedAccountIndex: Int) : Event() + + data class OnDeactivateUpiGlobalClicked(val linkedAccountIndex: Int) : Event() + + data object OnHelpCtaClicked : Event() + + data object OnBackClicked : Event() + + data object NoAccountLinked : Event() + + data class OnScroll(val isGlobalHeaderVisible: Boolean) : Event() + + data object OnAddAccountClicked : Event() + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/model/view/GlobalSendMoneyModels.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/model/view/GlobalSendMoneyModels.kt new file mode 100644 index 0000000000..624c7824d2 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/model/view/GlobalSendMoneyModels.kt @@ -0,0 +1,90 @@ +/* + * + * * Copyright © 2022-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.model.view + +import com.navi.pay.management.transactionhistory.model.view.TransactionEntity +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.ramcosta.composedestinations.spec.Direction + +sealed class GlobalSendMoneyScreenState { + data object MainScreen : GlobalSendMoneyScreenState() + + data object PaymentInProgressPostPinInput : GlobalSendMoneyScreenState() + + data class PaymentSuccess( + val transactionEntity: TransactionEntity, + val isTransactionEligibleForNpsComms: Boolean, + val isLiteAccountInActivatedState: Boolean, + val liteAccountBalance: String, + val tstoreOrderId: String, + ) : GlobalSendMoneyScreenState() + + data class PaymentPending( + val transactionEntity: TransactionEntity, + val isTransactionEligibleForNpsComms: Boolean, + val isLiteAccountInActivatedState: Boolean, + val tstoreOrderId: String, + ) : GlobalSendMoneyScreenState() +} + +sealed class GlobalBankAccountsState { + data object Loading : GlobalBankAccountsState() + + data object NoAccountLinked : GlobalBankAccountsState() + + data class AccountList(val accounts: List) : GlobalBankAccountsState() +} + +sealed interface GlobalSendMoneyNavigationAction { + data object FinishScreen : GlobalSendMoneyNavigationAction + + data object GoBack : GlobalSendMoneyNavigationAction + + data object GoToTransactions : GlobalSendMoneyNavigationAction + + data object DoNothing : GlobalSendMoneyNavigationAction + + data class Redirection(val direction: Direction, val shouldClearBackStack: Boolean = false) : + GlobalSendMoneyNavigationAction +} + +sealed class GlobalSendMoneyMainCtaState(open val ctaText: String) { + data class Pay(override val ctaText: String) : GlobalSendMoneyMainCtaState(ctaText) + + data class SetPin(override val ctaText: String) : GlobalSendMoneyMainCtaState(ctaText) + + data class Disabled(override val ctaText: String) : GlobalSendMoneyMainCtaState(ctaText) + + data class ActivateUpiGlobal(override val ctaText: String) : + GlobalSendMoneyMainCtaState(ctaText) +} + +sealed class GlobalSendMoneyBottomSheetType { + data object AccountSelection : GlobalSendMoneyBottomSheetType() + + data object ConversionDetails : GlobalSendMoneyBottomSheetType() + + data object UpiGlobalDeactivated : GlobalSendMoneyBottomSheetType() + + data object UpiGlobalDateSelection : GlobalSendMoneyBottomSheetType() + + data class UpiGlobalStatusPending(val descriptionId: Int) : GlobalSendMoneyBottomSheetType() + + data class UpiGlobalStatusSuccess(val title: String) : GlobalSendMoneyBottomSheetType() + + data class UpiGlobalLoading(val title: String) : GlobalSendMoneyBottomSheetType() + + data class UpiGlobalStatusActivationFailed(val descriptionId: Int) : + GlobalSendMoneyBottomSheetType() +} + +data class GlobalSendMoneyScreenBottomSheetStateHolder( + val showBottomSheet: Boolean, + val bottomSheetStateChange: Boolean, + val bottomSheetUIState: GlobalSendMoneyBottomSheetType, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalPayeeDetailsView.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalPayeeDetailsView.kt new file mode 100644 index 0000000000..45c4959c04 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalPayeeDetailsView.kt @@ -0,0 +1,156 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.ui + +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.naviwidgets.extensions.NaviText +import com.navi.pay.R +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.ui.ImageWithCircularBackground +import com.navi.pay.management.globalsendmoney.viewmodel.UpiGlobalSendMoneyScreenContract +import com.navi.pay.utils.AT_THE_RATE_CHAR +import com.navi.pay.utils.clickableDebounce +import com.navi.pay.utils.shimmerEffect + +@Composable +fun GlobalPayeeDetailsView( + modifier: Modifier, + showHeadingInTopBar: Boolean, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, + state: UpiGlobalSendMoneyScreenContract.State, +) { + + val alpha by + animateFloatAsState( + targetValue = if (showHeadingInTopBar) 0f else 1f, + label = "", + animationSpec = tween(durationMillis = 100, easing = FastOutLinearInEasing), + ) + + Column( + modifier = + modifier + .fillMaxWidth() + .background(color = NaviPayColor.bgDefault) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (state.isPayeeDetailsLoading) { + Box(modifier = Modifier.width(180.dp).height(22.dp).shimmerEffect()) + } else { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + NaviText( + modifier = Modifier.weight(1f, false).alpha(alpha), + text = state.payeeName, + fontSize = 20.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontFamily = naviFontFamily, + color = NaviPayColor.textPrimary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + ) + + Spacer(modifier = Modifier.width(4.dp)) + Image( + modifier = Modifier.size(12.dp).alpha(alpha), + painter = painterResource(id = R.drawable.ic_np_verified_tick_green_bg), + contentDescription = null, + ) + } + } + Spacer(modifier = Modifier.height(3.dp)) + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + ImageWithCircularBackground( + imageUrl = null, + backgroundColor = NaviPayColor.bgAlt, + fallbackIconUrl = R.drawable.ic_np_global_icon, + boxSize = 24.dp, + imageSize = 16.dp, + borderWidth = 1.dp, + borderColor = NaviPayColor.borderDefault, + ) + Spacer(modifier = Modifier.width(4.dp)) + NaviText( + modifier = Modifier.weight(1f, false), + text = state.payeeVpa.substringBefore(AT_THE_RATE_CHAR), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 14.sp, + fontFamily = naviFontFamily, + color = NaviPayColor.textTertiary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + ) + if (state.payeeVpa.contains(AT_THE_RATE_CHAR)) { + NaviText( + text = "$AT_THE_RATE_CHAR${state.payeeVpa.substringAfter(AT_THE_RATE_CHAR)}", + fontSize = 14.sp, + fontFamily = naviFontFamily, + color = NaviPayColor.textTertiary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + ) + } + } + + ClickableTextCta( + modifier = + Modifier.padding(top = 16.dp).clickableDebounce { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnViewHistoryClicked) + }, + text = stringResource(id = R.string.np_view_history), + ) + } +} + +@Composable +fun ClickableTextCta(modifier: Modifier, text: String) { + NaviText( + modifier = modifier, + text = text, + textDecoration = TextDecoration.Underline, + fontSize = 14.sp, + fontFamily = naviFontFamily, + color = NaviPayColor.brandPrimaryPurple, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + ) +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyAccountSelectionView.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyAccountSelectionView.kt new file mode 100644 index 0000000000..a3d72d3300 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyAccountSelectionView.kt @@ -0,0 +1,214 @@ +/* + * + * * Copyright © 2022-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.navi.common.utils.CommonUtils.getDisplayableAmount +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.naviwidgets.extensions.NaviText +import com.navi.pay.R +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.ui.AddBankCard +import com.navi.pay.common.ui.BankAccountItem +import com.navi.pay.common.ui.BottomSheetBottomGradientBox +import com.navi.pay.common.ui.BottomSheetTopGradientBox +import com.navi.pay.common.ui.LoaderRoundedButton +import com.navi.pay.management.globalsendmoney.model.view.GlobalBankAccountsState +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyMainCtaState +import com.navi.pay.management.globalsendmoney.viewmodel.UpiGlobalSendMoneyScreenContract +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.navi.pay.utils.BOTTOM_SHEET_LIST_HEIGHT_PERCENTAGE_45 +import com.navi.pay.utils.NAVI_PAY_LOADER +import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE +import com.navi.pay.utils.clickableDebounce + +@Composable +fun GlobalSendMoneyAccountSelectionView( + state: UpiGlobalSendMoneyScreenContract.State, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, + onAddAccountClicked: () -> Unit, +) { + val sheetHeight = LocalConfiguration.current.screenHeightDp.dp * 0.85f + + Column( + modifier = Modifier.fillMaxWidth().heightIn(max = sheetHeight).navigationBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + NaviText( + modifier = Modifier.weight(1f), + text = + stringResource( + id = R.string.rupee_symbol_x, + state.paymentAmountInINR.toString().getDisplayableAmount(), + ), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + fontSize = 28.sp, + lineHeight = 42.sp, + color = NaviPayColor.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_np_cross_purple_send_money_bs), + contentDescription = null, + modifier = + Modifier.clickableDebounce { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + } + .size(24.dp), + ) + } + + HorizontalDivider(thickness = 4.dp, color = NaviPayColor.bgNonEditable) + + Box { + BankAccountsListSectionBottomsheet( + modifier = Modifier.fillMaxWidth(), + state = state, + eventDispatcher = eventDispatcher, + ) + + BottomSheetTopGradientBox(modifier = Modifier.align(Alignment.TopStart)) + + BottomSheetBottomGradientBox(modifier = Modifier.align(Alignment.BottomStart)) + } + + Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 12.dp)) { + LoaderRoundedButton( + modifier = Modifier.fillMaxWidth(), + text = state.mainCtaState.ctaText, + paddingValues = PaddingValues(16.dp), + enabled = state.mainCtaState !is GlobalSendMoneyMainCtaState.Disabled, + lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE, + showLoader = state.showCtaLoader, + ) { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnMainCtaButtonClicked) + } + + Spacer(modifier = Modifier.height(16.dp)) + AddBankCard(modifier = Modifier, onAddAccountClicked = onAddAccountClicked) + Spacer(modifier = Modifier.height(16.dp)) + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +fun BankAccountsListSectionBottomsheet( + modifier: Modifier, + state: UpiGlobalSendMoneyScreenContract.State, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, +) { + when (state.bankAccountsState) { + is GlobalBankAccountsState.AccountList -> + BankAccountsViewInternational( + modifier = modifier, + bankAccounts = state.bankAccountsState.accounts, + state = state, + eventDispatcher = eventDispatcher, + ) + GlobalBankAccountsState.Loading -> BankAccountsLoading(modifier = modifier.padding(16.dp)) + GlobalBankAccountsState.NoAccountLinked -> Unit + } +} + +@Composable +fun BankAccountsViewInternational( + modifier: Modifier, + bankAccounts: List, + state: UpiGlobalSendMoneyScreenContract.State, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, +) { + + LazyColumn( + modifier = + modifier + .padding(horizontal = 16.dp) + .heightIn( + min = 0.dp, + max = + LocalConfiguration.current.screenHeightDp.dp * + BOTTOM_SHEET_LIST_HEIGHT_PERCENTAGE_45, + ) + .nestedScroll(rememberNestedScrollInteropConnection()) + ) { + item { Spacer(modifier = Modifier.height(24.dp)) } + + items(bankAccounts.size) { index -> + val item = bankAccounts[index] + BankAccountItem( + bankAccount = item, + bankAccountEligibilityState = item.eligibilityState, + onSelected = { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBankAccountSelected(it) + ) + }, + isSelected = state.selectedBankAccount?.accountId == item.accountId, + ) + } + } +} + +@Composable +fun BankAccountsLoading(modifier: Modifier) { + val composition by + rememberLottieComposition(spec = LottieCompositionSpec.Asset(NAVI_PAY_LOADER)) + LottieAnimation( + composition = composition, + iterations = LottieConstants.IterateForever, + modifier = modifier.size(40.dp), + ) +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyBottomSheetContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyBottomSheetContent.kt new file mode 100644 index 0000000000..7112d0b7c7 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyBottomSheetContent.kt @@ -0,0 +1,316 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.ui + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.navi.common.R as CommonR +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.naviwidgets.extensions.NaviText +import com.navi.naviwidgets.utils.EMPTY +import com.navi.pay.R +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.ui.BottomSheetContentWithDateSelectionCalendarAndRadioButtons +import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader +import com.navi.pay.common.ui.ConversionDetailSection +import com.navi.pay.common.ui.LoaderRoundedButton +import com.navi.pay.common.ui.SecondaryRoundedButton +import com.navi.pay.common.ui.ThemeRoundedButton +import com.navi.pay.common.utils.getBankCharges +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyBottomSheetType +import com.navi.pay.management.globalsendmoney.viewmodel.UpiGlobalSendMoneyScreenContract +import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE + +@Composable +fun GlobalSendMoneyBottomSheetContent( + state: UpiGlobalSendMoneyScreenContract.State, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, + onAddAccountClicked: () -> Unit, +) { + when (state.bottomSheetStateHolder.bottomSheetUIState) { + is GlobalSendMoneyBottomSheetType.AccountSelection -> + GlobalSendMoneyAccountSelectionView( + state = state, + eventDispatcher = eventDispatcher, + onAddAccountClicked = onAddAccountClicked, + ) + GlobalSendMoneyBottomSheetType.ConversionDetails -> + ConversionDetailsBottomSheet(state = state, eventDispatcher = eventDispatcher) + GlobalSendMoneyBottomSheetType.UpiGlobalDeactivated -> + UpiGlobalDeactivatedView(eventDispatcher = eventDispatcher) + GlobalSendMoneyBottomSheetType.UpiGlobalDateSelection -> { + BottomSheetContentWithDateSelectionCalendarAndRadioButtons( + bankName = state.selectedBankAccount?.bankNameTruncatedWithAccountNumber.toString(), + updateDate = { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnUpiGlobalEndDateSelected( + selectedUpiGlobalEndDate = it + ) + ) + }, + onPrimaryButtonClicked = { + if (state.selectedUpiGlobalEndDate != null) + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnActivateGlobalClicked + ) + }, + showCtaLoader = state.showCtaLoader, + deactivationDate = state.selectedUpiGlobalEndDate, + ) + } + is GlobalSendMoneyBottomSheetType.UpiGlobalLoading -> { + LoadingBottomSheet(state.bottomSheetStateHolder.bottomSheetUIState.title) + } + is GlobalSendMoneyBottomSheetType.UpiGlobalStatusPending -> { + BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( + iconId = CommonR.drawable.ic_info_solid_blue, + iconSize = 24.dp, + header = stringResource(id = R.string.np_your_request_is_under_process), + description = + stringResource( + id = state.bottomSheetStateHolder.bottomSheetUIState.descriptionId + ), + primaryButton = stringResource(id = R.string.np_okay_got_it), + onPrimaryButtonClicked = { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + }, + onSecondaryButtonClicked = {}, + secondaryButton = null, + ) + } + is GlobalSendMoneyBottomSheetType.UpiGlobalStatusSuccess -> { + SuccessBottomSheet(text = state.bottomSheetStateHolder.bottomSheetUIState.title) + } + is GlobalSendMoneyBottomSheetType.UpiGlobalStatusActivationFailed -> { + BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( + iconId = CommonR.drawable.ic_exclamation_red_border, + iconSize = 24.dp, + header = stringResource(id = R.string.np_activate_upi_global_failed), + description = + stringResource( + id = state.bottomSheetStateHolder.bottomSheetUIState.descriptionId + ), + primaryButton = stringResource(id = R.string.np_okay_got_it), + onPrimaryButtonClicked = { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + }, + onSecondaryButtonClicked = {}, + secondaryButton = null, + ) + } + } +} + +@Composable +fun UpiGlobalDeactivatedView(eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Image( + painter = painterResource(id = CommonR.drawable.ic_info_solid_blue), + contentDescription = EMPTY, + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + NaviText( + text = stringResource(R.string.np_activate_upi_global_to_proceed), + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + ) + Spacer(modifier = Modifier.height(8.dp)) + NaviText( + text = stringResource(R.string.np_upi_global_deactivated), + fontSize = 14.sp, + fontFamily = naviFontFamily, + lineHeight = 22.sp, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.textTertiary, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + LoaderRoundedButton( + text = stringResource(R.string.np_activate_upi_global), + onClick = { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = true, + bottomSheetStateChange = true, + bottomSheetUIState = GlobalSendMoneyBottomSheetType.UpiGlobalDateSelection, + ) + ) + }, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SecondaryRoundedButton( + text = stringResource(R.string.cancel), + onClick = { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + }, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +fun ConversionDetailsBottomSheet( + state: UpiGlobalSendMoneyScreenContract.State, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, +) { + Column( + modifier = + Modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 32.dp) + .animateContentSize() + ) { + NaviText( + text = stringResource(R.string.np_conversion_details), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + fontSize = 16.sp, + color = NaviPayColor.textPrimary, + ) + Spacer(modifier = Modifier.height(8.dp)) + + Column( + modifier = + Modifier.fillMaxWidth() + .background(color = NaviPayColor.bgAlt, shape = RoundedCornerShape(4.dp)) + .padding(16.dp) + ) { + ConversionDetailSection( + title = stringResource(R.string.np_converted_amount), + description = + stringResource( + id = R.string.np_global_amount_conversion, + state.baseCurr, + state.forex, + ), + value = "${stringResource(R.string.rupee_symbol)} ${state.paymentAmountInINR}", + ) + Spacer(modifier = Modifier.height(10.dp)) + ConversionDetailSection( + title = stringResource(R.string.np_bank_forex_charges), + description = "(${state.markUp}%)", + value = + "${stringResource(R.string.rupee_symbol)} ${ + getBankCharges( + forex = state.forex.toString(), + markUp = state.markUp.toString(), + baseAmount = state.paymentAmountInForeignCurrency, + ) + }", + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + ThemeRoundedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.np_okay_got_it), + onClick = { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + }, + ) + } +} + +@Composable +fun SuccessBottomSheet(text: String) { + Column( + modifier = + Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp) + ) { + Image( + painter = painterResource(id = R.drawable.navi_pay_ic_checked_circle_green), + contentDescription = EMPTY, + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + NaviText( + text = text, + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + ) + } +} + +@Composable +fun LoadingBottomSheet(text: String) { + val spec = LottieCompositionSpec.Asset(NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE) + val composition by rememberLottieComposition(spec) + Column( + modifier = + Modifier.navigationBarsPadding() + .fillMaxWidth() + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp) + ) { + LottieAnimation( + composition = composition, + iterations = LottieConstants.IterateForever, + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + NaviText( + text = text, + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyMainScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyMainScreen.kt new file mode 100644 index 0000000000..b27ec81d13 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/GlobalSendMoneyMainScreen.kt @@ -0,0 +1,492 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.Checkbox +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.utils.orFalse +import com.navi.common.R as CommonR +import com.navi.common.utils.CommonUtils.getDisplayableAmount +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.design.theme.ABABAB +import com.navi.design.theme.D1D9E6_30 +import com.navi.design.utils.clickableWithNoGesture +import com.navi.naviwidgets.extensions.NaviText +import com.navi.pay.R +import com.navi.pay.common.model.view.CheckBalanceState +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.ui.LoaderRoundedButton +import com.navi.pay.common.ui.NaviPayHeader +import com.navi.pay.common.ui.SelectedAccountViewWithRolodexAnimation +import com.navi.pay.common.ui.ShadowStrip +import com.navi.pay.common.ui.ShakeController +import com.navi.pay.management.common.model.view.WarningErrorInfoState +import com.navi.pay.management.common.sendmoney.ui.getHeaderText +import com.navi.pay.management.globalsendmoney.model.view.GlobalBankAccountsState +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyMainCtaState +import com.navi.pay.management.globalsendmoney.util.GlobalCurrencyMaskTransformation +import com.navi.pay.management.globalsendmoney.viewmodel.UpiGlobalSendMoneyScreenContract +import com.navi.pay.management.upinumber.link.viewmodel.ExternalLinkedVpaState +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.navi.pay.utils.AMOUNT_MAX_LENGTH +import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE +import com.navi.pay.utils.ZERO_STRING +import com.navi.pay.utils.clickableDebounce +import com.navi.pay.utils.conditional +import com.navi.pay.utils.shake +import com.navi.pay.utils.shimmerEffect + +@Composable +fun UpiGlobalSendMoneyMainScreen( + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, + state: UpiGlobalSendMoneyScreenContract.State, + focusRequester: FocusRequester, + focusManager: FocusManager, + modifier: Modifier, + amountFieldShakeController: ShakeController, +) { + val scrollState = rememberScrollState() + val showHeadingInTopBar by remember { derivedStateOf { scrollState.value > 50 } } + + Scaffold( + modifier = modifier, + topBar = { + Card( + modifier = + Modifier.fillMaxWidth().conditional(showHeadingInTopBar) { + shadow( + shape = RoundedCornerShape(0.dp), + elevation = 32.dp, + ambientColor = D1D9E6_30, + spotColor = ABABAB, + ) + }, + shape = RoundedCornerShape(0.dp), + elevation = 0.dp, + backgroundColor = NaviPayColor.bgDefault, + ) { + val title = + getHeaderText( + showHeadingInTopBar = showHeadingInTopBar, + resourceId = R.string.np_send_money_header_title, + name = state.payeeName, + ) + + NaviPayHeader( + title = title, + onNavigationIconClick = { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnBackClicked) + }, + modifier = Modifier.fillMaxWidth(), + actionIconText = stringResource(R.string.help), + backgroundColor = NaviPayColor.bgDefault, + navigationIcon = CommonR.drawable.ic_arrow_left_black_v2, + onActionClick = { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnHelpClicked) + }, + titleColor = NaviPayColor.textPrimary, + ) + } + }, + content = { paddingValues -> + Column( + modifier = + Modifier.padding(paddingValues) + .fillMaxSize() + .verticalScroll(state = scrollState) + .background(color = NaviPayColor.ctaWhite), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GlobalPayeeDetailsView( + modifier = Modifier.fillMaxWidth(), + showHeadingInTopBar = showHeadingInTopBar, + eventDispatcher = eventDispatcher, + state = state, + ) + + AmountTextFieldAndDetailsSection( + eventDispatcher = eventDispatcher, + state = state, + focusRequester = focusRequester, + focusManager = focusManager, + amountFieldShakeController = amountFieldShakeController, + ) + Spacer(modifier = Modifier.height(58.dp)) + } + }, + bottomBar = { MainCtaSectionWithCheckBox(eventDispatcher = eventDispatcher, state = state) }, + ) +} + +@Composable +fun MainCtaSectionWithCheckBox( + state: UpiGlobalSendMoneyScreenContract.State, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth().background(NaviPayColor.ctaWhite), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + ShadowStrip(modifier = Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + modifier = Modifier.scale(0.8f).size(16.dp).clip(shape = RoundedCornerShape(4.dp)), + checked = state.isConsentCheckBoxChecked, + onCheckedChange = { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnConsentCheckBoxClicked) + }, + ) + Spacer(modifier = Modifier.width(4.dp)) + NaviText( + text = stringResource(R.string.np_agree_to_global_payment), + fontSize = 12.sp, + fontFamily = naviFontFamily, + color = NaviPayColor.inputFieldDefault, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + BankAccountStateView( + modifier = Modifier.padding(horizontal = 16.dp), + selectedBankAccount = state.selectedBankAccount, + bankAccountsState = state.bankAccountsState, + onActionIconClick = { + eventDispatcher.invoke( + UpiGlobalSendMoneyScreenContract.Event.OnBankDropDownClicked( + bankAccountUniqueId = state.selectedBankAccount?.accountId.toString(), + bankUptimeStatus = + state.selectedBankAccount?.bankUptimeEntity?.status?.name.toString(), + ) + ) + }, + isSelectedAccountClickable = true, + isSelectedAccountEligible = true, + ) + Spacer(modifier = Modifier.height(16.dp)) + LoaderRoundedButton( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + text = state.mainCtaState.ctaText, + paddingValues = PaddingValues(16.dp), + enabled = state.mainCtaState !is GlobalSendMoneyMainCtaState.Disabled, + lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE, + showLoader = state.showCtaLoader, + loaderStatePaddingValues = PaddingValues(10.dp), + onClick = { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnMainCtaButtonClicked) + }, + ) + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +fun AmountTextFieldAndDetailsSection( + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, + state: UpiGlobalSendMoneyScreenContract.State, + focusRequester: FocusRequester, + focusManager: FocusManager, + amountFieldShakeController: ShakeController, +) { + Column( + modifier = Modifier.fillMaxWidth().padding(top = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AmountTextField( + eventDispatcher = eventDispatcher, + state = state, + focusRequester = focusRequester, + focusManager = focusManager, + amountFieldShakeController = amountFieldShakeController, + ) + NaviText( + text = + "= ${stringResource(R.string.rupee_symbol)}${state.paymentAmountInINR.toString().getDisplayableAmount()}", + fontSize = 20.sp, + fontFamily = naviFontFamily, + color = + if (state.isPaymentAmountNonZero) NaviPayColor.textTertiary + else NaviPayColor.inputFieldDefault, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + ) + Spacer(modifier = Modifier.height(8.dp)) + if ( + state.warningErrorInfoState?.isErrorState.orFalse() || + state.warningErrorInfoState?.isWarningState.orFalse() + ) { + RenderWarningOrErrorMessage(warningErrorInfoState = state.warningErrorInfoState) + } + Spacer(modifier = Modifier.height(8.dp)) + NaviText( + modifier = + Modifier.conditional(state.isPaymentAmountNonZero) { + clickableDebounce { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnConversionDetailClicked + ) + } + }, + text = stringResource(R.string.np_conversion_details), + textDecoration = TextDecoration.Underline, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + fontFamily = naviFontFamily, + color = + if (state.isPaymentAmountNonZero) NaviPayColor.brandPrimaryPurple + else NaviPayColor.ctaDisabled, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AmountTextField( + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, + state: UpiGlobalSendMoneyScreenContract.State, + focusRequester: FocusRequester, + focusManager: FocusManager, + amountFieldShakeController: ShakeController, +) { + val keyboardController = LocalSoftwareKeyboardController.current + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(state.isPayeeDetailsLoading) { + if (!state.isPayeeDetailsLoading) { + eventDispatcher.invoke(UpiGlobalSendMoneyScreenContract.Event.OnFocusAmount) + } + } + Row( + modifier = + Modifier.fillMaxWidth().shake(amountFieldShakeController).clickableWithNoGesture { + focusRequester.requestFocus() + keyboardController?.show() + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (state.isPayeeDetailsLoading) { + Box(modifier = Modifier.width(107.dp).height(48.dp).shimmerEffect()) + } else { + Spacer(modifier = Modifier.weight(1f)) + NaviText( + modifier = + Modifier.wrapContentWidth().height(IntrinsicSize.Min).conditional( + state.isPayeeDetailsLoading + ) { + shimmerEffect() + }, + text = state.baseCurr, + color = NaviPayColor.black, + fontSize = 20.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + ) + Spacer(modifier = Modifier.width(8.dp)) + BasicTextField( + modifier = + Modifier.focusRequester(focusRequester) + .height(IntrinsicSize.Min) + .width(IntrinsicSize.Min) + .clipToBounds(), + value = state.paymentAmount, + textStyle = + TextStyle( + color = NaviPayColor.textPrimary, + fontSize = 40.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + textAlign = TextAlign.Start, + ), + onValueChange = { + eventDispatcher.invoke( + UpiGlobalSendMoneyScreenContract.Event.OnAmountChanged(it) + ) + }, + readOnly = state.isDynamicQr, + visualTransformation = + if (state.isDynamicQr) VisualTransformation.None + else GlobalCurrencyMaskTransformation(AMOUNT_MAX_LENGTH), + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Number, + autoCorrect = false, + imeAction = ImeAction.Next, + ), + keyboardActions = + KeyboardActions( + onDone = { + if (state.paymentAmountInINR != 0.0) { + focusManager.moveFocus(FocusDirection.Next) + } + } + ), + ) { + TextFieldDefaults.TextFieldDecorationBox( + value = state.paymentAmountInForeignCurrency, + visualTransformation = + if (state.isDynamicQr) VisualTransformation.None + else GlobalCurrencyMaskTransformation(AMOUNT_MAX_LENGTH), + innerTextField = it, + singleLine = false, + enabled = !state.isDynamicQr, + interactionSource = interactionSource, + contentPadding = PaddingValues(0.dp), + placeholder = { AmountFieldPlaceholder() }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = Color.Transparent, + cursorColor = Color.Black, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + } + Spacer(modifier = Modifier.weight(1f)) + } + } +} + +@Composable +fun AmountFieldPlaceholder() { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopStart) { + NaviText( + modifier = Modifier.wrapContentWidth().align(Alignment.TopCenter), + text = ZERO_STRING, + fontSize = 36.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + color = NaviPayColor.inputFieldDefault, + textAlign = TextAlign.Start, + ) + } +} + +@Composable +fun BankAccountStateView( + modifier: Modifier, + selectedBankAccount: LinkedAccountEntity?, + bankAccountsState: GlobalBankAccountsState, + onActionIconClick: () -> Unit, + isSelectedAccountClickable: Boolean, + isSelectedAccountEligible: Boolean, + isBottomSheetVisible: Boolean = false, + externalLinkedVpaState: ExternalLinkedVpaState = ExternalLinkedVpaState.NotAvailable, + tweenDurationForRolodex: Int = 400, +) { + when (bankAccountsState) { + is GlobalBankAccountsState.Loading -> SelectBankShimmerView(modifier = modifier) + is GlobalBankAccountsState.AccountList -> { + SelectedAccountViewWithRolodexAnimation( + isSelectedAccountClickable = isSelectedAccountClickable, + modifier = modifier, + onActionIconClick = onActionIconClick, + selectedBankAccount = selectedBankAccount, + isSelectedAccountEligible = isSelectedAccountEligible, + tweenDurationInMillis = tweenDurationForRolodex, + isBottomSheetVisible = isBottomSheetVisible, + externalLinkedVpaState = externalLinkedVpaState, + onCheckBalanceClicked = {}, + selectedAccountCheckBalanceState = CheckBalanceState.None, + onArcNudgeClicked = {}, + ) + } + else -> {} + } +} + +@Composable +fun SelectBankShimmerView(modifier: Modifier) { + Box( + modifier = + modifier + .fillMaxWidth() + .height(48.dp) + .clip(shape = MaterialTheme.shapes.extraSmall) + .shimmerEffect() + ) + Spacer(modifier = Modifier.height(16.dp)) +} + +@Composable +fun RenderWarningOrErrorMessage( + modifier: Modifier = Modifier, + warningErrorInfoState: WarningErrorInfoState?, +) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + NaviText( + text = warningErrorInfoState?.errorMessage.orEmpty(), + color = NaviPayColor.onSurfaceCritical, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 12.sp, + textAlign = TextAlign.Center, + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/UpiGlobalSendMoneyScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/UpiGlobalSendMoneyScreen.kt new file mode 100644 index 0000000000..55a52ca457 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/ui/UpiGlobalSendMoneyScreen.kt @@ -0,0 +1,389 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.alfred.AlfredManager +import com.navi.common.utils.navigateUp +import com.navi.pay.R +import com.navi.pay.common.model.view.ShakeConfig +import com.navi.pay.common.setup.NaviPayRouter +import com.navi.pay.common.ui.FullScreenLottieV2 +import com.navi.pay.common.ui.NaviPayLottieAnimationV2 +import com.navi.pay.common.ui.NaviPayModalBottomSheet +import com.navi.pay.common.ui.rememberShakeController +import com.navi.pay.common.utils.NaviPayCommonUtils +import com.navi.pay.common.utils.NaviPayMediaPlayer +import com.navi.pay.destinations.NaviPayLauncherScreenDestination +import com.navi.pay.destinations.OrderDetailsScreenDestination +import com.navi.pay.destinations.OrderHistoryScreenDestination +import com.navi.pay.destinations.PaymentSummaryScreenDestination +import com.navi.pay.entry.NaviPayActivity +import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity +import com.navi.pay.management.global.models.view.UpiGlobalSendMoneyScreenSource +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyNavigationAction +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyScreenState +import com.navi.pay.management.globalsendmoney.viewmodel.UpiGlobalSendMoneyScreenContract +import com.navi.pay.management.globalsendmoney.viewmodel.UpiGlobalSendMoneyViewModel +import com.navi.pay.management.transactionhistory.model.view.TransactionEntity +import com.navi.pay.utils.NAVI_PAY_PAYMENT_PROGRESS_LOTTIE +import com.navi.pay.utils.NAVI_PAY_PAYMENT_SUCCESSFUL_MAIN_LOTTIE_V2 +import com.navi.pay.utils.clearBackStackUpToAndNavigate +import com.navi.pay.utils.customHide +import com.navi.pay.utils.globalFormattedCurrency +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import java.lang.ref.WeakReference +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun UPIGlobalSendMoneyScreen( + naviPayActivity: NaviPayActivity, + navigator: DestinationsNavigator, + payeeEntity: PayeeEntity? = null, + qrContent: String = "", + upiGlobalSendMoneyViewModel: UpiGlobalSendMoneyViewModel = hiltViewModel(), + upiGlobalSendMoneyScreenSource: UpiGlobalSendMoneyScreenSource? = + UpiGlobalSendMoneyScreenSource.QrScannerScreen, +) { + val state by upiGlobalSendMoneyViewModel.state.collectAsStateWithLifecycle() + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + val context = LocalContext.current + val view = LocalView.current + val lifecycleOwner = LocalLifecycleOwner.current + + val focusRequester = remember { FocusRequester() } + + val amountFieldShakeController = rememberShakeController() + + val scope = rememberCoroutineScope() + + val bottomSheetState = + rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { state.bottomSheetStateHolder.bottomSheetStateChange }, + ) + + val eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit = { event -> + upiGlobalSendMoneyViewModel.onEvent(event = event) + } + + val onAddAccountClicked = { + keyboardController?.customHide(context = context, view = view) + upiGlobalSendMoneyViewModel.onAddAccountClicked() + } + + DisposableEffect(Unit) { + onDispose { AlfredManager.setCurrentScreenName(naviPayActivity.screenName) } + } + + val onDismissBottomSheet: () -> Unit = { + scope + .launch { bottomSheetState.hide() } + .invokeOnCompletion { + if ( + !bottomSheetState.isVisible || + !state.bottomSheetStateHolder.bottomSheetStateChange + ) { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + } + } + } + + DisposableEffect(key1 = lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME, + Lifecycle.Event.ON_CREATE -> { + naviPayActivity.window.statusBarColor = + ContextCompat.getColor( + naviPayActivity, + R.color.navi_pay_status_bar_default_color, + ) + } + Lifecycle.Event.ON_PAUSE -> Unit + else -> Unit + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + val onBackClick = { + keyboardController?.customHide(context = context, view = view) + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnBackClicked) + } + + BackHandler { + if (bottomSheetState.isVisible) { + eventDispatcher( + UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged( + showBottomSheet = false + ) + ) + } else { + onBackClick() + } + } + + if (state.bottomSheetStateHolder.showBottomSheet) { + NaviPayModalBottomSheet( + modifier = Modifier.fillMaxWidth(), + bottomSheetState = bottomSheetState, + onDismissRequest = onDismissBottomSheet, + shouldDismissOnBackPress = true, + bottomSheetContent = { + GlobalSendMoneyBottomSheetContent( + state = state, + eventDispatcher = eventDispatcher, + onAddAccountClicked = onAddAccountClicked, + ) + }, + ) + } + + LaunchedEffect(upiGlobalSendMoneyViewModel.effect) { + upiGlobalSendMoneyViewModel.effect.collect { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnEffectCollected) + when (it) { + is UpiGlobalSendMoneyScreenContract.Effect.Navigate -> { + when (it.globalSendMoneyNavigationAction) { + GlobalSendMoneyNavigationAction.DoNothing -> { + // To disable back press during send money + } + GlobalSendMoneyNavigationAction.FinishScreen -> { + naviPayActivity.finish() + } + GlobalSendMoneyNavigationAction.GoBack -> { + navigator.navigateUp(naviPayActivity) + } + GlobalSendMoneyNavigationAction.GoToTransactions -> { + navigator.clearBackStackUpToAndNavigate( + destination = OrderHistoryScreenDestination(), + popUpTo = NaviPayLauncherScreenDestination, + inclusive = false, + ) + } + is GlobalSendMoneyNavigationAction.Redirection -> { + if (it.globalSendMoneyNavigationAction.shouldClearBackStack) { + navigator.clearBackStackUpToAndNavigate( + destination = it.globalSendMoneyNavigationAction.direction, + popUpTo = NaviPayLauncherScreenDestination, + inclusive = false, + ) + } else { + navigator.navigate(it.globalSendMoneyNavigationAction.direction) + } + } + } + } + is UpiGlobalSendMoneyScreenContract.Effect.StatusBarColorChange -> { + naviPayActivity.window.statusBarColor = + ContextCompat.getColor(naviPayActivity, it.color) + } + is UpiGlobalSendMoneyScreenContract.Effect.AutoFocusOnAmount -> { + if (it.shouldAutoFocusOnAmount) { + focusRequester.requestFocus() + } + } + is UpiGlobalSendMoneyScreenContract.Effect.KeyboardVisibility -> { + when (it.isKeyBoardVisible) { + true -> keyboardController?.show() + false -> { + naviPayActivity.runOnUiThread { + focusManager.clearFocus() + keyboardController?.customHide(context = context, view = view) + } + } + } + } + is UpiGlobalSendMoneyScreenContract.Effect.NavigateToHelpScreen -> { + NaviPayRouter.onCtaClick( + naviPayActivity = naviPayActivity, + ctaData = it.ctaData, + ) + } + is UpiGlobalSendMoneyScreenContract.Effect.TriggerErrorVibrationAndShakeAnimation -> { + amountFieldShakeController.shake( + ShakeConfig(iterations = 3, translateX = 7f, durationMillis = 30) + ) + NaviPayCommonUtils.playErrorHaptic(context) + } + } + } + } + + Column(modifier = Modifier.fillMaxSize().background(color = Color.White)) { + when (state.sendMoneyScreenState) { + GlobalSendMoneyScreenState.MainScreen -> + UpiGlobalSendMoneyMainScreen( + eventDispatcher = eventDispatcher, + state = state, + focusRequester = focusRequester, + focusManager = focusManager, + modifier = Modifier, + amountFieldShakeController = amountFieldShakeController, + ) + GlobalSendMoneyScreenState.PaymentInProgressPostPinInput -> { + PaymentProgressPostPinAnimation( + paymentAmount = state.paymentAmountInForeignCurrency, + baseCurrency = state.baseCurr, + ) + } + is GlobalSendMoneyScreenState.PaymentPending -> { + LaunchedEffect(Unit) { + goToOrderDetailsScreen( + navigator = navigator, + orderReferenceId = + (state.sendMoneyScreenState + as GlobalSendMoneyScreenState.PaymentPending) + .tstoreOrderId, + ) + } + } + is GlobalSendMoneyScreenState.PaymentSuccess -> { + + RenderPaymentSuccessScreen( + naviPayActivity = naviPayActivity, + navigator = navigator, + state = state, + eventDispatcher = eventDispatcher, + ) + } + } + } +} + +@Composable +private fun RenderPaymentSuccessScreen( + naviPayActivity: NaviPayActivity, + navigator: DestinationsNavigator, + state: UpiGlobalSendMoneyScreenContract.State, + eventDispatcher: (UpiGlobalSendMoneyScreenContract.Event) -> Unit, +) { + val mediaPlayer = remember { NaviPayMediaPlayer(activityRef = WeakReference(naviPayActivity)) } + NaviPayLottieAnimationV2( + lottieFileName = NAVI_PAY_PAYMENT_SUCCESSFUL_MAIN_LOTTIE_V2, + onAnimationEnd = { + goToPaymentSummaryScreen( + navigator = navigator, + transactionEntity = + (state.sendMoneyScreenState as GlobalSendMoneyScreenState.PaymentSuccess) + .transactionEntity, + isTransactionEligibleForNpsComms = + state.sendMoneyScreenState.isTransactionEligibleForNpsComms, + orderReferenceId = state.sendMoneyScreenState.tstoreOrderId, + ) + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.ResetLottieSpecs) + }, + contentScale = ContentScale.Crop, + onAnimationStart = { mediaPlayer.start(fileNameResId = R.raw.navi_pay_payment_success) }, + successText = + stringResource( + id = R.string.amount_paid_successfully_global, + state.baseCurr, + state.paymentAmountInForeignCurrency.globalFormattedCurrency(), + ), + progressText = + stringResource( + id = R.string.global_payment_in_progress, + state.baseCurr, + state.paymentAmountInForeignCurrency.globalFormattedCurrency(), + ), + isSuccessTextVisible = state.isSuccessTextVisible, + isProgressTextVisible = state.isProcessingTextVisible, + backgroundColor = state.lottieBackgroundColor, + onAnimationProgressTransition = { + eventDispatcher(UpiGlobalSendMoneyScreenContract.Event.OnLottieProgressTransition) + }, + transitionThresholdValue = 0.35f, + lottieSize = 230.dp, + ) +} + +fun goToOrderDetailsScreen(navigator: DestinationsNavigator, orderReferenceId: String) { + + navigator.clearBackStackUpToAndNavigate( + destination = OrderDetailsScreenDestination(orderReferenceId = orderReferenceId), + popUpTo = NaviPayLauncherScreenDestination, + inclusive = false, + ) +} + +private fun goToPaymentSummaryScreen( + navigator: DestinationsNavigator, + transactionEntity: TransactionEntity, + isTransactionEligibleForNpsComms: Boolean, + orderReferenceId: String, +) { + navigator.clearBackStackUpToAndNavigate( + destination = + PaymentSummaryScreenDestination( + upiRequestIdTxnEventType = transactionEntity.upiRequestIdTxnEventType, + isTransactionEligibleForNpsComms = isTransactionEligibleForNpsComms, + orderReferenceId = orderReferenceId, + transactionCompletionTime = 10L, + isFestiveThemeExperimentEnabled = false, + isIPLPowerPlayThemeExperimentEnabled = false, + isArcProtected = false, + ), + popUpTo = NaviPayLauncherScreenDestination, + inclusive = false, + ) +} + +@Composable +fun PaymentProgressPostPinAnimation(paymentAmount: String, baseCurrency: String) { + val title = + stringResource( + id = R.string.global_payment_in_progress, + baseCurrency, + paymentAmount.globalFormattedCurrency(), + ) + FullScreenLottieV2( + lottieFileName = NAVI_PAY_PAYMENT_PROGRESS_LOTTIE, + title = title, + lottieSize = 230.dp, + ) +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/util/GlobalCurrencyMaskTransformation.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/util/GlobalCurrencyMaskTransformation.kt new file mode 100644 index 0000000000..0532493ad4 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/util/GlobalCurrencyMaskTransformation.kt @@ -0,0 +1,57 @@ +/* + * + * * Copyright © 2022-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.util + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import com.navi.common.utils.Constants.COMMA_CHAR +import com.navi.pay.utils.globalFormattedCurrency + +class GlobalCurrencyMaskTransformation(private val amountMaxLength: Int) : VisualTransformation { + private fun maskFilter(text: AnnotatedString): TransformedText { + // Expected format is 9,122,123.99 + val trimmedAmount = + if (text.text.length > amountMaxLength) text.text.substring(0..amountMaxLength) + else text.text + val formattedText = trimmedAmount.globalFormattedCurrency() + + val originalToTransformed = mutableListOf() + val transformedToOriginal = mutableListOf() + var specialCharsCount = 0 + + formattedText.forEachIndexed { index, char -> + if (char == COMMA_CHAR) { + specialCharsCount++ + } else { + originalToTransformed.add(index) + } + transformedToOriginal.add(index - specialCharsCount) + } + originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(1) ?: 0) + transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(1) ?: 0) + + val numberOffsetTranslator = + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return originalToTransformed[offset] + } + + override fun transformedToOriginal(offset: Int): Int { + return transformedToOriginal[offset] + } + } + + return TransformedText(AnnotatedString(formattedText), numberOffsetTranslator) + } + + override fun filter(text: AnnotatedString): TransformedText { + return maskFilter(text) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/util/UpiGlobalSendMoneyUtils.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/util/UpiGlobalSendMoneyUtils.kt new file mode 100644 index 0000000000..6cf33ed3cc --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/util/UpiGlobalSendMoneyUtils.kt @@ -0,0 +1,27 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.util + +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle +import com.navi.pay.common.utils.getDefaultPayeeEntity +import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity + +object UpiGlobalSendMoneyUtils { + + fun getPayeeEntity(savedStateHandle: SavedStateHandle, intentData: Bundle?): PayeeEntity { + val payeeEntity: PayeeEntity? = savedStateHandle.get("payeeEntity") + return payeeEntity ?: (intentData?.getParcelable("payeeEntity") ?: getDefaultPayeeEntity()) + } + + fun getQrContent(savedStateHandle: SavedStateHandle, intentData: Bundle?): String { + return savedStateHandle.get("qrContent")?.ifBlank { + intentData?.getString("qrContent") + } ?: "" + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/viewmodel/UpiGlobalSendMoneyViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/viewmodel/UpiGlobalSendMoneyViewModel.kt new file mode 100644 index 0000000000..d48a4a8b76 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/globalsendmoney/viewmodel/UpiGlobalSendMoneyViewModel.kt @@ -0,0 +1,1766 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.management.globalsendmoney.viewmodel + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.google.gson.reflect.TypeToken +import com.navi.base.model.CtaData +import com.navi.base.utils.DateUtils +import com.navi.base.utils.EMPTY +import com.navi.base.utils.ResourceProvider +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.base.utils.orFalse +import com.navi.common.R as CommonR +import com.navi.common.network.models.RepoResult +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.CommonUtils.getDisplayableAmount +import com.navi.common.utils.NaviApiPoller +import com.navi.pay.R +import com.navi.pay.analytics.NaviPayAnalytics +import com.navi.pay.common.bankuptime.model.view.BankUptimeStatus +import com.navi.pay.common.bankuptime.repository.BankUptimeRepository +import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity +import com.navi.pay.common.model.config.NaviPayDefaultConfig +import com.navi.pay.common.model.view.NaviPayFlowType +import com.navi.pay.common.model.view.NaviPayScreenType +import com.navi.pay.common.model.view.NaviPaySessionHelper +import com.navi.pay.common.model.view.PspType +import com.navi.pay.common.model.view.SimInfo +import com.navi.pay.common.repository.SharedPreferenceRepository +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.usecase.ActivateUpiGlobalUseCase +import com.navi.pay.common.usecase.LinkedAccountsUseCase +import com.navi.pay.common.usecase.NaviPayConfigUseCase +import com.navi.pay.common.usecase.RefreshLinkedAccountsUseCase +import com.navi.pay.common.usecase.UpiLiteBalanceUseCase +import com.navi.pay.common.usecase.UpiRequestIdUseCase +import com.navi.pay.common.utils.DeviceInfoProvider +import com.navi.pay.common.utils.NaviPayCommonUtils +import com.navi.pay.common.utils.NaviPayCommonUtils.getHelpCtaData +import com.navi.pay.common.utils.NaviPayCommonUtils.roundOffToTwoDecimalPlaces +import com.navi.pay.common.utils.getDayEndDateTime +import com.navi.pay.common.utils.getDefaultPayeeEntity +import com.navi.pay.common.utils.getMetricInfo +import com.navi.pay.common.viewmodel.NaviPayBaseVM +import com.navi.pay.common.viewmodel.NaviPayViewModelContract +import com.navi.pay.destinations.LinkedAccountVerifyScreenDestination +import com.navi.pay.destinations.OrderHistoryScreenDestination +import com.navi.pay.entry.NaviPayActivityDataProvider +import com.navi.pay.management.common.model.view.WarningErrorInfoState +import com.navi.pay.management.common.sendmoney.model.network.GatewayPayeeInfo +import com.navi.pay.management.common.sendmoney.model.network.GatewayPayerInfo +import com.navi.pay.management.common.sendmoney.model.network.GatewayTxnInfo +import com.navi.pay.management.common.sendmoney.model.network.SendMoneyRequest +import com.navi.pay.management.common.sendmoney.model.network.TransactionInitiationType +import com.navi.pay.management.common.sendmoney.model.network.TransactionResponse +import com.navi.pay.management.common.sendmoney.model.network.UpiGlobalInfo +import com.navi.pay.management.common.sendmoney.model.view.EligibilityState +import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity +import com.navi.pay.management.common.sendmoney.model.view.PayeeTransactionHistoryEntity +import com.navi.pay.management.common.sendmoney.model.view.UpiTransactionType +import com.navi.pay.management.common.sendmoney.repository.SendMoneyRepository +import com.navi.pay.management.common.sendmoney.util.SendMoneyUtils +import com.navi.pay.management.common.transaction.model.network.TransactionCategory +import com.navi.pay.management.common.transaction.model.network.TransactionInstrumentType +import com.navi.pay.management.common.transaction.model.network.TransactionRole +import com.navi.pay.management.common.transaction.model.network.TransactionStatus +import com.navi.pay.management.common.transaction.util.toOrderEntity +import com.navi.pay.management.common.utils.NaviPayPspManager +import com.navi.pay.management.common.utils.PspEvaluationResult +import com.navi.pay.management.global.models.network.ForexDetail +import com.navi.pay.management.global.models.network.GlobalAccountStatusRequest +import com.navi.pay.management.global.models.network.GlobalAccountStatusResponse +import com.navi.pay.management.global.models.network.GlobalActivationRequest +import com.navi.pay.management.global.models.network.ValidateGlobalQrRequest +import com.navi.pay.management.global.models.network.ValidateGlobalQrResponse +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus.Companion.isStatusPending +import com.navi.pay.management.global.models.view.UpiGlobalSendMoneyScreenSource +import com.navi.pay.management.global.repository.UpiGlobalRepository +import com.navi.pay.management.globalsendmoney.model.view.GlobalBankAccountsState +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyBottomSheetType +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyMainCtaState +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyNavigationAction +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyScreenBottomSheetStateHolder +import com.navi.pay.management.globalsendmoney.model.view.GlobalSendMoneyScreenState +import com.navi.pay.management.globalsendmoney.util.UpiGlobalSendMoneyUtils +import com.navi.pay.management.globalsendmoney.viewmodel.UpiGlobalSendMoneyScreenContract.Effect.* +import com.navi.pay.management.transactionhistory.model.network.toTransactionEntity +import com.navi.pay.management.transactionhistory.model.view.TransactionDetailEntity +import com.navi.pay.management.transactionhistory.model.view.TransactionDetailMetaData +import com.navi.pay.management.transactionhistory.model.view.TransactionDetailPayeeInfo +import com.navi.pay.management.transactionhistory.model.view.TransactionDetailPayerInfo +import com.navi.pay.management.transactionhistory.model.view.TransactionEntity +import com.navi.pay.management.transactionhistory.repository.TransactionRepository +import com.navi.pay.npcicl.CredDataProvider +import com.navi.pay.npcicl.NpciRepository +import com.navi.pay.npcicl.NpciResult +import com.navi.pay.onboarding.account.add.model.view.AccountType +import com.navi.pay.onboarding.account.add.repository.BankRepository +import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity +import com.navi.pay.onboarding.binding.model.view.NaviPayCustomerOnboardingEntity +import com.navi.pay.tstore.list.repository.OrderRepository +import com.navi.pay.utils.ACCOUNT_ID +import com.navi.pay.utils.ACTION_PIN_SET +import com.navi.pay.utils.AMOUNT_MAX_LENGTH_AFTER_DECIMAL +import com.navi.pay.utils.AMOUNT_MAX_LENGTH_BEFORE_DECIMAL +import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR +import com.navi.pay.utils.DEFAULT_BANK_UP_TIME_SUCCESS_RATE +import com.navi.pay.utils.DEFAULT_CONFIG +import com.navi.pay.utils.DEFAULT_UPI_CURRENCY +import com.navi.pay.utils.KEY_IS_FIRST_TRANSACTION_SUCCESSFUL +import com.navi.pay.utils.NAVI_PAY_DEFAULT_MCC +import com.navi.pay.utils.RESOURCE_DEFAULT_ID +import com.navi.pay.utils.SAVINGS_ONLY_ENABLED_ACCOUNTS +import com.navi.pay.utils.SEND_MONEY_BACK_PRESS_BLOCK_TIME_MILLIS +import com.navi.pay.utils.UPI_GLOBAL_DEFAULT_INITIATION_MODE +import com.navi.pay.utils.UPI_GLOBAL_PURPOSE +import com.navi.pay.utils.getIfscForBankUptime +import com.navi.pay.utils.isAmountValidForSendMoney +import com.navi.pay.utils.safeConvertStringToDouble +import com.ramcosta.composedestinations.spec.Direction +import dagger.hilt.android.lifecycle.HiltViewModel +import java.util.Locale +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.joda.time.DateTime +import org.joda.time.DateTimeZone + +@HiltViewModel +class UpiGlobalSendMoneyViewModel +@Inject +constructor( + savedStateHandle: SavedStateHandle, + private val npciRepository: NpciRepository, + private val credDataProvider: CredDataProvider, + private val deviceInfoProvider: DeviceInfoProvider, + private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity, + private val linkedAccountsUseCase: LinkedAccountsUseCase, + private val upiRequestIdUseCase: UpiRequestIdUseCase, + private val upiGlobalRepository: UpiGlobalRepository, + private val bankRepository: BankRepository, + private val sendMoneyRepository: SendMoneyRepository, + private val naviPayConfigUseCase: NaviPayConfigUseCase, + private val transactionRepository: TransactionRepository, + private val sharedPreferenceRepository: SharedPreferenceRepository, + private val bankUptimeRepository: BankUptimeRepository, + private val orderRepository: OrderRepository, + private val upiLiteBalanceUseCase: UpiLiteBalanceUseCase, + private val resourceProvider: ResourceProvider, + private val refreshLinkedAccountsUseCase: RefreshLinkedAccountsUseCase, + private val naviPaySessionHelper: NaviPaySessionHelper, + private val activateUpiGlobalUseCase: ActivateUpiGlobalUseCase, + private val naviPayPspManager: NaviPayPspManager, + private val naviPayActivityDataProvider: NaviPayActivityDataProvider, +) : NaviPayBaseVM(), UpiGlobalSendMoneyScreenContract { + + companion object { + private val POLL_INTERVAL = 3.seconds + private const val POLL_MAX_ITERATIONS = 3 + } + + private val _state = MutableStateFlow(UpiGlobalSendMoneyScreenContract.State()) + override val state: StateFlow = _state.asStateFlow() + + private val _effect = MutableSharedFlow(replay = 1) + override val effect: SharedFlow = + _effect.asSharedFlow() + + val payeeEntity: PayeeEntity = + UpiGlobalSendMoneyUtils.getPayeeEntity( + savedStateHandle = savedStateHandle, + intentData = naviPayActivityDataProvider.getIntentData(), + ) + val qrContent: String = + UpiGlobalSendMoneyUtils.getQrContent( + savedStateHandle = savedStateHandle, + intentData = naviPayActivityDataProvider.getIntentData(), + ) + + private val upiGlobalSendMoneySource: UpiGlobalSendMoneyScreenSource = + savedStateHandle["upiGlobalSendMoneyScreenSource"] + ?: UpiGlobalSendMoneyScreenSource.getUpiGlobalSendMoneyScreenSource( + naviPayActivityDataProvider + .getIntentData() + ?.getString("upiGlobalSendMoneyScreenSource") + .orEmpty() + ) + + private var latestTransactionStartedAt: Long? = null + + private var naviPayDefaultConfig = NaviPayDefaultConfig() + + private val helpCtaData = + getHelpCtaData(screenName = NaviPayScreenType.NAVI_PAY_GLOBAL_SEND_MONEY.name) + + private val naviApiPoller by lazy { + NaviApiPoller(repeatInterval = POLL_INTERVAL, numberOfIterations = POLL_MAX_ITERATIONS) + } + + private val naviPayAnalytics: NaviPayAnalytics.NaviPayGlobalSendMoney = + NaviPayAnalytics.INSTANCE.NaviPayGlobalSendMoney() + + init { + onLand() + updatePayeeVpa() + updatePayeeEntity(payeeEntity) + updateNaviPayDefaultConfig() + linkedAccountsListener() + validateGlobalQr() + updateUpiLiteBalance() + } + + private fun onLand() { + naviPayAnalytics.onGlobalSendMoneyLanded( + naviPaySessionAttributes = getNaviPaySessionAttributes() + ) + } + + private fun validateGlobalQr() { + viewModelScope.launch(Dispatchers.IO) { + if (shouldBlockApiCallBecauseOfInternetOrSimOrAirplane()) { + return@launch + } + val validateGlobalQrRequest = + ValidateGlobalQrRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = deviceInfoProvider.getMerchantCustomerId(), + initiationMode = UPI_GLOBAL_DEFAULT_INITIATION_MODE, + qrPayLoad = qrContent, + ) + val validateGlobalQrResponse = + upiGlobalRepository.validateGlobalQr( + validateGlobalQrRequest, + metricInfo = getMetricInfo(screenName = screenName), + ) + if (validateGlobalQrResponse.isSuccessWithData()) { + val validateGlobalQrResponseData = validateGlobalQrResponse.data!! + updatePaymentDetails(validateGlobalQrResponseData = validateGlobalQrResponseData) + } else { + notifyError(response = validateGlobalQrResponse) + } + } + } + + private fun updatePaymentDetails(validateGlobalQrResponseData: ValidateGlobalQrResponse) { + val paymentDetails = validateGlobalQrResponseData.paymentDetails + val forexData = paymentDetails.forexList.first() + _state.update { + it.copy( + isPayeeDetailsLoading = false, + payeeName = paymentDetails.payeeName, + payeeMcc = paymentDetails.payeeMcc, + payeeType = paymentDetails.payeeType, + payeeVpa = paymentDetails.payeeVpa, + forex = forexData.forex.safeConvertStringToDouble(), + markUp = forexData.markUp.safeConvertStringToDouble(), + validateQrUpiRequestId = validateGlobalQrResponseData.upiRequestId, + baseCurr = forexData.baseCurr, + isDynamicQr = isDynamicQr(forexData = forexData), + ) + } + handleDynamicQr(forexData = forexData) + updateMainCtaButtonState() + } + + private fun handleDynamicQr(forexData: ForexDetail) { + if (state.value.isDynamicQr) { + _state.update { it.copy(paymentAmountInForeignCurrency = forexData.baseAmount) } + val paymentAmountInINR = forexData.convertedAmount.toDoubleOrNull() ?: 0.0 + _state.update { + it.copy( + paymentAmountInINR = paymentAmountInINR, + isPaymentAmountNonZero = paymentAmountInINR != 0.0, + ) + } + } + } + + private fun isDynamicQr(forexData: ForexDetail): Boolean { + val convertedAmountInDouble = forexData.convertedAmount.toDoubleOrNull() ?: return false + return convertedAmountInDouble > 0.0 + } + + private fun onPayButtonClicked() { + if (isAmountInValid(state.value.paymentAmountInINR)) { + updateErrorOrWarningState( + WarningErrorInfoState( + isWarningState = false, + isErrorState = true, + errorMessage = getWarningOrErrorMessage(), + ) + ) + emitErrorVibrationAndShakeAnimation() + updateBottomSheetUIState(showBottomSheet = false) + return + } + updateCtaLoaderState(showCtaLoader = true) + emitKeyboardVisibilityEffect(isKeyBoardVisible = false) + startPayment() + } + + private fun getWarningOrErrorMessage(): String { + return if ( + state.value.paymentAmountInINR > + naviPayDefaultConfig.config.amountLimits.sendMoneyDefaultLimit + ) { + return "${naviPayDefaultConfig.config.configMessage.maxPaymentAmountMessage} ₹${ + getMaxSendMoneyAmount().toString().getDisplayableAmount() + }" + } else { + resourceProvider.getString(R.string.np_global_min_payment_amount) + } + } + + private fun isAmountInValid(payAmountInINR: Double): Boolean { + return (payAmountInINR > getMaxSendMoneyAmount() || + !payAmountInINR.toString().isAmountValidForSendMoney()) + } + + private fun startPayment() { + viewModelScope.launch(Dispatchers.IO) { + naviPayPspManager.evaluateAndOnboardPspForFlow( + naviPayFlowType = NaviPayFlowType.UPI_GLOBAL, + vpaEntityList = state.value.selectedBankAccount?.vpaEntityList.orEmpty(), + screenName = screenName, + onPspEvaluated = { pspEvaluationResult -> + onPspEvaluatedForPayment(pspEvaluationResult = pspEvaluationResult) + }, + ) + } + } + + private suspend fun onPspEvaluatedForPayment(pspEvaluationResult: PspEvaluationResult) { + if (!checkIsInternetAvailableOrShowError()) { + + updateBottomSheetUIState(showBottomSheet = false) + return + } + if (checkIsAirplaneModeOnOrShowError()) { + + updateBottomSheetUIState(showBottomSheet = false) + return + } + val currentSimInfoList = naviPayNetworkConnectivity.getCurrentSimInfoList() + if (!validateSimInfoOrShowError(currentSimInfoList = currentSimInfoList)) { + updateBottomSheetUIState(showBottomSheet = false) + return + } + + val txnTimeStamp = + NaviPayCommonUtils.getDateTimeObjectFromDateTimeString( + dateTime = DateTime.now().toString() + ) + + val upiRequestId = + if (payeeEntity.transactionId.isNotNullAndNotEmpty()) { + payeeEntity.transactionId!! + } else + upiRequestIdUseCase.execute( + pspType = + pspEvaluationResult.onboardingDataEntity?.pspType ?: PspType.JUSPAY_AXIS + ) + + if (upiRequestId.isBlank()) { + notifyError(getGenericErrorConfig()) + return + } + + if (state.value.selectedBankAccount == null) { + notifyError(getGenericErrorConfig()) + return + } + naviPayAnalytics.onSendMoneyPayClicked( + upiRequestId = state.value.validateQrUpiRequestId, + purposeCode = UPI_GLOBAL_PURPOSE, + source = upiGlobalSendMoneySource, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + amount = state.value.paymentAmountInForeignCurrency, + currency = state.value.baseCurr, + tncApproval = state.value.isConsentCheckBoxChecked, + isDynamicQr = state.value.isDynamicQr, + ) + + updateBottomSheetUIState(showBottomSheet = false) + val npciCredData = + credDataProvider.payCred( + accountEntity = state.value.selectedBankAccount!!, + payeeEntity = + PayeeEntity( + note = naviPayDefaultConfig.config.configMessage.sendMoneyDefaultRemarks, + amount = getFormattedPaymentAmount(), + name = state.value.payeeName, + vpa = state.value.payeeVpa, + ), + upiRequestId = upiRequestId, + pspType = PspType.JUSPAY_AXIS, + ) + + val npciResult = + npciRepository.fetchCredentials( + npciCredData = npciCredData, + metricInfo = getMetricInfo(screenName = screenName), + customerOnboardingEntity = pspEvaluationResult.onboardingDataEntity ?: return, + ) + when (npciResult) { + is NpciResult.Success -> { + onClSuccessCallback( + upiRequestId = upiRequestId, + credBlock = npciResult.data, + txnTimeStamp = txnTimeStamp.toString(), + customerOnboardingEntity = pspEvaluationResult.onboardingDataEntity, + ) + } + + is NpciResult.Error -> { + updateScreenState(screenState = GlobalSendMoneyScreenState.MainScreen) + updateCtaLoaderState(false) + handleNpciError(npciResult = npciResult) + } + + else -> Unit + } + } + + private suspend fun onClSuccessCallback( + upiRequestId: String, + credBlock: String, + txnTimeStamp: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) { + updateScreenState(screenState = GlobalSendMoneyScreenState.PaymentInProgressPostPinInput) + executeSendMoneyPostCredBlockGeneration( + upiRequestId = upiRequestId, + credBlock = credBlock, + txnTimeStamp = txnTimeStamp, + customerOnboardingEntity = customerOnboardingEntity, + ) + } + + private suspend fun executeSendMoneyPostCredBlockGeneration( + upiRequestId: String, + credBlock: String, + txnTimeStamp: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) { + if (shouldBlockApiCallBecauseOfInternetOrSimOrAirplane()) { + return + } + + val sendMoneyRequest = + SendMoneyRequest( + gatewayPayeeInfo = + GatewayPayeeInfo( + vpa = state.value.payeeVpa, + name = state.value.payeeName, + mcc = state.value.payeeMcc, + ), + gatewayPayerInfo = + GatewayPayerInfo( + merchantCustomerId = deviceInfoProvider.getMerchantCustomerId(), + vpa = + state.value.selectedBankAccount + ?.getVpaEntityByPsp(PspType.JUSPAY_AXIS) + ?.vpa + .orEmpty(), + name = state.value.selectedBankAccount?.name.orEmpty(), + bankAccountUniqueId = + state.value.selectedBankAccount + ?.getBuidByPsp(psp = PspType.JUSPAY_AXIS) + .orEmpty(), + bankCode = state.value.selectedBankAccount?.bankCode.orEmpty(), + ), + gatewayTxnInfo = + GatewayTxnInfo( + amount = getFormattedPaymentAmount(), + upiRequestId = upiRequestId, + currency = DEFAULT_UPI_CURRENCY, + initiationMode = UPI_GLOBAL_DEFAULT_INITIATION_MODE, + purpose = UPI_GLOBAL_PURPOSE, + remarks = naviPayDefaultConfig.config.configMessage.sendMoneyDefaultRemarks, + timeStamp = txnTimeStamp, + credBlock = credBlock, + refCategory = payeeEntity.refCategory, + refUrl = payeeEntity.refUrl, + transactionType = UpiTransactionType.SCAN_PAY.name, + paymentMode = TransactionInitiationType.PAY_TO_UPI_ID.name, + transactionReference = payeeEntity.transactionReference, + instrumentType = TransactionInstrumentType.TRANSACTION.name, + txnRequestType = null, + upiGlobalInfo = + UpiGlobalInfo( + baseAmount = state.value.paymentAmountInForeignCurrency, + baseCurr = state.value.baseCurr, + forex = state.value.forex.toString(), + markUp = state.value.markUp.toString(), + validateQrUpiRequestId = state.value.validateQrUpiRequestId, + ), + ), + deviceFingerprint = customerOnboardingEntity.deviceFingerPrint, + pspType = customerOnboardingEntity.pspType, + ) + + latestTransactionStartedAt = System.currentTimeMillis() + + val response = + sendMoneyRepository.sendMoney(sendMoneyRequest, metricInfo = getMetricInfo(screenName)) + + val isTransactionEligibleForNpsComms = + !sharedPreferenceRepository.getBooleanValue( + key = KEY_IS_FIRST_TRANSACTION_SUCCESSFUL, + defValue = true, + ) + + if (response.isSuccessWithData()) { + processPaymentSuccess( + sendMoneyResponse = response.data!!, + isTransactionEligibleForNpsComms = isTransactionEligibleForNpsComms, + ) + } else { + processPaymentFailure( + response = response, + accountId = + state.value.selectedBankAccount + ?.getBuidByPsp(psp = PspType.JUSPAY_AXIS) + .orEmpty(), + ) + } + } + + private suspend fun processPaymentSuccess( + sendMoneyResponse: TransactionResponse, + isTransactionEligibleForNpsComms: Boolean, + ) { + updateBottomSheetUIState(showBottomSheet = false) + + val transactionStatus = + TransactionStatus.toTransactionStatus(status = sendMoneyResponse.status) + + val transactionEntity = + createTransactionEntityAndInsertInDB(sendMoneyResponse = sendMoneyResponse) + + if (sendMoneyResponse.tstoreOrderId != null) { + orderRepository.createLocalDbEntryOnOrder( + transactionEntity.toOrderEntity(tstoreOrderId = sendMoneyResponse.tstoreOrderId) + ) + } + + when (transactionStatus) { + TransactionStatus.SUCCESS -> { + updateScreenState( + screenState = + GlobalSendMoneyScreenState.PaymentSuccess( + transactionEntity = transactionEntity, + isTransactionEligibleForNpsComms = isTransactionEligibleForNpsComms, + isLiteAccountInActivatedState = state.value.upiLiteBalance.isNotEmpty(), + liteAccountBalance = state.value.upiLiteBalance, + tstoreOrderId = sendMoneyResponse.tstoreOrderId.orEmpty(), + ) + ) + } + + TransactionStatus.PENDING, + TransactionStatus.DEEMED, + TransactionStatus.INITIATED -> { + updateScreenState( + screenState = + GlobalSendMoneyScreenState.PaymentPending( + transactionEntity = transactionEntity, + isTransactionEligibleForNpsComms = isTransactionEligibleForNpsComms, + isLiteAccountInActivatedState = state.value.upiLiteBalance.isNotEmpty(), + tstoreOrderId = sendMoneyResponse.tstoreOrderId.orEmpty(), + ) + ) + } + + else -> { + // Ideally these cases wont come for upi international - collect request expired etc + } + } + } + + private suspend fun createTransactionEntityAndInsertInDB( + sendMoneyResponse: TransactionResponse + ): TransactionEntity { + + val payerInfo = + TransactionDetailPayerInfo( + vpa = + state.value.selectedBankAccount + ?.getVpaEntityByPsp(PspType.JUSPAY_AXIS) + ?.vpa + .orEmpty(), + name = state.value.selectedBankAccount?.name ?: "", + mcc = NAVI_PAY_DEFAULT_MCC, + mobNo = "", + txnEventType = sendMoneyResponse.transactionEventType, + mdAccNo = state.value.selectedBankAccount?.maskedAccountNumber ?: "", + category = + TransactionCategory.CUSTOMER_DEBITED + .name, // It will always be DEBITED or DEBITED_MERCHANT + bankName = state.value.selectedBankAccount?.bankName ?: "", + isNaviMerch = false, // Customer cannot be merchant + bankCode = state.value.selectedBankAccount?.bankCode ?: "", + bankIconUrl = state.value.selectedBankAccount?.bankIconImageUrl ?: "", + isMerchant = false, // Customer cannot be merchant + isMerchantVerified = false, // Customer cannot be merchant + lrn = null, + bAccType = state.value.selectedBankAccount?.accountType ?: "", + bAccUnqId = + state.value.selectedBankAccount?.getBuidByPsp(psp = PspType.JUSPAY_AXIS) ?: "", + bAccIfsc = state.value.selectedBankAccount?.ifsc ?: "", + upiNumber = "", + ) + + val payeeInfo = + TransactionDetailPayeeInfo( + vpa = state.value.payeeVpa, + name = state.value.payeeName, + mcc = state.value.payeeMcc, + mobNo = "", + txnEventType = sendMoneyResponse.transactionEventType, + mdAccNo = "", + category = TransactionCategory.CUSTOMER_CREDITED.name, + bankName = "", + bankIconUrl = "", + isNaviMerch = false, // Dummy value, actual would come in transaction sync + bankCode = "", + isMerchant = true, + isMerchantVerified = false, + lrn = null, + bAccType = "", + bAccUnqId = "", + bAccIfsc = "", + upiNumber = "", + ) + + val metaData = + TransactionDetailMetaData( + role = TransactionRole.PAYER.name, + npTxnId = sendMoneyResponse.naviPayTransactionId ?: "", + upiReqId = sendMoneyResponse.upiRequestId, + txnStatus = sendMoneyResponse.status, + txnType = UpiTransactionType.SCAN_PAY.name, + txnReqType = sendMoneyResponse.txnRequestType, + amount = getFormattedPaymentAmount(), + currency = DEFAULT_UPI_CURRENCY, + remarks = naviPayDefaultConfig.config.configMessage.sendMoneyDefaultRemarks, + txnTimestamp = + sendMoneyResponse.transactionTimestamp + ?: DateTime.now(DateTimeZone.UTC), // DB always stores in UTC + instrumentId = "", + instrumentType = "", + gwTxnId = "", + gwRefId = sendMoneyResponse.txnReference ?: sendMoneyResponse.upiRequestId, + gwPayerResCode = "", + gwPayeeResCode = "", + gwPayerRevResCode = "", + gwPayeeRevResCode = "", + arpc = null, + purposeCode = UPI_GLOBAL_PURPOSE, + initiationMode = UPI_GLOBAL_DEFAULT_INITIATION_MODE, + collectRequestExpiryMinutes = null, + responseCode = "", + naviPayRequestType = "", + transactionRef = "", + lastUpdatedAt = DateTime.now(DateTimeZone.UTC).toString(), + externalMetadata = null, + transactionInitiationMode = TransactionInitiationType.PAY_TO_UPI_ID.name, + baseCurr = state.value.baseCurr, + baseAmount = state.value.paymentAmountInForeignCurrency, + forex = state.value.forex.toString(), + markUp = state.value.markUp.toString(), + isArcProtected = false, + ) + + val transactionDetailEntity = + TransactionDetailEntity( + payerInfo = payerInfo, + payeeInfo = payeeInfo, + metaData = metaData, + ) + + val transactionEntity = transactionDetailEntity.toTransactionEntity() + + transactionRepository.insertTransactions(transactionEntityList = listOf(transactionEntity)) + + // After transaction entity is inserted, we re-fetch from DB for updated timestamp as per + // timezone + + return transactionRepository.getTransactionEntity( + upiRequestId = sendMoneyResponse.upiRequestId, + transactionEventType = sendMoneyResponse.transactionEventType, + ) ?: transactionEntity + } + + private suspend fun processPaymentFailure( + response: RepoResult, + accountId: String, + ) { + updateCtaLoaderState(showCtaLoader = false) + if ( + response.errors?.get(0)?.code == "IN" || response.errors?.get(0)?.code == "SD" + ) { // Global account deactivated case + updateBottomSheetUIState( + showBottomSheet = false, + bottomSheetStateChange = true, + bottomSheetUIState = GlobalSendMoneyBottomSheetType.UpiGlobalDeactivated, + ) + } else { + updateBottomSheetUIState(showBottomSheet = false) + updateScreenState(screenState = GlobalSendMoneyScreenState.MainScreen) + notifyError(response, extras = mapOf(ACCOUNT_ID to accountId)) + } + } + + private fun linkedAccountsListener() { + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + bankAccountsState = GlobalBankAccountsState.Loading, + selectedBankAccount = null, + ) + } + linkedAccountsUseCase.execute().collect { accounts -> + val postProcessedLinkedAccounts = + postProcessLinkedAccounts(linkedAccounts = accounts) + _state.update { + it.copy( + bankAccountsState = + GlobalBankAccountsState.AccountList( + accounts = postProcessedLinkedAccounts + ) + ) + } + _state.update { + it.copy(selectedBankAccount = getSelectedBankAccountForGlobalSendMoney()) + } + updateMainCtaButtonState() + } + } + } + + private suspend fun postProcessLinkedAccounts( + linkedAccounts: List + ): List { + + // Bank uptime status update + val updatedLinkedAccountsWithBankUpTime = + updateBankUptimeStatusToLinkedAccounts(linkedAccounts = linkedAccounts) + + // Account eligibility status update + val updatedLinkedAccountsWithEligibility = + updateAccountEligibilityStatusInLinkedAccounts( + linkedAccounts = updatedLinkedAccountsWithBankUpTime + ) + + return updatedLinkedAccountsWithEligibility + } + + private suspend fun updateBankUptimeStatusToLinkedAccounts( + linkedAccounts: List + ): List { + val bankUptimeEntitiesMap = + bankUptimeRepository.getBankUptimeEntitiesMapForIfscList( + ifscList = linkedAccounts.map { it.ifsc } + ) + + val updatedLinkedAccounts = + linkedAccounts.map { linkedAccount -> + val bankUptimeEntity = + bankUptimeEntitiesMap[linkedAccount.ifsc.getIfscForBankUptime()] + linkedAccount.bankUptimeEntity = bankUptimeEntity + linkedAccount + } + + return updatedLinkedAccounts + } + + private suspend fun updateAccountEligibilityStatusInLinkedAccounts( + linkedAccounts: List + ): List { + val updatedLinkedAccounts = + linkedAccounts.map { linkedAccount -> + val eligibilityState = + getEligibilityStateForLinkedAccount(linkedAccountEntity = linkedAccount) + linkedAccount.eligibilityState = eligibilityState + linkedAccount + } + + return updatedLinkedAccounts + } + + private suspend fun getEligibilityStateForLinkedAccount( + linkedAccountEntity: LinkedAccountEntity + ): EligibilityState { + if (isLinkedAccountEligibleForGlobalPayment(linkedAccountEntity = linkedAccountEntity)) { + when (linkedAccountEntity.upiGlobalInfo?.status) { + UpiGlobalAccountStatus.INIT_ACTIVE -> { + return EligibilityState( + isAccountEligible = false, + inEligibilityReasonResId = R.string.np_upi_global_activation_progress, + ) + } + + UpiGlobalAccountStatus.INIT_DEACTIVE -> { + return EligibilityState( + isAccountEligible = false, + inEligibilityReasonResId = R.string.np_upi_global_deactivation_progress, + ) + } + + UpiGlobalAccountStatus.DEACTIVE -> { + if (!linkedAccountEntity.isMPinSet) { + return EligibilityState( + isAccountEligible = true, + inEligibilityReasonResId = R.string.mpin_not_set, + ) + } + return EligibilityState( + isAccountEligible = true, + inEligibilityReasonResId = R.string.np_upi_global_not_activated, + ) + } + + UpiGlobalAccountStatus.ACTIVE, + null -> Unit + } + } else { + return EligibilityState( + isAccountEligible = false, + inEligibilityReasonResId = R.string.np_upi_global_not_eligible, + ) + } + + val bankUptimeStatus = linkedAccountEntity.bankUptimeEntity?.status + + if (bankUptimeStatus == BankUptimeStatus.DOWN) { + return EligibilityState( + isAccountEligible = false, + inEligibilityReasonResId = R.string.np_bank_down_message, + inEligibilityReasonIconId = CommonR.drawable.ic_exclamation_red_border, + inEligibilityReasonTextColor = NaviPayColor.inputFieldError, + shouldShowShakeAnimation = true, + inEligibilityReasonBgColor = NaviPayColor.bgError, + ) + } + + return EligibilityState( + isAccountEligible = true, + inEligibilityReasonResId = + if (bankUptimeStatus == BankUptimeStatus.WARNING) R.string.np_bank_warning_message + else RESOURCE_DEFAULT_ID, + inEligibilityReasonIconId = + if (bankUptimeStatus == BankUptimeStatus.WARNING) + CommonR.drawable.ic_exclamation_red_border + else CommonR.drawable.ic_purple_exclamation, + inEligibilityReasonTextColor = + if (bankUptimeStatus == BankUptimeStatus.WARNING) NaviPayColor.inputFieldError + else NaviPayColor.textPrimary, + shouldShowShakeAnimation = bankUptimeStatus == BankUptimeStatus.WARNING, + ) + } + + private suspend fun isLinkedAccountEligibleForGlobalPayment( + linkedAccountEntity: LinkedAccountEntity + ): Boolean { + return !AccountType.isAccountOfTypeCreditCardOrCreditLine( + type = linkedAccountEntity.accountType + ) && isUpiGlobalSupported(bankCode = linkedAccountEntity.bankCode) + } + + private suspend fun isUpiGlobalSupported(bankCode: String): Boolean { + val bankEntity = + bankRepository.getBankUiModelListFromBankCodes(listOf(bankCode)).firstOrNull() + return bankEntity?.isUpiInternationalSupported ?: false + } + + private fun getSelectedBankAccountForGlobalSendMoney(): LinkedAccountEntity? { + return state.value.bankAccountsState.let { bankAccountsState -> + when (bankAccountsState) { + is GlobalBankAccountsState.AccountList -> { + state.value.previousSelectedBankAccount?.let { previousSelectedAccount -> + if ( + previousSelectedAccount.eligibilityState.isAccountEligible && + previousSelectedAccount.upiGlobalInfo != null && + previousSelectedAccount.upiGlobalInfo.status == + UpiGlobalAccountStatus.ACTIVE + ) { + return previousSelectedAccount + } + } + bankAccountsState.accounts + .filter { linkedAccountEntity -> + linkedAccountEntity.eligibilityState.isAccountEligible && + linkedAccountEntity.upiGlobalInfo != null && + linkedAccountEntity.upiGlobalInfo.status == + UpiGlobalAccountStatus.ACTIVE + } + .maxByOrNull { + it.bankUptimeEntity?.successRate ?: DEFAULT_BANK_UP_TIME_SUCCESS_RATE + } ?: bankAccountsState.accounts.firstOrNull() + } + + else -> null + } + } + } + + private fun activateUpiGlobalAccount() { + viewModelScope.launch(Dispatchers.IO) { + val selectedBankAccount = state.value.selectedBankAccount + selectedBankAccount?.let { + activateUpiGlobalUseCase.execute( + screenName = screenName, + selectedBankAccount = state.value.selectedBankAccount!!, + onOnboardingTriggered = {}, + onOnboardingSuccess = {}, + onActivateUpiGlobalClicked = { + _state.update { it.copy(showCtaLoader = true) } + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetStateChange = false, + ) + }, + onNpciResult = { npciResult, upiRequestId, customerOnboardingEntity -> + when (npciResult) { + is NpciResult.Success -> { + updateBottomSheetUIState(showBottomSheet = false) + activateUpiGlobalPostCL( + upiRequestId = upiRequestId, + linkedAccountEntity = selectedBankAccount, + credBlock = npciResult.data, + customerOnboardingEntity = customerOnboardingEntity, + ) + } + + is NpciResult.Error -> { + updateBottomSheetUIState(showBottomSheet = false) + _state.update { it.copy(showCtaLoader = false) } + handleNpciError(npciResult = npciResult) + } + + else -> Unit + } + }, + onNoInternetError = { notifyError(getNoInternetErrorConfig()) }, + onAirplaneModeError = { notifyError(getAirplaneModeOnErrorConfig()) }, + onSimFailureError = { isNoSimPresent -> + notifyError(getSimFailureErrorConfig(isNoSimPresent)) + }, + ) + } + } + } + + private fun handleNpciError(npciResult: NpciResult.Error) { + if (!npciResult.isUserAborted) { + updateBottomSheetUIState(showBottomSheet = false) + notifyError() + } + } + + private suspend fun activateUpiGlobalPostCL( + upiRequestId: String, + linkedAccountEntity: LinkedAccountEntity, + credBlock: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) { + _state.update { it.copy(showCtaLoader = false) } + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetStateChange = false, + bottomSheetUIState = + GlobalSendMoneyBottomSheetType.UpiGlobalLoading( + title = + resourceProvider.getString(R.string.np_upi_global_activation_description) + ), + ) + val globalActivationRequest = + GlobalActivationRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = customerOnboardingEntity.merchantCustomerId, + upiRequestId = upiRequestId, + credBlock = credBlock, + baccId = linkedAccountEntity.accountId, + endDate = getDayEndDateTime(state.value.selectedUpiGlobalEndDate), + ) + val globalActivationResponse = + upiGlobalRepository.activateGlobalAccount( + globalActivationRequest = globalActivationRequest, + metricInfo = getMetricInfo(screenName = screenName), + ) + + if (globalActivationResponse.isSuccessWithData()) { + val globalActivationResponseData = globalActivationResponse.data!! + val endDate = globalActivationResponseData.endDate ?: DateTime.now() + val status = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus(globalActivationResponseData.status) + + updateLinkedAccountStatus() + + when (status) { + UpiGlobalAccountStatus.ACTIVE -> { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetStateChange = true, + bottomSheetUIState = + GlobalSendMoneyBottomSheetType.UpiGlobalStatusSuccess( + title = + resourceProvider.getString( + R.string.np_upi_global_activated_successfully, + DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = endDate, + format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, + ), + ) + ), + ) + updateMainCtaButtonState() + delay(1000) + updateBottomSheetUIState(showBottomSheet = false) + } + + UpiGlobalAccountStatus.INIT_ACTIVE -> { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetStateChange = true, + bottomSheetUIState = + GlobalSendMoneyBottomSheetType.UpiGlobalStatusPending( + descriptionId = R.string.np_upi_global_activation_pending + ), + ) + pollForStatus(linkedAccount = linkedAccountEntity) + } + + else -> { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetStateChange = true, + bottomSheetUIState = + GlobalSendMoneyBottomSheetType.UpiGlobalStatusActivationFailed( + descriptionId = R.string.np_global_payment_not_permitted + ), + ) + } + } + } else { + updateBottomSheetUIState(showBottomSheet = false) + notifyError(response = globalActivationResponse) + } + } + + private fun pollForStatus(linkedAccount: LinkedAccountEntity) { + viewModelScope.launch(Dispatchers.IO) { + val upiGlobalInfo = linkedAccount.upiGlobalInfo ?: return@launch + + if (!isStatusPending(upiGlobalInfo.status)) { + return@launch + } + + naviApiPoller + .startPolling { + upiGlobalRepository.globalAccountStatusCheck( + globalAccountStatusRequest = + GlobalAccountStatusRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = deviceInfoProvider.getMerchantCustomerId(), + baccId = linkedAccount.accountId, + ), + metricInfo = getMetricInfo(screenName = screenName), + ) + } + .collect { + try { + val activationStatusAPIResponse = + it as RepoResult + handlePollingResponse( + activationStatusAPIResponse = activationStatusAPIResponse + ) + } catch (_: Exception) { + naviApiPoller.stopPolling() + } + } + } + } + + private fun handlePollingResponse( + activationStatusAPIResponse: RepoResult + ) { + if (!activationStatusAPIResponse.isSuccessWithData()) { + naviApiPoller.stopPolling() + return + } + + val activationStatusData = activationStatusAPIResponse.data!! + val activationStatus = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus(activationStatusData.status) + + updateLinkedAccountStatus() + + when (activationStatus) { + UpiGlobalAccountStatus.ACTIVE -> { + if ( + state.value.bottomSheetStateHolder.bottomSheetUIState + is GlobalSendMoneyBottomSheetType.UpiGlobalStatusPending + ) { + updateBottomSheetUIState(showBottomSheet = false) + } + naviApiPoller.stopPolling() + } + + UpiGlobalAccountStatus.DEACTIVE -> { + naviApiPoller.stopPolling() + } + + UpiGlobalAccountStatus.INIT_ACTIVE, + UpiGlobalAccountStatus.INIT_DEACTIVE -> {} + } + } + + private fun updateLinkedAccountStatus() { + viewModelScope.launch(Dispatchers.IO) { + _state.update { it.copy(previousSelectedBankAccount = state.value.selectedBankAccount) } + refreshLinkedAccountsUseCase.execute(screenName = screenName) + } + } + + private fun getGlobalSendMoneyBackAction(): GlobalSendMoneyNavigationAction { + return when { + shouldGoToTransactions() -> GlobalSendMoneyNavigationAction.GoToTransactions + shouldDisableSendMoneyBackPress() -> GlobalSendMoneyNavigationAction.DoNothing + else -> GlobalSendMoneyNavigationAction.GoBack + } + } + + private fun shouldDisableSendMoneyBackPress(): Boolean { + return state.value.sendMoneyScreenState is + GlobalSendMoneyScreenState.PaymentInProgressPostPinInput && + latestTransactionStartedAt != null && + (System.currentTimeMillis() - latestTransactionStartedAt!!) <= + SEND_MONEY_BACK_PRESS_BLOCK_TIME_MILLIS + } + + private fun shouldGoToTransactions(): Boolean { + return state.value.sendMoneyScreenState is + GlobalSendMoneyScreenState.PaymentInProgressPostPinInput && + latestTransactionStartedAt != null && + (System.currentTimeMillis() - latestTransactionStartedAt!!) > + SEND_MONEY_BACK_PRESS_BLOCK_TIME_MILLIS + } + + private fun getMaxSendMoneyAmount(): Double { + return naviPayDefaultConfig.config.amountLimits.sendMoneyDefaultLimit + } + + private fun onAmountChanged(amount: String) { + val amountFiltered = amount.filter { it.isDigit() || it == '.' } + + if (!isConvertedAmountValid(amountFiltered)) { + return + } + _state.update { + it.copy( + paymentAmountInForeignCurrency = + SendMoneyUtils.getValidatedAmountNumber( + text = amountFiltered, + amountMaxLengthAfterDecimal = AMOUNT_MAX_LENGTH_AFTER_DECIMAL, + amountMaxLengthBeforeDecimal = AMOUNT_MAX_LENGTH_BEFORE_DECIMAL, + ) + ) + } + + updatePaymentAmount() + val paymentAmountInINR = getAmountInINR() + + _state.update { + it.copy( + paymentAmountInINR = paymentAmountInINR, + isPaymentAmountNonZero = paymentAmountInINR != 0.0, + warningErrorInfoState = + WarningErrorInfoState( + isWarningState = false, + isErrorState = isAmountInValid(paymentAmountInINR), + ), + ) + } + if ( + amountFiltered.isNotEmpty() && + (calculateConvertedAmountFromForeignAmount(amountFiltered.toDouble()) > + getMaxSendMoneyAmount() || + !calculateConvertedAmountFromForeignAmount(amountFiltered.toDouble()) + .toString() + .isAmountValidForSendMoney()) + ) { + updateErrorOrWarningState( + WarningErrorInfoState( + isWarningState = false, + isErrorState = true, + errorMessage = getWarningOrErrorMessage(), + ) + ) + } + updateMainCtaButtonState() + } + + private fun updatePaymentAmount() { + _state.update { + it.copy( + paymentAmount = + TextFieldValue( + state.value.paymentAmountInForeignCurrency, + selection = TextRange(state.value.paymentAmountInForeignCurrency.length), + ) + ) + } + } + + private fun isConvertedAmountValid(amountFiltered: String): Boolean { + val (integerPart, fractionalPart) = + amountFiltered.split('.').let { parts -> + val beforeDecimal = parts.getOrNull(0) ?: "" + val afterDecimal = parts.getOrNull(1) ?: "" + beforeDecimal to afterDecimal + } + if (amountFiltered.isEmpty()) return true + if ( + integerPart.length > AMOUNT_MAX_LENGTH_BEFORE_DECIMAL || + fractionalPart.length > AMOUNT_MAX_LENGTH_AFTER_DECIMAL + ) + return false + val amount = amountFiltered.toDoubleOrNull() ?: return false + return true + } + + private fun getAmountInINR(): Double { + val amountInForeignCurrency = state.value.paymentAmountInForeignCurrency.toDoubleOrNull() + val convertedAmount = + amountInForeignCurrency?.let { calculateConvertedAmountFromForeignAmount(it) } ?: 0.0 + return roundOffToTwoDecimalPlaces(convertedAmount) + } + + private fun calculateConvertedAmountFromForeignAmount(foreignAmount: Double): Double { + val inr = foreignAmount * state.value.forex * (100.0 + state.value.markUp) / 100.0 + return roundOffToTwoDecimalPlaces(inr) + } + + private fun getFormattedPaymentAmount() = + String.format(locale = Locale.US, "%.2f", state.value.paymentAmountInINR) + + @OptIn(ExperimentalCoroutinesApi::class) + private fun clearReplayCache() { + viewModelScope.launch(Dispatchers.IO) { _effect.resetReplayCache() } + } + + private fun updateNaviPayDefaultConfig() { + viewModelScope.launch(Dispatchers.IO) { + naviPayDefaultConfig = + naviPayConfigUseCase.execute( + configKey = DEFAULT_CONFIG, + type = object : TypeToken() {}.type, + screenName = screenName, + ) ?: NaviPayDefaultConfig() + } + } + + private fun getMainCtaButtonState(): GlobalSendMoneyMainCtaState { + val ctaTextWithAmount = + "Pay" + + if (state.value.isPaymentAmountNonZero) + " ₹" + state.value.paymentAmountInINR.toString().getDisplayableAmount() + else "" + return if (state.value.selectedBankAccount == null) + GlobalSendMoneyMainCtaState.Disabled(ctaText = ctaTextWithAmount) + else if (state.value.selectedBankAccount?.isMPinSet == false) { + GlobalSendMoneyMainCtaState.SetPin( + ctaText = resourceProvider.getString(R.string.set_upi_pin) + ) + } else if ( + !state.value.selectedBankAccount?.eligibilityState?.isAccountEligible.orFalse() + ) { + GlobalSendMoneyMainCtaState.Disabled(ctaText = ctaTextWithAmount) + } else if ( + state.value.selectedBankAccount?.upiGlobalInfo?.status != UpiGlobalAccountStatus.ACTIVE + ) + GlobalSendMoneyMainCtaState.ActivateUpiGlobal( + ctaText = resourceProvider.getString(R.string.np_activate_upi_global) + ) + else if (state.value.isPayeeDetailsLoading) { + GlobalSendMoneyMainCtaState.Disabled(ctaText = ctaTextWithAmount) + } else if (!state.value.isConsentCheckBoxChecked) + GlobalSendMoneyMainCtaState.Disabled(ctaText = ctaTextWithAmount) + else GlobalSendMoneyMainCtaState.Pay(ctaText = ctaTextWithAmount) + } + + private suspend fun shouldBlockApiCallBecauseOfInternetOrSimOrAirplane(): Boolean { + if (!checkIsInternetAvailableOrShowError()) return true + if (checkIsAirplaneModeOnOrShowError()) return true + + val currentSimInfoList = naviPayNetworkConnectivity.getCurrentSimInfoList() + if (!validateSimInfoOrShowError(currentSimInfoList)) return true + + return false + } + + private fun checkIsInternetAvailableOrShowError(): Boolean { + if (!naviPayNetworkConnectivity.isInternetConnected()) { + notifyError(getNoInternetErrorConfig()) + return false + } + return true + } + + private fun checkIsAirplaneModeOnOrShowError(): Boolean { + if (naviPayNetworkConnectivity.isAirplaneModeOn()) { + notifyError(getAirplaneModeOnErrorConfig()) + return true + } + return false + } + + private suspend fun validateSimInfoOrShowError(currentSimInfoList: List): Boolean { + val isSimInfoValid = + NaviPayCommonUtils.isSimInfoValid( + currentSimInfoList = currentSimInfoList, + deviceInfoProvider = deviceInfoProvider, + ) + if (!isSimInfoValid) { + notifyError(getSimFailureErrorConfig(isNoSimPresent = currentSimInfoList.isEmpty())) + return false + } + return true + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun onEvent(event: UpiGlobalSendMoneyScreenContract.Event) { + when (event) { + is UpiGlobalSendMoneyScreenContract.Event.OnConsentCheckBoxClicked -> { + onConsentCheckBoxClicked() + } + + UpiGlobalSendMoneyScreenContract.Event.OnMainCtaButtonClicked -> { + when (state.value.mainCtaState) { + is GlobalSendMoneyMainCtaState.Disabled -> {} + is GlobalSendMoneyMainCtaState.Pay -> { + onPayButtonClicked() + } + + is GlobalSendMoneyMainCtaState.ActivateUpiGlobal -> { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetStateChange = true, + bottomSheetUIState = + GlobalSendMoneyBottomSheetType.UpiGlobalDateSelection, + ) + } + + is GlobalSendMoneyMainCtaState.SetPin -> { + emitRedirectionEffect( + direction = + LinkedAccountVerifyScreenDestination( + accountId = + state.value.selectedBankAccount?.accountId.orEmpty(), + actionName = ACTION_PIN_SET, + ) + ) + updateBottomSheetUIState(showBottomSheet = false) + } + } + } + + is UpiGlobalSendMoneyScreenContract.Event.OnAmountChanged -> { + onAmountChanged(event.amount.text) + } + + UpiGlobalSendMoneyScreenContract.Event.ResetLottieSpecs -> { + resetLottieSpecs() + } + + UpiGlobalSendMoneyScreenContract.Event.OnLottieProgressTransition -> { + onLottieProgressTransition() + } + + UpiGlobalSendMoneyScreenContract.Event.OnBackClicked -> { + onBackClicked() + } + + is UpiGlobalSendMoneyScreenContract.Event.OnBottomSheetStateChanged -> { + emitKeyboardVisibilityEffect(isKeyBoardVisible = false) + updateBottomSheetUIState( + showBottomSheet = event.showBottomSheet, + bottomSheetStateChange = event.bottomSheetStateChange, + bottomSheetUIState = event.bottomSheetUIState, + ) + } + + is UpiGlobalSendMoneyScreenContract.Event.OnBankAccountSelected -> { + naviPayAnalytics.onBankSelected( + naviPaySessionAttributes = getNaviPaySessionAttributes(), + bankUptimeStatus = + event.linkedAccountEntity.bankUptimeEntity?.status?.name.toString(), + bankAccountUniqueId = event.linkedAccountEntity.accountId, + ) + onBankAccountSelected(event = event) + } + + UpiGlobalSendMoneyScreenContract.Event.OnViewHistoryClicked -> { + naviPayAnalytics.onViewHistoryClicked( + upiRequestId = state.value.validateQrUpiRequestId, + source = upiGlobalSendMoneySource, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + emitRedirectionEffect( + direction = + OrderHistoryScreenDestination( + payeeTransactionHistoryEntity = + PayeeTransactionHistoryEntity( + vpa = state.value.payeeVpa, + isPayeeLevelTransactionHistory = true, + name = state.value.payeeName, + vpaToDisplay = state.value.payeeVpa, + ) + ) + ) + } + + UpiGlobalSendMoneyScreenContract.Event.OnHelpClicked -> { + naviPayAnalytics.onHelpClicked( + bankName = state.value.selectedBankAccount?.bankName.toString(), + bankAccountId = state.value.selectedBankAccount?.accountId.toString(), + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + viewModelScope.launch(Dispatchers.IO) { + _effect.emit(NavigateToHelpScreen(helpCtaData)) + } + } + + UpiGlobalSendMoneyScreenContract.Event.OnActivateGlobalClicked -> { + activateUpiGlobalAccount() + } + + is UpiGlobalSendMoneyScreenContract.Event.OnUpiGlobalEndDateSelected -> { + onUpiGlobalEndDateSelected( + selectedUpiGlobalEndDate = event.selectedUpiGlobalEndDate + ) + } + + is UpiGlobalSendMoneyScreenContract.Event.OnFocusAmount -> { + emitAmountFocusEvent() + } + + UpiGlobalSendMoneyScreenContract.Event.OnConversionDetailClicked -> { + emitKeyboardVisibilityEffect(isKeyBoardVisible = false) + naviPayAnalytics.onViewConversionDetailClicked( + upiRequestId = state.value.validateQrUpiRequestId, + source = upiGlobalSendMoneySource, + currency = state.value.baseCurr, + amountInINR = state.value.paymentAmountInINR.toString(), + currencyAmount = state.value.paymentAmountInForeignCurrency, + bankForex = state.value.forex.toString(), + ) + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = GlobalSendMoneyBottomSheetType.ConversionDetails, + bottomSheetStateChange = true, + ) + } + + is UpiGlobalSendMoneyScreenContract.Event.OnBankDropDownClicked -> { + naviPayAnalytics.onBankDropDownClicked( + naviPaySessionAttributes = getNaviPaySessionAttributes(), + bankAccountUniqueId = event.bankAccountUniqueId, + bankUptimeStatus = event.bankUptimeStatus, + ) + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = GlobalSendMoneyBottomSheetType.AccountSelection, + bottomSheetStateChange = true, + ) + } + + UpiGlobalSendMoneyScreenContract.Event.OnEffectCollected -> { + _effect.resetReplayCache() + } + } + } + + private fun emitErrorVibrationAndShakeAnimation() { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit( + TriggerErrorVibrationAndShakeAnimation( + triggerErrorVibrationAndShakeAnimation = true + ) + ) + } + } + + private fun emitRedirectionEffect(direction: Direction, shouldClearBackStack: Boolean = false) { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit( + Navigate( + globalSendMoneyNavigationAction = + GlobalSendMoneyNavigationAction.Redirection( + direction = direction, + shouldClearBackStack = shouldClearBackStack, + ) + ) + ) + } + } + + private fun onBackClicked() { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit(Navigate(globalSendMoneyNavigationAction = getGlobalSendMoneyBackAction())) + } + } + + private fun emitKeyboardVisibilityEffect(isKeyBoardVisible: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit(KeyboardVisibility(isKeyBoardVisible = isKeyBoardVisible)) + } + } + + private fun emitAmountFocusEvent() { + if (!state.value.isDynamicQr) { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit(AutoFocusOnAmount(shouldAutoFocusOnAmount = true)) + } + } + } + + private fun onBankAccountSelected( + event: UpiGlobalSendMoneyScreenContract.Event.OnBankAccountSelected + ) { + _state.update { it.copy(selectedBankAccount = event.linkedAccountEntity) } + updateMainCtaButtonState() + } + + private fun onConsentCheckBoxClicked() { + _state.update { it.copy(isConsentCheckBoxChecked = !state.value.isConsentCheckBoxChecked) } + updateMainCtaButtonState() + } + + fun updateBottomSheetUIState( + showBottomSheet: Boolean, + bottomSheetStateChange: Boolean? = null, + bottomSheetUIState: GlobalSendMoneyBottomSheetType? = null, + ) { + _state.update { + it.copy( + bottomSheetStateHolder = + GlobalSendMoneyScreenBottomSheetStateHolder( + showBottomSheet = showBottomSheet, + bottomSheetStateChange = + bottomSheetStateChange + ?: it.bottomSheetStateHolder.bottomSheetStateChange, + bottomSheetUIState = + bottomSheetUIState ?: it.bottomSheetStateHolder.bottomSheetUIState, + ) + ) + } + } + + private fun resetLottieSpecs() { + viewModelScope.launch(Dispatchers.IO) { + delay(300) // Delay to wait for lottie to move out of screen + _state.update { + it.copy( + isProcessingTextVisible = true, + isSuccessTextVisible = false, + lottieBackgroundColor = Color.White, + ) + } + } + } + + private fun onLottieProgressTransition() { + viewModelScope.launch(Dispatchers.IO) { + _effect.emit(StatusBarColorChange(color = R.color.navi_pay_onSurfaceHighlight)) + _state.update { + it.copy( + isProcessingTextVisible = false, + isSuccessTextVisible = true, + lottieBackgroundColor = NaviPayColor.inputFieldSuccess, + ) + } + } + } + + fun onAddAccountClicked() { + viewModelScope.launch(Dispatchers.IO) { + naviPayPspManager.handleAccountAdditionFlow( + screenName = screenName, + enabledAccountTypes = SAVINGS_ONLY_ENABLED_ACCOUNTS, + ) + } + } + + fun getNaviPaySessionAttributes(): Map = + naviPaySessionHelper.getNaviPaySessionAttributes() + + private fun updateErrorOrWarningState(warningErrorInfoState: WarningErrorInfoState?) { + _state.update { it.copy(warningErrorInfoState = warningErrorInfoState) } + } + + private fun updateMainCtaButtonState() { + _state.update { it.copy(mainCtaState = getMainCtaButtonState()) } + } + + fun updateCtaLoaderState(showCtaLoader: Boolean) { + _state.update { it.copy(showCtaLoader = showCtaLoader) } + } + + private fun updateUpiLiteBalance() { + viewModelScope.launch(Dispatchers.IO) { + _state.update { it.copy(upiLiteBalance = upiLiteBalanceUseCase.execute()) } + } + } + + private fun updatePayeeVpa() { + _state.update { + it.copy(payeeVpa = if (payeeEntity.vpa != "Default") payeeEntity.vpa else EMPTY) + } + } + + private fun onUpiGlobalEndDateSelected(selectedUpiGlobalEndDate: DateTime) { + _state.update { it.copy(selectedUpiGlobalEndDate = selectedUpiGlobalEndDate) } + } + + private fun updateScreenState(screenState: GlobalSendMoneyScreenState) { + _state.update { it.copy(sendMoneyScreenState = screenState) } + } + + private fun updatePayeeEntity(payeeEntity: PayeeEntity) { + _state.update { it.copy(payeeEntity = payeeEntity) } + } + + override val screenName: String + get() = "NaviPay_UpiGlobal_SendMoneyScreen" +} + +interface UpiGlobalSendMoneyScreenContract : + NaviPayViewModelContract< + UpiGlobalSendMoneyScreenContract.State, + UpiGlobalSendMoneyScreenContract.Effect, + UpiGlobalSendMoneyScreenContract.Event, + > { + + data class State( + val sendMoneyScreenState: GlobalSendMoneyScreenState = + GlobalSendMoneyScreenState.MainScreen, + val bankAccountsState: GlobalBankAccountsState = GlobalBankAccountsState.Loading, + val selectedBankAccount: LinkedAccountEntity? = null, + val previousSelectedBankAccount: LinkedAccountEntity? = null, + val isConsentCheckBoxChecked: Boolean = true, + val paymentAmountInForeignCurrency: String = "", // baseAmount + val paymentAmount: TextFieldValue = TextFieldValue(), + val paymentAmountInINR: Double = 0.0, // This is the converted amount for send money + val forex: Double = 1.0, + val markUp: Double = 0.0, + val isPaymentAmountNonZero: Boolean = false, + val mainCtaState: GlobalSendMoneyMainCtaState = + GlobalSendMoneyMainCtaState.Disabled(ctaText = "Pay"), + val payeeVpa: String = "", + val payeeName: String = "", + val payeeMcc: String = "", + val payeeType: String = "", + val baseCurr: String = "", + val isProcessingTextVisible: Boolean = true, + val isSuccessTextVisible: Boolean = false, + val lottieBackgroundColor: Color = Color.White, + val validateQrUpiRequestId: String = "", + val bottomSheetStateHolder: GlobalSendMoneyScreenBottomSheetStateHolder = + GlobalSendMoneyScreenBottomSheetStateHolder( + showBottomSheet = false, + bottomSheetStateChange = true, + bottomSheetUIState = GlobalSendMoneyBottomSheetType.AccountSelection, + ), + val isPayeeDetailsLoading: Boolean = true, + val warningErrorInfoState: WarningErrorInfoState? = + WarningErrorInfoState(isErrorState = false, isWarningState = false), + val upiLiteBalance: String = EMPTY, + val isDynamicQr: Boolean = false, + val triggerErrorVibrationAndShakeAnimation: Boolean = false, + val selectedUpiGlobalEndDate: DateTime? = null, + val showCtaLoader: Boolean = false, + val payeeEntity: PayeeEntity = getDefaultPayeeEntity(), + ) + + sealed class Effect { + data class Navigate(val globalSendMoneyNavigationAction: GlobalSendMoneyNavigationAction) : + Effect() + + data class StatusBarColorChange(val color: Int) : Effect() + + data class AutoFocusOnAmount(val shouldAutoFocusOnAmount: Boolean) : Effect() + + data class KeyboardVisibility(val isKeyBoardVisible: Boolean) : Effect() + + data class NavigateToHelpScreen(val ctaData: CtaData?) : Effect() + + data class TriggerErrorVibrationAndShakeAnimation( + val triggerErrorVibrationAndShakeAnimation: Boolean + ) : Effect() + } + + sealed class Event { + data object OnConsentCheckBoxClicked : Event() + + data object OnMainCtaButtonClicked : Event() + + data class OnAmountChanged(val amount: TextFieldValue) : Event() + + data object ResetLottieSpecs : Event() + + data object OnLottieProgressTransition : Event() + + data object OnBackClicked : Event() + + data object OnEffectCollected : Event() + + data class OnBottomSheetStateChanged( + val showBottomSheet: Boolean, + val bottomSheetStateChange: Boolean? = null, + val bottomSheetUIState: GlobalSendMoneyBottomSheetType? = null, + ) : Event() + + data class OnBankAccountSelected(val linkedAccountEntity: LinkedAccountEntity) : Event() + + data object OnViewHistoryClicked : Event() + + data object OnHelpClicked : Event() + + data object OnActivateGlobalClicked : Event() + + data class OnUpiGlobalEndDateSelected(val selectedUpiGlobalEndDate: DateTime) : Event() + + data object OnFocusAmount : Event() + + data object OnConversionDetailClicked : Event() + + data class OnBankDropDownClicked( + val bankAccountUniqueId: String, + val bankUptimeStatus: String, + ) : Event() + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt index d9bc6f1128..0554583741 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt @@ -123,10 +123,15 @@ import com.navi.pay.common.utils.drawCorner import com.navi.pay.destinations.MandateDetailScreenOfPendingCategoryDestination import com.navi.pay.destinations.QrScannerScreenDestination import com.navi.pay.destinations.SendMoneyScreenDestination +import com.navi.pay.destinations.UPIGlobalSendMoneyScreenDestination +import com.navi.pay.destinations.UpiGlobalScreenDestination import com.navi.pay.entry.NaviPayActivity import com.navi.pay.entry.NaviPayActivityDataProvider +import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity import com.navi.pay.management.common.sendmoney.model.view.SendMoneyScreenSource import com.navi.pay.management.common.sendmoney.model.view.UpiTransactionType +import com.navi.pay.management.global.models.view.UpiGlobalMainScreenSource +import com.navi.pay.management.global.models.view.UpiGlobalSendMoneyScreenSource import com.navi.pay.management.moneytransfer.scanpay.model.view.QrData import com.navi.pay.management.moneytransfer.scanpay.model.view.QrScanState import com.navi.pay.management.moneytransfer.scanpay.model.view.UriType @@ -167,6 +172,20 @@ fun QrScannerScreen(naviPayActivity: NaviPayActivity, navigator: DestinationsNav sendMoneyScreenSource = sendMoneyScreenSource, ) }, + onGlobalQrNavigation = { + qrContent, + payeeEntity, + upiGlobalSendMoneyScreenSource, + upiGlobalMainScreenSource, + _ -> + navigateToGlobal( + navigator = navigator, + qrContent = qrContent, + payeeEntity = payeeEntity, + upiGlobalSendMoneyScreenSource = upiGlobalSendMoneyScreenSource, + upiGlobalMainScreenSource = upiGlobalMainScreenSource, + ) + }, ) } @@ -179,6 +198,14 @@ fun QrScannerScreenContent( onBackClick: () -> Unit, onNavigation: (SendMoneyScreenSource.ScanAndPay, String, UriType, NaviPayActivityDataProvider) -> Unit, + onGlobalQrNavigation: + ( + String?, + PayeeEntity?, + UpiGlobalSendMoneyScreenSource?, + UpiGlobalMainScreenSource.QrScannerScreen?, + String, + ) -> Unit, qrScannerViewModel: QrScannerViewModel = hiltViewModel(), naviPayAnalytics: NaviPayAnalytics.NaviPayQrScanner = NaviPayAnalytics.INSTANCE.NaviPayQrScanner(), @@ -190,6 +217,8 @@ fun QrScannerScreenContent( val qrImageErrorViewEnable by qrScannerViewModel.qrImageErrorViewEnable.collectAsStateWithLifecycle() + val isUpiGlobalActive by qrScannerViewModel.isUpiGlobalActive.collectAsStateWithLifecycle() + val imagePickerListener = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> uri?.let { @@ -361,6 +390,7 @@ fun QrScannerScreenContent( LaunchedEffect(Unit) { qrScannerViewModel.qrScanResult.collectLatest { qrScanResult -> + qrScannerViewModel.clearQrScanReplayCache() when (qrScanResult) { is QrScanState.Success -> { closeSheet() @@ -379,6 +409,18 @@ fun QrScannerScreenContent( } else { NaviPayScreenType.NAVI_PAY_SEND_MONEY_SCREEN.name } + if (qrScanResult.isUpiGlobal && qrScannerViewModel.upiGlobalActiveFlag) { + qrScannerViewModel.qrScanSuccessResult = + QrData( + payeeEntity = qrScanResult.payeeEntity, + uriType = qrScanResult.uriType, + isUpiGlobal = true, + qrContent = qrScanResult.qrContent, + isQrFromUploadImage = qrScanResult.isQrFromUploadImage, + ) + qrScannerViewModel.handleGlobalSendMoneyQrScan() + return@collectLatest + } val sendMoneyScreenSource = if (qrScanResult.uriType == UriType.MANDATE) { // For QR mandate, initiation mode should always be 01 @@ -422,6 +464,37 @@ fun QrScannerScreenContent( } } + LaunchedEffect(Unit) { + qrScannerViewModel.isUserOnboardedForGlobal.collectLatest { isUserOnboardedForGlobal -> + qrScannerViewModel.clearOnboardingSdkReplayCache() + if (isUserOnboardedForGlobal) { + if (isUpiGlobalActive) { + onGlobalQrNavigation( + qrScannerViewModel.qrScanSuccessResult?.qrContent.toString(), + qrScannerViewModel.qrScanSuccessResult?.payeeEntity, + UpiGlobalSendMoneyScreenSource.QrScannerScreen, + null, + "$NAVI_PAY_CTA_URL_PREFIX${NaviPayScreenType.NAVI_PAY_GLOBAL_SEND_MONEY.name}", + ) + } else { + qrScannerViewModel.qrScanSuccessResult?.payeeEntity?.let { payeeEntity -> + onGlobalQrNavigation( + null, + null, + null, + UpiGlobalMainScreenSource.QrScannerScreen( + qrContent = + qrScannerViewModel.qrScanSuccessResult?.qrContent.toString(), + payeeEntity = payeeEntity, + ), + "$NAVI_PAY_CTA_URL_PREFIX${NaviPayScreenType.NAVI_PAY_GLOBAL_MAIN_SCREEN.name}", + ) + } + } + } + } + } + val colorAdjustedContent = remember(cameraPermissionsState.allPermissionsGranted) { if (cameraPermissionsState.allPermissionsGranted) { @@ -1059,6 +1132,14 @@ fun QrScreenComposable( onBackClick: () -> Unit, onNavigation: (SendMoneyScreenSource.ScanAndPay, String, UriType, NaviPayActivityDataProvider) -> Unit, + onGlobalQrNavigation: + ( + String?, + PayeeEntity?, + UpiGlobalSendMoneyScreenSource?, + UpiGlobalMainScreenSource.QrScannerScreen?, + String, + ) -> Unit, qrScreenVisible: Boolean, ) { val uiController = rememberSystemUiController() @@ -1093,6 +1174,7 @@ fun QrScreenComposable( toolbarTopMargin = 46.dp, onBackClick = onBackClick, onNavigation = onNavigation, + onGlobalQrNavigation = onGlobalQrNavigation, ) } } @@ -1129,3 +1211,27 @@ private fun navigateTo( ) } } + +private fun navigateToGlobal( + navigator: DestinationsNavigator, + qrContent: String? = null, + payeeEntity: PayeeEntity? = null, + upiGlobalSendMoneyScreenSource: UpiGlobalSendMoneyScreenSource? = null, + upiGlobalMainScreenSource: UpiGlobalMainScreenSource.QrScannerScreen? = null, +) { + if (upiGlobalSendMoneyScreenSource != null) { + navigator.navigate( + direction = + UPIGlobalSendMoneyScreenDestination( + qrContent = qrContent.toString(), + payeeEntity = payeeEntity, + upiGlobalSendMoneyScreenSource = UpiGlobalSendMoneyScreenSource.QrScannerScreen, + ) + ) + } else { + navigator.navigate( + direction = + UpiGlobalScreenDestination(upiGlobalMainScreenSource = upiGlobalMainScreenSource!!) + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/viewmodel/QrScannerViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/viewmodel/QrScannerViewModel.kt index c81af1b1b2..c618907f16 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/viewmodel/QrScannerViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/viewmodel/QrScannerViewModel.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.google.mlkit.vision.barcode.common.Barcode import com.navi.base.utils.ResourceProvider +import com.navi.base.utils.TrustedTimeAccessor import com.navi.common.di.CoroutineDispatcherProvider import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_AUTO_FLASH_DISABLED @@ -25,16 +26,24 @@ import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_QR_SCANNER import com.navi.pay.common.model.view.NaviPayButtonAction import com.navi.pay.common.model.view.NaviPayButtonTheme import com.navi.pay.common.model.view.NaviPayErrorButtonConfig +import com.navi.pay.common.model.view.NaviPayFlowType import com.navi.pay.common.model.view.NaviPaySessionHelper +import com.navi.pay.common.usecase.LinkedAccountsUseCase +import com.navi.pay.common.utils.getDefaultPayeeEntity import com.navi.pay.common.viewmodel.NaviPayBaseVM import com.navi.pay.entry.NaviPayActivityDataProvider import com.navi.pay.management.common.sendmoney.util.NaviPayOffersHelper +import com.navi.pay.management.common.utils.NaviPayPspManager +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus import com.navi.pay.management.moneytransfer.scanpay.LightSensorManager +import com.navi.pay.management.moneytransfer.scanpay.model.view.QrData import com.navi.pay.management.moneytransfer.scanpay.model.view.QrError import com.navi.pay.management.moneytransfer.scanpay.model.view.QrScanState import com.navi.pay.management.moneytransfer.scanpay.model.view.QrScannerBottomSheetStateHolder import com.navi.pay.management.moneytransfer.scanpay.model.view.UpiUriResult +import com.navi.pay.management.moneytransfer.scanpay.model.view.UriType import com.navi.pay.management.moneytransfer.scanpay.util.UpiUriParser +import com.navi.pay.management.moneytransfer.scanpay.util.UpiUriParser.Companion.FLOATING_VALUE_DOMESTIC import com.navi.pay.management.moneytransfer.scanpay.util.extractUriFromQR import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE_DISABLED @@ -45,11 +54,13 @@ import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -67,6 +78,8 @@ constructor( val naviPayActivityDataProvider: NaviPayActivityDataProvider, private val litmusExperimentsUseCase: LitmusExperimentsUseCase, private val naviPayOffersHelper: NaviPayOffersHelper, + private val linkedAccountsUseCase: LinkedAccountsUseCase, + private val naviPayPspManager: NaviPayPspManager, ) : NaviPayBaseVM() { private val naviPayAnalytics: NaviPayAnalytics.NaviPayQrScanner = @@ -82,7 +95,7 @@ constructor( ) val bottomSheetStateHolder = _bottomSheetStateHolder.asStateFlow() - private val _qrScanResult = MutableSharedFlow() + private val _qrScanResult = MutableSharedFlow(replay = 1) val qrScanResult = _qrScanResult.asSharedFlow() private val _qrImageErrorViewEnable = MutableStateFlow(false) @@ -108,6 +121,19 @@ constructor( private val _genericOffersList = MutableStateFlow>(emptyList()) val genericOffersList = _genericOffersList.asStateFlow() + private val _isUpiGlobalActive = MutableStateFlow(false) + val isUpiGlobalActive = _isUpiGlobalActive.asStateFlow() + + private val _isUserOnboardedForGlobal = MutableSharedFlow(replay = 1) + val isUserOnboardedForGlobal = _isUserOnboardedForGlobal.asSharedFlow() + + var qrScanSuccessResult: QrData? = null + + val upiGlobalActiveFlag = + FirebaseRemoteConfigHelper.getBoolean( + FirebaseRemoteConfigHelper.NAVI_PAY_UPI_GLOBAL_ENABLED + ) + init { init() } @@ -226,12 +252,29 @@ constructor( withContext(Dispatchers.Main) { _isTorchEnabled.value = true } } + private suspend fun updateUpiGlobalDataFromLinkedAccounts() { + val linkedAccounts = linkedAccountsUseCase.execute().first() + if (linkedAccounts.isNotEmpty()) { + _isUpiGlobalActive.update { + linkedAccounts.any { + val endDate = (it.upiGlobalInfo?.endDate?.millis ?: DateTime.now().millis) + it.upiGlobalInfo?.status?.name == UpiGlobalAccountStatus.ACTIVE.name && + endDate > TrustedTimeAccessor.getCurrentTimeMillis() + } + } + } + } + private fun updateNaviPaySessionId() { viewModelScope.launch(coroutineDispatcherProvider.io) { naviPaySessionHelper.createNewSessionId() } } + private fun preProcessQrContent(qrContent: String): String { + return qrContent.replace("&", "&") + } + fun unregisterLightSensorListener() { if (!FirebaseRemoteConfigHelper.getBoolean(key = NAVI_PAY_AUTO_FLASH_DISABLED)) { lightSensorManager.unregisterListener() @@ -277,10 +320,11 @@ constructor( } toggleIsAnyActionPerformed() isQrCodeProcessing.set(true) + val preProcessedQrContent = preProcessQrContent(qrContent) viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { when { - upiUriParser.isUpiUri(upiUri = qrContent) -> { - parseUpiQrCode(qrContent = qrContent, isQrFromUploadImage = sendQrFromGallery) + upiUriParser.isUpiUri(preProcessedQrContent) -> { + parseUpiQrCode(qrContent = preProcessedQrContent, sendQrFromGallery) } upiUriParser.isBharatQr(bharatQrContent = qrContent) -> { @@ -324,7 +368,7 @@ constructor( ) ) naviPayAnalytics.onNonUPIQRScanned( - qrContent = qrContent, + qrContent = preProcessedQrContent, naviPaySessionAttributes = getNaviPaySessionAttributes(), qrCount = barcodes.size, qrUri = extractUriFromQR(barcodes), @@ -357,9 +401,12 @@ constructor( } } - private fun parseUpiQrCode(qrContent: String, isQrFromUploadImage: Boolean) { + private suspend fun parseUpiQrCode(qrContent: String, isQrFromUploadImage: Boolean) { when (val upiResult = upiUriParser.parseForUpiUri(uri = qrContent)) { is UpiUriResult.Success -> { + if (upiResult.isUpiGlobal) { + updateUpiGlobalDataFromLinkedAccounts() + } updateQrScanResult( qrScanState = QrScanState.Success( @@ -367,6 +414,7 @@ constructor( uriType = upiResult.uriType, isUpiGlobal = upiResult.isUpiGlobal, isQrFromUploadImage = isQrFromUploadImage, + qrContent = qrContent, ) ) } @@ -385,7 +433,22 @@ constructor( } } - private fun parseBharatQr(qrContent: String, isQrFromUploadImage: Boolean) { + private suspend fun parseBharatQr(qrContent: String, isQrFromUploadImage: Boolean) { + // Global Bharat QR handling + if (!qrContent.contains(FLOATING_VALUE_DOMESTIC)) { + updateUpiGlobalDataFromLinkedAccounts() + updateQrScanResult( + qrScanState = + QrScanState.Success( + payeeEntity = getDefaultPayeeEntity(), + uriType = UriType.PAY, + isUpiGlobal = true, + isQrFromUploadImage = isQrFromUploadImage, + qrContent = qrContent, + ) + ) + return + } when (val upiResult = upiUriParser.getPayeeEntityFromBharatQr(qrContent = qrContent)) { is UpiUriResult.Success -> { updateQrScanResult( @@ -451,6 +514,40 @@ constructor( override val screenName: String get() = NAVI_PAY_QR_SCANNER + + suspend fun setUserOnboardedForGlobal(value: Boolean) { + _isUserOnboardedForGlobal.emit(value = value) + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun clearOnboardingSdkReplayCache() { + _isUserOnboardedForGlobal.resetReplayCache() + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun clearQrScanReplayCache() { + _qrScanResult.resetReplayCache() + } + + fun handleGlobalSendMoneyQrScan() { + viewModelScope.launch(coroutineDispatcherProvider.io) { + val linkedAccounts = linkedAccountsUseCase.execute().first() + if (linkedAccounts.isEmpty()) { + return@launch + } + naviPayPspManager.evaluateAndOnboardPspForFlow( + naviPayFlowType = NaviPayFlowType.QR_HANDLING, + vpaEntityList = linkedAccounts[0].vpaEntityList, + screenName = screenName, + onPspEvaluated = { pspEvaluationResult -> + if (pspEvaluationResult.onboardingDataEntity == null) { + return@evaluateAndOnboardPspForFlow + } + setUserOnboardedForGlobal(true) + }, + ) + } + } } sealed class QrScannerBottomSheetUIState { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/network/TransactionItem.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/network/TransactionItem.kt index 006d8df7c0..f1fb55af09 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/network/TransactionItem.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/network/TransactionItem.kt @@ -7,6 +7,7 @@ package com.navi.pay.management.transactionhistory.model.network +import com.navi.pay.common.utils.NaviPayCommonUtils.getTagStringWithSeparator import com.navi.pay.management.common.transaction.model.network.TransactionRole import com.navi.pay.management.common.transaction.util.getMonthTag import com.navi.pay.management.common.transaction.util.getOtherUserInfoWithSeparator @@ -55,5 +56,7 @@ fun TransactionDetailEntity.toTransactionEntity(): TransactionEntity { monthTag = getMonthTag(dateTime = metaData.txnTimestamp.toString()), paymentModeTags = getPaymentModeTagsWithSeparator(), ownBankInfo = getOwnBankInfoWithSeparator(), + upiGlobalCurrencyAndAmount = + getTagStringWithSeparator(this.metaData.baseCurr, this.metaData.baseAmount), ) } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionDetailEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionDetailEntity.kt index 4b6c7be2a3..f3714753c6 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionDetailEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionDetailEntity.kt @@ -96,4 +96,8 @@ data class TransactionDetailMetaData( @SerializedName("externalMetadata") val externalMetadata: String?, @SerializedName("transactionInitiationType") val transactionInitiationMode: String?, @SerializedName("isArcProtected") val isArcProtected: Boolean, + @SerializedName("baseCurr") val baseCurr: String? = null, + @SerializedName("baseAmount") val baseAmount: String? = null, + @SerializedName("forex") val forex: String? = null, + @SerializedName("markUp") val markUp: String? = null, ) : Parcelable diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionEntity.kt index 2af5b325d6..fdf4abd97e 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/transactionhistory/model/view/TransactionEntity.kt @@ -71,6 +71,7 @@ data class TransactionEntity( @ColumnInfo(name = "monthTag") val monthTag: String, @ColumnInfo(name = "paymentModeTags") val paymentModeTags: String, @ColumnInfo(name = "ownBankInfo") val ownBankInfo: String, + @ColumnInfo(name = "upiGlobalCurrencyAndAmount") val upiGlobalCurrencyAndAmount: String, ) : Parcelable { @IgnoredOnParcel diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt index f594d70319..9ab6ea0594 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt @@ -40,6 +40,7 @@ import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_13_14 import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_14_15 import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_15_16 import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_16_17 +import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_17_18 import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_1_2 import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_2_3 import com.navi.pay.db.NAVI_PAY_APP_DATABASE_MIGRATION_3_4 @@ -162,6 +163,7 @@ object NaviPayNetworkModule { NAVI_PAY_APP_DATABASE_MIGRATION_14_15, NAVI_PAY_APP_DATABASE_MIGRATION_15_16, NAVI_PAY_APP_DATABASE_MIGRATION_16_17, + NAVI_PAY_APP_DATABASE_MIGRATION_17_18, ) .addTypeConverter(MessageContentConverter()) .build() diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt index 652c856bf9..b9ac3c36a5 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt @@ -33,6 +33,14 @@ import com.navi.pay.management.common.sendmoney.model.network.NpsCommsRequest import com.navi.pay.management.common.sendmoney.model.network.SendMoneyAdditionalInfoRequest import com.navi.pay.management.common.sendmoney.model.network.SendMoneyRequest import com.navi.pay.management.common.sendmoney.model.network.TransactionResponse +import com.navi.pay.management.global.models.network.GlobalAccountStatusRequest +import com.navi.pay.management.global.models.network.GlobalAccountStatusResponse +import com.navi.pay.management.global.models.network.GlobalActivationRequest +import com.navi.pay.management.global.models.network.GlobalActivationResponse +import com.navi.pay.management.global.models.network.GlobalDeactivationRequest +import com.navi.pay.management.global.models.network.GlobalDeactivationResponse +import com.navi.pay.management.global.models.network.ValidateGlobalQrRequest +import com.navi.pay.management.global.models.network.ValidateGlobalQrResponse import com.navi.pay.management.lite.models.network.LiteRegistrationRequest import com.navi.pay.management.lite.models.network.LiteRegistrationResponse import com.navi.pay.management.lite.models.network.LiteSyncRequest @@ -394,4 +402,24 @@ interface NaviPayRetrofitService { suspend fun fetchDigitalGoldInvoiceDownloadUrl( @Path("referenceId") transactionId: String ): Response> + + @POST("/gateway-service/$NAVI_PAY_API_VERSION/navipay/global/activate") + suspend fun activateInternationalAccount( + @Body globalActivationRequest: GlobalActivationRequest + ): Response> + + @POST("/gateway-service/$NAVI_PAY_API_VERSION/navipay/global/deactivate") + suspend fun deactivateInternationalAccount( + @Body globalDeactivationRequest: GlobalDeactivationRequest + ): Response> + + @POST("/gateway-service/$NAVI_PAY_API_VERSION/navipay/global/status") + suspend fun internationalAccountStatusCheck( + @Body globalAccountStatusRequest: GlobalAccountStatusRequest + ): Response> + + @POST("/gateway-service/$NAVI_PAY_API_VERSION/navipay/global/validate/qr") + suspend fun validateInternationalQr( + @Body validateGlobalQrRequest: ValidateGlobalQrRequest + ): Response> } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredDataProvider.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredDataProvider.kt index 30b16f4a88..d18dbc409c 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredDataProvider.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredDataProvider.kt @@ -15,6 +15,7 @@ import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_COLLECT import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_MANDATE import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_PAY import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_SET_RESET_MPIN +import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_UPI_GLOBAL import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_UPI_LITE_LOAD_MONEY import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_UPI_LITE_REGISTRATION import com.navi.pay.npcicl.CredItemsProvider.Companion.CRED_TYPE_UPI_LITE_SEND_MONEY @@ -343,4 +344,32 @@ class CredDataProvider @Inject constructor() { payeeAddr = payeeEntity.vpa, ) } + + fun upiGlobalCred( + linkedAccountEntity: LinkedAccountEntity, + upiRequestId: String, + txnTimeStamp: String, + ): NpciCredData { + + val credItems = + CredItemsProvider() + .getCredItems(credType = CRED_TYPE_UPI_GLOBAL, accountEntity = linkedAccountEntity) + + val credTypeTxIdMap = hashMapOf(CLConstants.CRED_TYPE_BINDING to upiRequestId) + + return NpciCredData( + payInfoArray = + listOf( + PayInfo(name = KEY_ACCOUNT, value = linkedAccountEntity.maskedAccountNumber) + ), + npciConfiguration = + NpciConfiguration( + payerBankName = linkedAccountEntity.bankName, + bankImageUrl = linkedAccountEntity.bankIconImageUrl, + ), + credAllowed = CredAllowed(credAllowed = credItems), + credTypeTxIdMap = credTypeTxIdMap, + txnTimestamp = txnTimeStamp, + ) + } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredItemsProvider.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredItemsProvider.kt index 8218a0068c..7bfff0448d 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredItemsProvider.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/npcicl/CredItemsProvider.kt @@ -32,6 +32,7 @@ class CredItemsProvider { const val CRED_TYPE_UPI_LITE_REGISTRATION = "binding" const val CRED_TYPE_UPI_LITE_LOAD_MONEY = "upiLiteLoadMoney" const val CRED_TYPE_UPI_LITE_SEND_MONEY = "upiLiteSendMoney" + const val CRED_TYPE_UPI_GLOBAL = "upiGlobal" const val DEFAULT_REG_FORMAT = "NO_FORMAT" @@ -65,6 +66,7 @@ class CredItemsProvider { CRED_TYPE_UPI_LITE_LOAD_MONEY -> getUpiLiteLoadMoneyCredItems(accountEntity = accountEntity) CRED_TYPE_UPI_LITE_SEND_MONEY -> getUpiLiteSendMoneyCredItems() + CRED_TYPE_UPI_GLOBAL -> getUpiGlobalCredItems(accountEntity = accountEntity) else -> getOtherCredItems(accountEntity) } @@ -209,6 +211,16 @@ class CredItemsProvider { ) ) + private fun getUpiGlobalCredItems(accountEntity: LinkedAccountEntity) = + listOf( + CredItem( + type = CREDTYPE_PIN, + subtype = CREDTYPE_MPIN, + dType = CREDTYPE_DEBIT_NUM, + dLength = accountEntity.mPinLength, + ) + ) + private fun getOtherCredItems(accountEntity: LinkedAccountEntity) = listOf( CredItem( diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/util/AccountMappingUtils.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/util/AccountMappingUtils.kt index dfffc3e8e2..441c714000 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/util/AccountMappingUtils.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/util/AccountMappingUtils.kt @@ -13,14 +13,17 @@ import com.navi.pay.common.model.view.PspType import com.navi.pay.common.utils.getAccountId import com.navi.pay.common.utils.getAccountTypeFormattedValue import com.navi.pay.common.utils.getBankIconUrlFromBankCode +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus import com.navi.pay.management.lite.models.network.UPILiteEntityResponse import com.navi.pay.management.lite.models.view.InitialTopUpStatus import com.navi.pay.management.lite.models.view.UPILiteAccountStatus import com.navi.pay.management.lite.models.view.UPILiteEntity import com.navi.pay.onboarding.account.add.model.network.AccountItemResponse import com.navi.pay.onboarding.account.add.model.view.BankUiModel +import com.navi.pay.onboarding.account.common.model.network.UpiGlobalInfoEntityResponse import com.navi.pay.onboarding.account.common.model.view.AccountEntity import com.navi.pay.onboarding.account.common.model.view.AccountStatus +import com.navi.pay.onboarding.account.common.model.view.UpiGlobalInfoEntity import com.navi.pay.onboarding.account.common.model.view.VpaEntity import com.navi.pay.onboarding.account.common.model.view.VpaStatus import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity @@ -69,6 +72,12 @@ fun LinkedAccountItemResponse.toAccountEntity(): AccountEntity { upiLiteEntityResponse.toUPILiteEntity(pspType) } ?: emptyList() } ?: emptyList() + val upiGlobalInfoEntity = + pspToLinkedAccountDataMap + ?.mapNotNull { (pspType, linkedAccountPspData) -> + linkedAccountPspData.upiGlobalInfo?.toUpiGlobalInfoEntity(pspType) + } + ?.first() return AccountEntity( bankCode = bankCode, bankName = bankName, @@ -86,6 +95,7 @@ fun LinkedAccountItemResponse.toAccountEntity(): AccountEntity { status = AccountStatus.toAccountStatus(status = status), updatedAt = updatedAt, upiLiteInfo = upiLiteInfoList, + upiGlobalInfo = upiGlobalInfoEntity ?: UpiGlobalInfoEntity(), ) } @@ -120,6 +130,7 @@ fun AccountEntity.toLinkedAccountEntity(vpaEntityList: List): LinkedA upiLiteInfo = upiLiteInfo, accountTypeFormatted = getAccountTypeFormattedValue(accountType = type), updatedAt = updatedAt, + upiGlobalInfo = upiGlobalInfo, ) } @@ -150,4 +161,13 @@ private fun UPILiteEntityResponse.toUPILiteEntity(pspType: PspType): UPILiteEnti ) } +fun UpiGlobalInfoEntityResponse.toUpiGlobalInfoEntity(pspType: PspType): UpiGlobalInfoEntity { + return UpiGlobalInfoEntity( + status = UpiGlobalAccountStatus.toUpiGlobalAccountStatus(status), + startDate = startDate, + endDate = endDate, + pspType = pspType, + ) +} + fun vpaPrefixToVpa(vpaPrefix: String, pspType: PspType) = "$vpaPrefix${pspType.handle}" diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/model/view/AccountEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/model/view/AccountEntity.kt index 12bd5f2e4f..2d9a218ba5 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/model/view/AccountEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/model/view/AccountEntity.kt @@ -14,6 +14,7 @@ import androidx.room.TypeConverters import com.navi.pay.common.utils.DateTimeConverter import com.navi.pay.management.lite.models.view.UPILiteEntity import com.navi.pay.onboarding.account.common.util.UPILiteInfoConverter +import com.navi.pay.onboarding.account.common.util.UpiGlobalInfoConverter import com.navi.pay.utils.NAVI_PAY_DATABASE_ACCOUNTS_TABLE_NAME import org.joda.time.DateTime @@ -40,4 +41,7 @@ data class AccountEntity( @ColumnInfo(name = "upiLiteInfo") @TypeConverters(UPILiteInfoConverter::class) val upiLiteInfo: List = emptyList(), + @ColumnInfo(name = "upiGlobalInfo") + @TypeConverters(UpiGlobalInfoConverter::class) + val upiGlobalInfo: UpiGlobalInfoEntity, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/model/view/UpiGlobalInfoEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/model/view/UpiGlobalInfoEntity.kt new file mode 100644 index 0000000000..ee2a31ef15 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/model/view/UpiGlobalInfoEntity.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.onboarding.account.common.model.view + +import android.os.Parcelable +import com.navi.pay.common.model.view.PspType +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus +import kotlinx.parcelize.Parcelize +import org.joda.time.DateTime + +@Parcelize +data class UpiGlobalInfoEntity( + val status: UpiGlobalAccountStatus = UpiGlobalAccountStatus.DEACTIVE, + val startDate: DateTime? = DateTime.now(), + val endDate: DateTime? = DateTime.now(), + val pspType: PspType? = null, +) : Parcelable diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/util/UpiGlobalInfoConverter.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/util/UpiGlobalInfoConverter.kt new file mode 100644 index 0000000000..de176fb4bf --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/common/util/UpiGlobalInfoConverter.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.onboarding.account.common.util + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.navi.pay.common.utils.DateTimeConverterAdapter +import com.navi.pay.onboarding.account.common.model.view.UpiGlobalInfoEntity +import org.joda.time.DateTime + +class UpiGlobalInfoConverter { + val gson: Gson = + GsonBuilder().registerTypeAdapter(DateTime::class.java, DateTimeConverterAdapter()).create() + + @TypeConverter + fun fromUpiGlobalInfo(upiGlobalInfo: UpiGlobalInfoEntity): String { + return gson.toJson(upiGlobalInfo) + } + + @TypeConverter + fun toUpiGlobalInfo(upiGlobalInfo: String): UpiGlobalInfoEntity { + return try { + gson.fromJson(upiGlobalInfo, UpiGlobalInfoEntity::class.java) + } catch (e: Exception) { + UpiGlobalInfoEntity() + } + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountDetailOptionEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountDetailOptionEntity.kt index d7a0e207bd..fa83d78771 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountDetailOptionEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountDetailOptionEntity.kt @@ -9,11 +9,14 @@ package com.navi.pay.onboarding.account.detail.model.view import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import com.navi.base.utils.EMPTY import com.navi.pay.onboarding.account.detail.viewmodel.LinkedAccountDetailsOptionType data class LinkedAccountDetailOptionEntity( @DrawableRes val startIconId: Int, - @StringRes val descriptionString: Int, + @StringRes val titleString: Int, val onItemClicked: () -> Unit, val defaultButtonState: LinkedAccountDetailsOptionType, + val description: String = EMPTY, + val isEnabled: Boolean = true, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountEntity.kt index f8efaa88a6..28bd88847c 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/model/view/LinkedAccountEntity.kt @@ -14,10 +14,12 @@ import com.navi.common.utils.Constants.OS_ANDROID import com.navi.pay.common.bankuptime.model.view.BankUptimeEntity import com.navi.pay.common.model.view.PspType import com.navi.pay.common.utils.getBankNameAccountNumberText +import com.navi.pay.common.utils.getBankNameTruncatedAccountNumberText import com.navi.pay.common.utils.getMaskedAccountNumberWithHyphen import com.navi.pay.management.common.sendmoney.model.view.EligibilityState import com.navi.pay.management.lite.models.view.UPILiteAccountStatus import com.navi.pay.management.lite.models.view.UPILiteEntity +import com.navi.pay.onboarding.account.common.model.view.UpiGlobalInfoEntity import com.navi.pay.onboarding.account.common.model.view.VpaEntity import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -46,6 +48,7 @@ data class LinkedAccountEntity( val balance: String? = null, val accountTypeFormatted: String, val updatedAt: DateTime?, + val upiGlobalInfo: UpiGlobalInfoEntity? = null, ) : Parcelable { // Set primary VPA as the first marked primary; fallback to first active VPA if none is marked. @@ -60,6 +63,14 @@ data class LinkedAccountEntity( getBankNameAccountNumberText(bankName = bankName, maskedAccountNumber = maskedAccountNumber) } + @IgnoredOnParcel + val bankNameTruncatedWithAccountNumber by lazy { + getBankNameTruncatedAccountNumberText( + bankName = bankName, + maskedAccountNumber = maskedAccountNumber, + ) + } + @IgnoredOnParcel val maskedAccountNumberWithHyphen by lazy { getMaskedAccountNumberWithHyphen(maskedAccountNumber = maskedAccountNumber) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/ui/LinkedAccountDetailScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/ui/LinkedAccountDetailScreen.kt index 5b27b6fed4..0b3bb49a59 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/ui/LinkedAccountDetailScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/ui/LinkedAccountDetailScreen.kt @@ -88,11 +88,14 @@ import com.navi.pay.common.settingscreen.utils.saveQrInGallery import com.navi.pay.common.settingscreen.utils.shareQR import com.navi.pay.common.setup.NaviPayRouter import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.ui.BottomSheetContentWithDateSelectionCalendarAndRadioButtons import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader import com.navi.pay.common.ui.ConfirmationBottomSheetContent +import com.navi.pay.common.ui.DeactivateUpiGlobalView import com.navi.pay.common.ui.FullScreenLottieWithRolodexAnimation import com.navi.pay.common.ui.ImageWithCircularBackground import com.navi.pay.common.ui.LinkUPINumberConfirmationBottomSheetWithVPAInfoContent +import com.navi.pay.common.ui.LoadingBottomSheetView import com.navi.pay.common.ui.LoadingScreen import com.navi.pay.common.ui.NaviPayCard import com.navi.pay.common.ui.NaviPayCreditCardSponsorView @@ -105,6 +108,7 @@ import com.navi.pay.common.ui.PrimaryAccountTypeTag import com.navi.pay.common.ui.RenderAPIResultScreen import com.navi.pay.common.ui.RenderUpiNumberLinkSuccessState import com.navi.pay.common.ui.SuccessBottomSheetContent +import com.navi.pay.common.ui.SuccessBottomSheetWithIconHeader import com.navi.pay.common.utils.ErrorEventHandler import com.navi.pay.common.utils.NaviPayEventBus import com.navi.pay.common.utils.getFormattedCreditCardNumber @@ -205,7 +209,7 @@ fun LinkedAccountDetailScreen( // bottomSheetState.isVisible remains true due to confirmValueChange being false // (e.g., when the back button is pressed). if (!bottomSheetState.isVisible || !bottomSheetStateHolder.bottomSheetStateChange) { - linkedAccountDetailViewModel.updateBottomSheetUIState(showBottomSheet = false) + linkedAccountDetailViewModel.updateBottomSheetUiState(showBottomSheet = false) } } } @@ -599,11 +603,13 @@ fun LinkedAccountDetailScreenContent( OptionsItem( modifier = Modifier.fillMaxWidth(), startIconId = optionItem.startIconId, - descriptionString = stringResource(id = optionItem.descriptionString), + title = stringResource(id = optionItem.titleString), onItemClicked = optionItem.onItemClicked, showLoader = showLoader, lastClickedCardState = lastClickedCardState, defaultButtonState = optionItem.defaultButtonState, + description = optionItem.description, + isEnabled = optionItem.isEnabled, ) Spacer(modifier = Modifier.height(8.dp)) } @@ -1117,50 +1123,81 @@ private fun getConfirmationBottomSheetDescriptionIdByAccountType(accountType: St fun OptionsItem( modifier: Modifier, @DrawableRes startIconId: Int, - descriptionString: String, + title: String, onItemClicked: () -> Unit, showLoader: Boolean = false, lastClickedCardState: LinkedAccountDetailsOptionType, defaultButtonState: LinkedAccountDetailsOptionType, + description: String, + isEnabled: Boolean, ) { Row( modifier = modifier .fillMaxWidth() - .conditional(showLoader.not()) { clickableDebounce { onItemClicked() } } + .conditional(showLoader.not() && isEnabled) { + clickableDebounce { onItemClicked() } + } .padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( painter = painterResource(id = startIconId), contentDescription = "", - modifier = Modifier.size(20.dp), + modifier = Modifier.size(20.dp).align(Alignment.Top).padding(top = 2.dp), ) Spacer(modifier = Modifier.width(12.dp)) - NaviText( - modifier = Modifier.weight(1f), - text = descriptionString, - fontFamily = naviFontFamily, - fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), - fontSize = 14.sp, - color = NaviPayColor.textPrimary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - if (showLoader && defaultButtonState == lastClickedCardState) { - NaviPayLottieAnimation( - modifier = Modifier.size(16.dp), - lottieFileName = NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE, - ) - } else { - Image( - painter = painterResource(id = CommonR.drawable.ic_chevron_right), - contentDescription = "", - modifier = Modifier.size(16.dp), - colorFilter = ColorFilter.tint(color = NaviPayColor.textPrimary), + Column(modifier = Modifier.weight(1f)) { + NaviText( + modifier = Modifier.fillMaxWidth(), + text = title, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 14.sp, + color = + if (isEnabled) { + NaviPayColor.textPrimary + } else { + NaviPayColor.inputFieldDefault + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) + if (description.isNotEmpty()) { + Spacer(modifier = Modifier.width(2.dp)) + NaviText( + modifier = Modifier.fillMaxWidth(), + text = description, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + fontSize = 12.sp, + color = + if (isEnabled) { + NaviPayColor.onSurfaceHighlight + } else { + NaviPayColor.textOrange + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + if (isEnabled) { + if (showLoader && defaultButtonState == lastClickedCardState) { + NaviPayLottieAnimation( + modifier = Modifier.size(16.dp), + lottieFileName = NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE, + ) + } else { + Image( + painter = painterResource(id = CommonR.drawable.ic_chevron_right), + contentDescription = "", + modifier = Modifier.size(16.dp).padding(top = 4.dp), + colorFilter = ColorFilter.tint(color = NaviPayColor.textPrimary), + ) + } } } } @@ -1270,6 +1307,8 @@ fun RenderLinkedAccountDetailScreenBottomSheet( linkedAccountDetailViewModel.isAutoTopUpSetUp.collectAsStateWithLifecycle() val isNonTerminalMandateStatusPresent by linkedAccountDetailViewModel.isNonTerminalMandateStatusPresent.collectAsStateWithLifecycle() + val selectedUpiGlobalEndDate by + linkedAccountDetailViewModel.selectedUpiGlobalEndDate.collectAsStateWithLifecycle() when (bottomSheetType) { LinkedAccountDetailBottomSheetUIState.RemoveAccountConfirmation -> { @@ -1411,6 +1450,48 @@ fun RenderLinkedAccountDetailScreenBottomSheet( }, ) } + is LinkedAccountDetailBottomSheetUIState.UpiGlobalDateSelection -> { + BottomSheetContentWithDateSelectionCalendarAndRadioButtons( + bankName = linkedAccountEntity?.bankNameTruncatedWithAccountNumber.orEmpty(), + deactivationDate = selectedUpiGlobalEndDate, + showCtaLoader = showCtaLoader, + updateDate = { endDate -> + linkedAccountDetailViewModel.updateUpiGlobalEndDate(endDate) + }, + onPrimaryButtonClicked = { linkedAccountDetailViewModel.activateUpiGlobalAccount() }, + ) + } + is LinkedAccountDetailBottomSheetUIState.UpiGlobalLoading -> { + LoadingBottomSheetView(text = stringResource(bottomSheetType.titleId)) + } + is LinkedAccountDetailBottomSheetUIState.UpiGlobalStatusSuccess -> { + SuccessBottomSheetWithIconHeader(headerText = bottomSheetType.title) + } + is LinkedAccountDetailBottomSheetUIState.UpiGlobalDeactivate -> { + DeactivateUpiGlobalView( + showCtaLoader = showCtaLoader, + onPrimaryButtonClicked = { + linkedAccountDetailViewModel.deactivateUpiGlobalAccount() + }, + onSecondaryButtonClicked = { + linkedAccountDetailViewModel.updateBottomSheetUiState(showBottomSheet = false) + }, + ) + } + is LinkedAccountDetailBottomSheetUIState.UpiGlobalStatusPending -> { + BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( + iconId = CommonR.drawable.ic_info_solid_blue, + iconSize = 24.dp, + header = stringResource(id = R.string.np_your_request_is_under_process), + description = stringResource(id = bottomSheetType.descriptionId), + primaryButton = stringResource(id = R.string.np_okay_got_it), + onPrimaryButtonClicked = { + linkedAccountDetailViewModel.updateBottomSheetUiState(showBottomSheet = false) + }, + onSecondaryButtonClicked = {}, + secondaryButton = null, + ) + } else -> {} } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/viewmodel/LinkedAccountDetailViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/viewmodel/LinkedAccountDetailViewModel.kt index 07a78a56b7..3890ff9b2f 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/viewmodel/LinkedAccountDetailViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/detail/viewmodel/LinkedAccountDetailViewModel.kt @@ -13,13 +13,17 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.navi.base.cache.model.NaviCacheEntity import com.navi.base.cache.repository.NaviCacheRepository +import com.navi.base.utils.DateUtils import com.navi.base.utils.ResourceProvider import com.navi.base.utils.isNull import com.navi.base.utils.orFalse +import com.navi.common.network.models.RepoResult import com.navi.common.network.models.isError import com.navi.common.network.models.isSuccess import com.navi.common.network.models.isSuccessWithData import com.navi.common.utils.Constants.OS_ANDROID +import com.navi.common.utils.EMPTY +import com.navi.common.utils.NaviApiPoller import com.navi.pay.R import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_ACCOUNT_DETAILS @@ -29,8 +33,10 @@ import com.navi.pay.common.model.view.NaviPayFlowType import com.navi.pay.common.model.view.PspType import com.navi.pay.common.model.view.SimInfo import com.navi.pay.common.settingscreen.model.QrDetails +import com.navi.pay.common.usecase.ActivateUpiGlobalUseCase import com.navi.pay.common.usecase.CheckAccountBalanceUseCase import com.navi.pay.common.usecase.CheckUpiNumberAvailabilityUseCase +import com.navi.pay.common.usecase.DeactivateUpiGlobalUseCase import com.navi.pay.common.usecase.DisableUpiLiteUseCase import com.navi.pay.common.usecase.LinkedAccountsUseCase import com.navi.pay.common.usecase.LiteAccountSyncUseCase @@ -45,6 +51,7 @@ import com.navi.pay.common.utils.NaviPayCommonUtils.getHelpCtaData import com.navi.pay.common.utils.NaviPayNotificationHandler import com.navi.pay.common.utils.VpaQRCodeManager import com.navi.pay.common.utils.fetchUserPhoneNumber +import com.navi.pay.common.utils.getDayEndDateTime import com.navi.pay.common.utils.getLinkedAccountQRDetails import com.navi.pay.common.utils.getLinkedAccountQrDetailsForInactiveVpa import com.navi.pay.common.utils.getMetricInfo @@ -59,6 +66,12 @@ import com.navi.pay.management.common.sendmoney.model.network.TransactionRespons import com.navi.pay.management.common.transaction.model.network.TransactionStatus import com.navi.pay.management.common.utils.NaviPayPspManager import com.navi.pay.management.common.utils.PspEvaluationResult +import com.navi.pay.management.global.models.network.GlobalAccountStatusRequest +import com.navi.pay.management.global.models.network.GlobalAccountStatusResponse +import com.navi.pay.management.global.models.network.GlobalActivationRequest +import com.navi.pay.management.global.models.network.GlobalDeactivationRequest +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus +import com.navi.pay.management.global.repository.UpiGlobalRepository import com.navi.pay.management.lite.models.view.InitialTopUpStatus.Companion.isPendingState import com.navi.pay.management.lite.models.view.UPILiteAccountStatus import com.navi.pay.management.lite.util.DisableUpiLiteStatus @@ -79,6 +92,8 @@ import com.navi.pay.npcicl.NpciResult import com.navi.pay.npcicl.UpiLiteBoundStatus import com.navi.pay.onboarding.account.add.model.view.AccountType import com.navi.pay.onboarding.account.add.model.view.AccountType.Companion.getAccountType +import com.navi.pay.onboarding.account.add.repository.BankRepository +import com.navi.pay.onboarding.account.common.model.view.UpiGlobalInfoEntity import com.navi.pay.onboarding.account.common.repository.AccountsRepository import com.navi.pay.onboarding.account.detail.model.network.AccountPrimaryRequest import com.navi.pay.onboarding.account.detail.model.network.DeleteAccountRequest @@ -94,6 +109,7 @@ import com.navi.pay.onboarding.common.NaviPayOnboardingActionsType import com.navi.pay.utils.ACCOUNT_ID import com.navi.pay.utils.ACTION_PIN_RESET import com.navi.pay.utils.ACTION_PIN_SET +import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR import com.navi.pay.utils.ERROR_UPI_NUMBER_MAPPING_ALREADY_EXIST_1 import com.navi.pay.utils.ERROR_UPI_NUMBER_MAPPING_ALREADY_EXIST_2 import com.navi.pay.utils.KEY_UPI_LITE_ACTIVE_ACCOUNT_INFO @@ -108,6 +124,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlin.collections.map import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -137,10 +154,10 @@ constructor( private val deviceInfoProvider: DeviceInfoProvider, private val linkedAccountsUseCase: LinkedAccountsUseCase, private val npciRepository: NpciRepository, + private val resourceProvider: ResourceProvider, private val credDataProvider: CredDataProvider, private val refreshLinkedAccountsUseCase: RefreshLinkedAccountsUseCase, private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity, - private val resourceProvider: ResourceProvider, private val upiLiteClHelper: UpiLiteClHelper, private val liteAccountSyncUseCase: LiteAccountSyncUseCase, private val disableUpiLiteUseCase: DisableUpiLiteUseCase, @@ -153,15 +170,23 @@ constructor( private val checkUpiNumberAvailabilityUseCase: CheckUpiNumberAvailabilityUseCase, private val refreshUpiNumbersUseCase: RefreshUpiNumbersUseCase, private val checkAccountBalanceUseCase: CheckAccountBalanceUseCase, - private val naviPayPspManager: NaviPayPspManager, private val updateCustomerStatusOnAccountRemovalUseCase: UpdateCustomerStatusOnAccountRemovalUseCase, private val upiNumbersHelper: UpiNumbersHelper, val naviPayNotificationHandler: NaviPayNotificationHandler, naviPayActivityDataProvider: NaviPayActivityDataProvider, savedStateHandle: SavedStateHandle, + private val upiGlobalRepository: UpiGlobalRepository, @NaviPayGsonBuilder private val gson: Gson, + private val bankRepository: BankRepository, + private val naviPayPspManager: NaviPayPspManager, + private val activateUpiGlobalUseCase: ActivateUpiGlobalUseCase, + private val deactivateUpiGlobalUseCase: DeactivateUpiGlobalUseCase, ) : NaviPayBaseVM() { + companion object { + private val POLL_INTERVAL = 3.seconds + private const val POLL_MAX_ITERATIONS = 3 + } private val naviPayAnalytics: NaviPayAnalytics.NaviPayAccountDetails = NaviPayAnalytics.INSTANCE.NaviPayAccountDetails() @@ -212,6 +237,9 @@ constructor( private val _showLoader = MutableStateFlow(false) val showLoader = _showLoader.asStateFlow() + private val _isUpiGlobalSupported = MutableStateFlow(false) + private val isUpiGlobalSupported = _isUpiGlobalSupported.asStateFlow() + private val _lastClickedCardState = MutableStateFlow(LinkedAccountDetailsOptionType.Default) val lastClickedCardState = _lastClickedCardState.asStateFlow() @@ -242,12 +270,17 @@ constructor( initialValue = null, ) + private val _upiGlobalButtonUIState = MutableStateFlow(UpiGlobalButtonState.Deactivated) + private val upiGlobalButtonUIState = _upiGlobalButtonUIState.asStateFlow() + val accountDetailOptionListState = combine( linkedAccountEntity, isActiveLiteAccountPresentForDisablement, isUpiNumberLinkedToAccount, - ) { _, _, _ -> + isUpiGlobalSupported, + upiGlobalButtonUIState, + ) { _, _, _, _, _ -> updateAccountDetailOptionItemList() } .flowOn(Dispatchers.Default) @@ -279,6 +312,13 @@ constructor( initialValue = getGradientColorsForBankCode(), ) + private val naviApiPoller by lazy { + NaviApiPoller(repeatInterval = POLL_INTERVAL, numberOfIterations = POLL_MAX_ITERATIONS) + } + + private val _selectedUpiGlobalEndDate = MutableStateFlow(null) + val selectedUpiGlobalEndDate = _selectedUpiGlobalEndDate.asStateFlow() + private var apiCallJob: Job? = null val helpCta = getHelpCtaData(screenName = screenName) @@ -314,6 +354,7 @@ constructor( qrDetails = qrDetails, ) checkAndUpdateLiteAccountDisablementEligibility() + updateIsUpiGlobalSupported() } } } @@ -396,6 +437,11 @@ constructor( optionType = LinkedAccountDetailsOptionType.UpiPinSettings, ) + checkAndAddMenuOptionItem( + optionItemList = optionItemList, + optionType = LinkedAccountDetailsOptionType.UpiGlobal, + ) + checkAndAddMenuOptionItem( optionItemList = optionItemList, optionType = LinkedAccountDetailsOptionType.DisableUpiLite, @@ -461,6 +507,13 @@ constructor( optionItemList.add(getUpiPinSettingsOptionItem()) } } + + LinkedAccountDetailsOptionType.UpiGlobal -> { + if (shouldShowUpiGlobalOptionItem()) { + optionItemList.add(getUpiGlobalOptionItem()) + } + } + else -> {} } } @@ -500,10 +553,14 @@ constructor( return isUpiNumberLinkedToAccount.value == true } + fun shouldShowUpiGlobalOptionItem(): Boolean { + return isUpiGlobalSupported.value && linkedAccountEntity.value?.isMPinSet == true + } + fun getSetUpiPinOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_set_upi_pin, - descriptionString = R.string.set_upi_pin, + titleString = R.string.set_upi_pin, onItemClicked = { naviPayAnalytics.onSetUpiPinClicked() handleSetPinClick() @@ -515,7 +572,7 @@ constructor( fun getSetAsPrimaryAccountOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_set_primary_account, - descriptionString = R.string.set_as_primary_account, + titleString = R.string.set_as_primary_account, onItemClicked = { naviPayAnalytics.onSetAsPrimaryClick() makeAccountPrimary() @@ -527,7 +584,7 @@ constructor( fun getCheckBalanceOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_check_balance, - descriptionString = R.string.np_check_balance, + titleString = R.string.np_check_balance, onItemClicked = { naviPayAnalytics.onCheckBalanceClicked() checkBalance() @@ -539,7 +596,7 @@ constructor( fun getManageUpiIdsOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_np_manage_upi_ids, - descriptionString = R.string.np_manage_upi_ids, + titleString = R.string.np_manage_upi_ids, onItemClicked = { onManageUpiIdsClicked() }, defaultButtonState = LinkedAccountDetailsOptionType.ManageUpiIds, ) @@ -548,7 +605,7 @@ constructor( fun getForgotUpiPinOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_forgot_upi, - descriptionString = R.string.forgot_upi_pin, + titleString = R.string.forgot_upi_pin, onItemClicked = { naviPayAnalytics.onForgotUpiPinClicked() handleForgotPinClick() @@ -560,7 +617,7 @@ constructor( fun getChangeUpiPinOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_change_upi_pin, - descriptionString = R.string.change_upi_pin, + titleString = R.string.change_upi_pin, onItemClicked = { naviPayAnalytics.onChangeUpiPinClicked() changePin() @@ -572,7 +629,7 @@ constructor( fun getDisableUpiLiteOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_disable_upi_lite, - descriptionString = R.string.disable_upi_lite_info, + titleString = R.string.disable_upi_lite_info, onItemClicked = { naviPayAnalytics.onDisableUpiLiteOptionClicked() showDisableUpiLiteBottomSheet() @@ -584,7 +641,7 @@ constructor( fun getManageUpiNumberOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_manage_upi_number, - descriptionString = R.string.np_manage_upi_number, + titleString = R.string.np_manage_upi_number, onItemClicked = ::handleManageUpiNumberClick, defaultButtonState = LinkedAccountDetailsOptionType.ManageUpiNumber, ) @@ -593,7 +650,7 @@ constructor( fun getUpiPinSettingsOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_np_upi_pin_settings, - descriptionString = R.string.np_upi_pin_settings, + titleString = R.string.np_upi_pin_settings, onItemClicked = ::handleUpiPinSettingsClick, defaultButtonState = LinkedAccountDetailsOptionType.UpiPinSettings, ) @@ -602,12 +659,64 @@ constructor( fun getRemoveAccountOptionItem(): LinkedAccountDetailOptionEntity { return LinkedAccountDetailOptionEntity( startIconId = R.drawable.ic_remove_account, - descriptionString = R.string.remove_account, + titleString = R.string.remove_account, onItemClicked = ::handleRemoveAccountClick, defaultButtonState = LinkedAccountDetailsOptionType.RemoveAccount, ) } + fun getUpiGlobalOptionItem(): LinkedAccountDetailOptionEntity { + return when (upiGlobalButtonUIState.value) { + UpiGlobalButtonState.Activated -> { + LinkedAccountDetailOptionEntity( + startIconId = R.drawable.ic_np_globe_with_upi, + titleString = R.string.np_deactivate_upi_global, + onItemClicked = ::handleDeactivateUpiGlobalClicked, + defaultButtonState = LinkedAccountDetailsOptionType.RemoveAccount, + description = + resourceProvider.getString( + R.string.np_global_active_till, + getFormattedUpiGlobalEndDate( + linkedAccountEntity = linkedAccountEntity.value!! + ), + ), + ) + } + + UpiGlobalButtonState.Deactivated -> { + LinkedAccountDetailOptionEntity( + startIconId = R.drawable.ic_np_globe_with_upi, + titleString = R.string.np_activate_upi_global, + onItemClicked = ::handleActivateUpiGlobalClicked, + defaultButtonState = LinkedAccountDetailsOptionType.RemoveAccount, + ) + } + + UpiGlobalButtonState.ActivationPending -> { + LinkedAccountDetailOptionEntity( + startIconId = R.drawable.ic_np_globe_with_upi, + titleString = R.string.np_activate_upi_global, + onItemClicked = { Unit }, + defaultButtonState = LinkedAccountDetailsOptionType.RemoveAccount, + description = resourceProvider.getString(R.string.np_activation_under_progress), + isEnabled = false, + ) + } + + UpiGlobalButtonState.DeactivationPending -> { + LinkedAccountDetailOptionEntity( + startIconId = R.drawable.ic_np_globe_with_upi, + titleString = R.string.np_deactivate_upi_global, + onItemClicked = { Unit }, + defaultButtonState = LinkedAccountDetailsOptionType.RemoveAccount, + description = + resourceProvider.getString(R.string.np_deactivation_under_progress), + isEnabled = false, + ) + } + } + } + private fun onManageUpiIdsClicked() { updateNavigateToNextScreen( nextScreen = @@ -635,7 +744,7 @@ constructor( viewModelScope.launch(Dispatchers.IO) { _navigateToNextScreen.emit(value = nextScreen) } } - fun updateBottomSheetUIState( + fun updateBottomSheetUiState( bottomSheetState: LinkedAccountDetailBottomSheetUIState? = null, showBottomSheet: Boolean, allowStateChange: Boolean? = null, @@ -724,7 +833,7 @@ constructor( naviPayAnalytics.onRemoveAccountOptionClicked( isPrimary = linkedAccountEntity.value?.isAccountPrimary ) - updateBottomSheetUIState( + updateBottomSheetUiState( bottomSheetState = LinkedAccountDetailBottomSheetUIState.RemoveAccountConfirmation, showBottomSheet = true, allowStateChange = true, @@ -781,7 +890,7 @@ constructor( linkedAccountEntity: LinkedAccountEntity?, ) { if (pspEvaluationResult.isOnboardingTriggered) { - updateBottomSheetUIState( + updateBottomSheetUiState( bottomSheetState = LinkedAccountDetailBottomSheetUIState.RemoveAccountConfirmation, showBottomSheet = true, allowStateChange = true, @@ -812,7 +921,7 @@ constructor( } updateLastClickedCardState(lastClickedCardState = LinkedAccountDetailsOptionType.Default) updateShowLoaderState(showLoader = true) - updateBottomSheetUIState(showBottomSheet = true, allowStateChange = false) + updateBottomSheetUiState(showBottomSheet = true, allowStateChange = false) val deleteAccountAPIResponse = accountsRepository.deleteAccount( @@ -899,7 +1008,7 @@ constructor( fun onDeleteMandateAndDisableLite() { val liteMandateInfo = upiLiteMandateInfo.value if (liteMandateInfo == null) { - updateBottomSheetUIState(showBottomSheet = false) + updateBottomSheetUiState(showBottomSheet = false) return } revokeLiteMandate() @@ -979,7 +1088,7 @@ constructor( if (!revokeMandateAPIResponse.isSuccessWithData()) { updateUIState(uiState = LinkedAccountDetailScreenUIState.AccountDetail) - updateBottomSheetUIState( + updateBottomSheetUiState( showBottomSheet = true, bottomSheetState = LinkedAccountDetailBottomSheetUIState @@ -995,7 +1104,7 @@ constructor( delay(200.milliseconds) updateUIState(uiState = LinkedAccountDetailScreenUIState.AccountDetail) delay(50.milliseconds) - updateBottomSheetUIState( + updateBottomSheetUiState( showBottomSheet = true, bottomSheetState = LinkedAccountDetailBottomSheetUIState.RedirectingBottomSheet( @@ -1007,12 +1116,14 @@ constructor( ) onDisableUpiLiteClicked(checkForActiveMandates = true) } + is NpciResult.Error -> { if (!npciResult.isUserAborted) { updateUIState(uiState = LinkedAccountDetailScreenUIState.AccountDetail) notifyError(npciResult.errorResult) } } + else -> Unit } } @@ -1038,7 +1149,7 @@ constructor( } ?: run { updateShowLoaderState(showLoader = false) - updateBottomSheetUIState(showBottomSheet = false) + updateBottomSheetUiState(showBottomSheet = false) notifyError() return@launch } @@ -1046,7 +1157,7 @@ constructor( evaluateAndOnboardPsp( naviPayFlowType = NaviPayFlowType.CHANGE_PIN, linkedAccountEntity = linkedAccountEntity!!, - onOnboardingTriggered = { updateBottomSheetUIState(showBottomSheet = false) }, + onOnboardingTriggered = { updateBottomSheetUiState(showBottomSheet = false) }, onPspEvaluated = { pspEvaluationResult -> val onboardingDataEntity = pspEvaluationResult.onboardingDataEntity ?: return@evaluateAndOnboardPsp @@ -1061,7 +1172,7 @@ constructor( if (!validateSimInfoOrShowError(currentSimInfoList)) return@evaluateAndOnboardPsp updateShowLoaderState(showLoader = true) - updateBottomSheetUIState(showBottomSheet = true, allowStateChange = false) + updateBottomSheetUiState(showBottomSheet = true, allowStateChange = false) val upiRequestId = upiRequestIdUseCase.execute(pspType = onboardingDataEntity.pspType) @@ -1097,7 +1208,7 @@ constructor( when (npciResult) { is NpciResult.Success -> { updateShowLoaderState(showLoader = false) - updateBottomSheetUIState(showBottomSheet = false) + updateBottomSheetUiState(showBottomSheet = false) updateUIState( uiState = LinkedAccountDetailScreenUIState.Loading( @@ -1141,14 +1252,16 @@ constructor( updateUIState(uiState = LinkedAccountDetailScreenUIState.ChangePinSuccess) } + is NpciResult.Error -> { updateShowLoaderState(showLoader = false) - updateBottomSheetUIState(showBottomSheet = true, allowStateChange = true) + updateBottomSheetUiState(showBottomSheet = true, allowStateChange = true) if (!npciResult.isUserAborted) { - updateBottomSheetUIState(showBottomSheet = false) + updateBottomSheetUiState(showBottomSheet = false) notifyError(npciResult.errorResult) } } + else -> Unit } } @@ -1285,7 +1398,7 @@ constructor( } evaluateAndOnboardPsp( naviPayFlowType = NaviPayFlowType.RESET_PIN, - onOnboardingTriggered = { updateBottomSheetUIState(showBottomSheet = false) }, + onOnboardingTriggered = { updateBottomSheetUiState(showBottomSheet = false) }, linkedAccountEntity = linkedAccountEntity!!, onPspEvaluated = { pspEvaluationResult -> val pspType = @@ -1294,7 +1407,7 @@ constructor( updateLastClickedCardState( lastClickedCardState = LinkedAccountDetailsOptionType.ForgotPin ) - updateBottomSheetUIState(showBottomSheet = false) + updateBottomSheetUiState(showBottomSheet = false) updateNavigateToNextScreen( nextScreen = LinkedAccountVerifyScreenDestination( @@ -1309,7 +1422,7 @@ constructor( } fun showDisableUpiLiteBottomSheet() { - updateBottomSheetUIState( + updateBottomSheetUiState( bottomSheetState = LinkedAccountDetailBottomSheetUIState.DisableUPILite, showBottomSheet = true, allowStateChange = true, @@ -1327,7 +1440,7 @@ constructor( } private fun handleUpiPinSettingsClick() { - updateBottomSheetUIState( + updateBottomSheetUiState( bottomSheetState = LinkedAccountDetailBottomSheetUIState.UpiPinSettings, showBottomSheet = true, allowStateChange = true, @@ -1405,7 +1518,7 @@ constructor( private fun handlePhoneNumberPortSuccess(externalVpa: String, targetVpa: String) { updateShowLoaderState(showLoader = false) - updateBottomSheetUIState( + updateBottomSheetUiState( showBottomSheet = true, bottomSheetState = LinkedAccountDetailBottomSheetUIState.LinkUPINumberConfirmationBottomSheet( @@ -1518,7 +1631,7 @@ constructor( ) if (!checkForActiveMandates) { updateShowLoaderState(showLoader = true) - updateBottomSheetUIState(showBottomSheet = true, allowStateChange = false) + updateBottomSheetUiState(showBottomSheet = true, allowStateChange = false) } disableUpiLiteUseCase @@ -1600,6 +1713,7 @@ constructor( TransactionStatus.FAILURE -> { handleDisableUpiLiteFailedCase() } + TransactionStatus.PENDING -> { handleDisableUpiLitePendingCase( linkedAccountEntity = linkedAccountEntity, @@ -1607,6 +1721,7 @@ constructor( lrn = lrn, ) } + TransactionStatus.SUCCESS -> { naviPayAnalytics.onDisableUpiLiteStatus(status = "Success") val registerUPILiteState = @@ -1634,6 +1749,7 @@ constructor( } refreshLinkedAccountsUseCase.execute(screenName = screenName) } + else -> {} } } @@ -1641,7 +1757,7 @@ constructor( private fun showDisableUpiFailedBottomSheet() { naviPayAnalytics.onDisableUpiLiteStatus(status = "Failure") - updateBottomSheetUIState( + updateBottomSheetUiState( bottomSheetState = LinkedAccountDetailBottomSheetUIState.DisableUPILiteFailed( title = resourceProvider.getString(resId = R.string.disabling_upi_lite_failed), @@ -1655,7 +1771,7 @@ constructor( private suspend fun showDisableUpiPendingBottomSheet() { naviPayAnalytics.onDisableUpiLiteStatus(status = "Pending") - updateBottomSheetUIState( + updateBottomSheetUiState( bottomSheetState = LinkedAccountDetailBottomSheetUIState.DisableUPILitePending( title = resourceProvider.getString(resId = R.string.disabling_upi_lite_failed), @@ -1735,6 +1851,440 @@ constructor( return true } + private fun updateUpiGlobalButtonUiState(uiState: UpiGlobalButtonState) { + _upiGlobalButtonUIState.update { uiState } + } + + private fun checkAndUpdateUpiGlobalStatus(upiGlobalInfo: UpiGlobalInfoEntity?) { + val upiGlobalStatus = upiGlobalInfo?.status + when (upiGlobalStatus) { + UpiGlobalAccountStatus.ACTIVE -> { + if ( + UpiGlobalAccountStatus.isStatusActive( + status = upiGlobalInfo.status, + activeDateTime = upiGlobalInfo.endDate ?: DateTime.now(), + ) + ) { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Activated) + } else { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Deactivated) + } + } + + UpiGlobalAccountStatus.DEACTIVE -> { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Deactivated) + } + + UpiGlobalAccountStatus.INIT_ACTIVE -> { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.ActivationPending) + pollForStatus(linkedAccount = linkedAccountEntity.value) + } + + UpiGlobalAccountStatus.INIT_DEACTIVE -> { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.DeactivationPending) + pollForStatus(linkedAccount = linkedAccountEntity.value) + } + + null -> updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Deactivated) + } + } + + private fun updateIsUpiGlobalSupported() { + viewModelScope.launch(Dispatchers.IO) { + val bankEntity = + bankRepository + .getBankUiModelListFromBankCodes( + listOf(linkedAccountEntity.value?.bankCode.orEmpty()) + ) + .firstOrNull() + if (bankEntity != null) { + _isUpiGlobalSupported.update { + bankEntity.isUpiInternationalSupported && + !AccountType.isAccountOfTypeCreditCardOrCreditLine( + linkedAccountEntity.value?.accountType ?: "" + ) + } + } + if (isUpiGlobalSupported.value) { + checkAndUpdateUpiGlobalStatus(linkedAccountEntity.value?.upiGlobalInfo) + } + } + } + + private fun pollForStatus(linkedAccount: LinkedAccountEntity?) { + viewModelScope.launch(Dispatchers.IO) { + val upiGlobalInfo = linkedAccount?.upiGlobalInfo + + if (upiGlobalInfo == null) { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Deactivated) + return@launch + } + + if (!UpiGlobalAccountStatus.isStatusPending(linkedAccount.upiGlobalInfo.status)) { + return@launch + } + naviApiPoller + .startPolling { + upiGlobalRepository.globalAccountStatusCheck( + globalAccountStatusRequest = + GlobalAccountStatusRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = deviceInfoProvider.getMerchantCustomerId(), + baccId = + linkedAccount.getBuidByPsp(psp = PspType.JUSPAY_AXIS).orEmpty(), + ), + metricInfo = getMetricInfo(screenName = screenName), + ) + } + .collect { + try { + val activationStatusApiResponse = + it as RepoResult + handlePollingResponse( + activationStatusApiResponse = activationStatusApiResponse + ) + } catch (_: Exception) { + naviApiPoller.stopPolling() + } + } + } + } + + private suspend fun handlePollingResponse( + activationStatusApiResponse: RepoResult + ) { + if (!activationStatusApiResponse.isSuccessWithData()) { + naviApiPoller.stopPolling() + return + } + + val activationStatusData = activationStatusApiResponse.data!! + val activationStatus = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus(activationStatusData.status) + + refreshLinkedAccountsUseCase.execute(screenName = screenName) + + when (activationStatus) { + UpiGlobalAccountStatus.ACTIVE -> { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Activated) + naviApiPoller.stopPolling() + } + + UpiGlobalAccountStatus.DEACTIVE -> { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Deactivated) + naviApiPoller.stopPolling() + } + + UpiGlobalAccountStatus.INIT_ACTIVE, + UpiGlobalAccountStatus.INIT_DEACTIVE -> {} + } + } + + fun updateUpiGlobalEndDate(selectedDate: DateTime?) { + _selectedUpiGlobalEndDate.update { selectedDate } + } + + private fun handleActivateUpiGlobalClicked() { + updateBottomSheetUiState( + showBottomSheet = true, + bottomSheetState = LinkedAccountDetailBottomSheetUIState.UpiGlobalDateSelection, + allowStateChange = true, + ) + } + + fun activateUpiGlobalAccount() { + viewModelScope.launch(Dispatchers.IO) { + var selectedBankAccount: LinkedAccountEntity? = null + this@LinkedAccountDetailViewModel.linkedAccountEntity.value?.let { + selectedBankAccount = it + } + ?: kotlin.run { + updateShowLoaderState(showLoader = false) + notifyError() + return@launch + } + + activateUpiGlobalUseCase.execute( + screenName = screenName, + selectedBankAccount = selectedBankAccount!!, + onOnboardingSuccess = { + showRedirectingBottomSheet(titleId = R.string.np_redirect_upi_global_activation) + }, + onOnboardingTriggered = { updateBottomSheetUiState(showBottomSheet = false) }, + onActivateUpiGlobalClicked = { + updateShowLoaderState(showLoader = true) + updateBottomSheetUiState(showBottomSheet = true, allowStateChange = false) + }, + onNpciResult = { npciResult, upiRequestId, customerOnboardingEntity -> + when (npciResult) { + is NpciResult.Success -> { + activateUpiGlobalPostCL( + upiRequestId = upiRequestId, + linkedAccountEntity = selectedBankAccount!!, + credBlock = npciResult.data, + customerOnboardingEntity = customerOnboardingEntity, + ) + } + + is NpciResult.Error -> { + handleNpciError(npciResult = npciResult) + } + + else -> updateBottomSheetUiState(showBottomSheet = false) + } + }, + onNoInternetError = { notifyError(getNoInternetErrorConfig()) }, + onAirplaneModeError = { notifyError(getAirplaneModeOnErrorConfig()) }, + onSimFailureError = { isNoSimPresent -> + notifyError(getSimFailureErrorConfig(isNoSimPresent)) + }, + ) + } + } + + private suspend fun activateUpiGlobalPostCL( + upiRequestId: String, + linkedAccountEntity: LinkedAccountEntity, + credBlock: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) { + updateShowLoaderState(false) + updateBottomSheetUiState( + showBottomSheet = true, + allowStateChange = true, + bottomSheetState = + LinkedAccountDetailBottomSheetUIState.UpiGlobalLoading( + titleId = R.string.np_upi_global_activation_description + ), + ) + val globalActivationRequest = + GlobalActivationRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = customerOnboardingEntity.merchantCustomerId, + upiRequestId = upiRequestId, + credBlock = credBlock, + baccId = linkedAccountEntity.getBuidByPsp(psp = PspType.JUSPAY_AXIS).orEmpty(), + endDate = getDayEndDateTime(selectedUpiGlobalEndDate.value), + ) + + val globalActivationResponse = + upiGlobalRepository.activateGlobalAccount( + globalActivationRequest = globalActivationRequest, + metricInfo = getMetricInfo(screenName = screenName), + ) + + if (globalActivationResponse.isSuccessWithData()) { + val globalActivationResponseData = globalActivationResponse.data!! + val endDate = globalActivationResponseData.endDate ?: DateTime.now() + updateUpiGlobalEndDate(endDate) + val status = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus(globalActivationResponseData.status) + + refreshLinkedAccountsUseCase.execute(screenName = screenName) + + when (status) { + UpiGlobalAccountStatus.ACTIVE -> { + updateBottomSheetUiState( + showBottomSheet = true, + bottomSheetState = + LinkedAccountDetailBottomSheetUIState.UpiGlobalStatusSuccess( + title = + resourceProvider.getString( + R.string.np_upi_global_activated_successfully, + DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = endDate, + format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, + ), + ) + ), + allowStateChange = true, + ) + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Activated) + // activated bottomsheet for 1 sec + delay(1000) + updateBottomSheetUiState(showBottomSheet = false) + } + + UpiGlobalAccountStatus.INIT_ACTIVE -> { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.ActivationPending) + updateBottomSheetUiState( + showBottomSheet = true, + bottomSheetState = + LinkedAccountDetailBottomSheetUIState.UpiGlobalStatusPending( + descriptionId = R.string.np_upi_global_activation_pending + ), + allowStateChange = true, + ) + pollForStatus(linkedAccount = linkedAccountEntity) + } + + else -> { + updateBottomSheetUiState(showBottomSheet = false) + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Deactivated) + } + } + } else { + updateBottomSheetUiState(showBottomSheet = false) + notifyError(response = globalActivationResponse) + } + } + + private fun handleDeactivateUpiGlobalClicked() { + updateBottomSheetUiState( + showBottomSheet = true, + bottomSheetState = LinkedAccountDetailBottomSheetUIState.UpiGlobalDeactivate, + allowStateChange = true, + ) + } + + private suspend fun showRedirectingBottomSheet(titleId: Int) { + updateBottomSheetUiState( + showBottomSheet = true, + bottomSheetState = LinkedAccountDetailBottomSheetUIState.UpiGlobalLoading(titleId), + allowStateChange = true, + ) + // redirecting bottomsheet with to avoid jerk + delay(2.seconds) + } + + fun deactivateUpiGlobalAccount() { + viewModelScope.launch(Dispatchers.IO) { + var selectedBankAccount: LinkedAccountEntity? = null + this@LinkedAccountDetailViewModel.linkedAccountEntity.value?.let { + selectedBankAccount = it + } + ?: kotlin.run { + updateShowLoaderState(showLoader = false) + notifyError() + return@launch + } + + deactivateUpiGlobalUseCase.execute( + screenName = screenName, + selectedBankAccount = selectedBankAccount!!, + onOnboardingTriggered = { updateBottomSheetUiState(showBottomSheet = false) }, + onOnboardingSuccess = { + showRedirectingBottomSheet( + titleId = R.string.np_redirect_upi_global_deactivation + ) + }, + onDeactivateUpiGlobalClicked = { updateShowLoaderState(true) }, + onNpciResult = { npciResult, upiRequestId, customerOnboardingEntity -> + when (npciResult) { + is NpciResult.Success -> { + updateBottomSheetUiState(showBottomSheet = false) + deactivateUpiGlobalPostCL( + upiRequestId = upiRequestId, + linkedAccountEntity = selectedBankAccount!!, + credBlock = npciResult.data, + customerOnboardingEntity = customerOnboardingEntity, + ) + } + + is NpciResult.Error -> { + handleNpciError(npciResult = npciResult) + } + + else -> updateBottomSheetUiState(showBottomSheet = false) + } + }, + onNoInternetError = { notifyError(getNoInternetErrorConfig()) }, + onAirplaneModeError = { notifyError(getAirplaneModeOnErrorConfig()) }, + onSimFailureError = { isNoSimPresent -> + notifyError(getSimFailureErrorConfig(isNoSimPresent)) + }, + ) + } + } + + private suspend fun deactivateUpiGlobalPostCL( + upiRequestId: String, + linkedAccountEntity: LinkedAccountEntity, + credBlock: String, + customerOnboardingEntity: NaviPayCustomerOnboardingEntity, + ) { + updateShowLoaderState(false) + updateBottomSheetUiState( + showBottomSheet = true, + allowStateChange = true, + bottomSheetState = + LinkedAccountDetailBottomSheetUIState.UpiGlobalLoading( + titleId = R.string.np_upi_global_deactivation_description + ), + ) + val globalDeactivationRequest = + GlobalDeactivationRequest( + deviceData = deviceInfoProvider.getDeviceData(), + merchantCustomerId = customerOnboardingEntity.merchantCustomerId, + upiRequestId = upiRequestId, + credBlock = credBlock, + baccId = linkedAccountEntity.getBuidByPsp(psp = PspType.JUSPAY_AXIS).orEmpty(), + ) + + val globalDeactivationResponse = + upiGlobalRepository.deactivateGlobalAccount( + globalDeactivationRequest = globalDeactivationRequest, + metricInfo = getMetricInfo(screenName = screenName), + ) + + if (globalDeactivationResponse.isSuccessWithData()) { + val globalDeactivationResponseData = globalDeactivationResponse.data!! + val status = + UpiGlobalAccountStatus.toUpiGlobalAccountStatus( + globalDeactivationResponseData.status + ) + refreshLinkedAccountsUseCase.execute(screenName = screenName) + when (status) { + UpiGlobalAccountStatus.DEACTIVE -> { + updateBottomSheetUiState( + showBottomSheet = true, + bottomSheetState = + LinkedAccountDetailBottomSheetUIState.UpiGlobalStatusSuccess( + title = + resourceProvider.getString( + R.string.np_upi_global_deactivated_successfully + ) + ), + allowStateChange = true, + ) + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Deactivated) + // To show deactivate bottomsheet for 1 sec + delay(1000) + updateBottomSheetUiState(showBottomSheet = false) + } + + UpiGlobalAccountStatus.INIT_DEACTIVE -> { + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.DeactivationPending) + updateBottomSheetUiState( + showBottomSheet = true, + bottomSheetState = + LinkedAccountDetailBottomSheetUIState.UpiGlobalStatusPending( + descriptionId = R.string.np_upi_global_deactivation_pending + ), + allowStateChange = true, + ) + pollForStatus(linkedAccount = linkedAccountEntity) + } + + else -> { + updateBottomSheetUiState(showBottomSheet = false) + updateUpiGlobalButtonUiState(uiState = UpiGlobalButtonState.Activated) + } + } + } else { + updateBottomSheetUiState(showBottomSheet = false) + notifyError(response = globalDeactivationResponse) + } + } + + private fun handleNpciError(npciResult: NpciResult.Error) { + updateBottomSheetUiState(showBottomSheet = false) + updateShowLoaderState(false) + if (!npciResult.isUserAborted) { + notifyError() + } + } + private suspend fun evaluateAndOnboardPsp( naviPayFlowType: NaviPayFlowType, linkedAccountEntity: LinkedAccountEntity, @@ -1750,6 +2300,15 @@ constructor( ) } + private fun getFormattedUpiGlobalEndDate(linkedAccountEntity: LinkedAccountEntity): String { + return linkedAccountEntity.upiGlobalInfo?.endDate?.let { + DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = it, + format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, + ) + } ?: EMPTY + } + override val screenName: String get() = NAVI_PAY_ACCOUNT_DETAILS } @@ -1784,6 +2343,20 @@ sealed class LinkedAccountDetailScreenUIState { sealed class LinkedAccountDetailBottomSheetUIState { data object RemoveAccountConfirmation : LinkedAccountDetailBottomSheetUIState() + data object UpiGlobalDateSelection : LinkedAccountDetailBottomSheetUIState() + + data class UpiGlobalStatusPending(val descriptionId: Int) : + LinkedAccountDetailBottomSheetUIState() + + data class UpiGlobalStatusSuccess(val title: String) : LinkedAccountDetailBottomSheetUIState() + + data class UpiGlobalLoading(val titleId: Int) : LinkedAccountDetailBottomSheetUIState() + + data object UpiGlobalDeactivate : LinkedAccountDetailBottomSheetUIState() + + data class Failure(val title: String, val description: String) : + LinkedAccountDetailBottomSheetUIState() + data object DisableUPILite : LinkedAccountDetailBottomSheetUIState() data object UpiPinSettings : LinkedAccountDetailBottomSheetUIState() @@ -1826,5 +2399,13 @@ enum class LinkedAccountDetailsOptionType { RemoveAccount, ManageUpiNumber, LinkUpiNumber, + UpiGlobal, Default, } + +enum class UpiGlobalButtonState { + Activated, + Deactivated, + ActivationPending, + DeactivationPending, +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/linked/ui/LinkedAccountsScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/linked/ui/LinkedAccountsScreen.kt index ae74ee48d6..cbdc9bf9f7 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/linked/ui/LinkedAccountsScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/linked/ui/LinkedAccountsScreen.kt @@ -20,6 +20,7 @@ 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -59,6 +60,7 @@ import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.common.model.view.CheckBalanceState import com.navi.pay.common.setup.NaviPayRouter import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.theme.color.NaviPayColor.bgDefault import com.navi.pay.common.ui.BankNameWithAccountNumberSection import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader import com.navi.pay.common.ui.BottomSheetLoadingScreen @@ -74,6 +76,7 @@ import com.navi.pay.common.ui.SuccessBottomSheetContent import com.navi.pay.common.ui.ThemeRoundedButton import com.navi.pay.destinations.LinkedAccountDetailScreenDestination import com.navi.pay.entry.NaviPayActivity +import com.navi.pay.management.global.models.view.UpiGlobalAccountStatus import com.navi.pay.management.upinumber.list.ui.LinkUpiNumberToNaviPayConfirmationModalContent import com.navi.pay.management.upinumber.list.ui.UpiNumberInfoModalContent import com.navi.pay.management.upinumber.list.ui.UpiNumberLinkedSuccessContent @@ -480,11 +483,21 @@ fun LinkedAccountCardItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(16.dp).fillMaxWidth(), ) { - ImageWithCircularBackground( - boxSize = 40.dp, - imageUrl = linkedAccountEntity.bankIconImageUrl, - imageSize = 28.dp, - ) + Box(modifier = Modifier.width(40.dp)) { + ImageWithCircularBackground( + boxSize = 40.dp, + imageUrl = linkedAccountEntity.bankIconImageUrl, + imageSize = 28.dp, + ) + if ( + linkedAccountEntity.upiGlobalInfo?.status == UpiGlobalAccountStatus.ACTIVE + ) { + GlobalIconWithCircleBackground( + modifier = + Modifier.align(Alignment.BottomEnd).offset(x = 4.dp, y = 4.dp) + ) + } + } Spacer(modifier = Modifier.width(12.dp)) @@ -729,3 +742,25 @@ private fun RenderLinkedAccountsShimmerScreen(onNavigationIconClick: () -> Unit) } } } + +@Composable +fun GlobalIconWithCircleBackground(modifier: Modifier) { + Box( + modifier = + modifier + .size(18.dp) + .background(color = bgDefault, shape = RoundedCornerShape(20.dp)) + .border( + width = 1.dp, + color = NaviPayColor.borderAlt, + shape = RoundedCornerShape(20.dp), + ), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = R.drawable.ic_np_global_icon), + contentDescription = null, + modifier = Modifier.size(14.dp).align(Alignment.Center), + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/model/view/OrderDetailsScreenBottomSheetStateHolder.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/model/view/OrderDetailsScreenBottomSheetStateHolder.kt index 0b1d734ee2..bc9cf93988 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/model/view/OrderDetailsScreenBottomSheetStateHolder.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/model/view/OrderDetailsScreenBottomSheetStateHolder.kt @@ -12,4 +12,5 @@ import com.navi.pay.tstore.details.viewmodel.OrderDetailsScreenBottomSheetUIStat data class OrderDetailsScreenBottomSheetStateHolder( val bottomSheetUIState: OrderDetailsScreenBottomSheetUIState, val showBottomSheet: Boolean, + val bottomSheetStateChange: Boolean, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreen.kt index 0c5f812535..7306b5183d 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreen.kt @@ -43,6 +43,7 @@ import com.navi.pay.common.setup.NaviPayRouter import com.navi.pay.common.ui.LoadingScreen import com.navi.pay.common.ui.NaviPayModalBottomSheet import com.navi.pay.common.utils.NaviPayCommonUtils.launchPermissionSettingsScreen +import com.navi.pay.common.utils.getBankCharges import com.navi.pay.destinations.OrderHistoryScreenDestination import com.navi.pay.destinations.UPILiteScreenDestination import com.navi.pay.entry.NaviPayActivity @@ -54,9 +55,12 @@ import com.navi.pay.tstore.details.ui.bbps.BbpsShareImageUtils import com.navi.pay.tstore.details.ui.gold.DigitalGoldShareImageUtils import com.navi.pay.tstore.details.ui.lending.LendingShareImageUtils import com.navi.pay.tstore.details.ui.upi.ConfeeInfoBottomSheet +import com.navi.pay.tstore.details.ui.upi.ConversionDetailsBottomSheet +import com.navi.pay.tstore.details.util.OrderDetailsMetadataProvider import com.navi.pay.tstore.details.viewmodel.OrderDetailsScreenBottomSheetUIState import com.navi.pay.tstore.details.viewmodel.OrderDetailsScreenUIState import com.navi.pay.tstore.details.viewmodel.OrderDetailsViewModel +import com.navi.pay.tstore.list.model.view.OrderEntity import com.navi.pay.tstore.list.model.view.OrderStatusOfView import com.navi.pay.tstore.utils.error.model.OrderErrorEntity import com.navi.pay.utils.COIN_HISTORY_URL @@ -151,17 +155,10 @@ fun OrderDetailsScreen( } } - val onBackClick = { - naviPayActivity.window.statusBarColor = - ContextCompat.getColor(naviPayActivity, R.color.navi_pay_status_bar_default_color) - navigator.navigateUp(naviPayActivity) - } - - BackHandler { onBackClick() } - val clipboardManager = remember { naviPayActivity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } + val context = LocalContext.current val scope = rememberCoroutineScope() val orderHistoryDetailItemProperty by @@ -389,19 +386,6 @@ fun OrderDetailsScreen( naviPayAnalytics.onHowToUseRrnBottomSheetLoaded() } - val bottomSheetState = - rememberModalBottomSheetState(skipPartiallyExpanded = true, confirmValueChange = { true }) - - val dismissBottomSheet: () -> Unit = { - scope - .launch { bottomSheetState.hide() } - .invokeOnCompletion { - if (!bottomSheetState.isVisible) { - orderDetailsViewModel.updateBottomSheetUIState(showBottomSheet = false) - } - } - } - val openNotifyPermissionBottomSheet = { orderDetailsViewModel.updateBottomSheetUIState( bottomSheetUIState = @@ -454,11 +438,45 @@ fun OrderDetailsScreen( } } + val onNotifyButtonClicked = { + orderDetailsViewModel.onNotifyButtonClicked() + notificationPermissionState.launchMultiplePermissionRequest() + } + + val bottomSheetState = + rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { bottomSheetStateHolder.bottomSheetStateChange }, + ) + + val dismissBottomSheet: () -> Unit = { + scope + .launch { bottomSheetState.hide() } + .invokeOnCompletion { + if (!bottomSheetState.isVisible) { + orderDetailsViewModel.updateBottomSheetUIState(showBottomSheet = false) + } + } + } + + val onBackClick = { + if (bottomSheetState.isVisible) { + dismissBottomSheet() + } else { + naviPayActivity.window.statusBarColor = + ContextCompat.getColor(naviPayActivity, R.color.navi_pay_status_bar_default_color) + navigator.navigateUp(naviPayActivity) + } + } + + BackHandler { onBackClick() } + if (bottomSheetStateHolder.showBottomSheet) { NaviPayModalBottomSheet( modifier = Modifier.fillMaxWidth(), bottomSheetState = bottomSheetState, onDismissRequest = dismissBottomSheet, + shouldDismissOnBackPress = true, bottomSheetContent = { RenderOrderDetailsScreenBottomSheet( bottomSheetUIState = bottomSheetStateHolder.bottomSheetUIState, @@ -466,16 +484,13 @@ fun OrderDetailsScreen( naviPayAnalytics = naviPayAnalytics, naviPayActivity = naviPayActivity, productType = orderEntity?.productType.orEmpty(), + orderEntity = orderEntity, + orderDetailsMetadataProvider = orderDetailsMetadataProvider, ) }, ) } - val onNotifyButtonClicked = { - orderDetailsViewModel.onNotifyButtonClicked() - notificationPermissionState.launchMultiplePermissionRequest() - } - when (uiState) { OrderDetailsScreenUIState.Loading -> LoadingScreen() OrderDetailsScreenUIState.Loaded -> { @@ -595,6 +610,7 @@ fun OrderDetailsScreen( onManageSipClicked = onManageSipClicked, onBuyAgainClicked = onBuyAgainClicked, onDownloadInvoiceClicked = onDownloadInvoiceClicked, + onConversionDetailsClicked = { orderDetailsViewModel.onConversionDetailsClicked() }, ) } } @@ -607,6 +623,8 @@ fun RenderOrderDetailsScreenBottomSheet( naviPayAnalytics: NaviPayAnalytics.OrderDetails, closeSheet: () -> Unit, productType: String, + orderEntity: OrderEntity?, + orderDetailsMetadataProvider: OrderDetailsMetadataProvider, ) { when (bottomSheetUIState) { @@ -677,5 +695,52 @@ fun RenderOrderDetailsScreenBottomSheet( onPrimaryButtonClicked = { closeSheet() }, ) } + OrderDetailsScreenBottomSheetUIState.ConversionDetails -> { + ConversionDetailsBottomSheet( + amountInRupees = orderEntity?.formattedAmount.orEmpty(), + bankCharges = + getBankCharges( + forex = + orderDetailsMetadataProvider + .getNaviPayMetadata() + ?.upiGlobalInfo + ?.forex + .orEmpty(), + markUp = + orderDetailsMetadataProvider + .getNaviPayMetadata() + ?.upiGlobalInfo + ?.markUp + .orEmpty(), + baseAmount = + orderDetailsMetadataProvider + .getNaviPayMetadata() + ?.upiGlobalInfo + ?.baseAmount + .orEmpty(), + ) + .toString(), + currencySymbol = + orderDetailsMetadataProvider + .getNaviPayMetadata() + ?.upiGlobalInfo + ?.baseCurr + .orEmpty(), + exchangeRate = + orderDetailsMetadataProvider + .getNaviPayMetadata() + ?.upiGlobalInfo + ?.forex + .orEmpty(), + foreignCurrencyMarkUpRate = + orderDetailsMetadataProvider + .getNaviPayMetadata() + ?.upiGlobalInfo + ?.markUp + .orEmpty(), + naviPayAnalytics = NaviPayAnalytics.INSTANCE.OrderDetails(), + onPrimaryButtonClicked = closeSheet, + ) + } } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreenContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreenContent.kt index 8f842b036f..56b8e8ca08 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreenContent.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsScreenContent.kt @@ -112,6 +112,7 @@ fun OrderDetailsScreenContent( onManageSipClicked: () -> Unit, onBuyAgainClicked: () -> Unit, onDownloadInvoiceClicked: () -> Unit, + onConversionDetailsClicked: () -> Unit, ) { val lifecycleOwner = LocalLifecycleOwner.current @@ -238,6 +239,7 @@ fun OrderDetailsScreenContent( onManageSipClicked = onManageSipClicked, onBuyAgainClicked = onBuyAgainClicked, onDownloadInvoiceClicked = onDownloadInvoiceClicked, + onConversionDetailsClicked = onConversionDetailsClicked, ) } }, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt index 9e1d517d06..f3557b4f80 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt @@ -143,6 +143,7 @@ internal fun OrderDetailsSummarySection( onManageSipClicked: () -> Unit, onBuyAgainClicked: () -> Unit, onDownloadInvoiceClicked: () -> Unit, + onConversionDetailsClicked: () -> Unit, ) { paymentStatusWidgetProperties?.let { @@ -218,6 +219,7 @@ internal fun OrderDetailsSummarySection( Column(modifier = Modifier.fillMaxWidth()) { when (orderEntity?.productType) { OrderProductType.UPI.name -> { + NaviPayOrderDetailsSummarySection( orderEntity = orderEntity, naviPayTransactionDetailsMetadata = @@ -247,6 +249,7 @@ internal fun OrderDetailsSummarySection( onAutopayDetailsCtaClicked = onAutopayDetailsCtaClicked, selectedCtaToShowLoader = selectedCtaToShowLoader, onConfeeInfoClicked = onConfeeInfoClicked, + onConversionDetailsClicked = onConversionDetailsClicked, ) } OrderProductType.BBPS.name -> { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/PaymentStatusSection.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/PaymentStatusSection.kt index 9be12ff89d..5433b1a9f4 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/PaymentStatusSection.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/PaymentStatusSection.kt @@ -80,6 +80,8 @@ internal fun PaymentStatusSection( id = R.string.rupee_symbol_x, orderEntity?.formattedAmountAfterCoins.orEmpty(), ) + } else if (orderEntity?.isUpiGlobalTransaction.orFalse()) { + "${orderEntity?.upiGlobalCurrency.orEmpty()} ${orderEntity?.upiGlobalAmount.orEmpty()}" } else { stringResource( id = R.string.rupee_symbol_x, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayOrderDetailsSummarySection.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayOrderDetailsSummarySection.kt index e3de03f7e8..48fb1c7d07 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayOrderDetailsSummarySection.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayOrderDetailsSummarySection.kt @@ -8,6 +8,7 @@ package com.navi.pay.tstore.details.ui.upi import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -26,9 +27,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,8 +53,10 @@ import com.navi.design.font.naviFontFamily import com.navi.naviwidgets.extensions.NaviText import com.navi.naviwidgets.utils.EMPTY import com.navi.pay.R +import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.common.theme.color.NaviPayColor import com.navi.pay.common.theme.color.NaviPayColor.ctaPrimary +import com.navi.pay.common.ui.ConversionDetailSection import com.navi.pay.common.ui.KeyValueWithCopySection import com.navi.pay.common.ui.OutlineRoundedThemeButton import com.navi.pay.common.ui.ThemeRoundedButton @@ -64,6 +69,7 @@ import com.navi.pay.utils.AT_THE_RATE_CHAR import com.navi.pay.utils.DOT_PNG import com.navi.pay.utils.NAVI_PAY_DEFAULT_MCC import com.navi.pay.utils.RUPEE_SYMBOL +import com.navi.pay.utils.clickableDebounce import com.navi.pay.utils.noRippleClickableWithDebounce @Composable @@ -92,8 +98,26 @@ fun NaviPayOrderDetailsSummarySection( onAutopayDetailsCtaClicked: () -> Unit, selectedCtaToShowLoader: OrderDetailsLoadingStateCtaType? = null, onConfeeInfoClicked: () -> Unit, + onConversionDetailsClicked: () -> Unit, ) { Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp)) { + if (orderEntity?.isUpiGlobalTransaction.orFalse()) { + + CurrencyConversionDetailsSection( + showLoader = showLoader, + currencySymbol = + naviPayTransactionDetailsMetadata?.upiGlobalInfo?.baseCurr.orEmpty(), + foreignExchangeRate = + naviPayTransactionDetailsMetadata?.upiGlobalInfo?.forex.orEmpty(), + amountInRupees = orderEntity?.formattedAmount.orEmpty(), + onConversionDetailsClicked = onConversionDetailsClicked, + ) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), + color = NaviPayColor.borderAlt, + ) + } PayeeDetailSection( onPayeeLevelTransactionHistoryCtaClicked = onPayeeLevelTransactionHistoryCtaClicked, onOrderDescriptionClicked = onOrderDescriptionClicked, @@ -689,3 +713,140 @@ fun ConfeeInfoBottomSheet(onDismissBottomSheet: () -> Unit) { ) } } + +@Composable +fun CurrencyConversionDetailsSection( + showLoader: Boolean, + currencySymbol: String, + foreignExchangeRate: String, + amountInRupees: String, + onConversionDetailsClicked: () -> Unit, +) { + + Column(modifier = Modifier.fillMaxWidth()) { + NaviText( + text = stringResource(id = R.string.np_converted_amount), + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = NaviPayColor.textTertiary, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + ) + + Spacer(modifier = Modifier.height(6.dp)) + + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { + NaviText( + text = stringResource(id = R.string.rupee_symbol_x, amountInRupees), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 14.sp, + color = NaviPayColor.textPrimary, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + modifier = Modifier.weight(1f, false), + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + NaviText( + modifier = + Modifier.clickableDebounce(rippleColor = ctaPrimary) { + if (showLoader.not()) onConversionDetailsClicked() + }, + text = stringResource(id = R.string.np_conversion_details), + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ctaPrimary, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + textDecoration = TextDecoration.Underline, + ) + } + + NaviText( + text = + stringResource( + id = R.string.np_global_amount_conversion, + currencySymbol, + foreignExchangeRate, + ), + fontSize = 14.sp, + color = NaviPayColor.textTertiary, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + ) + } +} + +@Composable +fun ConversionDetailsBottomSheet( + amountInRupees: String, + currencySymbol: String, + foreignCurrencyMarkUpRate: String, + bankCharges: String, + exchangeRate: String, + naviPayAnalytics: NaviPayAnalytics.OrderDetails, + onPrimaryButtonClicked: () -> Unit, +) { + + LaunchedEffect(key1 = Unit) { + naviPayAnalytics.onConversionDetailsBottomSheetLanded( + convertedAmount = amountInRupees, + bankMarkUpRate = bankCharges, + ) + } + + Column( + modifier = + Modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 32.dp) + .animateContentSize() + ) { + NaviText( + text = stringResource(id = R.string.np_conversion_details), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + fontSize = 16.sp, + lineHeight = 24.sp, + color = NaviPayColor.textPrimary, + ) + Spacer(modifier = Modifier.height(12.dp)) + + Column( + modifier = + Modifier.fillMaxWidth() + .background(color = NaviPayColor.bgAlt, shape = RoundedCornerShape(4.dp)) + .padding(16.dp) + ) { + ConversionDetailSection( + title = stringResource(id = R.string.np_converted_amount), + description = + stringResource( + id = R.string.np_global_amount_conversion, + currencySymbol, + exchangeRate, + ), + value = stringResource(id = R.string.rupee_symbol_x, amountInRupees), + ) + Spacer(modifier = Modifier.height(16.dp)) + ConversionDetailSection( + title = stringResource(id = R.string.np_bank_forex_charges), + description = "(${foreignCurrencyMarkUpRate}%)", + value = stringResource(id = R.string.rupee_symbol_x, bankCharges), + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + ThemeRoundedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.np_okay_got_it), + onClick = onPrimaryButtonClicked, + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayTransactionDetailsMetadata.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayTransactionDetailsMetadata.kt index b4554be280..0ab6a26df1 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayTransactionDetailsMetadata.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/NaviPayTransactionDetailsMetadata.kt @@ -38,6 +38,7 @@ data class NaviPayTransactionDetailsMetadata( @SerializedName("txnVersion") val txnVersion: String? = null, @SerializedName("updatedAt") val updatedAt: String?, @SerializedName("upiInfo") val upiInfo: UpiInfo, + @SerializedName("upiGlobalInfo") val upiGlobalInfo: UpiGlobalInfo? = null, @SerializedName("upiReqId") val upiReqId: String?, @SerializedName("splitDetails") val splitDetails: List? = null, ) { @@ -119,3 +120,10 @@ data class UpiInfo( @SerializedName("gwRefId") val gwRefId: String?, @SerializedName("gwTxnId") val gwTxnId: String?, ) + +data class UpiGlobalInfo( + @SerializedName("base_curr") val baseCurr: String? = null, + @SerializedName("base_amount") val baseAmount: String? = null, + @SerializedName("forex") val forex: String? = null, + @SerializedName("mark_up") val markUp: String? = null, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/UpiReceiptEntityProvider.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/UpiReceiptEntityProvider.kt index 6093fa5bda..8223785b63 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/UpiReceiptEntityProvider.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/upi/UpiReceiptEntityProvider.kt @@ -75,6 +75,11 @@ class UpiReceiptEntityProvider { isCredited = orderEntity?.orderStatusOfView == OrderStatusOfView.Credit, shareReceiptCallOutText = shareReceiptCallOutText, payeeMcc = naviPayTransactionDetailsMetadata?.payeeInfo?.mcc.orEmpty(), + currencySymbol = + naviPayTransactionDetailsMetadata?.upiGlobalInfo?.baseCurr.orEmpty(), + isUpiGlobalTransaction = orderEntity?.isUpiGlobalTransaction.orFalse(), + baseCurrencyAmount = + naviPayTransactionDetailsMetadata?.upiGlobalInfo?.baseAmount.orEmpty(), ) return receiptDetailsEntity } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/viewmodel/OrderDetailsViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/viewmodel/OrderDetailsViewModel.kt index 2ba5c7fcea..0f18b58b7a 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/viewmodel/OrderDetailsViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/viewmodel/OrderDetailsViewModel.kt @@ -354,6 +354,7 @@ constructor( bottomSheetUIState = OrderDetailsScreenBottomSheetUIState.NotificationPermissionsBottomSheet, showBottomSheet = false, + bottomSheetStateChange = true, ) ) @@ -806,6 +807,9 @@ constructor( if (isMerchantIncomingTransaction(orderEntity)) return false + if (orderEntity.isUpiGlobalTransaction) { + return false + } return true } @@ -1598,6 +1602,28 @@ constructor( _linkedAccounts.update { linkedAccounts } } + fun onConversionDetailsClicked() { + viewModelScope.launch(Dispatchers.IO) { + val naviPayTransactionDetailsMetadata = + orderDetailsMetadataProvider.value.getNaviPayMetadata() + + naviPayAnalytics.onConversionDetailsClicked( + upiRequestId = naviPayTransactionDetailsMetadata?.upiRequestIdFormatted.orEmpty(), + status = naviPayTransactionDetailsMetadata?.txnStatus.orEmpty(), + source = screenName, + accountAdded = isTransactionAccountActive, + transactionType = naviPayTransactionDetailsMetadata?.txnType.orEmpty(), + currency = naviPayTransactionDetailsMetadata?.currency.orEmpty(), + amountInRupees = orderEntity.value?.formattedAmount.orEmpty(), + ) + + updateBottomSheetUIState( + bottomSheetUIState = OrderDetailsScreenBottomSheetUIState.ConversionDetails, + showBottomSheet = true, + ) + } + } + fun checkBalance(bankAccountUniqueId: String?) { viewModelScope.launch(Dispatchers.IO) { val linkedAccounts = linkedAccountsUseCase.execute(screenName = screenName).first() @@ -2196,6 +2222,8 @@ sealed class OrderDetailsScreenBottomSheetUIState { val orderErrorEntity: OrderErrorEntity, val primaryButtonText: String, ) : OrderDetailsScreenBottomSheetUIState() + + data object ConversionDetails : OrderDetailsScreenBottomSheetUIState() } enum class ArcCoinsPromiseValueStateType { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/db/dao/OrderDao.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/db/dao/OrderDao.kt index 98f094fd4f..41584e223b 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/db/dao/OrderDao.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/db/dao/OrderDao.kt @@ -37,7 +37,7 @@ interface OrderDao { @Query( "SELECT " + "orderReferenceId, amount, currency, orderTitle, orderDescription, orderStatusOfView, orderTimestamp, orderImageUrl, " + - "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash, orderDetails " + + "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash, upiGlobalCurrencyAndAmount, orderDetails " + "FROM $NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME " + "ORDER BY orderTimestamp DESC" ) @@ -47,7 +47,7 @@ interface OrderDao { @Query( "SELECT " + "orderReferenceId, amount, currency, orderTitle, orderDescription, orderStatusOfView, orderTimestamp, orderImageUrl, " + - "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash " + + "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash, upiGlobalCurrencyAndAmount " + "FROM $NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME " + "WHERE orderTitle like :searchQuery OR orderDescription like :searchQuery OR amount like :searchQuery OR otherUserInfo like :searchQuery " + "ORDER BY orderTimestamp DESC" @@ -67,7 +67,7 @@ interface OrderDao { @Query( "SELECT " + "orderReferenceId, amount, currency, orderTitle, orderDescription, orderStatusOfView, orderTimestamp, orderImageUrl, " + - "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash " + + "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash, upiGlobalCurrencyAndAmount " + "FROM $NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME " + "WHERE productId = :umn " + "ORDER BY orderTimestamp DESC" @@ -95,7 +95,7 @@ interface OrderDao { @Query( "SELECT " + "orderReferenceId, amount, currency, orderTitle, orderDescription, orderStatusOfView, orderTimestamp, orderImageUrl, " + - "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash " + + "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash , upiGlobalCurrencyAndAmount " + "FROM $NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME " + "WHERE orderTimestamp >= :minDateTime AND orderTimestamp <= :maxDateTime " + "AND monthTag in (:monthTagList) " diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/network/OrderItem.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/network/OrderItem.kt index 9398229218..fe8734feb0 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/network/OrderItem.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/network/OrderItem.kt @@ -33,6 +33,7 @@ import com.navi.pay.tstore.list.util.getOrderTitleToDisplay import com.navi.pay.tstore.list.util.getPaymentModeTags import com.navi.pay.utils.NAVI_PAY_UPI_LITE_LOGO_URL import com.navi.pay.utils.NAVI_PAY_UPI_LITE_SEND_MONEY_PURPOSE_CODE +import com.navi.pay.utils.UPI_GLOBAL_PURPOSE import com.navi.rr.utils.ext.toJson import java.lang.reflect.Type import kotlinx.parcelize.Parcelize @@ -212,6 +213,26 @@ fun OrderItem.toOrderEntity( orderImageUrl.orEmpty() } + val isSendMoneyViaUpiGlobal = + if ( + productType.orEmpty().contains(OrderProductType.UPI.name) && + orderType.orEmpty().contains(OrderType.SEND_MONEY.name) + ) { + naviPayMetadata?.purposeCode == UPI_GLOBAL_PURPOSE + } else false + + val upiGlobalCurrencyAndAmount = + if (isSendMoneyViaUpiGlobal) { + + getTagStringWithSeparator( + naviPayMetadata?.upiGlobalInfo?.baseCurr, + naviPayMetadata?.upiGlobalInfo?.baseAmount, + shouldSort = false, + ) + } else { + "" + } + return OrderEntity( orderReferenceId = orderReferenceId, productType = productType.orEmpty(), @@ -251,5 +272,6 @@ fun OrderItem.toOrderEntity( errorCode = errorCode.orEmpty(), naviUpiAccInfo = orderDetails?.naviUpiAccInfo, ), + upiGlobalCurrencyAndAmount = upiGlobalCurrencyAndAmount, ) } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderEntity.kt index 9ef8b09b06..531a27a17c 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderEntity.kt @@ -35,11 +35,13 @@ import com.navi.pay.tstore.details.util.OrderDetailsMetadataProvider import com.navi.pay.tstore.list.db.converter.OrderDetailConverter import com.navi.pay.tstore.list.model.network.OrderType import com.navi.pay.tstore.list.util.OrderStatusOfViewConverter +import com.navi.pay.tstore.list.util.getUpiGlobalAmount +import com.navi.pay.tstore.list.util.getUpiGlobalCurrency import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR_AT_TIME import com.navi.pay.utils.NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME import com.navi.pay.utils.NAVI_PAY_UPI_LITE_SEND_MONEY_PURPOSE_CODE +import com.navi.pay.utils.UPI_GLOBAL_PURPOSE import com.navi.pay.utils.UPI_LITE -import kotlin.text.toLong import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.joda.time.DateTime @@ -75,6 +77,7 @@ data class OrderEntity( @ColumnInfo(name = "otherUserInfo") val otherUserInfo: String, @ColumnInfo(name = "monthTag") val monthTag: String, @ColumnInfo(name = "coinEquivalentCash") val coinEquivalentCash: String, + @ColumnInfo(name = "upiGlobalCurrencyAndAmount") val upiGlobalCurrencyAndAmount: String, @ColumnInfo(name = "orderDetails") @TypeConverters(OrderDetailConverter::class) val orderDetails: OrderDetailEntity, @@ -183,6 +186,29 @@ data class OrderEntity( } else false } + @IgnoredOnParcel + @delegate:Ignore + val isUpiGlobalTransaction by lazy { + if ( + productType.contains(OrderProductType.UPI.name) && + orderType.contains(OrderType.SEND_MONEY.name) + ) { + isTransactionViaUpiGlobal() + } else false + } + + @IgnoredOnParcel + @delegate:Ignore + val upiGlobalCurrency by lazy { + getUpiGlobalCurrency(upiGlobalCurrencyAndAmount = upiGlobalCurrencyAndAmount) + } + + @IgnoredOnParcel + @delegate:Ignore + val upiGlobalAmount by lazy { + getUpiGlobalAmount(upiGlobalCurrencyAndAmount = upiGlobalCurrencyAndAmount) + } + @IgnoredOnParcel @delegate:Ignore val isTransactionOfTypeUpiLite by lazy { @@ -359,4 +385,10 @@ data class OrderEntity( bankCode.isNullOrEmpty() } ?: true } + + private fun isTransactionViaUpiGlobal(): Boolean { + val metadataProvider = OrderDetailsMetadataProvider(this) + val naviPayMetadata = metadataProvider.getNaviPayMetadata() + return naviPayMetadata?.purposeCode == UPI_GLOBAL_PURPOSE + } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderHistoryEntity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderHistoryEntity.kt index 6e71e48632..d2b0bfe47a 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderHistoryEntity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/model/view/OrderHistoryEntity.kt @@ -17,6 +17,8 @@ import com.navi.pay.management.common.transaction.util.getOwnBankIconUrlFromOwnB import com.navi.pay.management.common.transaction.util.getOwnVpaFromOwnBankInfo import com.navi.pay.tstore.details.model.view.OrderPaymentStatus import com.navi.pay.tstore.list.model.network.OrderType +import com.navi.pay.tstore.list.util.getUpiGlobalAmount +import com.navi.pay.tstore.list.util.getUpiGlobalCurrency import com.navi.pay.utils.NAVI_PAY_SELF_TRANSFER_LOGO_URL import com.navi.pay.utils.NAVI_PAY_UPI_LITE_LOGO_URL import kotlinx.parcelize.IgnoredOnParcel @@ -44,6 +46,7 @@ data class OrderHistoryEntity( val productId: String, val monthTag: String, val coinEquivalentCash: String, + val upiGlobalCurrencyAndAmount: String, ) { val otherUserIconUrl by lazy { @@ -173,6 +176,24 @@ data class OrderHistoryEntity( @delegate:Ignore val isGoldTransaction by lazy { isOrderOfGoldBuy || isOrderOfGoldSell || isOrderOfGoldSip } + @IgnoredOnParcel + @delegate:Ignore + val isUpiGlobalTransaction by lazy { + categoryTags.contains(TransactionCategoryTags.UPI_GLOBAL.value) + } + + @IgnoredOnParcel + @delegate:Ignore + val upiGlobalCurrency by lazy { + getUpiGlobalCurrency(upiGlobalCurrencyAndAmount = upiGlobalCurrencyAndAmount) + } + + @IgnoredOnParcel + @delegate:Ignore + val upiGlobalAmount by lazy { + getUpiGlobalAmount(upiGlobalCurrencyAndAmount = upiGlobalCurrencyAndAmount) + } + @delegate:Ignore val formattedAmount by lazy { amount.getDisplayableAmount() } @IgnoredOnParcel diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/repository/OrderRepository.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/repository/OrderRepository.kt index f6c8e9764b..b039f85e4c 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/repository/OrderRepository.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/repository/OrderRepository.kt @@ -166,7 +166,7 @@ constructor( val selectQuery = "SELECT " + "orderReferenceId, amount, currency, orderTitle, orderDescription, orderStatusOfView, orderTimestamp, orderImageUrl, " + - "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash " + + "categoryTags, paymentModeTags, productType, productId, paymentStatus, orderType, ownBankInfo, monthTag, coinEquivalentCash, upiGlobalCurrencyAndAmount " + "FROM $NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME " val whereClauseWithSearchQuery = diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/ui/OrderHistoryItemView.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/ui/OrderHistoryItemView.kt index a68cd7082d..121ed98fa9 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/ui/OrderHistoryItemView.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/ui/OrderHistoryItemView.kt @@ -18,6 +18,7 @@ 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -50,6 +51,7 @@ import com.navi.naviwidgets.extensions.NaviText import com.navi.pay.R import com.navi.pay.common.theme.color.NaviPayColor import com.navi.pay.common.ui.ContactIconView +import com.navi.pay.common.ui.ImageWithCircularBackground import com.navi.pay.tstore.list.model.network.OrderType import com.navi.pay.tstore.list.model.view.OrderHistoryEntity import com.navi.pay.tstore.list.model.view.OrderStatusOfView @@ -152,7 +154,17 @@ fun RenderOrderHistoryItemView( modifier = Modifier.widthIn(max = 200.dp), maxLines = 1, ) - + if (orderHistoryEntity.isUpiGlobalTransaction) { + NaviText( + text = + "${orderHistoryEntity.upiGlobalCurrency} ${orderHistoryEntity.upiGlobalAmount}", + fontFamily = naviFontFamily, + fontSize = 12.sp, + lineHeight = 18.sp, + color = NaviPayColor.textTertiary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + ) + } if (orderHistoryEntity.isOrderOfMandateType && mandateTagEnabled) { Box( @@ -273,7 +285,11 @@ fun RenderOrderHistoryItemView( ) { AsyncImage( model = - "$upiAppLogoBaseUrl${orderHistoryEntity.ownVpa.substringAfter(AT_THE_RATE_CHAR)}$DOT_PNG", + "$upiAppLogoBaseUrl${ + orderHistoryEntity.ownVpa.substringAfter( + AT_THE_RATE_CHAR + ) + }$DOT_PNG", contentDescription = "", fallback = painterResource(id = R.drawable.ic_np_upi_app_logo), @@ -343,7 +359,6 @@ fun RenderOrderHistoryItemView( } } } - if (showDivider) { HorizontalDivider(modifier = Modifier.fillMaxWidth(), color = NaviPayColor.borderDefault) } @@ -353,7 +368,6 @@ fun RenderOrderHistoryItemView( fun LeftImageIconOfListView(index: Int, orderHistoryEntity: OrderHistoryEntity) { when { orderHistoryEntity.isOrderOfSelfTransferType -> { - Image( painter = painterResource( @@ -377,16 +391,16 @@ fun LeftImageIconOfListView(index: Int, orderHistoryEntity: OrderHistoryEntity) Box( modifier = Modifier.size(40.dp) - .background(color = NaviPayColor.bgAlt, shape = CircleShape) - .conditional( + .background(color = NaviPayColor.bgAlt, shape = CircleShape), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + modifier = + Modifier.fillMaxSize().padding(6.dp).conditional( orderHistoryEntity.orderStatusOfView == OrderStatusOfView.Failed ) { graphicsLayer(alpha = 0.75f) }, - contentAlignment = Alignment.Center, - ) { - AsyncImage( - modifier = Modifier.fillMaxSize().padding(6.dp), model = orderHistoryEntity.otherUserIconUrl, contentDescription = "", fallback = painterResource(id = R.drawable.ic_np_mcc_untagged), @@ -399,6 +413,25 @@ fun LeftImageIconOfListView(index: Int, orderHistoryEntity: OrderHistoryEntity) null }, ) + + if (orderHistoryEntity.isUpiGlobalTransaction) { + ImageWithCircularBackground( + modifier = + Modifier.align(Alignment.BottomEnd) + .offset(x = (4).dp, y = (4).dp) + .conditional( + orderHistoryEntity.orderStatusOfView == OrderStatusOfView.Failed + ) { + graphicsLayer(alpha = 0.75f) + }, + boxSize = 18.dp, + imageUrl = null, + imageSize = 16.dp, + fallbackIconUrl = R.drawable.ic_np_global_icon, + borderWidth = 1.dp, + borderColor = NaviPayColor.borderDefault, + ) + } } } orderHistoryEntity.isCashLoanTransaction || orderHistoryEntity.isHomeLoanTransaction -> { @@ -470,6 +503,7 @@ fun LeftImageIconOfListView(index: Int, orderHistoryEntity: OrderHistoryEntity) OrderStatusOfView.RequestMoneyFailed, OrderStatusOfView.CollectRequestExpired, OrderStatusOfView.CollectRequestFailed -> NaviPayColor.inputFieldDefault + else -> null }, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/util/OrderEntityMapperUtil.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/util/OrderEntityMapperUtil.kt index 08ad037937..0aeb2025d1 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/util/OrderEntityMapperUtil.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/util/OrderEntityMapperUtil.kt @@ -32,6 +32,8 @@ import com.navi.pay.utils.DEFAULT_INITIATION_MODE_DYNAMIC_SECURE_QR_OFFLINE import com.navi.pay.utils.DEFAULT_INITIATION_MODE_DYNAMIC_SECURE_QR_ONLINE import com.navi.pay.utils.DEFAULT_SECURE_INITIATION_MODE_INTENT_MANDATE_AND_TRANSACTION import com.navi.pay.utils.NAVI_PAY_DEFAULT_MCC +import com.navi.pay.utils.NAVI_PAY_TRANSACTION_HISTORY_TAG_SEPARATOR +import com.navi.pay.utils.UPI_GLOBAL_PURPOSE fun OrderItem.getOrderCategoryTagsWithSeparator( naviPayMetadata: NaviPayTransactionDetailsMetadata? @@ -99,6 +101,10 @@ fun OrderItem.getOrderCategoryTagsWithSeparator( eligibleTags.add(TransactionCategoryTags.SELF_PAY.value) } + if (naviPayMetadata?.purposeCode == UPI_GLOBAL_PURPOSE) { + eligibleTags.add(TransactionCategoryTags.UPI_GLOBAL.value) + } + if (productType == OrderProductType.BBPS.name) { eligibleTags.add(TransactionCategoryTags.BBPS.value) } @@ -234,6 +240,22 @@ fun isIntentTransaction( ?: false } +fun getUpiGlobalCurrency(upiGlobalCurrencyAndAmount: String): String { + return upiGlobalCurrencyAndAmount.split(NAVI_PAY_TRANSACTION_HISTORY_TAG_SEPARATOR).getOrElse( + 0 + ) { + "" + } +} + +fun getUpiGlobalAmount(upiGlobalCurrencyAndAmount: String): String { + return upiGlobalCurrencyAndAmount.split(NAVI_PAY_TRANSACTION_HISTORY_TAG_SEPARATOR).getOrElse( + 1 + ) { + "" + } +} + fun isMerchantTransactionWithDynamicQR( orderEntity: OrderEntity, naviPayTransactionDetailsMetaData: NaviPayTransactionDetailsMetadata?, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/viewmodel/OrderHistoryViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/viewmodel/OrderHistoryViewModel.kt index df70e2c5c9..36778c42cd 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/viewmodel/OrderHistoryViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/list/viewmodel/OrderHistoryViewModel.kt @@ -835,6 +835,14 @@ constructor( isActive = false, type = CATEGORY_FILTER_TAB_INDEX, ), + TransactionTagHolder( + id = 8, + displayTextId = R.string.upi_global_payments, + dbSearchTag = listOf(TransactionCategoryTags.UPI_GLOBAL.value), + isSelected = false, + isActive = false, + type = CATEGORY_FILTER_TAB_INDEX, + ), ) } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt index c5007a2d2c..5664b7a21b 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt @@ -117,9 +117,11 @@ const val DEFAULT_UPI_CURRENCY = "INR" const val DEFAULT_UPI_MODE = "00" const val DEFAULT_QR_SCAN_INITIATION_MODE = "01" const val BBPS_UPI_PURPOSE = "10" +const val UPI_GLOBAL_PURPOSE = "11" const val DEFAULT_UPI_PURPOSE = "00" const val DEFAULT_MANDATE_AMOUNT_RULE = "MAX" const val DEFAULT_INITIATION_MODE_QR_MANDATE = "01" +const val UPI_GLOBAL_DEFAULT_INITIATION_MODE = "02" const val DEFAULT_INITIATION_MODE_INTENT_MANDATE_AND_TRANSACTION = "04" const val DEFAULT_SECURE_INITIATION_MODE_INTENT_MANDATE_AND_TRANSACTION = "05" const val DEFAULT_INITIATION_MODE_DYNAMIC_QR_OFFLINE = "15" @@ -127,6 +129,7 @@ const val DEFAULT_INITIATION_MODE_DYNAMIC_SECURE_QR_OFFLINE = "16" const val DEFAULT_INITIATION_MODE_DYNAMIC_QR_ONLINE = "22" const val DEFAULT_INITIATION_MODE_DYNAMIC_SECURE_QR_ONLINE = "23" const val DEFAULT_INITIATION_MODE_DYNAMIC_ATM_QR = "18" +const val UPI_GLOBAL_MAX_ALLOWED_ACTIVATION_DAYS = 89 // Navi Common DB cache keys const val NAVI_PAY_NPCI_KEY = "naviPayNpciKey" diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayExt.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayExt.kt index 6f89ef4ec8..3af8228a0b 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayExt.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayExt.kt @@ -46,6 +46,8 @@ import com.navi.common.network.models.RepoResult import com.navi.common.utils.ClickDebounce import com.navi.common.utils.Constants import com.navi.common.utils.Constants.AT_THE_RATE +import com.navi.common.utils.Constants.DECIMAL +import com.navi.common.utils.Constants.MINUS import com.navi.common.utils.appendStrings import com.navi.common.utils.get import com.navi.design.utils.shimmerEffect @@ -100,6 +102,36 @@ fun String.toPlainAmount(): String { } } +fun String.filterValidCharacters(): String { + return this.filter { it.isDigit() || it == DECIMAL.first() || it == MINUS.first() } +} + +fun String.globalFormattedCurrency(): String { + if (this.isBlank()) return EMPTY + val parts = this.split(DECIMAL) + val beforeDecimal = parts.getOrNull(0)?.let { it.ifBlank { ZERO_STRING } } ?: ZERO_STRING + val afterDecimal = parts.getOrNull(1) ?: EMPTY + + val formattedText = StringBuilder() + val isNegative = beforeDecimal.startsWith(MINUS) + val reversedAmount = beforeDecimal.removePrefix(MINUS).reversed() + + for ((index, char) in reversedAmount.withIndex()) { + if (index != 0 && index % 3 == 0) { + formattedText.append(COMMA) + } + formattedText.append(char) + } + formattedText.reverse() + if (isNegative) { + formattedText.insert(0, MINUS) + } + if (this.contains(DECIMAL)) { + formattedText.append(DECIMAL).append(afterDecimal) + } + return formattedText.toString() +} + fun Double.roundTo(decimals: Int): Double { return when { this.isInfinite() -> this // Preserve Infinity as-is @@ -383,6 +415,11 @@ fun String.getIfscForBankUptime(): String { return this.take(4).uppercase() } +fun String?.safeConvertStringToDouble(): Double { + if (this == null) return 0.0 + return this.toDoubleOrNull() ?: 0.0 +} + fun Activity.launchPermissionSettingsScreen() { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, "package:${this.packageName}".toUri()) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/ShareReceiptUtils.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/ShareReceiptUtils.kt index 590f458dd7..a0667d5b64 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/ShareReceiptUtils.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/ShareReceiptUtils.kt @@ -112,6 +112,8 @@ fun shareReceipt( return "${shareReceiptEntity.payerName} successfully added $RUPEE_SYMBOL${shareReceiptEntity.formattedAmount} to UPI Lite on Navi. Enjoy $externalAppConfigurableText at: $externalAppShareDeepLink" if (shareReceiptEntity.isMandateTransaction) return "${shareReceiptEntity.payerName} paid $RUPEE_SYMBOL${shareReceiptEntity.formattedAmount} to ${shareReceiptEntity.payeeName} via UPI Autopay on Navi. Enjoy $externalAppConfigurableText at: $externalAppShareDeepLink" + if (shareReceiptEntity.isUpiGlobalTransaction) + return "${shareReceiptEntity.payerName} paid ${shareReceiptEntity.currencySymbol}${shareReceiptEntity.baseCurrencyAmount} to ${shareReceiptEntity.payeeName} via Navi UPI. Enjoy $externalAppConfigurableText at: $externalAppShareDeepLink" return "${shareReceiptEntity.payerName} paid $RUPEE_SYMBOL${shareReceiptEntity.formattedAmount} to ${shareReceiptEntity.payeeName} via Navi UPI. Enjoy $externalAppConfigurableText at: $externalAppShareDeepLink" } @@ -206,6 +208,21 @@ fun shareReceipt( .placeholder(CommonR.drawable.ic_upi_bbps_default_bank_logo) .error(CommonR.drawable.ic_upi_bbps_default_bank_logo) .into(icon) + } else if (shareReceiptEntity.isUpiGlobalTransaction) { + icon.visibility = VISIBLE + icon.background = + getNaviDrawable( + shape = DrawableShape.OVAL, + cornerRadius = + resources.getDimension(com.navi.design.R.dimen.dp_1).toInt(), + strokeWidth = + resources.getDimension(com.navi.design.R.dimen.dp_1).toInt(), + strokeColor = getColor(activity, R.color.navi_pay_bg_grey), + ) + Glide.with(context) + .load(R.drawable.ic_np_global_icon) + .placeholder(R.drawable.ic_np_global_icon) + .into(icon) } else if (showUpiAppLogo()) { Glide.with(context) .load(getS3UpiAppLogoUrl()) @@ -330,16 +347,53 @@ fun shareReceipt( getNaviDrawable(shape = DrawableShape.RECTANGLE, cornerRadius = dpToPxInInt(4)) } + if (shareReceiptEntity.isUpiGlobalTransaction) { + globalTransactionConvertedAmount.apply { + setText( + "= " + + resources.getString( + R.string.rupee_symbol_x, + shareReceiptEntity.formattedAmount, + ) + ) + background = + getNaviDrawable( + shape = DrawableShape.RECTANGLE, + backgroundColor = + getColor(activity, R.color.navi_pay_status_bar_default_color), + cornerRadius = dpToPxInInt(1), + strokeColor = getColor(activity, R.color.navi_pay_bg_grey), + strokeWidth = dpToPxInInt(1), + ) + visibility = VISIBLE + } + } + amountText.apply { val formattedAmountText = TextWithStyle( - text = "$RUPEE_SYMBOL ${shareReceiptEntity.formattedAmount}", + text = + if (shareReceiptEntity.isUpiGlobalTransaction) { + (shareReceiptEntity.currencySymbol + + " " + + shareReceiptEntity.baseCurrencyAmount) + } else { + "$RUPEE_SYMBOL ${shareReceiptEntity.formattedAmount}" + }, style = - listOf( - NaviSpan(startSpan = 0, endSpan = 1, fontSize = 32.0), - NaviSpan(startSpan = 1, endSpan = 2, fontSize = 8.0), - NaviSpan(startSpan = 2, endSpan = 10, fontSize = 40.0), - ), + if (shareReceiptEntity.isUpiGlobalTransaction) { + listOf( + NaviSpan(startSpan = 0, endSpan = 3, fontSize = 32.0), + NaviSpan(startSpan = 3, endSpan = 4, fontSize = 8.0), + NaviSpan(startSpan = 4, endSpan = 10, fontSize = 40.0), + ) + } else { + listOf( + NaviSpan(startSpan = 0, endSpan = 1, fontSize = 32.0), + NaviSpan(startSpan = 1, endSpan = 2, fontSize = 8.0), + NaviSpan(startSpan = 2, endSpan = 10, fontSize = 40.0), + ) + }, ) setSpannableString(formattedAmountText) setTextColor( @@ -388,7 +442,9 @@ fun shareReceipt( transactionNoteInfo.apply { setVisibility( view = this, - isVisible = shareReceiptEntity.notes.isNotNullAndNotEmpty(), + isVisible = + shareReceiptEntity.notes.isNotNullAndNotEmpty() && + !shareReceiptEntity.isUpiGlobalTransaction, ) setText(shareReceiptEntity.notes) } @@ -432,6 +488,7 @@ fun shareReceipt( shareReceiptEntity.isUpiLiteClosureTransaction || shareReceiptEntity.isUpiLiteTopUpTransaction || shareReceiptEntity.isSelfPayTransaction || + shareReceiptEntity.isUpiGlobalTransaction || showUpiAppLogo() ) { delay(400) // For Image Loading diff --git a/android/navi-pay/src/main/res/drawable/ic_np_exclamation_dark_grey.xml b/android/navi-pay/src/main/res/drawable/ic_np_exclamation_dark_grey.xml new file mode 100644 index 0000000000..eab041c8af --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_np_exclamation_dark_grey.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/android/navi-pay/src/main/res/drawable/ic_np_global.xml b/android/navi-pay/src/main/res/drawable/ic_np_global.xml new file mode 100644 index 0000000000..cd7554a168 --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_np_global.xml @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/navi-pay/src/main/res/drawable/ic_np_global_icon.xml b/android/navi-pay/src/main/res/drawable/ic_np_global_icon.xml new file mode 100644 index 0000000000..04a6c3bf36 --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_np_global_icon.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + diff --git a/android/navi-pay/src/main/res/drawable/ic_np_global_icon_big.xml b/android/navi-pay/src/main/res/drawable/ic_np_global_icon_big.xml new file mode 100644 index 0000000000..a71ea0d44b --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_np_global_icon_big.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/navi-pay/src/main/res/drawable/ic_np_globe_with_upi.xml b/android/navi-pay/src/main/res/drawable/ic_np_globe_with_upi.xml new file mode 100644 index 0000000000..3906cc8bb7 --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_np_globe_with_upi.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/android/navi-pay/src/main/res/layout/layout_share_transaction_details.xml b/android/navi-pay/src/main/res/layout/layout_share_transaction_details.xml index c6b614c840..5289e5cb2d 100644 --- a/android/navi-pay/src/main/res/layout/layout_share_transaction_details.xml +++ b/android/navi-pay/src/main/res/layout/layout_share_transaction_details.xml @@ -112,20 +112,20 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/dp_24" android:fontFamily="@font/navi_body_regular" - tools:text="Payment successful" android:textColor="@color/navi_pay_text_tertiary" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:text="Payment successful" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/autopay_or_payee" + app:layout_goneMarginTop="@dimen/dp_16"> + tools:text="155.64" /> - - - - + + + + @@ -277,20 +296,20 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/dp_30" android:fontFamily="@font/navi_body_regular" - tools:text="date" android:textColor="@color/navi_pay_text_tertiary" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/dotted_line" /> + app:layout_constraintTop_toBottomOf="@id/dotted_line" + tools:text="date" /> @@ -300,30 +319,30 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/dp_16" - android:paddingHorizontal="@dimen/dp_16" android:fontFamily="@font/navi_body_regular" android:gravity="center" - tools:text="upi id" + android:paddingHorizontal="@dimen/dp_16" android:textColor="@color/navi_pay_text_tertiary" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/payer_payee_second_info" /> + app:layout_constraintTop_toBottomOf="@id/payer_payee_second_info" + tools:text="upi id" /> + app:layout_constraintTop_toBottomOf="@id/transaction_id_info" + tools:text="umn: " /> #E4FFED #031106 #FAFFFC + #E9FFF5 + #EBEBEB \ No newline at end of file diff --git a/android/navi-pay/src/main/res/values/strings.xml b/android/navi-pay/src/main/res/values/strings.xml index d7f76e0ed7..6aa392509b 100644 --- a/android/navi-pay/src/main/res/values/strings.xml +++ b/android/navi-pay/src/main/res/values/strings.xml @@ -26,6 +26,9 @@ Phone settings > Apps > Navi > Permissions Bank accounts Set UPI PIN + Enter your debit card details + Enter your credit card details + Primary Set as primary account Forgot UPI PIN Change UPI PIN @@ -40,8 +43,8 @@ Okay, got it Check balance Manage UPI IDs - Last 6 digits of debit card - Last 6 digits of credit card + Last 6 digit of your debit card + Last 6 digit of your credit card XX-XXXX XXXX-XX Card expiry date @@ -333,7 +336,9 @@ Autopay Payment of ₹%s in progress Payment of ₹%s is in progress + Payment of %s %s is in progress ₹%s paid successfully + %s %s paid successfully Top-up of ₹%s successful Top-up of ₹%s is in progress Blocking %s @@ -346,6 +351,7 @@ Navi UPI View details Invalid debit card expiry date + "%s 1 = ₹%s" Invalid credit card expiry date Payment successful Payment received @@ -478,6 +484,10 @@ UPI Lite transactions are currently not available for same bank-transfers. Add bank account This bank is temporarily unavailable + UPI Global is not activated currently + UPI Global activation is under process + UPI Global deactivation is under process + Does not support UPI Global This bank is facing low success rate Payment service unavailable Unfortunately, our payment services are under maintenance. Please try again after sometime. @@ -983,4 +993,44 @@ Insurance & mutual fund transactions will be here soon. Need help? Report spam & block user + UPI Global payments + Activate UPI Global + Deactivate UPI Global + Redirecting you to activate UPI Global + Redirecting you to deactivate UPI Global + Active till %s + Activation under process + Deactivation under process + Converted amount + Converted amount:\u0020 + Start + End + Your request is under process + Failed to activate UPI Global + You will be notified once UPI Global is activated for this bank account. + This account is not permitted for international payment. Please try again with another account. + You will be notified once UPI Global is deactivated for this bank account. + Processing UPI Global activation + UPI Global activated successfully till %s + Processing UPI Global deactivation + UPI Global deactivated successfully + Select dates + UPI Global can be activated for a maximum of 90 days. You can deactivate it any time. + Select activation period for\n%s + DD/MM/YY + Bank forex charges + Active till + By continuing, you agree to this international payment + "Conversion details" + Activate UPI Global to proceed + It seems that your UPI Global has been deactivated from another app. Please activate it again to continue with the transaction. + International payments on all UPI apps will be deactivated. You can always activate again later. + UPI Global + Add bank account to start making\ninternational payments + You can activate UPI Global + Manage international\npayments for your accounts + Activation under progress + Deactivation under progress + Active till %s + "Amount can’t be less than ₹1" \ No newline at end of file diff --git a/android/navi-payment/src/main/java/com/navi/payment/tstore/ordermanagement/BBPSOrderManager.kt b/android/navi-payment/src/main/java/com/navi/payment/tstore/ordermanagement/BBPSOrderManager.kt index 1747f1a149..1170a07022 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/tstore/ordermanagement/BBPSOrderManager.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/tstore/ordermanagement/BBPSOrderManager.kt @@ -74,6 +74,7 @@ class BBPSOrderManager : IOrderManager { otherUserInfo = optString("orderTitle").orEmpty(), paymentStatus = OrderPaymentStatus.getStatusFromValue(value = optString("status").orEmpty()), + upiGlobalCurrencyAndAmount = "", ) } } diff --git a/android/navi-widgets/src/main/res/drawable/bg_border_white_rounded_4.xml b/android/navi-widgets/src/main/res/drawable/bg_border_white_rounded_4.xml new file mode 100644 index 0000000000..2611389fb9 --- /dev/null +++ b/android/navi-widgets/src/main/res/drawable/bg_border_white_rounded_4.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file