diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/viewmodel/NaviBbpsBaseVM.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/viewmodel/NaviBbpsBaseVM.kt
index 741b4a0c45..df45635d9e 100644
--- a/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/viewmodel/NaviBbpsBaseVM.kt
+++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/viewmodel/NaviBbpsBaseVM.kt
@@ -347,7 +347,9 @@ abstract class NaviBbpsBaseVM(open val naviBbpsVmData: NaviBbpsVmData) : BaseVM(
// Handle success for both responses
if (coinBurnResponse != null) {
val offerDataList =
- offersResponse?.data?.map { offersMapper.map(it) } ?: emptyList()
+ offersResponse?.data?.map {
+ offersMapper.map(offerResponse = it, vertical = ModuleNameV2.BBPS)
+ } ?: emptyList()
val coinBurnData = coinBurnResponse.data
// Update both state objects
@@ -390,7 +392,12 @@ abstract class NaviBbpsBaseVM(open val naviBbpsVmData: NaviBbpsVmData) : BaseVM(
val offerDataList =
data.nudgeResponseBundle.mapValues { (_, value) ->
value
- .map { offerResponse -> mapper.map(offerResponse) }
+ .map { offerResponse ->
+ mapper.map(
+ offerResponse = offerResponse,
+ vertical = ModuleNameV2.BBPS,
+ )
+ }
.filter {
it.titlePrefix.isNotNullAndNotEmpty() &&
it.tags?.contains(BAU)?.not() ?: true
diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/category/BillCategoryViewModelV2.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/category/BillCategoryViewModelV2.kt
index 95d4d77121..0c3a1464fb 100644
--- a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/category/BillCategoryViewModelV2.kt
+++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/category/BillCategoryViewModelV2.kt
@@ -32,7 +32,6 @@ import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getBbpsMetricInfo
import com.navi.bbps.feature.category.model.view.BillCategoryBottomSheetType
import com.navi.bbps.feature.category.model.view.BillCategoryStateV2
import com.navi.bbps.feature.contactlist.PhoneContactManager
-import com.navi.bbps.feature.mybills.DismissBillHandler
import com.navi.bbps.feature.mybills.MyBillsRepository
import com.navi.bbps.feature.mybills.MyBillsSyncJob
import com.navi.bbps.network.di.NaviBbpsGsonBuilder
@@ -60,7 +59,6 @@ constructor(
myBillsRepository: MyBillsRepository,
bbpsResourceProvider: ResourceProvider,
localJsonDataSource: LocalJsonDataSource,
- dismissBillHandler: DismissBillHandler,
myBillEntityToBillerDetailsEntityMapper: MyBillEntityToBillerDetailsEntityMapper,
billDetailsResponseToEntityMapper: BillDetailsResponseToEntityMapper,
myBillEntityToBillCategoryEntityMapper: MyBillEntityToBillCategoryEntityMapper,
diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/ui/BbpsPostPaymentScreenV2.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/ui/BbpsPostPaymentScreenV2.kt
index 3907763783..d8e2b1f881 100644
--- a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/ui/BbpsPostPaymentScreenV2.kt
+++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/ui/BbpsPostPaymentScreenV2.kt
@@ -2009,7 +2009,9 @@ private fun SharedTransitionScope.RewardsBottomBarWithStripSection(
Image(
painter =
- painterResource(id = CommonR.drawable.navi_common_ic_double_right_arrow),
+ painterResource(
+ id = CommonR.drawable.navi_common_ic_double_right_arrow_white
+ ),
contentDescription = EMPTY,
modifier = Modifier.size(20.dp),
)
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 ca0dfa9a78..77efbe6012 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
@@ -128,6 +128,7 @@ object FirebaseRemoteConfigHelper {
const val NAVI_PAY_REMOVE_APP_ON_INTENT_PAYMENT_SUCCESS =
"NAVI_PAY_REMOVE_APP_ON_INTENT_PAYMENT_SUCCESS"
const val NAVI_PAY_CL_MAX_RETRY_COUNT = "NAVI_PAY_CL_MAX_RETRY_COUNT"
+ const val NAVI_PAY_GENERIC_OFFERS_TTL_IN_MILLIS = "NAVI_PAY_GENERIC_OFFERS_TTL_IN_MILLIS"
// COMMON
const val LITMUS_EXPERIMENTS_CACHE_DURATION_IN_MILLIS =
diff --git a/android/navi-common/src/main/res/drawable/navi_common_ic_double_right_arrow.xml b/android/navi-common/src/main/res/drawable/navi_common_ic_double_right_arrow_white.xml
similarity index 62%
rename from android/navi-common/src/main/res/drawable/navi_common_ic_double_right_arrow.xml
rename to android/navi-common/src/main/res/drawable/navi_common_ic_double_right_arrow_white.xml
index 6072e36182..69c5383f00 100644
--- a/android/navi-common/src/main/res/drawable/navi_common_ic_double_right_arrow.xml
+++ b/android/navi-common/src/main/res/drawable/navi_common_ic_double_right_arrow_white.xml
@@ -5,10 +5,10 @@
android:viewportHeight="20">
diff --git a/android/navi-common/src/main/res/xml/default_remote_config.xml b/android/navi-common/src/main/res/xml/default_remote_config.xml
index 0bf8817de8..80e484d360 100644
--- a/android/navi-common/src/main/res/xml/default_remote_config.xml
+++ b/android/navi-common/src/main/res/xml/default_remote_config.xml
@@ -670,4 +670,8 @@
LOW_NETWORK_SIGNAL_THRESHOLD
0
+
+ NAVI_PAY_GENERIC_OFFERS_TTL_IN_MILLIS
+ 1800000
+
\ No newline at end of file
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 dca13595d5..afea5a317e 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
@@ -48,7 +48,6 @@ import com.navi.pay.onboarding.binding.model.network.ServiceProvider
import com.navi.pay.onboarding.faq.model.view.UpiVideoEntity
import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.pay.utils.asString
-import kotlin.text.orEmpty
import org.json.JSONObject
class NaviPayAnalytics private constructor() {
@@ -1358,21 +1357,17 @@ class NaviPayAnalytics private constructor() {
inner class NaviPayQrScanner {
fun onQrScannerLanded(
isPermissionGranted: Boolean,
- rewardNudgeShown: String = false.toString(),
naviPaySessionAttributes: Map,
- isFrequentOrderListEmpty: Boolean,
) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviPay_QrScanner_Landed",
eventValues =
mapOf(
Pair("isPermissionGranted", isPermissionGranted.toString()),
- "reward_nudge" to rewardNudgeShown,
"naviPaySessionId" to
naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
"naviPayCustomerStatus" to
naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
- "isFrequentTransactionListEmpty" to isFrequentOrderListEmpty.toString(),
),
)
}
@@ -1561,31 +1556,6 @@ class NaviPayAnalytics private constructor() {
)
}
- fun onContactSelected(
- source: String,
- inContactList: Boolean,
- isFromFrequentOrderList: Boolean,
- currentSearchQuery: String,
- naviPaySessionAttributes: Map,
- isPermissionGranted: Boolean,
- orderOfOrderItem: Int,
- ) {
- NaviTrackEvent.trackEventOnClickStream(
- "NaviPay_SendMoney_SelectContact_ContactClicked",
- mapOf(
- "source" to source,
- "In_contact_list" to inContactList.toString(),
- "from_frequent" to isFromFrequentOrderList.toString(),
- "search_query" to currentSearchQuery,
- "naviPaySessionId" to naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
- "naviPayCustomerStatus" to
- naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
- "isPermissionGranted" to isPermissionGranted.toString(),
- "orderOfTransactionItem" to orderOfOrderItem.toString(),
- ),
- )
- }
-
fun onUrlTypeQRScanned(qrContent: String) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviPay_QrScanner_UrlTypeQRScanned",
@@ -1611,6 +1581,47 @@ class NaviPayAnalytics private constructor() {
mapOf("message" to message),
)
}
+
+ fun onQRProcessingFailure(isQrCodeProcessing: Boolean) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_QrScanner_ProcessingFailure",
+ eventValues = mapOf("isQrCodeProcessing" to isQrCodeProcessing.toString()),
+ )
+ }
+
+ fun onOffersCalloutLanded(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_QrScanner_OffersCallOut_Landed",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
+
+ fun onOffersCalloutClicked(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_QrScanner_OffersCallOut_Clicked",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
}
inner class NaviPayBankDetailsInput {
@@ -1777,18 +1788,48 @@ class NaviPayAnalytics private constructor() {
),
)
}
+
+ fun onOffersCalloutLanded(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_UpiIdInput_OffersCallOut_Landed",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
+
+ fun onOffersCalloutClicked(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_UpiIdInput_OffersCallOut_Clicked",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
}
inner class NaviPayToContacts {
- fun onSendToContactsLanded(
- rewardNudgeShown: String,
- naviPaySessionAttributes: Map,
- ) {
+ fun onSendToContactsLanded(naviPaySessionAttributes: Map) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviPay_SendMoney_SelectContact_Landed",
eventValues =
mapOf(
- "reward_nudge" to rewardNudgeShown,
"naviPaySessionId" to
naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
"naviPayCustomerStatus" to
@@ -1798,7 +1839,6 @@ class NaviPayAnalytics private constructor() {
}
fun onSendToContactsLoaded(
- rewardNudgeShown: String,
naviPaySessionAttributes: Map,
isPermissionGranted: Boolean,
isContactListEmpty: Boolean,
@@ -1808,7 +1848,6 @@ class NaviPayAnalytics private constructor() {
eventName = "NaviPay_SendMoney_SelectContact_Loaded",
eventValues =
mapOf(
- "reward_nudge" to rewardNudgeShown,
"naviPaySessionId" to
naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
"naviPayCustomerStatus" to
@@ -1925,6 +1964,40 @@ class NaviPayAnalytics private constructor() {
),
)
}
+
+ fun onOffersCalloutLanded(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_SendMoney_SelectContact_OffersCallOut_Landed",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
+
+ fun onOffersCalloutClicked(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_SendMoney_SelectContact_OffersCallOut_Clicked",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
}
inner class NaviPayUpiNumber {
@@ -2733,6 +2806,23 @@ class NaviPayAnalytics private constructor() {
mapOf("transactionId" to transactionId, "action" to action),
)
}
+
+ fun onOffersCalloutLanded(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_PendingCollectRequests_OffersCallOut_Landed",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
}
inner class NaviPayPaymentSummary {
@@ -3680,6 +3770,45 @@ class NaviPayAnalytics private constructor() {
fun onArcNudgeBottomSheetCloseClicked() {
NaviTrackEvent.trackEvent("NaviPay_SendMoney_ProtectNudgeBottomSheetCloseClicked")
}
+
+ fun onOffersCalloutLanded(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ attributeMap: String,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_Send_Money_OffersCallOut_Landed",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ "attributeMap" to attributeMap,
+ ),
+ )
+ }
+
+ fun onOffersCalloutClicked(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ isFromAccountSelectionBottomSheet: Boolean,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_Send_Money_OffersCallOut_Clicked",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ "isFromAccountSelectionBottomSheet" to
+ isFromAccountSelectionBottomSheet.toString(),
+ ),
+ )
+ }
}
inner class NaviPayUPILite {
@@ -4412,17 +4541,12 @@ class NaviPayAnalytics private constructor() {
}
inner class SavedBeneficiaryScreen {
- fun onLanded(
- tab: String,
- rewardNudgeShown: String,
- naviPaySessionAttributes: Map,
- ) {
+ fun onLanded(tab: String, naviPaySessionAttributes: Map) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviPay_SendMoney_UpiOrBank_Landed",
eventValues =
mapOf(
"tab" to tab,
- "reward_nudge" to rewardNudgeShown,
"naviPaySessionId" to
naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
"naviPayCustomerStatus" to
@@ -4503,36 +4627,6 @@ class NaviPayAnalytics private constructor() {
)
}
- fun onSavedBeneficiarySearchScreenBackClicked(
- naviPaySessionAttributes: Map
- ) {
- NaviTrackEvent.trackEventOnClickStream(
- eventName = "NaviPay_SavedBeneficiaryScreen_SearchScreenBackClicked",
- eventValues =
- mapOf(
- "naviPaySessionId" to
- naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
- "naviPayCustomerStatus" to
- naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
- ),
- )
- }
-
- fun onSavedBeneficiaryListViewSearchAreaClicked(
- naviPaySessionAttributes: Map
- ) {
- NaviTrackEvent.trackEventOnClickStream(
- eventName = "NaviPay_SavedBeneficiaryScreen_ListViewSearchAreaClicked",
- eventValues =
- mapOf(
- "naviPaySessionId" to
- naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
- "naviPayCustomerStatus" to
- naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
- ),
- )
- }
-
fun onSavedBeneficiaryValidateVpaInvalid(
savedBeneficiaryEntity: SavedBeneficiaryEntity,
naviPaySessionAttributes: Map,
@@ -4549,6 +4643,40 @@ class NaviPayAnalytics private constructor() {
),
)
}
+
+ fun onOffersCalloutLanded(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_SendMoney_UpiOrBank_OffersCallOut_Landed",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
+
+ fun onOffersCalloutClicked(
+ numberOfOffers: Int,
+ naviPaySessionAttributes: Map,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_SendMoney_UpiOrBank_OffersCallOut_Clicked",
+ eventValues =
+ mapOf(
+ "numberOfOffers" to numberOfOffers.toString(),
+ "naviPaySessionId" to
+ naviPaySessionAttributes["naviPaySessionId"].orEmpty(),
+ "naviPayCustomerStatus" to
+ naviPaySessionAttributes["naviPayCustomerStatus"].orEmpty(),
+ ),
+ )
+ }
}
inner class NaviSettingSDK {
@@ -5176,6 +5304,41 @@ class NaviPayAnalytics private constructor() {
}
}
+ inner class NaviPayOffers {
+ fun onClearGenericOffersInfoInCache() {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_ClearGenericOffersInfoInCache"
+ )
+ }
+
+ fun onGenericOffersInfoNotRefreshed(currentTimeInMillis: Long, lastUpdatedAt: Long) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_GenericOffersInfoNotRefreshed",
+ eventValues =
+ mapOf(
+ "currentTimeInMillis" to currentTimeInMillis.toString(),
+ "lastUpdatedAt" to lastUpdatedAt.toString(),
+ ),
+ )
+ }
+
+ fun onGenericOffersFetchedFromNetwork(
+ genericOffersList: String,
+ isPresentInCache: Boolean,
+ screenName: String,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "NaviPay_GenericOffersFetchedFromNetwork",
+ eventValues =
+ mapOf(
+ "genericOffersList" to genericOffersList,
+ "isPresentInCache" to isPresentInCache.toString(),
+ "screenName" to screenName,
+ ),
+ )
+ }
+ }
+
companion object {
val INSTANCE = NaviPayAnalytics()
const val NAVI_PAY_ACTIVITY = "navipay_activity"
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPayDefaultConfig.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPayDefaultConfig.kt
index ede7bbade6..b1478ca9d5 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPayDefaultConfig.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/config/NaviPayDefaultConfig.kt
@@ -9,6 +9,7 @@ package com.navi.pay.common.model.config
import com.google.gson.annotations.SerializedName
import com.navi.pay.utils.CHECK_BALANCE_AUTO_HIDE_ON_SCREEN_DURATION_IN_MINUTES
+import com.navi.rr.common.models.OfferData
data class NaviPayDefaultConfig(
@SerializedName("config") val config: DefaultConfigContent = DefaultConfigContent(),
@@ -107,6 +108,17 @@ data class DefaultConfigContent(
@SerializedName("checkBalanceAutoHideOnScreenDurationInMinutes")
val checkBalanceAutoHideOnScreenDurationInMinutes: Int =
CHECK_BALANCE_AUTO_HIDE_ON_SCREEN_DURATION_IN_MINUTES,
+ @SerializedName("genericBauOffer")
+ val genericBauOffer: OfferData =
+ OfferData(
+ titlePrefix = "Win up to",
+ titleSuffix = "1,000",
+ descriptionPrefix = "on any bill payment ",
+ descriptionSuffix = "via Navi UPI",
+ iconUrl =
+ "https://public-assets.prod.navi-sa.in/navi-pay/png/upi-profile-icons/self_transfer.png",
+ applicableInfo = listOf("Applicable only if you pay via Navi UPI"),
+ ),
)
data class ConfigMessage(
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayManager.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayManager.kt
index fbc0f6671b..ca23e70347 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayManager.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/setup/NaviPayManager.kt
@@ -27,6 +27,7 @@ import com.navi.pay.common.setup.model.NaviPayCustomerStatus
import com.navi.pay.common.setup.model.NaviPaySetupStatus
import com.navi.pay.common.usecase.LinkedAccountsUseCase
import com.navi.pay.common.usecase.NaviPayConfigUseCase
+import com.navi.pay.common.usecase.RefreshGenericOffersUseCase
import com.navi.pay.common.usecase.RefreshLinkedAccountsUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
import com.navi.pay.common.utils.NaviPayCommonUtils
@@ -77,6 +78,7 @@ constructor(
private val naviPayWidgetManager: Lazy,
private val refreshLinkedAccountsUseCase: Lazy,
private val dataStoreHelper: Lazy,
+ private val refreshGenericOffersUseCase: RefreshGenericOffersUseCase,
@NaviPayGsonBuilder private val gson: Gson,
) {
companion object {
@@ -144,6 +146,10 @@ constructor(
}
sdkInitialized = true
handleUpiShortcutWidgetMigration()
+
+ launch(context = Dispatchers.IO) {
+ refreshGenericOffersUseCase.execute(screenName = "NaviPayManager")
+ }
}
}
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 5ed4a6f410..91cfcbdd79 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
@@ -142,8 +142,7 @@ object NaviPayRouter {
NaviPayScreenType.NAVI_PAY_BLOCKED_USERS_SCREEN.name -> BlockedUsersScreenDestination
NaviPayScreenType.NAVI_PAY_FAQ.name ->
NaviPayFaqScreenDestination(source = sourceScreenName)
- NaviPayScreenType.NAVI_PAY_TO_CONTACTS.name ->
- PayToContactsScreenDestination(openSearchState = false)
+ NaviPayScreenType.NAVI_PAY_TO_CONTACTS.name -> PayToContactsScreenDestination
NaviPayScreenType.NAVI_PAY_SEND_MONEY_SCREEN.name -> {
val payeeEntity =
Uri.parse(bundle.getString(NAVI_PAY_UPI_URI_KEY)).getPayeeEntity(true)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/theme/color/NaviPayColor.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/theme/color/NaviPayColor.kt
index 6e25e072eb..97b27a6f7b 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/theme/color/NaviPayColor.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/theme/color/NaviPayColor.kt
@@ -64,9 +64,9 @@ object NaviPayColor {
val rewardsColorGradient1 = Color(0xFF9618C0)
val rewardsColorGradient2 = Color(0xFF710097)
val rewardsColorGradient3 = Color(0xFF1F002A)
- val rewardsExchangeRatioWidgetBg = Color(0xFFD9D9D9)
val black = Color(0xFF000000)
val gray = Color(0xFF888888)
+ val lightGray = Color(0xFFCCCCCC)
val contactIconBg0 = Color(0xFF7415A7)
val contactIconBg1 = Color(0xFFCC5800)
val contactIconBg2 = Color(0xFF0047D6)
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 ef47825139..64acd8c0db 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
@@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@@ -73,6 +74,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -87,6 +89,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@@ -94,6 +97,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
@@ -137,6 +141,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.naviwidgets.R as WidgetsR
import com.navi.naviwidgets.extensions.NaviText
import com.navi.pay.R
import com.navi.pay.common.model.view.CheckBalanceState
@@ -178,6 +183,11 @@ import com.navi.pay.utils.noRippleClickable
import com.navi.pay.utils.noRippleClickableWithDebounce
import com.navi.pay.utils.shake
import com.navi.pay.utils.shimmerEffect
+import com.navi.rr.R as rrR
+import com.navi.rr.common.models.OfferData
+import com.navi.rr.common.models.OfferResponse
+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 kotlinx.coroutines.Dispatchers
@@ -241,13 +251,13 @@ fun NaviPayHeader(
onActionClick?.invoke()
},
)
- } else if (actionIconText.isNullOrEmpty().not()) {
+ } else if (!actionIconText.isNullOrEmpty()) {
NaviText(
text = actionIconText!!,
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
- color = titleColor,
+ color = NaviPayColor.textPrimary,
textAlign = TextAlign.Center,
modifier =
Modifier.padding(end = 16.dp).clickableDebounce {
@@ -1412,6 +1422,7 @@ fun InputTextFieldWithDescriptionHeader(
headerString: String?,
placeHolderString: String,
value: String,
+ headerStringFontSize: TextUnit = 14.sp,
warningErrorInfoState: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Text,
isTrailingIconEnabled: Boolean = false,
@@ -1449,7 +1460,7 @@ fun InputTextFieldWithDescriptionHeader(
headerString?.let { headerString ->
NaviText(
text = headerString,
- fontSize = 14.sp,
+ fontSize = headerStringFontSize,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
color = NaviPayColor.textPrimary,
@@ -2436,99 +2447,6 @@ fun LottieAnimationWithPlaceHolder(
)
}
-@Composable
-fun RewardsNudgeWithoutBg(
- modifier: Modifier,
- exchangeWidgetBgAlpha: Float = 0.35f,
- textColor: Color = NaviPayColor.textPrimary,
- nudgeDetailEntity: NudgeDetailEntity,
-) {
- Row(
- modifier = modifier.height(IntrinsicSize.Max),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- NaviText(
- text = nudgeDetailEntity.prefixText,
- color = textColor,
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
- )
-
- Spacer(modifier = Modifier.width(2.dp))
-
- Image(
- painter = painterResource(id = CommonR.drawable.ic_np_coin),
- contentDescription = null,
- modifier = Modifier.size(12.dp),
- )
-
- Spacer(modifier = Modifier.width(2.dp))
-
- NaviText(
- text = nudgeDetailEntity.formattedAmount,
- color = textColor,
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
- )
-
- Spacer(modifier = Modifier.width(2.dp))
-
- NaviText(
- text = nudgeDetailEntity.suffixText,
- color = textColor,
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
- )
-
- Spacer(modifier = Modifier.width(4.dp))
-
- if (nudgeDetailEntity.showExchangeRatio) {
- RewardsExchangeRatioWithBg(
- exchangeWidgetBgAlpha = exchangeWidgetBgAlpha,
- textColor = textColor,
- )
- }
- }
-}
-
-@Composable
-fun RewardsExchangeRatioWithBg(
- exchangeWidgetBgAlpha: Float,
- textColor: Color = NaviPayColor.textPrimary,
-) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier =
- Modifier.background(
- color =
- NaviPayColor.rewardsExchangeRatioWidgetBg.copy(
- alpha = exchangeWidgetBgAlpha
- ),
- shape = RoundedCornerShape(20.dp),
- )
- .padding(start = 4.dp, end = 8.dp, top = 1.dp, bottom = 1.dp),
- ) {
- Image(
- painter = painterResource(id = CommonR.drawable.ic_np_coin),
- contentDescription = null,
- modifier = Modifier.size(12.dp),
- )
-
- Spacer(modifier = Modifier.width(1.dp))
-
- NaviText(
- text = NAVI_PAY_REWARDS_EXCHANGE_RATIO,
- color = textColor,
- fontSize = 10.sp,
- fontFamily = naviFontFamily,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
- )
- }
-}
-
@Composable
fun ImageTitleDescriptionShimmerView() {
Row(
@@ -2783,19 +2701,17 @@ fun TitleDescriptionWithLinearProgressBar(
}
@Composable
-fun ShadowStrip(modifier: Modifier = Modifier) {
- Box(
- modifier =
- modifier
- .height(16.dp)
- .background(
- Brush.verticalGradient(
- colors = listOf(NaviPayColor.ctaWhite, Color.LightGray.copy(alpha = 0.3f)),
- startY = 0f,
- endY = 100f,
- )
- )
- )
+fun ShadowStrip(
+ modifier: Modifier = Modifier,
+ height: Dp = 16.dp,
+ brush: Brush =
+ Brush.verticalGradient(
+ colors = listOf(NaviPayColor.ctaWhite, NaviPayColor.lightGray.copy(alpha = 0.3f)),
+ startY = 0f,
+ endY = 100f,
+ ),
+) {
+ Box(modifier = modifier.height(height = height).background(brush = brush))
}
@Composable
@@ -3677,3 +3593,130 @@ fun CameraPermissionView(
)
}
}
+
+@Composable
+fun NaviPayOffersBottomSheet(
+ offerData: List,
+ heading: String,
+ params: Map? = null,
+ closeSheet: () -> Unit,
+) {
+ Column(
+ modifier =
+ Modifier.heightIn(
+ min = 0.dp,
+ max = LocalConfiguration.current.screenHeightDp.dp * 0.85f,
+ )
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.Start,
+ ) {
+ OfferBottomSheetWidget(offerData = offerData, offerListHeading = heading, params = params) {
+ closeSheet()
+ }
+ }
+}
+
+@Composable
+fun OfferStripView(
+ modifier: Modifier = Modifier,
+ bestOfferData: OfferData,
+ showLightVariant: Boolean = false,
+ totalOffers: Int,
+ showDropDown: Boolean = true,
+ showOfferAppliedStripText: Boolean = true,
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.Center,
+ verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+ onOffersStripClicked: (() -> Unit)? = null,
+ seperatorHeight: Dp = 12.dp,
+) {
+ val titlePrefix =
+ remember(showOfferAppliedStripText) {
+ if (showOfferAppliedStripText)
+ bestOfferData.offerAppliedStrip?.prefixText?.trim().orEmpty()
+ else bestOfferData.titlePrefix?.trim().orEmpty()
+ }
+
+ val suffixText =
+ remember(showOfferAppliedStripText) {
+ if (showOfferAppliedStripText)
+ bestOfferData.offerAppliedStrip?.suffixText?.trim().orEmpty()
+ else bestOfferData.titleSuffix?.trim().orEmpty()
+ }
+
+ Row(
+ modifier =
+ modifier.conditional(onOffersStripClicked != null) {
+ noRippleClickableWithDebounce { onOffersStripClicked?.invoke() }
+ },
+ verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ ) {
+ Image(
+ painter = painterResource(id = WidgetsR.drawable.ic_offer_purple),
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ NaviText(
+ text = titlePrefix,
+ color = if (showLightVariant) NaviPayColor.textWhite else NaviPayColor.textSecondary,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.widthIn(max = 200.dp),
+ lineHeight = 16.sp,
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ Image(
+ painter = painterResource(id = WidgetsR.drawable.navi_coin_12),
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ NaviText(
+ text = suffixText,
+ color = if (showLightVariant) NaviPayColor.textWhite else NaviPayColor.textSecondary,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
+ lineHeight = 16.sp,
+ )
+ if (showDropDown) {
+ Spacer(modifier = Modifier.width(4.dp))
+ VerticalDivider(
+ modifier = Modifier.height(seperatorHeight).align(Alignment.CenterVertically),
+ color =
+ if (showLightVariant) NaviPayColor.textWhite
+ else NaviPayColor.inputFieldDefault,
+ thickness = 1.dp,
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ NaviText(
+ text =
+ if (totalOffers == 1) DETAILS
+ else stringResource(rrR.string.n_offers, totalOffers),
+ color = if (showLightVariant) NaviPayColor.textWhite else ctaPrimary,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
+ maxLines = 1,
+ lineHeight = 16.sp,
+ )
+ Image(
+ painter =
+ painterResource(
+ id =
+ if (showLightVariant) CommonR.drawable.ic_chevron_right
+ else WidgetsR.drawable.ic_arrow_black_down
+ ),
+ contentDescription = null,
+ modifier =
+ Modifier.align(Alignment.CenterVertically).conditional(showLightVariant) {
+ rotate(90f)
+ },
+ )
+ }
+ }
+}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/RefreshGenericOffersUseCase.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/RefreshGenericOffersUseCase.kt
new file mode 100644
index 0000000000..428d0a09f5
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/usecase/RefreshGenericOffersUseCase.kt
@@ -0,0 +1,98 @@
+/*
+ *
+ * * Copyright © 2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.common.usecase
+
+import com.google.gson.Gson
+import com.navi.base.cache.model.NaviCacheEntity
+import com.navi.base.cache.repository.NaviCacheRepository
+import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
+import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_GENERIC_OFFERS_TTL_IN_MILLIS
+import com.navi.pay.analytics.NaviPayAnalytics
+import com.navi.pay.management.common.sendmoney.util.NaviPayOffersHelper
+import com.navi.pay.network.di.NaviPayGsonBuilder
+import com.navi.pay.utils.NAVI_PAY_GENERIC_OFFERS_INFO
+import javax.inject.Inject
+
+class RefreshGenericOffersUseCase
+@Inject
+constructor(
+ private val naviCacheRepository: NaviCacheRepository,
+ private val naviPayOffersHelper: NaviPayOffersHelper,
+ @NaviPayGsonBuilder private val gson: Gson,
+) {
+ private lateinit var screenName: String
+ private val naviPayAnalytics = NaviPayAnalytics.INSTANCE.NaviPayOffers()
+
+ suspend fun execute(screenName: String, shouldHardRefresh: Boolean = false) {
+ this.screenName = screenName
+
+ if (shouldHardRefresh) {
+ clearGenericOffersInfoInCache()
+ }
+
+ val currentValueInDb = naviCacheRepository.get(NAVI_PAY_GENERIC_OFFERS_INFO)
+
+ if (currentValueInDb == null) {
+ fetchOffersFromNetworkAndSaveToCache(isPresentInCache = false)
+ return
+ }
+
+ // TTL is yet not breached to refresh the offers
+ if (
+ System.currentTimeMillis() - currentValueInDb.updatedAt <
+ FirebaseRemoteConfigHelper.getLong(
+ key = NAVI_PAY_GENERIC_OFFERS_TTL_IN_MILLIS,
+ defaultValue = 1800000L,
+ )
+ ) {
+ naviPayAnalytics.onGenericOffersInfoNotRefreshed(
+ currentTimeInMillis = System.currentTimeMillis(),
+ lastUpdatedAt = currentValueInDb.updatedAt,
+ )
+ return
+ }
+ fetchOffersFromNetworkAndSaveToCache(isPresentInCache = true)
+ }
+
+ private suspend fun fetchOffersFromNetworkAndSaveToCache(isPresentInCache: Boolean) {
+ val genericOffersList = naviPayOffersHelper.getOffersFromNetwork(screenName = screenName)
+
+ // Todo: remove dev events after some time / subsequent release
+ naviPayAnalytics.onGenericOffersFetchedFromNetwork(
+ genericOffersList = genericOffersList.toString(),
+ isPresentInCache = isPresentInCache,
+ screenName = screenName,
+ )
+
+ if (genericOffersList.isEmpty()) {
+ // Clear cache if no offers are available, in order to sync offers on next land
+ clearGenericOffersInfoInCache()
+ return
+ }
+
+ // Save offers to cache
+ naviCacheRepository.save(
+ naviCacheEntity =
+ NaviCacheEntity(
+ key = NAVI_PAY_GENERIC_OFFERS_INFO,
+ value = gson.toJson(genericOffersList),
+ version = 1,
+ ttl =
+ FirebaseRemoteConfigHelper.getLong(
+ key = NAVI_PAY_GENERIC_OFFERS_TTL_IN_MILLIS,
+ defaultValue = 1800000L,
+ ),
+ )
+ )
+ }
+
+ private suspend fun clearGenericOffersInfoInCache() {
+ naviPayAnalytics.onClearGenericOffersInfoInCache()
+ naviCacheRepository.clear(key = NAVI_PAY_GENERIC_OFFERS_INFO)
+ }
+}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt
index 95930446f6..7b9b175b80 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt
@@ -13,9 +13,6 @@ import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.material.navigation.ModalBottomSheetLayout
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState
@@ -37,8 +34,6 @@ import com.navi.pay.common.utils.ErrorEventHandler
import com.navi.pay.entry.NaviPayActivity
import com.navi.pay.utils.ANIMATION_SPEC_DURATION_IN_MILLIS
import com.navi.pay.utils.GenericErrorCtaHandler
-import com.navi.pay.utils.PAY_TO_CONTACTS_SCREEN_ROUTE
-import com.navi.pay.utils.QR_SCANNER_SCREEN_ROUTE
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
@@ -117,17 +112,7 @@ fun NaviPayMainScreen(
RootNavGraphDefaultAnimations(
enterTransition = {
// TODO evaluate animation override at destination
- if (shouldEnableVerticalUpTransition()) {
- fadeIn() +
- slideInVertically(
- initialOffsetY = { it },
- animationSpec =
- tween(
- durationMillis =
- ANIMATION_SPEC_DURATION_IN_MILLIS
- ),
- )
- } else if (
+ if (
restrictEnterExitTransitionForLinkedAccountBalanceScreen() ||
restrictEnterExitTransitionForPaymentSummaryScreen()
) {
@@ -149,15 +134,7 @@ fun NaviPayMainScreen(
}
},
exitTransition = {
- if (shouldEnableVerticalUpTransition()) {
- fadeOut(
- animationSpec =
- tween(
- durationMillis =
- ANIMATION_SPEC_DURATION_IN_MILLIS
- )
- )
- } else if (
+ if (
restrictEnterExitTransitionForLinkedAccountBalanceScreen() ||
restrictEnterExitTransitionForPaymentSummaryScreen()
) {
@@ -177,15 +154,7 @@ fun NaviPayMainScreen(
}
},
popEnterTransition = {
- if (shouldEnableVerticalDownTransition()) {
- fadeIn(
- animationSpec =
- tween(
- durationMillis =
- ANIMATION_SPEC_DURATION_IN_MILLIS
- )
- )
- } else if (fadeInFadeOutTransition()) {
+ if (fadeInFadeOutTransition()) {
fadeIn(animationSpec = tween(durationMillis = 0))
} else {
slideIntoContainer(
@@ -202,35 +171,15 @@ fun NaviPayMainScreen(
}
},
popExitTransition = {
- if (shouldEnableVerticalDownTransition()) {
- fadeOut() +
- slideOutVertically(
- targetOffsetY = { it },
- animationSpec =
- tween(
- durationMillis =
- ANIMATION_SPEC_DURATION_IN_MILLIS
- ),
- )
- } else {
- slideOutOfContainer(
- towards =
- if (shouldEnableVerticalDownTransition()) {
- AnimatedContentTransitionScope.SlideDirection
- .Companion
- .Down
- } else {
- AnimatedContentTransitionScope.SlideDirection
- .Companion
- .Right
- },
- animationSpec =
- tween(
- durationMillis =
- ANIMATION_SPEC_DURATION_IN_MILLIS
- ),
- )
- }
+ slideOutOfContainer(
+ towards =
+ AnimatedContentTransitionScope.SlideDirection.Companion
+ .Right,
+ animationSpec =
+ tween(
+ durationMillis = ANIMATION_SPEC_DURATION_IN_MILLIS
+ ),
+ )
},
)
),
@@ -273,18 +222,6 @@ private fun AnimatedContentTransitionScope
this.targetState.destination.route?.contains(PAYMENT_SUMMARY_SCREEN).orFalse() &&
this.initialState.destination.route?.contains(SEND_MONEY_SCREEN).orFalse()
-private fun AnimatedContentTransitionScope.shouldEnableVerticalUpTransition():
- Boolean {
- return this.initialState.destination.route?.contains(QR_SCANNER_SCREEN_ROUTE).orFalse() &&
- this.targetState.destination.route?.contains(PAY_TO_CONTACTS_SCREEN_ROUTE).orFalse()
-}
-
-private fun AnimatedContentTransitionScope.shouldEnableVerticalDownTransition():
- Boolean {
- return this.targetState.destination.route?.contains(QR_SCANNER_SCREEN_ROUTE).orFalse() &&
- this.initialState.destination.route?.contains(PAY_TO_CONTACTS_SCREEN_ROUTE).orFalse()
-}
-
private fun AnimatedContentTransitionScope.fadeInFadeOutTransition(): Boolean {
return this.targetState.destination.route?.contains(LINK_UPI_NUMBER_SCREEN).orFalse()
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/model/view/CollectRequestModels.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/model/view/CollectRequestModels.kt
index e110335164..98a1d745a2 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/model/view/CollectRequestModels.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/model/view/CollectRequestModels.kt
@@ -9,6 +9,7 @@ package com.navi.pay.management.collectrequest.model.view
import com.navi.pay.management.collectrequest.getExpiryTimestampFormatted
import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity
+import com.navi.rr.common.models.OfferData
import org.joda.time.DateTime
/**
@@ -26,8 +27,8 @@ data class CollectRequestEntity(
val isMarkedSpam: Boolean,
val payerVpa: String,
val collectType: String,
+ val bestOfferData: OfferData? = null,
) {
-
val expiryTimestampFormatted by lazy {
getExpiryTimestampFormatted(
expiryTimestamp = this.expiryTimestamp,
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestItem.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestItem.kt
index 15f8bd0cdc..138fe6dd16 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestItem.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestItem.kt
@@ -7,9 +7,16 @@
package com.navi.pay.management.collectrequest.ui
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
+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.runtime.Composable
import androidx.compose.ui.Modifier
@@ -27,6 +34,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.NaviPayCard
+import com.navi.pay.common.ui.OfferStripView
import com.navi.pay.common.ui.TextImage
import com.navi.pay.common.ui.ThemedRoundedButton
import com.navi.pay.common.utils.NaviPayCommonUtils.getIndexedItemBgColor
@@ -35,6 +43,7 @@ import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE
import com.navi.pay.utils.clickableDebounce
import com.navi.pay.utils.contactInitials
import com.navi.pay.utils.getDisplayableAmount
+import com.navi.rr.common.models.OfferData
@Composable
fun CollectRequestItem(
@@ -43,6 +52,7 @@ fun CollectRequestItem(
onProceedBtnClick: (CollectRequestEntity) -> Unit,
showLoader: Boolean,
clickedCollectRequestItemId: String,
+ bestOfferData: OfferData?,
) {
NaviPayCard(
modifier =
@@ -52,7 +62,8 @@ fun CollectRequestItem(
borderStroke = BorderStroke(width = 1.dp, color = NaviPayColor.borderAlt),
) {
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
- val (userImage, userName, userVpa, amount, timestamp, btnProceed) = createRefs()
+ val (userImage, userName, userVpa, amount, timestamp, btnProceed, offerStrip) =
+ createRefs()
TextImage(
modifier =
Modifier.constrainAs(userImage) {
@@ -149,7 +160,10 @@ fun CollectRequestItem(
Modifier.constrainAs(btnProceed) {
top.linkTo(timestamp.bottom, 12.dp)
start.linkTo(userImage.end, 12.dp)
- bottom.linkTo(parent.bottom, 16.dp)
+ bottom.linkTo(
+ if (bestOfferData != null) offerStrip.top else parent.bottom,
+ 16.dp,
+ )
}
.layoutId(btnProceed),
text = stringResource(id = R.string.np_view_details),
@@ -162,6 +176,28 @@ fun CollectRequestItem(
lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE,
lottieFileHeight = 32.dp,
)
+
+ AnimatedVisibility(
+ visible = bestOfferData != null,
+ enter = expandVertically(),
+ exit = shrinkVertically(),
+ modifier =
+ Modifier.constrainAs(offerStrip) {
+ start.linkTo(parent.start)
+ bottom.linkTo(parent.bottom)
+ width = Dimension.fillToConstraints
+ }
+ .layoutId(offerStrip)
+ .background(NaviPayColor.bgNonEditable)
+ .padding(6.dp),
+ ) {
+ OfferStripView(
+ modifier = Modifier.fillMaxWidth(),
+ bestOfferData = bestOfferData!!,
+ totalOffers = 1,
+ showDropDown = false,
+ )
+ }
}
}
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestsScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestsScreen.kt
index 6abafcdc14..0732800964 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestsScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/ui/CollectRequestsScreen.kt
@@ -10,14 +10,14 @@ package com.navi.pay.management.collectrequest.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -168,31 +168,29 @@ fun CollectRequestsMainScreen(
if (collectRequests.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
- LazyColumn(
- modifier = Modifier.padding(it).padding(horizontal = 16.dp),
- contentPadding = PaddingValues(vertical = 16.dp),
+ Column(
+ modifier =
+ Modifier.padding(it)
+ .padding(all = 16.dp)
+ .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
- item {
- NaviText(
- text = stringResource(id = R.string.payment_request),
- textAlign = TextAlign.Start,
- fontSize = 24.sp,
- fontFamily = naviFontFamily,
- color = NaviPayColor.textPrimary,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
- )
- }
- itemsIndexed(
- items = collectRequests,
- key = { index, _ -> collectRequests[index].transactionId },
- ) { index, collectRequest ->
+ NaviText(
+ text = stringResource(id = R.string.payment_request),
+ textAlign = TextAlign.Start,
+ fontSize = 24.sp,
+ fontFamily = naviFontFamily,
+ color = NaviPayColor.textPrimary,
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
+ )
+ collectRequests.forEachIndexed { index, collectRequest ->
CollectRequestItem(
index = index,
collectRequestEntity = collectRequest,
onProceedBtnClick = onProceedRequestClick,
showLoader = showLoader,
clickedCollectRequestItemId = clickedCollectRequestItemId,
+ bestOfferData = collectRequest.bestOfferData,
)
}
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/viewmodel/CollectRequestViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/viewmodel/CollectRequestViewModel.kt
index 9d43f43caa..d4a2b284f1 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/viewmodel/CollectRequestViewModel.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/collectrequest/viewmodel/CollectRequestViewModel.kt
@@ -10,6 +10,7 @@ package com.navi.pay.management.collectrequest.viewmodel
import androidx.lifecycle.viewModelScope
import com.navi.base.utils.EMPTY
import com.navi.base.utils.ResourceProvider
+import com.navi.base.utils.orZero
import com.navi.common.network.models.isSuccessWithData
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
@@ -18,6 +19,7 @@ import com.navi.pay.common.model.view.NaviPaySessionHelper
import com.navi.pay.common.model.view.SnackBarState
import com.navi.pay.common.repository.CommonRepository
import com.navi.pay.common.repository.SharedPreferenceRepository
+import com.navi.pay.common.usecase.LinkedAccountsUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
import com.navi.pay.common.utils.getMetricInfo
import com.navi.pay.common.viewmodel.NaviPayBaseVM
@@ -31,11 +33,21 @@ import com.navi.pay.management.common.sendmoney.model.view.SendMoneyAction
import com.navi.pay.management.common.sendmoney.model.view.SendMoneyScreenSource
import com.navi.pay.management.common.sendmoney.model.view.SendMoneyUserAction
import com.navi.pay.management.common.sendmoney.model.view.UpiTransactionType
+import com.navi.pay.management.common.sendmoney.util.NaviPayOffersHelper
import com.navi.pay.management.mandate.model.network.MandateDetailRequest
+import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity
+import com.navi.pay.utils.DEFAULT_UPI_PURPOSE
import com.navi.pay.utils.NAVI_PAY_DEFAULT_MCC
+import com.navi.pay.utils.PAYEE_VPA
+import com.navi.pay.utils.PAYER_VPA
import com.navi.pay.utils.PENDING_REQUEST_REFRESHED_REQUIRED
import com.navi.pay.utils.REQUEST_TYPE_MANDATE
+import com.navi.pay.utils.TXN_AMOUNT
+import com.navi.pay.utils.UPI_PAYEE_MCC
+import com.navi.pay.utils.UPI_PAYER_BANK_CODE
+import com.navi.pay.utils.UPI_PURPOSE_CODE
import com.navi.pay.utils.refresh
+import com.navi.rr.common.models.OfferRequest
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -46,6 +58,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -60,6 +73,8 @@ constructor(
private val naviPaySessionHelper: NaviPaySessionHelper,
private val naviPayActivityDataProvider: NaviPayActivityDataProvider,
private val resourceProvider: ResourceProvider,
+ private val naviPayOffersHelper: NaviPayOffersHelper,
+ private val linkedAccountsUseCase: LinkedAccountsUseCase,
) : NaviPayBaseVM() {
private val naviPayAnalytics: NaviPayAnalytics.NaviPayCollectRequests =
@@ -188,6 +203,7 @@ constructor(
updateCollectRequestUIState(
uiState = CollectRequestUIState.Loaded(collectRequests = collectRequests)
)
+ fetchBestOfferForCollectRequests(collectRequests = collectRequests)
} else {
updateCollectRequestUIState(uiState = CollectRequestUIState.Loaded(emptyList()))
notifyError(
@@ -198,6 +214,81 @@ constructor(
}
}
+ private suspend fun fetchBestOfferForCollectRequests(
+ collectRequests: List
+ ) {
+ // Fetch linked accounts once for reuse
+ val linkedAccounts = linkedAccountsUseCase.execute().firstOrNull().orEmpty()
+
+ val offerRequestList =
+ collectRequests.map { collectRequestEntity ->
+ OfferRequest(
+ requestId = collectRequestEntity.transactionId,
+ attributes =
+ buildMap {
+ put(
+ TXN_AMOUNT,
+ collectRequestEntity.payeeEntity.amount.takeIf {
+ it.isNotEmpty()
+ },
+ )
+ put(
+ PAYER_VPA,
+ collectRequestEntity.payerVpa.takeIf { it.isNotEmpty() },
+ )
+ put(
+ UPI_PAYEE_MCC,
+ collectRequestEntity.payeeEntity.mcc ?: NAVI_PAY_DEFAULT_MCC,
+ )
+ put(UPI_PURPOSE_CODE, DEFAULT_UPI_PURPOSE)
+ put(
+ PAYEE_VPA,
+ collectRequestEntity.payeeEntity.vpa.takeIf { it.isNotEmpty() },
+ )
+ put(
+ UPI_PAYER_BANK_CODE,
+ getPayerBankCodeFromVpa(collectRequestEntity, linkedAccounts)
+ .takeIf { it.isNotEmpty() },
+ )
+ }
+ .filterValues { it != null } as Map,
+ )
+ }
+
+ val bestOfferMap =
+ naviPayOffersHelper.getBestOfferListForMultipleItems(
+ screenName = screenName,
+ offerRequestList = offerRequestList,
+ )
+
+ naviPayAnalytics.onOffersCalloutLanded(
+ numberOfOffers = bestOfferMap?.size.orZero(),
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+
+ // In order to trigger re-composition, change the list reference
+ val updatedCollectRequests =
+ collectRequests.map { collectRequestEntity ->
+ collectRequestEntity.copy(
+ bestOfferData = bestOfferMap?.get(collectRequestEntity.transactionId)
+ )
+ }
+
+ updateCollectRequestUIState(
+ uiState = CollectRequestUIState.Loaded(collectRequests = updatedCollectRequests)
+ )
+ }
+
+ private fun getPayerBankCodeFromVpa(
+ collectRequestEntity: CollectRequestEntity,
+ linkedAccounts: List,
+ ): String {
+ return linkedAccounts
+ .firstOrNull { it.vpa == collectRequestEntity.payerVpa }
+ ?.bankCode
+ .orEmpty()
+ }
+
private fun updatePendingRefreshFlag() {
viewModelScope.launch(Dispatchers.IO) {
sharedPreferenceRepository.saveBooleanValue(
@@ -211,6 +302,9 @@ constructor(
_collectRequestUIState.update { uiState }
}
+ fun getNaviPaySessionAttributes(): Map =
+ naviPaySessionHelper.getNaviPaySessionAttributes()
+
fun onProceedButtonClicked(
collectRequestEntity: CollectRequestEntity,
sendMoneyAction: SendMoneyAction?,
@@ -236,7 +330,7 @@ constructor(
naviPaySessionHelper.createNewSessionId()
naviPayAnalytics.onPendingCollectRequestItemClick(
- naviPaySessionAttributes = naviPaySessionHelper.getNaviPaySessionAttributes()
+ naviPaySessionAttributes = getNaviPaySessionAttributes()
)
if (collectRequestEntity.collectType == REQUEST_TYPE_MANDATE) {
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 817b63c105..b28eb6d2c9 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
@@ -18,7 +18,6 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -84,8 +83,10 @@ import com.navi.pay.management.common.paymentsummary.model.view.PaymentSummaryRe
import com.navi.pay.management.common.ui.TransactionInfoCard
import com.navi.pay.management.transactionhistory.model.view.TransactionDetailItemProperty
import com.navi.pay.management.transactionhistory.model.view.TransactionEntity
+import com.navi.pay.utils.COMMA
import com.navi.pay.utils.SMALL_FLOAT
import com.navi.pay.utils.clickableDebounce
+import com.navi.pay.utils.getDisplayableAmount
import com.navi.pay.utils.getImageRequestBuilder
import com.navi.pay.utils.noRippleClickableWithDebounce
import com.navi.rr.scratchcard.model.ScratchCardBackResponse
@@ -167,10 +168,6 @@ fun SharedTransitionScope.PaymentSummaryTransactionDetailSection(
ctaText = stringResource(id = R.string.np_view_details),
onCtaClicked = onViewDetailCtaClicked,
)
- RewardsWonGratificationSection(
- rewardsGratificationUIState = rewardsGratificationUIState,
- onRewardsWonGratificationClicked = onRewardsWonGratificationClicked,
- )
Spacer(
modifier =
Modifier.height(
@@ -230,6 +227,8 @@ fun SharedTransitionScope.PaymentSummaryTransactionDetailSection(
isFestiveThemeExperimentEnabled = isFestiveThemeExperimentEnabled,
festiveCelebrationWithCoinsImageUrl = festiveCelebrationWithCoinsImageUrl,
festiveCelebrationWithStripeImageUrl = festiveCelebrationWithStripeImageUrl,
+ onRewardsWonGratificationClicked = onRewardsWonGratificationClicked,
+ rewardsGratificationUIState = rewardsGratificationUIState,
)
},
)
@@ -255,6 +254,8 @@ private fun SharedTransitionScope.BottomBarSection(
isFestiveThemeExperimentEnabled: Boolean,
festiveCelebrationWithCoinsImageUrl: String?,
festiveCelebrationWithStripeImageUrl: String?,
+ onRewardsWonGratificationClicked: () -> Unit,
+ rewardsGratificationUIState: PaymentSummaryRewardsGratificationUIState,
) {
Column(modifier = Modifier.padding(top = 16.dp)) {
if (isRewardsCtaVisible) {
@@ -274,9 +275,16 @@ private fun SharedTransitionScope.BottomBarSection(
festiveCelebrationWithStripeImageUrl = festiveCelebrationWithStripeImageUrl,
)
}
+ RewardsWonGratificationSection(
+ rewardsGratificationUIState = rewardsGratificationUIState,
+ onRewardsWonGratificationClicked = onRewardsWonGratificationClicked,
+ )
Spacer(modifier = Modifier.height(16.dp))
Row(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .background(color = NaviPayColor.bgDefault)
+ .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ThemeRoundedButtonWithImage(
@@ -422,7 +430,9 @@ private fun SharedTransitionScope.RewardsBottomBarWithStripSection(
Image(
painter =
- painterResource(id = CommonR.drawable.navi_common_ic_double_right_arrow),
+ painterResource(
+ id = CommonR.drawable.navi_common_ic_double_right_arrow_white
+ ),
contentDescription = "",
modifier = Modifier.size(20.dp),
)
@@ -513,56 +523,55 @@ private fun RewardsWonGratificationSection(
Row(
modifier =
Modifier.fillMaxWidth()
- .padding(horizontal = 16.dp)
- .border(
- width = 1.dp,
- color = NaviPayColor.borderDefault,
- shape = RoundedCornerShape(4.dp),
+ .background(
+ color = NaviPayColor.bgNonEditable,
+ shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
)
.clickableDebounce { onRewardsWonGratificationClicked() }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
) {
- Image(
- painter = painterResource(id = CommonR.drawable.ic_np_coin),
- contentDescription = "",
- modifier = Modifier.size(24.dp),
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
NaviText(
text = stringResource(id = R.string.np_rewards_won_prefix),
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
fontSize = 14.sp,
color = NaviPayColor.ctaPrimary,
+ lineHeight = 22.sp,
)
+ Image(
+ painter = painterResource(id = CommonR.drawable.ic_np_coin),
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ )
+
+ Spacer(modifier = Modifier.width(2.dp))
+
NaviText(
- text =
- stringResource(
- id =
- if (
- (rewardsGratificationUIState.amount.toDoubleOrNull() ?: 0.0) ==
- 1.00
- )
- R.string.np_rewards_won_suffix_singular
- else R.string.np_rewards_won_suffix,
- rewardsGratificationUIState.amount,
- ),
+ text = "${rewardsGratificationUIState.amount.getDisplayableAmount()}$COMMA ",
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
fontSize = 14.sp,
color = NaviPayColor.ctaPrimary,
+ lineHeight = 22.sp,
)
- Spacer(modifier = Modifier.weight(1f))
+ NaviText(
+ text = stringResource(id = R.string.np_check_now_small),
+ fontFamily = naviFontFamily,
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
+ fontSize = 14.sp,
+ color = NaviPayColor.ctaPrimary,
+ lineHeight = 22.sp,
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = painterResource(id = CommonR.drawable.ic_double_right_arrow_v2),
contentDescription = "",
- modifier = Modifier.size(20.dp),
)
}
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt
index 4b316b5edd..ff8b7b3682 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt
@@ -27,6 +27,7 @@ import com.navi.pay.common.model.view.NaviPaySessionHelper
import com.navi.pay.common.repository.CommonRepository
import com.navi.pay.common.repository.SharedPreferenceRepository
import com.navi.pay.common.usecase.NaviPayConfigUseCase
+import com.navi.pay.common.usecase.RefreshGenericOffersUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
import com.navi.pay.common.utils.NaviPayCommonUtils
import com.navi.pay.common.utils.getMetricInfo
@@ -94,6 +95,7 @@ constructor(
private val transactionRepository: TransactionRepository,
private val orderRepository: OrderRepository,
val festiveThemeHelper: FestiveThemeHelper,
+ private val refreshGenericOffersUseCase: RefreshGenericOffersUseCase,
) : NaviPayBaseVM() {
val naviPayAnalytics: NaviPayAnalytics.NaviPayPaymentSummary =
@@ -311,6 +313,7 @@ constructor(
launch { checkForNpsCommsEligibility() }
launch { onTransactionCompletionTimeReceived() }
launch { evaluateButlerPrediction() }
+ launch { updateGenericOffersInfo() }
}
}
@@ -820,6 +823,11 @@ constructor(
)
}
+ private suspend fun updateGenericOffersInfo() {
+ delay(2.seconds) // delay for segments to update at backend
+ refreshGenericOffersUseCase.execute(screenName = screenName, shouldHardRefresh = true)
+ }
+
override val screenName: String
get() = NAVI_PAY_PAYMENT_STATUS
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/TransactionInitiationType.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/TransactionInitiationType.kt
index e680d3ffd7..b2b2c9a98c 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/TransactionInitiationType.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/network/TransactionInitiationType.kt
@@ -28,10 +28,6 @@ enum class TransactionInitiationType {
getTransactionInitiationType(
sendMoneyScreenSource.transactionInitiationMode.name
)
- is SendMoneyScreenSource.QrScan ->
- getTransactionInitiationType(
- sendMoneyScreenSource.transactionInitiationMode.name
- )
is SendMoneyScreenSource.TransactionHistoryDetail ->
getTransactionInitiationType(sendMoneyScreenSource.transactionInitiationType)
is SendMoneyScreenSource.BankDetailInput -> PAY_TO_BANK_ACCOUNT
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/view/SendMoneyModels.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/view/SendMoneyModels.kt
index 77a90dcbc4..d694348147 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/view/SendMoneyModels.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/model/view/SendMoneyModels.kt
@@ -237,13 +237,6 @@ sealed class SendMoneyScreenSource : Parcelable {
val transactionInitiationMode: TransactionInitiationType,
) : SendMoneyScreenSource()
- @Parcelize
- @Keep
- data class QrScan(
- val payeeVpaToDisplay: String,
- val transactionInitiationMode: TransactionInitiationType,
- ) : SendMoneyScreenSource()
-
@Parcelize
@Keep
data class TransactionHistoryDetail(
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyAccountSelectionView.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyAccountSelectionView.kt
index 3339fda7b4..0f77952418 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyAccountSelectionView.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyAccountSelectionView.kt
@@ -23,7 +23,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
+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.platform.LocalConfiguration
@@ -33,6 +35,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.navi.base.utils.isNull
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
@@ -65,15 +68,29 @@ fun SendMoneyAccountSelectionView(
val isAddAccountEnabled by sendMoneyViewModel.isAddAccountEnabled.collectAsStateWithLifecycle()
val paymentAmount by sendMoneyViewModel.paymentAmount.collectAsStateWithLifecycle()
val showPayButtonLoader by sendMoneyViewModel.showPayButtonLoader.collectAsStateWithLifecycle()
+ val sortedOffersList by sendMoneyViewModel.sortedOffersList.collectAsStateWithLifecycle()
+ val shouldShowBottomUpOfferStripTransition by
+ sendMoneyViewModel.shouldShowBottomUpOfferStripTransition.collectAsStateWithLifecycle()
+
+ val bestOfferStrip by
+ remember(sortedOffersList) {
+ derivedStateOf {
+ sortedOffersList.firstOrNull { evaluatedOffer ->
+ evaluatedOffer.disabledData.isNull()
+ }
+ }
+ }
val sheetHeight = LocalConfiguration.current.screenHeightDp.dp * 0.85f
- Column(
- modifier = Modifier.fillMaxWidth().heightIn(max = sheetHeight).navigationBarsPadding(),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
+ Column(modifier = Modifier.fillMaxWidth().heightIn(max = sheetHeight).navigationBarsPadding()) {
Row(
- Modifier.padding(16.dp),
+ Modifier.padding(
+ start = 16.dp,
+ top = 16.dp,
+ end = 16.dp,
+ bottom = if (bestOfferStrip.isNull()) 16.dp else 4.dp,
+ ),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
@@ -102,6 +119,23 @@ fun SendMoneyAccountSelectionView(
)
}
+ bestOfferStrip?.let { offer ->
+ RenderOffersStrip(
+ modifier = Modifier.fillMaxWidth(),
+ bestOfferInfo = offer,
+ totalOffers = sortedOffersList.size,
+ shouldShowBottomUpOfferStripTransition = shouldShowBottomUpOfferStripTransition,
+ onOffersStripClicked = {
+ sendMoneyViewModel.onOffersStripClicked(
+ isFromAccountSelectionBottomSheet = true
+ )
+ },
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+
HorizontalDivider(thickness = 4.dp, color = NaviPayColor.bgNonEditable)
Box {
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyBottomSheetContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyBottomSheetContent.kt
index b64727cebc..c983c6e017 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyBottomSheetContent.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyBottomSheetContent.kt
@@ -16,6 +16,7 @@ import com.navi.pay.common.arc.ui.ArcNudgeBottomSheetContent
import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader
import com.navi.pay.common.ui.BottomSheetLoadingScreen
import com.navi.pay.common.ui.ConfirmationBottomSheetContent
+import com.navi.pay.common.ui.NaviPayOffersBottomSheet
import com.navi.pay.common.ui.SuccessBottomSheetContent
import com.navi.pay.management.common.sendmoney.model.view.PayeeSeverity
import com.navi.pay.management.common.sendmoney.util.SendMoneyBottomSheetType
@@ -33,6 +34,7 @@ fun SendMoneyBottomSheetContent(
onBackClick: () -> Unit,
naviPaySessionAttributes: Map,
onArcNudgeClicked: () -> Unit,
+ onOffersBottomSheetDismissClicked: () -> Unit,
) {
when (bottomSheetType) {
is SendMoneyBottomSheetType.Loading ->
@@ -146,5 +148,16 @@ fun SendMoneyBottomSheetContent(
),
)
}
+ is SendMoneyBottomSheetType.OfferList -> {
+ NaviPayOffersBottomSheet(
+ offerData = bottomSheetType.offerData,
+ closeSheet =
+ if (bottomSheetType.isFromAccountSelectionBottomSheet)
+ onOffersBottomSheetDismissClicked
+ else onDismissClicked,
+ heading = bottomSheetType.heading,
+ params = bottomSheetType.params,
+ )
+ }
}
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyMainScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyMainScreen.kt
index b2d3abaebd..e752e04ac3 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyMainScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyMainScreen.kt
@@ -7,6 +7,12 @@
package com.navi.pay.management.common.sendmoney.ui
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -16,7 +22,9 @@ 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
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -29,7 +37,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
@@ -44,6 +54,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.navi.base.utils.EMPTY
+import com.navi.base.utils.isNull
import com.navi.common.R as CommonR
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
@@ -56,6 +68,7 @@ import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.model.view.ShakeConfig
import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.ui.NaviPayHeader
+import com.navi.pay.common.ui.OfferStripView
import com.navi.pay.common.ui.PayeeDetailsView
import com.navi.pay.common.ui.rememberShakeController
import com.navi.pay.common.utils.NaviPayCommonUtils
@@ -71,6 +84,7 @@ import com.navi.pay.utils.AMOUNT_MAX_LENGTH_RCC
import com.navi.pay.utils.conditional
import com.navi.pay.utils.customHide
import com.navi.pay.utils.shake
+import com.navi.rr.common.models.EvaluatedOffer
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -115,6 +129,9 @@ fun MainScreen(
val view = LocalView.current
val amountFieldShakeController = rememberShakeController()
val showPayButtonLoader by sendMoneyViewModel.showPayButtonLoader.collectAsStateWithLifecycle()
+ val sortedOffersList by sendMoneyViewModel.sortedOffersList.collectAsStateWithLifecycle()
+ val shouldShowBottomUpOfferStripTransition by
+ sendMoneyViewModel.shouldShowBottomUpOfferStripTransition.collectAsStateWithLifecycle()
sendMoneyViewModel.payeeEntityFromSource?.note?.let { sourceNote ->
if (sourceNote.isNotBlank()) {
@@ -136,6 +153,15 @@ fun MainScreen(
val scrollState = rememberScrollState()
val showHeadingInTopBar by remember { derivedStateOf { scrollState.value > 50 } }
+ val bestOfferInfo by
+ remember(sortedOffersList) {
+ derivedStateOf {
+ sortedOffersList.firstOrNull { evaluatedOffer ->
+ evaluatedOffer.disabledData.isNull()
+ }
+ }
+ }
+
Scaffold(
modifier = modifier,
topBar = {
@@ -161,12 +187,14 @@ fun MainScreen(
resourceId = R.string.np_send_money_collect_request_header_title,
name = payeeEntity.name,
)
+
transactionType == UpiTransactionType.SELF_PAY ->
getHeaderText(
showHeadingInTopBar = showHeadingInTopBar,
resourceId = R.string.np_self_transfer_to,
name = payeeEntity.bankName.orEmpty(),
)
+
else ->
getHeaderText(
showHeadingInTopBar = showHeadingInTopBar,
@@ -184,6 +212,12 @@ fun MainScreen(
titleColor =
if (showHeadingInTopBar) NaviPayColor.textPrimary
else NaviPayColor.textTertiary,
+ actionIconText = stringResource(R.string.help),
+ onActionClick = {
+ keyboardController?.customHide(context = context, view = view)
+ focusManager.clearFocus()
+ sendMoneyViewModel.onHelpCtaClicked()
+ },
)
}
},
@@ -266,6 +300,25 @@ fun MainScreen(
keyboardController?.customHide(context = context, view = view)
},
)
+
+ if (bestOfferInfo != null) {
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+
+ RenderOffersStrip(
+ modifier = Modifier.fillMaxWidth(),
+ bestOfferInfo = bestOfferInfo,
+ totalOffers = sortedOffersList.size,
+ shouldShowBottomUpOfferStripTransition = shouldShowBottomUpOfferStripTransition,
+ onOffersStripClicked = {
+ keyboardController?.customHide(context = context, view = view)
+ focusManager.clearFocus()
+ sendMoneyViewModel.onOffersStripClicked(
+ isFromAccountSelectionBottomSheet = false
+ )
+ },
+ )
+
Spacer(modifier = Modifier.height(58.dp))
Spacer(modifier = Modifier.weight(1f))
}
@@ -301,6 +354,68 @@ fun MainScreen(
)
}
+@Composable
+fun RenderOffersStrip(
+ modifier: Modifier = Modifier,
+ bestOfferInfo: EvaluatedOffer?,
+ totalOffers: Int,
+ shouldShowBottomUpOfferStripTransition: Boolean,
+ onOffersStripClicked: () -> Unit,
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.Center,
+ verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+) {
+ var isInitialRender by remember { mutableStateOf(true) }
+ AnimatedContent(
+ modifier = modifier,
+ targetState = bestOfferInfo,
+ transitionSpec = {
+ val slideDirection =
+ if (shouldShowBottomUpOfferStripTransition && !isInitialRender)
+ AnimatedContentTransitionScope.SlideDirection.Up
+ else AnimatedContentTransitionScope.SlideDirection.Down
+
+ (slideIntoContainer(
+ towards = slideDirection,
+ animationSpec = tween(durationMillis = 300),
+ ) +
+ fadeIn(animationSpec = tween(durationMillis = 300)).apply {
+ if (slideDirection == AnimatedContentTransitionScope.SlideDirection.Down) {
+ Modifier.offset(y = (-8).dp)
+ }
+ }) togetherWith
+ (slideOutOfContainer(
+ towards = slideDirection,
+ animationSpec = tween(durationMillis = 300),
+ ) +
+ fadeOut(animationSpec = tween(durationMillis = 300)).apply {
+ if (slideDirection == AnimatedContentTransitionScope.SlideDirection.Up) {
+ Modifier.offset(y = 8.dp)
+ }
+ })
+ },
+ label = EMPTY,
+ ) { offerInfo ->
+ if (offerInfo != null) {
+ isInitialRender = false
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth().height(20.dp).padding(horizontal = 16.dp),
+ verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ ) {
+ offerInfo?.let {
+ OfferStripView(
+ horizontalArrangement = horizontalArrangement,
+ verticalAlignment = verticalAlignment,
+ bestOfferData = it.offer,
+ totalOffers = totalOffers,
+ onOffersStripClicked = onOffersStripClicked,
+ )
+ }
+ }
+ }
+}
+
@Composable
fun getHeaderText(showHeadingInTopBar: Boolean, resourceId: Int, name: String): String {
return if (showHeadingInTopBar) {
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyScreen.kt
index a2f815f934..62133e01a0 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/ui/SendMoneyScreen.kt
@@ -46,6 +46,7 @@ import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.model.view.NaviPayButtonAction
import com.navi.pay.common.model.view.NaviPayScreenType
+import com.navi.pay.common.setup.NaviPayRouter
import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.ui.FullScreenLottie
import com.navi.pay.common.ui.FullScreenLottieV2
@@ -353,6 +354,12 @@ fun SendMoneyScreen(
sendMoneyViewModel.triggerDismissBottomSheet.collectLatest { dismissBottomSheet() }
}
+ LaunchedEffect(Unit) {
+ sendMoneyViewModel.navigateToNextScreenFromHelpCta.collect {
+ it?.let { NaviPayRouter.onCtaClick(naviPayActivity = naviPayActivity, ctaData = it) }
+ }
+ }
+
val onPayButtonClick = { payButtonSource: String ->
if (sendMoneyViewModel.isAmountValid()) { // TODO: move to single VM function
focusManager.clearFocus()
@@ -485,6 +492,8 @@ fun SendMoneyScreen(
sendMoneyViewModel.onArcNudgeClicked()
naviPayAnalytics.onArcNudgeClickedFromBottomSheet()
},
+ onOffersBottomSheetDismissClicked =
+ sendMoneyViewModel::onOffersBottomSheetDismissClicked,
)
}
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/util/NaviPayOffersHelper.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/util/NaviPayOffersHelper.kt
new file mode 100644
index 0000000000..68500dc497
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/util/NaviPayOffersHelper.kt
@@ -0,0 +1,230 @@
+/*
+ *
+ * * Copyright © 2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.management.common.sendmoney.util
+
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.navi.base.cache.repository.NaviCacheRepository
+import com.navi.base.utils.isNull
+import com.navi.common.model.ModuleNameV2
+import com.navi.pay.common.model.config.NaviPayDefaultConfig
+import com.navi.pay.common.usecase.NaviPayConfigUseCase
+import com.navi.pay.common.utils.getMetricInfo
+import com.navi.pay.network.di.NaviPayGsonBuilder
+import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity
+import com.navi.pay.utils.BAU
+import com.navi.pay.utils.DEFAULT_CONFIG
+import com.navi.pay.utils.DEFAULT_UPI_PURPOSE
+import com.navi.pay.utils.NAVI_PAY_GENERIC_OFFERS_INFO
+import com.navi.pay.utils.NAVI_PAY_UPI_LITE_SEND_MONEY_PURPOSE_CODE
+import com.navi.pay.utils.isAccountIdOfTypeUpiLite
+import com.navi.rr.common.models.EvaluatedOffer
+import com.navi.rr.common.models.OfferData
+import com.navi.rr.common.models.OfferRequest
+import com.navi.rr.common.models.OfferResponse
+import com.navi.rr.common.models.OfferType
+import com.navi.rr.common.repo.OffersRepo
+import com.navi.rr.utils.constants.Constants.UPI
+import com.navi.rr.utils.mapper.OfferResponseToOfferDataMapper
+import com.navi.rr.utils.sortOffersWithCondition
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.withContext
+
+/**
+ * Helper class for handling offers. It provides functions to fetch generic offers, evaluate the
+ * best offers, and sort offers based on conditions.
+ */
+class NaviPayOffersHelper
+@Inject
+constructor(
+ private val offersRepo: OffersRepo,
+ private val naviCacheRepository: NaviCacheRepository,
+ private val naviPayConfigUseCase: NaviPayConfigUseCase,
+ @NaviPayGsonBuilder private val gson: Gson,
+) {
+ private var currentBestOfferIndex = Int.MAX_VALUE
+
+ suspend fun getOffersFromNetwork(
+ screenName: String,
+ attributeMap: Map? = null,
+ ): List {
+ val offersData =
+ offersRepo
+ .fetchOffersForProduct(
+ product = UPI,
+ attributes = attributeMap,
+ metricInfo = getMetricInfo(screenName = screenName),
+ )
+ .firstOrNull()
+
+ val offerResponseToOfferDataMapper = OfferResponseToOfferDataMapper()
+ val offersDataList =
+ offersData?.data?.map {
+ offerResponseToOfferDataMapper.map(
+ offerResponse = it,
+ vertical = ModuleNameV2.NAVIPAY,
+ )
+ } ?: emptyList()
+
+ val validOffersList =
+ offersDataList.filter { !it.titlePrefix.isNullOrEmpty() }.toMutableList()
+
+ if (validOffersList.isEmpty()) {
+ return emptyList()
+ }
+
+ validOffersList[0] = validOffersList[0].copy(isBestOffer = true)
+ return validOffersList
+ }
+
+ suspend fun getCachedOffers(screenName: String): List {
+ val defaultConfig = getDefaultConfig(screenName = screenName)
+
+ val genericOffersInfo =
+ naviCacheRepository.get(key = NAVI_PAY_GENERIC_OFFERS_INFO)
+ ?: return getOfferListFromConfig(defaultConfig = defaultConfig)
+
+ return gson.fromJson(genericOffersInfo.value, object : TypeToken>() {}.type)
+ }
+
+ /**
+ * Fetches the best offer list for multiple items.
+ *
+ * @param screenName The name of the screen for which offers are to be fetched.
+ * @param offerRequestList A list of offer requests.
+ * @return A map of product identifiers to their corresponding best offer data.
+ */
+ suspend fun getBestOfferListForMultipleItems(
+ screenName: String,
+ offerRequestList: List,
+ ): Map? {
+ val response =
+ offersRepo
+ .fetchOffersForProductForMultipleRequests(
+ product = UPI,
+ offerRequestList = offerRequestList,
+ metricInfo = getMetricInfo(screenName),
+ offerType = OfferType.BEST.name,
+ )
+ .firstOrNull()
+
+ response?.data?.let { multipleOffersResponse ->
+ val offerResponseToOfferDataMapper = OfferResponseToOfferDataMapper()
+ return multipleOffersResponse.nudgeResponseBundle.mapValues { (_, responses) ->
+ responses.firstOrNull()?.let { bestOfferResponse ->
+ offerResponseToOfferDataMapper
+ .map(offerResponse = bestOfferResponse, vertical = ModuleNameV2.NAVIPAY)
+ .takeIf { !it.titlePrefix.isNullOrEmpty() } ?: OfferData()
+ } ?: OfferData()
+ }
+ }
+ return null
+ }
+
+ fun evaluateOfferStripAnimation(
+ sortedOffersList: List,
+ offersDataList: List,
+ paymentAmount: String,
+ ): Boolean {
+ if (sortedOffersList.isEmpty()) {
+ return true
+ }
+ val bestOffer = sortedOffersList.firstOrNull { it.disabledData.isNull() } ?: return true
+ val bestOfferIndex = offersDataList.indexOf(bestOffer.offer)
+
+ return if (bestOfferIndex == 0 && currentBestOfferIndex == Int.MAX_VALUE) true
+ else if (bestOfferIndex < currentBestOfferIndex) {
+ currentBestOfferIndex = if (paymentAmount.isBlank()) Int.MAX_VALUE else bestOfferIndex
+ paymentAmount.isNotBlank()
+ } else {
+ currentBestOfferIndex = bestOfferIndex
+ false
+ }
+ }
+
+ fun getSortedOffersListWithCondition(
+ offers: List,
+ paymentAmount: String,
+ selectedBankAccount: LinkedAccountEntity?,
+ payeeMcc: String?,
+ purposeCode: String?,
+ getBestOffer: Boolean,
+ ): List {
+ val paramsMap =
+ buildParamsMap(
+ paymentAmount = paymentAmount,
+ selectedBankAccount = selectedBankAccount,
+ payeeMcc = payeeMcc,
+ purposeCode = purposeCode,
+ )
+ val sortedOffersWithCondition = sortOffersWithCondition(offers = offers, params = paramsMap)
+ if (getBestOffer) {
+ val appliedOffer = sortedOffersWithCondition.firstOrNull { it.disabledData.isNull() }
+ return appliedOffer?.let { listOf(it) } ?: emptyList()
+ }
+ return sortedOffersWithCondition
+ }
+
+ fun buildParamsMap(
+ paymentAmount: String,
+ selectedBankAccount: LinkedAccountEntity?,
+ payeeMcc: String?,
+ purposeCode: String?,
+ ): MutableMap {
+ if (paymentAmount.isBlank()) return mutableMapOf()
+
+ val paramsMap =
+ mutableMapOf(
+ OfferResponse.SessionAttributesInfo.Type.TXN_AMOUNT to paymentAmount
+ )
+
+ selectedBankAccount?.let { account ->
+ val isUpiLiteAccount = account.accountId.isAccountIdOfTypeUpiLite()
+ paramsMap.apply {
+ this[OfferResponse.SessionAttributesInfo.Type.UPI_PURPOSE_CODE] =
+ if (isUpiLiteAccount) NAVI_PAY_UPI_LITE_SEND_MONEY_PURPOSE_CODE
+ else purposeCode ?: DEFAULT_UPI_PURPOSE
+ this[OfferResponse.SessionAttributesInfo.Type.UPI_PAYER_BANK_CODE] =
+ account.bankCode.takeUnless { isUpiLiteAccount }
+ }
+ }
+
+ payeeMcc
+ ?.takeIf { it.isNotBlank() }
+ ?.let { paramsMap[OfferResponse.SessionAttributesInfo.Type.UPI_PAYEE_MCC] = it }
+ return paramsMap
+ }
+
+ private fun getOfferListFromConfig(defaultConfig: NaviPayDefaultConfig): List {
+ val bauOfferInfo = defaultConfig.config.genericBauOffer
+ return listOf(
+ OfferData(
+ titlePrefix = bauOfferInfo.titlePrefix,
+ titleSuffix = bauOfferInfo.titleSuffix,
+ descriptionPrefix = bauOfferInfo.descriptionPrefix,
+ descriptionSuffix = bauOfferInfo.descriptionSuffix,
+ iconUrl = bauOfferInfo.iconUrl,
+ applicableInfo = bauOfferInfo.applicableInfo,
+ isBestOffer = true,
+ tags = listOf(BAU),
+ )
+ )
+ }
+
+ private suspend fun getDefaultConfig(screenName: String): NaviPayDefaultConfig {
+ return withContext(Dispatchers.IO) {
+ naviPayConfigUseCase.execute(
+ configKey = DEFAULT_CONFIG,
+ type = object : TypeToken() {}.type,
+ screenName = screenName,
+ ) ?: NaviPayDefaultConfig()
+ }
+ }
+}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/util/SendMoneyUtils.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/util/SendMoneyUtils.kt
index 90b16390d3..94bcccf646 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/util/SendMoneyUtils.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/sendmoney/util/SendMoneyUtils.kt
@@ -8,6 +8,8 @@
package com.navi.pay.management.common.sendmoney.util
import androidx.annotation.StringRes
+import com.navi.rr.common.models.OfferData
+import com.navi.rr.common.models.OfferResponse
object SendMoneyUtils {
@@ -82,4 +84,11 @@ sealed class SendMoneyBottomSheetType {
val isLocalArcTransactionCounterWidgetEnabled: Boolean,
val localArcTransactionCounterFormatted: String,
) : SendMoneyBottomSheetType()
+
+ data class OfferList(
+ val offerData: List,
+ val heading: String,
+ val params: Map?,
+ val isFromAccountSelectionBottomSheet: Boolean,
+ ) : SendMoneyBottomSheetType()
}
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 b599a3ba82..1a559d320b 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
@@ -19,6 +19,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.google.common.util.concurrent.AtomicDouble
import com.google.gson.reflect.TypeToken
+import com.navi.base.model.CtaData
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.isNotNull
import com.navi.base.utils.isNotNullAndNotEmpty
@@ -81,6 +82,7 @@ import com.navi.pay.common.usecase.UpiRequestIdUseCase
import com.navi.pay.common.usecase.ValidateVpaUseCase
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.NaviPaySdkUtils
import com.navi.pay.common.utils.getDefaultPayeeEntity
import com.navi.pay.common.utils.getMetricInfo
@@ -118,6 +120,7 @@ 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.AccountEligibilityMerchantHelper
import com.navi.pay.management.common.sendmoney.util.AccountTypeEligibility
+import com.navi.pay.management.common.sendmoney.util.NaviPayOffersHelper
import com.navi.pay.management.common.sendmoney.util.SendMoneyBottomSheetType
import com.navi.pay.management.common.sendmoney.util.SendMoneyUtils.getValidatedAmountNumber
import com.navi.pay.management.common.transaction.model.network.TransactionInstrumentType
@@ -161,13 +164,18 @@ import com.navi.pay.utils.INTENT_OR_SCAN_PAY_TRANSACTION_ERROR
import com.navi.pay.utils.INVALID_VPA
import com.navi.pay.utils.KEY_IS_FIRST_TRANSACTION_SUCCESSFUL
import com.navi.pay.utils.LITE_MAX_SEND_MONEY
+import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE
import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVI_FESTIVE_THEME
import com.navi.pay.utils.NAVI_AXIS_UPI_HANDLE
+import com.navi.pay.utils.NAVI_PAY_DEFAULT_MCC
import com.navi.pay.utils.NAVI_PAY_OF_TYPE_INTENT_TRANSACTION
import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE
import com.navi.pay.utils.NAVI_PAY_REWARDS_SCRATCH_CARD_RESPONSE_CACHE_KEY
+import com.navi.pay.utils.NAVI_PAY_UPI_LITE_SEND_MONEY_PURPOSE_CODE
import com.navi.pay.utils.NOTES_REGEX
import com.navi.pay.utils.NOTE_MAX_LENGTH
+import com.navi.pay.utils.PAYEE_VPA
+import com.navi.pay.utils.PAYER_VPA
import com.navi.pay.utils.PAY_AGAIN
import com.navi.pay.utils.PENDING_REQUEST_REFRESHED_REQUIRED
import com.navi.pay.utils.PSP_DOWN
@@ -178,7 +186,11 @@ import com.navi.pay.utils.RUPEE_SYMBOL
import com.navi.pay.utils.SAVINGS_ONLY_ENABLED_ACCOUNTS
import com.navi.pay.utils.SELF_TRANSFER_ERROR
import com.navi.pay.utils.SEND_MONEY_BACK_PRESS_BLOCK_TIME_MILLIS
+import com.navi.pay.utils.TXN_AMOUNT
import com.navi.pay.utils.UPI_LITE_CONFIG
+import com.navi.pay.utils.UPI_PAYEE_MCC
+import com.navi.pay.utils.UPI_PAYER_BANK_CODE
+import com.navi.pay.utils.UPI_PURPOSE_CODE
import com.navi.pay.utils.ZERO_STRING
import com.navi.pay.utils.getDisplayableAmount
import com.navi.pay.utils.getFormattedAmountWithDecimal
@@ -186,6 +198,7 @@ import com.navi.pay.utils.getTrimmedValue
import com.navi.pay.utils.isAccountIdOfTypeUpiLite
import com.navi.pay.utils.isAmountValidForSendMoney
import com.navi.pay.utils.toUpiMapperVpa
+import com.navi.rr.common.models.OfferData
import com.navi.rr.scratchcard.helper.ScratchCardNudgeHelper
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -249,6 +262,7 @@ constructor(
private val upiLiteCommsAndMandateExecutionHandler: UpiLiteCommsAndMandateExecutionHandler,
val accountListCheckBalanceUseCase: AccountListCheckBalanceUseCase,
private val sendMoneyUseCase: SendMoneyUseCase,
+ private val naviPayOffersHelper: NaviPayOffersHelper,
private val arcNudgeUseCase: ArcNudgeUseCase,
private val naviPayPspManager: NaviPayPspManager,
naviPayActivityDataProvider: NaviPayActivityDataProvider,
@@ -289,12 +303,19 @@ constructor(
private val _bankAccountsState = MutableStateFlow(BankAccountsState.Loading)
val bankAccountsState = _bankAccountsState.asStateFlow()
+ private val _navigateToNextScreenFromHelpCta = MutableSharedFlow()
+ val navigateToNextScreenFromHelpCta = _navigateToNextScreenFromHelpCta.asSharedFlow()
+
+ private val helpCtaData = getHelpCtaData(screenName = screenName)
+
private val _selectedBankAccount = MutableStateFlow(null)
val selectedBankAccount = _selectedBankAccount.asStateFlow()
private val _isSelectedAccountEligible = MutableStateFlow(true)
val isSelectedAccountEligible = _isSelectedAccountEligible.asStateFlow()
+ private val offersDataList = MutableStateFlow>(emptyList())
+
private val _bottomSheetStateHolder =
MutableStateFlow(
SendMoneyScreenBottomSheetStateHolder(
@@ -531,7 +552,6 @@ constructor(
when (source) {
is SendMoneyScreenSource.BankDetailInput -> source.displayableAccountNumber
is SendMoneyScreenSource.PhoneContact -> source.payeeVpaToDisplay
- is SendMoneyScreenSource.QrScan -> source.payeeVpaToDisplay
is SendMoneyScreenSource.TransactionHistoryDetail ->
source.payeeVpaToDisplay ?: payeeEntity.value.vpa
else -> payeeEntity.value.vpa
@@ -539,6 +559,8 @@ constructor(
private var accountTypeEligibilityMap: Map = mapOf()
+ private val isOfferExperienceEnabled = MutableStateFlow(false)
+
private val _isFestiveThemeExperimentEnabled = MutableStateFlow(false)
val isFestiveThemeExperimentEnabled = _isFestiveThemeExperimentEnabled.asStateFlow()
@@ -546,6 +568,49 @@ constructor(
private val startPaymentJobReference = AtomicReference()
+ private var fetchOffersJob: Job? = null
+
+ val sortedOffersList =
+ combine(paymentAmount.debounce(200), offersDataList, selectedBankAccount, payeeEntity) {
+ paymentAmount,
+ offersDataList,
+ selectedBankAccount,
+ payeeEntity ->
+ if (offersDataList.isEmpty()) {
+ emptyList()
+ } else {
+ naviPayOffersHelper.getSortedOffersListWithCondition(
+ offers = offersDataList,
+ paymentAmount = paymentAmount.text,
+ selectedBankAccount = selectedBankAccount,
+ payeeMcc = payeeEntity.mcc,
+ purposeCode = payeeEntity.purpose,
+ getBestOffer = isAmountReadOnly,
+ )
+ }
+ }
+ .flowOn(coroutineDispatcherProvider.default)
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = emptyList(),
+ )
+
+ val shouldShowBottomUpOfferStripTransition =
+ combine(sortedOffersList, offersDataList) { sortedOffersList, offersDataList ->
+ naviPayOffersHelper.evaluateOfferStripAnimation(
+ sortedOffersList = sortedOffersList,
+ offersDataList = offersDataList,
+ paymentAmount = paymentAmount.value.text,
+ )
+ }
+ .flowOn(coroutineDispatcherProvider.default)
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = true,
+ )
+
init {
recordScreenLandTime(screenName)
setLitmusExperimentValues()
@@ -564,9 +629,54 @@ constructor(
?.variant
?.enabled == true
}
+
+ isOfferExperienceEnabled.update {
+ litmusExperimentsUseCase
+ .execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE)
+ ?.variant
+ ?.enabled == true
+ }
}
}
+ private fun fetchOffersList() {
+ fetchOffersJob =
+ viewModelScope.launch(coroutineDispatcherProvider.io) {
+ val attributeMap =
+ mapOf(
+ UPI_PAYEE_MCC to (payeeEntity.value.mcc ?: NAVI_PAY_DEFAULT_MCC),
+ PAYEE_VPA to payeeEntity.value.vpa,
+ UPI_PAYER_BANK_CODE to selectedBankAccount.value?.bankCode.orEmpty(),
+ UPI_PURPOSE_CODE to
+ if (
+ selectedBankAccount.value
+ ?.accountId
+ ?.isAccountIdOfTypeUpiLite()
+ .orFalse()
+ ) {
+ NAVI_PAY_UPI_LITE_SEND_MONEY_PURPOSE_CODE
+ } else {
+ payeeEntity.value.purpose
+ },
+ TXN_AMOUNT to paymentAmount.value.text,
+ PAYER_VPA to selectedBankAccount.value?.vpa.orEmpty(),
+ )
+ .filterValues { it.isNotNullAndNotEmpty() }
+
+ val offers =
+ naviPayOffersHelper.getOffersFromNetwork(
+ screenName = screenName,
+ attributeMap = attributeMap,
+ )
+ naviPayAnalytics.onOffersCalloutLanded(
+ numberOfOffers = offers.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ attributeMap = attributeMap.toString(),
+ )
+ offersDataList.update { offers }
+ }
+ }
+
private fun autoTriggerCollectRequestActionListener() {
viewModelScope.launch(Dispatchers.IO) {
// Waiting for all listeners before triggering auto actions
@@ -1314,9 +1424,11 @@ constructor(
is SendMoneyScreenSource.TransactionHistoryDetail -> {
source.payerVpa
}
+
is SendMoneyScreenSource.CollectRequest -> {
source.payerVpa
}
+
else -> ""
}
@@ -1541,6 +1653,11 @@ constructor(
return
}
+ // Before proceeding, cancel the fetchOffers job if active
+ if (fetchOffersJob?.isActive == true) {
+ fetchOffersJob?.cancel()
+ }
+
if (isPaymentFromLiteAccount) { // For further process we shift to actual accountId
selectedBankAccount =
selectedBankAccount.deepCopy(
@@ -1895,6 +2012,11 @@ constructor(
executeAutoSelectionOfBankAccount(preferredBankAccountId = preferredBankAccountUniqueId)
+ // fetch offers post validate vpa success & bank account selection
+ if (isOfferExperienceEnabled.value) {
+ fetchOffersList()
+ }
+
checkIfPSPIsDown()
}
@@ -2284,6 +2406,7 @@ constructor(
)
)
}
+
TransactionStatus.DECLINED -> {
updateSetDefaultStatusBarColor(setDefaultStatusBarColor = false)
updateScreenState(
@@ -2291,6 +2414,7 @@ constructor(
SendMoneyScreenState.PaymentDeclined(transactionEntity = transactionEntity)
)
}
+
else -> {
initiateRewardScratchCardCalculation(transactionEntity = transactionEntity)
if (isPaymentThroughLiteAccount) {
@@ -2678,7 +2802,7 @@ constructor(
}
}
- fun declineCollectRequest() {
+ private fun declineCollectRequest() {
// TODO: - Check if we need to handle this for mandate decline case
viewModelScope.launch(Dispatchers.IO) {
if (source !is SendMoneyScreenSource.CollectRequest) {
@@ -2953,11 +3077,14 @@ constructor(
when {
transactionType == UpiTransactionType.SCAN_PAY ->
SendMoneyBackPressAction.GoBack
+
transactionType == UpiTransactionType.INTENT_PAY ||
isCollectRequestFromNotification() -> SendMoneyBackPressAction.FinishScreen
+
else -> SendMoneyBackPressAction.GoBack
}
}
+
else -> SendMoneyBackPressAction.GoBack
}
}
@@ -3206,6 +3333,12 @@ constructor(
}
}
+ fun onHelpCtaClicked() {
+ viewModelScope.launch(coroutineDispatcherProvider.default) {
+ _navigateToNextScreenFromHelpCta.emit(helpCtaData)
+ }
+ }
+
fun resetCheckBalanceStates() {
viewModelScope.launch(Dispatchers.IO) {
delay(300) // Delay added to wait for navigation to complete before state reset
@@ -3253,6 +3386,61 @@ constructor(
}
}
+ fun onOffersBottomSheetDismissClicked() {
+ viewModelScope.launch(Dispatchers.IO) {
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetUIState = SendMoneyBottomSheetType.PayAmount,
+ bottomSheetStateChange = true,
+ )
+ }
+ }
+
+ fun onOffersStripClicked(isFromAccountSelectionBottomSheet: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ naviPayAnalytics.onOffersCalloutClicked(
+ numberOfOffers = sortedOffersList.value.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ isFromAccountSelectionBottomSheet = false,
+ )
+
+ if (
+ offersDataList.value.isEmpty() ||
+ (isAmountReadOnly && sortedOffersList.value.isEmpty())
+ ) {
+ return@launch
+ }
+
+ val offerData: List =
+ if (isAmountReadOnly) listOf(sortedOffersList.value.first().offer)
+ else offersDataList.value
+
+ val params =
+ naviPayOffersHelper.buildParamsMap(
+ paymentAmount = paymentAmount.value.text,
+ selectedBankAccount = selectedBankAccount.value,
+ payeeMcc = payeeEntity.value.mcc,
+ purposeCode = payeeEntity.value.purpose,
+ )
+
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetStateChange = true,
+ bottomSheetUIState =
+ SendMoneyBottomSheetType.OfferList(
+ offerData = offerData,
+ heading =
+ resourceProvider.getString(
+ resId = R.string.np_n_upi_payment_offers,
+ if (isAmountReadOnly) 1 else offersDataList.value.size,
+ ),
+ params = params,
+ isFromAccountSelectionBottomSheet = isFromAccountSelectionBottomSheet,
+ ),
+ )
+ }
+ }
+
override val screenName: String
get() = NAVI_PAY_SEND_MONEY
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/ui/UPIIdInputScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/ui/UPIIdInputScreen.kt
index d36b2ef828..818acaaaea 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/ui/UPIIdInputScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/ui/UPIIdInputScreen.kt
@@ -24,15 +24,17 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.TextFieldDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -67,20 +69,26 @@ import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.ui.ContactIconView
import com.navi.pay.common.ui.ImageTitleDescriptionShimmerView
import com.navi.pay.common.ui.NaviPayHeader
+import com.navi.pay.common.ui.NaviPayModalBottomSheet
+import com.navi.pay.common.ui.NaviPayOffersBottomSheet
import com.navi.pay.common.ui.NaviPaySponsorView
import com.navi.pay.common.ui.SelfTransferCtaView
import com.navi.pay.entry.NaviPayActivity
import com.navi.pay.management.common.model.view.WarningErrorInfoState
+import com.navi.pay.management.common.upiid.viewmodel.UPIIDInputScreenBottomSheetUIState
import com.navi.pay.management.common.upiid.viewmodel.UPIIdInputViewModel
import com.navi.pay.utils.LINKED_ACCOUNT_SCREEN_SOURCE
import com.navi.pay.utils.SELF_TRANSFER
import com.navi.pay.utils.clickableDebounce
import com.navi.pay.utils.customHide
import com.navi.pay.utils.initials
+import com.navi.rr.common.models.OfferData
+import com.navi.rr.common.ui.OffersRolodex
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import kotlinx.coroutines.launch
-@OptIn(ExperimentalMaterialApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable
fun UPIIdInputScreen(
@@ -107,10 +115,30 @@ fun UPIIdInputScreen(
upiIdInputViewModel.isWarningOrErrorState.collectAsStateWithLifecycle()
val isSelfTransferCtaVisible by
upiIdInputViewModel.isSelfTransferCtaVisible.collectAsStateWithLifecycle()
+ val genericOffersList by upiIdInputViewModel.genericOffersList.collectAsStateWithLifecycle()
+ val bottomSheetStateHolder by
+ upiIdInputViewModel.bottomSheetStateHolder.collectAsStateWithLifecycle()
val keyboardController = LocalSoftwareKeyboardController.current
val context = LocalContext.current
val view = LocalView.current
+ val scope = rememberCoroutineScope()
+
+ val bottomSheetState =
+ rememberModalBottomSheetState(
+ skipPartiallyExpanded = true,
+ confirmValueChange = { bottomSheetStateHolder.bottomSheetStateChange },
+ )
+
+ val onDismissBottomSheet: () -> Unit = {
+ scope
+ .launch { bottomSheetState.hide() }
+ .invokeOnCompletion {
+ if (!bottomSheetState.isVisible || !bottomSheetStateHolder.bottomSheetStateChange) {
+ upiIdInputViewModel.updateBottomSheetUIState(showBottomSheet = false)
+ }
+ }
+ }
LaunchedEffect(key1 = hideKeyboard) {
if (hideKeyboard) {
@@ -136,6 +164,12 @@ fun UPIIdInputScreen(
upiIdInputViewModel.onSavedBeneficiaryItemClicked()
}
+ val onOffersRolodexClicked = {
+ keyboardController?.customHide(context = context, view = view)
+ focusManager.clearFocus()
+ upiIdInputViewModel.onOffersRolodexClicked()
+ }
+
RenderUPIIdInputScreen(
upiIdInput = upiIdInput,
onUpiIdInputValueChanged = { upiIdInputViewModel.onUPIIdInputValueChange(it) },
@@ -148,7 +182,41 @@ fun UPIIdInputScreen(
onSavedBeneficiaryItemClicked = onSavedBeneficiaryItemClicked,
onTrailingIconClicked = { upiIdInputViewModel.updateUpiIdInput(TextFieldValue()) },
isSelfTransferCtaVisible = isSelfTransferCtaVisible,
+ genericOffersList = genericOffersList,
+ onOffersRolodexClicked = onOffersRolodexClicked,
)
+
+ if (bottomSheetStateHolder.showBottomSheet) {
+ NaviPayModalBottomSheet(
+ modifier = Modifier.fillMaxWidth(),
+ bottomSheetState = bottomSheetState,
+ onDismissRequest = onDismissBottomSheet,
+ shouldDismissOnBackPress = true,
+ bottomSheetContent = {
+ RenderUPIIDScreenBottomSheetUIState(
+ bottomSheetUIState = bottomSheetStateHolder.bottomSheetUIState,
+ onDismissBottomSheet = onDismissBottomSheet,
+ )
+ },
+ )
+ }
+}
+
+@Composable
+fun RenderUPIIDScreenBottomSheetUIState(
+ bottomSheetUIState: UPIIDInputScreenBottomSheetUIState?,
+ onDismissBottomSheet: () -> Unit,
+) {
+ when (bottomSheetUIState) {
+ is UPIIDInputScreenBottomSheetUIState.OfferList -> {
+ NaviPayOffersBottomSheet(
+ offerData = bottomSheetUIState.offerData,
+ closeSheet = onDismissBottomSheet,
+ heading = bottomSheetUIState.heading,
+ )
+ }
+ else -> {}
+ }
}
@Composable
@@ -164,6 +232,8 @@ fun RenderUPIIdInputScreen(
onSavedBeneficiaryItemClicked: () -> Unit,
onTrailingIconClicked: () -> Unit,
isSelfTransferCtaVisible: Boolean,
+ genericOffersList: List,
+ onOffersRolodexClicked: () -> Unit,
) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
@@ -305,7 +375,6 @@ fun RenderUPIIdInputScreen(
.clickableDebounce { onSavedBeneficiaryItemClicked() },
)
} else {
-
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
@@ -366,6 +435,15 @@ fun RenderUPIIdInputScreen(
ImageTitleDescriptionShimmerView()
}
+ OffersRolodex(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = 24.dp, bottom = 8.dp, start = 16.dp, end = 16.dp),
+ offerData = genericOffersList,
+ shouldShowRolodexAnimation = genericOffersList.size > 1,
+ onClick = onOffersRolodexClicked,
+ )
+
Spacer(modifier = Modifier.weight(1f))
}
},
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIDInputScreenBottomSheetHolder.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIDInputScreenBottomSheetHolder.kt
new file mode 100644
index 0000000000..ef9fdcea5c
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIDInputScreenBottomSheetHolder.kt
@@ -0,0 +1,14 @@
+/*
+ *
+ * * Copyright © 2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.management.common.upiid.viewmodel
+
+data class UPIIDInputScreenBottomSheetHolder(
+ val showBottomSheet: Boolean,
+ val bottomSheetStateChange: Boolean,
+ val bottomSheetUIState: UPIIDInputScreenBottomSheetUIState?,
+)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModel.kt
index e44ff76deb..9ab72d03f5 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModel.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModel.kt
@@ -15,6 +15,7 @@ import com.navi.base.utils.ResourceProvider
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
+import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.common.utils.Constants.AT_THE_RATE
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
@@ -35,11 +36,13 @@ import com.navi.pay.destinations.SendMoneyScreenDestination
import com.navi.pay.management.common.model.view.WarningErrorInfoState
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.util.NaviPayOffersHelper
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.INVALID_UPI_ID
import com.navi.pay.utils.INVALID_UPI_NUMBER
import com.navi.pay.utils.INVALID_VPA
import com.navi.pay.utils.INVALID_VPA_HANDLE
+import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE
import com.navi.pay.utils.MAPPING_DOES_NOT_EXIST
import com.navi.pay.utils.MCC_MISMATCH
import com.navi.pay.utils.NAVI_PAY_SEARCH_QUERY_API_DELAY
@@ -49,6 +52,7 @@ import com.navi.pay.utils.isValidUpiId
import com.navi.pay.utils.isValidUpiNumber
import com.navi.pay.utils.isValidUpiNumberLength
import com.navi.pay.utils.toUpiMapperVpa
+import com.navi.rr.common.models.OfferData
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -80,6 +84,8 @@ constructor(
private val naviPaySessionHelper: NaviPaySessionHelper,
private val validateVpaUseCase: ValidateVpaUseCase,
private val resourceProvider: ResourceProvider,
+ private val naviPayOffersHelper: NaviPayOffersHelper,
+ private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
) : NaviPayBaseVM() {
private val naviPayAnalytics: NaviPayAnalytics.NaviPayUpiIdInput =
@@ -106,6 +112,16 @@ constructor(
private val _navigateToNextScreen = MutableSharedFlow()
val navigateToNextScreen = _navigateToNextScreen.asSharedFlow()
+ private val _bottomSheetStateHolder =
+ MutableStateFlow(
+ UPIIDInputScreenBottomSheetHolder(
+ showBottomSheet = false,
+ bottomSheetStateChange = false,
+ bottomSheetUIState = null,
+ )
+ )
+ val bottomSheetStateHolder = _bottomSheetStateHolder.asStateFlow()
+
private val _showShimmer = MutableStateFlow(false)
val showShimmer = _showShimmer.asStateFlow()
@@ -117,6 +133,9 @@ constructor(
private val userPhoneNumber = fetchUserPhoneNumber()
+ private val _genericOffersList = MutableStateFlow>(emptyList())
+ val genericOffersList = _genericOffersList.asStateFlow()
+
private val _isSelfTransferCtaVisible = MutableStateFlow(false)
val isSelfTransferCtaVisible = _isSelfTransferCtaVisible.asStateFlow()
@@ -170,6 +189,7 @@ constructor(
}
init {
+ getGenericOffersList()
updateNaviPayDefaultConfig()
viewModelScope.launch(Dispatchers.IO) { upiIdChangeObserver() }
}
@@ -185,6 +205,25 @@ constructor(
}
}
+ private fun getGenericOffersList() {
+ viewModelScope.launch(Dispatchers.IO) {
+ val isOfferExperienceEnabled =
+ litmusExperimentsUseCase
+ .execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE)
+ ?.variant
+ ?.enabled ?: false
+
+ if (isOfferExperienceEnabled) {
+ val genericOffersList = naviPayOffersHelper.getCachedOffers(screenName = screenName)
+ naviPayAnalytics.onOffersCalloutLanded(
+ numberOfOffers = genericOffersList.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+ _genericOffersList.update { genericOffersList }
+ }
+ }
+ }
+
fun updateHideKeyboard(hideKeyboard: Boolean) {
_hideKeyboard.update { hideKeyboard }
}
@@ -441,6 +480,48 @@ constructor(
}
}
+ fun updateBottomSheetUIState(
+ showBottomSheet: Boolean,
+ bottomSheetStateChange: Boolean? = null,
+ bottomSheetUIState: UPIIDInputScreenBottomSheetUIState? = null,
+ ) {
+ _bottomSheetStateHolder.update {
+ it.copy(
+ showBottomSheet = showBottomSheet,
+ bottomSheetStateChange = bottomSheetStateChange ?: it.bottomSheetStateChange,
+ bottomSheetUIState = bottomSheetUIState ?: it.bottomSheetUIState,
+ )
+ }
+ }
+
+ fun onOffersRolodexClicked() {
+ viewModelScope.launch(Dispatchers.IO) {
+ naviPayAnalytics.onOffersCalloutClicked(
+ numberOfOffers = genericOffersList.value.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetStateChange = true,
+ bottomSheetUIState =
+ UPIIDInputScreenBottomSheetUIState.OfferList(
+ offerData = genericOffersList.value,
+ heading =
+ resourceProvider.getString(
+ resId = R.string.np_n_upi_payment_offers,
+ genericOffersList.value.size,
+ ),
+ ),
+ )
+ }
+ }
+
override val screenName: String
get() = NAVI_PAY_UPI_ID_INPUT
}
+
+sealed class UPIIDInputScreenBottomSheetUIState {
+ data class OfferList(val offerData: List, val heading: String) :
+ UPIIDInputScreenBottomSheetUIState()
+}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/bank/ui/BankDetailInputScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/bank/ui/BankDetailInputScreen.kt
index 406a9926d4..62fbe49f9a 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/bank/ui/BankDetailInputScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/bank/ui/BankDetailInputScreen.kt
@@ -195,7 +195,7 @@ fun RenderBankDetailInputScreen(
topBar = {
NaviPayHeader(
navigationIcon = navigationIcon,
- title = stringResource(id = R.string.send_money_to_bank),
+ title = stringResource(id = R.string.np_pay_to_bank_account),
onNavigationIconClick = onBackClick,
modifier = Modifier,
)
@@ -208,18 +208,19 @@ fun RenderBankDetailInputScreen(
Spacer(modifier = Modifier.height(16.dp))
NaviText(
- text = stringResource(id = R.string.recipient_detail_header),
+ text = stringResource(id = R.string.np_enter_receiver_details),
fontFamily = naviFontFamily,
- fontSize = 18.sp,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
- color = NaviPayColor.inputFieldFilled,
+ fontSize = 24.sp,
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
+ color = NaviPayColor.textPrimary,
modifier = Modifier.padding(horizontal = 16.dp),
)
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(24.dp))
InputTextFieldWithDescriptionHeader(
headerString = stringResource(id = R.string.np_account_number),
+ headerStringFontSize = 16.sp,
placeHolderString =
stringResource(id = R.string.bank_input_account_num_placeholder),
value = accountNumberInput,
@@ -240,6 +241,7 @@ fun RenderBankDetailInputScreen(
InputTextFieldWithDescriptionHeader(
headerString = stringResource(id = R.string.re_enter_account_number),
+ headerStringFontSize = 16.sp,
placeHolderString =
stringResource(id = R.string.bank_input_reaccount_num_placeholder),
value = reAccountNumberInput,
@@ -270,6 +272,7 @@ fun RenderBankDetailInputScreen(
InputTextFieldWithDescriptionHeader(
headerString = stringResource(id = R.string.ifsc),
+ headerStringFontSize = 16.sp,
placeHolderString = stringResource(id = R.string.bank_input_ifsc_placeholder),
value = ifscInput,
imeAction = ImeAction.Next,
@@ -308,6 +311,7 @@ fun RenderBankDetailInputScreen(
InputTextFieldWithDescriptionHeader(
headerString = stringResource(id = R.string.recipient_name),
+ headerStringFontSize = 16.sp,
placeHolderString =
stringResource(id = R.string.bank_input_recipient_placeholder),
imeAction = ImeAction.Done,
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 aa6d4e97f1..0ee7a9a219 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
@@ -25,28 +25,20 @@ import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.PreviewView
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectDragGestures
-import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
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.wrapContentSize
-import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.SnackbarDuration
@@ -57,14 +49,12 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
@@ -73,8 +63,6 @@ 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.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
@@ -104,23 +92,17 @@ import com.navi.naviwidgets.extensions.NaviText
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.model.view.NaviPayButtonAction
-import com.navi.pay.common.model.view.NaviPayScreenType
import com.navi.pay.common.model.view.NaviPermissionResult
import com.navi.pay.common.model.view.rememberMultiplePermissions
import com.navi.pay.common.theme.color.NaviPayColor
-import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderDescButton
import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader
import com.navi.pay.common.ui.CameraPermissionView
-import com.navi.pay.common.ui.ContactIconView
-import com.navi.pay.common.ui.NaviPayLottieAnimation
import com.navi.pay.common.ui.NaviPayModalBottomSheet
-import com.navi.pay.common.ui.RewardsNudgeWithoutBg
-import com.navi.pay.common.ui.StaticSearchBarView
+import com.navi.pay.common.ui.NaviPayOffersBottomSheet
+import com.navi.pay.common.ui.OfferStripView
import com.navi.pay.common.utils.ErrorEventHandler
-import com.navi.pay.common.utils.NaviPayCommonUtils.getNormalisedPhoneNumber
import com.navi.pay.common.utils.NaviPayEventBus
import com.navi.pay.destinations.MandateDetailScreenOfPendingCategoryDestination
-import com.navi.pay.destinations.PayToContactsScreenDestination
import com.navi.pay.destinations.QrScannerScreenDestination
import com.navi.pay.destinations.SendMoneyScreenDestination
import com.navi.pay.entry.NaviPayActivity
@@ -133,13 +115,12 @@ import com.navi.pay.management.moneytransfer.scanpay.viewmodel.QrScannerBottomSh
import com.navi.pay.management.moneytransfer.scanpay.viewmodel.QrScannerViewModel
import com.navi.pay.permission.utils.PermissionKeys
import com.navi.pay.permission.utils.PermissionUtils
-import com.navi.pay.tstore.list.model.view.OrderEntity
+import com.navi.pay.utils.BAU
import com.navi.pay.utils.DEFAULT_INITIATION_MODE_QR_MANDATE
-import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE
import com.navi.pay.utils.clearBackStackUpToAndNavigate
import com.navi.pay.utils.clickableDebounce
-import com.navi.pay.utils.contactInitials
import com.navi.pay.utils.launchPermissionSettingsScreen
+import com.navi.rr.common.models.OfferData
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlin.time.Duration.Companion.milliseconds
@@ -160,16 +141,7 @@ fun QrScannerScreen(
) {
val lifecycleOwner = LocalLifecycleOwner.current
-
- val showLoader by qrScannerViewModel.showLoader.collectAsStateWithLifecycle()
- val clickedContactPhoneNumber by
- qrScannerViewModel.clickedContactPhoneNumber.collectAsStateWithLifecycle()
- val frequentOrdersHeading by
- qrScannerViewModel.frequentOrdersHeading.collectAsStateWithLifecycle()
- val rewardsNudgeDetailEntity by
- qrScannerViewModel.rewardsNudgeDetailEntity.collectAsStateWithLifecycle()
- val isFrequentTransactionsEnabled by
- qrScannerViewModel.isFrequentTransactionsEnabled.collectAsStateWithLifecycle()
+ val genericOffersList by qrScannerViewModel.genericOffersList.collectAsStateWithLifecycle()
val onBackClick = { navigator.navigateUp(naviPayActivity) }
@@ -232,17 +204,16 @@ fun QrScannerScreen(
}
}
- LaunchedEffect(Unit) {
- qrScannerViewModel.navigateToNextScreen.collect { navigator.navigate(direction = it) }
- }
-
val bottomSheetStateHolder by
qrScannerViewModel.bottomSheetStateHolder.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
val bottomSheetState =
- rememberModalBottomSheetState(skipPartiallyExpanded = true, confirmValueChange = { false })
+ rememberModalBottomSheetState(
+ skipPartiallyExpanded = true,
+ confirmValueChange = { bottomSheetStateHolder.bottomSheetStateChange },
+ )
DisposableEffect(key1 = lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
@@ -269,7 +240,6 @@ fun QrScannerScreen(
}
val isTorchEnabled by qrScannerViewModel.isTorchEnabled.observeAsState(false)
- val frequentOrders by qrScannerViewModel.frequentOrders.collectAsStateWithLifecycle()
val cameraPermissionsState =
rememberMultiplePermissions(
permissions =
@@ -284,6 +254,7 @@ fun QrScannerScreen(
)
// Camera screen will be shown on recomposition
}
+
NaviPermissionResult.HardDenied -> {
naviPayAnalytics.onCameraPermissionDenied(
showRationale = false,
@@ -294,6 +265,7 @@ fun QrScannerScreen(
naviPaySessionAttributes = qrScannerViewModel.getNaviPaySessionAttributes()
)
}
+
NaviPermissionResult.ShowRationale -> {
naviPayAnalytics.onCameraPermissionDenied(
showRationale = true,
@@ -303,25 +275,19 @@ fun QrScannerScreen(
}
}
+ LaunchedEffect(Unit) {
+ naviPayAnalytics.onQrScannerLanded(
+ isPermissionGranted = cameraPermissionsState.allPermissionsGranted,
+ naviPaySessionAttributes = qrScannerViewModel.getNaviPaySessionAttributes(),
+ )
+ }
+
val onAllowPermissionButtonClick = { cameraPermissionsState.launchMultiplePermissionRequest() }
val localHapticFeedback = LocalHapticFeedback.current
BackHandler { onBackClick() }
- LaunchedEffect(Unit) {
- qrScannerViewModel.isFrequentOrderListUpdated.collect {
- if (it) {
- naviPayAnalytics.onQrScannerLanded(
- isPermissionGranted = cameraPermissionsState.allPermissionsGranted,
- rewardNudgeShown = if (rewardsNudgeDetailEntity != null) "Y" else "N",
- naviPaySessionAttributes = qrScannerViewModel.getNaviPaySessionAttributes(),
- isFrequentOrderListEmpty = frequentOrders.isEmpty(),
- )
- }
- }
- }
-
val closeSheet: () -> Unit = {
scope
.launch { bottomSheetState.hide() }
@@ -332,10 +298,6 @@ fun QrScannerScreen(
}
}
- val navigateToPayToContactsScreen = {
- navigator.navigate(PayToContactsScreenDestination(openSearchState = true))
- }
-
val uriHandler = LocalUriHandler.current
LaunchedEffect(Unit) {
@@ -387,6 +349,7 @@ fun QrScannerScreen(
)
}
}
+
else -> {
closeSheet()
}
@@ -407,11 +370,12 @@ fun QrScannerScreen(
NaviPayModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
bottomSheetState = bottomSheetState,
+ shouldDismissOnBackPress = true,
onDismissRequest = closeSheet,
bottomSheetContent = {
QrScannerBottomSheetContent(
bottomSheetUIState = bottomSheetStateHolder.bottomSheetUIState,
- onInvalidVpaBottomSheetCtaClicked = closeSheet,
+ closeSheet = closeSheet,
onUrlRedirectionPrimaryCtaClicked = {
scope.launch {
try {
@@ -448,46 +412,23 @@ fun QrScannerScreen(
naviPaySessionAttributes = qrScannerViewModel.getNaviPaySessionAttributes(),
)
}
- Column(modifier = Modifier.fillMaxSize()) {
- Spacer(
- modifier =
- Modifier.fillMaxWidth()
- .height(144.dp)
- .background(color = colorAdjustedContent)
- )
-
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(modifier = Modifier.weight(1f).fillMaxWidth().background(colorAdjustedContent))
if (cameraPermissionsState.allPermissionsGranted) {
- rewardsNudgeDetailEntity?.let {
- Box(
- modifier =
- Modifier.fillMaxWidth().background(color = colorAdjustedContent),
- contentAlignment = Alignment.Center,
- ) {
- RewardsNudgeWithoutBg(
- modifier = Modifier,
- exchangeWidgetBgAlpha = 0.2f,
- textColor = NaviPayColor.textWhite,
- nudgeDetailEntity = it,
- )
- }
- }
-
- Spacer(
- modifier =
- Modifier.fillMaxWidth()
- .height(24.dp)
- .background(color = colorAdjustedContent)
- )
-
Image(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier.fillMaxWidth().wrapContentHeight(),
painter = painterResource(id = R.drawable.bg_scanner_overlay),
contentDescription = stringResource(id = R.string.title_scan_and_pay),
contentScale = ContentScale.FillWidth,
)
Column(
- modifier = Modifier.fillMaxSize().background(color = colorAdjustedContent),
+ modifier = Modifier.fillMaxWidth().background(colorAdjustedContent),
horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
TorchButton(
@@ -529,209 +470,88 @@ fun QrScannerScreen(
onAllowPermissionButtonClick = onAllowPermissionButtonClick,
)
}
+ Box(modifier = Modifier.weight(1f).fillMaxWidth().background(colorAdjustedContent))
}
}
+
NavigationIconWithOtherAppsView(
modifier = Modifier.fillMaxWidth().padding(16.dp),
needsResult = naviPayActivity.needsResult,
onBackClick = onBackClick,
cameraPermissionsGranted = cameraPermissionsState.allPermissionsGranted,
+ genericOffersList = genericOffersList,
+ onOffersStripClicked = qrScannerViewModel::onOffersStripClicked,
)
-
- if (isFrequentTransactionsEnabled) {
- FrequentOrdersListView(
- modifier =
- Modifier.fillMaxWidth().height(IntrinsicSize.Min).align(Alignment.BottomCenter),
- frequentOrders = frequentOrders,
- onFrequentOrderSelected = { orderEntity ->
- qrScannerViewModel.initiatePayment(orderEntity = orderEntity)
- naviPayAnalytics.onContactSelected(
- source = NaviPayScreenType.NAVI_PAY_QR_SCANNER_SCREEN.name,
- inContactList = false,
- isFromFrequentOrderList = true,
- currentSearchQuery = "",
- naviPaySessionAttributes = qrScannerViewModel.getNaviPaySessionAttributes(),
- isPermissionGranted = cameraPermissionsState.allPermissionsGranted,
- orderOfOrderItem = frequentOrders.indexOf(orderEntity),
- )
- },
- navigateToPayToContactsScreen = navigateToPayToContactsScreen,
- showLoader = showLoader,
- clickedContactPhoneNumber = clickedContactPhoneNumber,
- frequentOrdersHeading = frequentOrdersHeading,
- )
- }
}
}
@Composable
-fun FrequentOrdersListView(
+fun RenderOffersStrip(
+ genericOffersList: List,
modifier: Modifier = Modifier,
- frequentOrders: List,
- onFrequentOrderSelected: (OrderEntity) -> Unit,
- navigateToPayToContactsScreen: () -> Unit,
- showLoader: Boolean,
- clickedContactPhoneNumber: String,
- frequentOrdersHeading: String,
+ isCameraPermissionGranted: Boolean,
+ onOffersStripClicked: () -> Unit,
) {
- var verticalDragOffset by remember { mutableFloatStateOf(0f) }
- Card(
- modifier =
- modifier.pointerInput(Unit) {
- detectDragGestures(
- onDragStart = {},
- onDragEnd = { if (verticalDragOffset < 0) navigateToPayToContactsScreen() },
- onDragCancel = {},
- onDrag = { _, dragAmount -> verticalDragOffset = dragAmount.y },
- )
- },
- shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
- elevation = 24.dp,
- ) {
- Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) {
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
- Box(
+ if (genericOffersList.isNotEmpty()) {
+ val adjustedBgColor =
+ remember(isCameraPermissionGranted) {
+ if (isCameraPermissionGranted) NaviPayColor.black.copy(alpha = 0.3f)
+ else NaviPayColor.bgDefault
+ }
+ val adjustedShadowColor =
+ remember(isCameraPermissionGranted) {
+ if (isCameraPermissionGranted) NaviPayColor.ctaWhite.copy(alpha = 0.6f)
+ else NaviPayColor.darkGray.copy(alpha = 0.6f)
+ }
+ val bestOfferData =
+ remember(genericOffersList) {
+ genericOffersList.firstOrNull { it.tags?.contains(BAU) == true }
+ }
+
+ if (bestOfferData != null) {
+ Box(modifier = modifier, contentAlignment = Alignment.Center) {
+ Card(
modifier =
- Modifier.width(32.dp)
- .height(4.dp)
- .background(
- color = NaviPayColor.borderAlt,
- shape = RoundedCornerShape(4.dp),
+ Modifier.shadow(
+ elevation = 16.dp,
+ ambientColor = adjustedShadowColor,
+ spotColor = adjustedShadowColor,
+ shape = RoundedCornerShape(20.dp),
)
- )
- }
- Spacer(modifier = Modifier.height(8.dp))
- StaticSearchBarView(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
- onSearchBarClicked = navigateToPayToContactsScreen,
- placeHolderString = stringResource(id = R.string.search_name_or_mobile),
- )
- if (frequentOrders.isNotEmpty()) {
- FrequentOrdersSection(
- modifier = Modifier.fillMaxWidth().background(color = NaviPayColor.bgDefault),
- frequentOrders = frequentOrders,
- onFrequentOrderSelected = onFrequentOrderSelected,
- showLoader = showLoader,
- clickedContactPhoneNumber = clickedContactPhoneNumber,
- frequentOrdersHeading = frequentOrdersHeading,
- )
- } else {
- Spacer(modifier = Modifier.height(30.dp))
- }
- }
- }
-}
-
-@Composable
-fun FrequentOrdersSection(
- modifier: Modifier,
- frequentOrders: List,
- onFrequentOrderSelected: (OrderEntity) -> Unit,
- showLoader: Boolean,
- clickedContactPhoneNumber: String,
- frequentOrdersHeading: String,
-) {
- Column(modifier = modifier.padding(bottom = 8.dp)) {
- Spacer(modifier = Modifier.height(16.dp))
- NaviText(
- text = frequentOrdersHeading,
- fontSize = 16.sp,
- fontFamily = naviFontFamily,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
- color = NaviPayColor.textPrimary,
- modifier = Modifier.padding(start = 16.dp).wrapContentSize(),
- )
-
- AnimatedVisibility(
- visible = frequentOrders.isNotEmpty(),
- enter = slideInVertically(animationSpec = tween(300)),
- exit = ExitTransition.None,
- ) {
- Row(
- modifier =
- Modifier.fillMaxWidth()
- .horizontalScroll(state = rememberScrollState())
- .padding(start = 8.dp, end = 4.dp)
- ) {
- frequentOrders.forEachIndexed { index, frequentOrderEntity ->
- FrequentOrderItem(
- index = index,
- frequentOrderEntity = frequentOrderEntity,
- onFrequentOrderSelected = onFrequentOrderSelected,
- showLoader = showLoader,
- clickedContactPhoneNumber = clickedContactPhoneNumber,
+ .wrapContentWidth()
+ .border(
+ width = 0.5.dp,
+ color = NaviPayColor.inputFieldDefault,
+ shape = RoundedCornerShape(20.dp),
+ ),
+ shape = RoundedCornerShape(20.dp),
+ backgroundColor = adjustedBgColor,
+ ) {
+ OfferStripView(
+ modifier =
+ Modifier.wrapContentWidth()
+ .padding(start = 10.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
+ bestOfferData = bestOfferData,
+ showLightVariant = isCameraPermissionGranted,
+ totalOffers = genericOffersList.size,
+ showOfferAppliedStripText = false,
+ onOffersStripClicked = onOffersStripClicked,
+ seperatorHeight = 16.dp,
)
- if (index != remember(frequentOrders) { frequentOrders.size } - 1) {
- Spacer(modifier = Modifier.width(8.dp))
- }
}
- Spacer(modifier = Modifier.width(8.dp))
}
}
}
}
-@Composable
-fun FrequentOrderItem(
- index: Int,
- frequentOrderEntity: OrderEntity,
- onFrequentOrderSelected: (OrderEntity) -> Unit,
- showLoader: Boolean,
- clickedContactPhoneNumber: String,
-) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier =
- Modifier.width(74.dp)
- .height(IntrinsicSize.Min)
- .clickableDebounce { onFrequentOrderSelected(frequentOrderEntity) }
- .padding(horizontal = 2.dp, vertical = 12.dp),
- ) {
- ContactIconView(
- index = index,
- contactInitials = frequentOrderEntity.orderTitle.contactInitials(),
- phoneNumber =
- getNormalisedPhoneNumber(
- phoneNumber =
- frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.mobNo
- ),
- vpa = frequentOrderEntity.orderDescription,
- )
-
- Spacer(modifier = Modifier.height(8.dp))
-
- if (
- clickedContactPhoneNumber ==
- frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.vpa && showLoader
- ) {
- NaviPayLottieAnimation(
- modifier = Modifier.size(24.dp),
- lottieFileName = NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE,
- )
- } else {
- NaviText(
- text =
- frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.name.orEmpty(),
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
- color = NaviPayColor.textPrimary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.wrapContentSize().padding(bottom = 8.dp),
- textAlign = TextAlign.Center,
- lineHeight = 18.sp,
- )
- }
- }
-}
-
@Composable
fun NavigationIconWithOtherAppsView(
modifier: Modifier = Modifier,
needsResult: Boolean,
onBackClick: () -> Unit,
cameraPermissionsGranted: Boolean,
+ genericOffersList: List,
+ onOffersStripClicked: () -> Unit,
) {
val navigationIconId =
remember(needsResult, cameraPermissionsGranted) {
@@ -750,6 +570,15 @@ fun NavigationIconWithOtherAppsView(
NaviPayQrScannerOtherAppView(modifier = Modifier.fillMaxWidth())
}
}
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Spacer(modifier = Modifier.height(96.dp))
+ RenderOffersStrip(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ isCameraPermissionGranted = cameraPermissionsGranted,
+ genericOffersList = genericOffersList,
+ onOffersStripClicked = onOffersStripClicked,
+ )
+ }
}
}
@@ -984,29 +813,11 @@ private fun NaviPayQrScannerOtherAppView(modifier: Modifier) {
@Composable
private fun QrScannerBottomSheetContent(
bottomSheetUIState: QrScannerBottomSheetUIState?,
- onInvalidVpaBottomSheetCtaClicked: () -> Unit,
+ closeSheet: () -> Unit,
onUrlRedirectionPrimaryCtaClicked: (String) -> Unit,
onUrlRedirectionSecondaryCtaClicked: () -> Unit,
) {
when (bottomSheetUIState) {
- QrScannerBottomSheetUIState.InvalidVpa -> {
- BottomSheetContentWithIconHeaderDescButton(
- iconId = CommonR.drawable.ic_exclamation_red_border,
- headerTextId = R.string.np_saved_beneficiary_invalid_vpa_header,
- descriptionTextId = R.string.np_saved_beneficiary_invalid_vpa_description,
- buttonTextId = R.string.np_okay_got_it,
- onButtonClicked = onInvalidVpaBottomSheetCtaClicked,
- )
- }
- QrScannerBottomSheetUIState.ContactNotLinked -> {
- BottomSheetContentWithIconHeaderDescButton(
- iconId = CommonR.drawable.ic_exclamation_red_border,
- headerTextId = R.string.np_phone_contact_not_linked_header,
- descriptionTextId = R.string.np_phone_contact_not_linked_description,
- buttonTextId = R.string.np_okay_got_it,
- onButtonClicked = onInvalidVpaBottomSheetCtaClicked,
- )
- }
is QrScannerBottomSheetUIState.UrlRedirection -> {
val descriptionText = buildAnnotatedString {
val descriptionInfo =
@@ -1060,6 +871,15 @@ private fun QrScannerBottomSheetContent(
onSecondaryButtonClicked = onUrlRedirectionSecondaryCtaClicked,
)
}
+
+ is QrScannerBottomSheetUIState.OfferList -> {
+ NaviPayOffersBottomSheet(
+ offerData = bottomSheetUIState.offerData,
+ closeSheet = closeSheet,
+ heading = bottomSheetUIState.heading,
+ )
+ }
+
else -> {
Spacer(modifier = Modifier.height(1.dp))
}
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 f64f79e5ff..2834d006ae 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
@@ -17,52 +17,33 @@ import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_AUTO_FLASH_DISABLED
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_AUTO_FLASH_LIGHT_LUX_THRESHOLD
-import com.navi.common.model.common.NudgeDetailEntity
-import com.navi.common.network.models.RepoResult
-import com.navi.common.network.models.isSuccessWithData
import com.navi.common.usecase.LitmusExperimentsUseCase
-import com.navi.common.usecase.RewardsNudgeEntityFetchUseCase
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_QR_SCANNER
-import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity
import com.navi.pay.common.model.config.NaviPayDefaultConfig
-import com.navi.pay.common.model.network.ValidateVpaRequest
-import com.navi.pay.common.model.network.ValidateVpaResponse
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.NaviPaySessionHelper
import com.navi.pay.common.usecase.NaviPayConfigUseCase
-import com.navi.pay.common.usecase.ValidateVpaUseCase
-import com.navi.pay.common.utils.DeviceInfoProvider
-import com.navi.pay.common.utils.FrequentOrdersHelper
import com.navi.pay.common.viewmodel.NaviPayBaseVM
-import com.navi.pay.destinations.SendMoneyScreenDestination
import com.navi.pay.entry.NaviPayActivityDataProvider
-import com.navi.pay.management.common.sendmoney.model.network.getTransactionInitiationType
-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.util.NaviPayOffersHelper
import com.navi.pay.management.moneytransfer.scanpay.LightSensorManager
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.util.UpiUriParser
-import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.pay.utils.DEFAULT_CONFIG
-import com.navi.pay.utils.INVALID_VPA
-import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER
+import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE
import com.navi.pay.utils.NAVI_PAY_WIDGET_CLICKED_KEY
-import com.navi.pay.utils.NOT_LINKED_TO_UPI
-import com.ramcosta.composedestinations.spec.Direction
+import com.navi.rr.common.models.OfferData
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -78,18 +59,14 @@ class QrScannerViewModel
@Inject
constructor(
private val upiUriParser: UpiUriParser,
- private val deviceInfoProvider: DeviceInfoProvider,
private val resourceProvider: ResourceProvider,
private val naviPayConfigUseCase: NaviPayConfigUseCase,
private val naviPaySessionHelper: NaviPaySessionHelper,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
- private val rewardsNudgeEntityFetchUseCase: RewardsNudgeEntityFetchUseCase,
- private val frequentOrdersHelper: FrequentOrdersHelper,
- private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity,
- private val validateVpaUseCase: ValidateVpaUseCase,
private val lightSensorManager: LightSensorManager,
private val naviPayActivityDataProvider: NaviPayActivityDataProvider,
private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
+ private val naviPayOffersHelper: NaviPayOffersHelper,
) : NaviPayBaseVM() {
private var naviPayDefaultConfig = NaviPayDefaultConfig()
@@ -107,9 +84,6 @@ constructor(
)
val bottomSheetStateHolder = _bottomSheetStateHolder.asStateFlow()
- private val _isFrequentOrderListUpdated = MutableSharedFlow()
- val isFrequentOrderListUpdated = _isFrequentOrderListUpdated.asSharedFlow()
-
private val _qrScanResult = MutableSharedFlow()
val qrScanResult = _qrScanResult.asSharedFlow()
@@ -119,49 +93,41 @@ constructor(
private val _isTorchEnabled = MutableLiveData(false)
val isTorchEnabled = _isTorchEnabled as LiveData
- private val _navigateToNextScreen = MutableSharedFlow()
- val navigateToNextScreen = _navigateToNextScreen.asSharedFlow()
-
var isQrCodeProcessing: AtomicBoolean = AtomicBoolean(false)
- private val _frequentOrders = MutableStateFlow(emptyList())
- val frequentOrders = _frequentOrders.asStateFlow()
-
- private val _showLoader = MutableStateFlow(false)
- val showLoader = _showLoader.asStateFlow()
-
- private val _clickedContactPhoneNumber = MutableStateFlow("")
- val clickedContactPhoneNumber = _clickedContactPhoneNumber.asStateFlow()
-
- private val frequentOrdersTotalEntries =
- MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionsTotalEntries)
-
- private val frequentOrdersDays =
- MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionsDays)
-
- private val _frequentOrdersHeading =
- MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionHeading)
-
- val frequentOrdersHeading = _frequentOrdersHeading.asStateFlow()
-
- private val _rewardsNudgeDetailEntity = MutableStateFlow(null)
- val rewardsNudgeDetailEntity = _rewardsNudgeDetailEntity.asStateFlow()
-
private var isAutomaticTorchEnabled = false
private val lightSensorLuxValue = MutableStateFlow(Float.MAX_VALUE)
- private val _isFrequentTransactionsEnabled = MutableStateFlow(false)
- val isFrequentTransactionsEnabled = _isFrequentTransactionsEnabled.asStateFlow()
+ private val _genericOffersList = MutableStateFlow>(emptyList())
+ val genericOffersList = _genericOffersList.asStateFlow()
init {
- fetchRewardsNudgeDetail()
+ getGenericOffersList()
updateNaviPaySessionId()
updateNaviPayDefaultConfig()
- updateFrequentOrdersList()
triggerEventForShortcutWidget()
}
+ private fun getGenericOffersList() {
+ viewModelScope.launch(Dispatchers.IO) {
+ val isOfferExperienceEnabled =
+ litmusExperimentsUseCase
+ .execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE)
+ ?.variant
+ ?.enabled ?: false
+
+ if (isOfferExperienceEnabled) {
+ val genericOffersList = naviPayOffersHelper.getCachedOffers(screenName = screenName)
+ naviPayAnalytics.onOffersCalloutLanded(
+ numberOfOffers = genericOffersList.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+ _genericOffersList.update { genericOffersList }
+ }
+ }
+ }
+
private fun triggerEventForShortcutWidget() {
viewModelScope.launch(Dispatchers.IO) {
naviPayActivityDataProvider
@@ -239,58 +205,38 @@ constructor(
type = object : TypeToken() {}.type,
screenName = screenName,
) ?: NaviPayDefaultConfig()
-
- updateFrequentOrderTotalEntries(
- naviPayDefaultConfig.config.frequentTransactionsTotalEntries
- )
- updateFrequentOrderDays(naviPayDefaultConfig.config.frequentTransactionsDays)
- updateFrequentOrderHeading(naviPayDefaultConfig.config.frequentTransactionHeading)
}
}
- private fun updateFrequentOrdersList() {
- viewModelScope.launch(coroutineDispatcherProvider.io) {
- val frequentOrderJobs = mutableListOf>()
+ fun onOffersStripClicked() {
+ naviPayAnalytics.onOffersCalloutClicked(
+ numberOfOffers = genericOffersList.value.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
- frequentOrderJobs.add(
- async {
- updateFrequentOrders(
- frequentOrdersHelper.getFrequentOrderList(
- frequentOrdersTotalEntries = frequentOrdersTotalEntries.value,
- frequentOrdersDays = frequentOrdersDays.value,
- )
- )
- }
- )
-
- frequentOrderJobs.add(
- async {
- // This experiment is for disabling frequent transactions in QR scanner &
- // default is enabled
- val isFrequentTransactionEnabledLitmusResponse =
- litmusExperimentsUseCase
- .execute(
- experimentName =
- LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER
- )
- ?.variant
- ?.enabled
- ?.not() ?: true
-
- _isFrequentTransactionsEnabled.update {
- isFrequentTransactionEnabledLitmusResponse
- }
- }
- )
-
- frequentOrderJobs.awaitAll()
-
- _isFrequentOrderListUpdated.emit(value = true)
- }
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetStateChange = true,
+ bottomSheetUIState =
+ QrScannerBottomSheetUIState.OfferList(
+ offerData = genericOffersList.value,
+ heading =
+ resourceProvider.getString(
+ resId = R.string.np_n_upi_payment_offers,
+ genericOffersList.value.size,
+ ),
+ ),
+ )
}
+ private fun isOffersBottomSheetVisible() =
+ bottomSheetStateHolder.value.bottomSheetUIState is QrScannerBottomSheetUIState.OfferList &&
+ bottomSheetStateHolder.value.showBottomSheet
+
fun processQrContent(qrContent: String, sendQrFromGallery: Boolean = false) {
- if (isQrCodeProcessing.get()) return
+ if (isQrCodeProcessing.get() || isOffersBottomSheetVisible()) {
+ return
+ }
isQrCodeProcessing.set(true)
viewModelScope.safeLaunch(coroutineDispatcherProvider.io) {
when {
@@ -353,18 +299,6 @@ constructor(
}
}
- private fun updateFrequentOrderTotalEntries(totalEntries: Int) {
- frequentOrdersTotalEntries.update { totalEntries }
- }
-
- private fun updateFrequentOrderDays(numberOfDays: Int) {
- frequentOrdersDays.update { numberOfDays }
- }
-
- private fun updateFrequentOrderHeading(heading: String) {
- _frequentOrdersHeading.update { heading }
- }
-
fun updateBottomSheetUIState(
showBottomSheet: Boolean,
bottomSheetStateChange: Boolean? = null,
@@ -406,22 +340,6 @@ constructor(
}
}
- private fun updateFrequentOrders(frequentOrders: List) {
- _frequentOrders.update { frequentOrders }
- }
-
- private fun updateShowLoader(showLoader: Boolean) {
- _showLoader.update { showLoader }
- }
-
- private fun updateClickedContactPhoneNumber(phoneNumber: String) {
- _clickedContactPhoneNumber.update { phoneNumber }
- }
-
- private suspend fun updateNextScreenDestinationState(direction: Direction) {
- _navigateToNextScreen.emit(direction)
- }
-
private fun parseBharatQr(qrContent: String, isQrFromUploadImage: Boolean) {
when (val upiResult = upiUriParser.getPayeeEntityFromBharatQr(qrContent = qrContent)) {
is UpiUriResult.Success -> {
@@ -449,109 +367,6 @@ constructor(
}
}
- fun initiatePayment(orderEntity: OrderEntity) {
- viewModelScope.launch(Dispatchers.IO) {
- if (!naviPayNetworkConnectivity.isInternetConnected()) {
- notifyError(getNoInternetErrorConfig())
- return@launch
- }
-
- updateShowLoader(showLoader = true)
- updateClickedContactPhoneNumber(
- phoneNumber = orderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.vpa.orEmpty()
- )
- val response =
- validateVpaUseCase.execute(
- request =
- ValidateVpaRequest(
- deviceData = deviceInfoProvider.getDeviceData(),
- vpa =
- orderEntity.naviPayTransactionDetailsMetadata.payeeInfo
- ?.vpa
- .orEmpty(),
- merchantCustomerId = deviceInfoProvider.getMerchantCustomerId(),
- ),
- screenName = screenName,
- )
-
- if (response.isSuccessWithData()) {
- handleVpaCheckSuccessForInitiatingPayment(
- response = response,
- orderEntity = orderEntity,
- )
- } else {
- handleVpaCheckFailureForInitiatingPayment(response = response)
- }
- }
- }
-
- private suspend fun handleVpaCheckFailureForInitiatingPayment(
- response: RepoResult
- ) {
- updateShowLoader(showLoader = false)
- updateClickedContactPhoneNumber(phoneNumber = "")
- when (response.errors?.getOrNull(index = 0)?.code) {
- INVALID_VPA -> {
- updateBottomSheetUIState(
- showBottomSheet = true,
- bottomSheetStateChange = true,
- bottomSheetUIState = QrScannerBottomSheetUIState.InvalidVpa,
- )
- }
- NOT_LINKED_TO_UPI -> {
- updateBottomSheetUIState(
- showBottomSheet = true,
- bottomSheetStateChange = true,
- bottomSheetUIState = QrScannerBottomSheetUIState.ContactNotLinked,
- )
- }
- else -> {
- notifyError(response = response)
- }
- }
- }
-
- private suspend fun handleVpaCheckSuccessForInitiatingPayment(
- response: RepoResult,
- orderEntity: OrderEntity,
- ) {
- val validateVpa = response.data!!
- val payeeEntity =
- PayeeEntity(
- name = validateVpa.name,
- vpa = validateVpa.vpa,
- isMerchant = validateVpa.isMerchant,
- isVerifiedVpa = true,
- mcc = validateVpa.mcc,
- severity = validateVpa.riskParams!!.severity,
- bankCode = validateVpa.bankCode,
- phoneNumber = orderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.mobNo,
- bankIfsc = orderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.bAccIfsc,
- bankAccountUniqueId =
- orderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.bAccUnqId,
- upiNumber = orderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.upiNumber,
- isNpciData = validateVpa.isNpciData,
- featureTags = validateVpa.featureTags,
- )
- val sendMoneyScreenSource =
- SendMoneyScreenSource.QrScan(
- payeeVpaToDisplay = orderEntity.orderDescription,
- transactionInitiationMode =
- getTransactionInitiationType(
- transactionInitiationType =
- orderEntity.naviPayTransactionDetailsMetadata.paymentMode
- ),
- )
- updateNextScreenDestinationState(
- SendMoneyScreenDestination(
- payeeEntity = payeeEntity,
- sendMoneyScreenSource = sendMoneyScreenSource,
- )
- )
- updateShowLoader(showLoader = false)
- updateClickedContactPhoneNumber(phoneNumber = "")
- }
-
fun toggleTorchStatus(isTorchButtonClicked: Boolean = true) {
naviPayAnalytics.onTorchStatusToggle(
lightSensorLuxValue = lightSensorLuxValue.value,
@@ -575,12 +390,6 @@ constructor(
_qrImageErrorViewEnable.update { qrImageErrorViewEnable }
}
- private fun fetchRewardsNudgeDetail() {
- viewModelScope.launch(Dispatchers.IO) {
- _rewardsNudgeDetailEntity.update { rewardsNudgeEntityFetchUseCase.execute() }
- }
- }
-
private fun isQrContentOfTypeUrl(qrContent: String): Boolean {
return Patterns.WEB_URL.matcher(qrContent).matches()
}
@@ -599,9 +408,8 @@ constructor(
}
sealed class QrScannerBottomSheetUIState {
- data object InvalidVpa : QrScannerBottomSheetUIState()
-
- data object ContactNotLinked : QrScannerBottomSheetUIState()
-
data class UrlRedirection(val url: String) : QrScannerBottomSheetUIState()
+
+ data class OfferList(val offerData: List, val heading: String) :
+ QrScannerBottomSheetUIState()
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt
index 32eed29228..27e8c4c307 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt
@@ -10,9 +10,15 @@
package com.navi.pay.management.paytocontacts.ui
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
@@ -25,27 +31,31 @@ 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.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Scaffold
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Surface
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -55,33 +65,34 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.navi.common.R as CommonR
-import com.navi.common.model.common.NudgeDetailEntity
import com.navi.common.utils.navigateUp
import com.navi.design.common.NaviVerticalGrid
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
import com.navi.design.utils.maxScrollFlingBehavior
+import com.navi.naviwidgets.R as widgetsR
import com.navi.naviwidgets.extensions.NaviText
+import com.navi.naviwidgets.extensions.isAtTopPage
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.model.view.NaviPayScreenType
import com.navi.pay.common.model.view.NaviPermissionResult
import com.navi.pay.common.model.view.rememberMultiplePermissions
+import com.navi.pay.common.setup.NaviPayRouter
import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderDescButton
import com.navi.pay.common.ui.ContactIconView
import com.navi.pay.common.ui.EmptyDataScreen
-import com.navi.pay.common.ui.IconWithTitleDescriptionButton
import com.navi.pay.common.ui.ImageTitleDescriptionShimmerView
import com.navi.pay.common.ui.InputTextFieldWithDescriptionHeader
import com.navi.pay.common.ui.LoadingScreen
import com.navi.pay.common.ui.NaviPayHeader
import com.navi.pay.common.ui.NaviPayLottieAnimation
import com.navi.pay.common.ui.NaviPayModalBottomSheet
+import com.navi.pay.common.ui.NaviPayOffersBottomSheet
import com.navi.pay.common.ui.NaviPaySponsorView
-import com.navi.pay.common.ui.RewardsNudgeWithBg
-import com.navi.pay.common.ui.StaticSearchBarView
+import com.navi.pay.common.ui.ShadowStrip
import com.navi.pay.common.utils.NaviPayCommonUtils
import com.navi.pay.common.utils.NaviPayCommonUtils.getNormalisedPhoneNumber
import com.navi.pay.entry.NaviPayActivity
@@ -98,6 +109,9 @@ import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE
import com.navi.pay.utils.contactInitials
import com.navi.pay.utils.customHide
import com.navi.pay.utils.launchPermissionSettingsScreen
+import com.navi.pay.utils.noRippleClickable
+import com.navi.rr.common.models.OfferData
+import com.navi.rr.common.ui.OffersRolodex
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
@@ -111,7 +125,6 @@ fun PayToContactsScreen(
navigator: DestinationsNavigator,
naviPayAnalytics: NaviPayAnalytics.NaviPayToContacts =
NaviPayAnalytics.INSTANCE.NaviPayToContacts(),
- openSearchState: Boolean = false,
) {
val context = LocalContext.current
@@ -125,8 +138,6 @@ fun PayToContactsScreen(
payToContactsViewModel.isNewContactVisible.collectAsStateWithLifecycle()
val filteredFrequentOrdersList by
payToContactsViewModel.filteredFrequentOrdersList.collectAsStateWithLifecycle()
- val rewardsNudgeDetailEntity by
- payToContactsViewModel.rewardsNudgeDetailEntity.collectAsStateWithLifecycle()
val showLoader by payToContactsViewModel.showLoader.collectAsStateWithLifecycle()
val showShimmer by payToContactsViewModel.showShimmer.collectAsStateWithLifecycle()
val hideKeyboard by payToContactsViewModel.hideKeyboard.collectAsStateWithLifecycle()
@@ -139,26 +150,17 @@ fun PayToContactsScreen(
payToContactsViewModel.frequentOrdersHeading.collectAsStateWithLifecycle()
val shouldShowYourContactsTitle by
payToContactsViewModel.shouldShowYourContactsTitle.collectAsStateWithLifecycle()
+ val genericOffersList by payToContactsViewModel.genericOffersList.collectAsStateWithLifecycle()
val onBackClick = {
- if (uiState == PayToContactsUIState.Search) {
- payToContactsViewModel.cancelPaymentRequest()
- if (openSearchState) {
- navigator.navigateUp()
- } else {
- payToContactsViewModel.updateUiState(uiState = PayToContactsUIState.Loaded)
- payToContactsViewModel.updateSearchQueryStringState(searchQuery = "")
- }
- } else {
- navigator.navigateUp(naviPayActivity)
- }
- Unit
+ payToContactsViewModel.cancelPaymentRequest()
+ payToContactsViewModel.updateSearchQueryStringState(searchQuery = "")
+ navigator.navigateUp(naviPayActivity)
}
LaunchedEffect(Unit) {
naviPayAnalytics.onSendToContactsLanded(
- rewardNudgeShown = if (rewardsNudgeDetailEntity != null) "Y" else "N",
- naviPaySessionAttributes = payToContactsViewModel.getNaviPaySessionAttributes(),
+ naviPaySessionAttributes = payToContactsViewModel.getNaviPaySessionAttributes()
)
}
@@ -188,6 +190,12 @@ fun PayToContactsScreen(
}
}
+ LaunchedEffect(Unit) {
+ payToContactsViewModel.navigateToNextScreenFromHelpCta.collect {
+ it?.let { NaviPayRouter.onCtaClick(naviPayActivity = naviPayActivity, ctaData = it) }
+ }
+ }
+
BackHandler { onBackClick() }
val scope = rememberCoroutineScope()
@@ -250,19 +258,6 @@ fun PayToContactsScreen(
readContactsPermissionsState.launchMultiplePermissionRequest()
}
- val onSearchBarBackClicked = {
- payToContactsViewModel.cancelPaymentRequest()
- keyboardController.customHide(context = context, view = view)
- focusManager.clearFocus()
- if (openSearchState) {
- navigator.navigateUp()
- } else {
- payToContactsViewModel.updateUiState(uiState = PayToContactsUIState.Loaded)
- payToContactsViewModel.updateSearchQueryStringState(searchQuery = "")
- }
- Unit
- }
-
val onNewContactSelected: (PhoneContactEntity) -> Unit = {
keyboardController?.customHide(context = context, view = view)
focusManager.clearFocus()
@@ -299,6 +294,12 @@ fun PayToContactsScreen(
)
}
+ val onOffersRolodexClicked = {
+ keyboardController?.customHide(context = context, view = view)
+ focusManager.clearFocus()
+ payToContactsViewModel.onOffersRolodexClicked()
+ }
+
val isContactListNonEmpty by
payToContactsViewModel.isSavedContactListNonEmpty.collectAsStateWithLifecycle()
@@ -307,7 +308,6 @@ fun PayToContactsScreen(
payToContactsViewModel.fetchContacts()
} else {
naviPayAnalytics.onSendToContactsLoaded(
- rewardNudgeShown = if (rewardsNudgeDetailEntity != null) "Y" else "N",
naviPaySessionAttributes = payToContactsViewModel.getNaviPaySessionAttributes(),
isPermissionGranted = false,
isContactListEmpty = contactList.isEmpty(),
@@ -325,7 +325,7 @@ fun PayToContactsScreen(
bottomSheetContent = {
PayToContactsBottomSheetContent(
bottomSheetUIState = bottomSheetStateHolder.bottomSheetUIState,
- onInvalidVpaBottomSheetCtaClicked = onDismissBottomSheet,
+ onDismissBottomSheet = onDismissBottomSheet,
)
},
)
@@ -342,16 +342,10 @@ fun PayToContactsScreen(
onContactSelected = onContactSelected,
onNewContactSelected = onNewContactSelected,
onBackClick = onBackClick,
- onSearchBarClicked = {
- if (!showLoader) {
- payToContactsViewModel.updateUiState(uiState = PayToContactsUIState.Search)
- }
- },
frequentOrders = filteredFrequentOrdersList,
frequentOrderMaxColumns = payToContactsViewModel.getFrequentOrdersMaxColumns(),
frequentOrdersHeading = frequentOrdersHeading,
onFrequentOrderSelected = onFrequentOrderSelected,
- rewardsNudgeDetailEntity = rewardsNudgeDetailEntity,
areAllPermissionsGranted = readContactsPermissionsState.allPermissionsGranted,
onPrimaryButtonClicked = onAllowPermissionButtonClicked,
hasNonZeroContacts = isContactListNonEmpty,
@@ -360,67 +354,55 @@ fun PayToContactsScreen(
showShimmer = showShimmer,
newContact = newContact,
shouldShowYourContactsTitle = shouldShowYourContactsTitle,
- )
- PayToContactsUIState.Search ->
- RenderPayToContactsSearchScreen(
- searchQuery = searchQuery,
- onSearchInputValueChange = {
- payToContactsViewModel.updateSearchQueryStringState(searchQuery = it)
- },
- frequentOrders = filteredFrequentOrdersList,
+ onSearchInputValueChange = payToContactsViewModel::updateSearchQueryStringState,
onTrailingIconClicked = onTrailingIconClicked,
- onSearchBarBackClicked = onSearchBarBackClicked,
- contactList = contactList,
- onContactSelected = onContactSelected,
- onNewContactSelected = onNewContactSelected,
- isNewContactVisible = isNewContactVisible,
- onPrimaryButtonClicked = onAllowPermissionButtonClicked,
- onFrequentOrderSelected = onFrequentOrderSelected,
- frequentOrderMaxColumns = payToContactsViewModel.getFrequentOrdersMaxColumns(),
- frequentOrdersHeading = frequentOrdersHeading,
- areAllPermissionsGranted = readContactsPermissionsState.allPermissionsGranted,
- hasNonZeroContacts = isContactListNonEmpty,
- showLoader = showLoader,
- clickedContactPhoneNumber = clickedContactPhoneNumber,
- showShimmer = showShimmer,
- newContact = newContact,
invalidState = invalidState,
isEmptyState = isEmptyState,
- shouldShowYourContactsTitle = shouldShowYourContactsTitle,
+ genericOffersList = genericOffersList,
+ onOffersRolodexClicked = onOffersRolodexClicked,
+ onHelpCtaClicked = {
+ keyboardController?.customHide(context = context, view = view)
+ focusManager.clearFocus()
+ payToContactsViewModel.onHelpCtaClicked()
+ },
)
}
}
@Composable
-fun RenderPayToContactsSearchScreen(
+fun RenderPayToContactsScreen(
searchQuery: String,
- onSearchInputValueChange: (String) -> Unit,
- frequentOrders: List,
- onTrailingIconClicked: () -> Unit,
- onSearchBarBackClicked: () -> Unit,
+ isNewContactVisible: Boolean,
contactList: List,
onContactSelected: (PhoneContactEntity) -> Unit,
onNewContactSelected: (PhoneContactEntity) -> Unit,
- isNewContactVisible: Boolean,
- onPrimaryButtonClicked: () -> Unit,
- onFrequentOrderSelected: (OrderEntity) -> Unit,
+ onBackClick: () -> Unit,
+ frequentOrders: List,
frequentOrderMaxColumns: Int,
frequentOrdersHeading: String,
+ onFrequentOrderSelected: (OrderEntity) -> Unit,
areAllPermissionsGranted: Boolean,
+ onPrimaryButtonClicked: () -> Unit,
hasNonZeroContacts: Boolean,
showLoader: Boolean,
clickedContactPhoneNumber: String,
showShimmer: Boolean = false,
newContact: PhoneContactEntity?,
- invalidState: WarningErrorInfoState,
- isEmptyState: Boolean = false,
shouldShowYourContactsTitle: Boolean,
+ onSearchInputValueChange: (String) -> Unit,
+ onTrailingIconClicked: () -> Unit,
+ invalidState: WarningErrorInfoState,
+ isEmptyState: Boolean,
+ genericOffersList: List,
+ onOffersRolodexClicked: () -> Unit,
+ onHelpCtaClicked: () -> Unit,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val view = LocalView.current
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }
+ val scrollState = rememberLazyListState()
Scaffold(
modifier =
@@ -435,8 +417,11 @@ fun RenderPayToContactsSearchScreen(
),
topBar = {
Column(modifier = Modifier.fillMaxWidth()) {
- Spacer(modifier = Modifier.height(16.dp))
-
+ PayToContactsScreenHeader(
+ navigationIcon = CommonR.drawable.ic_arrow_left_black_v2,
+ onBackClick = onBackClick,
+ onHelpCtaClicked = onHelpCtaClicked,
+ )
InputTextFieldWithDescriptionHeader(
modifier =
Modifier.fillMaxWidth()
@@ -444,22 +429,18 @@ fun RenderPayToContactsSearchScreen(
.focusRequester(focusRequester)
.clickable(!showLoader) {},
headerString = null,
- placeHolderString = "",
- leadingIconId = CommonR.drawable.ic_arrow_left_black_v2,
- onLeadingIconClicked = onSearchBarBackClicked,
+ placeHolderString = stringResource(id = R.string.search_name_or_mobile),
+ leadingIconId = CommonR.drawable.ic_search,
+ onLeadingIconClicked = {},
value = searchQuery,
onValueChangeListener = onSearchInputValueChange,
isTrailingIconEnabled = searchQuery.isNotEmpty(),
onTrailingIconClicked = onTrailingIconClicked,
isLeadingIconEnabled = true,
warningErrorInfoState = invalidState.isErrorState,
+ enabled = !showLoader,
)
- LaunchedEffect(Unit) {
- focusRequester.requestFocus()
- keyboardController?.show()
- }
-
Spacer(modifier = Modifier.height(8.dp))
if (invalidState.isWarningState) {
@@ -483,6 +464,34 @@ fun RenderPayToContactsSearchScreen(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
)
}
+
+ OffersRolodex(
+ modifier = Modifier.fillMaxWidth().padding(all = 16.dp),
+ offerData = genericOffersList,
+ shouldShowRolodexAnimation = genericOffersList.size > 1,
+ onClick = onOffersRolodexClicked,
+ )
+
+ AnimatedVisibility(
+ visible = !scrollState.isAtTopPage(),
+ enter = expandVertically(),
+ exit = shrinkVertically(),
+ ) {
+ ShadowStrip(
+ modifier = Modifier.fillMaxWidth(),
+ brush =
+ Brush.verticalGradient(
+ colors =
+ listOf(
+ NaviPayColor.lightGray.copy(alpha = 0.18f),
+ NaviPayColor.lightGray.copy(alpha = 0.01f),
+ NaviPayColor.ctaWhite,
+ ),
+ startY = 0f,
+ endY = 100f,
+ ),
+ )
+ }
}
},
content = {
@@ -509,98 +518,25 @@ fun RenderPayToContactsSearchScreen(
frequentOrdersHeading = frequentOrdersHeading,
areAllPermissionsGranted = areAllPermissionsGranted,
onPrimaryButtonClicked = onPrimaryButtonClicked,
- isSearchState = true,
- navigationIcon = 0,
- onBackClick = {},
- rewardsNudgeDetailEntity = null,
- onSearchBarClicked = {},
+ isSearchState = searchQuery.isNotEmpty(),
hasNonZeroContacts = hasNonZeroContacts,
showLoader = showLoader,
clickedContactPhoneNumber = clickedContactPhoneNumber,
showShimmer = showShimmer,
newContact = newContact,
shouldShowYourContactsTitle = shouldShowYourContactsTitle,
+ scrollState = scrollState,
)
}
},
)
}
-@Composable
-fun RenderPayToContactsScreen(
- searchQuery: String,
- isNewContactVisible: Boolean,
- contactList: List,
- onContactSelected: (PhoneContactEntity) -> Unit,
- onNewContactSelected: (PhoneContactEntity) -> Unit,
- onBackClick: () -> Unit,
- onSearchBarClicked: () -> Unit,
- frequentOrders: List,
- frequentOrderMaxColumns: Int,
- frequentOrdersHeading: String,
- onFrequentOrderSelected: (OrderEntity) -> Unit,
- rewardsNudgeDetailEntity: NudgeDetailEntity?,
- areAllPermissionsGranted: Boolean,
- onPrimaryButtonClicked: () -> Unit,
- hasNonZeroContacts: Boolean,
- showLoader: Boolean,
- clickedContactPhoneNumber: String,
- showShimmer: Boolean = false,
- newContact: PhoneContactEntity?,
- shouldShowYourContactsTitle: Boolean,
-) {
-
- val scope = rememberCoroutineScope()
- val context = LocalContext.current
- val view = LocalView.current
- val keyboardController = LocalSoftwareKeyboardController.current
-
- Scaffold(
- modifier =
- Modifier.fillMaxSize()
- .nestedScroll(
- NaviPayCommonUtils.closeKeyboardOnScroll(
- scope = scope,
- context = context,
- view = view,
- keyboardController = keyboardController,
- )
- ),
- content = {
- PayToContactScreenScaffoldContent(
- modifier = Modifier.padding(it).fillMaxWidth(),
- searchQuery = searchQuery,
- frequentOrders = frequentOrders,
- isNewContactVisible = isNewContactVisible,
- contactList = contactList,
- onContactSelected = onContactSelected,
- onNewContactSelected = onNewContactSelected,
- onFrequentOrderSelected = onFrequentOrderSelected,
- frequentOrderMaxColumns = frequentOrderMaxColumns,
- frequentOrdersHeading = frequentOrdersHeading,
- areAllPermissionsGranted = areAllPermissionsGranted,
- onPrimaryButtonClicked = onPrimaryButtonClicked,
- isSearchState = false,
- navigationIcon = CommonR.drawable.ic_arrow_left_black_v2,
- onBackClick = onBackClick,
- rewardsNudgeDetailEntity = rewardsNudgeDetailEntity,
- onSearchBarClicked = onSearchBarClicked,
- hasNonZeroContacts = hasNonZeroContacts,
- showLoader = showLoader,
- clickedContactPhoneNumber = clickedContactPhoneNumber,
- showShimmer = showShimmer,
- newContact = newContact,
- shouldShowYourContactsTitle = shouldShowYourContactsTitle,
- )
- },
- )
-}
-
@Composable
private fun PayToContactsScreenHeader(
navigationIcon: Int,
onBackClick: () -> Unit,
- rewardsNudgeDetailEntity: NudgeDetailEntity?,
+ onHelpCtaClicked: () -> Unit,
) {
Column(modifier = Modifier.fillMaxWidth()) {
NaviPayHeader(
@@ -608,17 +544,12 @@ private fun PayToContactsScreenHeader(
title = stringResource(R.string.send_money_to_contacts),
onNavigationIconClick = onBackClick,
modifier = Modifier.fillMaxWidth(),
+ actionIconText = stringResource(R.string.help),
+ onActionClick = onHelpCtaClicked,
)
-
- rewardsNudgeDetailEntity?.let {
- RewardsNudgeWithBg(modifier = Modifier.fillMaxWidth(), nudgeDetailEntity = it)
-
- Spacer(modifier = Modifier.height(16.dp))
- }
}
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PayToContactScreenScaffoldContent(
modifier: Modifier,
@@ -634,39 +565,19 @@ private fun PayToContactScreenScaffoldContent(
areAllPermissionsGranted: Boolean,
onPrimaryButtonClicked: () -> Unit,
isSearchState: Boolean,
- navigationIcon: Int,
- onBackClick: () -> Unit,
- rewardsNudgeDetailEntity: NudgeDetailEntity?,
- onSearchBarClicked: () -> Unit,
hasNonZeroContacts: Boolean,
showLoader: Boolean,
clickedContactPhoneNumber: String,
showShimmer: Boolean = false,
newContact: PhoneContactEntity?,
shouldShowYourContactsTitle: Boolean,
+ scrollState: LazyListState,
) {
- LazyColumn(modifier = modifier.fillMaxHeight(), flingBehavior = maxScrollFlingBehavior()) {
- if (!isSearchState) {
- item {
- PayToContactsScreenHeader(
- navigationIcon = navigationIcon,
- onBackClick = onBackClick,
- rewardsNudgeDetailEntity = rewardsNudgeDetailEntity,
- )
- }
- stickyHeader {
- Surface(color = NaviPayColor.bgDefault) {
- Spacer(modifier = Modifier.height(8.dp))
- StaticSearchBarView(
- modifier =
- Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
- onSearchBarClicked = onSearchBarClicked,
- placeHolderString = stringResource(id = R.string.search_name_or_mobile),
- )
- Spacer(modifier = Modifier.height(8.dp))
- }
- }
- }
+ LazyColumn(
+ modifier = modifier.fillMaxHeight(),
+ flingBehavior = maxScrollFlingBehavior(),
+ state = scrollState,
+ ) {
item { Spacer(modifier = Modifier.height(16.dp)) }
if (frequentOrders.isNotEmpty()) {
@@ -708,7 +619,7 @@ private fun PayToContactScreenScaffoldContent(
text = stringResource(id = R.string.your_contacts),
fontFamily = naviFontFamily,
fontSize = 16.sp,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
color = NaviPayColor.textPrimary,
)
@@ -771,7 +682,7 @@ private fun FrequentOrdersSection(
text = frequentOrdersHeading,
fontFamily = naviFontFamily,
fontSize = 16.sp,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
color = NaviPayColor.textPrimary,
)
@@ -1022,28 +933,57 @@ fun PayToContactsPermissionView(onPrimaryButtonClicked: () -> Unit) {
text = stringResource(id = R.string.your_contacts),
fontFamily = naviFontFamily,
fontSize = 16.sp,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
color = NaviPayColor.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
- IconWithTitleDescriptionButton(
- modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp).wrapContentSize(),
- iconId = CommonR.drawable.ic_phone_book_permission,
- iconSize = 72.dp,
- titleId = R.string.contact_permission_title,
- descriptionText = stringResource(R.string.contact_permission_des),
- buttonTextId = R.string.allow_permission,
- onPrimaryButtonClicked = onPrimaryButtonClicked,
- )
+ Box(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ .background(
+ color = NaviPayColor.bgNonEditable,
+ shape = RoundedCornerShape(size = 4.dp),
+ )
+ .border(
+ width = 1.dp,
+ color = NaviPayColor.borderDefault,
+ shape = RoundedCornerShape(size = 4.dp),
+ )
+ .noRippleClickable { onPrimaryButtonClicked() }
+ ) {
+ Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
+ Image(
+ modifier = Modifier.size(32.dp),
+ painter = painterResource(id = CommonR.drawable.ic_phone_book_permission),
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ NaviText(
+ text = stringResource(id = R.string.np_contact_permission_title),
+ color = NaviPayColor.textPrimary,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
+ modifier = Modifier.weight(1f),
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Image(
+ modifier = Modifier.size(24.dp).align(Alignment.CenterVertically),
+ painter = painterResource(id = widgetsR.drawable.chevron_icon_black),
+ contentDescription = null,
+ )
+ }
+ }
}
}
@Composable
private fun PayToContactsBottomSheetContent(
bottomSheetUIState: PayToContactsBottomSheetUIState?,
- onInvalidVpaBottomSheetCtaClicked: () -> Unit,
+ onDismissBottomSheet: () -> Unit,
) {
when (bottomSheetUIState) {
PayToContactsBottomSheetUIState.InvalidVpa -> {
@@ -1052,7 +992,7 @@ private fun PayToContactsBottomSheetContent(
headerTextId = R.string.np_saved_beneficiary_invalid_vpa_header,
descriptionTextId = R.string.np_saved_beneficiary_invalid_vpa_description,
buttonTextId = R.string.np_okay_got_it,
- onButtonClicked = onInvalidVpaBottomSheetCtaClicked,
+ onButtonClicked = onDismissBottomSheet,
)
}
PayToContactsBottomSheetUIState.ContactNotLinked -> {
@@ -1061,7 +1001,14 @@ private fun PayToContactsBottomSheetContent(
headerTextId = R.string.np_phone_contact_not_linked_header,
descriptionTextId = R.string.np_phone_contact_not_linked_description,
buttonTextId = R.string.np_okay_got_it,
- onButtonClicked = onInvalidVpaBottomSheetCtaClicked,
+ onButtonClicked = onDismissBottomSheet,
+ )
+ }
+ is PayToContactsBottomSheetUIState.OfferList -> {
+ NaviPayOffersBottomSheet(
+ offerData = bottomSheetUIState.offerData,
+ closeSheet = onDismissBottomSheet,
+ heading = bottomSheetUIState.heading,
)
}
else -> {}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt
index 01010e3824..9085b24b92 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt
@@ -7,16 +7,15 @@
package com.navi.pay.management.paytocontacts.viewmodel
-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.ResourceProvider
import com.navi.common.constants.EMPTY
import com.navi.common.extensions.removeSpaces
-import com.navi.common.model.common.NudgeDetailEntity
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
-import com.navi.common.usecase.RewardsNudgeEntityFetchUseCase
+import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_SEND_MONEY_TO_CONTACTS_SCREEN_V2
@@ -30,6 +29,7 @@ import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.ValidateVpaUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
import com.navi.pay.common.utils.FrequentOrdersHelper
+import com.navi.pay.common.utils.NaviPayCommonUtils.getHelpCtaData
import com.navi.pay.common.utils.NaviPayCommonUtils.getNormalisedPhoneNumber
import com.navi.pay.common.utils.getMetricInfo
import com.navi.pay.common.viewmodel.NaviPayBaseVM
@@ -39,6 +39,7 @@ import com.navi.pay.management.common.sendmoney.model.network.TransactionInitiat
import com.navi.pay.management.common.sendmoney.model.network.getTransactionInitiationType
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.util.NaviPayOffersHelper
import com.navi.pay.management.paytocontacts.PhoneContactManager
import com.navi.pay.management.paytocontacts.model.network.PayToContactRequest
import com.navi.pay.management.paytocontacts.model.view.PayToContactsBottomSheetStateHolder
@@ -47,11 +48,13 @@ import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS
import com.navi.pay.utils.INVALID_VPA
+import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE
import com.navi.pay.utils.NAVI_PAY_SEARCH_QUERY_API_DELAY
import com.navi.pay.utils.NOT_LINKED_TO_UPI
import com.navi.pay.utils.PHONE_NUMBER_LENGTH
import com.navi.pay.utils.isValidPhoneNumberLength
import com.navi.pay.utils.isValidSearchQuery
+import com.navi.rr.common.models.OfferData
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -80,12 +83,12 @@ constructor(
private val contactManager: PhoneContactManager,
private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity,
private val naviPayConfigUseCase: NaviPayConfigUseCase,
- private val rewardsNudgeEntityFetchUseCase: RewardsNudgeEntityFetchUseCase,
private val naviPaySessionHelper: NaviPaySessionHelper,
private val frequentOrdersHelper: FrequentOrdersHelper,
private val resourceProvider: ResourceProvider,
private val validateVpaUseCase: ValidateVpaUseCase,
- savedStateHandle: SavedStateHandle,
+ private val naviPayOffersHelper: NaviPayOffersHelper,
+ private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
) : NaviPayBaseVM() {
private var naviPayDefaultConfig = NaviPayDefaultConfig()
@@ -96,8 +99,6 @@ constructor(
private val _uiState = MutableStateFlow(PayToContactsUIState.Loading)
val uiState = _uiState.asStateFlow()
- private var shouldOpenSearchState = savedStateHandle.get("openSearchState") ?: false
-
private val _searchQuery = MutableStateFlow("")
val searchQuery = _searchQuery.asStateFlow()
@@ -137,6 +138,12 @@ constructor(
private val _isNewContactVisible = MutableStateFlow(false)
val isNewContactVisible = _isNewContactVisible.asStateFlow()
+ private val _genericOffersList = MutableStateFlow>(emptyList())
+ val genericOffersList = _genericOffersList.asStateFlow()
+
+ private val _navigateToNextScreenFromHelpCta = MutableSharedFlow()
+ val navigateToNextScreenFromHelpCta = _navigateToNextScreenFromHelpCta.asSharedFlow()
+
private val _bottomSheetStateHolder =
MutableStateFlow(
PayToContactsBottomSheetStateHolder(
@@ -150,9 +157,6 @@ constructor(
// TODO: Make it private after using proper testing framework
val allContactList = MutableStateFlow>(emptyList())
- private val _rewardsNudgeDetailEntity = MutableStateFlow(null)
- val rewardsNudgeDetailEntity = _rewardsNudgeDetailEntity.asStateFlow()
-
private val frequentOrdersTotalEntries =
MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionsTotalEntries)
@@ -171,6 +175,8 @@ constructor(
private var lastSearchQuery = ""
+ private val helpCtaData = getHelpCtaData(screenName = screenName)
+
val filteredContactList =
combine(searchQuery, allContactList) { searchQuery, allContactList ->
apiCallJob?.cancel()
@@ -246,13 +252,32 @@ constructor(
)
init {
+ getGenericOffersList()
updateNaviPaySessionId()
updateNaviPayDefaultConfig()
- updateRewardsNudgeEntity()
fetchFrequentOrders()
observeAndHandleSearchResults()
}
+ private fun getGenericOffersList() {
+ viewModelScope.launch(Dispatchers.IO) {
+ val isOfferExperienceEnabled =
+ litmusExperimentsUseCase
+ .execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE)
+ ?.variant
+ ?.enabled ?: false
+
+ if (isOfferExperienceEnabled) {
+ val genericOffersList = naviPayOffersHelper.getCachedOffers(screenName = screenName)
+ naviPayAnalytics.onOffersCalloutLanded(
+ numberOfOffers = genericOffersList.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+ _genericOffersList.update { genericOffersList }
+ }
+ }
+ }
+
private fun observeAndHandleSearchResults() {
viewModelScope.launch(Dispatchers.IO) {
combine(searchQuery, filteredContactList, filteredFrequentOrdersList) {
@@ -336,7 +361,6 @@ constructor(
}
fun updateUiState(uiState: PayToContactsUIState) {
- shouldOpenSearchState = uiState == PayToContactsUIState.Search
_uiState.update { uiState }
}
@@ -594,7 +618,7 @@ constructor(
frequentOrdersDays = frequentOrdersDays.value,
)
updateFrequentOrderEntityList(getFrequentOrderList)
- updateUiAsPerSource()
+ updateUiState(uiState = PayToContactsUIState.Loaded)
}
}
@@ -604,7 +628,6 @@ constructor(
allContactList.update { savedContactListJob.await() }
_isSavedContactListNonEmpty.update { allContactList.value.isNotEmpty() }
naviPayAnalytics.onSendToContactsLoaded(
- rewardNudgeShown = if (rewardsNudgeDetailEntity.value != null) "Y" else "N",
naviPaySessionAttributes = getNaviPaySessionAttributes(),
isPermissionGranted = true,
isContactListEmpty = allContactList.value.isEmpty(),
@@ -613,24 +636,10 @@ constructor(
}
}
- private fun updateUiAsPerSource() {
- if (shouldOpenSearchState) {
- updateUiState(uiState = PayToContactsUIState.Search)
- } else {
- updateUiState(uiState = PayToContactsUIState.Loaded)
- }
- }
-
private fun updateFrequentOrderEntityList(orderEntityList: List) {
frequentOrders.update { orderEntityList }
}
- private fun updateRewardsNudgeEntity() {
- viewModelScope.launch(Dispatchers.IO) {
- _rewardsNudgeDetailEntity.update { rewardsNudgeEntityFetchUseCase.execute() }
- }
- }
-
private fun updateShowLoader(showLoader: Boolean) {
_showLoader.update { showLoader }
}
@@ -704,6 +713,34 @@ constructor(
return processedContactNumber.contains(processedSearchQuery)
}
+ fun onOffersRolodexClicked() {
+ viewModelScope.launch(Dispatchers.IO) {
+ naviPayAnalytics.onOffersCalloutClicked(
+ numberOfOffers = genericOffersList.value.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetStateChange = true,
+ bottomSheetUIState =
+ PayToContactsBottomSheetUIState.OfferList(
+ offerData = genericOffersList.value,
+ heading =
+ resourceProvider.getString(
+ resId = R.string.np_n_upi_payment_offers,
+ genericOffersList.value.size,
+ ),
+ ),
+ )
+ }
+ }
+
+ fun onHelpCtaClicked() {
+ viewModelScope.launch(Dispatchers.Default) {
+ _navigateToNextScreenFromHelpCta.emit(helpCtaData)
+ }
+ }
+
override val screenName: String
get() = NAVI_PAY_SEND_MONEY_TO_CONTACTS_SCREEN_V2
}
@@ -711,10 +748,13 @@ constructor(
enum class PayToContactsUIState {
Loading,
Loaded,
- Search,
}
-enum class PayToContactsBottomSheetUIState {
- InvalidVpa,
- ContactNotLinked,
+sealed class PayToContactsBottomSheetUIState {
+ data object InvalidVpa : PayToContactsBottomSheetUIState()
+
+ data object ContactNotLinked : PayToContactsBottomSheetUIState()
+
+ data class OfferList(val offerData: List, val heading: String) :
+ PayToContactsBottomSheetUIState()
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/model/view/SavedBeneficiaryScreenBottomSheetHolder.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/model/view/SavedBeneficiaryScreenBottomSheetHolder.kt
index ad75bbfc7c..a5bd025a5c 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/model/view/SavedBeneficiaryScreenBottomSheetHolder.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/model/view/SavedBeneficiaryScreenBottomSheetHolder.kt
@@ -7,7 +7,10 @@
package com.navi.pay.management.savedbeneficiary.model.view
+import com.navi.pay.management.savedbeneficiary.viewmodel.SavedBeneficiaryScreenBottomSheetUIState
+
data class SavedBeneficiaryScreenBottomSheetHolder(
val showBottomSheet: Boolean,
val bottomSheetStateChange: Boolean,
+ val bottomSheetUIState: SavedBeneficiaryScreenBottomSheetUIState?,
)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/ui/SavedBeneficiaryScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/ui/SavedBeneficiaryScreen.kt
index 34c659bd1a..2030988bac 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/ui/SavedBeneficiaryScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/ui/SavedBeneficiaryScreen.kt
@@ -10,7 +10,6 @@
package com.navi.pay.management.savedbeneficiary.ui
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -28,8 +27,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Scaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Tab
@@ -64,7 +61,6 @@ import androidx.paging.compose.itemKey
import com.navi.common.R as CommonR
import com.navi.common.commoncomposables.ui.ScrollableTabRow
import com.navi.common.commoncomposables.ui.tabIndicatorOffset
-import com.navi.common.model.common.NudgeDetailEntity
import com.navi.common.utils.EMPTY
import com.navi.common.utils.navigateUp
import com.navi.design.font.FontWeightEnum
@@ -79,14 +75,13 @@ import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderDescButton
import com.navi.pay.common.ui.ContactIconView
import com.navi.pay.common.ui.EmptyDataScreen
-import com.navi.pay.common.ui.IconImage
import com.navi.pay.common.ui.ImageTitleDescriptionShimmerView
import com.navi.pay.common.ui.InputTextFieldWithDescriptionHeader
import com.navi.pay.common.ui.NaviPayHeader
import com.navi.pay.common.ui.NaviPayLottieAnimation
import com.navi.pay.common.ui.NaviPayModalBottomSheet
+import com.navi.pay.common.ui.NaviPayOffersBottomSheet
import com.navi.pay.common.ui.NaviPaySponsorView
-import com.navi.pay.common.ui.RewardsNudgeWithBg
import com.navi.pay.common.ui.SelfTransferCtaView
import com.navi.pay.common.ui.TabIndicator
import com.navi.pay.common.ui.ThemeRoundedButton
@@ -96,8 +91,7 @@ import com.navi.pay.entry.NaviPayActivity
import com.navi.pay.management.common.model.view.WarningErrorInfoState
import com.navi.pay.management.savedbeneficiary.model.view.SavedBeneficiaryEntity
import com.navi.pay.management.savedbeneficiary.model.view.SavedBeneficiaryTabData
-import com.navi.pay.management.savedbeneficiary.model.view.SavedBeneficiaryType
-import com.navi.pay.management.savedbeneficiary.viewmodel.SavedBeneficiaryUIState
+import com.navi.pay.management.savedbeneficiary.viewmodel.SavedBeneficiaryScreenBottomSheetUIState
import com.navi.pay.management.savedbeneficiary.viewmodel.SavedBeneficiaryViewModel
import com.navi.pay.utils.LINKED_ACCOUNT_SCREEN_SOURCE
import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE
@@ -107,6 +101,8 @@ import com.navi.pay.utils.contactInitials
import com.navi.pay.utils.customHide
import com.navi.pay.utils.isEmpty
import com.navi.pay.utils.shimmerEffect
+import com.navi.rr.common.models.OfferData
+import com.navi.rr.common.ui.OffersRolodex
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
@@ -123,7 +119,6 @@ fun SavedBeneficiaryScreen(
val onBackClick = { navigator.navigateUp(naviPayActivity) }
val tabData by savedBeneficiaryViewModel.tabData.collectAsStateWithLifecycle()
- val uiState by savedBeneficiaryViewModel.uiState.collectAsStateWithLifecycle()
val searchQuery by savedBeneficiaryViewModel.searchQuery.collectAsStateWithLifecycle()
val bottomSheetStateHolder by
savedBeneficiaryViewModel.bottomSheetStateHolder.collectAsStateWithLifecycle()
@@ -134,9 +129,6 @@ fun SavedBeneficiaryScreen(
val bankAccountTabItems =
savedBeneficiaryViewModel.bankAccountTabDataPager.collectAsLazyPagingItems()
val searchDataItems = savedBeneficiaryViewModel.searchDataPager.collectAsLazyPagingItems()
-
- val rewardsNudgeDetailEntity by
- savedBeneficiaryViewModel.rewardsNudgeDetailEntity.collectAsStateWithLifecycle()
val showLoader by savedBeneficiaryViewModel.showLoader.collectAsStateWithLifecycle()
val showShimmer by savedBeneficiaryViewModel.showShimmer.collectAsStateWithLifecycle()
val activeQueryVpa by savedBeneficiaryViewModel.activeQueryVpa.collectAsStateWithLifecycle()
@@ -145,6 +137,8 @@ fun SavedBeneficiaryScreen(
savedBeneficiaryViewModel.isWarningOrErrorState.collectAsStateWithLifecycle()
val showSelfTransferCta by
savedBeneficiaryViewModel.showSelfTransferCta.collectAsStateWithLifecycle()
+ val genericOffersList by
+ savedBeneficiaryViewModel.genericOffersList.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
val keyboardController = LocalSoftwareKeyboardController.current
@@ -181,16 +175,11 @@ fun SavedBeneficiaryScreen(
}
BackHandler {
- if (uiState == SavedBeneficiaryUIState.Search) {
- savedBeneficiaryViewModel.updateSearchQuery(query = "")
- savedBeneficiaryViewModel.updateSavedBeneficiaryUIState(SavedBeneficiaryUIState.List)
- } else {
- onBackClick()
- }
+ savedBeneficiaryViewModel.updateSearchQuery(query = "")
+ savedBeneficiaryViewModel.cancelPayRequest()
+ onBackClick()
}
- val focusRequester = remember { FocusRequester() }
-
val onSavedBeneficiaryItemClicked: (SavedBeneficiaryEntity, Boolean) -> Unit =
{ savedBeneficiaryEntity, fromSaved ->
focusManager.clearFocus()
@@ -216,14 +205,22 @@ fun SavedBeneficiaryScreen(
)
}
+ val onOffersRolodexClicked = {
+ keyboardController?.customHide(context = context, view = view)
+ focusManager.clearFocus()
+ savedBeneficiaryViewModel.onOffersRolodexClicked()
+ }
+
if (bottomSheetStateHolder.showBottomSheet) {
NaviPayModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
bottomSheetState = bottomSheetState,
onDismissRequest = onDismissBottomSheet,
+ shouldDismissOnBackPress = true,
bottomSheetContent = {
RenderSavedBeneficiaryBottomSheetUIState(
- onInvalidVpaBottomSheetCtaClicked = onDismissBottomSheet
+ bottomSheetUIState = bottomSheetStateHolder.bottomSheetUIState,
+ onDismissBottomSheet = onDismissBottomSheet,
)
},
)
@@ -231,92 +228,53 @@ fun SavedBeneficiaryScreen(
val pagerState = key(tabData) { rememberPagerState(pageCount = { 2 }, initialPage = 0) }
- when (uiState) {
- SavedBeneficiaryUIState.List ->
- RenderSavedBeneficiaryListScreen(
- pagerState = pagerState,
- tabData = tabData,
- onTabClicked = {
- naviPayAnalytics.onLanded(
- tab = if (it == 0) "UPI" else "Bank",
- rewardNudgeShown = if (rewardsNudgeDetailEntity != null) "Y" else "N",
+ RenderSavedBeneficiaryListScreen(
+ pagerState = pagerState,
+ tabData = tabData,
+ onTabClicked = {
+ naviPayAnalytics.onLanded(
+ tab = if (it == 0) "UPI" else "Bank",
+ naviPaySessionAttributes = savedBeneficiaryViewModel.getNaviPaySessionAttributes(),
+ )
+ scope.launch { pagerState.animateScrollToPage(it) }
+ },
+ upiIdTabItems = upiIdTabItems,
+ bankAccountTabItems = bankAccountTabItems,
+ onSavedBeneficiaryListBottomCtaClicked = {
+ if (showLoader.not()) {
+ if (pagerState.currentPage == 0) {
+ naviPayAnalytics.onSavedBeneficiaryScreenCtaClicked(
+ tab = "UPI",
naviPaySessionAttributes =
savedBeneficiaryViewModel.getNaviPaySessionAttributes(),
)
- scope.launch { pagerState.animateScrollToPage(it) }
- },
- onSearchBarClicked = {
- naviPayAnalytics.onSavedBeneficiaryListViewSearchAreaClicked(
+ navigator.navigate(UPIIdInputScreenDestination())
+ } else {
+ naviPayAnalytics.onSavedBeneficiaryScreenCtaClicked(
+ tab = "Bank",
naviPaySessionAttributes =
- savedBeneficiaryViewModel.getNaviPaySessionAttributes()
+ savedBeneficiaryViewModel.getNaviPaySessionAttributes(),
)
- savedBeneficiaryViewModel.updateSavedBeneficiaryUIState(
- SavedBeneficiaryUIState.Search
- )
- },
- upiIdTabItems = upiIdTabItems,
- bankAccountTabItems = bankAccountTabItems,
- onSavedBeneficiaryListBottomCtaClicked = {
- if (showLoader.not()) {
- if (pagerState.currentPage == 0) {
- naviPayAnalytics.onSavedBeneficiaryScreenCtaClicked(
- tab = "UPI",
- naviPaySessionAttributes =
- savedBeneficiaryViewModel.getNaviPaySessionAttributes(),
- )
- navigator.navigate(UPIIdInputScreenDestination())
- } else {
- naviPayAnalytics.onSavedBeneficiaryScreenCtaClicked(
- tab = "Bank",
- naviPaySessionAttributes =
- savedBeneficiaryViewModel.getNaviPaySessionAttributes(),
- )
- navigator.navigate(BankDetailInputScreenDestination)
- }
- }
- },
- onSavedBeneficiaryItemClicked = onSavedBeneficiaryItemClicked,
- onBackClick = onBackClick,
- rewardsNudgeDetailEntity = rewardsNudgeDetailEntity,
- showLoader = showLoader,
- activeQueryVpa = activeQueryVpa,
- )
- SavedBeneficiaryUIState.Search -> {
- RenderSavedBeneficiarySearchScreen(
- searchQuery = searchQuery,
- onSearchQueryChanged = savedBeneficiaryViewModel::updateSearchQuery,
- onSearchBarBackClicked = {
- savedBeneficiaryViewModel.updateSearchQuery(query = "")
- savedBeneficiaryViewModel.cancelPayRequest()
- naviPayAnalytics.onSavedBeneficiarySearchScreenBackClicked(
- naviPaySessionAttributes =
- savedBeneficiaryViewModel.getNaviPaySessionAttributes()
- )
- savedBeneficiaryViewModel.updateSavedBeneficiaryUIState(
- SavedBeneficiaryUIState.List
- )
- },
- onSearchBarClearQueryClicked = {
- savedBeneficiaryViewModel.updateSearchQuery(query = "")
- },
- searchDataItems = searchDataItems,
- onSavedBeneficiaryItemClicked = onSavedBeneficiaryItemClicked,
- onSearchItemClicked = {
- keyboardController?.customHide(context = context, view = view)
- focusManager.clearFocus()
- savedBeneficiaryViewModel.onSearchItemClicked(it)
- },
- searchDataSavedBeneficiaryEntity = searchDataSavedBeneficiaryEntity,
- focusRequester = focusRequester,
- showLoader = showLoader,
- showShimmer = showShimmer,
- activeQueryVpa = activeQueryVpa,
- warningErrorInfoState = warningErrorInfoState,
- showSelfTransferCta = showSelfTransferCta,
- onSelfTransferCtaClicked = onSelfTransferCtaClicked,
- )
- }
- }
+ navigator.navigate(BankDetailInputScreenDestination)
+ }
+ }
+ },
+ onSavedBeneficiaryItemClicked = onSavedBeneficiaryItemClicked,
+ onBackClick = onBackClick,
+ showLoader = showLoader,
+ activeQueryVpa = activeQueryVpa,
+ searchQuery = searchQuery,
+ onSearchQueryChanged = savedBeneficiaryViewModel::updateSearchQuery,
+ onSearchBarClearQueryClicked = { savedBeneficiaryViewModel.updateSearchQuery(query = "") },
+ warningErrorInfoState = warningErrorInfoState,
+ showSelfTransferCta = showSelfTransferCta,
+ onSelfTransferCtaClicked = onSelfTransferCtaClicked,
+ searchDataSavedBeneficiaryEntity = searchDataSavedBeneficiaryEntity,
+ searchDataItems = searchDataItems,
+ showShimmer = showShimmer,
+ genericOffersList = genericOffersList,
+ onOffersRolodexClicked = onOffersRolodexClicked,
+ )
}
@Composable
@@ -324,39 +282,68 @@ private fun RenderSavedBeneficiaryListScreen(
pagerState: PagerState,
tabData: List,
onTabClicked: (Int) -> Unit,
- onSearchBarClicked: () -> Unit,
upiIdTabItems: LazyPagingItems,
bankAccountTabItems: LazyPagingItems,
onSavedBeneficiaryListBottomCtaClicked: () -> Unit,
onSavedBeneficiaryItemClicked: (SavedBeneficiaryEntity, Boolean) -> Unit,
onBackClick: () -> Unit,
- rewardsNudgeDetailEntity: NudgeDetailEntity?,
showLoader: Boolean,
activeQueryVpa: String = "",
+ searchQuery: String,
+ onSearchQueryChanged: (String) -> Unit,
+ onSearchBarClearQueryClicked: () -> Unit,
+ warningErrorInfoState: WarningErrorInfoState,
+ showSelfTransferCta: Boolean,
+ onSelfTransferCtaClicked: () -> Unit,
+ searchDataSavedBeneficiaryEntity: SavedBeneficiaryEntity?,
+ searchDataItems: LazyPagingItems,
+ showShimmer: Boolean,
+ genericOffersList: List,
+ onOffersRolodexClicked: () -> Unit,
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
SavedBeneficiaryListTopBar(
currentPage = pagerState.currentPage,
- onSearchBarClicked = onSearchBarClicked,
tabData = tabData,
onTabClicked = onTabClicked,
onBackClick = onBackClick,
- rewardsNudgeDetailEntity = rewardsNudgeDetailEntity,
showLoader = showLoader,
+ searchQuery = searchQuery,
+ onSearchQueryChanged = onSearchQueryChanged,
+ onSearchBarClearQueryClicked = onSearchBarClearQueryClicked,
+ warningErrorInfoState = warningErrorInfoState,
+ genericOffersList = genericOffersList,
+ onOffersRolodexClicked = onOffersRolodexClicked,
)
},
content = {
- RenderSavedBeneficiaryListContent(
- pagerState = pagerState,
- upiIdTabItems = upiIdTabItems,
- bankAccountTabItems = bankAccountTabItems,
- modifier = Modifier.padding(it),
- onSavedBeneficiaryItemClicked = { onSavedBeneficiaryItemClicked(it, true) },
- showLoader = showLoader,
- activeQueryVpa = activeQueryVpa,
- )
+ if (searchQuery.isEmpty()) {
+ RenderSavedBeneficiaryListContent(
+ pagerState = pagerState,
+ upiIdTabItems = upiIdTabItems,
+ bankAccountTabItems = bankAccountTabItems,
+ modifier = Modifier.padding(it),
+ onSavedBeneficiaryItemClicked = { onSavedBeneficiaryItemClicked(it, true) },
+ showLoader = showLoader,
+ activeQueryVpa = activeQueryVpa,
+ )
+ } else {
+ RenderSavedBeneficiarySearchItems(
+ modifier = Modifier.padding(it),
+ searchQuery = searchQuery,
+ searchDataItems = searchDataItems,
+ onSavedBeneficiaryItemClicked = onSavedBeneficiaryItemClicked,
+ onSearchItemClicked = { onSavedBeneficiaryItemClicked(it, false) },
+ searchDataSavedBeneficiaryEntity = searchDataSavedBeneficiaryEntity,
+ showLoader = showLoader,
+ showShimmer = showShimmer,
+ activeQueryVpa = activeQueryVpa,
+ showSelfTransferCta = showSelfTransferCta,
+ onSelfTransferCtaClicked = onSelfTransferCtaClicked,
+ )
+ }
},
bottomBar = {
SavedBeneficiaryListBottomBar(
@@ -371,13 +358,19 @@ private fun RenderSavedBeneficiaryListScreen(
@Composable
private fun SavedBeneficiaryListTopBar(
currentPage: Int,
- onSearchBarClicked: () -> Unit,
tabData: List,
onTabClicked: (Int) -> Unit,
onBackClick: () -> Unit,
- rewardsNudgeDetailEntity: NudgeDetailEntity?,
showLoader: Boolean,
+ searchQuery: String,
+ onSearchQueryChanged: (String) -> Unit,
+ onSearchBarClearQueryClicked: () -> Unit,
+ warningErrorInfoState: WarningErrorInfoState,
+ genericOffersList: List,
+ onOffersRolodexClicked: () -> Unit,
) {
+ val focusRequester = remember { FocusRequester() }
+
Column(modifier = Modifier.fillMaxWidth()) {
NaviPayHeader(
navigationIcon = CommonR.drawable.ic_arrow_left_black_v2,
@@ -386,103 +379,111 @@ private fun SavedBeneficiaryListTopBar(
modifier = Modifier.fillMaxWidth(),
)
- rewardsNudgeDetailEntity?.let {
- RewardsNudgeWithBg(modifier = Modifier.fillMaxWidth(), nudgeDetailEntity = it)
-
- Spacer(modifier = Modifier.height(24.dp))
- }
-
- SavedBeneficiaryListSearchBar(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
- onSearchBarClicked = onSearchBarClicked,
- showLoader = showLoader,
+ InputTextFieldWithDescriptionHeader(
+ modifier =
+ Modifier.fillMaxWidth().padding(horizontal = 16.dp).focusRequester(focusRequester),
+ headerString = null,
+ placeHolderString =
+ stringResource(id = R.string.np_saved_beneficiary_search_bar_placeholder),
+ value = searchQuery,
+ onValueChangeListener = onSearchQueryChanged,
+ leadingIconId = CommonR.drawable.ic_arrow_left_black_v2,
+ isTrailingIconEnabled = searchQuery.isNotEmpty(),
+ onTrailingIconClicked = onSearchBarClearQueryClicked,
+ warningErrorInfoState = warningErrorInfoState.isErrorState,
+ trailingIconId = WidgetsR.drawable.small_cross_purple,
+ enabled = !showLoader,
)
- Spacer(modifier = Modifier.height(16.dp))
+ if (warningErrorInfoState.isWarningState && searchQuery.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(8.dp))
+ NaviText(
+ text = warningErrorInfoState.warningMessage.orEmpty(),
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
+ fontFamily = naviFontFamily,
+ fontSize = 12.sp,
+ color = NaviPayColor.textPrimary,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ )
+ }
- ScrollableTabRow(
- edgePadding = 0.dp,
- selectedTabIndex = 2 * currentPage,
- modifier = Modifier.padding(horizontal = 16.dp),
- containerColor = NaviPayColor.transparent,
- indicator = { tabPositions ->
- TabIndicator(
- modifier =
- Modifier.tabIndicatorOffset(
- currentTabPosition = tabPositions[2 * currentPage],
- isSelectedTabIndexLastTab = currentPage == tabPositions.size - 1,
- ),
- height = 2.dp,
- color = NaviPayColor.ctaPrimary,
- )
- },
- ) {
- tabData.forEachIndexed { index, tabInfo ->
- Row(verticalAlignment = Alignment.CenterVertically) {
- Tab(
- modifier = Modifier.wrapContentSize(),
- selected = index == currentPage,
- onClick = { if (showLoader.not()) onTabClicked(index) },
- interactionSource = remember { NoRippleIndicationSource() },
- ) {
- NaviText(
- text = stringResource(id = tabInfo.textId),
- fontSize = 16.sp,
- fontFamily = naviFontFamily,
- fontWeight =
- if (index == currentPage) {
- getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD)
- } else {
- getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR)
- },
- color =
- if (index == currentPage) {
- NaviPayColor.ctaPrimary
- } else {
- NaviPayColor.textTertiary
- },
- lineHeight = 24.sp,
- )
+ if (warningErrorInfoState.isErrorState && searchQuery.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(8.dp))
+ NaviText(
+ text = warningErrorInfoState.errorMessage.orEmpty(),
+ fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
+ fontFamily = naviFontFamily,
+ fontSize = 12.sp,
+ color = NaviPayColor.inputFieldError,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ )
+ }
- Spacer(modifier = Modifier.height(4.dp))
+ OffersRolodex(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = 24.dp, bottom = 8.dp, start = 16.dp, end = 16.dp),
+ offerData = genericOffersList,
+ shouldShowRolodexAnimation = genericOffersList.size > 1,
+ onClick = onOffersRolodexClicked,
+ )
+
+ if (searchQuery.isEmpty()) {
+ Spacer(modifier = Modifier.height(16.dp))
+ ScrollableTabRow(
+ edgePadding = 0.dp,
+ selectedTabIndex = 2 * currentPage,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ containerColor = NaviPayColor.transparent,
+ indicator = { tabPositions ->
+ TabIndicator(
+ modifier =
+ Modifier.tabIndicatorOffset(
+ currentTabPosition = tabPositions[2 * currentPage],
+ isSelectedTabIndexLastTab = currentPage == tabPositions.size - 1,
+ ),
+ height = 2.dp,
+ color = NaviPayColor.ctaPrimary,
+ )
+ },
+ ) {
+ tabData.forEachIndexed { index, tabInfo ->
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Tab(
+ modifier = Modifier.wrapContentSize(),
+ selected = index == currentPage,
+ onClick = { if (showLoader.not()) onTabClicked(index) },
+ interactionSource = remember { NoRippleIndicationSource() },
+ ) {
+ NaviText(
+ text = stringResource(id = tabInfo.textId),
+ fontSize = 16.sp,
+ fontFamily = naviFontFamily,
+ fontWeight =
+ if (index == currentPage) {
+ getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD)
+ } else {
+ getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR)
+ },
+ color =
+ if (index == currentPage) {
+ NaviPayColor.ctaPrimary
+ } else {
+ NaviPayColor.textTertiary
+ },
+ lineHeight = 24.sp,
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+ if (index != tabData.size - 1) {
+ Spacer(modifier = Modifier.width(24.dp))
}
}
- if (index != tabData.size - 1) {
- Spacer(modifier = Modifier.width(24.dp))
- }
}
+ Spacer(modifier = Modifier.height(32.dp))
}
-
- Spacer(modifier = Modifier.height(32.dp))
- }
-}
-
-@Composable
-private fun SavedBeneficiaryListSearchBar(
- modifier: Modifier = Modifier,
- onSearchBarClicked: () -> Unit,
- showLoader: Boolean,
-) {
- Row(
- modifier =
- modifier
- .fillMaxWidth()
- .clickable { if (showLoader.not()) onSearchBarClicked() }
- .border(
- width = 1.dp,
- shape = RoundedCornerShape(4.dp),
- color = NaviPayColor.borderDefault,
- )
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- NaviText(
- text = stringResource(id = R.string.np_saved_beneficiary_search_bar_placeholder),
- fontSize = 14.sp,
- fontFamily = naviFontFamily,
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
- color = NaviPayColor.textTertiary,
- )
}
}
@@ -523,10 +524,10 @@ private fun RenderSavedBeneficiaryListData(
activeQueryVpa: String = "",
) {
var isLoading by remember { mutableStateOf(true) }
-
LaunchedEffect(items.loadState.refresh) {
isLoading = items.loadState.refresh is LoadState.Loading
}
+
if (items.isEmpty()) {
EmptyDataScreen(
iconResId = R.drawable.ic_empty_saved_beneficiary_screen,
@@ -563,136 +564,65 @@ private fun RenderSavedBeneficiaryListData(
}
@Composable
-private fun RenderSavedBeneficiarySearchScreen(
+private fun RenderSavedBeneficiarySearchItems(
+ modifier: Modifier = Modifier,
searchQuery: String,
- onSearchQueryChanged: (String) -> Unit,
- onSearchBarBackClicked: () -> Unit,
- onSearchBarClearQueryClicked: () -> Unit,
searchDataItems: LazyPagingItems,
onSavedBeneficiaryItemClicked: (SavedBeneficiaryEntity, Boolean) -> Unit,
onSearchItemClicked: (SavedBeneficiaryEntity) -> Unit,
searchDataSavedBeneficiaryEntity: SavedBeneficiaryEntity?,
- focusRequester: FocusRequester,
showLoader: Boolean = false,
showShimmer: Boolean = false,
activeQueryVpa: String = "",
- warningErrorInfoState: WarningErrorInfoState,
showSelfTransferCta: Boolean,
onSelfTransferCtaClicked: () -> Unit,
) {
-
- val keyboardController = LocalSoftwareKeyboardController.current
-
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- topBar = {
- Column(modifier = Modifier.fillMaxWidth()) {
- Spacer(modifier = Modifier.height(16.dp))
-
- InputTextFieldWithDescriptionHeader(
- modifier =
- Modifier.fillMaxWidth()
- .padding(horizontal = 16.dp)
- .focusRequester(focusRequester),
- headerString = null,
- placeHolderString = "",
- value = searchQuery,
- onValueChangeListener = onSearchQueryChanged,
- isLeadingIconEnabled = true,
- leadingIconId = CommonR.drawable.ic_arrow_left_black_v2,
- onLeadingIconClicked = onSearchBarBackClicked,
- isTrailingIconEnabled = searchQuery.isNotEmpty(),
- onTrailingIconClicked = onSearchBarClearQueryClicked,
- warningErrorInfoState = warningErrorInfoState.isErrorState,
- trailingIconId = WidgetsR.drawable.small_cross_purple,
- )
-
- LaunchedEffect(Unit) {
- focusRequester.requestFocus()
- keyboardController?.show()
- }
-
- if (warningErrorInfoState.isWarningState) {
- Spacer(modifier = Modifier.height(8.dp))
- NaviText(
- text = warningErrorInfoState.warningMessage.orEmpty(),
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
- fontFamily = naviFontFamily,
- fontSize = 12.sp,
- color = NaviPayColor.textPrimary,
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
- )
- }
-
- if (warningErrorInfoState.isErrorState) {
- Spacer(modifier = Modifier.height(8.dp))
- NaviText(
- text = warningErrorInfoState.errorMessage.orEmpty(),
- fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
- fontFamily = naviFontFamily,
- fontSize = 12.sp,
- color = NaviPayColor.inputFieldError,
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
- )
- }
- }
- },
- content = {
- if (showSelfTransferCta) {
- SelfTransferCtaView(
- description = searchQuery,
- modifier =
- Modifier.fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 24.dp)
- .clickable { onSelfTransferCtaClicked() },
- )
- }
- LazyColumn(
- modifier = Modifier.padding(it).fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- item { Spacer(modifier = Modifier.height(8.dp)) }
- items(
- count = searchDataItems.itemCount,
- key = searchDataItems.itemKey { it.vpa },
- ) { index ->
- searchDataItems[index]?.let { savedBeneficiaryEntity ->
- SavedBeneficiaryItemView(
- modifier = Modifier.padding(horizontal = 16.dp),
- savedBeneficiaryEntity = savedBeneficiaryEntity,
- onSavedBeneficiaryItemClicked = {
- onSavedBeneficiaryItemClicked(it, true)
- },
- index = index,
- showLoader = showLoader,
- activeQueryVpa = activeQueryVpa,
- )
- }
- }
- if (showShimmer) {
- item { ImageTitleDescriptionShimmerView() }
- }
-
- if (searchDataSavedBeneficiaryEntity != null) {
- item {
- SavedBeneficiaryItemView(
- modifier = Modifier.padding(horizontal = 16.dp),
- savedBeneficiaryEntity = searchDataSavedBeneficiaryEntity,
- onSavedBeneficiaryItemClicked = { onSearchItemClicked(it) },
- index = 0,
- showLoader = showLoader,
- activeQueryVpa = activeQueryVpa,
- )
- }
- }
- }
- },
- bottomBar = {
- NaviPaySponsorView(
- modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 32.dp)
+ Column(modifier = modifier) {
+ if (showSelfTransferCta) {
+ SelfTransferCtaView(
+ description = searchQuery,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 24.dp)
+ .clickable { onSelfTransferCtaClicked() },
)
- },
- )
+ }
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ item { Spacer(modifier = Modifier.height(8.dp)) }
+ items(count = searchDataItems.itemCount, key = searchDataItems.itemKey { it.vpa }) {
+ index ->
+ searchDataItems[index]?.let { savedBeneficiaryEntity ->
+ SavedBeneficiaryItemView(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ savedBeneficiaryEntity = savedBeneficiaryEntity,
+ onSavedBeneficiaryItemClicked = { onSavedBeneficiaryItemClicked(it, true) },
+ index = index,
+ showLoader = showLoader,
+ activeQueryVpa = activeQueryVpa,
+ )
+ }
+ }
+ if (showShimmer) {
+ item { ImageTitleDescriptionShimmerView() }
+ }
+
+ if (searchDataSavedBeneficiaryEntity != null) {
+ item {
+ SavedBeneficiaryItemView(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ savedBeneficiaryEntity = searchDataSavedBeneficiaryEntity,
+ onSavedBeneficiaryItemClicked = { onSearchItemClicked(it) },
+ index = 0,
+ showLoader = showLoader,
+ activeQueryVpa = activeQueryVpa,
+ )
+ }
+ }
+ }
+ }
}
@Composable
@@ -725,23 +655,12 @@ private fun SavedBeneficiaryItemView(
},
),
) {
- if (savedBeneficiaryEntity.type == SavedBeneficiaryType.BANK_ACCOUNT) {
- IconImage(
- modifier = Modifier,
- iconUrl = savedBeneficiaryEntity.bankIconUrl,
- iconSize = 36.dp,
- boxSize = 40.dp,
- bgColor = NaviPayColor.textWhite,
- shape = CircleShape,
- )
- } else {
- ContactIconView(
- index = index,
- contactInitials = savedBeneficiaryEntity.name.contactInitials(),
- vpa = savedBeneficiaryEntity.vpa,
- phoneNumber = EMPTY,
- )
- }
+ ContactIconView(
+ index = index,
+ contactInitials = savedBeneficiaryEntity.name.contactInitials(),
+ vpa = savedBeneficiaryEntity.vpa,
+ phoneNumber = EMPTY,
+ )
Spacer(modifier = Modifier.width(16.dp))
@@ -795,8 +714,8 @@ private fun SavedBeneficiaryListBottomBar(
text =
stringResource(
id =
- if (currentPage == 0) R.string.np_saved_beneficiary_add_upi_id
- else R.string.np_saved_beneficiary_add_bank_account
+ if (currentPage == 0) R.string.np_pay_to_upi_id
+ else R.string.np_pay_to_bank_account
),
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
onClick = {
@@ -812,15 +731,28 @@ private fun SavedBeneficiaryListBottomBar(
@Composable
private fun RenderSavedBeneficiaryBottomSheetUIState(
- onInvalidVpaBottomSheetCtaClicked: () -> Unit
+ bottomSheetUIState: SavedBeneficiaryScreenBottomSheetUIState?,
+ onDismissBottomSheet: () -> Unit,
) {
- BottomSheetContentWithIconHeaderDescButton(
- iconId = CommonR.drawable.ic_exclamation_red_border,
- headerTextId = R.string.np_saved_beneficiary_invalid_vpa_header,
- descriptionTextId = R.string.np_saved_beneficiary_invalid_vpa_description,
- buttonTextId = R.string.np_okay_got_it,
- onButtonClicked = onInvalidVpaBottomSheetCtaClicked,
- )
+ when (bottomSheetUIState) {
+ SavedBeneficiaryScreenBottomSheetUIState.InvalidVpa -> {
+ BottomSheetContentWithIconHeaderDescButton(
+ iconId = CommonR.drawable.ic_exclamation_red_border,
+ headerTextId = R.string.np_saved_beneficiary_invalid_vpa_header,
+ descriptionTextId = R.string.np_saved_beneficiary_invalid_vpa_description,
+ buttonTextId = R.string.np_okay_got_it,
+ onButtonClicked = onDismissBottomSheet,
+ )
+ }
+ is SavedBeneficiaryScreenBottomSheetUIState.OfferList -> {
+ NaviPayOffersBottomSheet(
+ offerData = bottomSheetUIState.offerData,
+ closeSheet = onDismissBottomSheet,
+ heading = bottomSheetUIState.heading,
+ )
+ }
+ else -> {}
+ }
}
@Composable
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/viewmodel/SavedBeneficiaryViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/viewmodel/SavedBeneficiaryViewModel.kt
index 063e1f24c1..6c62154a5b 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/viewmodel/SavedBeneficiaryViewModel.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/savedbeneficiary/viewmodel/SavedBeneficiaryViewModel.kt
@@ -13,10 +13,9 @@ import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.isNotNullAndNotEmpty
-import com.navi.common.model.common.NudgeDetailEntity
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
-import com.navi.common.usecase.RewardsNudgeEntityFetchUseCase
+import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.common.utils.EMPTY
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
@@ -36,6 +35,7 @@ import com.navi.pay.destinations.SendMoneyScreenDestination
import com.navi.pay.management.common.model.view.WarningErrorInfoState
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.util.NaviPayOffersHelper
import com.navi.pay.management.savedbeneficiary.model.view.SavedBeneficiaryEntity
import com.navi.pay.management.savedbeneficiary.model.view.SavedBeneficiaryScreenBottomSheetHolder
import com.navi.pay.management.savedbeneficiary.model.view.SavedBeneficiaryTabData
@@ -48,6 +48,7 @@ import com.navi.pay.utils.INVALID_UPI_ID
import com.navi.pay.utils.INVALID_UPI_NUMBER
import com.navi.pay.utils.INVALID_VPA
import com.navi.pay.utils.INVALID_VPA_HANDLE
+import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE
import com.navi.pay.utils.MAPPING_DOES_NOT_EXIST
import com.navi.pay.utils.MCC_MISMATCH
import com.navi.pay.utils.NAVI_PAY_SEARCH_QUERY_API_DELAY
@@ -58,6 +59,7 @@ import com.navi.pay.utils.isValidSearchQuery
import com.navi.pay.utils.isValidUpiId
import com.navi.pay.utils.isValidUpiNumberLength
import com.navi.pay.utils.toUpiMapperVpa
+import com.navi.rr.common.models.OfferData
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -91,9 +93,10 @@ constructor(
private val validateVpaUseCase: ValidateVpaUseCase,
private val bankRepository: BankRepository,
private val deviceInfoProvider: DeviceInfoProvider,
- private val rewardsNudgeEntityFetchUseCase: RewardsNudgeEntityFetchUseCase,
private val naviPaySessionHelper: NaviPaySessionHelper,
private val resourceProvider: ResourceProvider,
+ private val naviPayOffersHelper: NaviPayOffersHelper,
+ private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
) : NaviPayBaseVM() {
private val SEARCH_QUERY_DEBOUNCE_TIME = 100.milliseconds
@@ -103,9 +106,6 @@ constructor(
private val naviPayAnalytics: NaviPayAnalytics.SavedBeneficiaryScreen =
NaviPayAnalytics.INSTANCE.SavedBeneficiaryScreen()
- private val _uiState = MutableStateFlow(SavedBeneficiaryUIState.List)
- val uiState = _uiState.asStateFlow()
-
private val _tabData = MutableStateFlow(getDefaultTabData())
val tabData = _tabData.asStateFlow()
@@ -128,6 +128,9 @@ constructor(
private val _searchDataSavedBeneficiaryEntity = MutableStateFlow(null)
val searchDataSavedBeneficiaryEntity = _searchDataSavedBeneficiaryEntity.asStateFlow()
+ private val _genericOffersList = MutableStateFlow>(emptyList())
+ val genericOffersList = _genericOffersList.asStateFlow()
+
private val _hideKeyboard = MutableStateFlow(false)
val hideKeyboard = _hideKeyboard.asStateFlow()
@@ -146,6 +149,7 @@ constructor(
SavedBeneficiaryScreenBottomSheetHolder(
showBottomSheet = false,
bottomSheetStateChange = false,
+ bottomSheetUIState = null,
)
)
val bottomSheetStateHolder = _bottomSheetStateHolder.asStateFlow()
@@ -153,7 +157,7 @@ constructor(
private val _navigateToNextScreen = MutableSharedFlow()
val navigateToNextScreen = _navigateToNextScreen.asSharedFlow()
- val userPhoneNumber = fetchUserPhoneNumber()
+ private val userPhoneNumber = fetchUserPhoneNumber()
private val _shouldShowSelfTransferCta = MutableStateFlow(false)
val showSelfTransferCta = _shouldShowSelfTransferCta.asStateFlow()
@@ -272,10 +276,8 @@ constructor(
fetchNewBeneficiaryData(upiNumberOrId = upiNumberOrId, isValidUpiNumber = isValidUpiNumber)
}
- private val _rewardsNudgeDetailEntity = MutableStateFlow(null)
- val rewardsNudgeDetailEntity = _rewardsNudgeDetailEntity.asStateFlow()
-
init {
+ getGenericOffersList()
updateNaviPaySessionId()
updateLinkedAccount()
viewModelScope.launch(Dispatchers.IO) {
@@ -286,17 +288,24 @@ constructor(
savedBeneficiaryRepository.deleteAllPartiallyDeletedBeneficiaries()
}
launch(Dispatchers.IO) { searchQueryChangeObserver() }
- launch(Dispatchers.IO) {
- val rewardsNudgeDetailEntityResponseFromUseCase =
- rewardsNudgeEntityFetchUseCase.execute()
- _rewardsNudgeDetailEntity.update { rewardsNudgeDetailEntityResponseFromUseCase }
+ }
+ }
- naviPayAnalytics.onLanded(
- tab = "UPI",
- rewardNudgeShown =
- if (rewardsNudgeDetailEntityResponseFromUseCase != null) "Y" else "N",
+ private fun getGenericOffersList() {
+ viewModelScope.launch(Dispatchers.IO) {
+ val isOfferExperienceEnabled =
+ litmusExperimentsUseCase
+ .execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE)
+ ?.variant
+ ?.enabled ?: false
+
+ if (isOfferExperienceEnabled) {
+ val genericOffersList = naviPayOffersHelper.getCachedOffers(screenName = screenName)
+ naviPayAnalytics.onOffersCalloutLanded(
+ numberOfOffers = genericOffersList.size,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
+ _genericOffersList.update { genericOffersList }
}
}
}
@@ -345,10 +354,6 @@ constructor(
_showLoader.update { showLoader }
}
- fun updateSavedBeneficiaryUIState(savedBeneficiaryUIState: SavedBeneficiaryUIState) {
- _uiState.update { savedBeneficiaryUIState }
- }
-
private fun updateNaviPaySessionId() {
naviPaySessionHelper.createNewSessionId()
}
@@ -390,11 +395,13 @@ constructor(
fun updateBottomSheetUIState(
showBottomSheet: Boolean,
bottomSheetStateChange: Boolean? = null,
+ bottomSheetUIState: SavedBeneficiaryScreenBottomSheetUIState? = null,
) {
_bottomSheetStateHolder.update {
it.copy(
showBottomSheet = showBottomSheet,
bottomSheetStateChange = bottomSheetStateChange ?: it.bottomSheetStateChange,
+ bottomSheetUIState = bottomSheetUIState ?: it.bottomSheetUIState,
)
}
}
@@ -403,7 +410,6 @@ constructor(
viewModelScope.launch(Dispatchers.IO) {
updateNavigateToNextScreen(LinkedAccountsScreenDestination(shouldNavigateUp = true))
updateSearchQuery(query = "")
- updateSavedBeneficiaryUIState(savedBeneficiaryUIState = SavedBeneficiaryUIState.List)
}
}
@@ -495,15 +501,6 @@ constructor(
)
}
- fun onSearchItemClicked(savedBeneficiaryEntity: SavedBeneficiaryEntity) {
- updateSearchDataSavedBeneficiaryEntity(null)
- onSavedBeneficiaryItemClicked(
- savedBeneficiaryEntity = savedBeneficiaryEntity,
- isEntityFromSavedList = false,
- newContactResponse = newContactResponse,
- )
- }
-
fun onSavedBeneficiaryItemClicked(
savedBeneficiaryEntity: SavedBeneficiaryEntity,
isEntityFromSavedList: Boolean,
@@ -630,7 +627,6 @@ constructor(
)
)
updateShowLoader(showLoader = false)
- updateSavedBeneficiaryUIState(savedBeneficiaryUIState = SavedBeneficiaryUIState.List)
}
private suspend fun onValidateVpaFailureForSavedContact(
@@ -643,7 +639,11 @@ constructor(
savedBeneficiaryEntity = savedBeneficiaryEntity,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
- updateBottomSheetUIState(showBottomSheet = true)
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetStateChange = false,
+ bottomSheetUIState = SavedBeneficiaryScreenBottomSheetUIState.InvalidVpa,
+ )
savedBeneficiaryRepository.setIsPartiallyDeleted(vpa = savedBeneficiaryEntity.vpa)
} else {
updateHideKeyboard(hideKeyboard = true)
@@ -651,11 +651,36 @@ constructor(
}
}
+ fun onOffersRolodexClicked() {
+ viewModelScope.launch(Dispatchers.IO) {
+ naviPayAnalytics.onOffersCalloutClicked(
+ numberOfOffers = genericOffersList.value.size,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetStateChange = true,
+ bottomSheetUIState =
+ SavedBeneficiaryScreenBottomSheetUIState.OfferList(
+ offerData = genericOffersList.value,
+ heading =
+ resourceProvider.getString(
+ resId = R.string.np_n_upi_payment_offers,
+ genericOffersList.value.size,
+ ),
+ ),
+ )
+ }
+ }
+
override val screenName: String
get() = NAVI_PAY_SAVED_BENEFICIARY_SCREEN
}
-enum class SavedBeneficiaryUIState {
- List,
- Search,
+sealed class SavedBeneficiaryScreenBottomSheetUIState {
+ data class OfferList(val offerData: List, val heading: String) :
+ SavedBeneficiaryScreenBottomSheetUIState()
+
+ data object InvalidVpa : SavedBeneficiaryScreenBottomSheetUIState()
}
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 140e80ba9a..bec22653fe 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
@@ -27,6 +27,7 @@ const val BOTTOM_SHEET_HEIGHT_PERCENTAGE_80 = 0.80f
const val BOTTOM_SHEET_LIST_HEIGHT_PERCENTAGE_45 = 0.45f
const val LITE_MANDATE = "LITEMANDATE"
const val BUTLER_VPA_TRANSACTIONS_INFO_MAX_SIZE = 10
+const val BAU = "BAU"
const val MAX_VISIBLE_TRANSACTION_ITEMS_IN_SCREEN_HEIGHT = 5
const val NAVI_UPI = "Navi UPI"
@@ -151,6 +152,7 @@ const val NAVI_PAY_UPI_REQUEST_ID_CACHE_KEY = "naviPayUpiRequestId"
const val NAVI_PAY_SETTING_QR_PAGER_ANIMATION_COUNTER = "settingQrPagerAnimationCounter"
const val NAVI_PAY_AUTO_POPUP_SCRATCH_CARD_COUNTER = "autoPopupScratchCardCounter"
const val KEY_UPI_LITE_MANDATE_INFO = "liteMandateInfo"
+const val NAVI_PAY_GENERIC_OFFERS_INFO = "naviPayGenericOffersInfo"
const val KEY_UPI_LITE_LAST_NOTIFICATION_TIMESTAMP = "upiLiteLastNotificationTimestamp"
const val NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED =
"naviPayDeviceBindingIsSmvTriggeredAndFailed"
@@ -169,17 +171,16 @@ const val NAVI_PAY_SYNC_TABLE_UPI_LITE_MANDATE_INFO = "upiLiteMandateInfo"
const val LITMUS_EXPERIMENT_NAVIPAY_LITE_DEFAULT_ENTERED_AMOUNT =
"NaviPay-lite-default-entered-amount"
const val LITMUS_EXPERIMENT_NAVIPAY_ORDER_TAG_SUMMARY = "NaviPay-order-tag-summary"
-const val LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER =
- "NaviPay-frequent-contact-in-qr-scanner"
const val LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING = "NaviPay-exp-smv-binding"
const val LITMUS_EXPERIMENT_NAVI_FESTIVE_THEME = "festive-theme"
+const val LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE = "NaviPay-offer-experience"
val NAVI_PAY_LITMUS_EXPERIMENTS =
listOf(
LITMUS_EXPERIMENT_NAVIPAY_LITE_DEFAULT_ENTERED_AMOUNT,
LITMUS_EXPERIMENT_NAVIPAY_ORDER_TAG_SUMMARY,
- LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER,
LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING,
LITMUS_EXPERIMENT_NAVI_FESTIVE_THEME,
+ LITMUS_EXPERIMENT_NAVIPAY_OFFER_EXPERIENCE,
)
// Generic
@@ -221,6 +222,14 @@ const val CHECK_BALANCE_ERROR_TRANSITION_TOTAL_DURATION: Long =
(CHECK_BALANCE_ERROR_TO_INITIAL_TOTAL_TRANSITION_DURATION -
CHECK_BALANCE_ERROR_ANIMATION_DURATION)
+// Offer experience
+const val TXN_AMOUNT = "TXN_AMOUNT"
+const val PAYER_VPA = "PAYER_VPA"
+const val PAYEE_VPA = "PAYEE_VPA"
+const val UPI_PAYEE_MCC = "UPI_PAYEE_MCC"
+const val UPI_PURPOSE_CODE = "UPI_PURPOSE_CODE"
+const val UPI_PAYER_BANK_CODE = "UPI_PAYER_BANK_CODE"
+
// Deeplink
const val NAVI_PAY_INTENT_SCHEME = "upi"
const val NAVI_PAY_INTENT_MANDATE_HOST = "mandate"
diff --git a/android/navi-pay/src/main/res/values/strings.xml b/android/navi-pay/src/main/res/values/strings.xml
index 90ac21ff90..2a58c130d9 100644
--- a/android/navi-pay/src/main/res/values/strings.xml
+++ b/android/navi-pay/src/main/res/values/strings.xml
@@ -82,8 +82,7 @@
Scan & pay
Send money
Transfer now
- Send money to bank
- Enter recipient details
+ Enter receiver details
Account number
Enter UPI ID or number
Enter account number
@@ -146,7 +145,7 @@
Grant camera permission to scan QR codes for merchants, family, and friends.
Contacts
To allow UPI payments to your contacts.
- Allow contact permission
+ Allow contact permission to transfer money to your friends & family.
We collect this data so you can transfer money to your friends and family and earn rewards!
You have no contacts in your phone
Invalid QR code
@@ -470,8 +469,8 @@
Search UPI ID, UPI number or bank account
UPI IDs
Bank accounts
- Add UPI ID
- Add bank account
+ Pay to UPI ID
+ Pay to bank account
UPI ID is not active anymore!
Please try with some other UPI ID. This UPI ID will be removed from the list.
Contact number is not linked to UPI
@@ -518,8 +517,6 @@
Via UPI
Paid via
You\'ve won
- %s Navi coins
- %s Navi coin
Don\'t know user?
Account not linked
The bank account you are trying to send money is not linked to Navi UPI anymore.
@@ -841,4 +838,6 @@
up to
Pay mobile number
Transaction history
+ check now
+ %s UPI payment offers
\ No newline at end of file
diff --git a/android/navi-pay/src/test/kotlin/com/navi/pay/usecase/NaviPayOffersHelperTest.kt b/android/navi-pay/src/test/kotlin/com/navi/pay/usecase/NaviPayOffersHelperTest.kt
new file mode 100644
index 0000000000..b2d0860b32
--- /dev/null
+++ b/android/navi-pay/src/test/kotlin/com/navi/pay/usecase/NaviPayOffersHelperTest.kt
@@ -0,0 +1,139 @@
+/*
+ *
+ * * Copyright © 2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.usecase
+
+import com.google.gson.Gson
+import com.navi.base.cache.repository.NaviCacheRepository
+import com.navi.common.network.models.RepoResult
+import com.navi.pay.common.usecase.NaviPayConfigUseCase
+import com.navi.pay.management.common.sendmoney.util.NaviPayOffersHelper
+import com.navi.pay.network.di.NaviPayGsonBuilder
+import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity
+import com.navi.rr.common.models.OfferData
+import com.navi.rr.common.models.OfferResponse
+import com.navi.rr.common.repo.OffersRepo
+import com.navi.rr.utils.mapper.OfferResponseToOfferDataMapper
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.RelaxedMockK
+import io.mockk.mockkConstructor
+import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.joda.time.DateTime
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class NaviPayOffersHelperTest {
+
+ @RelaxedMockK private lateinit var offersRepo: OffersRepo
+ @RelaxedMockK private lateinit var naviCacheRepository: NaviCacheRepository
+ @RelaxedMockK private lateinit var naviPayConfigUseCase: NaviPayConfigUseCase
+ @RelaxedMockK @NaviPayGsonBuilder private lateinit var gson: Gson
+
+ private val testDispatcher = StandardTestDispatcher()
+ private lateinit var naviPayOffersHelper: NaviPayOffersHelper
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this, relaxed = true)
+ Dispatchers.setMain(testDispatcher)
+ naviPayOffersHelper =
+ NaviPayOffersHelper(
+ offersRepo = offersRepo,
+ naviCacheRepository = naviCacheRepository,
+ naviPayConfigUseCase = naviPayConfigUseCase,
+ gson = gson,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `getOffersFromNetwork should return empty list when no offers found`() = runTest {
+ coEvery { offersRepo.fetchOffersForProduct(any(), metricInfo = any()) } returns emptyFlow()
+ val result = naviPayOffersHelper.getOffersFromNetwork(screenName = "Test")
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `getOffersFromNetwork should return valid offers list`() = runTest {
+ val mockOffersResponse = listOf(OfferResponse(), OfferResponse())
+ val mappedOffers =
+ listOf(OfferData(titlePrefix = "Offer 1"), OfferData(titlePrefix = "Offer 2"))
+
+ coEvery { offersRepo.fetchOffersForProduct(any(), metricInfo = any()) } returns
+ flowOf(RepoResult(data = mockOffersResponse))
+
+ mockkConstructor(OfferResponseToOfferDataMapper::class)
+
+ every { anyConstructed().map(any(), any()) } returnsMany
+ mappedOffers
+
+ val result = naviPayOffersHelper.getOffersFromNetwork("Test")
+
+ assertEquals(2, result.size)
+ assertTrue(result[0].isBestOffer)
+ }
+
+ @Test
+ fun `buildParamsMap should return map with payment amount and bank details`() {
+ val selectedBankAccount =
+ LinkedAccountEntity(
+ accountId = "123",
+ bankCode = "HDFC",
+ bankName = "",
+ ifsc = "",
+ fallbackBankLogoResId = -1,
+ maskedAccountNumber = "",
+ originalMaskedAccountNumber = "",
+ accountType = "",
+ vpaEntityList = emptyList(),
+ isMPinSet = true,
+ mPinLength = "",
+ atmPinLength = "",
+ accountTypeFormatted = "",
+ isAccountPrimary = true,
+ creditLineAllowedMCC = emptyList(),
+ name = "",
+ bankIconImageUrl = "",
+ balance = "",
+ otpLength = "",
+ updatedAt = DateTime(),
+ isAadhaarConsentProvided = true,
+ upiLiteInfo = emptyList(),
+ creditLineNotAllowedMCC = emptyList(),
+ )
+ val result =
+ naviPayOffersHelper.buildParamsMap(
+ paymentAmount = "1000",
+ selectedBankAccount = selectedBankAccount,
+ payeeMcc = "5411",
+ purposeCode = "00",
+ )
+
+ assertEquals(4, result.size)
+ assertEquals("1000", result[OfferResponse.SessionAttributesInfo.Type.TXN_AMOUNT])
+ assertEquals("HDFC", result[OfferResponse.SessionAttributesInfo.Type.UPI_PAYER_BANK_CODE])
+ assertEquals("5411", result[OfferResponse.SessionAttributesInfo.Type.UPI_PAYEE_MCC])
+ assertEquals("00", result[OfferResponse.SessionAttributesInfo.Type.UPI_PURPOSE_CODE])
+ }
+}
diff --git a/android/navi-pay/src/test/kotlin/com/navi/pay/viewmodel/UpiIdViewModelUnitTest.kt b/android/navi-pay/src/test/kotlin/com/navi/pay/viewmodel/UpiIdViewModelUnitTest.kt
index 5a9e412cc5..d9371dc842 100644
--- a/android/navi-pay/src/test/kotlin/com/navi/pay/viewmodel/UpiIdViewModelUnitTest.kt
+++ b/android/navi-pay/src/test/kotlin/com/navi/pay/viewmodel/UpiIdViewModelUnitTest.kt
@@ -9,6 +9,7 @@ package com.navi.pay.viewmodel
import com.navi.base.utils.EMPTY
import com.navi.base.utils.ResourceProvider
+import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity
import com.navi.pay.common.mockCoroutinesDispatcherProvider
import com.navi.pay.common.mockDeviceInfoProvider
@@ -16,6 +17,7 @@ import com.navi.pay.common.model.view.NaviPaySessionHelper
import com.navi.pay.common.usecase.LinkedAccountsUseCase
import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.ValidateVpaUseCase
+import com.navi.pay.management.common.sendmoney.util.NaviPayOffersHelper
import com.navi.pay.management.common.upiid.viewmodel.UPIIdInputViewModel
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
@@ -32,6 +34,8 @@ class UpiIdViewModelUnitTest {
@MockK private lateinit var naviPaySessionHelper: NaviPaySessionHelper
@MockK private lateinit var validateVpaUseCase: ValidateVpaUseCase
@MockK private lateinit var resourceProvider: ResourceProvider
+ @MockK private lateinit var litmusExperimentsUseCase: LitmusExperimentsUseCase
+ @MockK private lateinit var naviPayOffersHelper: NaviPayOffersHelper
@OptIn(ExperimentalCoroutinesApi::class)
private val coroutineDispatcher = UnconfinedTestDispatcher()
@@ -53,6 +57,8 @@ class UpiIdViewModelUnitTest {
naviPaySessionHelper,
validateVpaUseCase,
resourceProvider,
+ naviPayOffersHelper,
+ litmusExperimentsUseCase,
)
// showSuggestions = false and List Empty Cases
@@ -219,6 +225,8 @@ class UpiIdViewModelUnitTest {
naviPaySessionHelper,
validateVpaUseCase,
resourceProvider,
+ naviPayOffersHelper,
+ litmusExperimentsUseCase,
)
Assert.assertEquals(EMPTY, upiIdInputViewModel.getFormattedUpiIdInput(input = EMPTY))
Assert.assertEquals("Adi", upiIdInputViewModel.getFormattedUpiIdInput(input = "Adi"))
diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferData.kt b/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferData.kt
index 23ac3b32d2..ec4949dde5 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferData.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferData.kt
@@ -1,6 +1,6 @@
/*
*
- * * Copyright © 2024 by Navi Technologies Limited
+ * * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -16,9 +16,10 @@ data class OfferData(
val descriptionSuffix: String? = null,
val applicableInfo: List? = null,
val sessionAttributesInfo: OfferResponse.SessionAttributesInfo? = null,
- val offerStrip: OfferResponse.BillPay.OfferStrip? = null,
- val offerAppliedStrip: OfferResponse.BillPay.OfferStrip? = null,
+ val offerStrip: OfferResponse.OfferStrip? = null,
+ val offerAppliedStrip: OfferResponse.OfferStrip? = null,
val tags: List? = null,
+ val isBestOffer: Boolean = false,
)
data class OfferDisabledData(
@@ -30,3 +31,8 @@ data class EvaluatedOffer(
val offer: OfferData,
val disabledData: OfferDisabledData?, // Null if the offer is enabled
)
+
+enum class OfferType {
+ ALL,
+ BEST,
+}
diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferResponse.kt b/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferResponse.kt
index 711bc62267..7cdd4bba4d 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferResponse.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/common/models/OfferResponse.kt
@@ -40,26 +40,43 @@ data class OfferResponse(
)
enum class Type {
- TXN_AMOUNT
+ TXN_AMOUNT,
+ UPI_PURPOSE_CODE,
+ UPI_PAYER_BANK_CODE,
+ UPI_PAYEE_MCC,
}
}
data class BillPay(
- @SerializedName("offerWidget", alternate = ["earnOffer"]) val earnOffer: EarnOffer? = null,
- val offerStrip: OfferStrip? = null,
- val offerAppliedStrip: OfferStrip? = null,
- ) {
- data class EarnOffer(
- val iconUrl: String? = null,
- val title: Title? = null,
- val description: Description? = null,
- val applicableInfo: List? = null,
- )
+ @SerializedName("offerWidget", alternate = ["earnOffer"])
+ override val earnOffer: EarnOffer? = null,
+ override val offerStrip: OfferStrip? = null,
+ override val offerAppliedStrip: OfferStrip? = null,
+ ) : OfferWidget
- data class OfferStrip(val prefixText: String? = null, val suffixText: String? = null)
+ data class NaviPay(
+ @SerializedName("offerWidget", alternate = ["earnOffer"])
+ override val earnOffer: EarnOffer? = null,
+ override val offerStrip: OfferStrip? = null,
+ override val offerAppliedStrip: OfferStrip? = null,
+ ) : OfferWidget
- data class Title(val prefixText: String? = null, val suffixText: String? = null)
-
- data class Description(val prefixText: String? = null, val suffixText: String? = null)
+ interface OfferWidget {
+ val earnOffer: EarnOffer?
+ val offerStrip: OfferStrip?
+ val offerAppliedStrip: OfferStrip?
}
+
+ data class EarnOffer(
+ val iconUrl: String? = null,
+ val title: Title? = null,
+ val description: Description? = null,
+ val applicableInfo: List? = null,
+ )
+
+ data class OfferStrip(val prefixText: String? = null, val suffixText: String? = null)
+
+ data class Title(val prefixText: String? = null, val suffixText: String? = null)
+
+ data class Description(val prefixText: String? = null, val suffixText: String? = null)
}
diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/network/retrofit/RetrofitService.kt b/android/navi-rr/src/main/java/com/navi/rr/common/network/retrofit/RetrofitService.kt
index 28fa5117b7..5a2de5fa15 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/common/network/retrofit/RetrofitService.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/common/network/retrofit/RetrofitService.kt
@@ -194,7 +194,6 @@ interface RetrofitService {
@Header("X-Target") target: String = XTarget.MERLIN.name,
@Path("product") product: String,
@Query("offerType") offerType: String,
- @Query("strategy") strategy: String,
@Body offerRequest: OfferRequest,
): Response>
@@ -204,7 +203,6 @@ interface RetrofitService {
@Header("X-Target") target: String = XTarget.MERLIN.name,
@Path("product") product: String,
@Query("offerType") offerType: String,
- @Query("strategy") strategy: String,
@Body offerRequest: List,
): Response
diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/repo/OffersRepo.kt b/android/navi-rr/src/main/java/com/navi/rr/common/repo/OffersRepo.kt
index 3d5fe6434c..b537fc3c32 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/common/repo/OffersRepo.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/common/repo/OffersRepo.kt
@@ -15,6 +15,7 @@ import com.navi.rr.common.models.CoinBurnData
import com.navi.rr.common.models.MultipleOffersResponse
import com.navi.rr.common.models.OfferRequest
import com.navi.rr.common.models.OfferResponse
+import com.navi.rr.common.models.OfferType
import com.navi.rr.common.network.retrofit.ResponseHandler
import com.navi.rr.common.network.retrofit.RetrofitService
import com.navi.rr.utils.cachemanager.CacheHandlerProxy
@@ -58,8 +59,7 @@ constructor(
.addKeyIfMissing(OS_TYPE, OFFERS_OS_TYPE_ANDROID),
requestId = requestId,
),
- offerType = ALL,
- strategy = CAMPAIGN_PRIORITY,
+ offerType = OfferType.ALL.name,
)
.toGenericResponse(),
metricInfo = metricInfo,
@@ -72,7 +72,6 @@ constructor(
product: String,
offerRequestList: List? = emptyList(),
offerType: String? = null,
- strategy: String? = null,
metricInfo: MetricInfo>,
): Flow> {
return cacheHandlerProxy.fetchData(
@@ -83,8 +82,7 @@ constructor(
.fetchOffersForProductsForMultipleRequests(
product = product,
offerRequest = offerRequestList ?: emptyList(),
- offerType = offerType ?: ALL,
- strategy = strategy ?: CAMPAIGN_PRIORITY,
+ offerType = offerType ?: OfferType.ALL.name,
)
.toGenericResponse(),
metricInfo = metricInfo,
@@ -109,6 +107,5 @@ constructor(
private companion object {
const val ALL = "ALL"
- const val CAMPAIGN_PRIORITY = "CAMPAIGN_PRIORITY"
}
}
diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferBottomSheetWidget.kt b/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferBottomSheetWidget.kt
index 3957898f92..cc4c1fecd6 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferBottomSheetWidget.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferBottomSheetWidget.kt
@@ -231,11 +231,16 @@ private fun OfferList(
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 16.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (params.isNullOrEmpty()) {
items(offerData) { offer ->
OfferTicketItem().DefaultOfferTicketItem(offerData = offer)
+ Spacer(
+ modifier =
+ Modifier.height(
+ if (offer.isBestOffer && offerData.size > 1) 4.dp else 12.dp
+ )
+ )
}
} else {
items(sortedOffers) { evaluatedOffer ->
@@ -255,15 +260,22 @@ private fun OfferList(
disabledData = evaluatedOffer.disabledData,
)
}
+ Spacer(
+ modifier =
+ Modifier.height(
+ if (evaluatedOffer.offer.isBestOffer && offerData.size > 1) 4.dp
+ else 12.dp
+ )
+ )
}
}
- item { BottomSheetFooter(imageUrl = BOTTOM_SHEET_FOOTER_URL) }
+ item { BottomSheetFooter() }
}
}
@Composable
-private fun BottomSheetFooter(imageUrl: String) {
+private fun BottomSheetFooter() {
Column(
modifier = Modifier.fillMaxWidth().padding(top = 12.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -272,7 +284,7 @@ private fun BottomSheetFooter(imageUrl: String) {
model =
ImageRequest.Builder(LocalContext.current)
.allowHardware(false)
- .data(imageUrl)
+ .data(BOTTOM_SHEET_FOOTER_URL)
.build(),
contentDescription = EMPTY,
modifier = Modifier.wrapContentSize(),
diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferTicketItem.kt b/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferTicketItem.kt
index 5ffd4ffbea..0c62533964 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferTicketItem.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/common/ui/OfferTicketItem.kt
@@ -21,6 +21,7 @@ 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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -54,6 +55,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.base.utils.SPACE
import com.navi.base.utils.orZero
+import com.navi.common.extensions.conditional
import com.navi.common.shape.TicketWidgetShape
import com.navi.common.utils.Constants.DEBOUNCE_TIME
import com.navi.design.font.FontWeightEnum
@@ -65,7 +67,6 @@ import com.navi.rr.R
import com.navi.rr.common.models.CoinBurnData
import com.navi.rr.common.models.OfferData
import com.navi.rr.common.models.OfferDisabledData
-import com.navi.rr.common.models.OfferResponse
import com.navi.rr.common.theme.color.NaviRRColor
import com.navi.rr.common.views.CommonAsyncImage
import com.navi.rr.common.views.DashedDivider
@@ -81,115 +82,171 @@ class OfferTicketItem {
fun DefaultOfferTicketItem(modifier: Modifier = Modifier, offerData: OfferData) {
var isExpanded by remember { mutableStateOf(false) }
var heightFromTop by remember { mutableFloatStateOf(0f) }
- TicketItem(modifier = modifier, heightFromTop = heightFromTop) {
- Column {
- Column(modifier = Modifier.onSizeChanged { heightFromTop = it.height.toFloat() }) {
- Row(
- modifier =
- Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
+
+ Column {
+ if (offerData.isBestOffer) {
+ BestOfferStrip(
+ modifier =
+ Modifier.fillMaxWidth()
+ .background(
+ color = NaviRRColor.purpleText,
+ shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp),
+ )
+ )
+ }
+ TicketItem(
+ modifier = modifier.conditional(offerData.isBestOffer) { offset(y = (-8).dp) },
+ heightFromTop = heightFromTop,
+ ) {
+ Column {
+ Column(
+ modifier = Modifier.onSizeChanged { heightFromTop = it.height.toFloat() }
) {
- Column(horizontalAlignment = Alignment.Start) {
- Box(
- modifier = Modifier.height(IntrinsicSize.Max),
- contentAlignment = Alignment.CenterStart,
- ) {
- Spacer(
- modifier =
- Modifier.padding(start = 16.dp)
- .background(
- brush =
- Brush.horizontalGradient(
- colors =
- listOf(
- NaviRRColor.earnOfferGradientStart,
- Color.White,
- )
- )
- )
- .width(180.dp)
- .height(24.dp)
- )
- Row(
- modifier = Modifier.padding(start = 18.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Spacer(modifier = Modifier.width(17.dp))
- NaviText(
- text = offerData.titlePrefix.orEmpty(),
- fontSize = 14.sp,
- fontWeight = FontWeight.SemiBold,
- fontFamily = naviFontFamily,
- color = NaviRRColor.primaryText,
- )
- Spacer(modifier = Modifier.width(3.dp))
- Image(
- painter =
- painterResource(id = WidgetsR.drawable.navi_coin_40),
- contentDescription = null,
- modifier = Modifier.size(16.dp),
- )
- Spacer(modifier = Modifier.width(2.dp))
- NaviText(
- text = offerData.titleSuffix.orEmpty(),
- fontSize = 14.sp,
- fontWeight = FontWeight.SemiBold,
- color = NaviRRColor.primaryText,
- fontFamily = naviFontFamily,
- modifier = Modifier,
- )
- }
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Column(horizontalAlignment = Alignment.Start) {
Box(
- modifier =
- Modifier.shadow(
- shape = CircleShape,
- elevation = 8.dp,
- ambientColor = Color.Black,
- spotColor = Color(0x4DD1D9E6),
- )
- .background(Color.White, shape = CircleShape)
- .size(30.dp),
- contentAlignment = Alignment.Center,
+ modifier = Modifier.height(IntrinsicSize.Max),
+ contentAlignment = Alignment.CenterStart,
) {
- CommonAsyncImage(
- imageUrl = offerData.iconUrl.orEmpty(),
- modifier = Modifier.width(24.dp),
- contentScale = ContentScale.FillWidth,
- placeholderIconResId =
- WidgetsR.drawable.navi_widgets_ic_biller_placeholder,
+ Spacer(
+ modifier =
+ Modifier.padding(start = 16.dp)
+ .background(
+ brush =
+ Brush.horizontalGradient(
+ colors =
+ listOf(
+ NaviRRColor
+ .earnOfferGradientStart,
+ Color.White,
+ )
+ )
+ )
+ .width(180.dp)
+ .height(24.dp)
+ )
+ Row(
+ modifier = Modifier.padding(start = 18.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Spacer(modifier = Modifier.width(17.dp))
+ NaviText(
+ text = offerData.titlePrefix.orEmpty(),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ fontFamily = naviFontFamily,
+ color = NaviRRColor.primaryText,
+ )
+ Spacer(modifier = Modifier.width(3.dp))
+ Image(
+ painter =
+ painterResource(
+ id = WidgetsR.drawable.navi_coin_40
+ ),
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ NaviText(
+ text = offerData.titleSuffix.orEmpty(),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = NaviRRColor.primaryText,
+ fontFamily = naviFontFamily,
+ modifier = Modifier,
+ )
+ }
+ Box(
+ modifier =
+ Modifier.shadow(
+ shape = CircleShape,
+ elevation = 8.dp,
+ ambientColor = Color.Black,
+ spotColor = Color(0x4DD1D9E6),
+ )
+ .background(Color.White, shape = CircleShape)
+ .size(30.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ CommonAsyncImage(
+ imageUrl = offerData.iconUrl.orEmpty(),
+ modifier = Modifier.width(24.dp),
+ contentScale = ContentScale.FillWidth,
+ placeholderIconResId =
+ WidgetsR.drawable.navi_widgets_ic_biller_placeholder,
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ NaviText(
+ text = offerData.descriptionPrefix.orEmpty(),
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.secondaryText,
+ )
+ NaviText(
+ text = offerData.descriptionSuffix.orEmpty(),
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ color = NaviRRColor.secondaryText,
)
}
}
-
- Spacer(modifier = Modifier.height(2.dp))
-
- Row(verticalAlignment = Alignment.CenterVertically) {
- NaviText(
- text = offerData.descriptionPrefix.orEmpty(),
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.Medium,
- color = NaviRRColor.secondaryText,
- )
- NaviText(
- text = offerData.descriptionSuffix.orEmpty(),
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.SemiBold,
- color = NaviRRColor.secondaryText,
- )
- }
}
}
+ DashedDivider()
+ // Expandable section
+ BulletPointSection(
+ offerData.applicableInfo,
+ isExpanded,
+ setIsExpanded = { isExpanded = it },
+ )
}
- DashedDivider()
- // Expandable section
- BulletPointSection(
- offerData.applicableInfo,
- isExpanded,
- setIsExpanded = { isExpanded = it },
+ }
+ }
+ }
+
+ @Composable
+ fun BestOfferStrip(modifier: Modifier = Modifier) {
+ Column(modifier = modifier) {
+ Row(
+ modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Image(
+ modifier = Modifier.height(20.dp).padding(vertical = 4.dp),
+ painter = painterResource(WidgetsR.drawable.ic_offer_purple),
+ contentDescription = null,
+ contentScale = ContentScale.FillHeight,
+ )
+ NaviText(
+ text = stringResource(id = R.string.best_offer_caps),
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ color = NaviRRColor.white,
+ letterSpacing = 1.5.sp,
+ modifier = Modifier.padding(horizontal = 6.dp),
+ )
+ Image(
+ modifier = Modifier.height(20.dp).padding(vertical = 4.dp),
+ painter = painterResource(WidgetsR.drawable.ic_offer_purple),
+ contentDescription = null,
+ contentScale = ContentScale.FillHeight,
)
}
+ Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -197,183 +254,214 @@ class OfferTicketItem {
fun AppliedOfferTicketItem(modifier: Modifier = Modifier, offerData: OfferData) {
var isExpanded by remember { mutableStateOf(false) }
var heightFromTop by remember { mutableFloatStateOf(0f) }
- TicketItem(modifier = modifier, heightFromTop = heightFromTop) {
- Column {
- Column(modifier = Modifier.onSizeChanged { heightFromTop = it.height.toFloat() }) {
- Row(
- modifier =
- Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
+
+ Column {
+ if (offerData.isBestOffer) {
+ BestOfferStrip(
+ modifier =
+ Modifier.fillMaxWidth()
+ .background(
+ color = NaviRRColor.purpleText,
+ shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp),
+ )
+ )
+ }
+ TicketItem(
+ modifier = modifier.conditional(offerData.isBestOffer) { offset(y = (-8).dp) },
+ heightFromTop = heightFromTop,
+ ) {
+ Column {
+ Column(
+ modifier = Modifier.onSizeChanged { heightFromTop = it.height.toFloat() }
) {
- Column(horizontalAlignment = Alignment.Start) {
- Box(
- modifier = Modifier.height(IntrinsicSize.Max),
- contentAlignment = Alignment.CenterStart,
- ) {
- Spacer(
- modifier =
- Modifier.padding(start = 16.dp)
- .background(
- brush =
- Brush.horizontalGradient(
- colors =
- listOf(
- NaviRRColor.earnOfferGradientStart,
- Color.White,
- )
- )
- )
- .width(180.dp)
- .height(24.dp)
- )
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Column(horizontalAlignment = Alignment.Start) {
+ Box(
+ modifier = Modifier.height(IntrinsicSize.Max),
+ contentAlignment = Alignment.CenterStart,
) {
- Row(
+ Spacer(
modifier =
- Modifier.padding(start = 18.dp).weight(1f, false),
+ Modifier.padding(start = 16.dp)
+ .background(
+ brush =
+ Brush.horizontalGradient(
+ colors =
+ listOf(
+ NaviRRColor
+ .earnOfferGradientStart,
+ Color.White,
+ )
+ )
+ )
+ .width(180.dp)
+ .height(24.dp)
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
) {
- Spacer(modifier = Modifier.width(17.dp))
Row(
- modifier = Modifier.weight(1f, false),
+ modifier =
+ Modifier.padding(start = 18.dp).weight(1f, false),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start,
+ ) {
+ Spacer(modifier = Modifier.width(17.dp))
+ Row(
+ modifier = Modifier.weight(1f, false),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start,
+ ) {
+ NaviText(
+ text =
+ buildAnnotatedString {
+ append(offerData.titlePrefix.orEmpty())
+ appendInlineContent(
+ SPACER_WIDGET,
+ SPACE,
+ )
+ appendInlineContent(
+ COIN_IMAGE_WIDGET,
+ SPACE,
+ )
+ appendInlineContent(
+ SPACER_WIDGET,
+ SPACE,
+ )
+ append(offerData.titleSuffix.orEmpty())
+ },
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ fontFamily = naviFontFamily,
+ color = NaviRRColor.primaryText,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ inlineContent =
+ mapOf(
+ COIN_IMAGE_WIDGET to
+ InlineTextContent(
+ placeholder =
+ Placeholder(
+ width = 16.sp,
+ height = 16.sp,
+ placeholderVerticalAlign =
+ PlaceholderVerticalAlign
+ .Center,
+ )
+ ) {
+ Image(
+ painter =
+ painterResource(
+ id =
+ WidgetsR
+ .drawable
+ .navi_coin_40
+ ),
+ contentDescription = null,
+ modifier =
+ Modifier.size(16.dp),
+ )
+ },
+ SPACER_WIDGET to
+ InlineTextContent(
+ placeholder =
+ Placeholder(
+ width = 3.sp,
+ height = 0.sp,
+ placeholderVerticalAlign =
+ PlaceholderVerticalAlign
+ .Center,
+ )
+ ) {
+ Spacer(
+ modifier =
+ Modifier.width(3.dp)
+ )
+ },
+ ),
+ )
+ }
+ }
+
+ Column(
+ modifier =
+ Modifier.background(
+ NaviRRColor.burnOfferGradientStart,
+ shape = CircleShape,
+ )
+ .padding(horizontal = 8.dp, vertical = 4.dp)
) {
NaviText(
- text =
- buildAnnotatedString {
- append(offerData.titlePrefix.orEmpty())
- appendInlineContent(SPACER_WIDGET, SPACE)
- appendInlineContent(
- COIN_IMAGE_WIDGET,
- SPACE,
- )
- appendInlineContent(SPACER_WIDGET, SPACE)
- append(offerData.titleSuffix.orEmpty())
- },
- fontSize = 14.sp,
- fontWeight = FontWeight.SemiBold,
- fontFamily = naviFontFamily,
- color = NaviRRColor.primaryText,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- inlineContent =
- mapOf(
- COIN_IMAGE_WIDGET to
- InlineTextContent(
- placeholder =
- Placeholder(
- width = 16.sp,
- height = 16.sp,
- placeholderVerticalAlign =
- PlaceholderVerticalAlign
- .Center,
- )
- ) {
- Image(
- painter =
- painterResource(
- id =
- WidgetsR.drawable
- .navi_coin_40
- ),
- contentDescription = null,
- modifier = Modifier.size(16.dp),
- )
- },
- SPACER_WIDGET to
- InlineTextContent(
- placeholder =
- Placeholder(
- width = 3.sp,
- height = 0.sp,
- placeholderVerticalAlign =
- PlaceholderVerticalAlign
- .Center,
- )
- ) {
- Spacer(
- modifier = Modifier.width(3.dp)
- )
- },
+ text = "Applied",
+ fontSize = 12.sp,
+ fontWeight =
+ getFontWeight(
+ FontWeightEnum.NAVI_HEADLINE_REGULAR
),
+ color = NaviRRColor.progressBarGreen,
+ fontFamily = naviFontFamily,
+ modifier = Modifier,
)
}
}
- Column(
+ Box(
modifier =
- Modifier.background(
- NaviRRColor.burnOfferGradientStart,
+ Modifier.shadow(
shape = CircleShape,
+ elevation = 8.dp,
+ ambientColor = Color.Black,
+ spotColor = Color(0x4DD1D9E6),
)
- .padding(horizontal = 8.dp, vertical = 4.dp)
+ .background(Color.White, shape = CircleShape)
+ .size(30.dp),
+ contentAlignment = Alignment.Center,
) {
- NaviText(
- text = "Applied",
- fontSize = 12.sp,
- fontWeight =
- getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
- color = NaviRRColor.progressBarGreen,
- fontFamily = naviFontFamily,
- modifier = Modifier,
+ CommonAsyncImage(
+ imageUrl = offerData.iconUrl.orEmpty(),
+ modifier = Modifier.width(24.dp),
+ contentScale = ContentScale.FillWidth,
+ placeholderIconResId =
+ WidgetsR.drawable.navi_widgets_ic_biller_placeholder,
)
}
}
- Box(
- modifier =
- Modifier.shadow(
- shape = CircleShape,
- elevation = 8.dp,
- ambientColor = Color.Black,
- spotColor = Color(0x4DD1D9E6),
- )
- .background(Color.White, shape = CircleShape)
- .size(30.dp),
- contentAlignment = Alignment.Center,
- ) {
- CommonAsyncImage(
- imageUrl = offerData.iconUrl.orEmpty(),
- modifier = Modifier.width(24.dp),
- contentScale = ContentScale.FillWidth,
- placeholderIconResId =
- WidgetsR.drawable.navi_widgets_ic_biller_placeholder,
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ NaviText(
+ text = offerData.descriptionPrefix.orEmpty(),
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.secondaryText,
+ )
+ NaviText(
+ text = offerData.descriptionSuffix.orEmpty(),
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ color = NaviRRColor.secondaryText,
)
}
}
-
- Spacer(modifier = Modifier.height(2.dp))
-
- Row(verticalAlignment = Alignment.CenterVertically) {
- NaviText(
- text = offerData.descriptionPrefix.orEmpty(),
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.Medium,
- color = NaviRRColor.secondaryText,
- )
- NaviText(
- text = offerData.descriptionSuffix.orEmpty(),
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.SemiBold,
- color = NaviRRColor.secondaryText,
- )
- }
}
}
+ DashedDivider()
+ // Expandable section
+ BulletPointSection(
+ offerData.applicableInfo,
+ isExpanded,
+ setIsExpanded = { isExpanded = it },
+ )
}
- DashedDivider()
- // Expandable section
- BulletPointSection(
- offerData.applicableInfo,
- isExpanded,
- setIsExpanded = { isExpanded = it },
- )
}
}
}
@@ -386,168 +474,196 @@ class OfferTicketItem {
) {
var isExpanded by remember { mutableStateOf(false) }
var heightFromTop by remember { mutableFloatStateOf(0f) }
- TicketItem(modifier = modifier, heightFromTop = heightFromTop) {
- Column {
- Column(modifier = Modifier.onSizeChanged { heightFromTop = it.height.toFloat() }) {
- Row(
- modifier =
- Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
+
+ Column {
+ if (offerData.isBestOffer) {
+ BestOfferStrip(
+ modifier =
+ Modifier.fillMaxWidth()
+ .background(
+ color = NaviRRColor.purpleText,
+ shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp),
+ )
+ )
+ }
+ TicketItem(
+ modifier = modifier.conditional(offerData.isBestOffer) { offset(y = (-8).dp) },
+ heightFromTop = heightFromTop,
+ ) {
+ Column {
+ Column(
+ modifier = Modifier.onSizeChanged { heightFromTop = it.height.toFloat() }
) {
- Column(horizontalAlignment = Alignment.Start) {
- Box(
- modifier = Modifier.height(IntrinsicSize.Max),
- contentAlignment = Alignment.CenterStart,
- ) {
- Spacer(
- modifier =
- Modifier.padding(start = 16.dp)
- .background(
- brush =
- Brush.horizontalGradient(
- colors =
- listOf(
- NaviRRColor
- .disabledOfferGradientStart,
- Color.White,
- )
- )
- )
- .width(180.dp)
- .height(24.dp)
- )
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Column(horizontalAlignment = Alignment.Start) {
+ Box(
+ modifier = Modifier.height(IntrinsicSize.Max),
+ contentAlignment = Alignment.CenterStart,
) {
- Row(
- modifier = Modifier.padding(start = 18.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Spacer(modifier = Modifier.width(17.dp))
- Row(
- modifier = Modifier.weight(1f, false),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start,
- ) {
- NaviText(
- text =
- buildAnnotatedString {
- append(offerData.titlePrefix.orEmpty())
- appendInlineContent(SPACER_WIDGET, SPACE)
- appendInlineContent(
- COIN_IMAGE_WIDGET,
- SPACE,
+ Spacer(
+ modifier =
+ Modifier.padding(start = 16.dp)
+ .background(
+ brush =
+ Brush.horizontalGradient(
+ colors =
+ listOf(
+ NaviRRColor
+ .disabledOfferGradientStart,
+ Color.White,
+ )
)
- appendInlineContent(SPACER_WIDGET, SPACE)
- append(offerData.titleSuffix.orEmpty())
- },
- fontSize = 14.sp,
- fontWeight = FontWeight.SemiBold,
- fontFamily = naviFontFamily,
- color = NaviRRColor.primaryText,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- inlineContent =
- mapOf(
- COIN_IMAGE_WIDGET to
- InlineTextContent(
- placeholder =
- Placeholder(
- width = 16.sp,
- height = 16.sp,
- placeholderVerticalAlign =
- PlaceholderVerticalAlign
- .Center,
+ )
+ .width(180.dp)
+ .height(24.dp)
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Row(
+ modifier = Modifier.padding(start = 18.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Spacer(modifier = Modifier.width(17.dp))
+ Row(
+ modifier = Modifier.weight(1f, false),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start,
+ ) {
+ NaviText(
+ text =
+ buildAnnotatedString {
+ append(offerData.titlePrefix.orEmpty())
+ appendInlineContent(
+ SPACER_WIDGET,
+ SPACE,
+ )
+ appendInlineContent(
+ COIN_IMAGE_WIDGET,
+ SPACE,
+ )
+ appendInlineContent(
+ SPACER_WIDGET,
+ SPACE,
+ )
+ append(offerData.titleSuffix.orEmpty())
+ },
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ fontFamily = naviFontFamily,
+ color = NaviRRColor.primaryText,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ inlineContent =
+ mapOf(
+ COIN_IMAGE_WIDGET to
+ InlineTextContent(
+ placeholder =
+ Placeholder(
+ width = 16.sp,
+ height = 16.sp,
+ placeholderVerticalAlign =
+ PlaceholderVerticalAlign
+ .Center,
+ )
+ ) {
+ Image(
+ painter =
+ painterResource(
+ id =
+ WidgetsR
+ .drawable
+ .ic_navi_coin_grey
+ ),
+ contentDescription = null,
+ modifier =
+ Modifier.size(16.dp),
)
- ) {
- Image(
- painter =
- painterResource(
- id =
- WidgetsR.drawable
- .ic_navi_coin_grey
- ),
- contentDescription = null,
- modifier = Modifier.size(16.dp),
- )
- },
- SPACER_WIDGET to
- InlineTextContent(
- placeholder =
- Placeholder(
- width = 3.sp,
- height = 0.sp,
- placeholderVerticalAlign =
- PlaceholderVerticalAlign
- .Center,
+ },
+ SPACER_WIDGET to
+ InlineTextContent(
+ placeholder =
+ Placeholder(
+ width = 3.sp,
+ height = 0.sp,
+ placeholderVerticalAlign =
+ PlaceholderVerticalAlign
+ .Center,
+ )
+ ) {
+ Spacer(
+ modifier =
+ Modifier.width(3.dp)
)
- ) {
- Spacer(
- modifier = Modifier.width(3.dp)
- )
- },
- ),
- )
+ },
+ ),
+ )
+ }
}
}
+
+ Box(
+ modifier =
+ Modifier.shadow(
+ shape = CircleShape,
+ elevation = 8.dp,
+ ambientColor = Color.Black,
+ spotColor = Color(0x4DD1D9E6),
+ )
+ .background(Color.White, shape = CircleShape)
+ .size(30.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ CommonAsyncImage(
+ imageUrl = offerData.iconUrl.orEmpty(),
+ modifier = Modifier.width(24.dp),
+ contentScale = ContentScale.FillWidth,
+ placeholderIconResId =
+ WidgetsR.drawable.navi_widgets_ic_biller_placeholder,
+ )
+ }
}
- Box(
- modifier =
- Modifier.shadow(
- shape = CircleShape,
- elevation = 8.dp,
- ambientColor = Color.Black,
- spotColor = Color(0x4DD1D9E6),
- )
- .background(Color.White, shape = CircleShape)
- .size(30.dp),
- contentAlignment = Alignment.Center,
- ) {
- CommonAsyncImage(
- imageUrl = offerData.iconUrl.orEmpty(),
- modifier = Modifier.width(24.dp),
- contentScale = ContentScale.FillWidth,
- placeholderIconResId =
- WidgetsR.drawable.navi_widgets_ic_biller_placeholder,
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ NaviText(
+ text = offerData.descriptionPrefix.orEmpty(),
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.secondaryText,
+ )
+ NaviText(
+ text = offerData.descriptionSuffix.orEmpty(),
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ color = NaviRRColor.secondaryText,
)
}
+
+ Spacer(modifier = Modifier.height(6.dp))
+
+ DisabledOfferItemReason(disabledData = disabledData)
}
-
- Spacer(modifier = Modifier.height(2.dp))
-
- Row(verticalAlignment = Alignment.CenterVertically) {
- NaviText(
- text = offerData.descriptionPrefix.orEmpty(),
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.Medium,
- color = NaviRRColor.secondaryText,
- )
- NaviText(
- text = offerData.descriptionSuffix.orEmpty(),
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.SemiBold,
- color = NaviRRColor.secondaryText,
- )
- }
-
- Spacer(modifier = Modifier.height(6.dp))
-
- DisabledOfferItemReason(disabledData)
}
}
+ DashedDivider()
+ // Expandable section
+ BulletPointSection(
+ offerData.applicableInfo,
+ isExpanded,
+ setIsExpanded = { isExpanded = it },
+ )
}
- DashedDivider()
- // Expandable section
- BulletPointSection(
- offerData.applicableInfo,
- isExpanded,
- setIsExpanded = { isExpanded = it },
- )
}
}
}
@@ -681,42 +797,6 @@ class OfferTicketItem {
}
}
- @Composable
- private fun DisabledOfferItemReason(disabledData: OfferDisabledData) {
- when (disabledData.type) {
- OfferResponse.SessionAttributesInfo.Type.TXN_AMOUNT -> {
- if (disabledData.threshold.toDoubleOrNull() == null) return
- Row(verticalAlignment = Alignment.CenterVertically) {
- NaviText(
- text = TXN_AMOUNT_DISABLED_PREFIX,
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.Medium,
- color = NaviRRColor.textOrange,
- )
- NaviText(
- text =
- buildString {
- append(RUPEE_SYMBOL)
- append(moneyFormat(disabledData.threshold))
- },
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.SemiBold,
- color = NaviRRColor.textOrange,
- )
- NaviText(
- text = TXN_AMOUNT_DISABLED_SUFFIX,
- fontSize = 12.sp,
- fontFamily = naviFontFamily,
- fontWeight = FontWeight.Medium,
- color = NaviRRColor.textOrange,
- )
- }
- }
- }
- }
-
@Composable
private fun TicketItem(
modifier: Modifier = Modifier,
@@ -839,10 +919,4 @@ class OfferTicketItem {
}
}
}
-
- private companion object {
- const val TXN_AMOUNT_DISABLED_PREFIX = "Pay "
- const val TXN_AMOUNT_DISABLED_SUFFIX = " more to get this offer"
- const val RUPEE_SYMBOL = "₹"
- }
}
diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/ui/OffersCommonComposables.kt b/android/navi-rr/src/main/java/com/navi/rr/common/ui/OffersCommonComposables.kt
new file mode 100644
index 0000000000..c49b77d1f8
--- /dev/null
+++ b/android/navi-rr/src/main/java/com/navi/rr/common/ui/OffersCommonComposables.kt
@@ -0,0 +1,107 @@
+/*
+ *
+ * * Copyright © 2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.rr.common.ui
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import com.navi.design.font.naviFontFamily
+import com.navi.naviwidgets.extensions.NaviText
+import com.navi.rr.common.models.OfferDisabledData
+import com.navi.rr.common.models.OfferResponse
+import com.navi.rr.common.theme.color.NaviRRColor
+import com.navi.rr.utils.constants.OffersConstants.RUPEE_SYMBOL
+import com.navi.rr.utils.constants.OffersConstants.TXN_AMOUNT_DISABLED_PREFIX
+import com.navi.rr.utils.constants.OffersConstants.TXN_AMOUNT_DISABLED_SUFFIX
+import com.navi.rr.utils.constants.OffersConstants.UPI_LITE
+import com.navi.rr.utils.constants.OffersConstants.UPI_PAYEE_MCC
+import com.navi.rr.utils.constants.OffersConstants.UPI_PAYER_BANK_CODE
+import com.navi.rr.utils.constants.OffersConstants.UPI_PURPOSE_CODE_PREFIX
+import com.navi.rr.utils.constants.OffersConstants.UPI_PURPOSE_CODE_SUFFIX
+import com.navi.uitron.utils.transformations.moneyFormat
+
+@Composable
+fun DisabledOfferItemReason(disabledData: OfferDisabledData) {
+ when (disabledData.type) {
+ OfferResponse.SessionAttributesInfo.Type.TXN_AMOUNT -> {
+ if (disabledData.threshold.toDoubleOrNull() == null) return
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ NaviText(
+ text = TXN_AMOUNT_DISABLED_PREFIX,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.textOrange,
+ )
+ NaviText(
+ text =
+ buildString {
+ append(RUPEE_SYMBOL)
+ append(moneyFormat(disabledData.threshold))
+ },
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ color = NaviRRColor.textOrange,
+ )
+ NaviText(
+ text = TXN_AMOUNT_DISABLED_SUFFIX,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.textOrange,
+ )
+ }
+ }
+ OfferResponse.SessionAttributesInfo.Type.UPI_PURPOSE_CODE -> {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ NaviText(
+ text = UPI_PURPOSE_CODE_PREFIX,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.textOrange,
+ )
+ NaviText(
+ text = UPI_LITE,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ color = NaviRRColor.textOrange,
+ )
+ NaviText(
+ text = UPI_PURPOSE_CODE_SUFFIX,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.textOrange,
+ )
+ }
+ }
+ OfferResponse.SessionAttributesInfo.Type.UPI_PAYER_BANK_CODE -> {
+ NaviText(
+ text = UPI_PAYER_BANK_CODE,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.textOrange,
+ )
+ }
+ OfferResponse.SessionAttributesInfo.Type.UPI_PAYEE_MCC -> {
+ NaviText(
+ text = UPI_PAYEE_MCC,
+ fontSize = 12.sp,
+ fontFamily = naviFontFamily,
+ fontWeight = FontWeight.Medium,
+ color = NaviRRColor.textOrange,
+ )
+ }
+ }
+}
diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt
index 3e67ccdb5c..184c9ded71 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt
@@ -185,4 +185,12 @@ object OffersConstants {
const val BOTTOM_SHEET_FOOTER_URL =
"https://public-assets.prod.navi-sa.in/bbps/bbps-landing-page-v2/reward_bottomsheet_end.png"
const val TXN_AMOUNT = "TXN_AMOUNT"
+ const val TXN_AMOUNT_DISABLED_PREFIX = "Pay "
+ const val TXN_AMOUNT_DISABLED_SUFFIX = " more to get this offer"
+ const val RUPEE_SYMBOL = "₹"
+ const val UPI_PURPOSE_CODE_PREFIX = "This offer is applicable only on "
+ const val UPI_PURPOSE_CODE_SUFFIX = " transactions"
+ const val UPI_LITE = "UPI Lite"
+ const val UPI_PAYER_BANK_CODE = "This bank is not eligible for this offer"
+ const val UPI_PAYEE_MCC = "This offer is not eligible for this transaction"
}
diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/mapper/OfferResponseToOfferDataMapper.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/mapper/OfferResponseToOfferDataMapper.kt
index 6efefcb515..22a219439c 100644
--- a/android/navi-rr/src/main/java/com/navi/rr/utils/mapper/OfferResponseToOfferDataMapper.kt
+++ b/android/navi-rr/src/main/java/com/navi/rr/utils/mapper/OfferResponseToOfferDataMapper.kt
@@ -7,6 +7,7 @@
package com.navi.rr.utils.mapper
+import com.navi.common.model.ModuleNameV2
import com.navi.rr.common.models.OfferData
import com.navi.rr.common.models.OfferResponse
import com.navi.rr.utils.ext.toJson
@@ -14,22 +15,34 @@ import com.navi.rr.utils.getGsonBuilders
import javax.inject.Inject
class OfferResponseToOfferDataMapper @Inject constructor() {
- fun map(offerResponse: OfferResponse): OfferData {
+ fun map(offerResponse: OfferResponse, vertical: ModuleNameV2): OfferData {
val metadata = offerResponse.metadata
+
+ val naviPayMap =
+ getGsonBuilders()
+ .fromJson(metadata?.get(NAVIPAY).toJson(), OfferResponse.NaviPay::class.java)
+
val billPayMap =
getGsonBuilders()
.fromJson(metadata?.get(BILLPAY).toJson(), OfferResponse.BillPay::class.java)
+ val selectedMap =
+ when (vertical) {
+ ModuleNameV2.NAVIPAY -> naviPayMap
+ ModuleNameV2.BBPS -> billPayMap
+ else -> null // Handle any other cases
+ }
+
return OfferData(
campaignId = offerResponse.campaignId,
- iconUrl = billPayMap?.earnOffer?.iconUrl,
- titlePrefix = billPayMap?.earnOffer?.title?.prefixText,
- titleSuffix = billPayMap?.earnOffer?.title?.suffixText,
- descriptionPrefix = billPayMap?.earnOffer?.description?.prefixText,
- descriptionSuffix = billPayMap?.earnOffer?.description?.suffixText,
- applicableInfo = billPayMap?.earnOffer?.applicableInfo,
- offerStrip = billPayMap?.offerStrip,
- offerAppliedStrip = billPayMap?.offerAppliedStrip,
+ iconUrl = selectedMap?.earnOffer?.iconUrl,
+ titlePrefix = selectedMap?.earnOffer?.title?.prefixText,
+ titleSuffix = selectedMap?.earnOffer?.title?.suffixText,
+ descriptionPrefix = selectedMap?.earnOffer?.description?.prefixText,
+ descriptionSuffix = selectedMap?.earnOffer?.description?.suffixText,
+ applicableInfo = selectedMap?.earnOffer?.applicableInfo,
+ offerStrip = selectedMap?.offerStrip,
+ offerAppliedStrip = selectedMap?.offerAppliedStrip,
sessionAttributesInfo = offerResponse.sessionAttributesInfo,
tags = offerResponse.tags,
)
@@ -37,5 +50,6 @@ class OfferResponseToOfferDataMapper @Inject constructor() {
companion object {
private const val BILLPAY = "billPay"
+ private const val NAVIPAY = "naviPay"
}
}
diff --git a/android/navi-rr/src/main/res/values/strings.xml b/android/navi-rr/src/main/res/values/strings.xml
index a0f269e191..e2aede83f3 100644
--- a/android/navi-rr/src/main/res/values/strings.xml
+++ b/android/navi-rr/src/main/res/values/strings.xml
@@ -47,5 +47,6 @@
Know more
Know less
Navi coins discount
+ BEST OFFER
diff --git a/android/navi-widgets/src/main/res/drawable/navi_coin_12.xml b/android/navi-widgets/src/main/res/drawable/navi_coin_12.xml
new file mode 100644
index 0000000000..f6c9cd66f2
--- /dev/null
+++ b/android/navi-widgets/src/main/res/drawable/navi_coin_12.xml
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+