From 091aca192ed34826f476fc2c063c273fa85fe9ab Mon Sep 17 00:00:00 2001 From: Prajjaval Verma Date: Wed, 4 Jun 2025 20:13:49 +0530 Subject: [PATCH] NTP-58342 | Renewal Revamp (#16434) Co-authored-by: Chirayu Mor --- .../network/RenewalPlanMigrationPageApi.ts | 39 +- .../MigrationBenefitScreen.tsx | 7 +- .../RenewalPlanMigrationScreen.tsx | 7 +- .../constants/AnalyticsEventsConstant.ts | 1 + .../constants/NavigationHandlerConstants.ts | 1 + App/common/constants/StringConstant.ts | 4 +- .../widget-actions/WidgetActionHandler.ts | 39 +- .../widget-actions/WidgetActionTypes.ts | 1 + .../com/navi/base/model/NaviClickAction.kt | 5 + .../src/main/AndroidManifest.xml | 7 + .../analytics/InsuranceAnalyticsConstants.kt | 3 + .../analytics/NaviInsuranceAnalytics.kt | 12 +- .../TitleWithImageListBottomSheet.kt | 34 +- .../common/models/GiErrorMetaData.kt | 1 + .../models/IconWithListBottomSheetData.kt | 2 + .../TitleWithImageListBottomSheetData.kt | 7 +- .../common/util/NavigationHandler.kt | 1 + .../health/activity/BaseComposeActivity.kt | 364 +++++++++++++++++ .../response/AlchemistScreenDefinition.kt | 4 + .../NaviInsuranceDeeplinkNavigator.kt | 10 +- .../navi/insurance/network/ApiErrorTagType.kt | 14 + .../network/retrofit/RetrofitService.kt | 30 +- .../repository/PaymentReviewRepository.kt | 2 +- .../autopayoption/ui/PaymentReviewFragment.kt | 23 +- .../viewmodel/PaymentReviewVM.kt | 96 +++-- .../TextEditTextCalendarItemComposable.kt | 261 ++++++++++++ .../TextEditTextCalendarWidgetComposable.kt | 179 +++++++++ .../factory/ComposableWidgetFactory.kt | 9 + .../data/PurchaseComplianceRepository.kt | 2 +- .../renewal_revamp/NewRenewalActivity.kt | 127 ++++++ .../composables/CoverAmountChangeScreen.kt | 374 ++++++++++++++++++ .../composables/RenewalActivityNavHost.kt | 76 ++++ .../composables/RenewalMemberEditScreen.kt | 324 +++++++++++++++ .../RenewalMemberUnderwritingScreen.kt | 242 ++++++++++++ .../composables/RenewalReviewScreen.kt | 291 ++++++++++++++ .../composables/RenewalScreenRegistry.kt | 71 ++++ .../model/navigation/RenewalNavigationItem.kt | 59 +++ .../request/MemberDetailsPatchRequest.kt | 15 + .../request/RenewalReviewScreenRequest.kt | 59 +++ .../CoverAmountChangeScreenResponse.kt | 55 +++ .../response/RenewalMemberEditResponse.kt | 46 +++ .../RenewalMemberUnderwritingResponse.kt | 41 ++ .../response/RenewalReviewScreenResponse.kt | 55 +++ .../repo/RenewalReviewRepository.kt | 63 +++ .../CoverAmountChangeScreenComposables.kt | 150 +++++++ .../ui_components/RemoveMemberBottomsheet.kt | 128 ++++++ .../RenewalMemberEditScreenComposables.kt | 231 +++++++++++ ...ewalMemberUnderwritingScreenComposables.kt | 117 ++++++ .../RenewalReviewScreenComposable.kt | 260 ++++++++++++ .../viewmodels/CoverAmountChangeVM.kt | 251 ++++++++++++ .../viewmodels/NewRenewalActivityVM.kt | 34 ++ .../viewmodels/RenewalMemberEditVM.kt | 314 +++++++++++++++ .../viewmodels/RenewalMemberUnderwritingVM.kt | 211 ++++++++++ .../viewmodels/RenewalReviewVM.kt | 305 ++++++++++++++ .../composables/PolicyReviewScreen.kt | 8 +- .../response/PolicyReviewPageResponse.kt | 4 + .../repo/PolicyReviewRepository.kt | 2 +- .../ui_components/AddonComposable.kt | 21 +- .../TitleWithSubtitleComposable.kt | 32 +- .../java/com/navi/insurance/util/Constants.kt | 1 + .../navi/insurance/util/IntentConstants.kt | 4 + .../src/main/res/values/strings.xml | 6 +- .../naviwidgets/WidgetDataDeserializer.kt | 4 + .../java/com/navi/naviwidgets/WidgetTypes.kt | 1 + .../TextEditTextCalendarItemAdapter.kt | 37 +- .../reusable/FooterCardComposable.kt | 31 +- ...oterWithCardAndSnackBarWidgetComposable.kt | 46 ++- .../FooterWithCardAndSnackbarWidgetData.kt | 1 + .../models/TextEditTextCalendarWidget.kt | 2 + ...extEditTextCalendarWidgetComposableData.kt | 73 ++++ .../navi/naviwidgets/utils/CalendarUtil.kt | 52 +++ .../naviwidgets/utils/NaviWidgetIconUtils.kt | 2 + 72 files changed, 5259 insertions(+), 132 deletions(-) create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/health/activity/BaseComposeActivity.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarItemComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarWidgetComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/NewRenewalActivity.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/CoverAmountChangeScreen.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalActivityNavHost.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberEditScreen.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberUnderwritingScreen.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalReviewScreen.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalScreenRegistry.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/navigation/RenewalNavigationItem.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/MemberDetailsPatchRequest.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/RenewalReviewScreenRequest.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/CoverAmountChangeScreenResponse.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberEditResponse.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberUnderwritingResponse.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalReviewScreenResponse.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/repo/RenewalReviewRepository.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/CoverAmountChangeScreenComposables.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RemoveMemberBottomsheet.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberEditScreenComposables.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberUnderwritingScreenComposables.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalReviewScreenComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/CoverAmountChangeVM.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/NewRenewalActivityVM.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberEditVM.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberUnderwritingVM.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalReviewVM.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidgetComposableData.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/CalendarUtil.kt diff --git a/App/Container/Navi-Insurance/network/RenewalPlanMigrationPageApi.ts b/App/Container/Navi-Insurance/network/RenewalPlanMigrationPageApi.ts index 049ef0f033..385dda9bd1 100644 --- a/App/Container/Navi-Insurance/network/RenewalPlanMigrationPageApi.ts +++ b/App/Container/Navi-Insurance/network/RenewalPlanMigrationPageApi.ts @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction } from "react"; import { getXTargetHeaderInfo } from "../../../../network/ApiClient"; -import { post } from "../../../../network/NetworkService"; +import { patch, post } from "../../../../network/NetworkService"; import { ActionMetaData } from "../../../common/actions/GenericAction"; import { ALCHEMIST, @@ -9,7 +9,7 @@ import { ApiMethod, CacheKeyConstants, CacheValueConstants, - GI, + GI_ZORO, } from "../../../common/constants"; import { CtaData } from "../../../common/interface"; import { ScreenData } from "../../../common/interface/widgets/screenData/ScreenData"; @@ -23,6 +23,23 @@ import { import { handleResponseData } from "../../../common/widgets/widget-actions/WidgetActionHandler"; import { RenewalPlanPatchRequest } from "../screen/renewal-plan-migration-screen/types"; +export interface PatchTransitionResponseData { + cta?: CtaData | null; + applicationId?: string | null; +} + +export interface PatchTransitionRequestData { + transition?: string | null; + pageType?: string | null; + applicationId?: string | null; +} + +export interface PatchTransitionRequestBodyData { + transition?: string | null; + applicationId?: string | null; + pageType?: string | null; +} + export const getRenewalPlanMigrationPageData = async ( screenMetaData: ActionMetaData, setScreenData: Dispatch>, @@ -101,3 +118,21 @@ export const handlePatchRenewalQuote = async ({ const isBottomSheetDisabled = async () => { return await getStringPreference(CacheKeyConstants.DISABLE_BOTTOM_SHEET); }; + +export const makeTransition = async ( + applicationId?: string | null, + transition?: string | null, + pageType?: string | null, +) => { + const url = `application/${applicationId}/transition/${transition}`; + const requestBody: PatchTransitionRequestBodyData = { + transition, + applicationId, + pageType, + }; + return patch>( + url, + requestBody, + getXTargetHeaderInfo(GI_ZORO.toLocaleUpperCase()), + ); +}; diff --git a/App/Container/Navi-Insurance/screen/migration-benefit-screen/MigrationBenefitScreen.tsx b/App/Container/Navi-Insurance/screen/migration-benefit-screen/MigrationBenefitScreen.tsx index 29a6eb289b..560526c1b4 100644 --- a/App/Container/Navi-Insurance/screen/migration-benefit-screen/MigrationBenefitScreen.tsx +++ b/App/Container/Navi-Insurance/screen/migration-benefit-screen/MigrationBenefitScreen.tsx @@ -42,7 +42,7 @@ const MigrationBenefitScreen = ({ }: MigrationBenefitScreenProps) => { const navigation = useNavigation(); - const { planId, previousPlanId, preQuoteId, planType } = + const { planId, previousPlanId, preQuoteId, planType, applicationId } = extractCtaParameters(ctaData); const handleClick = (cta?: CtaData) => { @@ -67,9 +67,12 @@ const MigrationBenefitScreen = ({ planId: planId, previousPlanId: previousPlanId, preQuoteId: preQuoteId, + applicationId: applicationId, planType: planType, } as MigrationBenefitScreenRequest, - screenName: UrlConstants.MIGRATION_BENEFIT_SCREEN_URL, + screenName: applicationId + ? UrlConstants.MIGRATION_BENEFIT_SCREEN_URL_V2 + : UrlConstants.MIGRATION_BENEFIT_SCREEN_URL, }; handleActions({ diff --git a/App/Container/Navi-Insurance/screen/renewal-plan-migration-screen/RenewalPlanMigrationScreen.tsx b/App/Container/Navi-Insurance/screen/renewal-plan-migration-screen/RenewalPlanMigrationScreen.tsx index 80a521277e..2dfab9d8a9 100644 --- a/App/Container/Navi-Insurance/screen/renewal-plan-migration-screen/RenewalPlanMigrationScreen.tsx +++ b/App/Container/Navi-Insurance/screen/renewal-plan-migration-screen/RenewalPlanMigrationScreen.tsx @@ -50,13 +50,16 @@ const RenewalPlanMigrationScreen = ({ y.value = event.nativeEvent.contentOffset.y; }; - const { preQuoteId } = extractCtaParameters(ctaData); + const { preQuoteId, applicationId } = extractCtaParameters(ctaData); const data: RenewalPlanMigrationScreenRequestData = { inputMap: { preQuoteId: preQuoteId, + applicationId: applicationId, } as RenewalPlanMigrationScreenRequest, - screenName: UrlConstants.RENEWAL_PLAN_MIGRATION_SCREEN_URL, + screenName: applicationId + ? UrlConstants.RENEWAL_PLAN_MIGRATION_SCREEN_URL_V2 + : UrlConstants.RENEWAL_PLAN_MIGRATION_SCREEN_URL, }; useEffect(() => { diff --git a/App/common/constants/AnalyticsEventsConstant.ts b/App/common/constants/AnalyticsEventsConstant.ts index ca1ea8a94b..8b17499c85 100644 --- a/App/common/constants/AnalyticsEventsConstant.ts +++ b/App/common/constants/AnalyticsEventsConstant.ts @@ -76,6 +76,7 @@ export const AnalyticsMethodNameConstant = { RENEWAL_PLAN_MIGRATION_SCREEN: "gi_renewal_plan_migration_screen_error", HANDLE_CTA_CLICK: "handleCtaClick", HANDLE_CTA_CLICK_BOTTOMSHEET: "handleCtaClickBottomSheet", + TRANSITION_PATCH_CALL: "transitionPatchCall", }; export const AnalyticsGlobalErrorTypeConstant = { diff --git a/App/common/constants/NavigationHandlerConstants.ts b/App/common/constants/NavigationHandlerConstants.ts index 9ee83cacbe..8d7f31f49f 100644 --- a/App/common/constants/NavigationHandlerConstants.ts +++ b/App/common/constants/NavigationHandlerConstants.ts @@ -1,2 +1,3 @@ export const GI = "gi"; +export const GI_ZORO = "GI-ZORO"; export const ALCHEMIST = "alchemist"; diff --git a/App/common/constants/StringConstant.ts b/App/common/constants/StringConstant.ts index 345d6d4f28..f4001805f9 100644 --- a/App/common/constants/StringConstant.ts +++ b/App/common/constants/StringConstant.ts @@ -81,7 +81,9 @@ export const FontMapping = { export const UrlConstants = { MIGRATION_BENEFIT_SCREEN_URL: "GI_MIGRATION_BENEFIT", - RENEWAL_PLAN_MIGRATION_SCREEN_URL: "GI_RENEWAL_PLAN_MIGRATION_SCREEN", + MIGRATION_BENEFIT_SCREEN_URL_V2: "GI_MIGRATION_BENEFIT_V2", + RENEWAL_PLAN_MIGRATION_SCREEN_URL: "GI_RENEWAL_PLAN_MIGRATION_SCREEN_V2", + RENEWAL_PLAN_MIGRATION_SCREEN_URL_V2: "GI_RENEWAL_PLAN_MIGRATION_SCREEN_V2", }; export enum CacheKeyConstants { diff --git a/App/common/widgets/widget-actions/WidgetActionHandler.ts b/App/common/widgets/widget-actions/WidgetActionHandler.ts index d4e0f47670..10d45c2e62 100644 --- a/App/common/widgets/widget-actions/WidgetActionHandler.ts +++ b/App/common/widgets/widget-actions/WidgetActionHandler.ts @@ -1,7 +1,9 @@ import { Dispatch, SetStateAction } from "react"; import { + PatchTransitionRequestData, SumInsuredRequestData, handlePatchRenewalQuote, + makeTransition, updateSumInsuredData, } from "../../../Container/Navi-Insurance/network"; @@ -10,7 +12,7 @@ import { GenericActionPayload, TargetWidgetPayload, } from "../../actions/GenericAction"; -import { ApiMethod } from "../../constants"; +import { ApiMethod, ConstantCta } from "../../constants"; import { AnalyticsEventNameConstants, AnalyticsFlowNameConstant, @@ -219,6 +221,41 @@ const WidgetActionHandler = { return; } + case WidgetActionTypes.TRANSITION_API: { + setScreenData({ + ...screenData, + screenState: ScreenState.OVERLAY, + }); + let applicationId = + getApplicationIdFromCta(ctaData) ?? + getApplicationFromScreenMetaData(screenData?.screenMetaData); + let transition = (widgetMetaData?.data as PatchTransitionRequestData) + .transition; + let pageType = (widgetMetaData?.data as PatchTransitionRequestData) + .pageType; + const url = `/application/${applicationId}/transition/${transition}`; + return makeTransition(applicationId, transition, pageType) + .then(response => { + handleResponseData( + response?.data?.cta ?? ConstantCta.STATIC_HEADER_LEFT_ICON_CTA, + setScreenData, + screenData, + ); + }) + .catch(error => { + handleErrorData( + error, + setScreenData, + widgetMetaData, + AnalyticsMethodNameConstant.TRANSITION_PATCH_CALL, + AnalyticsFlowNameConstant.GI_RN_RENEWAL_PLAN_MIGRATION, + screenData, + url, + ApiMethod.PATCH, + ); + }); + } + default: { return; } diff --git a/App/common/widgets/widget-actions/WidgetActionTypes.ts b/App/common/widgets/widget-actions/WidgetActionTypes.ts index bcdc0a0dae..a966da7561 100644 --- a/App/common/widgets/widget-actions/WidgetActionTypes.ts +++ b/App/common/widgets/widget-actions/WidgetActionTypes.ts @@ -9,4 +9,5 @@ export const WidgetActionTypes = { FINAL_PATCH_CALL: "FINAL_PATCH_CALL", ANALYTIC_ACTION: "ANALYTIC_ACTION", PATCH_RENEWAL_QUOTE: "PATCH_RENEWAL_QUOTE", + TRANSITION_API: "TRANSITION_API", }; 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 57e218859e..3ce1eb225b 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 @@ -183,6 +183,11 @@ enum class CtaType(val value: String?) { OPT_ADDON("OPT_ADDON"), RESEND_OTP("RESEND_OTP"), SUBMIT_OTP("SUBMIT_OTP"), + CANCEL_REMOVAL("CANCEL_REMOVAL"), + CONFIRM_REMOVAL("CONFIRM_REMOVAL"), + DELETE_ADDED_MEMBER("DELETE_ADDED_MEMBER"), + ADD_DELETED_MEMBER("ADD_DELETED_MEMBER"), + OPEN_BOTTOM_SHEET("OPEN_BOTTOM_SHEET"), } enum class CtaConstants(val value: String?) { diff --git a/android/navi-insurance/src/main/AndroidManifest.xml b/android/navi-insurance/src/main/AndroidManifest.xml index c923ab86ac..148cda7ec5 100644 --- a/android/navi-insurance/src/main/AndroidManifest.xml +++ b/android/navi-insurance/src/main/AndroidManifest.xml @@ -294,6 +294,13 @@ android:windowSoftInputMode="adjustResize" android:theme="@style/GiAppThemeWhiteStatusBar" /> + + ? = null, + eventProperties: Map? = null, isNeededForAppsflyer: Boolean = false, isNeededForFirebase: Boolean = false, ) { val updatedEventProperties = mutableMapOf() - eventProperties?.let { updatedEventProperties.putAll(it) } + eventProperties?.forEach { (key, value) -> + if (value != null) { + updatedEventProperties[key] = value + } + } // Passing device ID for every event updatedEventProperties[InsuranceAnalyticsConstants.DEVICE_ID] = BaseUtils.getDeviceId(applicationContext = AppServiceManager.application) diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithImageListBottomSheet.kt b/android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithImageListBottomSheet.kt index c59c5bc5e3..6c857c699e 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithImageListBottomSheet.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithImageListBottomSheet.kt @@ -144,12 +144,14 @@ class TitleWithImageListBottomSheet : BaseBottomSheet(), WidgetCallback { state = FooterButtonState.ENABLED.name, widgetCallback = widgetCallback, ) - FooterButtonComposable( - data = data?.secondaryFooterButton, - modifier = Modifier.padding(bottom = 0.dp).height(48.dp), - state = FooterButtonState.ENABLED.name, - widgetCallback = widgetCallback, - ) + data?.secondaryFooterButton?.title?.text?.let { + FooterButtonComposable( + data = data.secondaryFooterButton, + modifier = Modifier.height(48.dp), + state = FooterButtonState.ENABLED.name, + widgetCallback = widgetCallback, + ) + } } } @@ -177,15 +179,23 @@ class TitleWithImageListBottomSheet : BaseBottomSheet(), WidgetCallback { modifier = Modifier.size(28.dp), ) Spacer(modifier = Modifier.width(12.dp)) - NaviTextWidgetized( - textFieldData = item?.itemTitle, - modifier = Modifier.width(0.dp).weight(0.7f), - widgetCallback = widgetCallback, - ) + Column(modifier = Modifier.weight(0.7f)) { + NaviTextWidgetized( + textFieldData = item?.itemTitle, + widgetCallback = widgetCallback, + ) + item?.itemBottomTitle?.let { + NaviTextWidgetized( + textFieldData = it, + modifier = Modifier.padding(top = 4.dp), + widgetCallback = widgetCallback, + ) + } + } Spacer(modifier = Modifier.width(12.dp)) NaviTextWidgetized( textFieldData = item?.itemSubtitle, - modifier = Modifier.width(0.dp).weight(0.3f), + modifier = Modifier.weight(0.3f), widgetCallback = widgetCallback, ) } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/common/models/GiErrorMetaData.kt b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/GiErrorMetaData.kt index 6df033100d..14ec6f25f3 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/common/models/GiErrorMetaData.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/GiErrorMetaData.kt @@ -60,5 +60,6 @@ data class GiErrorMetaData( const val FLOW_PURCHASE_COMPLIANCE_JOURNEY = "purchase_compliance_journey" const val FLOW_POLICY_REVIEW = "policy_review" const val FLOW_OTP_CAPTCHA = "otp_captcha" + const val FLOW_RENEWAL_V2 = "renewal_v2" } } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/common/models/IconWithListBottomSheetData.kt b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/IconWithListBottomSheetData.kt index 51e4a0cc94..6f09532b5f 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/common/models/IconWithListBottomSheetData.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/IconWithListBottomSheetData.kt @@ -16,11 +16,13 @@ import com.navi.naviwidgets.models.response.TextFieldData data class IconWithListBottomSheetData( @SerializedName("topIcon") val topIcon: ImageFieldData? = null, + @SerializedName("topRightIcon") val topRightIcon: ImageFieldData? = null, @SerializedName("contentTitle") val contentTitle: TextFieldData? = null, @SerializedName("items") val items: List? = null, @SerializedName("box") val box: BoxData? = null, @SerializedName("footerButton") val footerButton: FooterButtonData? = null, @SerializedName("secondaryFooterButton") val secondaryFooterButton: FooterButtonData? = null, + @SerializedName("isFooterVertical") val isFooterVertical: Boolean? = null, @SerializedName("metaData") val metaData: PageMetaData? = null, ) { data class BoxData( diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithImageListBottomSheetData.kt b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithImageListBottomSheetData.kt index 3a7e9e6c9c..505c55c15d 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithImageListBottomSheetData.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithImageListBottomSheetData.kt @@ -21,7 +21,10 @@ data class TitleWithImageListBottomSheetData( ) { data class TextWithImageItem( @SerializedName("icon") val icon: ImageFieldData? = null, - @SerializedName("itemTitle") val itemTitle: TextFieldData? = null, - @SerializedName("itemSubtitle") val itemSubtitle: TextFieldData? = null, + @SerializedName("itemTitle", alternate = ["name"]) val itemTitle: TextFieldData? = null, + @SerializedName("itemSubtitle", alternate = ["age"]) + val itemSubtitle: TextFieldData? = null, + @SerializedName("itemBottomTitle", alternate = ["relation"]) + val itemBottomTitle: TextFieldData? = null, ) } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/common/util/NavigationHandler.kt b/android/navi-insurance/src/main/java/com/navi/insurance/common/util/NavigationHandler.kt index 0ba3f7993f..b2fa3c9fba 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/common/util/NavigationHandler.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/common/util/NavigationHandler.kt @@ -419,6 +419,7 @@ class NavigationHandler @Inject constructor(private val uiControllerUtil: UiCont const val ICON_WITH_LIST_BOTTOM_SHEET = "icon_with_list_bottomsheet" const val TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET = "title_with_permission_list_bottomsheet" const val TITLE_WITH_IMAGE_LIST_BOTTOM_SHEET = "title_with_image_list_bottom_sheet" + const val HEADER_WITH_ICON_WITH_LIST_BOTTOM_SHEET = "header_with_icon_with_list_bottomsheet" const val POLICY_DETAILS_SCREEN = "policy_details_screen" const val POLICY_SELECTOR_BOTTOMSHEET = "policy_selector_bottomsheet" const val AHC_ODC_TABULAR_BOTTOMSHEET = "ahc_odc_tabular_bottomsheet" diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/health/activity/BaseComposeActivity.kt b/android/navi-insurance/src/main/java/com/navi/insurance/health/activity/BaseComposeActivity.kt new file mode 100644 index 0000000000..868565ee27 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/health/activity/BaseComposeActivity.kt @@ -0,0 +1,364 @@ +/* + * + * * Copyright © 2019-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.health.activity + +import android.content.Context +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavHostController +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.install.InstallStateUpdatedListener +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.deeplink.DeepLinkManager +import com.navi.base.model.CtaData +import com.navi.base.utils.ConnectivityObserver +import com.navi.base.utils.orFalse +import com.navi.base.utils.orTrue +import com.navi.base.utils.orZero +import com.navi.base.utils.toLongWithSafe +import com.navi.common.constants.PROPERTY_EXCEPTION +import com.navi.common.constants.VENDOR_NAVI_API +import com.navi.common.model.ModuleName +import com.navi.common.model.ModuleNameV2 +import com.navi.common.network.models.ErrorMessage +import com.navi.common.ui.activity.NaviCoreActivity +import com.navi.common.ui.dialog.NaviLockScreenDialog +import com.navi.common.uitron.util.UiTronDependencyProvider +import com.navi.common.utils.BiometricPromptUtils +import com.navi.common.utils.BiometricPromptUtils.Companion.getScreenLockEventName +import com.navi.common.utils.CommonNaviAnalytics +import com.navi.common.utils.CommonUtils.formatComposeScreenName +import com.navi.common.utils.Constants +import com.navi.common.utils.getCtaObjectFromString +import com.navi.common.utils.getNetworkType +import com.navi.common.utils.getSessionId +import com.navi.common.utils.isDead +import com.navi.common.utils.isSessionExpired +import com.navi.common.utils.log +import com.navi.common.utils.updateSessionId +import com.navi.insurance.R +import com.navi.insurance.analytics.InsuranceAnalyticsHandler +import com.navi.insurance.analytics.NaviInsuranceAnalytics +import com.navi.insurance.health.fragment.BaseFragment +import com.navi.insurance.health.fragment.ErrorFragment +import com.navi.insurance.health.viewmodel.BaseVM +import com.navi.insurance.util.ERROR_MESSAGE +import com.navi.insurance.util.getGlobalErrorType +import com.navi.uitron.UiTronSdkManager +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber + +private val fragmentLifecycleCallbacks = + object : FragmentManager.FragmentLifecycleCallbacks() { + + override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) { + super.onFragmentAttached(fm, f, context) + Timber.d("onFragmentAttached : ${f::class.java}") + } + + override fun onFragmentDetached(fm: FragmentManager, f: Fragment) { + super.onFragmentDetached(fm, f) + Timber.d("onFragmentDetached : ${f::class.java}") + } + } + +abstract class BaseComposeActivity : NaviCoreActivity() { + + protected var appUpdateManager: AppUpdateManager? = null + protected lateinit var listener: InstallStateUpdatedListener + private var timeStamp = System.currentTimeMillis() + private var lockScreenDialog: NaviLockScreenDialog? = null + @Inject lateinit var connectivityObserver: ConnectivityObserver + protected val connectivityStateFlow by lazy { connectivityObserver.observe() } + + private val biometricPromptUtils by lazy { BiometricPromptUtils() } + + override val moduleName: ModuleNameV2 + get() = ModuleNameV2.Insurance + + open fun getViewModel(): BaseVM? = null + + private val analyticsHandler = InsuranceAnalyticsHandler() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true) + analyticsHandler.setCurrentScreen(screenName) + getViewModel()?.setAnalyticsHandler(analyticsHandler) + initUiTronSdkManager() + handleRedirection() + sendInitEvent() + } + + private fun sendInitEvent() { + if (screenName.isNotBlank()) { + NaviInsuranceAnalytics.postAnalyticsEvent( + "HI_event_init_screen_$screenName", + mapOf( + Pair("type", "activity"), + Pair("screen", screenName), + Pair("vertical", ModuleNameV2.Insurance.name), + ), + ) + } + } + + private fun initUiTronSdkManager() { + if (!UiTronSdkManager.isInitialized()) { + UiTronSdkManager.init(UiTronDependencyProvider(applicationContext)) + } + } + + private fun handleRedirection() { + try { + intent + ?.extras + ?.getString(Constants.KEY_REDIRECTION_CTA) + .getCtaObjectFromString() + ?.let { ctaData -> + lifecycleScope.launch(Dispatchers.Main) { + delay( + intent + .getStringExtra(Constants.KEY_REDIRECTION_DELAY) + ?.toLongWithSafe() + .orZero() + ) + sendAnalyticsEvent(ctaData) + DeepLinkManager.getDeepLinkListener() + ?.navigateTo( + activity = this@BaseComposeActivity, + ctaData = ctaData, + finish = ctaData.finish.orFalse(), + bundle = null, + clearTask = ctaData.clearTask.orFalse(), + ) + } + } + } catch (e: Exception) { + e.log() + } + } + + private fun sendAnalyticsEvent(ctaData: CtaData) { + ctaData.analyticsEventProperties?.let { analyticsEvent -> + val properties = mutableMapOf() + analyticsEvent.properties?.let { properties.putAll(it) } + NaviInsuranceAnalytics.postAnalyticsEvent( + eventName = analyticsEvent.name.orEmpty(), + eventProperties = properties.toMap(), + ) + } + } + + override fun onDestroy() { + super.onDestroy() + supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) + } + + companion object { + const val GI_REQUEST_CODE = 113 + } + + override fun onResume() { + super.onResume() + biometricPromptUtils.checkAndShowBiometricPrompt( + activity = this, + showLockScreenDialog = { showLockScreenDialog() }, + hideLockScreenDialog = { hideLockScreenDialog() }, + ) + if (isSessionExpired(timeStamp)) { + updateSessionId(applicationContext) + NaviTrackEvent.setSessionId(getSessionId().orEmpty()) + } + timeStamp = System.currentTimeMillis() + } + + fun onNavControllerSet(baseNavController: NavHostController) { + baseNavController.addOnDestinationChangedListener { _, destination, _ -> + destination.route?.let { originalRoute -> + originalRoute.formatComposeScreenName(moduleName.name).let { composeScreenName -> + NaviInsuranceAnalytics.postAnalyticsEvent( + "HI_event_init_screen_$composeScreenName", + mapOf( + Pair("type", "activity"), + Pair("screen", screenName), + Pair("vertical", ModuleNameV2.Insurance.name), + ), + ) + } + } + } + } + + private fun showLockScreenDialog() { + NaviTrackEvent.trackEventOnClickStream( + eventName = getScreenLockEventName(eventType = ::showLockScreenDialog.name) + ) + if ( + lockScreenDialog?.isAdded.orFalse().not() && lockScreenDialog?.isVisible.orFalse().not() + ) { + try { + if (isDead(this)) return + lockScreenDialog?.safelyDismissDialog() + val ft = supportFragmentManager.beginTransaction() + val prev = supportFragmentManager.findFragmentByTag("LockScreenDialog") + if ( + prev != null && + !supportFragmentManager.isStateSaved && + !supportFragmentManager.isDestroyed + ) { + ft.remove(prev).commitAllowingStateLoss() + } + lockScreenDialog = + NaviLockScreenDialog.getInstance( + onUnlock = { + NaviTrackEvent.trackEventOnClickStream( + eventName = + getScreenLockEventName(eventType = "dialog_unlock_clicked") + ) + biometricPromptUtils.showBiometricPrompt( + this@BaseComposeActivity, + onSuccess = { hideLockScreenDialog() }, + onFailed = { showLockScreenDialog() }, + ) + } + ) + lockScreenDialog?.isCancelable = false + lockScreenDialog?.apply { + ft.add(this, "LockScreenDialog") + ft.commitAllowingStateLoss() + } + } catch (e: Exception) { + Timber.e(e) + } + } + } + + private fun hideLockScreenDialog() { + NaviTrackEvent.trackEventOnClickStream( + eventName = getScreenLockEventName(eventType = ::hideLockScreenDialog.name) + ) + if (!isDead(this) && lockScreenDialog?.isAdded.orFalse()) { + if (!supportFragmentManager.isStateSaved && !supportFragmentManager.isDestroyed) { + lockScreenDialog?.safelyDismissDialog() + lockScreenDialog = null + } + } + } + + override fun onPause() { + super.onPause() + timeStamp = System.currentTimeMillis() + } + + private lateinit var errorFragment: ErrorFragment + + fun tryHandleError( + err: ErrorMessage, + fragment: BaseFragment?, + actionMessage: String? = null, + containerId: Int? = R.id.form_fragment, + screenName: String?, + actionCallback: (() -> Unit)? = null, + ): Boolean { + + // send data to click stream + val errorMap: MutableMap = + mutableMapOf(Pair("error_data", err.errors?.first().toString())) + if (err.errorTag?.isNotEmpty().orFalse()) { + errorMap["error_tag"] = err.errorTag ?: "" + } + if (err.exception?.isNotEmpty().orFalse()) { + errorMap["exception"] = err.exception ?: "" + } + + val analyticsTracker = CommonNaviAnalytics.naviAnalytics.GiError() + + if (getGlobalErrorType(err.statusCode) == CommonNaviAnalytics.GLOBAL_GENERIC_ERRORS) { + analyticsTracker.onGlobalError( + err.message, + screenName ?: this.screenName, + ModuleName.GI.name.lowercase(), + CommonNaviAnalytics.GLOBAL_GENERIC_ERRORS, + err.statusCode, + getNetworkType(applicationContext), + "${err.flowName}", + "${err.errorTag}", + VENDOR_NAVI_API, + mutableMapOf(Pair(ERROR_MESSAGE, errorMap.toString())).apply { + err.exception?.let { put(PROPERTY_EXCEPTION, it) } + put(com.navi.insurance.util.Constants.ERROR_INFO, err.errors.toString()) + err.extras?.let { putAll(it) } + }, + err.isDownTime.orFalse(), + ) + + if (supportFragmentManager.findFragmentByTag("error_fragment") == null) { + errorFragment = ErrorFragment() + errorFragment.init(err, actionMessage, actionCallback) + var transaction = + supportFragmentManager + .beginTransaction() + .setCustomAnimations( + R.anim.navi_insurance_fade_in, + R.anim.navi_insurance_fade_out, + ) + if (fragment != null && fragment.isAdded) { + transaction = transaction.hide(fragment) + } + transaction + .add(containerId ?: R.id.form_fragment, errorFragment, "error_fragment") + .addToBackStack("error_fragment") + .commit() + } else { + supportFragmentManager.beginTransaction().show(errorFragment) + } + + return true + } else { + analyticsTracker.onGlobalError( + err.message, + screenName ?: this.screenName, + ModuleName.GI.name.lowercase(), + CommonNaviAnalytics.GLOBAL_INTERNAL_ERRORS, + err.statusCode, + getNetworkType(applicationContext), + "${err.flowName}", + "${err.errorTag}", + VENDOR_NAVI_API, + mutableMapOf( + Pair(ERROR_MESSAGE, errorMap.toString()), + Pair(com.navi.insurance.util.Constants.ERROR_INFO, err.errors.toString()), + ) + .apply { + err.exception?.let { put(PROPERTY_EXCEPTION, it) } + err.extras?.let { putAll(it) } + }, + err.isDownTime.orFalse(), + ) + } + return false + } + + override fun onStop() { + appUpdateManager?.unregisterListener(listener) + super.onStop() + } + + fun safelyShowBottomSheet(bottomSheet: BottomSheetDialogFragment, tag: String) { + if (isFinishing.orTrue()) return + if (!supportFragmentManager.isDestroyed && !supportFragmentManager.isStateSaved) + bottomSheet.show(supportFragmentManager, tag) + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/models/response/AlchemistScreenDefinition.kt b/android/navi-insurance/src/main/java/com/navi/insurance/models/response/AlchemistScreenDefinition.kt index fa06a01253..46051a11fd 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/models/response/AlchemistScreenDefinition.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/models/response/AlchemistScreenDefinition.kt @@ -21,4 +21,8 @@ enum class AlchemistScreenName { GI_HEALTH_DECLARATION_REVIEW, GI_PRE_MANDATE_SCREEN, GI_POLICY_DETAILS_SCREEN, + GI_RENEWAL_REVIEW_SCREEN, + GI_MEMBER_EDIT_SCREEN, + GI_COVER_CHANGE_SCREEN, + GI_RENEWAL_UNDERWRITING_SCREEN, } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/navigator/NaviInsuranceDeeplinkNavigator.kt b/android/navi-insurance/src/main/java/com/navi/insurance/navigator/NaviInsuranceDeeplinkNavigator.kt index 8e69696987..6c05843250 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/navigator/NaviInsuranceDeeplinkNavigator.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/navigator/NaviInsuranceDeeplinkNavigator.kt @@ -92,6 +92,7 @@ import com.navi.insurance.common.util.NavigationHandler.Companion.GI_CLAIMS_BOTT import com.navi.insurance.common.util.NavigationHandler.Companion.GI_SURVEY_BOTTOMSHEET import com.navi.insurance.common.util.NavigationHandler.Companion.HEADER_LINE_ITEM_BOTTOMSHEET import com.navi.insurance.common.util.NavigationHandler.Companion.HEADER_WITH_ICON_CONTENT_FOOTER_BOTTOM_SHEET +import com.navi.insurance.common.util.NavigationHandler.Companion.HEADER_WITH_ICON_WITH_LIST_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.HEADER_WITH_ITEMS_AND_FOOTER_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.ICON_WITH_LIST_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.LOTTIE_WITH_TITLE_BOTTOM_SHEET @@ -149,6 +150,7 @@ import com.navi.insurance.purchase.compliance.ui.PurchaseComplianceActivity import com.navi.insurance.quoteredesign.QuoteActivity import com.navi.insurance.quoteredesign.fragments.QuotePremiumDetailsBottomSheet import com.navi.insurance.renewal.RenewalActivity +import com.navi.insurance.renewal_revamp.NewRenewalActivity import com.navi.insurance.review_policy.PolicyReviewActivity import com.navi.insurance.util.Constants.CTA_DATA import com.navi.insurance.util.Constants.FROM_DEEP_LINK @@ -185,6 +187,7 @@ object NaviInsuranceDeeplinkNavigator { const val SUPER_APP_DASHBOARD = "super_app_dashboard" const val GI_ROOT_URL = "gi/" const val ANNUAL_RENEWAL = "annual_renewal" + const val RENEWAL = "renewal" const val EMI_CALENDAR = "emi-calendar" const val EIA_DETAILS = "eia_detail" const val HEALTH_CARDS = "health_card" @@ -679,6 +682,10 @@ object NaviInsuranceDeeplinkNavigator { intent = Intent(activity, OtpCaptchaActivity::class.java) bundle.putParcelable(KEY_CTA_DATA, ctaData) } + RENEWAL -> { + intent = Intent(activity, NewRenewalActivity::class.java) + bundle.putParcelable(KEY_CTA_DATA, ctaData) + } } ctaData.parameters?.forEach { keyValue -> @@ -773,7 +780,8 @@ object NaviInsuranceDeeplinkNavigator { Pair(IconWithListBottomSheet.TAG, IconWithListBottomSheet()) TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET -> Pair(TitleWithPermissionListBottomSheet.TAG, TitleWithPermissionListBottomSheet()) - TITLE_WITH_IMAGE_LIST_BOTTOM_SHEET -> + TITLE_WITH_IMAGE_LIST_BOTTOM_SHEET, + HEADER_WITH_ICON_WITH_LIST_BOTTOM_SHEET -> Pair(TitleWithImageListBottomSheet.TAG, TitleWithImageListBottomSheet()) POLICY_SELECTOR_BOTTOMSHEET -> Pair(PolicySelectorBottomsheet.TAG, PolicySelectorBottomsheet()) diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/network/ApiErrorTagType.kt b/android/navi-insurance/src/main/java/com/navi/insurance/network/ApiErrorTagType.kt index f79597457a..2ad38ad86b 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/network/ApiErrorTagType.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/network/ApiErrorTagType.kt @@ -184,4 +184,18 @@ enum class ApiErrorTagType(val value: String) { OTP_CAPTCHA_SUBMIT_RESPONSE_ERROR("OTP_CAPTCHA_SUBMIT_RESPONSE_ERROR"), CAPTCHA_TOKEN_GENERATION_ERROR("CAPTCHA_TOKEN_GENERATION_ERROR"), HEALTH_CARD_SCREEN_LOAD_ERROR("HEALTH_CARD_SCREEN_LOAD_ERROR"), + RENEWAL_REVIEW_SCREEN_LOAD_ERROR("RENEWAL_REVIEW_SCREEN_LOAD_ERROR"), + RENEWAL_REVIEW_ADDON_LOAD_ERROR("RENEWAL_REVIEW_ADDON_LOAD_ERROR"), + RENEWAL_REVIEW_NEXT_PAGE_TRANSITION("RENEWAL_REVIEW_NEXT_PAGE_TRANSITION"), + RENEWAL_COVER_AMOUNT_CHANGE_SCREEN_LOAD_ERROR("RENEWAL_COVER_AMOUNT_CHANGE_SCREEN_LOAD_ERROR"), + RENEWAL_COVER_AMOUNT_CHANGE_NEXT_PAGE_TRANSITION( + "RENEWAL_COVER_AMOUNT_CHANGE_NEXT_PAGE_TRANSITION" + ), + RENEWAL_EDIT_MEMBER_SCREEN_LOAD_ERROR("RENEWAL_COVER_AMOUNT_CHANGE_SCREEN_LOAD_ERROR"), + RENEWAL_EDIT_MEMBER_NEXT_PAGE_TRANSITION("RENEWAL_COVER_AMOUNT_CHANGE_NEXT_PAGE_TRANSITION"), + RENEWAL_MEMBER_UNDERWRITING_SCREEN_LOAD_ERROR("RENEWAL_MEMBER_UNDERWRITING_SCREEN_LOAD_ERROR"), + RENEWAL_MEMBER_UNDERWRITING_NEXT_PAGE_TRANSITION( + "RENEWAL_MEMBER_UNDERWRITING_NEXT_PAGE_TRANSITION" + ), + ANNUAL_RENEWAL_PAYMENT_TRANSITION_ERROR("ANNUAL_RENEWAL_PAYMENT_TRANSITION_ERROR"), } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/network/retrofit/RetrofitService.kt b/android/navi-insurance/src/main/java/com/navi/insurance/network/retrofit/RetrofitService.kt index c0e1d0f4ec..c25bbfdf78 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/network/retrofit/RetrofitService.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/network/retrofit/RetrofitService.kt @@ -164,6 +164,10 @@ import com.navi.insurance.purchase.compliance.domain.dto.PatchRequestData import com.navi.insurance.purchase.compliance.domain.dto.PatchResponseData import com.navi.insurance.purchase.compliance.domain.dto.PinCodeResponseData import com.navi.insurance.purchase.compliance.domain.dto.RemoveNomineeData +import com.navi.insurance.renewal_revamp.model.response.CoverAmountChangeScreenResponse +import com.navi.insurance.renewal_revamp.model.response.RenewalMemberEditResponse +import com.navi.insurance.renewal_revamp.model.response.RenewalMemberUnderwritingResponse +import com.navi.insurance.renewal_revamp.model.response.RenewalReviewScreenResponse import com.navi.insurance.review_policy.model.response.HealthDeclarationReviewResponse import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse import com.navi.insurance.static_digital_claim.model.DigitalClaimScreenResponse @@ -1385,7 +1389,7 @@ interface RetrofitService { ): Response>> @PATCH("/application/{applicationId}/transition/{transition}") - suspend fun fetchPurchaseComplianceNextPage( + suspend fun fetchApplicationTransitionResponse( @Header("X-Target") xTarget: String = GI_ZORO, @Path("applicationId") applicationId: String, @Path("transition") transition: String, @@ -1420,4 +1424,28 @@ interface RetrofitService { @Header("X-Target") xTarget: String = ALCHEMIST, @Body request: AlchemistScreenRequest?, ): Response>> + + @POST("alchemist/inflate") + suspend fun fetchRenewalReviewScreen( + @Header("X-Target") xTarget: String = ALCHEMIST, + @Body request: AlchemistScreenRequest?, + ): Response>> + + @POST("alchemist/inflate") + suspend fun fetchCoverAmountChangeScreen( + @Header("X-Target") xTarget: String = ALCHEMIST, + @Body request: AlchemistScreenRequest?, + ): Response>> + + @POST("alchemist/inflate") + suspend fun fetchMemberEditScreen( + @Header("X-Target") xTarget: String = ALCHEMIST, + @Body request: AlchemistScreenRequest?, + ): Response>> + + @POST("alchemist/inflate") + suspend fun fetchMemberUnderwritingScreen( + @Header("X-Target") xTarget: String = ALCHEMIST, + @Body request: AlchemistScreenRequest?, + ): Response>> } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/repository/PaymentReviewRepository.kt b/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/repository/PaymentReviewRepository.kt index 3ad4c5b591..6231144ebb 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/repository/PaymentReviewRepository.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/repository/PaymentReviewRepository.kt @@ -62,7 +62,7 @@ class PaymentReviewRepository @Inject constructor(private val retrofitService: R transition: String, ): RepoResult { return giResponseCallback( - retrofitService.fetchPurchaseComplianceNextPage( + retrofitService.fetchApplicationTransitionResponse( request = request, applicationId = applicationId, transition = transition, diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/ui/PaymentReviewFragment.kt b/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/ui/PaymentReviewFragment.kt index 9b6ffc237a..e8a6fcb3a6 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/ui/PaymentReviewFragment.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/ui/PaymentReviewFragment.kt @@ -99,6 +99,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback { private var amount = "0" private var policyId: String? = null private var pageType: String? = null + private var transition: String? = null @Inject lateinit var idProvider: IdProvider @@ -142,6 +143,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback { planId = arguments?.getString(PLAN_ID) preQuoteId = arguments?.getString(PRE_QUOTE_ID) pageType = arguments?.getString(PAGE_TYPE) + transition = arguments?.getString(ARG_TRANSITION) applicationType = arguments?.getString(APPLICATION_TYPE_EXTRA) ?: run { activity?.intent?.getStringExtra(APPLICATION_TYPE_EXTRA) } @@ -263,7 +265,15 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback { } } .launchIn(viewLifecycleOwner.lifecycleScope) - viewModel.fetchPaymentReview(id, paymentFlowIdentifier, applicationId, preQuoteId, planId) + viewModel.fetchPaymentReview( + id = id, + paymentFlowIdentifier = paymentFlowIdentifier, + applicationId = applicationId, + preQuoteId = preQuoteId, + planId = planId, + transition = transition, + pageType = pageType, + ) } private fun handleSuccessResponse() { @@ -467,6 +477,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback { NaviInsuranceDeeplinkNavigator.navigate( activity = activity, ctaData = naviClickAction, + finish = naviClickAction.finish.orFalse(), bundle = bundle, callbackHandler = object : RequestToCallbackHandler { @@ -578,6 +589,16 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback { } } } + lifecycleScope.launch { + viewModel.annualRenewalTransitionFlow.collect { transitionResponse -> + when (transitionResponse) { + is ResponseState.Success -> { + transitionResponse.value.cta?.let { onClick(it, null) } + } + else -> {} + } + } + } } private fun initPaymentSDK(requestId: String, token: String, nextPageCta: CtaData?) { diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/viewmodel/PaymentReviewVM.kt b/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/viewmodel/PaymentReviewVM.kt index 1e8263bb5b..34c41cfae6 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/viewmodel/PaymentReviewVM.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/paymentreview/autopayoption/viewmodel/PaymentReviewVM.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope import com.navi.base.model.CtaData import com.navi.base.model.LineItem import com.navi.base.utils.isNotNull +import com.navi.base.utils.isNotNullAndNotEmpty import com.navi.base.utils.isNull import com.navi.common.ResponseState import com.navi.common.alchemist.model.AlchemistScreenRequest @@ -34,6 +35,7 @@ import com.navi.insurance.util.ARG_APPLICATION_ID import com.navi.insurance.util.ARG_TRANSITION import com.navi.insurance.util.ID import com.navi.insurance.util.PAGE_TYPE +import com.navi.insurance.util.PLAN_ID import com.navi.insurance.util.PaymentFlowIdentifier import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -67,6 +69,11 @@ constructor( val paymentTransitionFlow: StateFlow> get() = _paymentTransitionFlow.asStateFlow() + private val _annualRenewalTransitionFlow = + MutableStateFlow>(ResponseState.Idle) + val annualRenewalTransitionFlow: StateFlow> + get() = _annualRenewalTransitionFlow.asStateFlow() + var paymentAction: CtaData? = null private fun exceptionHandler(errorTag: String) = CoroutineExceptionHandler { _, exception -> @@ -86,11 +93,13 @@ constructor( it.copy(isLoading = false, data = null, hasErrorOccurred = true) } } + ApiErrorTagType.PRE_MANDATE_SCREEN_LOAD_ERROR.value -> { _preMandateFlow.update { it.copy(isLoading = false, data = null, hasErrorOccurred = true) } } + ApiErrorTagType.PRE_MANDATE_SCREEN_TRANSITION_ERROR.value -> { _paymentTransitionFlow.emit(ResponseState.Failure("Something went wrong")) } @@ -104,6 +113,8 @@ constructor( applicationId: String? = null, preQuoteId: String? = null, planId: String? = null, + transition: String? = null, + pageType: String? = null, ) { viewModelScope.launch( dispatcher.io + exceptionHandler(ApiErrorTagType.PAYMENT_REVIEW_SCREEN_LOAD_ERROR.value) @@ -120,35 +131,72 @@ constructor( screen = "paymentReviewScreen", isNae = { !it.isSuccessWithData() }, ) - val response = - repository.generateRenewalQuote(preQuoteId!!, renewalQuoteRequest, metricInfo) if ( - response.error.isNull() && - response.errors.isNullOrEmpty() && - response.data.isNotNull() + planId.isNotNullAndNotEmpty() && + applicationId.isNotNullAndNotEmpty() && + transition.isNotNullAndNotEmpty() ) { - response.data?.let { - val updatedId = getParameterFromCta(response.data?.cta, ID) - getPaymentReviewScreen(updatedId, paymentFlowIdentifier, applicationId) + val response = + repository.fetchNextPage( + PatchRequestData( + pageType = pageType.orEmpty(), + applicationId = applicationId.orEmpty(), + transition = transition.orEmpty(), + data = listOf(LineItem(PLAN_ID, planId.orEmpty())), + ), + applicationId = applicationId.orEmpty(), + transition = transition.orEmpty(), + ) + if (response.isSuccessWithData()) { + _annualRenewalTransitionFlow.emit(ResponseState.Success(response.data!!)) + } else { + _annualRenewalTransitionFlow.emit( + ResponseState.Failure("Something went wrong") + ) + logError( + response, + GiErrorMetaData( + ApiErrorTagType.ANNUAL_RENEWAL_PAYMENT_TRANSITION_ERROR.value, + flowName = GiErrorMetaData.FLOW_PAYMENT, + ), + ) } } else { - _paymentRequestFlow.value = - _paymentRequestFlow.value.copy( - isLoading = false, - data = null, - hasErrorOccurred = true, - errorMessage = - ApiErrorTagType.GENERATE_RENEWAL_QUOTE_BEFORE_LOADING_PAYMENT.value, - errorResponse = response.error, + val response = + repository.generateRenewalQuote( + preQuoteId!!, + renewalQuoteRequest, + metricInfo, ) - logError( - response, - GiErrorMetaData( - ApiErrorTagType.GENERATE_RENEWAL_QUOTE_BEFORE_LOADING_PAYMENT.value, - GiErrorMetaData.FLOW_POLICY_ACTIVATION, - isDownTime = true, - ), - ) + if ( + response.error.isNull() && + response.errors.isNullOrEmpty() && + response.data.isNotNull() + ) { + response.data?.let { + val updatedId = getParameterFromCta(response.data?.cta, ID) + getPaymentReviewScreen(updatedId, paymentFlowIdentifier, applicationId) + } + } else { + _paymentRequestFlow.value = + _paymentRequestFlow.value.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorMessage = + ApiErrorTagType.GENERATE_RENEWAL_QUOTE_BEFORE_LOADING_PAYMENT + .value, + errorResponse = response.error, + ) + logError( + response, + GiErrorMetaData( + ApiErrorTagType.GENERATE_RENEWAL_QUOTE_BEFORE_LOADING_PAYMENT.value, + GiErrorMetaData.FLOW_POLICY_ACTIVATION, + isDownTime = true, + ), + ) + } } } else { getPaymentReviewScreen(id, paymentFlowIdentifier, applicationId) diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarItemComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarItemComposable.kt new file mode 100644 index 0000000000..d3b479c121 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarItemComposable.kt @@ -0,0 +1,261 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables + +import android.app.DatePickerDialog +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.TextFieldDefaults +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.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.base.utils.orFalse +import com.navi.base.utils.orZero +import com.navi.design.font.FontWeightEnum +import com.navi.design.theme.RedEF0000 +import com.navi.insurance.pre.purchase.journey.composables.reusable.isValidName +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorBorderAlt +import com.navi.naviwidgets.composewidget.reusable.colorHIBlue +import com.navi.naviwidgets.composewidget.reusable.whiteColor +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.colorToHex +import com.navi.naviwidgets.extensions.getComposeTextStyling +import com.navi.naviwidgets.models.TextEditTextCalendarItemComposable +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData +import com.navi.naviwidgets.utils.getAgeInMonths +import com.navi.naviwidgets.utils.getCalendarStyle +import com.navi.naviwidgets.utils.getFormattedDateInEnglish +import com.navi.naviwidgets.utils.getMaximumAgePossible +import com.navi.naviwidgets.utils.getMinAgePossible +import java.util.Calendar + +@Composable +fun TextEditTextCalendarItemComposable( + item: TextEditTextCalendarItemComposable, + onNameChanged: (String?, String) -> Unit, + onDobChanged: (String?, String) -> Unit, + widgetCallback: WidgetCallback? = null, +) { + val context = LocalContext.current + val focusManager = LocalFocusManager.current + var nameText by + remember(item.editTextData?.title?.text) { + mutableStateOf(item.editTextData?.title?.text ?: "") + } + var dobText by + remember(item.calendar?.editTextData?.title?.text) { + mutableStateOf(item.calendar?.editTextData?.title?.text ?: "") + } + var nameError by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = LocalDimensions.current.dp16)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + item.title?.let { title -> NaviTextWidgetized(textFieldData = title) } + item.rightTitle?.let { rightTitle -> NaviTextWidgetized(textFieldData = rightTitle) } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + OutlinedTextField( + value = nameText, + onValueChange = { value -> + nameText = value + nameError = !isValidName(value) && value.isNotEmpty() + onNameChanged(item.id, value) + }, + modifier = + Modifier.weight(2f) + .height(48.dp) + .onFocusChanged { focusState -> + if (!focusState.isFocused && nameText.isNotEmpty()) { + nameError = !isValidName(nameText) + } + } + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + item.disabledCta?.let { widgetCallback?.onClick(it) } + }, + enabled = !(item.disableEditing.orFalse()), + placeholder = { NaviTextWidgetized(textFieldData = item.editTextData?.hint) }, + textStyle = getComposeTextStyling(item.editTextData?.title), + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + imeAction = ImeAction.Next, + ), + keyboardActions = KeyboardActions(onNext = { focusManager.clearFocus() }), + singleLine = true, + maxLines = 1, + shape = RoundedCornerShape(4.dp), + colors = + TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = colorBorderAlt, + unfocusedBorderColor = colorBorderAlt, + backgroundColor = whiteColor, + cursorColor = colorHIBlue, + errorBorderColor = Color.Red, + ), + isError = nameError, + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Box( + modifier = + Modifier.weight(1f) + .height(48.dp) + .border(1.dp, colorBorderAlt, RoundedCornerShape(4.dp)) + .background(whiteColor, RoundedCornerShape(4.dp)) + .clickable { + if (item.disabledCta != null) { + item.disabledCta?.let { widgetCallback?.onClick(it) } + } else { + val c = Calendar.getInstance() + val startYear = + item.calendar?.datePickerData?.startYear ?: c[Calendar.YEAR] + val startMonth = + (item.calendar?.datePickerData?.startMonth?.dec()) + ?: c[Calendar.MONTH] + val startDay = + item.calendar?.datePickerData?.startDay + ?: c[Calendar.DAY_OF_MONTH] + + val datePickerDialog = + DatePickerDialog( + context, + getCalendarStyle(item.calendar?.datePickerData?.style), + { view, year, monthOfYear, dayOfMonth -> + val date = + getFormattedDateInEnglish( + year, + monthOfYear, + dayOfMonth, + ) + item.calendar?.editTextData?.title = + item.calendar + ?.editTextData + ?.title + ?.copy(text = date) + item.calendar?.datePickerData?.startYear = year + item.calendar?.datePickerData?.startMonth = + monthOfYear + 1 + item.calendar?.datePickerData?.startDay = dayOfMonth + + dobText = date + onDobChanged(item.id, date) + focusManager.clearFocus() + }, + startYear, + startMonth, + startDay, + ) + val parentView = + datePickerDialog.window + ?.decorView + ?.findViewById(android.R.id.content) + ?.rootView as? ViewGroup + parentView?.descendantFocusability = + ViewGroup.FOCUS_BLOCK_DESCENDANTS + datePickerDialog.setCancelable(true) + datePickerDialog.datePicker.minDate = + getMaximumAgePossible( + item.calendar?.datePickerData?.maxDate.orZero() + ) + datePickerDialog.datePicker.maxDate = + item.calendar?.datePickerData?.minDate?.let { + getMinAgePossible(it) + } + ?: kotlin.run { + getAgeInMonths( + item.calendar?.datePickerData?.minMonth.orZero() + ) + } + if (!datePickerDialog.isShowing) { + datePickerDialog.show() + } + } + } + .padding(16.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + NaviTextWidgetized( + textFieldData = + item.calendar?.editTextData?.title.takeIf { + it?.text.isNotNullAndNotEmpty() + } ?: item.calendar?.editTextData?.hint, + modifier = Modifier.weight(1f), + ) + + if (item.calendar?.editTextData?.title?.text?.isEmpty() == true) { + NaviImage( + imageFieldData = + ImageFieldData( + iconCode = "CALENDAR_GREY", + iconWidth = 16, + iconHeight = 16, + ) + ) + } + } + } + } + + if (nameError && item.editTextData?.errorMessage != null) { + Spacer(modifier = Modifier.height(8.dp)) + NaviTextWidgetized( + textFieldData = + TextFieldData( + text = item.editTextData?.errorMessage, + textColor = colorToHex(RedEF0000), + font = FontWeightEnum.NAVI_BODY_REGULAR.name, + size = 12, + ) + ) + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarWidgetComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarWidgetComposable.kt new file mode 100644 index 0000000000..54e99c5824 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextEditTextCalendarWidgetComposable.kt @@ -0,0 +1,179 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables + +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.insurance.pre.purchase.journey.PreQuotePageData +import com.navi.insurance.pre.purchase.journey.PreQuotePatchData +import com.navi.insurance.pre.purchase.journey.WidgetKey +import com.navi.insurance.pre.purchase.journey.composables.reusable.QuestionHeaderComposable +import com.navi.insurance.pre.purchase.journey.composables.reusable.isValidName +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.models.GenericWidgetDataInfo +import com.navi.naviwidgets.models.TextEditTextCalendarItemComposable +import com.navi.naviwidgets.models.TextEditTextCalendarWidgetComposableData +import com.navi.naviwidgets.utils.getFormattedDateInEnglishToDDMMYYYY + +@Composable +fun TextEditTextCalendarWidgetComposable( + data: TextEditTextCalendarWidgetComposableData, + isValidWidget: (Boolean, GenericWidgetDataInfo) -> Unit, + updatePatchCallData: (PreQuotePatchData) -> Unit = {}, + widgetCallback: WidgetCallback? = null, +) { + var itemList by + remember(key1 = data.toString()) { + mutableStateOf(data.textEditTextCalendarWidgetBody?.items) + } + val infoIcon = data.textEditTextCalendarWidgetBody?.infoIcon + val memberNameMap = remember { HashMap() } + val memberDobMap = remember { HashMap() } + LaunchedEffect(Unit) { + val isValid = validateAllItems(itemList, memberNameMap, memberDobMap) + isValidWidget( + isValid, + data.copy( + textEditTextCalendarWidgetBody = + data.textEditTextCalendarWidgetBody?.copy(items = itemList) + ), + ) + } + Column { + QuestionHeaderComposable( + questionData = data.textEditTextCalendarWidgetBody?.questionHeaderWidget + ) + Spacer(modifier = Modifier.height(LocalDimensions.current.dp24)) + NaviImage(imageFieldData = infoIcon, modifier = Modifier.fillMaxWidth().padding(16.dp)) + Spacer(modifier = Modifier.height(LocalDimensions.current.dp24)) + Column(modifier = Modifier.verticalScroll(enabled = true, state = rememberScrollState())) { + itemList?.forEachIndexed { index, item -> + TextEditTextCalendarItemComposable( + item = item, + onNameChanged = { id, name -> + id?.let { memberNameMap[id] = name } + val updatedItem = + item.copy( + editTextData = + item.editTextData?.copy( + title = item.editTextData?.title?.copy(text = name) + ), + bottomContent = + if (!isValidName(name) && name.isNotEmpty()) { + item.editTextData?.error + } else null, + ) + itemList = itemList?.toMutableList()?.apply { set(index, updatedItem) } + val isValid = validateAllItems(itemList, memberNameMap, memberDobMap) + isValidWidget( + isValid, + data.copy( + textEditTextCalendarWidgetBody = + data.textEditTextCalendarWidgetBody?.copy(items = itemList) + ), + ) + }, + onDobChanged = { id, dob -> + id?.let { memberDobMap[id] = dob } + val updatedItem = + item.copy( + calendar = + item.calendar?.copy( + editTextData = + item.calendar + ?.editTextData + ?.copy( + title = + item.calendar + ?.editTextData + ?.title + ?.copy(text = dob), + value = dob, + ) + ) + ) + itemList = itemList?.toMutableList()?.apply { set(index, updatedItem) } + val isValid = validateAllItems(itemList, memberNameMap, memberDobMap) + isValidWidget( + isValid, + data.copy( + textEditTextCalendarWidgetBody = + data.textEditTextCalendarWidgetBody?.copy(items = itemList) + ), + ) + }, + widgetCallback = widgetCallback, + ) + Spacer(modifier = Modifier.height(LocalDimensions.current.dp37)) + } + Spacer(modifier = Modifier.height(LocalDimensions.current.dp186)) + } + } + updatePatchCallData(formatTextEditTextCalendarRequestData(itemList)) +} + +private fun validateAllItems( + itemList: List?, + nameMap: HashMap, + dobMap: HashMap, +): Boolean { + if (itemList.isNullOrEmpty()) return false + for (item in itemList) { + val id = item.id ?: continue + val name = nameMap[id].orEmpty() + val dob = dobMap[id].orEmpty() + if (!isValidName(name) || dob.isEmpty()) return false + } + return true +} + +private fun formatTextEditTextCalendarRequestData( + itemList: List? +): PreQuotePatchData { + val dataList = mutableListOf() + itemList?.forEach { item -> + if ( + !item.editTextData?.title?.text.isNullOrEmpty() && + !item.calendar?.editTextData?.title?.text.isNullOrEmpty() + ) { + dataList.add( + PreQuotePageData( + assetId = item.assetId, + optionId = item.nameKey ?: "name", + dropDownId = item.editTextData?.title?.text, + ) + ) + dataList.add( + PreQuotePageData( + assetId = item.assetId, + optionId = item.dobKey ?: "dob", + dropDownId = + getFormattedDateInEnglishToDDMMYYYY( + item.calendar?.editTextData?.title?.text.toString() + ), + ) + ) + } + } + return PreQuotePatchData(data = listOf(WidgetKey(type = "List", value = dataList))) +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/factory/ComposableWidgetFactory.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/factory/ComposableWidgetFactory.kt index c6c7ca89c7..a40f16d6d4 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/factory/ComposableWidgetFactory.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/factory/ComposableWidgetFactory.kt @@ -22,6 +22,7 @@ import com.navi.insurance.pre.purchase.journey.composables.NameDobEditTextWidget import com.navi.insurance.pre.purchase.journey.composables.NameDobTextWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.PincodeInputWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.PincodeInputWidgetV2Composable +import com.navi.insurance.pre.purchase.journey.composables.TextEditTextCalendarWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.TextWithAgeSelectorWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.TextWithAgeSelectorWidgetV2Composable import com.navi.insurance.pre.purchase.journey.composables.TextWithHeightWeightSelectorWidgetComposable @@ -39,6 +40,7 @@ import com.navi.naviwidgets.models.NameDobEditTextWidgetData import com.navi.naviwidgets.models.NameDobTextWidgetData import com.navi.naviwidgets.models.PincodeInputWidgetData import com.navi.naviwidgets.models.PincodeInputWidgetDataV2 +import com.navi.naviwidgets.models.TextEditTextCalendarWidgetComposableData import com.navi.naviwidgets.models.TextWithAgeSelectorWidget import com.navi.naviwidgets.models.TextWithAgeSelectorWidgetV2 import com.navi.naviwidgets.models.TextWithHeightWeightSelectorWidget @@ -97,6 +99,13 @@ fun ComposableWidgetFactory( updatePatchCallData, widgetCallback = widgetCallback, ) + is TextEditTextCalendarWidgetComposableData -> + TextEditTextCalendarWidgetComposable( + data, + isValidWidget, + updatePatchCallData, + widgetCallback, + ) is DetailsStatusWidget -> DetailsStatusWidgetComposable(data, widgetCallback, updatePatchCallData) is TextWithAgeSelectorWidget -> diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/purchase/compliance/data/PurchaseComplianceRepository.kt b/android/navi-insurance/src/main/java/com/navi/insurance/purchase/compliance/data/PurchaseComplianceRepository.kt index df803552b6..cf7fe04c23 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/purchase/compliance/data/PurchaseComplianceRepository.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/purchase/compliance/data/PurchaseComplianceRepository.kt @@ -84,7 +84,7 @@ class PurchaseComplianceRepository @Inject constructor(private val apiService: R transition: String, ): RepoResult { return giResponseCallback( - apiService.fetchPurchaseComplianceNextPage( + apiService.fetchApplicationTransitionResponse( request = request, applicationId = applicationId, transition = transition, diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/NewRenewalActivity.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/NewRenewalActivity.kt new file mode 100644 index 0000000000..27bc588b58 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/NewRenewalActivity.kt @@ -0,0 +1,127 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.navi.base.model.CtaData +import com.navi.base.model.CtaType +import com.navi.base.model.NaviClickAction +import com.navi.base.utils.orFalse +import com.navi.common.callback.RequestToCallbackHandler +import com.navi.insurance.analytics.InsuranceAnalyticsConstants +import com.navi.insurance.analytics.NaviInsuranceAnalytics +import com.navi.insurance.common.GiBaseVM +import com.navi.insurance.common.theme.color.GiMaterialTheme +import com.navi.insurance.health.activity.BaseComposeActivity +import com.navi.insurance.navigator.NaviInsuranceDeeplinkNavigator +import com.navi.insurance.renewal_revamp.composables.RenewalActivityNavHost +import com.navi.insurance.renewal_revamp.model.navigation.screenToDestinationMapper +import com.navi.insurance.renewal_revamp.viewmodels.NewRenewalActivityVM +import com.navi.insurance.util.Constants +import com.navi.insurance.util.PAGE_TYPE +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.views.NaviErrorPageView +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class NewRenewalActivity : BaseComposeActivity(), WidgetCallback { + private val viewModel by viewModels() + lateinit var view: NaviErrorPageView + lateinit var navController: NavHostController + private var screenIdentifier: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + overridePendingTransition(0, 0) + view = NaviErrorPageView(this) + intent.extras?.getParcelable(Constants.KEY_CTA_DATA)?.let { ctaData -> + ctaData.parameters?.forEach { lineItem -> + when (lineItem.key) { + PAGE_TYPE -> { + screenIdentifier = lineItem.value + } + } + } + } + setContent { + navController = rememberNavController() + onNavControllerSet(navController) + GiMaterialTheme { + RenewalActivityNavHost( + renewalActivity = this, + startDestination = screenToDestinationMapper(screenIdentifier), + navController = navController, + widgetCallback = this, + view = view, + connectivityObserver = connectivityStateFlow, + ) + } + } + } + + override fun onClick(naviClickAction: NaviClickAction, widgetId: String?) { + when (naviClickAction) { + is CtaData -> { + naviClickAction.analyticsEventProperties?.let { + NaviInsuranceAnalytics.postAnalyticsEvent(it.name.orEmpty(), it.properties) + } + when (naviClickAction.type) { + CtaType.GO_BACK.value -> onBackPressed() + CtaType.CLOSE_SCREEN.value -> finish() + + else -> { + NaviInsuranceDeeplinkNavigator.navigate( + this, + naviClickAction, + finish = naviClickAction.finish.orFalse(), + clearTask = naviClickAction.clearTask.orFalse(), + callbackHandler = + object : RequestToCallbackHandler { + override fun onCallbackRaised() {} + + override fun onPaymentCallback(ctaData: CtaData) = Unit + }, + ) + } + } + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + intent.extras?.getParcelable(Constants.KEY_CTA_DATA)?.let { ctaData -> + ctaData.parameters?.forEach { lineItem -> + when (lineItem.key) { + PAGE_TYPE -> { + screenIdentifier = lineItem.value + if (::navController.isInitialized && screenIdentifier != null) { + screenToDestinationMapper(screenIdentifier).let { destination -> + navController.navigate(destination) { + popUpTo(destination) { inclusive = true } + launchSingleTop = true + restoreState = false + } + } + } + } + } + } + } + } + + override val screenName: String = InsuranceAnalyticsConstants.RENEWAL_SCREEN + + override fun getViewModel(): GiBaseVM = viewModel +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/CoverAmountChangeScreen.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/CoverAmountChangeScreen.kt new file mode 100644 index 0000000000..bf33ff723c --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/CoverAmountChangeScreen.kt @@ -0,0 +1,374 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.composables + +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.foundation.shape.RoundedCornerShape +import androidx.compose.material.Scaffold +import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.model.CtaConstants +import com.navi.base.model.CtaData +import com.navi.base.model.CtaType +import com.navi.base.model.NaviClickAction +import com.navi.base.utils.ConnectivityObserver +import com.navi.base.utils.isNotNull +import com.navi.common.utils.Constants +import com.navi.common.utils.setStatusBarColorInt +import com.navi.design.decorator.DashedDivider +import com.navi.insurance.analytics.NaviInsuranceAnalytics +import com.navi.insurance.analytics.NaviInsuranceAnalytics.Companion.GI_COVER_CHANGE_SCREEN +import com.navi.insurance.common.reusable.components.InitErrorView +import com.navi.insurance.renewal_revamp.ui_components.ChooseCoverAmount +import com.navi.insurance.renewal_revamp.ui_components.FrictionBottomSheet +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewFooterComposable +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewHeaderComposable +import com.navi.insurance.renewal_revamp.viewmodels.CoverAmountChangeVM +import com.navi.insurance.review_policy.ui_components.ShowShimmer +import com.navi.insurance.review_policy.ui_components.TitleWithSubtitleComposable +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.Constants.SAVE_CHANGES +import com.navi.insurance.util.PAGE_TYPE +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.whiteColor +import com.navi.naviwidgets.extensions.FloatingButtonOverlay +import com.navi.naviwidgets.extensions.hexToColor +import com.navi.naviwidgets.extensions.hexToInt +import com.navi.naviwidgets.views.NaviErrorPageView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CoverAmountChangeScreen( + activity: Activity, + navController: NavHostController, + widgetCallback: WidgetCallback, + viewModel: CoverAmountChangeVM = hiltViewModel(), + view: NaviErrorPageView, + connectivityObserver: Flow, +) { + LaunchedEffect(Unit) { + activity.intent.extras?.getParcelable(Constants.KEY_CTA_DATA)?.let { ctaData -> + ctaData.parameters?.forEach { lineItem -> + when (lineItem.key) { + ARG_APPLICATION_ID -> { + viewModel.applicationId = lineItem.value + } + + PAGE_TYPE -> { + viewModel.screenName = lineItem.value + } + + else -> {} + } + } + } + viewModel.fetchPageResponse() + } + val pageResponseState = viewModel.pageDataFlow.collectAsStateWithLifecycle() + val nextPageResponseState = viewModel.transitionFlow.collectAsStateWithLifecycle() + val bottomSheetState = viewModel.bottomSheetOpened.collectAsStateWithLifecycle() + val scrollState = rememberLazyListState() + val backSheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + val expandFAB = false + val scaffoldState = rememberScaffoldState() + val localCallbackHandler = + createLocalCallbackHandler( + activity = activity, + navController = navController, + viewModel = viewModel, + coroutineScope = coroutineScope, + backSheetState = backSheetState, + widgetCallback = widgetCallback, + ) + + when { + pageResponseState.value.data.isNotNull() -> { + val successResponse = pageResponseState.value.data + val header = successResponse?.header + val backCta = successResponse?.backCta + val headerTitle = successResponse?.headerTitle + val carouselBackgroundColor = successResponse?.carouselBackgroundColor + val carouselList = successResponse?.carouselList + val footer = successResponse?.footer + val floatingButtonData = successResponse?.floatingButtonData + val backBottomSheetCta = + successResponse?.backBottomsheetCta?.copy(type = CtaType.OPEN_BOTTOM_SHEET.value) + if (backCta?.url != null || backCta?.type != null) { + viewModel.setBackCta(backCta) + } + if (nextPageResponseState.value.data.isNotNull()) { + val nextPageResponse = nextPageResponseState.value.data + nextPageResponse?.cta?.let { + localCallbackHandler.onClick(it) + viewModel.clearTransitionData() + } + } + DisposableEffect(hexToInt(header?.backgroundColor)) { + val originalColor = activity.window.statusBarColor + activity.setStatusBarColorInt(hexToInt(header?.backgroundColor)) + onDispose { activity.setStatusBarColorInt(originalColor) } + } + LaunchedEffect(Unit) { + successResponse?.pageMetaData?.analyticsEvents?.forEach { + NaviTrackEvent.trackEvent( + eventName = it.name.orEmpty(), + eventValues = it.properties, + ) + } + successResponse?.selectedIndex?.let { + viewModel.setSelectedIndex(it) + viewModel.setExistingSelectedIndex(it) + } + } + BackHandler { + if (viewModel.existingSelectedIndex.value != viewModel.selectedIndex.value) { + backBottomSheetCta?.let { localCallbackHandler.onClick(it) } + } else { + header?.leftIcon?.cta?.let { localCallbackHandler.onClick(it) } + ?: run { navController.popBackStack() } + } + } + Scaffold( + modifier = + Modifier.fillMaxSize() + .animateContentSize(animationSpec = tween(durationMillis = 600)) + .background(Color.White), + scaffoldState = scaffoldState, + topBar = { + if (header != null) { + RenewalReviewHeaderComposable( + header = header, + widgetCallback = localCallbackHandler, + isBackBSEnabled = + viewModel.existingSelectedIndex.value != + viewModel.selectedIndex.value, + backBottomsheetCta = backBottomSheetCta, + ) + } + }, + bottomBar = { + if (footer != null) { + RenewalReviewFooterComposable( + footer = footer, + widgetCallback = localCallbackHandler, + state = nextPageResponseState.value, + overrideFooterText = + if ( + viewModel.existingSelectedIndex.value != + viewModel.selectedIndex.value + ) + SAVE_CHANGES + else null, + ) + } + }, + ) { padding -> + val selectedIndex = viewModel.selectedIndex.value + LazyColumn( + modifier = Modifier.background(Color.White).fillMaxWidth().padding(padding), + state = scrollState, + ) { + item { + TitleWithSubtitleComposable( + title = headerTitle, + modifier = + Modifier.background(color = hexToColor(carouselBackgroundColor)) + .padding(16.dp, 16.dp, 16.dp, 0.dp), + widgetCallback = localCallbackHandler, + ) + } + item { + ChooseCoverAmount( + carouselList = carouselList, + carouselBackgroundColor = carouselBackgroundColor, + selectedIndex = selectedIndex, + onClick = { viewModel.setSelectedIndex(it) }, + ) + } + item { Spacer(modifier = Modifier.height(24.dp)) } + item { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + TitleWithSubtitleComposable( + title = successResponse?.baseCoverAmountText, + subTitle = + successResponse + ?.baseCoverAmountValue + ?.copy( + text = + successResponse.carouselList + ?.get(selectedIndex) + ?.baseCoverAmountValue + ), + titleTag = + if (viewModel.existingIndex != selectedIndex) + successResponse?.baseCoverAmountTag + else null, + modifier = Modifier.padding(horizontal = 16.dp).height(24.dp), + widgetCallback = localCallbackHandler, + ) + TitleWithSubtitleComposable( + title = successResponse?.bonusText, + subTitle = successResponse?.bonusValue, + modifier = Modifier.padding(horizontal = 16.dp), + widgetCallback = localCallbackHandler, + ) + DashedDivider( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + thickness = 1.dp, + ) + TitleWithSubtitleComposable( + title = successResponse?.totalAmountText, + subTitle = + successResponse + ?.totalAmountValue + ?.copy( + text = + successResponse.carouselList + ?.get(selectedIndex) + ?.totalCoverAmountValue + ), + modifier = Modifier.padding(horizontal = 16.dp), + widgetCallback = localCallbackHandler, + ) + } + } + item { Spacer(modifier = Modifier.height(120.dp)) } + } + } + if (floatingButtonData.isNotNull()) { + FloatingButtonOverlay( + floatingButtonData, + expandFAB, + localCallbackHandler, + GI_COVER_CHANGE_SCREEN, + ) + } + if (bottomSheetState.value == true) { + ModalBottomSheet( + onDismissRequest = { + localCallbackHandler.onClick( + CtaData(type = CtaType.DISMISS_BOTTOMSHEET.value) + ) + }, + sheetState = backSheetState, + shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + dragHandle = null, + containerColor = whiteColor, + ) { + FrictionBottomSheet( + ctaData = successResponse?.backBottomsheetCta, + widgetCallback = localCallbackHandler, + dismissSheet = { + viewModel.setBottomSheetOpened(false) + coroutineScope.launch { backSheetState.hide() } + }, + ) + } + } + } + + pageResponseState.value.isLoading -> ShowShimmer(activity) + + pageResponseState.value.hasErrorOccurred -> { + InitErrorView( + connectivityObserver = connectivityObserver, + view = view, + errorMessage = pageResponseState.value.errorResponse, + onRetryClick = { viewModel.fetchPageResponse() }, + onCloseButtonClick = { + localCallbackHandler.onClick( + CtaData( + url = CtaConstants.HOME.value, + type = CtaType.USE_ROOT_DEEPLINK_NAVIGATOR.value, + ) + ) + }, + ) + } + + else -> ShowShimmer(activity) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private fun createLocalCallbackHandler( + activity: Activity, + navController: NavHostController, + viewModel: CoverAmountChangeVM, + coroutineScope: CoroutineScope, + backSheetState: SheetState, + widgetCallback: WidgetCallback, +) = + object : WidgetCallback { + var shouldPostEvents = true + + override fun onClick(naviClickAction: NaviClickAction, widgetId: String?) { + when (naviClickAction) { + is CtaData -> { + when (naviClickAction.type) { + CtaType.NEXT_PAGE_API.value -> { + viewModel.nextPageTransitionResponse(naviClickAction.parameters) + } + + CtaType.OPEN_BOTTOM_SHEET.value -> { + viewModel.setBottomSheetOpened(true) + } + + CtaType.DISMISS_BOTTOMSHEET.value -> { + viewModel.setBottomSheetOpened(false) + coroutineScope.launch { backSheetState.hide() } + } + + else -> { + widgetCallback.onClick(naviClickAction) + shouldPostEvents = false + } + } + if (shouldPostEvents) { + naviClickAction.analyticsEventProperties?.let { + NaviInsuranceAnalytics.postAnalyticsEvent( + eventName = it.name.orEmpty(), + eventProperties = it.properties, + ) + } + } + } + } + } + } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalActivityNavHost.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalActivityNavHost.kt new file mode 100644 index 0000000000..c47387af57 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalActivityNavHost.kt @@ -0,0 +1,76 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.composables + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.navi.base.utils.ConnectivityObserver +import com.navi.insurance.renewal_revamp.NewRenewalActivity +import com.navi.insurance.renewal_revamp.model.navigation.getAllScreenNameList +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.views.NaviErrorPageView +import kotlinx.coroutines.flow.Flow + +@Composable +fun RenewalActivityNavHost( + renewalActivity: NewRenewalActivity, + startDestination: String, + navController: NavHostController, + widgetCallback: WidgetCallback, + view: NaviErrorPageView, + connectivityObserver: Flow, +) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = Modifier.fillMaxSize(), + enterTransition = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(500), + ) + }, + exitTransition = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(500), + ) + }, + popEnterTransition = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween(500), + ) + }, + popExitTransition = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween(500), + ) + }, + ) { + getAllScreenNameList().forEach { tab -> + composable(tab.tabId) { + RenewalScreenRegistry( + renewalActivity = renewalActivity, + navController = navController, + widgetCallback = widgetCallback, + tabId = tab.tabId, + view = view, + connectivityObserver = connectivityObserver, + ) + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberEditScreen.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberEditScreen.kt new file mode 100644 index 0000000000..2310b802fd --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberEditScreen.kt @@ -0,0 +1,324 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.composables + +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Scaffold +import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.model.CtaConstants +import com.navi.base.model.CtaData +import com.navi.base.model.CtaType +import com.navi.base.model.NaviClickAction +import com.navi.base.utils.ConnectivityObserver +import com.navi.base.utils.isNotNull +import com.navi.common.utils.Constants +import com.navi.insurance.analytics.NaviInsuranceAnalytics +import com.navi.insurance.analytics.NaviInsuranceAnalytics.Companion.GI_MEMBER_EDIT_SCREEN +import com.navi.insurance.common.reusable.components.InitErrorView +import com.navi.insurance.renewal_revamp.ui_components.AddEditMemberSection +import com.navi.insurance.renewal_revamp.ui_components.FrictionBottomSheet +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewFooterComposable +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewHeaderComposable +import com.navi.insurance.renewal_revamp.viewmodels.RenewalMemberEditVM +import com.navi.insurance.review_policy.ui_components.ShowShimmer +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.Constants.SAVE_CHANGES +import com.navi.insurance.util.PAGE_TYPE +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.whiteColor +import com.navi.naviwidgets.extensions.FloatingButtonOverlay +import com.navi.naviwidgets.views.NaviErrorPageView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RenewalMemberEditScreen( + activity: Activity, + navController: NavHostController, + widgetCallback: WidgetCallback, + viewModel: RenewalMemberEditVM = hiltViewModel(), + view: NaviErrorPageView, + connectivityObserver: Flow, +) { + LaunchedEffect(Unit) { + activity.intent.extras?.getParcelable(Constants.KEY_CTA_DATA)?.let { ctaData -> + ctaData.parameters?.forEach { lineItem -> + when (lineItem.key) { + ARG_APPLICATION_ID -> { + viewModel.applicationId = lineItem.value + } + + PAGE_TYPE -> { + viewModel.screenName = lineItem.value + } + + else -> {} + } + } + } + viewModel.fetchPageResponse() + } + val pageResponseState = viewModel.pageDataFlow.collectAsStateWithLifecycle() + val nextPageResponseState = viewModel.transitionFlow.collectAsStateWithLifecycle() + val bottomSheetState = viewModel.bottomSheetOpened.collectAsStateWithLifecycle() + val backBottomSheetState = viewModel.backBottomSheetOpened.collectAsStateWithLifecycle() + val scrollState = rememberLazyListState() + val expandFAB = false + val scaffoldState = rememberScaffoldState() + val sheetState = rememberModalBottomSheetState() + val backSheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + val localCallbackHandler = + createLocalCallbackHandler( + activity = activity, + navController = navController, + viewModel = viewModel, + coroutineScope = coroutineScope, + sheetState = sheetState, + backSheetState = backSheetState, + widgetCallback = widgetCallback, + ) + + when { + pageResponseState.value.data.isNotNull() -> { + val successResponse = pageResponseState.value.data + val header = successResponse?.header + val footer = successResponse?.footer + val backCta = successResponse?.backCta + val floatingButtonData = successResponse?.floatingButtonData + val backBottomSheetCta = + successResponse?.backBottomsheetCta?.copy(type = CtaType.OPEN_BOTTOM_SHEET.value) + if (backCta?.url != null || backCta?.type != null) { + viewModel.setBackCta(backCta) + } + val isBackBSEnabled = + viewModel.members.collectAsState().value.any { !it.statusConfirmed } + if (nextPageResponseState.value.data.isNotNull()) { + val nextPageResponse = nextPageResponseState.value.data + nextPageResponse?.cta?.let { + localCallbackHandler.onClick(it) + viewModel.clearTransitionData() + } + } + LaunchedEffect(Unit) { + successResponse?.pageMetaData?.analyticsEvents?.forEach { + NaviTrackEvent.trackEvent( + eventName = it.name.orEmpty(), + eventValues = it.properties, + ) + } + } + BackHandler { + if (isBackBSEnabled) { + backBottomSheetCta?.let { localCallbackHandler.onClick(it) } + } else { + header?.leftIcon?.cta?.let { localCallbackHandler.onClick(it) } + ?: run { navController.popBackStack() } + } + } + Scaffold( + modifier = + Modifier.fillMaxSize() + .animateContentSize(animationSpec = tween(durationMillis = 600)) + .background(Color.White), + scaffoldState = scaffoldState, + topBar = { + if (header != null) { + RenewalReviewHeaderComposable( + header = header, + widgetCallback = localCallbackHandler, + isBackBSEnabled = isBackBSEnabled, + backBottomsheetCta = backBottomSheetCta, + ) + } + }, + bottomBar = { + if (footer != null) { + RenewalReviewFooterComposable( + footer = footer, + widgetCallback = localCallbackHandler, + state = nextPageResponseState.value, + overrideFooterText = if (isBackBSEnabled) SAVE_CHANGES else null, + ) + } + }, + ) { padding -> + LazyColumn( + modifier = Modifier.background(Color.White).fillMaxWidth().padding(padding), + state = scrollState, + ) { + item { + AddEditMemberSection( + data = successResponse, + viewModel = viewModel, + widgetCallback = localCallbackHandler, + ) + } + } + } + if (floatingButtonData.isNotNull()) { + FloatingButtonOverlay( + floatingButtonData = floatingButtonData, + isExpanded = expandFAB, + widgetCallback = localCallbackHandler, + screenName = GI_MEMBER_EDIT_SCREEN, + ) + } + if (bottomSheetState.value == true) { + ModalBottomSheet( + onDismissRequest = { viewModel.cancelMemberRemoval() }, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + dragHandle = null, + containerColor = whiteColor, + ) { + FrictionBottomSheet( + ctaData = successResponse?.deleteButtonIcon?.cta, + widgetCallback = localCallbackHandler, + overrideItemTitle = viewModel.currentlyDeletedMember, + ) + } + } + if (backBottomSheetState.value == true) { + ModalBottomSheet( + onDismissRequest = { + localCallbackHandler.onClick( + CtaData(type = CtaType.DISMISS_BOTTOMSHEET.value) + ) + }, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + dragHandle = null, + containerColor = whiteColor, + ) { + FrictionBottomSheet( + ctaData = successResponse?.backBottomsheetCta, + widgetCallback = localCallbackHandler, + dismissSheet = { + viewModel.setBackBottomSheetOpened(false) + coroutineScope.launch { backSheetState.hide() } + }, + ) + } + } + } + + pageResponseState.value.isLoading -> ShowShimmer(activity) + + pageResponseState.value.hasErrorOccurred -> { + InitErrorView( + connectivityObserver = connectivityObserver, + view = view, + errorMessage = pageResponseState.value.errorResponse, + onRetryClick = { viewModel.fetchPageResponse() }, + onCloseButtonClick = { + localCallbackHandler.onClick( + CtaData( + url = CtaConstants.HOME.value, + type = CtaType.USE_ROOT_DEEPLINK_NAVIGATOR.value, + ) + ) + }, + ) + } + + else -> ShowShimmer(activity) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private fun createLocalCallbackHandler( + activity: Activity, + navController: NavHostController, + viewModel: RenewalMemberEditVM, + coroutineScope: CoroutineScope, + sheetState: SheetState, + backSheetState: SheetState, + widgetCallback: WidgetCallback, +) = + object : WidgetCallback { + var shouldPostEvents = true + + override fun onClick(naviClickAction: NaviClickAction, widgetId: String?) { + when (naviClickAction) { + is CtaData -> { + when (naviClickAction.type) { + CtaType.CANCEL_REMOVAL.value -> { + coroutineScope.launch { + sheetState.hide() + viewModel.cancelMemberRemoval() + } + } + + CtaType.CONFIRM_REMOVAL.value -> { + coroutineScope.launch { + sheetState.hide() + viewModel.confirmMemberRemoval() + } + } + + CtaType.OPEN_BOTTOM_SHEET.value -> { + viewModel.setBackBottomSheetOpened(true) + } + + CtaType.DISMISS_BOTTOMSHEET.value -> { + viewModel.setBackBottomSheetOpened(false) + coroutineScope.launch { backSheetState.hide() } + } + + CtaType.NEXT_PAGE_API.value -> { + viewModel.nextPageTransitionResponse(naviClickAction.parameters) + } + + CtaType.ADD_DELETED_MEMBER.value, + CtaType.DELETE_ADDED_MEMBER.value -> Unit + + else -> { + widgetCallback.onClick(naviClickAction) + shouldPostEvents = false + } + } + if (shouldPostEvents) { + naviClickAction.analyticsEventProperties?.let { + NaviInsuranceAnalytics.postAnalyticsEvent( + eventName = it.name.orEmpty(), + eventProperties = it.properties, + ) + } + } + } + } + } + } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberUnderwritingScreen.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberUnderwritingScreen.kt new file mode 100644 index 0000000000..670baa3187 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalMemberUnderwritingScreen.kt @@ -0,0 +1,242 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.composables + +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Scaffold +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.model.CtaConstants +import com.navi.base.model.CtaData +import com.navi.base.model.CtaType +import com.navi.base.model.NaviClickAction +import com.navi.base.utils.ConnectivityObserver +import com.navi.base.utils.isNotNull +import com.navi.common.utils.Constants +import com.navi.insurance.analytics.NaviInsuranceAnalytics +import com.navi.insurance.analytics.NaviInsuranceAnalytics.Companion.GI_RENEWAL_UNDERWRITING_SCREEN +import com.navi.insurance.common.reusable.components.InitErrorView +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewFooterComposable +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewHeaderComposable +import com.navi.insurance.renewal_revamp.ui_components.UnderwritingCalloutsComposable +import com.navi.insurance.renewal_revamp.ui_components.UnderwritingMemberDetailsComposable +import com.navi.insurance.renewal_revamp.viewmodels.RenewalMemberUnderwritingVM +import com.navi.insurance.review_policy.ui_components.ShowShimmer +import com.navi.insurance.review_policy.ui_components.TitleWithSubtitleComposable +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.PAGE_TYPE +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.extensions.FloatingButtonOverlay +import com.navi.naviwidgets.views.NaviErrorPageView +import kotlinx.coroutines.flow.Flow + +@Composable +fun RenewalMemberUnderwritingScreen( + activity: Activity, + navController: NavHostController, + widgetCallback: WidgetCallback, + viewModel: RenewalMemberUnderwritingVM = hiltViewModel(), + view: NaviErrorPageView, + connectivityObserver: Flow, +) { + LaunchedEffect(Unit) { + activity.intent.extras?.getParcelable(Constants.KEY_CTA_DATA)?.let { ctaData -> + ctaData.parameters?.forEach { lineItem -> + when (lineItem.key) { + ARG_APPLICATION_ID -> { + viewModel.applicationId = lineItem.value + } + + PAGE_TYPE -> { + viewModel.screenName = lineItem.value + } + + else -> {} + } + } + } + viewModel.fetchPageResponse() + } + val pageResponseState = viewModel.pageDataFlow.collectAsStateWithLifecycle() + val nextPageResponseState = viewModel.transitionFlow.collectAsStateWithLifecycle() + val scrollState = rememberLazyListState() + val expandFAB = false + val scaffoldState = rememberScaffoldState() + val localCallbackHandler = + createLocalCallbackHandler( + activity = activity, + navController = navController, + viewModel = viewModel, + widgetCallback = widgetCallback, + ) + + when { + pageResponseState.value.data.isNotNull() -> { + val successResponse = pageResponseState.value.data + val header = successResponse?.header + val backCta = successResponse?.backCta + val headerTitle = successResponse?.headerTitle + val memberDetails = successResponse?.memberDetails + val healthDetailsCallouts = successResponse?.healthDetailsCallouts + val footer = successResponse?.footer + val floatingButtonData = successResponse?.floatingButtonData + if (backCta?.url != null || backCta?.type != null) { + viewModel.setBackCta(backCta) + } + if (nextPageResponseState.value.data.isNotNull()) { + val nextPageResponse = nextPageResponseState.value.data + nextPageResponse?.cta?.let { + localCallbackHandler.onClick(it) + viewModel.clearTransitionData() + } + } + LaunchedEffect(Unit) { + successResponse?.pageMetaData?.analyticsEvents?.forEach { + NaviTrackEvent.trackEvent( + eventName = it.name.orEmpty(), + eventValues = it.properties, + ) + } + } + BackHandler { + header?.leftIcon?.cta?.let { localCallbackHandler.onClick(it) } + ?: run { navController.popBackStack() } + } + Scaffold( + modifier = + Modifier.fillMaxSize() + .animateContentSize(animationSpec = tween(durationMillis = 600)) + .background(Color.White), + scaffoldState = scaffoldState, + topBar = { + if (header != null) { + RenewalReviewHeaderComposable( + header = header, + widgetCallback = localCallbackHandler, + ) + } + }, + bottomBar = { + if (footer != null) { + RenewalReviewFooterComposable( + footer = footer, + widgetCallback = localCallbackHandler, + state = nextPageResponseState.value, + ) + } + }, + ) { padding -> + LazyColumn( + modifier = Modifier.background(Color.White).fillMaxWidth().padding(padding), + state = scrollState, + ) { + item { + TitleWithSubtitleComposable( + title = headerTitle, + modifier = Modifier.padding(16.dp).padding(bottom = 8.dp), + widgetCallback = localCallbackHandler, + ) + } + item { + UnderwritingMemberDetailsComposable( + memberDetails = memberDetails, + widgetCallback = localCallbackHandler, + ) + } + item { + UnderwritingCalloutsComposable( + callouts = healthDetailsCallouts, + widgetCallback = localCallbackHandler, + ) + } + } + } + if (floatingButtonData.isNotNull()) { + FloatingButtonOverlay( + floatingButtonData, + expandFAB, + localCallbackHandler, + GI_RENEWAL_UNDERWRITING_SCREEN, + ) + } + } + + pageResponseState.value.isLoading -> ShowShimmer(activity) + + pageResponseState.value.hasErrorOccurred -> { + InitErrorView( + connectivityObserver = connectivityObserver, + view = view, + errorMessage = pageResponseState.value.errorResponse, + onRetryClick = { viewModel.fetchPageResponse() }, + onCloseButtonClick = { + widgetCallback.onClick( + CtaData( + url = CtaConstants.HOME.value, + type = CtaType.USE_ROOT_DEEPLINK_NAVIGATOR.value, + ) + ) + }, + ) + } + + else -> ShowShimmer(activity) + } +} + +private fun createLocalCallbackHandler( + activity: Activity, + navController: NavHostController, + viewModel: RenewalMemberUnderwritingVM, + widgetCallback: WidgetCallback, +) = + object : WidgetCallback { + var shouldPostEvents = true + + override fun onClick(naviClickAction: NaviClickAction, widgetId: String?) { + when (naviClickAction) { + is CtaData -> { + when (naviClickAction.type) { + CtaType.NEXT_PAGE_API.value -> { + viewModel.nextPageTransitionResponse(naviClickAction.parameters) + } + + else -> { + widgetCallback.onClick(naviClickAction) + shouldPostEvents = false + } + } + if (shouldPostEvents) { + naviClickAction.analyticsEventProperties?.let { + NaviInsuranceAnalytics.postAnalyticsEvent( + eventName = it.name.orEmpty(), + eventProperties = it.properties, + ) + } + } + } + } + } + } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalReviewScreen.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalReviewScreen.kt new file mode 100644 index 0000000000..64b637f9cd --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalReviewScreen.kt @@ -0,0 +1,291 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.composables + +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +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.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Scaffold +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import com.navi.base.model.CtaConstants +import com.navi.base.model.CtaData +import com.navi.base.model.CtaType +import com.navi.base.model.NaviClickAction +import com.navi.base.utils.ConnectivityObserver +import com.navi.base.utils.isNotNull +import com.navi.base.utils.orFalse +import com.navi.common.utils.Constants +import com.navi.insurance.analytics.NaviInsuranceAnalytics +import com.navi.insurance.analytics.NaviInsuranceAnalytics.Companion.GI_RENEWAL_REVIEW_SCREEN +import com.navi.insurance.common.reusable.components.InitErrorView +import com.navi.insurance.renewal_revamp.ui_components.RenewalMemberDetailsComposable +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewFooterComposable +import com.navi.insurance.renewal_revamp.ui_components.RenewalReviewHeaderComposable +import com.navi.insurance.renewal_revamp.viewmodels.RenewalReviewVM +import com.navi.insurance.review_policy.ui_components.AddonComposable +import com.navi.insurance.review_policy.ui_components.ShowShimmer +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.ARG_APPLICATION_TYPE +import com.navi.insurance.util.ARG_TRANSITION +import com.navi.insurance.util.PAGE_TYPE +import com.navi.insurance.util.POLICY_ID_EXTRA +import com.navi.insurance.util.REFRESH_RENEWAL_APPLICATION +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.extensions.FloatingButtonOverlay +import com.navi.naviwidgets.views.NaviErrorPageView +import kotlinx.coroutines.flow.Flow + +@Composable +fun RenewalReviewScreen( + activity: Activity, + navController: NavHostController, + widgetCallback: WidgetCallback, + viewModel: RenewalReviewVM = hiltViewModel(), + view: NaviErrorPageView, + connectivityObserver: Flow, +) { + LaunchedEffect(Unit) { + activity.intent.extras?.getParcelable(Constants.KEY_CTA_DATA)?.let { ctaData -> + ctaData.parameters?.forEach { lineItem -> + when (lineItem.key) { + ARG_APPLICATION_ID -> { + viewModel.applicationId = lineItem.value + } + + PAGE_TYPE -> { + viewModel.screenName = lineItem.value + } + + POLICY_ID_EXTRA -> { + viewModel.policyId = lineItem.value + } + + ARG_APPLICATION_TYPE -> { + viewModel.applicationType = lineItem.value + } + + REFRESH_RENEWAL_APPLICATION -> { + viewModel.refreshRenewalApplication = lineItem.value + } + + ARG_TRANSITION -> { + viewModel.transition = lineItem.value + } + + else -> {} + } + } + } + if (viewModel.pageDataFlow.value.data == null) { + viewModel.fetchPageResponse() + } + } + val pageResponseState = viewModel.pageDataFlow.collectAsStateWithLifecycle() + val nextPageResponseState = viewModel.renewalReviewTransitionFlow.collectAsStateWithLifecycle() + val scrollState = rememberLazyListState() + val expandFAB = true + val scaffoldState = rememberScaffoldState() + val localCallbackHandler = + createLocalCallbackHandler( + activity = activity, + navController = navController, + viewModel = viewModel, + widgetCallback = widgetCallback, + ) + + when { + pageResponseState.value.data.isNotNull() -> { + val successResponse = pageResponseState.value.data + val header = successResponse?.header + val backCta = successResponse?.backCta + if (backCta?.url != null || backCta?.type != null) { + viewModel.setBackCta(backCta) + } + if (nextPageResponseState.value.data.isNotNull()) { + val nextPageResponse = nextPageResponseState.value.data + nextPageResponse?.cta?.let { + localCallbackHandler.onClick(it) + viewModel.clearTransitionData() + } + } + LaunchedEffect(Unit) { + successResponse?.pageMetaData?.analyticsEvents?.forEach { + NaviInsuranceAnalytics.postAnalyticsEvent( + eventName = it.name.orEmpty(), + eventProperties = it.properties, + ) + } + } + BackHandler { + header?.leftIcon?.cta?.let { localCallbackHandler.onClick(it) } + ?: run { navController.popBackStack() } + } + val memberDetails = successResponse?.memberDetails + val policyDetails = successResponse?.policyDetails + val addOnsDetails = successResponse?.addOnsDetails + val isOpted = addOnsDetails?.isOpted.orFalse() + viewModel.setAddonFlag(isOpted) + val footer = successResponse?.footer + val floatingButtonData = successResponse?.floatingButtonData + + Scaffold( + modifier = + Modifier.fillMaxSize() + .animateContentSize(animationSpec = tween(durationMillis = 600)) + .background(Color.White), + scaffoldState = scaffoldState, + topBar = { + if (header != null) { + RenewalReviewHeaderComposable( + header = header, + widgetCallback = localCallbackHandler, + ) + } + }, + bottomBar = { + if (footer != null) { + RenewalReviewFooterComposable( + footer = footer, + widgetCallback = localCallbackHandler, + state = nextPageResponseState.value, + ) + } + }, + ) { padding -> + val addonResponse by + viewModel.renewalReviewAddonDataFlow.collectAsStateWithLifecycle() + val isChecked by remember { viewModel.isAddonAdded } + LazyColumn( + modifier = + Modifier.background(Color.White) + .fillMaxWidth() + .padding(padding) + .wrapContentHeight(), + state = scrollState, + ) { + policyDetails?.let { + item { + RenewalMemberDetailsComposable( + memberDetails = policyDetails, + widgetCallback = localCallbackHandler, + ) + } + } + item { Spacer(modifier = Modifier.height(20.dp)) } + memberDetails?.let { + item { + RenewalMemberDetailsComposable( + memberDetails = memberDetails, + widgetCallback = localCallbackHandler, + ) + } + } + addOnsDetails?.let { addOnsDetails -> + item { + AddonComposable( + addOnsDetails = addOnsDetails, + widgetCallback = localCallbackHandler, + addonResponse = addonResponse, + isChecked = isChecked, + ) + } + } + item { Spacer(modifier = Modifier.height(120.dp)) } + } + } + if (floatingButtonData.isNotNull()) { + FloatingButtonOverlay( + floatingButtonData = floatingButtonData, + isExpanded = expandFAB, + widgetCallback = localCallbackHandler, + screenName = GI_RENEWAL_REVIEW_SCREEN, + ) + } + } + + pageResponseState.value.isLoading -> ShowShimmer(activity) + + pageResponseState.value.hasErrorOccurred -> { + InitErrorView( + connectivityObserver = connectivityObserver, + view = view, + errorMessage = pageResponseState.value.errorResponse, + onRetryClick = { viewModel.fetchPageResponse() }, + onCloseButtonClick = { + localCallbackHandler.onClick( + CtaData( + url = CtaConstants.HOME.value, + type = CtaType.USE_ROOT_DEEPLINK_NAVIGATOR.value, + ) + ) + }, + ) + } + + else -> ShowShimmer(activity) + } +} + +private fun createLocalCallbackHandler( + activity: Activity, + navController: NavHostController, + viewModel: RenewalReviewVM, + widgetCallback: WidgetCallback, +) = + object : WidgetCallback { + var shouldPostEvents = true + + override fun onClick(naviClickAction: NaviClickAction, widgetId: String?) { + when (naviClickAction) { + is CtaData -> { + when (naviClickAction.type) { + CtaType.OPT_ADDON.value -> { + viewModel.fetchPolicyReviewAddonResponse(naviClickAction.parameters) + } + + CtaType.NEXT_PAGE_API.value -> { + viewModel.nextPageTransitionResponse(naviClickAction.parameters) + } + + else -> { + widgetCallback.onClick(naviClickAction) + shouldPostEvents = false + } + } + if (shouldPostEvents) { + naviClickAction.analyticsEventProperties?.let { + NaviInsuranceAnalytics.postAnalyticsEvent( + eventName = it.name.orEmpty(), + eventProperties = it.properties, + ) + } + } + } + } + } + } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalScreenRegistry.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalScreenRegistry.kt new file mode 100644 index 0000000000..121dee690f --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/composables/RenewalScreenRegistry.kt @@ -0,0 +1,71 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.composables + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.navi.base.utils.ConnectivityObserver +import com.navi.insurance.renewal_revamp.NewRenewalActivity +import com.navi.insurance.renewal_revamp.model.navigation.RenewalNavigationItem +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.views.NaviErrorPageView +import kotlinx.coroutines.flow.Flow + +@Composable +fun RenewalScreenRegistry( + renewalActivity: NewRenewalActivity, + navController: NavHostController, + widgetCallback: WidgetCallback, + tabId: String, + view: NaviErrorPageView, + connectivityObserver: Flow, +) { + when (tabId) { + RenewalNavigationItem.ReviewScreen.tabId -> + RenewalReviewScreen( + activity = renewalActivity, + navController = navController, + widgetCallback = widgetCallback, + view = view, + connectivityObserver = connectivityObserver, + ) + RenewalNavigationItem.CoverAmountChangeScreen.tabId -> + CoverAmountChangeScreen( + activity = renewalActivity, + navController = navController, + widgetCallback = widgetCallback, + view = view, + connectivityObserver = connectivityObserver, + ) + RenewalNavigationItem.RenewalMemberEditScreen.tabId -> + RenewalMemberEditScreen( + activity = renewalActivity, + navController = navController, + widgetCallback = widgetCallback, + view = view, + connectivityObserver = connectivityObserver, + ) + RenewalNavigationItem.RenewalMemberUnderwritingScreen.tabId -> + RenewalMemberUnderwritingScreen( + activity = renewalActivity, + navController = navController, + widgetCallback = widgetCallback, + view = view, + connectivityObserver = connectivityObserver, + ) + else -> { + RenewalReviewScreen( + activity = renewalActivity, + navController = navController, + widgetCallback = widgetCallback, + view = view, + connectivityObserver = connectivityObserver, + ) + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/navigation/RenewalNavigationItem.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/navigation/RenewalNavigationItem.kt new file mode 100644 index 0000000000..0e62cf719c --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/navigation/RenewalNavigationItem.kt @@ -0,0 +1,59 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.model.navigation + +import com.navi.insurance.R + +sealed class RenewalNavigationItem(var tabId: String, var tabName: Int) { + data object ReviewScreen : + RenewalNavigationItem(RenewalPageType.REVIEW_SCREEN.name, R.string.review_screen) + + data object CoverAmountChangeScreen : + RenewalNavigationItem( + RenewalPageType.COVER_AMOUNT_CHANGE_SCREEN.name, + R.string.cover_amount_change_screen, + ) + + data object RenewalMemberEditScreen : + RenewalNavigationItem( + RenewalPageType.RENEWAL_MEMBER_EDIT_SCREEN.name, + R.string.renewal_member_edit_screen, + ) + + data object RenewalMemberUnderwritingScreen : + RenewalNavigationItem( + RenewalPageType.RENEWAL_MEMBER_UNDERWRITING_SCREEN.name, + R.string.renewal_member_underwriting_screen, + ) +} + +enum class RenewalPageType(val value: String) { + REVIEW_SCREEN("ReviewScreen"), + COVER_AMOUNT_CHANGE_SCREEN("CoverAmountChangeScreen"), + RENEWAL_MEMBER_EDIT_SCREEN("RenewalMemberEditScreen"), + RENEWAL_MEMBER_UNDERWRITING_SCREEN("RenewalMemberUnderwritingScreen"), +} + +fun getAllScreenNameList() = + listOf( + RenewalNavigationItem.ReviewScreen, + RenewalNavigationItem.CoverAmountChangeScreen, + RenewalNavigationItem.RenewalMemberEditScreen, + RenewalNavigationItem.RenewalMemberUnderwritingScreen, + ) + +fun screenToDestinationMapper(screen: String?): String { + return when (screen) { + "renewal_policy_review_page" -> RenewalNavigationItem.ReviewScreen.tabId + "premium_details_page" -> RenewalNavigationItem.CoverAmountChangeScreen.tabId + "assets_details_page" -> RenewalNavigationItem.RenewalMemberEditScreen.tabId + "renewal_member_underwriting_page" -> + RenewalNavigationItem.RenewalMemberUnderwritingScreen.tabId + else -> RenewalNavigationItem.ReviewScreen.tabId + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/MemberDetailsPatchRequest.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/MemberDetailsPatchRequest.kt new file mode 100644 index 0000000000..b6ca38ffe8 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/MemberDetailsPatchRequest.kt @@ -0,0 +1,15 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.model.request + +import com.google.gson.annotations.SerializedName + +data class MemberDetailsPatchRequest( + @SerializedName("assetId") val assetId: String? = null, + @SerializedName("selected") val selected: Boolean? = null, +) diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/RenewalReviewScreenRequest.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/RenewalReviewScreenRequest.kt new file mode 100644 index 0000000000..fef257a8da --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/request/RenewalReviewScreenRequest.kt @@ -0,0 +1,59 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.model.request + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import com.navi.insurance.review_policy.model.request.AddonData +import com.navi.insurance.util.ARG_ADDON_DATA +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.ARG_APPLICATION_TYPE +import com.navi.insurance.util.ARG_SCREEN_NAME +import com.navi.insurance.util.ARG_TRANSITION +import com.navi.insurance.util.POLICY_ID_EXTRA +import com.navi.insurance.util.REFRESH_RENEWAL_APPLICATION + +data class RenewalReviewPageRequest( + @SerializedName("applicationId") val applicationId: String? = null, + @SerializedName("applicationType") val applicationType: String? = null, + @SerializedName("policyId") val policyId: String? = null, + @SerializedName("screenName") val screenName: String? = null, + @SerializedName("transition") val transition: String? = null, + @SerializedName("refreshRenewalApplication") val refreshRenewalApplication: String? = null, +) { + fun toMap(): Map { + return mapOf( + ARG_APPLICATION_ID to applicationId, + ARG_SCREEN_NAME to screenName, + ARG_APPLICATION_TYPE to applicationType, + POLICY_ID_EXTRA to policyId, + ARG_TRANSITION to transition, + REFRESH_RENEWAL_APPLICATION to refreshRenewalApplication, + ) + } +} + +data class RenewalReviewAddonRequest( + @SerializedName("applicationId") val applicationId: String? = null, + @SerializedName("screenName") val screenName: String? = null, + @SerializedName("applicationType") val applicationType: String? = null, + @SerializedName("policyId") val policyId: String? = null, + @SerializedName("addonData") val addonData: List? = null, + @SerializedName("refreshRenewalApplication") val refreshRenewalApplication: String? = null, +) { + fun toMap(): Map { + return mapOf( + ARG_SCREEN_NAME to screenName, + ARG_APPLICATION_ID to applicationId, + ARG_ADDON_DATA to addonData?.let { Gson().toJson(it) }, + REFRESH_RENEWAL_APPLICATION to refreshRenewalApplication, + ARG_APPLICATION_TYPE to applicationType, + POLICY_ID_EXTRA to policyId, + ) + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/CoverAmountChangeScreenResponse.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/CoverAmountChangeScreenResponse.kt new file mode 100644 index 0000000000..221d57f1ed --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/CoverAmountChangeScreenResponse.kt @@ -0,0 +1,55 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.model.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import com.navi.base.model.CtaData +import com.navi.insurance.common.models.PageMetaData +import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse +import com.navi.naviwidgets.models.response.FloatingButtonData +import com.navi.naviwidgets.models.response.TextFieldData +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +@Parcelize +data class CoverAmountChangeScreenResponse( + @SerializedName("backCta") val backCta: CtaData? = null, + @SerializedName("header") val header: PolicyReviewPageResponse.PolicyReviewHeader? = null, + @SerializedName("bonusText") val bonusText: TextFieldData? = null, + @SerializedName("bonusValue") val bonusValue: TextFieldData? = null, + @SerializedName("totalAmountText") val totalAmountText: TextFieldData? = null, + @SerializedName("totalAmountValue") val totalAmountValue: TextFieldData? = null, + @SerializedName("baseCoverAmountText") val baseCoverAmountText: TextFieldData? = null, + @SerializedName("baseCoverAmountTag") val baseCoverAmountTag: TextFieldData? = null, + @SerializedName("baseCoverAmountValue") val baseCoverAmountValue: TextFieldData? = null, + @SerializedName("headerTitle") val headerTitle: TextFieldData? = null, + @SerializedName("selectedIndex") val selectedIndex: Int? = null, + @SerializedName("carouselBackgroundColor") val carouselBackgroundColor: String? = null, + @SerializedName("carouselList") val carouselList: List? = null, + @SerializedName("footer") val footer: PolicyReviewPageResponse.PolicyReviewFooter? = null, + @SerializedName("backBottomsheetCta") val backBottomsheetCta: CtaData? = null, + @SerializedName("floatingButtonData") + val floatingButtonData: @RawValue FloatingButtonData? = null, + @SerializedName("pageMetaData") val pageMetaData: @RawValue PageMetaData? = null, +) : Parcelable { + + @Parcelize + data class CarouselItem( + @SerializedName("coverAmount") val coverAmount: TextFieldData? = null, + @SerializedName("existingTag") val existingTag: TextFieldData? = null, + @SerializedName("monthlyPremiumText") val monthlyPremiumText: TextFieldData? = null, + @SerializedName("monthlyPremiumValue") val monthlyPremiumValue: TextFieldData? = null, + @SerializedName("guaranteedBonus") val guaranteedBonus: String? = null, + @SerializedName("baseCoverAmountValue") val baseCoverAmountValue: String? = null, + @SerializedName("totalCoverAmountValue") val totalCoverAmountValue: String? = null, + @SerializedName("headerBgColor") val headerBgColor: String? = null, + @SerializedName("contentBgColor") val contentBgColor: String? = null, + @SerializedName("numericCoverAmount") val numericCoverAmount: String? = null, + ) : Parcelable +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberEditResponse.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberEditResponse.kt new file mode 100644 index 0000000000..2c0069d313 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberEditResponse.kt @@ -0,0 +1,46 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.model.response + +import com.google.gson.annotations.SerializedName +import com.navi.base.model.CtaData +import com.navi.insurance.common.models.PageMetaData +import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse +import com.navi.naviwidgets.models.response.FloatingButtonData +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData +import kotlinx.parcelize.RawValue + +data class RenewalMemberEditResponse( + @SerializedName("backCta") val backCta: CtaData? = null, + @SerializedName("header") val header: PolicyReviewPageResponse.PolicyReviewHeader? = null, + @SerializedName("footer") val footer: PolicyReviewPageResponse.PolicyReviewFooter? = null, + @SerializedName("addButtonIcon") val addButtonIcon: ImageFieldData? = null, + @SerializedName("deleteButtonIcon") val deleteButtonIcon: ImageFieldData? = null, + @SerializedName("coveredMember") val coveredMember: TextFieldData? = null, + @SerializedName("removedMember") val removedMember: TextFieldData? = null, + @SerializedName("members") val members: List? = null, + @SerializedName("backBottomsheetCta") val backBottomsheetCta: CtaData? = null, + @SerializedName("addMemberButton") + val addMemberButton: PolicyReviewPageResponse.PolicyReviewHeader? = null, + @SerializedName("floatingButtonData") + val floatingButtonData: @RawValue FloatingButtonData? = null, + @SerializedName("pageMetaData") val pageMetaData: @RawValue PageMetaData? = null, +) + +data class RenewalMember( + @SerializedName("name") val name: TextFieldData? = null, + @SerializedName("age") val age: TextFieldData? = null, + @SerializedName("relation") val relation: TextFieldData? = null, + @SerializedName("icon") val icon: ImageFieldData? = null, + @SerializedName("newTag") val newTag: TextFieldData? = null, + @SerializedName("assetId") val assetId: String? = null, + @SerializedName("isCovered") val isCovered: Boolean = false, + @SerializedName("statusConfirmed") val statusConfirmed: Boolean = false, + @SerializedName("isExistingMember") val isExistingMember: Boolean = false, +) diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberUnderwritingResponse.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberUnderwritingResponse.kt new file mode 100644 index 0000000000..dfd7641b13 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalMemberUnderwritingResponse.kt @@ -0,0 +1,41 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.model.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import com.navi.base.model.CtaData +import com.navi.insurance.common.models.PageMetaData +import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse +import com.navi.naviwidgets.models.response.FloatingButtonData +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +data class RenewalMemberUnderwritingResponse( + @SerializedName("backCta") val backCta: CtaData? = null, + @SerializedName("header") val header: PolicyReviewPageResponse.PolicyReviewHeader? = null, + @SerializedName("memberDetails") + val memberDetails: List? = null, + @SerializedName("headerTitle") val headerTitle: TextFieldData? = null, + @SerializedName("healthDetailsCallout") + val healthDetailsCallouts: HealthDetailsCallouts? = null, + @SerializedName("footer") val footer: PolicyReviewPageResponse.PolicyReviewFooter? = null, + @SerializedName("floatingButtonData") + val floatingButtonData: @RawValue FloatingButtonData? = null, + @SerializedName("pageMetaData") val pageMetaData: @RawValue PageMetaData? = null, +) + +@Parcelize +data class HealthDetailsCallouts( + @SerializedName("icon") val icon: ImageFieldData? = null, + @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("backgroundColor") val backgroundColor: String? = null, + @SerializedName("subtitle") val subtitle: TextFieldData? = null, +) : Parcelable diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalReviewScreenResponse.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalReviewScreenResponse.kt new file mode 100644 index 0000000000..95a30e359d --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/model/response/RenewalReviewScreenResponse.kt @@ -0,0 +1,55 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.model.response + +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import com.navi.base.model.CtaData +import com.navi.insurance.common.models.PageMetaData +import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse +import com.navi.naviwidgets.models.response.FloatingButtonData +import com.navi.naviwidgets.models.response.TextFieldData +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +@Parcelize +data class RenewalReviewScreenResponse( + @SerializedName("backCta") val backCta: CtaData? = null, + @SerializedName("header") val header: PolicyReviewPageResponse.PolicyReviewHeader? = null, + @SerializedName("memberDetails") val memberDetails: MemberDetails? = null, + @SerializedName("addOnsDetails") + val addOnsDetails: PolicyReviewPageResponse.AddOnsDetails? = null, + @SerializedName("policyDetails") val policyDetails: MemberDetails? = null, + @SerializedName("footer") val footer: PolicyReviewPageResponse.PolicyReviewFooter? = null, + @SerializedName("floatingButtonData") + val floatingButtonData: @RawValue FloatingButtonData? = null, + @SerializedName("pageMetaData") val pageMetaData: @RawValue PageMetaData? = null, +) : Parcelable { + + @Parcelize + data class MemberDetails( + @SerializedName("headerTitle") val headerTitle: TextFieldData? = null, + @SerializedName("headerSubTitle", alternate = ["headerSubtitle"]) + val headerSubTitle: TextFieldData? = null, + @SerializedName("headerSubTitleTag") val headerSubTitleTag: TextFieldData? = null, + @SerializedName("rightTitle") val rightTitle: TextFieldData? = null, + @SerializedName("isMemberSectionVisible") val isMemberSectionVisible: Boolean? = null, + @SerializedName("members") + val members: List? = null, + @SerializedName("contentBackgroundColor") val contentBackgroundColor: String? = null, + @SerializedName("footerBackgroundColor") val footerBackgroundColor: String? = null, + @SerializedName("isDividerVisible") val isDividerVisible: Boolean? = null, + ) : Parcelable +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/repo/RenewalReviewRepository.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/repo/RenewalReviewRepository.kt new file mode 100644 index 0000000000..595b28fd14 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/repo/RenewalReviewRepository.kt @@ -0,0 +1,63 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.repo + +import com.navi.common.alchemist.model.AlchemistScreenRequest +import com.navi.common.network.models.RepoResult +import com.navi.common.network.retrofit.ResponseCallback +import com.navi.insurance.models.response.AlchemistScreenDefinition +import com.navi.insurance.network.retrofit.RetrofitService +import com.navi.insurance.purchase.compliance.domain.dto.PatchRequestData +import com.navi.insurance.purchase.compliance.domain.dto.PatchResponseData +import com.navi.insurance.renewal_revamp.model.response.CoverAmountChangeScreenResponse +import com.navi.insurance.renewal_revamp.model.response.RenewalMemberEditResponse +import com.navi.insurance.renewal_revamp.model.response.RenewalMemberUnderwritingResponse +import com.navi.insurance.renewal_revamp.model.response.RenewalReviewScreenResponse +import javax.inject.Inject + +class RenewalReviewRepository @Inject constructor(private val retrofitService: RetrofitService) : + ResponseCallback() { + + suspend fun fetchPolicyReviewScreen( + request: AlchemistScreenRequest + ): RepoResult> { + return giResponseCallback(retrofitService.fetchRenewalReviewScreen(request = request)) + } + + suspend fun fetchCoverAmountChangeScreen( + request: AlchemistScreenRequest + ): RepoResult> { + return giResponseCallback(retrofitService.fetchCoverAmountChangeScreen(request = request)) + } + + suspend fun fetchMemberEditScreen( + request: AlchemistScreenRequest + ): RepoResult> { + return giResponseCallback(retrofitService.fetchMemberEditScreen(request = request)) + } + + suspend fun fetchMemberUnderwritingScreen( + request: AlchemistScreenRequest + ): RepoResult> { + return giResponseCallback(retrofitService.fetchMemberUnderwritingScreen(request = request)) + } + + suspend fun fetchNextPage( + request: PatchRequestData, + applicationId: String, + transition: String, + ): RepoResult { + return giResponseCallback( + retrofitService.fetchApplicationTransitionResponse( + request = request, + applicationId = applicationId, + transition = transition, + ) + ) + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/CoverAmountChangeScreenComposables.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/CoverAmountChangeScreenComposables.kt new file mode 100644 index 0000000000..90b3ca9bbd --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/CoverAmountChangeScreenComposables.kt @@ -0,0 +1,150 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.ui_components + +import android.content.res.Resources +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.navi.design.theme.GreyEBEBEB +import com.navi.insurance.renewal_revamp.model.response.CoverAmountChangeScreenResponse +import com.navi.naviwidgets.composewidget.reusable.colorShadow +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.hexToColor + +@Composable +fun CoverAmountCard( + data: CoverAmountChangeScreenResponse.CarouselItem, + isSelected: Boolean, + onMeasured: (IntSize) -> Unit, + onClick: () -> Unit, +) { + Box(contentAlignment = Alignment.TopCenter) { + Column( + modifier = + Modifier.padding(top = 9.dp) + .shadow( + elevation = 16.dp, + shape = RoundedCornerShape(4.dp), + spotColor = if (isSelected) Color.Black else colorShadow, + ) + .border( + width = if (isSelected) 2.dp else 1.dp, + color = if (isSelected) Color.Black else GreyEBEBEB, + shape = RoundedCornerShape(4.dp), + ) + .onGloballyPositioned { coordinates -> onMeasured(coordinates.size) } + .background( + color = hexToColor(data.headerBgColor), + shape = RoundedCornerShape(8.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + onClick() + }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box(modifier = Modifier.padding(12.dp)) { + data.coverAmount?.text?.let { NaviTextWidgetized(textFieldData = data.coverAmount) } + } + Column( + modifier = + Modifier.background(color = hexToColor(data.contentBgColor)) + .padding(vertical = 10.dp, horizontal = 14.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + data.monthlyPremiumText?.text?.let { + NaviTextWidgetized(textFieldData = data.monthlyPremiumText) + } + Spacer(modifier = Modifier.height(6.dp)) + data.monthlyPremiumValue?.text?.let { + NaviTextWidgetized(textFieldData = data.monthlyPremiumValue) + } + } + } + data.existingTag?.text?.let { NaviTextWidgetized(textFieldData = data.existingTag) } + } +} + +@Composable +fun ChooseCoverAmount( + carouselList: List?, + carouselBackgroundColor: String?, + selectedIndex: Int, + onClick: (index: Int) -> Unit, +) { + val listState = rememberLazyListState() + val cardSize = remember { mutableStateOf(IntSize.Zero) } + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val density = LocalDensity.current + + LaunchedEffect(cardSize.value, selectedIndex) { + if (cardSize.value.width > 0) { + val cardWidthPx = cardSize.value.width + val screenWidthPx = with(density) { screenWidth.toPx() } + val centeredOffset = (screenWidthPx / 2 - cardWidthPx / 2).toInt() + listState.animateScrollToItem( + selectedIndex, + scrollOffset = + -centeredOffset + (16 * Resources.getSystem().displayMetrics.density).toInt(), + ) + } + } + + LazyRow( + state = listState, + modifier = + Modifier.background(color = hexToColor(carouselBackgroundColor)) + .padding(top = 20.dp, bottom = 28.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + items(carouselList?.size ?: 0) { index -> + val item = carouselList?.get(index) + if (item != null) { + CoverAmountCard( + data = item, + isSelected = selectedIndex == index, + onMeasured = { size -> + if (cardSize.value == IntSize.Zero) { + cardSize.value = size + } + }, + ) { + onClick(index) + } + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RemoveMemberBottomsheet.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RemoveMemberBottomsheet.kt new file mode 100644 index 0000000000..062a577bb6 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RemoveMemberBottomsheet.kt @@ -0,0 +1,128 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.ui_components + +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.gson.reflect.TypeToken +import com.navi.base.model.CtaData +import com.navi.base.utils.isNotNull +import com.navi.base.utils.log +import com.navi.base.utils.orFalse +import com.navi.insurance.common.models.IconWithListBottomSheetData +import com.navi.insurance.util.CONTENT_DATA_JSON_STRING +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.FooterButtonComposable +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.getJsonObject +import kotlinx.coroutines.flow.StateFlow + +@Composable +fun FrictionBottomSheet( + ctaData: CtaData?, + widgetCallback: WidgetCallback?, + overrideItemTitle: StateFlow? = null, + dismissSheet: () -> Unit = {}, +) { + val dataType = object : TypeToken() {}.type + val data = + getJsonObject( + dataType, + ctaData?.parameters?.firstOrNull { it.key == CONTENT_DATA_JSON_STRING }?.value, + onErrorOccured = { e -> e.log() }, + ) + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 24.dp) + .wrapContentHeight(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + data?.topIcon?.url?.let { + NaviImage( + imageFieldData = data.topIcon, + modifier = Modifier.size(24.dp), + widgetCallback = widgetCallback, + ) + } + data?.topRightIcon?.url?.let { + NaviImage( + imageFieldData = data.topRightIcon, + modifier = Modifier.size(24.dp), + widgetCallback = widgetCallback, + ) + } + } + data?.contentTitle?.text?.let { + NaviTextWidgetized( + textFieldData = data.contentTitle, + modifier = Modifier.padding(top = 16.dp).padding(horizontal = 16.dp), + widgetCallback = widgetCallback, + ) + } + + data?.items?.firstOrNull()?.itemTitle?.let { itemTitle -> + NaviTextWidgetized( + textFieldData = itemTitle.copy(text = overrideItemTitle?.value ?: itemTitle.text), + modifier = Modifier.padding(top = 8.dp).padding(horizontal = 16.dp), + widgetCallback = widgetCallback, + ) + } + if (data?.isFooterVertical.orFalse()) { + Column( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + val footerButtons = listOf(data?.secondaryFooterButton, data?.footerButton) + footerButtons.forEach { button -> + if (button?.title?.text?.isNotNull().orFalse()) { + FooterButtonComposable( + modifier = Modifier, + data = button, + widgetCallback = widgetCallback, + onClick = dismissSheet, + ) + } + } + } + } else { + Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { + val footerButtons = listOf(data?.secondaryFooterButton, data?.footerButton) + footerButtons.forEach { button -> + if (button?.title?.text?.isNotNull().orFalse()) { + FooterButtonComposable( + modifier = + Modifier.weight(1f) + .padding(top = 16.dp, bottom = 32.dp) + .wrapContentHeight() + .padding(horizontal = 8.dp), + data = button, + widgetCallback = widgetCallback, + ) + } + } + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberEditScreenComposables.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberEditScreenComposables.kt new file mode 100644 index 0000000000..5ab2f4dc03 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberEditScreenComposables.kt @@ -0,0 +1,231 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.ui_components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.alpha +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.insurance.renewal_revamp.model.response.RenewalMember +import com.navi.insurance.renewal_revamp.model.response.RenewalMemberEditResponse +import com.navi.insurance.renewal_revamp.viewmodels.RenewalMemberEditVM +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorCTASecondary +import com.navi.naviwidgets.composewidget.reusable.colorGrey +import com.navi.naviwidgets.composewidget.reusable.darkShadowColor +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.debounceClickable +import com.navi.naviwidgets.models.response.ImageFieldData + +@Composable +fun AddEditMemberSection( + data: RenewalMemberEditResponse?, + viewModel: RenewalMemberEditVM, + widgetCallback: WidgetCallback, +) { + val coveredMembers = viewModel.coveredMembers.collectAsStateWithLifecycle().value + val removedMembers = viewModel.removedMembers.collectAsStateWithLifecycle().value + + Column(modifier = Modifier.padding(16.dp)) { + if (data?.coveredMember?.text.isNotNullAndNotEmpty() && coveredMembers.isNotEmpty()) { + NaviTextWidgetized( + textFieldData = data?.coveredMember, + widgetCallback = widgetCallback, + modifier = Modifier.padding(bottom = 12.dp), + ) + } + AnimatedContentList( + members = coveredMembers, + onIconClick = { member -> + viewModel.updateMemberCoverageStatus(member, false) + data?.addButtonIcon?.cta?.let { widgetCallback.onClick(it) } + }, + icon = data?.deleteButtonIcon, + isRemoved = false, + widgetCallback = widgetCallback, + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = + Modifier.background(color = colorCTASecondary, shape = RoundedCornerShape(4.dp)) + .padding(16.dp) + .debounceClickable( + onClick = { data?.addMemberButton?.cta?.let { widgetCallback.onClick(it) } } + ), + horizontalArrangement = Arrangement.Start, + ) { + data?.addMemberButton?.leftIcon?.url?.let { + NaviImage( + imageFieldData = data.addMemberButton.leftIcon, + modifier = Modifier.padding(horizontal = 8.dp).size(16.dp), + widgetCallback = widgetCallback, + ) + } + data?.addMemberButton?.title?.text?.let { + NaviTextWidgetized( + textFieldData = data.addMemberButton.title, + widgetCallback = widgetCallback, + modifier = Modifier.padding(horizontal = 8.dp).weight(1f), + ) + } + data?.addMemberButton?.rightIcon?.let { + NaviImage( + imageFieldData = data.addMemberButton.rightIcon, + modifier = Modifier.size(24.dp), + widgetCallback = widgetCallback, + ) + } + } + + Spacer(modifier = Modifier.height(36.dp)) + if (data?.removedMember?.text.isNotNullAndNotEmpty() && removedMembers.isNotEmpty()) { + NaviTextWidgetized( + textFieldData = data?.removedMember, + widgetCallback = widgetCallback, + modifier = Modifier.padding(bottom = 12.dp), + ) + } + AnimatedContentList( + members = removedMembers, + onIconClick = { member -> + viewModel.updateMemberCoverageStatus(member, true) + data?.deleteButtonIcon?.cta?.let { widgetCallback.onClick(it) } + }, + icon = data?.addButtonIcon, + isRemoved = true, + widgetCallback = widgetCallback, + ) + } +} + +@Composable +fun AnimatedContentList( + members: List, + onIconClick: (RenewalMember) -> Unit, + icon: ImageFieldData?, + isRemoved: Boolean = false, + widgetCallback: WidgetCallback? = null, +) { + val initialRender = remember { mutableStateOf(true) } + LaunchedEffect(Unit) { initialRender.value = false } + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + members.forEach { member -> + val visibleState = remember { + MutableTransitionState(initialRender.value).apply { targetState = true } + } + AnimatedVisibility( + visibleState = visibleState, + enter = + fadeIn(animationSpec = tween(durationMillis = 500)) + + expandVertically(animationSpec = tween(durationMillis = 500)), + exit = + fadeOut(animationSpec = tween(durationMillis = 500)) + + shrinkVertically(animationSpec = tween(durationMillis = 500)), + ) { + Column( + modifier = + Modifier.fillMaxWidth() + .shadow( + elevation = 16.dp, + shape = RoundedCornerShape(4.dp), + spotColor = darkShadowColor, + ) + .background(color = Color.White, shape = RoundedCornerShape(4.dp)) + .border( + width = 1.dp, + color = colorGrey, + shape = RoundedCornerShape(4.dp), + ) + .padding(vertical = 16.dp, horizontal = 12.dp) + ) { + Row(modifier = Modifier, verticalAlignment = Alignment.Top) { + member.icon?.url?.let { + NaviImage( + imageFieldData = member.icon, + modifier = Modifier.size(24.dp).alpha(if (isRemoved) 0.4f else 1f), + widgetCallback = widgetCallback, + ) + } + Column( + modifier = + Modifier.padding(start = 12.dp) + .weight(1f) + .alpha(if (isRemoved) 0.4f else 1f) + ) { + Row { + member.name?.text?.let { + NaviTextWidgetized( + textFieldData = member.name, + widgetCallback = widgetCallback, + ) + } + member.newTag?.text?.let { + NaviTextWidgetized( + textFieldData = member.newTag, + widgetCallback = widgetCallback, + modifier = Modifier.padding(start = 12.dp), + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + member.age?.text?.let { + NaviTextWidgetized( + textFieldData = member.age, + widgetCallback = widgetCallback, + ) + } + } + icon?.url?.let { + NaviImage( + imageFieldData = icon, + modifier = + Modifier.padding(start = 12.dp) + .size(24.dp) + .clickable( + interactionSource = + remember { MutableInteractionSource() }, + indication = null, + onClick = { onIconClick(member) }, + ), + ) + } + } + } + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberUnderwritingScreenComposables.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberUnderwritingScreenComposables.kt new file mode 100644 index 0000000000..2bcb6451f9 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalMemberUnderwritingScreenComposables.kt @@ -0,0 +1,117 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.ui_components + +import androidx.compose.foundation.background +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.insurance.renewal_revamp.model.response.HealthDetailsCallouts +import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.darkShadowColor +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.hexToColor + +@Composable +fun UnderwritingMemberDetailsComposable( + memberDetails: List?, + widgetCallback: WidgetCallback, +) { + Column( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 40.dp) + .background(color = Color.White, shape = RoundedCornerShape(4.dp)) + .shadow( + elevation = 16.dp, + shape = RoundedCornerShape(4.dp), + spotColor = darkShadowColor, + ) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + memberDetails?.forEachIndexed { _, item -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + item.icon?.url?.let { + NaviImage( + modifier = Modifier.padding(end = 12.dp).size(24.dp), + imageFieldData = item.icon, + widgetCallback = widgetCallback, + ) + } + item.name?.text?.let { + NaviTextWidgetized( + modifier = Modifier.padding(end = 8.dp).weight(weight = 1f, fill = false), + textFieldData = item.name, + widgetCallback = widgetCallback, + ) + } + item.newTag?.text?.let { + NaviTextWidgetized(textFieldData = item.newTag, widgetCallback = widgetCallback) + } + } + } + } +} + +@Composable +fun UnderwritingCalloutsComposable( + callouts: HealthDetailsCallouts?, + widgetCallback: WidgetCallback, +) { + Row( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 16.dp) + .background( + color = hexToColor(callouts?.backgroundColor), + shape = RoundedCornerShape(4.dp), + ) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.Top, + ) { + callouts?.icon?.url?.let { + NaviImage( + modifier = Modifier.padding(end = 8.dp).size(24.dp), + imageFieldData = callouts.icon, + widgetCallback = widgetCallback, + ) + } + Column { + callouts?.title?.text?.let { + NaviTextWidgetized( + modifier = Modifier.padding(bottom = 2.dp), + textFieldData = callouts.title, + widgetCallback = widgetCallback, + ) + } + callouts?.subtitle?.text?.let { + NaviTextWidgetized( + textFieldData = callouts.subtitle, + widgetCallback = widgetCallback, + ) + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalReviewScreenComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalReviewScreenComposable.kt new file mode 100644 index 0000000000..8a04381879 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/ui_components/RenewalReviewScreenComposable.kt @@ -0,0 +1,260 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.ui_components + +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.navi.base.model.CtaData +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.base.utils.orFalse +import com.navi.base.utils.orTrue +import com.navi.design.decorator.DashedDivider +import com.navi.design.theme.GreyEBEBEB +import com.navi.insurance.models.GIResponseState +import com.navi.insurance.purchase.compliance.domain.dto.PatchResponseData +import com.navi.insurance.renewal_revamp.model.response.RenewalReviewScreenResponse +import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.FooterButtonComposable +import com.navi.naviwidgets.composewidget.reusable.darkShadowColor +import com.navi.naviwidgets.composewidget.reusable.footerColorShadow +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 + +@Composable +fun RenewalReviewFooterComposable( + footer: PolicyReviewPageResponse.PolicyReviewFooter, + widgetCallback: WidgetCallback, + state: GIResponseState? = null, + overrideFooterText: String? = null, +) { + Column( + modifier = + Modifier.background( + Brush.verticalGradient(colors = listOf(Color.Transparent, footerColorShadow)) + ) + .padding(top = 35.dp) + ) { + FooterButtonComposable( + modifier = + Modifier.background( + color = Color.White, + shape = RoundedCornerShape(8.dp, 8.dp, 0.dp, 0.dp), + ) + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 32.dp) + .shadow( + elevation = 16.dp, + shape = RoundedCornerShape(4.dp), + spotColor = darkShadowColor, + ) + .wrapContentHeight(), + data = + FooterButtonData( + title = footer.title?.copy(text = overrideFooterText ?: footer.title.text), + backgroundColor = footer.backgroundColor, + cta = footer.cta, + ), + state = + if (state?.isLoading == true) FooterButtonState.LOADING.name + else FooterButtonState.ENABLED.name, + widgetCallback = widgetCallback, + ) + } +} + +@Composable +fun RenewalReviewHeaderComposable( + header: PolicyReviewPageResponse.PolicyReviewHeader, + widgetCallback: WidgetCallback, + isBackBSEnabled: Boolean? = null, + backBottomsheetCta: CtaData? = null, +) { + var updatedLeftIcon = header.leftIcon + if (isBackBSEnabled.orFalse()) { + updatedLeftIcon = header.leftIcon?.copy(cta = backBottomsheetCta) + } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .background(color = hexToColor(header.backgroundColor)) + .padding(16.dp), + ) { + header.leftIcon?.let { + NaviImage( + imageFieldData = updatedLeftIcon, + modifier = Modifier.size(24.dp), + widgetCallback = widgetCallback, + ) + } + header.title?.text?.let { + NaviTextWidgetized(textFieldData = header.title, widgetCallback = widgetCallback) + } + header.rightIcon?.let { + NaviImage( + imageFieldData = header.rightIcon, + modifier = Modifier.size(24.dp), + widgetCallback = widgetCallback, + ) + } + } +} + +@Composable +fun RenewalMemberDetailsComposable( + memberDetails: RenewalReviewScreenResponse.MemberDetails, + widgetCallback: WidgetCallback, +) { + if (memberDetails.members.isNotNullAndNotEmpty()) { + Column( + modifier = + Modifier.padding(horizontal = 16.dp) + .padding(top = 16.dp) + .fillMaxWidth() + .wrapContentHeight() + .border(width = 1.dp, color = GreyEBEBEB, shape = RoundedCornerShape(4.dp)) + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(4.dp), + spotColor = darkShadowColor, + ) + .background( + color = hexToColor(memberDetails.contentBackgroundColor), + shape = RoundedCornerShape(4.dp), + ) + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column { + memberDetails.headerTitle?.text?.let { + NaviTextWidgetized( + textFieldData = memberDetails.headerTitle, + widgetCallback = widgetCallback, + debounceDelay = 600, + ) + } + Row(verticalAlignment = Alignment.Bottom) { + memberDetails.headerSubTitle?.text?.let { + NaviTextWidgetized( + textFieldData = memberDetails.headerSubTitle, + widgetCallback = widgetCallback, + debounceDelay = 600, + modifier = Modifier.padding(top = 4.dp), + ) + } + memberDetails.headerSubTitleTag?.text?.let { + NaviTextWidgetized( + textFieldData = memberDetails.headerSubTitleTag, + widgetCallback = widgetCallback, + debounceDelay = 600, + modifier = Modifier.padding(top = 4.dp, start = 8.dp), + ) + } + } + } + memberDetails.rightTitle?.text?.let { + NaviTextWidgetized( + textFieldData = memberDetails.rightTitle, + widgetCallback = widgetCallback, + debounceDelay = 600, + ) + } + } + if (memberDetails.isMemberSectionVisible.orTrue()) { + Spacer( + modifier = Modifier.height(1.dp).fillMaxWidth().background(color = GreyEBEBEB) + ) + Column( + modifier = + Modifier.background(color = hexToColor(memberDetails.footerBackgroundColor)) + .padding(16.dp), + verticalArrangement = + Arrangement.spacedBy( + if (memberDetails.isDividerVisible == true) 0.dp else 12.dp + ), + ) { + memberDetails.members?.forEachIndexed { index, item -> + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + ) { + item.icon?.url?.let { + NaviImage( + modifier = Modifier.padding(end = 8.dp).size(24.dp), + imageFieldData = item.icon, + widgetCallback = widgetCallback, + ) + } + item.name?.text?.let { + NaviTextWidgetized( + modifier = Modifier.padding(end = 8.dp), + textFieldData = item.name, + widgetCallback = widgetCallback, + ) + } + item.newTag?.text?.let { + NaviTextWidgetized( + textFieldData = item.newTag, + widgetCallback = widgetCallback, + ) + } + } + item.age?.text?.let { + NaviTextWidgetized( + modifier = Modifier.padding(start = 24.dp), + textFieldData = item.age, + widgetCallback = widgetCallback, + ) + } + } + if ( + memberDetails.isDividerVisible == true && + index != memberDetails.members.size - 1 + ) { + DashedDivider( + modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), + thickness = 1.dp, + ) + } + } + } + } + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/CoverAmountChangeVM.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/CoverAmountChangeVM.kt new file mode 100644 index 0000000000..407a93a5fc --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/CoverAmountChangeVM.kt @@ -0,0 +1,251 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.viewmodels + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableIntStateOf +import androidx.lifecycle.viewModelScope +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.common.alchemist.model.AlchemistScreenRequest +import com.navi.common.model.ModuleName +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.CommonNaviAnalytics +import com.navi.insurance.common.GiBaseVM +import com.navi.insurance.common.models.GiErrorMetaData +import com.navi.insurance.common.util.ActionHandler +import com.navi.insurance.models.GIResponseState +import com.navi.insurance.models.response.AlchemistScreenName +import com.navi.insurance.network.ApiErrorTagType +import com.navi.insurance.purchase.compliance.domain.dto.PatchRequestData +import com.navi.insurance.purchase.compliance.domain.dto.PatchResponseData +import com.navi.insurance.renewal_revamp.model.request.RenewalReviewPageRequest +import com.navi.insurance.renewal_revamp.model.response.CoverAmountChangeScreenResponse +import com.navi.insurance.renewal_revamp.repo.RenewalReviewRepository +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.ARG_SELECTED_SUM_INSURED +import com.navi.insurance.util.ARG_TRANSITION +import com.navi.insurance.util.logGiAppErrorEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class CoverAmountChangeVM +@Inject +constructor(private val repository: RenewalReviewRepository, actionHandler: ActionHandler) : + GiBaseVM(actionHandler) { + + private val _selectedIndex = mutableIntStateOf(0) + val selectedIndex: State = _selectedIndex + + private val _existingSelectedIndex = mutableIntStateOf(0) + val existingSelectedIndex: State = _existingSelectedIndex + + private val _bottomSheetOpened = MutableStateFlow(false) + val bottomSheetOpened = _bottomSheetOpened.asStateFlow() + + private val _backCta = MutableStateFlow(null) + val backCta = _backCta.asStateFlow() + + private val _pageDataFlow = MutableStateFlow(GIResponseState()) + val pageDataFlow = _pageDataFlow.asStateFlow() + + private val _numericCoverAmountList = MutableStateFlow>(emptyList()) + val numericCoverAmountList = _numericCoverAmountList.asStateFlow() + + private val _transitionFlow = MutableStateFlow(GIResponseState()) + val transitionFlow = _transitionFlow.asStateFlow() + var existingIndex: Int? = null + var applicationId: String? = null + var screenName: String? = null + + private fun exceptionHandler(errorTag: String) = CoroutineExceptionHandler { _, exception -> + CommonNaviAnalytics.naviAnalytics + .GiError() + .onGlobalError( + exception.message, + insuranceAnalyticsHandler?.getCurrentScreen(), + ModuleName.GI.name, + CommonNaviAnalytics.GLOBAL_GENERIC_ERRORS, + null, + null, + GiErrorMetaData.FLOW_RENEWAL_V2, + errorTag, + ) + logGiAppErrorEvent( + screen = insuranceAnalyticsHandler?.getCurrentScreen().toString(), + errorTitle = errorTag, + errorDes = exception.message.toString(), + isSourceExternal = false, + ) + viewModelScope.launch { + when (errorTag) { + ApiErrorTagType.RENEWAL_COVER_AMOUNT_CHANGE_SCREEN_LOAD_ERROR.value -> { + _pageDataFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + ApiErrorTagType.RENEWAL_COVER_AMOUNT_CHANGE_NEXT_PAGE_TRANSITION.value -> { + _transitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + } + } + } + + fun setSelectedIndex(index: Int) { + _selectedIndex.intValue = index + } + + fun setExistingSelectedIndex(index: Int) { + _existingSelectedIndex.intValue = index + } + + fun setBottomSheetOpened(isOpened: Boolean) { + _bottomSheetOpened.value = isOpened + } + + fun setBackCta(ctaData: CtaData) { + _backCta.value = ctaData + } + + fun fetchPageResponse() { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler( + ApiErrorTagType.RENEWAL_COVER_AMOUNT_CHANGE_SCREEN_LOAD_ERROR.value + ) + ) { + _pageDataFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + val response = + repository.fetchCoverAmountChangeScreen( + AlchemistScreenRequest( + screenName = AlchemistScreenName.GI_COVER_CHANGE_SCREEN.name, + inputMap = + RenewalReviewPageRequest( + applicationId = applicationId, + screenName = screenName, + ) + .toMap(), + ) + ) + val alchemistData = response.data + if (response.isSuccessWithData() && alchemistData?.screenStructure != null) { + _pageDataFlow.update { + it.copy( + isLoading = false, + data = alchemistData.screenStructure, + hasErrorOccurred = false, + ) + } + alchemistData.screenStructure.carouselList?.let { carouselItems -> + val numericCoverAmounts = + carouselItems.mapNotNull { item -> item.numericCoverAmount } + _numericCoverAmountList.value = numericCoverAmounts + existingIndex = + carouselItems.indexOfFirst { item -> + item.existingTag?.text?.isNotEmpty() == true + } + } + } else { + _pageDataFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_COVER_AMOUNT_CHANGE_SCREEN_LOAD_ERROR.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun nextPageTransitionResponse(lineItem: List? = null) { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler( + ApiErrorTagType.RENEWAL_COVER_AMOUNT_CHANGE_NEXT_PAGE_TRANSITION.value + ) + ) { + _transitionFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + var transition: String? = null + lineItem?.forEach { + if (it.key == ARG_TRANSITION) { + transition = it.value + } + if (it.key == ARG_APPLICATION_ID) { + applicationId = it.value + } + } + val response = + repository.fetchNextPage( + PatchRequestData( + applicationId = applicationId.orEmpty(), + pageType = screenName.orEmpty(), + transition = transition.orEmpty(), + data = + listOf( + LineItem( + key = ARG_SELECTED_SUM_INSURED, + value = numericCoverAmountList.value[selectedIndex.value], + ) + ), + ), + applicationId = applicationId.orEmpty(), + transition = transition.orEmpty(), + ) + if (response.isSuccessWithData()) { + _transitionFlow.update { + it.copy(isLoading = false, data = response.data, hasErrorOccurred = false) + } + } else { + _transitionFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_COVER_AMOUNT_CHANGE_NEXT_PAGE_TRANSITION.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun clearTransitionData() { + viewModelScope.launch { + _transitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = false) + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/NewRenewalActivityVM.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/NewRenewalActivityVM.kt new file mode 100644 index 0000000000..1932d0f992 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/NewRenewalActivityVM.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.viewmodels + +import com.navi.base.model.CtaData +import com.navi.insurance.common.GiBaseVM +import com.navi.insurance.common.util.ActionHandler +import com.navi.insurance.renewal_revamp.repo.RenewalReviewRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@HiltViewModel +class NewRenewalActivityVM +@Inject +constructor(private val repository: RenewalReviewRepository, actionHandler: ActionHandler) : + GiBaseVM(actionHandler) { + + private val _backCta = MutableStateFlow(null) + val backCta = _backCta.asStateFlow() + + var applicationId: String? = null + var screenName: String? = null + + fun setBackCta(ctaData: CtaData) { + _backCta.value = ctaData + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberEditVM.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberEditVM.kt new file mode 100644 index 0000000000..4be7d700b8 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberEditVM.kt @@ -0,0 +1,314 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.viewmodels + +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.common.alchemist.model.AlchemistScreenRequest +import com.navi.common.model.ModuleName +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.CommonNaviAnalytics +import com.navi.insurance.common.GiBaseVM +import com.navi.insurance.common.models.GiErrorMetaData +import com.navi.insurance.common.util.ActionHandler +import com.navi.insurance.models.GIResponseState +import com.navi.insurance.models.response.AlchemistScreenName +import com.navi.insurance.network.ApiErrorTagType +import com.navi.insurance.pre.purchase.journey.ERROR_CODE_400 +import com.navi.insurance.purchase.compliance.domain.dto.PatchRequestData +import com.navi.insurance.purchase.compliance.domain.dto.PatchResponseData +import com.navi.insurance.renewal_revamp.model.request.MemberDetailsPatchRequest +import com.navi.insurance.renewal_revamp.model.request.RenewalReviewPageRequest +import com.navi.insurance.renewal_revamp.model.response.RenewalMember +import com.navi.insurance.renewal_revamp.model.response.RenewalMemberEditResponse +import com.navi.insurance.renewal_revamp.repo.RenewalReviewRepository +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.ARG_ASSETS +import com.navi.insurance.util.ARG_CHANGES_CONFIRMED +import com.navi.insurance.util.ARG_TRANSITION +import com.navi.insurance.util.logGiAppErrorEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class RenewalMemberEditVM +@Inject +constructor(private val repository: RenewalReviewRepository, actionHandler: ActionHandler) : + GiBaseVM(actionHandler) { + + private val _pageDataFlow = MutableStateFlow(GIResponseState()) + val pageDataFlow = _pageDataFlow.asStateFlow() + + private val _backCta = MutableStateFlow(null) + val backCta = _backCta.asStateFlow() + + private val _bottomSheetOpened = MutableStateFlow(false) + val bottomSheetOpened = _bottomSheetOpened.asStateFlow() + + private val _backBottomSheetOpened = MutableStateFlow(false) + val backBottomSheetOpened = _backBottomSheetOpened.asStateFlow() + + private val _transitionFlow = MutableStateFlow(GIResponseState()) + val transitionFlow = _transitionFlow.asStateFlow() + + private val _currentlyDeletedMember = MutableStateFlow("") + val currentlyDeletedMember = _currentlyDeletedMember.asStateFlow() + + private val _members = MutableStateFlow>(emptyList()) + val members: StateFlow> = _members.asStateFlow() + + private val _coveredMembers = MutableStateFlow>(emptyList()) + val coveredMembers: StateFlow> = _coveredMembers.asStateFlow() + + private val _removedMembers = MutableStateFlow>(emptyList()) + val removedMembers: StateFlow> = _removedMembers.asStateFlow() + + private val _memberToRemove = MutableStateFlow(null) + var applicationId: String? = null + var screenName: String? = null + + private fun exceptionHandler(errorTag: String) = CoroutineExceptionHandler { _, exception -> + CommonNaviAnalytics.naviAnalytics + .GiError() + .onGlobalError( + exception.message, + insuranceAnalyticsHandler?.getCurrentScreen(), + ModuleName.GI.name, + CommonNaviAnalytics.GLOBAL_GENERIC_ERRORS, + null, + null, + GiErrorMetaData.FLOW_RENEWAL_V2, + errorTag, + ) + logGiAppErrorEvent( + screen = insuranceAnalyticsHandler?.getCurrentScreen().toString(), + errorTitle = errorTag, + errorDes = exception.message.toString(), + isSourceExternal = false, + ) + viewModelScope.launch { + when (errorTag) { + ApiErrorTagType.RENEWAL_EDIT_MEMBER_SCREEN_LOAD_ERROR.value -> { + _pageDataFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + ApiErrorTagType.RENEWAL_EDIT_MEMBER_NEXT_PAGE_TRANSITION.value -> { + _transitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + } + } + } + + fun setBackCta(ctaData: CtaData) { + _backCta.value = ctaData + } + + fun setBottomSheetOpened(isOpened: Boolean) { + _bottomSheetOpened.value = isOpened + } + + fun setBackBottomSheetOpened(isOpened: Boolean) { + _backBottomSheetOpened.value = isOpened + } + + fun setCurrentlyDeletedMember(memberName: String) { + _currentlyDeletedMember.value = memberName + } + + fun fetchPageResponse() { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler(ApiErrorTagType.RENEWAL_EDIT_MEMBER_SCREEN_LOAD_ERROR.value) + ) { + _pageDataFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + val response = + repository.fetchMemberEditScreen( + AlchemistScreenRequest( + screenName = AlchemistScreenName.GI_MEMBER_EDIT_SCREEN.name, + inputMap = + RenewalReviewPageRequest( + applicationId = applicationId, + screenName = screenName, + ) + .toMap(), + ) + ) + val alchemistData = response.data + if (response.isSuccessWithData() && alchemistData?.screenStructure != null) { + val responseData = alchemistData.screenStructure + _pageDataFlow.update { + it.copy(isLoading = false, data = responseData, hasErrorOccurred = false) + } + responseData.members?.let { members -> + _members.value = members + _coveredMembers.value = members.filter { it.isCovered } + _removedMembers.value = members.filter { !it.isCovered && it.isExistingMember } + } + } else { + _pageDataFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_EDIT_MEMBER_SCREEN_LOAD_ERROR.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun nextPageTransitionResponse(lineItem: List? = null) { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler(ApiErrorTagType.RENEWAL_EDIT_MEMBER_NEXT_PAGE_TRANSITION.value) + ) { + _transitionFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + var transition: String? = null + var changesConfirmed: String? = null + lineItem?.forEach { + if (it.key == ARG_TRANSITION) { + transition = it.value + } + if (it.key == ARG_APPLICATION_ID) { + applicationId = it.value + } + if (it.key == ARG_CHANGES_CONFIRMED) { + changesConfirmed = it.value + } + } + val membersData = + members.value.map { + MemberDetailsPatchRequest(assetId = it.assetId, selected = it.isCovered) + } + val response = + repository.fetchNextPage( + PatchRequestData( + applicationId = applicationId.orEmpty(), + pageType = screenName.orEmpty(), + transition = transition.orEmpty(), + data = + listOf( + LineItem(key = ARG_ASSETS, value = Gson().toJson(membersData)), + LineItem( + key = ARG_CHANGES_CONFIRMED, + value = changesConfirmed.orEmpty(), + ), + ), + ), + applicationId = applicationId.orEmpty(), + transition = transition.orEmpty(), + ) + if (response.isSuccessWithData()) { + _transitionFlow.update { + it.copy(isLoading = false, data = response.data, hasErrorOccurred = false) + } + } else { + if (response.statusCode == ERROR_CODE_400) { + response.errors?.getOrNull(0)?.actions?.getOrNull(0)?.cta?.let { errorCta -> + val transitionCta = PatchResponseData(cta = errorCta) + _transitionFlow.value = + _transitionFlow.value.copy( + data = transitionCta, + isLoading = false, + hasErrorOccurred = false, + ) + } + } else { + _transitionFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_EDIT_MEMBER_NEXT_PAGE_TRANSITION.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun updateMemberCoverageStatus(member: RenewalMember, isCovered: Boolean) { + if (member.isCovered == isCovered) return + if (isCovered) { + val updatedMember = + member.copy(isCovered = true, statusConfirmed = !(member.statusConfirmed)) + updateMemberList(member, updatedMember) + _removedMembers.update { it - member } + _coveredMembers.update { it + updatedMember } + } else { + setBottomSheetOpened(true) + setCurrentlyDeletedMember(member.name?.text ?: "") + _memberToRemove.value = member + } + } + + fun updateMemberList(member: RenewalMember, updatedMember: RenewalMember) { + _members.update { currentMembers -> + currentMembers.map { if (it.name?.text == member.name?.text) updatedMember else it } + } + } + + fun confirmMemberRemoval() { + _memberToRemove.value?.let { member -> + val updatedMember = + member.copy(isCovered = false, statusConfirmed = !(member.statusConfirmed)) + updateMemberList(member, updatedMember) + _coveredMembers.update { it - member } + if (member.isExistingMember) { + _removedMembers.update { it + updatedMember } + } + _memberToRemove.value = null + } + setBottomSheetOpened(false) + setCurrentlyDeletedMember("") + } + + fun cancelMemberRemoval() { + _memberToRemove.value = null + setBottomSheetOpened(false) + setCurrentlyDeletedMember("") + } + + fun clearTransitionData() { + viewModelScope.launch { + _transitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = false) + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberUnderwritingVM.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberUnderwritingVM.kt new file mode 100644 index 0000000000..bdb04ef652 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalMemberUnderwritingVM.kt @@ -0,0 +1,211 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.viewmodels + +import androidx.lifecycle.viewModelScope +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.common.alchemist.model.AlchemistScreenRequest +import com.navi.common.model.ModuleName +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.CommonNaviAnalytics +import com.navi.insurance.common.GiBaseVM +import com.navi.insurance.common.models.GiErrorMetaData +import com.navi.insurance.common.util.ActionHandler +import com.navi.insurance.models.GIResponseState +import com.navi.insurance.models.response.AlchemistScreenName +import com.navi.insurance.network.ApiErrorTagType +import com.navi.insurance.purchase.compliance.domain.dto.PatchRequestData +import com.navi.insurance.purchase.compliance.domain.dto.PatchResponseData +import com.navi.insurance.renewal_revamp.model.request.RenewalReviewPageRequest +import com.navi.insurance.renewal_revamp.model.response.RenewalMemberUnderwritingResponse +import com.navi.insurance.renewal_revamp.repo.RenewalReviewRepository +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.ARG_TRANSITION +import com.navi.insurance.util.logGiAppErrorEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class RenewalMemberUnderwritingVM +@Inject +constructor(private val repository: RenewalReviewRepository, actionHandler: ActionHandler) : + GiBaseVM(actionHandler) { + + private val _backCta = MutableStateFlow(null) + val backCta = _backCta.asStateFlow() + + private val _pageDataFlow = + MutableStateFlow(GIResponseState()) + val pageDataFlow = _pageDataFlow.asStateFlow() + + private val _transitionFlow = MutableStateFlow(GIResponseState()) + val transitionFlow = _transitionFlow.asStateFlow() + + var applicationId: String? = null + var screenName: String? = null + + private fun exceptionHandler(errorTag: String) = CoroutineExceptionHandler { _, exception -> + CommonNaviAnalytics.naviAnalytics + .GiError() + .onGlobalError( + exception.message, + insuranceAnalyticsHandler?.getCurrentScreen(), + ModuleName.GI.name, + CommonNaviAnalytics.GLOBAL_GENERIC_ERRORS, + null, + null, + GiErrorMetaData.FLOW_RENEWAL_V2, + errorTag, + ) + logGiAppErrorEvent( + screen = insuranceAnalyticsHandler?.getCurrentScreen().toString(), + errorTitle = errorTag, + errorDes = exception.message.toString(), + isSourceExternal = false, + ) + viewModelScope.launch { + when (errorTag) { + ApiErrorTagType.RENEWAL_MEMBER_UNDERWRITING_SCREEN_LOAD_ERROR.value -> { + _pageDataFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + + ApiErrorTagType.RENEWAL_MEMBER_UNDERWRITING_NEXT_PAGE_TRANSITION.value -> { + _transitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + } + } + } + + fun setBackCta(ctaData: CtaData) { + _backCta.value = ctaData + } + + fun fetchPageResponse() { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler( + ApiErrorTagType.RENEWAL_MEMBER_UNDERWRITING_SCREEN_LOAD_ERROR.value + ) + ) { + _pageDataFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + val response = + repository.fetchMemberUnderwritingScreen( + AlchemistScreenRequest( + screenName = AlchemistScreenName.GI_RENEWAL_UNDERWRITING_SCREEN.name, + inputMap = + RenewalReviewPageRequest( + applicationId = applicationId, + screenName = screenName, + ) + .toMap(), + ) + ) + val alchemistData = response.data + if (response.isSuccessWithData() && alchemistData?.screenStructure != null) { + _pageDataFlow.update { + it.copy( + isLoading = false, + data = alchemistData.screenStructure, + hasErrorOccurred = false, + ) + } + } else { + _pageDataFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_MEMBER_UNDERWRITING_SCREEN_LOAD_ERROR.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun nextPageTransitionResponse(lineItem: List? = null) { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler( + ApiErrorTagType.RENEWAL_MEMBER_UNDERWRITING_NEXT_PAGE_TRANSITION.value + ) + ) { + _transitionFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + var transition: String? = null + lineItem?.forEach { + if (it.key == ARG_TRANSITION) { + transition = it.value + } + if (it.key == ARG_APPLICATION_ID) { + applicationId = it.value + } + } + val response = + repository.fetchNextPage( + PatchRequestData( + applicationId = applicationId.orEmpty(), + pageType = screenName.orEmpty(), + transition = transition.orEmpty(), + data = listOf(), + ), + applicationId = applicationId.orEmpty(), + transition = transition.orEmpty(), + ) + if (response.isSuccessWithData()) { + _transitionFlow.update { + it.copy(isLoading = false, data = response.data, hasErrorOccurred = false) + } + } else { + _transitionFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_MEMBER_UNDERWRITING_NEXT_PAGE_TRANSITION.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun clearTransitionData() { + viewModelScope.launch { + _transitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = false) + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalReviewVM.kt b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalReviewVM.kt new file mode 100644 index 0000000000..cb7d43f6ec --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/renewal_revamp/viewmodels/RenewalReviewVM.kt @@ -0,0 +1,305 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.renewal_revamp.viewmodels + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.viewModelScope +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.common.ResponseState +import com.navi.common.alchemist.model.AlchemistScreenRequest +import com.navi.common.model.ModuleName +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.CommonNaviAnalytics +import com.navi.insurance.common.GiBaseVM +import com.navi.insurance.common.models.GiErrorMetaData +import com.navi.insurance.common.util.ActionHandler +import com.navi.insurance.models.GIResponseState +import com.navi.insurance.models.response.AlchemistScreenName +import com.navi.insurance.network.ApiErrorTagType +import com.navi.insurance.purchase.compliance.domain.dto.PatchRequestData +import com.navi.insurance.purchase.compliance.domain.dto.PatchResponseData +import com.navi.insurance.renewal_revamp.model.request.RenewalReviewAddonRequest +import com.navi.insurance.renewal_revamp.model.request.RenewalReviewPageRequest +import com.navi.insurance.renewal_revamp.model.response.RenewalReviewScreenResponse +import com.navi.insurance.renewal_revamp.repo.RenewalReviewRepository +import com.navi.insurance.review_policy.model.request.AddonData +import com.navi.insurance.util.ARG_APPLICATION_ID +import com.navi.insurance.util.ARG_TRANSITION +import com.navi.insurance.util.COVER_ID_EXTRA +import com.navi.insurance.util.REFRESH_RENEWAL_APPLICATION +import com.navi.insurance.util.logGiAppErrorEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class RenewalReviewVM +@Inject +constructor(private val repository: RenewalReviewRepository, actionHandler: ActionHandler) : + GiBaseVM(actionHandler) { + + private val _isAddonAdded = mutableStateOf(false) + val isAddonAdded: State = _isAddonAdded + + private val _backCta = MutableStateFlow(null) + val backCta = _backCta.asStateFlow() + + private val _pageDataFlow = MutableStateFlow(GIResponseState()) + val pageDataFlow = _pageDataFlow.asStateFlow() + + private val _renewalReviewAddonDataFlow = + MutableStateFlow>(ResponseState.Idle) + val renewalReviewAddonDataFlow = _renewalReviewAddonDataFlow.asStateFlow() + + private val _renewalReviewTransitionFlow = + MutableStateFlow(GIResponseState()) + val renewalReviewTransitionFlow = _renewalReviewTransitionFlow.asStateFlow() + + var applicationId: String? = null + var coverId: String? = null + var screenName: String? = null + var policyId: String? = null + var applicationType: String? = null + var refreshRenewalApplication: String? = null + var transition: String? = null + + private fun exceptionHandler(errorTag: String) = CoroutineExceptionHandler { _, exception -> + CommonNaviAnalytics.naviAnalytics + .GiError() + .onGlobalError( + exception.message, + insuranceAnalyticsHandler?.getCurrentScreen(), + ModuleName.GI.name, + CommonNaviAnalytics.GLOBAL_GENERIC_ERRORS, + null, + null, + GiErrorMetaData.FLOW_RENEWAL_V2, + errorTag, + ) + logGiAppErrorEvent( + screen = insuranceAnalyticsHandler?.getCurrentScreen().toString(), + errorTitle = errorTag, + errorDes = exception.message.toString(), + isSourceExternal = false, + ) + viewModelScope.launch { + when (errorTag) { + ApiErrorTagType.RENEWAL_REVIEW_SCREEN_LOAD_ERROR.value -> { + _pageDataFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + + ApiErrorTagType.RENEWAL_REVIEW_ADDON_LOAD_ERROR.value -> { + _renewalReviewAddonDataFlow.emit(ResponseState.Failure("Something went wrong")) + } + + ApiErrorTagType.RENEWAL_REVIEW_NEXT_PAGE_TRANSITION.value -> { + _renewalReviewTransitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = true) + } + } + } + } + } + + fun setAddonFlag(shown: Boolean) { + _isAddonAdded.value = shown + } + + fun setBackCta(ctaData: CtaData) { + _backCta.value = ctaData + } + + fun fetchPageResponse() { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler(ApiErrorTagType.RENEWAL_REVIEW_SCREEN_LOAD_ERROR.value) + ) { + _pageDataFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + val response = + repository.fetchPolicyReviewScreen( + AlchemistScreenRequest( + screenName = AlchemistScreenName.GI_RENEWAL_REVIEW_SCREEN.name, + inputMap = + RenewalReviewPageRequest( + applicationId = applicationId, + screenName = screenName, + applicationType = applicationType, + policyId = policyId, + transition = transition, + refreshRenewalApplication = refreshRenewalApplication, + ) + .toMap(), + ) + ) + val alchemistData = response.data + if (response.isSuccessWithData() && alchemistData?.screenStructure != null) { + _pageDataFlow.update { + it.copy( + isLoading = false, + data = alchemistData.screenStructure, + hasErrorOccurred = false, + ) + } + } else { + _pageDataFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_REVIEW_SCREEN_LOAD_ERROR.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun fetchPolicyReviewAddonResponse(lineItem: List? = null) { + var refreshRenewalApplication: String? = null + lineItem?.forEach { + if (it.key == COVER_ID_EXTRA) { + coverId = it.value + } + if (it.key == REFRESH_RENEWAL_APPLICATION) { + refreshRenewalApplication = it.value + } + } + viewModelScope.launch( + Dispatchers.IO + exceptionHandler(ApiErrorTagType.RENEWAL_REVIEW_ADDON_LOAD_ERROR.value) + ) { + _renewalReviewAddonDataFlow.emit(ResponseState.Loading) + val request = + RenewalReviewAddonRequest( + applicationId = applicationId, + screenName = screenName, + addonData = listOf(AddonData(coverId, isAddonAdded.value.not())), + refreshRenewalApplication = refreshRenewalApplication, + applicationType = applicationType, + policyId = policyId, + ) + .toMap() + val response = + repository.fetchPolicyReviewScreen( + AlchemistScreenRequest( + screenName = AlchemistScreenName.GI_RENEWAL_REVIEW_SCREEN.name, + inputMap = request, + ) + ) + val alchemistData = response.data + if (response.isSuccessWithData() && alchemistData?.screenStructure != null) { + _renewalReviewAddonDataFlow.emit( + ResponseState.Success(alchemistData.screenStructure) + ) + _pageDataFlow.update { + it.copy( + isLoading = false, + data = alchemistData.screenStructure, + hasErrorOccurred = false, + ) + } + setAddonFlag(isAddonAdded.value.not()) + } else { + _renewalReviewAddonDataFlow.emit(ResponseState.Failure("Something went wrong")) + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_REVIEW_ADDON_LOAD_ERROR.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun nextPageTransitionResponse(lineItem: List? = null) { + viewModelScope.launch( + Dispatchers.IO + + exceptionHandler(ApiErrorTagType.RENEWAL_REVIEW_NEXT_PAGE_TRANSITION.value) + ) { + _renewalReviewTransitionFlow.update { + it.copy(isLoading = true, data = null, hasErrorOccurred = false) + } + var transition: String? = null + var refreshRenewalApplication: String? = null + lineItem?.forEach { + if (it.key == ARG_TRANSITION) { + transition = it.value + } + if (it.key == REFRESH_RENEWAL_APPLICATION) { + refreshRenewalApplication = it.value + } + if (it.key == ARG_APPLICATION_ID) { + applicationId = it.value + } + } + val response = + repository.fetchNextPage( + PatchRequestData( + applicationId = applicationId.orEmpty(), + pageType = screenName.orEmpty(), + transition = transition.orEmpty(), + data = + listOf( + LineItem( + key = REFRESH_RENEWAL_APPLICATION, + value = refreshRenewalApplication, + ) + ), + ), + applicationId = applicationId.orEmpty(), + transition = transition.orEmpty(), + ) + if (response.isSuccessWithData()) { + _renewalReviewTransitionFlow.update { + it.copy(isLoading = false, data = response.data, hasErrorOccurred = false) + } + } else { + _renewalReviewTransitionFlow.update { + it.copy( + isLoading = false, + data = null, + hasErrorOccurred = true, + errorResponse = response.error, + ) + } + logError( + response, + GiErrorMetaData( + ApiErrorTagType.RENEWAL_REVIEW_NEXT_PAGE_TRANSITION.value, + flowName = GiErrorMetaData.FLOW_RENEWAL_V2, + ), + ) + } + } + } + + fun clearTransitionData() { + viewModelScope.launch { + _renewalReviewTransitionFlow.update { + it.copy(isLoading = false, data = null, hasErrorOccurred = false) + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/composables/PolicyReviewScreen.kt b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/composables/PolicyReviewScreen.kt index bc5c601654..5d427e2597 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/composables/PolicyReviewScreen.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/composables/PolicyReviewScreen.kt @@ -24,7 +24,9 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -205,6 +207,9 @@ fun PolicyReviewScreen( } }, ) { padding -> + val addonResponse by + viewModel.policyReviewAddonDataFlow.collectAsStateWithLifecycle() + val isChecked by remember { viewModel.isAddonAdded } LazyColumn( modifier = Modifier.background(Color.White) @@ -234,7 +239,8 @@ fun PolicyReviewScreen( AddonComposable( addOnsDetails = addOnsDetails, widgetCallback = widgetCallback, - viewModel = viewModel, + addonResponse = addonResponse, + isChecked = isChecked, ) } } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/model/response/PolicyReviewPageResponse.kt b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/model/response/PolicyReviewPageResponse.kt index a94d6934ef..c0583a61c3 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/model/response/PolicyReviewPageResponse.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/model/response/PolicyReviewPageResponse.kt @@ -44,6 +44,7 @@ data class PolicyReviewPageResponse( @SerializedName("title") val title: TextFieldData? = null, @SerializedName("backgroundColor") val backgroundColor: String? = null, @SerializedName("rightIcon") val rightIcon: ImageFieldData? = null, + @SerializedName("cta") val cta: CtaData? = null, ) : Parcelable @Parcelize @@ -73,6 +74,7 @@ data class PolicyReviewPageResponse( @SerializedName("age") val age: TextFieldData? = null, @SerializedName("relation") val relation: TextFieldData? = null, @SerializedName("icon") val icon: ImageFieldData? = null, + @SerializedName("newTag") val newTag: TextFieldData? = null, ) : Parcelable @Parcelize @@ -142,6 +144,8 @@ data class PolicyReviewPageResponse( @SerializedName("description") val description: TextFieldData? = null, @SerializedName("footerCta") val footerCta: FooterButton? = null, @SerializedName("footerCard") val footerCard: FooterCard? = null, + @SerializedName("backgroundColor") val backgroundColor: String? = null, + @SerializedName("cta") val cta: CtaData? = null, ) : Parcelable { @Parcelize diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/repo/PolicyReviewRepository.kt b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/repo/PolicyReviewRepository.kt index 2429f88945..eab2961be6 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/repo/PolicyReviewRepository.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/repo/PolicyReviewRepository.kt @@ -41,7 +41,7 @@ class PolicyReviewRepository @Inject constructor(private val retrofitService: Re transition: String, ): RepoResult { return giResponseCallback( - retrofitService.fetchPurchaseComplianceNextPage( + retrofitService.fetchApplicationTransitionResponse( request = request, applicationId = applicationId, transition = transition, diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/AddonComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/AddonComposable.kt index e6c220cab5..989e53c409 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/AddonComposable.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/AddonComposable.kt @@ -20,20 +20,17 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.common.ResponseState import com.navi.insurance.review_policy.model.response.PolicyReviewPageResponse -import com.navi.insurance.review_policy.viewmodel.PolicyReviewVM import com.navi.naviwidgets.callbacks.WidgetCallback import com.navi.naviwidgets.composewidget.reusable.darkShadowColor import com.navi.naviwidgets.extensions.NaviImage @@ -44,7 +41,8 @@ import com.navi.naviwidgets.extensions.hexToColor fun AddonComposable( addOnsDetails: PolicyReviewPageResponse.AddOnsDetails, widgetCallback: WidgetCallback, - viewModel: PolicyReviewVM, + addonResponse: ResponseState<*>, + isChecked: Boolean, ) { addOnsDetails.title?.text?.let { TitleWithSubtitleComposable( @@ -55,7 +53,8 @@ fun AddonComposable( AddonCard( addOnsDetails = addOnsDetails, widgetCallback = widgetCallback, - viewModel = viewModel, + addonResponse = addonResponse, + isChecked = isChecked, ) } } @@ -64,12 +63,12 @@ fun AddonComposable( fun AddonCard( addOnsDetails: PolicyReviewPageResponse.AddOnsDetails, widgetCallback: WidgetCallback, - viewModel: PolicyReviewVM, + addonResponse: ResponseState<*>, + isChecked: Boolean, ) { - val addonResponse = viewModel.policyReviewAddonDataFlow.collectAsStateWithLifecycle() - val isChecked by remember { viewModel.isAddonAdded } - val isLoading = addonResponse.value is ResponseState.Loading - val isFailed = addonResponse.value is ResponseState.Failure + val isLoading = addonResponse is ResponseState.Loading + val isFailed = addonResponse is ResponseState.Failure + Column( modifier = Modifier.fillMaxWidth() @@ -139,7 +138,7 @@ fun AddonCard( ) .padding(horizontal = 12.dp, vertical = 8.dp) ) { - if (addonResponse.value is ResponseState.Loading) { + if (isLoading) { LoaderLottie(addOnsDetails.loaderLottieUrl) } else { NaviTextWidgetized( diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/TitleWithSubtitleComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/TitleWithSubtitleComposable.kt index 4979b48ebc..0e6540666e 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/TitleWithSubtitleComposable.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/review_policy/ui_components/TitleWithSubtitleComposable.kt @@ -10,8 +10,11 @@ package com.navi.insurance.review_policy.ui_components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.navi.naviwidgets.callbacks.WidgetCallback import com.navi.naviwidgets.extensions.NaviTextWidgetized import com.navi.naviwidgets.models.response.TextFieldData @@ -21,15 +24,30 @@ fun TitleWithSubtitleComposable( title: TextFieldData?, modifier: Modifier = Modifier, subTitle: TextFieldData? = null, + titleTag: TextFieldData? = null, widgetCallback: WidgetCallback? = null, ) { - Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - title?.text?.let { - NaviTextWidgetized( - textFieldData = title, - widgetCallback = widgetCallback, - debounceDelay = 600, - ) + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + title?.text?.let { + NaviTextWidgetized( + textFieldData = title, + widgetCallback = widgetCallback, + debounceDelay = 600, + ) + } + titleTag?.text?.let { + NaviTextWidgetized( + textFieldData = titleTag, + widgetCallback = widgetCallback, + debounceDelay = 600, + modifier = Modifier.padding(start = 8.dp), + ) + } } subTitle?.text?.let { NaviTextWidgetized( diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/util/Constants.kt b/android/navi-insurance/src/main/java/com/navi/insurance/util/Constants.kt index b9e320a3f5..c1b3e5c80e 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/util/Constants.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/util/Constants.kt @@ -277,6 +277,7 @@ object Constants { const val PLUS_SIGN = "+" const val MINUS_SIGN = "-" const val DOB_ID = "dob" + const val SAVE_CHANGES = "Save changes" // This is required for animating a component for Health risk score val numberConstant: List = diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/util/IntentConstants.kt b/android/navi-insurance/src/main/java/com/navi/insurance/util/IntentConstants.kt index dcf43537eb..6be12e2e4f 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/util/IntentConstants.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/util/IntentConstants.kt @@ -79,6 +79,10 @@ const val PRE_MANDATE = "pre_mandate" const val PAYMENT_SCHEDULE_TYPE = "paymentScheduleType" const val OTP_TOKEN_EXTRA = "otpToken" const val LOAN_ACCOUNT_NUMBER = "loanAccountNumber" +const val REFRESH_RENEWAL_APPLICATION = "refreshRenewalApplication" +const val ARG_CHANGES_CONFIRMED = "changesConfirmed" +const val ARG_ASSETS = "assets" +const val ARG_SELECTED_SUM_INSURED = "selectedSumInsured" enum class PaymentFlowIdentifier { ANNUAL_RENEWAL diff --git a/android/navi-insurance/src/main/res/values/strings.xml b/android/navi-insurance/src/main/res/values/strings.xml index f4bd901e2b..b4d1fd3100 100644 --- a/android/navi-insurance/src/main/res/values/strings.xml +++ b/android/navi-insurance/src/main/res/values/strings.xml @@ -610,4 +610,8 @@ I highly recommend Navi Health policy because of its comprehensive benefits with "Health card " Something went wrong! Please check your internet and try again. Preparing your file... - \ No newline at end of file + REVIEW_SCREEN + COVER_AMOUNT_CHANGE_SCREEN + RENEWAL_MEMBER_EDIT_SCREEN + RENEWAL_MEMBER_UNDERWRITING_SCREEN + diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetDataDeserializer.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetDataDeserializer.kt index a01c65ad73..bee9960fb3 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetDataDeserializer.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetDataDeserializer.kt @@ -529,6 +529,10 @@ class WidgetDataDeserializer : JsonDeserializer { WidgetTypes.TITLE_SUBTITLE_DIVIDER_LIST_WIDGET.value -> { Gson().fromJson(jsonObject, TitleSubtitleDividerListWidgetData::class.java) } + WidgetTypes.MEMBER_IDENTITY_DETAILS_WIDGET.value -> { + Gson() + .fromJson(jsonObject, TextEditTextCalendarWidgetComposableData::class.java) + } else -> { Gson().fromJson(jsonObject, GenericWidgetDataInfo::class.java) } diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetTypes.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetTypes.kt index f82d647c61..dfd6c1fc21 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetTypes.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/WidgetTypes.kt @@ -152,4 +152,5 @@ enum class WidgetTypes(val value: String) { CARD_WITH_TITLE_AND_BOTTOM_TAG_WIDGET("CARD_WITH_TITLE_AND_BOTTOM_TAG_WIDGET"), TITLE_SUBTITLE_IMAGE_FOOTER_WIDGET("TITLE_SUBTITLE_IMAGE_FOOTER_WIDGET"), TITLE_SUBTITLE_DIVIDER_LIST_WIDGET("TITLE_SUBTITLE_DIVIDER_LIST_WIDGET"), + MEMBER_IDENTITY_DETAILS_WIDGET("MEMBER_IDENTITY_DETAILS_WIDGET"), } diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/adapters/TextEditTextCalendarItemAdapter.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/adapters/TextEditTextCalendarItemAdapter.kt index 3ad65b5b88..f7593c2dc3 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/adapters/TextEditTextCalendarItemAdapter.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/adapters/TextEditTextCalendarItemAdapter.kt @@ -18,7 +18,6 @@ import android.widget.TextView import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView -import com.navi.base.utils.MILLISECONDS_PER_SECOND import com.navi.base.utils.isNotNull import com.navi.base.utils.orZero import com.navi.naviwidgets.R @@ -31,7 +30,11 @@ import com.navi.naviwidgets.models.EditTextData import com.navi.naviwidgets.models.TextEditTextCalendarItem import com.navi.naviwidgets.models.response.TextFieldData import com.navi.naviwidgets.utils.EMPTY +import com.navi.naviwidgets.utils.getAgeInMonths +import com.navi.naviwidgets.utils.getCalendarStyle import com.navi.naviwidgets.utils.getFormattedDate +import com.navi.naviwidgets.utils.getMaximumAgePossible +import com.navi.naviwidgets.utils.getMinAgePossible import java.util.* const val STYLE_SPINNER = "SPINNER" @@ -207,7 +210,7 @@ class TextEditTextCalendarItemAdapter( val datePickerDialog = DatePickerDialog( binding.root.context, - getStyle(datePickerData?.style), + getCalendarStyle(datePickerData?.style), { view, year, monthOfYear, dayOfMonth -> binding.tvDob.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) val date = getFormattedDate(year, monthOfYear, dayOfMonth) @@ -239,36 +242,6 @@ class TextEditTextCalendarItemAdapter( } } - private fun getMinAgePossible(maxDate: Int): Long { - val minCalendar = - Calendar.getInstance().apply { timeZone = TimeZone.getTimeZone("GMT+05:30") } - val maxCalendar = - Calendar.getInstance().apply { timeZone = TimeZone.getTimeZone("GMT+05:30") } - minCalendar.add(Calendar.YEAR, maxDate) - return minCalendar.timeInMillis - } - - private fun getMaximumAgePossible(minDate: Int): Long { - val maxCalendar = - Calendar.getInstance().apply { timeZone = TimeZone.getTimeZone("GMT+05:30") } - maxCalendar.add(Calendar.YEAR, minDate) - return maxCalendar.timeInMillis - MILLISECONDS_PER_SECOND - } - - private fun getAgeInMonths(minMonth: Int): Long { - val maxCalendar = - Calendar.getInstance().apply { timeZone = TimeZone.getTimeZone("GMT+05:30") } - maxCalendar.add(Calendar.MONTH, minMonth) - return maxCalendar.timeInMillis - } - - private fun getStyle(style: String?): Int { - return when (style) { - STYLE_SPINNER -> R.style.SpinnerDatePickerDialog - else -> R.style.CalendarDatePickerDialog - } - } - private fun setEditTextData( editTextData: EditTextData?, etName: AppCompatEditText?, diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/FooterCardComposable.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/FooterCardComposable.kt index a752a190d2..f87bcf1f08 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/FooterCardComposable.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/FooterCardComposable.kt @@ -10,9 +10,12 @@ package com.navi.naviwidgets.composewidget.reusable 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.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape @@ -44,15 +47,27 @@ fun FooterCardComposable(data: FooterInfo? = null, widgetCallback: WidgetCallbac onClick = { data.cta?.let { ctaData -> widgetCallback?.onClick(ctaData) } }, ), ) { - if (data.rightTitle.isNotNull()) { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), + if (data.rightTitle.isNotNull() || data.subtitle.isNotNull()) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp) ) { - NaviTextWidgetized(textFieldData = data.title, modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.width(16.dp)) - NaviTextWidgetized(textFieldData = data.rightTitle) + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + NaviTextWidgetized( + textFieldData = data.title, + modifier = Modifier.weight(1f), + widgetCallback = widgetCallback, + ) + Spacer(modifier = Modifier.width(16.dp)) + NaviTextWidgetized( + textFieldData = data.rightTitle, + widgetCallback = widgetCallback, + ) + } + Spacer(modifier = Modifier.height(4.dp)) + NaviTextWidgetized(textFieldData = data.subtitle) } } else { NaviTextWidgetized(textFieldData = data.title) diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/widgets/FooterWithCardAndSnackBarWidgetComposable.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/widgets/FooterWithCardAndSnackBarWidgetComposable.kt index b88fc3fde7..8b3a74feb5 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/widgets/FooterWithCardAndSnackBarWidgetComposable.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/widgets/FooterWithCardAndSnackBarWidgetComposable.kt @@ -38,6 +38,12 @@ fun FooterWithCardAndSnackBarWidgetComposable( widgetCallback: WidgetCallback?, ) { val data = data ?: return + val isCardVisible = + data.footerWithCardAndSnackbarWidgetBody?.let { body -> + (body.cardInfo.isNotNull() || body.errorCardInfo.isNotNull()) && + (body.footerButton?.title?.text.isNotNull() || + body.secondaryFooterButton?.title?.text.isNotNull()) + } ?: false Column { data.footerWithCardAndSnackbarWidgetBody?.titleInfo?.let { titleInfo -> NaviTextWidgetized( @@ -51,20 +57,7 @@ fun FooterWithCardAndSnackBarWidgetComposable( if (state == FooterButtonState.SNACKBAR.name) FooterSnackbarComposable(data = data.footerWithCardAndSnackbarWidgetBody?.snackBarInfo) Column(modifier = Modifier.fillMaxWidth()) { - if ( - (data.footerWithCardAndSnackbarWidgetBody?.cardInfo.isNotNull() || - data.footerWithCardAndSnackbarWidgetBody?.errorCardInfo.isNotNull()) && - (data.footerWithCardAndSnackbarWidgetBody - ?.footerButton - ?.title - ?.text - .isNotNull() || - data.footerWithCardAndSnackbarWidgetBody - ?.secondaryFooterButton - ?.title - ?.text - .isNotNull()) - ) { + if (isCardVisible) { Box( modifier = Modifier.background( @@ -74,20 +67,37 @@ fun FooterWithCardAndSnackBarWidgetComposable( ), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), ) - .padding(top = 8.dp) + .padding(top = 12.dp) ) { FooterCardComposable( data = if (state == FooterButtonState.ERROR.name) - data.footerWithCardAndSnackbarWidgetBody?.errorCardInfo - else data.footerWithCardAndSnackbarWidgetBody?.cardInfo, + data.footerWithCardAndSnackbarWidgetBody.errorCardInfo + else data.footerWithCardAndSnackbarWidgetBody.cardInfo, widgetCallback = widgetCallback, ) } } Row( horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth().background(whiteColor).padding(horizontal = 8.dp), + modifier = + Modifier.fillMaxWidth() + .background( + brush = + Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.LightGray) + ), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + ) + .padding(top = if (isCardVisible) 0.dp else 24.dp) + .background( + color = whiteColor, + shape = + if (isCardVisible) + RoundedCornerShape(topStart = 0.dp, topEnd = 0.dp) + else RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + ) + .padding(horizontal = 8.dp), ) { val primaryFooterButtonData = data.footerWithCardAndSnackbarWidgetBody?.footerButton val secondaryFooterButtonData = diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FooterWithCardAndSnackbarWidgetData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FooterWithCardAndSnackbarWidgetData.kt index ac2e55d344..f5457076df 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FooterWithCardAndSnackbarWidgetData.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FooterWithCardAndSnackbarWidgetData.kt @@ -69,6 +69,7 @@ data class FooterWithCardAndSnackbarWidgetBody( data class FooterInfo( @SerializedName("title") val title: TextFieldData? = null, @SerializedName("rightTitle") val rightTitle: TextFieldData? = null, + @SerializedName("subtitle", alternate = ["subTitle"]) val subtitle: TextFieldData? = null, @SerializedName("backgroundColor") val backgroundColor: String? = null, @SerializedName("cta") val cta: CtaData? = null, ) diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidget.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidget.kt index 219b8865cb..efdc3dcea8 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidget.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidget.kt @@ -67,6 +67,8 @@ data class EditTextData( @SerializedName("title") var title: TextFieldData? = null, @SerializedName("hint") var hint: TextFieldData? = null, @SerializedName("trailingContent") var trailingContent: TextFieldData? = null, + @SerializedName("error") var error: TextFieldData? = null, + @SerializedName("errorMessage") var errorMessage: String? = null, ) { fun updateName(value: String?) { this.value = value diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidgetComposableData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidgetComposableData.kt new file mode 100644 index 0000000000..d081f70317 --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextEditTextCalendarWidgetComposableData.kt @@ -0,0 +1,73 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.models + +import com.google.gson.annotations.SerializedName +import com.navi.base.model.CtaData +import com.navi.naviwidgets.interfaces.GenericWidgetInfo +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData +import com.navi.naviwidgets.models.response.WidgetError + +data class TextEditTextCalendarWidgetComposableData( + @SerializedName("widgetData") + val textEditTextCalendarWidgetBody: TextEditTextCalendarWidgetComposableBody? = null +) : + GenericWidgetDataInfo( + widgetId = WIDGET_ID, + widgetNameForBaseAdapter = WIDGET_ID, + isDependentWidget = false, + dependencyWidgetId = null, + isDependencyWidgetShowing = false, + widgetError = null, + ), + GenericWidgetInfo { + + companion object { + const val WIDGET_ID = "MEMBER_IDENTITY_DETAILS_WIDGET" + } + + override fun widgetId(): String? = widgetId + + override fun widgetData(): TextEditTextCalendarWidgetComposableBody? = + textEditTextCalendarWidgetBody + + override fun widgetLayoutParams(): WidgetLayoutParams? = widgetLayoutParams + + override fun isDependentWidget(): Boolean = isDependentWidget ?: false + + override fun setDependencyWidgetState(isShowing: Boolean) { + isDependencyWidgetShowing = isShowing + } + + override fun widgetError(): WidgetError? = widgetError + + override fun widgetError(widgetError: WidgetError?) { + this.widgetError = widgetError + } +} + +data class TextEditTextCalendarWidgetComposableBody( + @SerializedName("items") val items: List? = null, + @SerializedName("questionHeaderWidget") val questionHeaderWidget: QuestionHeaderWidget? = null, + @SerializedName("infoIcon") val infoIcon: ImageFieldData? = null, +) + +data class TextEditTextCalendarItemComposable( + @SerializedName("nameKey") val nameKey: String? = null, + @SerializedName("dobKey") val dobKey: String? = null, + @SerializedName("id") var id: String? = null, + @SerializedName("title") var title: TextFieldData? = null, + @SerializedName("editTextData") var editTextData: EditTextData? = null, + @SerializedName("calendar") val calendar: CalendarData? = null, + @SerializedName("disableEditing") var disableEditing: Boolean? = false, + @SerializedName("disabledCta") var disabledCta: CtaData? = null, + @SerializedName("rightTitle") val rightTitle: TextFieldData? = null, + @SerializedName("assetId") val assetId: String? = null, + @SerializedName("bottomContent") val bottomContent: TextFieldData? = null, +) diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/CalendarUtil.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/CalendarUtil.kt new file mode 100644 index 0000000000..9193470e39 --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/CalendarUtil.kt @@ -0,0 +1,52 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.utils + +import com.navi.base.utils.MILLISECONDS_PER_SECOND +import com.navi.naviwidgets.adapters.STYLE_SPINNER +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import java.util.TimeZone + +private val DEFAULT_TIMEZONE = TimeZone.getTimeZone("GMT+05:30") + +private fun getCalendarInstance() = Calendar.getInstance().apply { timeZone = DEFAULT_TIMEZONE } + +fun getMinAgePossible(maxDate: Int): Long { + return getCalendarInstance().apply { add(Calendar.YEAR, maxDate) }.timeInMillis +} + +fun getMaximumAgePossible(minDate: Int): Long { + return getCalendarInstance().apply { add(Calendar.YEAR, minDate) }.timeInMillis - + MILLISECONDS_PER_SECOND +} + +fun getAgeInMonths(minMonth: Int): Long { + return getCalendarInstance().apply { add(Calendar.MONTH, minMonth) }.timeInMillis +} + +fun getCalendarStyle(style: String?) = + when (style) { + STYLE_SPINNER -> com.navi.naviwidgets.R.style.SpinnerDatePickerDialog + else -> com.navi.naviwidgets.R.style.CalendarDatePickerDialog + } + +fun getFormattedDateInEnglish(year: Int, month: Int, day: Int): String { + val calendar = Calendar.getInstance() + calendar.set(year, month, day) + val monthName = calendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.ENGLISH) + return "$day $monthName $year" +} + +fun getFormattedDateInEnglishToDDMMYYYY(input: String): String { + val inFmt = SimpleDateFormat("d MMM yyyy", Locale.ENGLISH) + val outFmt = SimpleDateFormat("ddMMyyyy", Locale.ENGLISH) + val date = inFmt.parse(input) ?: return "" + return outFmt.format(date) +} diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt index 5a4a4308e5..bddb0790aa 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt @@ -324,6 +324,7 @@ object NaviWidgetIconUtils { private const val ARC_LOTTIE_PLACEHOLDER_BANNER = "ARC_LOTTIE_PLACEHOLDER_BANNER" private const val NAVI_ICON_WITH_PURPLE_CIRCLE_BACKGROUND = "NAVI_ICON_WITH_PURPLE_CIRCLE_BACKGROUND" + private const val CALENDAR_GREY = "CALENDAR_GREY" fun updateIcon(imageDetail: ImageDetail, imageView: ImageView) { imageDetail.iconCode?.let { iconCode -> @@ -672,6 +673,7 @@ object NaviWidgetIconUtils { NOTIFICATION_ICON_WITH_BORDER -> R.drawable.ic_home_notification ARC_LOTTIE_PLACEHOLDER_BANNER -> R.drawable.arc_banner NAVI_ICON_WITH_PURPLE_CIRCLE_BACKGROUND -> R.drawable.navi_icon + CALENDAR_GREY -> DesignR.drawable.ic_calendar_grey else -> ImageCode.IMAGE_NOT_FOUND.code } }