NTP-64314 | Divyesh | Add bill options enhancement (#16214)

This commit is contained in:
Divyesh Shinde
2025-06-09 15:11:57 +05:30
committed by GitHub
parent f3d2277e0c
commit a44b029117
19 changed files with 1363 additions and 796 deletions

View File

@@ -296,6 +296,48 @@ class NaviBbpsAnalytics private constructor() {
)
}
fun billDeletionSuccess(
data: BbpsGenericResponse,
myBillEntity: MyBillEntity,
sessionAttribute: Map<String, String>,
source: String,
initialSource: String,
) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviBBPS_CategoryPage_DeletedBill_Success",
eventValues =
mapOf(
"data" to data.toString(),
"billId" to myBillEntity.billId,
"billerName" to myBillEntity.billerName,
NAVI_BBPS_SESSION_ID to sessionAttribute[NAVI_BBPS_SESSION_ID].orEmpty(),
NAVI_BBPS_SOURCE to source,
NAVI_BBPS_INITIAL_SOURCE to initialSource,
),
)
}
fun billDeletionFailed(
deleteBillResponse: RepoResult<BbpsGenericResponse>,
myBillEntity: MyBillEntity,
sessionAttribute: Map<String, String>,
source: String,
initialSource: String,
) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviBBPS_CategoryPage_DeletedBill_Failed",
eventValues =
mapOf(
"deleteBillResponse" to deleteBillResponse.toString(),
"billId" to myBillEntity.billId,
"billerName" to myBillEntity.billerName,
NAVI_BBPS_SESSION_ID to sessionAttribute[NAVI_BBPS_SESSION_ID].orEmpty(),
NAVI_BBPS_SOURCE to source,
NAVI_BBPS_INITIAL_SOURCE to initialSource,
),
)
}
fun onUnpaidBillOptionsClicked(
sessionAttribute: Map<String, String>,
billEntity: MyBillEntity,
@@ -728,6 +770,25 @@ class NaviBbpsAnalytics private constructor() {
)
}
fun onBillOptionsClicked(
sessionAttribute: Map<String, String>,
billEntity: MyBillEntity,
action: String,
) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviBBPS_BillerList_BillsOptions_Clicked",
eventValues =
mapOf(
"billId" to billEntity.billId,
"billerId" to billEntity.billerId,
"categoryId" to billEntity.categoryId,
"billerName" to billEntity.billerName,
"action" to action,
NAVI_BBPS_SESSION_ID to sessionAttribute[NAVI_BBPS_SESSION_ID].orEmpty(),
),
)
}
fun onNavigateToNextScreen(
billCategoryEntity: BillCategoryEntity,
route: String,
@@ -848,7 +909,7 @@ class NaviBbpsAnalytics private constructor() {
)
}
fun billDeletedSuccessfully(
fun billDeletionSuccess(
data: BbpsGenericResponse,
myBillEntity: MyBillEntity,
sessionAttribute: Map<String, String>,
@@ -1658,6 +1719,25 @@ class NaviBbpsAnalytics private constructor() {
)
}
fun onBillOptionsClicked(
sessionAttribute: Map<String, String>,
billEntity: MyBillEntity,
action: String,
) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviBBPS_ContactList_BillsOptions_Clicked",
eventValues =
mapOf(
"billId" to billEntity.billId,
"billerId" to billEntity.billerId,
"categoryId" to billEntity.categoryId,
"billerName" to billEntity.billerName,
"action" to action,
NAVI_BBPS_SESSION_ID to sessionAttribute[NAVI_BBPS_SESSION_ID].orEmpty(),
),
)
}
fun onDeleteAccountKebabMenuClicked(
myBillEntity: MyBillEntity,
sessionAttribute: Map<String, String>,
@@ -1711,7 +1791,7 @@ class NaviBbpsAnalytics private constructor() {
)
}
fun billDeletedSuccessfully(
fun billDeletionSuccess(
data: BbpsGenericResponse,
myBillEntity: MyBillEntity,
sessionAttribute: Map<String, String>,
@@ -2506,6 +2586,48 @@ class NaviBbpsAnalytics private constructor() {
)
}
fun billDeletionSuccess(
data: BbpsGenericResponse,
myBillEntity: MyBillEntity,
sessionAttribute: Map<String, String>,
source: String,
initialSource: String,
) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviBBPS_MyBills_DeletedBill_Success",
eventValues =
mapOf(
"data" to data.toString(),
"billId" to myBillEntity.billId,
"billerName" to myBillEntity.billerName,
NAVI_BBPS_SESSION_ID to sessionAttribute[NAVI_BBPS_SESSION_ID].orEmpty(),
NAVI_BBPS_SOURCE to source,
NAVI_BBPS_INITIAL_SOURCE to initialSource,
),
)
}
fun billDeletionFailed(
deleteBillResponse: RepoResult<BbpsGenericResponse>,
myBillEntity: MyBillEntity,
sessionAttribute: Map<String, String>,
source: String,
initialSource: String,
) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviBBPS_MyBills_DeletedBill_Failed",
eventValues =
mapOf(
"deleteBillResponse" to deleteBillResponse.toString(),
"billId" to myBillEntity.billId,
"billerName" to myBillEntity.billerName,
NAVI_BBPS_SESSION_ID to sessionAttribute[NAVI_BBPS_SESSION_ID].orEmpty(),
NAVI_BBPS_SOURCE to source,
NAVI_BBPS_INITIAL_SOURCE to initialSource,
),
)
}
fun onMyBillMoreOptionsBottomSheetClicked(billEntity: MyBillEntity, action: String) {
NaviTrackEvent.trackEventOnClickStream(
eventName = "NaviBBPS_MyBills_BottomSheet_Options_Clicked",

View File

@@ -12,6 +12,7 @@ import android.graphics.Rect
import android.view.ViewTreeObserver
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.animateFloatAsState
@@ -125,7 +126,12 @@ import com.navi.bbps.common.BULLET
import com.navi.bbps.common.CATEGORY_ID_CREDIT_CARD
import com.navi.bbps.common.DASH
import com.navi.bbps.common.DATE_TIME_FORMAT_DATE_MONTH_NAME
import com.navi.bbps.common.ICON_MARK_AS_PAID_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.ICON_REFRESH_BILL_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.ICON_REMOVE_ACCOUNT_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.ICON_VIEW_BILL_HISTORY_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.NaviBbpsDimens
import com.navi.bbps.common.SYMBOL_RUPEE
import com.navi.bbps.common.model.view.DetectedBillEntity
import com.navi.bbps.common.model.view.NaviPermissionResult
import com.navi.bbps.common.model.view.PrepaidPlanItemLoadingState
@@ -1940,7 +1946,10 @@ fun BillAmountAndDueDateSectionWithShimmer(
Spacer(modifier = Modifier.height(2.dp))
Box(
modifier =
Modifier.padding(start = 48.dp).width(200.dp).height(20.dp).bbpsShimmerEffect()
Modifier.padding(start = if (startWithPadding) 48.dp else 0.dp)
.width(200.dp)
.height(20.dp)
.bbpsShimmerEffect()
)
} else {
if (myBillEntity.isBillPaid.not()) {
@@ -2340,6 +2349,195 @@ fun OriginLandingWidget(
}
}
@Composable
fun BillOptionsBottomSheetContent(
billerLogoUrl: String,
title: String,
amount: String,
billerName: String,
expiringOn: String,
isBillPaid: Boolean = false,
isRechargeCategory: Boolean,
onMarkAsPaidClicked: () -> Unit,
onRefreshBillClicked: () -> Unit,
onViewBillHistoryClicked: () -> Unit,
onRemoveAccountClicked: () -> Unit,
onCloseClicked: () -> Unit,
) {
val descriptionText = buildAnnotatedString {
if (isBillPaid) {
append(billerName)
} else {
val fullText = stringResource(id = R.string.bbps_payment_due, billerName, expiringOn)
withStyle(
style =
SpanStyle(
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
fontSize = 14.sp,
color = NaviBbpsColor.textTertiary,
)
) {
append(fullText)
}
addStyle(
style =
SpanStyle(
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
fontSize = 14.sp,
color = NaviBbpsColor.inputFieldError,
),
start = fullText.indexOf(expiringOn),
end = fullText.indexOf(expiringOn) + expiringOn.length,
)
}
}
Column(
modifier =
Modifier.navigationBarsPadding()
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp)
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
BbpsAsyncImage(
imageUrl = billerLogoUrl,
modifier = Modifier.size(24.dp),
placeholderIconResId = CommonR.drawable.navi_common_ic_biller_placeholder,
)
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(id = CommonR.drawable.ic_close_black),
contentDescription = null,
modifier = Modifier.size(24.dp).clickableDebounce { onCloseClicked() },
)
}
Spacer(modifier = Modifier.height(8.dp))
NaviText(
text =
buildString {
if (title.isNotBlank()) {
append(title)
append(com.navi.common.utils.SPACE)
}
if (!isBillPaid) {
if (title.isNotBlank() && amount.isNotBlank()) {
append(BULLET)
append(com.navi.common.utils.SPACE)
}
if (amount.isNotBlank()) {
append(SYMBOL_RUPEE)
append(amount)
}
}
},
fontSize = 16.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
color = NaviBbpsColor.textPrimary,
)
Spacer(modifier = Modifier.height(2.dp))
if (descriptionText.isNotBlank()) {
NaviText(
text = descriptionText,
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
color = NaviBbpsColor.textTertiary,
lineHeight = 22.sp,
)
}
Spacer(modifier = Modifier.height(24.dp))
val actions = mutableListOf<ActionItem>()
if (!isBillPaid) {
actions.add(
ActionItem(
imageS3Url = ICON_MARK_AS_PAID_UNPAID_BILL_BOTTOMSHEET,
textResId = R.string.bbps_mark_as_paid,
onClick = onMarkAsPaidClicked,
)
)
}
if (!isRechargeCategory) {
actions.add(
ActionItem(
imageS3Url = ICON_REFRESH_BILL_UNPAID_BILL_BOTTOMSHEET,
textResId = R.string.bbps_refresh_bill,
onClick = onRefreshBillClicked,
)
)
}
actions.addAll(
listOf(
ActionItem(
imageS3Url = ICON_VIEW_BILL_HISTORY_UNPAID_BILL_BOTTOMSHEET,
textResId =
if (isRechargeCategory) {
R.string.bbps_view_recharge_history
} else R.string.bbps_view_bill_history,
onClick = onViewBillHistoryClicked,
),
ActionItem(
imageS3Url = ICON_REMOVE_ACCOUNT_UNPAID_BILL_BOTTOMSHEET,
textResId = R.string.bbps_remove_account,
onClick = onRemoveAccountClicked,
),
)
)
actions.forEachIndexed { index, action ->
IconActionRow(
imageS3Url = action.imageS3Url,
text = stringResource(id = action.textResId),
onClick = action.onClick,
)
if (index < actions.lastIndex) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 20.dp),
thickness = 1.dp,
color = NaviBbpsColor.borderAlt,
)
}
}
}
}
data class ActionItem(
val imageS3Url: String,
@StringRes val textResId: Int,
val onClick: () -> Unit,
)
@Composable
fun IconActionRow(
imageS3Url: String,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth().noRippleClickableWithDebounce(onClick = onClick),
verticalAlignment = Alignment.CenterVertically,
) {
AsyncImage(modifier = Modifier.size(24.dp), model = imageS3Url, contentDescription = "")
Spacer(modifier = Modifier.width(12.dp))
NaviText(
text = text,
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
color = NaviBbpsColor.textPrimary,
)
}
}
@Preview
@Composable
fun OriginLandingWidgetPreview() {

View File

@@ -0,0 +1,106 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.bbps.common.usecase
import com.navi.base.utils.NaviNetworkConnectivity
import com.navi.base.utils.retry
import com.navi.bbps.common.DEFAULT_RETRY_COUNT
import com.navi.bbps.common.RETRY_INTERVAL_IN_SECONDS
import com.navi.bbps.common.model.network.BbpsGenericResponse
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getBbpsMetricInfo
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.mybills.MyBillsRepository
import com.navi.bbps.feature.mybills.MyBillsSyncJob
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
import javax.inject.Inject
class MyBillActionsHandler
@Inject
constructor(
private val bbpsCommonRepository: BbpsCommonRepository,
private val naviNetworkConnectivity: NaviNetworkConnectivity,
private val myBillsRepository: MyBillsRepository,
private val myBillsSyncJob: MyBillsSyncJob,
) {
suspend fun refreshBill(
myBillEntity: MyBillEntity,
screenName: String,
onSuccess: () -> Unit,
onError: suspend (RepoResult<BillDetailsResponse>) -> Unit,
) {
val billDetailsRequest =
BillDetailsRequest(
billerId = myBillEntity.billerId,
customerParams = myBillEntity.customerParams,
deviceDetails = DeviceDetails(ip = naviNetworkConnectivity.getIpAddress()),
amount = myBillEntity.actualLastPaidAmount,
isConsentProvided = true,
)
val billDetailsResponse =
bbpsCommonRepository.fetchBillDetails(
billDetailsRequest = billDetailsRequest,
metricInfo = getBbpsMetricInfo(screenName = screenName),
)
if (billDetailsResponse.isSuccessWithData() && billDetailsResponse.data != null) {
onSuccess()
} else {
onError(billDetailsResponse)
}
myBillsSyncJob.refreshBills(screenName = screenName)
}
suspend fun deleteBill(
billId: String,
categoryId: String,
screenName: String,
onSuccess: suspend (RepoResult<BbpsGenericResponse>) -> Unit,
onError: suspend (RepoResult<BbpsGenericResponse>) -> Unit,
) {
val savedBills = bbpsCommonRepository.fetchSavedBillsByCategory(category = categoryId)
if (savedBills.isNotEmpty()) {
val deleteBillResponse =
bbpsCommonRepository.deleteBill(
savedBillId = billId,
metricInfo = getBbpsMetricInfo(screenName = screenName),
)
if (deleteBillResponse.isSuccessWithData()) {
myBillsSyncJob.refreshBills(screenName = screenName)
onSuccess(deleteBillResponse)
} else {
onError(deleteBillResponse)
}
}
}
suspend fun markAsPaid(billId: String, screenName: String, onSuccess: () -> Unit) {
val response =
retry(
retryCount = DEFAULT_RETRY_COUNT,
retryIntervalInSeconds = RETRY_INTERVAL_IN_SECONDS,
execute = {
myBillsRepository.markBillAsPaid(
billId = billId,
metricInfo = getBbpsMetricInfo(screenName = screenName, isNae = { false }),
)
},
shouldRetry = { !it.isSuccessWithData() },
)
if (response.isSuccessWithData()) {
myBillsSyncJob.refreshBills(screenName = screenName)
onSuccess()
}
}
}

View File

@@ -12,8 +12,8 @@ import androidx.lifecycle.viewModelScope
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.navi.base.utils.BaseUtils
import com.navi.base.utils.EMPTY
import com.navi.base.utils.NaviNetworkConnectivity
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.isNotNull
import com.navi.bbps.R
import com.navi.bbps.common.CATEGORY_ID_FASTAG
import com.navi.bbps.common.CATEGORY_ID_MOBILE_POSTPAID
@@ -23,9 +23,11 @@ import com.navi.bbps.common.NaviBbpsScreen
import com.navi.bbps.common.model.NaviBbpsVmData
import com.navi.bbps.common.model.config.NaviBbpsDefaultConfig
import com.navi.bbps.common.model.view.NaviPermissionResult
import com.navi.bbps.common.model.view.RefreshBillState
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.session.NaviBbpsSessionHelper
import com.navi.bbps.common.usecase.FindLastOrderWithSuccessfulPaymentUseCase
import com.navi.bbps.common.usecase.MyBillActionsHandler
import com.navi.bbps.common.usecase.NaviBbpsConfigUseCase
import com.navi.bbps.common.usecase.RewardNudgeUseCase
import com.navi.bbps.common.utils.BillDetailsResponseToEntityMapper
@@ -48,8 +50,9 @@ 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.MyBillHistoryDetailsScreenDestination
import com.navi.bbps.feature.destinations.PayBillScreenDestination
import com.navi.bbps.feature.mybills.MyBillsSyncJob
import com.navi.bbps.feature.mybills.MyBillsViewModel.SnackBarState
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.paybill.model.view.PayBillSource
import com.navi.bbps.isRedirectToCustomerInputRequired
@@ -96,14 +99,16 @@ constructor(
private val bbpsCommonRepository: BbpsCommonRepository,
private val naviBbpsConfigUseCase: NaviBbpsConfigUseCase,
private val deviceLocationProvider: DeviceLocationProvider,
private val myBillsSyncJob: MyBillsSyncJob,
private val rewardsNudgeEntityFetchUseCase: RewardNudgeUseCase,
private val naviBbpsSessionHelper: NaviBbpsSessionHelper,
private val myBillEntityToBillDetailsResponseMapper: MyBillEntityToBillDetailsResponseMapper,
private val billDetailsResponseToEntityMapper: BillDetailsResponseToEntityMapper,
private val resourceProvider: ResourceProvider,
private val billerItemResponseToEntityMapper: BillerItemResponseToEntityMapper,
private val findLastOrderWithSuccessfulPaymentUseCase: FindLastOrderWithSuccessfulPaymentUseCase,
private val findLastOrderWithSuccessfulPaymentUseCase:
FindLastOrderWithSuccessfulPaymentUseCase,
private val naviNetworkConnectivity: NaviNetworkConnectivity,
private val myBillActionsHandler: MyBillActionsHandler,
) : NaviBbpsBaseVM(naviBbpsVmData = NaviBbpsVmData(screen = NaviBbpsScreen.NAVI_BBPS_BILLER_LIST)) {
private val naviBbpsAnalytics: NaviBbpsAnalytics.BillerList =
@@ -130,15 +135,11 @@ constructor(
val rewardsNudgeDetailEntity = _rewardsNudgeDetailEntity.asStateFlow()
private val _billerListBottomSheetType =
MutableStateFlow<BillerListBottomSheetType>(
BillerListBottomSheetType.MenuOptions(
BillerListBottomSheetType.createEmptyMyBillEntity()
)
)
MutableStateFlow<BillerListBottomSheetType>(BillerListBottomSheetType.None)
val billerListBottomSheetType = _billerListBottomSheetType.asStateFlow()
private val _showSnackBar = MutableStateFlow(false)
val showSnackBar = _showSnackBar.asStateFlow()
private val _refreshBillItemAndStatus = MutableStateFlow(RefreshBillState())
val refreshBillItemAndStatus = _refreshBillItemAndStatus.asStateFlow()
val phoneNumber = BaseUtils.getPhoneNumber().toString()
val normalisedPhoneNumber = getNormalisedPhoneNumber(phoneNumber = phoneNumber)
@@ -184,6 +185,9 @@ constructor(
private val _naviBbpsDefaultConfig = MutableStateFlow(NaviBbpsDefaultConfig())
val naviBbpsDefaultConfig = _naviBbpsDefaultConfig.asStateFlow()
private val _snackBarState = MutableStateFlow(SnackBarState(show = false))
val snackBarState = _snackBarState.asStateFlow()
private var recentBillsEntity = RecentBillsEntity(title = "", bills = listOf())
val isTrailingIconEnabled =
@@ -236,8 +240,12 @@ constructor(
_navigateToNextScreen.emit(direction)
}
fun updateSnackBarState(showSnackBar: Boolean) {
_showSnackBar.update { showSnackBar }
fun updateSnackBarState(show: Boolean, messageId: Int = R.string.bbps_copied_to_clipboard) {
_snackBarState.update { SnackBarState(show = show, messageId = messageId) }
}
private fun updateRefreshState(isRefreshing: Boolean, bill: MyBillEntity? = null) {
_refreshBillItemAndStatus.value = RefreshBillState(isRefreshing, bill)
}
private val _permissionResult =
@@ -290,25 +298,128 @@ constructor(
}
}
fun onDeleteMenuClicked(myBillEntity: MyBillEntity) {
_billerListBottomSheetType.update { BillerListBottomSheetType.MenuOptions(myBillEntity) }
fun onBillOptionsKebabMenuClicked(myBillEntity: MyBillEntity, openSheet: () -> Unit) {
_billerListBottomSheetType.update {
BillerListBottomSheetType.MenuOptions(
myBillEntity = myBillEntity,
onMarkAsPaidClicked = {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_mark_as_paid),
)
viewModelScope.launch {
delay(200)
/* this is added to make sure the bottom sheet is closed before
opening the new one and to make the transition smoother */
_billerListBottomSheetType.update {
BillerListBottomSheetType.MarkAsPaid(
myBillEntity = myBillEntity,
title =
resourceProvider.getString(
R.string.bbps_bill_mark_as_paid_heading
),
description =
resourceProvider.getString(
R.string.bbps_bill_mark_as_paid_description
),
firstBtnText = resourceProvider.getString(R.string.bbps_no),
secondButtonText = resourceProvider.getString(R.string.bbps_yes),
onFirstBtnClick = {},
onSecondBtnClick = {
onMarkBillAsPaidConfirmedCtaClicked(myBillEntity = myBillEntity)
},
)
}
openSheet.invoke()
}
},
onRefreshBillClicked = {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_refresh_bill),
)
viewModelScope.launch(dispatcherProvider.io) {
if (!naviNetworkConnectivity.isInternetConnected()) {
notifyError(getNoInternetErrorConfig())
return@launch
} else {
refreshBillAndUpdateInfo(myBillEntity = myBillEntity)
}
}
},
onViewBillHistoryClicked = {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_view_bill_history),
)
viewModelScope.launch {
delay(200)
_navigateToNextScreen.emit(
MyBillHistoryDetailsScreenDestination(
myBillEntity = myBillEntity,
source = naviBbpsVmData.screen.name,
initialSource = initialSource,
)
)
}
},
onRemoveAccountClicked = {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_remove_account),
)
viewModelScope.launch {
delay(200)
_billerListBottomSheetType.update {
BillerListBottomSheetType.RemoveAccount(
myBillEntity = myBillEntity,
title =
naviBbpsDefaultConfig.value.configMessage
.removeAccountBottomSheetTitle,
description =
naviBbpsDefaultConfig.value.configMessage
.removeAccountBottomSheetMessage,
firstBtnTextResId = R.string.bbps_cancel,
secondButtonTextResId = R.string.bbps_remove,
onFirstBtnClick = {},
onSecondBtnClick = { deleteBill(myBillEntity) },
)
}
openSheet.invoke()
}
},
)
}
}
fun onDeleteAccountBillClicked(myBillEntity: MyBillEntity) {
viewModelScope.launch {
delay(100) // for smooth transition of next bottom sheet
_billerListBottomSheetType.update {
BillerListBottomSheetType.Confirmation(
title = naviBbpsDefaultConfig.value.configMessage.removeAccountBottomSheetTitle,
description =
naviBbpsDefaultConfig.value.configMessage.removeAccountBottomSheetMessage,
firstBtnTextResId = R.string.bbps_cancel,
secondButtonTextResId = R.string.bbps_remove,
onFirstBtnClick = {},
onSecondBtnClick = { deleteBill(myBillEntity) },
private suspend fun refreshBillAndUpdateInfo(myBillEntity: MyBillEntity) {
updateRefreshState(isRefreshing = true, bill = myBillEntity)
myBillActionsHandler.refreshBill(
myBillEntity = myBillEntity,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_sucessfully,
)
}
}
},
onError = { response ->
val errorCode = getError(response).code
if (errorCode in naviBbpsDefaultConfig.value.billAlreadyPaidErrorCodes) {
notifyError(response = response)
} else {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_failed,
)
}
},
)
updateRefreshState(isRefreshing = false, bill = myBillEntity)
}
private suspend fun updateRecentBillsEntity() {
@@ -321,55 +432,91 @@ constructor(
}
private fun deleteBill(myBillEntity: MyBillEntity) {
viewModelScope.launch(Dispatchers.IO) {
val savedBills =
bbpsCommonRepository.fetchSavedBillsByCategory(
category = billCategoryEntity.categoryId
)
if (savedBills.isNotEmpty()) {
if (myBillEntity.isNotNull()) {
val deleteBillResponse =
bbpsCommonRepository.deleteBill(
myBillEntity.billId,
metricInfo = getBbpsMetricInfo(screenName = naviBbpsVmData.screen.name),
)
if (deleteBillResponse.isSuccessWithData()) {
myBillsSyncJob.refreshBills(naviBbpsVmData.screen.screenName)
updateRecentBillsEntity()
if (billerListState.value is BillerListState.Loaded) {
updateBillerListState(
BillerListState.Loaded(
recentBills = recentBillsEntity,
billerGroups =
(billerListState.value as BillerListState.Loaded)
.billerGroups,
popularBillers =
(billerListState.value as BillerListState.Loaded)
.popularBillers,
)
viewModelScope.launch(dispatcherProvider.io) {
myBillActionsHandler.deleteBill(
billId = myBillEntity.billId,
categoryId = billCategoryEntity.categoryId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = { deleteBillResponse ->
updateRecentBillsEntity()
if (billerListState.value is BillerListState.Loaded) {
updateBillerListState(
BillerListState.Loaded(
recentBills = recentBillsEntity,
billerGroups =
(billerListState.value as BillerListState.Loaded).billerGroups,
popularBillers =
(billerListState.value as BillerListState.Loaded).popularBillers,
)
}
updateSnackBarState(showSnackBar = true)
naviBbpsAnalytics.billDeletedSuccessfully(
data = deleteBillResponse.data!!,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
} else {
notifyError(deleteBillResponse)
naviBbpsAnalytics.billDeletionFailed(
deleteBillResponse = deleteBillResponse,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
}
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_removed_successfully,
)
naviBbpsAnalytics.billDeletionSuccess(
data = deleteBillResponse.data!!,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
},
onError = { deleteBillResponse ->
notifyError(deleteBillResponse)
naviBbpsAnalytics.billDeletionFailed(
deleteBillResponse = deleteBillResponse,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
},
)
}
}
private fun onMarkBillAsPaidConfirmedCtaClicked(myBillEntity: MyBillEntity) {
viewModelScope.launch(dispatcherProvider.io) {
delay(50)
/* before this bottom sheet is getting closed, added for smoother transition */
if (!naviNetworkConnectivity.isInternetConnected()) {
notifyError(getNoInternetErrorConfig())
return@launch
}
myBillActionsHandler.markAsPaid(
billId = myBillEntity.billId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(show = true, messageId = R.string.bbps_bill_marked_as_paid)
},
)
}
}
private fun observeRecentBills() {
viewModelScope.launch(dispatcherProvider.io) {
bbpsCommonRepository
.fetchSavedBillsByCategoryAsFlow(categoryId = billCategoryEntity.categoryId)
.collectLatest { recentBills ->
recentBillsEntity =
RecentBillsEntity(
title = resourceProvider.getString(R.string.bbps_recents),
bills = recentBills,
)
if (billerListState.value is BillerListState.Loaded) {
updateBillerListState(
BillerListState.Loaded(
recentBills = recentBillsEntity,
billerGroups =
(billerListState.value as BillerListState.Loaded).billerGroups,
popularBillers =
(billerListState.value as BillerListState.Loaded).popularBillers,
)
)
}
}
}
}
}
@@ -587,32 +734,6 @@ constructor(
)
}
private fun observeRecentBills() {
viewModelScope.launch(dispatcherProvider.io) {
bbpsCommonRepository
.fetchSavedBillsByCategoryAsFlow(categoryId = billCategoryEntity.categoryId)
.distinctUntilChanged()
.collectLatest { recentBills ->
recentBillsEntity =
RecentBillsEntity(
title = resourceProvider.getString(R.string.bbps_recents),
bills = recentBills,
)
if (billerListState.value is BillerListState.Loaded) {
updateBillerListState(
BillerListState.Loaded(
recentBills = recentBillsEntity,
billerGroups =
(billerListState.value as BillerListState.Loaded).billerGroups,
popularBillers =
(billerListState.value as BillerListState.Loaded).popularBillers,
)
)
}
}
}
}
private fun createLoadedStateFromBillerResponse(
billerListResponse: RepoResult<BillerListResponse>
): BillerListState.Loaded {

View File

@@ -13,7 +13,33 @@ import com.navi.rr.common.models.CoinBurnData
import com.navi.rr.common.models.OfferData
sealed class BillerListBottomSheetType {
data class MenuOptions(val myBillEntity: MyBillEntity) : BillerListBottomSheetType()
data class MenuOptions(
val myBillEntity: MyBillEntity,
val onMarkAsPaidClicked: () -> Unit,
val onRefreshBillClicked: () -> Unit,
val onViewBillHistoryClicked: () -> Unit,
val onRemoveAccountClicked: () -> Unit,
) : BillerListBottomSheetType()
data class MarkAsPaid(
val myBillEntity: MyBillEntity,
val title: String,
val description: String? = null,
val firstBtnText: String,
val secondButtonText: String,
val onFirstBtnClick: () -> Unit,
val onSecondBtnClick: () -> Unit,
) : BillerListBottomSheetType()
data class RemoveAccount(
val myBillEntity: MyBillEntity,
val title: String,
val description: String? = null,
val firstBtnTextResId: Int,
val secondButtonTextResId: Int,
val onFirstBtnClick: () -> Unit,
val onSecondBtnClick: () -> Unit,
) : BillerListBottomSheetType()
data object FastagBankDetails : BillerListBottomSheetType()
@@ -32,6 +58,8 @@ sealed class BillerListBottomSheetType {
val coinBurnData: CoinBurnData?,
) : BillerListBottomSheetType()
data object None : BillerListBottomSheetType()
companion object {
fun createEmptyMyBillEntity(): MyBillEntity {
return MyBillEntity(

View File

@@ -8,19 +8,13 @@
package com.navi.bbps.feature.billerlist.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Start
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
@@ -34,11 +28,16 @@ import com.navi.bbps.common.ICON_FASTAG_BOTTOMSHEET
import com.navi.bbps.common.NaviBbpsAnalytics
import com.navi.bbps.common.theme.NaviBbpsColor
import com.navi.bbps.common.ui.BbpsOfferBottomSheet
import com.navi.bbps.common.ui.BillOptionsBottomSheetContent
import com.navi.bbps.common.ui.ConfirmationBottomSheetContent
import com.navi.bbps.common.ui.ThemeRoundedButton
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.isRechargeCategory
import com.navi.bbps.feature.billerlist.model.view.BillerListBottomSheetType
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.mybills.ui.MarkAsPaidBottomSheetContent
import com.navi.bbps.feature.mybills.ui.RemoveAccountBottomSheetContent
import com.navi.bbps.getBillTitleFromAccountHolderNameOrPrimaryCustomerParams
import com.navi.common.R as CommonR
import com.navi.common.utils.firstLetterToLowerCase
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
@@ -48,7 +47,6 @@ import com.navi.naviwidgets.extensions.NaviText
fun BillerListBottomSheetContent(
billerListBottomSheetType: BillerListBottomSheetType,
closeSheet: () -> Unit,
onDeleteAccountBillClicked: (MyBillEntity) -> Unit,
naviBbpsAnalytics: NaviBbpsAnalytics.BillerList,
sessionAttribute: Map<String, String>,
source: String,
@@ -56,10 +54,45 @@ fun BillerListBottomSheetContent(
) {
when (billerListBottomSheetType) {
is BillerListBottomSheetType.MenuOptions -> {
BillerListMenuBottomSheetContent(
onDeleteAccountBillClicked = {
onDeleteAccountBillClicked.invoke(billerListBottomSheetType.myBillEntity)
}
BillOptionsBottomSheetContent(
billerLogoUrl = billerListBottomSheetType.myBillEntity.billerLogoUrl,
title =
getBillTitleFromAccountHolderNameOrPrimaryCustomerParams(
accountHolderName =
billerListBottomSheetType.myBillEntity.unpaidBillDetails
?.accountHolderName,
primaryCustomerParams =
billerListBottomSheetType.myBillEntity.primaryCustomerParamValue,
),
expiringOn =
billerListBottomSheetType.myBillEntity.unpaidBillWarning
.firstLetterToLowerCase(),
amount = billerListBottomSheetType.myBillEntity.unpaidBillDetails?.amount.orEmpty(),
billerName = billerListBottomSheetType.myBillEntity.billerName,
onRemoveAccountClicked = {
closeSheet()
billerListBottomSheetType.onRemoveAccountClicked()
},
onCloseClicked = { closeSheet() },
isBillPaid = billerListBottomSheetType.myBillEntity.isBillPaid,
isRechargeCategory =
isRechargeCategory(
categoryId = billerListBottomSheetType.myBillEntity.categoryId
),
onMarkAsPaidClicked = {
closeSheet()
if (!billerListBottomSheetType.myBillEntity.isBillPaid) {
billerListBottomSheetType.onMarkAsPaidClicked()
}
},
onRefreshBillClicked = {
closeSheet()
billerListBottomSheetType.onRefreshBillClicked()
},
onViewBillHistoryClicked = {
closeSheet()
billerListBottomSheetType.onViewBillHistoryClicked()
},
)
}
is BillerListBottomSheetType.FastagBankDetails -> {
@@ -102,38 +135,43 @@ fun BillerListBottomSheetContent(
closeSheet = closeSheet,
)
}
}
}
@Composable
fun BillerListMenuBottomSheetContent(onDeleteAccountBillClicked: () -> Unit) {
Column(
modifier =
Modifier.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp)
) {
Row(
modifier =
Modifier.fillMaxWidth().padding(vertical = 16.dp).clickable {
onDeleteAccountBillClicked()
is BillerListBottomSheetType.MarkAsPaid -> {
MarkAsPaidBottomSheetContent(
title = billerListBottomSheetType.title,
description = billerListBottomSheetType.description,
secondaryButtonText = billerListBottomSheetType.firstBtnText,
primaryButtonText = billerListBottomSheetType.secondButtonText,
onSecondaryButtonClicked = {
closeSheet()
billerListBottomSheetType.onFirstBtnClick()
},
onPrimaryButtonClicked = {
closeSheet()
billerListBottomSheetType.onSecondBtnClick()
},
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = R.drawable.ic_remove_account),
contentDescription = null,
)
Spacer(modifier = Modifier.width(14.dp))
NaviText(
text = stringResource(id = R.string.bbps_remove_account),
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
color = NaviBbpsColor.textPrimary,
}
is BillerListBottomSheetType.RemoveAccount -> {
RemoveAccountBottomSheetContent(
title = billerListBottomSheetType.title,
description = billerListBottomSheetType.description,
secondaryButtonText =
stringResource(id = billerListBottomSheetType.firstBtnTextResId),
primaryButtonText =
stringResource(id = billerListBottomSheetType.secondButtonTextResId),
onSecondaryButtonClicked = {
closeSheet()
billerListBottomSheetType.onFirstBtnClick()
},
onPrimaryButtonClicked = {
closeSheet()
billerListBottomSheetType.onSecondBtnClick()
},
)
}
is BillerListBottomSheetType.None -> {
// Do nothing
}
}
}

View File

@@ -77,6 +77,7 @@ import com.navi.bbps.initials
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
import com.navi.design.snackbar.ErrorSnackBar
import com.navi.design.snackbar.SuccessSnackBar
import com.navi.naviwidgets.extensions.NaviText
import com.ramcosta.composedestinations.annotation.Destination
@@ -116,7 +117,6 @@ fun BillerListScreen(
billerListViewModel.showSpaceBelowSearchBar.collectAsStateWithLifecycle()
val billerListBottomSheetType by
billerListViewModel.billerListBottomSheetType.collectAsStateWithLifecycle()
val showSnackBar by billerListViewModel.showSnackBar.collectAsStateWithLifecycle()
val bbpsSnackBarPredefinedConfig = remember { BbpsSnackBarPredefinedConfig() }
val permissionResult by billerListViewModel.permissionResult.collectAsStateWithLifecycle()
@@ -126,6 +126,9 @@ fun BillerListScreen(
val coinBurnData by billerListViewModel.coinBurnData.collectAsStateWithLifecycle()
val multipleOffersDataList by
billerListViewModel.multipleOffersDataList.collectAsStateWithLifecycle()
val refreshBillItemAndStatus by
billerListViewModel.refreshBillItemAndStatus.collectAsStateWithLifecycle()
val snackBarState by billerListViewModel.snackBarState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
naviBbpsAnalytics.onLanded(
@@ -214,11 +217,6 @@ fun BillerListScreen(
billerListViewModel.onRecentBillItemClicked(myBillEntity = myBillEntity)
}
val onDeleteAccountBillClicked = { myBillEntity: MyBillEntity ->
billerListViewModel.onDeleteAccountBillClicked(myBillEntity)
openSheet()
}
val onBackClick = {
naviBbpsAnalytics.onBackClicked(
billCategoryEntity = billCategoryEntity,
@@ -348,7 +346,6 @@ fun BillerListScreen(
sheetContent = {
BillerListBottomSheetContent(
billerListBottomSheetType = billerListBottomSheetType,
onDeleteAccountBillClicked = onDeleteAccountBillClicked,
closeSheet = closeSheet,
naviBbpsAnalytics = naviBbpsAnalytics,
sessionAttribute = billerListViewModel.getNaviBbpsSessionAttributes(),
@@ -466,7 +463,7 @@ fun BillerListScreen(
)
onRecentBillItemClicked(it)
},
onDeleteRecentBillClicked = {
onBillOptionsKebabMenuClicked = {
naviBbpsAnalytics.onDeleteAccountKebabMenuClicked(
myBillEntity = it,
sessionAttribute =
@@ -474,7 +471,10 @@ fun BillerListScreen(
source = source,
initialSource = initialSource,
)
billerListViewModel.onDeleteMenuClicked(myBillEntity = it)
billerListViewModel.onBillOptionsKebabMenuClicked(
myBillEntity = it,
openSheet = openSheet,
)
openSheet()
},
isSearchBillerRunning = isSearchBillerRunning,
@@ -482,6 +482,7 @@ fun BillerListScreen(
fetchLocationPermissionState.allPermissionsGranted,
naviBbpsAnalytics = naviBbpsAnalytics,
multipleOffersDataList = multipleOffersDataList,
refreshBillItemAndStatus = refreshBillItemAndStatus,
onFailedOrderCalloutClicked = {
val failedOrder = it.first
val billDetails = it.second
@@ -523,23 +524,43 @@ fun BillerListScreen(
}
},
snackbarHost = {
if (showSnackBar) {
if (snackBarState.show) {
Column {
SuccessSnackBar(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
show = true,
snackBarConfig =
bbpsSnackBarPredefinedConfig.successConfig(
title =
stringResource(
id = R.string.bbps_account_removed_successfully
)
),
onDismissed = { billerListViewModel.updateSnackBarState(false) },
onTrailingIconClicked = {
billerListViewModel.updateSnackBarState(false)
},
)
if (snackBarState.messageId == R.string.bbps_bill_refreshed_failed) {
ErrorSnackBar(
modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 32.dp),
show = true,
snackBarConfig =
bbpsSnackBarPredefinedConfig.errorConfig(
title = stringResource(id = snackBarState.messageId)
),
onDismissed = {
billerListViewModel.updateSnackBarState(show = false)
},
onTrailingIconClicked = {
billerListViewModel.updateSnackBarState(show = false)
},
)
} else {
SuccessSnackBar(
modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 32.dp),
show = true,
snackBarConfig =
bbpsSnackBarPredefinedConfig.successConfig(
title = stringResource(id = snackBarState.messageId)
),
onDismissed = {
billerListViewModel.updateSnackBarState(show = false)
},
onTrailingIconClicked = {
billerListViewModel.updateSnackBarState(show = false)
},
)
}
Spacer(modifier = Modifier.height(16.dp))
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -60,7 +61,6 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.bbps.R
import com.navi.bbps.common.BILL_ITEM_SLIDE_OUT_DURATION
import com.navi.bbps.common.BULLET
import com.navi.bbps.common.NaviBbpsAnalytics
import com.navi.bbps.common.NaviBbpsDimens
import com.navi.bbps.common.model.view.RefreshBillState
@@ -99,6 +99,7 @@ import com.navi.rr.common.models.OfferData
import kotlin.math.ceil
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RenderBillerListScreen(
billerListState: BillerListState.Loaded,
@@ -108,11 +109,12 @@ fun RenderBillerListScreen(
openSheet: () -> Unit,
onBillerItemClicked: (BillerItemEntity) -> Unit,
onRecentBillItemClicked: (MyBillEntity) -> Unit,
onDeleteRecentBillClicked: (MyBillEntity) -> Unit,
onBillOptionsKebabMenuClicked: (MyBillEntity) -> Unit,
isSearchBillerRunning: Boolean,
isLocationPermissionProvided: Boolean,
naviBbpsAnalytics: NaviBbpsAnalytics.BillerList,
multipleOffersDataList: Map<String, List<OfferData>>?,
refreshBillItemAndStatus: RefreshBillState,
onFailedOrderCalloutClicked: (Pair<OrderEntity, MyBillEntity>) -> Unit,
) {
val isScreenFastagRecharge by
@@ -217,6 +219,17 @@ fun RenderBillerListScreen(
mutableStateOf(billerListState.recentBills.bills.toList())
}
val vmRecentBills = rememberUpdatedState(billerListState.recentBills.bills)
fun updateAnimatedRecentBills() {
animatedRecentBills = vmRecentBills.value.toList()
}
LaunchedEffect(vmRecentBills.value) {
if (animatedRecentBills.size == vmRecentBills.value.size) {
updateAnimatedRecentBills()
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
flingBehavior = maxScrollFlingBehavior(),
@@ -301,14 +314,15 @@ fun RenderBillerListScreen(
RecentBillerItem(
myBillEntity = billItem,
onRecentBillItemClicked = { onRecentBillItemClicked(billItem) },
onDeleteRecentBillClicked = {
onDeleteRecentBillClicked(billItem)
onBillOptionsKebabMenuClicked = {
onBillOptionsKebabMenuClicked(billItem)
},
offerData =
getOfferDataForMultipleBillItems(
multipleOffersDataList = multipleOffersDataList,
billId = billItem.billId,
),
refreshBillItemAndStatus = refreshBillItemAndStatus,
failedOrder = failedOrder,
onFailedOrderCalloutClicked = onFailedOrderCalloutClicked,
)
@@ -454,10 +468,11 @@ fun RecentBillerItem(
modifier: Modifier = Modifier,
myBillEntity: MyBillEntity,
onRecentBillItemClicked: () -> Unit,
onDeleteRecentBillClicked: (MyBillEntity) -> Unit,
onBillOptionsKebabMenuClicked: (MyBillEntity) -> Unit,
offerData: OfferData?,
failedOrder: OrderEntity? = null,
onFailedOrderCalloutClicked: (Pair<OrderEntity, MyBillEntity>) -> Unit,
refreshBillItemAndStatus: RefreshBillState,
) {
Box(modifier = modifier) {
Column(
@@ -491,52 +506,11 @@ fun RecentBillerItem(
Spacer(modifier = Modifier.height(2.dp))
BillerNameWithPrimaryCustomerParamValueSection(myBillEntity = myBillEntity)
Spacer(modifier = Modifier.height(2.dp))
if (myBillEntity.isBillPaid) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (myBillEntity.formattedLastPaidAmount.isNotBlank()) {
NaviText(
text =
stringResource(
id = R.string.bbps_rupee_symbol_x,
myBillEntity.formattedLastPaidAmount,
),
color = NaviBbpsColor.textPrimary,
fontFamily = naviFontFamily,
fontSize = 14.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
overflow = TextOverflow.Ellipsis,
)
}
if (myBillEntity.formattedLastPaidDate.isNotBlank()) {
Spacer(modifier = Modifier.width(4.dp))
NaviText(
text = BULLET,
color = NaviBbpsColor.textPrimary,
fontFamily = naviFontFamily,
fontSize = 16.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
)
Spacer(modifier = Modifier.width(4.dp))
NaviText(
text =
"${stringResource(id = R.string.bbps_last_paid)} ${myBillEntity.formattedLastPaidDate}",
color = NaviBbpsColor.textTertiary,
fontFamily = naviFontFamily,
fontSize = 12.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
} else {
BillAmountAndDueDateSectionWithShimmer(
myBillEntity = myBillEntity,
refreshBillItemAndStatus = RefreshBillState(),
startWithPadding = false,
)
}
BillAmountAndDueDateSectionWithShimmer(
myBillEntity = myBillEntity,
refreshBillItemAndStatus = refreshBillItemAndStatus,
startWithPadding = false,
)
}
Spacer(modifier = Modifier.width(16.dp))
@@ -576,7 +550,7 @@ fun RecentBillerItem(
Modifier.padding(top = 6.dp)
.size(24.dp)
.align(Alignment.Top)
.clickableDebounce { onDeleteRecentBillClicked(myBillEntity) },
.clickableDebounce { onBillOptionsKebabMenuClicked(myBillEntity) },
)
}
if (!myBillEntity.isBillPaid) {

View File

@@ -19,18 +19,15 @@ import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.ZERO_STRING
import com.navi.base.utils.isNotNull
import com.navi.base.utils.orFalse
import com.navi.base.utils.retry
import com.navi.bbps.R
import com.navi.bbps.common.APP_VERSION_CODE
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.DEFAULT_RETRY_COUNT
import com.navi.bbps.common.NaviBbpsAnalytics
import com.navi.bbps.common.NaviBbpsScreen
import com.navi.bbps.common.RCBP_CATEGORY
import com.navi.bbps.common.RETRY_INTERVAL_IN_SECONDS
import com.navi.bbps.common.TXN_AMOUNT
import com.navi.bbps.common.model.NaviBbpsVmData
import com.navi.bbps.common.model.config.NaviBbpsDefaultConfig
@@ -38,6 +35,7 @@ import com.navi.bbps.common.model.view.RefreshBillState
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.session.NaviBbpsSessionHelper
import com.navi.bbps.common.usecase.FindLastOrderWithSuccessfulPaymentUseCase
import com.navi.bbps.common.usecase.MyBillActionsHandler
import com.navi.bbps.common.usecase.NaviBbpsConfigUseCase
import com.navi.bbps.common.usecase.UploadUserDataUseCase
import com.navi.bbps.common.utils.BillDetailsResponseToEntityMapper
@@ -60,9 +58,7 @@ import com.navi.bbps.feature.category.model.view.PendingBillsShowMoreLessButtonS
import com.navi.bbps.feature.category.model.view.RewardDataEntity
import com.navi.bbps.feature.contactlist.PhoneContactManager
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.BillDetailsResponse
import com.navi.bbps.feature.customerinput.model.network.DeviceDetails
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
@@ -133,6 +129,7 @@ constructor(
val uploadUserDataUseCase: UploadUserDataUseCase,
open val bbpsSharedPreferences: BbpsSharedPreferences,
val findLastOrderWithSuccessfulPaymentUseCase: FindLastOrderWithSuccessfulPaymentUseCase,
private val myBillActionsHandler: MyBillActionsHandler,
) :
NaviBbpsBaseVM(
naviBbpsVmData = NaviBbpsVmData(screen = NaviBbpsScreen.NAVI_BBPS_BILL_CATEGORIES)
@@ -639,20 +636,37 @@ constructor(
fun deleteBill(myBillEntity: MyBillEntity) {
viewModelScope.launch(dispatcherProvider.io) {
val deleteBillResponse =
bbpsCommonRepository.deleteBill(
myBillEntity.billId,
metricInfo = getBbpsMetricInfo(screenName = naviBbpsVmData.screen.screenName),
)
if (deleteBillResponse.isSuccessWithData()) {
myBillsSyncJob.refreshBills(naviBbpsVmData.screen.screenName)
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_removed_successfully,
)
} else {
updateSnackBarState(show = true, messageId = R.string.bbps_account_delete_failed)
}
myBillActionsHandler.deleteBill(
billId = myBillEntity.billId,
categoryId = myBillEntity.categoryId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = { deleteBillResponse ->
naviBbpsAnalytics.billDeletionSuccess(
data = deleteBillResponse.data!!,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_removed_successfully,
)
},
onError = { deleteBillResponse ->
naviBbpsAnalytics.billDeletionFailed(
deleteBillResponse = deleteBillResponse,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_delete_failed,
)
},
)
}
}
@@ -818,27 +832,13 @@ constructor(
notifyError(getNoInternetErrorConfig())
return@launch
}
val response =
retry(
retryCount = DEFAULT_RETRY_COUNT,
retryIntervalInSeconds = RETRY_INTERVAL_IN_SECONDS,
execute = {
myBillsRepository.markBillAsPaid(
myBillEntity.billId,
metricInfo =
getBbpsMetricInfo(
screenName = naviBbpsVmData.screen.screenName,
isNae = { false },
),
)
},
shouldRetry = { !it.isSuccessWithData() },
)
if (response.isSuccessWithData()) {
myBillsSyncJob.refreshBills(naviBbpsVmData.screen.screenName)
updateSnackBarState(show = true, messageId = R.string.bbps_bill_marked_as_paid)
}
myBillActionsHandler.markAsPaid(
billId = myBillEntity.billId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(show = true, messageId = R.string.bbps_bill_marked_as_paid)
},
)
}
}
@@ -852,33 +852,27 @@ constructor(
}
updateRefreshState(isRefreshing = true, bill = myBillEntity)
val billDetailsRequest =
BillDetailsRequest(
billerId = myBillEntity.billerId,
customerParams = myBillEntity.customerParams,
deviceDetails = DeviceDetails(ip = naviNetworkConnectivity.getIpAddress()),
amount = myBillEntity.actualLastPaidAmount,
isConsentProvided = true,
)
val billDetailsResponse =
bbpsCommonRepository.fetchBillDetails(
billDetailsRequest = billDetailsRequest,
metricInfo = getBbpsMetricInfo(screenName = naviBbpsVmData.screen.screenName),
)
if (billDetailsResponse.isSuccessWithData()) {
myBillEntity.unpaidBillDetails?.let {
myBillsSyncJob.refreshBills(naviBbpsVmData.screen.screenName)
myBillActionsHandler.refreshBill(
myBillEntity = myBillEntity,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_sucessfully,
)
}
} else {
updateSnackBarState(show = true, messageId = R.string.bbps_bill_refreshed_failed)
}
},
onError = { response ->
val errorCode = getError(response).code
if (errorCode in naviBbpsDefaultConfig.billAlreadyPaidErrorCodes) {
notifyError(response = response)
} else {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_failed,
)
}
},
)
updateRefreshState(isRefreshing = false, bill = myBillEntity)
}

View File

@@ -30,6 +30,7 @@ import com.navi.bbps.common.model.view.NaviPermissionResult
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.session.NaviBbpsSessionHelper
import com.navi.bbps.common.usecase.FindLastOrderWithSuccessfulPaymentUseCase
import com.navi.bbps.common.usecase.MyBillActionsHandler
import com.navi.bbps.common.usecase.NaviBbpsConfigUseCase
import com.navi.bbps.common.usecase.OriginExperimentUtils
import com.navi.bbps.common.usecase.UploadUserDataUseCase
@@ -91,6 +92,7 @@ constructor(
bbpsCommonRepository: BbpsCommonRepository,
naviNetworkConnectivity: NaviNetworkConnectivity,
uploadUserDataUseCase: UploadUserDataUseCase,
myBillActionsHandler: MyBillActionsHandler,
@NaviBbpsGsonBuilder val naviBbpsGson: Gson,
private val resourceProvider: ResourceProvider,
val originBillDetectionHandler: OriginBillDetectionHandler,
@@ -120,6 +122,7 @@ constructor(
uploadUserDataUseCase = uploadUserDataUseCase,
bbpsSharedPreferences = bbpsSharedPreferences,
findLastOrderWithSuccessfulPaymentUseCase = findLastOrderWithSuccessfulPaymentUseCase,
myBillActionsHandler = myBillActionsHandler,
),
BottomSheetController by BottomSheetControllerImpl() {

View File

@@ -12,12 +12,13 @@ import androidx.compose.ui.res.stringResource
import com.navi.bbps.common.NaviBbpsAnalytics
import com.navi.bbps.common.arc.ui.ArcNudgeBottomSheetContent
import com.navi.bbps.common.ui.BbpsOfferBottomSheet
import com.navi.bbps.common.ui.BillOptionsBottomSheetContent
import com.navi.bbps.common.ui.TitleDescriptionWithLinearProgressBar
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.isRechargeCategory
import com.navi.bbps.common.utils.getDisplayableAmount
import com.navi.bbps.feature.category.model.view.BillCategoryBottomSheetType
import com.navi.bbps.feature.mybills.ui.MarkAsPaidBottomSheetContent
import com.navi.bbps.feature.mybills.ui.RemoveAccountBottomSheetContent
import com.navi.bbps.feature.mybills.ui.UnpaidBillOptionsBottomSheetContent
import com.navi.bbps.getBillTitleFromAccountHolderNameOrPrimaryCustomerParams
import com.navi.common.utils.firstLetterToLowerCase
@@ -90,13 +91,15 @@ fun BillCategoryBottomSheetContent(
)
}
is BillCategoryBottomSheetType.UnpaidBillOptions -> {
UnpaidBillOptionsBottomSheetContent(
BillOptionsBottomSheetContent(
billerLogoUrl = billCategoryBottomSheetType.myBillEntity.billerLogoUrl,
title =
getBillTitleFromAccountHolderNameOrPrimaryCustomerParams(
billCategoryBottomSheetType.myBillEntity.unpaidBillDetails
?.accountHolderName,
billCategoryBottomSheetType.myBillEntity.primaryCustomerParamValue,
accountHolderName =
billCategoryBottomSheetType.myBillEntity.unpaidBillDetails
?.accountHolderName,
primaryCustomerParams =
billCategoryBottomSheetType.myBillEntity.primaryCustomerParamValue,
),
expiringOn =
billCategoryBottomSheetType.myBillEntity.unpaidBillWarning
@@ -106,7 +109,11 @@ fun BillCategoryBottomSheetContent(
?.amount
?.getDisplayableAmount()
.orEmpty(),
operatorName = billCategoryBottomSheetType.myBillEntity.billerName,
billerName = billCategoryBottomSheetType.myBillEntity.billerName,
isRechargeCategory =
isRechargeCategory(
categoryId = billCategoryBottomSheetType.myBillEntity.categoryId
),
onMarkAsPaidClicked = {
closeSheet()
naviBbpsAnalytics.onMyBillMoreOptionsBottomSheetClicked(

View File

@@ -29,9 +29,11 @@ import com.navi.bbps.common.mapper.BillResponseToBillDetailsEntityMapper
import com.navi.bbps.common.model.NaviBbpsVmData
import com.navi.bbps.common.model.config.NaviBbpsDefaultConfig
import com.navi.bbps.common.model.view.NaviPermissionResult
import com.navi.bbps.common.model.view.RefreshBillState
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.session.NaviBbpsSessionHelper
import com.navi.bbps.common.usecase.FindLastOrderWithSuccessfulPaymentUseCase
import com.navi.bbps.common.usecase.MyBillActionsHandler
import com.navi.bbps.common.usecase.NaviBbpsConfigUseCase
import com.navi.bbps.common.usecase.RewardNudgeUseCase
import com.navi.bbps.common.utils.BillDetailsResponseToEntityMapper
@@ -50,9 +52,12 @@ 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.MyBillHistoryDetailsScreenDestination
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
import com.navi.bbps.feature.mybills.MyBillsViewModel.SnackBarState
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.paybill.model.network.PaymentAmountExactness
import com.navi.bbps.feature.paybill.model.view.PayBillSource
@@ -68,6 +73,7 @@ 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
@@ -76,7 +82,6 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -101,7 +106,10 @@ constructor(
private val billDetailsResponseToEntityMapper: BillDetailsResponseToEntityMapper,
private val resourceProvider: ResourceProvider,
private val naviBbpsDateUtils: NaviBbpsDateUtils,
private val findLastOrderWithSuccessfulPaymentUseCase: FindLastOrderWithSuccessfulPaymentUseCase,
private val findLastOrderWithSuccessfulPaymentUseCase:
FindLastOrderWithSuccessfulPaymentUseCase,
private val myBillsRepository: MyBillsRepository,
private val myBillActionsHandler: MyBillActionsHandler,
) :
NaviBbpsBaseVM(
naviBbpsVmData = NaviBbpsVmData(screen = NaviBbpsScreen.NAVI_BBPS_CONTACT_LIST_SCREEN)
@@ -137,6 +145,12 @@ constructor(
private var operatorBillersList = emptyList<BillerItemEntity>()
private val _snackBarState = MutableStateFlow(SnackBarState(show = false))
val snackBarState = _snackBarState.asStateFlow()
private val _refreshBillItemAndStatus = MutableStateFlow(RefreshBillState())
val refreshBillItemAndStatus = _refreshBillItemAndStatus.asStateFlow()
val isSearchQueryEmpty: StateFlow<Boolean> =
_searchQuery
.map { it.isEmpty() }
@@ -156,9 +170,6 @@ constructor(
private val _showPostpaidBottomSheet = MutableSharedFlow<Boolean>()
val showPostpaidBottomSheet = _showPostpaidBottomSheet.asSharedFlow()
private val _showSnackBar = MutableStateFlow(false)
val showSnackBar = _showSnackBar.asStateFlow()
private val _allRecentBills = MutableStateFlow(RecentBillsEntity("", emptyList()))
val allRecentBills = _allRecentBills.asStateFlow()
@@ -166,11 +177,7 @@ constructor(
val isContactListEmpty = _isContactListEmpty.asStateFlow()
private val _contactListBottomSheetType =
MutableStateFlow<ContactListBottomSheetType>(
ContactListBottomSheetType.MenuOptions(
ContactListBottomSheetType.createEmptyMyBillEntity()
)
)
MutableStateFlow<ContactListBottomSheetType>(ContactListBottomSheetType.None)
val contactListBottomSheetType = _contactListBottomSheetType.asStateFlow()
var isPermissionPopupSeenOnLanded = false
@@ -187,8 +194,12 @@ constructor(
_contactListState.update { contactListState }
}
fun updateSnackBarState(showSnackBar: Boolean) {
_showSnackBar.update { showSnackBar }
fun updateSnackBarState(show: Boolean, messageId: Int = R.string.bbps_copied_to_clipboard) {
_snackBarState.update { SnackBarState(show = show, messageId = messageId) }
}
private fun updateRefreshState(isRefreshing: Boolean, bill: MyBillEntity? = null) {
_refreshBillItemAndStatus.value = RefreshBillState(isRefreshing, bill)
}
val phoneNumber = MutableStateFlow("")
@@ -495,11 +506,20 @@ constructor(
}
}
private suspend fun getRecentBills(): RecentBillsEntity {
val savedBills =
bbpsCommonRepository.fetchSavedBillsByCategory(category = billCategoryEntity.categoryId)
return RecentBillsEntity(
title = resourceProvider.getString(R.string.bbps_recent_recharges),
bills = savedBills,
)
}
private fun observeRecentBills() {
viewModelScope.launch(dispatcherProvider.io) {
bbpsCommonRepository
.fetchSavedBillsByCategoryAsFlow(categoryId = billCategoryEntity.categoryId)
.distinctUntilChanged()
.collectLatest { recentBills ->
if (contactListUIState.value is ContactListState.Loaded) {
updateContactListUIState(
@@ -519,33 +539,148 @@ constructor(
}
}
private suspend fun getRecentBills(): RecentBillsEntity {
val savedBills =
bbpsCommonRepository.fetchSavedBillsByCategory(category = billCategoryEntity.categoryId)
return RecentBillsEntity(
title = resourceProvider.getString(R.string.bbps_recent_recharges),
bills = savedBills,
)
}
fun onDeleteMenuClicked(myBillEntity: MyBillEntity) {
_contactListBottomSheetType.update { ContactListBottomSheetType.MenuOptions(myBillEntity) }
}
fun onDeleteAccountBillClicked(myBillEntity: MyBillEntity) {
fun onBillOptionsKebabMenuClicked(myBillEntity: MyBillEntity, openSheet: () -> Unit) {
_contactListBottomSheetType.update {
ContactListBottomSheetType.Confirmation(
title = naviBbpsDefaultConfig.configMessage.removeAccountBottomSheetTitle,
description = naviBbpsDefaultConfig.configMessage.removeAccountBottomSheetMessage,
firstBtnTextResId = R.string.bbps_cancel,
secondButtonTextResId = R.string.bbps_remove,
onFirstBtnClick = {},
onSecondBtnClick = { deleteBill(myBillEntity) },
ContactListBottomSheetType.MenuOptions(
myBillEntity = myBillEntity,
onMarkAsPaidClicked = {
viewModelScope.launch {
delay(200)
/* this is added to make sure the bottom sheet is closed before
opening the new one and to make the transition smoother */
_contactListBottomSheetType.update {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_mark_as_paid),
)
ContactListBottomSheetType.MarkAsPaid(
myBillEntity = myBillEntity,
title =
resourceProvider.getString(
R.string.bbps_bill_mark_as_paid_heading
),
description =
resourceProvider.getString(
R.string.bbps_bill_mark_as_paid_description
),
firstBtnText = resourceProvider.getString(R.string.bbps_no),
secondButtonText = resourceProvider.getString(R.string.bbps_yes),
onFirstBtnClick = {},
onSecondBtnClick = {
onMarkBillAsPaidConfirmedCtaClicked(myBillEntity = myBillEntity)
},
)
}
openSheet.invoke()
}
},
onRefreshBillClicked = {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_refresh_bill),
)
viewModelScope.launch {
if (!naviNetworkConnectivity.isInternetConnected()) {
notifyError(getNoInternetErrorConfig())
return@launch
} else {
refreshBillAndUpdateInfo(myBillEntity = myBillEntity)
}
}
},
onViewBillHistoryClicked = {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_view_bill_history),
)
viewModelScope.launch {
delay(200)
_navigateToNextScreen.emit(
MyBillHistoryDetailsScreenDestination(
myBillEntity = myBillEntity,
source = naviBbpsVmData.screen.name,
initialSource = initialSource,
)
)
}
},
onRemoveAccountClicked = {
naviBbpsAnalytics.onBillOptionsClicked(
sessionAttribute = getNaviBbpsSessionAttributes(),
billEntity = myBillEntity,
action = resourceProvider.getString(R.string.bbps_remove_account),
)
viewModelScope.launch {
delay(200)
_contactListBottomSheetType.update {
ContactListBottomSheetType.RemoveAccount(
myBillEntity = myBillEntity,
title =
naviBbpsDefaultConfig.configMessage
.removeAccountBottomSheetTitle,
description =
naviBbpsDefaultConfig.configMessage
.removeAccountBottomSheetMessage,
firstBtnTextResId = R.string.bbps_cancel,
secondButtonTextResId = R.string.bbps_remove,
onFirstBtnClick = {},
onSecondBtnClick = { deleteBill(myBillEntity) },
)
}
openSheet.invoke()
}
},
)
}
}
private fun onMarkBillAsPaidConfirmedCtaClicked(myBillEntity: MyBillEntity) {
viewModelScope.launch(dispatcherProvider.io) {
delay(50)
/* before this bottom sheet is getting closed, added for smoother transition */
if (!naviNetworkConnectivity.isInternetConnected()) {
notifyError(getNoInternetErrorConfig())
return@launch
}
myBillActionsHandler.markAsPaid(
billId = myBillEntity.billId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(show = true, messageId = R.string.bbps_bill_marked_as_paid)
},
)
}
}
private suspend fun refreshBillAndUpdateInfo(myBillEntity: MyBillEntity) {
updateRefreshState(isRefreshing = true, bill = myBillEntity)
myBillActionsHandler.refreshBill(
myBillEntity = myBillEntity,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_sucessfully,
)
},
onError = { response ->
val errorCode = getError(response).code
if (errorCode in naviBbpsDefaultConfig.billAlreadyPaidErrorCodes) {
notifyError(response = response)
} else {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_failed,
)
}
},
)
updateRefreshState(isRefreshing = false, bill = myBillEntity)
}
fun updateInvalidContactListBottomSheetType() {
_contactListBottomSheetType.update {
ContactListBottomSheetType.InvalidContactNumberSelection(
@@ -557,32 +692,38 @@ constructor(
private fun deleteBill(myBillEntity: MyBillEntity) {
viewModelScope.launch(Dispatchers.IO) {
val deleteBillResponse =
bbpsCommonRepository.deleteBill(
myBillEntity.billId,
metricInfo = getBbpsMetricInfo(screenName = naviBbpsVmData.screen.screenName),
)
if (deleteBillResponse.isSuccessWithData()) {
naviBbpsAnalytics.billDeletedSuccessfully(
data = deleteBillResponse.data!!,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
myBillsSyncJob.refreshBills(naviBbpsVmData.screen.screenName)
fetchContacts()
updateSnackBarState(true)
} else {
naviBbpsAnalytics.billDeletionFailed(
deleteBillResponse = deleteBillResponse,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
notifyError(deleteBillResponse)
}
myBillActionsHandler.deleteBill(
billId = myBillEntity.billId,
categoryId = billCategoryEntity.categoryId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = { deleteBillResponse ->
naviBbpsAnalytics.billDeletionSuccess(
data = deleteBillResponse.data!!,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
fetchContacts()
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_removed_successfully,
)
},
onError = { deleteBillResponse ->
naviBbpsAnalytics.billDeletionFailed(
deleteBillResponse = deleteBillResponse,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_delete_failed,
)
},
)
}
}

View File

@@ -14,7 +14,33 @@ import com.navi.rr.common.models.CoinBurnData
import com.navi.rr.common.models.OfferData
sealed class ContactListBottomSheetType {
data class MenuOptions(val myBillEntity: MyBillEntity) : ContactListBottomSheetType()
data class MenuOptions(
val myBillEntity: MyBillEntity,
val onMarkAsPaidClicked: () -> Unit,
val onRefreshBillClicked: () -> Unit,
val onViewBillHistoryClicked: () -> Unit,
val onRemoveAccountClicked: () -> Unit,
) : ContactListBottomSheetType()
data class MarkAsPaid(
val myBillEntity: MyBillEntity,
val title: String,
val description: String? = null,
val firstBtnText: String,
val secondButtonText: String,
val onFirstBtnClick: () -> Unit,
val onSecondBtnClick: () -> Unit,
) : ContactListBottomSheetType()
data class RemoveAccount(
val myBillEntity: MyBillEntity,
val title: String,
val description: String? = null,
val firstBtnTextResId: Int,
val secondButtonTextResId: Int,
val onFirstBtnClick: () -> Unit,
val onSecondBtnClick: () -> Unit,
) : ContactListBottomSheetType()
data class Confirmation(
val title: String,
@@ -39,6 +65,8 @@ sealed class ContactListBottomSheetType {
val phoneNumberDetailEntity: PhoneContactEntity,
) : ContactListBottomSheetType()
data object None : ContactListBottomSheetType()
companion object {
fun createEmptyMyBillEntity(): MyBillEntity {
return MyBillEntity(

View File

@@ -7,9 +7,6 @@
package com.navi.bbps.feature.contactlist.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -19,7 +16,6 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
@@ -29,7 +25,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -40,14 +35,19 @@ import com.navi.bbps.common.NaviBbpsAnalytics
import com.navi.bbps.common.theme.NaviBbpsColor
import com.navi.bbps.common.ui.BbpsCircleImage
import com.navi.bbps.common.ui.BbpsOfferBottomSheet
import com.navi.bbps.common.ui.BillOptionsBottomSheetContent
import com.navi.bbps.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButton
import com.navi.bbps.common.ui.ConfirmationBottomSheetContent
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.isRechargeCategory
import com.navi.bbps.feature.billerlist.model.view.BillerItemEntity
import com.navi.bbps.feature.contactlist.model.view.ContactListBottomSheetType
import com.navi.bbps.feature.contactlist.model.view.PhoneContactEntity
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.mybills.ui.MarkAsPaidBottomSheetContent
import com.navi.bbps.feature.mybills.ui.RemoveAccountBottomSheetContent
import com.navi.bbps.getBillTitleFromAccountHolderNameOrPrimaryCustomerParams
import com.navi.common.R as CommonR
import com.navi.common.customview.LoaderRoundedButton
import com.navi.common.utils.firstLetterToLowerCase
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
@@ -57,7 +57,6 @@ import com.navi.naviwidgets.extensions.NaviText
fun ContactListBottomSheetContent(
contactListBottomSheetType: ContactListBottomSheetType,
closeSheet: () -> Unit,
onDeleteAccountBillClicked: (MyBillEntity) -> Unit,
naviBbpsAnalytics: NaviBbpsAnalytics.ContactList,
sessionAttribute: Map<String, String>,
source: String,
@@ -70,10 +69,46 @@ fun ContactListBottomSheetContent(
when (contactListBottomSheetType) {
is ContactListBottomSheetType.MenuOptions -> {
ContactListMenuBottomSheetContent(
onDeleteAccountBillClicked = {
onDeleteAccountBillClicked.invoke(contactListBottomSheetType.myBillEntity)
}
BillOptionsBottomSheetContent(
billerLogoUrl = contactListBottomSheetType.myBillEntity.billerLogoUrl,
title =
getBillTitleFromAccountHolderNameOrPrimaryCustomerParams(
accountHolderName =
contactListBottomSheetType.myBillEntity.unpaidBillDetails
?.accountHolderName,
primaryCustomerParams =
contactListBottomSheetType.myBillEntity.primaryCustomerParamValue,
),
expiringOn =
contactListBottomSheetType.myBillEntity.unpaidBillWarning
.firstLetterToLowerCase(),
isBillPaid = contactListBottomSheetType.myBillEntity.isBillPaid,
amount =
contactListBottomSheetType.myBillEntity.unpaidBillDetails?.amount.toString(),
billerName = contactListBottomSheetType.myBillEntity.billerName,
isRechargeCategory =
isRechargeCategory(
categoryId = contactListBottomSheetType.myBillEntity.categoryId
),
onRemoveAccountClicked = {
closeSheet()
contactListBottomSheetType.onRemoveAccountClicked()
},
onCloseClicked = { closeSheet() },
onMarkAsPaidClicked = {
closeSheet()
if (!contactListBottomSheetType.myBillEntity.isBillPaid) {
contactListBottomSheetType.onMarkAsPaidClicked()
}
},
onRefreshBillClicked = {
closeSheet()
contactListBottomSheetType.onRefreshBillClicked()
},
onViewBillHistoryClicked = {
closeSheet()
contactListBottomSheetType.onViewBillHistoryClicked()
},
)
}
is ContactListBottomSheetType.Confirmation -> {
@@ -113,6 +148,40 @@ fun ContactListBottomSheetContent(
closeSheet = closeSheet,
)
}
is ContactListBottomSheetType.MarkAsPaid -> {
MarkAsPaidBottomSheetContent(
title = contactListBottomSheetType.title,
description = contactListBottomSheetType.description,
secondaryButtonText = contactListBottomSheetType.firstBtnText,
primaryButtonText = contactListBottomSheetType.secondButtonText,
onSecondaryButtonClicked = {
closeSheet()
contactListBottomSheetType.onFirstBtnClick()
},
onPrimaryButtonClicked = {
closeSheet()
contactListBottomSheetType.onSecondBtnClick()
},
)
}
is ContactListBottomSheetType.RemoveAccount -> {
RemoveAccountBottomSheetContent(
title = contactListBottomSheetType.title,
description = contactListBottomSheetType.description,
secondaryButtonText =
stringResource(id = contactListBottomSheetType.firstBtnTextResId),
primaryButtonText =
stringResource(id = contactListBottomSheetType.secondButtonTextResId),
onSecondaryButtonClicked = {
closeSheet()
contactListBottomSheetType.onFirstBtnClick()
},
onPrimaryButtonClicked = {
closeSheet()
contactListBottomSheetType.onSecondBtnClick()
},
)
}
is ContactListBottomSheetType.InvalidContactNumberSelection -> {
BottomSheetContentWithIconHeaderPrimarySecondaryButton(
iconId = CommonR.drawable.ic_purple_exclamation,
@@ -135,37 +204,8 @@ fun ContactListBottomSheetContent(
onPostpaidOperatorSelectionContinueClicked,
)
}
}
}
@Composable
fun ContactListMenuBottomSheetContent(onDeleteAccountBillClicked: () -> Unit) {
Column(
modifier =
Modifier.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp)
) {
Row(
modifier =
Modifier.fillMaxWidth().padding(vertical = 16.dp).clickable {
onDeleteAccountBillClicked()
},
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = R.drawable.ic_remove_account),
contentDescription = null,
)
Spacer(modifier = Modifier.width(14.dp))
NaviText(
text = stringResource(id = R.string.bbps_remove_account),
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
color = NaviBbpsColor.textPrimary,
)
is ContactListBottomSheetType.None -> {
// Do nothing
}
}
}

View File

@@ -61,9 +61,9 @@ import com.navi.bbps.feature.contactlist.model.view.ContactListState.Loaded
import com.navi.bbps.feature.contactlist.model.view.ContactListState.Loading
import com.navi.bbps.feature.contactlist.model.view.PhoneContactEntity
import com.navi.bbps.feature.destinations.NaviBbpsPermissionScreenDestination
import com.navi.bbps.feature.mybills.model.view.MyBillEntity
import com.navi.bbps.feature.permission.utils.PermissionKeys
import com.navi.bbps.feature.permission.utils.PermissionUtils
import com.navi.design.snackbar.ErrorSnackBar
import com.navi.design.snackbar.SuccessSnackBar
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -101,7 +101,6 @@ fun ContactListScreen(
val isSearchQueryEmpty by contactListViewModel.isSearchQueryEmpty.collectAsState(true)
val contactListBottomSheetType by
contactListViewModel.contactListBottomSheetType.collectAsStateWithLifecycle()
val showSnackBar by contactListViewModel.showSnackBar.collectAsStateWithLifecycle()
val recentBills by contactListViewModel.allRecentBills.collectAsStateWithLifecycle()
val bbpsSnackBarPredefinedConfig = remember { BbpsSnackBarPredefinedConfig() }
val permissionResult by contactListViewModel.permissionResult.collectAsStateWithLifecycle()
@@ -123,6 +122,9 @@ fun ContactListScreen(
val context = LocalContext.current
val view = LocalView.current
val keyboardController = LocalSoftwareKeyboardController.current
val refreshBillItemAndStatus by
contactListViewModel.refreshBillItemAndStatus.collectAsStateWithLifecycle()
val snackBarState by contactListViewModel.snackBarState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
contactListViewModel.fetchOffersAndCoinBurnForProduct(
@@ -269,16 +271,6 @@ fun ContactListScreen(
LaunchedEffect(Unit) { contactListViewModel.fetchContacts() }
}
val onDeleteMenuClicked = { myBillEntity: MyBillEntity ->
contactListViewModel.onDeleteMenuClicked(myBillEntity)
openSheet()
}
val onDeleteAccountBillClicked = { myBillEntity: MyBillEntity ->
contactListViewModel.onDeleteAccountBillClicked(myBillEntity)
openSheet()
}
val onContactSelected = { phoneNumberDetail: PhoneContactEntity ->
naviBbpsAnalytics.onContactClick(
billCategoryEntity = billCategoryEntity,
@@ -345,7 +337,6 @@ fun ContactListScreen(
ContactListBottomSheetContent(
contactListBottomSheetType = contactListBottomSheetType,
closeSheet = closeSheet,
onDeleteAccountBillClicked = onDeleteAccountBillClicked,
naviBbpsAnalytics = naviBbpsAnalytics,
sessionAttribute = contactListViewModel.getNaviBbpsSessionAttributes(),
source = source,
@@ -391,7 +382,7 @@ fun ContactListScreen(
contactList = contactList,
onContactSelected = onContactSelected,
contactListState = contactListUIState as Loaded,
onDeleteRecentBillClicked = {
onBillOptionsKebabMenuClicked = {
naviBbpsAnalytics.onDeleteAccountKebabMenuClicked(
myBillEntity = it,
sessionAttribute =
@@ -399,7 +390,10 @@ fun ContactListScreen(
source = source,
initialSource = initialSource,
)
onDeleteMenuClicked(it)
contactListViewModel.onBillOptionsKebabMenuClicked(
myBillEntity = it,
openSheet = openSheet,
)
openSheet()
},
onRecentBillItemClicked =
@@ -433,6 +427,7 @@ fun ContactListScreen(
openSheet()
},
multipleOffersDataList = multipleOffersDataList,
refreshBillItemAndStatus = refreshBillItemAndStatus,
onFailedOrderCalloutClicked = {
val failedOrder = it.first
val billDetails = it.second
@@ -472,23 +467,41 @@ fun ContactListScreen(
}
},
snackbarHost = {
if (showSnackBar) {
if (snackBarState.show) {
Column {
SuccessSnackBar(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
show = true,
snackBarConfig =
bbpsSnackBarPredefinedConfig.successConfig(
title =
stringResource(
id = R.string.bbps_account_removed_successfully
)
),
onDismissed = { contactListViewModel.updateSnackBarState(false) },
onTrailingIconClicked = {
contactListViewModel.updateSnackBarState(false)
},
)
if (snackBarState.messageId == R.string.bbps_bill_refreshed_failed) {
ErrorSnackBar(
modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 32.dp),
show = true,
snackBarConfig =
bbpsSnackBarPredefinedConfig.errorConfig(
title = stringResource(id = snackBarState.messageId)
),
onDismissed = {
contactListViewModel.updateSnackBarState(show = false)
},
onTrailingIconClicked = {
contactListViewModel.updateSnackBarState(show = false)
},
)
} else {
SuccessSnackBar(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
show = true,
snackBarConfig =
bbpsSnackBarPredefinedConfig.successConfig(
title = stringResource(id = snackBarState.messageId)
),
onDismissed = {
contactListViewModel.updateSnackBarState(show = false)
},
onTrailingIconClicked = {
contactListViewModel.updateSnackBarState(show = false)
},
)
}
Spacer(modifier = Modifier.height(16.dp))
}

View File

@@ -50,13 +50,14 @@ import androidx.compose.ui.unit.sp
import com.navi.bbps.R
import com.navi.bbps.common.BBPS_CROSS_FADE_DURATION
import com.navi.bbps.common.BILL_ITEM_SLIDE_OUT_DURATION
import com.navi.bbps.common.BULLET
import com.navi.bbps.common.MOBILE_NUMBER_LENGTH
import com.navi.bbps.common.NaviBbpsDimens
import com.navi.bbps.common.model.view.NaviPermissionResult
import com.navi.bbps.common.model.view.RefreshBillState
import com.navi.bbps.common.theme.NaviBbpsColor
import com.navi.bbps.common.ui.BbpsCircleImage
import com.navi.bbps.common.ui.BbpsListDivider
import com.navi.bbps.common.ui.BillAmountAndDueDateSectionWithShimmer
import com.navi.bbps.common.ui.BillItemOfferWithShimmer
import com.navi.bbps.common.ui.ContactIconView
import com.navi.bbps.common.ui.FailedOrderCallout
@@ -66,7 +67,6 @@ import com.navi.bbps.common.ui.OfferRolodexWithShimmer
import com.navi.bbps.common.ui.PermissionTile
import com.navi.bbps.common.ui.getOfferDataForMultipleBillItems
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getNormalisedPhoneNumber
import com.navi.bbps.common.utils.getDisplayableAmount
import com.navi.bbps.feature.contactlist.ContactListViewModel
import com.navi.bbps.feature.contactlist.model.view.ContactListState
import com.navi.bbps.feature.contactlist.model.view.PhoneContactEntity
@@ -92,7 +92,7 @@ fun RenderContactListScreen(
contactList: List<PhoneContactEntity>,
onContactSelected: (PhoneContactEntity) -> Unit,
contactListState: ContactListState.Loaded,
onDeleteRecentBillClicked: (MyBillEntity) -> Unit,
onBillOptionsKebabMenuClicked: (MyBillEntity) -> Unit,
onRecentBillItemClicked: (PhoneContactEntity, MyBillEntity) -> Unit,
getContactNameFromPhoneNumber: (String) -> String,
searchQuery: String,
@@ -107,6 +107,7 @@ fun RenderContactListScreen(
offerData: List<OfferData>?,
coinBurnData: CoinBurnData?,
multipleOffersDataList: Map<String, List<OfferData>>?,
refreshBillItemAndStatus: RefreshBillState,
onFailedOrderCalloutClicked: (Pair<OrderEntity, MyBillEntity>) -> Unit,
) {
val shouldShowRecentBills =
@@ -166,6 +167,16 @@ fun RenderContactListScreen(
}
val vmRecentBills = rememberUpdatedState(contactListState.recentBills.bills)
fun updateAnimatedRecentBills() {
animatedRecentBills = vmRecentBills.value.toList()
}
LaunchedEffect(vmRecentBills.value) {
if (animatedRecentBills.size == vmRecentBills.value.size) {
updateAnimatedRecentBills()
}
}
LazyColumn {
if (isSearchQueryEmpty) {
if (!isMyNumberItemVisible) {
@@ -207,9 +218,10 @@ fun RenderContactListScreen(
},
getContactNameFromPhoneNumber = getContactNameFromPhoneNumber,
onRecentBillItemClicked = onRecentBillItemClicked,
onDeleteRecentBillClicked = onDeleteRecentBillClicked,
onBillOptionsKebabMenuClicked = onBillOptionsKebabMenuClicked,
multipleOffersDataList = multipleOffersDataList,
onFailedOrderCalloutClicked = onFailedOrderCalloutClicked,
refreshBillItemAndStatus = refreshBillItemAndStatus,
)
}
}
@@ -276,9 +288,10 @@ fun RecentBillsSection(
updateAnimatedRecentBills: () -> Unit,
getContactNameFromPhoneNumber: (String) -> String,
onRecentBillItemClicked: (PhoneContactEntity, MyBillEntity) -> Unit,
onDeleteRecentBillClicked: (MyBillEntity) -> Unit,
onBillOptionsKebabMenuClicked: (MyBillEntity) -> Unit,
multipleOffersDataList: Map<String, List<OfferData>>?,
onFailedOrderCalloutClicked: (Pair<OrderEntity, MyBillEntity>) -> Unit,
refreshBillItemAndStatus: RefreshBillState,
) {
animatedRecentBills.forEachIndexed { index, billItem ->
var failedOrder: OrderEntity? by remember { mutableStateOf(null) }
@@ -315,7 +328,7 @@ fun RecentBillsSection(
onRecentBillItemClicked = {
onRecentBillItemClicked(phoneContactEntity, billItem)
},
onDeleteRecentBillClicked = { onDeleteRecentBillClicked(billItem) },
onBillOptionsKebabMenuClicked = { onBillOptionsKebabMenuClicked(billItem) },
getContactNameFromPhoneNumber = getContactNameFromPhoneNumber,
offerData =
getOfferDataForMultipleBillItems(
@@ -325,6 +338,7 @@ fun RecentBillsSection(
primaryCustomerParamValue = billItem.primaryCustomerParamValue,
failedOrder = failedOrder,
onFailedOrderCalloutClicked = onFailedOrderCalloutClicked,
refreshBillItemAndStatus = refreshBillItemAndStatus,
)
if (index < recentBills.size - 1) {
BbpsListDivider(
@@ -457,11 +471,12 @@ fun RecentBillItem(
myBillEntity: MyBillEntity,
onRecentBillItemClicked: () -> Unit,
primaryCustomerParamValue: String,
onDeleteRecentBillClicked: (MyBillEntity) -> Unit,
onBillOptionsKebabMenuClicked: (MyBillEntity) -> Unit,
getContactNameFromPhoneNumber: (String) -> String,
offerData: OfferData?,
failedOrder: OrderEntity? = null,
onFailedOrderCalloutClicked: (Pair<OrderEntity, MyBillEntity>) -> Unit,
refreshBillItemAndStatus: RefreshBillState,
) {
Box(modifier = modifier) {
Column(
@@ -500,87 +515,11 @@ fun RecentBillItem(
)
Spacer(modifier = Modifier.height(2.dp))
if (myBillEntity.isBillPaid) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (myBillEntity.formattedLastPaidAmount.isNotBlank()) {
NaviText(
text =
stringResource(
id = R.string.bbps_rupee_symbol_x,
myBillEntity.formattedLastPaidAmount,
),
color = NaviBbpsColor.textPrimary,
fontFamily = naviFontFamily,
fontSize = 14.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
overflow = TextOverflow.Ellipsis,
)
}
if (myBillEntity.formattedLastPaidDate.isNotBlank()) {
Spacer(modifier = Modifier.width(4.dp))
NaviText(
text = BULLET,
color = NaviBbpsColor.textPrimary,
fontFamily = naviFontFamily,
fontSize = 16.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
)
Spacer(modifier = Modifier.width(4.dp))
NaviText(
text =
"${
stringResource(
id = R.string.bbps_last_recharged,
myBillEntity.formattedLastPaidDate,
)
} ",
color = NaviBbpsColor.textTertiary,
fontFamily = naviFontFamily,
fontSize = 12.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
if (myBillEntity.unpaidBillDetails?.amount?.isNotEmpty() == true) {
NaviText(
text =
stringResource(
id = R.string.bbps_rupee_symbol_x,
myBillEntity.unpaidBillDetails.amount
.getDisplayableAmount(),
),
color = NaviBbpsColor.textPrimary,
fontFamily = naviFontFamily,
fontSize = 14.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.width(4.dp))
NaviText(
text = BULLET,
color = NaviBbpsColor.textPrimary,
fontFamily = naviFontFamily,
fontSize = 16.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
)
Spacer(modifier = Modifier.width(4.dp))
}
NaviText(
text = myBillEntity.unpaidBillWarning,
color = NaviBbpsColor.onSurfaceCritical,
fontFamily = naviFontFamily,
fontSize = 12.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
BillAmountAndDueDateSectionWithShimmer(
myBillEntity = myBillEntity,
refreshBillItemAndStatus = refreshBillItemAndStatus,
startWithPadding = false,
)
}
Spacer(modifier = Modifier.width(16.dp))
@@ -620,7 +559,7 @@ fun RecentBillItem(
Modifier.padding(top = 6.dp)
.size(24.dp)
.align(Alignment.Top)
.clickableDebounce { onDeleteRecentBillClicked(myBillEntity) },
.clickableDebounce { onBillOptionsKebabMenuClicked(myBillEntity) },
)
}
if (!myBillEntity.isBillPaid) {

View File

@@ -15,16 +15,13 @@ import com.navi.base.utils.EMPTY
import com.navi.base.utils.NaviNetworkConnectivity
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.orZero
import com.navi.base.utils.retry
import com.navi.bbps.R
import com.navi.bbps.common.APP_VERSION_CODE
import com.navi.bbps.common.BILLER_UNIQUE_ID
import com.navi.bbps.common.CATEGORY_ID_MOBILE_PREPAID
import com.navi.bbps.common.DEFAULT_RETRY_COUNT
import com.navi.bbps.common.NaviBbpsAnalytics
import com.navi.bbps.common.NaviBbpsScreen
import com.navi.bbps.common.RCBP_CATEGORY
import com.navi.bbps.common.RETRY_INTERVAL_IN_SECONDS
import com.navi.bbps.common.TXN_AMOUNT
import com.navi.bbps.common.model.NaviBbpsVmData
import com.navi.bbps.common.model.config.NaviBbpsDefaultConfig
@@ -32,9 +29,9 @@ import com.navi.bbps.common.model.view.DetectedBillEntity
import com.navi.bbps.common.model.view.DetectedBillSource
import com.navi.bbps.common.model.view.NaviPermissionResult
import com.navi.bbps.common.model.view.RefreshBillState
import com.navi.bbps.common.repository.BbpsCommonRepository
import com.navi.bbps.common.session.NaviBbpsSessionHelper
import com.navi.bbps.common.usecase.FindLastOrderWithSuccessfulPaymentUseCase
import com.navi.bbps.common.usecase.MyBillActionsHandler
import com.navi.bbps.common.usecase.OriginExperimentUtils
import com.navi.bbps.common.usecase.UploadUserDataUseCase
import com.navi.bbps.common.utils.BbpsOriginSessionHandler
@@ -43,7 +40,6 @@ import com.navi.bbps.common.utils.MyBillEntityToBillCategoryEntityMapper
import com.navi.bbps.common.utils.MyBillEntityToBillDetailsResponseMapper
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.NaviBbpsCommonUtils.getNormalisedPhoneNumber
import com.navi.bbps.common.utils.OriginSessionAttributes
import com.navi.bbps.common.utils.OriginWidgetStatus
@@ -54,9 +50,7 @@ import com.navi.bbps.feature.billerlist.model.view.BillerItemEntity
import com.navi.bbps.feature.category.model.view.BillCategoryEntity
import com.navi.bbps.feature.contactlist.PhoneContactManager
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.BillDetailsResponse
import com.navi.bbps.feature.customerinput.model.network.DeviceDetails
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
@@ -75,7 +69,6 @@ import com.navi.bbps.feature.prepaidrecharge.model.view.PlanItemEntity
import com.navi.bbps.isPrepaidRechargePaidOrWithoutPlanDetails
import com.navi.bbps.isRedirectToCustomerInputRequired
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.network.models.isSuccessWithData
import com.navi.naviwidgets.utils.ZERO
import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.rr.common.models.OfferRequest
@@ -113,7 +106,6 @@ constructor(
private val naviNetworkConnectivity: NaviNetworkConnectivity,
private val phoneContactManager: Lazy<PhoneContactManager>,
private val myBillsSyncJob: MyBillsSyncJob,
private val bbpsCommonRepository: BbpsCommonRepository,
private val resourceProvider: ResourceProvider,
val originBillDetectionHandler: OriginBillDetectionHandler,
private val originSessionHandler: BbpsOriginSessionHandler,
@@ -122,6 +114,7 @@ constructor(
private val findLastOrderWithSuccessfulPaymentUseCase:
FindLastOrderWithSuccessfulPaymentUseCase,
private val originExperimentUtils: OriginExperimentUtils,
private val myBillActionsHandler: MyBillActionsHandler,
) :
NaviBbpsBaseVM(
naviBbpsVmData = NaviBbpsVmData(screen = NaviBbpsScreen.NAVI_BBPS_MY_SAVED_BILLS)
@@ -417,32 +410,13 @@ constructor(
notifyError(getNoInternetErrorConfig())
return@launch
}
val response =
retry(
retryCount = DEFAULT_RETRY_COUNT,
retryIntervalInSeconds = RETRY_INTERVAL_IN_SECONDS,
execute = {
myBillsRepository.markBillAsPaid(
myBillEntity.billId,
metricInfo =
getBbpsMetricInfo(
screenName = naviBbpsVmData.screen.screenName,
isNae = { false },
),
)
},
shouldRetry = { !it.isSuccessWithData() },
)
if (response.isSuccessWithData()) {
val updatedBillEntity = myBillEntity.copy(isBillPaid = true)
updateSavedBills(
updatedBillEntity = updatedBillEntity,
oldBillEntity = myBillEntity,
)
updateSnackBarState(show = true, messageId = R.string.bbps_bill_marked_as_paid)
}
myBillActionsHandler.markAsPaid(
billId = myBillEntity.billId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(show = true, messageId = R.string.bbps_bill_marked_as_paid)
},
)
}
}
@@ -455,34 +429,27 @@ constructor(
private suspend fun refreshBillAndUpdateInfo(myBillEntity: MyBillEntity) {
updateRefreshState(isRefreshing = true, myBillEntity = myBillEntity)
val billDetailsRequest =
BillDetailsRequest(
billerId = myBillEntity.billerId,
customerParams = myBillEntity.customerParams,
deviceDetails = DeviceDetails(ip = naviNetworkConnectivity.getIpAddress()),
amount = myBillEntity.actualLastPaidAmount,
isConsentProvided = true,
)
val billDetailsResponse =
bbpsCommonRepository.fetchBillDetails(
billDetailsRequest = billDetailsRequest,
metricInfo = getBbpsMetricInfo(screenName = naviBbpsVmData.screen.screenName),
)
if (billDetailsResponse.isSuccessWithData() && billDetailsResponse.data != null) {
myBillEntity.unpaidBillDetails?.let {
myBillsSyncJob.refreshBills(naviBbpsVmData.screen.screenName)
myBillActionsHandler.refreshBill(
myBillEntity = myBillEntity,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_sucessfully,
)
}
} else {
updateSnackBarState(show = true, messageId = R.string.bbps_bill_refreshed_failed)
}
},
onError = { response ->
val errorCode = getError(response).code
if (errorCode in naviBbpsDefaultConfig.billAlreadyPaidErrorCodes) {
notifyError(response = response)
} else {
updateSnackBarState(
show = true,
messageId = R.string.bbps_bill_refreshed_failed,
)
}
},
)
updateRefreshState(isRefreshing = false, myBillEntity = myBillEntity)
}
@@ -765,20 +732,37 @@ constructor(
fun deleteBill(myBillEntity: MyBillEntity) {
viewModelScope.launch(dispatcherProvider.io) {
val deleteBillResponse =
bbpsCommonRepository.deleteBill(
myBillEntity.billId,
metricInfo = getBbpsMetricInfo(screenName = naviBbpsVmData.screen.screenName),
)
if (deleteBillResponse.isSuccessWithData()) {
myBillsSyncJob.refreshBills(naviBbpsVmData.screen.screenName)
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_removed_successfully,
)
} else {
updateSnackBarState(show = true, messageId = R.string.bbps_account_delete_failed)
}
myBillActionsHandler.deleteBill(
billId = myBillEntity.billId,
categoryId = myBillEntity.categoryId,
screenName = naviBbpsVmData.screen.screenName,
onSuccess = { deleteBillResponse ->
naviBbpsAnalytics.billDeletionSuccess(
data = deleteBillResponse.data!!,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_removed_successfully,
)
},
onError = { deleteBillResponse ->
naviBbpsAnalytics.billDeletionFailed(
deleteBillResponse = deleteBillResponse,
myBillEntity = myBillEntity,
sessionAttribute = getNaviBbpsSessionAttributes(),
source = source,
initialSource = initialSource,
)
updateSnackBarState(
show = true,
messageId = R.string.bbps_account_delete_failed,
)
},
)
}
}

View File

@@ -7,57 +7,21 @@
package com.navi.bbps.feature.mybills.ui
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.HorizontalDivider
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.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.navi.bbps.R
import com.navi.bbps.common.BULLET
import com.navi.bbps.common.ICON_MARK_AS_PAID_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.ICON_REFRESH_BILL_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.ICON_REMOVE_ACCOUNT_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.ICON_VIEW_BILL_HISTORY_UNPAID_BILL_BOTTOMSHEET
import com.navi.bbps.common.NaviBbpsAnalytics
import com.navi.bbps.common.SYMBOL_RUPEE
import com.navi.bbps.common.theme.NaviBbpsColor
import com.navi.bbps.common.ui.BbpsAsyncImage
import com.navi.bbps.common.ui.BbpsOfferBottomSheet
import com.navi.bbps.common.ui.BillOptionsBottomSheetContent
import com.navi.bbps.common.ui.ConfirmationBottomSheetContent
import com.navi.bbps.common.ui.TitleDescriptionWithLinearProgressBar
import com.navi.bbps.common.utils.NaviBbpsCommonUtils.isRechargeCategory
import com.navi.bbps.common.utils.getDisplayableAmount
import com.navi.bbps.feature.category.ui.DetectedBillsBottomSheetContent
import com.navi.bbps.feature.category.ui.NoBillDetectedErrorBottomSheetContent
import com.navi.bbps.feature.mybills.model.view.MyBillsBottomSheetType
import com.navi.bbps.getBillTitleFromAccountHolderNameOrPrimaryCustomerParams
import com.navi.bbps.noRippleClickableWithDebounce
import com.navi.common.R as CommonR
import com.navi.common.utils.SPACE
import com.navi.common.utils.firstLetterToLowerCase
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
import com.navi.guarddog.utils.clickableDebounce
import com.navi.naviwidgets.extensions.NaviText
@Composable
fun MyBillsBottomSheetContent(
@@ -69,12 +33,15 @@ fun MyBillsBottomSheetContent(
) {
when (myBillsBottomSheetType) {
is MyBillsBottomSheetType.UnpaidBillOptions -> {
UnpaidBillOptionsBottomSheetContent(
BillOptionsBottomSheetContent(
billerLogoUrl = myBillsBottomSheetType.myBillEntity.billerLogoUrl,
title =
getBillTitleFromAccountHolderNameOrPrimaryCustomerParams(
myBillsBottomSheetType.myBillEntity.unpaidBillDetails?.accountHolderName,
myBillsBottomSheetType.myBillEntity.primaryCustomerParamValue,
accountHolderName =
myBillsBottomSheetType.myBillEntity.unpaidBillDetails
?.accountHolderName,
primaryCustomerParams =
myBillsBottomSheetType.myBillEntity.primaryCustomerParamValue,
),
expiringOn =
(myBillsBottomSheetType.myBillEntity.unpaidBillWarning)
@@ -84,7 +51,9 @@ fun MyBillsBottomSheetContent(
?.amount
?.getDisplayableAmount()
.orEmpty(),
operatorName = myBillsBottomSheetType.myBillEntity.billerName,
billerName = myBillsBottomSheetType.myBillEntity.billerName,
isRechargeCategory =
isRechargeCategory(categoryId = myBillsBottomSheetType.myBillEntity.categoryId),
onMarkAsPaidClicked = {
closeSheet()
naviBbpsAnalytics.onMyBillMoreOptionsBottomSheetClicked(
@@ -105,7 +74,14 @@ fun MyBillsBottomSheetContent(
closeSheet()
naviBbpsAnalytics.onMyBillMoreOptionsBottomSheetClicked(
billEntity = myBillsBottomSheetType.myBillEntity,
action = "view_bill_history",
action =
if (
isRechargeCategory(
categoryId = myBillsBottomSheetType.myBillEntity.categoryId
)
)
"view_recharge_history"
else "view_bill_history",
)
myBillsBottomSheetType.onViewBillHistoryClicked()
},
@@ -254,170 +230,3 @@ fun RemoveAccountBottomSheetContent(
onPrimaryButtonClicked = { onPrimaryButtonClicked.invoke() },
)
}
@Composable
fun UnpaidBillOptionsBottomSheetContent(
billerLogoUrl: String,
title: String,
amount: String,
operatorName: String,
expiringOn: String,
onMarkAsPaidClicked: () -> Unit,
onRefreshBillClicked: () -> Unit,
onViewBillHistoryClicked: () -> Unit,
onRemoveAccountClicked: () -> Unit,
onCloseClicked: () -> Unit,
) {
val descriptionText = buildAnnotatedString {
val fullText = stringResource(id = R.string.bbps_payment_due, operatorName, expiringOn)
withStyle(
style =
SpanStyle(
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
fontSize = 14.sp,
color = NaviBbpsColor.textTertiary,
)
) {
append(fullText)
}
addStyle(
style =
SpanStyle(
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
fontSize = 14.sp,
color = NaviBbpsColor.inputFieldError,
),
start = fullText.indexOf(expiringOn),
end = fullText.indexOf(expiringOn) + expiringOn.length,
)
}
Column(
modifier =
Modifier.navigationBarsPadding()
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp)
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
BbpsAsyncImage(
imageUrl = billerLogoUrl,
modifier = Modifier.size(24.dp),
placeholderIconResId = com.navi.common.R.drawable.navi_common_ic_biller_placeholder,
)
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(id = com.navi.common.R.drawable.ic_close_black),
contentDescription = null,
modifier = Modifier.size(24.dp).clickableDebounce { onCloseClicked() },
)
}
Spacer(modifier = Modifier.height(8.dp))
NaviText(
text =
buildString {
if (title.isNotBlank()) {
append(title)
append(SPACE)
}
if (title.isNotBlank() && amount.isNotBlank()) {
append(BULLET)
append(SPACE)
}
if (amount.isNotBlank()) {
append(SYMBOL_RUPEE)
append(amount)
}
},
fontSize = 16.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
color = NaviBbpsColor.textPrimary,
)
Spacer(modifier = Modifier.height(2.dp))
if (descriptionText.isNotBlank()) {
NaviText(
text = descriptionText,
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
color = NaviBbpsColor.textTertiary,
lineHeight = 22.sp,
)
}
Spacer(modifier = Modifier.height(24.dp))
val actions =
listOf(
ActionItem(
imageS3Url = ICON_MARK_AS_PAID_UNPAID_BILL_BOTTOMSHEET,
textResId = R.string.bbps_mark_as_paid,
onClick = onMarkAsPaidClicked,
),
ActionItem(
imageS3Url = ICON_REFRESH_BILL_UNPAID_BILL_BOTTOMSHEET,
textResId = R.string.bbps_refresh_bill,
onClick = onRefreshBillClicked,
),
ActionItem(
imageS3Url = ICON_VIEW_BILL_HISTORY_UNPAID_BILL_BOTTOMSHEET,
textResId = R.string.bbps_view_bill_history,
onClick = onViewBillHistoryClicked,
),
ActionItem(
imageS3Url = ICON_REMOVE_ACCOUNT_UNPAID_BILL_BOTTOMSHEET,
textResId = R.string.bbps_remove_account,
onClick = onRemoveAccountClicked,
),
)
actions.forEachIndexed { index, action ->
IconActionRow(
imageS3Url = action.imageS3Url,
text = stringResource(id = action.textResId),
onClick = action.onClick,
)
if (index < actions.lastIndex) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 20.dp),
thickness = 1.dp,
color = NaviBbpsColor.borderAlt,
)
}
}
}
}
data class ActionItem(
val imageS3Url: String,
@StringRes val textResId: Int,
val onClick: () -> Unit,
)
@Composable
fun IconActionRow(
imageS3Url: String,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth().noRippleClickableWithDebounce(onClick = onClick),
verticalAlignment = Alignment.CenterVertically,
) {
AsyncImage(modifier = Modifier.size(24.dp), model = imageS3Url, contentDescription = "")
Spacer(modifier = Modifier.width(12.dp))
NaviText(
text = text,
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
color = NaviBbpsColor.textPrimary,
)
}
}

View File

@@ -270,6 +270,7 @@
<string name="bbps_payment_due">%s payment %s.</string>
<string name="bbps_due_on">due on %s</string>
<string name="bbps_refresh_bill">Refresh bill</string>
<string name="bbps_view_recharge_history">View recharge history</string>
<string name="bbps_view_bill_history">View bill history</string>
<string name="bbps_offer_bottomsheet_count_title_singular">%s Bill payment offer</string>
<string name="bbps_offer_bottomsheet_count_title_plural">%s Bill payment offers</string>