TP-81333: Add HP sections impression event instrumentation (#12642)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.common.handler
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import com.navi.base.utils.orFalse
|
||||
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.HOME_SCREEN_IMPRESSION_THRESHOLD
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.navi.uitron.model.data.UiTronActionData
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomePageSectionImpressionTracker @Inject constructor() {
|
||||
|
||||
private var bottomOverlayBounds: Rect = Rect.Zero
|
||||
private val sectionImpressionStatesMap = mutableMapOf<String, Boolean>()
|
||||
private var impressionThreshold: Float = FLOAT_ZERO
|
||||
|
||||
companion object {
|
||||
private const val FLOAT_ZERO = 0F
|
||||
}
|
||||
|
||||
init {
|
||||
impressionThreshold =
|
||||
FirebaseRemoteConfigHelper.getDouble(HOME_SCREEN_IMPRESSION_THRESHOLD).toFloat()
|
||||
}
|
||||
|
||||
fun updateSectionImpressionState(
|
||||
widget: AlchemistWidgetModelDefinition<UiTronResponse>,
|
||||
coordinates: LayoutCoordinates,
|
||||
onImpression: (UiTronActionData?) -> Unit
|
||||
) {
|
||||
val widgetId = widget.widgetId.orEmpty()
|
||||
if (isImpressionRecorded(widgetId)) return
|
||||
|
||||
if (isSectionHeightAboveThreshold(coordinates)) {
|
||||
onImpression(widget.widgetRenderActions?.onImpressionAction)
|
||||
setImpressionState(widgetId, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSectionHeightAboveThreshold(coordinates: LayoutCoordinates): Boolean {
|
||||
val sectionBounds = coordinates.boundsInWindow()
|
||||
val heightUnderBottomOverlay = calculateHeightUnderOverlay(sectionBounds)
|
||||
val maxSectionHeight = coordinates.size.height
|
||||
val visibleSectionHeight = sectionBounds.height
|
||||
|
||||
return visibleSectionHeight - heightUnderBottomOverlay >=
|
||||
impressionThreshold * maxSectionHeight
|
||||
}
|
||||
|
||||
private fun calculateHeightUnderOverlay(sectionBounds: Rect): Float {
|
||||
return if (bottomOverlayBounds == Rect.Zero) FLOAT_ZERO
|
||||
else maxOf(FLOAT_ZERO, sectionBounds.bottom - bottomOverlayBounds.top)
|
||||
}
|
||||
|
||||
private fun setImpressionState(widgetId: String, isVisible: Boolean) {
|
||||
sectionImpressionStatesMap[widgetId] = isVisible
|
||||
}
|
||||
|
||||
private fun isImpressionRecorded(widgetId: String): Boolean {
|
||||
return sectionImpressionStatesMap[widgetId].orFalse()
|
||||
}
|
||||
|
||||
fun updateBottomOverlayBounds(newRect: Rect) {
|
||||
bottomOverlayBounds = newRect
|
||||
}
|
||||
|
||||
fun resetImpressionStates() {
|
||||
sectionImpressionStatesMap.keys.forEach { widgetId -> setImpressionState(widgetId, false) }
|
||||
}
|
||||
|
||||
fun getItemImpressionThreshold(): Float {
|
||||
return impressionThreshold
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
@@ -47,10 +48,12 @@ import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
|
||||
import com.navi.common.alchemist.model.WidgetRenderState
|
||||
import com.navi.naviwidgets.R
|
||||
import com.navi.pay.utils.conditional
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.naviapp.home.reducer.HpEffects
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
|
||||
@@ -60,6 +63,7 @@ fun FrontLayerContent(
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
renderingFirstTime: Boolean,
|
||||
homeScrollState: () -> ScrollState,
|
||||
homeVM: () -> HomeViewModel,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
onEffect: (effect: HpEffects) -> Unit
|
||||
) {
|
||||
@@ -77,6 +81,7 @@ fun FrontLayerContent(
|
||||
renderingFirstTime = renderingFirstTime,
|
||||
elementList = widgets,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
onEffect = onEffect
|
||||
)
|
||||
@@ -106,10 +111,25 @@ private fun ContentLoaderLottie(renderingFirstTime: Boolean) {
|
||||
@Composable
|
||||
private fun AnimatedContainerForWidgets(
|
||||
isVisible: MutableState<Boolean>,
|
||||
widget: AlchemistWidgetModelDefinition<UiTronResponse>,
|
||||
homeVM: HomeViewModel,
|
||||
homeWidget: @Composable () -> Unit
|
||||
) {
|
||||
val setImpressionTracker =
|
||||
widget.widgetRenderActions?.onImpressionAction?.actions.isNullOrEmpty().not()
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
modifier =
|
||||
Modifier.conditional(setImpressionTracker) {
|
||||
onGloballyPositioned { layoutCoordinates ->
|
||||
homeVM.sectionVisibilityTracker.updateSectionImpressionState(
|
||||
widget,
|
||||
layoutCoordinates
|
||||
) { onImpressionAction ->
|
||||
homeVM.handleActions(onImpressionAction)
|
||||
}
|
||||
}
|
||||
},
|
||||
enter =
|
||||
expandVertically(
|
||||
expandFrom = Alignment.Top,
|
||||
@@ -136,6 +156,7 @@ private fun RenderUiTronContent(
|
||||
renderingFirstTime: Boolean,
|
||||
elementList: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
homeVM: () -> HomeViewModel,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
onEffect: (effect: HpEffects) -> Unit
|
||||
) {
|
||||
@@ -166,7 +187,9 @@ private fun RenderUiTronContent(
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedContainerForWidgets(visible) { homeWidgetRenderer(element.widgetData) }
|
||||
AnimatedContainerForWidgets(visible, element, homeVM()) {
|
||||
homeWidgetRenderer(element.widgetData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
@@ -23,8 +24,10 @@ import com.naviapp.dashboard.viewmodels.DashboardSharedVM
|
||||
import com.naviapp.home.compose.home.ui.footer.utils.HomeFooterEvents
|
||||
import com.naviapp.home.compose.home.ui.footer.utils.HomeFooterStates
|
||||
import com.naviapp.home.compose.home.ui.footer.utils.handleHomeFooterEvent
|
||||
import com.naviapp.home.compose.home.utils.updateBottomOverlayBounds
|
||||
import com.naviapp.home.model.BottomBarTabType
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
import com.naviapp.home.viewmodel.SharedVM
|
||||
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
|
||||
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
|
||||
@@ -39,6 +42,7 @@ fun HomeFooterRoot(
|
||||
dashboardSharedVM: DashboardSharedVM,
|
||||
bottomNavBarVM: BottomNavBarVM,
|
||||
sharedVM: SharedVM,
|
||||
homeViewModel: () -> HomeViewModel,
|
||||
navController: NavHostController,
|
||||
selectedTabId: String,
|
||||
nudgeState: () -> NudgeState,
|
||||
@@ -52,7 +56,10 @@ fun HomeFooterRoot(
|
||||
val isHomeTabSelected = remember(selectedTabId) { selectedTabId == BottomBarTabType.HOME.name }
|
||||
|
||||
HomeFooter(
|
||||
modifier = modifier,
|
||||
modifier =
|
||||
modifier.onGloballyPositioned { layoutCoordinates ->
|
||||
updateBottomOverlayBounds(homeViewModel(), layoutCoordinates)
|
||||
},
|
||||
selectedTabId = selectedTabId,
|
||||
bottomNudgeData = bottomStickyNudgeData,
|
||||
isHomeTabSelected = isHomeTabSelected,
|
||||
|
||||
@@ -131,6 +131,7 @@ fun HomeContentFrame(
|
||||
bottomNavBarVM = bottomNavBarVM,
|
||||
dashboardSharedVM = dashboardSharedVM,
|
||||
sharedVM = sharedVM,
|
||||
homeViewModel = homeVM,
|
||||
navController = navController,
|
||||
selectedTabId = selectedTabId,
|
||||
onHomeEvent = onHomeScreenEvent,
|
||||
|
||||
@@ -95,6 +95,7 @@ fun HomeScreenScaffoldRoot(
|
||||
widgets = (hpStates().screenDefinition)?.screenStructure?.content?.widgets,
|
||||
frontLayerShape = frontLayerShape,
|
||||
homeScrollState = homeScrollState,
|
||||
homeVM = homeVM,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
onEffect = { homeVM().setEffect { it } },
|
||||
onEvent = { homeVM().sendEvent(it) },
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.compose.material.BackdropScaffoldState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -67,3 +69,13 @@ private fun normalizeOffset(offset: Float, minOffset: Float, maxOffset: Float):
|
||||
else -> roundedOffset
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBottomOverlayBounds(homeVM: HomeViewModel, layoutCoordinates: LayoutCoordinates) {
|
||||
homeVM.sectionVisibilityTracker.updateBottomOverlayBounds(layoutCoordinates.boundsInWindow())
|
||||
homeVM.viewImpressionTracker.updateBottomOverlayBounds(layoutCoordinates.boundsInWindow())
|
||||
}
|
||||
|
||||
fun resetImpressionStates(homeVM: HomeViewModel) {
|
||||
homeVM.sectionVisibilityTracker.resetImpressionStates()
|
||||
homeVM.viewImpressionTracker.resetImpressionStates()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ fun InitLifecycleListener(homeVM: () -> HomeViewModel, activity: HomePageActivit
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_CREATE -> {
|
||||
val threshold = homeVM().sectionVisibilityTracker.getItemImpressionThreshold()
|
||||
homeVM().viewImpressionTracker.setViewImpressionThreshold(threshold)
|
||||
AlfredManager.setCurrentScreenName(screenName = activity.screenName)
|
||||
homeVM()
|
||||
.observeUPIVpa(
|
||||
@@ -50,6 +52,7 @@ fun InitLifecycleListener(homeVM: () -> HomeViewModel, activity: HomePageActivit
|
||||
homeVM().setEffect { HpEffects.OnRenderActions }
|
||||
homeVM().setEffect { HpEffects.FetchHomeApi }
|
||||
}
|
||||
resetImpressionStates(homeVM())
|
||||
homeVM().setHomeApiRefreshFlag(true)
|
||||
homeVM().handleUpiAdaptations()
|
||||
AlfredManager.setCurrentScreenName(screenName = activity.screenName)
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.navi.uitron.model.data.UiTronActionData
|
||||
import com.naviapp.BuildConfig
|
||||
import com.naviapp.analytics.utils.NaviAnalytics
|
||||
import com.naviapp.common.navigator.NaviDeepLinkNavigator
|
||||
import com.naviapp.home.common.handler.HomePageSectionImpressionTracker
|
||||
import com.naviapp.home.compose.activity.HomePageActivity
|
||||
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
|
||||
import com.naviapp.home.reducer.HomeReducer
|
||||
@@ -78,6 +79,7 @@ constructor(
|
||||
private val upiUseCase: HandleUpiUseCase,
|
||||
val rotatingViewHelper: Lazy<RotatingViewHelper>,
|
||||
val videoViewHelper: Lazy<VideoViewHelper>,
|
||||
val sectionVisibilityTracker: HomePageSectionImpressionTracker,
|
||||
) :
|
||||
BaseMviViewModel<HpStates, HpEvents, HpEffects>(
|
||||
initialState = HpStates(),
|
||||
|
||||
@@ -101,7 +101,7 @@ navi-adverse = "1.3.0-20240917.145607-3"
|
||||
navi-alfred = "1.15.1"
|
||||
navi-guarddog = "3.5.0"
|
||||
navi-pulse = "1.7.0"
|
||||
navi-uitron = "1.23.0"
|
||||
navi-uitron = "1.23.0-20240924.072418-2"
|
||||
navigation = "2.5.3"
|
||||
okhttp-bom = "4.12.0"
|
||||
otaliastudios-cameraview = "2.7.2"
|
||||
|
||||
@@ -62,7 +62,9 @@ data class AlchemistRenderActions(
|
||||
null, // These actions will trigger only upon screen rendering after a successful API call,
|
||||
// not when rendering occurs by consuming data from db
|
||||
val actionsCorrespondingToKey: List<AlchemistActionsCorrespondingToKey?>? =
|
||||
null // This contains a list of actions corresponding to specific app events
|
||||
null, // This contains a list of actions corresponding to specific app events
|
||||
val onImpressionAction: UiTronActionData? =
|
||||
null, // These actions will trigger on actual view of the widget on the screen
|
||||
)
|
||||
|
||||
data class AlchemistActionsCorrespondingToKey(
|
||||
|
||||
@@ -22,6 +22,7 @@ object FirebaseRemoteConfigHelper {
|
||||
private const val CONFIG_SYNC_INTERVAL: Long = 60 * 60
|
||||
const val FRONT_LAYER_UPPER_CONTENT_LENGTH = "FRONT_LAYER_UPPER_CONTENT_LENGTH"
|
||||
const val SEND_SCREEN_HASH_IN_HOME_SCREEN_API = "SEND_SCREEN_HASH_IN_HOME_SCREEN_API"
|
||||
const val HOME_SCREEN_IMPRESSION_THRESHOLD = "HOME_SCREEN_IMPRESSION_THRESHOLD"
|
||||
const val RAGE_TAP_COUNT = "RAGE_TAP_COUNT"
|
||||
const val RAGE_TAP_DELAY_TIME = "RAGE_TAP_DELAY_TIME"
|
||||
const val GST_USERNAME_PASSWORD_MAX_LIMIT = "GST_USERNAME_PASSWORD_MAX_LIMIT"
|
||||
|
||||
@@ -602,4 +602,8 @@
|
||||
<key>NAVI_PAY_SEND_MONEY_FTUE_LIMIT</key>
|
||||
<value>3</value>
|
||||
</entry>
|
||||
<entry>
|
||||
<key>HOME_SCREEN_IMPRESSION_THRESHOLD</key>
|
||||
<value>1.0</value>
|
||||
</entry>
|
||||
</defaultsMap>
|
||||
Reference in New Issue
Block a user