From 47c60844dd3f34406ddca8fbebf2275be1d82aaa Mon Sep 17 00:00:00 2001 From: Venkat Praneeth Reddy Date: Wed, 18 Jun 2025 17:08:28 +0530 Subject: [PATCH] NTP-65804 | add in app rating pop in coin redemption screen (#16628) --- .../ui/compose/screen/CoinHistoryScreen.kt | 14 +++- .../ui/compose/screen/RewardsShareScreen.kt | 42 +++++++++++ .../com/navi/coin/utils/constant/Constants.kt | 1 + .../FirebaseRemoteConfigHelper.kt | 3 + .../ui/compose/ScratchCardComposable.kt | 36 ++++++++- .../java/com/navi/rr/utils/AppRatingHelper.kt | 74 +++++++++++++++++++ 6 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 android/navi-rr/src/main/java/com/navi/rr/utils/AppRatingHelper.kt diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHistoryScreen.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHistoryScreen.kt index 23d489c980..88d8f40756 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHistoryScreen.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHistoryScreen.kt @@ -137,6 +137,11 @@ fun CoinHistoryScreen( ) } + // Track if userData came from bundle (for app rating popup logic) + var isUserFromCoinRedemption by remember { + mutableStateOf(bundle.getStringSafely(USER_DATA) != null) + } + var isLeftToRightTransition by remember { val isLeftToRightTransition by bundle.bundleDelegate(defaultValue = true) mutableStateOf(isLeftToRightTransition) @@ -401,7 +406,14 @@ fun CoinHistoryScreen( } userData?.let { it -> if (!isReferralShareScreen) { - RewardsShareScreen(navigator = navigator, userData = it) { userData = null } + RewardsShareScreen( + navigator = navigator, + userData = it, + isUserFromCoinRedemption = isUserFromCoinRedemption, + ) { + userData = null + isUserFromCoinRedemption = false + } } else { ReferralShareScreen( navigator = navigator, diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsShareScreen.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsShareScreen.kt index 5d4feb28bf..bbb3bdf7ca 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsShareScreen.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsShareScreen.kt @@ -74,8 +74,12 @@ import com.navi.coin.utils.constant.ImageConstants.SHAREABILITY_LEFT_POLYGON_URL import com.navi.coin.utils.constant.ImageConstants.SHAREABILITY_RIGHT_POLYGON_URL import com.navi.coin.vm.RewardsShareScreenVm import com.navi.common.R +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.APP_RATING_POP_UP_DELAY_INTERVAL import com.navi.common.ui.compose.GratificationLottieAnimation import com.navi.common.uitron.model.action.CtaAction +import com.navi.common.utils.PlayStoreInAppRatingHelper +import com.navi.common.utils.log import com.navi.design.font.FontWeightEnum import com.navi.design.font.getFontWeight import com.navi.design.font.naviFontFamily @@ -83,6 +87,7 @@ import com.navi.design.theme.FF1F002A import com.navi.design.theme.WhiteFFFFFF import com.navi.naviwidgets.R as WidgetR import com.navi.rr.referral.models.ReferralContactList +import com.navi.rr.utils.RewardsAppRatingHelper import com.navi.rr.utils.capturable import com.navi.rr.utils.ext.clickable import com.navi.rr.utils.ext.hideGenericShareLoader @@ -94,6 +99,7 @@ import com.navi.rr.utils.handleContactClick import com.navi.rr.utils.rememberCaptureController import com.navi.uitron.utils.transformations.moneyFormat import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @OptIn(ExperimentalComposeApi::class) @@ -102,6 +108,7 @@ fun RewardsShareScreen( userData: ReferralContactList? = null, navigator: DestinationsNavigator, rewardsShareScreenVm: RewardsShareScreenVm = hiltViewModel(), + isUserFromCoinRedemption: Boolean = false, onClose: () -> Unit, ) { val context = LocalContext.current as CoinActivity @@ -113,6 +120,13 @@ fun RewardsShareScreen( val coroutineScope = rememberCoroutineScope() var absoluteTranslationY by remember { mutableFloatStateOf(2000f) } + var showAppRatingPopUp by remember { mutableStateOf(false) } + + // show app rating pop up only when share screen is triggered post redemption + val shouldShowAppRatingPopUp = remember { + isUserFromCoinRedemption && RewardsAppRatingHelper.shouldShowAppRatingPopup() + } + Init( screenName = Constants.SCREENS.REWARDS_SHARE_SCREEN_SCREEN_NAME, activity = context, @@ -120,6 +134,15 @@ fun RewardsShareScreen( navigator = navigator, ) + val playStoreRatingHelper = + remember(shouldShowAppRatingPopUp) { + if (shouldShowAppRatingPopUp) { + PlayStoreInAppRatingHelper(context, "rewards_share_screen") + } else { + null + } + } + BackHandler { absoluteTranslationY = 2000f } val translateYState by animateFloatAsState( @@ -144,6 +167,24 @@ fun RewardsShareScreen( LaunchedEffect(Unit) { rewardsShareScreenVm.inflateData(userData) { message = it } } + // delay is to ensure playStoreRatingHelper initialisation completes + LaunchedEffect(showAppRatingPopUp) { + if (shouldShowAppRatingPopUp && showAppRatingPopUp) { + delay( + FirebaseRemoteConfigHelper.getLong( + APP_RATING_POP_UP_DELAY_INTERVAL, + defaultValue = 2000, + ) + ) + RewardsAppRatingHelper.updateLastShownTime() + try { + playStoreRatingHelper?.inAppRating(context) + } catch (e: Exception) { + e.log() + } + } + } + LaunchedEffect(Unit) { rewardsShareScreenVm.screenActions.collect { action -> when (action) { @@ -686,6 +727,7 @@ fun RewardsShareScreen( modifier = Modifier.fillMaxSize(), isRemoteLottie = false, showLottieInfiniteTimes = false, + onAnimationStart = { showAppRatingPopUp = true }, lottie = R.raw.gratifaction_confetti, ) diff --git a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt index 1f17645969..4acf11e194 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt @@ -75,6 +75,7 @@ object Constants { const val METADATA_SUBTITLE = "metadata.subtitle" const val SCRATCH_CARD_EXPERIENCE = "SCRATCH_CARD_EXPERIENCE" const val DRIP_REWARD_VALUE = "dripRewardValue" + const val APP_RATING_LAST_SHOWN_TIME = "APP_RATING_LAST_SHOWN_TIME" const val SCROLL_THRESHOLD_FOR_STATUS_BAR_COLOR_CHANGE = "SCROLL_THRESHOLD_FOR_STATUS_BAR_COLOR_CHANGE" const val SCROLL_THRESHOLD_FOR_STATUS_BAR_COLOR_CHANGE_IN_DP = 120 diff --git a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt index 02c721adac..e924e8302a 100644 --- a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt +++ b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt @@ -289,6 +289,9 @@ object FirebaseRemoteConfigHelper { const val OKHTTP_CUSTOM_DNS_EXPERIMENT_V2 = "OKHTTP_CUSTOM_DNS_EXPERIMENT_V2" const val OKHTTP_RETRY_EXPERIMENT = "OKHTTP_RETRY_EXPERIMENT" const val OKHTTP_CUSTOM_DNS_VALUE = "OKHTTP_CUSTOM_DNS_VALUE" + const val APP_RATING_DAYS_INTERVAL = "APP_RATING_DAYS_INTERVAL" + const val MIN_REWARD_AMOUNT_FOR_APP_RATING = "MIN_REWARD_AMOUNT_FOR_APP_RATING" + const val APP_RATING_POP_UP_DELAY_INTERVAL = "APP_RATING_POP_UP_DELAY_INTERVAL" // Events for firebase config private const val FIREBASE_REMOTE_CONFIG_FETCH_SUCCESS = 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 02fdb70890..25132102f5 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 @@ -51,6 +51,8 @@ import com.navi.common.R import com.navi.common.uitron.model.action.ApiType import com.navi.common.uitron.model.action.CtaAction import com.navi.common.utils.Constants +import com.navi.common.utils.PlayStoreInAppRatingHelper +import com.navi.common.utils.log import com.navi.rr.common.constants.SCRATCH_CARD_GRATIFICATION_SCREEN import com.navi.rr.common.views.NaviRRLottieAnimationWithTimeout import com.navi.rr.common.widgetFactory.WidgetRenderer @@ -63,6 +65,7 @@ import com.navi.rr.scratchcard.utils.ScratchCardTheme.ThemeConfig.Companion.SCRA 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.RewardsAppRatingHelper 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 @@ -104,6 +107,23 @@ fun ScratchCardComposable( .orEmpty() .isNotEmpty() + var showAppRatingPopUp by remember { mutableStateOf(false) } + + val rewardAmount = screenContent?.scratchCardResponse?.amount + val shouldShowAppRatingPopup = + remember(rewardAmount) { + RewardsAppRatingHelper.shouldShowAppRatingPopupForReward(rewardAmount) + } + + val playStoreRatingHelper = + remember(shouldShowAppRatingPopup) { + if (shouldShowAppRatingPopup) { + PlayStoreInAppRatingHelper(context, "scratch_card_composable") + } else { + null + } + } + screenContent?.scratchCardResponse?.rewardRefId?.let { scratchCardVM.setRequestId(it) } scratchCardVM.setScratchCardBackResponse(ScratchCardBackResponse.NotOpened) @@ -171,6 +191,17 @@ fun ScratchCardComposable( ) } + LaunchedEffect(showAppRatingPopUp) { + if (shouldShowAppRatingPopup && showAppRatingPopUp) { + RewardsAppRatingHelper.updateLastShownTime() + try { + playStoreRatingHelper?.inAppRating(context) + } catch (e: Exception) { + e.log() + } + } + } + BoxWithConstraints( modifier = Modifier.fillMaxSize() @@ -193,7 +224,10 @@ fun ScratchCardComposable( showLottieInfiniteTimes = false, lottieUrl = lottieUrl, lottie = getLottieForScratchCard(themeValue = theme), - onAnimationEnd = { showConfetti = false }, + onAnimationEnd = { + showConfetti = false + showAppRatingPopUp = true + }, placeHolder = R.raw.gratifaction_confetti, ) } diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/AppRatingHelper.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/AppRatingHelper.kt new file mode 100644 index 0000000000..2ba2efcbbe --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/AppRatingHelper.kt @@ -0,0 +1,74 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.utils + +import com.navi.base.sharedpref.PreferenceManager +import com.navi.base.utils.TrustedTimeAccessor +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.APP_RATING_DAYS_INTERVAL +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.MIN_REWARD_AMOUNT_FOR_APP_RATING + +object RewardsAppRatingHelper { + + private const val ONE_DAY_IN_MILLI_SECONDS = 24 * 60 * 60 * 1000L + private const val APP_RATING_LAST_SHOWN_TIME = "APP_RATING_LAST_SHOWN_TIME" + + /** + * Determines whether the app rating popup should be shown based on: + * 1. Time elapsed since last shown + * 2. Firebase remote config threshold + * + * @return true if popup should be shown, false otherwise + */ + fun shouldShowAppRatingPopup(): Boolean { + val lastUpdatedTime = + PreferenceManager.getLongPreference(APP_RATING_LAST_SHOWN_TIME, defValue = 0L) + val currentTimeInMillis = TrustedTimeAccessor.getCurrentTimeMillis() + val daysThreshold = + FirebaseRemoteConfigHelper.getLong(APP_RATING_DAYS_INTERVAL, defaultValue = 90) + + if (daysThreshold < 0) { + return false + } + + val timeDifferenceInMillis = currentTimeInMillis - lastUpdatedTime + val daysSinceLastShown = timeDifferenceInMillis / ONE_DAY_IN_MILLI_SECONDS + + return when { + daysSinceLastShown >= daysThreshold -> true + else -> false + } + } + + /** + * Determines whether the app rating popup should be shown for reward-based scenarios. Checks + * both time-based conditions and reward amount threshold. + * + * @param rewardAmount The reward amount to validate against minimum threshold + * @return true if popup should be shown, false otherwise + */ + fun shouldShowAppRatingPopupForReward(rewardAmount: Int?): Boolean { + if (rewardAmount == null || rewardAmount <= 0) { + return false + } + + val minRewardAmount = + FirebaseRemoteConfigHelper.getLong(MIN_REWARD_AMOUNT_FOR_APP_RATING, defaultValue = 100) + + return (rewardAmount >= minRewardAmount) && shouldShowAppRatingPopup() + } + + /** + * Updates the last shown time to current time. Call this when the app rating popup is actually + * displayed. + */ + fun updateLastShownTime() { + val currentTimeInMillis = TrustedTimeAccessor.getCurrentTimeMillis() + PreferenceManager.setLongPreference(APP_RATING_LAST_SHOWN_TIME, currentTimeInMillis) + } +}