NTP-60702 | P2C enhancement (#16012)
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -314,4 +314,8 @@ constructor(
|
||||
suspend fun getName(): String? {
|
||||
return getAllAccounts().firstOrNull()?.name
|
||||
}
|
||||
|
||||
suspend fun getAllVpa(): List<String> {
|
||||
return vpaDao.getAllVpa()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user