NTP-56713 | Upi Lite Banner Rotation (#16013)

This commit is contained in:
Balrambhai Sharma
2025-05-05 18:42:03 +05:30
committed by GitHub
parent e4e759a1bf
commit dd13c08502
5 changed files with 258 additions and 59 deletions

View File

@@ -0,0 +1,64 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.common.usecase
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.pay.management.lite.models.view.UpiLiteBannerDisplayType
import javax.inject.Inject
class UpiLiteBannerRotationUseCase
@Inject
constructor(private val naviCacheRepository: NaviCacheRepository) {
companion object {
private const val UPI_LITE_BANNER_POSITION_KEY = "upiLiteBannerPosition"
private const val UPI_LITE_BANNERS_COUNT = 3
private const val BANNER_ROTATION_INTERVAL = 2
private const val TOTAL_VISIT_COUNT_PER_ROTATION =
UPI_LITE_BANNERS_COUNT * BANNER_ROTATION_INTERVAL
}
/**
* Returns the current UPI Lite banner display type based on the rotation position and advances
* the position counter for the next call.
*/
suspend fun execute(): UpiLiteBannerDisplayType {
val currentPosition = getCurrentPosition()
val bannerType = getBannerTypeForPosition(position = currentPosition)
moveToNextPosition(currentPosition = currentPosition)
return bannerType
}
private fun getBannerTypeForPosition(position: Int): UpiLiteBannerDisplayType {
return when (position / BANNER_ROTATION_INTERVAL) {
0 -> UpiLiteBannerDisplayType.FAST_TRANSACTION
1 -> UpiLiteBannerDisplayType.SIMPLIFY_BANK_STATEMENTS
else -> UpiLiteBannerDisplayType.BANK_SERVER_DOWN
}
}
private suspend fun getCurrentPosition(): Int {
return naviCacheRepository
.get(UPI_LITE_BANNER_POSITION_KEY, 0)
?.value
?.toIntOrNull()
?.mod(TOTAL_VISIT_COUNT_PER_ROTATION) ?: 0
}
private suspend fun moveToNextPosition(currentPosition: Int) {
val nextPosition = (currentPosition + 1) % TOTAL_VISIT_COUNT_PER_ROTATION
naviCacheRepository.save(
NaviCacheEntity(
key = UPI_LITE_BANNER_POSITION_KEY,
value = nextPosition.toString(),
version = 0,
)
)
}
}

View File

@@ -7,7 +7,7 @@
package com.navi.pay.management.lite.models.view
enum class UpiLiteUspExperimentVariant {
enum class UpiLiteBannerDisplayType {
DEFAULT,
FAST_TRANSACTION,
BANK_SERVER_DOWN,

View File

@@ -108,7 +108,7 @@ import com.navi.pay.common.utils.SnackBarPredefinedConfig
import com.navi.pay.management.common.sendmoney.model.view.BankAccountsState
import com.navi.pay.management.lite.models.NaviPayUpiLiteConfig
import com.navi.pay.management.lite.models.view.AmountChipEntity
import com.navi.pay.management.lite.models.view.UpiLiteUspExperimentVariant
import com.navi.pay.management.lite.models.view.UpiLiteBannerDisplayType
import com.navi.pay.management.lite.util.AddBalanceButtonSource
import com.navi.pay.management.lite.util.SetPinButtonSource
import com.navi.pay.management.lite.util.UpiLiteMainCtaState
@@ -198,10 +198,9 @@ fun UpiLiteSection(
upiLiteViewModel.liteMandateTopUpAmount.collectAsStateWithLifecycle()
val maximumTopUpAmountAllowed by
upiLiteViewModel.maximumTopUpAmountAllowed.collectAsStateWithLifecycle()
val naviPayUpiLiteConfig by upiLiteViewModel.naviPayUpiLiteConfig.collectAsStateWithLifecycle()
val isMainCtaEnabled by upiLiteViewModel.isMainCtaEnabled.collectAsStateWithLifecycle()
val upiLiteUspExperimentVariant by
upiLiteViewModel.upiLiteUspExperimentVariant.collectAsStateWithLifecycle()
val upiLiteBannerDisplayType by
upiLiteViewModel.upiLiteBannerDisplayType.collectAsStateWithLifecycle()
val scrollState = rememberScrollState()
val linkedAccountWithActiveLiteAccount by
@@ -243,8 +242,7 @@ fun UpiLiteSection(
onBackClick = onBackClick,
onMenuOptionClicked = onMenuOptionClicked,
isLiteInSyncedState = isLiteInSyncedState,
naviPayUpiLiteConfig = naviPayUpiLiteConfig,
upiLiteUspExperimentVariant = upiLiteUspExperimentVariant,
upiLiteBannerDisplayType = upiLiteBannerDisplayType,
isRefreshBalanceLottieVisible = isRefreshBalanceLottieVisible,
onRefreshBalanceButtonClicked =
upiLiteViewModel::onRefreshBalanceButtonClicked,
@@ -667,27 +665,17 @@ fun UpiLiteBannerSection(
onBackClick: () -> Unit,
onMenuOptionClicked: () -> Unit,
isLiteInSyncedState: Boolean,
naviPayUpiLiteConfig: NaviPayUpiLiteConfig,
upiLiteUspExperimentVariant: UpiLiteUspExperimentVariant,
upiLiteBannerDisplayType: UpiLiteBannerDisplayType,
isRefreshBalanceLottieVisible: Boolean,
onRefreshBalanceButtonClicked: () -> Unit,
) {
if (!isUserOnboarded) {
if (upiLiteUspExperimentVariant == UpiLiteUspExperimentVariant.DEFAULT) {
UpiLiteNonOnboardedMultipleUspBanner(
onHelpCtaClicked = onHelpCtaClicked,
onBackClick = onBackClick,
onMenuOptionClicked = onMenuOptionClicked,
naviPayUpiLiteConfig = naviPayUpiLiteConfig,
)
} else {
UpiLiteNonOnboardedSingleUspBanner(
onHelpCtaClicked = onHelpCtaClicked,
onBackClick = onBackClick,
onMenuOptionClicked = onMenuOptionClicked,
upiLiteExperimentVariant = upiLiteUspExperimentVariant,
)
}
UpiLiteNonOnboardedSingleUspBanner(
onHelpCtaClicked = onHelpCtaClicked,
onBackClick = onBackClick,
onMenuOptionClicked = onMenuOptionClicked,
upiLiteBannerDisplayType = upiLiteBannerDisplayType,
)
} else if (isLiteBalanceLow) {
UpiLiteLowBalanceBanner(
onHelpCtaClicked = onHelpCtaClicked,
@@ -980,7 +968,7 @@ fun UpiLiteNonOnboardedSingleUspBanner(
onHelpCtaClicked: () -> Unit,
onBackClick: () -> Unit,
onMenuOptionClicked: () -> Unit,
upiLiteExperimentVariant: UpiLiteUspExperimentVariant,
upiLiteBannerDisplayType: UpiLiteBannerDisplayType,
) {
Column(
modifier =
@@ -1000,10 +988,10 @@ fun UpiLiteNonOnboardedSingleUspBanner(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column {
when (upiLiteExperimentVariant) {
UpiLiteUspExperimentVariant.FAST_TRANSACTION -> FastPaymentsUspText()
UpiLiteUspExperimentVariant.BANK_SERVER_DOWN -> BankServerDownText()
UpiLiteUspExperimentVariant.SIMPLIFY_BANK_STATEMENTS -> BankStatementFullText()
when (upiLiteBannerDisplayType) {
UpiLiteBannerDisplayType.FAST_TRANSACTION -> FastPaymentsUspText()
UpiLiteBannerDisplayType.BANK_SERVER_DOWN -> BankServerDownText()
UpiLiteBannerDisplayType.SIMPLIFY_BANK_STATEMENTS -> BankStatementFullText()
else -> {
FastPaymentsUspText()
}

View File

@@ -25,7 +25,6 @@ import com.navi.common.R as CommonR
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.common.utils.CommonUtils.getDisplayableAmount
import com.navi.common.utils.CommonUtils.isAmountValid
import com.navi.common.utils.Constants
@@ -60,6 +59,7 @@ import com.navi.pay.common.usecase.LiteAccountSyncUseCase
import com.navi.pay.common.usecase.LocationUseCase
import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.UpiLiteBalanceUseCase
import com.navi.pay.common.usecase.UpiLiteBannerRotationUseCase
import com.navi.pay.common.usecase.UpiLiteExperimentationUseCase
import com.navi.pay.common.usecase.UpiRequestIdUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
@@ -99,9 +99,9 @@ import com.navi.pay.management.lite.models.view.InitialTopUpStatus
import com.navi.pay.management.lite.models.view.InitialTopUpStatus.Companion.isPendingState
import com.navi.pay.management.lite.models.view.UPILiteAccountStatus
import com.navi.pay.management.lite.models.view.UPILiteEntity
import com.navi.pay.management.lite.models.view.UpiLiteBannerDisplayType
import com.navi.pay.management.lite.models.view.UpiLiteBottomSheetStateHolder
import com.navi.pay.management.lite.models.view.UpiLiteDefaultEnteredAmountExperimentData
import com.navi.pay.management.lite.models.view.UpiLiteUspExperimentVariant
import com.navi.pay.management.lite.repository.UPILiteRepository
import com.navi.pay.management.lite.util.AddBalanceButtonSource
import com.navi.pay.management.lite.util.DisableUpiLiteStatus
@@ -155,7 +155,6 @@ import com.navi.pay.utils.DEFAULT_UPI_MODE
import com.navi.pay.utils.KEY_UPI_LITE_ACTIVE_ACCOUNT_INFO
import com.navi.pay.utils.KEY_UPI_LITE_MANDATE_INFO
import com.navi.pay.utils.LITE_MANDATE
import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_UPI_LITE_USP
import com.navi.pay.utils.MONEY_ADDED_TO_UPI_LITE
import com.navi.pay.utils.NAVI_PAY_DEFAULT_MCC
import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE
@@ -231,7 +230,7 @@ constructor(
private val paymentStatusRepository: PaymentStatusRepository,
private val upiLiteExperimentationUseCase: UpiLiteExperimentationUseCase,
val accountListCheckBalanceUseCase: AccountListCheckBalanceUseCase,
private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
private val upiLiteBannerRotationUseCase: UpiLiteBannerRotationUseCase,
private val naviPayPspManager: NaviPayPspManager,
@NaviPayGsonBuilder private val gson: Gson,
private val arcNudgeUseCase: ArcNudgeUseCase,
@@ -413,8 +412,8 @@ constructor(
private val noOfAutoTopUpSupportedBanks = MutableStateFlow(0)
val isAutoTopUpSetUp = MutableStateFlow(false)
private val _upiLiteUspExperimentVariant = MutableStateFlow(UpiLiteUspExperimentVariant.DEFAULT)
val upiLiteUspExperimentVariant = _upiLiteUspExperimentVariant.asStateFlow()
private val _upiLiteBannerDisplayType = MutableStateFlow(UpiLiteBannerDisplayType.DEFAULT)
val upiLiteBannerDisplayType = _upiLiteBannerDisplayType.asStateFlow()
private var previousLinkedAccountsSize = emptyList<String>()
@@ -756,18 +755,17 @@ constructor(
)
val statusBarColor =
combine(uiState, isUpiLiteBalanceLow, upiLiteUspExperimentVariant, isUserOnboarded) {
combine(uiState, isUpiLiteBalanceLow, upiLiteBannerDisplayType, isUserOnboarded) {
uiState,
isUpiLiteBalanceLow,
upiLiteExperimentVariant,
upiLiteBannerDisplayType,
isUserOnboarded ->
if (uiState != UpiLiteUiState.MAIN_SCREEN) {
R.color.navi_pay_status_bar_default_color
} else if (isUpiLiteBalanceLow) {
R.color.navi_pay_bg_alt
} else if (
!isUserOnboarded &&
upiLiteExperimentVariant != UpiLiteUspExperimentVariant.DEFAULT
!isUserOnboarded && upiLiteBannerDisplayType != UpiLiteBannerDisplayType.DEFAULT
) {
R.color.navi_pay_status_bar_lottie_green
} else {
@@ -971,7 +969,7 @@ constructor(
init {
fetchNaviPayUpiLiteConfig()
setLitmusExperimentValues()
setUpiLiteUspExperimentValues()
setUpUpiLiteBannerDisplayType()
getLiteMandateInfo()
updateNaviPaySessionAttributes()
getLiteInfoIfActiveAccountIsPresent()
@@ -1169,26 +1167,9 @@ constructor(
}
}
private fun setUpiLiteUspExperimentValues() {
private fun setUpUpiLiteBannerDisplayType() {
viewModelScope.launch(coroutineDispatcherProvider.io) {
val upiLiteUspExperimentVariantInfo =
litmusExperimentsUseCase
.execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_UPI_LITE_USP)
?.variant
val upiLiteUspExperimentVariantType =
if (upiLiteUspExperimentVariantInfo?.enabled == false) {
UpiLiteUspExperimentVariant.DEFAULT
} else {
when (upiLiteUspExperimentVariantInfo?.name) {
"v1_fast_txns" -> UpiLiteUspExperimentVariant.FAST_TRANSACTION
"v2_bank_downtime" -> UpiLiteUspExperimentVariant.BANK_SERVER_DOWN
"v3_clean_statement" -> UpiLiteUspExperimentVariant.SIMPLIFY_BANK_STATEMENTS
else -> UpiLiteUspExperimentVariant.DEFAULT
}
}
_upiLiteUspExperimentVariant.update { upiLiteUspExperimentVariantType }
_upiLiteBannerDisplayType.update { upiLiteBannerRotationUseCase.execute() }
}
}

View File

@@ -0,0 +1,166 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.common
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.pay.common.usecase.UpiLiteBannerRotationUseCase
import com.navi.pay.management.lite.models.view.UpiLiteBannerDisplayType
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class UpiLiteBannerRotationUseCaseTest {
private lateinit var naviCacheRepository: NaviCacheRepository
private lateinit var useCase: UpiLiteBannerRotationUseCase
@Before
fun setUp() {
naviCacheRepository = mockk(relaxed = true)
useCase = UpiLiteBannerRotationUseCase(naviCacheRepository)
}
@Test
fun `execute returns FAST_TRANSACTION for positions 0 and 1`() = runBlocking {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "0", version = 0)
val result = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.FAST_TRANSACTION, result)
coVerify { naviCacheRepository.save(match { it.value == "1" }) }
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "1", version = 0)
val result2 = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.FAST_TRANSACTION, result2)
coVerify { naviCacheRepository.save(match { it.value == "2" }) }
}
@Test
fun `execute returns SIMPLIFY_BANK_STATEMENTS for positions 2 and 3`() = runBlocking {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "2", version = 0)
val result = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.SIMPLIFY_BANK_STATEMENTS, result)
coVerify { naviCacheRepository.save(match { it.value == "3" }) }
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "3", version = 0)
val result2 = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.SIMPLIFY_BANK_STATEMENTS, result2)
coVerify { naviCacheRepository.save(match { it.value == "4" }) }
}
@Test
fun `execute returns BANK_SERVER_DOWN for positions 4 and 5`() = runBlocking {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "4", version = 0)
val result = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.BANK_SERVER_DOWN, result)
coVerify { naviCacheRepository.save(match { it.value == "5" }) }
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "5", version = 0)
val result2 = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.BANK_SERVER_DOWN, result2)
coVerify { naviCacheRepository.save(match { it.value == "0" }) }
}
@Test
fun `execute cycles correctly through all positions`() = runBlocking {
val expectedBannerTypes =
listOf(
UpiLiteBannerDisplayType.FAST_TRANSACTION,
UpiLiteBannerDisplayType.FAST_TRANSACTION,
UpiLiteBannerDisplayType.SIMPLIFY_BANK_STATEMENTS,
UpiLiteBannerDisplayType.SIMPLIFY_BANK_STATEMENTS,
UpiLiteBannerDisplayType.BANK_SERVER_DOWN,
UpiLiteBannerDisplayType.BANK_SERVER_DOWN,
)
for (i in 0..5) {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = i.toString(), version = 0)
val result = useCase.execute()
assertEquals(expectedBannerTypes[i], result)
val expectedNextPosition = if (i == 5) 0 else i + 1
coVerify {
naviCacheRepository.save(match { it.value == expectedNextPosition.toString() })
}
}
}
@Test
fun `execute handles null repository result`() = runBlocking {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns null
val result = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.FAST_TRANSACTION, result)
coVerify { naviCacheRepository.save(match { it.value == "1" }) }
}
@Test
fun `execute handles non-numeric repository value`() = runBlocking {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "not_a_number", version = 0)
val result = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.FAST_TRANSACTION, result)
coVerify { naviCacheRepository.save(match { it.value == "1" }) }
}
@Test
fun `execute handles position values larger than cycle length`() = runBlocking {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "12", version = 0)
val result = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.FAST_TRANSACTION, result)
coVerify { naviCacheRepository.save(match { it.value == "1" }) }
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "16", version = 0)
val result2 = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.BANK_SERVER_DOWN, result2)
coVerify { naviCacheRepository.save(match { it.value == "5" }) }
}
@Test
fun `execute handles negative position values`() = runBlocking {
coEvery { naviCacheRepository.get(key = "upiLiteBannerPosition", version = 0) } returns
NaviCacheEntity(key = "upiLiteBannerPosition", value = "-1", version = 0)
val result = useCase.execute()
assertEquals(UpiLiteBannerDisplayType.BANK_SERVER_DOWN, result)
coVerify { naviCacheRepository.save(match { it.value == "0" }) }
}
}