NTP-43715 | Nudge V2 (#15256)

Signed-off-by: Naman Khurmi <naman.khurmi@navi.com>
This commit is contained in:
Naman Khurmi
2025-03-12 14:46:07 +05:30
committed by GitHub
parent 928cdfb273
commit f38f54c89f
65 changed files with 2076 additions and 1448 deletions

View File

@@ -174,7 +174,7 @@ import com.naviapp.registration.helper.isReadSmsPermissionGranted
import com.naviapp.registration.viewmodel.RegistrationVM
import com.naviapp.registration.viewmodel.UploadUserDataUseCase
import com.naviapp.screenOverlay.handler.ScreenOverlayEffectHandler
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.popup.model.PopupEffect
import com.naviapp.screenOverlay.popup.model.PopupEvent
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@@ -378,8 +378,8 @@ class HomePageActivity :
homeVM.sendEvent(HpEvents.UpdateProfileDrawerState(false))
} else if (screenOverlayVM.popupState.value.isPopupListVisible) {
handlePopupDismissal()
} else if (screenOverlayVM.nudgeState.value.isNudgeExpanded) {
screenOverlayVM.sendEvent(NudgeEvent.UpdateNudgeExpandedState(expandState = false))
} else if (screenOverlayVM.nudgeState.value.descriptiveViewEnabled) {
screenOverlayVM.sendEvent(NudgeEvent.ToggleDescriptiveView(enabled = false))
} else if (selectedTabId == BottomBarTabType.HOME.name) {
TemporaryStorageHelper.homePageBackPressed = true
super.onBackPressed()
@@ -487,9 +487,9 @@ class HomePageActivity :
private fun observeScreenOverlayEffect() {
lifecycleScope.launch {
screenOverlayVM.nudgeEffect.collect {
screenOverlayVM.nudgeEffect.collect { effect ->
screenOverlayEffectHandler.handleNudgeEffect(
effect = it,
effect = effect,
deletedItemsMap = screenOverlayVM.deletedItemsMap,
triggerStateUpdateApiCall = { nudgeTransitionState ->
screenOverlayVM.triggerStateUpdateApiCall(
@@ -497,6 +497,12 @@ class HomePageActivity :
naeScreenName = screenName,
)
},
triggerActionUpdateApiCall = { nudgeActions ->
screenOverlayVM.triggerActionUpdateApiCall(
nudgeActions,
naeScreenName = screenName,
)
},
)
}
}

View File

@@ -25,7 +25,7 @@ import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable

View File

@@ -30,7 +30,7 @@ import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable

View File

@@ -31,10 +31,10 @@ import com.naviapp.home.compose.home.utils.updateBottomOverlayBounds
import com.naviapp.home.model.BottomBarTabType
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
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.ui.NudgeContainer
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.nudge.ui.container.NudgeContainer
@Composable
fun HomeFooterRoot(
@@ -46,7 +46,7 @@ fun HomeFooterRoot(
navController: NavHostController,
selectedTabId: String,
nudgeState: () -> NudgeState,
nudgeUitronRenderer: @Composable (UiTronResponse?) -> Unit,
nudgeUitronRenderer: @Composable (response: UiTronResponse?, modifier: Modifier) -> Unit,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
) {
@@ -85,7 +85,7 @@ fun HomeFooter(
bottomNudgeData: BottomStickyNudgeData? = null,
state: HomeFooterStates,
nudgeState: () -> NudgeState,
nudgeUitronRenderer: @Composable (UiTronResponse?) -> Unit,
nudgeUitronRenderer: @Composable (response: UiTronResponse?, modifier: Modifier) -> Unit,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
onFooterEvent: (event: HomeFooterEvents) -> Unit,
@@ -134,7 +134,7 @@ fun HomeFooter(
bottomNavBarState = { state.bottomNavBarState },
onTabSelected = { tabId ->
onFooterEvent(HomeFooterEvents.BottomBarOnTabClick(tabId))
onNudgeEvent(NudgeEvent.UpdateNudgeExpandedState(expandState = false))
onNudgeEvent(NudgeEvent.ToggleDescriptiveView(enabled = false))
},
)
}

View File

@@ -33,8 +33,7 @@ import com.naviapp.home.viewmodel.ProfileVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.model.OverlayItemActionData
import com.naviapp.screenOverlay.model.OverlayItemStateUpdate
import com.naviapp.screenOverlay.nudge.utils.getOverlayItemAction
import com.naviapp.screenOverlay.nudge.utils.getOverlayItemTransitionState
import com.naviapp.screenOverlay.nudge.utils.mapper.mapToTransitionState
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable
@@ -130,7 +129,7 @@ fun handleBottomSheetAction(
action.states.map {
OverlayItemStateUpdate(
nudgeId = it.nudgeId,
state = getOverlayItemTransitionState(overlayItemState = it.state),
state = mapToTransitionState(overlayItemState = it.state),
)
}
screenOverlayVM.triggerStateUpdateApiCall(
@@ -141,10 +140,7 @@ fun handleBottomSheetAction(
is ScreenOverlayActionUpdateAction -> {
val nudgeActions =
action.actions.map {
OverlayItemActionData(
nudgeId = it.nudgeId,
action = getOverlayItemAction(action = it.action),
)
OverlayItemActionData(nudgeId = it.nudgeId, action = it.action)
}
screenOverlayVM.triggerActionUpdateApiCall(nudgeActions, activity.screenName)
}

View File

@@ -42,8 +42,8 @@ import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.ProfileVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.bottomsheet.ui.HomeScreenBottomSheet
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.utils.InitScreenOverlayComponents
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.initializer.InitScreenOverlayComponents
import com.naviapp.screenOverlay.popup.ui.PopupRenderer
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@@ -143,8 +143,8 @@ fun HomeContentFrame(
navController = navController,
selectedTabId = selectedTabId,
nudgeState = { nudgeState },
nudgeUitronRenderer = { uiTronResponse ->
HandleUitronRenderer(uiTronResponse, screenOverlayVM)
nudgeUitronRenderer = { uiTronResponse, modifier ->
HandleUitronRenderer(uiTronResponse, screenOverlayVM, modifier)
},
onNudgeEvent = { event -> screenOverlayVM.sendEvent(event) },
onNudgeEffect = { effect -> screenOverlayVM.setEffect { effect } },
@@ -165,12 +165,11 @@ fun HomeContentFrame(
FullLayerScrim(
color = scrimColor,
onDismiss = {
screenOverlayVM.sendEvent(
NudgeEvent.UpdateNudgeExpandedState(expandState = false)
)
screenOverlayVM.sendEvent(NudgeEvent.ToggleDescriptiveView(enabled = false))
},
visible =
(nudgeState.isNudgeExpanded && selectedTabId == BottomBarTabType.HOME.value),
(nudgeState.descriptiveViewEnabled &&
selectedTabId == BottomBarTabType.HOME.value),
)
},
)

View File

@@ -33,9 +33,9 @@ import com.naviapp.home.utils.WidgetRenderer
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.model.StaticNudgeUiState
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeUiState
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.nudge.ui.StaticNudgeContainer
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM

View File

@@ -54,10 +54,10 @@ import com.naviapp.home.model.HpBottomSheetRenderType
import com.naviapp.home.model.HpBottomSheetState
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.StaticNudgeData
import com.naviapp.screenOverlay.nudge.model.StaticNudgeUiState
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeUiState
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
import com.naviapp.utils.Constants.HOME_SCREEN_IN_CAPS
import kotlinx.coroutines.Dispatchers
@@ -127,9 +127,13 @@ fun <T> getHomeWidgetAnimationSpec(): FiniteAnimationSpec<T> {
}
@Composable
fun HandleUitronRenderer(uiTronResponse: UiTronResponse?, viewModel: BaseVM) {
fun HandleUitronRenderer(
uiTronResponse: UiTronResponse?,
viewModel: BaseVM,
modifier: Modifier = Modifier,
) {
UiTronRenderer(dataMap = uiTronResponse?.data, uiTronViewModel = viewModel)
.Render(composeViews = uiTronResponse?.parentComposeView.orEmpty())
.Render(composeViews = uiTronResponse?.parentComposeView.orEmpty(), modifier = modifier)
}
@Composable
@@ -157,9 +161,9 @@ fun WidgetRenderer(
NudgeEvent.UpdateStaticNudgeUiState(StaticNudgeUiState.DISMISSED)
)
viewModel.setEffect {
NudgeEffect.OnDeleteNudge(
nudgeId = staticNudgeData.nudgeId.orEmpty(),
overlayItemTransitionState = OverlayItemTransitionState.PAUSED,
NudgeEffect.OnNudgeStateUpdate(
id = staticNudgeData.nudgeId.orEmpty(),
transitionState = OverlayItemTransitionState.PAUSED,
)
}
},
@@ -199,9 +203,9 @@ fun WidgetRenderer(
NudgeEvent.UpdateStaticNudgeUiState(StaticNudgeUiState.DISMISSED)
)
viewModel.setEffect {
NudgeEffect.OnDeleteNudge(
nudgeId = staticNudgeData.nudgeId.orEmpty(),
overlayItemTransitionState = OverlayItemTransitionState.PAUSED,
NudgeEffect.OnNudgeStateUpdate(
id = staticNudgeData.nudgeId.orEmpty(),
transitionState = OverlayItemTransitionState.PAUSED,
)
}
},

View File

@@ -7,9 +7,10 @@
package com.naviapp.screenOverlay.handler
import com.naviapp.screenOverlay.model.OverlayItemActionData
import com.naviapp.screenOverlay.model.OverlayItemStateUpdate
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.utils.NudgeConstants.NUDGE
import com.naviapp.screenOverlay.utils.NudgeConstants.NUDGE_DISMISSED_EVENT
import com.naviapp.screenOverlay.utils.NudgeConstants.NUDGE_ID
@@ -23,19 +24,18 @@ constructor(private val screenOverlayHandler: ScreenOverlayHandler) {
effect: NudgeEffect,
deletedItemsMap: MutableMap<String, MutableSet<String>>,
triggerStateUpdateApiCall: (nudgeTransitionState: List<OverlayItemStateUpdate>) -> Unit,
triggerActionUpdateApiCall: (nudgeActions: List<OverlayItemActionData>) -> Unit,
) {
when (effect) {
is NudgeEffect.OnDeleteNudge -> {
deletedItemsMap.getOrPut(NUDGE) { mutableSetOf() }.add(effect.nudgeId)
is NudgeEffect.OnNudgeStateUpdate -> {
deletedItemsMap.getOrPut(NUDGE) { mutableSetOf() }.add(effect.id)
triggerStateUpdateApiCall(
mutableListOf(
OverlayItemStateUpdate(effect.nudgeId, effect.overlayItemTransitionState)
)
mutableListOf(OverlayItemStateUpdate(effect.id, effect.transitionState))
)
if (effect.overlayItemTransitionState == OverlayItemTransitionState.PAUSED) {
if (effect.transitionState == OverlayItemTransitionState.PAUSED) {
screenOverlayHandler.triggerClickStreamEvent(
NUDGE_DISMISSED_EVENT,
mapOf(NUDGE_ID to effect.nudgeId),
mapOf(NUDGE_ID to effect.id),
)
}
}
@@ -45,6 +45,13 @@ constructor(private val screenOverlayHandler: ScreenOverlayHandler) {
effect.eventValue.orEmpty(),
)
}
is NudgeEffect.OnNudgeDismissAction -> {
deletedItemsMap.getOrPut(NUDGE) { mutableSetOf() }.add(effect.id)
triggerActionUpdateApiCall(
mutableListOf(OverlayItemActionData(effect.id, effect.action))
)
}
}
}
}

View File

@@ -9,8 +9,9 @@ package com.naviapp.screenOverlay.handler
import com.navi.uitron.model.data.UiTronActionData
import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetEffect
import com.naviapp.screenOverlay.model.OverlayItemActionData
import com.naviapp.screenOverlay.model.OverlayItemStateUpdate
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.popup.model.PopupEffect
import com.naviapp.screenOverlay.popup.model.PopupState
import javax.inject.Inject
@@ -27,8 +28,14 @@ constructor(
effect: NudgeEffect,
deletedItemsMap: MutableMap<String, MutableSet<String>>,
triggerStateUpdateApiCall: (nudgeTransitionState: List<OverlayItemStateUpdate>) -> Unit,
triggerActionUpdateApiCall: (nudgeActions: List<OverlayItemActionData>) -> Unit,
) {
nudgeEffectHandler.handleNudgeEffect(effect, deletedItemsMap, triggerStateUpdateApiCall)
nudgeEffectHandler.handleNudgeEffect(
effect,
deletedItemsMap,
triggerStateUpdateApiCall,
triggerActionUpdateApiCall,
)
}
fun handlePopupEffect(

View File

@@ -14,8 +14,8 @@ import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetData
import com.naviapp.screenOverlay.model.OverlayItemsStateUpdates
import com.naviapp.screenOverlay.model.OverlayScreenStructure
import com.naviapp.screenOverlay.model.ScreenOverlayActionUpdateRequest
import com.naviapp.screenOverlay.nudge.model.NudgeListData
import com.naviapp.screenOverlay.nudge.model.StaticNudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeListData
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
import com.naviapp.screenOverlay.popup.model.PopupListData
import com.naviapp.screenOverlay.repositories.ScreenOverlayRepository
import com.naviapp.screenOverlay.utils.NudgeConstants.NUDGE

View File

@@ -13,9 +13,13 @@ import com.navi.common.basemvi.UiEvent
import com.navi.common.uitron.model.action.CtaAction
import com.navi.uitron.model.action.AnalyticsAction
import com.navi.uitron.model.data.UiTronAction
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeUiState
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.NudgeDraggedStateUpdateType
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeClickAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeCtaAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeDragAction
import com.naviapp.screenOverlay.popup.model.PopupEvent
import com.naviapp.screenOverlay.popup.model.PopupUiState
@@ -26,6 +30,7 @@ import javax.inject.Inject
class ScreenOverlayUitronActionHandler @Inject constructor() {
fun onActionTriggered(
state: NudgeState,
uiTronAction: UiTronAction,
sendEvent: (event: UiEvent) -> Unit,
lastClickedNudgeId: (nudgeId: String) -> Unit,
@@ -34,15 +39,18 @@ class ScreenOverlayUitronActionHandler @Inject constructor() {
when (uiTronAction) {
is NudgeDragAction -> {
sendEvent(
NudgeEvent.UpdateNudgeUiState(
uiTronAction.nudgeId.orEmpty(),
NudgeUiState.DRAGGED,
NudgeEvent.UpdateNudgeDraggedStateMap(
uiTronAction.id.orEmpty(),
NudgeDraggedStateData(
state = NudgeDraggedState.DRAGGED,
type = NudgeDraggedStateUpdateType.TOGGLE,
),
)
)
}
is NudgeClickAction -> {
lastClickedNudgeId(uiTronAction.nudgeId.orEmpty())
sendEvent(NudgeEvent.UpdateNudgeExpandedState(false))
lastClickedNudgeId(uiTronAction.id.orEmpty())
sendEvent(NudgeEvent.ToggleDescriptiveView(false))
}
is PopupDismissAction -> {
sendEvent(
@@ -52,6 +60,24 @@ class ScreenOverlayUitronActionHandler @Inject constructor() {
)
)
}
is NudgeCtaAction -> {
val nudgeUiState = state.nudgeDraggedStateMap[uiTronAction.id]?.state
if (nudgeUiState == NudgeDraggedState.DRAGGED) {
sendEvent(
NudgeEvent.UpdateNudgeDraggedStateMap(
uiTronAction.id,
NudgeDraggedStateData(
state = NudgeDraggedState.IDLE,
type = NudgeDraggedStateUpdateType.TOGGLE,
),
)
)
} else {
uiTronAction.ctaAction.ctaData?.let { ctaAction(it) }
}
}
is CtaAction -> {
uiTronAction.ctaData?.let { ctaAction(it) }
}

View File

@@ -9,10 +9,4 @@ package com.naviapp.screenOverlay.model
data class ScreenOverlayActionUpdateRequest(val nudgeItems: List<OverlayItemActionData>)
data class OverlayItemActionData(val nudgeId: String, val action: OverlayItemAction)
enum class OverlayItemAction {
VIEW,
CLICK,
DISMISS,
}
data class OverlayItemActionData(val nudgeId: String, val action: String)

View File

@@ -9,8 +9,8 @@ package com.naviapp.screenOverlay.model
import com.google.gson.annotations.SerializedName
import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetData
import com.naviapp.screenOverlay.nudge.model.NudgeListData
import com.naviapp.screenOverlay.nudge.model.StaticNudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeListData
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
import com.naviapp.screenOverlay.popup.model.PopupListData
data class OverlayScreenStructure(

View File

@@ -0,0 +1,60 @@
/*
*
* * 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

@@ -1,17 +1,17 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.model
package com.naviapp.screenOverlay.nudge.domain.model.data
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.uitron.model.UiTronResponse
data class StaticNudgeData(
val nudgeId: String? = null,
val nudgeId: NudgeId? = null,
val nudgeData: AlchemistWidgetModelDefinition<UiTronResponse>? = null,
val nudgeUiState: StaticNudgeUiState = StaticNudgeUiState.INITIAL,
)

View File

@@ -0,0 +1,29 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.domain.model.effect
import androidx.compose.runtime.Immutable
import com.navi.common.basemvi.UiEffect
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
@Immutable
sealed interface NudgeEffect : UiEffect {
data class OnNudgeDismissAction(val id: NudgeId, val action: String) : NudgeEffect
data class OnNudgeStateUpdate(
val id: NudgeId,
val transitionState: OverlayItemTransitionState,
) : NudgeEffect
data class TriggerClickStreamEvent(
val eventName: String,
val eventValue: Map<String, String>? = null,
) : NudgeEffect
}

View File

@@ -0,0 +1,34 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
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.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeUiState
@Immutable
sealed interface NudgeEvent : UiEvent {
data class ToggleDescriptiveView(val enabled: Boolean) : NudgeEvent
data class UpdateNudgeList(val data: List<NudgeData>?) : NudgeEvent
data class DismissNudge(val id: NudgeId) : NudgeEvent
data class DismissibleActionClicked(val id: NudgeId, val index: Int) : NudgeEvent
data class UpdateNudgeDraggedStateMap(val key: NudgeId, val value: NudgeDraggedStateData) :
NudgeEvent
data class UpdateStaticNudgeData(val data: StaticNudgeData?) : NudgeEvent
data class UpdateStaticNudgeUiState(val uiState: StaticNudgeUiState) : NudgeEvent
}

View File

@@ -0,0 +1,35 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
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.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
@Immutable
data class NudgeState(
val isNudgeListVisible: Boolean,
val descriptiveViewEnabled: Boolean,
val nudgeList: List<NudgeData>?,
val staticNudgeData: StaticNudgeData? = null,
val nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData> = emptyMap(),
val selectedDismissibleActionIndexMap: Map<NudgeId, Int> = emptyMap(),
) : UiState {
companion object {
val initialState =
NudgeState(
isNudgeListVisible = false,
descriptiveViewEnabled = false,
nudgeList = null,
staticNudgeData = null,
)
}
}

View File

@@ -0,0 +1,122 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.domain.reducer
import com.navi.common.basemvi.BaseReducer
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.NudgeDraggedStateUpdateType
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeStatus
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
class NudgeReducer : BaseReducer<NudgeState, NudgeEvent> {
override fun reduce(previousState: NudgeState, event: NudgeEvent): NudgeState {
return when (event) {
/**
* Handles the UpdateNudgeExpandedState event. Updates the expanded state of the nudge
* container and sets the UI state of any currently dragged nudge to IDLE.
*/
is NudgeEvent.ToggleDescriptiveView -> {
val draggedStateMap = previousState.nudgeDraggedStateMap.toMutableMap()
draggedStateMap.entries.forEach {
draggedStateMap[it.key] =
NudgeDraggedStateData(
NudgeDraggedState.IDLE,
NudgeDraggedStateUpdateType.TOGGLE,
)
}
previousState.copy(
descriptiveViewEnabled = event.enabled,
nudgeDraggedStateMap = draggedStateMap,
)
}
is NudgeEvent.UpdateNudgeList -> {
val nudgeDraggedStateMap = mutableMapOf<NudgeId, NudgeDraggedStateData>()
event.data?.forEach { nudgeData ->
nudgeData.nudgeId?.let {
nudgeDraggedStateMap[nudgeData.nudgeId] =
NudgeDraggedStateData(
state = NudgeDraggedState.IDLE,
type = NudgeDraggedStateUpdateType.TOGGLE,
)
}
}
previousState.copy(
nudgeList = event.data,
isNudgeListVisible = true,
nudgeDraggedStateMap = nudgeDraggedStateMap,
)
}
is NudgeEvent.DismissNudge -> {
previousState.copy(
nudgeList =
previousState.nudgeList?.map { nudge ->
if (nudge.nudgeId == event.id) {
nudge.copy(nudgeStatus = NudgeStatus.DISMISSED)
} else {
nudge
}
}
)
}
is NudgeEvent.UpdateStaticNudgeData -> {
previousState.copy(staticNudgeData = event.data)
}
is NudgeEvent.UpdateStaticNudgeUiState -> {
previousState.copy(
staticNudgeData =
previousState.staticNudgeData?.copy(nudgeUiState = event.uiState)
)
}
is NudgeEvent.DismissibleActionClicked -> {
val previousMap = previousState.selectedDismissibleActionIndexMap.toMutableMap()
previousMap[event.id] = event.index
previousState.copy(selectedDismissibleActionIndexMap = previousMap)
}
is NudgeEvent.UpdateNudgeDraggedStateMap -> {
val previousMap = previousState.nudgeDraggedStateMap.toMutableMap()
previousMap.getUpdatedNudgeUiState(event)
previousState.copy(nudgeDraggedStateMap = previousMap)
}
}
}
private fun MutableMap<NudgeId, NudgeDraggedStateData>.getUpdatedNudgeUiState(
event: NudgeEvent.UpdateNudgeDraggedStateMap
) {
this[event.key] =
when {
this[event.key]?.state == NudgeDraggedState.DRAGGED &&
event.value.state == NudgeDraggedState.DRAGGED ->
NudgeDraggedStateData(NudgeDraggedState.IDLE, event.value.type)
else -> event.value
}
entries
.filter { it.key != event.key && it.value.state == NudgeDraggedState.DRAGGED }
.forEach {
this[it.key] =
NudgeDraggedStateData(
NudgeDraggedState.IDLE,
NudgeDraggedStateUpdateType.TOGGLE,
)
}
}
}

View File

@@ -0,0 +1,78 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.handler.action
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.navi.ap.common.handler.HandlePublishEventAction
import com.navi.common.uitron.model.action.ScreenOverlayActionUpdateAction
import com.navi.common.uitron.model.action.ScreenOverlayApiAction
import com.navi.common.uitron.model.action.ScreenOverlayStateUpdateAction
import com.naviapp.screenOverlay.model.OverlayItemActionData
import com.naviapp.screenOverlay.model.OverlayItemStateUpdate
import com.naviapp.screenOverlay.nudge.utils.mapper.mapToTransitionState
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable
fun HandleNudgeActions(
viewModel: ScreenOverlayVM,
naeScreenName: String,
isNotificationPermissionEnabled: Boolean,
) {
HandlePublishEventAction(viewModel = viewModel)
HandleApiAction(
viewModel = viewModel,
naeScreenName = naeScreenName,
isNotificationPermissionEnabled = isNotificationPermissionEnabled,
)
}
@Composable
private fun HandleApiAction(
viewModel: ScreenOverlayVM,
naeScreenName: String,
isNotificationPermissionEnabled: Boolean,
) {
LaunchedEffect(Unit) {
viewModel.getActionCallback().collect { action ->
when (action) {
is ScreenOverlayApiAction -> {
viewModel.fetchOverlayScreenData(
triggerLoadingState = true,
naeScreenName = naeScreenName,
isNotificationPermissionEnabled = isNotificationPermissionEnabled,
)
}
is ScreenOverlayStateUpdateAction -> {
val nudgeTransitionState =
action.states.map {
OverlayItemStateUpdate(
nudgeId = it.nudgeId,
state = mapToTransitionState(overlayItemState = it.state),
)
}
viewModel.triggerStateUpdateApiCall(
nudgeTransitionState = nudgeTransitionState,
naeScreenName = naeScreenName,
)
}
is ScreenOverlayActionUpdateAction -> {
val nudgeActions =
action.actions.map {
OverlayItemActionData(nudgeId = it.nudgeId, action = it.action)
}
viewModel.triggerActionUpdateApiCall(nudgeActions, naeScreenName)
}
else -> {}
}
}
}
}

View File

@@ -0,0 +1,51 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.handler.bottomsheet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.model.HpBottomSheetState
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetState
import com.naviapp.screenOverlay.bottomsheet.utils.toHpBottomSheetConfig
import com.naviapp.screenOverlay.bottomsheet.utils.toHpBottomSheetContent
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable
fun HandleNudgeBottomSheetState(
bottomSheetState: BottomSheetState,
selectedTabId: String,
hpStates: () -> HpStates,
sharedVM: SharedVM,
screenOverlayVM: ScreenOverlayVM,
) {
LaunchedEffect(
bottomSheetState,
selectedTabId,
hpStates().profileDrawerState,
hpStates().isHomePageRendered,
) {
val bottomSheetData = bottomSheetState.bottomSheetData ?: return@LaunchedEffect
bottomSheetData.let {
sharedVM.updateBottomSheetState(
state =
if (
selectedTabId == BottomBarTabType.HOME.value &&
hpStates().isHomePageRendered &&
hpStates().profileDrawerState.not()
) {
HpBottomSheetState.Visible
} else HpBottomSheetState.Hidden,
config = bottomSheetData.toHpBottomSheetConfig(viewModel = screenOverlayVM),
content = bottomSheetData.bottomSheetUiTronData?.toHpBottomSheetContent(),
)
}
}
}

View File

@@ -0,0 +1,61 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.handler.drag
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.NudgeDraggedStateUpdateType
enum class DragAnchors {
Normal,
Dragged,
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun HandleAnchorDragStateChange(
nudgeDraggedStateData: () -> NudgeDraggedStateData?,
anchoredDraggableState: AnchoredDraggableState<DragAnchors>,
) {
LaunchedEffect(nudgeDraggedStateData()) {
if (nudgeDraggedStateData()?.type == NudgeDraggedStateUpdateType.TOGGLE) {
if (nudgeDraggedStateData()?.state == NudgeDraggedState.DRAGGED) {
anchoredDraggableState.animateTo(DragAnchors.Dragged)
} else if (nudgeDraggedStateData()?.state == NudgeDraggedState.IDLE) {
anchoredDraggableState.animateTo(DragAnchors.Normal)
}
}
}
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun UpdateNudgeDraggedStateOnDrag(
anchoredDraggableState: AnchoredDraggableState<DragAnchors>,
onNudgeUiStateChange: (uiState: NudgeDraggedState) -> Unit,
nudgeDraggedState: () -> NudgeDraggedState?,
) {
LaunchedEffect(anchoredDraggableState.currentValue) {
if (
nudgeDraggedState() == NudgeDraggedState.DRAGGED &&
anchoredDraggableState.currentValue == DragAnchors.Normal
) {
onNudgeUiStateChange(NudgeDraggedState.IDLE)
} else if (
nudgeDraggedState() == NudgeDraggedState.IDLE &&
anchoredDraggableState.currentValue == DragAnchors.Dragged
) {
onNudgeUiStateChange(NudgeDraggedState.DRAGGED)
}
}
}

View File

@@ -0,0 +1,35 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.handler.navigation
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.base.utils.orFalse
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable
fun HandleNudgeCtaNavigation(screenOverlayVM: ScreenOverlayVM, activity: HomePageActivity) {
LaunchedEffect(Unit) {
screenOverlayVM.redirectionCtaData.collect { ctaData ->
ctaData?.let { navigateCtaDeepLink(activity, it) }
}
}
}
private fun navigateCtaDeepLink(activity: HomePageActivity, ctaData: CtaData) {
DeepLinkManager.getDeepLinkListener()
?.navigateTo(
activity = activity,
ctaData = ctaData,
finish = ctaData.finish.orFalse(),
clearTask = ctaData.clearTask.orFalse(),
)
}

View File

@@ -0,0 +1,44 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.initializer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.naviapp.appsettings.utils.hasNotificationPermission
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.nudge.handler.action.HandleNudgeActions
import com.naviapp.screenOverlay.nudge.handler.bottomsheet.HandleNudgeBottomSheetState
import com.naviapp.screenOverlay.nudge.handler.navigation.HandleNudgeCtaNavigation
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable
fun InitScreenOverlayComponents(
screenOverlayVM: ScreenOverlayVM,
sharedVM: SharedVM,
selectedTabId: String,
hpStates: () -> HpStates,
activity: HomePageActivity,
) {
val bottomSheetState by screenOverlayVM.bottomSheetState.collectAsStateWithLifecycle()
HandleNudgeActions(
viewModel = screenOverlayVM,
naeScreenName = activity.screenName,
isNotificationPermissionEnabled = hasNotificationPermission(activity),
)
HandleNudgeCtaNavigation(screenOverlayVM = screenOverlayVM, activity = activity)
HandleNudgeBottomSheetState(
bottomSheetState = bottomSheetState,
selectedTabId = selectedTabId,
hpStates = hpStates,
sharedVM = sharedVM,
screenOverlayVM = screenOverlayVM,
)
}

View File

@@ -1,60 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.model
import androidx.compose.runtime.Immutable
import com.navi.common.basemvi.UiEffect
import com.navi.common.basemvi.UiEvent
import com.navi.common.basemvi.UiState
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
@Immutable
data class NudgeState(
val isNudgeListVisible: Boolean,
val isNudgeExpanded: Boolean,
val nudgeList: List<NudgeData>?,
val staticNudgeData: StaticNudgeData? = null,
) : UiState {
companion object {
val initialState =
NudgeState(
isNudgeListVisible = false,
isNudgeExpanded = false,
nudgeList = null,
staticNudgeData = null,
)
}
}
@Immutable
sealed interface NudgeEffect : UiEffect {
data class OnDeleteNudge(
val nudgeId: String,
val overlayItemTransitionState: OverlayItemTransitionState,
) : NudgeEffect
data class TriggerClickStreamEvent(
val eventName: String,
val eventValue: Map<String, String>? = null,
) : NudgeEffect
}
@Immutable
sealed interface NudgeEvent : UiEvent {
data class UpdateNudgeExpandedState(val expandState: Boolean) : NudgeEvent
data class UpdateNudgeList(val data: List<NudgeData>?) : NudgeEvent
data class DeleteNudge(val nudgeId: String) : NudgeEvent
data class UpdateNudgeUiState(val nudgeId: String, val state: NudgeUiState) : NudgeEvent
data class UpdateStaticNudgeData(val data: StaticNudgeData?) : NudgeEvent
data class UpdateStaticNudgeUiState(val nudgeUiState: StaticNudgeUiState) : NudgeEvent
}

View File

@@ -1,37 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.model
import androidx.compose.runtime.Stable
import com.google.gson.annotations.SerializedName
import com.navi.uitron.model.UiTronResponse
@Stable
data class NudgeListData(@SerializedName("contentList") val nudgeList: List<NudgeData>? = null)
@Stable
data class NudgeData(
@SerializedName("nudgeId") val nudgeId: String? = null,
@SerializedName("nudgeData") val nudgeUitronData: UiTronResponse? = null,
@SerializedName("isNudgeDragEnabled") val isNudgeDragEnabled: Boolean? = null,
@SerializedName("nudgeStatus") val nudgeStatus: NudgeStatus = NudgeStatus.DEFAULT,
val nudgeUiState: NudgeUiState = NudgeUiState.IDLE,
)
// Don't remove UnUsed Status From Here
enum class NudgeStatus {
SUCCESS,
IN_PROGRESS,
DEFAULT,
DELETED,
}
enum class NudgeUiState {
IDLE,
DRAGGED,
}

View File

@@ -1,104 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.reducer
import com.navi.common.basemvi.BaseReducer
import com.naviapp.screenOverlay.nudge.model.NudgeData
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.model.NudgeStatus
import com.naviapp.screenOverlay.nudge.model.NudgeUiState
class NudgeReducer : BaseReducer<NudgeState, NudgeEvent> {
override fun reduce(previousState: NudgeState, event: NudgeEvent): NudgeState {
return when (event) {
/**
* Handles the UpdateNudgeExpandedState event. Updates the expanded state of the nudge
* container and sets the UI state of any currently dragged nudge to IDLE.
*/
is NudgeEvent.UpdateNudgeExpandedState -> {
previousState.copy(
isNudgeExpanded = event.expandState,
nudgeList =
previousState.nudgeList?.map { nudge ->
if (nudge.nudgeUiState == NudgeUiState.DRAGGED) {
nudge.copy(nudgeUiState = NudgeUiState.IDLE)
} else {
nudge
}
},
)
}
is NudgeEvent.UpdateNudgeList -> {
previousState.copy(nudgeList = event.data, isNudgeListVisible = true)
}
is NudgeEvent.UpdateNudgeUiState -> {
previousState.copy(
nudgeList =
previousState.nudgeList?.map { nudge ->
getUpdatedNudgeUiState(nudge, event)
}
)
}
is NudgeEvent.DeleteNudge -> {
previousState.copy(
nudgeList =
previousState.nudgeList?.map { nudge ->
if (nudge.nudgeId == event.nudgeId) {
nudge.copy(nudgeStatus = NudgeStatus.DELETED)
} else {
nudge
}
}
)
}
is NudgeEvent.UpdateStaticNudgeData -> {
previousState.copy(staticNudgeData = event.data)
}
is NudgeEvent.UpdateStaticNudgeUiState -> {
previousState.copy(
staticNudgeData =
previousState.staticNudgeData?.copy(nudgeUiState = event.nudgeUiState)
)
}
}
}
/**
* Determines the updated UI state for a given nudge element based on a UI state update event.
* Handles the following scenarios:* - If the event targets the given nudge and it's currently
* being dragged, set its state to IDLE.
* - If any other nudge is currently being dragged and the event indicates a new drag, set the
* other nudge's state to IDLE.
*
* @param nudge The current nudge element data.
* @param event The UI state update event containing the new state and target nudge ID.
* @return The updated nudgeData with the appropriate UI state.
*/
private fun getUpdatedNudgeUiState(
nudge: NudgeData,
event: NudgeEvent.UpdateNudgeUiState,
): NudgeData {
return when {
nudge.nudgeId == event.nudgeId ->
nudge.copy(
nudgeUiState =
if (
event.state == NudgeUiState.DRAGGED &&
nudge.nudgeUiState == NudgeUiState.DRAGGED
)
NudgeUiState.IDLE
else event.state
)
nudge.nudgeUiState == NudgeUiState.DRAGGED && event.state == NudgeUiState.DRAGGED ->
nudge.copy(nudgeUiState = NudgeUiState.IDLE)
else -> nudge
}
}
}

View File

@@ -1,71 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.utils.NudgeColor.borderColor
import com.naviapp.screenOverlay.nudge.utils.NudgeRootUtils.HandleExpandedStateByListSize
import com.naviapp.screenOverlay.nudge.utils.NudgeRootUtils.filterDeletedNudges
import com.naviapp.screenOverlay.nudge.utils.NudgeRootUtils.nudgeContainerEnterTransition
/**
* Nudge Container, managing its visibility based on the nudge visibility state in the
* `NudgeReducer.NudgeState`. It orchestrates the display of both collapsed and expanded nudge
* states within the container.
*
* @param state The current state of the nudge system, controlling the container's visibility and
* providing nudge data.
* @param nudgeUitronRenderer Composable function responsible for rendering the Uitron content of
* nudge elements.
* @param onNudgeEvent Callback function to dispatch NudgeReducer events triggered by user
* interactions.
*/
@Composable
fun NudgeContainer(
state: () -> NudgeState,
nudgeUitronRenderer: @Composable (UiTronResponse?) -> Unit,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
) {
AnimatedVisibility(
visible = state().isNudgeListVisible,
enter = nudgeContainerEnterTransition,
exit = ExitTransition.None,
) {
Box(contentAlignment = Alignment.BottomCenter) {
if (state().isNudgeExpanded.not()) {
NudgeContainerCollapsedState(
nudgeState = state(),
nudgeUitronRenderer = nudgeUitronRenderer,
onNudgeEvent = onNudgeEvent,
onNudgeEffect = onNudgeEffect,
)
}
NudgeContainerExpandedState(
nudgeState = state(),
nudgeUitronRenderer = nudgeUitronRenderer,
onNudgeEvent = onNudgeEvent,
onNudgeEffect = onNudgeEffect,
)
if (filterDeletedNudges(state()).isNotEmpty()) {
HorizontalDivider(color = borderColor, thickness = 1.dp)
}
}
}
HandleExpandedStateByListSize(state, onNudgeEvent)
}

View File

@@ -1,106 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.keyframes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.navi.base.utils.orZero
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.NudgeUI
import com.naviapp.screenOverlay.nudge.utils.NudgeCollapsedStateUtils.collapsedContainerExitTransition
import com.naviapp.screenOverlay.nudge.utils.NudgeCollapsedStateUtils.collapsedContainerRankChangeTransition
import com.naviapp.screenOverlay.nudge.utils.NudgeRootUtils.filterDeletedNudges
import com.naviapp.screenOverlay.utils.NudgeConstants.NudgeAnimationConstants.COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION
import com.naviapp.screenOverlay.utils.NudgeConstants.NudgeAnimationConstants.REPLACE_NUDGE_IN_COLLAPSED_STATE
/**
* The collapsed state for nudge container, showing a single nudge element and a middle pill if more
* are available. It manages the transition between different nudge elements in the collapsed state
* and handles the expansion to the full nudge list.
*
* @param nudgeState The current state of the nudge system, containing information about available
* nudges.
* @param nudgeUitronRenderer Composable function responsible for rendering the Uitron content of
* individual nudge elements.
* @param onNudgeEvent Callback function to dispatch NudgeReducer events, such as expanding the
* nudge list.
*/
@Composable
fun NudgeContainerCollapsedState(
nudgeState: NudgeState,
nudgeUitronRenderer: @Composable (UiTronResponse?) -> Unit,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
) {
val filteredNudge = filterDeletedNudges(nudgeState).firstOrNull()
if (filteredNudge != null) {
Box(Modifier.fillMaxWidth()) {
AnimatedContent(
targetState = filteredNudge,
contentKey = { it.nudgeId },
transitionSpec = {
getCollapsedNudgeTransitionSpec(
nudgeState,
this.initialState.nudgeId.orEmpty(),
) using
SizeTransform { old, new ->
keyframes {
IntSize(new.width, old.height) at
COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION
durationMillis = COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION
}
}
},
label = REPLACE_NUDGE_IN_COLLAPSED_STATE,
) {
it.let {
NudgeUI(
modifier = Modifier.padding(top = 13.dp),
nudgeUitronRenderer = nudgeUitronRenderer,
nudgeData = it,
onNudgeEvent = onNudgeEvent,
enableFrontLayerElevation = true,
onNudgeEffect = onNudgeEffect,
)
}
}
NudgeMiddlePill(
nudgeCount = filterDeletedNudges(nudgeState).size.orZero(),
onClick = {
onNudgeEvent(NudgeEvent.UpdateNudgeExpandedState(true))
onNudgeEffect(
NudgeEffect.TriggerClickStreamEvent(eventName = "nudge_middle_pill_clicked")
)
},
)
}
}
}
private fun getCollapsedNudgeTransitionSpec(
nudgeState: NudgeState,
initialNudgeId: String,
): ContentTransform {
return if (filterDeletedNudges(nudgeState).map { it.nudgeId }.contains(initialNudgeId)) {
collapsedContainerRankChangeTransition
} else {
collapsedContainerExitTransition
}
}

View File

@@ -1,153 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
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.navi.design.theme.FF191919
import com.navi.naviwidgets.composewidget.reusable.whiteColor
import com.navi.naviwidgets.extensions.NaviText
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.getImageFromIconCode
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.model.NudgeStatus
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.NudgeUI
import com.naviapp.screenOverlay.nudge.utils.NudgeExpandedStateUtils.expandedNudgeExitTransition
import com.naviapp.screenOverlay.nudge.utils.NudgeExpandedStateUtils.getExpandedStateEnterTransitionSpec
import com.naviapp.screenOverlay.nudge.utils.NudgeExpandedStateUtils.getExpandedStateExitTransitionSpec
import com.naviapp.screenOverlay.utils.NudgeConstants.YOUR_PAYMENTS
import com.naviapp.utils.IconUtils.ICON_CROSS_BLACK
/**
* Expanded State of Nudge Container, showing a list of available nudge elements. The visibility of
* this UI is controlled by the `nudgeExpandedState` in the `NudgeReducer.NudgeState` and the
* presence of nudge elements.
*
* @param nudgeState The current state of the nudge, containing information about the nudge states
* and available nudges.
* @param nudgeUitronRenderer Composable function responsible for rendering the Uitron content of
* individual nudge elements.
* @param onNudgeEvent Callback function to dispatch NudgeReducer events, such as updating the
* expanded state.
*/
@Composable
fun NudgeContainerExpandedState(
nudgeState: NudgeState,
nudgeUitronRenderer: @Composable (UiTronResponse?) -> Unit,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
) {
val localConfiguration = LocalConfiguration.current
val screenHeight = remember { localConfiguration.screenHeightDp.dp }
AnimatedVisibility(
modifier =
Modifier.fillMaxWidth()
.heightIn(min = 0.dp, max = screenHeight - 200.dp)
.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
.background(whiteColor)
.clickable(enabled = false) {},
visible = nudgeState.isNudgeExpanded,
enter = getExpandedStateEnterTransitionSpec,
exit = getExpandedStateExitTransitionSpec,
) {
Column {
NudgeExpandedStateHeader {
onNudgeEvent(NudgeEvent.UpdateNudgeExpandedState(expandState = false))
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
nudgeState.nudgeList?.forEach { nudgeData ->
key(nudgeData.nudgeId) {
val nudgeVisible by
remember(nudgeData.nudgeStatus) {
derivedStateOf { nudgeData.nudgeStatus != NudgeStatus.DELETED }
}
AnimatedVisibility(
visible = nudgeVisible,
enter = EnterTransition.None,
exit = expandedNudgeExitTransition,
) {
NudgeUI(
nudgeUitronRenderer = nudgeUitronRenderer,
nudgeData = nudgeData,
onNudgeEvent = onNudgeEvent,
onNudgeEffect = onNudgeEffect,
)
}
}
}
}
}
}
}
/**
* The header for the expanded nudge state UI, including a title and a close icon.
*
* @param onClick Callback function to be invoked when the close icon is clicked.
*/
@Composable
private fun NudgeExpandedStateHeader(onClick: () -> Unit) {
Row(
modifier =
Modifier.fillMaxWidth()
.background(whiteColor)
.padding(top = 12.dp, bottom = 12.dp, start = 16.dp, end = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
NaviText(
text = YOUR_PAYMENTS,
color = FF191919,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
style = TextStyle(fontSize = 16.sp, fontFamily = naviFontFamily),
)
Box(
modifier = Modifier.size(28.dp).clip(CircleShape).clickable(onClick = { onClick() }),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(getImageFromIconCode(ICON_CROSS_BLACK)),
contentDescription = null,
)
}
}
}

View File

@@ -1,110 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.coin.theme.borderAlt
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
import com.navi.naviwidgets.composewidget.reusable.whiteColor
import com.navi.naviwidgets.extensions.NaviText
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.CHEVRON_UP_BLACK
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.getImageFromIconCode
import com.naviapp.screenOverlay.nudge.utils.NudgeCollapsedStateUtils.middlePillContentTransitionSpec
import com.naviapp.screenOverlay.nudge.utils.NudgeCollapsedStateUtils.middlePillEnterTransitionSpec
import com.naviapp.screenOverlay.nudge.utils.NudgeCollapsedStateUtils.middlePillExitTransitionSpec
import com.naviapp.screenOverlay.nudge.utils.NudgeColor.darkTextColor
import com.naviapp.screenOverlay.utils.NudgeConstants.MORE
import com.naviapp.screenOverlay.utils.NudgeConstants.NudgeAnimationConstants.MIDDLE_PILL_CONTENT_ANIMATION
/**
* Middle pill on collapsed nudge state depicting the number of nudge Elements. The pill's
* visibility is determined by the `nudgeCount`, becoming visible when there are more than one nudge
* elements.
*
* @param nudgeCount The total number of nudge elements available.
* @param onClick Callback function to be invoked when the middle pill is clicked.
*/
@Composable
fun BoxScope.NudgeMiddlePill(nudgeCount: Int, onClick: () -> Unit) {
val nudgeMiddlePillVisibility by remember(nudgeCount) { derivedStateOf { nudgeCount > 1 } }
AnimatedVisibility(
modifier = Modifier.align(Alignment.TopCenter),
visible = nudgeMiddlePillVisibility,
enter = middlePillEnterTransitionSpec,
exit = middlePillExitTransitionSpec,
) {
Card(
modifier = Modifier,
shape = RoundedCornerShape(50),
border = BorderStroke(width = 1.dp, color = borderAlt),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
colors = CardDefaults.cardColors(containerColor = whiteColor),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.clip(RoundedCornerShape(50))
.heightIn(min = 24.dp)
.clickable(onClick = { onClick() })
.padding(start = 14.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
) {
AnimatedContent(
targetState = nudgeCount,
transitionSpec = { middlePillContentTransitionSpec },
label = MIDDLE_PILL_CONTENT_ANIMATION,
) {
NaviText(
text = "+${(it - 1).coerceAtMost(9)} ",
color = darkTextColor,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
style = TextStyle(fontSize = 12.sp, fontFamily = naviFontFamily),
)
}
NaviText(
text = MORE,
color = darkTextColor,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
style = TextStyle(fontSize = 12.sp, fontFamily = naviFontFamily),
)
Spacer(Modifier.width(4.dp))
Image(
modifier = Modifier.size(12.dp),
painter = painterResource(id = getImageFromIconCode(CHEVRON_UP_BLACK)),
contentDescription = null,
)
}
}
}
}

View File

@@ -18,8 +18,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
import com.naviapp.screenOverlay.nudge.model.StaticNudgeData
import com.naviapp.screenOverlay.nudge.model.StaticNudgeUiState
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeData
import com.naviapp.screenOverlay.nudge.domain.model.data.StaticNudgeUiState
@Composable
fun StaticNudgeContainer(state: StaticNudgeData?, widgetRenderer: @Composable () -> Unit) {

View File

@@ -0,0 +1,104 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.container
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.nudge.ui.container.collapsed.NudgeContainerCollapsedState
import com.naviapp.screenOverlay.nudge.ui.container.collapsed.NudgeMiddlePillContainer
import com.naviapp.screenOverlay.nudge.ui.container.expanded.NudgeContainerExpandedState
import com.naviapp.screenOverlay.nudge.ui.container.expanded.NudgeExpandedStateHeader
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeEnterAnimation
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeExitAnimation
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeConstants
import com.naviapp.screenOverlay.nudge.utils.extensions.filterDeletedNudges
import com.naviapp.screenOverlay.nudge.utils.root.HandleExpandedStateByListSize
@Composable
fun NudgeContainer(
state: () -> NudgeState,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
nudgeUitronRenderer: @Composable (response: UiTronResponse?, modifier: Modifier) -> Unit,
) {
val singleNudgeHeight = remember { mutableIntStateOf(0) }
val descriptiveViewEnabled =
remember(state().descriptiveViewEnabled) { state().descriptiveViewEnabled }
val nudgeList = remember(state().nudgeList) { state().nudgeList }
val selectedDismissibleActionIndexMap =
remember(state().selectedDismissibleActionIndexMap) {
state().selectedDismissibleActionIndexMap
}
val nudgeDraggedStateMap =
remember(state().nudgeDraggedStateMap) { state().nudgeDraggedStateMap }
AnimatedVisibility(
visible = state().isNudgeListVisible,
enter = nudgeEnterAnimation(),
exit = nudgeExitAnimation(),
) {
Column {
NudgeExpandedStateHeader(state().descriptiveViewEnabled) {
onNudgeEvent(NudgeEvent.ToggleDescriptiveView(false))
}
Box(contentAlignment = Alignment.BottomCenter) {
NudgeContainerCollapsedState(
nudgeList = nudgeList,
isDescriptiveViewEnabled = descriptiveViewEnabled,
onHeightChange = { singleNudgeHeight.intValue = it },
onNudgeEvent = onNudgeEvent,
onNudgeEffect = onNudgeEffect,
nudgeUitronRenderer = nudgeUitronRenderer,
selectedDismissibleActionIndexMap = selectedDismissibleActionIndexMap,
nudgeDraggedStateMap = nudgeDraggedStateMap,
)
NudgeContainerExpandedState(
nudgeList = nudgeList,
isDescriptiveViewEnabled = descriptiveViewEnabled,
singleNudgeHeight = singleNudgeHeight.intValue,
onNudgeEvent = onNudgeEvent,
onNudgeEffect = onNudgeEffect,
nudgeUitronRenderer = nudgeUitronRenderer,
selectedDismissibleActionIndexMap = selectedDismissibleActionIndexMap,
nudgeDraggedStateMap = nudgeDraggedStateMap,
)
NudgeMiddlePillContainer(
nudgeDraggedStateMap = state().nudgeDraggedStateMap,
isDescriptiveViewEnabled = descriptiveViewEnabled,
nudgeList = nudgeList.filterDeletedNudges(),
onClick = {
onNudgeEvent(NudgeEvent.ToggleDescriptiveView(true))
onNudgeEffect(
NudgeEffect.TriggerClickStreamEvent(
eventName = NudgeConstants.MIDDLE_PILL_CLICKED
)
)
},
)
}
}
}
HandleExpandedStateByListSize(state, onNudgeEvent)
}

View File

@@ -0,0 +1,71 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.container.collapsed
import androidx.compose.animation.AnimatedContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
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
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.NudgeUI
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeCollapsedStateTransitionSpec
import com.naviapp.screenOverlay.nudge.utils.extensions.filterDeletedNudges
import com.naviapp.screenOverlay.nudge.utils.extensions.getFirstNonDismissedNudge
import com.naviapp.screenOverlay.nudge.utils.modifier.BorderSides
@Composable
fun NudgeContainerCollapsedState(
nudgeList: List<NudgeData>?,
isDescriptiveViewEnabled: Boolean,
onHeightChange: (Int) -> Unit,
nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData>,
selectedDismissibleActionIndexMap: Map<NudgeId, Int>,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
nudgeUitronRenderer: @Composable (response: UiTronResponse?, modifier: Modifier) -> Unit,
) {
val nudgeData =
remember(nudgeList.getFirstNonDismissedNudge()) { nudgeList.getFirstNonDismissedNudge() }
val showBorder =
remember(nudgeData?.nudgeStatus) { nudgeData?.nudgeStatus != NudgeStatus.SUCCESS }
AnimatedContent(
modifier = Modifier.onSizeChanged { onHeightChange(it.height) },
targetState = nudgeData,
contentKey = { it?.nudgeId },
contentAlignment = Alignment.BottomCenter,
transitionSpec = {
nudgeCollapsedStateTransitionSpec(
isDescriptiveViewEnabled = isDescriptiveViewEnabled,
filteredNudgeList = nudgeList.filterDeletedNudges(),
initialNudgeId = this.initialState?.nudgeId.toString(),
)
},
) { data ->
NudgeUI(
modifier = Modifier,
nudgeUitronRenderer = nudgeUitronRenderer,
nudgeData = data,
nudgeDraggedStateData = { nudgeDraggedStateMap[data?.nudgeId] },
onNudgeEvent = onNudgeEvent,
elevateFrontLayer = true,
onNudgeEffect = onNudgeEffect,
borderSides = if (showBorder) BorderSides.VERTICAL else BorderSides.NONE,
selectedDismissibleActionIndex = selectedDismissibleActionIndexMap[data?.nudgeId],
)
}
}

View File

@@ -0,0 +1,221 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.container.collapsed
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
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.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.NudgeDraggedState
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeMiddlePillEnterAnimationSpec
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeMiddlePillExitAnimationSpec
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants.defaultAnimationSpec
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeColor.darkTextColor
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeColor.shadowColor
import com.naviapp.screenOverlay.utils.NudgeConstants.UNPAID
@Composable
fun BoxScope.NudgeMiddlePillContainer(
nudgeList: List<NudgeData>,
isDescriptiveViewEnabled: Boolean,
onClick: () -> Unit,
nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData>,
) {
val firstNudge = nudgeList.firstOrNull()
var previousNudgeId by remember { mutableStateOf(firstNudge?.nudgeId) }
val isFirstNudgeReplaced = firstNudge?.nudgeId != previousNudgeId
LaunchedEffect(firstNudge?.nudgeId) { previousNudgeId = firstNudge?.nudgeId }
val visibilityBaseCondition by
remember(nudgeList, isDescriptiveViewEnabled) {
derivedStateOf { nudgeList.size > 1 && isDescriptiveViewEnabled.not() }
}
val visibilityConditionDueToUiState by
remember(nudgeDraggedStateMap[firstNudge?.nudgeId]?.state) {
derivedStateOf {
nudgeDraggedStateMap[firstNudge?.nudgeId]?.state == NudgeDraggedState.IDLE
}
}
val subsequentTopNudgeIcons =
remember(nudgeList) { nudgeList.drop(1).take(2).mapNotNull { it.nudgeIconUrl } }
val nudgeCount = remember(nudgeList) { (nudgeList.size - 1).coerceAtMost(9) }
val defaultElevation = remember {
Animatable(if (visibilityConditionDueToUiState || visibilityBaseCondition) 1f else 0f)
}
LaunchedEffect(visibilityConditionDueToUiState, visibilityBaseCondition) {
defaultElevation.animateTo(
if (visibilityConditionDueToUiState && visibilityBaseCondition) 1f else 0f,
defaultAnimationSpec(),
)
}
Column(
modifier =
Modifier.graphicsLayer { translationY = size.height.times(-0.67f) }
.shadow(
elevation = (defaultElevation.value * 4).dp,
shape = CircleShape,
clip = true,
ambientColor = shadowColor.copy(defaultElevation.value * 0.42f),
spotColor = shadowColor.copy(defaultElevation.value * 0.42f),
)
.align(Alignment.TopCenter)
) {
AnimatedVisibility(
visible = visibilityBaseCondition,
enter = nudgeMiddlePillEnterAnimationSpec(NudgeAnimationConstants.DURATION),
exit = ExitTransition.None,
) {
AnimatedVisibility(
visible = visibilityConditionDueToUiState,
enter =
nudgeMiddlePillEnterAnimationSpec(
if (isFirstNudgeReplaced)
NudgeAnimationConstants.COLLAPSED_NUDGE_ELEMENT_TRANSITION_DELAY
else 0
),
exit = nudgeMiddlePillExitAnimationSpec(),
content = nudgeMiddlePillContent(subsequentTopNudgeIcons, nudgeCount, onClick),
)
}
}
}
private fun nudgeMiddlePillContent(
iconUrlList: List<String>,
nudgeCount: Int,
onClick: () -> Unit,
): @Composable AnimatedVisibilityScope.() -> Unit = {
Box(
modifier =
Modifier.border(width = 0.9.dp, color = darkTextColor, shape = CircleShape)
.padding(0.7.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.graphicsLayer {
clip = true
shape = CircleShape
}
.background(Color.White)
.heightIn(min = 24.dp)
.clickable(onClick = { onClick() })
.padding(start = 4.dp, end = 8.dp, top = 4.dp, bottom = 4.dp),
) {
MiddlePillIllustration(iconUrlList = iconUrlList)
ElexText(
text = "+$nudgeCount",
color = darkTextColor,
fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD,
fontSize = 12.sp,
modifier = Modifier.padding(start = if (iconUrlList.isEmpty()) 8.dp else 0.dp),
)
ElexText(
text = UNPAID,
color = darkTextColor,
fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD,
fontSize = 12.sp,
)
Spacer(Modifier.width(2.dp))
Image(
modifier = Modifier.size(12.dp),
painter = painterResource(id = R.drawable.cheveron_up),
contentDescription = null,
)
}
}
}
@Composable
fun MiddlePillIllustration(iconUrlList: List<String>) {
if (iconUrlList.isEmpty()) return
val iconModifier = remember {
Modifier.size(20.dp)
.clip(CircleShape)
.background(Color.White, CircleShape)
.border(0.24.dp, Color(0xff6B6B6B), CircleShape)
}
Box(modifier = Modifier.padding(end = if (iconUrlList.size == 1) 4.dp else 12.dp)) {
if (iconUrlList.size == 1) {
ElexAsyncImage(
icon = iconUrlList.first(),
contentDescription = null,
modifier = iconModifier,
)
} else {
ElexAsyncImage(
icon = iconUrlList.first(),
contentDescription = null,
modifier = iconModifier,
contentScale = ContentScale.Crop,
)
ElexAsyncImage(
icon = iconUrlList.last(),
contentDescription = null,
modifier =
Modifier.size(20.dp)
.offset(x = 8.dp, y = 0.dp)
.clip(CircleShape)
.background(Color.White, CircleShape)
.border(0.24.dp, Color(0xff6B6B6B), CircleShape),
contentScale = ContentScale.Crop,
)
}
}
}

View File

@@ -0,0 +1,87 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.container.expanded
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
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.NudgeDraggedStateData
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
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
import com.naviapp.screenOverlay.nudge.ui.nudgeUI.NudgeUI
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeDescriptiveViewEnterAnimation
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeDescriptiveViewExitAnimation
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeExitTransitionInDescriptiveView
import com.naviapp.screenOverlay.nudge.utils.modifier.BorderSides
@Composable
fun NudgeContainerExpandedState(
nudgeList: List<NudgeData>?,
isDescriptiveViewEnabled: Boolean,
singleNudgeHeight: Int,
nudgeDraggedStateMap: Map<NudgeId, NudgeDraggedStateData>,
selectedDismissibleActionIndexMap: Map<String, Int>,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
nudgeUitronRenderer: @Composable (UiTronResponse?, Modifier) -> Unit,
) {
val localConfiguration = LocalConfiguration.current
val maxViewHeight = remember { localConfiguration.screenHeightDp.dp - 200.dp }
val scrollState = rememberScrollState()
AnimatedVisibility(
modifier = Modifier.heightIn(min = 0.dp, max = maxViewHeight).verticalScroll(scrollState),
visible = isDescriptiveViewEnabled,
enter = nudgeDescriptiveViewEnterAnimation(singleNudgeHeight),
exit = nudgeDescriptiveViewExitAnimation(singleNudgeHeight),
) {
Column {
nudgeList?.forEachIndexed { index, nudgeData ->
key(nudgeData.nudgeId) {
AnimatedVisibility(
visible = nudgeData.nudgeStatus != NudgeStatus.DISMISSED,
enter = EnterTransition.None,
exit = nudgeExitTransitionInDescriptiveView(),
) {
NudgeUI(
nudgeUitronRenderer = nudgeUitronRenderer,
nudgeData = nudgeData,
onNudgeEvent = onNudgeEvent,
onNudgeEffect = onNudgeEffect,
selectedDismissibleActionIndex =
selectedDismissibleActionIndexMap[nudgeData.nudgeId],
nudgeDraggedStateData = { nudgeDraggedStateMap[nudgeData.nudgeId] },
borderSides =
BorderSides(
top =
index ==
nudgeList.indexOfFirst {
it.nudgeStatus == NudgeStatus.DEFAULT
},
bottom = true,
),
)
}
}
}
}
}
}

View File

@@ -0,0 +1,102 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.container.expanded
import androidx.compose.animation.AnimatedVisibility
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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.design.theme.FF191919
import com.navi.elex.atoms.ElexText
import com.navi.elex.font.FontWeightEnum
import com.navi.naviwidgets.composewidget.reusable.whiteColor
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_CROSS_WHITE_24_24
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.getImageFromIconCode
import com.naviapp.home.ui.theme.color.HomeScreenColor.scrimColor
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeHeaderEnterAnimation
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeHeaderExitAnimation
import com.naviapp.screenOverlay.utils.NudgeConstants.UNPAID_BILLS_RECHARGES
@Composable
fun NudgeExpandedStateHeader(isVisible: Boolean, crossIconOnClick: () -> Unit) {
AnimatedVisibility(
visible = isVisible,
enter = nudgeHeaderEnterAnimation(),
exit = nudgeHeaderExitAnimation(),
) {
Column(verticalArrangement = Arrangement.spacedBy(32.dp)) {
CrossIcon(crossIconOnClick)
HeaderTitle()
}
}
}
@Composable
private fun HeaderTitle() {
Row(
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
.background(whiteColor, RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
.padding(16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
ElexText(
text = UNPAID_BILLS_RECHARGES,
color = FF191919,
fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD,
fontSize = 16.sp,
)
}
}
@Composable
private fun CrossIcon(onClick: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier.size(48.dp).clip(CircleShape).background(scrimColor),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(getImageFromIconCode(ICON_CROSS_WHITE_24_24)),
contentDescription = null,
modifier =
Modifier.size(24.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = true),
onClick = onClick,
),
)
}
}
}

View File

@@ -1,77 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.nudgeUI
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
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.navi.naviwidgets.extensions.NaviText
import com.naviapp.screenOverlay.nudge.utils.NudgeColor.colorRed
import com.naviapp.screenOverlay.nudge.utils.NudgeColor.lightRed
import com.naviapp.screenOverlay.nudge.utils.animateDismissBoxWidth
import com.naviapp.screenOverlay.utils.NudgeConstants.ANCHOR_DRAG_WIDTH
import com.naviapp.screenOverlay.utils.NudgeConstants.DISMISS
/**
* Nudge element back layer, revealed when the front layer is dragged.
*
* @param nudgeHeight The height of the front layer, used to provide the height of the back layer.
* @param frontLayerVisible Indicates whether the front layer is visible or not.
* @param onDismissed Invoked upon clicking the dismiss row to hide the front layer.
* @param deleteNudge Invoked upon completion of the dismiss animation to initiate the deletion of
* the nudge element.
*/
@Composable
fun NudgeBackLayer(
nudgeHeight: Dp,
frontLayerVisible: Boolean,
onDismissed: () -> Unit,
deleteNudge: () -> Unit,
) {
Row(
Modifier.height(nudgeHeight).fillMaxWidth().background(lightRed).clickable {
onDismissed()
},
verticalAlignment = Alignment.CenterVertically,
) {
Box(
Modifier.width(animateDismissBoxWidth(frontLayerVisible, deleteNudge).value)
.fillMaxHeight()
)
NaviText(
text = DISMISS,
color = colorRed,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style =
TextStyle(
fontSize = 12.sp,
fontFamily = naviFontFamily,
textAlign = TextAlign.Center,
),
modifier = Modifier.width(ANCHOR_DRAG_WIDTH),
)
}
}

View File

@@ -1,103 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.nudgeUI
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import com.naviapp.screenOverlay.nudge.model.NudgeUiState
import com.naviapp.screenOverlay.nudge.utils.DragAnchors
import com.naviapp.screenOverlay.nudge.utils.HandleAnchorDragStateChange
import com.naviapp.screenOverlay.nudge.utils.UpdateNudgeUiStateOnDrag
import com.naviapp.screenOverlay.nudge.utils.getFrontLayerExitTransitionSpec
import com.naviapp.screenOverlay.utils.NudgeConstants.ANCHOR_DRAG_WIDTH
import kotlin.math.roundToInt
/**
* The front layer of a nudge element, supporting anchored dragging for dismissal.
*
* @param modifier Modifier for customizing the appearance and behavior of the front layer.
* @param frontLayerVisibility Controls the visibility of the front layer.
* @param nudgeUiState Provides access to the current UI state of the nudge element.
* @param frontLayerContent Composable content to be displayed within the front layer.
* @param onNudgeUiStateChange Callback invoked when the nudge element's UI state needs to be
* updated.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NudgeFrontLayer(
modifier: Modifier,
frontLayerVisibility: Boolean,
isNudgeDragEnabled: Boolean,
nudgeUiState: () -> NudgeUiState?,
frontLayerContent: @Composable () -> Unit,
onNudgeUiStateChange: (state: NudgeUiState) -> Unit,
) {
val density = LocalDensity.current
val anchoredDragWidth = remember { with(density) { (ANCHOR_DRAG_WIDTH).toPx() } }
val anchoredDraggableState = remember {
AnchoredDraggableState(
initialValue = DragAnchors.Normal,
anchors =
DraggableAnchors {
DragAnchors.Normal at 0f
DragAnchors.Dragged at anchoredDragWidth
},
positionalThreshold = { distance: Float -> distance * 0.5f },
velocityThreshold = { anchoredDragWidth },
snapAnimationSpec = tween(),
decayAnimationSpec = exponentialDecay(),
)
}
AnimatedVisibility(
visible = frontLayerVisibility,
enter = EnterTransition.None,
exit = getFrontLayerExitTransitionSpec,
) {
Box(
modifier
.fillMaxWidth()
.offset {
IntOffset(x = -anchoredDraggableState.requireOffset().roundToInt(), y = 0)
}
.anchoredDraggable(
state = anchoredDraggableState,
orientation = Orientation.Horizontal,
enabled = isNudgeDragEnabled,
reverseDirection = true,
)
) {
frontLayerContent()
}
}
HandleAnchorDragStateChange(
nudgeUiState = nudgeUiState,
anchoredDraggableState = anchoredDraggableState,
)
UpdateNudgeUiStateOnDrag(
nudgeUiState = nudgeUiState,
anchoredDraggableState = anchoredDraggableState,
onNudgeUiStateChange = onNudgeUiStateChange,
)
}

View File

@@ -7,99 +7,142 @@
package com.naviapp.screenOverlay.nudge.ui.nudgeUI
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.navi.base.utils.orTrue
import com.navi.base.utils.orZero
import com.navi.naviwidgets.composewidget.reusable.whiteColor
import com.navi.pay.utils.pxToDp
import com.navi.uitron.model.UiTronResponse
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.nudge.model.NudgeData
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeStatus
import com.naviapp.screenOverlay.nudge.utils.HandleNudgeStatusChange
import com.naviapp.screenOverlay.nudge.utils.NudgeColor.borderColor
import com.navi.uitron.utils.hexToComposeColor
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
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.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.dismissibleActionHorizontalPadding
import com.naviapp.screenOverlay.nudge.utils.measurement.getMaxWidthForDismissibleActionItem
import com.naviapp.screenOverlay.nudge.utils.modifier.BorderSides
import com.naviapp.screenOverlay.nudge.utils.modifier.DirectionalBorderModifier
import com.naviapp.screenOverlay.nudge.utils.root.HandleNudgeStatusChange
/**
* Nudge element, managing its front and back layers, and handling interactions.
*
* @param modifier Modifier for customizing the appearance and behavior of the root container.
* @param nudgeData Nudge element data.
* @param nudgeUitronRenderer Composable function responsible for rendering the Uitron content of
* the nudge element.
* @param onNudgeEvent Callback function to dispatchNudgeReducer events.
*/
@Composable
fun NudgeUI(
modifier: Modifier = Modifier,
nudgeData: NudgeData,
nudgeUitronRenderer: @Composable (UiTronResponse?) -> Unit,
borderSides: BorderSides,
nudgeData: NudgeData?,
nudgeDraggedStateData: () -> NudgeDraggedStateData?,
selectedDismissibleActionIndex: Int?,
elevateFrontLayer: Boolean = false,
onNudgeEvent: (event: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
enableFrontLayerElevation: Boolean = false,
nudgeUitronRenderer: @Composable (response: UiTronResponse?, modifier: Modifier) -> Unit,
) {
var frontLayerVisible by remember { mutableStateOf(true) }
var frontLayerHeight by remember { mutableIntStateOf(0) }
if (nudgeData == null) return
val density = LocalDensity.current
val nudgeId = remember(nudgeData.nudgeId) { nudgeData.nudgeId.orEmpty() }
var frontLayerVisibility by remember { mutableStateOf(true) }
var frontLayerHeight by remember(nudgeId) { mutableIntStateOf(0) }
var isFrontLayerHeightMeasured by remember(nudgeId) { mutableStateOf(false) }
val maxWidthForDismissibleAction =
getMaxWidthForDismissibleActionItem(actions = nudgeData.dismissibleActionList).pxToDp() +
dismissibleActionHorizontalPadding
val contextMenuWidth by
remember(maxWidthForDismissibleAction) {
derivedStateOf {
maxWidthForDismissibleAction.times(nudgeData.dismissibleActionList?.size.orZero())
}
}
Card(
modifier,
modifier = modifier,
elevation =
CardDefaults.cardElevation(
defaultElevation = if (enableFrontLayerElevation) 4.dp else 0.dp
),
shape = RoundedCornerShape(size = 0.dp),
CardDefaults.cardElevation(defaultElevation = if (elevateFrontLayer) 4.dp else 0.dp),
shape = RectangleShape,
colors = CardDefaults.cardColors(containerColor = whiteColor),
) {
Box {
Box(modifier = Modifier.fillMaxWidth()) {
NudgeBackLayer(
nudgeHeight = with(LocalDensity.current) { frontLayerHeight.toDp() },
frontLayerVisible = frontLayerVisible,
onDismissed = { frontLayerVisible = false },
deleteNudge = {
onNudgeEvent(NudgeEvent.DeleteNudge(nudgeId = nudgeData.nudgeId.orEmpty()))
onNudgeEffect(
NudgeEffect.OnDeleteNudge(
nudgeId = nudgeData.nudgeId.orEmpty(),
overlayItemTransitionState = OverlayItemTransitionState.PAUSED,
)
)
modifier =
Modifier.fillMaxWidth()
.height(with(density) { frontLayerHeight.toDp() })
.background(
nudgeData.dismissibleActionList
?.first()
?.actionBackgroundColor
?.hexToComposeColor ?: Color.White
),
dismissibleActionList = nudgeData.dismissibleActionList,
dismissibleActionItemWidth = maxWidthForDismissibleAction,
onActionItemClicked = { index ->
onNudgeEvent(NudgeEvent.DismissibleActionClicked(nudgeId, index))
frontLayerVisibility = false
},
selectedIndex = selectedDismissibleActionIndex,
onDismissNudge = { action ->
onNudgeEvent(NudgeEvent.DismissNudge(nudgeId))
onNudgeEffect(NudgeEffect.OnNudgeDismissAction(nudgeId, action))
},
)
NudgeFrontLayer(
modifier =
Modifier.fillMaxWidth().onGloballyPositioned {
frontLayerHeight = it.size.height
Modifier.fillMaxWidth().onSizeChanged {
if (isFrontLayerHeightMeasured.not()) {
isFrontLayerHeightMeasured = true
frontLayerHeight = it.height
}
},
nudgeUiState = { nudgeData.nudgeUiState },
nudgeDraggedStateData = nudgeDraggedStateData,
isNudgeDragEnabled = nudgeData.isNudgeDragEnabled.orTrue(),
frontLayerVisibility = frontLayerVisible,
frontLayerContent = { nudgeUitronRenderer(nudgeData.nudgeUitronData) },
onNudgeUiStateChange = { state ->
frontLayerVisibility = frontLayerVisibility,
contextMenuWidth = contextMenuWidth,
frontLayerContent = {
nudgeUitronRenderer(
nudgeData.nudgeUitronData,
Modifier.then(DirectionalBorderModifier(borderSides, 1.dp, borderColor)),
)
},
onNudgeDraggedStateChange = { state ->
onNudgeEvent(
NudgeEvent.UpdateNudgeUiState(
nudgeId = nudgeData.nudgeId.orEmpty(),
state = state,
NudgeEvent.UpdateNudgeDraggedStateMap(
key = nudgeId,
value =
NudgeDraggedStateData(
state = state,
type = NudgeDraggedStateUpdateType.DRAG,
),
)
)
},
)
if (nudgeData.nudgeStatus != NudgeStatus.SUCCESS) {
HorizontalDivider(color = borderColor, thickness = 1.dp)
}
}
}
HandleNudgeStatusChange(nudgeData, onNudgeEvent, onNudgeEffect)
}

View File

@@ -0,0 +1,94 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.nudgeUI.action
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.elex.atoms.ElexAsyncImage
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.utils.constants.NudgeAnimationConstants
import com.naviapp.utils.Constants.EMPTY
@Composable
fun NudgeDismissibleActionItem(
item: NudgeDismissibleActionData,
shouldExpand: () -> Boolean,
actionWidth: Dp,
itemContainerWidth: Dp,
onClick: () -> Unit,
dismissAction: () -> Unit,
) {
if (item.actionText == null) return
val configuration = LocalConfiguration.current
val maxScreenWidth = remember { configuration.screenWidthDp.dp }
Row(
modifier =
Modifier.fillMaxHeight()
.background(item.actionBackgroundColor?.hexToComposeColor ?: Color.Gray)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = onClick,
),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.width(actionWidth).padding(vertical = 13.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
ElexAsyncImage(
icon = item.actionIllustration,
modifier = Modifier.size(20.dp),
contentDescription = EMPTY,
placeholder = NaviWidgetR.drawable.image_placeholder_small,
)
ElexText(
text = item.actionText,
color = item.actionTextColor?.hexToComposeColor ?: Color.White,
fontSize = 12.sp,
lineHeight = 18.sp,
fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD,
)
}
Spacer(
modifier =
Modifier.animateContentSize(
animationSpec = NudgeAnimationConstants.defaultAnimationSpec(),
finishedListener = { _, _ -> dismissAction() },
)
.width(if (shouldExpand()) maxScreenWidth - itemContainerWidth else 0.dp)
)
}
}

View File

@@ -0,0 +1,87 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.nudgeUI.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.ui.nudgeUI.action.NudgeDismissibleActionItem
import com.naviapp.screenOverlay.nudge.utils.animation.nudgeDismissibleActionExitFadeAnimation
@Composable
fun NudgeBackLayer(
modifier: Modifier,
selectedIndex: Int?,
dismissibleActionItemWidth: Dp = 0.dp,
dismissibleActionList: List<NudgeDismissibleActionData>?,
onActionItemClicked: (Int) -> Unit = {},
onDismissNudge: (String) -> Unit,
) {
val size = remember(dismissibleActionList) { dismissibleActionList?.size ?: 0 }
Box(modifier = modifier, contentAlignment = Alignment.CenterEnd) {
dismissibleActionList?.forEachIndexed { index, actionData ->
key(actionData.dismissAction) {
AnimatedVisibility(
visible =
!dismissibleActionList.subList(0, index).indices.any {
it == selectedIndex
},
exit = nudgeDismissibleActionExitFadeAnimation(),
) {
Row(
modifier =
Modifier.background(
actionData.actionBackgroundColor?.hexToComposeColor
?: Color.Gray
)
.widthIn(min = dismissibleActionItemWidth.times(size - index))
.fillMaxHeight()
) {
NudgeDismissibleActionItem(
item = actionData,
shouldExpand = {
dismissibleActionList
.subList(index, size)
.indices
.map { it + index }
.any { it == selectedIndex }
},
onClick = { onActionItemClicked(index) },
dismissAction = {
if (index == selectedIndex && actionData.dismissAction != null) {
onDismissNudge(actionData.dismissAction)
}
},
itemContainerWidth = dismissibleActionItemWidth * (size - index),
actionWidth = dismissibleActionItemWidth,
)
Spacer(
modifier =
Modifier.width(dismissibleActionItemWidth * ((size - 1) - index))
)
}
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.ui.nudgeUI.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
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.fillMaxWidth
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.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedState
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeDraggedStateData
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
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NudgeFrontLayer(
modifier: Modifier,
frontLayerVisibility: Boolean,
contextMenuWidth: Dp,
isNudgeDragEnabled: Boolean,
nudgeDraggedStateData: () -> NudgeDraggedStateData?,
frontLayerContent: @Composable BoxScope.() -> Unit,
onNudgeDraggedStateChange: (state: NudgeDraggedState) -> Unit,
) {
val density = LocalDensity.current
val anchoredDragWidth = remember { with(density) { contextMenuWidth.toPx() } }
val anchoredDraggableState = remember {
AnchoredDraggableState(
initialValue = DragAnchors.Normal,
anchors =
DraggableAnchors {
DragAnchors.Normal at 0f
DragAnchors.Dragged at anchoredDragWidth
},
positionalThreshold = { distance: Float -> distance * 0.5f },
velocityThreshold = { anchoredDragWidth },
snapAnimationSpec = tween(),
decayAnimationSpec = exponentialDecay(),
)
}
AnimatedVisibility(
visible = frontLayerVisibility,
enter = EnterTransition.None,
exit = nudgeFrontLayerExitAnimationSpec(),
) {
Box(
modifier =
modifier
.fillMaxWidth()
.graphicsLayer { translationX = -anchoredDraggableState.requireOffset() }
.anchoredDraggable(
state = anchoredDraggableState,
orientation = Orientation.Horizontal,
enabled = isNudgeDragEnabled,
reverseDirection = true,
),
content = frontLayerContent,
)
}
HandleAnchorDragStateChange(
nudgeDraggedStateData = nudgeDraggedStateData,
anchoredDraggableState = anchoredDraggableState,
)
UpdateNudgeDraggedStateOnDrag(
nudgeDraggedState = { nudgeDraggedStateData()?.state },
anchoredDraggableState = anchoredDraggableState,
onNudgeUiStateChange = onNudgeDraggedStateChange,
)
}

View File

@@ -1,6 +1,6 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -10,8 +10,9 @@ package com.naviapp.screenOverlay.nudge.uitronAction
import com.google.gson.annotations.SerializedName
import com.navi.uitron.model.data.ActionDetails
import com.navi.uitron.model.data.UiTronAction
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
data class NudgeClickAction(@SerializedName("nudgeId") val nudgeId: String?) : UiTronAction() {
data class NudgeClickAction(@SerializedName("nudgeId") val id: NudgeId?) : UiTronAction() {
override suspend fun manageAction(actionDetails: ActionDetails) {
val action = actionDetails.uiTronAction as NudgeClickAction
actionDetails.actionCallbackFlow?.emit(action)

View File

@@ -0,0 +1,24 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.uitronAction
import com.google.gson.annotations.SerializedName
import com.navi.common.uitron.model.action.CtaAction
import com.navi.uitron.model.data.ActionDetails
import com.navi.uitron.model.data.UiTronAction
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
data class NudgeCtaAction(
@SerializedName("nudgeId") val id: NudgeId,
@SerializedName("ctaAction") val ctaAction: CtaAction,
) : UiTronAction() {
override suspend fun manageAction(actionDetails: ActionDetails) {
val action = actionDetails.uiTronAction as NudgeCtaAction
actionDetails.actionCallbackFlow?.emit(action)
}
}

View File

@@ -1,6 +1,6 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -10,8 +10,9 @@ package com.naviapp.screenOverlay.nudge.uitronAction
import com.google.gson.annotations.SerializedName
import com.navi.uitron.model.data.ActionDetails
import com.navi.uitron.model.data.UiTronAction
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeId
data class NudgeDragAction(@SerializedName("nudgeId") val nudgeId: String?) : UiTronAction() {
data class NudgeDragAction(@SerializedName("nudgeId") val id: NudgeId?) : UiTronAction() {
override suspend fun manageAction(actionDetails: ActionDetails) {
val action = actionDetails.uiTronAction as NudgeDragAction
actionDetails.actionCallbackFlow?.emit(action)

View File

@@ -10,4 +10,5 @@ package com.naviapp.screenOverlay.nudge.uitronAction
enum class NudgeUiTronActions {
DRAG_NUDGE,
NUDGE_CLICKED,
NUDGE_CTA_ACTION,
}

View File

@@ -1,196 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.ap.common.handler.HandlePublishEventAction
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.model.CtaData
import com.navi.base.utils.orFalse
import com.navi.common.uitron.model.action.ScreenOverlayActionUpdateAction
import com.navi.common.uitron.model.action.ScreenOverlayApiAction
import com.navi.common.uitron.model.action.ScreenOverlayStateUpdateAction
import com.naviapp.appsettings.utils.hasNotificationPermission
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.model.HpBottomSheetState
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetState
import com.naviapp.screenOverlay.bottomsheet.utils.toHpBottomSheetConfig
import com.naviapp.screenOverlay.bottomsheet.utils.toHpBottomSheetContent
import com.naviapp.screenOverlay.model.OverlayItemAction
import com.naviapp.screenOverlay.model.OverlayItemActionData
import com.naviapp.screenOverlay.model.OverlayItemStateUpdate
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.viewModel.ScreenOverlayVM
@Composable
fun InitScreenOverlayComponents(
screenOverlayVM: ScreenOverlayVM,
sharedVM: SharedVM,
selectedTabId: String,
hpStates: () -> HpStates,
activity: HomePageActivity,
) {
val bottomSheetState by screenOverlayVM.bottomSheetState.collectAsStateWithLifecycle()
InitActionsHandler(
viewModel = screenOverlayVM,
naeScreenName = activity.screenName,
isNotificationPermissionEnabled = hasNotificationPermission(activity),
)
LaunchedEffect(Unit) {
screenOverlayVM.redirectionCtaData.collect {
it?.let { handleCta(activity = activity, ctaData = it) }
}
}
HandleBottomSheetNudgeState(
bottomSheetState,
selectedTabId,
hpStates,
sharedVM,
screenOverlayVM,
)
}
@Composable
private fun HandleBottomSheetNudgeState(
bottomSheetState: BottomSheetState,
selectedTabId: String,
hpStates: () -> HpStates,
sharedVM: SharedVM,
screenOverlayVM: ScreenOverlayVM,
) {
LaunchedEffect(
bottomSheetState,
selectedTabId,
hpStates().profileDrawerState,
hpStates().isHomePageRendered,
) {
bottomSheetState.bottomSheetData?.let {
sharedVM.updateBottomSheetState(
state =
if (
selectedTabId == BottomBarTabType.HOME.value &&
hpStates().isHomePageRendered &&
hpStates().profileDrawerState.not()
)
HpBottomSheetState.Visible
else HpBottomSheetState.Hidden,
config =
bottomSheetState.bottomSheetData?.toHpBottomSheetConfig(
viewModel = screenOverlayVM
),
content =
bottomSheetState.bottomSheetData
?.bottomSheetUiTronData
?.toHpBottomSheetContent(),
)
}
}
}
@Composable
fun InitActionsHandler(
viewModel: ScreenOverlayVM,
naeScreenName: String,
isNotificationPermissionEnabled: Boolean,
) {
HandlePublishEventAction(viewModel = viewModel)
HandleApiAction(
viewModel = viewModel,
naeScreenName = naeScreenName,
isNotificationPermissionEnabled = isNotificationPermissionEnabled,
)
}
@Composable
fun HandleApiAction(
viewModel: ScreenOverlayVM,
naeScreenName: String,
isNotificationPermissionEnabled: Boolean,
) {
LaunchedEffect(Unit) {
viewModel.getActionCallback().collect { action ->
when (action) {
is ScreenOverlayApiAction -> {
viewModel.fetchOverlayScreenData(
triggerLoadingState = true,
naeScreenName = naeScreenName,
isNotificationPermissionEnabled = isNotificationPermissionEnabled,
)
}
is ScreenOverlayStateUpdateAction -> {
val nudgeTransitionState =
action.states.map {
OverlayItemStateUpdate(
nudgeId = it.nudgeId,
state = getOverlayItemTransitionState(overlayItemState = it.state),
)
}
viewModel.triggerStateUpdateApiCall(
nudgeTransitionState = nudgeTransitionState,
naeScreenName = naeScreenName,
)
}
is ScreenOverlayActionUpdateAction -> {
val nudgeActions =
action.actions.map {
OverlayItemActionData(
nudgeId = it.nudgeId,
action = getOverlayItemAction(action = it.action),
)
}
viewModel.triggerActionUpdateApiCall(nudgeActions, naeScreenName)
}
else -> {}
}
}
}
}
fun getOverlayItemTransitionState(overlayItemState: String): OverlayItemTransitionState {
return when (overlayItemState) {
OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED.name -> {
OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED
}
OverlayItemTransitionState.COMPLETED.name -> {
OverlayItemTransitionState.COMPLETED
}
else -> {
OverlayItemTransitionState.PAUSED
}
}
}
fun getOverlayItemAction(action: String): OverlayItemAction {
return when (action) {
OverlayItemAction.VIEW.name -> {
OverlayItemAction.VIEW
}
OverlayItemAction.CLICK.name -> {
OverlayItemAction.CLICK
}
else -> {
OverlayItemAction.DISMISS
}
}
}
private fun handleCta(activity: HomePageActivity, ctaData: CtaData) {
DeepLinkManager.getDeepLinkListener()
?.navigateTo(
activity = activity,
ctaData = ctaData,
finish = ctaData.finish.orFalse(),
clearTask = ctaData.clearTask.orFalse(),
)
}

View File

@@ -1,17 +0,0 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils
import androidx.compose.ui.graphics.Color
object NudgeColor {
val colorRed = Color(0xFFBD0000)
val borderColor = Color(0xFFE3E5E5)
val lightRed = Color(0xFFFEECEC)
val darkTextColor = Color(0xFF1F002A)
}

View File

@@ -1,164 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
import com.naviapp.screenOverlay.nudge.model.NudgeData
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeStatus
import com.naviapp.screenOverlay.nudge.model.NudgeUiState
import com.naviapp.screenOverlay.utils.NudgeConstants.ANCHOR_DRAG_WIDTH
import com.naviapp.screenOverlay.utils.NudgeConstants.NudgeAnimationConstants.DISMISS_TEXT_SYNC_ANIMATION
import kotlinx.coroutines.delay
/**
* Observes changes in the nudge element's UI state and updates the draggable state of the front
* layer accordingly. When the UI state transitions to `Dragged` (e.g., after clicking the cross
* icon), the front layer animates to its dragged position. Conversely, when the UI state
* transitions to `Normal` (e.g., after clicking the cross icon again), the front layer animates
* back to its normal position.
*
* @param nudgeUiState A lambda providing access to the current NudgeUiState, reflecting the UI
* state of the nudge element.
* @param anchoredDraggableState The AnchoredDraggableState that controls the draggable behavior of
* the front layer.
*/
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun HandleAnchorDragStateChange(
nudgeUiState: () -> NudgeUiState?,
anchoredDraggableState: AnchoredDraggableState<DragAnchors>,
) {
LaunchedEffect(nudgeUiState()) {
if (nudgeUiState() == NudgeUiState.DRAGGED) {
anchoredDraggableState.animateTo(DragAnchors.Dragged)
} else if (nudgeUiState() == NudgeUiState.IDLE) {
anchoredDraggableState.animateTo(DragAnchors.Normal)
}
}
}
/**
* Observes changes in the AnchoredDraggableState and updates the NudgeUiState accordingly,
* reflecting the current drag state of the nudge element.
*
* When the AnchoredDraggableState transitions to `Normal` from `Dragged`, the NudgeUiState is
* updated to `IDLE`. Conversely, when the AnchoredDraggableState transitions to `Dragged` from
* `Normal`, the NudgeUiState is updated to `DRAGGED`.
*
* @param anchoredDraggableState The AnchoredDraggableState that controls the draggable behavior of
* the nudge element.
* @param onNudgeUiStateChange A callback function to be invoked when the NudgeUiState needs to be
* updated.
* @param nudgeUiState A function providing the current NudgeUiState.
*/
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun UpdateNudgeUiStateOnDrag(
anchoredDraggableState: AnchoredDraggableState<DragAnchors>,
onNudgeUiStateChange: (state: NudgeUiState) -> Unit,
nudgeUiState: () -> NudgeUiState?,
) {
var oldAnchoredDraggableState by remember {
mutableStateOf(anchoredDraggableState.currentValue)
}
LaunchedEffect(anchoredDraggableState.currentValue) {
if (
nudgeUiState() == NudgeUiState.DRAGGED &&
oldAnchoredDraggableState == DragAnchors.Dragged &&
anchoredDraggableState.currentValue == DragAnchors.Normal
) {
onNudgeUiStateChange(NudgeUiState.IDLE)
} else if (
nudgeUiState() == NudgeUiState.IDLE &&
oldAnchoredDraggableState == DragAnchors.Normal &&
anchoredDraggableState.currentValue == DragAnchors.Dragged
) {
onNudgeUiStateChange(NudgeUiState.DRAGGED)
}
oldAnchoredDraggableState = anchoredDraggableState.currentValue
}
}
/**
* Reacts to changes in the status of a nudge element and dispatches corresponding events to the
* NudgeReducer. When the nudge element's status transitions to either `SUCCESS` or `IN_PROGRESS`,
* an `UpdateNudgeStatus` event is dispatched to the reducer, carrying the element's ID and its
* updated status.
*
* @param nudgeData Nudge Element Data.
* @param onNudgeEvent Callback function to dispatch NudgeReducer events.
*/
@Composable
fun HandleNudgeStatusChange(
nudgeData: NudgeData,
onNudgeEvent: (nudgeEvent: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
) {
LaunchedEffect(nudgeData.nudgeStatus) {
if (nudgeData.nudgeStatus == NudgeStatus.SUCCESS) {
/**
* Note: When the status is set to SUCCESS, a 1300ms(300ms is success animation time)
* delay is introduced before triggering the deletion of the nudge element to allow for
* a delete animation.
*/
delay(1300)
onNudgeEvent(NudgeEvent.DeleteNudge(nudgeId = nudgeData.nudgeId.orEmpty()))
onNudgeEffect(
NudgeEffect.OnDeleteNudge(
nudgeId = nudgeData.nudgeId.orEmpty(),
overlayItemTransitionState = OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED,
)
)
}
}
}
@Composable
fun animateDismissBoxWidth(frontLayerVisible: Boolean, deleteNudge: () -> Unit): State<Dp> {
val localConfiguration = LocalConfiguration.current
val screenWidth = remember { localConfiguration.screenWidthDp.dp }
val width by
animateDpAsState(
targetValue = if (frontLayerVisible) screenWidth - ANCHOR_DRAG_WIDTH else 0.dp,
animationSpec = frontLayerAnimationSpec(),
label = DISMISS_TEXT_SYNC_ANIMATION,
) {
deleteNudge()
}
return remember(width) { mutableStateOf(width) }
}
private fun <T> frontLayerAnimationSpec(): FiniteAnimationSpec<T> =
tween(400, easing = CubicBezierEasing(0.83f, 0.17f, 0.23f, 0.89f))
val getFrontLayerExitTransitionSpec = slideOutHorizontally(frontLayerAnimationSpec()) { -it }
enum class DragAnchors {
Normal,
Dragged,
}

View File

@@ -1,92 +0,0 @@
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import com.naviapp.screenOverlay.nudge.model.NudgeData
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.model.NudgeStatus
import com.naviapp.screenOverlay.utils.NudgeConstants.NudgeAnimationConstants.COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION
import com.naviapp.utils.dpToPx
object NudgeExpandedStateUtils {
val getExpandedStateEnterTransitionSpec: EnterTransition =
expandVertically(
initialHeight = { dpToPx(70) },
animationSpec = expandedStateAnimationSpec(),
)
val getExpandedStateExitTransitionSpec: ExitTransition =
shrinkVertically(
targetHeight = { dpToPx(70) },
animationSpec = expandedStateAnimationSpec(),
)
val expandedNudgeExitTransition =
fadeOut(tween(300)) +
shrinkVertically(shrinkTowards = Alignment.Bottom, animationSpec = tween(300)) { -it }
private fun <T> expandedStateAnimationSpec(): FiniteAnimationSpec<T> =
tween(200, easing = CubicBezierEasing(0.8f, 0.09f, 0.14f, 1f))
}
object NudgeCollapsedStateUtils {
val collapsedContainerExitTransition: ContentTransform =
slideInHorizontally(tween(COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION)) { it }
.togetherWith(
slideOutHorizontally(tween(COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION)) { -it }
)
val collapsedContainerRankChangeTransition: ContentTransform =
slideInVertically(tween(COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION)) { it }
.togetherWith(fadeOut(tween(COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION)))
val middlePillEnterTransitionSpec: EnterTransition = fadeIn(tween(200))
val middlePillExitTransitionSpec: ExitTransition = fadeOut(tween(100))
val middlePillContentTransitionSpec: ContentTransform =
expandVertically(tween(durationMillis = 400)) { -it }
.togetherWith(shrinkVertically(tween(durationMillis = 400)) { it })
}
object NudgeRootUtils {
fun filterDeletedNudges(nudgeState: NudgeState): List<NudgeData> =
nudgeState.nudgeList?.filterNot { it.nudgeStatus == NudgeStatus.DELETED }.orEmpty()
val nudgeContainerEnterTransition = slideInVertically(tween(300)) { it }
@Composable
fun HandleExpandedStateByListSize(
state: () -> NudgeState,
onEventSent: (event: NudgeEvent) -> Unit,
) {
LaunchedEffect(key1 = filterDeletedNudges(state()).size) {
if (state().isNudgeExpanded && filterDeletedNudges(state()).size <= 1) {
onEventSent(NudgeEvent.UpdateNudgeExpandedState(expandState = false))
}
}
}
}

View File

@@ -0,0 +1,124 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils.animation
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
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.NudgeId
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants.defaultAnimationSpec
// --------------------- Nudge Visibility ---------------------
fun nudgeEnterAnimation() = slideInVertically(defaultAnimationSpec()) { it.times(1.4).toInt() }
fun nudgeExitAnimation() = ExitTransition.None
// --------------------- Nudge Collapsed State container transitions ---------------------
fun AnimatedContentTransitionScope<NudgeData?>.nudgeCollapsedStateTransitionSpec(
filteredNudgeList: List<NudgeData>?,
initialNudgeId: NudgeId,
isDescriptiveViewEnabled: Boolean,
): ContentTransform {
if (isDescriptiveViewEnabled) return EnterTransition.None togetherWith ExitTransition.None
return if (filteredNudgeList?.any { it.nudgeId == initialNudgeId } == true) {
collapsedContainerRankChangeTransition()
} else {
collapsedContainerElementChangeTransition()
} using
SizeTransform { old, new ->
keyframes {
IntSize(new.width, old.height) at NudgeAnimationConstants.DURATION
durationMillis = NudgeAnimationConstants.DURATION
}
}
}
private fun collapsedContainerRankChangeTransition(): ContentTransform {
return slideInVertically(defaultAnimationSpec()) { it }
.togetherWith(fadeOut(defaultAnimationSpec()))
}
private fun collapsedContainerElementChangeTransition(): ContentTransform {
val animationSpec: FiniteAnimationSpec<IntOffset> =
tween(
durationMillis = NudgeAnimationConstants.DURATION,
easing = NudgeAnimationConstants.AnimationEasing,
delayMillis = NudgeAnimationConstants.COLLAPSED_NUDGE_ELEMENT_TRANSITION_DELAY,
)
return slideInHorizontally(animationSpec) { it }
.togetherWith(slideOutHorizontally(animationSpec) { -it })
}
// --------------------- Nudge descriptive view Transitions ---------------------
fun nudgeDescriptiveViewEnterAnimation(firstWidgetHeight: Int): EnterTransition {
return slideInVertically(defaultAnimationSpec()) { 0 } +
expandVertically(defaultAnimationSpec(), expandFrom = Alignment.Top) { firstWidgetHeight }
}
fun nudgeDescriptiveViewExitAnimation(firstWidgetHeight: Int): ExitTransition {
return slideOutVertically(defaultAnimationSpec()) { it - firstWidgetHeight } +
shrinkVertically(defaultAnimationSpec(), shrinkTowards = Alignment.Bottom) {
firstWidgetHeight
}
}
fun nudgeExitTransitionInDescriptiveView(): ExitTransition {
return shrinkVertically(defaultAnimationSpec(), shrinkTowards = Alignment.Bottom) { -it }
}
// --------------------- Front Layer transitions ---------------------
fun nudgeFrontLayerExitAnimationSpec(): ExitTransition {
return slideOutHorizontally(defaultAnimationSpec()) { -it }
}
// --------------------- Nudge header visibility transitions ---------------------
fun nudgeHeaderEnterAnimation(): EnterTransition {
return fadeIn(defaultAnimationSpec())
.plus(slideInVertically(defaultAnimationSpec()) { it })
.plus(expandVertically(defaultAnimationSpec()))
}
fun nudgeHeaderExitAnimation(): ExitTransition {
return fadeOut(defaultAnimationSpec())
.plus(slideOutVertically(defaultAnimationSpec()) { it })
.plus(shrinkVertically(defaultAnimationSpec()))
}
// --------------------- Nudge Middle Pill transitions ---------------------
fun nudgeMiddlePillEnterAnimationSpec(delayMillis: Int = 0): EnterTransition {
return slideInVertically(defaultAnimationSpec(delayMillis)) { -it }
.plus(expandVertically(defaultAnimationSpec(delayMillis)))
}
fun nudgeMiddlePillExitAnimationSpec(): ExitTransition {
return slideOutVertically(defaultAnimationSpec()) { -it }
.plus(shrinkVertically(defaultAnimationSpec()) { 0 })
}
// --------------------- Nudge Dismissible Actions transition ------------------
fun nudgeDismissibleActionExitFadeAnimation(): ExitTransition = fadeOut(defaultAnimationSpec())

View File

@@ -0,0 +1,36 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils.constants
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.tween
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
object NudgeAnimationConstants {
const val DURATION = 400
const val SUCCESS_DELAY = 1300L
val AnimationEasing = CubicBezierEasing(0.8f, 0.09f, 0.14f, 1f)
const val COLLAPSED_NUDGE_ELEMENT_TRANSITION_DELAY = 200
fun <T> defaultAnimationSpec(delayMillis: Int = 0): TweenSpec<T> {
return tween(durationMillis = DURATION, easing = AnimationEasing, delayMillis = delayMillis)
}
}
object NudgeColor {
val borderColor = Color(0xFFE3E5E5)
val darkTextColor = Color(0xFF1F002A)
val shadowColor = Color(0xFF535252)
}
object NudgeConstants {
const val MIDDLE_PILL_CLICKED = "nudge_middle_pill_clicked"
val dismissibleActionHorizontalPadding = 26.dp
}

View File

@@ -0,0 +1,19 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils.extensions
import com.naviapp.screenOverlay.nudge.domain.model.data.NudgeData
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<NudgeData>?.getFirstNonDismissedNudge(): NudgeData? {
return this?.firstOrNull { it.nudgeStatus != NudgeStatus.DISMISSED }
}

View File

@@ -0,0 +1,24 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils.mapper
import com.naviapp.screenOverlay.model.OverlayItemTransitionState
fun mapToTransitionState(overlayItemState: String): OverlayItemTransitionState {
return when (overlayItemState) {
OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED.name -> {
OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED
}
OverlayItemTransitionState.COMPLETED.name -> {
OverlayItemTransitionState.COMPLETED
}
else -> OverlayItemTransitionState.PAUSED
}
}

View File

@@ -0,0 +1,55 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils.measurement
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
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
@Composable
fun getMaxWidthForDismissibleActionItem(actions: List<NudgeDismissibleActionData>?): Int {
val density = LocalDensity.current
val textMeasurer = rememberTextMeasurer()
val baseWidthPx = with(density) { 56.dp.toPx().toInt() }
if (actions.isNullOrEmpty()) {
return 0
}
return remember(
key1 = actions.map { it.actionText },
calculation = {
val textWidths =
actions
.filter { it.actionText != null }
.map { action -> measureActionWidth(textMeasurer, action.actionText!!) }
val maxTextWidth = textWidths.maxOrNull() ?: 0
maxOf(maxTextWidth, baseWidthPx)
},
)
}
private fun measureActionWidth(textMeasurer: TextMeasurer, content: String): Int =
textMeasurer.measure(text = content, style = actionTextStyle).size.width
private val actionTextStyle =
TextStyle(
fontSize = 12.sp,
lineHeight = 18.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
fontFamily = naviFontFamily,
)

View File

@@ -0,0 +1,76 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.nudge.utils.modifier
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.unit.Dp
data class BorderSides(
val top: Boolean = false,
val bottom: Boolean = false,
val start: Boolean = false,
val end: Boolean = false,
) {
companion object {
val NONE = BorderSides()
val ALL = BorderSides(top = true, bottom = true, start = true, end = true)
val HORIZONTAL = BorderSides(start = true, end = true)
val VERTICAL = BorderSides(top = true, bottom = true)
}
}
class DirectionalBorderModifier(
private val directionBorders: BorderSides,
private val width: Dp,
private val color: Color,
) : DrawModifier {
override fun ContentDrawScope.draw() {
drawContent()
val strokeWidth = width.toPx()
if (directionBorders.top) {
drawLine(
color = color,
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
strokeWidth = strokeWidth,
)
}
if (directionBorders.bottom) {
drawLine(
color = color,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth,
)
}
if (directionBorders.start) {
drawLine(
color = color,
start = Offset(0f, 0f),
end = Offset(0f, size.height),
strokeWidth = strokeWidth,
)
}
if (directionBorders.end) {
drawLine(
color = color,
start = Offset(size.width, 0f),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth,
)
}
}
}

View File

@@ -0,0 +1,62 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
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.NudgeStatus
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants
import com.naviapp.screenOverlay.nudge.utils.constants.NudgeAnimationConstants.SUCCESS_DELAY
import com.naviapp.screenOverlay.nudge.utils.extensions.filterDeletedNudges
import kotlinx.coroutines.delay
@Composable
fun HandleExpandedStateByListSize(state: () -> NudgeState, onEvent: (event: NudgeEvent) -> Unit) {
val nudgeList = state().nudgeList.filterDeletedNudges()
LaunchedEffect(
key1 = nudgeList.size,
block = {
if (state().descriptiveViewEnabled && nudgeList.size <= 1) {
delay(NudgeAnimationConstants.DURATION.toLong())
onEvent(NudgeEvent.ToggleDescriptiveView(enabled = false))
}
},
)
}
/**
* Note: When the status is set to SUCCESS, a 1300ms(300ms is success animation time) delay is
* introduced before triggering the deletion of the nudge element to allow for a delete animation.
*/
@Composable
fun HandleNudgeStatusChange(
nudgeData: NudgeData,
onNudgeEvent: (nudgeEvent: NudgeEvent) -> Unit,
onNudgeEffect: (effect: NudgeEffect) -> Unit,
) {
LaunchedEffect(
key1 = nudgeData.nudgeStatus,
block = {
if (nudgeData.nudgeStatus == NudgeStatus.SUCCESS) {
delay(SUCCESS_DELAY)
onNudgeEvent(NudgeEvent.DismissNudge(id = nudgeData.nudgeId.orEmpty()))
onNudgeEffect(
NudgeEffect.OnNudgeStateUpdate(
id = nudgeData.nudgeId.orEmpty(),
transitionState = OverlayItemTransitionState.SUCCESS_ACKNOWLEDGED,
)
)
}
},
)
}

View File

@@ -1,14 +1,12 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.screenOverlay.utils
import androidx.compose.ui.unit.dp
object PopupConstants {
const val CLOSE_ALL = "Close all"
const val POPUP = "popup"
@@ -19,19 +17,10 @@ object PopupConstants {
}
object NudgeConstants {
const val DISMISS = "Dismiss"
const val HOME_NUDGE = "HOME_NUDGE"
const val YOUR_PAYMENTS = "Your payments"
const val MORE = "more"
const val UNPAID_BILLS_RECHARGES = "Unpaid bills/recharges"
const val UNPAID = " unpaid"
const val NUDGE = "nudge"
const val NUDGE_ID = "nudgeId"
const val NUDGE_DISMISSED_EVENT = "home_page_nudge_dismissed"
val ANCHOR_DRAG_WIDTH = 70.dp
object NudgeAnimationConstants {
const val DISMISS_TEXT_SYNC_ANIMATION = "DismissTextSyncAnimation"
const val REPLACE_NUDGE_IN_COLLAPSED_STATE = "replaceNudgeInCollapsedState"
const val COLLAPSED_NUDGE_ELEMENT_TRANSITION_DURATION = 300
const val MIDDLE_PILL_CONTENT_ANIMATION = "MiddlePillContentAnimation"
}
}

View File

@@ -24,12 +24,12 @@ import com.naviapp.screenOverlay.model.OverlayItemActionData
import com.naviapp.screenOverlay.model.OverlayItemStateUpdate
import com.naviapp.screenOverlay.model.OverlayItemsStateUpdates
import com.naviapp.screenOverlay.model.ScreenOverlayActionUpdateRequest
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeListData
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.model.NudgeStatus
import com.naviapp.screenOverlay.nudge.model.StaticNudgeData
import com.naviapp.screenOverlay.nudge.reducer.NudgeReducer
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.popup.model.PopupEvent
import com.naviapp.screenOverlay.popup.model.PopupListData
import com.naviapp.screenOverlay.popup.model.PopupState
@@ -186,7 +186,8 @@ constructor(
private fun onActionTriggered(uiTronAction: UiTronAction?) {
uiTronAction?.let {
screenOverlayUitronActionHandler.onActionTriggered(
it,
state = nudgeState.value,
uiTronAction = it,
sendEvent = { event -> sendEvent(event) },
lastClickedNudgeId = { nudgeId -> updateLastClickedNudgeId(nudgeId = nudgeId) },
ctaAction = { ctaData ->

View File

@@ -15,9 +15,9 @@ import com.navi.common.viewmodel.BaseVM
import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetEffect
import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetEvent
import com.naviapp.screenOverlay.bottomsheet.model.BottomSheetState
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
import com.naviapp.screenOverlay.nudge.model.NudgeState
import com.naviapp.screenOverlay.nudge.domain.model.effect.NudgeEffect
import com.naviapp.screenOverlay.nudge.domain.model.event.NudgeEvent
import com.naviapp.screenOverlay.nudge.domain.model.state.NudgeState
import com.naviapp.screenOverlay.popup.model.PopupEffect
import com.naviapp.screenOverlay.popup.model.PopupEvent
import com.naviapp.screenOverlay.popup.model.PopupState

View File

@@ -12,6 +12,7 @@ import com.google.gson.JsonElement
import com.navi.common.uitron.deserializer.UiTronActionDeserializer
import com.navi.uitron.model.data.UiTronAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeClickAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeCtaAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeDragAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeUiTronActions
import com.naviapp.screenOverlay.popup.uitronAction.PopupDismissAction
@@ -32,6 +33,8 @@ class HomeCustomActionDeserializer : UiTronActionDeserializer() {
context?.deserialize(jsonObject, NudgeDragAction::class.java)
NudgeUiTronActions.NUDGE_CLICKED.name ->
context?.deserialize(jsonObject, NudgeClickAction::class.java)
NudgeUiTronActions.NUDGE_CTA_ACTION.name ->
context?.deserialize(jsonObject, NudgeCtaAction::class.java)
PopupUitronActions.DISMISS_POPUP.name ->
context?.deserialize(jsonObject, PopupDismissAction::class.java)
else -> super.deserialize(json, typeOfT, context)

View File

@@ -12,6 +12,7 @@ import com.google.gson.JsonSerializationContext
import com.navi.common.uitron.serializer.UiTronActionSerializer
import com.navi.uitron.model.data.UiTronAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeClickAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeCtaAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeDragAction
import com.naviapp.screenOverlay.nudge.uitronAction.NudgeUiTronActions
import com.naviapp.screenOverlay.popup.uitronAction.PopupDismissAction
@@ -30,6 +31,8 @@ class HomeCustomActionSerializer : UiTronActionSerializer() {
context?.serialize(src as NudgeDragAction, NudgeDragAction::class.java)
NudgeUiTronActions.NUDGE_CLICKED.name ->
context?.serialize(src as NudgeDragAction, NudgeClickAction::class.java)
NudgeUiTronActions.NUDGE_CTA_ACTION.name ->
context?.serialize(src as NudgeCtaAction, NudgeCtaAction::class.java)
PopupUitronActions.DISMISS_POPUP.name ->
context?.serialize(src as PopupDismissAction, PopupDismissAction::class.java)
else -> super.serialize(src, typeOfSrc, context)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="13dp"
android:height="12dp"
android:viewportWidth="13"
android:viewportHeight="12">
<path
android:pathData="M3.148,7.853C2.953,7.658 2.857,7.241 3.052,7.046L6.052,4.046C6.247,3.851 6.756,3.851 6.951,4.046L9.951,7.046C10.146,7.241 10.05,7.658 9.855,7.853C9.66,8.048 9.247,8.148 9.052,7.953L6.501,5.406L3.951,7.953C3.756,8.148 3.343,8.048 3.148,7.853Z"
android:fillColor="#1F002A"
android:fillType="evenOdd"/>
</vector>