diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt index 7cda12df6e..3257f94c95 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt @@ -40,7 +40,6 @@ import com.navi.common.model.UploadDataAsyncResponse import com.navi.common.network.models.SuccessResponse import com.navi.common.utils.SingleLiveEvent import com.navi.naviwidgets.models.response.CSATResponse -import com.navi.rr.common.network.di.V1 import com.navi.rr.common.network.di.V2 import com.navi.rr.scratchcard.helper.ScratchCardNudgeHelper import com.navi.rr.scratchcard.helper.SpecialRewardType @@ -61,7 +60,6 @@ class OrderStatusViewModel constructor( private val repository: OrderStatusRepository, private val cartUseCase: CartUseCase, - @V1 private val scratchCardNudgeHelperV1: ScratchCardNudgeHelper, @V2 private val scratchCardNudgeHelperV2: ScratchCardNudgeHelper, ) : BaseAmcVM() { private val _orderStatusScreenData = MutableLiveData() @@ -462,11 +460,16 @@ constructor( SpecialRewardType.IPL_REWARD -> { processIplRewardGratification(requestId = requestId, screenName = screenName) } + + else -> { + // no rewards in personalised offer special Reward + processRewardGratification(requestId = requestId, screenName = screenName) + } } } private suspend fun processRewardGratification(requestId: String, screenName: String) { - scratchCardNudgeHelperV1 + scratchCardNudgeHelperV2 .getGratification(requestId = requestId, screenName = screenName) .collect { processRewardGratificationCallback( diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/viewmodel/BbpsPostPaymentScreenViewModelV2.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/viewmodel/BbpsPostPaymentScreenViewModelV2.kt index 04a526749b..737f0919e3 100644 --- a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/viewmodel/BbpsPostPaymentScreenViewModelV2.kt +++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/transactiondetails/viewmodel/BbpsPostPaymentScreenViewModelV2.kt @@ -490,12 +490,16 @@ constructor( SpecialRewardType.IPL_REWARD -> { processIplRewardGratification() } + + else -> { + processRewardGratification() + } } } private fun processRewardGratification() { viewModelScope.launch(Dispatchers.IO) { - scratchCardNudgeHelperV1 + scratchCardNudgeHelperV2 .getGratification( requestId = billTransactionItemEntity.value.transactionDetails.upiRequestId, screenName = NaviBbpsScreen.NAVI_BBPS_POST_PAYMENT_SCREEN.name, diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt index 8117972395..634771b738 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt @@ -351,6 +351,9 @@ object Constants { // navi-ipl scratch card experience experiment keys const val LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY = "hpc-Navi-Powerplay" + // navi-personalised offer experience experiment keys + const val LITMUS_EXPERIMENT_NAVIPAY_PERSONALISED_OFFERS = "personalised_offers_check" + const val COMPOSABLE_ID = "COMPOSABLE_ID" // Custom UiTron Renderers constants @@ -405,5 +408,6 @@ object Constants { enum class ScratchCardExperiment(val experimentName: String) { SINGLE_SCRATCH_CARD_EXPERIENCE("single-scratch-card-experience"), DOUBLE_SCRATCH_CARD_EXPERIENCE("double-scratch-card-experience"), + PERSONALISED_OFFERS("personalised-offers-enabled"), } } diff --git a/android/navi-gold/src/main/java/com/navi/gold/viewmodels/DigitalGoldTransactionVM.kt b/android/navi-gold/src/main/java/com/navi/gold/viewmodels/DigitalGoldTransactionVM.kt index 1b1a2a917e..30794b5677 100644 --- a/android/navi-gold/src/main/java/com/navi/gold/viewmodels/DigitalGoldTransactionVM.kt +++ b/android/navi-gold/src/main/java/com/navi/gold/viewmodels/DigitalGoldTransactionVM.kt @@ -232,11 +232,15 @@ constructor( SpecialRewardType.IPL_REWARD -> { processIplRewardGratification(requestId = requestId, screenName = screenName) } + + else -> { + processRewardGratification(requestId = requestId, screenName = screenName) + } } } private suspend fun processRewardGratification(requestId: String, screenName: String) { - scratchCardNudgeHelperV1 + scratchCardNudgeHelperV2 .getGratification(requestId = requestId, screenName = screenName) .collect { gratificationStatus -> processRewardGratificationCallback(gratificationStatus, screenName) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt index 5f67582d45..149a395621 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryScreen.kt @@ -461,6 +461,10 @@ fun PaymentSummaryScreen( .getPaymentStripeImageUrl(), upiLiteInfoBannerData = upiLiteInfoBannerData, playerCardDataProvider = { playerCardData }, + personalisedOffersProvider = { + paymentSummaryViewModel.scratchCardNudgeHelperV2 + .fetchPersonalisedOfferData() + }, isArcProtected = isArcProtected, transactionProcessedCalloutType = paymentSummaryViewModel.transactionProcessedCalloutType, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt index 43f011cd87..dd59e9a67f 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/ui/PaymentSummaryTransactionDetailSection.kt @@ -68,6 +68,7 @@ import com.navi.common.adverse.fallbackTemplates.BbpsBannerFallbackAd import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_PPS_CROSS_SELL_AD_FALLBACK_TIMEOUT import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_PPS_CROSS_SELL_AD_RE_ID +import com.navi.common.forge.model.ScreenDefinition import com.navi.common.lottie.LottieRepository import com.navi.common.model.NaviLottieCompositionSpecType import com.navi.common.utils.CommonUtils.getDisplayableAmount @@ -110,9 +111,7 @@ import com.navi.pay.utils.getImageRequestBuilder import com.navi.pay.utils.noRippleClickableWithDebounce import com.navi.rr.scratchcard.model.ScratchCardBackResponse import com.navi.rr.scratchcard.model.ScratchCardModel -import com.navi.rr.scratchcard.ui.compose.ScratchCardComposable -import com.navi.rr.scratchcard.ui.compose.ScratchCardDeck -import com.navi.rr.utils.datasource.FixedScratchCardDataSource +import com.navi.rr.scratchcard.ui.compose.ScratchCardScreen import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.delay @@ -153,6 +152,7 @@ fun SharedTransitionScope.PaymentSummaryTransactionDetailSection( festiveCelebrationWithStripeImageUrl: String?, upiLiteInfoBannerData: UpiLiteInfoBannerData?, playerCardDataProvider: () -> ScratchCardModel?, + personalisedOffersProvider: () -> ScreenDefinition?, isArcProtected: Boolean, transactionProcessedCalloutType: TransactionProcessedCalloutType, currencySymbol: String, @@ -282,34 +282,19 @@ fun SharedTransitionScope.PaymentSummaryTransactionDetailSection( is PaymentSummaryRewardsGratificationUIState.Gratification ) { with(rewardsGratificationUIState.gratificationResponse) { - if ( - playerId != null && - screenDefinition != null && - scratchCardResponse != null - ) { - ScratchCardDeck( - screenDefinition = screenDefinition!!, - previousBalance = - scratchCardResponse?.naviCoinDetails?.previousBalance, - screenName = - NaviPayAnalytics.NAVI_PAY_PAYMENT_SUMMARY_SCREEN, - dataSource = - FixedScratchCardDataSource( - scratchCard = - scratchCardResponse?.toScratchCardModel()!! - ) { - playerCardDataProvider() - }, - sendBackResponse = onRewardsPopUpClosed, - onBackPress = {}, - ) - } else { - ScratchCardComposable( - backHandling = onRewardsPopUpClosed, - screenContent = - rewardsGratificationUIState.gratificationResponse, - ) - } + ScratchCardScreen( + playerId = playerId, + screenDefinition = screenDefinition, + scratchCardResponse = scratchCardResponse, + gratificationResponse = + rewardsGratificationUIState.gratificationResponse, + playerCardDataProvider = playerCardDataProvider, + personalisedOffersProvider = personalisedOffersProvider, + screenName = NaviPayAnalytics.NAVI_PAY_PAYMENT_SUMMARY_SCREEN, + previousBalance = + scratchCardResponse?.naviCoinDetails?.previousBalance, + onRewardsPopUpClosed = onRewardsPopUpClosed, + ) } } } @@ -629,7 +614,7 @@ private fun SharedTransitionScope.BottomBarSection( ) else { ThemeRoundedButtonWithImage( - text = stringResource(id = R.string.np_scratch_and_win), + text = stringResource(id = R.string.np_claim_now), imageResId = CommonR.drawable.ic_scratch_card, fontSize = 12.sp, modifier = Modifier.weight(1f), @@ -739,7 +724,7 @@ private fun SharedTransitionScope.RewardsBottomBarWithStripSection( color = ctaWhite, ) ) { - append(stringResource(R.string.np_scratch_card)) + append(stringResource(R.string.np_reward)) } } ) @@ -970,7 +955,7 @@ private fun ScratchAndWinButton(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.width(8.dp)) NaviText( - text = stringResource(id = R.string.np_scratch_and_win), + text = stringResource(id = R.string.np_claim_now), fontSize = 12.sp, fontFamily = naviFontFamily, fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt index 4d07d3835b..be53e28d47 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/common/paymentsummary/viewmodel/PaymentSummaryViewModel.kt @@ -75,7 +75,6 @@ import com.navi.pay.utils.NAVI_PAY_AUTO_POPUP_SCRATCH_CARD_COUNTER import com.navi.pay.utils.NAVI_PAY_REWARDS_SCRATCH_CARD_RESPONSE_CACHE_KEY import com.navi.pay.utils.getShareReceiptUiProperties import com.navi.pay.utils.roundTo -import com.navi.rr.common.network.di.V1 import com.navi.rr.common.network.di.V2 import com.navi.rr.scratchcard.helper.ScratchCardNudgeHelper import com.navi.rr.scratchcard.helper.SpecialRewardType @@ -103,6 +102,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope @HiltViewModel class PaymentSummaryViewModel @@ -112,8 +112,7 @@ constructor( private val syncOrderHistoryUseCase: SyncOrderHistoryUseCase, private val sharedPreferenceRepository: SharedPreferenceRepository, private val commonRepository: CommonRepository, - @V1 private val scratchCardNudgeHelperV1: ScratchCardNudgeHelper, - @V2 private val scratchCardNudgeHelperV2: ScratchCardNudgeHelper, + @V2 val scratchCardNudgeHelperV2: ScratchCardNudgeHelper, naviPaySessionHelper: NaviPaySessionHelper, private val naviPayWidgetManager: NaviPayWidgetManager, private val naviCacheRepository: NaviCacheRepository, @@ -827,6 +826,7 @@ constructor( scratchCardNudgeHelperV2.fetchSelectedExperimentReward( onStandardReward = ::processRewardGratification, onSpecialReward = ::processSpecialReward, + onMultipleSpecialRewards = ::processSpecialRewards, ) } @@ -835,11 +835,31 @@ constructor( SpecialRewardType.IPL_REWARD -> { processIplRewardGratification() } + SpecialRewardType.PERSONALIZED_OFFER_REWARD -> { + processPersonalizedOffersRewardGratification() + } + } + } + + private suspend fun processSpecialRewards(rewardTypes: Set) { + if (rewardTypes.isEmpty()) return + + if (rewardTypes.size == 1) { + when (rewardTypes.first()) { + SpecialRewardType.IPL_REWARD -> processIplRewardGratification() + SpecialRewardType.PERSONALIZED_OFFER_REWARD -> + processPersonalizedOffersRewardGratification() + } + return + } + + if (rewardTypes.contains(SpecialRewardType.PERSONALIZED_OFFER_REWARD)) { + processPersonalizedOffersRewardGratification() } } private suspend fun processRewardGratification() { - scratchCardNudgeHelperV1 + scratchCardNudgeHelperV2 .getGratification( requestId = upiRequestId, screenName = NaviPayAnalytics.NAVI_PAY_PAYMENT_SUMMARY_SCREEN, @@ -856,10 +876,24 @@ constructor( .collect(::processRewardGratificationCallback) } + private suspend fun processPersonalizedOffersRewardGratification() { + supervisorScope { + launch { + scratchCardNudgeHelperV2 + .getGratification( + requestId = upiRequestId, + screenName = NaviPayAnalytics.NAVI_PAY_PAYMENT_SUMMARY_SCREEN, + ) + .collect(::processRewardGratificationCallback) + } + + launch { scratchCardNudgeHelperV2.fetchPersonalizedOffers() } + } + } + private suspend fun processRewardGratificationCallback( gratificationStatus: GratificationStatus? ) { - if (gratificationStatus == null) { return } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt index fe6068b204..73635ff801 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/tstore/details/ui/common/OrderDetailsSummarySection.kt @@ -77,7 +77,7 @@ import com.navi.pay.tstore.utils.error.model.OrderErrorEntity import com.navi.pay.utils.clickableDebounce import com.navi.pay.utils.shimmerEffect import com.navi.rr.scratchcard.model.ScratchCardBackResponse -import com.navi.rr.scratchcard.ui.compose.ScratchCardComposable +import com.navi.rr.scratchcard.ui.compose.ScratchCardPagerComposable /** * Only this section can be modified by vertical specific developers. Other sections to be remained @@ -349,7 +349,7 @@ internal fun OrderDetailsSummarySection( if (showRewardsPopUp) { Popup { - ScratchCardComposable( + ScratchCardPagerComposable( backHandling = onRewardsPopUpClosed, screenContent = (rewardsGratificationUIState diff --git a/android/navi-pay/src/main/res/values/strings.xml b/android/navi-pay/src/main/res/values/strings.xml index acef523106..e848f9b49d 100644 --- a/android/navi-pay/src/main/res/values/strings.xml +++ b/android/navi-pay/src/main/res/values/strings.xml @@ -503,6 +503,7 @@ Account not linked The bank account you are trying to send money is not linked to Navi UPI anymore. Scratch & win + Claim now Scratch & win up to Your scratch card is ready! Credit line @@ -634,6 +635,7 @@ is ready Hurray! You’ve won a\u0020 scratch card + reward How to set UPI PIN? In order to make payments using your bank account, you need to have a UPI PIN for that account. You can follow these steps to set your UPI PIN on Navi: On the Navi homepage, click on the profile icon on the top left of the screen diff --git a/android/navi-rr/src/main/java/com/navi/rr/leaderboard/utils/Constants.kt b/android/navi-rr/src/main/java/com/navi/rr/leaderboard/utils/Constants.kt index 0110736812..a12f130c56 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/leaderboard/utils/Constants.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/leaderboard/utils/Constants.kt @@ -21,6 +21,7 @@ object Constants { const val SHARE_LOADER = "share_loader" const val IC_GENERIC_SHARE = "ic_generic_share" const val SHARE_CTA = "shareCta" + const val PERSONALISED_OFFER_CARD_SCREEN = "PERSONALISED_OFFER_CARD_SCREEN" const val REWARDS_SCRATCH_CARD_SCREEN = "REWARDS_SCRATCH_CARD_SCREEN" const val POWERPLAY_SCRATCH_CARD_SCREEN = "POWERPLAY_SCRATCH_CARD_SCREEN" } diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/BaseScratchCardNudgeHelper.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/BaseScratchCardNudgeHelper.kt index 19cf96c7c3..5135e484ba 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/BaseScratchCardNudgeHelper.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/BaseScratchCardNudgeHelper.kt @@ -14,8 +14,9 @@ import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.UPI_SCRAT import com.navi.common.model.RequestConfig import com.navi.common.usecase.LitmusExperimentsUseCase import com.navi.common.usecase.SyncLitmusExperimentUseCase -import com.navi.common.utils.Constants import com.navi.common.utils.Constants.LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY +import com.navi.common.utils.Constants.LITMUS_EXPERIMENT_NAVIPAY_PERSONALISED_OFFERS +import com.navi.common.utils.Constants.ScratchCardExperiment import com.navi.common.utils.NaviApiPoller import com.navi.common.utils.isValidResponse import com.navi.common.utils.log @@ -54,7 +55,7 @@ abstract class BaseScratchCardNudgeHelper( Dispatchers.IO + CoroutineExceptionHandler { _, throwable -> throwable.log() } ) - protected var currentExperimentForReward: Constants.ScratchCardExperiment? = null + protected var currentExperimentForReward: ScratchCardExperiment? = null protected val scratchCardPoller: NaviApiPoller by lazy { NaviApiPoller( @@ -99,38 +100,77 @@ abstract class BaseScratchCardNudgeHelper( override suspend fun fetchSelectedExperimentReward( onStandardReward: suspend () -> Unit, onSpecialReward: suspend (rewardType: SpecialRewardType) -> Unit, + onMultipleSpecialRewards: (suspend (Set) -> Unit)?, ) { - val litmusResponse = litmusUseCase.execute(LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY) - refreshIplExperimentData() - when (litmusResponse?.variant?.name) { - Constants.ScratchCardExperiment.DOUBLE_SCRATCH_CARD_EXPERIENCE.experimentName -> { - currentExperimentForReward = - Constants.ScratchCardExperiment.DOUBLE_SCRATCH_CARD_EXPERIENCE - onSpecialReward(SpecialRewardType.IPL_REWARD) + val rewards = getActiveSpecialRewards() + refreshScratchCardExperimentData() + + when { + rewards.size > 1 && onMultipleSpecialRewards != null -> { + currentExperimentForReward = ScratchCardExperiment.DOUBLE_SCRATCH_CARD_EXPERIENCE + onMultipleSpecialRewards(rewards) + return } - Constants.ScratchCardExperiment.SINGLE_SCRATCH_CARD_EXPERIENCE.experimentName -> { - currentExperimentForReward = - Constants.ScratchCardExperiment.SINGLE_SCRATCH_CARD_EXPERIENCE - onStandardReward() + rewards.size > 1 && onMultipleSpecialRewards == null -> { + currentExperimentForReward = ScratchCardExperiment.DOUBLE_SCRATCH_CARD_EXPERIENCE + onSpecialReward(rewards.first()) + return + } + + rewards.size == 1 -> { + val reward = rewards.first() + currentExperimentForReward = reward.experiment + onSpecialReward(reward) + return } else -> { - currentExperimentForReward = - Constants.ScratchCardExperiment.SINGLE_SCRATCH_CARD_EXPERIENCE + currentExperimentForReward = fallbackStandardExperiment() onStandardReward() } } } - protected fun refreshIplExperimentData() { + private suspend fun getActiveSpecialRewards(): Set = buildSet { + val iplVariant = + litmusUseCase.execute(LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY)?.variant?.name + + val personalizedVariant = + litmusUseCase.execute(LITMUS_EXPERIMENT_NAVIPAY_PERSONALISED_OFFERS)?.variant?.name + + if (iplVariant == ScratchCardExperiment.DOUBLE_SCRATCH_CARD_EXPERIENCE.experimentName) { + add(SpecialRewardType.IPL_REWARD) + } + + if (personalizedVariant == ScratchCardExperiment.PERSONALISED_OFFERS.experimentName) { + add(SpecialRewardType.PERSONALIZED_OFFER_REWARD) + } + } + + private fun refreshScratchCardExperimentData() { coroutineScope.launch { syncLitmusUseCase.refreshExperimentData( - listOf(LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY) + listOf( + LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY, + LITMUS_EXPERIMENT_NAVIPAY_PERSONALISED_OFFERS, + ) ) } } + private val SpecialRewardType.experiment: ScratchCardExperiment + get() = + when (this) { + SpecialRewardType.IPL_REWARD -> ScratchCardExperiment.DOUBLE_SCRATCH_CARD_EXPERIENCE + SpecialRewardType.PERSONALIZED_OFFER_REWARD -> + ScratchCardExperiment.SINGLE_SCRATCH_CARD_EXPERIENCE + } + + private fun fallbackStandardExperiment(): ScratchCardExperiment { + return ScratchCardExperiment.SINGLE_SCRATCH_CARD_EXPERIENCE + } + companion object { const val SCRATCH_CARD_OVERLAY_SCREEN_NAME = "SCRATCH_CARD_OVERLAY_SCREEN_NAME" const val TRANSACTION_ID = "transaction_id" @@ -144,6 +184,8 @@ abstract class BaseScratchCardNudgeHelper( const val HELD_REWARD = "HELD_REWARD" const val INSTANT_REWARD = "INSTANT_REWARD" const val NONE = "NONE" + const val ASSURED_REWARD = "ASSURED_REWARD" + const val DEFAULT = "DEFAULT" const val ZERO_AMOUNT = "0" val POLL_INTERVAL = 1.seconds const val MAX_POLL_COUNT = 20 @@ -155,6 +197,12 @@ abstract class BaseScratchCardNudgeHelper( "dev_scratch_card_gratification_received_via_polling" const val DEV_SCRATCH_CARD_GRATIFICATION_RESPONSE_RECEIVED_VIA_FIREBASE = "dev_scratch_card_gratification_received_via_firebase" + const val DEV_SCRATCH_CARD_V2_PERSONALIZED_OFFERS_RESPONSE = + "dev_scratch_card_v2_personalized_offers_response" + const val DEV_SCRATCH_CARD_V2_IMPL_PERSONALIZED_OFFERS_ERROR = + "dev_scratch_card_v2_impl_personalized_offers_error" + const val DEV_SCRATCH_CARD_V2_IMPL_PERSONALIZED_OFFERS_EXCEPTION = + "dev_scratch_card_v2_impl_personalized_offers_exception" const val REWARDS_ON_UPI_COLLECTION_PATH = "rewards_upi" const val DEV_REWARDS_ON_UPI_COLLECTION_PATH = "dev_reward_upi_collection_path" diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelper.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelper.kt index 1b4e482daf..6206b62b44 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelper.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelper.kt @@ -7,13 +7,15 @@ package com.navi.rr.scratchcard.helper +import com.navi.common.forge.model.ScreenDefinition import com.navi.rr.scratchcard.model.GratificationStatus import com.navi.rr.scratchcard.model.ScratchCardModel import com.navi.rr.scratchcard.model.states.RewardNudgeApiStatus import java.io.Closeable import kotlinx.coroutines.flow.Flow -interface ScratchCardNudgeHelper : GratificationExperimentHelper, Closeable { +interface ScratchCardNudgeHelper : + GratificationExperimentHelper, PersonalizedOfferProvider, Closeable { suspend fun getNudgeDetails(product: String, objective: String): RewardNudgeApiStatus @@ -26,6 +28,8 @@ interface ScratchCardNudgeHelper : GratificationExperimentHelper, Closeable { screenName: String, onScratchCardModel: (scratchCardModel: ScratchCardModel?) -> Unit, ) + + suspend fun fetchPersonalizedOffers() } interface GratificationExperimentHelper { @@ -33,9 +37,18 @@ interface GratificationExperimentHelper { suspend fun fetchSelectedExperimentReward( onStandardReward: suspend () -> Unit, onSpecialReward: suspend (rewardType: SpecialRewardType) -> Unit, + onMultipleSpecialRewards: (suspend (rewardTypes: Set) -> Unit)? = null, ) } -enum class SpecialRewardType { - IPL_REWARD +interface PersonalizedOfferProvider { + + fun fetchPersonalisedOfferData(): ScreenDefinition? { + return null + } +} + +enum class SpecialRewardType { + IPL_REWARD, + PERSONALIZED_OFFER_REWARD, } diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelperImpl.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelperImpl.kt index 1fb27981bc..6c533bd6ab 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelperImpl.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardNudgeHelperImpl.kt @@ -207,6 +207,8 @@ constructor( } } + override suspend fun fetchPersonalizedOffers() {} + private suspend fun collectScratchCardResponse( requestId: String, screenName: String, diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardScreenResolver.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardScreenResolver.kt new file mode 100644 index 0000000000..5133da0afb --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/ScratchCardScreenResolver.kt @@ -0,0 +1,49 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.helper + +import com.navi.common.forge.model.ScreenDefinition +import com.navi.rr.scratchcard.model.GratificationResponse +import com.navi.rr.scratchcard.model.ScratchCardResponse +import com.navi.rr.scratchcard.model.states.ScratchCardScreenVariant +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ScratchCardScreenResolver @Inject constructor() { + + fun resolve( + playerId: String?, + screenDefinition: ScreenDefinition?, + scratchCardResponse: ScratchCardResponse?, + personalisedOffersProvider: () -> ScreenDefinition?, + gratificationResponse: GratificationResponse?, + previousBalance: Int? = null, + ): ScratchCardScreenVariant? { + return when { + playerId != null && screenDefinition != null && scratchCardResponse != null -> { + scratchCardResponse.toScratchCardModel()?.let { model -> + ScratchCardScreenVariant.Deck( + screenDefinition = screenDefinition, + scratchCardModel = model, + previousBalance = previousBalance, + ) + } + } + + gratificationResponse != null -> { + ScratchCardScreenVariant.Pager( + personalisedOffers = personalisedOffersProvider, + gratificationResponse = gratificationResponse, + ) + } + + else -> null + } + } +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardNudgeHelperV2Impl.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardNudgeHelperV2Impl.kt index 6840e2385e..e18afd3a9e 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardNudgeHelperV2Impl.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardNudgeHelperV2Impl.kt @@ -19,6 +19,7 @@ import com.navi.common.utils.Constants import com.navi.common.utils.NaviApiPoller import com.navi.common.utils.isValidResponse import com.navi.common.utils.log +import com.navi.rr.leaderboard.utils.Constants.PERSONALISED_OFFER_CARD_SCREEN import com.navi.rr.leaderboard.utils.Constants.POWERPLAY_SCRATCH_CARD_SCREEN import com.navi.rr.leaderboard.utils.Constants.REWARDS_SCRATCH_CARD_SCREEN import com.navi.rr.scratchcard.helper.BaseScratchCardNudgeHelper @@ -75,6 +76,8 @@ constructor( ) } + private var personalisedOffers: ScreenDefinition? = null + private val fbSnapshotListener by lazy { FirebaseSnapshotListener( coroutineScope = coroutineScope, @@ -315,6 +318,39 @@ constructor( } } + override suspend fun fetchPersonalizedOffers() { + return runCatching { + scratchCardRepo.fetchPersonalisedOffers( + key = SCRATCH_CARD_UI_CONFIG, + screenId = PERSONALISED_OFFER_CARD_SCREEN, + metricInfo = + MetricInfo.RewardMetric( + screen = SCRATCH_CARD_OVERLAY_SCREEN_NAME, + isNae = { false }, + ), + ) + } + .fold( + onSuccess = { response -> + if (response.isValidResponse()) { + eventTracker.sendEvent(DEV_SCRATCH_CARD_V2_PERSONALIZED_OFFERS_RESPONSE) + personalisedOffers = response.data + } else { + eventTracker.sendEvent( + DEV_SCRATCH_CARD_V2_IMPL_PERSONALIZED_OFFERS_ERROR, + hashMapOf(EXCEPTION to response.errors.toString()), + ) + } + }, + onFailure = { throwable -> + eventTracker.sendEvent( + DEV_SCRATCH_CARD_V2_IMPL_PERSONALIZED_OFFERS_EXCEPTION, + hashMapOf(EXCEPTION to throwable.message.orEmpty()), + ) + }, + ) + } + private fun cancelJobs() { if (::firebaseJob.isInitialized && firebaseJob.isActive) { firebaseJob.cancel() @@ -324,6 +360,10 @@ constructor( } } + override fun fetchPersonalisedOfferData(): ScreenDefinition? { + return personalisedOffers + } + override fun close() { cancelJobs() } diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardResponseHandler.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardResponseHandler.kt index fda02cd429..57fd849911 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardResponseHandler.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/helper/nudgeHelperV2/ScratchCardResponseHandler.kt @@ -7,9 +7,11 @@ package com.navi.rr.scratchcard.helper.nudgeHelperV2 -import com.navi.base.utils.isNotNull +import com.navi.base.utils.orFalse import com.navi.common.forge.model.ScreenDefinition import com.navi.common.utils.toJsonObject +import com.navi.rr.scratchcard.helper.BaseScratchCardNudgeHelper.Companion.ASSURED_REWARD +import com.navi.rr.scratchcard.helper.BaseScratchCardNudgeHelper.Companion.DEFAULT import com.navi.rr.scratchcard.helper.BaseScratchCardNudgeHelper.Companion.HELD_REWARD import com.navi.rr.scratchcard.helper.BaseScratchCardNudgeHelper.Companion.INSTANT_REWARD import com.navi.rr.scratchcard.helper.BaseScratchCardNudgeHelper.Companion.N @@ -40,33 +42,66 @@ class ScratchCardResponseHandler @Inject constructor(private val fieldInjector: template: ScreenDefinition?, callback: (ScreenDefinition) -> Unit, ) { - val dataMap: MutableMap = - scratchCardData?.toFlatMap()?.toMutableMap() ?: mutableMapOf() - val coinAmount = scratchCardData?.metadata?.subtitle?.replace(",", "") - val coinAmountValue = coinAmount?.toInt() ?: 0 - dataMap[REWARD_EARNED] = N - if (coinAmountValue > 0) { - dataMap[REWARD_EARNED] = Y - } - dataMap[NAVI_COINS_WITH_UNDERSCORE] = getNaviCoinsString(coinAmountValue) - dataMap[TRANSACTION_ID] = requestId - dataMap[SCREEN_NAME] = screenName + if (scratchCardData == null || template == null) return - val screenTemplate = - getTemplateName( - amount = scratchCardData?.metadata?.subtitle, - rewardType = scratchCardData?.rewardType, - template = template, - rewardForm = scratchCardData?.rewardForm, - isRewardLocked = scratchCardData?.isRewardLocked, + val coinAmount = extractCoinAmount(scratchCardData) + val dataMap = buildDataMap(scratchCardData, coinAmount, requestId, screenName) + val templateName = resolveTemplateName(template, scratchCardData, coinAmount) + + val screenTemplate = template.screenStructure?.templates?.get(templateName) ?: return + + getInflatedScreenDefinitionTemplate( + JSONObject(getGsonBuilders().toJson(screenTemplate)), + dataMap, ) - template?.screenStructure?.templates?.get(screenTemplate)?.let { - val inflatedTemplate = - getInflatedScreenDefinitionTemplate( - template = JSONObject(getGsonBuilders().toJson(it)), - dataMap = dataMap, - ) - inflatedTemplate?.let { callback(it) } + ?.let(callback) + } + + private fun extractCoinAmount(scratchCard: ScratchCard?): Int { + return scratchCard?.metadata?.subtitle?.replace(",", "")?.toIntOrNull() ?: 0 + } + + private fun resolveTemplateName( + template: ScreenDefinition, + scratchCard: ScratchCard, + coinAmount: Int, + ): String? { + val rewardType = scratchCard.rewardType ?: return null + + val rewardMeta = + template.jsonMetaData?.get(TEMPLATE_NAME)?.toJsonObject()?.optJSONObject(rewardType) + ?: return null + + val rewardForm = scratchCard.rewardForm + val isLocked = scratchCard.isRewardLocked.orFalse() + + data class TemplateRule(val key: String, val condition: () -> Boolean) + + val rules = + listOf( + TemplateRule(NO_REWARD) { coinAmount.toString() == ZERO_AMOUNT }, + TemplateRule(INSTANT_REWARD) { rewardForm == NONE }, + TemplateRule(HELD_REWARD) { isLocked }, + TemplateRule(ASSURED_REWARD) { + scratchCard.rewardCategory?.let { it != DEFAULT } == true + }, + TemplateRule(REWARD) { true }, // fallback + ) + + return rules.firstOrNull { it.condition() }?.let { rewardMeta.optString(it.key) } + } + + private fun buildDataMap( + scratchCard: ScratchCard, + coinAmount: Int, + requestId: String, + screenName: String, + ): MutableMap { + return scratchCard.toFlatMap().toMutableMap().apply { + this[REWARD_EARNED] = if (coinAmount > 0) Y else N + this[NAVI_COINS_WITH_UNDERSCORE] = getNaviCoinsString(coinAmount) + this[TRANSACTION_ID] = requestId + this[SCREEN_NAME] = screenName } } @@ -80,30 +115,4 @@ class ScratchCardResponseHandler @Inject constructor(private val fieldInjector: } return null } - - private fun getTemplateName( - amount: String?, - rewardType: String? = null, - rewardForm: String? = null, - isRewardLocked: Boolean? = null, - template: ScreenDefinition?, - ): String? { - val jsonMetaData = template?.jsonMetaData?.get(TEMPLATE_NAME).toJsonObject() - if (rewardForm.isNotNull() && rewardForm == NONE) { - return if (amount != ZERO_AMOUNT) { - jsonMetaData?.optJSONObject(rewardType)?.getString(INSTANT_REWARD) - } else { - null - } - } - return if (amount != ZERO_AMOUNT) { - if (isRewardLocked == true) { - jsonMetaData?.optJSONObject(rewardType)?.getString(HELD_REWARD) - } else { - jsonMetaData?.optJSONObject(rewardType)?.getString(REWARD) - } - } else { - jsonMetaData?.optJSONObject(rewardType)?.getString(NO_REWARD) - } - } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCard.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCard.kt index 2473662e00..e9bf026d3d 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCard.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCard.kt @@ -52,6 +52,7 @@ data class ScratchCardResponse( val metadata: ScratchCardHistoryCardMetaData? = null, val isPlayerCard: Boolean? = null, val isRewardLocked: Boolean? = null, + val rewardCategory: String? = null, ) { fun toScratchCard(): ScratchCard { return ScratchCard( @@ -71,6 +72,7 @@ data class ScratchCardResponse( naviCoinDetails = naviCoinDetails, expiryEpoch = accrualExpireAtEpoch, isRewardLocked = isRewardLocked, + rewardCategory = rewardCategory, ) } @@ -92,6 +94,7 @@ data class ScratchCardResponse( naviCoinDetails = naviCoinDetails, expiryEpoch = accrualExpireAtEpoch, isRewardLocked = isRewardLocked, + rewardCategory = rewardCategory, ) } } @@ -110,6 +113,7 @@ data class ScratchCard( val expiryEpoch: Long? = null, val templateName: String? = null, val isRewardLocked: Boolean? = null, + val rewardCategory: String? = null, ) data class ScratchCardHistoryCardMetaData( diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCardEntity.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCardEntity.kt index 05fcdbbe02..78301eb10a 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCardEntity.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCardEntity.kt @@ -32,6 +32,7 @@ data class ScratchCardModel( val metaData: Map? = null, val type: String? = null, val isRewardLocked: Boolean? = null, + val rewardCategory: String? = null, ) data class ScratchCardModelDataEntity( diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCardUiState.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCardUiState.kt new file mode 100644 index 0000000000..709b9b2d15 --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/ScratchCardUiState.kt @@ -0,0 +1,27 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.model + +import android.util.Size +import com.navi.common.forge.model.WidgetModelDefinition +import com.navi.uitron.model.UiTronResponse + +data class ScratchCardUiState( + val scratchCardScratched: ScratchCardBackResponse, + val screenSize: Size?, + val showConfetti: Boolean, + val triggerAutoScroll: Boolean, + val translateYState: Float, +) + +data class ScratchCardEventHandlers( + val onScreenSizeChange: (Size) -> Unit, + val onConfettiEnd: () -> Unit, +) + +data class PageItem(val widgets: List>) diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/states/ScratchCardScreenVariant.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/states/ScratchCardScreenVariant.kt new file mode 100644 index 0000000000..a9fc9f1e79 --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/model/states/ScratchCardScreenVariant.kt @@ -0,0 +1,25 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.model.states + +import com.navi.common.forge.model.ScreenDefinition +import com.navi.rr.scratchcard.model.GratificationResponse +import com.navi.rr.scratchcard.model.ScratchCardModel + +sealed class ScratchCardScreenVariant { + data class Deck( + val screenDefinition: ScreenDefinition, + val scratchCardModel: ScratchCardModel, + val previousBalance: Int?, + ) : ScratchCardScreenVariant() + + data class Pager( + val personalisedOffers: () -> ScreenDefinition?, + val gratificationResponse: GratificationResponse, + ) : ScratchCardScreenVariant() +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/repo/ScratchCardRepo.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/repo/ScratchCardRepo.kt index 33846f24e8..868f2b1ad4 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/repo/ScratchCardRepo.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/repo/ScratchCardRepo.kt @@ -17,6 +17,7 @@ import com.navi.common.forge.model.ScreenDefinition import com.navi.common.model.ModuleNameV2 import com.navi.common.network.models.RepoResult import com.navi.common.utils.Constants.GZIP +import com.navi.common.utils.retrofitService import com.navi.rr.common.models.XTarget import com.navi.rr.common.network.retrofit.ResponseHandler import com.navi.rr.common.network.retrofit.RetrofitService @@ -97,6 +98,26 @@ constructor( .first() } + suspend fun fetchPersonalisedOffers( + key: String, + screenId: String = "", + metricInfo: MetricInfo>, + ): RepoResult { + return cacheHandlerProxy + .fetchData(key = key) { + responseHandler.handleResponse( + response = + retrofitService.fetchAlchemistScreenV2( + acceptEncoding = GZIP, + screenId = screenId, + target = ModuleNameV2.ALCHEMIST.name, + ), + metricInfo = metricInfo, + ) + } + .first() + } + suspend fun fetchPlayerCard( playerReferenceId: String, metricInfo: MetricInfo>, diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ActionHandler.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ActionHandler.kt new file mode 100644 index 0000000000..aa7fd93638 --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ActionHandler.kt @@ -0,0 +1,129 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.ui.compose + +import android.app.Activity +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.navi.base.deeplink.DeepLinkManager +import com.navi.base.model.CtaData +import com.navi.common.uitron.model.action.ApiType +import com.navi.common.uitron.model.action.CtaAction +import com.navi.common.utils.Constants.FINISH +import com.navi.rr.scratchcard.model.ScratchCardBackResponse +import com.navi.rr.scratchcard.vm.ScratchCardVM +import com.navi.rr.utils.constants.Constants.AUTO_REDEEM_KEY +import com.navi.rr.utils.constants.Constants.BACK +import com.navi.rr.utils.constants.Constants.COIN_TRANSFER_COMPLETE +import com.navi.rr.utils.constants.Constants.SCRATCH_STARTED +import com.navi.uitron.model.action.TriggerApiAction + +/** Handles various actions triggered by the scratch card screen */ +@Composable +fun ActionHandler( + scratchCardVM: ScratchCardVM, + requestId: String?, + bundle: Bundle, + onRewardDisbursement: () -> Unit, + activity: Activity, + showConfetti: () -> Unit, + backHandling: ((data: ScratchCardBackResponse?) -> Unit)? = null, + onCoinTransferComplete: () -> Unit = {}, +) { + LaunchedEffect(Unit) { + scratchCardVM.screenActions.collect { action -> + when (action) { + is CtaAction -> + handleCtaAction( + action = action, + scratchCardVM = scratchCardVM, + backHandling = backHandling, + activity = activity, + bundle = bundle, + showConfetti = showConfetti, + onCoinTransferComplete = onCoinTransferComplete, + ) + + is TriggerApiAction -> + handleApiAction( + action = action, + showConfetti = showConfetti, + onRewardDisbursement = onRewardDisbursement, + scratchCardVM = scratchCardVM, + requestId = requestId, + ) + } + } + } +} + +private fun handleCtaAction( + action: CtaAction, + scratchCardVM: ScratchCardVM, + backHandling: ((data: ScratchCardBackResponse?) -> Unit)?, + activity: Activity, + bundle: Bundle, + showConfetti: () -> Unit, + onCoinTransferComplete: () -> Unit, +) { + when (action.ctaData?.url) { + SCRATCH_STARTED -> showConfetti() + COIN_TRANSFER_COMPLETE -> onCoinTransferComplete() + else -> + navigate( + action = action, + activity = activity, + bundle = bundle, + backHandling = backHandling, + scratchCardVM = scratchCardVM, + ) + } + + if (action.ctaData?.type == BACK) { + backHandling?.invoke(scratchCardVM.scratchCardBackResponse.value) + } +} + +private fun navigate( + action: CtaAction, + activity: Activity, + bundle: Bundle, + backHandling: ((data: ScratchCardBackResponse?) -> Unit)?, + scratchCardVM: ScratchCardVM, +) { + backHandling?.invoke(scratchCardVM.scratchCardBackResponse.value) + + val autoRedeem = + action.ctaData?.parameters?.firstOrNull { it.key == AUTO_REDEEM_KEY }?.value?.toBoolean() + ?: false + + val updatedBundle = bundle.apply { putBoolean(AUTO_REDEEM_KEY, autoRedeem) } + + DeepLinkManager.getDeepLinkListener() + ?.navigateTo( + activity = activity, + ctaData = CtaData(url = action.ctaData?.url, parameters = action.ctaData?.parameters), + bundle = updatedBundle, + finish = bundle.getBoolean(FINISH), + ) +} + +private suspend fun handleApiAction( + action: TriggerApiAction, + showConfetti: () -> Unit, + onRewardDisbursement: () -> Unit, + scratchCardVM: ScratchCardVM, + requestId: String?, +) { + if (action.apiType == ApiType.RewardsAndReferralAction.toString()) { + showConfetti() + onRewardDisbursement() + scratchCardVM.putScratchCompleted(requestId = requestId ?: "") + } +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/RewardsCardsPager.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/RewardsCardsPager.kt new file mode 100644 index 0000000000..26f9073f56 --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/RewardsCardsPager.kt @@ -0,0 +1,178 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.ui.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.navi.common.forge.model.ScreenDefinition +import com.navi.rr.common.widgetFactory.WidgetRenderer +import com.navi.rr.scratchcard.model.PageItem +import com.navi.rr.scratchcard.utils.PagerConstants.AUTO_SCROLL_DELAY +import com.navi.rr.scratchcard.utils.PagerConstants.CARD_WIDTH +import com.navi.rr.scratchcard.utils.PagerConstants.DEFAULT_AUTO_SCROLL_DELAY +import com.navi.rr.scratchcard.utils.PagerConstants.DEFAULT_CARD_WIDTH +import com.navi.rr.scratchcard.utils.PagerConstants.DEFAULT_PAGE_SPACING +import com.navi.rr.scratchcard.utils.PagerConstants.PAGE_SPACING +import com.navi.rr.scratchcard.utils.calculatePageScaleFactor +import com.navi.rr.scratchcard.utils.getPageDistanceOffset +import com.navi.rr.scratchcard.vm.ScratchCardVM +import com.navi.rr.utils.constants.Constants.LOTTIE +import kotlinx.coroutines.delay + +@Composable +fun RewardCardsPager( + scratchCardVM: ScratchCardVM, + screenDefinition: ScreenDefinition, + personalisedOffers: (() -> ScreenDefinition?)? = null, + triggerAutoScroll: Boolean = false, +) { + val pages = + remember(screenDefinition) { getPagerItemsList(screenDefinition, personalisedOffers) } + + val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp + val cardWidth = screenDefinition.metaData?.get(CARD_WIDTH)?.toIntOrNull() ?: DEFAULT_CARD_WIDTH + val pageSpacing = + screenDefinition.metaData?.get(PAGE_SPACING)?.toIntOrNull() ?: DEFAULT_PAGE_SPACING + val contentPadding: PaddingValues = + remember(screenWidthDp) { + calculateContentPadding(screenWidthDp, cardWidth.dp, pageSpacing.dp) + } + + val pagerState = rememberPagerState(initialPage = 0) { pages.size } + + val currentPageIndex by remember { derivedStateOf { pagerState.currentPage } } + val isLastPage by remember { derivedStateOf { currentPageIndex >= pages.size - 1 } } + + var autoScrollEnabled by remember { mutableStateOf(false) } + var userScrollEnabled by remember { mutableStateOf(false) } + + LaunchedEffect(triggerAutoScroll) { + if (triggerAutoScroll) { + autoScrollEnabled = true + userScrollEnabled = true + } + } + + LaunchedEffect(autoScrollEnabled) { + if (autoScrollEnabled) { + while (autoScrollEnabled) { + if (!isLastPage) { + pagerState.animateScrollToPage(currentPageIndex + 1) + delay( + screenDefinition.metaData?.get(AUTO_SCROLL_DELAY)?.toLongOrNull() + ?: DEFAULT_AUTO_SCROLL_DELAY + ) + } else { + autoScrollEnabled = false + break + } + } + } + } + + HorizontalPager( + state = pagerState, + contentPadding = if (userScrollEnabled) contentPadding else PaddingValues(0.dp), + userScrollEnabled = userScrollEnabled, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + verticalAlignment = Alignment.Top, + beyondViewportPageCount = 10, + ) { page -> + PagerItem( + pageItem = pages[page], + index = page, + pagerState = pagerState, + currentPage = currentPageIndex, + viewModel = scratchCardVM, + ) + } +} + +@Composable +private fun PagerItem( + pageItem: PageItem, + index: Int, + pagerState: PagerState, + currentPage: Int, + viewModel: ScratchCardVM, +) { + val pageOffset = + getPageDistanceOffset( + page = index, + currentPage = currentPage, + currentPageOffset = pagerState.currentPageOffsetFraction, + ) + val scaleFactor = calculatePageScaleFactor(pageOffset) + + Box( + modifier = + Modifier.fillMaxWidth().fillMaxHeight().graphicsLayer { + scaleX = scaleFactor + scaleY = scaleFactor + } + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // Use stable widget keys + pageItem.widgets.forEach { widget -> + key(widget.widgetId) { WidgetRenderer(widget = widget, viewModel = viewModel) } + } + } + } +} + +private fun getPagerItemsList( + screenDefinition: ScreenDefinition, + personalisedOffers: (() -> ScreenDefinition?)? = null, +): List { + val pages = mutableListOf() + + val scratchCardWidgets = + screenDefinition.screenStructure?.content?.widgets?.filter { it.widgetName != LOTTIE } + + if (!scratchCardWidgets.isNullOrEmpty()) { + pages.add(PageItem(widgets = scratchCardWidgets)) + } + + personalisedOffers?.invoke()?.screenStructure?.content?.widgets?.forEachIndexed { index, widget + -> + pages.add(PageItem(widgets = listOf(widget))) + } + + return pages +} + +private fun calculateContentPadding( + screenWidth: Dp, + pageSpacing: Dp, + cardWidth: Dp, +): PaddingValues { + return PaddingValues(horizontal = (screenWidth - pageSpacing - cardWidth) / 2) +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardComposable.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardComposable.kt index 0ecc8a5d4f..86fb63373f 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardComposable.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardComposable.kt @@ -14,11 +14,8 @@ import android.util.Size import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -43,7 +40,6 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -65,12 +61,9 @@ import com.navi.rr.scratchcard.model.ScratchCardBackData import com.navi.rr.scratchcard.model.ScratchCardBackResponse import com.navi.rr.scratchcard.model.states.ScratchCardScreenUIState import com.navi.rr.scratchcard.utils.ScratchCardTheme.ThemeConfig.Companion.SCRATCH_CARD_THEME_KEY -import com.navi.rr.scratchcard.utils.getActivity import com.navi.rr.scratchcard.utils.getLottieForScratchCard import com.navi.rr.scratchcard.utils.isFestiveTheme import com.navi.rr.scratchcard.vm.ScratchCardVM -import com.navi.rr.utils.NaviRRAnalytics -import com.navi.rr.utils.NaviRRAnalytics.Companion.REWARD_SCRATCH_CARD_DISMISS_OUTSIDE import com.navi.rr.utils.composeutils.Init import com.navi.rr.utils.constants.Constants.AUTO_REDEEM_KEY import com.navi.rr.utils.constants.Constants.BACK @@ -78,7 +71,6 @@ import com.navi.rr.utils.constants.Constants.LOTTIE import com.navi.rr.utils.constants.Constants.SCRATCH_STARTED import com.navi.rr.utils.ext.clickable import com.navi.uitron.model.action.TriggerApiAction -import com.navi.uitron.model.data.UiTronActionData import com.navi.uitron.utils.pxToDp import kotlinx.coroutines.cancelChildren @@ -436,52 +428,6 @@ fun ScratchCardComposable( } } -private fun handleScratchCardBackPress( - context: Context, - backCtaAction: UiTronActionData?, - scratchCardScratched: ScratchCardBackResponse, - backHandling: ((data: ScratchCardBackResponse?) -> Unit)? = null, -) { - backCtaAction - ?.actions - ?.firstOrNull { it is CtaAction } - ?.let { action -> - backHandling?.invoke(scratchCardScratched) - (action as CtaAction).ctaData?.let { ctaData -> - DeepLinkManager.getDeepLinkListener() - ?.navigateTo(context.getActivity(), ctaData, finish = true) - } - } ?: run { backHandling?.invoke(ScratchCardBackResponse.NoReward) } -} - -@Composable -private fun ScratchCardScreenHeader( - context: Context, - backCtaAction: UiTronActionData?, - scratchCardBackResponse: ScratchCardBackResponse, - backHandling: ((data: ScratchCardBackResponse?) -> Unit)? = null, -) { - val interactionSource = remember { MutableInteractionSource() } - Box(modifier = Modifier.padding(start = 16.dp, top = 40.dp, bottom = 20.dp, end = 16.dp)) { - Image( - modifier = - Modifier.clickable(interactionSource = interactionSource, indication = null) { - NaviRRAnalytics.naviRRAnalytics - .Rewards() - .sendEvent(REWARD_SCRATCH_CARD_DISMISS_OUTSIDE) - handleScratchCardBackPress( - context, - backCtaAction, - scratchCardScratched = scratchCardBackResponse, - backHandling, - ) - }, - painter = painterResource(R.drawable.ic_close_cross_white), - contentDescription = null, - ) - } -} - @Composable private fun CtaActionHandler( scratchCardVM: ScratchCardVM, diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardComposeHelpers.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardComposeHelpers.kt new file mode 100644 index 0000000000..9222221b55 --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardComposeHelpers.kt @@ -0,0 +1,145 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.ui.compose + +import android.content.Context +import android.util.Size +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.navi.base.deeplink.DeepLinkManager +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.base.utils.orElse +import com.navi.common.R +import com.navi.common.forge.model.WidgetModelDefinition +import com.navi.common.uitron.model.action.CtaAction +import com.navi.rr.common.views.NaviRRLottieAnimationWithTimeout +import com.navi.rr.common.widgetFactory.WidgetRenderer +import com.navi.rr.scratchcard.model.GratificationResponse +import com.navi.rr.scratchcard.model.ScratchCardBackResponse +import com.navi.rr.scratchcard.utils.ScratchCardTheme.ThemeConfig.Companion.SCRATCH_CARD_THEME_KEY +import com.navi.rr.scratchcard.utils.getActivity +import com.navi.rr.scratchcard.utils.getLottieForScratchCard +import com.navi.rr.scratchcard.utils.isFestiveTheme +import com.navi.rr.scratchcard.vm.ScratchCardVM +import com.navi.rr.utils.NaviRRAnalytics +import com.navi.rr.utils.NaviRRAnalytics.Companion.REWARD_SCRATCH_CARD_DISMISS_OUTSIDE +import com.navi.rr.utils.constants.Constants.LOTTIE +import com.navi.uitron.model.UiTronResponse +import com.navi.uitron.model.data.UiTronActionData +import com.navi.uitron.utils.orTrue +import com.navi.uitron.utils.pxToDp + +@Composable +fun RenderLottieWidgets( + widgets: List>?, + scratchCardVM: ScratchCardVM, + screenSize: Size?, +) { + widgets?.forEach { item -> + if (item.widgetName == LOTTIE) { + val aspectRatio = item.widgetProperty?.widgetAspectRatio + val widthPercentage = item.widgetProperty?.widthPercentage + val screenWidth = screenSize?.width.orElse(0) + + if (aspectRatio != null && widthPercentage != null) { + val calculatedWidth = (screenWidth / widthPercentage).toInt() + val calculatedHeight = (calculatedWidth * aspectRatio).toInt() + + val widthDp = pxToDp(px = calculatedWidth.toFloat()).dp + val heightDp = pxToDp(px = calculatedHeight.toFloat()).dp + + Column(modifier = Modifier.height(heightDp).width(widthDp)) { + WidgetRenderer(widget = item, viewModel = scratchCardVM) + } + } else { + WidgetRenderer(widget = item, viewModel = scratchCardVM) + } + } + } +} + +@Composable +fun ScratchCardConfetti( + screenContent: GratificationResponse?, + scratchCardVM: ScratchCardVM, + onAnimationEnd: () -> Unit, +) { + val theme = + screenContent?.screenDefinition?.jsonMetaData?.get(SCRATCH_CARD_THEME_KEY)?.toString() + val lottieUrl = scratchCardVM.festiveThemeHelper.getGratificationConfettiLottieUrl() + val isFestiveTheme = isFestiveTheme(theme) + val shouldShowFestiveThemeLottie = isFestiveTheme && lottieUrl.isNotNullAndNotEmpty() + + NaviRRLottieAnimationWithTimeout( + modifier = Modifier.fillMaxSize(), + isRemoteLottie = shouldShowFestiveThemeLottie, + showLottieInfiniteTimes = false, + lottieUrl = lottieUrl, + lottie = getLottieForScratchCard(themeValue = theme), + onAnimationEnd = onAnimationEnd, + placeHolder = R.raw.gratifaction_confetti, + ) +} + +@Composable +fun ScratchCardScreenHeader( + context: Context, + backCtaAction: UiTronActionData?, + scratchCardBackResponse: ScratchCardBackResponse, + backHandling: ((data: ScratchCardBackResponse?) -> Unit)? = null, +) { + val interactionSource = remember { MutableInteractionSource() } + Box(modifier = Modifier.padding(start = 16.dp, top = 40.dp, bottom = 20.dp, end = 16.dp)) { + Image( + modifier = + Modifier.clickable(interactionSource = interactionSource, indication = null) { + NaviRRAnalytics.naviRRAnalytics + .Rewards() + .sendEvent(REWARD_SCRATCH_CARD_DISMISS_OUTSIDE) + handleScratchCardBackPress( + context, + backCtaAction, + scratchCardScratched = scratchCardBackResponse, + backHandling, + ) + }, + painter = painterResource(R.drawable.ic_close_cross_white), + contentDescription = null, + ) + } +} + +fun handleScratchCardBackPress( + context: Context, + backCtaAction: UiTronActionData?, + scratchCardScratched: ScratchCardBackResponse, + backHandling: ((data: ScratchCardBackResponse?) -> Unit)? = null, +) { + backCtaAction + ?.actions + ?.firstOrNull { it is CtaAction } + ?.let { action -> + backHandling?.invoke(scratchCardScratched) + (action as CtaAction).ctaData?.let { ctaData -> + DeepLinkManager.getDeepLinkListener() + ?.navigateTo(context.getActivity(), ctaData, finish = ctaData.finish.orTrue()) + } + } ?: run { backHandling?.invoke(ScratchCardBackResponse.NoReward) } +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardPagerComposable.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardPagerComposable.kt new file mode 100644 index 0000000000..70986af66c --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardPagerComposable.kt @@ -0,0 +1,263 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.ui.compose + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.os.Bundle +import android.util.Size +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Density +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import com.navi.alfred.AlfredManager +import com.navi.common.forge.model.ScreenDefinition +import com.navi.rr.common.constants.SCRATCH_CARD_GRATIFICATION_SCREEN +import com.navi.rr.common.widgetFactory.WidgetRenderer +import com.navi.rr.scratchcard.helper.BaseScratchCardNudgeHelper.Companion.ASSURED_REWARD +import com.navi.rr.scratchcard.model.GratificationResponse +import com.navi.rr.scratchcard.model.ScratchCardBackData +import com.navi.rr.scratchcard.model.ScratchCardBackResponse +import com.navi.rr.scratchcard.model.ScratchCardEventHandlers +import com.navi.rr.scratchcard.model.ScratchCardUiState +import com.navi.rr.scratchcard.vm.ScratchCardVM +import com.navi.rr.utils.composeutils.Init +import kotlinx.coroutines.cancelChildren + +@OptIn(ExperimentalFoundationApi::class) +@SuppressLint("UnusedBoxWithConstraintsScope") +@Composable +fun ScratchCardPagerComposable( + scratchCardVM: ScratchCardVM = hiltViewModel(), + bundle: Bundle = Bundle(), + screenContent: GratificationResponse? = null, + personalisedOffersProvider: (() -> ScreenDefinition?)? = null, + context: Context = LocalContext.current, + backHandling: ((data: ScratchCardBackResponse?) -> Unit)? = null, +) { + AlfredManager.setBottomSheetView(LocalView.current.rootView) + val isAssuredReward = + screenContent + ?.screenDefinition + ?.screenStructure + ?.content + ?.widgets + ?.filter { it.widgetName == ASSURED_REWARD } + .orEmpty() + .isNotEmpty() + + val (uiState, eventHandlers) = + setupScratchCardState(scratchCardVM, screenContent, bundle, context, backHandling) + BoxWithConstraints( + modifier = + Modifier.fillMaxSize() + .onGloballyPositioned { eventHandlers.onScreenSizeChange } + .background(Color(0xFF000000).copy(alpha = 0.9f)) + ) { + if (uiState.showConfetti) { + ScratchCardConfetti( + screenContent = screenContent, + scratchCardVM = scratchCardVM, + onAnimationEnd = eventHandlers.onConfettiEnd, + ) + } + + screenContent?.screenDefinition?.let { screenDefinition -> + Scaffold( + modifier = Modifier.fillMaxSize(), + backgroundColor = Color.Transparent, + topBar = {}, + content = { paddingValues -> + Column( + modifier = + Modifier.fillMaxWidth() + .fillMaxHeight() + .padding(end = paddingValues.calculateBottomPadding()) + .background(Color.Transparent) + .graphicsLayer { + if (!isAssuredReward) translationY = uiState.translateYState + }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CompositionLocalProvider( + LocalDensity provides + Density( + LocalDensity.current.density, + 1f, // - we set here default font scale instead of system one + ), + LocalOverscrollConfiguration provides null, + ) { + RewardCardsPager( + scratchCardVM = scratchCardVM, + screenDefinition = screenDefinition, + personalisedOffers = personalisedOffersProvider, + triggerAutoScroll = uiState.triggerAutoScroll, + ) + } + } + }, + bottomBar = { + WidgetRenderer( + widget = screenDefinition.screenStructure?.footer, + viewModel = scratchCardVM, + ) + }, + ) + + WidgetRenderer( + widget = screenDefinition.screenStructure?.header, + viewModel = scratchCardVM, + ) + + ScratchCardScreenHeader( + context = context, + backCtaAction = scratchCardVM.getSystemBackCtaAction(), + scratchCardBackResponse = uiState.scratchCardScratched, + backHandling = backHandling, + ) + + RenderLottieWidgets( + widgets = screenDefinition.screenStructure?.content?.widgets, + scratchCardVM = scratchCardVM, + screenSize = uiState.screenSize, + ) + } + } +} + +@Composable +private fun setupScratchCardState( + scratchCardVM: ScratchCardVM, + screenContent: GratificationResponse?, + bundle: Bundle, + context: Context, + backHandling: ((data: ScratchCardBackResponse?) -> Unit)?, +): Pair { + + val scratchCardScratched by scratchCardVM.scratchCardBackResponse.collectAsStateWithLifecycle() + var showConfetti by remember { mutableStateOf(false) } + var triggerAutoScroll by remember { mutableStateOf(false) } + var screenSize by remember { mutableStateOf(null) } + var absoluteTranslationY by remember { mutableFloatStateOf(2000f) } + val translateYState by + animateFloatAsState( + targetValue = absoluteTranslationY, + animationSpec = + spring(dampingRatio = 0.65f, stiffness = 130f, visibilityThreshold = 0.1f), + label = "", + ) + + DisposableEffect(Unit) { + onDispose { + screenContent?.screenDefinition?.screenStructure?.content?.widgets?.forEach { + scratchCardVM.handleActions(it.widgetRenderActions?.onDisposeAction) + } + scratchCardVM.viewModelScope.coroutineContext.cancelChildren() + } + } + + Init( + screenName = SCRATCH_CARD_GRATIFICATION_SCREEN, + activity = context as Activity, + viewModel = scratchCardVM, + ) + + BackHandler { + absoluteTranslationY = 2500f + handleScratchCardBackPress( + context, + scratchCardVM.getSystemBackCtaAction(), + scratchCardScratched, + backHandling, + ) + } + + ActionHandler( + scratchCardVM = scratchCardVM, + requestId = scratchCardVM.getRequestId(), + bundle = bundle, + activity = context, + showConfetti = { + screenContent?.scratchCardResponse?.apply { + showConfetti = this.amount != null && this.amount > 0 + } + }, + onRewardDisbursement = { + screenContent?.scratchCardResponse?.apply { + val backResponse = + if (this.amount != null && this.amount > 0) { + ScratchCardBackResponse.Success( + ScratchCardBackData( + reward = amount.toString(), + verticalName = rewardType, + statusTag = status, + ) + ) + } else ScratchCardBackResponse.NoReward + scratchCardVM.setScratchCardBackResponse(backResponse) + } + }, + onCoinTransferComplete = { triggerAutoScroll = true }, + backHandling = backHandling, + ) + + LaunchedEffect(Unit) { + absoluteTranslationY = 0f + screenContent?.scratchCardResponse?.rewardRefId?.let { scratchCardVM.setRequestId(it) } + scratchCardVM.setScratchCardBackResponse(ScratchCardBackResponse.NotOpened) + scratchCardVM.getGratificationDataFromIntent(bundle) + scratchCardVM.setSystemBackCtaAction( + screenContent?.screenDefinition?.screenStructure?.systemBackCta + ) + } + + return Pair( + ScratchCardUiState( + scratchCardScratched = scratchCardScratched, + showConfetti = showConfetti, + screenSize = screenSize, + triggerAutoScroll = triggerAutoScroll, + translateYState = translateYState, + ), + ScratchCardEventHandlers( + onScreenSizeChange = { screenSize = Size(it.width, it.height) }, + onConfettiEnd = { showConfetti = false }, + ), + ) +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardScreen.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardScreen.kt new file mode 100644 index 0000000000..01d7769515 --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/ui/compose/ScratchCardScreen.kt @@ -0,0 +1,72 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.ui.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.navi.common.forge.model.ScreenDefinition +import com.navi.rr.scratchcard.helper.ScratchCardScreenResolver +import com.navi.rr.scratchcard.model.GratificationResponse +import com.navi.rr.scratchcard.model.ScratchCardBackResponse +import com.navi.rr.scratchcard.model.ScratchCardModel +import com.navi.rr.scratchcard.model.ScratchCardResponse +import com.navi.rr.scratchcard.model.states.ScratchCardScreenVariant +import com.navi.rr.utils.datasource.FixedScratchCardDataSource + +@Composable +fun ScratchCardScreen( + playerId: String?, + screenDefinition: ScreenDefinition?, + scratchCardResponse: ScratchCardResponse?, + gratificationResponse: GratificationResponse, + playerCardDataProvider: () -> ScratchCardModel?, + personalisedOffersProvider: () -> ScreenDefinition?, + screenName: String, + previousBalance: Int? = null, + onRewardsPopUpClosed: (data: ScratchCardBackResponse?) -> Unit, +) { + val resolver = remember { ScratchCardScreenResolver() } + + when ( + val variant = + resolver.resolve( + playerId, + screenDefinition, + scratchCardResponse, + personalisedOffersProvider, + gratificationResponse, + previousBalance, + ) + ) { + is ScratchCardScreenVariant.Deck -> { + ScratchCardDeck( + screenDefinition = variant.screenDefinition, + previousBalance = variant.previousBalance, + screenName = screenName, + dataSource = + FixedScratchCardDataSource(scratchCard = variant.scratchCardModel) { + playerCardDataProvider() + }, + sendBackResponse = onRewardsPopUpClosed, + onBackPress = {}, + ) + } + + is ScratchCardScreenVariant.Pager -> { + ScratchCardPagerComposable( + personalisedOffersProvider = variant.personalisedOffers, + backHandling = onRewardsPopUpClosed, + screenContent = variant.gratificationResponse, + ) + } + + null -> { + // Optional fallback composable + } + } +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/scratchcard/utils/ScratchCardPagerUtils.kt b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/utils/ScratchCardPagerUtils.kt new file mode 100644 index 0000000000..89db23cd1a --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/scratchcard/utils/ScratchCardPagerUtils.kt @@ -0,0 +1,40 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.scratchcard.utils + +import kotlin.math.abs + +fun getPageDistanceOffset(page: Int, currentPage: Int, currentPageOffset: Float): Float { + return when (page) { + currentPage -> -currentPageOffset + currentPage + 1 -> 1 - currentPageOffset + currentPage - 1 -> -1 - currentPageOffset + else -> 0f + } +} + +fun calculatePageScaleFactor(pageOffset: Float): Float { + return 0.9f + (1f - 0.9f) * (1f - minOf(1f, abs(pageOffset))) +} + +/* +Center page (current view): When a page is centered (pageOffset = 0), it's displayed at 100% scale +Off-center pages: As pages move away from center, they gradually shrink down to 90% of their original size +Fully off-center: When a page is completely off-center (pageOffset = 1 or -1), it's at the minimum 90% scale + */ + +object PagerConstants { + const val DEFAULT_CARD_WIDTH = 288 + const val DEFAULT_PAGE_SPACING = 10 + const val DEFAULT_AUTO_SCROLL_DELAY = 2000L + + // Metadata keys + const val CARD_WIDTH = "card_width" + const val PAGE_SPACING = "page_spacing" + const val AUTO_SCROLL_DELAY = "auto_scroll_delay" +}