NTP-48074 | Divyesh | Bill refresh before payment (#15539)

This commit is contained in:
Divyesh Shinde
2025-04-11 15:40:00 +05:30
committed by GitHub
parent be514343a0
commit bd36deeb63
31 changed files with 608 additions and 494 deletions

View File

@@ -180,6 +180,7 @@ const val BILL_FETCH_CONSENT_DEFAULT_MESSAGE =
"By continuing, you allow Navi to fetch current bill & send reminders for future bills."
const val TAG_BILL_FETCH_ERROR = "TAG_BILL_FETCH_ERROR"
const val ACTION_CIRCLE_OPERATOR_SELECTION = "ACTION_CIRCLE_OPERATOR_SELECTION"
const val TAG_BILL_ALREADY_PAID_ON_REFETCH = "TAG_BILL_ALREADY_PAID"
// offer experience event attributes and tags
const val RCBP_CATEGORY = "RCBP_CATEGORY"

View File

@@ -49,6 +49,7 @@ data class NaviBbpsDefaultConfig(
val detectedBillsPollingTimeoutMillis: Long = 30000,
@SerializedName("detectedBillsPollingWaitTimeMillis")
val detectedBillsPollingWaitTimeMillis: Long = 2000,
val billAlreadyPaidErrorCodes: List<String> = listOf("INVAL003"),
) {
data class PaymentCheckoutConfigItem(
@SerializedName("categoryId") val categoryId: String = EMPTY,
@@ -488,6 +489,11 @@ data class ConfigMessage(
@SerializedName("invalidContactSelectedMessage")
val invalidContactSelectedDescription: String =
"It looks like you've selected an invalid phone number. Please choose another contact or enter a new phone number",
@SerializedName("billDetailsRefreshedTitle")
val billDetailsUpdatedTitle: String = "Bill details have been updated",
@SerializedName("billDetailsRefreshedDescription")
val billDetailsUpdatedDescription: String =
"We have fetched the latest bill details from your biller. The amount or due date may have changed. Please review before proceeding.",
)
data class CoinUtilisationConfig(

View File

@@ -49,6 +49,7 @@ constructor(
suspend fun fetchBillDetails(
billDetailsRequest: BillDetailsRequest,
metricInfo: MetricInfo<RepoResult<BillDetailsResponse>>,
onSavedBillRefresh: (suspend (BillDetailsResponse) -> Unit)? = null,
): RepoResult<BillDetailsResponse> {
val response =
apiResponseCallback(
@@ -57,7 +58,10 @@ constructor(
metricInfo = metricInfo,
)
if (response.isSuccessWithData()) {
myBillsSyncJob.refreshBillsAsync(screenName = metricInfo.screen)
myBillsSyncJob.refreshBillsAsync(
screenName = metricInfo.screen,
onSavedBillRefresh = { onSavedBillRefresh?.invoke(response.data!!) },
)
}
return response
}

View File

@@ -9,6 +9,8 @@ package com.navi.bbps.common.usecase
import com.navi.base.utils.NaviNetworkConnectivity
import com.navi.base.utils.orFalse
import com.navi.base.utils.orZero
import com.navi.bbps.common.TAG_BILL_ALREADY_PAID_ON_REFETCH
import com.navi.bbps.common.TAG_BILL_FETCH_ERROR
import com.navi.bbps.common.mapper.BillResponseToBillDetailsEntityMapper
import com.navi.bbps.common.repository.BbpsCommonRepository
@@ -18,6 +20,9 @@ import com.navi.bbps.feature.customerinput.model.network.BillDetailsRequest
import com.navi.bbps.feature.customerinput.model.network.BillDetailsResponse
import com.navi.bbps.feature.customerinput.model.network.DeviceDetails
import com.navi.bbps.feature.customerinput.model.view.BillDetailsEntity
import com.navi.bbps.feature.mybills.MyBillsRepository
import com.navi.bbps.feature.mybills.MyBillsSyncJob
import com.navi.bbps.feature.mybills.model.view.UnpaidBillDetails
import com.navi.bbps.feature.paybill.PayBillViewModel
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
@@ -25,12 +30,16 @@ import javax.inject.Inject
typealias IsConsentRequired = Boolean
typealias IsBillDetailsChanged = Boolean
class FetchBillHandler
@Inject
constructor(
private val bbpsCommonRepository: BbpsCommonRepository,
private val naviNetworkConnectivity: NaviNetworkConnectivity,
private val naviBbpsDateUtils: NaviBbpsDateUtils,
private val myBillsRepository: MyBillsRepository,
private val myBillsSyncJob: MyBillsSyncJob,
) {
suspend fun fetchBill(
payBillViewModel: PayBillViewModel,
@@ -69,6 +78,90 @@ constructor(
}
}
suspend fun refetchBill(
payBillViewModel: PayBillViewModel,
screenName: String,
onReFetchSuccess: suspend (IsBillDetailsChanged, BillDetailsEntity) -> Unit,
billAlreadyPaidErrorCodes: List<String>,
) {
val billDetailsRequest =
BillDetailsRequest(
billerId = payBillViewModel.payBillScreenSource.billerId,
referenceId = payBillViewModel.payBillScreenSource.billDetailsEntity!!.referenceId,
customerParams = payBillViewModel.payBillScreenSource.customerParams,
isConsentProvided =
payBillViewModel.payBillScreenSource.isConsentRequired.orFalse(),
deviceDetails = DeviceDetails(ip = naviNetworkConnectivity.getIpAddress()),
)
val billDetailsResponse =
bbpsCommonRepository.fetchBillDetails(
billDetailsRequest = billDetailsRequest,
metricInfo = getBbpsMetricInfo(screenName = screenName, isNae = { false }),
onSavedBillRefresh = { billDetailsResponse ->
val billDetailsEntity =
BillResponseToBillDetailsEntityMapper(naviBbpsDateUtils)
.map(billDetailsResponse)
val billFetchTimestamp = billDetailsResponse.apiCallTimestamp?.millis.orZero()
if (
billFetchTimestamp >
payBillViewModel.myBillEntity.value
?.unpaidBillDetails
?.lastGeneratedTimestamp
?.toLongOrNull()
.orZero() && billDetailsResponse.isBillDetailsChanged
) {
payBillViewModel.myBillEntity.value?.let {
val updatedMyBillEntity =
payBillViewModel.myBillEntity.value!!.copy(
unpaidBillDetails =
UnpaidBillDetails(
referenceId = billDetailsEntity.referenceId,
amount = billDetailsEntity.amount,
billDate = billDetailsEntity.billDate,
dueDate = billDetailsEntity.dueDate,
billNumber = billDetailsEntity.billNumber,
accountHolderName = billDetailsEntity.accountHolderName,
billerAdditionalParams =
billDetailsEntity.billerAdditionalParams,
planItemDetails =
payBillViewModel.myBillEntity.value
?.unpaidBillDetails
?.planItemDetails ?: emptyMap(),
lastGeneratedTimestamp =
billDetailsResponse.apiCallTimestamp
?.millis
.toString(),
)
)
myBillsRepository.updateSavedBill(myBillEntity = updatedMyBillEntity)
}
}
onReFetchSuccess(
billDetailsResponse.isBillDetailsChanged.orFalse(),
billDetailsEntity,
)
},
)
if (!billDetailsResponse.isSuccessWithData() && !payBillViewModel.isPayButtonClicked) {
val error = payBillViewModel.getError(billDetailsResponse)
if (error.code in billAlreadyPaidErrorCodes) {
payBillViewModel.notifyError(
response = billDetailsResponse,
cancelable = false,
tag = TAG_BILL_ALREADY_PAID_ON_REFETCH,
)
myBillsSyncJob.refreshBillsAsync(screenName = screenName)
}
}
}
private suspend fun showBillError(
payBillViewModel: PayBillViewModel,
billResponse: RepoResult<BillDetailsResponse>,

View File

@@ -24,7 +24,7 @@ import com.navi.bbps.feature.customerinput.model.view.BillerAdditionalParamsEnti
import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity
import com.navi.bbps.feature.customerinput.model.view.CustomerParamsExtraData
import com.navi.bbps.feature.destinations.CustomerDataInputScreenDestination
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.destinations.PrepaidRechargeScreenDestination
import com.navi.bbps.feature.paybill.model.network.PaymentAmountExactness
import com.navi.bbps.feature.paybill.model.view.PayBillSource
@@ -112,7 +112,7 @@ constructor(
getOthersPayBillSource(bundle)
}
return PayBillScreenV2Destination(
return PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
phoneNumberDetail = phoneContactEntity,
payBillScreenSource = payBillSource,

View File

@@ -36,6 +36,7 @@ constructor(
myBillEntity.unpaidBillDetails?.billerAdditionalParams?.map {
it.toBillerAdditionalParams()
},
isBillDetailsChanged = false,
)
}
}

View File

@@ -48,7 +48,7 @@ import com.navi.bbps.feature.contactlist.model.view.PhoneContactEntity
import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity
import com.navi.bbps.feature.customerinput.model.view.CustomerParamsExtraData
import com.navi.bbps.feature.destinations.CustomerDataInputScreenDestination
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.mybills.MyBillsSyncJob
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.paybill.model.view.PayBillSource
@@ -419,7 +419,7 @@ constructor(
)
val destination =
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
payBillScreenSource = payBillSource,
phoneNumberDetail = phoneNumberDetailEntity,

View File

@@ -52,7 +52,7 @@ import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity
import com.navi.bbps.feature.customerinput.model.view.CustomerParamsExtraData
import com.navi.bbps.feature.destinations.BbpsTransactionDetailsScreenDestination
import com.navi.bbps.feature.destinations.CustomerDataInputScreenDestination
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.destinations.PrepaidRechargeScreenDestination
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.paybill.model.view.PayBillSource
@@ -381,7 +381,7 @@ constructor(
val source = NaviBbpsScreen.NAVI_BBPS_MY_BILL_HISTORY_DETAILS.name
val destination =
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
payBillScreenSource = payBillSource,
phoneNumberDetail = phoneNumberDetail,

View File

@@ -29,6 +29,8 @@ import com.navi.bbps.common.BILLER_UNIQUE_ID
import com.navi.bbps.common.BbpsSharedPreferences
import com.navi.bbps.common.CATEGORY_ID_MOBILE_PREPAID
import com.navi.bbps.common.CoinsSyncManager
import com.navi.bbps.common.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR
import com.navi.bbps.common.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR_INPUT
import com.navi.bbps.common.DEFAULT_RETRY_COUNT
import com.navi.bbps.common.LocalJsonDataSource
import com.navi.bbps.common.NaviBbpsAnalytics
@@ -48,6 +50,7 @@ import com.navi.bbps.common.utils.MyBillEntityToBillCategoryEntityMapper
import com.navi.bbps.common.utils.MyBillEntityToBillerDetailsEntityMapper
import com.navi.bbps.common.utils.NaviBbpsCommonUtils
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getBbpsMetricInfo
import com.navi.bbps.common.utils.NaviBbpsDateUtils
import com.navi.bbps.common.utils.getDefaultConfig
import com.navi.bbps.common.utils.getDisplayableAmount
import com.navi.bbps.common.utils.refresh
@@ -72,7 +75,7 @@ import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity
import com.navi.bbps.feature.customerinput.model.view.CustomerParamsExtraData
import com.navi.bbps.feature.destinations.CustomerDataInputScreenDestination
import com.navi.bbps.feature.destinations.MyBillHistoryDetailsScreenDestination
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.destinations.PrepaidRechargeScreenDestination
import com.navi.bbps.feature.mybills.MyBillsRepository
import com.navi.bbps.feature.mybills.MyBillsSyncJob
@@ -137,6 +140,7 @@ constructor(
val naviNetworkConnectivity: NaviNetworkConnectivity,
private val bbpsCommonRepository: BbpsCommonRepository,
private val resourceProvider: ResourceProvider,
private val naviBbpsDateUtils: NaviBbpsDateUtils,
val uploadUserDataUseCase: UploadUserDataUseCase,
@NaviBbpsGsonBuilder private val naviBbpsGson: Gson,
open val bbpsSharedPreferences: BbpsSharedPreferences,
@@ -308,14 +312,29 @@ constructor(
billerId = billerDetails.billerId,
referenceId = myBillEntity.unpaidBillDetails?.referenceId.orEmpty(),
amount = myBillEntity.unpaidBillDetails?.amount,
billDate = myBillEntity.unpaidBillDetails?.billDate,
dueDate = myBillEntity.unpaidBillDetails?.dueDate,
billDate =
naviBbpsDateUtils
.getFormattedDate(
dateTime = myBillEntity.unpaidBillDetails?.billDate,
inputDateFormat = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR,
outputDateFormat = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR_INPUT,
)
.orEmpty(),
dueDate =
naviBbpsDateUtils
.getFormattedDate(
dateTime = myBillEntity.unpaidBillDetails?.dueDate,
inputDateFormat = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR,
outputDateFormat = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR_INPUT,
)
.orEmpty(),
billNumber = myBillEntity.unpaidBillDetails?.billNumber,
accountHolderName = myBillEntity.unpaidBillDetails?.accountHolderName,
billerAdditionalParams =
myBillEntity.unpaidBillDetails?.billerAdditionalParams?.map {
it.toBillerAdditionalParams()
},
isBillDetailsChanged = false,
)
}
@@ -427,7 +446,7 @@ constructor(
val source = NaviBbpsScreen.NAVI_BBPS_BILL_CATEGORIES.name
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
payBillScreenSource = payBillSource,
phoneNumberDetail = phoneNumberDetail,
@@ -437,6 +456,7 @@ constructor(
)
)
} else {
val billCategoryEntity =
myBillEntityToBillCategoryEntityMapper.mapMyBillEntityToBillCategoryEntity(
myBillEntity
@@ -470,7 +490,7 @@ constructor(
val source = NaviBbpsScreen.NAVI_BBPS_BILL_CATEGORIES.name
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
payBillScreenSource = payBillSource,
phoneNumberDetail = phoneNumberDetail,

View File

@@ -38,6 +38,7 @@ import com.navi.bbps.common.utils.BillDetailsResponseToEntityMapper
import com.navi.bbps.common.utils.MyBillEntityToBillCategoryEntityMapper
import com.navi.bbps.common.utils.MyBillEntityToBillerDetailsEntityMapper
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getBbpsMetricInfo
import com.navi.bbps.common.utils.NaviBbpsDateUtils
import com.navi.bbps.common.utils.OriginSessionAttributes
import com.navi.bbps.common.utils.OriginWidgetStatus
import com.navi.bbps.common.viewmodel.OriginBillDetectionHandler
@@ -86,6 +87,7 @@ constructor(
naviBbpsSessionHelper: NaviBbpsSessionHelper,
coinsSyncManager: CoinsSyncManager,
naviBbpsConfigUseCase: NaviBbpsConfigUseCase,
naviBbpsDateUtils: NaviBbpsDateUtils,
resProvider: ResourceProvider,
bbpsCommonRepository: BbpsCommonRepository,
naviNetworkConnectivity: NaviNetworkConnectivity,
@@ -118,6 +120,7 @@ constructor(
resourceProvider = resourceProvider,
uploadUserDataUseCase = uploadUserDataUseCase,
bbpsSharedPreferences = bbpsSharedPreferences,
naviBbpsDateUtils = naviBbpsDateUtils,
),
BottomSheetController by BottomSheetControllerImpl() {

View File

@@ -49,7 +49,7 @@ import com.navi.bbps.feature.contactlist.model.view.PhoneContactEntity
import com.navi.bbps.feature.customerinput.model.network.BillDetailsRequest
import com.navi.bbps.feature.customerinput.model.network.DeviceDetails
import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.destinations.PrepaidRechargeScreenDestination
import com.navi.bbps.feature.mybills.MyBillsSyncJob
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
@@ -423,7 +423,7 @@ constructor(
_showPostpaidBottomSheet.emit(false)
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategory,
payBillScreenSource = payBillScreenSource,
phoneNumberDetail = phoneNumberDetailEntity,
@@ -596,7 +596,7 @@ constructor(
val screenSource = NaviBbpsScreen.NAVI_BBPS_CONTACT_LIST_SCREEN.name
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategory,
payBillScreenSource = payBillSource,
phoneNumberDetail = phoneNumber,

View File

@@ -52,7 +52,7 @@ import com.navi.bbps.feature.customerinput.model.view.CustomerInputScreenState
import com.navi.bbps.feature.customerinput.model.view.CustomerParam
import com.navi.bbps.feature.customerinput.model.view.CustomerParamValueState
import com.navi.bbps.feature.customerinput.model.view.CustomerParamsExtraData
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.detectedbills.DetectedBillsRepository
import com.navi.bbps.feature.detectedbills.model.view.DetectedBillCustomerInputResult
import com.navi.bbps.feature.mybills.MyBillsRepository
@@ -572,7 +572,7 @@ constructor(
PhoneContactEntity(name = "", phoneNumber = "", normalisedPhoneNumber = "")
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
payBillScreenSource = payBillScreenSource,
phoneNumberDetail = phoneNumberDetail,

View File

@@ -12,6 +12,7 @@ import com.google.gson.annotations.SerializedName
data class BillDetailsRequest(
@SerializedName("billerId") val billerId: String,
@SerializedName("customerParams") val customerParams: Map<String, String>,
@SerializedName("referenceId") val referenceId: String? = null,
@SerializedName("deviceDetails") val deviceDetails: DeviceDetails,
@SerializedName("amount") val amount: String? = null,
@SerializedName("isConsentProvided") val isConsentProvided: Boolean,

View File

@@ -8,16 +8,19 @@
package com.navi.bbps.feature.customerinput.model.network
import com.google.gson.annotations.SerializedName
import org.joda.time.DateTime
data class BillDetailsResponse(
@SerializedName("billId") val billId: String?,
@SerializedName("billerId") val billerId: String,
@SerializedName("isBillDetailsChanged") val isBillDetailsChanged: Boolean,
@SerializedName("referenceId") val referenceId: String,
@SerializedName("amount") val amount: String?,
@SerializedName("billDate") val billDate: String?,
@SerializedName("dueDate") val dueDate: String?,
@SerializedName("billNumber") val billNumber: String?,
@SerializedName("accountHolderName") val accountHolderName: String?,
@SerializedName("apiCallTimestamp") val apiCallTimestamp: DateTime? = null,
@SerializedName("billerAdditionalParams")
val billerAdditionalParams: List<BillerAdditionalParams>?,
)

View File

@@ -10,6 +10,7 @@ package com.navi.bbps.feature.mybills
import com.navi.bbps.feature.mybills.db.MyBillsDao
import com.navi.bbps.feature.mybills.model.network.BillMarkAsPaidResponse
import com.navi.bbps.feature.mybills.model.network.MyBillsResponse
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.network.service.NaviBbpsRetrofitService
import com.navi.common.checkmate.model.MetricInfo
import com.navi.common.network.models.RepoResult
@@ -33,6 +34,10 @@ constructor(
fun fetchMySavedBills() = myBillsDao.getAllBills()
suspend fun updateSavedBill(myBillEntity: MyBillEntity) {
myBillsDao.updateSavedBill(myBillEntity = myBillEntity)
}
suspend fun fetchSavedBillsByCategory(category: String) =
myBillsDao.getBillsByCategory(categoryId = category)

View File

@@ -81,6 +81,8 @@ constructor(
it.unpaidBillDetails?.billerAdditionalParams?.map {
it.toBillerAdditionalParamsEntity()
} ?: emptyList(),
lastGeneratedTimestamp =
it.unpaidBillDetails?.lastGeneratedTimestamp?.millis.toString(),
),
)
}

View File

@@ -27,7 +27,7 @@ constructor(
) {
private var syncJob: Job? = null
suspend fun refreshBills(screenName: String) {
suspend fun refreshBills(screenName: String, onSavedBillRefresh: (suspend () -> Unit)? = null) {
val response =
myBillsRepository.fetchBillsFromNetwork(
metricInfo = getBbpsMetricInfo(screenName = screenName, isNae = { false })
@@ -41,9 +41,13 @@ constructor(
myBillsDao.refresh(myBills)
}
}
onSavedBillRefresh?.invoke()
}
fun refreshBillsAsync(screenName: String): Job? {
fun refreshBillsAsync(
screenName: String,
onSavedBillRefresh: (suspend () -> Unit)? = null,
): Job? {
if (syncJob?.isActive == true) {
try {
syncJob?.cancel()
@@ -53,7 +57,9 @@ constructor(
}
}
syncJob =
CoroutineScope(dispatcherProvider.io).launch { refreshBills(screenName = screenName) }
CoroutineScope(dispatcherProvider.io).launch {
refreshBills(screenName = screenName, onSavedBillRefresh = onSavedBillRefresh)
}
return syncJob
}
}

View File

@@ -61,7 +61,7 @@ import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity
import com.navi.bbps.feature.customerinput.model.view.CustomerParamsExtraData
import com.navi.bbps.feature.destinations.CustomerDataInputScreenDestination
import com.navi.bbps.feature.destinations.MyBillHistoryDetailsScreenDestination
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.destinations.PrepaidRechargeScreenDestination
import com.navi.bbps.feature.detectedbills.DetectedBillsRepository
import com.navi.bbps.feature.detectedbills.model.view.NewAddedBills
@@ -547,7 +547,7 @@ constructor(
val source = NaviBbpsScreen.NAVI_BBPS_MY_SAVED_BILLS.name
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
payBillScreenSource = payBillScreenSource,
phoneNumberDetail = phoneNumberDetail,
@@ -625,7 +625,7 @@ constructor(
val source = NaviBbpsScreen.NAVI_BBPS_MY_SAVED_BILLS.name
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
billCategoryEntity = billCategoryEntity,
payBillScreenSource = payBillScreenSource,
phoneNumberDetail = phoneNumberDetail,

View File

@@ -1,6 +1,6 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -13,6 +13,7 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.navi.bbps.db.NaviBbpsAppDatabase.Companion.NAVI_BBPS_TABLE_MY_SAVED_BILLS
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
@@ -26,6 +27,8 @@ interface MyBillsDao {
@Query("SELECT * FROM $NAVI_BBPS_TABLE_MY_SAVED_BILLS WHERE categoryId == :categoryId")
suspend fun getBillsByCategory(categoryId: String): List<MyBillEntity>
@Update suspend fun updateSavedBill(myBillEntity: MyBillEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(myBills: List<MyBillEntity>)

View File

@@ -9,6 +9,7 @@ package com.navi.bbps.feature.mybills.model.network
import com.google.gson.annotations.SerializedName
import com.navi.bbps.feature.customerinput.model.network.BillerAdditionalParams
import org.joda.time.DateTime
data class MyBillsResponse(@SerializedName("bills") val bills: List<SavedBillItem>?)
@@ -45,6 +46,7 @@ data class UnpaidBillDetailsResponse(
@SerializedName("billNumber") val billNumber: String?,
@SerializedName("accountHolderName") val accountHolderName: String?,
@SerializedName("planItemDetails") val planItemDetails: Map<String, String>?,
@SerializedName("lastGeneratedTimestamp") val lastGeneratedTimestamp: DateTime?,
@SerializedName("billerAdditionalParams")
val billerAdditionalParams: List<BillerAdditionalParams>?,
)

View File

@@ -63,4 +63,5 @@ data class UnpaidBillDetails(
val accountHolderName: String,
val billerAdditionalParams: List<BillerAdditionalParamsEntity>,
val planItemDetails: Map<String, String>,
val lastGeneratedTimestamp: String,
) : Parcelable

View File

@@ -12,9 +12,11 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.navi.adverse.flux.provider.GsonProvider.gson
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.model.CtaData
import com.navi.base.utils.EMPTY
import com.navi.base.utils.NaviNetworkConnectivity
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.TrustedTimeAccessor
import com.navi.base.utils.ZERO_STRING
import com.navi.base.utils.isNotNull
import com.navi.base.utils.orFalse
@@ -40,8 +42,10 @@ import com.navi.bbps.common.model.config.NaviBbpsDefaultConfig
import com.navi.bbps.common.model.view.NaviBbpsSessionHelper
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.usecase.FetchBillHandler
import com.navi.bbps.common.usecase.GetABTestingExperimentUseCase
import com.navi.bbps.common.usecase.NaviBbpsConfigUseCase
import com.navi.bbps.common.usecase.RewardNudgeUseCase
import com.navi.bbps.common.utils.NaviBbpsCommonUtils
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.evaluateMvelExpression
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getBbpsMetricInfo
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getValidatedAmountNumber
@@ -74,16 +78,21 @@ import com.navi.bbps.feature.paybill.model.network.PayBillResponse
import com.navi.bbps.feature.paybill.model.network.PaymentAmountExactness
import com.navi.bbps.feature.paybill.model.view.AmountChipEntity
import com.navi.bbps.feature.paybill.model.view.CoinUtilisationProperties
import com.navi.bbps.feature.paybill.model.view.CoinUtilisationPropertiesV2
import com.navi.bbps.feature.paybill.model.view.CreditCardAmountType
import com.navi.bbps.feature.paybill.model.view.CreditCardPaymentOption
import com.navi.bbps.feature.paybill.model.view.PayBillBottomSheetType
import com.navi.bbps.feature.paybill.model.view.PayBillHeaderState
import com.navi.bbps.feature.paybill.model.view.PayBillScreenState
import com.navi.bbps.feature.paybill.model.view.PayBillSource
import com.navi.bbps.feature.paybill.util.getBillOrderModel
import com.navi.bbps.feature.prepaidrecharge.model.view.OperatorItemEntity
import com.navi.bbps.feature.prepaidrecharge.model.view.PlanItemEntity
import com.navi.common.R as CommonR
import com.navi.common.constants.DBCacheConstants.ARC_NUDGE_RESPONSE_CACHE_KEY
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_BBPS_BILL_REFRESH_DURATION
import com.navi.common.model.common.NudgeDetailEntity
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
@@ -95,6 +104,7 @@ import com.navi.common.utils.Constants
import com.navi.common.utils.Constants.LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY
import com.navi.common.utils.TemporaryStorageHelper
import com.navi.common.utils.toJsonObject
import com.navi.payment.nativepayment.utils.NaviPaymentRewardsEventBus
import com.navi.payment.nativepayment.utils.getDiscountAdjustedAmount
import com.navi.payment.paymentscreen.utils.PaymentNavigator
import com.navi.payment.tstore.repository.TStoreOrderHandler
@@ -104,17 +114,20 @@ import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
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 kotlinx.coroutines.runBlocking
import org.json.JSONObject
@HiltViewModel
@@ -126,6 +139,7 @@ constructor(
private val payBillRepository: PayBillRepository,
private val naviBbpsDateUtils: NaviBbpsDateUtils,
private val naviBbpsConfigUseCase: NaviBbpsConfigUseCase,
private val getABTestingExperimentUseCase: GetABTestingExperimentUseCase,
private val myBillsRepository: MyBillsRepository,
private val tStoreOrderHandler: TStoreOrderHandler,
private val naviNetworkConnectivity: NaviNetworkConnectivity,
@@ -135,6 +149,7 @@ constructor(
private val billCategoriesRepository: BillCategoriesRepository,
private val resProvider: ResourceProvider,
private val rewardsNudgeEntityFetchUseCase: RewardNudgeUseCase,
private val naviPaymentRewardEventBus: NaviPaymentRewardsEventBus,
private val naviCacheRepository: NaviCacheRepository,
private val naviBbpsCommonRepository: BbpsCommonRepository,
private val resourceProvider: ResourceProvider,
@@ -166,7 +181,7 @@ constructor(
private val _initialPaymentAmount = MutableStateFlow("")
val initialPaymentAmount = _initialPaymentAmount.asStateFlow()
internal val _paymentAmount = MutableStateFlow("")
private val _paymentAmount = MutableStateFlow("")
val paymentAmount = _paymentAmount.asStateFlow()
private val _showErrorText = MutableStateFlow(false)
@@ -191,6 +206,10 @@ constructor(
MutableStateFlow<PayBillScreenState>(PayBillScreenState.BillDetailsUnavailable)
val payBillScreenState = _payBillScreenState.asStateFlow()
open val _payBillBottomSheetType =
MutableStateFlow<PayBillBottomSheetType>(PayBillBottomSheetType.None)
val payBillBottomSheetType = _payBillBottomSheetType.asStateFlow()
private val _rewardsNudgeDetailEntity = MutableStateFlow<NudgeDetailEntity?>(null)
val rewardsNudgeDetailEntity = _rewardsNudgeDetailEntity.asStateFlow()
@@ -252,6 +271,11 @@ constructor(
private val amountChips = MutableStateFlow(emptyList<String>())
private val _showBillDetailsUpdatedBottomSheet = MutableSharedFlow<Boolean>()
val showBillDetailsUpdatedBottomSheet = _showBillDetailsUpdatedBottomSheet.asSharedFlow()
var isPayButtonClicked = false
var arcNudgeResponse: ArcNudgeResponse? = null
@Inject lateinit var paymentNavigator: PaymentNavigator
@@ -272,6 +296,25 @@ constructor(
var isIPLPowerPlayThemeExperimentEnabled = false
var updatedBillDetailsEntity: BillDetailsEntity? = null
private val _navigateToNextScreenFromHelpCta = MutableSharedFlow<CtaData?>()
val navigateToNextScreenFromHelpCta = _navigateToNextScreenFromHelpCta.asSharedFlow()
val helpCta = NaviBbpsCommonUtils.getHelpCtaData(NaviBbpsScreen.NAVI_BBPS_PAY_BILL_SCREEN.name)
private val _coinsUtilisationPropertiesV2 = MutableStateFlow<CoinUtilisationPropertiesV2?>(null)
val coinUtilisationPropertiesV2 = _coinsUtilisationPropertiesV2.asStateFlow()
private val _amountAfterCoinDiscount = MutableStateFlow(ZERO_STRING)
val amountAfterCoinDiscount = _amountAfterCoinDiscount.asStateFlow()
private val _isCoinDiscountApplied = MutableStateFlow(false)
val isCoinDiscountApplied = _isCoinDiscountApplied.asStateFlow()
private val _isLottieAnimationShown = MutableStateFlow(false)
val isLottieAnimationShown = _isLottieAnimationShown.asStateFlow()
init {
viewModelScope.launch(dispatcherProvider.io) {
// Concurrent calls section
@@ -281,6 +324,7 @@ constructor(
launch { fetchBillAndUpdateScreen() }
launch { updateScreenState() }
launch { updateBottomRewardsNudgeEntity() }
launch { pmsDiscountListener() }
}
}
@@ -437,48 +481,81 @@ constructor(
)
updateIsBillLoading(isLoading = false)
} else {
fetchMatchingBillAndUpdateEntity()
updateBillDetails()
myBillEntity.value?.unpaidBillDetails.let { unpaidBillDetails ->
if (
TrustedTimeAccessor.getCurrentTimeMillis() -
unpaidBillDetails?.lastGeneratedTimestamp?.toLongOrNull().orZero() >
FirebaseRemoteConfigHelper.getLong(
key = NAVI_BBPS_BILL_REFRESH_DURATION,
defaultValue = 10000L,
)
) {
refetchBillDetails()
}
}
}
}
private fun fetchMatchingBillAndUpdateEntity() {
viewModelScope.launch(dispatcherProvider.io) {
val savedBill = findMatchingSavedBill()
private suspend fun refetchBillDetails() {
fetchBillHandler.refetchBill(
payBillViewModel = this,
onReFetchSuccess = { isBillDetailsChanged, billDetailsEntity ->
if (!isPayButtonClicked && isBillDetailsChanged) {
updatedBillDetailsEntity = billDetailsEntity
_payBillBottomSheetType.update {
PayBillBottomSheetType.BillDetailsUpdateInfo(
title =
naviBbpsDefaultConfig.value.configMessage.billDetailsUpdatedTitle,
description =
naviBbpsDefaultConfig.value.configMessage
.billDetailsUpdatedDescription,
)
}
_showBillDetailsUpdatedBottomSheet.emit(true)
}
},
screenName = naviBbpsVmData.screen.screenName,
billAlreadyPaidErrorCodes = naviBbpsDefaultConfig.value.billAlreadyPaidErrorCodes,
)
}
updateMyBillEntity(myBillEntity = savedBill)
}
fun updateLatestBillDetails() {
_billDetailsEntity.update { updatedBillDetailsEntity }
updateBillDetails()
}
private suspend fun fetchMatchingBillAndUpdateEntity() {
val savedBill = findMatchingSavedBill()
updateMyBillEntity(myBillEntity = savedBill)
}
private fun updateBillDetails() {
if (payBillScreenSource is PayBillSource.Others) {
fetchMatchingBillAndUpdateEntity()
billDetailsEntity.value?.let { updateBillAndAmountValues(billDetailsEntity = it) }
billDetailsEntity.value?.let { updateBillAndAmountValues(billDetailsEntity = it) }
billDetailsEntity.value?.let {
fetchAndUpdateBillerAdditionalParams(billerAdditionalParams = it.billerAdditionalParams)
}
if (billCategoryEntity?.categoryId == CATEGORY_ID_CREDIT_CARD) {
billDetailsEntity.value?.let {
fetchAndUpdateBillerAdditionalParams(
billerAdditionalParams = it.billerAdditionalParams
)
}
if (billCategoryEntity?.categoryId == CATEGORY_ID_CREDIT_CARD) {
billDetailsEntity.value?.let {
updateCreditCardPaymentOptions(billDetailsEntity = it)
viewModelScope.launch(dispatcherProvider.main) {
onCreditCardPaymentOptionSelected(
creditCardPaymentOption = creditCardPaymentOptions.first()
)
}
updateCreditCardPaymentOptions(billDetailsEntity = it)
viewModelScope.launch(dispatcherProvider.main) {
onCreditCardPaymentOptionSelected(
creditCardPaymentOption = creditCardPaymentOptions.first()
)
}
}
if (
!isCategoryOfTypeAmountChipsRequired(
categoryId = billCategoryEntity?.categoryId.orEmpty()
)
) {
updateShouldAutoFocusOnAmount(paymentAmount = paymentAmount.value)
return
}
updateAmountChips(chipsConfigData = naviBbpsDefaultConfig.value.chipsConfigData)
}
if (
!isCategoryOfTypeAmountChipsRequired(
categoryId = billCategoryEntity?.categoryId.orEmpty()
)
) {
updateShouldAutoFocusOnAmount(paymentAmount = paymentAmount.value)
return
}
updateAmountChips(chipsConfigData = naviBbpsDefaultConfig.value.chipsConfigData)
}
private fun updateAmountChips(chipsConfigData: ChipsConfigData) {
@@ -763,6 +840,10 @@ constructor(
}
}
}
if (isCategoryOfTypeAmountChipsRequired(billCategoryEntity?.categoryId.orEmpty())) {
updateAmountChipEntity(amount = newAmountValue)
}
}
internal val isPayButtonEnabled =
@@ -807,11 +888,197 @@ constructor(
}
}
fun getApplicableDiscount() {
if (coinBurnData.value?.expression.isNullOrEmpty()) return
handleDiscountProperties(
discountExpression = coinBurnData.value?.expression.toString(),
cashEquivalentOfDiscount = coinBurnData.value?.cashEquivalentOfDiscount,
coinValue = coinBurnData.value?.config?.coins.toString(),
)
}
private fun handleDiscountProperties(
discountExpression: String,
cashEquivalentOfDiscount: Int?,
coinValue: String,
) {
if (discountExpression.isNotNullAndNotEmpty()) {
val discountAmount = runBlocking {
calculateDiscountedAmountFromFormula(
discountedFormula = discountExpression,
cashEquivalentOfDiscount = cashEquivalentOfDiscount.toString(),
)
}
val coinsUsed = (discountAmount.toInt() * (coinValue.toInt())).toString()
val discountInfoText =
resProvider.getString(
R.string.bbps_save_rupee_symbol_amount,
discountAmount.getDisplayableAmount(),
)
val discountInfoTextAfterDiscount =
resProvider.getString(
R.string.bbps_saved_rupee_symbol_amount,
discountAmount.getDisplayableAmount(),
)
updateCoinsUtilisationProperties(
CoinUtilisationPropertiesV2(
discountAmount = discountAmount,
coinUtilisationText = discountInfoText,
coinUsedForDiscount = coinsUsed.getDisplayableAmount(),
discountAmountTextAfterDiscount = discountInfoTextAfterDiscount,
)
)
} else return
}
private suspend fun calculateDiscountedAmountFromFormula(
discountedFormula: String,
cashEquivalentOfDiscount: String? = null,
): String {
return evaluateMvelExpression(
key = discountedFormula,
data =
mapOf(
"cashEquivalentOfDiscount" to
(cashEquivalentOfDiscount
?: coinsSyncManager.getCashEquivalentOfDiscount().toDoubleOrNull()
?: 0.0),
"billAmount" to (paymentAmount.value.toDoubleOrNull() ?: 0.0),
),
ZERO_STRING,
)
}
private fun updateCoinsUtilisationProperties(
coinUtilisationPropertiesV2: CoinUtilisationPropertiesV2
) {
_coinsUtilisationPropertiesV2.update { coinUtilisationPropertiesV2 }
}
fun onMoreClicked() {
val description =
when (payBillScreenSource) {
is PayBillSource.Prepaid -> {
payBillScreenSource.planItemEntity.packageDescription
}
else -> EMPTY
}
_payBillBottomSheetType.update {
PayBillBottomSheetType.PlanDetails(
title = resProvider.getString(R.string.bbps_plan_details),
description = description,
icon = CommonR.drawable.ic_purple_exclamation,
ctaText = resProvider.getString(R.string.bbps_okay_got_it),
amount = paymentAmount.value.getDisplayableAmount(),
)
}
}
fun enteredAmountInputChanged(
newInput: String,
isAmountChangedByChipSelection: Boolean = false,
) {
viewModelScope.launch(dispatcherProvider.io) {
var newAmount = ""
if (newInput.isNotEmpty() && newInput != ZERO_STRING) {
newAmount =
getValidatedAmountNumber(
text = newInput,
amountMaxLengthAfterDecimal = AMOUNT_MAX_LENGTH_AFTER_DECIMAL,
amountMaxLengthBeforeDecimal = AMOUNT_MAX_LENGTH_BEFORE_DECIMAL,
)
if (isAmountChangedByChipSelection) {
newAmount = newInput
}
}
updatePaymentAmount(newAmountValue = newAmount)
}
}
fun onAppliedDiscountClicked() {
val discountAmount = coinUtilisationPropertiesV2.value?.discountAmount.orEmpty()
if (discountAmount.isNotEmpty()) {
val paymentAmountAfterCoinDiscount =
(paymentAmount.value.toDoubleOrNull().orZero() -
discountAmount.toDoubleOrNull().orZero())
.toString()
_amountAfterCoinDiscount.update { paymentAmountAfterCoinDiscount }
_isCoinDiscountApplied.update { true }
}
}
fun triggerAnimation() {
viewModelScope.launch(dispatcherProvider.io) {
delay(3000)
_isLottieAnimationShown.update { true }
}
}
fun onRemoveDiscountClicked() {
_isCoinDiscountApplied.update { false }
}
fun onOfferRolodexClicked(transactionAmount: Double?) {
offerDataList.value?.let { offerDataList ->
_payBillBottomSheetType.update {
PayBillBottomSheetType.OfferList(
offerData = offerDataList,
transactionAmount = transactionAmount,
offerListHeading =
if (offerDataList.size == 1) {
resourceProvider.getString(
R.string.bbps_offer_category_bottomsheet_count_title_singular,
offerDataList.size,
billCategoryEntity?.title ?: EMPTY,
)
} else {
resourceProvider.getString(
R.string.bbps_offer_category_bottomsheet_count_title_plural,
offerDataList.size,
billCategoryEntity?.title ?: EMPTY,
)
},
coinBurnData = null,
)
}
}
}
fun onHelpCtaClicked() {
viewModelScope.launch(Dispatchers.IO) { updateNavigateToNextScreenOnHelpCta(helpCta) }
}
private suspend fun updateNavigateToNextScreenOnHelpCta(ctaData: CtaData?) {
_navigateToNextScreenFromHelpCta.emit(ctaData)
}
suspend fun pmsDiscountListener() {
naviPaymentRewardEventBus.events.collectLatest { event ->
if (event.showDiscountedAmount) {
onAppliedDiscountClicked()
} else {
onRemoveDiscountClicked()
}
if (event.isDiscountApplyLottieShown) {
_isLottieAnimationShown.update { true }
}
}
}
fun handleDiscountRemovalOnAmountFieldClick() {
if (billCategoryEntity?.categoryId != CATEGORY_ID_MOBILE_PREPAID) {
onRemoveDiscountClicked()
}
}
internal fun updateIsTokenLoading(isLoading: Boolean) {
_isTokenLoading.update { isLoading }
}
internal fun generatePaymentToken() {
isPayButtonClicked = true
viewModelScope.launch(dispatcherProvider.io) {
if (billDetailsEntity.value == null) {
return@launch

View File

@@ -1,346 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.bbps.feature.paybill
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.model.CtaData
import com.navi.base.utils.EMPTY
import com.navi.base.utils.NaviNetworkConnectivity
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.ZERO_STRING
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.base.utils.orZero
import com.navi.bbps.R
import com.navi.bbps.common.AMOUNT_MAX_LENGTH_AFTER_DECIMAL
import com.navi.bbps.common.AMOUNT_MAX_LENGTH_BEFORE_DECIMAL
import com.navi.bbps.common.CATEGORY_ID_MOBILE_PREPAID
import com.navi.bbps.common.CoinsSyncManager
import com.navi.bbps.common.NaviBbpsScreen
import com.navi.bbps.common.model.view.NaviBbpsSessionHelper
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.usecase.FetchBillHandler
import com.navi.bbps.common.usecase.GetABTestingExperimentUseCase
import com.navi.bbps.common.usecase.NaviBbpsConfigUseCase
import com.navi.bbps.common.usecase.RewardNudgeUseCase
import com.navi.bbps.common.utils.NaviBbpsCommonUtils
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.evaluateMvelExpression
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getValidatedAmountNumber
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.isCategoryOfTypeAmountChipsRequired
import com.navi.bbps.common.utils.NaviBbpsDateUtils
import com.navi.bbps.common.utils.getDisplayableAmount
import com.navi.bbps.feature.category.BillCategoriesRepository
import com.navi.bbps.feature.mybills.MyBillsRepository
import com.navi.bbps.feature.paybill.model.network.PaymentAmountExactness
import com.navi.bbps.feature.paybill.model.view.CoinUtilisationPropertiesV2
import com.navi.bbps.feature.paybill.model.view.PayBillBottomSheetType
import com.navi.bbps.feature.paybill.model.view.PayBillSource
import com.navi.common.R as CommonR
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.payment.nativepayment.utils.NaviPaymentRewardsEventBus
import com.navi.payment.tstore.repository.TStoreOrderHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@HiltViewModel
class PayBillViewModelV2
@Inject
constructor(
savedStateHandle: SavedStateHandle,
private val dispatcherProvider: CoroutineDispatcherProvider,
payBillRepository: PayBillRepository,
naviBbpsDateUtils: NaviBbpsDateUtils,
naviBbpsConfigUseCase: NaviBbpsConfigUseCase,
getABTestingExperimentUseCase: GetABTestingExperimentUseCase,
myBillsRepository: MyBillsRepository,
tStoreOrderHandler: TStoreOrderHandler,
naviNetworkConnectivity: NaviNetworkConnectivity,
fetchBillHandler: FetchBillHandler,
naviBbpsSessionHelper: NaviBbpsSessionHelper,
private val coinsSyncManager: CoinsSyncManager,
billCategoriesRepository: BillCategoriesRepository,
private val resProvider: ResourceProvider,
rewardsNudgeEntityFetchUseCase: RewardNudgeUseCase,
private val naviPaymentRewardEventBus: NaviPaymentRewardsEventBus,
naviCacheRepository: NaviCacheRepository,
naviBbpsCommonRepository: BbpsCommonRepository,
private val resourceProvider: ResourceProvider,
private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
) :
PayBillViewModel(
savedStateHandle = savedStateHandle,
dispatcherProvider = dispatcherProvider,
naviBbpsDateUtils = naviBbpsDateUtils,
naviBbpsConfigUseCase = naviBbpsConfigUseCase,
myBillsRepository = myBillsRepository,
tStoreOrderHandler = tStoreOrderHandler,
naviNetworkConnectivity = naviNetworkConnectivity,
fetchBillHandler = fetchBillHandler,
naviBbpsSessionHelper = naviBbpsSessionHelper,
coinsSyncManager = coinsSyncManager,
billCategoriesRepository = billCategoriesRepository,
resProvider = resProvider,
rewardsNudgeEntityFetchUseCase = rewardsNudgeEntityFetchUseCase,
payBillRepository = payBillRepository,
naviCacheRepository = naviCacheRepository,
naviBbpsCommonRepository = naviBbpsCommonRepository,
resourceProvider = resourceProvider,
litmusExperimentsUseCase = litmusExperimentsUseCase,
) {
private val _payBillBottomSheetType =
MutableStateFlow<PayBillBottomSheetType>(PayBillBottomSheetType.None)
val payBillBottomSheetType = _payBillBottomSheetType.asStateFlow()
private val _navigateToNextScreenFromHelpCta = MutableSharedFlow<CtaData?>()
val navigateToNextScreenFromHelpCta = _navigateToNextScreenFromHelpCta.asSharedFlow()
val helpCta = NaviBbpsCommonUtils.getHelpCtaData(NaviBbpsScreen.NAVI_BBPS_PAY_BILL_SCREEN.name)
private val _coinsUtilisationPropertiesV2 = MutableStateFlow<CoinUtilisationPropertiesV2?>(null)
val coinUtilisationPropertiesV2 = _coinsUtilisationPropertiesV2.asStateFlow()
private val _amountAfterCoinDiscount = MutableStateFlow(ZERO_STRING)
val amountAfterCoinDiscount = _amountAfterCoinDiscount.asStateFlow()
private val _isCoinDiscountApplied = MutableStateFlow(false)
val isCoinDiscountApplied = _isCoinDiscountApplied.asStateFlow()
private val _isLottieAnimationShown = MutableStateFlow(false)
val isLottieAnimationShown = _isLottieAnimationShown.asStateFlow()
init {
viewModelScope.launch(dispatcherProvider.io) { launch { pmsDiscountListener() } }
}
override fun updatePaymentAmount(newAmountValue: String) {
val validatedNewAmount =
if (newAmountValue.toDoubleOrNull().orZero() <= 0) "" else newAmountValue
_paymentAmount.update { validatedNewAmount }
val paymentAmountValue = paymentAmount.value.toDoubleOrNull() ?: 0.0
val initialPaymentAmountValue = initialPaymentAmount.value.toDoubleOrNull() ?: 0.0
if (validatedNewAmount.isEmpty()) {
updateShowErrorText(value = false)
} else {
when (paymentAmountExactness.value) {
PaymentAmountExactness.EXACT_AND_ABOVE -> {
updateShowErrorText(value = paymentAmountValue < initialPaymentAmountValue)
}
PaymentAmountExactness.EXACT_AND_BELOW -> {
updateShowErrorText(value = paymentAmountValue > initialPaymentAmountValue)
}
PaymentAmountExactness.EXACT -> {
updateShowErrorText(value = paymentAmountValue != initialPaymentAmountValue)
}
}
}
if (isCategoryOfTypeAmountChipsRequired(billCategoryEntity?.categoryId.orEmpty())) {
updateAmountChipEntity(amount = newAmountValue)
}
}
fun getApplicableDiscount() {
if (coinBurnData.value?.expression.isNullOrEmpty()) return
handleDiscountProperties(
discountExpression = coinBurnData.value?.expression.toString(),
cashEquivalentOfDiscount = coinBurnData.value?.cashEquivalentOfDiscount,
coinValue = coinBurnData.value?.config?.coins.toString(),
)
}
private fun handleDiscountProperties(
discountExpression: String,
cashEquivalentOfDiscount: Int?,
coinValue: String,
) {
if (discountExpression.isNotNullAndNotEmpty()) {
val discountAmount = runBlocking {
calculateDiscountedAmountFromFormula(
discountedFormula = discountExpression,
cashEquivalentOfDiscount = cashEquivalentOfDiscount.toString(),
)
}
val coinsUsed = (discountAmount.toInt() * (coinValue.toInt())).toString()
val discountInfoText =
resProvider.getString(
R.string.bbps_save_rupee_symbol_amount,
discountAmount.getDisplayableAmount(),
)
val discountInfoTextAfterDiscount =
resProvider.getString(
R.string.bbps_saved_rupee_symbol_amount,
discountAmount.getDisplayableAmount(),
)
updateCoinsUtilisationProperties(
CoinUtilisationPropertiesV2(
discountAmount = discountAmount,
coinUtilisationText = discountInfoText,
coinUsedForDiscount = coinsUsed.getDisplayableAmount(),
discountAmountTextAfterDiscount = discountInfoTextAfterDiscount,
)
)
} else return
}
private suspend fun calculateDiscountedAmountFromFormula(
discountedFormula: String,
cashEquivalentOfDiscount: String? = null,
): String {
return evaluateMvelExpression(
key = discountedFormula,
data =
mapOf(
"cashEquivalentOfDiscount" to
(cashEquivalentOfDiscount
?: coinsSyncManager.getCashEquivalentOfDiscount().toDoubleOrNull()
?: 0.0),
"billAmount" to (paymentAmount.value.toDoubleOrNull() ?: 0.0),
),
ZERO_STRING,
)
}
private fun updateCoinsUtilisationProperties(
coinUtilisationPropertiesV2: CoinUtilisationPropertiesV2
) {
_coinsUtilisationPropertiesV2.update { coinUtilisationPropertiesV2 }
}
fun onMoreClicked() {
val description =
when (payBillScreenSource) {
is PayBillSource.Prepaid -> {
payBillScreenSource.planItemEntity.packageDescription
}
else -> EMPTY
}
_payBillBottomSheetType.update {
PayBillBottomSheetType.PlanDetails(
title = resProvider.getString(R.string.bbps_plan_details),
description = description,
icon = CommonR.drawable.ic_purple_exclamation,
ctaText = resProvider.getString(R.string.bbps_okay_got_it),
amount = paymentAmount.value.getDisplayableAmount(),
)
}
}
fun enteredAmountInputChanged(
newInput: String,
isAmountChangedByChipSelection: Boolean = false,
) {
viewModelScope.launch(dispatcherProvider.io) {
var newAmount = ""
if (newInput.isNotEmpty() && newInput != ZERO_STRING) {
newAmount =
getValidatedAmountNumber(
text = newInput,
amountMaxLengthAfterDecimal = AMOUNT_MAX_LENGTH_AFTER_DECIMAL,
amountMaxLengthBeforeDecimal = AMOUNT_MAX_LENGTH_BEFORE_DECIMAL,
)
if (isAmountChangedByChipSelection) {
newAmount = newInput
}
}
updatePaymentAmount(newAmountValue = newAmount)
}
}
fun onAppliedDiscountClicked() {
val discountAmount = coinUtilisationPropertiesV2.value?.discountAmount.orEmpty()
if (discountAmount.isNotEmpty()) {
val paymentAmountAfterCoinDiscount =
(paymentAmount.value.toDoubleOrNull().orZero() -
discountAmount.toDoubleOrNull().orZero())
.toString()
_amountAfterCoinDiscount.update { paymentAmountAfterCoinDiscount }
_isCoinDiscountApplied.update { true }
}
}
fun triggerAnimation() {
viewModelScope.launch(dispatcherProvider.io) {
delay(3000)
_isLottieAnimationShown.update { true }
}
}
fun onRemoveDiscountClicked() {
_isCoinDiscountApplied.update { false }
}
fun onOfferRolodexClicked(transactionAmount: Double?) {
offerDataList.value?.let { offerDataList ->
_payBillBottomSheetType.update {
PayBillBottomSheetType.OfferList(
offerData = offerDataList,
transactionAmount = transactionAmount,
offerListHeading =
if (offerDataList.size == 1) {
resourceProvider.getString(
R.string.bbps_offer_category_bottomsheet_count_title_singular,
offerDataList.size,
billCategoryEntity?.title ?: EMPTY,
)
} else {
resourceProvider.getString(
R.string.bbps_offer_category_bottomsheet_count_title_plural,
offerDataList.size,
billCategoryEntity?.title ?: EMPTY,
)
},
coinBurnData = null,
)
}
}
}
fun onHelpCtaClicked() {
viewModelScope.launch(Dispatchers.IO) { updateNavigateToNextScreenOnHelpCta(helpCta) }
}
private suspend fun updateNavigateToNextScreenOnHelpCta(ctaData: CtaData?) {
_navigateToNextScreenFromHelpCta.emit(ctaData)
}
suspend fun pmsDiscountListener() {
naviPaymentRewardEventBus.events.collectLatest { event ->
if (event.showDiscountedAmount) {
onAppliedDiscountClicked()
} else {
onRemoveDiscountClicked()
}
if (event.isDiscountApplyLottieShown) {
_isLottieAnimationShown.update { true }
}
}
}
fun handleDiscountRemovalOnAmountFieldClick() {
if (billCategoryEntity?.categoryId != CATEGORY_ID_MOBILE_PREPAID) {
onRemoveDiscountClicked()
}
}
}

View File

@@ -26,5 +26,8 @@ sealed class PayBillBottomSheetType {
val transactionAmount: Double?,
) : PayBillBottomSheetType()
data class BillDetailsUpdateInfo(val title: String, val description: String) :
PayBillBottomSheetType()
data object None : PayBillBottomSheetType()
}

View File

@@ -83,7 +83,7 @@ private val AMOUNT_FIELD_FONT_WEIGHT = FontWeightEnum.NAVI_HEADLINE_REGULAR
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AmountTextFieldV2(
fun AmountTextField(
modifier: Modifier,
isReadOnly: Boolean,
focusManager: FocusManager,

View File

@@ -20,15 +20,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.base.utils.EMPTY
import com.navi.base.utils.SPACE
import com.navi.bbps.R
import com.navi.bbps.common.BULLET
import com.navi.bbps.common.SYMBOL_RUPEE
import com.navi.bbps.common.theme.NaviBbpsColor
import com.navi.bbps.common.ui.BbpsOfferBottomSheet
import com.navi.bbps.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButton
import com.navi.bbps.feature.paybill.model.view.PayBillBottomSheetType
import com.navi.common.R as CommonR
import com.navi.common.customview.LoaderRoundedButton
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
@@ -61,6 +65,17 @@ fun PayBillBottomSheetContent(
closeSheet()
}
}
is PayBillBottomSheetType.BillDetailsUpdateInfo -> {
BottomSheetContentWithIconHeaderPrimarySecondaryButton(
iconId = CommonR.drawable.ic_purple_exclamation,
onPrimaryButtonClicked = closeSheet,
description = payBillBottomSheetType.description,
header = payBillBottomSheetType.title,
primaryButton = stringResource(id = R.string.bbps_okay_got_it),
secondaryButton = null,
onSecondaryButtonClicked = {},
)
}
is PayBillBottomSheetType.None -> {
// Do nothing
}

View File

@@ -82,6 +82,7 @@ import com.navi.bbps.common.NaviBbpsScreen
import com.navi.bbps.common.RCBP_CATEGORY
import com.navi.bbps.common.SCROLL_OFFSET_FOR_TITLE_IN_HEADER
import com.navi.bbps.common.SYMBOL_RUPEE
import com.navi.bbps.common.TAG_BILL_ALREADY_PAID_ON_REFETCH
import com.navi.bbps.common.TAG_BILL_FETCH_ERROR
import com.navi.bbps.common.model.config.NaviBbpsDefaultConfig
import com.navi.bbps.common.model.view.LoadingState
@@ -111,12 +112,13 @@ import com.navi.bbps.feature.destinations.BillCategoriesScreenV2Destination
import com.navi.bbps.feature.destinations.MyBillHistoryDetailsScreenDestination
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.mybills.model.view.UnpaidBillDetails
import com.navi.bbps.feature.paybill.PayBillViewModelV2
import com.navi.bbps.feature.paybill.PayBillViewModel
import com.navi.bbps.feature.paybill.model.network.PayBillResponse
import com.navi.bbps.feature.paybill.model.network.PaymentAmountExactness
import com.navi.bbps.feature.paybill.model.view.AmountChipEntity
import com.navi.bbps.feature.paybill.model.view.CoinUtilisationPropertiesV2
import com.navi.bbps.feature.paybill.model.view.CreditCardPaymentOption
import com.navi.bbps.feature.paybill.model.view.PayBillBottomSheetType
import com.navi.bbps.feature.paybill.model.view.PayBillHeaderState
import com.navi.bbps.feature.paybill.model.view.PayBillScreenState
import com.navi.bbps.feature.paybill.model.view.PayBillSource
@@ -159,9 +161,9 @@ import kotlinx.coroutines.launch
@Destination
@Composable
fun PayBillScreenV2(
fun PayBillScreen(
naviBbpsActivity: NaviBbpsActivity,
payBillViewModelV2: PayBillViewModelV2 = hiltViewModel(),
payBillViewModel: PayBillViewModel = hiltViewModel(),
naviCheckoutViewModel: NaviCheckoutViewModelV2 = hiltViewModel(),
billCategoryEntity: BillCategoryEntity,
navigator: DestinationsNavigator,
@@ -172,16 +174,16 @@ fun PayBillScreenV2(
initialSource: String,
naviBbpsAnalytics: NaviBbpsAnalytics.PayBill = NaviBbpsAnalytics.INSTANCE.PayBill(),
) {
val billDetailsEntity by payBillViewModelV2.billDetailsEntity.collectAsStateWithLifecycle()
val billDetailsEntity by payBillViewModel.billDetailsEntity.collectAsStateWithLifecycle()
val keyboardController = LocalSoftwareKeyboardController.current
val paymentsResultLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result
->
payBillViewModelV2.updateIsTokenLoading(isLoading = false)
payBillViewModel.updateIsTokenLoading(isLoading = false)
val status = result.data?.extras?.getString(STATUS.uppercase(Locale.getDefault()))
val paymentCancelSource =
result.data?.extras?.getString(PAYMENT_CANCEL_SOURCE).orEmpty()
payBillViewModelV2.prevPaymentCancelSource = paymentCancelSource
payBillViewModel.prevPaymentCancelSource = paymentCancelSource
val data = result.data?.extras?.getString(KEY_DATA)
val shouldReopenKeyboard =
result.data?.extras?.getBoolean(SHOULD_PROMPT_AMOUNT_EDIT).orFalse()
@@ -190,7 +192,7 @@ fun PayBillScreenV2(
}
when (status) {
PaymentSdkTypes.DISMISS_LOADER.name -> {
payBillViewModelV2.updateIsTokenLoading(isLoading = false)
payBillViewModel.updateIsTokenLoading(isLoading = false)
}
else -> {
val dataJSONObject = data?.stringToJsonObject()
@@ -200,20 +202,20 @@ fun PayBillScreenV2(
paymentStatus = dataJSONObject?.optString(STATUS).toString(),
response = data,
categoryId = billCategoryEntity.categoryId,
sessionAttribute = payBillViewModelV2.getNaviBbpsSessionAttributes(),
sessionAttribute = payBillViewModel.getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
if (dataJSONObject != null) {
payBillViewModelV2.onPaymentResultReceived(
bbpsTransactionId = payBillViewModelV2.txnId,
payBillViewModel.onPaymentResultReceived(
bbpsTransactionId = payBillViewModel.txnId,
paymentResponseMetadata = dataJSONObject,
)
}
}
}
}
val orderReferenceId by payBillViewModelV2.orderReferenceId.collectAsStateWithLifecycle()
val orderReferenceId by payBillViewModel.orderReferenceId.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
naviBbpsActivity.errorEventHandler.bbpsErrorCtaClickEvent.collectLatest { event ->
// In case of bill fetch error, go back to previous screen
@@ -226,7 +228,7 @@ fun PayBillScreenV2(
LaunchedEffect(Unit) {
val category = billCategoryEntity.categoryId
val paymentCheckoutConfig =
payBillViewModelV2.naviBbpsDefaultConfig.value.paymentCheckoutConfig.firstOrNull {
payBillViewModel.naviBbpsDefaultConfig.value.paymentCheckoutConfig.firstOrNull {
it.categoryId == category
}
naviCheckoutViewModel.setUpOneClickCheckout(
@@ -238,7 +240,7 @@ fun PayBillScreenV2(
}
LaunchedEffect(Unit) {
payBillViewModelV2.navigateToOrderDetailsScreen.collectLatest { navigateToOrderDetailsScreen
payBillViewModel.navigateToOrderDetailsScreen.collectLatest { navigateToOrderDetailsScreen
->
if (navigateToOrderDetailsScreen) {
val orderDetailsCtaData =
@@ -270,48 +272,45 @@ fun PayBillScreenV2(
val view = LocalView.current
val scope = rememberCoroutineScope()
val isBillLoading by payBillViewModelV2.isBillLoading.collectAsStateWithLifecycle()
val paymentAmount by payBillViewModelV2.paymentAmount.collectAsStateWithLifecycle()
val isBillLoading by payBillViewModel.isBillLoading.collectAsStateWithLifecycle()
val paymentAmount by payBillViewModel.paymentAmount.collectAsStateWithLifecycle()
val shouldAutoFocusOnAmount by
payBillViewModelV2.shouldAutoFocusOnAmount.collectAsStateWithLifecycle()
val errorMessageId by payBillViewModelV2.errorMessageId.collectAsStateWithLifecycle()
val showErrorText by payBillViewModelV2.showErrorText.collectAsStateWithLifecycle()
val isTokenLoading by payBillViewModelV2.isTokenLoading.collectAsStateWithLifecycle()
val initialPaymentAmount by
payBillViewModelV2.initialPaymentAmount.collectAsStateWithLifecycle()
val isAmountReadOnly by payBillViewModelV2.isAmountReadOnly.collectAsStateWithLifecycle()
val isPayButtonEnabled by payBillViewModelV2.isPayButtonEnabled.collectAsStateWithLifecycle()
val payBillHeaderState by payBillViewModelV2.payBillHeaderState.collectAsStateWithLifecycle()
val payBillScreenState by payBillViewModelV2.payBillScreenState.collectAsStateWithLifecycle()
payBillViewModel.shouldAutoFocusOnAmount.collectAsStateWithLifecycle()
val errorMessageId by payBillViewModel.errorMessageId.collectAsStateWithLifecycle()
val showErrorText by payBillViewModel.showErrorText.collectAsStateWithLifecycle()
val isTokenLoading by payBillViewModel.isTokenLoading.collectAsStateWithLifecycle()
val initialPaymentAmount by payBillViewModel.initialPaymentAmount.collectAsStateWithLifecycle()
val isAmountReadOnly by payBillViewModel.isAmountReadOnly.collectAsStateWithLifecycle()
val isPayButtonEnabled by payBillViewModel.isPayButtonEnabled.collectAsStateWithLifecycle()
val payBillHeaderState by payBillViewModel.payBillHeaderState.collectAsStateWithLifecycle()
val payBillScreenState by payBillViewModel.payBillScreenState.collectAsStateWithLifecycle()
val billerAdditionalParams by
payBillViewModelV2.billerAdditionalParams.collectAsStateWithLifecycle()
payBillViewModel.billerAdditionalParams.collectAsStateWithLifecycle()
val formattedLastPaidDate by
payBillViewModelV2.formattedLastPaidDate.collectAsStateWithLifecycle()
payBillViewModel.formattedLastPaidDate.collectAsStateWithLifecycle()
val formattedLastPaidAmount by
payBillViewModelV2.formattedLastPaidAmount.collectAsStateWithLifecycle()
val myBillEntity by payBillViewModelV2.myBillEntity.collectAsStateWithLifecycle()
payBillViewModel.formattedLastPaidAmount.collectAsStateWithLifecycle()
val myBillEntity by payBillViewModel.myBillEntity.collectAsStateWithLifecycle()
val coinUtilisationProperties by
payBillViewModelV2.coinUtilisationPropertiesV2.collectAsStateWithLifecycle()
val creditCardPaymentOptions = payBillViewModelV2.creditCardPaymentOptions
payBillViewModel.coinUtilisationPropertiesV2.collectAsStateWithLifecycle()
val creditCardPaymentOptions = payBillViewModel.creditCardPaymentOptions
val payBillBottomSheetType by
payBillViewModelV2.payBillBottomSheetType.collectAsStateWithLifecycle()
val amountChipEntityList by
payBillViewModelV2.amountChipEntityList.collectAsStateWithLifecycle()
val offerDataList by payBillViewModelV2.offerDataList.collectAsStateWithLifecycle()
val coinBurnData by payBillViewModelV2.coinBurnData.collectAsStateWithLifecycle()
payBillViewModel.payBillBottomSheetType.collectAsStateWithLifecycle()
val amountChipEntityList by payBillViewModel.amountChipEntityList.collectAsStateWithLifecycle()
val offerDataList by payBillViewModel.offerDataList.collectAsStateWithLifecycle()
val coinBurnData by payBillViewModel.coinBurnData.collectAsStateWithLifecycle()
val amountAfterCoinDiscount by
payBillViewModelV2.amountAfterCoinDiscount.collectAsStateWithLifecycle()
payBillViewModel.amountAfterCoinDiscount.collectAsStateWithLifecycle()
val isCoinDiscountApplied by
payBillViewModelV2.isCoinDiscountApplied.collectAsStateWithLifecycle()
payBillViewModel.isCoinDiscountApplied.collectAsStateWithLifecycle()
val isLottieAnimationShown by
payBillViewModelV2.isLottieAnimationShown.collectAsStateWithLifecycle()
payBillViewModel.isLottieAnimationShown.collectAsStateWithLifecycle()
val naviBbpsDefaultConfig by
payBillViewModelV2.naviBbpsDefaultConfig.collectAsStateWithLifecycle()
val isConsentViewVisible by
payBillViewModelV2.isConsentViewVisible.collectAsStateWithLifecycle()
val isConsentProvided by payBillViewModelV2.isConsentProvided.collectAsStateWithLifecycle()
val isArcProtected by payBillViewModelV2.isArcProtected.collectAsStateWithLifecycle()
payBillViewModel.naviBbpsDefaultConfig.collectAsStateWithLifecycle()
val isConsentViewVisible by payBillViewModel.isConsentViewVisible.collectAsStateWithLifecycle()
val isConsentProvided by payBillViewModel.isConsentProvided.collectAsStateWithLifecycle()
val isArcProtected by payBillViewModel.isArcProtected.collectAsStateWithLifecycle()
val sortedOfferList by
remember(offerDataList, paymentAmount) {
@@ -332,7 +331,7 @@ fun PayBillScreenV2(
if (offerDataList.isNotNullAndNotEmpty()) {
naviBbpsAnalytics.rewardCampaignList(
offerDataList = offerDataList.orEmpty(),
sessionAttribute = payBillViewModelV2.getNaviBbpsSessionAttributes(),
sessionAttribute = payBillViewModel.getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
@@ -344,28 +343,28 @@ fun PayBillScreenV2(
billerId = payBillScreenSource.billerId,
billCategoryEntity = billCategoryEntity,
billDetailsEntity = billDetailsEntity,
sessionAttribute = payBillViewModelV2.getNaviBbpsSessionAttributes(),
sessionAttribute = payBillViewModel.getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
payBillViewModelV2.fetchOffersAndCoinBurnForProduct(
payBillViewModel.fetchOffersAndCoinBurnForProduct(
attributes =
mapOf(
RCBP_CATEGORY to billCategoryEntity.categoryId,
BILLER_UNIQUE_ID to payBillScreenSource.billerId,
)
)
payBillViewModelV2.startPaymentFlow.collectLatest { payBillResponse ->
payBillViewModel.startPaymentFlow.collectLatest { payBillResponse ->
if (payBillResponse != null) {
startPaymentFlow(
payBillResponse = payBillResponse,
initiatePayment = naviCheckoutViewModel::onTokenGenerated,
naviBbpsAnalytics = naviBbpsAnalytics,
billCategoryEntity = billCategoryEntity,
sessionAttribute = payBillViewModelV2.getNaviBbpsSessionAttributes(),
sessionAttribute = payBillViewModel.getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
prevPaymentCancelSource = payBillViewModelV2.prevPaymentCancelSource,
prevPaymentCancelSource = payBillViewModel.prevPaymentCancelSource,
coinUtilisationPropertiesV2 = coinUtilisationProperties,
isAppliedDiscountClicked = isCoinDiscountApplied,
isDiscountApplyLottieShown = isLottieAnimationShown,
@@ -379,13 +378,13 @@ fun PayBillScreenV2(
}
LaunchedEffect(Unit) {
payBillViewModelV2.navigateToNextScreenFromHelpCta.collectLatest {
payBillViewModel.navigateToNextScreenFromHelpCta.collectLatest {
it?.let { NaviBbpsRouter.onCtaClick(naviBbpsActivity = naviBbpsActivity, ctaData = it) }
}
}
LaunchedEffect(paymentAmount, offerDataList, coinBurnData) {
payBillViewModelV2.getApplicableDiscount()
payBillViewModel.getApplicableDiscount()
}
val bottomSheetState =
@@ -401,10 +400,17 @@ fun PayBillScreenV2(
event.errorConfig.tag == TAG_BILL_FETCH_ERROR -> {
navigator.navigateUp()
}
event.errorConfig.tag == TAG_BILL_ALREADY_PAID_ON_REFETCH -> {
if (source == NAVI_HOME || source == EMPTY) {
naviBbpsActivity.finish()
} else {
navigator.navigateUp()
}
}
event.clickedButtonConfig.action is NaviBbpsButtonAction.Redirect -> {
val apiUrl = event.clickedButtonConfig.action.url
if (isUrlPathValidForSavedBills(urlPath = apiUrl)) {
payBillViewModelV2.onSavedBillCtaClicked(apiUrl = apiUrl)
payBillViewModel.onSavedBillCtaClicked(apiUrl = apiUrl)
bottomSheetState.hide()
navigator.navigateUp()
}
@@ -414,7 +420,7 @@ fun PayBillScreenV2(
}
LaunchedEffect(Unit) {
payBillViewModelV2.goToNextScreen.collectLatest { nextScreen ->
payBillViewModel.goToNextScreen.collectLatest { nextScreen ->
val direction = nextScreen.first
val clearBackStack = nextScreen.second
if (direction != null) {
@@ -459,16 +465,16 @@ fun PayBillScreenV2(
isChecked = !isConsentProvided,
billDetailsEntity = payBillScreenSource.billDetailsEntity,
billCategoryEntity = billCategoryEntity,
sessionAttribute = payBillViewModelV2.getNaviBbpsSessionAttributes(),
sessionAttribute = payBillViewModel.getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
payBillViewModelV2.toggleConsentProvided()
payBillViewModel.toggleConsentProvided()
}
val onAmountChipClicked = { amount: String ->
focusManager.clearFocus()
payBillViewModelV2.enteredAmountInputChanged(
payBillViewModel.enteredAmountInputChanged(
newInput = amount,
isAmountChangedByChipSelection = true,
)
@@ -484,12 +490,12 @@ fun PayBillScreenV2(
}
val onMoreClicked = {
payBillViewModelV2.onMoreClicked()
payBillViewModel.onMoreClicked()
openSheet()
}
val onCreditCardPaymentOptionSelected = { creditCardPaymentOption: CreditCardPaymentOption ->
payBillViewModelV2.onCreditCardPaymentOptionSelected(creditCardPaymentOption)
payBillViewModel.onCreditCardPaymentOptionSelected(creditCardPaymentOption)
}
var loadingState by remember {
@@ -530,6 +536,24 @@ fun PayBillScreenV2(
}
}
LaunchedEffect(Unit) {
payBillViewModel.showBillDetailsUpdatedBottomSheet.collectLatest {
showBillDetailsUpdatedBottomSheet ->
if (showBillDetailsUpdatedBottomSheet) {
openSheet()
}
}
}
LaunchedEffect(bottomSheetState.isVisible) {
if (
!bottomSheetState.isVisible &&
payBillBottomSheetType is PayBillBottomSheetType.BillDetailsUpdateInfo
) {
payBillViewModel.updateLatestBillDetails()
}
}
NaviBbpsModalBottomSheetLayout(
sheetContent = {
PayBillBottomSheetContent(
@@ -549,8 +573,8 @@ fun PayBillScreenV2(
scrollState = scrollState,
isCreditCardCategory = isCreditCardCategory,
billCategoryEntity = billCategoryEntity,
helpCtaText = payBillViewModelV2.helpCta.title.orEmpty(),
onHelpCtaClicked = payBillViewModelV2::onHelpCtaClicked,
helpCtaText = payBillViewModel.helpCta.title.orEmpty(),
onHelpCtaClicked = payBillViewModel::onHelpCtaClicked,
payBillScreenSource = payBillScreenSource,
)
},
@@ -614,7 +638,7 @@ fun PayBillScreenV2(
isAmountReadOnly = isAmountReadOnly,
generateToken = {
keyboardController?.customHide(context = context, view = view)
payBillViewModelV2.generatePaymentToken()
payBillViewModel.generatePaymentToken()
},
)
}
@@ -634,7 +658,7 @@ fun PayBillScreenV2(
else {
if (isCreditCardCategory) {
if (payBillScreenState is PayBillScreenState.BillDetailsAvailable) {
RenderCreditCardPayBillScreenV2(
RenderCreditCardPayBillScreen(
payBillScreenState =
payBillScreenState
as PayBillScreenState.BillDetailsAvailable,
@@ -655,17 +679,17 @@ fun PayBillScreenV2(
initialPaymentAmount = initialPaymentAmount,
paymentAmount = paymentAmount,
onAmountValueChanged =
payBillViewModelV2::onAmountValueChanged,
payBillViewModel::onAmountValueChanged,
scrollState = scrollState,
coinBurnData = coinBurnData,
onAppliedDiscountClicked =
payBillViewModelV2::onAppliedDiscountClicked,
payBillViewModel::onAppliedDiscountClicked,
onRemoveDiscountClicked =
payBillViewModelV2::onRemoveDiscountClicked,
payBillViewModel::onRemoveDiscountClicked,
isAppliedDiscountClicked = isCoinDiscountApplied,
sortedOfferList = sortedOfferList,
onOfferClicked = {
payBillViewModelV2.onOfferRolodexClicked(
payBillViewModel.onOfferRolodexClicked(
if (paymentAmount.isNotEmpty())
paymentAmount.toDouble()
else null
@@ -676,8 +700,7 @@ fun PayBillScreenV2(
initialSource = initialSource,
coinBurnData = coinBurnData,
sessionAttribute =
payBillViewModelV2
.getNaviBbpsSessionAttributes(),
payBillViewModel.getNaviBbpsSessionAttributes(),
billCategoryEntity = billCategoryEntity,
)
openSheet()
@@ -690,7 +713,7 @@ fun PayBillScreenV2(
)
}
} else {
RenderNonCreditCardPayBillScreenV2(
RenderNonCreditCardPayBillScreen(
billDetailsEntity = billDetailsEntity,
phoneNumberDetail = phoneNumberDetail,
payBillHeaderState = payBillHeaderState,
@@ -700,7 +723,7 @@ fun PayBillScreenV2(
initialSource = initialSource,
formattedLastPaidDate = formattedLastPaidDate,
billCategoryEntity = billCategoryEntity,
payBillViewModelV2 = payBillViewModelV2,
payBillViewModel = payBillViewModel,
keyboardController = keyboardController,
context = context,
view = view,
@@ -771,7 +794,7 @@ fun PayBillScreenV2(
.padding(top = 16.dp)
.fillMaxWidth(),
)
payBillViewModelV2.triggerAnimation()
payBillViewModel.triggerAnimation()
}
BottomBarShadow(shadowHeight = 32.dp)
}
@@ -968,6 +991,7 @@ fun onBillHistoryClicked(
accountHolderName = payBillHeaderState.phoneContactEntity.name,
billerAdditionalParams = emptyList(),
planItemDetails = emptyMap(),
lastGeneratedTimestamp = EMPTY,
),
)
is PayBillHeaderState.OtherHeaders ->
@@ -989,6 +1013,7 @@ fun onBillHistoryClicked(
accountHolderName = billDetailsEntity?.accountHolderName.orEmpty(),
billerAdditionalParams = emptyList(),
planItemDetails = emptyMap(),
lastGeneratedTimestamp = EMPTY,
),
)
else -> defaultMyBillEntity

View File

@@ -106,7 +106,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
@Composable
fun RenderCreditCardPayBillScreenV2(
fun RenderCreditCardPayBillScreen(
payBillScreenState: PayBillScreenState.BillDetailsAvailable,
formattedLastPaidDate: String,
formattedLastPaidAmount: String,

View File

@@ -53,7 +53,7 @@ import com.navi.bbps.feature.customerinput.model.view.BillDetailsEntity
import com.navi.bbps.feature.customerinput.model.view.BillerAdditionalParamsEntity
import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.paybill.PayBillViewModelV2
import com.navi.bbps.feature.paybill.PayBillViewModel
import com.navi.bbps.feature.paybill.model.view.AmountChipEntity
import com.navi.bbps.feature.paybill.model.view.CoinUtilisationPropertiesV2
import com.navi.bbps.feature.paybill.model.view.PayBillHeaderState
@@ -73,7 +73,7 @@ import com.navi.rr.common.models.OfferData
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@Composable
fun RenderNonCreditCardPayBillScreenV2(
fun RenderNonCreditCardPayBillScreen(
billDetailsEntity: BillDetailsEntity?,
phoneNumberDetail: PhoneContactEntity,
payBillHeaderState: PayBillHeaderState?,
@@ -83,7 +83,7 @@ fun RenderNonCreditCardPayBillScreenV2(
initialSource: String,
formattedLastPaidDate: String,
billCategoryEntity: BillCategoryEntity,
payBillViewModelV2: PayBillViewModelV2,
payBillViewModel: PayBillViewModel,
keyboardController: SoftwareKeyboardController?,
context: Context,
view: View,
@@ -120,12 +120,12 @@ fun RenderNonCreditCardPayBillScreenV2(
initialSource = initialSource,
formattedLastPaidDate = formattedLastPaidDate,
billCategoryEntity = billCategoryEntity,
removeDiscountOnViewHistoryClicked = payBillViewModelV2::onRemoveDiscountClicked,
removeDiscountOnViewHistoryClicked = payBillViewModel::onRemoveDiscountClicked,
keyboardController = keyboardController,
context = context,
view = view,
)
AmountTextFieldV2(
AmountTextField(
modifier = Modifier.Companion.fillMaxWidth().padding(top = 32.dp),
isReadOnly = isAmountReadOnly,
focusManager = focusManager,
@@ -133,14 +133,13 @@ fun RenderNonCreditCardPayBillScreenV2(
stringResource(id = errorMessageId, initialPaymentAmount.getDisplayableAmount()),
paymentAmount =
if (isAmountReadOnly) paymentAmount.getDisplayableAmount() else paymentAmount,
onPaymentAmountChanged = payBillViewModelV2::onAmountValueChanged,
onPaymentAmountChanged = payBillViewModel::onAmountValueChanged,
amountMaxLength = MAX_AMOUNT_LENGTH,
showErrorText = showErrorText,
isAppliedClicked = isCoinDiscountApplied,
discountedAmount = amountAfterCoinDiscount,
shouldAutoFocus = shouldAutoFocusOnAmount,
removeDiscountOnAmountFieldClick =
payBillViewModelV2::handleDiscountRemovalOnAmountFieldClick,
removeDiscountOnAmountFieldClick = payBillViewModel::handleDiscountRemovalOnAmountFieldClick,
)
if (amountChipEntityList.isNotEmpty()) {
Column {
@@ -149,7 +148,7 @@ fun RenderNonCreditCardPayBillScreenV2(
onAmountChipClicked = onAmountChipClicked,
amountChipEntityList = amountChipEntityList,
isCoinDiscountApplied = isCoinDiscountApplied,
onRemoveDiscountClicked = payBillViewModelV2::onRemoveDiscountClicked,
onRemoveDiscountClicked = payBillViewModel::onRemoveDiscountClicked,
)
Spacer(modifier = Modifier.Companion.height(24.dp))
@@ -175,17 +174,17 @@ fun RenderNonCreditCardPayBillScreenV2(
offerSuffix = offerItem.offer.titleSuffix.orEmpty(),
coinUtilisationPropertiesV2 = coinUtilisationProperties,
onAppliedDiscountClicked =
payBillViewModelV2::onAppliedDiscountClicked,
payBillViewModel::onAppliedDiscountClicked,
isCoinDiscountAppliedClicked = isCoinDiscountApplied,
onRemoveDiscountClicked =
payBillViewModelV2::onRemoveDiscountClicked,
payBillViewModel::onRemoveDiscountClicked,
offerData = offerDataList,
isApplied =
paymentAmount.isNotEmpty() &&
sortedOfferList.any { it.disabledData.isNull() },
isBurnDisabled = showErrorText,
onViewMoreClick = {
payBillViewModelV2.onOfferRolodexClicked(
payBillViewModel.onOfferRolodexClicked(
if (paymentAmount.isNotEmpty()) paymentAmount.toDouble()
else null
)
@@ -195,7 +194,7 @@ fun RenderNonCreditCardPayBillScreenV2(
initialSource = initialSource,
coinBurnData = coinBurnData,
sessionAttribute =
payBillViewModelV2.getNaviBbpsSessionAttributes(),
payBillViewModel.getNaviBbpsSessionAttributes(),
billCategoryEntity = billCategoryEntity,
)
openSheet()
@@ -215,7 +214,7 @@ fun RenderNonCreditCardPayBillScreenV2(
paymentAmount.isNotEmpty() &&
sortedOfferList.any { it.disabledData.isNull() },
onViewMoreClick = {
payBillViewModelV2.onOfferRolodexClicked(
payBillViewModel.onOfferRolodexClicked(
if (paymentAmount.isNotEmpty()) paymentAmount.toDouble()
else null
)
@@ -225,7 +224,7 @@ fun RenderNonCreditCardPayBillScreenV2(
initialSource = initialSource,
coinBurnData = coinBurnData,
sessionAttribute =
payBillViewModelV2.getNaviBbpsSessionAttributes(),
payBillViewModel.getNaviBbpsSessionAttributes(),
billCategoryEntity = billCategoryEntity,
)
openSheet()
@@ -263,11 +262,11 @@ fun RenderNonCreditCardPayBillScreenV2(
billDetailsEntity = billDetailsEntity,
billCategoryEntity = billCategoryEntity,
)
payBillViewModelV2.onViewOtherPlansClicked()
payBillViewModel.onViewOtherPlansClicked()
},
onMoreClicked = onMoreClicked,
removeDiscountAnimationOnOtherPlansClicked =
payBillViewModelV2::onRemoveDiscountClicked,
payBillViewModel::onRemoveDiscountClicked,
)
}
}

View File

@@ -40,7 +40,7 @@ import com.navi.bbps.feature.contactlist.model.view.PhoneContactEntity
import com.navi.bbps.feature.customerinput.model.network.BillDetailsRequest
import com.navi.bbps.feature.customerinput.model.network.DeviceDetails
import com.navi.bbps.feature.customerinput.model.view.BillDetailsEntity
import com.navi.bbps.feature.destinations.PayBillScreenV2Destination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.mybills.MyBillsRepository
import com.navi.bbps.feature.paybill.model.view.PayBillSource
import com.navi.bbps.feature.prepaidrecharge.model.network.CircleItemResponse
@@ -959,7 +959,7 @@ constructor(
)
_navigateToNextScreen.emit(
PayBillScreenV2Destination(
PayBillScreenDestination(
payBillScreenSource = payBillScreenSource,
phoneNumberDetail = phoneNumberDetail,
billCategoryEntity = billCategoryEntity,

View File

@@ -194,11 +194,11 @@ object FirebaseRemoteConfigHelper {
"NAVI_CHECK_BALANCE_CROSS_SELL_AD_FALLBACK_TIMEOUT"
const val NAVI_BBPS_PPS_CROSS_SELL_AD_RE_ID = "NAVI_BBPS_PPS_CROSS_SELL_AD_RE_ID"
const val NAVI_BBPS_DISMISS_BILL_DURATION = "NAVI_BBPS_DISMISS_BILL_DURATION"
const val NAVI_BBPS_PPS_SHARE_RECEIPT_CALLOUT_TEXT = "NAVI_BBPS_PPS_SHARE_RECEIPT_CALLOUT_TEXT"
const val NAVI_BBPS_TDS_SHARE_RECEIPT_CALLOUT_TEXT = "NAVI_BBPS_TDS_SHARE_RECEIPT_CALLOUT_TEXT"
const val NAVI_BBPS_EXTERNAL_APP_SHARE_CONFIGURABLE_TEXT =
"NAVI_BBPS_EXTERNAL_APP_SHARE_CONFIGURABLE_TEXT"
const val NAVI_BBPS_BILL_REFRESH_DURATION = "NAVI_BBPS_BILL_REFRESH_DURATION"
const val NAVI_BBPS_OFFER_SHIMMER_TIMEOUT = "NAVI_BBPS_OFFER_SHIMMER_TIMEOUT"