NTP-55320 || Shrihari | FTUE v0 (#15820)

Co-authored-by: Aman S <aman.s@navi.com>
This commit is contained in:
A Shrihari Raju
2025-04-25 13:04:34 +05:30
committed by GitHub
parent ac8d5273b0
commit 03ea9fdb02
57 changed files with 2782 additions and 28 deletions

View File

@@ -13,6 +13,8 @@ import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.amc.common.model.widgets.SpacerWidget
import com.navi.amc.fundbuy.models.widgets.FtueWithTrackerWidget
import com.navi.amc.fundbuy.models.widgets.RepeatOrderWidget
import com.navi.amc.fundbuy.models.widgets.RiskFreeFundWidget
import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.ActionCardWidget
@@ -24,7 +26,6 @@ import com.naviapp.home.dashboard.models.investmentTabWidgetData.HighestReturnFu
import com.naviapp.home.dashboard.models.investmentTabWidgetData.MonthlyInvestmentGoalWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.OrdersInProgressWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.PortfolioWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.RepeatOrderWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.RewardNudgeWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.SipAutoPayNudgeWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.TopInvestingFundsWidget
@@ -76,6 +77,8 @@ class InvestmentTabWidgetJsonDeserializer : JsonDeserializer<GenericComposableWi
MonthlyInvestmentGoalWidget::class.java
InvestmentTabWidgetType.SETUP_MONTHLY_TARGET_WIDGET.value ->
SetupMonthlyTargetWidget::class.java
InvestmentTabWidgetType.FTUE_WITH_TRACKER_WIDGET.value ->
FtueWithTrackerWidget::class.java
else -> null
}
return if (className != null) {

View File

@@ -39,6 +39,7 @@ data class ExploreMoreSectionData(
data class TitleWithIconsCard(
@SerializedName("property") val property: InvestmentBaseProperty? = null,
@SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null,
@SerializedName("title") val title: TextFieldData? = null,
@SerializedName("leftIcon") val leftIcon: ImageFieldData? = null,
@SerializedName("rightIcon") val rightIcon: ImageFieldData? = null,

View File

@@ -27,4 +27,5 @@ enum class InvestmentTabWidgetType(val value: String) {
SIP_AUTOPAY_NUDGE_WIDGET("sip_autopay_nudge_widget"),
MONTHLY_INVESTMENT_GOAL_WIDGET("monthly_investment_goal_widget"),
SETUP_MONTHLY_TARGET_WIDGET("setup_monthly_target_widget"),
FTUE_WITH_TRACKER_WIDGET("ftue_with_tracker_widget"),
}

View File

@@ -18,6 +18,8 @@ import com.navi.amc.common.composables.widgets.SpacerWidgetComposable
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.amc.common.model.widgets.SpacerWidget
import com.navi.amc.fundbuy.composables.widgets.SetupMonthlyTargetWidgetComposable
import com.navi.amc.fundbuy.models.widgets.FtueWithTrackerWidget
import com.navi.amc.fundbuy.models.widgets.RepeatOrderWidget
import com.navi.amc.fundbuy.models.widgets.RiskFreeFundWidget
import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetWidget
import com.navi.base.model.ActionData
@@ -33,7 +35,6 @@ import com.naviapp.home.dashboard.models.investmentTabWidgetData.HighestReturnFu
import com.naviapp.home.dashboard.models.investmentTabWidgetData.MonthlyInvestmentGoalWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.OrdersInProgressWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.PortfolioWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.RepeatOrderWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.RewardNudgeWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.SipAutoPayNudgeWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.TopInvestingFundsWidget
@@ -44,6 +45,7 @@ import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.BannerWithAct
import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.CutOffSellTimerComposable
import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.CutOffTimerWidgetComposable
import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.ExploreMoreWidgetComposable
import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.FtueWithTrackerWidgetComposable
import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.FundCategoriesWidgetComposable
import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.MonthlyInvestmentGoalWidgetComposable
import com.naviapp.home.dashboard.ui.compose.investmentTab.widgets.OrdersInProgressWidgetComposable
@@ -219,5 +221,14 @@ fun InvestmentGenericComposableWidgetFactory(
)
}
}
InvestmentTabWidgetType.FTUE_WITH_TRACKER_WIDGET.value -> {
VisibilityTracker(widgetData = data, onVisible = onVisible) {
FtueWithTrackerWidgetComposable(
widget = data as FtueWithTrackerWidget,
onClick = onClick,
onHopperStart = onHopperStart,
)
}
}
}
}

View File

@@ -11,9 +11,11 @@ import BottomSheetData
import android.app.Activity
import android.os.Bundle
import androidx.compose.runtime.MutableState
import com.navi.amc.common.activity.CheckerActivity
import com.navi.amc.common.model.AdditionalDataAsyncResponse
import com.navi.amc.common.model.NextCtaResponse
import com.navi.amc.navigator.NaviAmcDeeplinkNavigator
import com.navi.amc.navigator.NaviAmcDeeplinkNavigator.FTUE
import com.navi.amc.utils.AmcAnalytics
import com.navi.amc.utils.Constant
import com.navi.amc.utils.Constant.CAPS_DATA
@@ -81,6 +83,26 @@ class InvestmentsScreenHelper {
fun handlePennyDropSuccessCase(ctaData: CtaData) {
if (
(ctaData.url
?.contains(
NaviAmcDeeplinkNavigator.AMC.plus("/").plus(NaviAmcDeeplinkNavigator.KYC),
true,
)
.orFalse() ||
ctaData.url?.contains(CheckerActivity.HPC_PAN_REDIRECTION_PAGE).orFalse() ||
ctaData.url?.contains(CheckerActivity.HPC_NAME_REDIRECTION_PAGE).orFalse()) &&
ctaData.parameters
?.firstOrNull { it.key == Constant.SOURCE }
?.value
?.equals(FTUE) == true
) {
val sourceParam =
hashMapOf<String, String>().apply {
ctaData.parameters?.forEach { put(it.key.toString(), it.value.orEmpty()) }
put(Constant.KYC_SOURCE_SCREEN, FTUE)
}
TempStorageHelper.kycSourceInfo = sourceParam
} else if (
ctaData
?.url
?.contains(

View File

@@ -8,6 +8,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import com.navi.amc.fundbuy.models.widgets.FundCardData
import com.navi.amc.fundbuy.models.widgets.RepeatOrderWidget
import com.navi.amc.fundbuy.models.widgets.RiskFreeFundWidget
import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetWidget
import com.navi.base.model.GenericAnalytics
@@ -21,7 +22,6 @@ import com.naviapp.home.dashboard.models.investmentTabWidgetData.MonthlyInvestme
import com.naviapp.home.dashboard.models.investmentTabWidgetData.OrderStatusCardData
import com.naviapp.home.dashboard.models.investmentTabWidgetData.OrdersInProgressWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.PortfolioWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.RepeatOrderWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.SipAutoPayNudgeWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.TopInvestingFundsWidget
import com.naviapp.home.dashboard.models.investmentTabWidgetData.WhyInvestWidget

View File

@@ -0,0 +1,178 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.dashboard.ui.compose.investmentTab.genericComposables
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.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.navi.amc.fundbuy.composables.widgets.CustomDashedDivider
import com.navi.amc.fundbuy.models.widgets.FtueWithTrackerActionCardData
import com.navi.base.model.ActionData
import com.navi.base.model.CtaData
import com.navi.base.utils.orElse
import com.navi.common.utils.Constants.HOPPER
import com.navi.design.utils.clickableWithNoGesture
import com.navi.naviwidgets.extensions.NaviImage
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.naviwidgets.extensions.hexToColor
import com.navi.naviwidgets.models.FooterButtonState
import com.navi.uitron.model.ui.ComposePadding
import com.navi.uitron.utils.setBackground
import com.navi.uitron.utils.setBorderStroke
import com.navi.uitron.utils.setPadding
import com.naviapp.home.dashboard.ui.compose.investmentTab.InvestmentsScreenHelper
@Composable
fun FtueTrackerCardComposable(
data: FtueWithTrackerActionCardData,
onClick: (actionData: ActionData?) -> Unit,
onHopperStart: (ctaData: CtaData, buttonState: MutableState<FooterButtonState>) -> Unit =
{ _, _ ->
},
) {
Column(
modifier =
Modifier.shadow(
elevation = (data.actionCardProperty?.cardProperty?.elevation ?: 16).dp,
spotColor = (hexToColor(data.actionCardProperty?.cardProperty?.spotColor)),
ambientColor = (hexToColor(data.actionCardProperty?.cardProperty?.ambientColor)),
)
.setBorderStroke(data.actionCardProperty?.cardProperty?.borderStrokeData)
.setBackground(
brushData = data.actionCardProperty?.cardProperty?.backGroundBrushData,
uiTronShape = data.actionCardProperty?.cardProperty?.shape,
backgroundColor = data.actionCardProperty?.cardProperty?.backgroundColor,
)
.setPadding(data.actionCardProperty?.cardProperty?.padding)
.wrapContentHeight()
.fillMaxWidth()
) {
data.trackerItems?.let { items ->
Row(modifier = Modifier.fillMaxWidth()) {
items.forEachIndexed { index, iconWithTextCard ->
Column(
modifier =
Modifier.fillMaxHeight()
.width(
(data.actionCardProperty
?.trackerItemsProperty
?.width
?.toInt()
?.orElse(65) ?: 65)
.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
NaviImage(imageFieldData = iconWithTextCard.icon)
Spacer(
modifier =
Modifier.fillMaxWidth()
.height(
(data.actionCardProperty?.trackerItemsProperty?.margin?.top
?: 4)
.dp
)
)
NaviTextWidgetized(textFieldData = iconWithTextCard.text)
}
if (index != items.size - 1) {
Box(
modifier =
Modifier.fillMaxSize()
.weight(1f)
.setPadding(
data.actionCardProperty?.trackerDividerProperty?.padding
?: ComposePadding(
start = 8,
end = 8,
top = 20,
bottom = 0,
)
),
contentAlignment = Alignment.CenterStart,
) {
CustomDashedDivider(
modifier = Modifier.padding(top = 0.dp, bottom = 0.dp),
color = Color.Black,
thickness = 1.dp,
)
}
}
}
}
}
data.buttonText?.let {
Column(
modifier =
Modifier.fillMaxWidth()
.setPadding(data.actionCardProperty?.buttonProperty?.padding)
.setBackground(
brushData =
data.actionCardProperty?.buttonProperty?.backGroundBrushData,
uiTronShape = data.actionCardProperty?.buttonProperty?.shape,
backgroundColor =
data.actionCardProperty?.buttonProperty?.backgroundColor,
)
.clickableWithNoGesture {
data.actionData?.let { actionData ->
if (actionData.url == HOPPER) {
InvestmentsScreenHelper()
.setActionStatus(
actionData = actionData,
buttonState =
mutableStateOf<FooterButtonState>(
FooterButtonState.ENABLED
),
onClick = onClick,
onHopperStart = onHopperStart,
)
} else {
onClick(actionData)
}
}
},
horizontalAlignment = Alignment.CenterHorizontally,
) {
NaviTextWidgetized(
textFieldData = it,
modifier =
Modifier.setPadding(data.actionCardProperty?.buttonTextProperty?.padding),
)
}
}
data.footerTexts?.let {
Row(
modifier =
Modifier.fillMaxWidth()
.setPadding(data.actionCardProperty?.footerTextsProperty?.padding),
horizontalArrangement = Arrangement.Center,
) {
NaviTextWidgetized(textFieldData = it.leftText)
NaviTextWidgetized(textFieldData = it.rightText)
}
}
}
}

View File

@@ -109,8 +109,16 @@ fun ExploreMoreCardComposable(
)
Spacer(
modifier =
Modifier.weight(
cardData.property?.spacingWeight?.start ?: DEFAULT_CARD_WEIGHT
Modifier.then(
cardData.titleProperty?.padding?.start?.let { startPadding ->
Modifier.width(startPadding.dp)
}
?: run {
Modifier.weight(
cardData.property?.spacingWeight?.start
?: DEFAULT_CARD_WEIGHT
)
}
)
)
NaviTextWidgetized(textFieldData = cardData.title)

View File

@@ -0,0 +1,213 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.dashboard.ui.compose.investmentTab.widgets
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import com.navi.amc.fundbuy.models.widgets.FtueWithTrackerWidget
import com.navi.base.model.ActionData
import com.navi.base.model.CtaData
import com.navi.common.ui.compose.RolodexAnimationComposableV2
import com.navi.naviwidgets.extensions.NaviImage
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.naviwidgets.models.FooterButtonState
import com.navi.uitron.model.ui.BrushData
import com.navi.uitron.model.ui.BrushType
import com.navi.uitron.model.ui.ColorStop
import com.navi.uitron.model.ui.ComposePadding
import com.navi.uitron.utils.setBackground
import com.navi.uitron.utils.setPadding
import com.naviapp.home.dashboard.ui.compose.investmentTab.genericComposables.FtueTrackerCardComposable
@Composable
fun FtueWithTrackerWidgetComposable(
widget: FtueWithTrackerWidget,
onClick: (actionData: ActionData?) -> Unit,
onHopperStart: (ctaData: CtaData, buttonState: MutableState<FooterButtonState>) -> Unit =
{ _, _ ->
},
) {
widget.widgetData?.content?.let { content ->
Box {
val illustrationHeight =
(LocalConfiguration.current.screenWidthDp) *
(content.properties?.cardProperty?.heightFactor ?: 0.65f)
Box(
modifier =
Modifier.height(illustrationHeight.dp)
.width(LocalConfiguration.current.screenWidthDp.dp)
.setBackground(
brushData =
content.properties?.cardProperty?.backGroundBrushData
?: BrushData(
brushType = BrushType.LINEAR.name,
colorStops =
listOf(
ColorStop(0.0f, "#FFFFFF"),
ColorStop(1f, "#FFFBD6"),
),
),
uiTronShape = content.properties?.cardProperty?.shape,
backgroundColor = content.properties?.cardProperty?.backgroundColor,
)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Column(horizontalAlignment = Alignment.Start) {
content.topLogo?.let {
NaviImage(
imageFieldData = it,
modifier =
Modifier.setPadding(
content.properties?.topLogoProperty?.padding
?: ComposePadding(0, 0, 14, 0)
)
.align(Alignment.CenterHorizontally),
)
}
content.topText?.let {
NaviTextWidgetized(
textFieldData = it,
modifier =
Modifier.padding(
start =
(content.properties?.topTextProperty?.padding?.start
?: 16)
.dp,
top =
(content.properties?.topTextProperty?.padding?.top
?: 30)
.dp,
),
)
}
content.rolodexItems?.let { items ->
if (items.isNotEmpty()) {
Row(
modifier =
Modifier.setPadding(
content.properties?.rolodexItemProperty?.padding
?: ComposePadding(0, 0, 6, 0)
),
horizontalArrangement = Arrangement.Start,
) {
RolodexAnimationComposableV2(
composableList =
List(items.size ?: 0) {
{ NaviTextWidgetized(textFieldData = items[it]) }
},
delayInMillis =
content.properties
?.rolodexItemProperty
?.animationData
?.delayInMillis ?: 2200,
enterAnimation = slideInVertically { it },
exitAnimation = slideOutVertically { -it },
contentAlignment = Alignment.CenterStart,
)
}
}
}
content.chipItems?.let { chipItems ->
if (chipItems.isNotEmpty()) {
Row(
modifier =
Modifier.fillMaxWidth()
.wrapContentHeight()
.setPadding(
content.properties?.chipProperty?.padding
?: ComposePadding(16, 0, 12, 0)
)
) {
repeat(chipItems.size) {
Column(
modifier =
Modifier.setBackground(
backgroundColor =
content.properties
?.chipItemProperty
?.backgroundColor,
brushData =
content.properties
?.chipItemProperty
?.backGroundBrushData,
uiTronShape =
content.properties?.chipItemProperty?.shape,
)
) {
NaviTextWidgetized(
modifier =
Modifier.setPadding(
content.properties
?.chipItemProperty
?.padding ?: ComposePadding(8, 8, 4, 4)
),
textFieldData = chipItems[it],
)
}
Spacer(
modifier =
Modifier.width(
(content.properties?.chipProperty?.padding?.end
?: 10)
.dp
)
)
}
}
}
}
}
}
}
content.actionCardData?.let { actionCardData ->
Box(
modifier =
Modifier.padding(
top =
(illustrationHeight *
(actionCardData.actionCardProperty
?.cardProperty
?.heightFactor ?: 0.65f))
.dp,
start =
(actionCardData.actionCardProperty?.cardProperty?.margin?.start
?: 16)
.dp,
end =
(actionCardData.actionCardProperty?.cardProperty?.margin?.end ?: 16)
.dp,
)
) {
FtueTrackerCardComposable(
data = actionCardData,
onClick = onClick,
onHopperStart = onHopperStart,
)
}
}
}
}
}

View File

@@ -10,11 +10,11 @@ package com.naviapp.home.dashboard.ui.compose.investmentTab.widgets
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.navi.amc.fundbuy.models.widgets.RepeatOrderWidget
import com.navi.base.model.ActionData
import com.navi.base.model.GenericAnalytics
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.uitron.utils.setPadding
import com.naviapp.home.dashboard.models.investmentTabWidgetData.RepeatOrderWidget
import com.naviapp.home.dashboard.ui.compose.investmentTab.genericComposables.CardListComposable
import com.naviapp.home.dashboard.viewmodels.InvestmentsVm
import com.naviapp.utils.Constants.DEFAULT_CARD_WIDTH_FACTOR

View File

@@ -74,6 +74,7 @@ dependencies {
implementation libs.digio.gateway.kyc
implementation libs.digitap
implementation libs.philjay.mpAndroidChart
implementation libs.raamcosta.composeDestinations.animation.core
androidTestImplementation libs.androidx.test.espresso.core
androidTestImplementation libs.androidx.test.junit
@@ -82,4 +83,5 @@ dependencies {
ksp libs.androidx.hilt.compiler
ksp libs.dagger.hiltCompiler
ksp libs.raamcosta.composeDestinations.ksp
}

View File

@@ -44,6 +44,13 @@
android:theme="@style/BaseThemeStyle"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name="com.navi.amc.compose.entry.AmcComposeActivity"
android:screenOrientation="portrait"
android:theme="@style/BaseThemeStyle"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize" />
</application>
</manifest>

View File

@@ -7,23 +7,28 @@
package com.navi.amc.common.composables
import PaymentCard
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.navi.amc.fundbuy.composables.cards.MonthlyInvestmentGoalCardComposable
import com.navi.amc.fundbuy.composables.cards.PaymentCardComposable
import com.navi.amc.fundbuy.models.cards.InvestmentGoalCardData
import com.navi.amc.utils.Constant.FREE_SCROLL
import com.navi.amc.utils.Constant.FUND_CARD_DEFAULT_COLUMN_WEIGHT
import com.navi.amc.utils.Constant.MONTHLY_INVESTMENT_GOAL_CARD
import com.navi.amc.utils.Constant.RANK_OF_CARD
import com.navi.amc.utils.Constant.REPEAT_ORDER
import com.navi.base.model.ActionData
@Composable
@@ -36,6 +41,8 @@ fun AmcCardListComposable(
listHorizontalPadding: Dp = 16.dp,
spacingBetweenCard: Dp = 16.dp,
scrollType: String,
listState: LazyListState? = null,
pagerState: PagerState? = null,
) {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
@@ -44,6 +51,7 @@ fun AmcCardListComposable(
when (scrollType) {
FREE_SCROLL -> {
LazyRow(
state = listState ?: LazyListState(),
contentPadding = PaddingValues(horizontal = listHorizontalPadding),
horizontalArrangement = Arrangement.spacedBy(spacingBetweenCard),
) {
@@ -67,9 +75,8 @@ fun AmcCardListComposable(
}
}
else -> {
val pagerState = rememberPagerState(pageCount = { cardList.size })
HorizontalPager(
state = pagerState,
state = pagerState ?: rememberPagerState(pageCount = { cardList.size }),
pageSize = PageSize.Fixed(cardWidth),
contentPadding = PaddingValues(horizontal = listHorizontalPadding),
pageSpacing = spacingBetweenCard,
@@ -111,5 +118,13 @@ fun RenderCardBasedOnType(
onClick = onClick,
)
}
REPEAT_ORDER -> {
PaymentCardComposable(
paymentCard = data as PaymentCard,
cardWidth = cardWidth,
onClick = onClick,
cardType = "NORMAL_CARD",
)
}
}
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.amc.fundbuy.models.AmcHeaderExtraData
import com.navi.amc.fundbuy.models.SelectionData
import com.navi.amc.utils.Constant.WHITE
import com.navi.base.model.ActionData
import kotlinx.coroutines.delay
@@ -34,6 +35,7 @@ fun AmcCommonWidgetListRenderer(
scrollToIndex: Int? = null,
extraData: AmcHeaderExtraData? = null,
eventHandler: (actionData: ActionData?) -> Unit,
selectionExtraData: SelectionData? = null,
) {
val scrollState = rememberLazyListState()
@@ -88,6 +90,7 @@ fun AmcCommonWidgetListRenderer(
val widget = widgetList?.get(index)
AmcComposableWidgetFactory(
data = widget,
selectionData = selectionExtraData,
onClick = { actionData -> actionListener(actionData) },
onVisible = { actionData -> eventHandler(actionData) },
)

View File

@@ -13,26 +13,34 @@ import com.navi.amc.common.composables.widgets.SpacerWidgetComposable
import com.navi.amc.common.model.AmcWidgetType
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.amc.common.model.widgets.SpacerWidget
import com.navi.amc.fundbuy.composables.BannerWidgetComposable
import com.navi.amc.fundbuy.composables.FooterWidgetComposable
import com.navi.amc.fundbuy.composables.HelpInfoWidgetComposable
import com.navi.amc.fundbuy.composables.OnTimeAssuranceWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.BannerWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.BannerWithLottieWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.FooterWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.HelpInfoWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.InvestmentDetailsWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.InvestmentTrackerWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.MonthlyInvestmentGoalWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.OnTimeAssuranceWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.RepeatOrderWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.SetupMonthlyTargetWidgetComposable
import com.navi.amc.fundbuy.composables.widgets.TitleContentRadioWidgetComposable
import com.navi.amc.fundbuy.models.SelectionData
import com.navi.amc.fundbuy.models.widgets.InvestmentDetailsWidget
import com.navi.amc.fundbuy.models.widgets.MonthlyInvestmentGoalWidget
import com.navi.amc.fundbuy.models.widgets.MonthlyInvestmentTrackerWidget
import com.navi.amc.fundbuy.models.widgets.OnTimeAssuranceInfoWidget
import com.navi.amc.fundbuy.models.widgets.RepeatOrderWidget
import com.navi.amc.fundbuy.models.widgets.RiskFreeFundWidget
import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetWidget
import com.navi.amc.fundbuy.models.widgets.TitleContentRadioWidget
import com.navi.amc.fundbuy.models.widgets.TitleSubtitleImageWidget
import com.navi.amc.fundbuy.models.widgets.TitleSubtitleLottieWidget
import com.navi.base.model.ActionData
@Composable
fun AmcComposableWidgetFactory(
data: GenericComposableWidget? = null,
selectionData: SelectionData? = null,
onClick: (actionData: ActionData?) -> Unit,
onVisible: (actionData: ActionData?) -> Unit,
) {
@@ -74,6 +82,9 @@ fun AmcComposableWidgetFactory(
onVisible = onVisible,
)
}
AmcWidgetType.REPEAT_ORDER_WIDGET.value -> {
RepeatOrderWidgetComposable(widget = data as RepeatOrderWidget, onClick = onClick)
}
AmcWidgetType.FOOTER_WIDGET.value -> {
FooterWidgetComposable(widget = data as TitleSubtitleImageWidget)
}
@@ -83,5 +94,15 @@ fun AmcComposableWidgetFactory(
AmcWidgetType.BANNER_WIDGET.value -> {
BannerWidgetComposable(widget = data as TitleSubtitleImageWidget)
}
AmcWidgetType.BANNER_WITH_LOTTIE_WIDGET.value -> {
BannerWithLottieWidgetComposable(widget = data as TitleSubtitleLottieWidget)
}
AmcWidgetType.TITLE_CONTENT_RADIO_WIDGET.value -> {
TitleContentRadioWidgetComposable(
widget = data as TitleContentRadioWidget,
onClick = onClick,
selectionData = selectionData,
)
}
}
}

View File

@@ -7,17 +7,25 @@
package com.navi.amc.common.composables.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.navi.amc.common.model.widgets.SpacerWidget
import com.navi.amc.utils.Constant.SPACER_DEFAULT
import com.navi.naviwidgets.extensions.hexToColor
@Composable
fun SpacerWidgetComposable(spacerData: SpacerWidget) {
spacerData.widgetData?.let { data ->
Spacer(modifier = Modifier.height((data.height ?: SPACER_DEFAULT).dp))
Spacer(
modifier =
Modifier.fillMaxWidth()
.height((data.height ?: SPACER_DEFAULT).dp)
.background(color = hexToColor(data.backgroundColor))
)
}
}

View File

@@ -15,9 +15,12 @@ import com.navi.amc.fundbuy.models.widgets.InvestmentDetailsWidget
import com.navi.amc.fundbuy.models.widgets.MonthlyInvestmentGoalWidget
import com.navi.amc.fundbuy.models.widgets.MonthlyInvestmentTrackerWidget
import com.navi.amc.fundbuy.models.widgets.OnTimeAssuranceInfoWidget
import com.navi.amc.fundbuy.models.widgets.RepeatOrderWidget
import com.navi.amc.fundbuy.models.widgets.RiskFreeFundWidget
import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetWidget
import com.navi.amc.fundbuy.models.widgets.TitleContentRadioWidget
import com.navi.amc.fundbuy.models.widgets.TitleSubtitleImageWidget
import com.navi.amc.fundbuy.models.widgets.TitleSubtitleLottieWidget
import java.lang.reflect.Type
class AmcWidgetJsonDeserializer : JsonDeserializer<GenericComposableWidget> {
@@ -45,6 +48,11 @@ class AmcWidgetJsonDeserializer : JsonDeserializer<GenericComposableWidget> {
AmcWidgetType.FOOTER_WIDGET.value,
AmcWidgetType.BANNER_WIDGET.value,
AmcWidgetType.HELP_INFO_WIDGET.value -> TitleSubtitleImageWidget::class.java
AmcWidgetType.REPEAT_ORDER_WIDGET.value -> RepeatOrderWidget::class.java
AmcWidgetType.BANNER_WITH_LOTTIE_WIDGET.value ->
TitleSubtitleLottieWidget::class.java
AmcWidgetType.TITLE_CONTENT_RADIO_WIDGET.value ->
TitleContentRadioWidget::class.java
else -> null
}
return if (className != null) {

View File

@@ -18,4 +18,7 @@ enum class AmcWidgetType(val value: String) {
HELP_INFO_WIDGET("help_info_widget"),
BANNER_WIDGET("banner_widget"),
FOOTER_WIDGET("footer_widget"),
REPEAT_ORDER_WIDGET("repeat_order_widget"),
BANNER_WITH_LOTTIE_WIDGET("banner_with_lottie_widget"),
TITLE_CONTENT_RADIO_WIDGET("fund_investing_type"),
}

View File

@@ -10,6 +10,7 @@ package com.navi.amc.common.model
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.navi.base.model.ActionData
import com.navi.common.model.InvestmentBaseProperty
import com.navi.design.textview.model.TextWithStyle
import com.navi.naviwidgets.models.response.DataSafeWidget
import kotlinx.parcelize.Parcelize
@@ -24,6 +25,8 @@ data class Footer(
@SerializedName("note") var note: DataSafeWidget? = null,
@SerializedName("footerCallout") var footerCallout: FooterCallout? = null,
@SerializedName("footerCalloutList") var footerCalloutList: FooterCalloutList? = null,
@SerializedName("footerProperties") var footerProperties: FooterProperties? = null,
@SerializedName("showShadow") var showShadow: Boolean? = null,
) : Parcelable
@Parcelize
@@ -47,3 +50,9 @@ class TimerTextList(
@SerializedName("atTimerInfo") var atTimerInfo: TextWithStyle? = null,
@SerializedName("afterTimerInfo") var afterTimerInfo: TextWithStyle? = null,
) : Parcelable
@Parcelize
data class FooterProperties(
@SerializedName("nextCtaProperty") val nextCtaProperty: InvestmentBaseProperty? = null,
@SerializedName("backCtaProperty") val backCtaProperty: InvestmentBaseProperty? = null,
) : Parcelable

View File

@@ -16,4 +16,7 @@ data class SpacerWidget(
@SerializedName("widgetData") val widgetData: SpacerWidgetData? = null,
) : GenericComposableWidget
data class SpacerWidgetData(@SerializedName("height") val height: Int? = null)
data class SpacerWidgetData(
@SerializedName("height") val height: Int? = null,
@SerializedName("backgroundColor", alternate = ["bgColor"]) val backgroundColor: String? = null,
)

View File

@@ -0,0 +1,19 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.model
import com.google.gson.annotations.SerializedName
import com.navi.amc.common.model.Footer
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.common.model.Header
data class AmcGenericScreenResponse(
@SerializedName("header") val header: Header? = null,
@SerializedName("content") val content: List<GenericComposableWidget>? = null,
@SerializedName("footer") val footer: Footer? = null,
)

View File

@@ -0,0 +1,20 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.model
import com.navi.common.network.models.GenericErrorResponse
sealed class ScreenState {
data object Loading : ScreenState()
data class Success(val amcGenericScreenResponse: AmcGenericScreenResponse) : ScreenState()
data class Error(val error: GenericErrorResponse? = null) : ScreenState()
data object Empty : ScreenState()
}

View File

@@ -0,0 +1,75 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import com.navi.amc.compose.common.utils.NaviAmcScreen
import com.navi.amc.compose.common.utils.clearBackStackUpToAndNavigate
import com.navi.amc.compose.destinations.FtueEducateScreenDestination
import com.navi.amc.compose.destinations.FtueFundSelectScreenDestination
import com.navi.amc.compose.entry.AmcComposeActivity
import com.navi.amc.navigator.NaviAmcDeeplinkNavigator.FTUE
import com.navi.amc.utils.Constant.SUB_REDIRECT
import com.navi.common.utils.Constants.SECOND_IDENTIFIER
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
@Destination
@Composable
fun AmcRouterScreen(amcComposeActivity: AmcComposeActivity, navigator: DestinationsNavigator) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
val moduleName = amcComposeActivity.intent.getStringExtra(SECOND_IDENTIFIER).orEmpty()
val screenName = amcComposeActivity.intent.getStringExtra(SUB_REDIRECT).orEmpty()
val destination =
when (moduleName) {
FTUE -> {
when (screenName) {
NaviAmcScreen.AMC_FTUE_EDUCATE_SCREEN.screenName ->
FtueEducateScreenDestination
NaviAmcScreen.AMC_FTUE_FUND_SELECT_SCREEN.screenName ->
FtueFundSelectScreenDestination
else -> {
null
}
}
}
else -> {
null
}
}
scope.launch {
destination?.let {
navigator.clearBackStackUpToAndNavigate(
destination = destination,
inclusive = true,
popUpTo = FtueEducateScreenDestination,
)
} ?: run { amcComposeActivity.finish() }
}
}
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
topBar = {},
content = { innerPadding ->
Column(modifier = Modifier.fillMaxSize().padding(innerPadding)) { AmcShimmer() }
},
)
}

View File

@@ -0,0 +1,51 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.ui
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AmcShimmer() {
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp)) {
Spacer(modifier = Modifier.height(24.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Box(modifier = Modifier.height(34.dp).width(94.dp).shimmerEffect())
Box(modifier = Modifier.height(34.dp).width(148.dp).shimmerEffect())
}
Spacer(modifier = Modifier.height(24.dp))
Box(modifier = Modifier.height(108.dp).fillMaxWidth().shimmerEffect())
Spacer(modifier = Modifier.height(32.dp))
Box(modifier = Modifier.height(24.dp).width(176.dp).shimmerEffect())
Spacer(modifier = Modifier.height(24.dp))
Box(modifier = Modifier.height(132.dp).fillMaxWidth().shimmerEffect())
Spacer(modifier = Modifier.height(32.dp))
Box(modifier = Modifier.height(24.dp).width(120.dp).shimmerEffect())
Spacer(modifier = Modifier.height(24.dp))
Box(modifier = Modifier.height(104.dp).fillMaxWidth().shimmerEffect())
Spacer(modifier = Modifier.height(24.dp))
Box(modifier = Modifier.height(78.dp).fillMaxWidth().shimmerEffect())
Spacer(modifier = Modifier.height(32.dp))
Box(modifier = Modifier.height(24.dp).width(176.dp).shimmerEffect())
Spacer(modifier = Modifier.height(24.dp))
Box(modifier = Modifier.height(132.dp).fillMaxWidth().shimmerEffect())
}
}

View File

@@ -0,0 +1,151 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.ui
import android.app.Activity
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ModalBottomSheetDefaults
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.navi.amc.utils.AmcColor
import com.navi.common.utils.ClickDebounce
import com.navi.common.utils.get
import com.navi.naviwidgets.extensions.hexToInt
@Composable
fun AmcModalBottomSheetLayout(
sheetContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
sheetState: ModalBottomSheetState,
sheetShape: Shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
content: @Composable () -> Unit,
) {
Column(modifier = Modifier.navigationBarsPadding()) {
ModalBottomSheetLayout(
sheetContent = sheetContent,
modifier = modifier,
sheetState = sheetState,
sheetShape = sheetShape,
sheetElevation = sheetElevation,
scrimColor = scrimColor,
content = content,
)
}
}
fun Modifier.clickableDebounce(
debounceTime: Long = 300L,
showRipple: Boolean = true,
rippleColor: Color = Color.Black.copy(alpha = 0.3f),
onClick: () -> Unit,
) = composed {
val clickDebounce = remember { ClickDebounce.get() }
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = if (showRipple) ripple(color = rippleColor) else null,
onClick = { clickDebounce.processClick(onClick = onClick, debounceTime = debounceTime) },
)
}
fun Modifier.shimmerEffect(shimmerColor: List<Color>? = null): Modifier = composed {
var size by remember { mutableFloatStateOf(0f) }
val transition = rememberInfiniteTransition(label = "")
val startOffsetX by
transition.animateFloat(
initialValue = -2 * size,
targetValue = 2 * size,
animationSpec =
infiniteRepeatable(animation = tween(durationMillis = 1000, easing = LinearEasing)),
label = "",
)
drawBehind {
size = this.size.width
drawRect(
size = this.size,
brush =
Brush.linearGradient(
colors =
shimmerColor
?: listOf(Color(0xFFF9F9FB), Color(0xFFE9E7F0), Color(0xFFF9F9FB)),
start = Offset(startOffsetX, 0f),
end = Offset(startOffsetX + size, this.size.height),
),
)
}
}
@Composable
fun ShadowStrip(
modifier: Modifier = Modifier,
height: Dp = 16.dp,
brush: Brush =
Brush.verticalGradient(
colors = listOf(AmcColor.bgDefaultWhite, Color(0xFFCCCCCC).copy(alpha = 0.3f)),
startY = 0f,
endY = 100f,
),
) {
Box(modifier = modifier.height(height = height).background(brush = brush))
}
@Composable
fun SetStatusBarColor(activity: Activity, colorResId: Int = hexToInt("#FFFFFF")) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME,
Lifecycle.Event.ON_CREATE -> {
activity.window.statusBarColor = colorResId
}
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
}

View File

@@ -0,0 +1,105 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.navi.amc.common.model.Footer
import com.navi.amc.utils.AmcColor
import com.navi.base.model.ActionData
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.naviwidgets.extensions.setPadding
import com.navi.naviwidgets.models.response.TextFieldData
import com.navi.uitron.model.ui.ComposePadding
import com.navi.uitron.utils.setBackground
import com.navi.uitron.utils.setPadding
@Composable
fun NaviAmcFooter(
footer: Footer? = null,
onNextCtaClicked: ((actionData: ActionData?) -> Unit)? = null,
onBackCtaClicked: ((actionData: ActionData?) -> Unit)? = null,
) {
Column(modifier = Modifier.background(color = AmcColor.bgDefaultWhite)) {
if (footer?.showShadow == true) {
ShadowStrip(modifier = Modifier.fillMaxWidth())
}
Spacer(modifier = Modifier.height(32.dp))
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
footer?.backCta?.let {
val backCtaProperty = footer.footerProperties?.backCtaProperty
NaviTextWidgetized(
modifier =
Modifier.clickable { onBackCtaClicked?.invoke(footer.backCta) }
.setBackground(
backgroundColor = backCtaProperty?.backgroundColor,
uiTronShape = backCtaProperty?.shape,
brushData = backCtaProperty?.backGroundBrushData,
)
.setPadding(backCtaProperty?.padding ?: ComposePadding(16, 16, 12, 12))
.weight(backCtaProperty?.columnWeight ?: 1f),
textFieldData =
buildFooterTextFieldData(
text = footer.backCta?.title,
textColor = "#1F002A",
),
)
Spacer(modifier = Modifier.width(16.dp))
}
footer?.nextCta?.let {
val nextCtaProperty = footer.footerProperties?.nextCtaProperty
NaviTextWidgetized(
modifier =
Modifier.clickable { onNextCtaClicked?.invoke(footer.nextCta) }
.setBackground(
backgroundColor = nextCtaProperty?.backgroundColor,
uiTronShape = nextCtaProperty?.shape,
brushData = nextCtaProperty?.backGroundBrushData,
)
.setPadding(nextCtaProperty?.padding ?: ComposePadding(16, 16, 12, 12))
.weight(nextCtaProperty?.columnWeight ?: 1f),
textFieldData =
buildFooterTextFieldData(
text = footer.nextCta?.title,
textColor = footer.nextCta?.titleColor,
),
)
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
fun buildFooterTextFieldData(text: String? = null, textColor: String? = null): TextFieldData? {
text?.let {
return TextFieldData(
text = text,
size = 14,
font = "NAVI_BODY_DEMI_BOLD",
alignment = "CENTER",
textColor = textColor,
)
}
return null
}

View File

@@ -0,0 +1,112 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.IconButton
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
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.base.model.ActionData
import com.navi.common.R
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
import com.navi.guarddog.utils.clickableDebounce
import com.navi.naviwidgets.extensions.NaviText
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NaviAmcHeader(
modifier: Modifier = Modifier,
title: String,
navigationIcon: Int = R.drawable.ic_arrow_left_black_v2,
onNavigationIconClick: () -> Unit,
actionIconId: Int = -1,
actionIconText: String? = null,
showRippleOnActionIcon: Boolean = true,
onActionClick: ((actionData: ActionData?) -> Unit)? = null,
backgroundColor: Color = Color.White,
maxLines: Int = 1,
) {
CenterAlignedTopAppBar(
title = {
Box(Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
NaviText(
text = title,
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
color = Color.Black,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 40.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
},
navigationIcon = {
Box(Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
IconButton(onClick = onNavigationIconClick) {
Image(painter = painterResource(navigationIcon), contentDescription = null)
}
}
},
actions = {
Box(Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
if (actionIconId > 0) {
Image(
painter = painterResource(id = actionIconId),
contentDescription = "",
modifier =
Modifier.padding(end = 16.dp).clickableDebounce(
showRipple = showRippleOnActionIcon
) {
onActionClick?.invoke(ActionData())
},
)
} else if (!actionIconText.isNullOrEmpty()) {
NaviText(
text = actionIconText,
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
color = Color.Black,
textAlign = TextAlign.Center,
modifier =
Modifier.padding(end = 16.dp).clickableDebounce {
onActionClick?.invoke(ActionData())
},
)
} else {
Image(
painter = painterResource(id = navigationIcon),
contentDescription = "",
modifier = Modifier.padding(end = 16.dp).alpha(0f),
)
}
}
},
modifier = modifier,
colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor = backgroundColor),
)
}

View File

@@ -0,0 +1,15 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.utils
import com.navi.amc.fundbuy.models.Temperature
enum class NaviAmcScreen(val screenName: String, val screenTemperature: Temperature) {
AMC_FTUE_EDUCATE_SCREEN("educate_screen", Temperature.ORANGE),
AMC_FTUE_FUND_SELECT_SCREEN("fund_select_screen", Temperature.ORANGE),
}

View File

@@ -0,0 +1,52 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.common.utils
import com.navi.amc.compose.entry.AmcComposeActivity
import com.navi.amc.compose.entry.NaviAmcRouter
import com.navi.amc.fundbuy.models.Temperature
import com.navi.base.model.ActionData
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.Direction
import com.ramcosta.composedestinations.spec.Route
fun DestinationsNavigator.clearBackStackUpToAndNavigate(
destination: Direction,
popUpTo: Route,
inclusive: Boolean = true,
) {
navigate(destination) { popUpTo(popUpTo) { this.inclusive = inclusive } }
}
/**
* Navigates to next screen on the basis of the current screen temperature
*
* GREEN SCREEN -> this denotes a source screen, should be added to stack ORANGE SCREEN ->
* intermediate screen, should be removed from fragment stack RED SCREEN -> destination screen, on
* back press from this screen go back to last green screen find the last green screen and pop till
* then
*/
fun navigateToNextScreen(
actionData: ActionData?,
navigator: DestinationsNavigator,
amcComposeActivity: AmcComposeActivity,
screen: NaviAmcScreen,
) {
val direction = NaviAmcRouter.getDirectionFromCta(actionData, amcComposeActivity)
val screenTemperature = screen.screenTemperature
direction?.let {
when (screenTemperature) {
Temperature.GREEN -> {}
Temperature.ORANGE -> {
navigator.popBackStack()
}
Temperature.RED -> {}
}
navigator.navigate(direction)
}
}

View File

@@ -0,0 +1,89 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.entry
import android.graphics.Color
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.view.WindowCompat
import androidx.navigation.NavHostController
import com.navi.amc.common.activity.AmcBaseActivity
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.deeplink.util.DeeplinkConstants
import com.navi.base.model.CtaData
import com.navi.common.model.HelpBottomSheetData
import com.navi.common.model.ModuleNameV2
import com.navi.common.utils.screenEnterTransition
import com.navi.common.utils.screenExitTransition
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AmcComposeActivity : AmcBaseActivity(), BackButtonHandler {
override val screenName: String
get() = "AmcComposeActivity"
override val moduleName: ModuleNameV2
get() = ModuleNameV2.AMC
lateinit var navController: NavHostController
override val isNavControllerInitialized: Boolean
get() = ::navController.isInitialized
private val onBackPressedCallback =
object : OnBackPressedCallback(true) {
// review this
override fun handleOnBackPressed() {
handleBackPress()
}
}
private val amcComposeViewModel by viewModels<AmcComposeViewModel>()
@OptIn(ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
screenEnterTransition()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, true)
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
setContent {
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
AmcMainScreen(amcComposeActivity = this)
}
}
window.decorView.setBackgroundColor(Color.WHITE)
}
override fun finish() {
if (isTaskRoot) {
DeepLinkManager.getDeepLinkListener()
?.navigateTo(
activity = this,
ctaData = CtaData(url = DeeplinkConstants.INVESTMENT),
finish = false,
)
}
super.finish()
screenExitTransition()
}
fun onHelpClick(helpBottomSheetData: HelpBottomSheetData?, bundle: Bundle?) {
openHelpInfo(helpBottomSheetData, bundle)
}
override fun initialiseNavController(navHostController: NavHostController) {
this.navController = navHostController
onNavControllerSet(navHostController)
}
}

View File

@@ -0,0 +1,18 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.entry
import com.navi.amc.common.viewmodel.BaseAmcVM
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class AmcComposeViewModel @Inject constructor() : BaseAmcVM() {
fun getDefaultScreenName() = ""
}

View File

@@ -0,0 +1,95 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.entry
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.navigation.ModalBottomSheetLayout
import androidx.compose.material.navigation.rememberBottomSheetNavigator
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.plusAssign
import com.navi.amc.compose.NavGraphs
import com.navi.amc.compose.common.ui.AmcModalBottomSheetLayout
import com.navi.amc.compose.common.utils.NaviAmcScreen
import com.navi.amc.utils.Constant.START_SCREEN_NAME
import com.navi.amc.utils.Constant.TRANSITION_DURATION_IN_MILLIS
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
import com.ramcosta.composedestinations.navigation.dependency
import com.ramcosta.composedestinations.spec.NavHostEngine
@Composable
fun AmcMainScreen(amcComposeActivity: AmcComposeActivity) {
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController =
rememberNavController().apply { this.navigatorProvider += bottomSheetNavigator }
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator,
content = {
AmcModalBottomSheetLayout(
sheetState =
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
sheetContent = {},
) {
amcComposeActivity.initialiseNavController(navController)
DestinationsNavHost(
startRoute =
NaviAmcRouter.getStartRoute(
amcComposeActivity.intent.getStringExtra(START_SCREEN_NAME)
?: NaviAmcScreen.AMC_FTUE_EDUCATE_SCREEN.screenName
),
navGraph = NavGraphs.root,
engine = naviHostEngine(),
navController = amcComposeActivity.navController,
dependenciesContainerBuilder = { dependency(amcComposeActivity) },
)
}
},
)
// generic error bottomsheet
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun naviHostEngine(): NavHostEngine {
return rememberAnimatedNavHostEngine(
rootDefaultAnimations =
RootNavGraphDefaultAnimations(
enterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(TRANSITION_DURATION_IN_MILLIS),
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(TRANSITION_DURATION_IN_MILLIS),
)
},
popEnterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(TRANSITION_DURATION_IN_MILLIS),
)
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
animationSpec = tween(TRANSITION_DURATION_IN_MILLIS),
)
},
)
)
}

View File

@@ -0,0 +1,44 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.entry
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.deeplink.util.DeeplinkConstants
import com.navi.base.model.CtaData
internal interface BackButtonHandler {
val isNavControllerInitialized: Boolean
fun AmcComposeActivity.handleBackPress() {
when {
isBackPressedOnAmcRootScreen() -> {
if (isTaskRoot) {
goToInvestmentTab()
} else {
finish()
}
}
else -> {
finish()
}
}
}
private fun AmcComposeActivity.isBackPressedOnAmcRootScreen(): Boolean {
return isNavControllerInitialized
}
private fun AmcComposeActivity.goToInvestmentTab() {
DeepLinkManager.getDeepLinkListener()
?.navigateTo(
activity = this,
ctaData = CtaData(url = DeeplinkConstants.INVESTMENT),
finish = true,
)
}
}

View File

@@ -0,0 +1,59 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.entry
import com.navi.amc.compose.common.utils.NaviAmcScreen
import com.navi.amc.compose.destinations.AmcRouterScreenDestination
import com.navi.amc.compose.destinations.DirectionDestination
import com.navi.amc.compose.destinations.FtueEducateScreenDestination
import com.navi.amc.compose.destinations.FtueFundSelectScreenDestination
import com.navi.amc.navigator.NaviAmcDeeplinkNavigator.FTUE
import com.navi.amc.utils.toNavigateAmcModule
import com.navi.base.model.ActionData
import com.ramcosta.composedestinations.spec.Route
object NaviAmcRouter {
fun getStartRoute(startScreenName: String): Route {
return when (startScreenName) {
NaviAmcScreen.AMC_FTUE_EDUCATE_SCREEN.screenName -> FtueEducateScreenDestination
NaviAmcScreen.AMC_FTUE_FUND_SELECT_SCREEN.screenName -> FtueFundSelectScreenDestination
else -> AmcRouterScreenDestination
}
}
fun onCtaClick(amcComposeActivity: AmcComposeActivity, actionData: ActionData?) {
actionData?.toNavigateAmcModule(amcComposeActivity)
}
fun getDirectionFromCta(
actionData: ActionData?,
amcComposeActivity: AmcComposeActivity,
): DirectionDestination? {
val deepLink = actionData?.url
val splitDeepLink = deepLink?.split("/")
val firstIdentifier = splitDeepLink?.getOrNull(1)
val secondIdentifier = splitDeepLink?.getOrNull(2)
return when (firstIdentifier) {
FTUE -> {
when (secondIdentifier) {
NaviAmcScreen.AMC_FTUE_EDUCATE_SCREEN.screenName -> FtueEducateScreenDestination
NaviAmcScreen.AMC_FTUE_FUND_SELECT_SCREEN.screenName ->
FtueFundSelectScreenDestination
else -> {
null
}
}
}
else -> {
actionData?.toNavigateAmcModule(activity = amcComposeActivity, finish = true)
return null
}
}
}
}

View File

@@ -0,0 +1,40 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.feature.ftue
import com.navi.amc.compose.common.model.AmcGenericScreenResponse
import com.navi.amc.compose.feature.ftue.model.FundSelectionData
import com.navi.amc.network.retrofit.RetrofitService
import com.navi.amc.utils.getAmcMetricInfo
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.SuccessResponse
import com.navi.common.network.retrofit.ResponseCallback
import javax.inject.Inject
class FtueRepository @Inject constructor(private val retrofitService: RetrofitService) :
ResponseCallback() {
suspend fun fetchFtueEducateScreenData(): RepoResult<AmcGenericScreenResponse> =
apiResponseCallback(
response = retrofitService.fetchFtueEducateScreenData(),
metricInfo = getAmcMetricInfo(),
)
suspend fun fetchFtueFundSelectScreenData(): RepoResult<AmcGenericScreenResponse> =
apiResponseCallback(
response = retrofitService.fetchFtueFundSelectScreenData(),
metricInfo = getAmcMetricInfo(),
)
suspend fun postSelectedFund(
fundSelectionData: FundSelectionData
): RepoResult<SuccessResponse> =
apiResponseCallback(
response = retrofitService.postSelectedFund(fundSelectionData),
metricInfo = getAmcMetricInfo(),
)
}

View File

@@ -0,0 +1,12 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.feature.ftue.model
import com.google.gson.annotations.SerializedName
data class FundSelectionData(@SerializedName("isin") val isin: String? = null)

View File

@@ -0,0 +1,159 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.feature.ftue.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.amc.common.composables.AmcCommonWidgetListRenderer
import com.navi.amc.compose.common.model.AmcGenericScreenResponse
import com.navi.amc.compose.common.model.ScreenState
import com.navi.amc.compose.common.ui.AmcShimmer
import com.navi.amc.compose.common.ui.NaviAmcFooter
import com.navi.amc.compose.common.ui.NaviAmcHeader
import com.navi.amc.compose.common.ui.SetStatusBarColor
import com.navi.amc.compose.common.utils.NaviAmcScreen
import com.navi.amc.compose.common.utils.navigateToNextScreen
import com.navi.amc.compose.entry.AmcComposeActivity
import com.navi.amc.compose.entry.NaviAmcRouter
import com.navi.amc.compose.feature.ftue.viewmodel.FtueViewModel
import com.navi.amc.fundbuy.models.AmcHeaderExtraData
import com.navi.amc.utils.AmcAnalytics.sendEvent
import com.navi.base.model.ActionData
import com.navi.base.utils.EMPTY
import com.navi.common.ui.errorview.FullScreenErrorComposeView
import com.navi.naviwidgets.extensions.hexToColor
import com.navi.naviwidgets.extensions.hexToInt
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@RootNavGraph(start = true)
@Destination
@Composable
fun FtueEducateScreen(
amcComposeActivity: AmcComposeActivity,
navigator: DestinationsNavigator,
ftueViewModel: FtueViewModel = hiltViewModel(),
) {
val ftueEducateScreenState by ftueViewModel.ftueEducateScreenState.collectAsStateWithLifecycle()
LaunchedEffect(key1 = ftueEducateScreenState) {
if (ftueEducateScreenState is ScreenState.Empty) {
ftueViewModel.fetchFtueEducateScreenData()
}
}
val onFooterNextCtaClicked = { actionData: ActionData? ->
sendEvent(
actionData?.metaData?.clickedData,
screenName = NaviAmcScreen.AMC_FTUE_EDUCATE_SCREEN.screenName,
)
navigateToNextScreen(
actionData = actionData,
navigator = navigator,
amcComposeActivity = amcComposeActivity,
screen = NaviAmcScreen.AMC_FTUE_EDUCATE_SCREEN,
)
}
val onBackPressed = {
// analytics
amcComposeActivity.finish()
}
BackHandler { onBackPressed() }
val onHelpClicked = { actionData: ActionData? ->
amcComposeActivity.onHelpClick(helpBottomSheetData = null, bundle = null)
}
Column(modifier = Modifier.fillMaxSize()) {
when (ftueEducateScreenState) {
is ScreenState.Loading -> {
AmcShimmer()
}
is ScreenState.Success -> {
val data = ftueEducateScreenState as ScreenState.Success
FtueEducateScreenRenderer(
amcComposeActivity = amcComposeActivity,
amcGenericScreenResponse = data.amcGenericScreenResponse,
onBackPressed = onBackPressed,
onHelpClicked = { onHelpClicked(it) },
onFooterNextCtaClicked = onFooterNextCtaClicked,
)
}
is ScreenState.Error -> {
val data = ftueEducateScreenState as ScreenState.Error
FullScreenErrorComposeView(
error = data.error,
onRetryClick = { ftueViewModel.fetchFtueEducateScreenData() },
)
}
else -> {}
}
}
}
@Composable
fun FtueEducateScreenRenderer(
amcComposeActivity: AmcComposeActivity,
amcGenericScreenResponse: AmcGenericScreenResponse,
onBackPressed: () -> Unit,
onHelpClicked: ((actionData: ActionData?) -> Unit)? = null,
onFooterNextCtaClicked: ((actionData: ActionData?) -> Unit)? = null,
onFooterBackCtaClicked: ((actionData: ActionData?) -> Unit)? = null,
) {
SetStatusBarColor(amcComposeActivity, hexToInt(amcGenericScreenResponse.header?.bgColor))
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
topBar = {
NaviAmcHeader(
title = EMPTY,
onNavigationIconClick = onBackPressed,
actionIconText = "HELP",
onActionClick = onHelpClicked,
backgroundColor = hexToColor(amcGenericScreenResponse.header?.bgColor),
)
},
content = { innerPadding ->
AmcCommonWidgetListRenderer(
widgetList = amcGenericScreenResponse.content,
actionListener = { actionData ->
NaviAmcRouter.onCtaClick(
amcComposeActivity = amcComposeActivity,
actionData = actionData,
)
},
extraData =
AmcHeaderExtraData(
header = amcGenericScreenResponse.header,
toggleStatusBarAndHeaderData = { color, showTitle -> },
showHeaderTitle = false,
toggleShadow = { shadowState -> },
),
eventHandler = { actionData -> },
)
},
bottomBar = {
NaviAmcFooter(
footer = amcGenericScreenResponse.footer,
onNextCtaClicked = onFooterNextCtaClicked,
onBackCtaClicked = onFooterBackCtaClicked,
)
},
)
}

View File

@@ -0,0 +1,190 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.feature.ftue.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.amc.common.activity.CheckerActivity
import com.navi.amc.common.composables.AmcCommonWidgetListRenderer
import com.navi.amc.compose.common.model.AmcGenericScreenResponse
import com.navi.amc.compose.common.model.ScreenState
import com.navi.amc.compose.common.ui.AmcShimmer
import com.navi.amc.compose.common.ui.NaviAmcFooter
import com.navi.amc.compose.common.ui.NaviAmcHeader
import com.navi.amc.compose.common.ui.SetStatusBarColor
import com.navi.amc.compose.common.utils.NaviAmcScreen
import com.navi.amc.compose.common.utils.navigateToNextScreen
import com.navi.amc.compose.entry.AmcComposeActivity
import com.navi.amc.compose.feature.ftue.viewmodel.FtueViewModel
import com.navi.amc.fundbuy.models.AmcHeaderExtraData
import com.navi.amc.fundbuy.models.SelectionData
import com.navi.amc.navigator.NaviAmcDeeplinkNavigator
import com.navi.amc.navigator.NaviAmcDeeplinkNavigator.FTUE
import com.navi.amc.utils.AmcAnalytics.ISIN
import com.navi.amc.utils.AmcAnalytics.sendEvent
import com.navi.amc.utils.Constant
import com.navi.amc.utils.TempStorageHelper
import com.navi.base.model.ActionData
import com.navi.base.utils.EMPTY
import com.navi.base.utils.orFalse
import com.navi.common.ui.errorview.FullScreenErrorComposeView
import com.navi.naviwidgets.extensions.hexToInt
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@Destination
@Composable
fun FtueFundSelectScreen(
amcComposeActivity: AmcComposeActivity,
navigator: DestinationsNavigator,
ftueViewModel: FtueViewModel = hiltViewModel(),
) {
val ftueFundSelectScreenState by
ftueViewModel.ftueFundSelectScreenState.collectAsStateWithLifecycle()
LaunchedEffect(key1 = ftueFundSelectScreenState) {
if (ftueFundSelectScreenState is ScreenState.Empty) {
ftueViewModel.fetchFtueFundSelectScreenState()
}
}
val onFooterNextCtaClicked = { actionData: ActionData? ->
ftueViewModel.postSelectedFund()
sendEvent(
actionData?.metaData?.clickedData,
screenName = NaviAmcScreen.AMC_FTUE_FUND_SELECT_SCREEN.screenName,
)
if (
actionData
?.url
?.contains(
NaviAmcDeeplinkNavigator.AMC.plus("/").plus(NaviAmcDeeplinkNavigator.KYC),
true,
)
.orFalse() ||
actionData?.url?.contains(CheckerActivity.HPC_PAN_REDIRECTION_PAGE).orFalse() ||
actionData?.url?.contains(CheckerActivity.HPC_NAME_REDIRECTION_PAGE).orFalse()
) {
TempStorageHelper.kycSourceInfo =
mapOf(
Constant.KYC_SOURCE_SCREEN to FTUE,
ISIN to ftueViewModel.selectedIsin.orEmpty(),
)
}
navigateToNextScreen(
actionData = actionData,
navigator = navigator,
amcComposeActivity = amcComposeActivity,
screen = NaviAmcScreen.AMC_FTUE_FUND_SELECT_SCREEN,
)
}
val onHelpClicked = { actionData: ActionData? ->
amcComposeActivity.onHelpClick(helpBottomSheetData = null, bundle = null)
}
val onBackPressed = {
// analytics
amcComposeActivity.finish()
Unit
}
BackHandler { onBackPressed() }
val onFundSelected = { actionData: ActionData? ->
val isin = actionData?.parameters?.find { it.key == ISIN }?.value
ftueViewModel.updateSelectedIsin(isin)
}
val selectedIsin = ftueViewModel.selectedIsin
Column(modifier = Modifier.fillMaxSize()) {
when (ftueFundSelectScreenState) {
is ScreenState.Loading -> {
AmcShimmer()
}
is ScreenState.Success -> {
val data = ftueFundSelectScreenState as ScreenState.Success
FtueFundSelectScreenRenderer(
amcComposeActivity = amcComposeActivity,
amcGenericScreenResponse = data.amcGenericScreenResponse,
onFundSelected = onFundSelected,
onBackPressed = onBackPressed,
onHelpClicked = onHelpClicked,
onFooterNextCtaClicked = onFooterNextCtaClicked,
selectedIsin = selectedIsin,
)
}
is ScreenState.Error -> {
val data = ftueFundSelectScreenState as ScreenState.Error
FullScreenErrorComposeView(
error = data.error,
onRetryClick = { ftueViewModel.fetchFtueFundSelectScreenState() },
)
}
else -> {}
}
}
}
@Composable
fun FtueFundSelectScreenRenderer(
amcComposeActivity: AmcComposeActivity,
amcGenericScreenResponse: AmcGenericScreenResponse,
onFundSelected: ((actionData: ActionData?) -> Unit)? = null,
onBackPressed: () -> Unit,
onHelpClicked: ((actionData: ActionData?) -> Unit)? = null,
onFooterNextCtaClicked: ((actionData: ActionData?) -> Unit)? = null,
onFooterBackCtaClicked: ((actionData: ActionData?) -> Unit)? = null,
selectedIsin: String? = null,
) {
SetStatusBarColor(amcComposeActivity, hexToInt(amcGenericScreenResponse.header?.bgColor))
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
topBar = {
// generalise this function to receive header dto
NaviAmcHeader(
title = EMPTY,
onNavigationIconClick = onBackPressed,
actionIconText = amcGenericScreenResponse.header?.help?.title?.text ?: EMPTY,
onActionClick = onHelpClicked,
)
},
content = { innerPadding ->
AmcCommonWidgetListRenderer(
widgetList = amcGenericScreenResponse.content,
actionListener = { actionData -> onFundSelected?.invoke(actionData) },
extraData =
AmcHeaderExtraData(
header = amcGenericScreenResponse.header,
toggleStatusBarAndHeaderData = { color, showTitle -> },
showHeaderTitle = false,
toggleShadow = { shadowState -> },
),
eventHandler = { actionData -> },
selectionExtraData = SelectionData(selectedId = selectedIsin),
)
},
bottomBar = {
NaviAmcFooter(
footer = amcGenericScreenResponse.footer,
onNextCtaClicked = onFooterNextCtaClicked,
onBackCtaClicked = onFooterBackCtaClicked,
)
},
)
}

View File

@@ -0,0 +1,105 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.compose.feature.ftue.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.navi.amc.common.viewmodel.BaseAmcVM
import com.navi.amc.compose.common.model.ScreenState
import com.navi.amc.compose.feature.ftue.FtueRepository
import com.navi.amc.compose.feature.ftue.model.FundSelectionData
import com.navi.common.network.models.isSuccessWithData
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@HiltViewModel
class FtueViewModel @Inject constructor(private val ftueRepository: FtueRepository) : BaseAmcVM() {
// implement caching
private val _ftueEducateScreenState = MutableStateFlow<ScreenState>(ScreenState.Empty)
val ftueEducateScreenState = _ftueEducateScreenState.asStateFlow()
private val _ftueFundSelectScreenState = MutableStateFlow<ScreenState>(ScreenState.Empty)
val ftueFundSelectScreenState = _ftueFundSelectScreenState.asStateFlow()
var selectedIsin by mutableStateOf<String?>(null)
private set
private fun updateFtueEducateScreenState(state: ScreenState) {
_ftueEducateScreenState.value = state
}
private fun updateFtueFundSelectScreenState(state: ScreenState) {
_ftueFundSelectScreenState.value = state
}
fun updateSelectedIsin(isin: String?) {
isin?.let { selectedIsin = isin }
}
fun postSelectedFund() {
val isin = selectedIsin
isin?.let {
viewModelScope.safeLaunch {
if (isin.isNotEmpty()) {
val fundSelectionData = FundSelectionData(isin = isin)
ftueRepository.postSelectedFund(fundSelectionData)
}
}
}
}
fun fetchFtueEducateScreenData() {
viewModelScope.safeLaunch {
updateFtueEducateScreenState(ScreenState.Loading)
val ftueEducateResponse = ftueRepository.fetchFtueEducateScreenData()
if (ftueEducateResponse.isSuccessWithData()) {
updateFtueEducateScreenState(
ScreenState.Success(
amcGenericScreenResponse = ftueEducateResponse.data ?: return@safeLaunch
)
)
} else {
updateFtueEducateScreenState(
ScreenState.Error(
error = ftueEducateResponse.errors?.firstOrNull() ?: return@safeLaunch
)
)
}
}
}
fun fetchFtueFundSelectScreenState() {
viewModelScope.safeLaunch {
updateFtueFundSelectScreenState(ScreenState.Loading)
val ftueFundSelectResponse = ftueRepository.fetchFtueFundSelectScreenData()
if (ftueFundSelectResponse.isSuccessWithData()) {
updateFtueFundSelectScreenState(
ScreenState.Success(
amcGenericScreenResponse = ftueFundSelectResponse.data ?: return@safeLaunch
)
)
} else {
updateFtueFundSelectScreenState(
ScreenState.Error(
error = ftueFundSelectResponse.errors?.firstOrNull() ?: return@safeLaunch
)
)
}
}
}
}

View File

@@ -0,0 +1,233 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.fundbuy.composables.cards
/*
*
* * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
import PaymentCard
import androidx.compose.foundation.BorderStroke
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.DefaultShadowColor
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.navi.amc.utils.Constant.UPCOMING_SIP_PAYMENT_CARD
import com.navi.base.model.ActionData
import com.navi.base.utils.isNotNull
import com.navi.base.utils.orFalse
import com.navi.base.utils.orTrue
import com.navi.design.utils.clickableWithNoGesture
import com.navi.naviwidgets.extensions.NaviImage
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.uitron.utils.ShapeUtil
import com.navi.uitron.utils.getBorderStrokeBrushData
import com.navi.uitron.utils.hexToComposeColor
import com.navi.uitron.utils.setBackground
import com.navi.uitron.utils.setHorizontalArrangement
import com.navi.uitron.utils.setPadding
@Composable
fun PaymentCardComposable(
paymentCard: PaymentCard,
cardWidth: Dp = LocalConfiguration.current.screenWidthDp.dp,
onClick: (actionData: ActionData?) -> Unit,
cardType: String? = UPCOMING_SIP_PAYMENT_CARD,
) {
if (paymentCard.properties?.cardProperty?.visible.orTrue().not()) {
return
}
Card(
shape = ShapeUtil.run { getShape(shape = paymentCard.properties?.cardProperty?.shape) },
elevation = 0.dp,
backgroundColor =
paymentCard.properties?.cardProperty?.backgroundColor?.hexToComposeColor ?: Color.White,
border =
BorderStroke(
width =
((paymentCard.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp,
brush =
getBorderStrokeBrushData(paymentCard.properties?.cardProperty?.borderStrokeData),
),
modifier =
Modifier.width(cardWidth)
.shadow(
elevation = (paymentCard.properties?.cardProperty?.elevation ?: 0).dp,
ambientColor =
paymentCard.properties?.cardProperty?.ambientColor?.hexToComposeColor
?: DefaultShadowColor,
spotColor =
paymentCard.properties?.cardProperty?.spotColor?.hexToComposeColor
?: DefaultShadowColor,
shape = ShapeUtil.getShape(shape = paymentCard.properties?.cardProperty?.shape),
)
.clickableWithNoGesture(onClick = { onClick(paymentCard.actionData) }),
) {
Column(
modifier =
Modifier.padding(
top = (paymentCard.properties?.cardProperty?.padding?.top ?: 0).dp,
bottom = (paymentCard.properties?.cardProperty?.padding?.bottom ?: 0).dp,
start = (paymentCard.properties?.cardProperty?.padding?.start ?: 0).dp,
end = (paymentCard.properties?.cardProperty?.padding?.end ?: 0).dp,
)
) {
if (
paymentCard.paymentCardHeader.isNotNull() &&
paymentCard.properties?.paymentCardHeaderProperty?.visible.orTrue()
) {
PaymentCardHeader(paymentCard = paymentCard)
}
PaymentDetailsComposable(paymentCard = paymentCard, cardType = cardType)
}
}
}
@Composable
fun PaymentDetailsComposable(paymentCard: PaymentCard, cardType: String?) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
if (paymentCard.properties?.investmentsIconProperty?.visible.orFalse()) {
paymentCard.investmentsIcon?.let {
NaviImage(
imageFieldData = it,
modifier =
Modifier.width((it.iconWidth ?: 12).dp).height((it.iconHeight ?: 12).dp),
)
Spacer(
modifier =
Modifier.wrapContentHeight()
.width(
(paymentCard.properties?.investmentsIconProperty?.margin?.end ?: 8)
.dp
)
)
}
}
Column(
modifier = Modifier.weight(paymentCard.properties?.cardProperty?.cardWeight ?: 1.0f)
) {
if (cardType == "NORMAL_CARD") {
NaviTextWidgetized(textFieldData = paymentCard.fundName)
} else if (cardType == UPCOMING_SIP_PAYMENT_CARD) {
Row(verticalAlignment = Alignment.CenterVertically) {
NaviTextWidgetized(textFieldData = paymentCard.paymentAmount)
NaviTextWidgetized(textFieldData = paymentCard.fundName)
}
NaviTextWidgetized(
textFieldData = paymentCard.paymentCardSubtitle,
modifier =
Modifier.height((paymentCard.paymentCardSubtitle?.lineSpacing ?: 16).dp),
)
} else {
NaviTextWidgetized(
textFieldData = paymentCard.fundName,
modifier = Modifier.height((paymentCard.fundName?.lineSpacing ?: 16).dp),
)
paymentCard.paymentAmount?.let {
NaviTextWidgetized(
textFieldData = paymentCard.paymentAmount,
modifier =
Modifier.height((paymentCard.paymentAmount?.lineSpacing ?: 16).dp),
)
}
}
}
Spacer(
modifier =
Modifier.weight((1 - (paymentCard.properties?.cardProperty?.cardWeight ?: 1.0f)))
)
NaviTextWidgetized(
textFieldData = paymentCard.buttonText,
modifier =
Modifier.setBackground(
paymentCard.properties?.buttonProperty?.backgroundColor,
paymentCard.properties?.buttonProperty?.shape,
paymentCard.properties?.buttonProperty?.backGroundBrushData,
)
.padding(
top = (paymentCard.properties?.buttonProperty?.padding?.top ?: 0).dp,
bottom = (paymentCard.properties?.buttonProperty?.padding?.bottom ?: 0).dp,
start = (paymentCard.properties?.buttonProperty?.padding?.start ?: 0).dp,
end = (paymentCard.properties?.buttonProperty?.padding?.end ?: 0).dp,
),
)
}
}
@Composable
fun PaymentCardHeader(paymentCard: PaymentCard) {
Row(
modifier =
Modifier.setPadding(paymentCard.properties?.paymentCardHeaderProperty?.padding)
.fillMaxWidth(),
horizontalArrangement =
Arrangement.setHorizontalArrangement(
arrangementData = paymentCard.properties?.paymentCardHeaderProperty?.arrangementData
),
) {
paymentCard.paymentCardHeader?.paymentType?.let {
NaviTextWidgetized(
modifier =
Modifier.setBackground(
paymentCard.properties?.paymentTypeProperty?.backgroundColor,
paymentCard.properties?.paymentTypeProperty?.shape,
paymentCard.properties?.paymentTypeProperty?.backGroundBrushData,
)
.padding(
start =
(paymentCard.properties?.paymentTypeProperty?.padding?.start ?: 0)
.dp,
end =
(paymentCard.properties?.paymentTypeProperty?.padding?.end ?: 0).dp,
top =
(paymentCard.properties?.paymentTypeProperty?.padding?.top ?: 0).dp,
bottom =
(paymentCard.properties?.paymentTypeProperty?.padding?.bottom ?: 0)
.dp,
),
textFieldData = it,
)
}
paymentCard.paymentCardHeader?.paymentDate?.let {
NaviTextWidgetized(
textFieldData = it,
modifier = Modifier.setPadding(paymentCard.properties?.paymentDateProperty?.padding),
)
}
}
}

View File

@@ -5,12 +5,11 @@
*
*/
package com.navi.amc.fundbuy.composables
package com.navi.amc.fundbuy.composables.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -42,10 +41,19 @@ fun BannerWidgetComposable(widget: TitleSubtitleImageWidget?) {
NaviImage(
imageFieldData = data.image,
modifier =
Modifier.setPadding(
data.properties?.titleProperty?.padding
?: ComposePadding(top = 16, bottom = 16)
),
Modifier.then(
if (data.properties?.imageProperty?.cardWeight != null) {
Modifier.fillMaxWidth(
data.properties.imageProperty.cardWeight ?: 1f
)
} else {
Modifier
}
)
.setPadding(
data.properties?.titleProperty?.padding
?: ComposePadding(top = 16, bottom = 16)
),
)
NaviTextWidgetized(
textFieldData = data.subTitle,

View File

@@ -0,0 +1,61 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.fundbuy.composables.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.navi.amc.fundbuy.models.widgets.TitleSubtitleLottieWidget
import com.navi.naviwidgets.extensions.NaviLottie
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.naviwidgets.extensions.hexToColor
import com.navi.uitron.model.ui.ComposePadding
import com.navi.uitron.utils.ShapeUtil
import com.navi.uitron.utils.setPadding
@Composable
fun BannerWithLottieWidgetComposable(widget: TitleSubtitleLottieWidget?) {
widget?.widgetData?.let { data ->
Column(
modifier =
Modifier.fillMaxWidth()
.background(
color = hexToColor(data.properties?.cardProperty?.backgroundColor),
shape = ShapeUtil.getShape(data.properties?.cardProperty?.shape),
)
.setPadding(
data.properties?.cardProperty?.padding
?: ComposePadding(start = 16, end = 16, top = 12)
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
NaviTextWidgetized(textFieldData = data.title)
data.lottie?.let {
NaviLottie(
lottie = it,
modifier =
Modifier.fillMaxWidth()
.setPadding(
data.properties?.lottieProperty?.padding
?: ComposePadding(top = 16, bottom = 16)
),
)
}
NaviTextWidgetized(
textFieldData = data.subTitle,
modifier =
Modifier.setPadding(
data.properties?.subTitleProperty?.padding ?: ComposePadding(bottom = 24)
),
)
}
}
}

View File

@@ -5,7 +5,7 @@
*
*/
package com.navi.amc.fundbuy.composables
package com.navi.amc.fundbuy.composables.widgets
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth

View File

@@ -5,7 +5,7 @@
*
*/
package com.navi.amc.fundbuy.composables
package com.navi.amc.fundbuy.composables.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement

View File

@@ -5,7 +5,7 @@
*
*/
package com.navi.amc.fundbuy.composables
package com.navi.amc.fundbuy.composables.widgets
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

View File

@@ -0,0 +1,101 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.fundbuy.composables.widgets
import androidx.compose.foundation.background
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.navi.amc.common.composables.AmcCardListComposable
import com.navi.amc.fundbuy.models.widgets.RepeatOrderWidget
import com.navi.amc.utils.Constant.DEFAULT_CARD_WIDTH_FACTOR
import com.navi.amc.utils.Constant.FREE_SCROLL
import com.navi.amc.utils.Constant.REPEAT_ORDER
import com.navi.base.model.ActionData
import com.navi.base.model.GenericAnalytics
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.naviwidgets.extensions.hexToColor
import com.navi.uitron.utils.setPadding
@Composable
fun RepeatOrderWidgetComposable(
widget: RepeatOrderWidget,
onClick: (actionData: ActionData?) -> Unit,
onVisible: ((genericAnalytics: GenericAnalytics?) -> Unit)? = null,
) {
widget.widgetData?.let {
Column(modifier = Modifier.setPadding(it.extraData?.property?.padding)) {
val listState = rememberLazyListState()
val pagerState =
rememberPagerState(pageCount = { it.content?.repeatOrderCardList?.size ?: 0 })
NaviTextWidgetized(
textFieldData = it.header?.title,
modifier = Modifier.setPadding(it.header?.property?.padding),
)
AmcCardListComposable(
cardType = REPEAT_ORDER,
cardWidthFactor =
(it.extraData?.property?.cardWidthFactor ?: DEFAULT_CARD_WIDTH_FACTOR),
cardList = it.content?.repeatOrderCardList,
onClick = onClick,
scrollType = it.extraData?.scrollType ?: FREE_SCROLL,
listState = listState,
pagerState = pagerState,
)
it.content?.repeatOrderCardList?.size?.let { listSize ->
it.content.carouselData?.let { carouselData ->
Row(
horizontalArrangement = Arrangement.Center,
modifier =
Modifier.setPadding(carouselData.carouselProperty?.padding)
.fillMaxWidth(),
) {
repeat(listSize) { index ->
Box(
modifier =
Modifier.size(
width =
if (index == pagerState.currentPage) {
(carouselData.activeCarouselWidth ?: 24).dp
} else {
(carouselData.inactiveCarouselWidth ?: 16).dp
},
height = (carouselData.carouselHeight ?: 3).dp,
)
.clip(CircleShape)
.background(
if (index == pagerState.currentPage) {
hexToColor(carouselData.activeCarouselColor)
} else hexToColor(carouselData.inactiveCarouselColor)
)
.padding(horizontal = 4.dp)
)
Spacer(modifier = Modifier.width(4.dp))
}
}
}
}
}
}
}

View File

@@ -0,0 +1,183 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.fundbuy.composables.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.navi.amc.fundbuy.models.SelectionData
import com.navi.amc.fundbuy.models.widgets.TitleContentRadioWidget
import com.navi.base.model.ActionData
import com.navi.base.model.LineItem
import com.navi.base.utils.orZero
import com.navi.design.utils.clickableWithNoGesture
import com.navi.naviwidgets.extensions.NaviImage
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.naviwidgets.extensions.hexToColor
import com.navi.uitron.model.ui.ComposePadding
import com.navi.uitron.utils.ShapeUtil
import com.navi.uitron.utils.setPadding
@Composable
fun TitleContentRadioWidgetComposable(
widget: TitleContentRadioWidget?,
onClick: (actionData: ActionData?) -> Unit,
selectionData: SelectionData? = null,
) {
LaunchedEffect(Unit) {
if (widget?.widgetData?.selectionProperties?.isSelected == true) {
onClick(
ActionData(
parameters =
listOf(
LineItem(
key = "isin",
value = widget.widgetData.selectionProperties.isin,
)
)
)
)
}
}
widget?.widgetData?.let { data ->
Column(
modifier =
Modifier.fillMaxWidth()
.clickableWithNoGesture {
onClick(
ActionData(
parameters =
listOf(
LineItem(
key = "isin",
value = data.selectionProperties?.isin,
)
)
)
)
}
.background(
color = hexToColor(data.properties?.cardProperty?.backgroundColor),
shape = ShapeUtil.getShape(data.properties?.cardProperty?.shape),
)
.setPadding(
data.properties?.cardProperty?.padding
?: ComposePadding(start = 16, end = 16, top = 16, bottom = 16)
)
.border(
width = data.properties?.cardProperty?.borderStrokeData?.width?.dp ?: 0.dp,
color = hexToColor(data.properties?.cardProperty?.borderStrokeData?.color),
shape =
ShapeUtil.getShape(
data.properties?.cardProperty?.borderStrokeData?.shape
),
),
horizontalAlignment = Alignment.Start,
) {
Row(
modifier =
Modifier.fillMaxWidth()
.setPadding(
data.properties?.titleProperty?.padding
?: ComposePadding(start = 16, end = 16, top = 16, bottom = 4)
),
verticalAlignment = Alignment.CenterVertically,
) {
data.title?.let { NaviTextWidgetized(textFieldData = it) }
Spacer(modifier = Modifier.width(8.dp))
data.tagTitle?.let {
NaviTextWidgetized(
textFieldData = it,
modifier =
Modifier.background(
color =
hexToColor(data.properties?.tagProperty?.backgroundColor),
shape = ShapeUtil.getShape(data.properties?.tagProperty?.shape),
)
.setPadding(
data.properties?.tagProperty?.padding
?: ComposePadding(start = 10, end = 10, top = 2, bottom = 2)
),
)
}
Spacer(modifier = Modifier.weight(1f))
if (data.showTitleRightIcon == true) {
data.selectionProperties?.let {
NaviImage(
imageFieldData =
if (selectionData?.selectedId == it.isin) it.selectedTitleRightIcon
else it.unselectedTitleRightIcon,
modifier =
Modifier.setPadding(
data.selectionProperties.titleRightIconProperties?.padding
?: ComposePadding(start = 0, end = 0, top = 0, bottom = 0)
),
)
}
}
}
data.subTitle?.let {
NaviTextWidgetized(
textFieldData = it,
modifier =
Modifier.setPadding(
data.properties?.subTitleProperty?.padding
?: ComposePadding(start = 16, end = 16, top = 0, bottom = 16)
),
)
}
data.contentTitle?.let {
NaviTextWidgetized(
textFieldData = it,
modifier =
Modifier.setPadding(
data.properties?.contentTitleProperty?.padding
?: ComposePadding(start = 16, end = 16, top = 0, bottom = 4)
),
)
}
Row(
modifier =
Modifier.fillMaxWidth()
.setPadding(
data.properties?.contentSubtitleProperty?.padding
?: ComposePadding(start = 16, end = 16, top = 0, bottom = 16)
),
verticalAlignment = Alignment.CenterVertically,
) {
data.contentLeftSubtitle?.let {
NaviTextWidgetized(
textFieldData = it,
modifier =
Modifier.then(
if (data.contentLeftSubtitle.text?.length.orZero() > 40) {
Modifier.weight(1f)
} else {
Modifier
}
),
)
}
data.contentMiddleImage?.let { NaviImage(imageFieldData = it) }
data.contentRightSubtitle?.let {
NaviTextWidgetized(textFieldData = it, modifier = Modifier.wrapContentWidth())
}
}
}
}
}

View File

@@ -46,3 +46,5 @@ data class AmcHeaderExtraData(
val scrollOffsetForHeaderStateToggle: Int? = null,
val toggleShadow: ((showShadow: Boolean) -> Unit)? = null,
)
data class SelectionData(val selectedId: String? = null)

View File

@@ -6,6 +6,7 @@
*/
import com.google.gson.annotations.SerializedName
import com.navi.amc.common.model.NextCtaResponse
import com.navi.base.model.ActionData
import com.navi.common.model.GenericBottomSheetData
import com.navi.common.model.InvestmentBaseProperty
@@ -40,3 +41,30 @@ data class CardProperties(
@SerializedName("investmentsIconProperty")
val investmentsIconProperty: InvestmentBaseProperty? = null,
)
data class BottomSheetData(
@SerializedName("title") val title: TextFieldData? = null,
@SerializedName("subtitle") val subtitle: TextFieldData? = null,
@SerializedName("leftIcon") val leftIcon: ImageFieldData? = null,
@SerializedName("rightIcon") val rightIcon: ImageFieldData? = null,
@SerializedName("buttonText", alternate = ["primaryButtonText"])
val buttonText: TextFieldData? = null,
@SerializedName("secondaryButtonText") val secondaryButtonText: TextFieldData? = null,
@SerializedName("noteData") val noteData: BottomSheetData? = null,
@SerializedName("imageUrl") val imageUrl: ImageFieldData? = null,
@SerializedName("properties") val properties: BottomSheetProperties? = null,
@SerializedName("actionData") val actionData: ActionData? = null,
@SerializedName("nextCtaResponse") val nextCtaResponse: NextCtaResponse? = null,
)
data class BottomSheetProperties(
@SerializedName("buttonProperty", alternate = ["primaryButtonProperty"])
val primaryButtonProperty: InvestmentBaseProperty? = null,
@SerializedName("secondaryButtonProperty")
val secondaryButtonProperty: InvestmentBaseProperty? = null,
@SerializedName("subtitleProperty") val subtitleProperty: InvestmentBaseProperty? = null,
@SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null,
@SerializedName("imageProperty") val imageProperty: InvestmentBaseProperty? = null,
@SerializedName("noteProperty") val noteProperty: InvestmentBaseProperty? = null,
@SerializedName("bottomSheetProperty") val bottomSheetProperty: InvestmentBaseProperty? = null,
)

View File

@@ -0,0 +1,81 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.fundbuy.models.widgets
import com.google.gson.annotations.SerializedName
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.base.model.ActionData
import com.navi.base.model.GenericAnalytics
import com.navi.common.model.InvestmentBaseProperty
import com.navi.naviwidgets.models.response.ImageFieldData
import com.navi.naviwidgets.models.response.TextFieldData
data class FtueWithTrackerWidget(
@SerializedName("widgetName") override val widgetName: String? = null,
@SerializedName("widgetId") override val widgetId: String? = null,
@SerializedName("widgetData") val widgetData: FtueWithTrackerWidgetData? = null,
) : GenericComposableWidget
data class FtueWithTrackerWidgetData(
@SerializedName("content") val content: FtueWithTrackerWidgetContent? = null,
@SerializedName("extraData") val extraData: FtueWithTrackerWidgetExtraData? = null,
)
data class FtueWithTrackerWidgetContent(
@SerializedName("topLogo") val topLogo: ImageFieldData? = null,
@SerializedName("topText") val topText: TextFieldData? = null,
@SerializedName("rolodexItems") val rolodexItems: List<TextFieldData>? = null,
@SerializedName("chipItems") val chipItems: List<TextFieldData>? = null,
@SerializedName("actionCardData") val actionCardData: FtueWithTrackerActionCardData? = null,
@SerializedName("properties") val properties: FtueWithTrackerWidgetProperties? = null,
)
data class FtueWithTrackerActionCardData(
@SerializedName("trackerItems") val trackerItems: List<IconWithTextCard>? = null,
@SerializedName("buttonText") val buttonText: TextFieldData? = null,
@SerializedName("footerTexts") val footerTexts: FooterTexts? = null,
@SerializedName("actionData") val actionData: ActionData? = null,
@SerializedName("actionCardProperty") val actionCardProperty: FtueActionCardProperty? = null,
)
data class IconWithTextCard(
@SerializedName("icon") val icon: ImageFieldData? = null,
@SerializedName("text") val text: TextFieldData? = null,
@SerializedName("actionData") val actionData: ActionData? = null,
@SerializedName("property") val property: InvestmentBaseProperty? = null,
)
data class FooterTexts(
@SerializedName("leftText") val leftText: TextFieldData? = null,
@SerializedName("rightText") val rightText: TextFieldData? = null,
)
data class FtueActionCardProperty(
@SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null,
@SerializedName("trackerItemsProperty")
val trackerItemsProperty: InvestmentBaseProperty? = null,
@SerializedName("trackerDividerProperty")
val trackerDividerProperty: InvestmentBaseProperty? = null,
@SerializedName("buttonProperty") val buttonProperty: InvestmentBaseProperty? = null,
@SerializedName("buttonTextProperty") val buttonTextProperty: InvestmentBaseProperty? = null,
@SerializedName("footerTextsProperty") val footerTextsProperty: InvestmentBaseProperty? = null,
)
data class FtueWithTrackerWidgetProperties(
@SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null,
@SerializedName("topLogoProperty") val topLogoProperty: InvestmentBaseProperty? = null,
@SerializedName("topTextProperty") val topTextProperty: InvestmentBaseProperty? = null,
@SerializedName("rolodexItemProperty") val rolodexItemProperty: InvestmentBaseProperty? = null,
@SerializedName("chipProperty") val chipProperty: InvestmentBaseProperty? = null,
@SerializedName("chipItemProperty") val chipItemProperty: InvestmentBaseProperty? = null,
)
data class FtueWithTrackerWidgetExtraData(
@SerializedName("extraProperties") val extraProperties: InvestmentBaseProperty? = null,
@SerializedName("metaData") val metaData: GenericAnalytics? = null,
)

View File

@@ -5,7 +5,7 @@
*
*/
package com.naviapp.home.dashboard.models.investmentTabWidgetData
package com.navi.amc.fundbuy.models.widgets
import PaymentCard
import com.google.gson.annotations.SerializedName
@@ -32,10 +32,21 @@ data class RepeatOrderWidgetHeader(
)
data class RepeatOrderWidgetContent(
@SerializedName("repeatOrderCardList") val repeatOrderCardList: List<PaymentCard>? = null
@SerializedName("repeatOrderCardList") val repeatOrderCardList: List<PaymentCard>? = null,
@SerializedName("carouselData") val carouselData: CarouselData? = null,
)
data class RepeatOrderWidgetExtraData(
@SerializedName("property") val property: InvestmentBaseProperty? = null,
@SerializedName("scrollType") val scrollType: String? = null,
@SerializedName("metaData") val metaData: GenericAnalytics? = null,
)
data class CarouselData(
@SerializedName("carouselHeight") val carouselHeight: Int? = null,
@SerializedName("activeCarouselWidth") val activeCarouselWidth: Int? = null,
@SerializedName("inactiveCarouselWidth") val inactiveCarouselWidth: Int? = null,
@SerializedName("activeCarouselColor") val activeCarouselColor: String? = null,
@SerializedName("inactiveCarouselColor") val inactiveCarouselColor: String? = null,
@SerializedName("carouselProperty") val carouselProperty: InvestmentBaseProperty? = null,
)

View File

@@ -0,0 +1,57 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.fundbuy.models.widgets
import com.google.gson.annotations.SerializedName
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.base.model.ActionData
import com.navi.common.model.InvestmentBaseProperty
import com.navi.naviwidgets.models.response.ImageFieldData
import com.navi.naviwidgets.models.response.TextFieldData
data class TitleContentRadioWidget(
@SerializedName("widgetName") override val widgetName: String? = null,
@SerializedName("widgetId") override val widgetId: String? = null,
@SerializedName("widgetData") val widgetData: TitleContentRadioWidgetData? = null,
) : GenericComposableWidget
data class TitleContentRadioWidgetData(
@SerializedName("title") val title: TextFieldData? = null,
@SerializedName("subTitle", alternate = ["subtitle"]) val subTitle: TextFieldData? = null,
@SerializedName("tagTitle") val tagTitle: TextFieldData? = null,
@SerializedName("showTitleRightIcon") val showTitleRightIcon: Boolean? = null,
@SerializedName("contentTitle") val contentTitle: TextFieldData? = null,
@SerializedName("contentLeftSubtitle") val contentLeftSubtitle: TextFieldData? = null,
@SerializedName("contentRightSubtitle") val contentRightSubtitle: TextFieldData? = null,
@SerializedName("contentMiddleImage") val contentMiddleImage: ImageFieldData? = null,
@SerializedName("properties") val properties: TitleContentRadioWidgetProperties? = null,
@SerializedName("actionData") val actionData: ActionData? = null,
@SerializedName("selectionProperties") val selectionProperties: SelectionProperties? = null,
)
data class TitleContentRadioWidgetProperties(
@SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null,
@SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null,
@SerializedName("subTitleProperty") val subTitleProperty: InvestmentBaseProperty? = null,
@SerializedName("tagProperty") val tagProperty: InvestmentBaseProperty? = null,
@SerializedName("contentTitleProperty")
val contentTitleProperty: InvestmentBaseProperty? = null,
@SerializedName("contentSubtitleProperty")
val contentSubtitleProperty: InvestmentBaseProperty? = null,
)
data class SelectionProperties(
@SerializedName("type") val type: String? = null,
@SerializedName("isin") val isin: String? = null,
@SerializedName("isSelected") val isSelected: Boolean? = null,
@SerializedName("selectedTitleRightIcon") val selectedTitleRightIcon: ImageFieldData? = null,
@SerializedName("unselectedTitleRightIcon")
val unselectedTitleRightIcon: ImageFieldData? = null,
@SerializedName("titleRightIconProperties")
val titleRightIconProperties: InvestmentBaseProperty? = null,
)

View File

@@ -31,7 +31,8 @@ data class TitleSubtitleImageWidgetData(
data class TitleSubtitleImageWidgetProperties(
@SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null,
@SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null,
@SerializedName("subTitleProperty") val subTitleProperty: InvestmentBaseProperty? = null,
@SerializedName("subTitleProperty", alternate = ["subtitleProperty"])
val subTitleProperty: InvestmentBaseProperty? = null,
@SerializedName("imageProperty") val imageProperty: InvestmentBaseProperty? = null,
@SerializedName("footerProperty") val footerProperty: InvestmentBaseProperty? = null,
)

View File

@@ -0,0 +1,38 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.amc.fundbuy.models.widgets
import com.google.gson.annotations.SerializedName
import com.navi.amc.common.model.GenericComposableWidget
import com.navi.base.model.ActionData
import com.navi.common.model.InvestmentBaseProperty
import com.navi.naviwidgets.models.LottieFieldData
import com.navi.naviwidgets.models.response.TextFieldData
data class TitleSubtitleLottieWidget(
@SerializedName("widgetName") override val widgetName: String? = null,
@SerializedName("widgetId") override val widgetId: String? = null,
@SerializedName("widgetData") val widgetData: TitleSubtitleLottieWidgetData? = null,
) : GenericComposableWidget
data class TitleSubtitleLottieWidgetData(
@SerializedName("title") val title: TextFieldData? = null,
@SerializedName("subTitle", alternate = ["subtitle"]) val subTitle: TextFieldData? = null,
@SerializedName("lottie", alternate = ["icon"]) val lottie: LottieFieldData? = null,
@SerializedName("properties") val properties: TitleSubtitleLottieWidgetProperties? = null,
@SerializedName("actionData") val actionData: ActionData? = null,
)
data class TitleSubtitleLottieWidgetProperties(
@SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null,
@SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null,
@SerializedName("subTitleProperty", alternate = ["subtitleProperty"])
val subTitleProperty: InvestmentBaseProperty? = null,
@SerializedName("lottieProperty") val lottieProperty: InvestmentBaseProperty? = null,
@SerializedName("footerProperty") val footerProperty: InvestmentBaseProperty? = null,
)

View File

@@ -12,9 +12,11 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.navi.amc.common.activity.CheckerActivity
import com.navi.amc.compose.entry.AmcComposeActivity
import com.navi.amc.fundbuy.activities.FundBuyActivity
import com.navi.amc.kyc.activity.KycActivity
import com.navi.amc.kyc.activity.KycOnboardActivity
import com.navi.amc.utils.AmcAnalytics.TRUE
import com.navi.amc.utils.getCtaToNavigateToInvestmentTab
import com.navi.amc.utils.updateSecondIdentifier
import com.navi.base.deeplink.DeepLinkManager
@@ -22,7 +24,6 @@ import com.navi.base.model.ActionData
import com.navi.base.utils.orFalse
import com.navi.base.utils.orZero
import com.navi.common.utils.Constants
import com.navi.common.utils.Constants.TRUE
import com.navi.common.utils.toCtaData
import timber.log.Timber
@@ -39,6 +40,7 @@ object NaviAmcDeeplinkNavigator {
const val WEB_URL = "webUrl"
const val FUND_LANDING = "fund_landing"
const val V2_PARAMETER = "v2"
const val FTUE = "ftue"
fun navigate(
activity: Activity?,
@@ -113,6 +115,9 @@ object NaviAmcDeeplinkNavigator {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
FTUE -> {
Intent(activity, AmcComposeActivity::class.java)
}
WEB_URL -> {
var url: String? = null
ctaData.parameters?.forEach { keyValue ->

View File

@@ -9,6 +9,8 @@ package com.navi.amc.network.retrofit
import com.navi.amc.common.model.*
import com.navi.amc.common.model.cart.CartRequest
import com.navi.amc.compose.common.model.AmcGenericScreenResponse
import com.navi.amc.compose.feature.ftue.model.FundSelectionData
import com.navi.amc.digio.AadhaarVerificationData
import com.navi.amc.digio.DigioKycPollingResponse
import com.navi.amc.digio.DigioKycResponse
@@ -599,4 +601,15 @@ interface RetrofitService {
@Path("checkoutId") checkoutId: String?,
@Path(value = "nextStepCta", encoded = true) nextStepCta: String?,
): Response<GenericResponse<T>>
@GET("/investor/ftue/educate")
suspend fun fetchFtueEducateScreenData(): Response<GenericResponse<AmcGenericScreenResponse>>
@GET("/investor/ftue/fund-details")
suspend fun fetchFtueFundSelectScreenData(): Response<GenericResponse<AmcGenericScreenResponse>>
@POST("/investor/ftue/update")
suspend fun postSelectedFund(
@Body fundSelectionData: FundSelectionData
): Response<GenericResponse<SuccessResponse>>
}

View File

@@ -240,4 +240,10 @@ object Constant {
const val ERROR_RESPONSE = "ERROR_RESPONSE"
const val NEW_FLOW_TYPE = "new_flow_type"
const val SIP_TYPE = "sip_type"
const val UPCOMING_SIP_PAYMENT_CARD = "upcomingSipPaymentCard"
const val REPEAT_ORDER = "repeatOrder"
const val START_SCREEN_NAME = "start_screen_name"
const val TRANSITION_DURATION_IN_MILLIS = 400
}