diff --git a/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/HopperHelper.kt b/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/HopperHelper.kt index 95791a635e..e060f7f1eb 100644 --- a/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/HopperHelper.kt +++ b/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/HopperHelper.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope import com.navi.amc.common.viewmodel.CheckerVM +import com.navi.amc.compose.feature.goalBasedSip.viewmodel.GoalBasedSipSetupVM import com.navi.amc.fundbuy.models.AutoPaySetupRequestData import com.navi.amc.fundbuy.viewmodel.FundBuyFlowViewModel import com.navi.amc.fundbuy.viewmodel.FundListViewModel @@ -30,12 +31,17 @@ import com.navi.amc.utils.AmcAnalytics.ORDER_TYPE import com.navi.amc.utils.Constant.ACTION_PERFORMED import com.navi.amc.utils.Constant.AMOUNT import com.navi.amc.utils.Constant.BANK_DETAILS_REF_ID +import com.navi.amc.utils.Constant.GOAL_NAME +import com.navi.amc.utils.Constant.GOAL_REFERENCE_ID import com.navi.amc.utils.Constant.MANDATE_OPTED_IN import com.navi.amc.utils.Constant.PAYMENT_MODE import com.navi.amc.utils.Constant.SETUP_AUTOPAY_EXISTING_SIP import com.navi.amc.utils.Constant.SIP_REFERENCE_ID import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.model.CtaData +import com.navi.common.utils.Constants.AMC_GOAL_BASED_SIP_AMOUNT_SCREEN +import com.navi.common.utils.Constants.AMC_GOAL_BASED_SIP_SUMMARY_SCREEN +import com.navi.common.utils.Constants.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN import com.navi.common.utils.Constants.CTAData import com.navi.common.utils.Constants.HOPPER_API_RESPONSE_NOT_RECEIVED import com.navi.common.utils.Constants.HOPPER_API_TIMEOUT @@ -52,6 +58,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -180,6 +187,62 @@ class HopperHelper { ) } } + is GoalBasedSipSetupVM -> { + when (ctaData.url) { + AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN -> { + viewModel.fetchTargetSetupScreenDataFromRemote( + goalName = + ctaData.parameters + ?.firstOrNull { it.key == GOAL_NAME } + ?.value + .orEmpty(), + goalReferenceId = + ctaData.parameters + ?.firstOrNull { it.key == GOAL_REFERENCE_ID } + ?.value + .orEmpty(), + ) + observeAndHandleResponse( + activity, + ctaData, + onResult = onResult, + stateFlow = viewModel.goalBasedSipScreenState, + ) + } + AMC_GOAL_BASED_SIP_AMOUNT_SCREEN -> { + viewModel.fetchSipAmountScreenDataFromRemote( + screenName = AMC_GOAL_BASED_SIP_AMOUNT_SCREEN, + goalReferenceId = + ctaData.parameters + ?.firstOrNull { it.key == GOAL_REFERENCE_ID } + ?.value + .orEmpty(), + ) + observeAndHandleResponse( + activity, + ctaData, + viewModel.sipAmountScreenData, + onResult, + ) + } + AMC_GOAL_BASED_SIP_SUMMARY_SCREEN -> { + viewModel.fetchGoalSummaryScreenDataFromRemote( + screenName = AMC_GOAL_BASED_SIP_SUMMARY_SCREEN, + goalReferenceId = + ctaData.parameters + ?.firstOrNull { it.key == GOAL_REFERENCE_ID } + ?.value + .orEmpty(), + ) + observeAndHandleResponse( + activity, + ctaData, + viewModel.goalSummaryScreenData, + onResult, + ) + } + } + } else -> { onResult(false) } @@ -191,7 +254,10 @@ class HopperHelper { CoroutineScope(Dispatchers.Main).launch { when (viewModel) { is InvestmentsVm -> { - viewModel.getDynamicCTA(ctaData.action ?: KYC_JOURNEY) + viewModel.getDynamicCTA( + ctaData.action ?: KYC_JOURNEY, + ctaData.parameters ?: emptyList(), + ) observeAndHandleDynamicCtaResponse(viewModel.dynamicCta, onResult) } } @@ -213,9 +279,10 @@ class HopperHelper { private fun observeAndHandleResponse( activity: ComponentActivity, ctaData: CtaData, - liveData: LiveData<*>, + liveData: LiveData<*>? = null, onResult: (Boolean) -> Unit, timeoutMillis: Long = HOPPER_API_TIMEOUT, + stateFlow: StateFlow<*>? = null, ) { var responseReceived = false val job = @@ -225,8 +292,13 @@ class HopperHelper { NaviTrackEvent.trackEvent(eventName = HOPPER_API_RESPONSE_NOT_RECEIVED) onResult(false) } + stateFlow?.collectLatest { response -> + responseReceived = true + cancel() + handleResponse(ctaData, response, onResult) + } } - liveData.observeNullable(activity) { response -> + liveData?.observeNullable(activity) { response -> responseReceived = true liveData.removeObservers(activity) job.cancel() diff --git a/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/ViewModelMapper.kt b/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/ViewModelMapper.kt index 023c7782e2..04065103de 100644 --- a/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/ViewModelMapper.kt +++ b/android/app/src/main/java/com/naviapp/home/common/hopperProcessor/processHandlerImpl/ViewModelMapper.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2024 by Navi Technologies Limited + * * Copyright © 2024-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.navi.amc.common.viewmodel.CheckerVM import com.navi.amc.common.viewmodel.OTPVM +import com.navi.amc.compose.feature.goalBasedSip.viewmodel.GoalBasedSipSetupVM import com.navi.amc.fundbuy.viewmodel.FundBuyFlowViewModel import com.navi.amc.fundbuy.viewmodel.FundListViewModel import com.navi.amc.kyc.viewmodel.BankDetailsVM @@ -29,6 +30,9 @@ import com.navi.amc.portfolio.viewmodels.SipModificationVM import com.navi.base.model.CtaData import com.navi.common.utils.Constants.AMC_FUND_AUTOPAY_SETUP_V3 import com.navi.common.utils.Constants.AMC_FUND_OTP +import com.navi.common.utils.Constants.AMC_GOAL_BASED_SIP_AMOUNT_SCREEN +import com.navi.common.utils.Constants.AMC_GOAL_BASED_SIP_SUMMARY_SCREEN +import com.navi.common.utils.Constants.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN import com.navi.common.utils.Constants.AMC_HPC_NAME_REDIRECT_PAGE_URL import com.navi.common.utils.Constants.AMC_HPC_PAN_REDIRECT_PAGE_URL import com.navi.common.utils.Constants.AMC_KYC_BANK_DETAILS_URL @@ -72,6 +76,10 @@ class ViewModelMapper { AMC_FUND_OTP -> ViewModelProvider(activity)[OTPVM::class.java] AMC_FUND_AUTOPAY_SETUP_V3 -> ViewModelProvider(activity)[FundBuyFlowViewModel::class.java] + AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN, + AMC_GOAL_BASED_SIP_AMOUNT_SCREEN, + AMC_GOAL_BASED_SIP_SUMMARY_SCREEN -> + ViewModelProvider(activity)[GoalBasedSipSetupVM::class.java] else -> null } } diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/models/investmentTabWidgetData/TopInvestingFundsWidget.kt b/android/app/src/main/java/com/naviapp/home/dashboard/models/investmentTabWidgetData/TopInvestingFundsWidget.kt index 96bfa0284a..66001c8bab 100644 --- a/android/app/src/main/java/com/naviapp/home/dashboard/models/investmentTabWidgetData/TopInvestingFundsWidget.kt +++ b/android/app/src/main/java/com/naviapp/home/dashboard/models/investmentTabWidgetData/TopInvestingFundsWidget.kt @@ -10,6 +10,7 @@ package com.naviapp.home.dashboard.models.investmentTabWidgetData import com.google.gson.annotations.SerializedName import com.navi.amc.common.model.GenericComposableWidget import com.navi.amc.fundbuy.models.widgets.FundCardData +import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetCard import com.navi.base.model.GenericAnalytics import com.navi.common.model.InvestmentBaseProperty import com.navi.naviwidgets.models.response.Footer @@ -35,7 +36,8 @@ data class TopInvestingFundsExtraData( ) data class TopInvestingFundsContentData( - @SerializedName("fundCards") val fundCards: List? = null + @SerializedName("goalCards") val goalCards: List? = null, + @SerializedName("fundCards") val fundCards: List? = null, ) data class HeaderData( diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/repo/InvestmentsTabV2Repository.kt b/android/app/src/main/java/com/naviapp/home/dashboard/repo/InvestmentsTabV2Repository.kt index 2ccf7c4d6c..a09246d025 100644 --- a/android/app/src/main/java/com/naviapp/home/dashboard/repo/InvestmentsTabV2Repository.kt +++ b/android/app/src/main/java/com/naviapp/home/dashboard/repo/InvestmentsTabV2Repository.kt @@ -9,6 +9,7 @@ package com.naviapp.home.dashboard.repo import com.navi.base.model.ActionData import com.navi.base.model.JourneyType +import com.navi.base.model.LineItem import com.navi.common.checkmate.model.MetricInfo import com.navi.common.model.ModuleNameV2 import com.navi.common.network.models.RepoResult @@ -42,12 +43,13 @@ constructor(@SuperAppRetroFit private val superAppRetrofitService: RetrofitServi suspend fun getDynamicCTA( context: String?, + parameters: List? = null, metricInfo: MetricInfo>, ): RepoResult { return apiResponseCallback( superAppRetrofitService.getDynamicCta( target = ModuleNameV2.AMC.name, - journeyType = JourneyType(context = context), + journeyType = JourneyType(context = context, parameters = parameters), ), metricInfo = metricInfo, ) diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/InvestmentGenericComposableWidgetFactory.kt b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/InvestmentGenericComposableWidgetFactory.kt index 01a947a668..6ebe9d58ac 100644 --- a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/InvestmentGenericComposableWidgetFactory.kt +++ b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/InvestmentGenericComposableWidgetFactory.kt @@ -91,6 +91,7 @@ fun InvestmentGenericComposableWidgetFactory( onClick = onClick, investmentsTabVm = investmentsTabVm, onVisible = onVisible, + onHopperStart = onHopperStart, ) } } diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/VisibilityTracker.kt b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/VisibilityTracker.kt index 5e7f8ac769..7017b5abc3 100644 --- a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/VisibilityTracker.kt +++ b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/VisibilityTracker.kt @@ -10,11 +10,13 @@ package com.naviapp.home.dashboard.ui.compose.investmentTab import UpcomingSipPaymentWidget import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import com.navi.amc.fundbuy.models.cards.InvestmentGoalCardData import com.navi.amc.fundbuy.models.cards.PaymentCard import com.navi.amc.fundbuy.models.widgets.FtueWithTrackerWidget 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.SetupMonthlyTargetCard import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetWidget import com.navi.base.model.GenericAnalytics import com.naviapp.home.dashboard.models.investmentTabWidgetData.ActionCardWidget @@ -73,6 +75,8 @@ fun extractMetaData(widgetData: T): GenericAnalytics? { is CutOffTimerWidget -> widgetData.widgetData?.extraData?.metaData is BuyTheDipWidget -> widgetData.widgetData?.extraData?.metaData is FtueWithTrackerWidget -> widgetData.widgetData?.extraData?.metaData + is SetupMonthlyTargetCard -> widgetData.actionData?.metaData + is InvestmentGoalCardData -> widgetData.actionData?.metaData else -> null } } diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/genericComposables/CardListComposable.kt b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/genericComposables/CardListComposable.kt index 012a5526d6..f9d7467e39 100644 --- a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/genericComposables/CardListComposable.kt +++ b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/genericComposables/CardListComposable.kt @@ -23,9 +23,12 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.navi.amc.fundbuy.composables.cards.MonthlyInvestmentGoalCardComposable +import com.navi.amc.fundbuy.composables.cards.SetupMonthlyTargetCardComposable import com.navi.amc.fundbuy.models.cards.InvestmentGoalCardData import com.navi.amc.fundbuy.models.cards.PaymentCard import com.navi.amc.fundbuy.models.widgets.FundCardData +import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetCard +import com.navi.amc.utils.Constant.GOAL_BASED_SIP_SETUP_CARD import com.navi.base.model.ActionData import com.navi.base.model.CtaData import com.navi.base.model.GenericAnalytics @@ -47,6 +50,7 @@ import com.naviapp.utils.Constants.ORDER_IN_PROGRESS_CARD import com.naviapp.utils.Constants.PAGER_SCROLL import com.naviapp.utils.Constants.RANK_OF_CARD import com.naviapp.utils.Constants.REPEAT_ORDER +import com.naviapp.utils.Constants.SETUP_MONTHLY_INVESTMENT_GOAL_CARD import com.naviapp.utils.Constants.SIP_AUTOPAY_NUDGE_CARD import com.naviapp.utils.Constants.SIP_AUTOPAY_NUDGE_CARD_V2 import com.naviapp.utils.Constants.UPCOMING_SIP_PAYMENT_CARD @@ -254,5 +258,26 @@ fun RenderCardBasedOnType( ) } } + + SETUP_MONTHLY_INVESTMENT_GOAL_CARD -> { + VisibilityTracker(widgetData = data, onVisible = onVisible) { + SetupMonthlyTargetCardComposable( + cardData = data as SetupMonthlyTargetCard, + onClick = onClick, + cardWidth = cardWidth, + ) + } + } + + GOAL_BASED_SIP_SETUP_CARD -> { + VisibilityTracker(widgetData = data, onVisible = onVisible) { + GoalBasedSipSetupCardComposable( + cardData = data as SetupMonthlyTargetCard, + onClick = onClick, + cardWidth = cardWidth, + onHopperStart = onHopperStart, + ) + } + } } } diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/genericComposables/GoalBasedSipSetupCardComposable.kt b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/genericComposables/GoalBasedSipSetupCardComposable.kt new file mode 100644 index 0000000000..3cc8ed7097 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/genericComposables/GoalBasedSipSetupCardComposable.kt @@ -0,0 +1,235 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.dashboard.ui.compose.investmentTab.genericComposables + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material.Card +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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 androidx.compose.ui.unit.dp +import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetCard +import com.navi.amc.utils.Constant.DEFAULT_COLUMN_WEIGHT +import com.navi.base.model.ActionData +import com.navi.base.model.CtaData +import com.navi.common.utils.Constants.HOPPER +import com.navi.common.utils.toCtaData +import com.navi.design.utils.clickableWithNoGesture +import com.navi.naviwidgets.composewidget.reusable.ButtonComposable +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.hexToColor +import com.navi.naviwidgets.models.FooterButtonData +import com.navi.naviwidgets.models.FooterButtonState +import com.navi.uitron.utils.ShapeUtil +import com.navi.uitron.utils.getBorderStrokeBrushData +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.setPadding +import com.naviapp.R +import com.naviapp.home.dashboard.ui.compose.investmentTab.InvestmentsScreenHelper + +@Composable +fun GoalBasedSipSetupCardComposable( + cardData: SetupMonthlyTargetCard? = null, + onClick: (actionData: ActionData?) -> Unit = {}, + cardWidth: Dp, + onHopperStart: (ctaData: CtaData, buttonState: MutableState) -> Unit = + { _, _ -> + }, +) { + val buttonState = remember { mutableStateOf(FooterButtonState.ENABLED) } + + cardData?.let { it -> + Card( + shape = ShapeUtil.run { getShape(shape = it.properties?.cardProperty?.shape) }, + elevation = it.properties?.cardProperty?.elevation?.dp ?: 0.dp, + backgroundColor = + it.properties?.cardProperty?.backgroundColor?.hexToComposeColor ?: Color.White, + border = + BorderStroke( + width = ((it.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp, + brush = getBorderStrokeBrushData(it.properties?.cardProperty?.borderStrokeData), + ), + modifier = + Modifier.width(cardWidth) + .shadow( + elevation = (it.properties?.cardProperty?.elevation ?: 0).dp, + ambientColor = hexToColor(it.properties?.cardProperty?.ambientColor), + spotColor = hexToColor(it.properties?.cardProperty?.spotColor), + shape = ShapeUtil.getShape(shape = it.properties?.cardProperty?.shape), + ) + .clickableWithNoGesture { + it.actionData?.let { actionData -> + if (actionData.url == HOPPER) { + InvestmentsScreenHelper() + .setActionStatus( + actionData = actionData, + buttonState = buttonState, + onClick = onClick, + onHopperStart = onHopperStart, + ) + } else { + onClick(actionData) + } + } + }, + ) { + Box( + modifier = + Modifier.padding(end = it.properties?.cardProperty?.padding?.end?.dp ?: 0.dp), + contentAlignment = Alignment.TopEnd, + ) { + NaviTextWidgetized( + textFieldData = it.tagData, + modifier = + Modifier.background( + color = + it.properties?.tagProperty?.backgroundColor?.hexToComposeColor + ?: Color.Transparent, + shape = ShapeUtil.getShape(it.properties?.tagProperty?.shape), + ) + .border( + BorderStroke( + width = + it.properties?.tagProperty?.borderStrokeData?.width?.dp + ?: 0.dp, + brush = + getBorderStrokeBrushData( + it.properties?.tagProperty?.borderStrokeData + ), + ), + shape = ShapeUtil.getShape(it.properties?.tagProperty?.shape), + ) + .setPadding(it.properties?.tagProperty?.padding), + ) + } + Column(modifier = Modifier.fillMaxWidth()) { + Column(Modifier.fillMaxWidth().setPadding(it.properties?.cardProperty?.padding)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column( + modifier = + Modifier.weight( + it.properties?.cardProperty?.columnWeight + ?: DEFAULT_COLUMN_WEIGHT + ) + ) { + NaviTextWidgetized( + textFieldData = it.title, + modifier = + Modifier.setPadding(it.properties?.titleProperty?.padding), + ) + NaviTextWidgetized( + textFieldData = it.subtitle, + modifier = + Modifier.setPadding(it.properties?.subtitleProperty?.padding), + ) + + Spacer(modifier = Modifier.weight(1f)) + + ButtonComposable( + data = + FooterButtonData( + title = it.buttonText, + backgroundColor = + if ( + buttonState.value.name == + FooterButtonState.LOADING.name + ) + "#8F8095" + else "#1F002A", + cta = it.actionData?.toCtaData(), + ), + modifier = Modifier.wrapContentWidth().wrapContentHeight(), + textModifier = Modifier.wrapContentWidth().wrapContentHeight(), + contentPadding = + if (buttonState.value.name == FooterButtonState.LOADING.name) { + PaddingValues( + start = 16.dp, + end = 16.dp, + top = 12.dp, + bottom = 12.dp, + ) + } else { + PaddingValues( + start = + ((it.properties?.buttonProperty?.padding?.start) + ?: R.integer.value_16) + .dp, + end = + ((it.properties?.buttonProperty?.padding?.end) + ?: R.integer.value_16) + .dp, + top = + ((it.properties?.buttonProperty?.padding?.top) + ?: R.integer.value_12) + .dp, + bottom = + ((it.properties?.buttonProperty?.padding?.bottom) + ?: R.integer.value_12) + .dp, + ) + }, + state = buttonState.value.name, + onClick = { ctaData -> + it.actionData?.let { + InvestmentsScreenHelper() + .setActionStatus( + actionData = it, + buttonState = buttonState, + onClick = onClick, + onHopperStart = onHopperStart, + ) + } + }, + ) + } + + Column( + modifier = + Modifier.weight( + 1 - + (it.properties?.cardProperty?.columnWeight + ?: DEFAULT_COLUMN_WEIGHT) + ) + .align(Alignment.Bottom), + horizontalAlignment = Alignment.End, + ) { + NaviImage( + imageFieldData = it.icon, + modifier = + Modifier.setPadding(it.properties?.cardIconProperty?.padding), + ) + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/widgets/TopInvestingFundsWidgetComposable.kt b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/widgets/TopInvestingFundsWidgetComposable.kt index 5ae7e23913..33b353defa 100644 --- a/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/widgets/TopInvestingFundsWidgetComposable.kt +++ b/android/app/src/main/java/com/naviapp/home/dashboard/ui/compose/investmentTab/widgets/TopInvestingFundsWidgetComposable.kt @@ -15,17 +15,21 @@ 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.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.unit.dp +import com.navi.amc.utils.Constant.GOAL_BASED_SIP_SETUP_CARD import com.navi.base.model.ActionData +import com.navi.base.model.CtaData import com.navi.base.model.GenericAnalytics import com.navi.design.theme.FFF6D5 import com.navi.design.theme.FFF9E0 import com.navi.naviwidgets.extensions.NaviImage import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.models.FooterButtonState import com.navi.rr.utils.composeutils.brushType import com.navi.uitron.utils.setPadding import com.naviapp.R @@ -43,6 +47,9 @@ fun TopInvestingFundsWidgetComposable( investmentsTabVm: InvestmentsVm, onClick: (actionData: ActionData?) -> Unit, onVisible: (genericAnalytics: GenericAnalytics?) -> Unit, + onHopperStart: (ctaData: CtaData, buttonState: MutableState) -> Unit = + { _, _ -> + }, ) { widget?.widgetData?.let { widgetData -> Column( @@ -64,7 +71,7 @@ fun TopInvestingFundsWidgetComposable( widgetData.header.let { headerData -> Row( modifier = Modifier.fillMaxWidth().setPadding(headerData?.property?.padding), - verticalAlignment = Alignment.Top, + verticalAlignment = Alignment.CenterVertically, ) { Column( modifier = @@ -118,8 +125,7 @@ fun TopInvestingFundsWidgetComposable( Spacer( modifier = Modifier.height( - widgetData.topInvestingFundsContent.fundCards - .get(0) + widgetData.topInvestingFundsContent.fundCards[0] .property ?.margin ?.top @@ -142,6 +148,34 @@ fun TopInvestingFundsWidgetComposable( investmentsTabVm = investmentsTabVm, ) } + + if (widgetData.topInvestingFundsContent?.goalCards?.isNotEmpty() == true) { + Spacer( + modifier = + Modifier.height( + widgetData.topInvestingFundsContent.goalCards[0] + .properties + ?.cardProperty + ?.margin + ?.top + ?.dp ?: R.integer.value_24.dp + ) + ) + } + + widgetData.topInvestingFundsContent?.goalCards?.let { list -> + CardListComposable( + cardType = GOAL_BASED_SIP_SETUP_CARD, + cardWidthFactor = + (widgetData.extraData?.extraProperties?.cardWidthFactor + ?: DEFAULT_CARD_WIDTH_FACTOR), + cardList = list, + onClick = onClick, + onVisible = onVisible, + investmentsTabVm = investmentsTabVm, + onHopperStart = onHopperStart, + ) + } } } } diff --git a/android/app/src/main/java/com/naviapp/home/dashboard/viewmodels/InvestmentsVm.kt b/android/app/src/main/java/com/naviapp/home/dashboard/viewmodels/InvestmentsVm.kt index dad805b32b..312c71997b 100644 --- a/android/app/src/main/java/com/naviapp/home/dashboard/viewmodels/InvestmentsVm.kt +++ b/android/app/src/main/java/com/naviapp/home/dashboard/viewmodels/InvestmentsVm.kt @@ -27,6 +27,7 @@ import com.navi.base.cache.repository.NaviCacheRepositoryImpl import com.navi.base.cache.util.NaviSharedDbKeys import com.navi.base.model.ActionData import com.navi.base.model.CtaData +import com.navi.base.model.LineItem import com.navi.base.sharedpref.PreferenceManager import com.navi.base.utils.isNotNull import com.navi.base.utils.orFalse @@ -390,14 +391,15 @@ constructor( return bottomSheetDataFromType[bottomSheetType] } - fun getDynamicCTA(context: String?) { + fun getDynamicCTA(context: String?, parameters: List) { viewModelScope.safeLaunch(Dispatchers.IO) { val metricInfo = MetricInfo.AMCMetric( screen = INVESTMENT_TAB_SCREEN_V3, isNae = { !(it.errors.isNullOrEmpty() && it.error == null) }, ) - val response = investmentsTabRepository.getDynamicCTA(context, metricInfo = metricInfo) + val response = + investmentsTabRepository.getDynamicCTA(context, parameters, metricInfo = metricInfo) if (response.error == null && response.errors.isNullOrEmpty()) { response.data?.let { _dynamicCta.emit(it.toCtaData()) } } else { diff --git a/android/app/src/main/java/com/naviapp/utils/Constants.kt b/android/app/src/main/java/com/naviapp/utils/Constants.kt index 83c1b92406..4599796e5d 100644 --- a/android/app/src/main/java/com/naviapp/utils/Constants.kt +++ b/android/app/src/main/java/com/naviapp/utils/Constants.kt @@ -196,6 +196,7 @@ object Constants { const val SIP_AUTOPAY_NUDGE_CARD = "sipAutopayNudgeCard" const val SIP_AUTOPAY_NUDGE_CARD_V2 = "sipAutopayNudgeCardV2" const val MONTHLY_INVESTMENT_GOAL_CARD = "monthlyInvestmentGoalCard" + const val SETUP_MONTHLY_INVESTMENT_GOAL_CARD = "setupMonthlyInvestmentGoalCard" const val DEFAULT_CARD_WIDTH_FACTOR = 0.92f const val FUND_CARD = "fundCard" const val FUND_BOX_CARD = "fundBoxCard" diff --git a/android/navi-amc/src/main/AndroidManifest.xml b/android/navi-amc/src/main/AndroidManifest.xml index 7a521fbc80..5582b06df2 100644 --- a/android/navi-amc/src/main/AndroidManifest.xml +++ b/android/navi-amc/src/main/AndroidManifest.xml @@ -43,7 +43,7 @@ android:screenOrientation="portrait" android:theme="@style/BaseThemeStyle" android:launchMode="singleTop" - android:windowSoftInputMode="adjustNothing" /> + android:windowSoftInputMode="adjustResize" /> { sendEvent( screenName = NaviTrackEvent.foregroundScreen.orEmpty(), - eventName = AMC_INVALID_CHECKOUT_STEP_WHILE_PARSING_INPUT, - extraAttributes = hashMapOf(CHECKOUT_STEP to CartState.checkoutStep), + eventName = AmcAnalytics.AMC_INVALID_CHECKOUT_STEP_WHILE_PARSING_INPUT, + extraAttributes = hashMapOf(Constant.CHECKOUT_STEP to CartState.checkoutStep), ) return CheckoutRequest(cartId = CartState.cartId, type = CartState.checkoutStep) } @@ -99,16 +91,16 @@ class CartHelper @Inject constructor(private var naviCacheRepository: NaviCacheR } sendEvent( - eventName = AMC_CART_RESPONSE_SUCCESS, + eventName = AmcAnalytics.AMC_CART_RESPONSE_SUCCESS, screenName = NaviTrackEvent.foregroundScreen.orEmpty(), extraAttributes = hashMapOf( - CURRENT_STEP to CartState.currentStep.orEmpty(), - CHECKOUT_STEP to CartState.checkoutStep, - CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), - CHECKOUT_ID to CartState.checkoutId.orEmpty(), - CART_ID to CartState.cartId.orEmpty(), - CART_RESPONSE to checkoutData.toString(), + Constant.CURRENT_STEP to CartState.currentStep.orEmpty(), + Constant.CHECKOUT_STEP to CartState.checkoutStep, + Constant.CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), + Constant.CHECKOUT_ID to CartState.checkoutId.orEmpty(), + Constant.CART_ID to CartState.cartId.orEmpty(), + Constant.CART_RESPONSE to checkoutData.toString(), ), ) val formattedResponseData: Any? = @@ -133,6 +125,12 @@ class CartHelper @Inject constructor(private var naviCacheRepository: NaviCacheR else -> return checkoutData } + + if (CartState.currentStep == CheckoutSteps.OTP_GENERATION.name) { + checkoutData?.stepResponse?.otpGenerateResponse?.redirectionCta = + checkoutData.redirectionCta + } + setCartIdAndCheckoutStep(checkoutData) coroutineScope.launch(Dispatchers.IO) { checkoutData?.let { saveResponse(it) } } val processedResponse = @@ -144,15 +142,15 @@ class CartHelper @Inject constructor(private var naviCacheRepository: NaviCacheR return processedResponse } else { sendEvent( - eventName = AMC_CART_RESPONSE_ERROR, + eventName = AmcAnalytics.AMC_CART_RESPONSE_ERROR, screenName = NaviTrackEvent.foregroundScreen.orEmpty(), extraAttributes = hashMapOf( - CURRENT_STEP to CartState.currentStep.orEmpty(), - CHECKOUT_STEP to CartState.checkoutStep, - CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), - CHECKOUT_ID to CartState.checkoutId.orEmpty(), - CART_ID to CartState.cartId.orEmpty(), + Constant.CURRENT_STEP to CartState.currentStep.orEmpty(), + Constant.CHECKOUT_STEP to CartState.checkoutStep, + Constant.CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), + Constant.CHECKOUT_ID to CartState.checkoutId.orEmpty(), + Constant.CART_ID to CartState.cartId.orEmpty(), ), ) } @@ -188,8 +186,8 @@ class CartHelper @Inject constructor(private var naviCacheRepository: NaviCacheR else -> { sendEvent( screenName = NaviTrackEvent.foregroundScreen.orEmpty(), - eventName = AMC_INVALID_CHECKOUT_STEP, - extraAttributes = hashMapOf(CHECKOUT_STEP to step.orEmpty()), + eventName = AmcAnalytics.AMC_INVALID_CHECKOUT_STEP, + extraAttributes = hashMapOf(Constant.CHECKOUT_STEP to step.orEmpty()), ) } } @@ -198,17 +196,23 @@ class CartHelper @Inject constructor(private var naviCacheRepository: NaviCacheR private suspend fun saveResponse(response: CheckoutResponse) { val entitiesToSave = listOfNotNull( - NaviCacheEntity(key = CART_RESPONSE, value = gson.toJson(response), version = 1), + NaviCacheEntity( + key = Constant.CART_RESPONSE, + value = GsonProvider.gson.toJson(response), + version = 1, + ), CartState.checkoutStep?.let { - NaviCacheEntity(key = CHECKOUT_STEP, value = it, version = 1) + NaviCacheEntity(key = Constant.CHECKOUT_STEP, value = it, version = 1) }, CartState.checkoutId?.let { - NaviCacheEntity(key = CHECKOUT_ID, value = it, version = 1) + NaviCacheEntity(key = Constant.CHECKOUT_ID, value = it, version = 1) }, CartState.checkoutStepCta?.let { - NaviCacheEntity(key = CHECKOUT_STEP_CTA, value = it, version = 1) + NaviCacheEntity(key = Constant.CHECKOUT_STEP_CTA, value = it, version = 1) + }, + CartState.cartId?.let { + NaviCacheEntity(key = Constant.CART_ID, value = it, version = 1) }, - CartState.cartId?.let { NaviCacheEntity(key = CART_ID, value = it, version = 1) }, ) entitiesToSave.forEach { naviCacheRepository.save(it) } diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/CartUseCase.kt b/android/navi-amc/src/main/java/com/navi/amc/common/cart/CartUseCase.kt similarity index 69% rename from android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/CartUseCase.kt rename to android/navi-amc/src/main/java/com/navi/amc/common/cart/CartUseCase.kt index 27a25e9757..c38f001049 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/CartUseCase.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/cart/CartUseCase.kt @@ -5,26 +5,21 @@ * */ -package com.navi.amc.common.viewmodel +package com.navi.amc.common.cart +import com.navi.amc.common.model.CheckoutRequest import com.navi.amc.common.model.CheckoutResponse import com.navi.amc.common.model.cart.CartRequest import com.navi.amc.common.model.cart.CartState import com.navi.amc.common.model.cart.CheckoutSteps import com.navi.amc.common.repo.CartRepository import com.navi.amc.utils.AmcAnalytics -import com.navi.amc.utils.AmcAnalytics.AMC_CART_POLLING -import com.navi.amc.utils.AmcAnalytics.AMC_CART_SERVICE_CALLED -import com.navi.amc.utils.Constant.CART_ID -import com.navi.amc.utils.Constant.CART_REQUEST -import com.navi.amc.utils.Constant.CHECKOUT_ID -import com.navi.amc.utils.Constant.CHECKOUT_STEP -import com.navi.amc.utils.Constant.CHECKOUT_STEP_CTA +import com.navi.amc.utils.Constant import com.navi.amc.utils.getAmcMetricInfo import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.cache.repository.NaviCacheRepository import com.navi.common.network.models.RepoResult -import com.navi.common.resourcemanager.manager.ResourceManager.exceptionHandler +import com.navi.common.resourcemanager.manager.ResourceManager import com.navi.common.utils.log import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -39,22 +34,24 @@ constructor( private val cartHelper: CartHelper, ) { - private val coroutineScope = CoroutineScope(Dispatchers.IO + exceptionHandler) + private val coroutineScope = CoroutineScope(Dispatchers.IO + ResourceManager.exceptionHandler) init { - setCheckoutIdAndStep() + if (CartState.cartId == null) { + setCheckoutIdAndStep() + } } suspend fun doPolling(): RepoResult { sendEvent( - eventName = AMC_CART_POLLING, + eventName = AmcAnalytics.AMC_CART_POLLING, screenName = NaviTrackEvent.foregroundScreen.orEmpty(), extraAttributes = hashMapOf( - CHECKOUT_ID to CartState.checkoutId.orEmpty(), - CHECKOUT_STEP to CartState.checkoutStep, - CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), - CART_ID to CartState.cartId.orEmpty(), + Constant.CHECKOUT_ID to CartState.checkoutId.orEmpty(), + Constant.CHECKOUT_STEP to CartState.checkoutStep, + Constant.CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), + Constant.CART_ID to CartState.cartId.orEmpty(), ), ) val response = @@ -81,14 +78,14 @@ constructor( sendEvent( screenName = NaviTrackEvent.foregroundScreen.orEmpty(), - eventName = AMC_CART_SERVICE_CALLED, + eventName = AmcAnalytics.AMC_CART_SERVICE_CALLED, extraAttributes = hashMapOf( - CHECKOUT_STEP to CartState.checkoutStep, - CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), - CHECKOUT_ID to CartState.checkoutId.orEmpty(), - CART_ID to CartState.cartId.orEmpty(), - CART_REQUEST to data.toString(), + Constant.CHECKOUT_STEP to CartState.checkoutStep, + Constant.CHECKOUT_STEP_CTA to CartState.checkoutStepCta.orEmpty(), + Constant.CHECKOUT_ID to CartState.checkoutId.orEmpty(), + Constant.CART_ID to CartState.cartId.orEmpty(), + Constant.CART_REQUEST to data.toString(), ), ) @@ -105,6 +102,17 @@ constructor( } ?: RepoResult() } + CheckoutSteps.INITIATE_CHECKOUT.name -> { + (data as? CheckoutRequest)?.let { checkoutRequest -> + val response = + cartRepository.initiateCheckout( + checkoutRequest = checkoutRequest, + metricInfo = getAmcMetricInfo(), + ) + response + } ?: RepoResult() + } + CheckoutSteps.OTP_VERIFICATION.name, CheckoutSteps.PAYMENT.name, CheckoutSteps.SELL.name, @@ -136,10 +144,13 @@ constructor( private fun setCheckoutIdAndStep() = coroutineScope.launch(Dispatchers.IO) { listOf( - CHECKOUT_ID to { value: String -> CartState.checkoutId = value }, - CHECKOUT_STEP to { value: String -> CartState.checkoutStep = value }, - CHECKOUT_STEP_CTA to { value: String -> CartState.checkoutStepCta = value }, - CART_ID to { value: String -> CartState.cartId = value }, + Constant.CHECKOUT_ID to { value: String -> CartState.checkoutId = value }, + Constant.CHECKOUT_STEP to { value: String -> CartState.checkoutStep = value }, + Constant.CHECKOUT_STEP_CTA to + { value: String -> + CartState.checkoutStepCta = value + }, + Constant.CART_ID to { value: String -> CartState.cartId = value }, ) .forEach { (key, assignValue) -> naviCacheRepository.get(key)?.value?.let(assignValue) diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcCardListComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcCardListComposable.kt index a50fe63f04..299ad3553e 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcCardListComposable.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcCardListComposable.kt @@ -22,10 +22,13 @@ 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.composables.cards.SetupGoalBasedSipCardComposable import com.navi.amc.fundbuy.models.cards.InvestmentGoalCardData import com.navi.amc.fundbuy.models.cards.PaymentCard +import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetCard 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.GOAL_BASED_SIP_SETUP_CARD 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 @@ -126,5 +129,13 @@ fun RenderCardBasedOnType( cardType = "NORMAL_CARD", ) } + + GOAL_BASED_SIP_SETUP_CARD -> { + SetupGoalBasedSipCardComposable( + cardData = data as SetupMonthlyTargetCard, + onClick = onClick, + cardWidth = cardWidth, + ) + } } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcComposableWidgetFactory.kt b/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcComposableWidgetFactory.kt index 6a972f1bc9..d7bca4d43e 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcComposableWidgetFactory.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/composables/AmcComposableWidgetFactory.kt @@ -22,6 +22,7 @@ import com.navi.amc.fundbuy.composables.widgets.InvestmentTrackerWidgetComposabl 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.SetupGoalBasedSipWidgetComposable import com.navi.amc.fundbuy.composables.widgets.SetupMonthlyTargetWidgetComposable import com.navi.amc.fundbuy.composables.widgets.TitleContentRadioWidgetComposable import com.navi.amc.fundbuy.models.SelectionData @@ -104,5 +105,12 @@ fun AmcComposableWidgetFactory( selectionData = selectionData, ) } + + AmcWidgetType.SETUP_GOAL_BASED_SIP_WIDGET.value -> { + SetupGoalBasedSipWidgetComposable( + widget = data as SetupMonthlyTargetWidget, + onClick = onClick, + ) + } } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OrderStatusFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OrderStatusFragment.kt index d9a3aeafda..8d42c8807c 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OrderStatusFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OrderStatusFragment.kt @@ -371,6 +371,9 @@ class OrderStatusFragment : it.content?.investmentGoalSetupWidget?.let { binding.composeView.apply { setContent { ContentRenderer(it) } } } + it.content?.investmentGoalBasedSipSetupWidget?.let { + binding.composeView.apply { setContent { ContentRenderer(it) } } + } it.content?.onTimeAssuranceData?.let { data -> setOnTimeAssuranceCalloutData(data) } diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt index 89f92226ea..65ad05073e 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt @@ -21,13 +21,15 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import com.google.android.gms.auth.api.phone.SmsRetriever import com.navi.amc.R +import com.navi.amc.common.cart.CartUseCase +import com.navi.amc.common.model.CheckoutRequest import com.navi.amc.common.model.KycValidationData import com.navi.amc.common.model.OtpGenerateData import com.navi.amc.common.model.OtpResponseData import com.navi.amc.common.model.OtpVerificationBody import com.navi.amc.common.model.OtpVerificationData import com.navi.amc.common.taskProcessor.AmcTaskManager -import com.navi.amc.common.viewmodel.CartUseCase +import com.navi.amc.common.viewmodel.OTPSharedVM import com.navi.amc.common.viewmodel.OTPVM import com.navi.amc.common.viewmodel.PaymentSharedVM import com.navi.amc.databinding.OtpFragmentAmcBinding @@ -54,6 +56,7 @@ import com.navi.amc.utils.Constant.API_CALL_MULTI_CLICK_THRESOLD_DUR import com.navi.amc.utils.Constant.AUTOPAY_CHECKED import com.navi.amc.utils.Constant.CAPS_DATA import com.navi.amc.utils.Constant.CART_EXPERIMENT_ENABLED +import com.navi.amc.utils.Constant.CART_ID_SMALL_CASE import com.navi.amc.utils.Constant.CREATE_REDEEM_ORDER import com.navi.amc.utils.Constant.DATA_SOURCE import com.navi.amc.utils.Constant.DELETED_SIP_REFERENCE_ID @@ -78,6 +81,7 @@ import com.navi.amc.utils.Constant.SIP_DATE import com.navi.amc.utils.Constant.SIP_NEXT_INSTALLMENT_DATE import com.navi.amc.utils.Constant.SIP_REFERENCE_ID import com.navi.amc.utils.Constant.SOURCE_REF_ID +import com.navi.amc.utils.Constant.TYPE import com.navi.amc.utils.TempStorageHelper import com.navi.amc.utils.createCartRequest import com.navi.amc.utils.getPaymentCtaUrl @@ -122,12 +126,15 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { private val viewModel by viewModels() private val paymentVM: PaymentManager by activityViewModels() private val paymentSharedVM: PaymentSharedVM by activityViewModels() + private val OTPSharedVM: OTPSharedVM by activityViewModels() private var timer: CountDownTimer? = null private var apiCallLastTime: Long = 0 private val otpReceiver by lazy { SmsAutoReadReceiver() } - private var flowType: String = "" + private var flowType: String = EMPTY private var isin: String? = null + private var cartId: String? = null + private var checkoutType: String? = null private val paymentScreenHelper by lazy { PaymentScreenHelper(owner = requireActivity()) } @Inject lateinit var cartUseCase: CartUseCase @@ -258,7 +265,7 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { } ?: generateOtp(false) } - viewModel.generateOtpResponse.observe(viewLifecycleOwner) { + OTPSharedVM.generateOtpResponse.observe(viewLifecycleOwner) { hideLoader() binding.otpLayout.clear() if (it?.isResendOtp.orFalse()) hideResendOtpOnCallUiState() @@ -371,7 +378,7 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { } viewModel.sipCreateResponse.observe(viewLifecycleOwner) { response -> - val bundle = Bundle() + val bundle = Bundle(arguments) response?.data?.statusData?.let { bundle.putParcelable(CAPS_DATA, it) bundle.putString(SIP_REFERENCE_ID, viewModel.sipReferenceId) @@ -466,7 +473,8 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { sipDate = arguments?.getString(SIP_DATE), autoPayChecked = true, deletedSipReferenceId = arguments?.getString(DELETED_SIP_REFERENCE_ID), - ) + ), + source = arguments?.getString(SOURCE), ) } @@ -663,6 +671,8 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { if (PreferenceManager.getBooleanPreference(CART_EXPERIMENT_ENABLED)) { arguments?.getString(NEW_FLOW_TYPE).orEmpty() } else arguments?.getString(OTP_FLOW_TYPE).orEmpty() + this.cartId = arguments?.getString(CART_ID_SMALL_CASE) + this.checkoutType = arguments?.getString(TYPE) val fundIdParam = arguments?.getString(AmcAnalytics.FUND_ID) val isinParam = arguments?.getString(ISIN) @@ -770,7 +780,7 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { otpValidateRequest = OtpVerificationData( otp = binding.otpLayout.getText(), - otpToken = viewModel.generateOtpResponse.value?.otpToken, + otpToken = OTPSharedVM.generateOtpResponse.value?.otpToken, otpAutofill = isAutoFetchOtp, ), redemptionOrderId = arguments?.getString(REDEMPTION_ORDER_ID)?.toInt(), @@ -789,24 +799,41 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { } private fun generateOtp(isResendOtp: Boolean) { - showLoader() if (isResendOtp) { binding.otpLayout.setBoxBg(false) binding.enterCorrectOtpTv.isVisible = false } - viewModel.generateOtp( - OtpGenerateData( - phoneNumber = BaseUtils.getPhoneNumber(), - deliveryType = TEXT, - isResendOtp = isResendOtp, - isPhoneNumberGenerated = true, - sourceRefId = arguments?.getString(SOURCE_REF_ID), - ), - flowType, - isResendOtp, - screenName = screenName, - cartRequest = createCartRequest(arguments), - ) + + if (cartId.isNullOrEmpty()) { + showLoader() + OTPSharedVM.generateOtp( + OtpGenerateData( + phoneNumber = BaseUtils.getPhoneNumber(), + deliveryType = TEXT, + isResendOtp = isResendOtp, + isPhoneNumberGenerated = true, + sourceRefId = arguments?.getString(SOURCE_REF_ID), + ), + flowType, + isResendOtp, + screenName = screenName, + cartRequest = createCartRequest(arguments), + ) + } else { + if (isResendOtp) { + showLoader() + OTPSharedVM.generateOtp( + screenName = screenName, + checkoutRequest = + CheckoutRequest( + cartId = cartId, + type = checkoutType, + source = arguments?.getString(SOURCE).orEmpty(), + ), + cartId = cartId, + ) + } + } } private fun validateKyc() { diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetJsonDeserializer.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetJsonDeserializer.kt index 97bc1a1683..86dc903198 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetJsonDeserializer.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetJsonDeserializer.kt @@ -53,6 +53,8 @@ class AmcWidgetJsonDeserializer : JsonDeserializer { TitleSubtitleLottieWidget::class.java AmcWidgetType.TITLE_CONTENT_RADIO_WIDGET.value -> TitleContentRadioWidget::class.java + AmcWidgetType.SETUP_GOAL_BASED_SIP_WIDGET.value -> + SetupMonthlyTargetWidget::class.java else -> null } return if (className != null) { diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetType.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetType.kt index 490838aa6e..bd2c4c7b98 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetType.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/AmcWidgetType.kt @@ -21,4 +21,5 @@ enum class AmcWidgetType(val value: String) { REPEAT_ORDER_WIDGET("repeat_order_widget"), BANNER_WITH_LOTTIE_WIDGET("banner_with_lottie_widget"), TITLE_CONTENT_RADIO_WIDGET("fund_investing_type"), + SETUP_GOAL_BASED_SIP_WIDGET("setup_goal_based_sip_widget"), } diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/CheckoutResponse.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/CheckoutResponse.kt index 3eb2c74f92..77673f85d0 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/CheckoutResponse.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/CheckoutResponse.kt @@ -21,6 +21,7 @@ data class CheckoutRequest( @SerializedName("mandateCreationRequest") val mandateTokenRequest: AutoPaySetupRequestData? = null, @SerializedName("sipCreationRequest") val sipCreationRequest: SipDetailsData? = null, + @SerializedName("source") val source: String? = null, ) data class CheckoutResponse( @@ -31,6 +32,7 @@ data class CheckoutResponse( @SerializedName("nextStepCta") val nextStepCta: String? = null, @SerializedName("nextStep") val nextStep: String? = null, @SerializedName("stepResponse") val stepResponse: GenericStepResponse? = null, + @SerializedName("redirectionCta") var redirectionCta: String? = null, ) data class GenericStepResponse( diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/Footer.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/Footer.kt index 7143561e72..6e3d2988f2 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/Footer.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/Footer.kt @@ -27,6 +27,7 @@ data class Footer( @SerializedName("footerCalloutList") var footerCalloutList: FooterCalloutList? = null, @SerializedName("footerProperties") var footerProperties: FooterProperties? = null, @SerializedName("showShadow") var showShadow: Boolean? = null, + @SerializedName("showDivider") var showDivider: Boolean? = null, ) : Parcelable @Parcelize diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/OrderStatusScreenData.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/OrderStatusScreenData.kt index 3494a2b834..667874b065 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/OrderStatusScreenData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/OrderStatusScreenData.kt @@ -45,6 +45,8 @@ data class OrderStatusContent( @SerializedName("investmentGoalSetupWidget") val investmentGoalSetupWidget: GenericComposableWidget? = null, @SerializedName("onTimeAssuranceData") val onTimeAssuranceData: OnTimeAssuranceData? = null, + @SerializedName("investmentGoalBasedSipSetupWidget") + val investmentGoalBasedSipSetupWidget: GenericComposableWidget? = null, ) data class OrderDetailsData( diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/OtpGenerateResponse.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/OtpGenerateResponse.kt index 92663ae9ec..a78a411c9b 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/OtpGenerateResponse.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/OtpGenerateResponse.kt @@ -15,4 +15,5 @@ import kotlinx.parcelize.Parcelize data class OtpGenerateResponse( @SerializedName("otpToken", alternate = ["token"]) var otpToken: String? = null, @SerializedName("isResendOtp") var isResendOtp: Boolean = false, + @SerializedName("redirectionCta") var redirectionCta: String? = null, ) : Parcelable diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CartRequest.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CartRequest.kt index 04d0748063..c3f0da061d 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CartRequest.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CartRequest.kt @@ -31,4 +31,5 @@ data class CartOrderDetails( @SerializedName("mandateType") val mandateType: String? = null, @SerializedName("deletedSipReferenceId") val deletedSipReferenceId: String? = null, @SerializedName("folioNumber") val folioNumber: String? = null, + @SerializedName("goalReferenceId") val goalReferenceId: String? = null, ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CheckoutSteps.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CheckoutSteps.kt index de9d3b2652..9d471a27cb 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CheckoutSteps.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/cart/CheckoutSteps.kt @@ -10,6 +10,7 @@ package com.navi.amc.common.model.cart enum class CheckoutSteps(val value: String) { CART_IDLE("CART_IDLE"), INITIATE_CART("INITIATE_CART"), + INITIATE_CHECKOUT("INITIATE_CHECKOUT"), OTP_GENERATION("OTP_GENERATION"), OTP_VERIFICATION("OTP_VERIFICATION"), PAYMENT("PAYMENT"), diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/repo/CartRepository.kt b/android/navi-amc/src/main/java/com/navi/amc/common/repo/CartRepository.kt index afeaa972c7..376811f3a8 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/repo/CartRepository.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/repo/CartRepository.kt @@ -28,6 +28,19 @@ class CartRepository @Inject constructor(private val retrofitService: RetrofitSe metricInfo = metricInfo, ) + suspend fun initiateCheckout( + checkoutRequest: CheckoutRequest, + metricInfo: MetricInfo>, + ) = + apiResponseCallback( + response = + retrofitService.initiateCheckout( + source = checkoutRequest.source, + checkoutRequest = checkoutRequest, + ), + metricInfo = metricInfo, + ) + suspend fun checkoutNextAction( checkoutRequest: CheckoutRequest, nextStepCta: String?, diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/repo/CheckerRepository.kt b/android/navi-amc/src/main/java/com/navi/amc/common/repo/CheckerRepository.kt index 4940910dc0..2d77238035 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/repo/CheckerRepository.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/repo/CheckerRepository.kt @@ -167,8 +167,11 @@ class CheckerRepository @Inject constructor(private val retrofitService: Retrofi metricInfo = getAmcMetricInfo(), ) - suspend fun fetchSipSuccessPage() = - apiResponseCallback(retrofitService.fetchSipSuccessPage(), metricInfo = getAmcMetricInfo()) + suspend fun fetchSipSuccessPage(source: String?) = + apiResponseCallback( + retrofitService.fetchSipSuccessPage(source = source), + metricInfo = getAmcMetricInfo(), + ) suspend fun modifySipDetails(data: ModifySipRequestDetails, sipReferenceId: String) = apiResponseCallback( diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/repo/OTPRepository.kt b/android/navi-amc/src/main/java/com/navi/amc/common/repo/OTPRepository.kt index 575264a86b..e4a1e5e2e8 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/repo/OTPRepository.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/repo/OTPRepository.kt @@ -108,9 +108,9 @@ class OTPRepository @Inject constructor(private val retrofitService: RetrofitSer metricInfo = getAmcMetricInfo(), ) - suspend fun fetchSipSuccessPage() = + suspend fun fetchSipSuccessPage(source: String? = null) = apiResponseCallback( - response = retrofitService.fetchSipSuccessPage(), + response = retrofitService.fetchSipSuccessPage(source = source), metricInfo = getAmcMetricInfo(), ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/view/FooterView.kt b/android/navi-amc/src/main/java/com/navi/amc/common/view/FooterView.kt index f0d4d302bc..f919e7714c 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/view/FooterView.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/view/FooterView.kt @@ -75,7 +75,7 @@ class FooterView(context: Context, attrs: AttributeSet?) : ConstraintLayout(cont footer?.title?.let { binding.title.isVisible = true - binding.divider.isVisible = true + binding.divider.isVisible = footer.showDivider != false binding.title.highlightColor = WHITE.parseColorSafe() binding.title.setSpannableString(it, ::action) } @@ -151,6 +151,10 @@ class FooterView(context: Context, attrs: AttributeSet?) : ConstraintLayout(cont binding.shadow.isVisible = false } + fun removeElevation() { + binding.footerContainer.elevation = resources.getDimension(CommonR.dimen.layout_dp_0) + } + fun changeNextButtonBackground(enabled: Boolean) { if (enabled) { binding.nextCta.setBackgroundResource(WidgetsR.drawable.bg_cta_primary_amc) @@ -192,8 +196,7 @@ class FooterView(context: Context, attrs: AttributeSet?) : ConstraintLayout(cont binding.nextCta.background = getNaviDrawable( cornerRadius = resources.getDimension(com.navi.design.R.dimen.dp_4).toInt(), - backgroundColor = - ContextCompat.getColor(context, R.color.button_loader_loading_color), + backgroundColor = ContextCompat.getColor(context, R.color.disabled_button_color), ) } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/CheckerVM.kt b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/CheckerVM.kt index 8ea15266e9..22ff4de9a3 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/CheckerVM.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/CheckerVM.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.navi.amc.common.activity.CheckerActivity +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.CheckerResponse import com.navi.amc.common.model.KycCheckerContent @@ -384,10 +385,10 @@ constructor(private val repository: CheckerRepository, private val cartUseCase: } } - fun postSipDetails(details: SipDetailsData) { + fun postSipDetails(details: SipDetailsData, source: String? = null) { viewModelScope.launch { val responseAsync = async { repository.postSipDetails(details) } - val successDataAsync = async { repository.fetchSipSuccessPage() } + val successDataAsync = async { repository.fetchSipSuccessPage(source = source) } val response = responseAsync.await() val successData = successDataAsync.await() if (response.isSuccessWithData() && successData.isSuccessWithData()) { @@ -398,9 +399,9 @@ constructor(private val repository: CheckerRepository, private val cartUseCase: } } - fun getSipSuccessPage(sipRefId: String?) { + fun getSipSuccessPage(sipRefId: String?, source: String? = null) { viewModelScope.launch { - val response = repository.fetchSipSuccessPage() + val response = repository.fetchSipSuccessPage(source = source) if (response.error == null && response.errors.isNullOrEmpty()) { sipReferenceId = sipRefId.orEmpty() _asyncResponse.value = response diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OTPSharedVM.kt b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OTPSharedVM.kt new file mode 100644 index 0000000000..fcbdf70d3a --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OTPSharedVM.kt @@ -0,0 +1,104 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.common.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope +import com.navi.amc.common.cart.CartUseCase +import com.navi.amc.common.model.CheckoutRequest +import com.navi.amc.common.model.OtpGenerateData +import com.navi.amc.common.model.OtpGenerateResponse +import com.navi.amc.common.model.cart.CartRequest +import com.navi.amc.common.model.cart.CheckoutSteps +import com.navi.amc.common.repo.OTPRepository +import com.navi.amc.utils.AmcAnalytics +import com.navi.amc.utils.Constant.CART_EXPERIMENT_ENABLED +import com.navi.amc.utils.getAmcMetricInfo +import com.navi.base.sharedpref.PreferenceManager +import com.navi.common.network.models.RepoResult +import com.navi.common.utils.SingleLiveEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class OTPSharedVM +@Inject +constructor(private val cartUseCase: CartUseCase, private val otpRepository: OTPRepository) : + BaseAmcVM() { + + private val _generateOtpResponse = SingleLiveEvent() + val generateOtpResponse: LiveData + get() = _generateOtpResponse + + fun generateOtp( + otpGenerateData: OtpGenerateData? = null, + flowType: String? = null, + isResendOtp: Boolean = false, + screenName: String, + cartRequest: CartRequest? = null, + checkoutRequest: CheckoutRequest? = null, + cartId: String? = null, + ) { + viewModelScope.launch { + val response: RepoResult = + if (PreferenceManager.getBooleanPreference(CART_EXPERIMENT_ENABLED)) { + cartId?.let { + cartUseCase.performCheckoutStep( + data = checkoutRequest, + step = CheckoutSteps.INITIATE_CHECKOUT.value, + ) + } + ?: run { + cartUseCase.performCheckoutStep( + data = cartRequest, + step = CheckoutSteps.INITIATE_CART.value, + ) + } + } else { + otpGenerateData?.let { it -> + if (flowType?.isBlank() == true) { + otpRepository.generateRedeemOtp(it) + } else { + otpRepository.generateOtp( + otpGenerateData = it, + metricInfo = getAmcMetricInfo(screenName = screenName), + ) + } + } ?: return@launch + } + if (response.error == null && response.errors.isNullOrEmpty()) { + sendEvent( + eventName = AmcAnalytics.AMC_OTP_GENERATE_SUCCESSFUL, + extraAttributes = + hashMapOf( + AmcAnalytics.AMC_OTP_GENERATE_SUCCESSFUL_RESPONSE_DATA to + response.data.toString() + ), + screenName = AmcAnalytics.OTP_INIT, + ) + response.data?.isResendOtp = isResendOtp + _generateOtpResponse.value = response.data + } else { + sendEvent( + eventName = AmcAnalytics.AMC_OTP_GENERATE_ERROR, + extraAttributes = + hashMapOf( + AmcAnalytics.AMC_OTP_GENERATE_ERROR_RESPONSE_DATA to + response.data.toString(), + AmcAnalytics.AMC_OTP_GENERATE_ERROR_ERRORS to + response.errors.toString(), + AmcAnalytics.AMC_OTP_GENERATE_ERROR_ERROR to response.error.toString(), + ), + screenName = AmcAnalytics.OTP_INIT, + ) + setErrorData(response.errors, response.error) + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OTPVM.kt b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OTPVM.kt index 1b924aaf9e..cbe7a80b0b 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OTPVM.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OTPVM.kt @@ -10,17 +10,15 @@ package com.navi.amc.common.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.CheckoutResponse import com.navi.amc.common.model.KycValidationData import com.navi.amc.common.model.KycValidationResponse import com.navi.amc.common.model.NextCtaResponse -import com.navi.amc.common.model.OtpGenerateData -import com.navi.amc.common.model.OtpGenerateResponse import com.navi.amc.common.model.OtpResponse import com.navi.amc.common.model.OtpResponseData import com.navi.amc.common.model.OtpVerificationBody -import com.navi.amc.common.model.cart.CartRequest import com.navi.amc.common.model.cart.CheckoutStepCta import com.navi.amc.common.model.cart.CheckoutSteps import com.navi.amc.common.repo.OTPRepository @@ -57,10 +55,6 @@ constructor(private val repository: OTPRepository, private val cartUseCase: Cart val dataResponse: LiveData get() = _dataResponse - private val _generateOtpResponse = SingleLiveEvent() - val generateOtpResponse: LiveData - get() = _generateOtpResponse - private val _verifyOtpResponse = SingleLiveEvent() val verifyOtpResponse: LiveData get() = _verifyOtpResponse @@ -130,60 +124,6 @@ constructor(private val repository: OTPRepository, private val cartUseCase: Cart } } - fun generateOtp( - otpGenerateData: OtpGenerateData, - flowType: String, - isResendOtp: Boolean, - screenName: String, - cartRequest: CartRequest, - ) { - viewModelScope.launch { - val response: RepoResult = - if (PreferenceManager.getBooleanPreference(CART_EXPERIMENT_ENABLED)) { - cartUseCase.performCheckoutStep( - data = cartRequest, - step = CheckoutSteps.INITIATE_CART.value, - ) - } else { - if (flowType.isBlank()) { - repository.generateRedeemOtp(otpGenerateData) - } else { - repository.generateOtp( - otpGenerateData = otpGenerateData, - metricInfo = getAmcMetricInfo(screenName = screenName), - ) - } - } - if (response.error == null && response.errors.isNullOrEmpty()) { - sendEvent( - eventName = AmcAnalytics.AMC_OTP_GENERATE_SUCCESSFUL, - extraAttributes = - hashMapOf( - AmcAnalytics.AMC_OTP_GENERATE_SUCCESSFUL_RESPONSE_DATA to - response.data.toString() - ), - screenName = AmcAnalytics.OTP_INIT, - ) - response.data?.isResendOtp = isResendOtp - _generateOtpResponse.value = response.data - } else { - sendEvent( - eventName = AmcAnalytics.AMC_OTP_GENERATE_ERROR, - extraAttributes = - hashMapOf( - AmcAnalytics.AMC_OTP_GENERATE_ERROR_RESPONSE_DATA to - response.data.toString(), - AmcAnalytics.AMC_OTP_GENERATE_ERROR_ERRORS to - response.errors.toString(), - AmcAnalytics.AMC_OTP_GENERATE_ERROR_ERROR to response.error.toString(), - ), - screenName = AmcAnalytics.OTP_INIT, - ) - setErrorData(response.errors, response.error) - } - } - } - fun verifyOtp(otpVerifyData: OtpVerificationBody, screenName: String) { viewModelScope.launch { val response: RepoResult = @@ -306,14 +246,14 @@ constructor(private val repository: OTPRepository, private val cartUseCase: Cart } } - fun createSip(details: SipDetailsData) { + fun createSip(details: SipDetailsData, source: String? = null) { viewModelScope.launch { val responseAsync = async { if (PreferenceManager.getBooleanPreference(CART_EXPERIMENT_ENABLED)) cartUseCase.performCheckoutStep() else repository.postSipDetails(details) } - val successDataAsync = async { repository.fetchSipSuccessPage() } + val successDataAsync = async { repository.fetchSipSuccessPage(source = source) } val response = responseAsync.await() val successData = successDataAsync.await() if (response.isSuccessWithData() && successData.isSuccessWithData()) { diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt index 48c86280c1..7cda12df6e 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/viewmodel/OrderStatusViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.google.gson.reflect.TypeToken +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.NextCtaResponse import com.navi.amc.common.model.OrderStatusScreenData diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/common/ui/CommonComposables.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/common/ui/CommonComposables.kt new file mode 100644 index 0000000000..c6ddfadd13 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/common/ui/CommonComposables.kt @@ -0,0 +1,360 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.common.ui + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.animateScrollBy +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.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.lazy.LazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.amc.common.composables.base.AMCTextField +import com.navi.amc.compose.common.utils.KeyboardVisibilityObserver +import com.navi.amc.fundbuy.models.SliderRangeData +import com.navi.amc.fundbuy.models.TextFieldSliderData +import com.navi.amc.fundbuy.models.cards.SliderData +import com.navi.amc.fundbuy.viewmodel.TargetSetupViewModel +import com.navi.amc.utils.AmcColor +import com.navi.amc.utils.convertToTextFieldData +import com.navi.amc.utils.getVisualTransformation +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.design.textview.model.TextWithStyle +import com.navi.design.utils.NoRippleIndicationSource +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.RED_TICK_MARK_24 +import com.navi.naviwidgets.widgets.fixedhinttextinput.TextInputFixedHintWidgetData +import com.navi.uitron.utils.hexToComposeColor +import kotlin.collections.forEach +import kotlin.collections.get + +@RequiresApi(Build.VERSION_CODES.R) +@Composable +fun TextFieldWithSlider( + items: List?, + listState: LazyListState, + viewModel: TargetSetupViewModel, + screenName: String, +) { + Column(modifier = Modifier.fillMaxWidth().padding(0.dp)) { + items?.forEach { item -> + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + NaviTextWidgetized(textFieldData = convertToTextFieldData(item.title)) + NaviTextWidgetized(textFieldData = convertToTextFieldData(item.subtitle)) + } + UserInputText( + item.textFieldData, + item.itemType, + listState, + viewModel = viewModel, + screenName = screenName, + ) + } + item.sliderData?.let { + DynamicSlider( + it, + item.itemType, + viewModel = viewModel, + screenName = screenName, + sliderTransitionData = item.sliderTransition, + ) + } + ErrorContainer(item, viewModel = viewModel) + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Composable +fun ErrorContainer(item: TextFieldSliderData?, viewModel: TargetSetupViewModel) { + val errorValue = viewModel.errorMap[item?.itemType] + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + errorValue?.let { + NaviImage( + imageFieldData = + ImageFieldData(iconCode = RED_TICK_MARK_24, iconWidth = 16, iconHeight = 16), + modifier = Modifier.padding(1.dp), + ) + NaviTextWidgetized( + textFieldData = convertToTextFieldData(TextWithStyle(text = errorValue)), + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DynamicSlider( + sliderData: SliderData, + id: String?, + viewModel: TargetSetupViewModel, + screenName: String, + sliderTransitionData: List? = null, +) { + val interactionSource = remember { NoRippleIndicationSource() } + + val currentValue = viewModel.getMapValue(id) + + LaunchedEffect(currentValue, sliderTransitionData) { + val numericValue = currentValue.ifEmpty { "0" }.toLongOrNull() + + val matchedTitle = + sliderTransitionData + ?.firstOrNull { data -> + val min = data?.minValue + val max = data?.maxValue + min != null && max != null && numericValue != null && numericValue in min..max + } + ?.title + + viewModel.setDynamicText(matchedTitle) + } + + Box(modifier = Modifier.fillMaxWidth()) { + val minSliderValue = sliderData.minValue?.toFloat() ?: 0f + val maxSliderValue = sliderData.maxValue?.toFloat() ?: 100000f + val sliderStepValue = sliderData.stepValue?.toInt() ?: 500 + val defaultValue = sliderData.selectedValue?.toFloat() ?: 0f + LaunchedEffect(sliderData.stepValue) { + viewModel.updateSliderMap( + id, + SliderRangeData(minSliderValue, maxSliderValue, sliderStepValue), + ) + viewModel.updateSliderValue(id, defaultValue, screenName) + } + val sliderValue = viewModel.sliderValueMap[id] ?: 0f + Slider( + value = sliderValue.toFloat(), + onValueChange = { viewModel.updateSliderValue(id, it) }, + valueRange = minSliderValue..maxSliderValue, + steps = maxSliderValue.toInt() / sliderStepValue, + modifier = Modifier.fillMaxWidth().padding(horizontal = 0.dp), + colors = + SliderDefaults.colors( + thumbColor = AmcColor.purplePrimary, + activeTrackColor = + sliderData.minTrackTintColor?.hexToComposeColor ?: AmcColor.purplePrimary, + inactiveTrackColor = + sliderData.maxTrackTintColor?.hexToComposeColor ?: AmcColor.borderDefault, + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ), + thumb = { + Box( + modifier = + Modifier.height(24.dp) + .width(24.dp) + .background(Color.White, shape = CircleShape), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier.height(20.dp) + .width(20.dp) + .background(AmcColor.purplePrimary, shape = CircleShape) + ) + } + }, + track = { sliderPositions -> + val fraction by remember { + derivedStateOf { + (sliderPositions.value - sliderPositions.valueRange.start) / + (sliderPositions.valueRange.endInclusive - + sliderPositions.valueRange.start) + } + } + Box( + modifier = + Modifier.shadow( + elevation = 4.dp, + spotColor = AmcColor.shadowSpotColorOpaque, + ambientColor = AmcColor.shadowSpotColorOpaque, + ) + .fillMaxWidth() + .height(4.dp) + .background( + sliderData.maxTrackTintColor?.hexToComposeColor + ?: AmcColor.borderDefault, + shape = RoundedCornerShape(1.dp), + ) + ) + Box( + modifier = + Modifier.fillMaxWidth(fraction) + .height(4.dp) + .background( + sliderData.minTrackTintColor?.hexToComposeColor + ?: AmcColor.purplePrimary, + shape = RoundedCornerShape(1.dp), + ) + ) + }, + interactionSource = interactionSource, + enabled = true, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@RequiresApi(Build.VERSION_CODES.R) +@Composable +fun UserInputText( + textFieldData: TextInputFixedHintWidgetData?, + id: String?, + listState: LazyListState, + viewModel: TargetSetupViewModel, + screenName: String, +) { + val focusRequester = remember { FocusRequester() } + var isKeyboardOpen by remember { mutableStateOf(false) } + + KeyboardVisibilityObserver { isKeyboardOpen = it } + + LaunchedEffect(isKeyboardOpen) { + if (isKeyboardOpen) { + listState.animateScrollBy(500f) + } else { + listState.animateScrollBy(-500f) + } + } + + LaunchedEffect(textFieldData?.inputTextFixedHintItemData?.savedText) { + viewModel.updateValidationMap( + key = id, + validation = textFieldData?.inputValidationList ?: emptyList(), + ) + viewModel.updateValue( + key = id, + value = textFieldData?.inputTextFixedHintItemData?.savedText ?: "", + screenName = screenName, + ) + } + val textFieldBgColor = + if (textFieldData?.isNonEditable == true) AmcColor.bgNonEditable + else AmcColor.bgDefaultWhite + val textFieldBorderColor = + if (viewModel.errorMap[id] != null) AmcColor.inputFieldErrorBorder + else AmcColor.inputFieldDefaultBorder + val textStyleColor = + if (textFieldData?.isNonEditable == true) AmcColor.textInputNonEditable + else AmcColor.textInputPrimary + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.width(148.dp) + .height(42.dp) + .background(color = textFieldBgColor, shape = RoundedCornerShape(4.dp)), + ) { + AMCTextField( + value = viewModel.getMapValue(id), + onValueChange = { + if ( + it.length <= viewModel.getMaxChar(id, textFieldData) && + it.all { digit -> digit.isDigit() } && + !it.startsWith("0") + ) { + viewModel.updateValue(id, it, screenName) + } + }, + textStyle = + TextStyle( + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = textStyleColor, + textAlign = TextAlign.End, + ), + colors = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + focusedContainerColor = textFieldBgColor, + unfocusedContainerColor = textFieldBgColor, + errorContainerColor = textFieldBgColor, + disabledContainerColor = textFieldBgColor, + cursorColor = Color.Black, + errorCursorColor = Color.Black, + ), + visualTransformation = + getVisualTransformation(textFieldData?.inputTextFixedHintItemData?.format), + modifier = + Modifier.fillMaxSize() + .padding(0.dp) + .border( + width = 1.dp, + color = textFieldBorderColor, + shape = RoundedCornerShape(4.dp), + ) + .width(148.dp) + .height(42.dp) + .background(color = textFieldBgColor, shape = RoundedCornerShape(4.dp)) + .focusRequester(focusRequester), + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), + enabled = (textFieldData?.isNonEditable)?.not() ?: true, + contentPadding = + OutlinedTextFieldDefaults.contentPadding( + start = 16.dp, + top = 8.dp, + end = 16.dp, + bottom = 8.dp, + ), + ) + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/common/ui/NaviAmcFooter.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/common/ui/NaviAmcFooter.kt index d82f19734a..f901e3a3c9 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/compose/common/ui/NaviAmcFooter.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/common/ui/NaviAmcFooter.kt @@ -10,6 +10,7 @@ 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -18,15 +19,20 @@ 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.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition 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.naviwidgets.utils.FOOTER_DEFAULT_LOTTIE_URL import com.navi.uitron.model.ui.ComposePadding import com.navi.uitron.utils.setBackground import com.navi.uitron.utils.setPadding @@ -36,7 +42,11 @@ fun NaviAmcFooter( footer: Footer? = null, onNextCtaClicked: ((actionData: ActionData?) -> Unit)? = null, onBackCtaClicked: ((actionData: ActionData?) -> Unit)? = null, + showLoader: Boolean = false, + nextCtaEnabled: Boolean = true, ) { + val spec = LottieCompositionSpec.Url(url = FOOTER_DEFAULT_LOTTIE_URL) + val composition by rememberLottieComposition(spec) Column(modifier = Modifier.background(color = AmcColor.bgDefaultWhite)) { if (footer?.showShadow == true) { ShadowStrip(modifier = Modifier.fillMaxWidth()) @@ -69,22 +79,41 @@ fun NaviAmcFooter( } footer?.nextCta?.let { val nextCtaProperty = footer.footerProperties?.nextCtaProperty - NaviTextWidgetized( + + Box( modifier = - Modifier.clickable { onNextCtaClicked?.invoke(footer.nextCta) } + Modifier.clickable(enabled = nextCtaEnabled && !showLoader) { + onNextCtaClicked?.invoke(footer.nextCta) + } .setBackground( - backgroundColor = nextCtaProperty?.backgroundColor, + backgroundColor = + if (showLoader || !nextCtaEnabled) + nextCtaProperty?.disabledBgColor + else 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, - ), - ) + contentAlignment = Alignment.Center, + ) { + if (showLoader) { + LottieAnimation( + composition = composition, + iterations = LottieConstants.IterateForever, + modifier = Modifier.width(32.dp).height(20.dp), + ) + } else { + NaviTextWidgetized( + modifier = Modifier.fillMaxWidth(), + textFieldData = + buildFooterTextFieldData( + text = footer.nextCta?.title, + textColor = footer.nextCta?.titleColor, + ), + ) + } + } } } Spacer(modifier = Modifier.height(32.dp)) diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/common/utils/KeyboardUtils.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/common/utils/KeyboardUtils.kt new file mode 100644 index 0000000000..9ecf2a6916 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/common/utils/KeyboardUtils.kt @@ -0,0 +1,38 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.common.utils + +import android.graphics.Rect +import android.view.ViewTreeObserver +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalView + +@Composable +fun KeyboardVisibilityObserver(threshold: Float = 0.1f, isKeyboardVisible: (Boolean) -> Unit) { + val view = LocalView.current + + DisposableEffect(view) { + val observer = view.viewTreeObserver + val listener = + ViewTreeObserver.OnGlobalLayoutListener { + val windowVisibleRect = Rect() + view.getWindowVisibleDisplayFrame(windowVisibleRect) + val screenHeight = view.rootView.height + val keyboardHeight = screenHeight - windowVisibleRect.bottom + if (keyboardHeight > 0) { + val thresholdPixels = screenHeight * threshold + val isVisible = keyboardHeight > thresholdPixels + isKeyboardVisible(isVisible) + } + } + observer.addOnGlobalLayoutListener(listener) + + onDispose { observer.removeOnGlobalLayoutListener(listener) } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/common/utils/NaviAmcScreen.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/common/utils/NaviAmcScreen.kt index a34db8afa6..6a44f53f5c 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/compose/common/utils/NaviAmcScreen.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/common/utils/NaviAmcScreen.kt @@ -12,4 +12,8 @@ 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), + AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN( + "amc_goal_based_sip_target_setup_screen", + Temperature.ORANGE, + ), } diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/entry/NaviAmcRouter.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/entry/NaviAmcRouter.kt index 5592004cd6..d63408d625 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/compose/entry/NaviAmcRouter.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/entry/NaviAmcRouter.kt @@ -12,7 +12,9 @@ import com.navi.amc.compose.common.utils.NaviAmcScreen import com.navi.amc.compose.destinations.AmcRouterScreenDestination import com.navi.amc.compose.destinations.FtueEducateScreenDestination import com.navi.amc.compose.destinations.FtueFundSelectScreenDestination +import com.navi.amc.compose.destinations.GoalBasedSipTargetSetupScreenDestination import com.navi.amc.navigator.NaviAmcDeeplinkNavigator.FTUE +import com.navi.amc.navigator.NaviAmcDeeplinkNavigator.GOAL import com.navi.amc.utils.AmcAnalytics.SOURCE import com.navi.amc.utils.toNavigateAmcModule import com.navi.base.model.ActionData @@ -25,12 +27,19 @@ object NaviAmcRouter { return when (startScreenName) { NaviAmcScreen.AMC_FTUE_EDUCATE_SCREEN.screenName -> FtueEducateScreenDestination NaviAmcScreen.AMC_FTUE_FUND_SELECT_SCREEN.screenName -> FtueFundSelectScreenDestination + NaviAmcScreen.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN.screenName -> + GoalBasedSipTargetSetupScreenDestination + else -> AmcRouterScreenDestination } } - fun onCtaClick(amcComposeActivity: AmcComposeActivity, actionData: ActionData?) { - actionData?.toNavigateAmcModule(amcComposeActivity) + fun onCtaClick( + amcComposeActivity: AmcComposeActivity, + actionData: ActionData?, + bundle: Bundle = Bundle(), + ) { + actionData?.toNavigateAmcModule(amcComposeActivity, bundle = bundle) } fun getDirectionFromCta( @@ -51,6 +60,17 @@ object NaviAmcRouter { FtueFundSelectScreenDestination( sourceScreen = bundle.getString(SOURCE).orEmpty() ) + NaviAmcScreen.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN.screenName -> + GoalBasedSipTargetSetupScreenDestination() + else -> { + null + } + } + } + GOAL -> { + when (secondIdentifier) { + NaviAmcScreen.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN.screenName -> + GoalBasedSipTargetSetupScreenDestination() else -> { null } diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/GoalBasedSipRepository.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/GoalBasedSipRepository.kt new file mode 100644 index 0000000000..d2cc063375 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/GoalBasedSipRepository.kt @@ -0,0 +1,89 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip + +import com.navi.amc.common.model.cart.CartRequest +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenData +import com.navi.amc.compose.feature.goalBasedSip.model.GoalDetailsScreenData +import com.navi.amc.compose.feature.goalBasedSip.model.GoalSummaryScreenData +import com.navi.amc.fundbuy.models.CreateInvestmentGoalData +import com.navi.amc.fundbuy.models.CreateInvestmentGoalResponse +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.retrofit.ResponseCallback +import javax.inject.Inject + +class GoalBasedSipRepository @Inject constructor(private val retrofitService: RetrofitService) : + ResponseCallback() { + + suspend fun fetchTargetSetupScreenData( + goalName: String, + goalReferenceId: String, + ): RepoResult { + return apiResponseCallback( + response = + retrofitService.fetchGoalSetupPageData( + goalName = goalName, + goalReferenceId = goalReferenceId, + ), + metricInfo = getAmcMetricInfo(), + ) + } + + suspend fun createGoal( + createInvestmentGoalData: CreateInvestmentGoalData + ): RepoResult { + return apiResponseCallback( + response = retrofitService.createGoal(createInvestmentGoalData), + metricInfo = getAmcMetricInfo(), + ) + } + + suspend fun fetchSipAmountScreenData( + goalReferenceId: String + ): RepoResult { + return apiResponseCallback( + response = retrofitService.fetchGoalSipAmountPageData(goalReferenceId), + metricInfo = getAmcMetricInfo(), + ) + } + + suspend fun createCart( + source: String, + cartRequest: CartRequest, + ): RepoResult { + return apiResponseCallback( + response = retrofitService.createCart(source, cartRequest), + metricInfo = getAmcMetricInfo(), + ) + } + + suspend fun fetchGoalSummaryScreenData( + goalReferenceId: String + ): RepoResult { + return apiResponseCallback( + response = retrofitService.fetchGoalSummaryScreenData(goalReferenceId), + metricInfo = getAmcMetricInfo(), + ) + } + + suspend fun fetchGoalDetailsScreenData( + goalReferenceId: String, + source: String, + ): RepoResult { + return apiResponseCallback( + response = + retrofitService.fetchGoalDetailsScreenData( + goalReferenceId = goalReferenceId, + source = source, + ), + metricInfo = getAmcMetricInfo(), + ) + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalBasedSipScreenData.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalBasedSipScreenData.kt new file mode 100644 index 0000000000..0bd13616e6 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalBasedSipScreenData.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.model + +import com.google.gson.annotations.SerializedName +import com.navi.amc.common.model.Footer +import com.navi.amc.fundbuy.models.InstallmentDateTypes +import com.navi.amc.fundbuy.models.TextFieldSliderData +import com.navi.base.model.ActionData +import com.navi.common.model.Header +import com.navi.common.model.InvestmentBaseProperty +import com.navi.naviwidgets.models.NaviWidget +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData + +data class GoalBasedSipScreenData( + @SerializedName("header") val header: Header? = null, + @SerializedName("content") val content: GoalBasedSipScreenContent? = null, + @SerializedName("footer") val footer: Footer? = null, +) + +data class GoalBasedSipScreenContent( + @SerializedName("goalCard") val goalCard: GoalBasedSipScreenGoalCard? = null, + @SerializedName("goalCardPostTransition") + val goalCardPostTransition: GoalBasedSipScreenGoalCard? = null, + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("subtitle") val subtitle: TextFieldData? = null, + @SerializedName("items") val items: List? = null, + @SerializedName("installmentDates") val installmentDates: InstallmentDateTypes? = null, + @SerializedName("sipInstallmentDate") val sipInstallmentDate: NaviWidget? = null, +) + +data class GoalBasedSipScreenGoalCard( + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("subtitle") val subtitle: TextFieldData? = null, + @SerializedName("bottomText") val bottomText: TextFieldData? = null, + @SerializedName("rightIcon") val rightIcon: ImageFieldData? = null, + @SerializedName("actionData") val actionData: ActionData? = null, + @SerializedName("properties") val properties: GoalBasedSipScreenGoalCardProperties? = null, +) + +data class GoalBasedSipScreenGoalCardProperties( + @SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null, + @SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null, + @SerializedName("subtitleProperty") val subtitleProperty: InvestmentBaseProperty? = null, + @SerializedName("bottomTextProperty") val bottomTextProperty: InvestmentBaseProperty? = null, + @SerializedName("rightIconProperty") val rightIconProperty: InvestmentBaseProperty? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalBasedSipScreenState.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalBasedSipScreenState.kt new file mode 100644 index 0000000000..16ff683f33 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalBasedSipScreenState.kt @@ -0,0 +1,31 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.model + +import com.navi.amc.fundbuy.models.CreateInvestmentGoalResponse +import com.navi.common.network.models.GenericErrorResponse + +sealed class GoalBasedSipScreenState { + data object Loading : GoalBasedSipScreenState() + + data class Success(val screenResponse: GoalBasedSipScreenData) : GoalBasedSipScreenState() + + data class Error(val error: GenericErrorResponse? = null) : GoalBasedSipScreenState() + + data object Empty : GoalBasedSipScreenState() +} + +sealed class CreateGoalState { + data object Loading : CreateGoalState() + + data class Success(val createGoalResponse: CreateInvestmentGoalResponse) : CreateGoalState() + + data class Error(val error: GenericErrorResponse? = null) : CreateGoalState() + + data object Nothing : CreateGoalState() +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalDetailsScreenData.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalDetailsScreenData.kt new file mode 100644 index 0000000000..3ae13d6243 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalDetailsScreenData.kt @@ -0,0 +1,97 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.model + +import com.google.gson.annotations.SerializedName +import com.navi.amc.fundbuy.models.AmountPageFooter +import com.navi.amc.fundbuy.models.cards.PaymentCard +import com.navi.base.model.ActionData +import com.navi.base.model.GenericAnalytics +import com.navi.common.model.Header +import com.navi.common.model.InvestmentBaseProperty +import com.navi.common.network.models.GenericErrorResponse +import com.navi.naviwidgets.models.SingleSelectionBottomSheetData +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData + +data class GoalDetailsScreenData( + @SerializedName("header") val header: Header? = null, + @SerializedName("content") val content: GoalDetailsScreenContent? = null, + @SerializedName("footer") val footer: AmountPageFooter? = null, + @SerializedName("extraData") val extraData: GoalDetailsScreenExtraData? = null, +) + +data class GoalDetailsScreenContent( + @SerializedName("goalProgressCardData") val goalProgressCardData: GoalProgressCardData? = null, + @SerializedName("transactionHistoryCardData") + val transactionHistoryCardData: TransactionHistoryCardData? = null, + @SerializedName("modifySipCardData") val modifySipCardData: PaymentCard? = null, + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("modifyBottomSheetData") + val modifyBottomSheetData: SingleSelectionBottomSheetData? = null, + @SerializedName("genericBottomSheets") + val genericBottomSheets: Map? = null, + @SerializedName("disclaimerText") val disclaimerText: TextFieldData? = null, +) + +data class GoalProgressCardData( + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("cardData") val cardData: GoalProgressCard? = null, +) + +data class TransactionHistoryCardData( + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("cardData") val cardData: TransactionHistoryCard? = null, +) + +data class GoalProgressCard( + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("topRightIcon") val topRightIcon: ImageFieldData? = null, + @SerializedName("currentValue") val currentValue: TextFieldData? = null, + @SerializedName("goalAmount") val goalAmount: TextFieldData? = null, + @SerializedName("progressIndicatorData") + val goalProgressData: GoalProgressIndicatorData? = null, + @SerializedName("sipText") val sipText: TextFieldData? = null, + @SerializedName("items") val items: List? = null, + @SerializedName("actionData") val actionData: ActionData? = null, + @SerializedName("properties") val properties: GoalCardProperties? = null, +) + +data class TransactionHistoryCard( + @SerializedName("leftTitle") val leftTitle: TextFieldData? = null, + @SerializedName("rightTitle") val rightTitle: TextFieldData? = null, + @SerializedName("items") val items: List? = null, + @SerializedName("actionData") val actionData: ActionData? = null, + @SerializedName("properties") val properties: GoalCardProperties? = null, +) + +data class GoalProgressIndicatorData( + @SerializedName("goalProgressRatio") val goalProgressRatio: Float? = null, + @SerializedName("progressColor") val progressColor: String? = null, + @SerializedName("bgColor") val bgColor: String? = null, +) + +data class GoalCardProperties( + @SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null, + @SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null, + @SerializedName("currentValueProperty") + val currentValueProperty: InvestmentBaseProperty? = null, + @SerializedName("topRightIconProperty") + val topRightIconProperty: InvestmentBaseProperty? = null, + @SerializedName("investedAmountProperty") + val investedAmountProperty: InvestmentBaseProperty? = null, + @SerializedName("goalAmountProperty") val goalAmountProperty: InvestmentBaseProperty? = null, + @SerializedName("progressIndicatorProperty") + val goalProgressTextProperty: InvestmentBaseProperty? = null, +) + +data class GoalDetailsScreenExtraData( + @SerializedName("properties") val properties: InvestmentBaseProperty? = null, + @SerializedName("metaData") val metaData: GenericAnalytics? = null, + @SerializedName("sipReferenceId") val sipReferenceId: String? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalSummaryCardData.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalSummaryCardData.kt new file mode 100644 index 0000000000..ff75879c83 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalSummaryCardData.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.model + +import com.google.gson.annotations.SerializedName +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 GoalSummaryCardData( + @SerializedName("items") val items: List? = null, + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("bottomText") val bottomText: BottomTextData? = null, + @SerializedName("actionData") val actionData: ActionData? = null, + @SerializedName("buttonText") val buttonText: TextFieldData? = null, + @SerializedName("properties") val properties: SipDetailsCardProperties? = null, +) + +data class BottomTextData( + @SerializedName("leftIcon") val leftIcon: ImageFieldData? = null, + @SerializedName("title") val title: TextFieldData? = null, +) + +data class LabelItem( + @SerializedName("label") val label: TextFieldData? = null, + @SerializedName("subtitle") val subtitle: TextFieldData? = null, + @SerializedName("leftIcon") val leftIcon: ImageFieldData? = null, + @SerializedName("tagData") val tagData: TextFieldData? = null, + @SerializedName("value") val value: TextFieldData? = null, + @SerializedName("properties") val properties: LabelItemProperties? = null, +) + +data class LabelItemProperties( + @SerializedName("leftIconProperty") val leftIconProperty: InvestmentBaseProperty? = null, + @SerializedName("tagProperty") val tagProperty: InvestmentBaseProperty? = null, + @SerializedName("valueProperty") val valueProperty: InvestmentBaseProperty? = null, + @SerializedName("labelProperty") val labelProperty: InvestmentBaseProperty? = null, +) + +data class SipDetailsCardProperties( + @SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null, + @SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null, + @SerializedName("subtitleProperty") val subtitleProperty: InvestmentBaseProperty? = null, + @SerializedName("bottomTextProperty") val bottomTextProperty: InvestmentBaseProperty? = null, + @SerializedName("buttonProperty") val buttonProperty: InvestmentBaseProperty? = null, + @SerializedName("itemProperty") val itemProperty: InvestmentBaseProperty? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalSummaryScreenData.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalSummaryScreenData.kt new file mode 100644 index 0000000000..dda634058e --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/model/GoalSummaryScreenData.kt @@ -0,0 +1,45 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.model + +import com.google.gson.annotations.SerializedName +import com.navi.amc.fundbuy.models.AmountPageFooter +import com.navi.amc.fundbuy.models.TextFieldSliderData +import com.navi.base.model.GenericAnalytics +import com.navi.common.model.Header +import com.navi.common.model.InvestmentBaseProperty +import com.navi.design.textview.model.TextWithStyle +import com.navi.naviwidgets.models.response.TextFieldData + +data class GoalSummaryScreenData( + @SerializedName("header") val header: Header? = null, + @SerializedName("content") val content: GoalSummaryScreenContent? = null, + @SerializedName("footer") val footer: AmountPageFooter? = null, + @SerializedName("extraData") val extraData: GoalSummaryScreenExtraData? = null, +) + +data class GoalSummaryScreenContent( + @SerializedName("sipDetailsCard") val sipDetailsCard: GoalSummaryCardData? = null, + @SerializedName("goalDetailsCard") val goalDetailsCard: GoalSummaryCardData? = null, + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("subtitle") val subtitle: TextWithStyle? = null, + @SerializedName("items") val items: List? = null, +) + +data class GoalSummaryScreenProperties( + @SerializedName("cardProperty") val cardProperty: InvestmentBaseProperty? = null, + @SerializedName("titleProperty") val titleProperty: InvestmentBaseProperty? = null, + @SerializedName("subtitleProperty") val subtitleProperty: InvestmentBaseProperty? = null, + @SerializedName("bottomTextProperty") val bottomTextProperty: InvestmentBaseProperty? = null, + @SerializedName("rightIconProperty") val rightIconProperty: InvestmentBaseProperty? = null, +) + +data class GoalSummaryScreenExtraData( + @SerializedName("properties") val properties: GoalSummaryScreenProperties? = null, + @SerializedName("metaData") val metaData: GenericAnalytics? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipAmountScreen.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipAmountScreen.kt new file mode 100644 index 0000000000..03d31c4046 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipAmountScreen.kt @@ -0,0 +1,74 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +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.wrapContentHeight +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.navi.amc.compose.common.ui.TextFieldWithSlider +import com.navi.amc.compose.common.utils.KeyboardVisibilityObserver +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenContent +import com.navi.amc.fundbuy.composables.cards.GoalBasedSipCardComposable +import com.navi.amc.fundbuy.composables.cards.GoalCardPostTransitionComposable +import com.navi.amc.fundbuy.viewmodel.TargetSetupViewModel +import com.navi.amc.utils.AmcColor +import com.navi.amc.utils.Constant.DYNAMIC +import com.navi.base.model.ActionData +import com.navi.naviwidgets.extensions.NaviTextWidgetized + +@Composable +fun GoalBasedSipAmountScreen( + content: GoalBasedSipScreenContent?, + viewModel: TargetSetupViewModel, + modifier: Modifier = Modifier, + onClick: (actionData: ActionData?) -> Unit = {}, + screenName: String, +) { + val listState = rememberLazyListState() + var isKeyboardOpen by remember { mutableStateOf(false) } + + KeyboardVisibilityObserver { isKeyboardOpen = it } + + content?.let { data -> + Column( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .background(color = AmcColor.bgDefaultWhite) + .padding(start = 16.dp, end = 16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + NaviTextWidgetized(textFieldData = data.title, modifier = Modifier.fillMaxWidth()) + if (isKeyboardOpen) { + GoalCardPostTransitionComposable(cardData = data.goalCardPostTransition) + } else { + GoalBasedSipCardComposable(cardData = data.goalCard, cardType = DYNAMIC) + } + Spacer(modifier = Modifier.height(32.dp)) + NaviTextWidgetized(textFieldData = data.subtitle, modifier = Modifier.fillMaxWidth()) + TextFieldWithSlider( + data.items, + listState, + viewModel = viewModel, + screenName = screenName, + ) + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipTargetSetupScreen.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipTargetSetupScreen.kt new file mode 100644 index 0000000000..6ef9653016 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipTargetSetupScreen.kt @@ -0,0 +1,246 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.ui + +import android.os.Build +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.utils.NaviAmcScreen +import com.navi.amc.compose.entry.AmcComposeActivity +import com.navi.amc.compose.entry.NaviAmcRouter +import com.navi.amc.compose.feature.goalBasedSip.model.CreateGoalState +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenData +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenState +import com.navi.amc.compose.feature.goalBasedSip.viewmodel.GoalBasedSipSetupVM +import com.navi.amc.fundbuy.viewmodel.TargetSetupViewModel +import com.navi.amc.utils.AmcAnalytics.sendBackPressEvent +import com.navi.amc.utils.AmcAnalytics.sendEvent +import com.navi.amc.utils.AmcAnalytics.sendScreenLandEvent +import com.navi.amc.utils.Constant.GOAL_NAME +import com.navi.amc.utils.Constant.GOAL_REFERENCE_ID +import com.navi.amc.utils.Constant.SET_GOAL_TARGET +import com.navi.base.deeplink.DeepLinkManager +import com.navi.base.deeplink.util.DeeplinkConstants +import com.navi.base.model.ActionData +import com.navi.base.model.CtaData +import com.navi.common.ui.errorview.FullScreenErrorComposeView +import com.navi.naviwidgets.extensions.hexToColor +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator + +@RequiresApi(Build.VERSION_CODES.R) +@Destination +@Composable +fun GoalBasedSipTargetSetupScreen( + amcComposeActivity: AmcComposeActivity, + navigator: DestinationsNavigator, + viewmodel: GoalBasedSipSetupVM = hiltViewModel(), + targetSetupVM: TargetSetupViewModel = hiltViewModel(), + screenName: String = NaviAmcScreen.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN.name, +) { + val goalBasedSipScreenState by viewmodel.goalBasedSipScreenState.collectAsStateWithLifecycle() + val createGoalResponse by targetSetupVM.createGoalResponse.collectAsStateWithLifecycle() + + LaunchedEffect(createGoalResponse) { + setGoalState( + createGoalState = createGoalResponse, + viewModel = viewmodel, + amcComposeActivity = amcComposeActivity, + ) + } + + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewmodel.fetchTargetSetupScreenData( + goalName = amcComposeActivity.intent.extras?.getString(GOAL_NAME).orEmpty(), + goalReferenceId = + amcComposeActivity.intent.extras?.getString(GOAL_REFERENCE_ID).orEmpty(), + ) + sendScreenLandEvent(screenName = screenName) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + val onFooterNextCtaClicked = { actionData: ActionData? -> + sendEvent( + actionData?.metaData?.clickedData, + screenName = NaviAmcScreen.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN.screenName, + ) + if (actionData?.url == SET_GOAL_TARGET) { + targetSetupVM.createGoal( + goalName = amcComposeActivity.intent.extras?.getString(GOAL_NAME).orEmpty(), + goalReferenceId = + amcComposeActivity.intent.extras?.getString(GOAL_REFERENCE_ID).orEmpty(), + ) + } else { + actionData?.let { + NaviAmcRouter.onCtaClick(actionData = it, amcComposeActivity = amcComposeActivity) + } + Unit + } + } + + val onBackPressed = { + sendBackPressEvent(screenName = screenName) + DeepLinkManager.getDeepLinkListener() + ?.navigateTo( + activity = amcComposeActivity, + ctaData = CtaData(url = DeeplinkConstants.INVESTMENT), + finish = true, + ) + Unit + } + + BackHandler { onBackPressed() } + + val onHelpClicked = { actionData: ActionData? -> amcComposeActivity.onHelpClick(null, null) } + + Column(modifier = Modifier.fillMaxSize()) { + when (goalBasedSipScreenState) { + is GoalBasedSipScreenState.Loading -> { + AmcShimmer() + } + + is GoalBasedSipScreenState.Success -> { + val data = goalBasedSipScreenState as GoalBasedSipScreenState.Success + GoalBasedSipScreenRenderer( + viewModel = viewmodel, + targetSetupVM = targetSetupVM, + screenResponse = data.screenResponse, + onBackPressed = onBackPressed, + onHelpClicked = { onHelpClicked(it) }, + onFooterNextCtaClicked = onFooterNextCtaClicked, + screenName = screenName, + ) + } + + is GoalBasedSipScreenState.Error -> { + val data = goalBasedSipScreenState as GoalBasedSipScreenState.Error + FullScreenErrorComposeView( + error = data.error, + onRetryClick = { + viewmodel.fetchTargetSetupScreenDataFromRemote( + goalName = + amcComposeActivity.intent.extras?.getString(GOAL_NAME).orEmpty(), + goalReferenceId = + amcComposeActivity.intent.extras + ?.getString(GOAL_REFERENCE_ID) + .orEmpty(), + ) + }, + ) + } + + else -> {} + } + } +} + +private fun setGoalState( + createGoalState: CreateGoalState, + viewModel: GoalBasedSipSetupVM, + amcComposeActivity: AmcComposeActivity, +) { + when (createGoalState) { + is CreateGoalState.Loading -> { + viewModel.updateShowButtonLoader(true) + } + + is CreateGoalState.Success -> { + viewModel.updateShowButtonLoader(false) + val data = createGoalState.createGoalResponse + val bundle = Bundle().apply { putString(GOAL_REFERENCE_ID, data.goalId) } + + data.let { + NaviAmcRouter.onCtaClick( + actionData = it.nextCta, + amcComposeActivity = amcComposeActivity, + bundle = bundle, + ) + } + } + + is CreateGoalState.Error -> { + viewModel.updateShowButtonLoader(false) + } + + else -> {} + } +} + +@RequiresApi(Build.VERSION_CODES.R) +@Composable +fun GoalBasedSipScreenRenderer( + viewModel: GoalBasedSipSetupVM, + targetSetupVM: TargetSetupViewModel, + screenResponse: GoalBasedSipScreenData, + onBackPressed: () -> Unit, + onHelpClicked: ((actionData: ActionData?) -> Unit)? = null, + onFooterNextCtaClicked: ((actionData: ActionData?) -> Unit)? = null, + onFooterBackCtaClicked: ((actionData: ActionData?) -> Unit)? = null, + screenName: String, +) { + val showButtonLoader by viewModel.showButtonLoader.collectAsStateWithLifecycle() + val nextCtaEnabled by targetSetupVM.areAllFieldsValidFlow.collectAsStateWithLifecycle() + val listState = rememberLazyListState() + + Scaffold( + modifier = Modifier.fillMaxSize().imePadding(), + topBar = { + NaviAmcHeader( + title = screenResponse.header?.title?.text.orEmpty(), + onNavigationIconClick = onBackPressed, + actionIconText = "HELP", + onActionClick = onHelpClicked, + backgroundColor = hexToColor(screenResponse.header?.bgColor), + ) + }, + content = { innerPadding -> + GoalBasedSipTargetSetupScreenContent( + content = screenResponse.content, + viewModel = targetSetupVM, + listState = listState, + innerPadding = innerPadding, + screenName = screenName, + ) + }, + bottomBar = { + NaviAmcFooter( + footer = screenResponse.footer, + onNextCtaClicked = onFooterNextCtaClicked, + onBackCtaClicked = onFooterBackCtaClicked, + showLoader = showButtonLoader, + nextCtaEnabled = nextCtaEnabled, + ) + }, + ) +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipTargetSetupScreenContent.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipTargetSetupScreenContent.kt new file mode 100644 index 0000000000..1d49b754ed --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalBasedSipTargetSetupScreenContent.kt @@ -0,0 +1,88 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.ui + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.navi.amc.compose.common.ui.TextFieldWithSlider +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenContent +import com.navi.amc.fundbuy.composables.cards.GoalBasedSipCardComposable +import com.navi.amc.fundbuy.viewmodel.TargetSetupViewModel +import com.navi.amc.utils.AmcColor +import com.navi.naviwidgets.extensions.NaviTextWidgetized + +@OptIn(ExperimentalFoundationApi::class) +@RequiresApi(Build.VERSION_CODES.R) +@Composable +fun GoalBasedSipTargetSetupScreenContent( + content: GoalBasedSipScreenContent?, + viewModel: TargetSetupViewModel, + modifier: Modifier = Modifier, + listState: LazyListState = rememberLazyListState(), + innerPadding: PaddingValues, + screenName: String, +) { + content?.let { it -> + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + LazyColumn(state = listState, modifier = modifier.fillMaxSize()) { + items(1) { item -> + Column( + modifier = + Modifier.fillMaxWidth() + .background( + color = AmcColor.bgDefaultWhite, + shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + ) + .padding(start = 16.dp, end = 16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + NaviTextWidgetized( + textFieldData = it.title, + modifier = Modifier.fillMaxWidth(), + ) + + GoalBasedSipCardComposable(cardData = it.goalCard) + + Spacer(modifier = Modifier.height(32.dp)) + + NaviTextWidgetized( + textFieldData = it.subtitle, + modifier = Modifier.fillMaxWidth(), + ) + TextFieldWithSlider( + it.items, + listState, + viewModel = viewModel, + screenName = screenName, + ) + Spacer(modifier = Modifier.height(100.dp).background(Color(0xFFFFFFFF))) + } + } + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalDetailsScreenContent.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalDetailsScreenContent.kt new file mode 100644 index 0000000000..d29ef8eead --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalDetailsScreenContent.kt @@ -0,0 +1,104 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.navi.amc.compose.feature.goalBasedSip.model.GoalDetailsScreenContent +import com.navi.amc.fundbuy.composables.cards.GoalProgressCard +import com.navi.amc.fundbuy.composables.cards.PaymentCardComposable +import com.navi.amc.fundbuy.composables.cards.TransactionHistoryCard +import com.navi.amc.utils.AmcColor +import com.navi.amc.utils.Constant.DISMISS +import com.navi.amc.utils.Constant.MODIFY_SIP_CARD +import com.navi.base.model.ActionData +import com.navi.naviwidgets.extensions.NaviTextWidgetized + +@Composable +fun GoalDetailsScreenContent( + content: GoalDetailsScreenContent?, + modifier: Modifier = Modifier, + onClick: (actionData: ActionData?) -> Unit = {}, +) { + val listState = rememberLazyListState() + var showModifySipCard by remember { mutableStateOf(true) } + + content?.let { data -> + LazyColumn(state = listState, modifier = modifier.fillMaxSize()) { + items(1) { + Column( + modifier = + Modifier.fillMaxSize() + .background(color = AmcColor.bgDefaultWhite) + .padding(start = 16.dp, end = 16.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + NaviTextWidgetized( + textFieldData = data.goalProgressCardData?.title, + modifier = Modifier.fillMaxWidth(), + ) + GoalProgressCard( + cardData = data.goalProgressCardData?.cardData, + onClick = onClick, + ) + + val modifySipData = data.modifySipCardData + + when { + modifySipData != null && showModifySipCard -> { + Spacer(modifier = Modifier.height(24.dp)) + PaymentCardComposable( + paymentCard = modifySipData, + onClick = { action -> + if (action?.url == DISMISS) { + showModifySipCard = false + } else onClick(action) + }, + cardType = MODIFY_SIP_CARD, + ) + Spacer(modifier = Modifier.height(24.dp)) + } + else -> { + Spacer(modifier = Modifier.height(32.dp)) + } + } + + NaviTextWidgetized( + data.transactionHistoryCardData?.title, + modifier = Modifier.fillMaxWidth(), + ) + + TransactionHistoryCard( + cardData = data.transactionHistoryCardData?.cardData, + onClick = onClick, + ) + + NaviTextWidgetized( + textFieldData = data.disclaimerText, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally), + ) + } + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalSummaryScreenContent.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalSummaryScreenContent.kt new file mode 100644 index 0000000000..d7c5c36aee --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/ui/GoalSummaryScreenContent.kt @@ -0,0 +1,62 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.navi.amc.compose.feature.goalBasedSip.model.GoalSummaryScreenContent +import com.navi.amc.fundbuy.composables.cards.GoalSummaryCard +import com.navi.amc.utils.AmcColor +import com.navi.base.model.ActionData +import com.navi.naviwidgets.extensions.NaviTextWidgetized + +@Composable +fun GoalSummaryScreenContent( + content: GoalSummaryScreenContent?, + modifier: Modifier = Modifier, + onClick: (actionData: ActionData?) -> Unit = {}, +) { + val listState = rememberLazyListState() + + content?.let { data -> + LazyColumn(state = listState, modifier = modifier.fillMaxSize()) { + items(1) { + Column( + modifier = + Modifier.fillMaxWidth() + .fillParentMaxHeight() + .background(color = AmcColor.bgDefaultWhite) + .padding(start = 16.dp, end = 16.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + NaviTextWidgetized( + textFieldData = data.title, + modifier = Modifier.fillMaxWidth(), + ) + GoalSummaryCard(data = data.sipDetailsCard, onClick) + + Spacer(modifier = Modifier.height(18.dp)) + + GoalSummaryCard(data = data.goalDetailsCard, onClick) + + Spacer(modifier = Modifier.height(136.dp)) + } + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/viewmodel/GoalBasedSipSetupVM.kt b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/viewmodel/GoalBasedSipSetupVM.kt new file mode 100644 index 0000000000..2fa4fb81b2 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/compose/feature/goalBasedSip/viewmodel/GoalBasedSipSetupVM.kt @@ -0,0 +1,203 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.compose.feature.goalBasedSip.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.navi.amc.common.model.cart.CartRequest +import com.navi.amc.common.viewmodel.BaseAmcVM +import com.navi.amc.compose.feature.goalBasedSip.GoalBasedSipRepository +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenData +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenState +import com.navi.amc.compose.feature.goalBasedSip.model.GoalDetailsScreenData +import com.navi.amc.compose.feature.goalBasedSip.model.GoalSummaryScreenData +import com.navi.amc.fundbuy.models.CreateInvestmentGoalResponse +import com.navi.amc.utils.Constant +import com.navi.base.model.ActionData +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.Constants +import com.navi.common.utils.SingleLiveEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +@HiltViewModel +class GoalBasedSipSetupVM @Inject constructor(private val repository: GoalBasedSipRepository) : + BaseAmcVM() { + + private val _goalBasedSipScreenState = + MutableStateFlow(GoalBasedSipScreenState.Empty) + val goalBasedSipScreenState = _goalBasedSipScreenState.asStateFlow() + + private val _showButtonLoader = MutableStateFlow(false) + val showButtonLoader = _showButtonLoader.asStateFlow() + + private val _goalSummaryScreenData = SingleLiveEvent() + val goalSummaryScreenData: LiveData + get() = _goalSummaryScreenData + + private val _sipAmountScreenData = SingleLiveEvent() + val sipAmountScreenData: LiveData + get() = _sipAmountScreenData + + private val _goalDetailsScreenData = MutableLiveData() + val goalDetailsScreenData: LiveData + get() = _goalDetailsScreenData + + private val _createCartResponse = SingleLiveEvent() + val createCartResponse: SingleLiveEvent + get() = _createCartResponse + + val paymentMode: MutableLiveData = MutableLiveData(null) + + var sipDateId: String? = null + var monthlyDefaultSipDateText: String? = null + var sipType: String = Constant.MONTHLY + var formattedSipDate: String? = null + + fun fetchTargetSetupScreenData(goalName: String, goalReferenceId: String) { + getCachedResponseOrNull(Constants.AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN) + ?.let { it as? GoalBasedSipScreenState } + ?.let { cachedState -> _goalBasedSipScreenState.value = cachedState } + ?: fetchTargetSetupScreenDataFromRemote(goalName, goalReferenceId) + } + + fun fetchTargetSetupScreenDataFromRemote(goalName: String, goalReferenceId: String) { + viewModelScope.safeLaunch { + updateTargetSetupScreenState(GoalBasedSipScreenState.Loading) + + val goalBasedSipScreenResponse = + repository.fetchTargetSetupScreenData( + goalName = goalName, + goalReferenceId = goalReferenceId, + ) + + if (goalBasedSipScreenResponse.isSuccessWithData()) { + updateTargetSetupScreenState( + GoalBasedSipScreenState.Success( + screenResponse = goalBasedSipScreenResponse.data ?: return@safeLaunch + ) + ) + } else { + updateTargetSetupScreenState( + GoalBasedSipScreenState.Error( + error = + goalBasedSipScreenResponse.errors?.firstOrNull() ?: return@safeLaunch + ) + ) + } + } + } + + private fun updateTargetSetupScreenState(state: GoalBasedSipScreenState) { + _goalBasedSipScreenState.value = state + } + + fun fetchSipAmountScreenData(screenName: String, goalReferenceId: String) { + getCachedResponseOrNull(Constants.AMC_GOAL_BASED_SIP_AMOUNT_SCREEN)?.let { cachedResponse -> + _sipAmountScreenData.value = (cachedResponse as? GoalBasedSipScreenData) + } + ?: run { + fetchSipAmountScreenDataFromRemote( + screenName = screenName, + goalReferenceId = goalReferenceId, + ) + } + } + + fun fetchSipAmountScreenDataFromRemote(screenName: String, goalReferenceId: String) { + viewModelScope.safeLaunch { + val response = repository.fetchSipAmountScreenData(goalReferenceId = goalReferenceId) + if (response.errors.isNullOrEmpty() && response.error == null) { + _sipAmountScreenData.value = response.data + } else { + setErrorData(response.errors, response.error) + } + } + } + + fun createCart(screenName: String, cartRequest: CartRequest, source: String) { + viewModelScope.safeLaunch { + val response = repository.createCart(source = source, cartRequest = cartRequest) + if (response.errors.isNullOrEmpty() && response.error == null) { + _createCartResponse.value = response.data + } else { + setErrorData(response.errors, response.error) + } + } + } + + fun fetchGoalSummaryScreenData(screenName: String, goalReferenceId: String) { + getCachedResponseOrNull(Constants.AMC_GOAL_BASED_SIP_SUMMARY_SCREEN)?.let { cachedResponse + -> + _goalSummaryScreenData.value = (cachedResponse as? GoalSummaryScreenData) + } + ?: run { + fetchGoalSummaryScreenDataFromRemote( + screenName = screenName, + goalReferenceId = goalReferenceId, + ) + } + } + + fun fetchGoalSummaryScreenDataFromRemote(screenName: String, goalReferenceId: String) { + viewModelScope.safeLaunch { + val response = repository.fetchGoalSummaryScreenData(goalReferenceId = goalReferenceId) + if (response.errors.isNullOrEmpty() && response.error == null) { + _goalSummaryScreenData.value = response.data + } else { + setErrorData(response.errors, response.error) + } + } + } + + fun getGoalDetailsScreenData(screenName: String, goalReferenceId: String, source: String) { + viewModelScope.safeLaunch { + val response = + repository.fetchGoalDetailsScreenData( + goalReferenceId = goalReferenceId, + source = source, + ) + if (response.errors.isNullOrEmpty() && response.error == null) { + _goalDetailsScreenData.value = response.data + } else { + setErrorData(response.errors, response.error) + } + } + } + + fun setPaymentMode(it: ActionData) { + paymentMode.value = + it.parameters + ?.firstOrNull { it.key?.contains(Constant.PAYMENT_MODE, true) == true } + ?.value ?: paymentMode.value + } + + fun getPaymentModeText(): String { + return when (paymentMode.value) { + Constant.UPI -> "UPI" + Constant.NET_BANKING -> "Netbanking" + else -> "UPI" + } + } + + fun getPaymentModeFooterIcon(): String? { + return when (paymentMode.value) { + Constant.UPI -> Constant.UPI_ICON_FOOTER + Constant.NET_BANKING -> Constant.NET_BANKING_ICON_FOOTER + else -> null + } + } + + fun updateShowButtonLoader(showButtonLoader: Boolean) { + _showButtonLoader.update { showButtonLoader } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/activities/FundBuyActivity.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/activities/FundBuyActivity.kt index 9c81a218b3..920da54550 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/activities/FundBuyActivity.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/activities/FundBuyActivity.kt @@ -55,6 +55,8 @@ import com.navi.amc.utils.Constant.SPECIAL_STATE import com.navi.amc.utils.Constant.TYPE import com.navi.amc.utils.Constant.VALID_STATE import com.navi.amc.utils.SubPageStatusType +import com.navi.amc.utils.SubPageStatusType.GOAL_BASED_SIP_AMOUNT_SCREEN +import com.navi.amc.utils.SubPageStatusType.GOAL_DETAILS_SCREEN import com.navi.amc.utils.SubPageStatusType.ORDER_STATUS import com.navi.amc.utils.bottomSheetNavigationAnimation import com.navi.amc.utils.getBottomSheet @@ -262,7 +264,8 @@ class FundBuyActivity : actionData?.url?.let { url -> if (url == "back") { navigateBack() - } else if (navigationNotNeeded(url)) { + } else if (url == "goBackTwice") goBackTwice() + else if (navigationNotNeeded(url)) { return } else { val requestCodeForResult = needActivityResultCode(url) @@ -275,7 +278,15 @@ class FundBuyActivity : needsResult = requestCodeForResult.isNotNull(), ) } - } ?: kotlin.run { navigateBack() } + } + ?: kotlin.run { + val previousScreenName = this.getCurrentFragmentScreenName() + if (previousScreenName == GOAL_BASED_SIP_AMOUNT_SCREEN) { + this.finish() + return + } + navigateBack() + } } } catch (exception: Exception) { exception.log() @@ -294,6 +305,10 @@ class FundBuyActivity : } else null } + private fun goBackTwice() { + finish() + } + private fun navigateBack() { when { viewModel.isLastFragmentAndIsFundLanding() -> { @@ -330,7 +345,17 @@ class FundBuyActivity : } private fun navigateToHomeScreenIfTaskRoot() { - if (isTaskRoot) navigateToHome() else finish() + val previousScreenName = this.getCurrentFragmentScreenName() + if (isTaskRoot) navigateToHome() + else if (previousScreenName == GOAL_DETAILS_SCREEN) { + DeepLinkManager.getDeepLinkListener() + ?.navigateTo( + activity = this, + ctaData = CtaData(url = DeeplinkConstants.INVESTMENT), + finish = true, + clearTask = true, + ) + } else finish() } private fun popFragment(index: Int) { diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalBasedSipCardComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalBasedSipCardComposable.kt new file mode 100644 index 0000000000..439c1c1dd8 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalBasedSipCardComposable.kt @@ -0,0 +1,130 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.cards + +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.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.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 androidx.hilt.navigation.compose.hiltViewModel +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenGoalCard +import com.navi.amc.fundbuy.viewmodel.TargetSetupViewModel +import com.navi.amc.utils.Constant.DYNAMIC +import com.navi.amc.utils.Constant.STATIC +import com.navi.base.model.ActionData +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.utils.ShapeUtil +import com.navi.uitron.utils.getBorderStrokeBrushData +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.setPadding + +@Composable +fun GoalBasedSipCardComposable( + modifier: Modifier = Modifier, + cardData: GoalBasedSipScreenGoalCard? = null, + onClick: (actionData: ActionData?) -> Unit = {}, + viewModel: TargetSetupViewModel = hiltViewModel(), + cardType: String? = STATIC, +) { + cardData?.let { it -> + Card( + shape = ShapeUtil.getShape(shape = it.properties?.cardProperty?.shape), + elevation = it.properties?.cardProperty?.elevation?.dp ?: 0.dp, + backgroundColor = + it.properties?.cardProperty?.backgroundColor?.hexToComposeColor ?: Color.White, + border = + BorderStroke( + width = + ((cardData.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp, + brush = + getBorderStrokeBrushData( + cardData.properties?.cardProperty?.borderStrokeData + ), + ), + modifier = + modifier + .shadow( + shape = + ShapeUtil.getShape(shape = cardData.properties?.cardProperty?.shape), + elevation = cardData.properties?.cardProperty?.elevation?.dp ?: 16.dp, + ambientColor = hexToColor(cardData.properties?.cardProperty?.ambientColor), + spotColor = hexToColor(cardData.properties?.cardProperty?.spotColor), + ) + .clickableWithNoGesture( + onClick = { it.actionData?.let { action -> onClick(action) } } + ), + ) { + Column( + modifier = Modifier.fillMaxWidth().setPadding(it.properties?.cardProperty?.padding) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + NaviTextWidgetized( + textFieldData = cardData.title, + modifier = Modifier.setPadding(it.properties?.titleProperty?.padding), + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.weight(1f).padding(end = 20.dp).align(Alignment.Top) + ) { + NaviTextWidgetized( + textFieldData = + if (cardType == DYNAMIC) + cardData.subtitle?.copy( + text = viewModel.getProjectedGoalValue() + ) + else cardData.subtitle, + modifier = Modifier.setPadding(it.properties?.subtitleProperty?.padding), + ) + NaviTextWidgetized( + textFieldData = + if (cardType == DYNAMIC) viewModel.getDynamicText() + else cardData.bottomText, + modifier = + Modifier.setPadding(it.properties?.bottomTextProperty?.padding), + ) + } + Column( + modifier = Modifier.align(Alignment.Bottom), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.Bottom, + ) { + NaviImage( + imageFieldData = cardData.rightIcon, + modifier = + Modifier.width( + (cardData.rightIcon?.iconWidth + ?: com.navi.design.R.integer.integer_90) + .dp + ) + .height( + (cardData.rightIcon?.iconHeight + ?: com.navi.design.R.integer.integer_90) + .dp + ), + ) + } + } + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalCardPostTransitionComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalCardPostTransitionComposable.kt new file mode 100644 index 0000000000..95eae8c27d --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalCardPostTransitionComposable.kt @@ -0,0 +1,109 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.cards + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.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 androidx.hilt.navigation.compose.hiltViewModel +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenGoalCard +import com.navi.amc.fundbuy.viewmodel.TargetSetupViewModel +import com.navi.base.model.ActionData +import com.navi.design.R +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.utils.ShapeUtil +import com.navi.uitron.utils.getBorderStrokeBrushData +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.setPadding + +@Composable +fun GoalCardPostTransitionComposable( + modifier: Modifier = Modifier, + cardData: GoalBasedSipScreenGoalCard? = null, + onClick: (actionData: ActionData?) -> Unit = {}, + viewModel: TargetSetupViewModel = hiltViewModel(), +) { + cardData?.let { it -> + Card( + shape = ShapeUtil.getShape(shape = it.properties?.cardProperty?.shape), + elevation = it.properties?.cardProperty?.elevation?.dp ?: 0.dp, + backgroundColor = + it.properties?.cardProperty?.backgroundColor?.hexToComposeColor ?: Color.White, + border = + BorderStroke( + width = + ((cardData.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp, + brush = + getBorderStrokeBrushData( + cardData.properties?.cardProperty?.borderStrokeData + ), + ), + modifier = + modifier + .shadow( + shape = + ShapeUtil.getShape(shape = cardData.properties?.cardProperty?.shape), + elevation = cardData.properties?.cardProperty?.elevation?.dp ?: 16.dp, + ambientColor = hexToColor(cardData.properties?.cardProperty?.ambientColor), + spotColor = hexToColor(cardData.properties?.cardProperty?.spotColor), + ) + .clickableWithNoGesture( + onClick = { it.actionData?.let { action -> onClick(action) } } + ), + ) { + Column( + modifier = Modifier.fillMaxWidth().setPadding(it.properties?.cardProperty?.padding) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f).padding(end = 20.dp).align(Alignment.Top) + ) { + NaviTextWidgetized( + textFieldData = cardData.title, + modifier = Modifier.setPadding(it.properties?.titleProperty?.padding), + ) + NaviTextWidgetized( + textFieldData = + cardData.subtitle?.copy(text = viewModel.getProjectedGoalValue()), + modifier = Modifier.setPadding(it.properties?.subtitleProperty?.padding), + ) + } + Column(modifier = Modifier, horizontalAlignment = Alignment.End) { + NaviImage( + imageFieldData = cardData.rightIcon, + modifier = + Modifier.width( + (cardData.rightIcon?.iconWidth ?: R.integer.integer_74).dp + ) + .height( + (cardData.rightIcon?.iconHeight ?: R.integer.integer_48).dp + ), + ) + } + } + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalProgressCard.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalProgressCard.kt new file mode 100644 index 0000000000..0956f1f793 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalProgressCard.kt @@ -0,0 +1,146 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.cards + +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.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.navi.amc.compose.common.ui.clickableDebounce +import com.navi.amc.compose.feature.goalBasedSip.model.GoalProgressCard +import com.navi.base.model.ActionData +import com.navi.common.utils.toActionData +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.hexToColor +import com.navi.rr.utils.ext.clickable +import com.navi.uitron.utils.ShapeUtil.getShape +import com.navi.uitron.utils.getBorderStrokeBrushData +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.setPadding + +@Composable +fun GoalProgressCard( + modifier: Modifier = Modifier, + cardData: GoalProgressCard? = null, + onClick: (actionData: ActionData?) -> Unit = {}, +) { + cardData?.let { + Card( + shape = getShape(shape = cardData.properties?.cardProperty?.shape), + elevation = cardData.properties?.cardProperty?.elevation?.dp ?: 0.dp, + backgroundColor = + cardData.properties?.cardProperty?.backgroundColor?.hexToComposeColor + ?: Color.White, + border = + BorderStroke( + width = + ((cardData.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp, + brush = + getBorderStrokeBrushData( + cardData.properties?.cardProperty?.borderStrokeData + ), + ), + modifier = + Modifier.shadow( + shape = getShape(shape = cardData.properties?.cardProperty?.shape), + elevation = cardData.properties?.cardProperty?.elevation?.dp ?: 16.dp, + ambientColor = hexToColor(cardData.properties?.cardProperty?.ambientColor), + spotColor = hexToColor(cardData.properties?.cardProperty?.spotColor), + ) + .clickableDebounce( + onClick = { cardData.actionData?.let { action -> onClick(action) } } + ), + ) { + Column(modifier = Modifier.setPadding(cardData.properties?.cardProperty?.padding)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + NaviTextWidgetized( + textFieldData = cardData.title, + modifier = Modifier.setPadding(cardData.properties?.titleProperty?.padding), + ) + NaviImage( + imageFieldData = cardData.topRightIcon, + modifier = + Modifier.clickable { + onClick(cardData.topRightIcon?.cta?.toActionData()) + } + .width((cardData.topRightIcon?.iconWidth ?: 16).dp) + .height((cardData.topRightIcon?.iconHeight ?: 16).dp), + ) + } + + NaviTextWidgetized( + textFieldData = cardData.currentValue, + modifier = + Modifier.setPadding(cardData.properties?.currentValueProperty?.padding), + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + NaviTextWidgetized(textFieldData = cardData.goalAmount) + NaviTextWidgetized(cardData.sipText) + } + + Spacer( + modifier = + Modifier.height( + cardData.properties?.goalProgressTextProperty?.padding?.top?.dp ?: 8.dp + ) + ) + + LinearProgressIndicator( + progress = cardData.goalProgressData?.goalProgressRatio ?: 0.02f, + backgroundColor = hexToColor(cardData.goalProgressData?.bgColor), + color = hexToColor(cardData.goalProgressData?.progressColor), + modifier = Modifier.fillMaxWidth().height(6.dp).clip(RoundedCornerShape(1.dp)), + ) + + Spacer( + modifier = + Modifier.height( + cardData.properties?.goalProgressTextProperty?.padding?.bottom?.dp + ?: 16.dp + ) + ) + + repeat(cardData.items?.size ?: 0) { index -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + NaviTextWidgetized(textFieldData = cardData.items?.get(index)?.label) + Spacer(modifier = Modifier.width(12.dp)) + NaviTextWidgetized(textFieldData = cardData.items?.get(index)?.value) + } + } + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalSummaryCardComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalSummaryCardComposable.kt new file mode 100644 index 0000000000..8e7dfa2b72 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/GoalSummaryCardComposable.kt @@ -0,0 +1,180 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.cards + +import androidx.compose.foundation.BorderStroke +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.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.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.compose.feature.goalBasedSip.model.GoalSummaryCardData +import com.navi.amc.compose.feature.goalBasedSip.model.LabelItem +import com.navi.base.model.ActionData +import com.navi.common.R +import com.navi.common.utils.toActionData +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.getBorderStrokeBrushData +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.setBackground +import com.navi.uitron.utils.setBorderStroke +import com.navi.uitron.utils.setPadding + +@Composable +fun GoalSummaryCard( + data: GoalSummaryCardData? = null, + onClick: (actionData: ActionData?) -> Unit = {}, +) { + data?.let { cardData -> + Card( + shape = ShapeUtil.getShape(shape = cardData.properties?.cardProperty?.shape), + elevation = cardData.properties?.cardProperty?.elevation?.dp ?: R.integer.integer_0.dp, + backgroundColor = + cardData.properties?.cardProperty?.backgroundColor?.hexToComposeColor + ?: Color.White, + border = + BorderStroke( + width = + ((cardData.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp, + brush = + getBorderStrokeBrushData( + cardData.properties?.cardProperty?.borderStrokeData + ), + ), + modifier = + Modifier.shadow( + shape = + ShapeUtil.getShape(shape = cardData.properties?.cardProperty?.shape), + elevation = cardData.properties?.cardProperty?.elevation?.dp ?: 16.dp, + ambientColor = hexToColor(cardData.properties?.cardProperty?.ambientColor), + spotColor = hexToColor(cardData.properties?.cardProperty?.spotColor), + ) + .clickableWithNoGesture( + onClick = { cardData.actionData?.let { action -> onClick(action) } } + ), + ) { + Column { + Column( + modifier = + Modifier.setPadding( + cardData.properties?.cardProperty?.padding + ?: ComposePadding(start = 16, end = 16, top = 16, bottom = 16) + ) + ) { + cardData.items?.forEachIndexed { _, item -> + DetailsRow(item, onClick = onClick) + } + + cardData.buttonText?.let { + Box( + modifier = + Modifier.setBackground( + backgroundColor = + cardData.properties?.buttonProperty?.backgroundColor, + uiTronShape = cardData.properties?.buttonProperty?.shape, + brushData = + cardData.properties?.buttonProperty?.backGroundBrushData, + ) + .setBorderStroke( + data.properties?.buttonProperty?.borderStrokeData + ) + .setPadding(cardData.properties?.buttonProperty?.padding) + .clickableWithNoGesture { + cardData.buttonText.cta?.let { ctaData -> + onClick(ctaData.toActionData()) + } + } + ) { + NaviTextWidgetized(textFieldData = cardData.buttonText) + } + } + } + + cardData.bottomText?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = + Modifier.fillMaxWidth() + .background( + color = + hexToColor( + cardData.properties?.bottomTextProperty?.backgroundColor + ), + shape = + ShapeUtil.getShape( + (cardData.properties?.bottomTextProperty?.shape) + ), + ) + .setPadding( + cardData.properties?.bottomTextProperty?.padding + ?: ComposePadding(start = 16, end = 16, top = 8, bottom = 8) + ), + ) { + it.leftIcon?.let { + NaviImage( + imageFieldData = cardData.bottomText.leftIcon, + modifier = + Modifier.width( + (cardData.bottomText.leftIcon?.iconWidth + ?: com.navi.design.R.integer.integer_12) + .dp + ) + .height( + (cardData.bottomText.leftIcon?.iconHeight + ?: com.navi.design.R.integer.integer_12) + .dp + ), + ) + } + NaviTextWidgetized(textFieldData = cardData.bottomText.title) + } + } + } + } + } +} + +@Composable +fun DetailsRow(item: LabelItem? = null, onClick: (actionData: ActionData?) -> Unit) { + Row(horizontalArrangement = Arrangement.Start, modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.weight(1f)) { + NaviTextWidgetized(textFieldData = item?.label) + NaviTextWidgetized(textFieldData = item?.subtitle) + } + + Spacer(modifier = Modifier.padding(8.dp)) + + Column(horizontalAlignment = Alignment.End) { + NaviTextWidgetized( + textFieldData = item?.value, + modifier = + Modifier.clickableWithNoGesture { + item?.value?.cta?.let { ctaData -> onClick(ctaData.toActionData()) } + }, + ) + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/MonthlyInvestmentGoalCardComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/MonthlyInvestmentGoalCardComposable.kt index 2399cdddcd..96b9f4b51b 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/MonthlyInvestmentGoalCardComposable.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/MonthlyInvestmentGoalCardComposable.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight 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.shape.RoundedCornerShape import androidx.compose.material.Card @@ -31,6 +30,7 @@ import androidx.compose.ui.graphics.DefaultShadowColor import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.navi.amc.fundbuy.models.cards.InvestmentGoalCardData +import com.navi.amc.fundbuy.models.widgets.TrackerContent import com.navi.base.model.ActionData import com.navi.design.utils.clickableWithNoGesture import com.navi.naviwidgets.extensions.NaviImage @@ -69,6 +69,9 @@ fun MonthlyInvestmentGoalCardComposable( it.properties?.cardProperty?.spotColor?.hexToComposeColor ?: DefaultShadowColor, shape = ShapeUtil.getShape(shape = it.properties?.cardProperty?.shape), + ) + .clickableWithNoGesture( + onClick = { it.actionData?.let { action -> onClick(action) } } ), ) { Column { @@ -133,14 +136,25 @@ fun MonthlyInvestmentGoalCardComposable( Modifier.setPadding(it.properties?.goalAmountProperty?.padding), ) } - CustomLinearProgressIndicator( - progress = it.goalProgressRatio ?: 0.2f, - progressColor = - it.goalSliderData?.minTrackTintColor?.hexToComposeColor ?: Color.Blue, - defaultColor = - it.goalSliderData?.maxTrackTintColor?.hexToComposeColor ?: Color.Gray, + it.goalProgressRatio?.let { progressRatio -> + CustomLinearProgressIndicator( + progress = it.goalProgressRatio ?: 0.2f, + progressColor = + it.goalSliderData?.minTrackTintColor?.hexToComposeColor + ?: Color.Blue, + defaultColor = + it.goalSliderData?.maxTrackTintColor?.hexToComposeColor + ?: Color.Gray, + modifier = Modifier.fillMaxWidth(), + ) + } + + Row( modifier = Modifier.fillMaxWidth(), - ) + horizontalArrangement = Arrangement.SpaceBetween, + ) { + it.items?.forEach { trackerItem -> MonthlyTrackerItem(item = trackerItem) } + } } Column { @@ -277,3 +291,16 @@ fun CustomLinearProgressIndicator( ) } } + +@Composable +fun MonthlyTrackerItem(item: TrackerContent) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + NaviImage( + imageFieldData = item.icon, + modifier = + Modifier.width((item.icon?.iconWidth ?: com.navi.common.R.integer.integer_16).dp) + .height((item.icon?.iconHeight ?: com.navi.common.R.integer.integer_16).dp), + ) + NaviTextWidgetized(textFieldData = item.title) + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/PaymentCardComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/PaymentCardComposable.kt index d07b123902..271a848278 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/PaymentCardComposable.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/PaymentCardComposable.kt @@ -16,6 +16,7 @@ package com.navi.amc.fundbuy.composables.cards import androidx.compose.foundation.BorderStroke 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 @@ -35,11 +36,13 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.navi.amc.fundbuy.models.cards.PaymentCard +import com.navi.amc.utils.Constant.MODIFY_SIP_CARD 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.common.utils.toActionData import com.navi.design.utils.clickableWithNoGesture import com.navi.naviwidgets.extensions.NaviImage import com.navi.naviwidgets.extensions.NaviTextWidgetized @@ -104,13 +107,17 @@ fun PaymentCardComposable( PaymentCardHeader(paymentCard = paymentCard) } - PaymentDetailsComposable(paymentCard = paymentCard, cardType = cardType) + PaymentDetailsComposable(paymentCard = paymentCard, cardType = cardType, onClick) } } } @Composable -fun PaymentDetailsComposable(paymentCard: PaymentCard, cardType: String?) { +fun PaymentDetailsComposable( + paymentCard: PaymentCard, + cardType: String?, + onClick: (actionData: ActionData?) -> Unit, +) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start, @@ -159,7 +166,15 @@ fun PaymentDetailsComposable(paymentCard: PaymentCard, cardType: String?) { NaviTextWidgetized( textFieldData = paymentCard.paymentAmount, modifier = - Modifier.height((paymentCard.paymentAmount?.lineSpacing ?: 16).dp), + Modifier.then( + if (cardType == MODIFY_SIP_CARD) { + Modifier.wrapContentHeight() + } else { + Modifier.height( + (paymentCard.paymentAmount.lineSpacing ?: 16).dp + ) + } + ), ) } } @@ -170,21 +185,41 @@ fun PaymentDetailsComposable(paymentCard: PaymentCard, cardType: String?) { 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, - ), - ) + paymentCard.buttonText?.let { + NaviTextWidgetized( + textFieldData = paymentCard.buttonText, + modifier = + Modifier.setBackground( + backgroundColor = + paymentCard.properties?.buttonProperty?.backgroundColor, + uiTronShape = paymentCard.properties?.buttonProperty?.shape, + brushData = 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, + ), + ) + } + + paymentCard.rightIcon?.let { + Box { + NaviImage( + imageFieldData = it, + modifier = + Modifier.align(Alignment.TopEnd) + .width((it.iconWidth ?: 16).dp) + .height((it.iconHeight ?: 16).dp) + .clickableWithNoGesture( + onClick = { onClick(paymentCard.rightIcon.cta?.toActionData()) } + ), + ) + } + } } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/SetupGoalBasedSipCardComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/SetupGoalBasedSipCardComposable.kt new file mode 100644 index 0000000000..8056b606bc --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/SetupGoalBasedSipCardComposable.kt @@ -0,0 +1,184 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.cards + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.width +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.unit.Dp +import androidx.compose.ui.unit.dp +import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetCard +import com.navi.amc.utils.Constant.DEFAULT_COLUMN_WEIGHT +import com.navi.base.model.ActionData +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.utils.ShapeUtil +import com.navi.uitron.utils.getBorderStrokeBrushData +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.setPadding + +@Composable +fun SetupGoalBasedSipCardComposable( + cardData: SetupMonthlyTargetCard? = null, + onClick: (actionData: ActionData?) -> Unit = {}, + cardWidth: Dp, +) { + cardData?.let { it -> + Card( + shape = ShapeUtil.run { getShape(shape = it.properties?.cardProperty?.shape) }, + elevation = it.properties?.cardProperty?.elevation?.dp ?: 0.dp, + backgroundColor = + it.properties?.cardProperty?.backgroundColor?.hexToComposeColor ?: Color.White, + border = + BorderStroke( + width = ((it.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp, + brush = getBorderStrokeBrushData(it.properties?.cardProperty?.borderStrokeData), + ), + modifier = + Modifier.width(cardWidth) + .shadow( + elevation = (it.properties?.cardProperty?.elevation ?: 0).dp, + ambientColor = hexToColor(it.properties?.cardProperty?.ambientColor), + spotColor = hexToColor(it.properties?.cardProperty?.spotColor), + shape = ShapeUtil.getShape(shape = it.properties?.cardProperty?.shape), + ) + .clickableWithNoGesture( + onClick = { it.actionData?.let { action -> onClick(action) } } + ), + ) { + Box( + modifier = + Modifier.padding(end = it.properties?.cardProperty?.padding?.end?.dp ?: 0.dp), + contentAlignment = Alignment.TopEnd, + ) { + NaviTextWidgetized( + textFieldData = it.tagData, + modifier = + Modifier.background( + color = + it.properties?.tagProperty?.backgroundColor?.hexToComposeColor + ?: Color.Transparent, + shape = ShapeUtil.getShape(it.properties?.tagProperty?.shape), + ) + .border( + BorderStroke( + width = + it.properties?.tagProperty?.borderStrokeData?.width?.dp + ?: 0.dp, + brush = + getBorderStrokeBrushData( + it.properties?.tagProperty?.borderStrokeData + ), + ), + shape = ShapeUtil.getShape(it.properties?.tagProperty?.shape), + ) + .setPadding(it.properties?.tagProperty?.padding), + ) + } + Column(modifier = Modifier.fillMaxWidth()) { + Column(Modifier.fillMaxWidth().setPadding(it.properties?.cardProperty?.padding)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column( + modifier = + Modifier.weight( + it.properties?.cardProperty?.columnWeight + ?: DEFAULT_COLUMN_WEIGHT + ) + ) { + NaviTextWidgetized( + textFieldData = it.title, + modifier = + Modifier.setPadding(it.properties?.titleProperty?.padding), + ) + NaviTextWidgetized( + textFieldData = it.subtitle, + modifier = + Modifier.setPadding(it.properties?.subtitleProperty?.padding), + ) + + Spacer(modifier = Modifier.weight(1f)) + + NaviTextWidgetized( + textFieldData = it.buttonText, + modifier = + Modifier.background( + color = + it.properties + ?.buttonProperty + ?.backgroundColor + ?.hexToComposeColor ?: Color.Transparent, + shape = + ShapeUtil.getShape( + it.properties?.buttonProperty?.shape + ), + ) + .border( + BorderStroke( + width = + it.properties + ?.buttonProperty + ?.borderStrokeData + ?.width + ?.dp ?: 0.dp, + brush = + getBorderStrokeBrushData( + it.properties + ?.buttonProperty + ?.borderStrokeData + ), + ), + shape = + ShapeUtil.getShape( + it.properties?.buttonProperty?.shape + ), + ) + .setPadding(it.properties?.buttonProperty?.padding), + ) + } + + Column( + modifier = + Modifier.weight( + 1 - + (it.properties?.cardProperty?.columnWeight + ?: DEFAULT_COLUMN_WEIGHT) + ) + .align(Alignment.Bottom), + horizontalAlignment = Alignment.End, + ) { + NaviImage( + imageFieldData = it.icon, + modifier = + Modifier.setPadding(it.properties?.cardIconProperty?.padding), + ) + } + } + } + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/TransactionHistoryCard.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/TransactionHistoryCard.kt new file mode 100644 index 0000000000..2c5bb89810 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/cards/TransactionHistoryCard.kt @@ -0,0 +1,169 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.cards + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.width +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.unit.dp +import com.navi.amc.compose.feature.goalBasedSip.model.LabelItem +import com.navi.amc.compose.feature.goalBasedSip.model.TransactionHistoryCard +import com.navi.base.model.ActionData +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.getBorderStrokeBrushData +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.setPadding + +@Composable +fun TransactionHistoryCard( + modifier: Modifier = Modifier, + cardData: TransactionHistoryCard? = null, + onClick: (actionData: ActionData?) -> Unit = {}, +) { + cardData?.let { data -> + Card( + shape = ShapeUtil.getShape(shape = data.properties?.cardProperty?.shape), + elevation = data.properties?.cardProperty?.elevation?.dp ?: 0.dp, + backgroundColor = + data.properties?.cardProperty?.backgroundColor?.hexToComposeColor ?: Color.White, + border = + BorderStroke( + width = ((data.properties?.cardProperty?.borderStrokeData?.width) ?: 0.0f).dp, + brush = + getBorderStrokeBrushData(data.properties?.cardProperty?.borderStrokeData), + ), + modifier = + modifier + .shadow( + shape = ShapeUtil.getShape(shape = data.properties?.cardProperty?.shape), + elevation = data.properties?.cardProperty?.elevation?.dp ?: 16.dp, + ambientColor = hexToColor(data.properties?.cardProperty?.ambientColor), + spotColor = hexToColor(data.properties?.cardProperty?.spotColor), + ) + .clickableWithNoGesture( + onClick = { data.actionData?.let { action -> onClick(action) } } + ), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = + Modifier.fillMaxWidth() + .background( + color = + data.properties + ?.cardProperty + ?.backgroundColor + ?.hexToComposeColor ?: Color.Gray + ) + .setPadding(data.properties?.cardProperty?.padding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + NaviTextWidgetized(textFieldData = data.leftTitle) + NaviTextWidgetized(textFieldData = data.rightTitle) + } + Column( + modifier = + Modifier.background(color = Color.White) + .setPadding(ComposePadding(start = 12, end = 12)) + ) { + data.items?.forEach { item -> + Spacer(modifier = Modifier.height(24.dp)) + RowItem(item) + } + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + } +} + +@Composable +fun RowItem(item: LabelItem? = null) { + item?.let { data -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + item.leftIcon?.let { + NaviImage( + imageFieldData = it, + modifier = + Modifier.width( + (it.iconWidth ?: com.navi.common.R.integer.integer_16).dp + ) + .height((it.iconHeight ?: com.navi.common.R.integer.integer_16).dp), + ) + } + + NaviTextWidgetized( + textFieldData = data.label, + modifier = Modifier.setPadding(data.properties?.labelProperty?.padding), + ) + + item.tagData?.let { tag -> + NaviTextWidgetized( + textFieldData = tag, + modifier = + Modifier.background( + color = + data.properties + ?.tagProperty + ?.backgroundColor + ?.hexToComposeColor ?: Color.Transparent, + shape = ShapeUtil.getShape(data.properties?.tagProperty?.shape), + ) + .border( + BorderStroke( + width = + data.properties + ?.tagProperty + ?.borderStrokeData + ?.width + ?.dp ?: 0.dp, + brush = + getBorderStrokeBrushData( + data.properties?.tagProperty?.borderStrokeData + ), + ), + shape = ShapeUtil.getShape(data.properties?.tagProperty?.shape), + ) + .setPadding(data.properties?.tagProperty?.padding), + ) + } + } + + Spacer(modifier = Modifier.width(8.dp)) + + NaviTextWidgetized( + textFieldData = data.value, + modifier = Modifier.setPadding(data.properties?.valueProperty?.padding), + ) + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/widgets/SetupGoalBasedWidgetComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/widgets/SetupGoalBasedWidgetComposable.kt new file mode 100644 index 0000000000..81e406273a --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/widgets/SetupGoalBasedWidgetComposable.kt @@ -0,0 +1,45 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.widgets + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.navi.amc.common.composables.AmcCardListComposable +import com.navi.amc.fundbuy.models.widgets.SetupMonthlyTargetWidget +import com.navi.amc.utils.Constant.DEFAULT_CARD_WIDTH_FACTOR +import com.navi.amc.utils.Constant.FREE_SCROLL +import com.navi.amc.utils.Constant.GOAL_BASED_SIP_SETUP_CARD +import com.navi.base.model.ActionData +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.uitron.utils.setPadding + +@Composable +fun SetupGoalBasedSipWidgetComposable( + widget: SetupMonthlyTargetWidget? = null, + onClick: (actionData: ActionData?) -> Unit = {}, +) { + widget?.widgetData?.let { widgetData -> + Column(modifier = Modifier.fillMaxWidth()) { + NaviTextWidgetized( + textFieldData = widgetData.header?.title, + modifier = Modifier.setPadding(widgetData.header?.property?.padding), + ) + AmcCardListComposable( + cardType = GOAL_BASED_SIP_SETUP_CARD, + cardWidthFactor = + widgetData.extraData?.extraProperties?.cardWidthFactor + ?: DEFAULT_CARD_WIDTH_FACTOR, + cardList = widgetData.items, + onClick = onClick, + scrollType = FREE_SCROLL, + ) + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt index 7cc992f1ea..9fb05ffb72 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt @@ -473,7 +473,10 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { arguments?.putString(ISIN, arguments?.getString(FUND_ID)) } val isin = arguments?.getString(ISIN) - isin?.let { viewModel.getFundScreenData(isin = isin, screenName = screenName) } + val source = arguments?.getString(Constant.SOURCE) + isin?.let { + viewModel.getFundScreenData(isin = isin, source = source, screenName = screenName) + } } private fun pillClickAction(key: String?, errorRefresh: Boolean? = null) { diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalBasedSipAmountFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalBasedSipAmountFragment.kt new file mode 100644 index 0000000000..192b3cf6e0 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalBasedSipAmountFragment.kt @@ -0,0 +1,397 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.fragments + +import android.content.Context +import android.content.res.Resources +import android.os.Bundle +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewStub +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.viewModels +import com.navi.amc.R +import com.navi.amc.common.fragment.AmcBaseFragment +import com.navi.amc.common.fragment.AmcDynamicBottomSheet +import com.navi.amc.common.listener.FooterInteractionListener +import com.navi.amc.compose.common.utils.KeyboardVisibilityObserver +import com.navi.amc.compose.feature.goalBasedSip.model.GoalBasedSipScreenData +import com.navi.amc.compose.feature.goalBasedSip.ui.GoalBasedSipAmountScreen +import com.navi.amc.compose.feature.goalBasedSip.viewmodel.GoalBasedSipSetupVM +import com.navi.amc.databinding.GoalBasedSipAmountScreenLayoutBinding +import com.navi.amc.fundbuy.models.InstallmentDateTypes +import com.navi.amc.fundbuy.viewmodel.TargetSetupViewModel +import com.navi.amc.utils.AmcAnalytics.AMC_GOAL_BASED_SIP_DATE_SELECTION +import com.navi.amc.utils.Constant.AMC_PAYMENT_BOTTOMSHEET +import com.navi.amc.utils.Constant.AMOUNT +import com.navi.amc.utils.Constant.CALENDAR_DATE +import com.navi.amc.utils.Constant.CREATE_CART +import com.navi.amc.utils.Constant.DATA +import com.navi.amc.utils.Constant.DISMISS +import com.navi.amc.utils.Constant.FIRST_INSTALLMENT_DATE +import com.navi.amc.utils.Constant.FREQUENCY +import com.navi.amc.utils.Constant.GOAL_NAME +import com.navi.amc.utils.Constant.GOAL_REFERENCE_ID +import com.navi.amc.utils.Constant.MIG_AMOUNT +import com.navi.amc.utils.Constant.MONTHLY +import com.navi.amc.utils.Constant.SHOW_BOTTOMSHEET +import com.navi.amc.utils.Constant.SIP_DATE +import com.navi.amc.utils.Constant.SOURCE +import com.navi.amc.utils.Constant.WEEKLY +import com.navi.amc.utils.SubPageStatusType.GOAL_BASED_SIP_AMOUNT_SCREEN +import com.navi.amc.utils.createCartRequest +import com.navi.amc.utils.getBottomSheet +import com.navi.base.model.ActionData +import com.navi.base.model.LineItem +import com.navi.base.model.NaviClickAction +import com.navi.base.model.NaviWidgetClickWithActionData +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.common.constants.EMPTY +import com.navi.common.listeners.FragmentInterchangeListener +import com.navi.common.listeners.HeaderInteractionListener +import com.navi.common.ui.activity.BaseActivity +import com.navi.common.utils.SPACE +import com.navi.common.utils.getDateSuffix +import com.navi.naviwidgets.base.InputWidgetModel +import com.navi.naviwidgets.callbacks.WidgetCallback +import dagger.hilt.android.AndroidEntryPoint +import kotlin.getValue + +@AndroidEntryPoint +class GoalBasedSipAmountFragment : AmcBaseFragment(), WidgetCallback, FooterInteractionListener { + + private lateinit var binding: GoalBasedSipAmountScreenLayoutBinding + private val viewModel by viewModels() + private val targetSetupVM by viewModels() + private var installmentDates: InstallmentDateTypes? = null + private var monthlySelectedDateId: Int? = null + private var monthlySipDateId: String? = null + private var isNextCtaEnabled = true + + override val screenName: String + get() = GOAL_BASED_SIP_AMOUNT_SCREEN + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun setContainerView(viewStub: ViewStub) { + viewStub.layoutResource = R.layout.goal_based_sip_amount_screen_layout + binding = DataBindingUtil.getBinding(viewStub.inflate())!! + initError(viewModel) + initObservers() + fetchData() + } + + private fun fetchData() { + showShimmer() + val goalReferenceId = arguments?.getString(GOAL_REFERENCE_ID).orEmpty() + viewModel.fetchSipAmountScreenData( + screenName = screenName, + goalReferenceId = goalReferenceId, + ) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + headerInteractionListener = context as? HeaderInteractionListener + fragmentInterchangeListener = context as? FragmentInterchangeListener + } + + private fun initObservers() { + viewModel.sipAmountScreenData.observe(viewLifecycleOwner) { data -> + hideShimmer() + data?.header?.let { + setHeaderProperties(it) + hideDivider() + } + viewModel.createCartResponse.observe(viewLifecycleOwner) { + it?.let { + updateLoadingState(false) + val bundle = Bundle().apply { putString(GOAL_REFERENCE_ID, it.goalId) } + handleOnClick(action = it.nextCta, bundle = bundle) + } + } + viewModel.errorResponse.observe(viewLifecycleOwner) { + updateLoadingState(false) + hideShimmer() + } + data?.content?.goalCard?.subtitle?.cta?.parameters?.forEach { parameter -> + val key = parameter.key + val value = parameter.value + targetSetupVM.updateValue(key = key, value = value) + } + setupSIPInstallmentDateData(data = data) + binding.composeView.apply { + setContent { + var keyboardVisible by remember { mutableStateOf(false) } + + KeyboardVisibilityObserver { isVisible -> + keyboardVisible = isVisible + + val shiftInPx = 50 * Resources.getSystem().displayMetrics.density + binding.ll + .animate() + .translationY(if (isVisible) -shiftInPx else 0f) + .setDuration(200) + .setInterpolator(AccelerateDecelerateInterpolator()) + .start() + } + GoalBasedSipAmountScreen( + content = data?.content, + viewModel = targetSetupVM, + onClick = ::handleOnClick, + screenName = screenName, + ) + } + } + data?.footer?.let { footer -> + binding.footerView.setProperties(footer, this) + binding.footerView.disableShadow() + binding.footerView.removeElevation() + } + + targetSetupVM.areAllFieldsValid.observe(viewLifecycleOwner) { + it?.let { + if (it) { + binding.footerView.toggleNextCtaColor(true) + isNextCtaEnabled = true + } else { + binding.footerView.toggleNextCtaColor(false) + isNextCtaEnabled = false + } + } + } + } + } + + private fun showShimmer() { + binding.shimmerLayout.startShimmer() + binding.shimmerLayout.visibility = VISIBLE + } + + private fun hideShimmer() { + binding.shimmerLayout.stopShimmer() + binding.shimmerLayout.visibility = GONE + } + + private fun setupSIPInstallmentDateData(data: GoalBasedSipScreenData? = null) { + data?.content?.installmentDates?.let { installmentDates -> + this.installmentDates = installmentDates + } + data?.content?.sipInstallmentDate.let { date -> + binding.date.visibility = VISIBLE + binding.date.toggleRightIconState(true) + binding.date.setDefaultShowCrossIcon(false) + binding.date.updateData(date as InputWidgetModel, 0, this) + binding.date.setLeftIcon(date.widgetData?.leftIconCode) + binding.date.widgetBinding.plainTextInput.id = R.id.date + if ( + viewModel.sipType == MONTHLY && + viewModel.monthlyDefaultSipDateText.isNotNullAndNotEmpty() + ) { + binding.date.setBindingText(viewModel.monthlyDefaultSipDateText.toString()) + } else { + binding.date.setSavedTextVariations(id = viewModel.sipType) + viewModel.sipDateId = binding.date.setDateId(viewModel.sipType) + monthlySipDateId = viewModel.sipDateId + setSipFormattedDate(viewModel.sipDateId) + } + } + } + + override fun onClick(naviClickAction: NaviClickAction, widgetId: String?) { + when (naviClickAction) { + is NaviWidgetClickWithActionData -> { + sendEvent( + eventName = AMC_GOAL_BASED_SIP_DATE_SELECTION, + extraAttributes = + hashMapOf( + SOURCE to screenName, + GOAL_REFERENCE_ID to arguments?.getString(GOAL_REFERENCE_ID).orEmpty(), + AMOUNT to targetSetupVM.getMapValue(MIG_AMOUNT), + FREQUENCY to viewModel.sipType, + SIP_DATE to viewModel.formattedSipDate.orEmpty(), + GOAL_NAME to arguments?.getString(GOAL_NAME).orEmpty(), + ), + ) + when (viewModel.sipType) { + MONTHLY -> { + val firstInstallmentMap = installmentDateMapMaker(MONTHLY) + val bundle = + Bundle().apply { + putString( + DATA, + naviClickAction.actionData?.parameters?.get(2)?.value, + ) + firstInstallmentMap?.let { + putParcelableArrayList( + FIRST_INSTALLMENT_DATE, + it as ArrayList, + ) + } + } + val bottomSheet = + AmcDynamicBottomSheet.newInstance( + bundle, + subtitleListener = null, + action = { action -> + val selectedDate = + action.parameters + ?.firstOrNull { it.key == CALENDAR_DATE } + ?.value + selectedDate?.toInt()?.let { intDate -> + monthlySelectedDateId = intDate + binding.date.apply { + setText( + selectedDate + + getDateSuffix(intDate) + + SPACE + + resources.getString(R.string.of_every_month) + ) + } + viewModel.sipDateId = selectedDate + setSipFormattedDate(selectedDate) + monthlySipDateId = viewModel.sipDateId + viewModel.monthlyDefaultSipDateText = + selectedDate + + getDateSuffix(intDate) + + SPACE + + resources.getString(R.string.of_every_month) + } + }, + prevSelectedDate = monthlySelectedDateId, + ) + safelyShowBottomSheet(bottomSheet, AmcDynamicBottomSheet.TAG) + } + else -> { + handleOnClick(naviClickAction.actionData) + } + } + } + } + } + + fun setSipFormattedDate(date: String?) { + val sipDate = + installmentDates?.monthly?.firstInstallmentDate?.firstOrNull { it.id == date }?.sipDate + viewModel.formattedSipDate = sipDate + } + + private fun installmentDateMapMaker(sipType: String): List? { + val installmentDatesMap = + when (sipType) { + WEEKLY -> installmentDates?.weekly + MONTHLY -> installmentDates?.monthly + else -> null + } + val installmentDatesList = mutableListOf() + installmentDatesMap?.firstInstallmentDate?.let { + for (i in it.indices) { + installmentDatesList.add(LineItem(key = it[i].id, value = it[i].formattedSipDate)) + } + } + return installmentDatesList + } + + private fun updateLoadingState(isLoading: Boolean) { + binding.footerView.updateButtonLoaderState(isLoading) + if (isLoading) { + (activity as? BaseActivity)?.blockInteractability() + } else { + (activity as? BaseActivity)?.unblockInteractability() + } + } + + private fun handleOnClick(action: ActionData?, bundle: Bundle? = Bundle()) { + sendEvent( + action?.metaData?.clickedData, + extraAttributes = + hashMapOf( + SOURCE to screenName, + GOAL_REFERENCE_ID to arguments?.getString(GOAL_REFERENCE_ID).orEmpty(), + AMOUNT to targetSetupVM.getMapValue(MIG_AMOUNT), + FREQUENCY to viewModel.sipType, + SIP_DATE to viewModel.formattedSipDate.orEmpty(), + GOAL_NAME to arguments?.getString(GOAL_NAME).orEmpty(), + ), + ) + + val updatedArgs = + arguments?.let { bundle -> + bundle.keySet().associateWith { bundle.getString(it).orEmpty() }.toMutableMap() + } ?: mutableMapOf() + + action?.parameters?.forEach { parameter -> + updatedArgs[parameter.key] = parameter.value.orEmpty() + arguments?.putString(parameter.key, parameter.value.orEmpty()) + } + updatedArgs += + mapOf( + AMOUNT to targetSetupVM.getMapValue(MIG_AMOUNT), + FREQUENCY to viewModel.sipType, + SIP_DATE to viewModel.formattedSipDate.orEmpty(), + ) + + val updatedBundle = + Bundle().apply { updatedArgs.forEach { (key, value) -> putString(key, value) } } + + if (action?.type == DISMISS) return + val url = action?.url + if (url == SHOW_BOTTOMSHEET) { + val data = action.parameters?.getOrNull(0)?.value + val key = action.parameters?.getOrNull(0)?.key + val bundle = Bundle().apply { putString(DATA, data) } + key?.let { key -> + getBottomSheet( + key, + bundle, + genericListener = { action -> + if (key == AMC_PAYMENT_BOTTOMSHEET) { + viewModel.setPaymentMode(action) + } else { + handleOnClick(action) + } + }, + ) + ?.let { safelyShowBottomSheet(it, key) } + } + } else if (url == CREATE_CART) { + updateLoadingState(true) + viewModel.createCart( + source = updatedArgs[SOURCE] ?: EMPTY, + screenName = screenName, + cartRequest = createCartRequest(updatedBundle), + ) + } else { + fragmentInterchangeListener?.navigateToNextScreen( + actionData = action, + bundle = updatedBundle, + ) + } + } + + override fun onFooterBackPress(actionData: ActionData?) { + handleOnClick(actionData) + } + + override fun onFooterNextPress(actionData: ActionData?, skipValidation: Boolean?) { + if (isNextCtaEnabled) handleOnClick(actionData) + } + + companion object { + fun newInstance(bundle: Bundle? = null): GoalBasedSipAmountFragment { + return GoalBasedSipAmountFragment().apply { arguments = bundle } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalDetailsFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalDetailsFragment.kt new file mode 100644 index 0000000000..45dd070757 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalDetailsFragment.kt @@ -0,0 +1,204 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.fragments + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewStub +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.viewModels +import com.navi.amc.R +import com.navi.amc.common.fragment.AmcBaseFragment +import com.navi.amc.common.taskProcessor.AmcTaskManager +import com.navi.amc.compose.feature.goalBasedSip.ui.GoalDetailsScreenContent +import com.navi.amc.compose.feature.goalBasedSip.viewmodel.GoalBasedSipSetupVM +import com.navi.amc.databinding.GoalDetailsScreenLayoutBinding +import com.navi.amc.portfolio.viewmodels.SipModificationVM +import com.navi.amc.utils.Constant +import com.navi.amc.utils.Constant.AMC_PAYMENT_BOTTOMSHEET +import com.navi.amc.utils.Constant.DATA +import com.navi.amc.utils.Constant.DELETED +import com.navi.amc.utils.Constant.DISMISS +import com.navi.amc.utils.Constant.GOAL_REFERENCE_ID +import com.navi.amc.utils.Constant.MODIFY_BOTTOM_SHEET +import com.navi.amc.utils.Constant.SHOW_BOTTOMSHEET +import com.navi.amc.utils.SubPageStatusType.GOAL_DETAILS_SCREEN +import com.navi.amc.utils.getBottomSheet +import com.navi.amc.utils.shouldCallUpdateSip +import com.navi.base.model.ActionData +import com.navi.base.utils.orFalse +import com.navi.common.listeners.FragmentInterchangeListener +import com.navi.common.listeners.HeaderInteractionListener +import com.navi.common.model.ModuleNameV2 +import com.navi.common.network.models.GenericErrorResponse +import com.navi.common.ui.fragment.HorizontalActionErrorFragment +import com.navi.common.ui.fragment.SingleSelectionBottomSheet +import com.navi.common.utils.observeNonNull +import com.navi.common.utils.toActionData +import com.navi.naviwidgets.models.SingleSelectionBottomSheetData +import dagger.hilt.android.AndroidEntryPoint +import kotlin.collections.get + +@AndroidEntryPoint +class GoalDetailsFragment : AmcBaseFragment() { + + private lateinit var binding: GoalDetailsScreenLayoutBinding + private val viewModel by viewModels() + private var bottomSheetData: GenericErrorResponse? = null + private val sipModificationVM by viewModels() + + override val screenName: String + get() = GOAL_DETAILS_SCREEN + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun setContainerView(viewStub: ViewStub) { + viewStub.layoutResource = R.layout.goal_details_screen_layout + binding = DataBindingUtil.getBinding(viewStub.inflate())!! + initError(viewModel) + initObservers() + fetchData() + } + + private fun fetchData(source: String? = null) { + showShimmer() + binding.composeView.visibility = GONE + val goalReferenceId = arguments?.getString(GOAL_REFERENCE_ID).orEmpty() + viewModel.getGoalDetailsScreenData( + screenName = screenName, + goalReferenceId = goalReferenceId, + source = source.orEmpty(), + ) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + headerInteractionListener = context as? HeaderInteractionListener + fragmentInterchangeListener = context as? FragmentInterchangeListener + } + + private fun initObservers() { + viewModel.goalDetailsScreenData.observe(viewLifecycleOwner) { data -> + hideShimmer() + binding.composeView.visibility = VISIBLE + data?.header?.let { + setHeaderProperties(it) + hideDivider() + } + binding.composeView.apply { + setContent { + GoalDetailsScreenContent(content = data?.content, onClick = ::handleOnClick) + } + } + } + sipModificationVM.sipUpdateData.observeNonNull(viewLifecycleOwner) { + it?.let { + if (it.actionPerformed?.equals(DELETED).orFalse()) { + AmcTaskManager.requestParams[Constant.SOURCE] = DELETED + } + AmcTaskManager.onPrefetchTaskRequired( + AmcTaskManager.SIP_PREFETCH_TASK, + buyFlowVM.provideLatestGreenScreen(), + ) + fetchData(it.actionPerformed.orEmpty()) + } + } + + viewModel.errorResponse.observe(viewLifecycleOwner) { hideShimmer() } + } + + private fun showShimmer() { + binding.shimmerLayout.startShimmer() + binding.shimmerLayout.visibility = VISIBLE + } + + private fun hideShimmer() { + binding.shimmerLayout.stopShimmer() + binding.shimmerLayout.visibility = GONE + } + + private fun handleOnClick(action: ActionData?) { + sendEvent(action?.metaData?.clickedData) + if (action?.type == DISMISS || action?.url == DISMISS) return + val url = action?.url + if (url == SHOW_BOTTOMSHEET) { + val data = action.parameters?.getOrNull(0)?.value + val key = action.parameters?.getOrNull(0)?.key + val bundle = Bundle().apply { putString(DATA, data) } + key?.let { key -> + getBottomSheet( + key, + bundle, + genericListener = { action -> + if (key == AMC_PAYMENT_BOTTOMSHEET) { + viewModel.setPaymentMode(action) + } else { + handleOnClick(action) + } + }, + ) + ?.let { safelyShowBottomSheet(it, key) } + } + } else if (url == MODIFY_BOTTOM_SHEET) { + openBottomSheet(viewModel.goalDetailsScreenData.value?.content?.modifyBottomSheetData) + } else if ( + viewModel.goalDetailsScreenData.value?.content?.genericBottomSheets?.get(url) != null + ) { + bottomSheetData = + viewModel.goalDetailsScreenData.value?.content?.genericBottomSheets?.get(url) + bottomSheetData?.let { + val fragment = + HorizontalActionErrorFragment.getInstance( + error = it, + action = primaryClickListener, + cancelable = true, + sourceScreenName = screenName, + secondaryAction = secondaryClickListener, + moduleName = ModuleNameV2.AMC.name, + ) + safelyShowBottomSheet(fragment, HorizontalActionErrorFragment.TAG) + } + } else if (shouldCallUpdateSip(url)) { + sipModificationVM.updateSip( + viewModel.goalDetailsScreenData.value?.extraData?.sipReferenceId.orEmpty(), + action?.url.orEmpty(), + ) + } else { + fragmentInterchangeListener?.navigateToNextScreen(actionData = action) + } + } + + private fun openBottomSheet(data: SingleSelectionBottomSheetData?) { + val bottomSheet = SingleSelectionBottomSheet.getInstance(data) + safelyShowBottomSheet(bottomSheet, SingleSelectionBottomSheet.SINGLE_SELECTION_BOTTOM_SHEET) + bottomSheet.selectedItem.observeNonNull(this) { item -> + handleOnClick(ActionData(url = item.id)) + } + } + + private val primaryClickListener: View.OnClickListener = + View.OnClickListener { + handleOnClick(bottomSheetData?.actions?.firstOrNull()?.cta?.toActionData()) + } + + private val secondaryClickListener: View.OnClickListener = + View.OnClickListener { + handleOnClick(bottomSheetData?.actions?.getOrNull(1)?.cta?.toActionData()) + } + + companion object { + fun newInstance(bundle: Bundle? = null): GoalDetailsFragment { + return GoalDetailsFragment().apply { arguments = bundle } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalSummaryFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalSummaryFragment.kt new file mode 100644 index 0000000000..b2800ad382 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/GoalSummaryFragment.kt @@ -0,0 +1,314 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.fragments + +import android.content.Context +import android.os.Bundle +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewStub +import androidx.core.view.isVisible +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.activityViewModels +import com.navi.amc.R +import com.navi.amc.common.fragment.AmcBaseFragment +import com.navi.amc.common.model.CheckoutRequest +import com.navi.amc.common.viewmodel.OTPSharedVM +import com.navi.amc.compose.feature.goalBasedSip.ui.GoalSummaryScreenContent +import com.navi.amc.compose.feature.goalBasedSip.viewmodel.GoalBasedSipSetupVM +import com.navi.amc.databinding.GoalSummaryScreenLayoutBinding +import com.navi.amc.fundbuy.viewmodel.FundBuyFlowViewModel +import com.navi.amc.utils.AmcAnalytics +import com.navi.amc.utils.AmcAnalytics.GOAL_BASED_SIP_PAGE_LAND +import com.navi.amc.utils.AmcAnalytics.GOAL_BASED_SIP_PAYMENT_INITIATE +import com.navi.amc.utils.AmcAnalytics.ISIN +import com.navi.amc.utils.AmcAnalytics.STEP +import com.navi.amc.utils.Constant +import com.navi.amc.utils.Constant.AMC_PAYMENT_BOTTOMSHEET +import com.navi.amc.utils.Constant.AMOUNT +import com.navi.amc.utils.Constant.DATA +import com.navi.amc.utils.Constant.DISMISS +import com.navi.amc.utils.Constant.FLOW_TYPE +import com.navi.amc.utils.Constant.GOAL_NAME +import com.navi.amc.utils.Constant.GOAL_REFERENCE_ID +import com.navi.amc.utils.Constant.INITIATE_CHECKOUT +import com.navi.amc.utils.Constant.NEW_FLOW_TYPE +import com.navi.amc.utils.Constant.PAYMENT_MODE +import com.navi.amc.utils.Constant.SHOW_BOTTOMSHEET +import com.navi.amc.utils.Constant.SIP_REFERENCE_ID +import com.navi.amc.utils.Constant.SIP_TYPE_CAMEL_CASE +import com.navi.amc.utils.Constant.SOURCE +import com.navi.amc.utils.Constant.WHITE +import com.navi.amc.utils.SubPageStatusType.GOAL_SUMMARY_SCREEN +import com.navi.amc.utils.getBottomSheet +import com.navi.base.model.ActionData +import com.navi.common.listeners.FragmentInterchangeListener +import com.navi.common.listeners.HeaderInteractionListener +import com.navi.common.utils.Constants.AMC_FUND_OTP +import com.navi.common.utils.observeNonNull +import com.navi.design.utils.getNaviDrawable +import com.navi.design.utils.parseColorSafe +import com.navi.design.utils.setSpannableString +import com.navi.naviwidgets.extensions.addOnMultipleClicksHandler +import com.navi.naviwidgets.extensions.showWhenDataIsAvailable +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class GoalSummaryFragment : AmcBaseFragment() { + + private lateinit var binding: GoalSummaryScreenLayoutBinding + private val viewModel by activityViewModels() + private val OTPSharedVM by activityViewModels() + private val buyFlowViewModel: FundBuyFlowViewModel by activityViewModels() + private var cartId: String? = null + + override val screenName: String + get() = GOAL_SUMMARY_SCREEN + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + buyFlowViewModel.paymentMode.value = null + viewModel.paymentMode.value = null + fetchData() + } + + override fun setContainerView(viewStub: ViewStub) { + viewStub.layoutResource = R.layout.goal_summary_screen_layout + binding = DataBindingUtil.getBinding(viewStub.inflate())!! + initError(viewModel) + initObservers() + showShimmer() + } + + private fun fetchData() { + val goalReferenceId = arguments?.getString(GOAL_REFERENCE_ID).orEmpty() + viewModel.fetchGoalSummaryScreenData( + screenName = screenName, + goalReferenceId = goalReferenceId, + ) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + headerInteractionListener = context as? HeaderInteractionListener + fragmentInterchangeListener = context as? FragmentInterchangeListener + } + + private fun initObservers() { + viewModel.goalSummaryScreenData.observe(viewLifecycleOwner) { data -> + hideShimmer() + data?.header?.let { + setHeaderProperties(it) + hideDivider() + } + binding.composeView.apply { + setContent { + GoalSummaryScreenContent(content = data?.content, onClick = ::handleOnClick) + } + } + setFooterData() + val params = viewModel.goalSummaryScreenData.value?.footer?.nextCta?.parameters + arguments?.apply { + params?.forEach { + if (!it.key.isNullOrBlank() && !it.value.isNullOrBlank()) { + putString(it.key, it.value) + } + } + } + + buyFlowVM.setInitialData( + FundBuyFlowViewModel.TransactionFunnelEventModel( + isin = arguments?.getString(ISIN), + value = arguments?.getString(AMOUNT), + transactionType = Constant.SIP, + sipReferenceId = arguments?.getString(SIP_REFERENCE_ID), + initialSource = arguments?.getString(SOURCE).orEmpty(), + goalReferenceId = arguments?.getString(GOAL_REFERENCE_ID).orEmpty(), + goalName = arguments?.getString(GOAL_NAME).orEmpty(), + sipDate = arguments?.getString(Constant.SIP_DATE), + sipFrequency = arguments?.getString(Constant.FREQUENCY), + sipType = arguments?.getString(SIP_TYPE_CAMEL_CASE), + ) + ) + + AmcAnalytics.sendTxnFunnelEvent( + transactionFunnelEventModel = buyFlowVM.getTxnFunnelEventObject(), + extraAttributes = + hashMapOf(STEP to GOAL_BASED_SIP_PAGE_LAND, AmcAnalytics.SOURCE to screenName), + screenName = screenName, + ) + } + viewModel.paymentMode.observeNonNull(viewLifecycleOwner) { + buyFlowVM.paymentMode.value = it + binding.paymentFooter.amount.setText(viewModel.getPaymentModeText()) + binding.paymentFooter.leftIcon.showWhenDataIsAvailable( + viewModel.getPaymentModeFooterIcon() + ) + arguments?.putString(PAYMENT_MODE, it) + } + + buyFlowViewModel.paymentMode.observeNonNull(viewLifecycleOwner) { + if (buyFlowViewModel.paymentMode.value != viewModel.paymentMode.value) { + viewModel.paymentMode.value = buyFlowViewModel.paymentMode.value + } + } + + OTPSharedVM.generateOtpResponse.observe(viewLifecycleOwner) { response -> + val redirectionCta = response?.redirectionCta ?: AMC_FUND_OTP + handleOnClick(action = ActionData(url = redirectionCta), bundle = arguments) + } + + viewModel.errorResponse.observe(viewLifecycleOwner) { hideShimmer() } + } + + private fun showShimmer() { + binding.shimmerLayout.startShimmer() + binding.shimmerLayout.visibility = VISIBLE + } + + private fun hideShimmer() { + binding.shimmerLayout.stopShimmer() + binding.shimmerLayout.visibility = GONE + } + + private fun setFooterData() { + val newFooterData = viewModel.goalSummaryScreenData.value?.footer + newFooterData?.let { + binding.paymentFooter.apply { + root.isVisible = true + accountContainer.background = + getNaviDrawable( + cornerRadius = resources.getDimension(com.navi.design.R.dimen.dp_4).toInt(), + backgroundColor = + newFooterData.paymentCta?.accountBackgroundColor.parseColorSafe(WHITE), + ) + accountContainer.setBackgroundColor( + newFooterData.paymentCta?.accountBackgroundColor.parseColorSafe(WHITE) + ) + account.setSpannableString(newFooterData.paymentCta?.account) + accountLeftIcon.showWhenDataIsAvailable(newFooterData.paymentCta?.accountLeftIcon) + newFooterData.paymentCta?.let { it -> + rightIcon.showWhenDataIsAvailable(it.rightIcon) + llAction.addOnMultipleClicksHandler { _ -> + handleMultiBottomSheetAction( + action = it.actionData, + orderSummaryBottomSheetList = + listOfNotNull( + viewModel.goalSummaryScreenData.value?.footer?.bottomSheetData + ), + listener = { setPaymentMode(it) }, + ) + } + actionText.showWhenDataIsAvailable(it.actionText) + if (viewModel.paymentMode.value == null) { + amount.setSpannableString(it.paymentMode) + viewModel.paymentMode.value = it.paymentType + buyFlowVM.paymentMode.value = it.paymentType + leftIcon.showWhenDataIsAvailable(viewModel.getPaymentModeFooterIcon()) + } else { + amount.setText(viewModel.getPaymentModeText()) + leftIcon.showWhenDataIsAvailable(viewModel.getPaymentModeFooterIcon()) + } + it.actionData?.let { actionData -> handleOnClick(actionData) } + } + btn.text = newFooterData.nextCta?.title + btn.setOnClickListener { handleOnClick(newFooterData.nextCta) } + } + } + } + + private fun setPaymentMode(action: ActionData?) { + viewModel.paymentMode.value = + action?.parameters?.firstOrNull { it.key == PAYMENT_MODE }?.value + buyFlowVM.paymentMode.value = viewModel.paymentMode.value + } + + private fun handleOnClick(action: ActionData?, bundle: Bundle? = Bundle()) { + sendEvent( + action?.metaData?.clickedData, + extraAttributes = + hashMapOf( + SOURCE to screenName, + Constant.CART_ID to cartId.orEmpty(), + Constant.TYPE to + action + ?.parameters + ?.firstOrNull { it.key == Constant.TYPE } + ?.value + .orEmpty(), + AMOUNT to action?.parameters?.firstOrNull { it.key == AMOUNT }?.value.orEmpty(), + FLOW_TYPE to + action?.parameters?.firstOrNull { it.key == FLOW_TYPE }?.value.orEmpty(), + NEW_FLOW_TYPE to + action + ?.parameters + ?.firstOrNull { it.key == NEW_FLOW_TYPE } + ?.value + .orEmpty(), + GOAL_REFERENCE_ID to arguments?.getString(GOAL_REFERENCE_ID).orEmpty(), + GOAL_NAME to + action?.parameters?.firstOrNull { it.key == GOAL_NAME }?.value.orEmpty(), + ), + ) + if (action?.type == DISMISS) return + val url = action?.url + if (url == SHOW_BOTTOMSHEET) { + val data = action.parameters?.getOrNull(0)?.value + val key = action.parameters?.getOrNull(0)?.key + val bundle = Bundle().apply { putString(DATA, data) } + key?.let { key -> + getBottomSheet( + key, + bundle, + genericListener = { action -> + if (key == AMC_PAYMENT_BOTTOMSHEET) { + viewModel.setPaymentMode(action) + } else { + handleOnClick(action, bundle) + } + }, + ) + ?.let { safelyShowBottomSheet(it, key) } + } + } else if (url == INITIATE_CHECKOUT) { + buyFlowViewModel.updateData(paymentMethod = viewModel.paymentMode.value) + AmcAnalytics.sendTxnFunnelEvent( + transactionFunnelEventModel = buyFlowVM.getTxnFunnelEventObject(), + extraAttributes = + hashMapOf( + STEP to GOAL_BASED_SIP_PAYMENT_INITIATE, + AmcAnalytics.SOURCE to screenName, + ), + screenName = screenName, + ) + cartId = + action.parameters + ?.firstOrNull { it.key == Constant.CART_ID_SMALL_CASE } + ?.value + .orEmpty() + val type = action.parameters?.firstOrNull { it.key == Constant.TYPE }?.value.orEmpty() + val source = action.parameters?.firstOrNull { it.key == SOURCE }?.value.orEmpty() + OTPSharedVM.generateOtp( + screenName = screenName, + checkoutRequest = CheckoutRequest(cartId = cartId, type = type, source = source), + cartId = cartId, + ) + } else { + fragmentInterchangeListener?.navigateToNextScreen( + actionData = action, + bundle = bundle ?: Bundle(), + ) + } + } + + companion object { + fun newInstance(bundle: Bundle? = null): GoalSummaryFragment { + return GoalSummaryFragment().apply { arguments = bundle } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundBuyScreenData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundBuyScreenData.kt index fd709bd03c..1798e10b2d 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundBuyScreenData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundBuyScreenData.kt @@ -14,6 +14,7 @@ import com.navi.amc.common.model.Footer import com.navi.amc.common.model.InformationCardData import com.navi.amc.common.model.OnTimeAssuranceData import com.navi.base.model.ActionData +import com.navi.common.model.AmcBottomSheetData import com.navi.common.model.Header import com.navi.common.model.LabelDataVariation import com.navi.design.textview.model.TextWithStyle @@ -69,11 +70,13 @@ open class GenericFooter( data class AmountPageFooter( @SerializedName("paymentCta") val paymentCta: PaymentCtaData? = null, @SerializedName("nextCta") val nextCta: ActionData? = null, + @SerializedName("bottomSheetData") val bottomSheetData: AmcBottomSheetData? = null, ) : CardType() data class PaymentCtaData( @SerializedName("account") val account: TextWithStyle? = null, @SerializedName("paymentMode") val paymentMode: TextWithStyle? = null, + @SerializedName("paymentType") val paymentType: String? = null, @SerializedName("leftIcon") val leftIcon: String? = null, @SerializedName("rightIcon") val rightIcon: String? = null, @SerializedName("actionText") val actionText: TextWithStyle? = null, @@ -183,6 +186,8 @@ data class InstallmentDateMap( @SerializedName("id") val id: String? = null, @SerializedName("amountLessThanMandate") val amountLessThanMandate: String? = null, @SerializedName("amountGreaterThanMandate") val amountGreaterThanMandate: String? = null, + @SerializedName("formattedSipDate") val formattedSipDate: String? = null, + @SerializedName("sipDate") val sipDate: String? = null, ) data class NavInfoData( diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/InvestmentGoalData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/InvestmentGoalData.kt index 33c25a1ec9..d60b17e964 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/InvestmentGoalData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/InvestmentGoalData.kt @@ -43,6 +43,7 @@ data class TextFieldSliderData( @SerializedName("title") val title: TextWithStyle? = null, @SerializedName("subtitle") val subtitle: TextWithStyle? = null, @SerializedName("textFieldData") val textFieldData: TextInputFixedHintWidgetData? = null, + @SerializedName("sliderTransition") val sliderTransition: List? = null, @SerializedName("sliderData") val sliderData: SliderData? = null, @SerializedName("itemType") val itemType: String? = null, ) @@ -54,10 +55,11 @@ data class CreateInvestmentGoalData( @SerializedName("duration") val duration: String? = null, @SerializedName("rateOfInterest") val rateOfInterest: String? = null, @SerializedName("goalName") val goalName: String? = null, + @SerializedName("goalReferenceId") val goalReferenceId: String? = null, ) data class CreateInvestmentGoalResponse( - @SerializedName("goalId") val goalId: String? = null, + @SerializedName("goalId", alternate = ["goalReferenceId"]) val goalId: String? = null, @SerializedName("lottieDismissTime") val lottieDismissTime: Double? = null, @SerializedName("lottieUrl") val lottieUrl: String? = null, @SerializedName("nextCta") val nextCta: ActionData? = null, diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt index f5b5a48edf..224d5f304d 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt @@ -8,6 +8,7 @@ package com.navi.amc.fundbuy.models.cards import com.google.gson.annotations.SerializedName +import com.navi.amc.fundbuy.models.widgets.TrackerContent import com.navi.base.model.ActionData import com.navi.common.model.InvestmentBaseProperty import com.navi.naviwidgets.models.response.ImageFieldData @@ -26,6 +27,7 @@ data class InvestmentGoalCardData( @SerializedName("footer") val footer: InvestmentGoalCardFooterData? = null, @SerializedName("properties") val properties: InvestmentGoalCardProperties? = null, @SerializedName("actionData") val actionData: ActionData? = null, + @SerializedName("items") val items: List? = null, ) data class InvestmentGoalCardFooterData( @@ -61,10 +63,11 @@ data class InvestmentGoalCardFooterProperties( data class SliderData( @SerializedName("selectedValue") val selectedValue: Long? = null, - @SerializedName("minValue") val minValue: Long? = null, - @SerializedName("maxValue") val maxValue: Long? = null, + @SerializedName("minValue", alternate = ["minAmount"]) val minValue: Long? = null, + @SerializedName("maxValue", alternate = ["maxAmount"]) val maxValue: Long? = null, @SerializedName("stepValue") val stepValue: Long? = null, @SerializedName("thumbImage") val thumbImage: String? = null, @SerializedName("minTrackTintColor") val minTrackTintColor: String? = null, @SerializedName("maxTrackTintColor") val maxTrackTintColor: String? = null, + @SerializedName("title") val title: TextFieldData? = null, ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/PaymentCard.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/PaymentCard.kt index 458e659489..4089094078 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/PaymentCard.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/PaymentCard.kt @@ -16,13 +16,15 @@ import com.navi.naviwidgets.models.response.ImageFieldData import com.navi.naviwidgets.models.response.TextFieldData data class PaymentCard( - @SerializedName("fundName") val fundName: TextFieldData? = null, + @SerializedName("fundName", alternate = ["title"]) val fundName: TextFieldData? = null, @SerializedName("paymentCardHeader") val paymentCardHeader: PaymentCardHeader? = null, - @SerializedName("paymentAmount") val paymentAmount: TextFieldData? = null, + @SerializedName("paymentAmount", alternate = ["subtitle"]) + val paymentAmount: TextFieldData? = null, @SerializedName("paymentCardSubtitle") val paymentCardSubtitle: TextFieldData? = null, @SerializedName("buttonText") val buttonText: TextFieldData? = null, @SerializedName("actionData") val actionData: ActionData? = null, @SerializedName("investmentsIcon") val investmentsIcon: ImageFieldData? = null, + @SerializedName("rightIcon") val rightIcon: ImageFieldData? = null, @SerializedName("properties") val properties: CardProperties? = null, @SerializedName("bottomSheetData") val bottomSheetData: BottomSheetData? = null, @SerializedName("multiBottomSheetData") val multiBottomSheetData: GenericBottomSheetData? = null, diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/widgets/SetupMonthlyTargetWidget.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/widgets/SetupMonthlyTargetWidget.kt index 51db20599e..c01f2e7a57 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/widgets/SetupMonthlyTargetWidget.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/widgets/SetupMonthlyTargetWidget.kt @@ -24,6 +24,7 @@ data class SetupMonthlyTargetWidget( data class SetupMonthlyTargetWidgetData( @SerializedName("header") val header: SetupMonthlyTargetWidgetHeader? = null, @SerializedName("content") val content: SetupMonthlyTargetCard? = null, + @SerializedName("items") val items: List? = null, @SerializedName("extraData") val extraData: SetupMonthlyTargetWidgetExtraData? = null, ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt index 18d08952cc..ab2013212e 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt @@ -36,9 +36,13 @@ constructor( private val naviCacheRepository: NaviCacheRepositoryImpl, ) : ResponseCallback() { - suspend fun getFundDetail(isin: String, metricInfo: MetricInfo>) = + suspend fun getFundDetail( + isin: String, + source: String?, + metricInfo: MetricInfo>, + ) = apiResponseCallback( - response = retrofitService.fetchFundDetails(isin), + response = retrofitService.fetchFundDetails(isin = isin, source = source), metricInfo = metricInfo, ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/AutoPaySetupScreenViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/AutoPaySetupScreenViewModel.kt index 377bf28915..bafa70cbad 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/AutoPaySetupScreenViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/AutoPaySetupScreenViewModel.kt @@ -9,13 +9,13 @@ package com.navi.amc.fundbuy.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.NextCtaResponse import com.navi.amc.common.model.cart.CartRequest import com.navi.amc.common.model.cart.CheckoutStepCta import com.navi.amc.common.model.cart.CheckoutSteps import com.navi.amc.common.viewmodel.BaseAmcVM -import com.navi.amc.common.viewmodel.CartUseCase import com.navi.amc.fundbuy.models.AutoPaySetupRequestData import com.navi.amc.fundbuy.models.AutoPaySetupScreenState import com.navi.amc.fundbuy.models.OverviewSectionData diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundBuyFlowViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundBuyFlowViewModel.kt index 6714c46d02..5f187ba30b 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundBuyFlowViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundBuyFlowViewModel.kt @@ -11,12 +11,12 @@ import android.os.Parcelable import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.NextCtaResponse import com.navi.amc.common.model.SipOrderSummaryData import com.navi.amc.common.model.cart.CheckoutSteps import com.navi.amc.common.viewmodel.BaseAmcVM -import com.navi.amc.common.viewmodel.CartUseCase import com.navi.amc.fundbuy.models.AutoPaySetupRequestData import com.navi.amc.fundbuy.models.ScreenDetails import com.navi.amc.fundbuy.models.Temperature @@ -232,6 +232,7 @@ constructor(private val repository: FundBuyRepository, private val cartUseCase: value: String? = null, sipReferenceId: String? = null, orderId: String? = null, + initialSource: String? = null, ) { eventData = eventData.copy( @@ -244,6 +245,7 @@ constructor(private val repository: FundBuyRepository, private val cartUseCase: value = value ?: eventData.value, sipReferenceId = sipReferenceId ?: eventData.sipReferenceId, orderId = orderId ?: eventData.orderId, + initialSource = initialSource ?: eventData.initialSource, ) } @@ -267,5 +269,7 @@ constructor(private val repository: FundBuyRepository, private val cartUseCase: val sipReferenceId: String? = null, val orderId: String? = null, val initialSource: String? = null, + val goalReferenceId: String? = null, + val goalName: String? = null, ) : Parcelable } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt index b2aa34ab72..bd921caef1 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt @@ -52,10 +52,14 @@ class FundDetailViewModel @Inject constructor(private val repository: FundDetail var fundName: String? = null var selectedRadio: String? = null - fun getFundScreenData(isin: String, screenName: String) { + fun getFundScreenData(isin: String, source: String?, screenName: String) { viewModelScope.launch { val response = - repository.getFundDetail(isin = isin, metricInfo = getAmcMetricInfo(screenName)) + repository.getFundDetail( + isin = isin, + source = source, + metricInfo = getAmcMetricInfo(screenName), + ) if (response.error == null && response.errors.isNullOrEmpty()) { _fundDetailScreenData.value = response.data } else { diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/PaymentSummaryViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/PaymentSummaryViewModel.kt index 539c93168b..15854ce34c 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/PaymentSummaryViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/PaymentSummaryViewModel.kt @@ -10,11 +10,11 @@ package com.navi.amc.fundbuy.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.NextCtaResponse import com.navi.amc.common.model.cart.CartRequest import com.navi.amc.common.model.cart.CheckoutSteps -import com.navi.amc.common.viewmodel.CartUseCase import com.navi.amc.fundbuy.models.PaymentOrder import com.navi.amc.fundbuy.models.PaymentPostData import com.navi.amc.fundbuy.models.PaymentSummaryScreen diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/TargetSetupViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/TargetSetupViewModel.kt index 5dc16c9f76..4c472c3376 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/TargetSetupViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/TargetSetupViewModel.kt @@ -13,30 +13,46 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.navi.amc.common.viewmodel.BaseAmcVM +import com.navi.amc.compose.feature.goalBasedSip.GoalBasedSipRepository +import com.navi.amc.compose.feature.goalBasedSip.model.CreateGoalState import com.navi.amc.fundbuy.models.CreateInvestmentGoalData import com.navi.amc.fundbuy.models.CreateInvestmentGoalResponse import com.navi.amc.fundbuy.models.SliderRangeData import com.navi.amc.fundbuy.models.TargetSetupPageData import com.navi.amc.fundbuy.repository.InvestmentGoalRepository +import com.navi.amc.utils.AmcAnalytics.VALUE_CHANGED_USING_SLIDER +import com.navi.amc.utils.AmcAnalytics.VALUE_CHANGED_USING_TEXT_FIELD +import com.navi.amc.utils.Constant.KEY import com.navi.amc.utils.Constant.MIG_AMOUNT import com.navi.amc.utils.Constant.MIG_DURATION import com.navi.amc.utils.Constant.MIG_RATE_OF_INTEREST +import com.navi.amc.utils.Constant.VALUE import com.navi.amc.utils.getAmcMetricInfo import com.navi.amc.utils.moneyFormatWithRupeePrefix import com.navi.amc.utils.validateText +import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.utils.EMPTY import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.common.network.models.isSuccessWithData +import com.navi.naviwidgets.models.response.TextFieldData import com.navi.naviwidgets.validations.BaseInputValidation import com.navi.naviwidgets.widgets.fixedhinttextinput.TextInputFixedHintWidgetData import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlin.math.min import kotlin.math.pow import kotlin.math.round import kotlin.math.roundToInt +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow @HiltViewModel -class TargetSetupViewModel @Inject constructor(private val repository: InvestmentGoalRepository) : - BaseAmcVM() { +class TargetSetupViewModel +@Inject +constructor( + private val repository: InvestmentGoalRepository, + private val goalBasedSipRepository: GoalBasedSipRepository, +) : BaseAmcVM() { private val _targetSetupPageData = MutableLiveData() val targetSetupData: MutableLiveData @@ -70,10 +86,19 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen val areAllFieldsValid: MutableLiveData get() = _areAllFieldsValid + private val _areAllFieldsValidFlow = MutableStateFlow(true) + val areAllFieldsValidFlow = _areAllFieldsValidFlow.asStateFlow() + + private val _createGoalResponse = MutableStateFlow(CreateGoalState.Nothing) + val createGoalResponse = _createGoalResponse.asStateFlow() + private var validationMap = mutableMapOf?>() private var sliderDataMap = mutableMapOf>() + private var title = mutableStateOf(null) + private var projectedGoalValue = mutableStateOf(EMPTY) + var currentYear: Int? = null fun getTargetSetupPageContent(goalName: String? = null, screenName: String) { @@ -88,11 +113,7 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen } } - fun createInvestmentGoal( - screenName: String, - goalName: String?, - createInvestmentGoalData: CreateInvestmentGoalData? = null, - ) { + fun createInvestmentGoal(screenName: String, goalName: String?) { viewModelScope.safeLaunch { val response = repository.createInvestmentGoal( @@ -112,7 +133,42 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen } } - fun updateValue(key: String?, value: String?) { + private fun updateCreateGoalState(state: CreateGoalState) { + _createGoalResponse.value = state + } + + fun createGoal(screenName: String? = null, goalName: String?, goalReferenceId: String) { + updateCreateGoalState(CreateGoalState.Loading) + viewModelScope.safeLaunch { + val response = + goalBasedSipRepository.createGoal( + CreateInvestmentGoalData( + amount = getMapValue(MIG_AMOUNT), + duration = getMapValue(MIG_DURATION), + goalName = goalName, + goalReferenceId = goalReferenceId, + ) + ) + if (response.isSuccessWithData()) { + updateCreateGoalState( + CreateGoalState.Success(createGoalResponse = response.data ?: return@safeLaunch) + ) + } else { + updateCreateGoalState( + CreateGoalState.Error( + error = response.errors?.firstOrNull() ?: return@safeLaunch + ) + ) + } + } + } + + fun updateValue(key: String?, value: String?, screenName: String? = null) { + sendEvent( + eventName = VALUE_CHANGED_USING_TEXT_FIELD, + extraAttributes = hashMapOf(KEY to key.orEmpty(), VALUE to value.orEmpty()), + screenName = screenName ?: NaviTrackEvent.foregroundScreen.orEmpty(), + ) if (key != null && value != null) { val formattedValue = value.ifEmpty { "0" } if (_valueMap.contains(key)) { @@ -122,7 +178,9 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen } _sliderValueMap[key] = if (value.isNotNullAndNotEmpty()) value.toInt() else 0 _errorMap[key] = validateText(validationMap[key], formattedValue) - _areAllFieldsValid.value = _errorMap.values.all { it.isNullOrEmpty() } + val isValid = _errorMap.values.all { it.isNullOrEmpty() } + _areAllFieldsValid.value = isValid + _areAllFieldsValidFlow.value = isValid } } @@ -141,13 +199,20 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen key?.let { validationMap[key] = validation } } - fun updateSliderValue(key: String?, value: Float) { + fun updateSliderValue(key: String?, value: Float, screenName: String? = null) { + sendEvent( + eventName = VALUE_CHANGED_USING_SLIDER, + extraAttributes = hashMapOf(KEY to key.orEmpty(), VALUE to value.toString()), + screenName = screenName ?: NaviTrackEvent.foregroundScreen.orEmpty(), + ) key?.let { val newValue = getNextStepValue(key, value) _sliderValueMap[key] = newValue _valueMap[key]?.value = newValue.toString() _errorMap[key] = validateText(validationMap[key], newValue.toString()) - _areAllFieldsValid.value = _errorMap.values.all { it.isNullOrEmpty() } + val isValid = _errorMap.values.all { it.isNullOrEmpty() } + _areAllFieldsValid.value = isValid + _areAllFieldsValidFlow.value = isValid } } @@ -157,7 +222,7 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen val stepSize = sliderDataMap[id]?.value?.step?.toFloat() ?: 1f val stepIndex = ((value - minValue) / stepSize).roundToInt() val newVal = minValue + (stepIndex * stepSize) - return newVal.toInt() + return min(newVal, maxValue).toInt() } fun updateSliderMap(key: String?, sliderData: SliderRangeData) { @@ -187,7 +252,9 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen val exponentFactor = (1.0 + (expectedRate)).pow(durationMonths) - 1 val multiplyFactor = (1 + expectedRate).div(expectedRate) val futureValue = monthlyAmount * (multiplyFactor * exponentFactor) + val roundedToNearestThousand = (round(futureValue / 1000) * 1000).toLong() _potentialReturns.value = moneyFormatWithRupeePrefix(round(futureValue).toString()) + projectedGoalValue.value = moneyFormatWithRupeePrefix(roundedToNearestThousand.toString()) } fun setPotentialReturns(returns: String) { @@ -199,6 +266,19 @@ class TargetSetupViewModel @Inject constructor(private val repository: Investmen return _potentialReturns.value } + fun getProjectedGoalValue(): String { + calculatePotentialReturns() + return projectedGoalValue.value + } + + fun setDynamicText(title: TextFieldData?) { + this.title.value = title + } + + fun getDynamicText(): TextFieldData? { + return title.value + } + fun getMaxChar(id: String? = null, textFieldData: TextInputFixedHintWidgetData? = null): Int { return when (id) { MIG_AMOUNT -> textFieldData?.inputTextFixedHintItemData?.maxCharLength ?: 6 diff --git a/android/navi-amc/src/main/java/com/navi/amc/navigator/NaviAmcDeeplinkNavigator.kt b/android/navi-amc/src/main/java/com/navi/amc/navigator/NaviAmcDeeplinkNavigator.kt index d536875f18..2aa47a04c8 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/navigator/NaviAmcDeeplinkNavigator.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/navigator/NaviAmcDeeplinkNavigator.kt @@ -17,6 +17,7 @@ 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.Constant.START_SCREEN_NAME import com.navi.amc.utils.getCtaToNavigateToInvestmentTab import com.navi.amc.utils.updateSecondIdentifier import com.navi.base.deeplink.DeepLinkManager @@ -41,6 +42,7 @@ object NaviAmcDeeplinkNavigator { const val FUND_LANDING = "fund_landing" const val V2_PARAMETER = "v2" const val FTUE = "ftue" + const val GOAL = "goal" fun navigate( activity: Activity?, @@ -118,7 +120,8 @@ object NaviAmcDeeplinkNavigator { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } } - FTUE -> { + FTUE, + GOAL -> { Intent(activity, AmcComposeActivity::class.java) } WEB_URL -> { @@ -135,6 +138,7 @@ object NaviAmcDeeplinkNavigator { } intent?.let { intent -> intent.putExtras(bundle) + intent.putExtra(START_SCREEN_NAME, secondIdentifier) intent.putExtra(Constants.SECOND_IDENTIFIER, secondIdentifier) intent.putExtra(Constants.SUB_REDIRECT, thirdIdentifier) updateSecondIdentifier( diff --git a/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt b/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt index b7492b5b7e..f75590d255 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt @@ -11,6 +11,9 @@ 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.compose.feature.goalBasedSip.model.GoalBasedSipScreenData +import com.navi.amc.compose.feature.goalBasedSip.model.GoalDetailsScreenData +import com.navi.amc.compose.feature.goalBasedSip.model.GoalSummaryScreenData import com.navi.amc.digio.AadhaarVerificationData import com.navi.amc.digio.DigioKycPollingResponse import com.navi.amc.digio.DigioKycResponse @@ -413,7 +416,8 @@ interface RetrofitService { @GET("/fund/fund-details") @RetryPolicy suspend fun fetchFundDetails( - @Query("isin") isin: String + @Query("isin") isin: String, + @Query("source") source: String? = null, ): Response> @GET("/autopay/set-autopay") @@ -551,8 +555,9 @@ interface RetrofitService { ): Response>> @GET("fund/get-sip-success") - suspend fun fetchSipSuccessPage(): - Response>> + suspend fun fetchSipSuccessPage( + @Query("source") source: String? = null + ): Response>> @POST("/autopay/v2/set-autopay") suspend fun fetchAutoPaySetupDetailsV2( @@ -589,6 +594,12 @@ interface RetrofitService { @POST("/cart/auto-checkout") suspend fun initiateCart(@Body cartRequest: CartRequest): Response> + @POST("/checkout") + suspend fun initiateCheckout( + @Query("source") source: String? = null, + @Body checkoutRequest: CheckoutRequest, + ): Response> + @POST("/checkout/{checkoutId}{nextStepCta}") suspend fun checkoutNextAction( @Body checkoutRequest: CheckoutRequest, @@ -612,4 +623,37 @@ interface RetrofitService { suspend fun postSelectedFund( @Body fundSelectionData: FundSelectionData ): Response> + + @GET("/goal/setup-page") + suspend fun fetchGoalSetupPageData( + @Query("goalReferenceId") goalReferenceId: String, + @Query("goalName") goalName: String? = null, + ): Response> + + @POST("/goal") + suspend fun createGoal( + @Body createInvestmentGoalData: CreateInvestmentGoalData + ): Response> + + @GET("/goal/sip-setup-page/{goalReferenceId}") + suspend fun fetchGoalSipAmountPageData( + @Path("goalReferenceId") goalReferenceId: String + ): Response> + + @POST("/cart") + suspend fun createCart( + @Query("source") source: String? = null, + @Body cartRequest: CartRequest, + ): Response> + + @GET("/goal/summary/{goalReferenceId}") + suspend fun fetchGoalSummaryScreenData( + @Path("goalReferenceId") goalReferenceId: String + ): Response> + + @GET("/goal/details/{goalReferenceId}") + suspend fun fetchGoalDetailsScreenData( + @Path("goalReferenceId") goalReferenceId: String, + @Query("source") source: String? = null, + ): Response> } diff --git a/android/navi-amc/src/main/java/com/navi/amc/portfolio/fragments/SipModifyFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/portfolio/fragments/SipModifyFragment.kt index dffaf245dc..28d281e469 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/portfolio/fragments/SipModifyFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/portfolio/fragments/SipModifyFragment.kt @@ -58,21 +58,16 @@ import com.navi.amc.utils.Constant.FLOW_TYPE import com.navi.amc.utils.Constant.FLOW_TYPE_SIP_PURCHASE import com.navi.amc.utils.Constant.FORMATTED_AMOUNT import com.navi.amc.utils.Constant.GET_SIP_SUMMARY_BOTTOMSHEET -import com.navi.amc.utils.Constant.MANDATE_OPTED_IN -import com.navi.amc.utils.Constant.MANDATE_OPTED_OUT import com.navi.amc.utils.Constant.MODIFY_BOTTOM_SHEET import com.navi.amc.utils.Constant.NEW_FLOW_TYPE import com.navi.amc.utils.Constant.ORDER_HEADER_SUBTITLE -import com.navi.amc.utils.Constant.PAUSED -import com.navi.amc.utils.Constant.RESUMED -import com.navi.amc.utils.Constant.RESUMED_INSTALLMENT import com.navi.amc.utils.Constant.SETUP_AUTOPAY_EXISTING_SIP import com.navi.amc.utils.Constant.SIP_REFERENCE_ID -import com.navi.amc.utils.Constant.SKIPPED import com.navi.amc.utils.SubPageStatusType import com.navi.amc.utils.createCartRequest import com.navi.amc.utils.getJsonObject import com.navi.amc.utils.getPaymentSyncFlowStatusCta +import com.navi.amc.utils.shouldCallUpdateSip import com.navi.amc.utils.showToastMessage import com.navi.base.model.ActionData import com.navi.base.model.CtaData @@ -333,7 +328,7 @@ class SipModifyFragment : AmcBaseFragment(), FooterInteractionListener { openBottomSheet(viewModel.sipDetailsData.value?.content?.modifyBottomSheetData) } else if (actionData?.url == DELETE_SIP_WITH_REASON) { openCsatBottomSheet(viewModel.sipDetailsData.value?.content?.csatWidgetData) - } else if (isApiNeedsToCall(actionData?.url)) { + } else if (shouldCallUpdateSip(actionData?.url)) { showLoader() viewModel.updateSip( arguments?.getString(SIP_REFERENCE_ID).orEmpty(), @@ -452,16 +447,6 @@ class SipModifyFragment : AmcBaseFragment(), FooterInteractionListener { } } - private fun isApiNeedsToCall(url: String?): Boolean { - return url?.contains(SKIPPED).orFalse() || - url?.contains(PAUSED).orFalse() || - url?.contains(RESUMED).orFalse() || - url?.contains(DELETED).orFalse() || - url?.contains(MANDATE_OPTED_OUT).orFalse() || - url?.contains(MANDATE_OPTED_IN).orFalse() || - url?.contains(RESUMED_INSTALLMENT).orFalse() - } - private fun onClick(actionData: ActionData) { handleOnClick(actionData) } diff --git a/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipModificationVM.kt b/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipModificationVM.kt index 86fbefd083..f419590075 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipModificationVM.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipModificationVM.kt @@ -10,11 +10,11 @@ package com.navi.amc.portfolio.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.NextCtaResponse import com.navi.amc.common.model.cart.CartRequest import com.navi.amc.common.model.cart.CheckoutSteps -import com.navi.amc.common.viewmodel.CartUseCase import com.navi.amc.fundbuy.models.SipDetailsData import com.navi.amc.portfolio.models.SipModificationResponse import com.navi.amc.portfolio.models.SipUpdateResponse diff --git a/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipViewModel.kt index 0d930a85eb..9bf5c578e3 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/portfolio/viewmodels/SipViewModel.kt @@ -10,6 +10,7 @@ package com.navi.amc.portfolio.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.model.AdditionalDataAsyncResponse import com.navi.amc.common.model.NextCtaResponse import com.navi.amc.common.model.cart.CheckoutSteps @@ -17,7 +18,6 @@ import com.navi.amc.common.taskProcessor.AmcTaskListener import com.navi.amc.common.taskProcessor.AmcTaskManager import com.navi.amc.common.taskProcessor.SipListPrefetchTask import com.navi.amc.common.viewmodel.BaseAmcVM -import com.navi.amc.common.viewmodel.CartUseCase import com.navi.amc.fundbuy.models.AutoPaySetupRequestData import com.navi.amc.portfolio.models.SipScreenData import com.navi.amc.portfolio.repositories.SipDetailsRepository diff --git a/android/navi-amc/src/main/java/com/navi/amc/redemption/fragment/FundSellFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/redemption/fragment/FundSellFragment.kt index a678502296..e051e3c368 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/redemption/fragment/FundSellFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/redemption/fragment/FundSellFragment.kt @@ -21,9 +21,9 @@ import androidx.fragment.app.viewModels import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.gson.Gson import com.navi.amc.R +import com.navi.amc.common.cart.CartUseCase import com.navi.amc.common.fragment.AmcBaseFragment import com.navi.amc.common.model.OnTimeAssuranceData -import com.navi.amc.common.viewmodel.CartUseCase import com.navi.amc.databinding.FundSellLayoutBinding import com.navi.amc.fundbuy.models.SubItemData import com.navi.amc.redemption.model.AmountSellType diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/AmcAnalytics.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/AmcAnalytics.kt index 3c94f7e826..4d4b280ae3 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/AmcAnalytics.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/AmcAnalytics.kt @@ -10,6 +10,8 @@ package com.navi.amc.utils import androidx.annotation.Keep import com.navi.amc.fundbuy.viewmodel.FundBuyFlowViewModel import com.navi.amc.utils.Constant.FREQUENCY +import com.navi.amc.utils.Constant.GOAL_NAME +import com.navi.amc.utils.Constant.GOAL_REFERENCE_ID import com.navi.amc.utils.Constant.INITIAL_SOURCE import com.navi.amc.utils.Constant.SIP_REFERENCE_ID import com.navi.amc.utils.Constant.SIP_TYPE @@ -300,6 +302,8 @@ object AmcAnalytics { const val AMOUNT_PAGE_PAYMENT_INITIATE = "amount_page_payment_initiate" const val PAYMENT_SUMMARY_PAYMENT_INITIATE = "payment_summary_payment_initiate" const val SIP_DETAILS_PAYMENT_INITITATE = "sip_details_payment_initiate" + const val GOAL_BASED_SIP_PAGE_LAND = "goal_based_sip_page_land" + const val GOAL_BASED_SIP_PAYMENT_INITIATE = "goal_based_sip_payment_initiate" const val AUTOPAY_MANUAL_CHOOSE_SCREEN_LAND = "autopay_manual_choose_screen_land" const val AUTOPAY_MANUAL_CHOOSE_BUTTON_CLICKED = "autopay_manual_choose_button_clicked" @@ -318,6 +322,9 @@ object AmcAnalytics { const val PORTFOLIO_FILTER_CLICKED = "amc_portfolio_filter_clicked" const val PORTFOLIO_RETURN_FILTER = "amc_portfolio_return_filter" const val PORTFOLIO_DEFAULT_FILTER = "amc_portfolio_default_filter" + const val VALUE_CHANGED_USING_TEXT_FIELD = "amc_value_changed_using_text_field" + const val VALUE_CHANGED_USING_SLIDER = "amc_value_changed_using_slider" + const val AMC_GOAL_BASED_SIP_DATE_SELECTION = "amc_goal_based_sip_date_selection" fun sendEvent( eventsData: GenericAnalyticsData?, @@ -384,6 +391,8 @@ object AmcAnalytics { Pair(Constant.SIP_DATE, transactionFunnelEventModel?.sipDate.orEmpty()), Pair(SIP_REFERENCE_ID, transactionFunnelEventModel?.sipReferenceId.orEmpty()), Pair(INITIAL_SOURCE, transactionFunnelEventModel?.initialSource.orEmpty()), + Pair(GOAL_NAME, transactionFunnelEventModel?.goalName.orEmpty()), + Pair(GOAL_REFERENCE_ID, transactionFunnelEventModel?.goalReferenceId.orEmpty()), ) attributes.putAll(funnelAttributes) attributes[SOURCE_SCREEN_NAME] = TempStorageHelper.getPreviousScreenName().toString() diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/CartUtils.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/CartUtils.kt index bfd83bd436..355955b1d5 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/CartUtils.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/CartUtils.kt @@ -36,8 +36,8 @@ fun createCartRequest(arguments: Bundle?): CartRequest { CartItem( flowType = if (PreferenceManager.getBooleanPreference(CART_EXPERIMENT_ENABLED)) { - arguments?.getString(NEW_FLOW_TYPE).orEmpty() - } else arguments?.getString(OTP_FLOW_TYPE).orEmpty(), + arguments?.getString(NEW_FLOW_TYPE) + } else arguments?.getString(OTP_FLOW_TYPE), orderType = arguments?.getString(ORDER_TYPE), orderDetails = CartOrderDetails( @@ -56,6 +56,7 @@ fun createCartRequest(arguments: Bundle?): CartRequest { }, mandateType = arguments?.getString(Constant.MANDATE_TYPE), folioNumber = arguments?.getString(FOLIO_NUMBER), + goalReferenceId = arguments?.getString(Constant.GOAL_REFERENCE_ID), ), ) ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt index d8b2761179..1c2bada400 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt @@ -20,7 +20,15 @@ import com.navi.amc.common.model.NextCtaResponse import com.navi.amc.fundbuy.models.FundDuration import com.navi.amc.fundbuy.models.NavInfo import com.navi.amc.network.deserializer.FundListDeserializer +import com.navi.amc.utils.Constant.DELETED +import com.navi.amc.utils.Constant.GOAL_SIP_DELETED +import com.navi.amc.utils.Constant.MANDATE_OPTED_IN +import com.navi.amc.utils.Constant.MANDATE_OPTED_OUT +import com.navi.amc.utils.Constant.PAUSED +import com.navi.amc.utils.Constant.RESUMED +import com.navi.amc.utils.Constant.RESUMED_INSTALLMENT import com.navi.amc.utils.Constant.RUPEE_SYMBOL +import com.navi.amc.utils.Constant.SKIPPED import com.navi.amc.utils.Constant.UPI_APP_INTENT_URL import com.navi.base.AppServiceManager import com.navi.base.model.ActionData @@ -218,3 +226,14 @@ fun roundOffAmount(value: Double): String { else -> df.format(floor(value * 100) / 100.0f) } } + +fun shouldCallUpdateSip(url: String?): Boolean { + return url?.contains(SKIPPED).orFalse() || + url?.contains(PAUSED).orFalse() || + url?.contains(RESUMED).orFalse() || + url?.contains(DELETED).orFalse() || + url?.contains(MANDATE_OPTED_OUT).orFalse() || + url?.contains(MANDATE_OPTED_IN).orFalse() || + url?.contains(RESUMED_INSTALLMENT).orFalse() || + url?.contains(GOAL_SIP_DELETED).orFalse() +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt index 99ae5f0833..63e06e3a23 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt @@ -62,6 +62,7 @@ object Constant { const val PAUSED = "PAUSED" const val RESUMED = "RESUMED" const val DELETED = "DELETED" + const val GOAL_SIP_DELETED = "GOAL_SIP_DELETED" const val MANDATE_OPTED_OUT = "MANDATE_OPTED_OUT" const val MANDATE_OPTED_IN = "MANDATE_OPTED_IN" const val RESUMED_INSTALLMENT = "RESUMED_INSTALLMENT" @@ -218,7 +219,11 @@ object Constant { const val SPACER_DEFAULT = 16 const val GOAL_TYPE = "goalType" + const val GOAL_NAME = "goalName" const val SET_TARGET = "set_target" + const val SET_GOAL_TARGET = "set_goal_target" + const val CREATE_CART = "create_cart" + const val GOAL_REFERENCE_ID = "goalReferenceId" const val MIG_AMOUNT = "AMOUNT" const val MIG_DURATION = "DURATION" const val MIG_RATE_OF_INTEREST = "RATE_OF_INTEREST" @@ -235,6 +240,7 @@ object Constant { const val CHECKOUT_STEP = "CHECKOUT_STEP" const val CHECKOUT_STEP_CTA = "CHECKOUT_STEP_CTA" const val CART_ID = "CART_ID" + const val CART_ID_SMALL_CASE = "cartId" const val CART_RESPONSE = "CART_RESPONSE" const val CART_REQUEST = "CART_REQUEST" const val ERROR_RESPONSE = "ERROR_RESPONSE" @@ -252,4 +258,14 @@ object Constant { const val AP_ISIN = "applicant.isin" const val JOURNEY_SEGMENT = "journey_segment" const val INITIAL_SOURCE = "initialSource" + const val DEFAULT_COLUMN_WEIGHT = 0.68f + const val AMC_PAYMENT_BOTTOMSHEET = "AMC_PAYMENT_BOTTOMSHEET" + const val MODIFY_SIP_CARD = "MODIFY_SIP_CARD" + const val GOAL_BASED_SIP_SETUP_CARD = "goalBasedSipSetupCard" + const val STATIC = "STATIC" + const val DYNAMIC = "DYNAMIC" + const val INITIATE_CHECKOUT = "initiate_checkout" + const val KEY = "key" + const val VALUE = "value" + const val SIP_TYPE_CAMEL_CASE = "sipType" } diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/Ext.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/Ext.kt index 89e9cda021..e000982fce 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/Ext.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/Ext.kt @@ -248,6 +248,10 @@ fun getFragment(screen: String, bundle: Bundle): Fragment? { SubPageStatusType.TARGET_SETUP_PAGE -> TargetSetupFragment.newInstance(bundle) SubPageStatusType.INVESTMENT_GOAL_DETAILS -> TargetDetailsFragment.newInstance(bundle) SubPageStatusType.ON_TIME_ASSURANCE -> OnTimeAssuranceInfoFragment.newInstance(bundle) + SubPageStatusType.GOAL_SUMMARY_SCREEN -> GoalSummaryFragment.newInstance(bundle) + SubPageStatusType.GOAL_BASED_SIP_AMOUNT_SCREEN -> + GoalBasedSipAmountFragment.newInstance(bundle) + SubPageStatusType.GOAL_DETAILS_SCREEN -> GoalDetailsFragment.newInstance(bundle) else -> null } } @@ -312,7 +316,8 @@ fun screenTemperature(screenName: String): Temperature { SubPageStatusType.SIP_MODIFICATION, SubPageStatusType.STATUS, SubPageStatusType.INVESTMENT_GOAL_DETAILS, - SubPageStatusType.AUTO_PAY_SUCCESS -> Temperature.RED + SubPageStatusType.AUTO_PAY_SUCCESS, + SubPageStatusType.GOAL_DETAILS_SCREEN -> Temperature.RED else -> Temperature.ORANGE } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/SubPageStatusType.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/SubPageStatusType.kt index abd28549f2..c4042df470 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/SubPageStatusType.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/SubPageStatusType.kt @@ -72,4 +72,7 @@ object SubPageStatusType { const val TARGET_SETUP_PAGE = "amc_target_setup_page" const val INVESTMENT_GOAL_DETAILS = "investment_goal_details" const val ON_TIME_ASSURANCE = "on_time_assurance" + const val GOAL_SUMMARY_SCREEN = "amc_goal_based_sip_summary_screen" + const val GOAL_BASED_SIP_AMOUNT_SCREEN = "amc_goal_based_sip_amount_screen" + const val GOAL_DETAILS_SCREEN = "amc_goal_based_sip_details_screen" } diff --git a/android/navi-amc/src/main/res/layout/footer_layout.xml b/android/navi-amc/src/main/res/layout/footer_layout.xml index 2f9ecb7906..322c594136 100644 --- a/android/navi-amc/src/main/res/layout/footer_layout.xml +++ b/android/navi-amc/src/main/res/layout/footer_layout.xml @@ -15,6 +15,7 @@ app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/goal_details_screen_layout.xml b/android/navi-amc/src/main/res/layout/goal_details_screen_layout.xml new file mode 100644 index 0000000000..383dce0680 --- /dev/null +++ b/android/navi-amc/src/main/res/layout/goal_details_screen_layout.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/goal_summary_screen_layout.xml b/android/navi-amc/src/main/res/layout/goal_summary_screen_layout.xml new file mode 100644 index 0000000000..14256ad241 --- /dev/null +++ b/android/navi-amc/src/main/res/layout/goal_summary_screen_layout.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/shimmer_layout_goal_summary_screen.xml b/android/navi-amc/src/main/res/layout/shimmer_layout_goal_summary_screen.xml new file mode 100644 index 0000000000..f911096def --- /dev/null +++ b/android/navi-amc/src/main/res/layout/shimmer_layout_goal_summary_screen.xml @@ -0,0 +1,52 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/shimmer_layout_target_setup_screen.xml b/android/navi-amc/src/main/res/layout/shimmer_layout_target_setup_screen.xml new file mode 100644 index 0000000000..f1b71b42b3 --- /dev/null +++ b/android/navi-amc/src/main/res/layout/shimmer_layout_target_setup_screen.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/values/colors.xml b/android/navi-amc/src/main/res/values/colors.xml index 56c5176e14..13819e51c2 100644 --- a/android/navi-amc/src/main/res/values/colors.xml +++ b/android/navi-amc/src/main/res/values/colors.xml @@ -80,4 +80,5 @@ #6B6B6B #EBEBEB #584460 + #8F8095 \ No newline at end of file diff --git a/android/navi-base/src/main/java/com/navi/base/model/NaviClickAction.kt b/android/navi-base/src/main/java/com/navi/base/model/NaviClickAction.kt index 8dd017a67f..57e218859e 100644 --- a/android/navi-base/src/main/java/com/navi/base/model/NaviClickAction.kt +++ b/android/navi-base/src/main/java/com/navi/base/model/NaviClickAction.kt @@ -111,7 +111,7 @@ data class NaviWidgetClickWithConsent( data class NaviCheckBoxWidgetClick(val widgetId: String?, val isSelected: Boolean) : NaviClickAction() -data class JourneyType(val context: String? = null) +data class JourneyType(val context: String? = null, val parameters: List? = null) enum class CtaType(val value: String?) { NAVIGATION_ACTION("NAVIGATION_ACTION"), diff --git a/android/navi-common/src/main/java/com/navi/common/model/InvestmentBaseProperty.kt b/android/navi-common/src/main/java/com/navi/common/model/InvestmentBaseProperty.kt index e7c4d13784..14cb976d47 100644 --- a/android/navi-common/src/main/java/com/navi/common/model/InvestmentBaseProperty.kt +++ b/android/navi-common/src/main/java/com/navi/common/model/InvestmentBaseProperty.kt @@ -23,6 +23,7 @@ data class InvestmentBaseProperty( @SerializedName("spacingWeight") var spacingWeight: SpacingWeight? = null, @SerializedName("animationData") var animationData: AnimationData? = null, @SerializedName("variant") var variant: String? = null, + @SerializedName("disabledBgColor") var disabledBgColor: String? = null, ) : BaseProperty() { inner class SpacingWeight( diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt index 36dfde7c6a..8117972395 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt @@ -271,6 +271,10 @@ object Constants { const val SETUP_AUTOPAY_EXISTING_SIP = "SETUP_AUTOPAY_EXISTING_SIP" const val NULL_STRING = "null" const val NULL_STRING_CAPS = "NULL" + const val AMC_GOAL_BASED_SIP_TARGET_SETUP_SCREEN = + "amc/goal/amc_goal_based_sip_target_setup_screen" + const val AMC_GOAL_BASED_SIP_AMOUNT_SCREEN = "amc/fund/goal_based_sip_amount_screen" + const val AMC_GOAL_BASED_SIP_SUMMARY_SCREEN = "amc/fund/goal_summary_screen" /* Cta_Data */ const val KEY_CTA_DATA = "CtaData" diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt b/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt index 6bf61c4512..630893f13c 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt @@ -222,6 +222,7 @@ fun ActionData.toCtaData(): CtaData { requestCode = requestCode, needsResult = needsResult, type = type, + action = action, ) } diff --git a/android/navi-design/src/main/res/values/integers.xml b/android/navi-design/src/main/res/values/integers.xml index d765b8f5ce..6abf1718b4 100644 --- a/android/navi-design/src/main/res/values/integers.xml +++ b/android/navi-design/src/main/res/values/integers.xml @@ -25,7 +25,9 @@ 1 30 40 + 48 64 + 74 88 90 100