NTP-60702 | P2C enhancement (#16012)

This commit is contained in:
shreyansu raj
2025-07-02 15:29:56 +05:30
committed by GitHub
parent 17def405de
commit b9616f7d8a
14 changed files with 2240 additions and 7 deletions

View File

@@ -67,12 +67,12 @@ object NaviDateFormatter {
.capitalizeMeridiem()
}
private fun isDifferenceOneDay(transactionTimestamp: DateTime): Boolean {
fun isDifferenceOneDay(transactionTimestamp: DateTime): Boolean {
val currentDate = LocalDate.now()
return currentDate.minusDays(1) == transactionTimestamp.toLocalDate()
}
private fun getTimeInHoursAndMinutes(transactionTimestamp: DateTime, locale: Locale): String {
fun getTimeInHoursAndMinutes(transactionTimestamp: DateTime, locale: Locale): String {
return DateUtils.getFormattedDateTimeAsStringFromDateTimeObject(
dateTime = transactionTimestamp,
format = DATE_TIME_FORMAT_HOUR_MINUTE,
@@ -81,7 +81,7 @@ object NaviDateFormatter {
.capitalizeMeridiem()
}
private fun isSameDay(transactionTimestamp: DateTime): Boolean {
fun isSameDay(transactionTimestamp: DateTime): Boolean {
val currentDate = LocalDate.now()
return currentDate == transactionTimestamp.toLocalDate()
}

View File

@@ -81,9 +81,9 @@ data class DefaultConfigContent(
"Your transaction has been blocked due to potential fraud. Please review the details or contact customer support for help.",
@SerializedName("frequentTransactionMaxColumns") val frequentTransactionsMaxColumns: Int = 4,
@SerializedName("frequentTransactionTotalEntries")
val frequentTransactionsTotalEntries: Int = 12,
val frequentTransactionsTotalEntries: Int = 6,
@SerializedName("frequentTransactionHeading")
val frequentTransactionHeading: String = "Pay again",
val frequentTransactionHeading: String = "Suggestions",
@SerializedName("upiAppLogoS3BaseUrl")
val upiAppLogoS3BaseUrl: String =
"https://public-assets.prod.navi-sa.in/navi-pay/png/upi-app-logos/",

View File

@@ -15,6 +15,8 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.orFalse
import com.navi.common.constants.EMPTY
import com.navi.common.upi.TYPE
import com.navi.common.upi.UpiDataType
@@ -46,6 +48,7 @@ import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity
import com.navi.pay.onboarding.binding.model.view.NaviPayCustomerOnboardingEntity
import com.navi.pay.utils.KEY_UPI_LITE_ACTIVE_ACCOUNT_INFO
import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_RCC_LANDING_EXP
import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT
import com.navi.pay.utils.RUPEE_SYMBOL
import com.navi.pay.utils.UPI_LITE_CONFIG
import dagger.Lazy
@@ -144,6 +147,8 @@ constructor(
refreshGenericOffersUseCase.execute(screenName = SCREEN_NAME)
}
launch(context = Dispatchers.IO) { storeRccLandingExperiment() }
launch(context = Dispatchers.IO) { savePayToContactsExperimentData() }
}
}
@@ -287,4 +292,16 @@ constructor(
}
}
}
private suspend fun savePayToContactsExperimentData() {
val payToContactsExperimentInfo =
litmusExperimentUseCase.execute(
experimentName = LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT
)
PreferenceManager.setBooleanPreference(
LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT,
payToContactsExperimentInfo?.variant?.enabled.orFalse(),
)
}
}

View File

@@ -14,6 +14,7 @@ import android.os.Bundle
import androidx.core.net.toUri
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.model.CtaData
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.EMPTY
import com.navi.common.upi.PAYEE_ENTITY
import com.navi.common.upi.PIN_ACTION
@@ -40,6 +41,7 @@ import com.navi.pay.compose.destinations.NaviPayFaqScreenDestination
import com.navi.pay.compose.destinations.OrderDetailsScreenDestination
import com.navi.pay.compose.destinations.OrderHistoryScreenDestination
import com.navi.pay.compose.destinations.PayToContactsScreenDestination
import com.navi.pay.compose.destinations.PayToContactsScreenV2Destination
import com.navi.pay.compose.destinations.QrScannerScreenDestination
import com.navi.pay.compose.destinations.SavedBeneficiaryScreenDestination
import com.navi.pay.compose.destinations.SendMoneyScreenDestination
@@ -57,6 +59,7 @@ import com.navi.pay.onboarding.account.linked.model.view.LinkedAccountsScreenSou
import com.navi.pay.utils.ACCOUNT_ID
import com.navi.pay.utils.ACTION_PIN_SET
import com.navi.pay.utils.IS_FROM_IAN
import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT
import com.navi.pay.utils.NAVI_PAY_LOCAL_URI_SCHEME
import com.navi.pay.utils.NAVI_PAY_NOTIFICATION_DATA_BUNDLE
import com.navi.pay.utils.NAVI_PAY_NOTIFICATION_ID
@@ -134,7 +137,17 @@ 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
NaviPayScreenType.NAVI_PAY_TO_CONTACTS.name -> {
if (
PreferenceManager.getBooleanPreference(
LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT
)
) {
PayToContactsScreenV2Destination
} else {
PayToContactsScreenDestination
}
}
NaviPayScreenType.NAVI_PAY_SEND_MONEY_SCREEN.name -> {
naviPayActivityDataProvider.setSendMoneyScreenData(
payeeEntity =
@@ -207,7 +220,17 @@ object NaviPayRouter {
return when (screenIdentifier) {
NaviPayScreenType.NAVI_PAY_QR_SCANNER_SCREEN.name -> QrScannerScreenDestination
NaviPayScreenType.NAVI_PAY_VIA_BANK_ACCOUNT.name -> BankDetailInputScreenDestination
NaviPayScreenType.NAVI_PAY_TO_CONTACTS.name -> PayToContactsScreenDestination
NaviPayScreenType.NAVI_PAY_TO_CONTACTS.name -> {
if (
PreferenceManager.getBooleanPreference(
LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT
)
) {
PayToContactsScreenV2Destination
} else {
PayToContactsScreenDestination
}
}
NaviPayScreenType.NAVI_PAY_VIA_UPI_ID.name -> UPIIdInputScreenDestination
NaviPayScreenType.NAVI_PAY_FAQ.name -> NaviPayFaqScreenDestination.invoke()
NaviPayScreenType.NAVI_PAY_FTUE_ONBOARDING.name -> NaviPayEmptyHomeScreenDestination

View File

@@ -0,0 +1,139 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.common.utils
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.utils.coroutine.CoroutineManager
import com.navi.pay.common.utils.NaviPayCommonUtils.getValidPhoneNumberOrEmpty
import com.navi.pay.management.common.sendmoney.model.network.TransactionInitiationType
import com.navi.pay.network.di.NaviPayGsonBuilder
import com.navi.pay.onboarding.account.common.repository.AccountsRepository
import com.navi.pay.tstore.list.model.view.FrequentOrderEntityV2
import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.pay.tstore.list.model.view.OrderStatusOfView
import com.navi.pay.tstore.list.model.view.toFrequentOrderEntityListV2
import com.navi.pay.tstore.list.repository.OrderRepository
import com.navi.pay.utils.NAVI_PAY_SUGGESTED_FREQUENT_ORDER_CACHE_KEY
import com.navi.pay.utils.parallelFilter
import javax.inject.Inject
import kotlinx.coroutines.launch
/**
* Filtering all successful outgoing pay to contact orders
*
* Grouping filtered orders based on vpa, sorting on basis of count and recency
*/
class FrequentOrdersHelperV2
@Inject
constructor(
private val orderRepository: OrderRepository,
private val naviCacheRepository: NaviCacheRepository,
private val accountRepository: AccountsRepository,
@NaviPayGsonBuilder private val gson: Gson,
) {
suspend fun getFrequentOrderList(frequentOrdersTotalEntries: Int): List<FrequentOrderEntityV2> {
val latestCachedFrequentOrderListFromDB =
naviCacheRepository.get(key = NAVI_PAY_SUGGESTED_FREQUENT_ORDER_CACHE_KEY)?.value?.let {
gson.fromJson<List<FrequentOrderEntityV2>>(
it,
object : TypeToken<List<FrequentOrderEntityV2>>() {}.type,
)
} ?: emptyList()
CoroutineManager.io.launch {
processAndRefreshFrequentOrderCache(frequentOrdersTotalEntries)
}
return latestCachedFrequentOrderListFromDB
}
suspend fun processAndRefreshFrequentOrderCache(frequentOrdersTotalEntries: Int) {
val aggregatedOrdersGroupedByVpa =
fetchAndAggregateOrders(frequentOrdersTotalEntries = frequentOrdersTotalEntries)
val finalFrequentEntityList =
aggregatedOrdersGroupedByVpa
.getTopFrequentEntries(frequentOrdersTotalEntries)
.map { it.value.first() }
.toFrequentOrderEntityListV2()
naviCacheRepository.save(
NaviCacheEntity(
key = NAVI_PAY_SUGGESTED_FREQUENT_ORDER_CACHE_KEY,
value = gson.toJson(finalFrequentEntityList),
version = 1,
)
)
}
private suspend fun fetchAndAggregateOrders(
frequentOrdersTotalEntries: Int
): Map<String, MutableList<OrderEntity>> {
var offset = 0
val limit = 500
val aggregatedOrders = mutableMapOf<String, MutableList<OrderEntity>>()
do {
val ordersByOffset =
orderRepository.getOrderEntitiesByOffset(limit = limit, offset = offset)
if (ordersByOffset.isNotEmpty()) {
ordersByOffset
.filteredOrderEntityList()
.groupBy { it.orderDescription }
.getTopFrequentEntries(frequentOrdersTotalEntries)
.forEach { (vpa, orders) ->
aggregatedOrders.getOrPut(vpa) { mutableListOf() }.addAll(orders)
}
}
offset += limit
} while (ordersByOffset.isNotEmpty())
return aggregatedOrders
}
private fun Map<String, List<OrderEntity>>.getTopFrequentEntries(
frequentOrdersTotalEntries: Int
): List<Map.Entry<String, List<OrderEntity>>> {
return this.entries
.sortedWith(
compareByDescending<Map.Entry<String, List<OrderEntity>>> { it.value.size }
.thenByDescending { it.value.first().orderTimestamp }
)
.take(frequentOrdersTotalEntries)
}
private suspend fun List<OrderEntity>.filteredOrderEntityList(): List<OrderEntity> {
val userVpaList = accountRepository.getAllVpa()
val acceptedPaymentMode = listOf(TransactionInitiationType.PAY_TO_CONTACT.name)
val successfulOrderList =
this.parallelFilter {
(it.orderStatusOfView == OrderStatusOfView.Debit ||
it.orderStatusOfView == OrderStatusOfView.Credit)
}
val payToContactOrderList =
successfulOrderList.parallelFilter {
it.naviPayTransactionDetailsMetadata.paymentMode in acceptedPaymentMode &&
!userVpaList.any { vpa -> vpa.equals(it.orderDescription, ignoreCase = true) }
}
val validPhoneNumberOrderList =
payToContactOrderList.parallelFilter {
getValidPhoneNumberOrEmpty(
mobNo = it.naviPayTransactionDetailsMetadata.payeeInfo?.mobNo.orEmpty()
)
.isNotBlank()
}
return validPhoneNumberOrderList
}
}

View File

@@ -42,6 +42,7 @@ import com.navi.pay.common.usecase.BankSenseUseCase
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.utils.FrequentOrdersHelperV2
import com.navi.pay.common.utils.NaviPayCommonUtils
import com.navi.pay.common.utils.NaviPayCommonUtils.getDateTimeObjectFromEpochString
import com.navi.pay.common.utils.getMetricInfo
@@ -137,6 +138,7 @@ constructor(
private val linkedAccountsUseCase: LinkedAccountsUseCase,
@NaviPayGsonBuilder private val gson: Gson,
private val bankSenseUseCase: BankSenseUseCase,
private val frequentOrdersHelper: FrequentOrdersHelperV2,
) : NaviPayBaseVM() {
val naviPayAnalytics: NaviPayAnalytics.NaviPayPaymentSummary =
@@ -602,6 +604,7 @@ constructor(
type = object : TypeToken<NaviPayDefaultConfig>() {}.type,
) ?: NaviPayDefaultConfig()
}
syncFrequentOrderCache()
}
private fun logBiometricStrongStatus() {
@@ -1154,6 +1157,12 @@ constructor(
)
}
private suspend fun syncFrequentOrderCache() {
frequentOrdersHelper.processAndRefreshFrequentOrderCache(
naviPayDefaultConfig.value.config.frequentTransactionsTotalEntries
)
}
private suspend fun updateGenericOffersInfo() {
delay(2.seconds) // delay for segments to update at backend
refreshGenericOffersUseCase.execute(screenName = screenName, shouldHardRefresh = true)

View File

@@ -8,7 +8,9 @@
package com.navi.pay.management.paytocontacts.viewmodel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.model.CtaData
import com.navi.base.utils.QA
import com.navi.base.utils.ResourceProvider
@@ -18,6 +20,7 @@ import com.navi.common.constants.EMPTY
import com.navi.common.extensions.removeSpaces
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.pay.BuildConfig
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_SEND_MONEY_TO_CONTACTS_SCREEN_V2
@@ -48,6 +51,7 @@ 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
import com.navi.pay.management.paytocontacts.model.view.PhoneContactEntity
import com.navi.pay.network.di.NaviPayGsonBuilder
import com.navi.pay.tstore.list.model.view.FrequentOrderEntity
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.FUNNEL_STEP
@@ -94,6 +98,9 @@ constructor(
private val validateVpaUseCase: ValidateVpaUseCase,
private val naviPayActivityDataProvider: NaviPayActivityDataProvider,
private val naviPayAnalytics: NaviPayToContacts,
private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
@NaviPayGsonBuilder private val gsonBuilder: Gson,
private val naviCacheRepository: NaviCacheRepository,
) : NaviPayBaseVM() {
private var naviPayDefaultConfig = NaviPayDefaultConfig()

View File

@@ -0,0 +1,817 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.management.paytocontacts.viewmodel
import androidx.lifecycle.viewModelScope
import com.google.gson.reflect.TypeToken
import com.navi.base.model.CtaData
import com.navi.base.utils.QA
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.orFalse
import com.navi.base.utils.orTrue
import com.navi.common.constants.EMPTY
import com.navi.common.extensions.removeSpaces
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
import com.navi.pay.BuildConfig
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_SEND_MONEY_TO_CONTACTS_SCREEN_V2
import com.navi.pay.analytics.NaviPayToContacts
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.NaviPaySessionHelper
import com.navi.pay.common.model.view.NaviPermissionResult
import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.ValidateVpaUseCase
import com.navi.pay.common.utils.FrequentOrdersHelperV2
import com.navi.pay.common.utils.NaviPayCommonUtils.getHelpCtaData
import com.navi.pay.common.utils.NaviPayCommonUtils.getNormalisedPhoneNumber
import com.navi.pay.common.utils.NaviPayCommonUtils.getValidPhoneNumberOrEmpty
import com.navi.pay.common.utils.fetchUserPhoneNumber
import com.navi.pay.common.viewmodel.NaviPayBaseVM
import com.navi.pay.compose.destinations.DevModeScreenDestination
import com.navi.pay.compose.destinations.LinkedAccountsScreenDestination
import com.navi.pay.compose.destinations.SendMoneyScreenDestination
import com.navi.pay.entry.NaviPayActivityDataProvider
import com.navi.pay.management.common.model.view.WarningErrorInfoState
import com.navi.pay.management.common.sendmoney.model.network.TransactionInitiationType
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.PayeeSeverity
import com.navi.pay.management.common.sendmoney.model.view.SendMoneyScreenSource
import com.navi.pay.management.common.sendmoney.model.view.UpiTransactionType
import com.navi.pay.management.paytocontacts.PhoneContactManager
import com.navi.pay.management.paytocontacts.model.network.PayToContactRequest
import com.navi.pay.management.paytocontacts.model.view.PayToContactsBottomSheetStateHolder
import com.navi.pay.management.paytocontacts.model.view.PhoneContactEntity
import com.navi.pay.tstore.list.model.network.OrderType
import com.navi.pay.tstore.list.model.view.FrequentOrderEntityV2
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.FUNNEL_STEP
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS
import com.navi.pay.utils.INVALID_VPA
import com.navi.pay.utils.NAVI_PAY_SEARCH_QUERY_API_DELAY
import com.navi.pay.utils.NAVI_PAY_WIDGET_CLICKED_KEY
import com.navi.pay.utils.NOT_LINKED_TO_UPI
import com.navi.pay.utils.PHONE_NUMBER_LENGTH
import com.navi.pay.utils.isAsciiDigit
import com.navi.pay.utils.isValidPhoneNumberLength
import com.navi.pay.utils.isValidSearchQuery
import com.navi.pay.utils.phoneNumberWithoutCountryCode
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
@HiltViewModel
class PayToContactsViewModelV2
@Inject
constructor(
private val contactManager: PhoneContactManager,
private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity,
private val naviPayConfigUseCase: NaviPayConfigUseCase,
private val naviPaySessionHelper: NaviPaySessionHelper,
private val frequentOrdersHelper: FrequentOrdersHelperV2,
private val resourceProvider: ResourceProvider,
private val validateVpaUseCase: ValidateVpaUseCase,
private val naviPayActivityDataProvider: NaviPayActivityDataProvider,
private val naviPayAnalytics: NaviPayToContacts,
) : NaviPayBaseVM() {
private var naviPayDefaultConfig = NaviPayDefaultConfig()
private val _uiState = MutableStateFlow(PayToContactsUIState.Loading)
val uiState = _uiState.asStateFlow()
private val _searchQuery = MutableStateFlow("")
val searchQuery = _searchQuery.asStateFlow()
private val _shouldShowYourContactsTitle = MutableStateFlow(true)
val shouldShowYourContactsTitle = _shouldShowYourContactsTitle.asStateFlow()
private val _showLoader = MutableStateFlow(false)
val showLoader = _showLoader.asStateFlow()
private val _showShimmer = MutableStateFlow(false)
val showShimmer = _showShimmer.asStateFlow()
private val _isWarningOrErrorState =
MutableStateFlow(WarningErrorInfoState(isWarningState = false, isErrorState = false))
val isWarningOrErrorState = _isWarningOrErrorState.asStateFlow()
private val _newContact = MutableStateFlow<PhoneContactEntity?>(null)
val newContact = _newContact.asStateFlow()
private val _hideKeyboard = MutableStateFlow(false)
val hideKeyboard = _hideKeyboard.asStateFlow()
private val _activeQueryContactNumber = MutableStateFlow("")
val activeQueryContactNumber = _activeQueryContactNumber.asStateFlow()
private val frequentOrders = MutableStateFlow<List<FrequentOrderEntityV2>>(emptyList())
private val _navigateToNextScreen = MutableSharedFlow<Direction>()
val navigateToNextScreen = _navigateToNextScreen.asSharedFlow()
private val _apiErrorMessage = MutableSharedFlow<String>()
val apiErrorMessage = _apiErrorMessage.asSharedFlow()
private val _isSavedContactListNonEmpty = MutableStateFlow(true)
val isSavedContactListNonEmpty = _isSavedContactListNonEmpty.asStateFlow()
private val _isNewContactVisible = MutableStateFlow(false)
val isNewContactVisible = _isNewContactVisible.asStateFlow()
private val _permissionResult =
MutableStateFlow<NaviPermissionResult>(NaviPermissionResult.None)
val permissionResult = _permissionResult.asStateFlow()
val showPermissionWidget =
permissionResult
.map { permissionResult != NaviPermissionResult.None }
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
private val _navigateToNextScreenFromHelpCta = MutableSharedFlow<CtaData?>()
val navigateToNextScreenFromHelpCta = _navigateToNextScreenFromHelpCta.asSharedFlow()
private val _isSelfTransferCtaVisible = MutableStateFlow(false)
val isSelfTransferCtaVisible = _isSelfTransferCtaVisible.asStateFlow()
private val _bottomSheetStateHolder =
MutableStateFlow(
PayToContactsBottomSheetStateHolder(
showBottomSheet = false,
bottomSheetStateChange = false,
bottomSheetUIState = null,
)
)
val bottomSheetStateHolder = _bottomSheetStateHolder.asStateFlow()
// TODO: Make it private after using proper testing framework
val allContactList = MutableStateFlow<List<PhoneContactEntity>>(emptyList())
private val frequentOrdersTotalEntries =
MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionsTotalEntries)
private val _frequentOrdersHeading =
MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionHeading)
val frequentOrdersHeading = _frequentOrdersHeading.asStateFlow()
private var apiCallJob: Job? = null
private var paymentJob: Job? = null
private var newContactResponse: RepoResult<ValidateVpaResponse>? = null
private var lastSearchQuery = ""
var isPermissionPopupSeenOnLanded = false
var isPermissionLaunchedFromAllowClick = false
private var isContactPermissionGranted = false
private val helpCtaData = getHelpCtaData(screenName = screenName)
val dropOffFunnelStep = naviPayActivityDataProvider.getString(FUNNEL_STEP)
val userPhoneNumber = fetchUserPhoneNumber()
val filteredContactList =
combine(searchQuery, allContactList) { searchQuery, allContactList ->
apiCallJob?.cancel()
var filteredContacts =
allContactList.filter { contact ->
contact.name.contains(searchQuery, ignoreCase = true) ||
phoneNumberContainSearchQuery(
phoneNumber = contact.normalisedPhoneNumber,
searchQuery = searchQuery,
)
}
if (searchQuery == lastSearchQuery) {
return@combine filteredContacts
}
lastSearchQuery = searchQuery
updateSelfTransferCtaVisibility(false)
if (searchQuery.isNotEmpty()) {
val filteredContactsListSizeBefore = filteredContacts.size
filteredContacts =
filteredContacts.filter { contact ->
contact.phoneNumber.removeSpaces().contains(userPhoneNumber).not()
}
val filteredContactsListSizeAfter = filteredContacts.size
if (
filteredContactsListSizeBefore != filteredContactsListSizeAfter ||
userPhoneNumber.contains(searchQuery.phoneNumberWithoutCountryCode())
) {
updateSelfTransferCtaVisibility(true)
}
}
filteredContacts
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = allContactList.value,
)
val filteredFrequentOrdersList =
combine(searchQuery, frequentOrders) { searchQuery, frequentOrders ->
var filteredFrequentOrder =
frequentOrders.filter { frequentOrder ->
val payeeInfo = frequentOrder.payeeInfo ?: return@filter false
(payeeInfo.name.orEmpty().contains(searchQuery, ignoreCase = true) ||
phoneNumberContainSearchQuery(
phoneNumber = payeeInfo.mobNo.orEmpty(),
searchQuery = searchQuery,
))
}
filteredFrequentOrder
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = emptyList(),
)
val isEmptyState =
combine(
searchQuery,
isNewContactVisible,
filteredContactList,
isWarningOrErrorState,
filteredFrequentOrdersList,
) {
searchQuery,
isNewContactVisible,
filteredContactList,
invalidInfoState,
filteredFrequentOrdersList ->
updateShouldShowYourContactsTitle()
filteredContactList.isEmpty() &&
filteredFrequentOrdersList.isEmpty() &&
searchQuery.removeSpaces().isNotEmpty() &&
!isNewContactVisible &&
!invalidInfoState.isWarningState &&
!userPhoneNumber.contains(searchQuery.phoneNumberWithoutCountryCode())
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
init {
viewModelScope.launch(context = Dispatchers.IO) {
Timber.tag("test").d("init viewModel")
initializeSynchronously()
initializeAsynchronously()
}
}
private suspend fun initializeSynchronously() {
updateNaviPaySessionId()
updateNaviPayDefaultConfig()
}
private suspend fun initializeAsynchronously() {
coroutineScope {
launch { triggerEventForShortcutWidget() }
launch { fetchFrequentOrders() }
launch { observeAndHandleSearchResults() }
}
}
private fun triggerEventForShortcutWidget() {
naviPayActivityDataProvider.getString(NAVI_PAY_WIDGET_CLICKED_KEY)?.let {
naviPayAnalytics.onSendToContactShortcutClicked(
eventName = it,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
}
}
private suspend fun observeAndHandleSearchResults() {
combine(searchQuery, filteredContactList, filteredFrequentOrdersList) {
searchQuery,
filteredContactList,
filteredFrequentOrdersList ->
Triple(searchQuery, filteredContactList, filteredFrequentOrdersList)
}
.collectLatest { (searchQuery, filteredContactList, filteredFrequentOrdersList) ->
if (
userPhoneNumber.contains(searchQuery.phoneNumberWithoutCountryCode()) &&
searchQuery.isNotEmpty()
) {
updateSelfTransferCtaVisibility(true)
} else {
updateSelfTransferCtaVisibility(false)
}
updateShouldShowYourContactsTitle()
updateIsWarningOrErrorState(
isError = false,
isWarning =
filteredContactList.isEmpty() &&
filteredFrequentOrdersList.isEmpty() &&
searchQuery.removeSpaces().isValidSearchQuery() &&
searchQuery.removeSpaces().isValidPhoneNumberLength().not() &&
!userPhoneNumber.contains(searchQuery.phoneNumberWithoutCountryCode()),
warningMessage =
resourceProvider.getString(R.string.np_please_enter_valid_number),
)
_isNewContactVisible.update {
filteredContactList.isEmpty() &&
filteredFrequentOrdersList.isEmpty() &&
searchQuery.removeSpaces().isValidSearchQuery() &&
searchQuery.removeSpaces().isValidPhoneNumberLength() &&
!userPhoneNumber.contains(searchQuery.phoneNumberWithoutCountryCode())
}
if (isNewContactVisible.value) {
updateShimmerState(showShimmer = true)
updateNewContact(newContactEntity = null)
fetchNewContactDetails(searchQuery)
} else {
updateShimmerState(showShimmer = false)
}
}
}
private suspend fun updateNaviPayDefaultConfig() {
naviPayDefaultConfig =
naviPayConfigUseCase.execute<NaviPayDefaultConfig>(
configKey = DEFAULT_CONFIG,
type = object : TypeToken<NaviPayDefaultConfig>() {}.type,
) ?: NaviPayDefaultConfig()
updateFrequentOrderTotalEntries(
naviPayDefaultConfig.config.frequentTransactionsTotalEntries
)
updateFrequentOrderHeading(naviPayDefaultConfig.config.frequentTransactionHeading)
}
private suspend fun updateNaviPaySessionId() {
naviPaySessionHelper.createNewSessionId()
}
fun getNaviPaySessionAttributes(): Map<String, String> {
return naviPaySessionHelper.getNaviPaySessionAttributes()
}
private fun updateFrequentOrderTotalEntries(totalEntries: Int) {
frequentOrdersTotalEntries.update { totalEntries }
}
private fun updateFrequentOrderHeading(heading: String) {
_frequentOrdersHeading.update { heading }
}
fun updateSearchQueryStringState(searchQuery: String = "") {
if (searchQuery.length <= naviPayDefaultConfig.config.contactSearchQueryMaxLength) {
_searchQuery.update { searchQuery }
}
}
private fun updateUiState(uiState: PayToContactsUIState) {
_uiState.update { uiState }
}
fun updateHideKeyboard(hideKeyboard: Boolean) {
_hideKeyboard.update { hideKeyboard }
}
private fun updateShimmerState(showShimmer: Boolean) {
_showShimmer.update { showShimmer }
}
private fun updateActiveQueryContactNumber(phoneNumber: String) {
_activeQueryContactNumber.update { phoneNumber }
}
fun cancelPaymentRequest() {
updateShowLoader(showLoader = false)
paymentJob?.cancel()
updateShimmerState(showShimmer = false)
}
fun onPermissionPopupSeenOnLanded() {
isPermissionPopupSeenOnLanded = true
}
fun onPermissionLaunchedFromAllowClick(isFromAllowClick: Boolean) {
isPermissionLaunchedFromAllowClick = isFromAllowClick
}
fun updatePermissionResult(permissionResult: NaviPermissionResult) {
_permissionResult.update { permissionResult }
}
private fun updateIsWarningOrErrorState(
isError: Boolean? = null,
isWarning: Boolean? = null,
warningMessage: String? = null,
errorMessage: String? = null,
) {
_isWarningOrErrorState.update {
it.copy(
isErrorState = isError ?: it.isErrorState,
isWarningState = isWarning ?: it.isWarningState,
warningMessage = warningMessage ?: it.warningMessage,
errorMessage = errorMessage ?: it.errorMessage,
)
}
}
private fun updateShouldShowYourContactsTitle() {
_shouldShowYourContactsTitle.update {
(isNewContactVisible.value.not() &&
!(searchQuery.value.isNotEmpty() && filteredContactList.value.isEmpty())) ||
isSelfTransferCtaVisible.value
}
}
private fun updateNewContact(newContactEntity: PhoneContactEntity?) {
_newContact.update { newContactEntity }
}
fun updateBottomSheetUIState(
showBottomSheet: Boolean,
bottomSheetStateChange: Boolean? = null,
bottomSheetUIState: PayToContactsBottomSheetUIState? = null,
) {
_bottomSheetStateHolder.update {
it.copy(
showBottomSheet = showBottomSheet,
bottomSheetStateChange = bottomSheetStateChange ?: it.bottomSheetStateChange,
bottomSheetUIState = bottomSheetUIState ?: it.bottomSheetUIState,
)
}
}
private suspend fun updateNextScreenDestinationState(direction: Direction) {
_navigateToNextScreen.emit(direction)
}
fun initiatePaymentForNewContact() {
if (isNewContactVisible.value) {
initiatePaymentToContact(
phoneNumber = searchQuery.value,
newContactResponse = newContactResponse,
isNewContact = true,
)
}
}
fun initiatePaymentToFrequentOrder(frequentOrderEntity: FrequentOrderEntityV2) {
val transactionInfo =
if (frequentOrderEntity.orderType == OrderType.RECEIVE_MONEY.name)
frequentOrderEntity.payerInfo
else frequentOrderEntity.payeeInfo
paymentJob =
viewModelScope.launch(Dispatchers.IO) {
if (!naviPayNetworkConnectivity.isInternetConnected()) {
updateHideKeyboard(hideKeyboard = true)
notifyError(getNoInternetErrorConfig())
return@launch
}
updateShowLoader(showLoader = true)
if (frequentOrderEntity.orderType == UpiTransactionType.SELF_PAY.name) {
redirectToSelfTransferScreen()
} else {
updateActiveQueryContactNumber(phoneNumber = transactionInfo?.vpa.orEmpty())
val vpaToBeValidated = transactionInfo?.vpa.orEmpty()
val response =
validateVpaUseCase.execute(
request = ValidateVpaRequest(vpa = vpaToBeValidated),
screenName = screenName,
)
// Temporary fix for mobile number
val payeeVpaToDisplay =
getValidPhoneNumberOrEmpty(mobNo = frequentOrderEntity.payeeInfo?.mobNo)
.ifBlank { frequentOrderEntity.orderDescription }
if (response == null || response.isSuccessWithData()) {
handleAPIResponseSuccessAndUpdateNextDestination(
validateVpaResponseData = response?.data,
phoneNumber = transactionInfo?.mobNo.orEmpty(),
vpa = vpaToBeValidated,
payeeVpaToDisplay = payeeVpaToDisplay,
transactionInitiationType =
getTransactionInitiationType(
transactionInitiationType = frequentOrderEntity.paymentMode
),
)
} else {
handleApiResponseErrorsForInitiatingPayment(response = response)
}
}
}
}
fun initiatePaymentToContact(
phoneNumber: String,
newContactResponse: RepoResult<ValidateVpaResponse>? = null,
isNewContact: Boolean = false,
) {
paymentJob =
viewModelScope.launch(Dispatchers.IO) {
if (!naviPayNetworkConnectivity.isInternetConnected()) {
updateHideKeyboard(hideKeyboard = true)
notifyError(getNoInternetErrorConfig())
return@launch
}
val response =
newContactResponse
?: run {
updateShowLoader(showLoader = true)
updateActiveQueryContactNumber(phoneNumber = phoneNumber)
validateVpaUseCase.execute(
request = PayToContactRequest(payeeMobileNumber = phoneNumber),
screenName = screenName,
)
}
if (response == null || response.isSuccessWithData()) {
handleAPIResponseSuccessAndUpdateNextDestination(
validateVpaResponseData = response?.data,
phoneNumber = phoneNumber,
payeeVpaToDisplay = getNormalisedPhoneNumber(phoneNumber),
transactionInitiationType = TransactionInitiationType.PAY_TO_CONTACT,
isNewContact = isNewContact,
)
} else {
handleApiResponseErrorsForInitiatingPayment(response = response)
}
}
}
private suspend fun handleApiResponseErrorsForInitiatingPayment(
response: RepoResult<ValidateVpaResponse>
) {
updateShowLoader(showLoader = false)
updateActiveQueryContactNumber(phoneNumber = "")
updateHideKeyboard(hideKeyboard = true)
val error = getError(response)
naviPayAnalytics.onVpaVerificationFailure(
errorMessage = error.title,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
when (response.errors?.getOrNull(index = 0)?.code) {
INVALID_VPA -> {
updateBottomSheetUIState(
showBottomSheet = true,
bottomSheetStateChange = true,
bottomSheetUIState = PayToContactsBottomSheetUIState.InvalidVpa,
)
}
NOT_LINKED_TO_UPI -> {
updateBottomSheetUIState(
showBottomSheet = true,
bottomSheetStateChange = true,
bottomSheetUIState = PayToContactsBottomSheetUIState.ContactNotLinked,
)
}
else -> {
notifyError(response = response)
}
}
}
private suspend fun handleAPIResponseSuccessAndUpdateNextDestination(
validateVpaResponseData: ValidateVpaResponse?,
phoneNumber: String,
vpa: String = "",
payeeVpaToDisplay: String,
transactionInitiationType: TransactionInitiationType,
isNewContact: Boolean = false,
) {
val payeeEntity =
PayeeEntity(
externalId = validateVpaResponseData?.saExternalCustomerId,
name =
validateVpaResponseData?.name.orEmpty().ifBlank {
resourceProvider.getString(R.string.np_make_payment_to)
},
vpa = validateVpaResponseData?.vpa ?: vpa,
isMerchant = validateVpaResponseData?.isMerchant.orFalse(),
isVerifiedVpa = validateVpaResponseData != null,
mcc = validateVpaResponseData?.mcc,
severity =
validateVpaResponseData?.riskParams?.severity ?: PayeeSeverity.NO_RISK.name,
bankCode = validateVpaResponseData?.bankCode,
bankIfsc = validateVpaResponseData?.ifsc,
phoneNumber = phoneNumber,
upiNumber = getUpiNumber(validateVpaResponseData, phoneNumber),
isNpciData = validateVpaResponseData?.isNpciData.orFalse(),
featureTags = validateVpaResponseData?.featureTags,
merchantCustomerId = validateVpaResponseData?.merchantCustomerId,
maskedAccountNumber = validateVpaResponseData?.maskedAccountNumber,
bankName = validateVpaResponseData?.bankName,
accountType = validateVpaResponseData?.bankAccountType,
bankAccountUniqueId = validateVpaResponseData?.bankAccountUniqueId,
)
val sendMoneyScreenSource =
SendMoneyScreenSource.PhoneContact(
payeeVpaToDisplay = payeeVpaToDisplay,
transactionInitiationMode = transactionInitiationType,
isNumberPresentInContactsOrTransactedBefore = !isNewContact,
isContactPermissionGranted = isContactPermissionGranted,
)
naviPayActivityDataProvider.setSendMoneyScreenData(
payeeEntity = payeeEntity,
sendMoneyScreenSource = sendMoneyScreenSource,
)
updateNextScreenDestinationState(SendMoneyScreenDestination())
updateShowLoader(showLoader = false)
updateActiveQueryContactNumber(phoneNumber = "")
updateSearchQueryStringState(searchQuery = EMPTY)
}
private suspend fun fetchFrequentOrders() {
val getFrequentOrderList =
frequentOrdersHelper.getFrequentOrderList(
frequentOrdersTotalEntries = frequentOrdersTotalEntries.value
)
updateFrequentOrderEntityList(getFrequentOrderList)
naviPayAnalytics.onSendToContactsLoaded(
naviPaySessionAttributes = getNaviPaySessionAttributes(),
isPermissionGranted = true,
isContactListEmpty = allContactList.value.isEmpty(),
isFrequentOrderListEmpty = frequentOrders.value.isEmpty(),
)
updateUiState(uiState = PayToContactsUIState.Loaded)
}
fun fetchContacts() {
viewModelScope.launch(Dispatchers.IO) {
val savedContactListJob = async { contactManager.getPhoneContacts() }
allContactList.update { savedContactListJob.await() }
_isSavedContactListNonEmpty.update { allContactList.value.isNotEmpty() }
}
}
private fun updateFrequentOrderEntityList(
frequentOrderEntityList: List<FrequentOrderEntityV2>
) {
// Initialize formattedTimestampText for each entity before updating the list
frequentOrderEntityList.forEach { it.formattedTimestampText }
frequentOrders.update { frequentOrderEntityList }
}
private fun updateShowLoader(showLoader: Boolean) {
_showLoader.update { showLoader }
}
private fun updateSelfTransferCtaVisibility(isVisible: Boolean) {
_isSelfTransferCtaVisible.update { isVisible }
}
private fun fetchNewContactDetails(phoneNumber: String) {
apiCallJob =
viewModelScope.launch(Dispatchers.IO) {
if (!naviPayNetworkConnectivity.isInternetConnected()) {
updateShimmerState(showShimmer = false)
updateHideKeyboard(hideKeyboard = true)
notifyError(getNoInternetErrorConfig())
return@launch
}
delay(NAVI_PAY_SEARCH_QUERY_API_DELAY)
validateVpaUseCase
.execute(
request = PayToContactRequest(payeeMobileNumber = phoneNumber),
screenName = screenName,
)
.also { newContactResponse = it }
updateShimmerState(showShimmer = false)
if (newContactResponse?.isSuccessWithData().orTrue()) {
handleAPIResultSuccessForNewContact(phoneNumber = phoneNumber)
} else {
handleAPIResponseErrorsForNewContact(phoneNumber = phoneNumber)
}
}
}
private suspend fun handleAPIResponseErrorsForNewContact(phoneNumber: String) {
if (newContactResponse == null) {
notifyError()
return
}
if (newContactResponse?.errors?.getOrNull(0)?.code == NOT_LINKED_TO_UPI) {
updateIsWarningOrErrorState(
isError = phoneNumber == searchQuery.value,
errorMessage = resourceProvider.getString(R.string.np_not_linked_to_upi_error),
)
} else if (phoneNumber == searchQuery.value) {
updateHideKeyboard(hideKeyboard = true)
notifyError(newContactResponse!!)
newContactResponse?.errors?.firstOrNull()?.message?.let { errorMessage ->
_apiErrorMessage.emit(errorMessage)
}
}
}
private fun handleAPIResultSuccessForNewContact(phoneNumber: String) {
if (newContactResponse?.data?.isMerchant.orFalse()) {
updateIsWarningOrErrorState(
isError = phoneNumber == searchQuery.value,
errorMessage = resourceProvider.getString(R.string.merchant_vpa_send_money_error),
)
} else {
updateNewContact(
PhoneContactEntity(
name = newContactResponse?.data?.name.orEmpty(),
phoneNumber = phoneNumber,
normalisedPhoneNumber = phoneNumber,
)
)
}
}
private fun phoneNumberContainSearchQuery(phoneNumber: String, searchQuery: String): Boolean {
val processedContactNumber =
INDIA_COUNTRY_CODE_WITH_PLUS + getNormalisedPhoneNumber(phoneNumber)
val processedSearchQuery = searchQuery.removeSpaces().trimStart('0')
return processedContactNumber.contains(processedSearchQuery)
}
fun onHelpCtaClicked() {
viewModelScope.launch(Dispatchers.Default) {
if (BuildConfig.FLAVOR == QA && BuildConfig.DEBUG) {
_navigateToNextScreen.emit(DevModeScreenDestination)
} else {
_navigateToNextScreenFromHelpCta.emit(helpCtaData)
}
}
}
fun updateContactPermissionStatus(isContactPermissionGranted: Boolean) {
this.isContactPermissionGranted = isContactPermissionGranted
}
private fun getUpiNumber(
validateVpaResponseData: ValidateVpaResponse?,
phoneNumber: String,
): String? {
return validateVpaResponseData?.upiNumber.orEmpty().ifBlank {
if (validateVpaResponseData?.isUpiNumber.orFalse())
phoneNumber.filter { it.isAsciiDigit() }.takeLast(PHONE_NUMBER_LENGTH)
else null
}
}
fun redirectToSelfTransferScreen() {
viewModelScope.launch(Dispatchers.IO) {
updateShowLoader(showLoader = true)
updateNextScreenDestinationState(
LinkedAccountsScreenDestination(shouldNavigateUp = true)
)
updateShowLoader(showLoader = false)
updateSearchQueryStringState(searchQuery = EMPTY)
}
}
override val screenName: String
get() = NAVI_PAY_SEND_MONEY_TO_CONTACTS_SCREEN_V2
}

View File

@@ -51,4 +51,7 @@ interface VpaDao {
@Query("SELECT * FROM $NAVI_PAY_DATABASE_VPA_TABLE_NAME")
suspend fun getAllVpaEntities(): List<VpaEntity>
@Query("SELECT vpa from $NAVI_PAY_DATABASE_VPA_TABLE_NAME")
suspend fun getAllVpa(): List<String>
}

View File

@@ -314,4 +314,8 @@ constructor(
suspend fun getName(): String? {
return getAllAccounts().firstOrNull()?.name
}
suspend fun getAllVpa(): List<String> {
return vpaDao.getAllVpa()
}
}

View File

@@ -0,0 +1,83 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.tstore.list.model.view
import com.navi.base.utils.DateUtils
import com.navi.base.utils.NaviDateFormatter.TWENTY_FOUR_HOURS_IN_MILLIS
import com.navi.base.utils.NaviDateFormatter.capitalizeMeridiem
import com.navi.base.utils.NaviDateFormatter.isDifferenceOneDay
import com.navi.base.utils.NaviDateFormatter.isSameDay
import com.navi.base.utils.TrustedTimeAccessor
import com.navi.pay.tstore.details.ui.upi.UserTxnInfo
import com.navi.pay.tstore.list.model.network.OrderType
import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR
import com.navi.pay.utils.parallelMap
import org.joda.time.DateTime
data class FrequentOrderEntityV2(
val orderTitle: String,
val orderDescription: String,
val orderTimestamp: DateTime,
val payeeInfo: UserTxnInfo?,
val payerInfo: UserTxnInfo?,
val paymentMode: String?,
val paymentAmount: String,
val orderType: String,
) {
val formattedTimestampText: String
get() {
val transactionTimeInMillis = orderTimestamp.millis
val difference = TrustedTimeAccessor.getCurrentTimeMillis() - transactionTimeInMillis
val isSendMoney = orderType == OrderType.SEND_MONEY.name
val within24Hours = difference < TWENTY_FOUR_HOURS_IN_MILLIS
val isOneDayDifference =
difference < 2 * TWENTY_FOUR_HOURS_IN_MILLIS && isDifferenceOneDay(orderTimestamp)
return when {
within24Hours && isSameDay(orderTimestamp) -> {
if (isSendMoney) "Paid today" else "Received today"
}
(within24Hours && !isSameDay(orderTimestamp)) || isOneDayDifference -> {
if (isSendMoney) "Paid yesterday" else "Received yesterday"
}
else -> {
val formattedDate =
DateUtils.getFormattedDateTimeAsStringFromDateTimeObject(
dateTime = orderTimestamp,
format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR,
)
.capitalizeMeridiem()
if (isSendMoney) {
"Paid on $formattedDate"
} else {
"Received on $formattedDate"
}
}
}
}
}
suspend fun List<OrderEntity>.toFrequentOrderEntityListV2(): List<FrequentOrderEntityV2> {
return this.parallelMap { orderEntity ->
FrequentOrderEntityV2(
orderTitle = orderEntity.orderTitle,
orderDescription = orderEntity.orderDescription,
orderTimestamp = orderEntity.orderTimestamp,
payeeInfo = orderEntity.naviPayTransactionDetailsMetadata.payeeInfo,
payerInfo = orderEntity.naviPayTransactionDetailsMetadata.payerInfo,
paymentMode = orderEntity.naviPayTransactionDetailsMetadata.paymentMode,
paymentAmount = orderEntity.amount,
orderType = orderEntity.orderType,
)
}
}

View File

@@ -176,6 +176,7 @@ const val NAVI_PAY_SIM_PROVIDER_CACHE_KEY = "naviPaySimProviderKey"
const val NAVI_PAY_PACKAGE_NAME_CACHE_KEY = "naviPayPackageNameKey"
const val NAVI_PAY_SSID_CACHE_KEY = "naviPaySsidKey"
const val NAVI_PAY_FREQUENT_ORDER_CACHE_KEY = "naviPayFrequentOrderCacheKey"
const val NAVI_PAY_SUGGESTED_FREQUENT_ORDER_CACHE_KEY = "naviPaySuggestedFrequentOrderCacheKey"
const val NAVI_PAY_FUNNEL_DROPOFF_RETRY_CACHE_KEY = "naviPayFunnelDropoffRetryCacheKey"
const val NAVI_PAY_FUNNEL_STEP_CACHE_KEY = "naviPayFunnelStepCacheKey"
@@ -209,6 +210,7 @@ const val LITMUS_EXPERIMENT_NAVIPAY_REVERSE_SMS_BINDING = "NaviPay-exp-reverse-s
const val LITMUS_EXPERIMENT_NAVIPAY_RCC_LANDING_EXP = "NaviPay-rcc-landing-experience"
const val LITMUS_EXPERIMENT_NAVIPAY_ONBOARDING_COMMS = "NaviPay-exp-onb-comms"
const val LITMUS_EXPERIMENT_NAVIPAY_MULTIBANK = "NaviPay-exp-multibank"
const val LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT = "NaviPay-exp-p2c-frequent-contacts"
val NAVI_PAY_LITMUS_EXPERIMENTS =
listOf(
LITMUS_EXPERIMENT_NAVIPAY_TRANSACTION_LEDGER,
@@ -228,6 +230,7 @@ val NAVI_PAY_LITMUS_EXPERIMENTS =
LITMUS_EXPERIMENT_HPC_MM_ROLLOUT,
LITMUS_EXPERIMENT_NAVIPAY_ONBOARDING_COMMS,
LITMUS_EXPERIMENT_NAVIPAY_MULTIBANK,
LITMUS_EXPERIMENT_NAVI_PAY_FREQUENT_CONTACT,
)
// Generic

View File

@@ -1196,4 +1196,10 @@
<string name="np_qr_code_unavailable_desc">Unfortunately, your QR has been blocked due to suspicious activity. For help, contact us.</string>
<string name="np_select_bank_to_relink">Select bank account to re-link %s as UPI number</string>
<string name="np_select_bank_to_relink_desc">UPI number can only be re-linked to a bank account with UPI ID ending in @naviaxis.</string>
<string name="np_paid_yesterday">Paid yesterday</string>
<string name="np_paid_today">Paid today</string>
<string name="np_paid_on">Paid on %s</string>
<string name="np_received_today">Received today</string>
<string name="np_received_yesterday">Received yesterday</string>
<string name="np_received_on">Received on %s</string>
</resources>