NTP-43715 | Nudge cache (#15394)

Signed-off-by: Naman Khurmi <naman.khurmi@navi.com>
This commit is contained in:
Naman Khurmi
2025-03-17 12:49:00 +05:30
committed by GitHub
parent af0b91795a
commit a0109d7dbb
22 changed files with 353 additions and 166 deletions

View File

@@ -36,7 +36,7 @@ constructor(
deletedItemsMap: Map<String, Set<String>>,
lastClickedNudgeId: String,
onSuccess:
(
suspend (
nudgeListData: NudgeListData?,
popupListData: PopupListData?,
bottomSheetData: BottomSheetData?,
@@ -62,11 +62,11 @@ constructor(
}
}
private fun handleResponse(
private suspend fun handleResponse(
response: RepoResult<OverlayScreenStructure>,
deletedItemsMap: Map<String, Set<String>>,
onSuccess:
(
suspend (
nudgeListData: NudgeListData?,
popupListData: PopupListData?,
bottomSheetData: BottomSheetData?,
@@ -75,15 +75,15 @@ constructor(
) {
response.data?.screenOverlayData?.let { screenOverlayData ->
val updatedNudgeList =
screenOverlayData.nudgeListData?.nudgeList?.filterNot { nudge ->
deletedItemsMap[NUDGE]?.contains(nudge.nudgeId) == true
screenOverlayData.nudgeListData?.nudges?.filterNot { nudge ->
deletedItemsMap[NUDGE]?.contains(nudge.id) == true
}
val updatedPopupList =
screenOverlayData.popupListData?.popupList?.filterNot { popup ->
deletedItemsMap[POPUP]?.contains(popup.popupId) == true
}
onSuccess(
screenOverlayData.nudgeListData?.copy(nudgeList = updatedNudgeList),
screenOverlayData.nudgeListData?.copy(nudges = updatedNudgeList),
screenOverlayData.popupListData?.copy(popupList = updatedPopupList),
screenOverlayData.bottomSheetData,
screenOverlayData.staticNudgeData,
@@ -91,7 +91,7 @@ constructor(
collectRequestPopupExists =
updatedPopupList?.any { it.popupType == COLLECT_REQUEST } ?: false
analyticsEventTracker.screenOverlayApi(
updatedNudgeList?.map { it.nudgeId + Constants.COMMA + it.nudgeStatus },
updatedNudgeList?.map { it.id + Constants.COMMA + it.status },
updatedPopupList?.mapNotNull { it.popupId },
screenOverlayData.bottomSheetData?.bottomSheetId,
)

View File

@@ -0,0 +1,66 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.domain.model.data
import androidx.compose.runtime.Stable
import com.google.gson.annotations.SerializedName
import com.navi.uitron.model.UiTronResponse
typealias NudgeId = String
@Stable data class NudgeListData(@SerializedName("contentList") val nudges: List<Nudge>? = null)
@Stable
data class Nudge(
@SerializedName("nudge_id") val id: NudgeId? = null,
@SerializedName("nudge_icon_url") val iconUrl: String? = null,
@SerializedName("nudge_data") val data: NudgeStateData? = null,
@SerializedName("nudge_status") val status: NudgeStatus = NudgeStatus.DEFAULT,
@SerializedName("nudge_drag_enabled") val isDraggable: Boolean? = true,
@SerializedName("nudge_dismissible_actions")
val dismissibleActions: List<NudgeDismissibleAction>? = null,
)
@Stable
data class NudgeDismissibleAction(
val text: String? = null,
val illustration: String? = null,
val backgroundColor: String? = null,
val textColor: String? = null,
val dismissAction: String? = null,
)
@Stable
data class NudgeDraggedStateData(
val state: NudgeDraggedState,
val type: NudgeDraggedStateUpdateType,
)
@Stable
data class NudgeStateData(
@SerializedName("nudge_success_state") val successState: UiTronResponse? = null,
@SerializedName("nudge_in_progress_state") val inProgressState: UiTronResponse? = null,
@SerializedName("nudge_default_state") val defaultState: UiTronResponse? = null,
)
enum class NudgeStatus {
IN_PROGRESS,
SUCCESS,
DEFAULT,
DISMISSED,
}
enum class NudgeDraggedState {
IDLE,
DRAGGED,
}
enum class NudgeDraggedStateUpdateType {
DRAG,
TOGGLE,
}

View File

@@ -1,60 +0,0 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.domain.model.data
import androidx.compose.runtime.Stable
import com.google.gson.annotations.SerializedName
import com.navi.uitron.model.UiTronResponse
typealias NudgeId = String
@Stable
data class NudgeListData(@SerializedName("contentList") val nudgeList: List<NudgeData>? = null)
@Stable
data class NudgeData(
@SerializedName("nudgeId") val nudgeId: NudgeId? = null,
@SerializedName("nudgeIconUrl") val nudgeIconUrl: String? = null,
@SerializedName("nudgeData") val nudgeUitronData: UiTronResponse? = null,
@SerializedName("nudgeStatus") val nudgeStatus: NudgeStatus = NudgeStatus.DEFAULT,
@SerializedName("nudgeDragEnabled") val isNudgeDragEnabled: Boolean? = true,
@SerializedName("nudgeDismissibleActions")
val dismissibleActionList: List<NudgeDismissibleActionData>? = null,
)
@Stable
data class NudgeDismissibleActionData(
val actionText: String? = null,
val actionIllustration: String? = null,
val actionBackgroundColor: String? = null,
val actionTextColor: String? = null,
val dismissAction: String? = null,
)
@Stable
data class NudgeDraggedStateData(
val state: NudgeDraggedState,
val type: NudgeDraggedStateUpdateType,
)
enum class NudgeStatus {
IN_PROGRESS,
SUCCESS,
DEFAULT,
DISMISSED,
}
enum class NudgeDraggedState {
IDLE,
DRAGGED,
}
enum class NudgeDraggedStateUpdateType {
DRAG,
TOGGLE,
}

View File

@@ -9,7 +9,7 @@ package com.naviapp.screenOverlay.nudge.domain.model.event
import androidx.compose.runtime.Immutable
import com.navi.common.basemvi.UiEvent
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
@@ -19,7 +19,7 @@ import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeUiState
sealed interface NudgeEvent : UiEvent {
data class ToggleDescriptiveView(val enabled: Boolean) : NudgeEvent
data class UpdateNudgeList(val data: List<NudgeData>?) : NudgeEvent
data class UpdateNudgeList(val data: List<Nudge>?) : NudgeEvent
data class DismissNudge(val id: NudgeId) : NudgeEvent

View File

@@ -9,7 +9,7 @@ package com.naviapp.screenOverlay.nudge.domain.model.state
import androidx.compose.runtime.Immutable
import com.navi.common.basemvi.UiState
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
@@ -18,7 +18,7 @@ import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
data class NudgeState(
val isNudgeListVisible: Boolean,
val descriptiveViewEnabled: Boolean,
val nudgeList: List<NudgeData>?,
val nudgeList: List<Nudge>?,
val staticNudgeData: StaticNudgeData? = null,
val nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData> = emptyMap(),
val selectedDismissibleActionIndexMap: Map<NudgeId, Int> = emptyMap(),

View File

@@ -45,8 +45,8 @@ class NudgeReducer : BaseReducer<NudgeState, NudgeEvent> {
val nudgeDraggedStateMap = mutableMapOf<NudgeId, NudgeDraggedStateData>()
event.data?.forEach { nudgeData ->
nudgeData.nudgeId?.let {
nudgeDraggedStateMap[nudgeData.nudgeId] =
nudgeData.id?.let {
nudgeDraggedStateMap[nudgeData.id] =
NudgeDraggedStateData(
state = NudgeDraggedState.IDLE,
type = NudgeDraggedStateUpdateType.TOGGLE,
@@ -65,8 +65,8 @@ class NudgeReducer : BaseReducer<NudgeState, NudgeEvent> {
previousState.copy(
nudgeList =
previousState.nudgeList?.map { nudge ->
if (nudge.nudgeId == event.id) {
nudge.copy(nudgeStatus = NudgeStatus.DISMISSED)
if (nudge.id == event.id) {
nudge.copy(status = NudgeStatus.DISMISSED)
} else {
nudge
}

View File

@@ -14,7 +14,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStatus
@@ -28,7 +28,7 @@ import com.naviapp.screenOverlay.nudge.utils.modifier.BorderSides
@Composable
fun NudgeContainerCollapsedState(
nudgeList: List<NudgeData>?,
nudgeList: List<Nudge>?,
isDescriptiveViewEnabled: Boolean,
onHeightChange: (Int) -> Unit,
nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData>,
@@ -40,32 +40,31 @@ fun NudgeContainerCollapsedState(
val nudgeData =
remember(nudgeList.getFirstNonDismissedNudge()) { nudgeList.getFirstNonDismissedNudge() }
val showBorder =
remember(nudgeData?.nudgeStatus) { nudgeData?.nudgeStatus != NudgeStatus.SUCCESS }
val showBorder = remember(nudgeData?.status) { nudgeData?.status != NudgeStatus.SUCCESS }
AnimatedContent(
modifier = Modifier.onSizeChanged { onHeightChange(it.height) },
targetState = nudgeData,
contentKey = { it?.nudgeId },
contentKey = { it?.id },
contentAlignment = Alignment.BottomCenter,
transitionSpec = {
nudgeCollapsedStateTransitionSpec(
isDescriptiveViewEnabled = isDescriptiveViewEnabled,
filteredNudgeList = nudgeList.filterDeletedNudges(),
initialNudgeId = this.initialState?.nudgeId.toString(),
initialNudgeId = this.initialState?.id.toString(),
)
},
) { data ->
NudgeUI(
modifier = Modifier,
nudgeUitronRenderer = nudgeUitronRenderer,
nudgeData = data,
nudgeDraggedStateData = { nudgeDraggedStateMap[data?.nudgeId] },
nudge = data,
nudgeDraggedStateData = { nudgeDraggedStateMap[data?.id] },
onNudgeEvent = onNudgeEvent,
elevateFrontLayer = true,
onNudgeEffect = onNudgeEffect,
borderSides = if (showBorder) BorderSides.VERTICAL else BorderSides.NONE,
selectedDismissibleActionIndex = selectedDismissibleActionIndexMap[data?.nudgeId],
selectedDismissibleActionIndex = selectedDismissibleActionIndexMap[data?.id],
)
}
}

View File

@@ -47,7 +47,7 @@ import com.navi.elex.atoms.ElexAsyncImage
import com.navi.elex.atoms.ElexText
import com.navi.elex.font.FontWeightEnum
import com.naviapp.R
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedState
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
@@ -61,18 +61,18 @@ import com.naviapp.screenOverlay.utils.NudgeConstants.UNPAID
@Composable
fun BoxScope.NudgeMiddlePillContainer(
nudgeList: List<NudgeData>,
nudgeList: List<Nudge>,
isDescriptiveViewEnabled: Boolean,
onClick: () -> Unit,
nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData>,
) {
val firstNudge = nudgeList.firstOrNull()
var previousNudgeId by remember { mutableStateOf(firstNudge?.nudgeId) }
var previousNudgeId by remember { mutableStateOf(firstNudge?.id) }
val isFirstNudgeReplaced = firstNudge?.nudgeId != previousNudgeId
val isFirstNudgeReplaced = firstNudge?.id != previousNudgeId
LaunchedEffect(firstNudge?.nudgeId) { previousNudgeId = firstNudge?.nudgeId }
LaunchedEffect(firstNudge?.id) { previousNudgeId = firstNudge?.id }
val visibilityBaseCondition by
remember(nudgeList, isDescriptiveViewEnabled) {
@@ -80,14 +80,12 @@ fun BoxScope.NudgeMiddlePillContainer(
}
val visibilityConditionDueToUiState by
remember(nudgeDraggedStateMap[firstNudge?.nudgeId]?.state) {
derivedStateOf {
nudgeDraggedStateMap[firstNudge?.nudgeId]?.state == NudgeDraggedState.IDLE
}
remember(nudgeDraggedStateMap[firstNudge?.id]?.state) {
derivedStateOf { nudgeDraggedStateMap[firstNudge?.id]?.state == NudgeDraggedState.IDLE }
}
val subsequentTopNudgeIcons =
remember(nudgeList) { nudgeList.drop(1).take(2).mapNotNull { it.nudgeIconUrl } }
remember(nudgeList) { nudgeList.drop(1).take(2).mapNotNull { it.iconUrl } }
val nudgeCount = remember(nudgeList) { (nudgeList.size - 1).coerceAtMost(9) }

View File

@@ -20,7 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStatus
@@ -34,7 +34,7 @@ import com.naviapp.screenOverlay.nudge.utils.modifier.BorderSides
@Composable
fun NudgeContainerExpandedState(
nudgeList: List<NudgeData>?,
nudgeList: List<Nudge>?,
isDescriptiveViewEnabled: Boolean,
singleNudgeHeight: Int,
nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData>,
@@ -55,26 +55,26 @@ fun NudgeContainerExpandedState(
) {
Column {
nudgeList?.forEachIndexed { index, nudgeData ->
key(nudgeData.nudgeId) {
key(nudgeData.id) {
AnimatedVisibility(
visible = nudgeData.nudgeStatus != NudgeStatus.DISMISSED,
visible = nudgeData.status != NudgeStatus.DISMISSED,
enter = EnterTransition.None,
exit = nudgeExitTransitionInDescriptiveView(),
) {
NudgeUI(
nudgeUitronRenderer = nudgeUitronRenderer,
nudgeData = nudgeData,
nudge = nudgeData,
onNudgeEvent = onNudgeEvent,
onNudgeEffect = onNudgeEffect,
selectedDismissibleActionIndex =
selectedDismissibleActionIndexMap[nudgeData.nudgeId],
nudgeDraggedStateData = { nudgeDraggedStateMap[nudgeData.nudgeId] },
selectedDismissibleActionIndexMap[nudgeData.id],
nudgeDraggedStateData = { nudgeDraggedStateMap[nudgeData.id] },
borderSides =
BorderSides(
top =
index ==
nudgeList.indexOfFirst {
it.nudgeStatus == NudgeStatus.DEFAULT
it.status == NudgeStatus.DEFAULT
},
bottom = true,
),

View File

@@ -32,14 +32,17 @@ import com.navi.naviwidgets.composewidget.reusable.whiteColor
import com.navi.pay.utils.pxToDp
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.utils.hexToComposeColor
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateUpdateType
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.components.FrontLayerContent
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.components.NudgeBackLayer
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.components.NudgeFrontLayer
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeColor.borderColor
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeConstants
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeConstants.DISMISS_ACTION_CLICKED
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeConstants.dismissibleActionHorizontalPadding
import com.naviapp.screenOverlay.nudge.utils.measurement.getMaxWidthForDismissibleActionItem
import com.naviapp.screenOverlay.nudge.utils.modifier.BorderSides
@@ -50,7 +53,7 @@ import com.naviapp.screenOverlay.nudge.utils.root.HandleNudgeStatusChange
fun NudgeUI(
modifier: Modifier = Modifier,
borderSides: BorderSides,
nudgeData: NudgeData?,
nudge: Nudge?,
nudgeDraggedStateData: () -> NudgeDraggedStateData?,
selectedDismissibleActionIndex: Int?,
elevateFrontLayer: Boolean = false,
@@ -58,25 +61,26 @@ fun NudgeUI(
onNudgeEffect: (effect: NudgeEffect) -> Unit,
nudgeUitronRenderer: @Composable (response: UiTronResponse?, modifier: Modifier) -> Unit,
) {
if (nudgeData == null) return
if (nudge == null) return
val density = LocalDensity.current
val nudgeId = remember(nudgeData.nudgeId) { nudgeData.nudgeId.orEmpty() }
val nudgeId = remember(nudge.id) { nudge.id.orEmpty() }
var frontLayerVisibility by remember { mutableStateOf(true) }
var frontLayerHeight by remember(nudgeId) { mutableIntStateOf(0) }
var frontLayerHeight by remember(nudgeId, nudge.data?.defaultState) { mutableIntStateOf(0) }
var isFrontLayerHeightMeasured by remember(nudgeId) { mutableStateOf(false) }
var isFrontLayerHeightMeasured by
remember(nudgeId, nudge.data?.defaultState) { mutableStateOf(false) }
val maxWidthForDismissibleAction =
getMaxWidthForDismissibleActionItem(actions = nudgeData.dismissibleActionList).pxToDp() +
getMaxWidthForDismissibleActionItem(actions = nudge.dismissibleActions).pxToDp() +
dismissibleActionHorizontalPadding
val contextMenuWidth by
remember(maxWidthForDismissibleAction) {
derivedStateOf {
maxWidthForDismissibleAction.times(nudgeData.dismissibleActionList?.size.orZero())
maxWidthForDismissibleAction.times(nudge.dismissibleActions?.size.orZero())
}
}
@@ -93,12 +97,10 @@ fun NudgeUI(
Modifier.fillMaxWidth()
.height(with(density) { frontLayerHeight.toDp() })
.background(
nudgeData.dismissibleActionList
?.first()
?.actionBackgroundColor
?.hexToComposeColor ?: Color.White
nudge.dismissibleActions?.first()?.backgroundColor?.hexToComposeColor
?: Color.White
),
dismissibleActionList = nudgeData.dismissibleActionList,
dismissibleActionList = nudge.dismissibleActions,
dismissibleActionItemWidth = maxWidthForDismissibleAction,
onActionItemClicked = { index ->
onNudgeEvent(NudgeEvent.DismissibleActionClicked(nudgeId, index))
@@ -108,6 +110,12 @@ fun NudgeUI(
onDismissNudge = { action ->
onNudgeEvent(NudgeEvent.DismissNudge(nudgeId))
onNudgeEffect(NudgeEffect.OnNudgeDismissAction(nudgeId, action))
onNudgeEffect(
NudgeEffect.TriggerClickStreamEvent(
eventName = DISMISS_ACTION_CLICKED,
eventValue = mapOf(NudgeConstants.ACTION to action),
)
)
},
)
NudgeFrontLayer(
@@ -119,13 +127,17 @@ fun NudgeUI(
}
},
nudgeDraggedStateData = nudgeDraggedStateData,
isNudgeDragEnabled = nudgeData.isNudgeDragEnabled.orTrue(),
isNudgeDragEnabled = nudge.isDraggable.orTrue(),
frontLayerVisibility = frontLayerVisibility,
contextMenuWidth = contextMenuWidth,
frontLayerContent = {
nudgeUitronRenderer(
nudgeData.nudgeUitronData,
Modifier.then(DirectionalBorderModifier(borderSides, 1.dp, borderColor)),
FrontLayerContent(
modifier =
Modifier.then(
DirectionalBorderModifier(borderSides, 1.dp, borderColor)
),
nudgeStateData = nudge.data,
nudgeUitronRenderer = nudgeUitronRenderer,
)
},
onNudgeDraggedStateChange = { state ->
@@ -144,5 +156,5 @@ fun NudgeUI(
}
}
HandleNudgeStatusChange(nudgeData, onNudgeEvent, onNudgeEffect)
HandleNudgeStatusChange(nudge, onNudgeEvent, onNudgeEffect)
}

View File

@@ -33,20 +33,20 @@ import com.navi.elex.atoms.ElexText
import com.navi.elex.font.FontWeightEnum
import com.navi.naviwidgets.R as NaviWidgetR
import com.navi.uitron.utils.hexToComposeColor
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDismissibleActionData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDismissibleAction
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants
import com.naviapp.utils.Constants.EMPTY
@Composable
fun NudgeDismissibleActionItem(
item: NudgeDismissibleActionData,
item: NudgeDismissibleAction,
shouldExpand: () -> Boolean,
actionWidth: Dp,
itemContainerWidth: Dp,
onClick: () -> Unit,
dismissAction: () -> Unit,
) {
if (item.actionText == null) return
if (item.text == null) return
val configuration = LocalConfiguration.current
val maxScreenWidth = remember { configuration.screenWidthDp.dp }
@@ -54,7 +54,7 @@ fun NudgeDismissibleActionItem(
Row(
modifier =
Modifier.fillMaxHeight()
.background(item.actionBackgroundColor?.hexToComposeColor ?: Color.Gray)
.background(item.backgroundColor?.hexToComposeColor ?: Color.Gray)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
@@ -68,14 +68,14 @@ fun NudgeDismissibleActionItem(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
ElexAsyncImage(
icon = item.actionIllustration,
icon = item.illustration,
modifier = Modifier.size(20.dp),
contentDescription = EMPTY,
placeholder = NaviWidgetR.drawable.image_placeholder_small,
)
ElexText(
text = item.actionText,
color = item.actionTextColor?.hexToComposeColor ?: Color.White,
text = item.text,
color = item.textColor?.hexToComposeColor ?: Color.White,
fontSize = 12.sp,
lineHeight = 18.sp,
fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD,

View File

@@ -24,7 +24,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.navi.uitron.utils.hexToComposeColor
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDismissibleActionData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDismissibleAction
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.action.NudgeDismissibleActionItem
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeDismissibleActionExitFadeAnimation
@@ -33,7 +33,7 @@ fun NudgeBackLayer(
modifier: Modifier,
selectedIndex: Int?,
dismissibleActionItemWidth: Dp = 0.dp,
dismissibleActionList: List<NudgeDismissibleActionData>?,
dismissibleActionList: List<NudgeDismissibleAction>?,
onActionItemClicked: (Int) -> Unit = {},
onDismissNudge: (String) -> Unit,
) {
@@ -51,8 +51,7 @@ fun NudgeBackLayer(
Row(
modifier =
Modifier.background(
actionData.actionBackgroundColor?.hexToComposeColor
?: Color.Gray
actionData.backgroundColor?.hexToComposeColor ?: Color.Gray
)
.widthIn(min = dismissibleActionItemWidth.times(size - index))
.fillMaxHeight()

View File

@@ -18,19 +18,25 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedState
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStateData
import com.naviapp.screenOverlay.nudge.handler.drag.DragAnchors
import com.naviapp.screenOverlay.nudge.handler.drag.HandleAnchorDragStateChange
import com.naviapp.screenOverlay.nudge.handler.drag.UpdateNudgeDraggedStateOnDrag
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeFrontLayerExitAnimationSpec
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeFrontLayerStateEnterAnimationSpec
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -90,3 +96,28 @@ fun NudgeFrontLayer(
onNudgeUiStateChange = onNudgeDraggedStateChange,
)
}
@Composable
fun FrontLayerContent(
modifier: Modifier,
nudgeStateData: NudgeStateData?,
nudgeUitronRenderer: @Composable (response: UiTronResponse?, modifier: Modifier) -> Unit,
) {
if (nudgeStateData == null) return
Box(modifier.height(IntrinsicSize.Max)) {
nudgeUitronRenderer(nudgeStateData.defaultState, Modifier.fillMaxHeight())
AnimatedVisibility(
visible = (nudgeStateData.inProgressState != null),
enter = nudgeFrontLayerStateEnterAnimationSpec(),
) {
nudgeUitronRenderer(nudgeStateData.inProgressState, Modifier.fillMaxHeight())
}
AnimatedVisibility(
visible = (nudgeStateData.successState != null),
enter = nudgeFrontLayerStateEnterAnimationSpec(),
) {
nudgeUitronRenderer(nudgeStateData.successState, Modifier.fillMaxHeight())
}
}
}

View File

@@ -27,7 +27,7 @@ import androidx.compose.animation.togetherWith
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants.defaultAnimationSpec
@@ -38,13 +38,13 @@ fun nudgeEnterAnimation() = slideInVertically(defaultAnimationSpec()) { it.times
fun nudgeExitAnimation() = ExitTransition.None
// --------------------- Nudge Collapsed State container transitions ---------------------
fun AnimatedContentTransitionScope<NudgeData?>.nudgeCollapsedStateTransitionSpec(
filteredNudgeList: List<NudgeData>?,
fun AnimatedContentTransitionScope<Nudge?>.nudgeCollapsedStateTransitionSpec(
filteredNudgeList: List<Nudge>?,
initialNudgeId: NudgeId,
isDescriptiveViewEnabled: Boolean,
): ContentTransform {
if (isDescriptiveViewEnabled) return EnterTransition.None togetherWith ExitTransition.None
return if (filteredNudgeList?.any { it.nudgeId == initialNudgeId } == true) {
return if (filteredNudgeList?.any { it.id == initialNudgeId } == true) {
collapsedContainerRankChangeTransition()
} else {
collapsedContainerElementChangeTransition()
@@ -96,6 +96,11 @@ fun nudgeFrontLayerExitAnimationSpec(): ExitTransition {
return slideOutHorizontally(defaultAnimationSpec()) { -it }
}
fun nudgeFrontLayerStateEnterAnimationSpec(): EnterTransition {
return expandVertically(defaultAnimationSpec()) { 0 } +
slideInVertically(defaultAnimationSpec()) { -it }
}
// --------------------- Nudge header visibility transitions ---------------------
fun nudgeHeaderEnterAnimation(): EnterTransition {
return fadeIn(defaultAnimationSpec())

View File

@@ -32,5 +32,9 @@ object NudgeColor {
object NudgeConstants {
const val MIDDLE_PILL_CLICKED = "nudge_middle_pill_clicked"
const val DISMISS_ACTION_CLICKED = "dismiss_action_clicked"
const val ACTION = "action"
val dismissibleActionHorizontalPadding = 26.dp
const val PAID_NUDGE_ACTION = "PAID"
const val DISMISS_NUDGE_ACTION = "DISMISS"
}

View File

@@ -7,13 +7,13 @@
package com.naviapp.screenOverlay.nudge.utils.extensions
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStatus
fun List<NudgeData>?.filterDeletedNudges(): List<NudgeData> {
return this?.filterNot { it.nudgeStatus == NudgeStatus.DISMISSED }.orEmpty()
fun List<Nudge>?.filterDeletedNudges(): List<Nudge> {
return this?.filterNot { it.status == NudgeStatus.DISMISSED }.orEmpty()
}
fun List<NudgeData>?.getFirstNonDismissedNudge(): NudgeData? {
return this?.firstOrNull { it.nudgeStatus != NudgeStatus.DISMISSED }
fun List<Nudge>?.getFirstNonDismissedNudge(): Nudge? {
return this?.firstOrNull { it.status != NudgeStatus.DISMISSED }
}

View File

@@ -18,10 +18,10 @@ import androidx.compose.ui.unit.sp
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDismissibleActionData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDismissibleAction
@Composable
fun getMaxWidthForDismissibleActionItem(actions: List<NudgeDismissibleActionData>?): Int {
fun getMaxWidthForDismissibleActionItem(actions: List<NudgeDismissibleAction>?): Int {
val density = LocalDensity.current
val textMeasurer = rememberTextMeasurer()
val baseWidthPx = with(density) { 56.dp.toPx().toInt() }
@@ -31,12 +31,12 @@ fun getMaxWidthForDismissibleActionItem(actions: List<NudgeDismissibleActionData
}
return remember(
key1 = actions.map { it.actionText },
key1 = actions.map { it.text },
calculation = {
val textWidths =
actions
.filter { it.actionText != null }
.map { action -> measureActionWidth(textMeasurer, action.actionText!!) }
.filter { it.text != null }
.map { action -> measureActionWidth(textMeasurer, action.text!!) }
val maxTextWidth = textWidths.maxOrNull() ?: 0
maxOf(maxTextWidth, baseWidthPx)
},

View File

@@ -10,7 +10,7 @@ package com.naviapp.screenOverlay.nudge.utils.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStatus
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
@@ -40,19 +40,19 @@ fun HandleExpandedStateByListSize(state: () -> NudgeState, onEvent: (event: Nudg
*/
@Composable
fun HandleNudgeStatusChange(
nudgeData: NudgeData,
nudge: Nudge,
onNudgeEvent: (nudgeEvent: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
) {
LaunchedEffect(
key1 = nudgeData.nudgeStatus,
key1 = nudge.status,
block = {
if (nudgeData.nudgeStatus == NudgeStatus.SUCCESS) {
if (nudge.status == NudgeStatus.SUCCESS) {
delay(SUCCESS_DELAY)
onNudgeEvent(NudgeEvent.DismissNudge(id = nudgeData.nudgeId.orEmpty()))
onNudgeEvent(NudgeEvent.DismissNudge(id = nudge.id.orEmpty()))
onNudgeEffect(
NudgeEffect.OnNudgeStateUpdate(
id = nudgeData.nudgeId.orEmpty(),
id = nudge.id.orEmpty(),
transitionState = OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED,
)
)

View File

@@ -0,0 +1,120 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.usecase
import com.google.gson.Gson
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepositoryImpl
import com.navi.common.utils.log
import com.navi.uitron.model.UiTronResponse
import com.naviapp.network.di.DataDeserializers
import com.naviapp.network.di.DataSerializers
import com.naviapp.screenOverlay.nudge.domain.model.data.Nudge
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStatus
import com.naviapp.utils.fromJsonSafely
import javax.inject.Inject
class NudgeCacheUseCase
@Inject
constructor(
private val naviCacheRepository: NaviCacheRepositoryImpl,
@DataSerializers private val dataSerializer: Gson,
@DataDeserializers private val dataDeserializer: Gson,
) {
suspend fun getUpdatedNudgeList(nudgeList: List<Nudge>?): List<Nudge> {
if (nudgeList.isNullOrEmpty()) return emptyList()
val cachedNudges = getCachedNudges().toMutableMap()
return nudgeList
.mapNotNull { nudge ->
val nudgeId = nudge.id ?: return@mapNotNull null
val nudgeConfig = nudge.data ?: return@mapNotNull null
val cachedNudge = cachedNudges[nudgeId]
when (nudge.status) {
NudgeStatus.DEFAULT ->
nudgeConfig.defaultState?.let {
cachedNudges[nudgeId] = NudgeCache(it)
nudge.copy(
data = nudgeConfig.copy(inProgressState = null, successState = null)
)
}
NudgeStatus.IN_PROGRESS ->
cachedNudge?.defaultStateData?.let {
cachedNudges[nudgeId] =
cachedNudge.copy(inProgressStateData = nudgeConfig.inProgressState)
nudge.copy(
data = nudgeConfig.copy(defaultState = it, successState = null)
)
}
NudgeStatus.SUCCESS ->
cachedNudge?.defaultStateData?.let {
nudge.copy(
data =
nudgeConfig.copy(
defaultState = it,
inProgressState = cachedNudge.inProgressStateData,
)
)
}
else -> {
cachedNudges.remove(nudgeId)
null
}
}
}
.also { cachedNudges.saveToCache(naviCacheRepository, dataSerializer) }
}
suspend fun removeNudgeFromCache(nudgeId: NudgeId) {
getCachedNudges().toMutableMap().apply {
remove(nudgeId)
saveToCache(naviCacheRepository, dataSerializer)
}
}
private suspend fun getCachedNudges(): Map<NudgeId, NudgeCache> =
naviCacheRepository.get(HOME_NUDGE_KEY)?.let {
dataDeserializer.fromJsonSafely(it.value, emptyMap())
} ?: emptyMap()
data class NudgeCache(
val defaultStateData: UiTronResponse? = null,
val inProgressStateData: UiTronResponse? = null,
)
private suspend fun Map<NudgeId, NudgeCache>.saveToCache(
repository: NaviCacheRepositoryImpl,
serializer: Gson,
): Boolean {
if (isEmpty()) return false
return runCatching {
repository.save(
NaviCacheEntity(
key = HOME_NUDGE_KEY,
value = serializer.toJson(this),
version = 1,
ttl = -1L,
clearOnLogout = true,
)
)
true
}
.onFailure { it.log() }
.getOrDefault(false)
}
companion object {
private const val HOME_NUDGE_KEY = "HOME_NUDGE"
}
}

View File

@@ -22,18 +22,21 @@ import com.naviapp.screenOverlay.handler.ScreenOverlayHandler
import com.naviapp.screenOverlay.handler.ScreenOverlayUitronActionHandler
import com.naviapp.screenOverlay.model.OverlayItemActionData
import com.naviapp.screenOverlay.model.OverlayItemStateUpdate
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.model.OverlayItemsStateUpdates
import com.naviapp.screenOverlay.model.ScreenOverlayActionUpdateRequest
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeListData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStatus
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.nudge.domain.reducer.NudgeReducer
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeConstants.DISMISS_NUDGE_ACTION
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeConstants.PAID_NUDGE_ACTION
import com.naviapp.screenOverlay.popup.model.PopupEvent
import com.naviapp.screenOverlay.popup.model.PopupListData
import com.naviapp.screenOverlay.popup.model.PopupState
import com.naviapp.screenOverlay.popup.reducer.PopupReducer
import com.naviapp.screenOverlay.usecase.NudgeCacheUseCase
import com.naviapp.utils.SelectiveRefreshHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -53,6 +56,7 @@ constructor(
private val screenOverlayHandler: ScreenOverlayHandler,
private val screenOverlayUitronActionHandler: ScreenOverlayUitronActionHandler,
private val selectiveRefreshHandler: SelectiveRefreshHandler,
private val nudgeCacheUseCase: NudgeCacheUseCase,
) :
ScreenOverlayVMComponent(
initialNudgeState = NudgeState.initialState,
@@ -70,7 +74,6 @@ constructor(
val deletedItemsMap = mutableMapOf<String, MutableSet<String>>()
private var fetchOverlayScreenApiJob: Job? = null
private var lastClickedNudgeId = EMPTY
private var lastViewedNudgeIds = mutableSetOf<String>()
init {
viewModelScope.launch { getActionCallback().collect { onActionTriggered(it) } }
@@ -86,6 +89,9 @@ constructor(
OverlayItemsStateUpdates(nudgeItems = nudgeTransitionState),
naeScreenName = naeScreenName,
)
nudgeTransitionState
.filter { it.state == OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED }
.forEach { nudgeCacheUseCase.removeNudgeFromCache(it.nudgeId) }
}
}
}
@@ -100,6 +106,9 @@ constructor(
ScreenOverlayActionUpdateRequest(nudgeItems = nudgeActions),
naeScreenName = naeScreenName,
)
nudgeActions
.filter { it.action == PAID_NUDGE_ACTION || it.action == DISMISS_NUDGE_ACTION }
.forEach { nudgeCacheUseCase.removeNudgeFromCache(it.nudgeId) }
}
}
}
@@ -124,7 +133,6 @@ constructor(
popupData,
bottomSheetData,
staticNudgeData,
lastViewedNudgeIds,
)
},
onError = { handleScreenOverlayApiError() },
@@ -149,22 +157,15 @@ constructor(
}
}
private fun handleScreenOverlayApiSuccess(
private suspend fun handleScreenOverlayApiSuccess(
nudgeListData: NudgeListData?,
popupListData: PopupListData?,
bottomSheetData: BottomSheetData?,
staticNudgeData: StaticNudgeData?,
lastViewedNudgeIds: MutableSet<String>,
) {
nudgeListData?.nudgeList?.let { nudgeList ->
val updatedNudgeList =
nudgeList.filterNot {
it.nudgeStatus == NudgeStatus.SUCCESS && it.nudgeId !in lastViewedNudgeIds
}
sendEvent(NudgeEvent.UpdateNudgeList(updatedNudgeList))
lastViewedNudgeIds.clear()
lastViewedNudgeIds.addAll(updatedNudgeList.mapNotNull { it.nudgeId })
}
sendEvent(
NudgeEvent.UpdateNudgeList(nudgeCacheUseCase.getUpdatedNudgeList(nudgeListData?.nudges))
)
popupListData?.popupList?.let {
sendEvent(PopupEvent.UpdatePopupData(it, popupListVisibilityState = it.isNotEmpty()))
}

View File

@@ -19,6 +19,8 @@ import android.widget.Toast
import androidx.annotation.StringRes
import androidx.viewpager.widget.ViewPager
import com.facebook.react.bridge.ReadableMap
import com.google.common.reflect.TypeToken
import com.google.gson.Gson
import com.navi.base.utils.BaseUtils
import com.navi.common.utils.Constants
import com.navi.common.utils.Constants.EMPTY
@@ -286,3 +288,13 @@ fun Long.formatTimeStamp(): String {
val formatter = SimpleDateFormat("dd MMM, h:mm a", Locale.getDefault())
return formatter.format(date)
}
inline fun <reified T> Gson.fromJsonSafely(json: String?, defaultValue: T): T {
if (json.isNullOrEmpty()) return defaultValue
return try {
fromJson(json, object : TypeToken<T>() {}.type)
} catch (e: Exception) {
e.log()
defaultValue
}
}

View File

@@ -30,7 +30,7 @@ class HomeCustomActionSerializer : UiTronActionSerializer() {
NudgeUiTronActions.DRAG_NUDGE.name ->
context?.serialize(src as NudgeDragAction, NudgeDragAction::class.java)
NudgeUiTronActions.NUDGE_CLICKED.name ->
context?.serialize(src as NudgeDragAction, NudgeClickAction::class.java)
context?.serialize(src as NudgeClickAction, NudgeClickAction::class.java)
NudgeUiTronActions.NUDGE_CTA_ACTION.name ->
context?.serialize(src as NudgeCtaAction, NudgeCtaAction::class.java)
PopupUitronActions.DISMISS_POPUP.name ->