From c6fd07bc03dc59d2a96967f03381eb3bf108a3a8 Mon Sep 17 00:00:00 2001 From: Kshitij Pramod Ghongadi Date: Wed, 4 Jun 2025 15:17:29 +0530 Subject: [PATCH] NTP-66100 | Price Quote Journey Revamp (#16410) --- .../common/navigator/NaviDeepLinkNavigator.kt | 11 + .../main/java/com/navi/common/utils/Ext.kt | 18 + .../analytics/InsuranceAnalyticsConstants.kt | 8 + .../TitleWithPermissionListBottomSheet.kt | 255 +++++++++++ .../TitleWithPermissionListBottomSheetData.kt | 28 ++ .../common/util/NavigationHandler.kt | 1 + .../NaviInsuranceDeeplinkNavigator.kt | 4 + .../journey/PreQuoteJourneyPageResponse.kt | 2 + .../journey/PreQuoteJourneyRespository.kt | 12 +- .../HeaderWithTrackerWidgetComposable.kt | 16 +- .../composables/MemberCounterComposable.kt | 130 ++++++ .../MemberSelectionGridComposable.kt | 210 +++++++++ .../MemberSelectionWidgetComposable.kt | 424 +++++++++++++++++ .../MultiTypeSelectionWidgetComposable.kt | 5 + .../NameDobTextWidgetComposable.kt | 124 +++++ .../composables/OutlinedCheckWithDropDown.kt | 6 +- .../PincodeInputWidgetV2Composable.kt | 428 ++++++++++++++++++ .../TextWithAgeSelectorWidgetComposable.kt | 186 ++++++-- .../TextWithAgeSelectorWidgetV2Composable.kt | 81 ++++ .../reusable/CheckboxComposable.kt | 13 +- .../reusable/HeroSectionComposable.kt | 107 +++++ .../reusable/HorizontalTrackerComposable.kt | 63 +++ .../reusable/InputFieldComposable.kt | 8 +- ...bserveLocationAndFetchPinCodeComposable.kt | 93 ++++ .../reusable/TextViewComposable.kt | 56 +++ .../composables/reusable/WheelPicker.kt | 7 +- .../factory/ComposableWidgetFactory.kt | 40 ++ .../theme/PrePurchaseJourneyDimensions.kt | 3 + .../journey/ui/PreQuoteJourneyFragment.kt | 52 ++- .../journey/ui/PreQuoteJourneyViewModel.kt | 89 +++- .../purchase/journey/utils/LocationHandler.kt | 96 ++++ .../journey/utils/LocationPermissionUtils.kt | 57 +++ .../java/com/navi/insurance/util/Constants.kt | 5 + .../java/com/navi/insurance/util/Utility.kt | 34 ++ .../naviwidgets/WidgetDataDeserializer.kt | 13 + .../java/com/navi/naviwidgets/WidgetTypes.kt | 4 + .../composewidget/reusable/AppColors.kt | 2 + .../extensions/ComposeWidgetExt.kt | 3 +- .../models/HeaderWithTrackerWidgetData.kt | 1 + .../naviwidgets/models/HeroSectionData.kt | 18 + .../models/MemberSelectionWidgetData.kt | 76 ++++ .../models/NameDobTextWidgetData.kt | 44 ++ .../models/PincodeInputWidgetDataV2.kt | 48 ++ .../models/TextEditTextCalendarWidget.kt | 1 + .../models/TextWithAgeSelectorWidget.kt | 3 + .../models/TextWithAgeSelectorWidgetV2.kt | 40 ++ .../navi/naviwidgets/models/TrackerData.kt | 14 + 47 files changed, 2864 insertions(+), 75 deletions(-) create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithPermissionListBottomSheet.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithPermissionListBottomSheetData.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberCounterComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionGridComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionWidgetComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/NameDobTextWidgetComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/PincodeInputWidgetV2Composable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetV2Composable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HeroSectionComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HorizontalTrackerComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/ObserveLocationAndFetchPinCodeComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/TextViewComposable.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationHandler.kt create mode 100644 android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationPermissionUtils.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeroSectionData.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/models/MemberSelectionWidgetData.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/models/NameDobTextWidgetData.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/models/PincodeInputWidgetDataV2.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidgetV2.kt create mode 100644 android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TrackerData.kt diff --git a/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt b/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt index 5f04ea97bd..3a5177ab73 100644 --- a/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt +++ b/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt @@ -21,6 +21,7 @@ import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle import android.provider.MediaStore +import android.provider.Settings import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -32,6 +33,7 @@ import com.navi.ap.common.ui.ApplicationPlatformActivity import com.navi.ap.common.ui.StandardLauncherApActivity import com.navi.ap.utils.constants.CtaIdentifier import com.navi.ap.utils.constants.PL +import com.navi.base.AppServiceManager import com.navi.base.deeplink.listener.DeepLinkListener import com.navi.base.deeplink.util.DeeplinkConstants.LOGOUT import com.navi.base.deeplink.util.DeeplinkConstants.NPAY_INTENT_ACTIVITY @@ -72,6 +74,7 @@ import com.navi.common.utils.Constants.CLEAR_TOP import com.navi.common.utils.Constants.CTA_URL import com.navi.common.utils.Constants.FALSE import com.navi.common.utils.Constants.KEY_CTA_DATA +import com.navi.common.utils.Constants.PACKAGE import com.navi.common.utils.Constants.VERTICAL_TYPE import com.navi.common.utils.ShareUtil import com.navi.common.utils.log @@ -158,6 +161,7 @@ import timber.log.Timber object NaviDeepLinkNavigator : DeepLinkListener { private const val APP_SETTINGS = "APP_SETTINGS" + const val PHONE_SETTINGS = "PHONE_SETTINGS" const val FAQ = "faq" private const val CALL = "call" const val CHAT = "chat" @@ -311,6 +315,13 @@ object NaviDeepLinkNavigator : DeepLinkListener { APP_SETTINGS -> { intent = Intent(activity, AppSettingsActivity::class.java) } + PHONE_SETTINGS -> { + intent = + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + PACKAGE.plus(AppServiceManager.applicationId).toUri(), + ) + } MENU -> { when (secondIdentifier) { ABOUT_US -> intent = Intent(activity, AboutUsActivity::class.java) diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt b/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt index 630893f13c..f465c58aee 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt @@ -710,6 +710,24 @@ fun Map?.addKeyIfMissing(key: K, value: V): Map { } } +fun Map?.addKeyIfMissingConditionally( + key: K, + value: V, + condition: Boolean, +): Map { + // If condition is false, return the map as is + if (!condition) return this ?: mapOf() + + // Otherwise, add the key if missing + return when { + this == null -> mapOf(key to value) + !this.containsKey(key) -> { + this.toMutableMap().apply { put(key, value) } + } + else -> this + } +} + fun Map.filterLiteralNullValues(): Map { return this.filter { entry -> entry.value !in listOf(null, NULL_STRING, NULL_STRING_CAPS) } .mapValues { it.value.orEmpty() } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/analytics/InsuranceAnalyticsConstants.kt b/android/navi-insurance/src/main/java/com/navi/insurance/analytics/InsuranceAnalyticsConstants.kt index 2003c014e1..e241ddbee6 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/analytics/InsuranceAnalyticsConstants.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/analytics/InsuranceAnalyticsConstants.kt @@ -451,6 +451,7 @@ object InsuranceAnalyticsConstants { const val CHECKBOX_WITH_DROPDOWN_BOTTOMSHEET = "CHECKBOX_WITH_DROPDOWN_BOTTOMSHEET" const val UITRON_BOTTOM_SHEET = "UITRON_BOTTOM_SHEET" const val ICON_WITH_LIST_BOTTOM_SHEET = "ICON_WITH_LIST_BOTTOM_SHEET" + const val TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET = "TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET" const val TITLE_WITH_IMAGE_LIST_BOTTOM_SHEET = "TITLE_WITH_IMAGE_LIST_BOTTOM_SHEET" // Endorsement @@ -636,4 +637,11 @@ object InsuranceAnalyticsConstants { // OTP Captcha Screen const val OTP_CAPTCHA_SCREEN = "otp_captcha" + + // Pre Quote Journey Revamp + const val SELECTED_GENDER = "selected_gender" + const val MEMBER_SELECTED = "member_selected" + const val MEMBER_TYPE = "member_type" + const val MEMBER_COUNT = "member_count" + const val MEMBER_AGE = "member_age" } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithPermissionListBottomSheet.kt b/android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithPermissionListBottomSheet.kt new file mode 100644 index 0000000000..3f7f0e3504 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/common/bottom_sheet/TitleWithPermissionListBottomSheet.kt @@ -0,0 +1,255 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.common.bottom_sheet + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.view.ViewStub +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.databinding.DataBindingUtil +import com.google.gson.reflect.TypeToken +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.AppServiceManager +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.constants.VENDOR_NAVI_API +import com.navi.common.model.ModuleName +import com.navi.common.utils.CommonNaviAnalytics +import com.navi.common.utils.getNetworkType +import com.navi.insurance.R +import com.navi.insurance.analytics.InsuranceAnalyticsConstants +import com.navi.insurance.analytics.InsuranceAnalyticsHandler +import com.navi.insurance.common.fragment.BaseBottomSheet +import com.navi.insurance.common.models.GiErrorMetaData +import com.navi.insurance.common.models.TitleWithPermissionListBottomSheetData +import com.navi.insurance.databinding.LayoutClaimsStepsBottomSheetBinding +import com.navi.insurance.navigator.NaviInsuranceDeeplinkNavigator +import com.navi.insurance.util.CONTENT_DATA_JSON_STRING +import com.navi.insurance.util.Constants +import com.navi.insurance.util.logGiAppErrorEvent +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.debounceClickable +import com.navi.naviwidgets.extensions.getJsonObject +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class TitleWithPermissionListBottomSheet : BaseBottomSheet(), WidgetCallback { + @Inject lateinit var analyticsHandler: InsuranceAnalyticsHandler + private lateinit var binding: LayoutClaimsStepsBottomSheetBinding + private val errorTracker = CommonNaviAnalytics.naviAnalytics.GiError() + + override fun setContainerView(viewStub: ViewStub) { + viewStub.layoutResource = R.layout.layout_claims_steps_bottom_sheet + binding = DataBindingUtil.getBinding(viewStub.inflate())!! + initUI() + } + + private fun initUI() { + val dataType = object : TypeToken() {}.type + val bottomSheetData = + getJsonObject( + dataType, + (arguments?.getString(CONTENT_DATA_JSON_STRING)), + onErrorOccured = { exception -> trackError(exception) }, + ) + bottomSheetData?.let { data -> + NaviTrackEvent.sendEvent(data.metaData?.analyticsEventProperties, screenName) + binding.root.setContent { + setBottomSheetRadius(8f) + setPadding(0, 0, 0, 0) + TitleWithPermissionListBottomSheetComposable(data = data, widgetCallback = this) + } + } + } + + override fun onClick(naviClickAction: NaviClickAction, widgetId: String?) { + if (naviClickAction is CtaData) { + naviClickAction.analyticsEventProperties?.let { analyticsEvent -> + analyticsHandler.sendEvent(analyticsEvent, screenName) + } + val bundle = Bundle() + bundle.putParcelable(Constants.PARAMS_EXTRA, naviClickAction) + when (naviClickAction.type) { + CtaType.DISMISS_BOTTOM_SHEET.name -> { + safelyDismissDialog() + } + else -> { + NaviInsuranceDeeplinkNavigator.navigate( + activity = activity, + ctaData = naviClickAction, + bundle = bundle, + finish = naviClickAction.finish.orFalse(), + clearTask = naviClickAction.clearTask.orFalse(), + callbackHandler = requestToCallbackHAndler, + ) + safelyDismissDialog() + } + } + } + } + + @Composable + fun TitleWithPermissionListBottomSheetComposable( + modifier: Modifier = Modifier, + data: TitleWithPermissionListBottomSheetData? = null, + widgetCallback: WidgetCallback? = null, + ) { + Column(modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + Row( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + data?.headerTitle?.text?.let { + NaviTextWidgetized( + textFieldData = data.headerTitle, + widgetCallback = widgetCallback, + ) + } + } + data?.rightIcon?.url?.let { + NaviImage( + imageFieldData = data.rightIcon, + modifier = Modifier.size(24.dp), + widgetCallback = widgetCallback, + ) + } + } + + data?.permissionListData?.let { + PermissionListComposable(listData = it, widgetCallback = widgetCallback) + } + Spacer(modifier = Modifier.height(24.dp)) + data?.footerTitle?.text.let { + NaviTextWidgetized( + textFieldData = data?.footerTitle, + widgetCallback = widgetCallback, + ) + } + FooterButtonComposable( + modifier = + Modifier.debounceClickable( + onClick = { + val intent = + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse( + com.navi.common.utils.Constants.PACKAGE.plus( + AppServiceManager.applicationId + ) + ), + ) + startActivity(intent) + } + ) + .padding(vertical = 32.dp) + .wrapContentHeight(), + data = data?.footerButton, + widgetCallback = widgetCallback, + ) + } + } + + @Composable + fun PermissionListComposable( + modifier: Modifier = Modifier, + listData: List?, + widgetCallback: WidgetCallback? = null, + ) { + Column(modifier = modifier.fillMaxWidth().wrapContentHeight().padding(top = 16.dp)) { + listData?.forEachIndexed { index, itemData -> + PermissionItemComposable( + itemData = itemData, + widgetCallback = widgetCallback, + isLastItem = index == listData.size - 1, + ) + } + } + } + + @Composable + fun PermissionItemComposable( + itemData: TitleWithPermissionListBottomSheetData.PermissionItem?, + widgetCallback: WidgetCallback? = null, + isLastItem: Boolean = false, + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + NaviImage( + imageFieldData = itemData?.icon, + modifier = Modifier.padding(end = 8.dp).size(40.dp), + widgetCallback = widgetCallback, + ) + Column(modifier = Modifier.weight(1f)) { + NaviTextWidgetized( + textFieldData = itemData?.itemTitle, + modifier = Modifier, + widgetCallback = widgetCallback, + ) + itemData?.itemDescription?.text?.let { + NaviTextWidgetized( + textFieldData = itemData.itemDescription, + modifier = Modifier.padding(top = 4.dp), + widgetCallback = widgetCallback, + ) + } + } + } + if (!isLastItem) { + Spacer(modifier = Modifier.height(8.dp)) + } + } + + private fun trackError(error: Exception) { + errorTracker.onGlobalError( + error.stackTrace.getOrNull(0).toString(), + screenName, + ModuleName.GI.name, + CommonNaviAnalytics.GLOBAL_GENERIC_ERRORS, + null, + context?.let { getNetworkType(it) }, + GiErrorMetaData.FLOW_NEW_PRE_PURCHASE, + GiErrorMetaData.GET_JSON_OBJECT, + VENDOR_NAVI_API, + ) + logGiAppErrorEvent( + screen = screenName, + errorTitle = GiErrorMetaData.GET_JSON_OBJECT, + errorDes = error.stackTrace.getOrNull(0).toString(), + isSourceExternal = true, + vendor = VENDOR_NAVI_API, + ) + } + + override val screenName = InsuranceAnalyticsConstants.TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET + + companion object { + const val TAG = InsuranceAnalyticsConstants.TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithPermissionListBottomSheetData.kt b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithPermissionListBottomSheetData.kt new file mode 100644 index 0000000000..c6de239967 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/common/models/TitleWithPermissionListBottomSheetData.kt @@ -0,0 +1,28 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.common.models + +import com.navi.naviwidgets.models.FooterButtonData +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.PageMetaData +import com.navi.naviwidgets.models.response.TextFieldData + +data class TitleWithPermissionListBottomSheetData( + val headerTitle: TextFieldData? = null, + val rightIcon: ImageFieldData? = null, + val permissionListData: List? = null, + val footerTitle: TextFieldData? = null, + val footerButton: FooterButtonData? = null, + val metaData: PageMetaData? = null, +) { + data class PermissionItem( + val icon: ImageFieldData? = null, + val itemTitle: TextFieldData? = null, + val itemDescription: 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 9bc611a745..0ba3f7993f 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 @@ -417,6 +417,7 @@ class NavigationHandler @Inject constructor(private val uiControllerUtil: UiCont const val HI_OTP_SCREEN = "enter_otp_screen" const val KYC_STATUS = "gi/kyc_status" 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 POLICY_DETAILS_SCREEN = "policy_details_screen" const val POLICY_SELECTOR_BOTTOMSHEET = "policy_selector_bottomsheet" 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 2f114c8dea..8e69696987 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 @@ -62,6 +62,7 @@ import com.navi.insurance.common.bottom_sheet.TitleWithDescriptionAndContentList import com.navi.insurance.common.bottom_sheet.TitleWithFooterCardBottomSheet import com.navi.insurance.common.bottom_sheet.TitleWithGridBottomSheet import com.navi.insurance.common.bottom_sheet.TitleWithImageListBottomSheet +import com.navi.insurance.common.bottom_sheet.TitleWithPermissionListBottomSheet import com.navi.insurance.common.bottom_sheet.TrialInfoBottomSheet import com.navi.insurance.common.bottom_sheet.UITronBottomSheet import com.navi.insurance.common.fragment.AutoPaySetUpBottomSheet @@ -106,6 +107,7 @@ import com.navi.insurance.common.util.NavigationHandler.Companion.TITLE_WITH_DES import com.navi.insurance.common.util.NavigationHandler.Companion.TITLE_WITH_FOOTER_CARD_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.TITLE_WITH_GRID_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.TITLE_WITH_IMAGE_LIST_BOTTOM_SHEET +import com.navi.insurance.common.util.NavigationHandler.Companion.TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.TI_EXPLAINER_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.TRIAL_INFO_BOTTOM_SHEET import com.navi.insurance.common.util.NavigationHandler.Companion.UITRON_BOTTOM_SHEET @@ -769,6 +771,8 @@ object NaviInsuranceDeeplinkNavigator { UITRON_BOTTOM_SHEET -> Pair(UITronBottomSheet.TAG, UITronBottomSheet()) ICON_WITH_LIST_BOTTOM_SHEET -> Pair(IconWithListBottomSheet.TAG, IconWithListBottomSheet()) + TITLE_WITH_PERMISSION_LIST_BOTTOM_SHEET -> + Pair(TitleWithPermissionListBottomSheet.TAG, TitleWithPermissionListBottomSheet()) TITLE_WITH_IMAGE_LIST_BOTTOM_SHEET -> Pair(TitleWithImageListBottomSheet.TAG, TitleWithImageListBottomSheet()) POLICY_SELECTOR_BOTTOMSHEET -> diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyPageResponse.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyPageResponse.kt index 3d9462e746..e3ebcb1856 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyPageResponse.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyPageResponse.kt @@ -10,6 +10,7 @@ package com.navi.insurance.pre.purchase.journey import com.google.gson.annotations.SerializedName import com.navi.base.model.AnalyticsEvent import com.navi.base.model.CtaData +import com.navi.base.model.LineItem import com.navi.common.network.models.ErrorMessage import com.navi.insurance.common.models.AmountUpdateBottomSheetWidget import com.navi.insurance.models.response.PageLayoutParams @@ -39,6 +40,7 @@ data class PreQuoteMetaData( @SerializedName("transitionEvent") val transitionEvent: AnalyticsEvent? = null, @SerializedName("amountUpdatedBottomSheet") val amountUpdatedBottomSheet: AmountUpdateBottomSheetWidget? = null, + @SerializedName("pageProperties") val pageProperties: List? = null, ) data class PreQuoteJourneyState( diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyRespository.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyRespository.kt index 3aa55d3c1c..77b31d5772 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyRespository.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/PreQuoteJourneyRespository.kt @@ -11,6 +11,7 @@ import com.navi.common.checkmate.model.MetricInfo import com.navi.common.network.models.RepoResult import com.navi.common.network.retrofit.ResponseCallback import com.navi.insurance.network.retrofit.RetrofitService +import com.navi.insurance.purchase.compliance.domain.dto.PinCodeResponseData import javax.inject.Inject class PreQuoteJourneyRespository @Inject constructor(private val apiService: RetrofitService) : @@ -51,13 +52,6 @@ class PreQuoteJourneyRespository @Inject constructor(private val apiService: Ret ) } - suspend fun fetchFormPageData( - applicationType: String, - applicationId: String? = null, - ): RepoResult { - return giResponseCallback(apiService.fetchPreQuoteJourney(applicationType, applicationId)) - } - suspend fun makeNextPageFormRequest( formRequest: FormWidgetRequest, preQuoteId: String, @@ -69,4 +63,8 @@ class PreQuoteJourneyRespository @Inject constructor(private val apiService: Ret metricInfo, ) } + + suspend fun fetchAddressUsingPinCode(pinCode: String): RepoResult { + return giResponseCallback(apiService.fetchAddressUsingPinCode(pinCode = pinCode)) + } } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/HeaderWithTrackerWidgetComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/HeaderWithTrackerWidgetComposable.kt index 6027a0bda2..54d61d7e20 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/HeaderWithTrackerWidgetComposable.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/HeaderWithTrackerWidgetComposable.kt @@ -7,6 +7,7 @@ package com.navi.insurance.pre.purchase.journey.composables +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -18,10 +19,13 @@ import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import com.navi.common.extensions.or import com.navi.insurance.pre.purchase.journey.composables.reusable.HeaderTrackerComposable import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorWhiteHex import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.hexToColor import com.navi.naviwidgets.models.HeaderWithTrackerWidgetData @Composable @@ -31,7 +35,17 @@ fun HeaderWithTrackerWidgetComposable( handleVisibility: Boolean = true, ) { data.let { - Row(modifier = Modifier.fillMaxWidth().padding(top = LocalDimensions.current.dp26)) { + Row( + modifier = + Modifier.fillMaxWidth() + .background( + color = + hexToColor( + it.headerWithTrackerWidgetBody?.backgroundColor.or(colorWhiteHex) + ) + ) + .padding(top = LocalDimensions.current.dp26) + ) { Box( modifier = Modifier.padding( diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberCounterComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberCounterComposable.kt new file mode 100644 index 0000000000..26552ad6cf --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberCounterComposable.kt @@ -0,0 +1,130 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import com.navi.base.utils.orZero +import com.navi.design.font.naviFontFamily +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.insurance.util.Constants +import com.navi.naviwidgets.composewidget.reusable.colorBorderAlt +import com.navi.naviwidgets.composewidget.reusable.colorTextPrimary +import com.navi.naviwidgets.composewidget.reusable.text_secondary +import com.navi.naviwidgets.composewidget.reusable.whiteColor +import com.navi.naviwidgets.models.CounterDropDownData + +@Composable +fun MemberCounter(countData: CounterDropDownData?, onCountChanged: (CounterDropDownData) -> Unit) { + countData?.let { counterData -> + var counter by remember { + mutableIntStateOf(counterData.selectedCount.orZero().coerceAtLeast(1)) + } + + Row( + modifier = + Modifier.background( + color = whiteColor, + shape = + RoundedCornerShape( + topStart = LocalDimensions.current.dp4, + topEnd = LocalDimensions.current.dp4, + bottomStart = LocalDimensions.current.dp0, + bottomEnd = LocalDimensions.current.dp0, + ), + ), + verticalAlignment = Alignment.CenterVertically, + ) { + CounterButton( + text = Constants.MINUS_SIGN, + enabled = counter > counterData.minCount.orZero(), + onClick = { + val newCount = + (counter - 1).coerceIn( + counterData.minCount.orZero(), + counterData.maxCount.orZero(), + ) + counter = newCount + onCountChanged(counterData.copy(selectedCount = counter)) + }, + ) + + Box( + modifier = Modifier.width(LocalDimensions.current.dp40).wrapContentHeight(), + contentAlignment = Alignment.Center, + ) { + Text( + text = counter.toString(), + color = colorTextPrimary, + fontSize = 12.sp, + lineHeight = 16.sp, + textAlign = TextAlign.Center, + fontFamily = naviFontFamily, + fontWeight = FontWeight.Medium, + style = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = true)), + ) + } + + CounterButton( + text = Constants.PLUS_SIGN, + enabled = counter < counterData.maxCount.orZero(), + onClick = { + counter = + (counter + 1).coerceIn( + counterData.minCount.orZero(), + counterData.maxCount.orZero(), + ) + onCountChanged(counterData.copy(selectedCount = counter)) + }, + ) + } + } +} + +@Composable +private fun CounterButton(text: String, enabled: Boolean, onClick: () -> Unit) { + Box( + modifier = + Modifier.padding(LocalDimensions.current.dp4) + .size(width = LocalDimensions.current.dp24, height = LocalDimensions.current.dp20) + .background( + color = colorBorderAlt.copy(alpha = 0.2f), + shape = RoundedCornerShape(LocalDimensions.current.dp4), + ) + .clickable(enabled = enabled) { onClick() }, + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + fontSize = 12.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.Bold, + color = if (enabled) colorTextPrimary else text_secondary, + ) + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionGridComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionGridComposable.kt new file mode 100644 index 0000000000..fdc3b75998 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionGridComposable.kt @@ -0,0 +1,210 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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 com.navi.insurance.pre.purchase.journey.composables.reusable.CustomCheckBox +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorCTAPrimary +import com.navi.naviwidgets.composewidget.reusable.colorCTAPrimaryHex +import com.navi.naviwidgets.composewidget.reusable.colorWhiteHex +import com.navi.naviwidgets.composewidget.reusable.darkShadowColorHex +import com.navi.naviwidgets.composewidget.reusable.midBlue +import com.navi.naviwidgets.composewidget.reusable.whiteColor +import com.navi.naviwidgets.extensions.NaviCard +import com.navi.naviwidgets.extensions.NaviGrid +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.debounceClickable +import com.navi.naviwidgets.models.DropdownType +import com.navi.naviwidgets.models.MemberSelectionWidgetBody +import com.navi.naviwidgets.models.response.CardProperties + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun MemberSelectionGrid( + modifier: Modifier = Modifier, + items: List, + widgetCallback: WidgetCallback?, + onCountChanged: (Int, MemberSelectionWidgetBody.MemberSelectionItemData) -> Unit = { _, _ -> }, + onItemSelected: (Int, MemberSelectionWidgetBody.MemberSelectionItemData) -> Unit = { _, _ -> }, +) { + NaviGrid(itemCount = items.size, spanCount = 2, modifier = modifier.fillMaxWidth()) { index -> + MemberGridItem( + itemData = items[index], + modifier = + Modifier.padding( + vertical = LocalDimensions.current.dp4, + horizontal = LocalDimensions.current.dp4, + ), + widgetCallback = widgetCallback, + onCountChanged = { updatedItem -> onCountChanged(index, updatedItem) }, + onItemSelected = { updatedItem -> onItemSelected(index, updatedItem) }, + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun MemberGridItem( + itemData: MemberSelectionWidgetBody.MemberSelectionItemData, + modifier: Modifier = Modifier, + widgetCallback: WidgetCallback?, + onCountChanged: (MemberSelectionWidgetBody.MemberSelectionItemData) -> Unit = {}, + onItemSelected: (MemberSelectionWidgetBody.MemberSelectionItemData) -> Unit = {}, +) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + NaviCard( + cardProperties = + CardProperties( + backgroundColor = colorWhiteHex, + elevation = 32, + borderRadius = 4, + spotShadowColor = darkShadowColorHex, + strokeColor = if (itemData.isSelected) colorCTAPrimaryHex else null, + strokeWidth = if (itemData.isSelected) 1 else 0, + ), + modifier = + Modifier.debounceClickable( + onClick = { onItemSelected(itemData.copy(isSelected = !itemData.isSelected)) } + ), + ) { + Box { + Column(modifier = Modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = + Modifier.fillMaxWidth() + .padding( + start = LocalDimensions.current.dp6, + end = LocalDimensions.current.dp6, + top = LocalDimensions.current.dp6, + ), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier.fillMaxWidth() + .background( + color = midBlue, + shape = RoundedCornerShape(LocalDimensions.current.dp4), + ), + contentAlignment = Alignment.Center, + ) { + AnimatedContent( + targetState = itemData.image, + transitionSpec = { + fadeIn(animationSpec = tween(300)) togetherWith + fadeOut(animationSpec = tween(300)) + }, + ) { targetImage -> + targetImage?.let { image -> + NaviImage( + imageFieldData = image, + modifier = Modifier.size(LocalDimensions.current.dp80), + widgetCallback = widgetCallback, + ) + } + } + } + + if ( + itemData.dropdownType == DropdownType.COUNTER && + itemData.countData != null + ) { + Box( + modifier = Modifier.align(Alignment.BottomCenter), + contentAlignment = Alignment.Center, + ) { + androidx.compose.animation.AnimatedVisibility( + visible = itemData.isSelected, + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + ) { + MemberCounter( + countData = itemData.countData, + onCountChanged = { updatedCountData -> + if (updatedCountData.selectedCount == 0) { + onItemSelected(itemData.copy(isSelected = false)) + } else { + onCountChanged( + itemData.copy(countData = updatedCountData) + ) + } + }, + ) + } + } + } + } + + Box( + modifier = + Modifier.background(whiteColor) + .fillMaxWidth() + .padding( + horizontal = LocalDimensions.current.dp8, + vertical = LocalDimensions.current.dp6, + ), + contentAlignment = Alignment.Center, + ) { + AnimatedContent( + targetState = itemData.title, + transitionSpec = { + fadeIn(animationSpec = tween(300)) togetherWith + fadeOut(animationSpec = tween(300)) + }, + ) { targetTitle -> + NaviTextWidgetized( + textFieldData = targetTitle, + widgetCallback = widgetCallback, + ) + } + } + } + Box( + modifier = Modifier.align(Alignment.TopEnd), + contentAlignment = Alignment.Center, + ) { + if (itemData.isSelected) { + CustomCheckBox( + shape = + RoundedCornerShape( + topEnd = LocalDimensions.current.dp4, + bottomStart = LocalDimensions.current.dp4, + bottomEnd = LocalDimensions.current.dp0, + topStart = LocalDimensions.current.dp0, + ), + checked = true, + onCheckedChange = { onItemSelected(itemData.copy(isSelected = it)) }, + selectedColor = colorCTAPrimary, + modifier = Modifier.size(LocalDimensions.current.dp24), + ) + } + } + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionWidgetComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionWidgetComposable.kt new file mode 100644 index 0000000000..1a7af1c817 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MemberSelectionWidgetComposable.kt @@ -0,0 +1,424 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.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.neverEqualPolicy +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.common.utils.addKeyIfMissing +import com.navi.common.utils.addKeyIfMissingConditionally +import com.navi.design.decorator.DashedDivider +import com.navi.elex.font.FontWeightEnum +import com.navi.insurance.analytics.InsuranceAnalyticsConstants.MEMBER_COUNT +import com.navi.insurance.analytics.InsuranceAnalyticsConstants.MEMBER_SELECTED +import com.navi.insurance.analytics.InsuranceAnalyticsConstants.MEMBER_TYPE +import com.navi.insurance.analytics.InsuranceAnalyticsConstants.SELECTED_GENDER +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.CustomRadioButton +import com.navi.insurance.pre.purchase.journey.composables.reusable.HeroSectionWithCardComposable +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.colorCTAPrimary +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.debounceClickable +import com.navi.naviwidgets.extensions.setWidgetLayoutParams +import com.navi.naviwidgets.models.DropdownType +import com.navi.naviwidgets.models.GenericWidgetDataInfo +import com.navi.naviwidgets.models.MemberSelectionWidgetBody +import com.navi.naviwidgets.models.MemberSelectionWidgetData +import com.navi.naviwidgets.models.SelectionType + +@Composable +fun MemberSelectionWidgetComposable( + data: MemberSelectionWidgetData, + isValidWidget: (Boolean, GenericWidgetDataInfo) -> Unit, + updatePatchCallData: (PreQuotePatchData) -> Unit = {}, + widgetCallback: WidgetCallback?, +) { + val widgetData by remember(key1 = data.toString(), calculation = { mutableStateOf(data) }) + + val checkWidgetValidity = { items: List -> + val isValid = items.any { it.isSelected } + isValidWidget(isValid, data) + } + var memberItems by + remember( + key1 = data.toString(), + calculation = { + mutableStateOf(widgetData.widgetData?.itemList ?: emptyList(), neverEqualPolicy()) + }, + ) + var genderItems by + remember( + key1 = data.toString(), + calculation = { + mutableStateOf( + widgetData.widgetData?.genderData?.items ?: emptyList(), + neverEqualPolicy(), + ) + }, + ) + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + setWidgetLayoutParams(widgetLayoutParams = widgetData.widgetLayoutParams) { + widgetData.widgetData?.let { data -> + Column(modifier = Modifier.fillMaxWidth()) { + HeroSectionWithCardComposable( + headerData = data.headerData, + trackerData = data.trackerData, + cardProperties = data.cardProperties, + ) { + Column(modifier = Modifier.fillMaxWidth()) { + LaunchedEffect(key1 = Unit) { checkWidgetValidity(memberItems) } + + data.genderData?.let { genderData -> + GenderSelectionComposable( + modifier = + Modifier.padding(horizontal = LocalDimensions.current.dp16), + genderData = genderData, + widgetCallback = widgetCallback, + onGenderChanged = { selectedGenderIndex -> + if ( + genderItems.isNotEmpty() && + selectedGenderIndex < genderItems.size + ) { + genderItems = + genderItems + .mapIndexed { index, item -> + item.copy( + isSelected = + index == selectedGenderIndex + ) + } + .toMutableList() + + // Update member items with swapped titles/images based + // on gender + val updatedItems = + updateMemberItemsForGenderChange(memberItems) + memberItems = updatedItems + + checkWidgetValidity(memberItems) + + val selectedGender = genderItems[selectedGenderIndex].id + updatePatchCallData( + formatMemberSelectionRequestData( + updatedItems, + selectedGender ?: "MALE", + ) + ) + genderData.analyticsEventProperties?.name?.let { + eventName -> + NaviTrackEvent.trackEvent( + eventName = eventName, + eventValues = + data.genderData + ?.analyticsEventProperties + ?.properties + .addKeyIfMissing( + SELECTED_GENDER, + selectedGender ?: "MALE", + ), + ) + } + } + }, + ) + } + + DashedDivider( + thickness = LocalDimensions.current.dp1, + color = colorBorderAlt, + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = LocalDimensions.current.dp16), + ) + + NaviTextWidgetized( + textFieldData = data.title, + widgetCallback = widgetCallback, + modifier = + Modifier.padding( + start = LocalDimensions.current.dp16, + end = LocalDimensions.current.dp16, + top = LocalDimensions.current.dp24, + bottom = LocalDimensions.current.dp12, + ), + ) + + if (memberItems.isNotEmpty()) { + MemberSelectionGrid( + modifier = + Modifier.padding(horizontal = LocalDimensions.current.dp12), + items = memberItems, + widgetCallback = widgetCallback, + onCountChanged = { index, updatedItem -> + val updatedItems = memberItems.toMutableList() + updatedItems[index] = updatedItem + memberItems = updatedItems + + checkWidgetValidity(updatedItems) + + val selectedGender = + genderItems.find { it.isSelected }?.id ?: "MALE" + updatePatchCallData( + formatMemberSelectionRequestData( + updatedItems, + selectedGender, + ) + ) + }, + onItemSelected = { index, updatedItem -> + // Initialize counter to 1 if this is a counter item and + // it's being selected + val itemToUpdate = + updatedItem.let { item -> + if ( + item.isSelected && + item.dropdownType == DropdownType.COUNTER && + (item.countData?.selectedCount ?: 0) <= 0 + ) { + + item.copy( + countData = + item.countData?.copy(selectedCount = 1) + ) + } else { + item + } + } + + val updatedItems = + when (data.selectionType) { + SelectionType.RADIO -> { + memberItems.mapIndexed { i, item -> + if (i == index) { + itemToUpdate + } else { + item.copy(isSelected = false) + } + } + } + else -> { + val list = memberItems.toMutableList() + list[index] = itemToUpdate + list + } + } + + memberItems = updatedItems + checkWidgetValidity(updatedItems) + + // Update patch data with the selection + val selectedGender = + genderItems.find { it.isSelected }?.id ?: "MALE" + updatePatchCallData( + formatMemberSelectionRequestData( + updatedItems, + selectedGender, + ) + ) + updatedItem.analyticsEventProperties?.name?.let { eventName + -> + NaviTrackEvent.trackEvent( + eventName = eventName, + eventValues = + updatedItem.analyticsEventProperties + ?.properties + .addKeyIfMissing( + MEMBER_SELECTED, + updatedItem.isSelected.toString(), + ) + .addKeyIfMissing( + MEMBER_TYPE, + updatedItem.title?.text.orEmpty(), + ) + .addKeyIfMissingConditionally( + MEMBER_COUNT, + updatedItem.countData + ?.selectedCount + .toString(), + updatedItem.dropdownType == + DropdownType.COUNTER, + ) + .addKeyIfMissingConditionally( + SELECTED_GENDER, + selectedGender, + updatedItem.id == "SELF", + ), + ) + } + }, + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.dp16)) + } + } + } + + Spacer(modifier = Modifier.height(LocalDimensions.current.dp186)) + } + } + } + } + + // Initial patch data update + val selectedGender = genderItems.find { it.isSelected }?.id ?: "MALE" + updatePatchCallData(formatMemberSelectionRequestData(memberItems, selectedGender)) +} + +@Composable +private fun GenderSelectionComposable( + modifier: Modifier = Modifier, + genderData: MemberSelectionWidgetBody.GenderSelectionData, + widgetCallback: WidgetCallback?, + onGenderChanged: (Int) -> Unit = {}, +) { + val initialSelectedIndex = genderData.items?.indexOfFirst { it.isSelected } ?: -1 + var selectedIndex by remember { + mutableStateOf(if (initialSelectedIndex >= 0) initialSelectedIndex else null) + } + + Row(modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + genderData.title?.let { title -> + NaviTextWidgetized( + textFieldData = title, + widgetCallback = widgetCallback, + modifier = Modifier.padding(vertical = LocalDimensions.current.dp24), + ) + } + Spacer(modifier = Modifier.weight(1f)) + genderData.items?.forEachIndexed { index, item -> + if (index > 0) { + Spacer(modifier = Modifier.width(LocalDimensions.current.dp24)) + } + + Row( + modifier = + Modifier.debounceClickable( + onClick = { + if (selectedIndex != index) { + selectedIndex = index + onGenderChanged(index) + } + } + ) + .padding(vertical = LocalDimensions.current.dp24), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + CustomRadioButton( + modifier = Modifier.padding(end = LocalDimensions.current.dp4), + selected = selectedIndex == index, + onClick = { + if (selectedIndex != index) { + selectedIndex = index + onGenderChanged(index) + } + }, + deselectedColor = colorBorderAlt, + selectedColor = colorCTAPrimary, + ) + + val fontStyle = + if (selectedIndex == index) { + FontWeightEnum.NAVI_BODY_DEMI_BOLD.name + } else { + FontWeightEnum.NAVI_BODY_REGULAR.name + } + val updatedTitle = item.title?.copy(font = fontStyle) + + NaviTextWidgetized(textFieldData = updatedTitle, widgetCallback = widgetCallback) + } + } + } +} + +private fun formatMemberSelectionRequestData( + items: List, + selectedGender: String, +): PreQuotePatchData { + val dataList = mutableListOf() + + items.forEach { item -> + if (item.isSelected) { + val dropDownId = + when (item.id) { + "SELF" -> selectedGender + "SPOUSE" -> if (selectedGender == "MALE") "FEMALE" else "MALE" + else -> item.id + } + + val pageData = + PreQuotePageData( + assetId = item.assetId, + questionId = "", + optionId = getMappedOptionId(item.id), + dropDownId = dropDownId, + count = item.countData?.selectedCount, + ) + dataList.add(pageData) + } + } + + return PreQuotePatchData(data = listOf(WidgetKey(type = "List", value = dataList))) +} + +private fun getMappedOptionId(id: String?): String { + return when (id) { + "SELF" -> "SELF" + "SPOUSE" -> "SPOUSE" + "SON", + "DAUGHTER" -> "CHILD" + "MOTHER", + "FATHER" -> "PARENT" + "MOTHER_IN_LAW", + "FATHER_IN_LAW" -> "PARENT_IN_LAW" + else -> "" + } +} + +private fun updateMemberItemsForGenderChange( + items: List +): List { + return items.map { item -> + if ( + item.dropdownType == DropdownType.GENDER && + item.complementaryTitle != null && + item.complementaryImage != null + ) { + item.copy( + title = item.complementaryTitle, + image = item.complementaryImage, + complementaryTitle = item.title, + complementaryImage = item.image, + ) + } else { + item + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MultiTypeSelectionWidgetComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MultiTypeSelectionWidgetComposable.kt index 7a22e34240..7a6f6909c9 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MultiTypeSelectionWidgetComposable.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/MultiTypeSelectionWidgetComposable.kt @@ -173,6 +173,11 @@ fun CheckboxList( } Type.RADIO.name -> { CustomRadioButton( + modifier = + Modifier.padding( + horizontal = LocalDimensions.current.dp16, + vertical = LocalDimensions.current.dp13, + ), selected = item.isChecked.orFalse(), onClick = { if (!item.isChecked) { diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/NameDobTextWidgetComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/NameDobTextWidgetComposable.kt new file mode 100644 index 0000000000..ee63d87135 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/NameDobTextWidgetComposable.kt @@ -0,0 +1,124 @@ +/* + * + * * 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.Box +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.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +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.HeroSectionWithCardComposable +import com.navi.insurance.pre.purchase.journey.composables.reusable.TextViewComposable +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.insurance.util.Constants +import com.navi.insurance.util.formatDateToDDMMYYYY +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorCTASecondary +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.setWidgetLayoutParams +import com.navi.naviwidgets.models.GenericWidgetDataInfo +import com.navi.naviwidgets.models.NameDobTextBody +import com.navi.naviwidgets.models.NameDobTextWidgetData +import com.navi.uitron.utils.isNotNullAndNotEmpty + +@Composable +fun NameDobTextWidgetComposable( + data: NameDobTextWidgetData, + isValidWidget: (Boolean, GenericWidgetDataInfo) -> Unit, + updatePatchCallData: (PreQuotePatchData) -> Unit = {}, + widgetCallback: WidgetCallback?, +) { + + val widgetData by remember(key1 = data.toString(), calculation = { mutableStateOf(data) }) + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + setWidgetLayoutParams(widgetLayoutParams = widgetData.widgetLayoutParams) { + widgetData.widgetData?.let { data -> + Box(modifier = Modifier.fillMaxWidth()) { + HeroSectionWithCardComposable( + headerData = data.headerData, + trackerData = data.trackerData, + cardProperties = data.cardProperties, + ) { + Column( + modifier = + Modifier.fillMaxWidth() + .padding( + vertical = LocalDimensions.current.dp16, + horizontal = LocalDimensions.current.dp16, + ) + ) { + NaviTextWidgetized( + textFieldData = data.title, + widgetCallback = widgetCallback, + ) + + data.itemList?.forEach { item -> + NaviTextWidgetized( + widgetCallback = widgetCallback, + textFieldData = item.title, + modifier = + Modifier.padding( + top = LocalDimensions.current.dp32, + bottom = LocalDimensions.current.dp8, + ), + ) + TextViewComposable( + textFieldData = item.inputData, + cta = item.cta, + widgetCallback = widgetCallback, + backgroundColor = colorCTASecondary, + ) + } + Spacer(modifier = Modifier.height(LocalDimensions.current.dp24)) + } + } + } + updatePatchCallData(formatNameDobRequestData(itemList = data.itemList)) + } + } + Spacer(modifier = Modifier.height(LocalDimensions.current.dp186)) + } + + isValidWidget(true, widgetData) +} + +private fun formatNameDobRequestData( + itemList: List? +): PreQuotePatchData { + val dataList = mutableListOf() + itemList?.forEach { item -> + if (item.inputData?.text.isNotNullAndNotEmpty()) { + val value = + when { + item.id.equals(Constants.DOB_ID, ignoreCase = true) -> { + // Format "31 May 1995" to "31051995" + formatDateToDDMMYYYY(item.inputData?.text.orEmpty()) + } + else -> { + item.inputData?.text + } + } + + dataList.add( + PreQuotePageData(assetId = item.assetId, optionId = item.id, dropDownId = value) + ) + } + } + return PreQuotePatchData(data = listOf(WidgetKey(type = "List", value = dataList))) +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/OutlinedCheckWithDropDown.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/OutlinedCheckWithDropDown.kt index 464a0d422b..51521f49c4 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/OutlinedCheckWithDropDown.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/OutlinedCheckWithDropDown.kt @@ -261,7 +261,11 @@ fun OutlinedSelectionItem( } Type.RADIO.name -> { CustomRadioButton( - modifier = Modifier, + modifier = + Modifier.padding( + horizontal = LocalDimensions.current.dp16, + vertical = LocalDimensions.current.dp13, + ), selected = data.isChecked.orFalse(), onClick = { if (!data.isChecked) { diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/PincodeInputWidgetV2Composable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/PincodeInputWidgetV2Composable.kt new file mode 100644 index 0000000000..24764c0705 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/PincodeInputWidgetV2Composable.kt @@ -0,0 +1,428 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables + +import android.Manifest +import android.app.Activity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.lifecycle.ViewModel +import com.navi.common.ResponseState +import com.navi.insurance.location.NaviLocationManager +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.HeroSectionWithCardComposable +import com.navi.insurance.pre.purchase.journey.composables.reusable.ObserveLocationAndFetchPinCodeComposable +import com.navi.insurance.pre.purchase.journey.composables.reusable.TextFieldComposable +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.insurance.pre.purchase.journey.ui.PreQuoteJourneyViewModel +import com.navi.insurance.pre.purchase.journey.utils.LocationHandler +import com.navi.insurance.pre.purchase.journey.utils.LocationPermissionUtils +import com.navi.insurance.purchase.compliance.domain.dto.PinCodeResponseData +import com.navi.insurance.util.isValidIndianPincode +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorOffWhite +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.debounceClickable +import com.navi.naviwidgets.extensions.setWidgetLayoutParams +import com.navi.naviwidgets.models.EditTextData +import com.navi.naviwidgets.models.GenericWidgetDataInfo +import com.navi.naviwidgets.models.PincodeInputWidgetBodyV2 +import com.navi.naviwidgets.models.PincodeInputWidgetDataV2 +import com.navi.naviwidgets.models.response.TextFieldData + +@Composable +fun PincodeInputWidgetV2Composable( + data: PincodeInputWidgetDataV2, + naviLocationManager: NaviLocationManager? = null, + commonNaviLocationManager: com.navi.common.managers.NaviLocationManager? = null, + isValidWidget: (Boolean, GenericWidgetDataInfo) -> Unit, + updatePatchCallData: (PreQuotePatchData) -> Unit = {}, + widgetCallback: WidgetCallback?, + activity: Activity? = null, + viewModel: ViewModel? = null, +) { + val data by remember(key1 = data.toString(), calculation = { mutableStateOf(data) }) + var pincode by remember { mutableStateOf(data.widgetData?.editTextData?.title?.text.orEmpty()) } + val focusManager = LocalFocusManager.current + val context = LocalContext.current + + // Create a dedicated location handler for "Locate Me" button + val locationHandler = remember { + LocationHandler( + context = context, + activity = activity, + naviLocationManager = naviLocationManager, + commonNaviLocationManager = commonNaviLocationManager, + viewModel = viewModel as? PreQuoteJourneyViewModel, + ) + } + + // Set up the pincode callback for the dedicated handler + locationHandler.pincodeCallback = { newPincode -> + pincode = newPincode + (viewModel as? PreQuoteJourneyViewModel)?.fetchAddressUsingPinCode(newPincode) + } + + // Set up the permission denied callback + locationHandler.onPermissionDenied = { + data.widgetData?.settingsCta?.let { cta -> widgetCallback?.onClick(cta) } + } + + // Permission launcher specifically for "Locate Me" button + val locatePermissionLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val fineLocationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false + val coarseLocationGranted = + permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false + val isPermissionGranted = fineLocationGranted || coarseLocationGranted + + // Handle permission result + locationHandler.handlePermissionResult(isPermissionGranted) + } + + val preQuoteViewModel = viewModel as? PreQuoteJourneyViewModel + var currentValidationState by remember { + mutableStateOf>(ResponseState.Idle) + } + + LaunchedEffect(preQuoteViewModel) { + preQuoteViewModel?.fieldValidationEvent?.collect { event -> currentValidationState = event } + } + + LaunchedEffect(Unit) { + if (pincode.isValidIndianPincode()) { + preQuoteViewModel?.fetchAddressUsingPinCode(pincode) + } + } + + val bottomContent = + remember(currentValidationState) { + when (currentValidationState) { + is ResponseState.Success -> { + val success = currentValidationState as ResponseState.Success + data.widgetData + ?.bottomContent + ?.copy(text = "${success.value.city}, ${success.value.stateName}") + } + is ResponseState.Idle -> { + data.widgetData?.bottomContent?.copy(text = "") + } + else -> null + } + } + + val errorBottomContent = + remember(currentValidationState) { + when (currentValidationState) { + is ResponseState.Failure -> { + data.widgetData + ?.errorBottomContent + ?.copy( + text = (currentValidationState as ResponseState.Failure).errorMessage + ) + } + is ResponseState.Idle -> { + data.widgetData?.errorBottomContent?.copy(text = "") + } + else -> null + } + } + + LaunchedEffect(currentValidationState) { + val isValid = currentValidationState is ResponseState.Success + updateAndValidatePincode( + pincode = pincode, + data = data, + isValidWidget = isValidWidget, + isValid = isValid, + ) + } + + ObserveLocationAndFetchPinCodeComposable( + viewModel = preQuoteViewModel, + commonNaviLocationManager = commonNaviLocationManager, + naviLocationManager = naviLocationManager, + activity = activity, + getPincode = { pincode }, + setPincode = { newPin -> + pincode = newPin + if (newPin.length == 6) { + preQuoteViewModel?.fetchAddressUsingPinCode(newPin) + } + }, + inputSize = 6, + onUpdateData = { newPin -> + pincode = newPin + data.copy( + widgetData = + data.widgetData?.copy( + editTextData = + data.widgetData + ?.editTextData + ?.copy( + title = + data.widgetData?.editTextData?.title?.copy(text = pincode) + ) + ) + ) + }, + isValidWidget = { isValid, updatedData -> + updatedData.widgetLayoutParams = data.widgetLayoutParams + isValidWidget(isValid, updatedData as PincodeInputWidgetDataV2) + }, + ) + + setWidgetLayoutParams(widgetLayoutParams = data.widgetLayoutParams) { + data.widgetData?.let { widgetData -> + Box(modifier = Modifier.fillMaxWidth()) { + HeroSectionWithCardComposable( + headerData = widgetData.headerData, + trackerData = widgetData.trackerData, + cardProperties = widgetData.cardProperties, + ) { + Column( + modifier = + Modifier.fillMaxWidth() + .padding( + top = LocalDimensions.current.dp16, + bottom = LocalDimensions.current.dp24, + ) + ) { + TitleSection( + title = widgetData.title, + subtitle = widgetData.subtitle, + widgetCallback = widgetCallback, + ) + + PincodeInputField( + data = widgetData.editTextData, + pincode = pincode, + bottomContent = errorBottomContent, + formattedBottomContent = bottomContent, + onPincodeChange = { value -> + pincode = value + if (value.length < 6) { + preQuoteViewModel?.resetFieldValidationState() + } else { + focusManager.clearFocus() + preQuoteViewModel?.fetchAddressUsingPinCode(value) + } + updateAndValidatePincode( + pincode = pincode, + data = data, + isValidWidget = isValidWidget, + ) + }, + onLocateMeClick = { + val isPermissionGranted = + LocationPermissionUtils.isLocationPermissionGranted(context) + + if (isPermissionGranted) { + // Permission already granted, fetch location directly + locationHandler.fetchLocation() + } else { + // Request permission using our dedicated launcher + // Mark this as a user-triggered request + locationHandler.prepareForPermissionRequest(true) + locatePermissionLauncher.launch( + LocationPermissionUtils.getLocationPermissions() + ) + } + }, + onClearClick = { + pincode = "" + preQuoteViewModel?.resetFieldValidationState() + updateAndValidatePincode( + pincode = pincode, + data = data, + isValidWidget = isValidWidget, + ) + }, + widgetCallback = widgetCallback, + ) + + CalloutSection( + calloutData = widgetData.calloutData, + widgetCallback = widgetCallback, + ) + } + } + } + } + } + + updatePatchCallData(formatPincodeRequestData(pincode)) +} + +@Composable +private fun TitleSection( + title: TextFieldData?, + subtitle: TextFieldData?, + widgetCallback: WidgetCallback?, +) { + NaviTextWidgetized( + textFieldData = title, + widgetCallback = widgetCallback, + modifier = + Modifier.padding( + start = LocalDimensions.current.dp16, + end = LocalDimensions.current.dp16, + ), + ) + + NaviTextWidgetized( + textFieldData = subtitle, + widgetCallback = widgetCallback, + modifier = + Modifier.padding( + top = LocalDimensions.current.dp4, + start = LocalDimensions.current.dp16, + end = LocalDimensions.current.dp16, + bottom = LocalDimensions.current.dp24, + ), + ) +} + +@Composable +private fun PincodeInputField( + data: EditTextData?, + pincode: String, + bottomContent: TextFieldData?, + formattedBottomContent: TextFieldData?, + onPincodeChange: (String) -> Unit, + onLocateMeClick: () -> Unit, + onClearClick: () -> Unit, + widgetCallback: WidgetCallback?, +) { + TextFieldComposable( + data = data, + maxLength = 6, + bottomContent = bottomContent, + formattedBottomContent = formattedBottomContent, + trailingContent = { + if (pincode.isBlank()) { + NaviTextWidgetized( + textFieldData = data?.trailingContent, + widgetCallback = widgetCallback, + modifier = + Modifier.debounceClickable(onClick = onLocateMeClick) + .padding(end = LocalDimensions.current.dp12), + ) + } else { + Icon( + painter = painterResource(com.navi.naviwidgets.R.drawable.ic_cross_black), + contentDescription = "Clear", + modifier = + Modifier.size(LocalDimensions.current.dp16) + .debounceClickable(onClick = onClearClick), + ) + } + }, + keyboardType = KeyboardType.NumberPassword, + onValueChange = onPincodeChange, + widgetCallback = widgetCallback, + ) +} + +@Composable +private fun CalloutSection( + calloutData: PincodeInputWidgetBodyV2.CalloutData?, + widgetCallback: WidgetCallback?, +) { + calloutData?.let { callout -> + Row( + modifier = + Modifier.fillMaxWidth() + .padding( + start = LocalDimensions.current.dp16, + end = LocalDimensions.current.dp16, + ) + .background( + color = colorOffWhite, + shape = RoundedCornerShape(LocalDimensions.current.dp4), + ) + .padding( + top = LocalDimensions.current.dp12, + bottom = LocalDimensions.current.dp12, + start = LocalDimensions.current.dp16, + end = LocalDimensions.current.dp16, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + callout.icon?.let { icon -> + NaviImage(imageFieldData = icon, widgetCallback = widgetCallback) + } + + Column(modifier = Modifier.weight(1f)) { + callout.title?.let { + NaviTextWidgetized(textFieldData = it, widgetCallback = widgetCallback) + } + callout.description?.let { + NaviTextWidgetized( + textFieldData = it, + widgetCallback = widgetCallback, + modifier = Modifier.padding(top = LocalDimensions.current.dp2), + ) + } + } + } + } +} + +private fun updateAndValidatePincode( + pincode: String, + data: PincodeInputWidgetDataV2? = null, + isValidWidget: (Boolean, GenericWidgetDataInfo) -> Unit, + isValid: Boolean = pincode.isValidIndianPincode(), +) { + val updatedData = + data?.copy( + widgetData = + data.widgetData?.copy( + editTextData = + data.widgetData + ?.editTextData + ?.copy( + title = data.widgetData?.editTextData?.title?.copy(text = pincode) + ) + ) + ) + updatedData?.widgetLayoutParams = data?.widgetLayoutParams + isValidWidget(isValid, updatedData as GenericWidgetDataInfo) +} + +private fun formatPincodeRequestData(pincode: String): PreQuotePatchData { + return PreQuotePatchData( + data = listOf(WidgetKey(type = "String", value = pincode, widgetKey = "SELF")) + ) +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetComposable.kt index 5e869e758f..4bbf643ee6 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetComposable.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetComposable.kt @@ -34,22 +34,32 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.utils.isNotNull import com.navi.base.utils.orFalse +import com.navi.common.utils.addKeyIfMissing +import com.navi.elex.font.FontWeightEnum +import com.navi.insurance.analytics.InsuranceAnalyticsConstants.MEMBER_AGE +import com.navi.insurance.analytics.InsuranceAnalyticsConstants.MEMBER_TYPE 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.WheelPicker import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.insurance.util.Constants.AGE_DISPLAY_TEXT_REGEX import com.navi.insurance.util.Constants.ANIMATION_DELAY import com.navi.naviwidgets.R as NaviWidgetsR import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorBorderAlt import com.navi.naviwidgets.composewidget.reusable.colorCTASecondary +import com.navi.naviwidgets.extensions.NaviImage import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.models.AgeSelectorStyle import com.navi.naviwidgets.models.GenericWidgetDataInfo import com.navi.naviwidgets.models.SelectionItemData import com.navi.naviwidgets.models.TextWithAgeSelectorWidget +import com.navi.naviwidgets.models.response.TextFieldData import kotlinx.coroutines.delay @Composable @@ -76,12 +86,22 @@ fun TextWithAgeSelectorWidgetComposable( @Composable fun ItemListWithDropdowns( + selectedItemStyle: AgeSelectorStyle = AgeSelectorStyle.V1, itemList: List, onSelection: (Int, SelectionItemData) -> Unit, widgetCallback: WidgetCallback?, ) { val interactionSource = remember { MutableInteractionSource() } - Column(modifier = Modifier.verticalScroll(enabled = true, state = rememberScrollState())) { + + // For V2 style, we don't use verticalScroll at all to avoid nested scrolling issues + // For V1 style, we keep the existing behavior + val columnModifier = + when (selectedItemStyle) { + AgeSelectorStyle.V1 -> Modifier.verticalScroll(rememberScrollState()) + else -> Modifier + } + + Column(modifier = columnModifier) { Spacer(modifier = Modifier.height(LocalDimensions.current.dp24)) itemList.forEachIndexed { index, item -> var isDialogOpen by remember { mutableStateOf(false) } @@ -95,44 +115,77 @@ fun ItemListWithDropdowns( Modifier.fillMaxWidth() .padding(horizontal = LocalDimensions.current.dp16), colors = - CardDefaults.outlinedCardColors(containerColor = Color.Transparent), - border = BorderStroke(LocalDimensions.current.dp1, colorCTASecondary), + CardDefaults.outlinedCardColors( + containerColor = + getSelectionItemContainerColor( + item.disableEditing, + selectedItemStyle, + ) + ), + border = BorderStroke(LocalDimensions.current.dp1, colorBorderAlt), shape = RoundedCornerShape(LocalDimensions.current.dp4), ) { Row( modifier = - Modifier.padding( - horizontal = LocalDimensions.current.dp16, - vertical = LocalDimensions.current.dp12, - ) - .clickable( - interactionSource = interactionSource, - indication = null, - ) { - if (!item.disableEditing) { - isDialogOpen = true - } else { - item.disabledCta?.let { widgetCallback?.onClick(it) } - } - }, + Modifier.clickable( + interactionSource = interactionSource, + indication = null, + ) { + if (!item.disableEditing) { + isDialogOpen = true + } else { + item.disabledCta?.let { widgetCallback?.onClick(it) } + } + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, ) { - NaviTextWidgetized( - textFieldData = item.selectionItemTitle, + item.assetImage?.let { + NaviImage( + imageFieldData = it, + modifier = Modifier.size(LocalDimensions.current.dp48), + ) + } + Row( modifier = - Modifier.weight(1f).padding(end = LocalDimensions.current.dp16), - ) - NaviTextWidgetized( - textFieldData = item.selectionItemDetails, - modifier = Modifier.padding(end = LocalDimensions.current.dp8), - ) - Icon( - painter = - painterResource(id = NaviWidgetsR.drawable.ic_arrow_black_down), - contentDescription = "down arrow", - modifier = Modifier.size(LocalDimensions.current.dp24), - ) + Modifier.padding( + horizontal = LocalDimensions.current.dp16, + vertical = LocalDimensions.current.dp12, + ) + .clickable( + interactionSource = interactionSource, + indication = null, + ) { + if (!item.disableEditing) { + isDialogOpen = true + } else { + item.disabledCta?.let { + widgetCallback?.onClick(it) + } + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + NaviTextWidgetized( + textFieldData = item.selectionItemTitle, + modifier = + Modifier.weight(1f) + .padding(end = LocalDimensions.current.dp16), + ) + NaviTextWidgetized( + textFieldData = item.selectionItemDetails, + modifier = Modifier.padding(end = LocalDimensions.current.dp16), + ) + Icon( + painter = + painterResource( + id = NaviWidgetsR.drawable.ic_arrow_black_down + ), + contentDescription = "down arrow", + modifier = Modifier.size(LocalDimensions.current.dp24), + ) + } } } @@ -145,7 +198,11 @@ fun ItemListWithDropdowns( val updatedItem = item.copy( pickerData = updatedPickerData, - selectionItemDetails = pickerItemData.title, + selectionItemDetails = + getSelectionItemDetails( + pickerItemData.title, + selectedItemStyle, + ), ) onSelection(index, updatedItem) isDialogOpen = false @@ -156,7 +213,9 @@ fun ItemListWithDropdowns( } } } - Spacer(modifier = Modifier.height(LocalDimensions.current.dp186)) + if (selectedItemStyle == AgeSelectorStyle.V1) { + Spacer(modifier = Modifier.height(LocalDimensions.current.dp186)) + } } } @@ -166,27 +225,45 @@ fun AgeSelectorComposable( isValidWidget: (Boolean, List?) -> Unit, updatePatchCallData: (PreQuotePatchData) -> Unit, widgetCallback: WidgetCallback?, + selectedItemStyle: AgeSelectorStyle = AgeSelectorStyle.V1, ) { var selectedItemList by remember(key1 = itemList.toString(), calculation = { mutableStateOf(itemList) }) LaunchedEffect(Unit) { delay(ANIMATION_DELAY.toLong()) isValidWidget( - selectedItemList?.all { it.pickerData?.selected.isNotNull() }.orFalse(), + selectedItemList?.all { it.pickerData?.selected?.id.isNotNull() }.orFalse(), itemList, ) } selectedItemList?.let { ItemListWithDropdowns( + selectedItemStyle = selectedItemStyle, itemList = it, onSelection = { index, item -> val updatedItemList = it.toMutableList() updatedItemList[index] = item selectedItemList = updatedItemList.toList() isValidWidget( - selectedItemList?.all { it.pickerData?.selected.isNotNull() }.orFalse(), + selectedItemList?.all { it.pickerData?.selected?.id.isNotNull() }.orFalse(), selectedItemList, ) + item.analyticsEventProperties?.name?.let { eventName -> + NaviTrackEvent.trackEvent( + eventName = eventName, + eventValues = + item.analyticsEventProperties + ?.properties + .addKeyIfMissing( + MEMBER_TYPE, + item.selectionItemTitle?.text.orEmpty(), + ) + .addKeyIfMissing( + MEMBER_AGE, + item.selectionItemDetails?.text.orEmpty(), + ), + ) + } }, widgetCallback = widgetCallback, ) @@ -211,3 +288,42 @@ private fun formatAgeSelectionRequestData( } return PreQuotePatchData(data = listOf(WidgetKey(type = "List", value = dataList))) } + +private fun getSelectionItemDetails( + title: TextFieldData?, + style: AgeSelectorStyle = AgeSelectorStyle.V1, +): TextFieldData? { + return when (style) { + AgeSelectorStyle.V1 -> title + AgeSelectorStyle.V2 -> + title?.let { + val displayText = + title.text?.let { original -> + if (Regex(AGE_DISPLAY_TEXT_REGEX).matches(original.trim())) { + original.substringBefore(" ") + } else { + original + } + } + + title.copy( + text = displayText, + size = 14, + lineSpacing = 22, + font = FontWeightEnum.NAVI_HEADLINE_REGULAR.name, + ) + } + else -> title + } +} + +private fun getSelectionItemContainerColor( + disableEditing: Boolean = false, + style: AgeSelectorStyle = AgeSelectorStyle.V1, +): Color { + return when (style) { + AgeSelectorStyle.V1 -> Color.Transparent + AgeSelectorStyle.V2 -> if (disableEditing) colorCTASecondary else Color.Transparent + else -> Color.Transparent + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetV2Composable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetV2Composable.kt new file mode 100644 index 0000000000..9be5100a2a --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/TextWithAgeSelectorWidgetV2Composable.kt @@ -0,0 +1,81 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables + +import androidx.compose.foundation.layout.Box +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.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.navi.insurance.pre.purchase.journey.PreQuotePatchData +import com.navi.insurance.pre.purchase.journey.composables.reusable.HeroSectionWithCardComposable +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.setWidgetLayoutParams +import com.navi.naviwidgets.models.AgeSelectorStyle +import com.navi.naviwidgets.models.GenericWidgetDataInfo +import com.navi.naviwidgets.models.TextWithAgeSelectorWidgetV2 + +@Composable +fun TextWithAgeSelectorWidgetV2Composable( + data: TextWithAgeSelectorWidgetV2, + isValidWidget: (Boolean, GenericWidgetDataInfo) -> Unit, + updatePatchCallData: (PreQuotePatchData) -> Unit = {}, + widgetCallback: WidgetCallback?, +) { + val widgetData by remember(key1 = data.toString(), calculation = { mutableStateOf(data) }) + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + setWidgetLayoutParams(widgetLayoutParams = widgetData.widgetLayoutParams) { + widgetData.widgetData?.let { data -> + Box(modifier = Modifier.fillMaxWidth()) { + HeroSectionWithCardComposable( + headerData = data.headerData, + trackerData = data.trackerData, + cardProperties = data.cardProperties, + ) { + Column(modifier = Modifier.fillMaxWidth()) { + NaviTextWidgetized( + textFieldData = data.title, + widgetCallback = widgetCallback, + modifier = + Modifier.padding( + top = LocalDimensions.current.dp16, + start = LocalDimensions.current.dp16, + end = LocalDimensions.current.dp16, + ), + ) + AgeSelectorComposable( + data.itemList, + isValidWidget = { isValid, list -> + val updatedData = + widgetData.copy(widgetData = data.copy(itemList = list)) + updatedData.widgetLayoutParams = widgetData.widgetLayoutParams + isValidWidget(isValid, updatedData) + }, + updatePatchCallData, + widgetCallback, + selectedItemStyle = AgeSelectorStyle.V2, + ) + } + } + } + } + } + Spacer(modifier = Modifier.height(LocalDimensions.current.dp186)) + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/CheckboxComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/CheckboxComposable.kt index c1a7c2b57c..7aded9f481 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/CheckboxComposable.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/CheckboxComposable.kt @@ -10,7 +10,6 @@ package com.navi.insurance.pre.purchase.journey.composables.reusable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape @@ -32,13 +31,14 @@ import com.navi.naviwidgets.composewidget.reusable.whiteColor fun CustomCheckBox( modifier: Modifier = Modifier, checked: Boolean = false, + shape: RoundedCornerShape = RoundedCornerShape(LocalDimensions.current.dp4), onCheckedChange: (Boolean) -> Unit = {}, selectedColor: Color = colorHIBlue, ) { Card( modifier = modifier, elevation = LocalDimensions.current.dp0, - shape = RoundedCornerShape(LocalDimensions.current.dp4), + shape = shape, backgroundColor = whiteColor, border = BorderStroke( @@ -70,14 +70,11 @@ fun CustomRadioButton( modifier: Modifier = Modifier, selected: Boolean = false, onClick: (Boolean) -> Unit = {}, + deselectedColor: Color = colorTextTertiary, selectedColor: Color = colorHIBlue, ) { Card( - modifier = - modifier.padding( - horizontal = LocalDimensions.current.dp16, - vertical = LocalDimensions.current.dp13, - ), + modifier = modifier, elevation = LocalDimensions.current.dp0, shape = CircleShape, backgroundColor = whiteColor, @@ -87,7 +84,7 @@ fun CustomRadioButton( selected.let { if (it) LocalDimensions.current.dp2 else LocalDimensions.current.dp1 }, - color = selected.let { if (it) selectedColor else colorTextTertiary }, + color = selected.let { if (it) selectedColor else deselectedColor }, ), ) { Box( diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HeroSectionComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HeroSectionComposable.kt new file mode 100644 index 0000000000..5da7ee4aa1 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HeroSectionComposable.kt @@ -0,0 +1,107 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables.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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.naviwidgets.composewidget.reusable.midBlue +import com.navi.naviwidgets.extensions.NaviCard +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.getBackground +import com.navi.naviwidgets.models.HeroSectionData +import com.navi.naviwidgets.models.TrackerData +import com.navi.naviwidgets.models.response.CardProperties + +@Composable +fun HeroSectionComposable(data: HeroSectionData?, modifier: Modifier = Modifier) { + if (data == null) return + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + NaviTextWidgetized( + textFieldData = data.title, + modifier = + Modifier.weight(1f) + .padding(top = LocalDimensions.current.dp12, end = LocalDimensions.current.dp8), + ) + NaviImage( + modifier = + Modifier.size( + width = LocalDimensions.current.dp124, + height = LocalDimensions.current.dp72, + ), + imageFieldData = data.image, + ) + } +} + +@Composable +fun HeroSectionWithCardComposable( + headerData: HeroSectionData?, + trackerData: TrackerData?, + cardProperties: CardProperties?, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Box(modifier = modifier.fillMaxWidth()) { + headerData?.backgroundData?.let { bgData -> + Box( + modifier = + Modifier.fillMaxWidth() + .height(LocalDimensions.current.dp175) + .getBackground(bgData) + ) + } + + Column(modifier = Modifier.fillMaxWidth()) { + HeroSectionComposable( + data = headerData, + modifier = + Modifier.fillMaxWidth() + .padding( + start = LocalDimensions.current.dp20, + end = LocalDimensions.current.dp26, + ), + ) + NaviCard( + modifier = + Modifier.fillMaxWidth() + .padding( + start = LocalDimensions.current.dp16, + end = LocalDimensions.current.dp16, + ), + cardProperties = cardProperties, + ) { + Column(modifier = Modifier.fillMaxWidth()) { + trackerData?.let { + HorizontalTrackerComposable( + trackerData = it, + modifier = Modifier.background(color = midBlue), + ) + } + content() + } + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HorizontalTrackerComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HorizontalTrackerComposable.kt new file mode 100644 index 0000000000..a2010b81e3 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/HorizontalTrackerComposable.kt @@ -0,0 +1,63 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables.reusable + +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.navi.design.decorator.DashedDivider +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions +import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.colorGreyB5ACB9 +import com.navi.naviwidgets.composewidget.reusable.colorHIBlue +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.models.TrackerData + +@Composable +fun HorizontalTrackerComposable( + trackerData: TrackerData?, + modifier: Modifier = Modifier, + activeColor: Color = colorHIBlue, + inactiveColor: Color = colorGreyB5ACB9, + widgetCallback: WidgetCallback? = null, +) { + trackerData?.items?.let { listData -> + Row( + modifier = + modifier + .fillMaxWidth() + .padding( + horizontal = LocalDimensions.current.dp16, + vertical = LocalDimensions.current.dp12, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + listData.forEachIndexed { index, item -> + NaviImage( + imageFieldData = item.image, + widgetCallback = widgetCallback, + modifier = Modifier.size(LocalDimensions.current.dp24), + ) + if (index < listData.lastIndex) { + DashedDivider( + thickness = LocalDimensions.current.dp1, + color = if (item.isCompleted == true) activeColor else inactiveColor, + intervals = floatArrayOf(20f, 0f), + modifier = + Modifier.padding(horizontal = LocalDimensions.current.dp4).weight(1f), + ) + } + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/InputFieldComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/InputFieldComposable.kt index 37b2005339..182de285e3 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/InputFieldComposable.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/InputFieldComposable.kt @@ -51,6 +51,7 @@ fun TextFieldComposable( data: EditTextData? = null, bottomContent: TextFieldData? = null, formattedBottomContent: TextFieldData? = null, + trailingContent: @Composable (() -> Unit)? = null, maxLength: Int = Int.MAX_VALUE, keyboardType: KeyboardType = KeyboardType.Text, imeAction: ImeAction = ImeAction.Default, @@ -58,6 +59,7 @@ fun TextFieldComposable( onValueChange: (String) -> Unit, disableEditing: Boolean = false, disabledCta: CtaData? = null, + enableDateFormatting: Boolean = false, widgetCallback: WidgetCallback? = null, ) { Column(modifier = Modifier.padding(horizontal = LocalDimensions.current.dp16)) { @@ -96,14 +98,15 @@ fun TextFieldComposable( errorBorderColor = Color.Red, ), isError = bottomContent?.text.isNotNullAndNotEmpty(), + trailingIcon = trailingContent, ) - if (bottomContent?.text.isNullOrBlank()) { + if (bottomContent?.text.isNullOrBlank() && enableDateFormatting) { formattedBottomContent?.text = formatDate(data?.title?.text) } Box( modifier = Modifier.padding( - top = LocalDimensions.current.dp8, + top = LocalDimensions.current.dp4, bottom = LocalDimensions.current.dp16, ) ) { @@ -164,6 +167,7 @@ fun DOBInputFieldComposable( data = data, bottomContent = bottomContent, formattedBottomContent = formattedContent, + enableDateFormatting = true, maxLength = 8, keyboardType = KeyboardType.NumberPassword, visualTransformation = CardNumberTransformation(), diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/ObserveLocationAndFetchPinCodeComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/ObserveLocationAndFetchPinCodeComposable.kt new file mode 100644 index 0000000000..1fd9102e0f --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/ObserveLocationAndFetchPinCodeComposable.kt @@ -0,0 +1,93 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables.reusable + +import android.Manifest +import android.app.Activity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.navi.insurance.location.NaviLocationManager +import com.navi.insurance.pre.purchase.journey.ui.PreQuoteJourneyViewModel +import com.navi.insurance.pre.purchase.journey.utils.LocationHandler +import com.navi.insurance.pre.purchase.journey.utils.LocationPermissionUtils +import com.navi.insurance.util.Constants +import com.navi.naviwidgets.models.GenericWidgetDataInfo +import kotlinx.coroutines.delay + +@Composable +fun ObserveLocationAndFetchPinCodeComposable( + viewModel: PreQuoteJourneyViewModel?, + commonNaviLocationManager: com.navi.common.managers.NaviLocationManager?, + naviLocationManager: NaviLocationManager?, + activity: Activity?, + getPincode: () -> String, + setPincode: (String) -> Unit, + inputSize: Int, + onUpdateData: (String) -> GenericWidgetDataInfo, + isValidWidget: (Boolean, GenericWidgetDataInfo) -> Unit, + onPermissionDenied: (() -> Unit)? = null, +) { + val context = LocalContext.current + + val locationHandler = remember { + LocationHandler( + context = context, + activity = activity, + naviLocationManager = naviLocationManager, + commonNaviLocationManager = commonNaviLocationManager, + viewModel = viewModel, + ) + } + + locationHandler.pincodeCallback = { newPin -> + setPincode(newPin) + val updatedData = onUpdateData(newPin) + isValidWidget(newPin.isNotEmpty() && newPin.length == inputSize, updatedData) + } + + locationHandler.onPermissionDenied = onPermissionDenied + + val permissionLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val fineLocationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false + val coarseLocationGranted = + permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false + val isPermissionGranted = fineLocationGranted || coarseLocationGranted + + locationHandler.handlePermissionResult(isPermissionGranted) + } + + // Auto-fetch on initial render + LaunchedEffect(Unit) { + delay(Constants.ANIMATION_DELAY.toLong()) + val pincode = getPincode() + val isEnabled = pincode.isNotEmpty() && pincode.length == inputSize + val updatedData = onUpdateData(pincode) + isValidWidget(isEnabled, updatedData) + + if (!isEnabled) { + if (LocationPermissionUtils.isLocationPermissionGranted(context)) { + // Have permission but no pincode, try to fetch + if (!locationHandler.tryFetchFromSavedLocation()) { + locationHandler.fetchLocation() + } + } else { + // No permission and no pincode, request permission + // This is not user-triggered + locationHandler.prepareForPermissionRequest(false) + permissionLauncher.launch(LocationPermissionUtils.getLocationPermissions()) + } + } + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/TextViewComposable.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/TextViewComposable.kt new file mode 100644 index 0000000000..0704c017f0 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/TextViewComposable.kt @@ -0,0 +1,56 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.composables.reusable + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.navi.base.model.CtaData +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.extensions.NaviTextWidgetized +import com.navi.naviwidgets.extensions.debounceClickable +import com.navi.naviwidgets.models.response.TextFieldData + +@Composable +fun TextViewComposable( + textFieldData: TextFieldData?, + modifier: Modifier = Modifier, + cta: CtaData? = null, + widgetCallback: WidgetCallback? = null, + backgroundColor: Color = Color.Transparent, +) { + Column( + modifier = + modifier + .debounceClickable(onClick = { cta?.let { widgetCallback?.onClick(it) } }) + .fillMaxWidth() + .background(color = backgroundColor) + .border( + width = LocalDimensions.current.dp1, + color = colorBorderAlt, + shape = RoundedCornerShape(LocalDimensions.current.dp4), + ) + ) { + NaviTextWidgetized( + textFieldData = textFieldData, + modifier = + Modifier.padding( + horizontal = LocalDimensions.current.dp16, + vertical = LocalDimensions.current.dp12, + ), + ) + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/WheelPicker.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/WheelPicker.kt index 6416748bfa..b93f32e110 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/WheelPicker.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/composables/reusable/WheelPicker.kt @@ -52,7 +52,12 @@ import kotlinx.coroutines.flow.map @Composable fun WheelPicker(pickerData: PickerData, onDismiss: (PickerItemData) -> Unit) { pickerData.itemList?.let { - OpenAgePicker(pickerData.title, pickerData.selected ?: pickerData.default, it, onDismiss) + OpenAgePicker( + pickerData.title, + pickerData.selected?.id?.let { pickerData.selected } ?: pickerData.default, + it, + onDismiss, + ) } } 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 7179ca8b90..c6c7ca89c7 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 @@ -16,10 +16,14 @@ import com.navi.insurance.pre.purchase.journey.PreQuotePatchData import com.navi.insurance.pre.purchase.journey.composables.CheckBoxWithDropDownSelectorWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.DetailsStatusWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.HeaderWithTrackerWidgetComposable +import com.navi.insurance.pre.purchase.journey.composables.MemberSelectionWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.MultiTypeSelectionWidgetComposable import com.navi.insurance.pre.purchase.journey.composables.NameDobEditTextWidgetComposable +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.TextWithAgeSelectorWidgetComposable +import com.navi.insurance.pre.purchase.journey.composables.TextWithAgeSelectorWidgetV2Composable import com.navi.insurance.pre.purchase.journey.composables.TextWithHeightWeightSelectorWidgetComposable import com.navi.insurance.util.EMPTY import com.navi.naviwidgets.callbacks.WidgetCallback @@ -29,10 +33,14 @@ import com.navi.naviwidgets.models.DetailsStatusWidget import com.navi.naviwidgets.models.FooterWithCardAndSnackbarWidgetData import com.navi.naviwidgets.models.GenericWidgetDataInfo import com.navi.naviwidgets.models.HeaderWithTrackerWidgetData +import com.navi.naviwidgets.models.MemberSelectionWidgetData import com.navi.naviwidgets.models.MultiTypeSelectionWidget 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.TextWithAgeSelectorWidget +import com.navi.naviwidgets.models.TextWithAgeSelectorWidgetV2 import com.navi.naviwidgets.models.TextWithHeightWeightSelectorWidget @Composable @@ -58,6 +66,13 @@ fun ComposableWidgetFactory( FooterWithCardAndSnackBarWidgetComposable(data, state, widgetCallback) is HeaderWithTrackerWidgetData -> HeaderWithTrackerWidgetComposable(data, widgetCallback, handleVisibility) + is MemberSelectionWidgetData -> + MemberSelectionWidgetComposable( + data, + isValidWidget, + updatePatchCallData, + widgetCallback, + ) is PincodeInputWidgetData -> PincodeInputWidgetComposable( data, @@ -75,6 +90,13 @@ fun ComposableWidgetFactory( updatePatchCallData, widgetCallback = widgetCallback, ) + is NameDobTextWidgetData -> + NameDobTextWidgetComposable( + data, + isValidWidget, + updatePatchCallData, + widgetCallback = widgetCallback, + ) is DetailsStatusWidget -> DetailsStatusWidgetComposable(data, widgetCallback, updatePatchCallData) is TextWithAgeSelectorWidget -> @@ -84,6 +106,24 @@ fun ComposableWidgetFactory( updatePatchCallData, widgetCallback, ) + is TextWithAgeSelectorWidgetV2 -> + TextWithAgeSelectorWidgetV2Composable( + data, + isValidWidget, + updatePatchCallData, + widgetCallback, + ) + is PincodeInputWidgetDataV2 -> + PincodeInputWidgetV2Composable( + data, + naviLocationManager, + commonNaviLocationManager, + isValidWidget, + updatePatchCallData, + widgetCallback, + activity, + viewModel, + ) is TextWithHeightWeightSelectorWidget -> TextWithHeightWeightSelectorWidgetComposable(data, isValidWidget, updatePatchCallData) is MultiTypeSelectionWidget -> diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/theme/PrePurchaseJourneyDimensions.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/theme/PrePurchaseJourneyDimensions.kt index 26d22cc467..6c0d428022 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/theme/PrePurchaseJourneyDimensions.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/theme/PrePurchaseJourneyDimensions.kt @@ -35,13 +35,16 @@ data class PrePurchaseJourneyDimensions( val dp32: Dp = 32.dp, val dp37: Dp = 37.dp, val dp40: Dp = 40.dp, + val dp43: Dp = 43.dp, val dp48: Dp = 48.dp, val dp60: Dp = 60.dp, val dp64: Dp = 64.dp, val dp67: Dp = 67.dp, val dp72: Dp = 72.dp, + val dp80: Dp = 80.dp, val dp124: Dp = 124.dp, val dp150: Dp = 150.dp, + val dp175: Dp = 175.dp, val dp186: Dp = 186.dp, val dp168: Dp = 168.dp, ) diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyFragment.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyFragment.kt index e506cf2e9c..9ec3d4a503 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyFragment.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyFragment.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -43,6 +44,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalFocusManager @@ -55,6 +59,7 @@ import com.navi.base.model.AnalyticsEvent import com.navi.base.model.CtaConstants import com.navi.base.model.CtaData import com.navi.base.model.CtaType +import com.navi.base.model.LineItem import com.navi.base.model.NaviClickAction import com.navi.base.utils.isNotNull import com.navi.base.utils.orFalse @@ -66,6 +71,7 @@ import com.navi.common.managers.NaviLocationManager as CommonNaviLocationManager import com.navi.common.network.models.ErrorMessage import com.navi.common.react.ReactPreLoadHeadLessActivity import com.navi.common.utils.log +import com.navi.common.utils.setStatusBarColorInt import com.navi.insurance.analytics.InsuranceAnalyticsConstants import com.navi.insurance.common.GiBaseFragment import com.navi.insurance.common.GiBaseVM @@ -81,6 +87,7 @@ import com.navi.insurance.pre.purchase.journey.TRANSITION import com.navi.insurance.pre.purchase.journey.WidgetKey import com.navi.insurance.pre.purchase.journey.composables.ShowLoaderScreen import com.navi.insurance.pre.purchase.journey.factory.ComposableWidgetFactory +import com.navi.insurance.pre.purchase.journey.theme.LocalDimensions import com.navi.insurance.quoteredesign.fragments.KYCBottomSheetFragment import com.navi.insurance.util.ARG_APPLICATION_ID import com.navi.insurance.util.ARG_APPLICATION_TYPE @@ -95,9 +102,11 @@ import com.navi.insurance.util.PRE_QUOTE_ID_EXTRA import com.navi.insurance.util.QUOTE_ID_EXTRA import com.navi.insurance.util.launchHelpCenter import com.navi.naviwidgets.callbacks.WidgetCallback +import com.navi.naviwidgets.composewidget.reusable.footerColorShadow import com.navi.naviwidgets.composewidget.reusable.whiteColor import com.navi.naviwidgets.extensions.FloatingButtonOverlay import com.navi.naviwidgets.extensions.getJsonObject +import com.navi.naviwidgets.extensions.hexToInt import com.navi.naviwidgets.models.FooterButtonState import com.navi.naviwidgets.models.FooterWithCardAndSnackbarWidgetData import com.navi.naviwidgets.models.GenericWidgetDataInfo @@ -109,6 +118,7 @@ class PreQuoteJourneyFragment() : GiBaseFragment(), WidgetCallback, NewBottomShe private val viewModel by viewModels() private var naviLocationManager: NaviLocationManager? = null private var commonNaviLocationManager: CommonNaviLocationManager? = null + private var enableFooterShadow = false var view: NaviErrorPageView? = null var isBackPressInProgress: Boolean = false @@ -220,7 +230,7 @@ class PreQuoteJourneyFragment() : GiBaseFragment(), WidgetCallback, NewBottomShe } } } - LazyColumn(modifier = Modifier.fillMaxWidth().background(whiteColor)) { + LazyColumn(modifier = Modifier.fillMaxWidth()) { items(it.footer?.size.orZero()) { index -> it.footer?.get(index)?.let { data -> if (data is FooterWithCardAndSnackbarWidgetData) { @@ -279,7 +289,27 @@ class PreQuoteJourneyFragment() : GiBaseFragment(), WidgetCallback, NewBottomShe } } viewModel.getFooterData()?.let { - ComposableWidgetFactory(it, state = footerState, widgetCallback = widgetCallback) + if (enableFooterShadow) { + Column( + modifier = + Modifier.background( + brush = + Brush.verticalGradient( + colors = listOf(Color.Transparent, footerColorShadow) + ), + shape = RectangleShape, + ) + .padding(top = LocalDimensions.current.dp32) + ) { + ComposableWidgetFactory( + it, + state = footerState, + widgetCallback = widgetCallback, + ) + } + } else { + ComposableWidgetFactory(it, state = footerState, widgetCallback = widgetCallback) + } } } @@ -369,6 +399,11 @@ class PreQuoteJourneyFragment() : GiBaseFragment(), WidgetCallback, NewBottomShe } state.data.isNotNull() -> { sendPageViewEvent(state.data?.metaData?.analyticsInitEvent) + extractPageProperties( + properties = state.data?.metaData?.pageProperties, + onStatusBarColor = { color -> activity?.setStatusBarColorInt(hexToInt(color)) }, + onFooterShadow = { enableShadow -> enableFooterShadow = enableShadow }, + ) isBackPressInProgress = false viewModel.updateCurrentPageData(state.data) } @@ -525,6 +560,19 @@ class PreQuoteJourneyFragment() : GiBaseFragment(), WidgetCallback, NewBottomShe activity?.let { viewModel.fetchPreQuoteJourneyResponse() } } + private fun extractPageProperties( + properties: List?, + onStatusBarColor: (String) -> Unit, + onFooterShadow: (Boolean) -> Unit = { _ -> }, + ) { + properties?.forEach { + when (it.key) { + Constants.STATUS_BAR_COLOR -> onStatusBarColor(it.value.orEmpty()) + Constants.FOOTER_SHADOW -> onFooterShadow(it.value.orEmpty().toBoolean()) + } + } + } + override fun buttonClick(actionData: ActionData?) = Unit override val screenName: String = TAG diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyViewModel.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyViewModel.kt index b0fada47e0..a15a6a4365 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyViewModel.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/ui/PreQuoteJourneyViewModel.kt @@ -15,6 +15,7 @@ import com.navi.base.utils.isNotNull import com.navi.base.utils.isNotNullAndNotEmpty import com.navi.base.utils.isNull import com.navi.base.utils.orFalse +import com.navi.common.ResponseState import com.navi.common.checkmate.model.MetricInfo import com.navi.common.model.PermissionVerticalType import com.navi.common.network.models.RepoResult @@ -35,10 +36,12 @@ import com.navi.insurance.pre.purchase.journey.PreQuoteJourneyState import com.navi.insurance.pre.purchase.journey.PreQuoteMetaData import com.navi.insurance.pre.purchase.journey.PreQuotePatchData import com.navi.insurance.pre.purchase.journey.PreQuotePatchResponse +import com.navi.insurance.purchase.compliance.domain.dto.PinCodeResponseData import com.navi.insurance.util.APPLICATION_TYPE_EXTRA import com.navi.insurance.util.ARG_APPLICATION_ID import com.navi.insurance.util.Constants.DELAY_1000 import com.navi.insurance.util.PRE_QUOTE_ID_EXTRA +import com.navi.insurance.util.isValidIndianPincode import com.navi.naviwidgets.models.CheckBoxWithDropDownSelectorWidget import com.navi.naviwidgets.models.FooterButtonState import com.navi.naviwidgets.models.FooterInfo @@ -53,7 +56,9 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -85,6 +90,10 @@ constructor( val footerState: StateFlow get() = _footerState.asStateFlow() + private val _fieldValidationEvent = + MutableSharedFlow>(replay = 0) + val fieldValidationEvent: SharedFlow> = _fieldValidationEvent + private val _scrollIndex: MutableStateFlow = MutableStateFlow(null) val scrollIndex: StateFlow get() = _scrollIndex.asStateFlow() @@ -150,14 +159,10 @@ constructor( ) } else { response = - repository.fetchFormPageData(applicationType.orEmpty(), applicationId) - if (response.data.isNull()) { - response = - repository.postFormPageData( - applicationType.orEmpty(), - FormWidgetRequest(applicationId), - ) - } + repository.postFormPageData( + applicationType.orEmpty(), + FormWidgetRequest(applicationId), + ) } } if ( @@ -319,14 +324,11 @@ constructor( pageName = pageName, ) } else { - response = repository.fetchFormPageData(applicationType.orEmpty(), applicationId) - if (response.data.isNull()) { - response = - repository.postFormPageData( - applicationType.orEmpty(), - FormWidgetRequest(applicationId), - ) - } + response = + repository.postFormPageData( + applicationType.orEmpty(), + FormWidgetRequest(applicationId), + ) } if ( response.error.isNull() && @@ -410,7 +412,15 @@ constructor( ) } else { if (pageResponse.statusCode == ERROR_CODE_400) { - showFooterError(pageResponse.errors?.getOrNull(0)?.message) + pageResponse.errors?.getOrNull(0)?.actions?.getOrNull(0)?.cta?.let { + errorCta -> + _preQuoteJourneyFlow.value = + _preQuoteJourneyFlow.value.copy( + ctaData = errorCta, + isLoading = false, + hasErrorOccurred = false, + ) + } ?: showFooterError(pageResponse.errors?.getOrNull(0)?.message) } else { _preQuoteJourneyFlow.value = _preQuoteJourneyFlow.value.copy( @@ -446,6 +456,51 @@ constructor( } } + fun fetchAddressUsingPinCode(pinCode: String) { + if (!pinCode.isValidIndianPincode()) { + viewModelScope.launch { + _fieldValidationEvent.emit(ResponseState.Failure("Please enter a valid pincode")) + } + return + } + viewModelScope.launch( + Dispatchers.IO + + giBaseExceptionHandler(ApiErrorTagType.ADDRESS_FETCH_USING_PIN_CODE_ERROR.value) + ) { + _fieldValidationEvent.emit(ResponseState.Loading) + _showOverLay.value = true + val response = repository.fetchAddressUsingPinCode(pinCode) + _showOverLay.value = false + if (response.isSuccessWithData()) { + response.data?.let { data -> + _fieldValidationEvent.emit(ResponseState.Success(data)) + } + } else if (response.statusCode == ERROR_CODE_400) { + _fieldValidationEvent.emit(ResponseState.Failure("Please enter a valid pincode")) + _footerState.value = FooterButtonState.DISABLED.name + logError( + response, + GiErrorMetaData( + ApiErrorTagType.ADDRESS_FETCH_USING_PIN_CODE_ERROR.value, + flowName = GiErrorMetaData.FLOW_NEW_PRE_PURCHASE, + ), + ) + } else { + logError( + response, + GiErrorMetaData( + ApiErrorTagType.ADDRESS_FETCH_USING_PIN_CODE_ERROR.value, + flowName = GiErrorMetaData.FLOW_NEW_PRE_PURCHASE, + ), + ) + } + } + } + + fun resetFieldValidationState() { + viewModelScope.launch { _fieldValidationEvent.emit(ResponseState.Idle) } + } + fun hideOverLay() { viewModelScope.launch { delay(DELAY_1000) diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationHandler.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationHandler.kt new file mode 100644 index 0000000000..f86e09017b --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationHandler.kt @@ -0,0 +1,96 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.utils + +import android.app.Activity +import android.content.Context +import com.navi.common.utils.CommonUtils +import com.navi.insurance.location.NaviLocationManager +import com.navi.insurance.models.UserLocation +import com.navi.insurance.pre.purchase.journey.composables.getSavedUserLocation +import com.navi.insurance.pre.purchase.journey.ui.PreQuoteJourneyViewModel + +class LocationHandler( + private val context: Context, + private val activity: Activity?, + private val naviLocationManager: NaviLocationManager?, + private val commonNaviLocationManager: com.navi.common.managers.NaviLocationManager?, + private val viewModel: PreQuoteJourneyViewModel?, +) { + // Track if permission request was triggered by user action + private var isUserTriggeredRequest: Boolean = false + + var pincodeCallback: ((String) -> Unit)? = null + + var onPermissionDenied: (() -> Unit)? = null + + fun processPostalCode(postalCode: String?, latitude: Double?, longitude: Double?) { + postalCode?.let { newPin -> pincodeCallback?.invoke(newPin) } + } + + fun processLocationAndFetchPincode(userLocation: UserLocation?) { + userLocation?.let { location -> + val updatedUserLocation = + com.navi.analytics.model.UserLocation( + location.latitude, + location.longitude, + location.date, + ) + CommonUtils.saveUserLocation(updatedUserLocation) + + naviLocationManager?.getPostalCodeFromLocation( + context = context, + userLocation = location, + callback = this::processPostalCode, + ) + + commonNaviLocationManager?.postLocation(updatedUserLocation) + } + } + + fun fetchLocation() { + if (naviLocationManager?.isLocationEnabled() == true) { + naviLocationManager.getLastLocation(this::processLocationAndFetchPincode) + } else { + activity?.let { + naviLocationManager?.requestLocationUpdates( + activity = it, + false, + this::processLocationAndFetchPincode, + ) + } + } + } + + fun handlePermissionResult(isGranted: Boolean) { + LocationPermissionUtils.postPermissionAnalytics(isGranted) + + if (isGranted) { + viewModel?.sendPermissionData() + fetchLocation() + } else if (isUserTriggeredRequest) { + onPermissionDenied?.invoke() + isUserTriggeredRequest = false + } + } + + fun tryFetchFromSavedLocation(): Boolean { + return getSavedUserLocation()?.let { + naviLocationManager?.getPostalCodeFromLocation( + context = context, + userLocation = it, + callback = this::processPostalCode, + ) + true + } ?: false + } + + fun prepareForPermissionRequest(isUserTriggered: Boolean) { + isUserTriggeredRequest = isUserTriggered + } +} diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationPermissionUtils.kt b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationPermissionUtils.kt new file mode 100644 index 0000000000..56d2865d59 --- /dev/null +++ b/android/navi-insurance/src/main/java/com/navi/insurance/pre/purchase/journey/utils/LocationPermissionUtils.kt @@ -0,0 +1,57 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.insurance.pre.purchase.journey.utils + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import com.navi.analytics.utils.OPTION_SELECTED +import com.navi.analytics.utils.SCREEN_NAME +import com.navi.insurance.analytics.InsuranceAnalyticsConstants +import com.navi.insurance.analytics.NaviInsuranceAnalytics + +object LocationPermissionUtils { + + private val LOCATION_PERMISSIONS = + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) + + fun isLocationPermissionGranted(context: Context): Boolean { + val isFineLocationGranted = + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED + + val isCoarseLocationGranted = + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED + + return isFineLocationGranted || isCoarseLocationGranted + } + + fun getLocationPermissions(): Array = LOCATION_PERMISSIONS + + fun postPermissionAnalytics(isGranted: Boolean) { + NaviInsuranceAnalytics.postAnalyticsEvent( + InsuranceAnalyticsConstants.HI_ALLOW_LOCATION_ACCESS_POPUP, + eventProperties = + mutableMapOf( + Pair( + OPTION_SELECTED, + if (isGranted) InsuranceAnalyticsConstants.ALLOW + else InsuranceAnalyticsConstants.DENY, + ), + Pair(SCREEN_NAME, InsuranceAnalyticsConstants.PRE_QUOTE_JOURNEY_FRAGMENT), + ), + ) + } +} 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 62aa0d9027..b9e320a3f5 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 @@ -256,6 +256,7 @@ object Constants { const val POLL = "POLL" const val ANIMATE_FOOTER_SECTION_VISIBILITY = "animateFooterSectionVisibility" const val STATUS_BAR_COLOR = "statusBarColor" + const val FOOTER_SHADOW = "footerShadow" const val SOMETHING_WENT_WRONG = "Something went wrong" const val ERROR_SUBTITLE = "It appears we are facing trouble.\nPlease try again" const val RETRY = "Retry" @@ -272,6 +273,10 @@ object Constants { const val BLANK_CAPTCHA_TOKEN = "Captcha token is blank" const val OTP_REGENERATION_FAILED = "OTP Regeneration Failed" const val OTP_SUBMISSION_FAILED = "OTP Submission Failed" + const val AGE_DISPLAY_TEXT_REGEX = "^\\d+ (Year|Years)$" + const val PLUS_SIGN = "+" + const val MINUS_SIGN = "-" + const val DOB_ID = "dob" // 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/Utility.kt b/android/navi-insurance/src/main/java/com/navi/insurance/util/Utility.kt index 2e0b9bb688..9ecd9890f3 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/util/Utility.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/util/Utility.kt @@ -240,6 +240,23 @@ fun timestampToddMMMYYYY(timeStamp: Long): String { return DateFormat.format("dd MMM, yyyy", calendar).toString() } +/** + * Formats a date string from "DD Month YYYY" format to "DDMMYYYY" format Example: "31 May 1995" -> + * "31051995" + */ +fun formatDateToDDMMYYYY(dateString: String): String { + return try { + val inputFormat = SimpleDateFormat("d MMM yyyy", Locale.ENGLISH) + val outputFormat = SimpleDateFormat("ddMMyyyy", Locale.ENGLISH) + + val date = inputFormat.parse(dateString) + date?.let { outputFormat.format(it) } ?: dateString + } catch (e: Exception) { + e.log() + EMPTY + } +} + fun getSpinnerIndex(spinner: AppCompatSpinner, myString: String): Int { for (i in 0 until spinner.getCount()) { if (spinner.getItemAtPosition(i).toString().equals(myString, ignoreCase = true)) { @@ -389,6 +406,23 @@ fun String.getDateOrdinal(): String { fun CharSequence?.isValidEmail() = !this.isNullOrEmpty() && PatternsCompat.EMAIL_ADDRESS.matcher(this).matches() +/** + * Validates if the given string is a valid Indian pincode. Indian pincodes are 6-digit numbers + * starting with a non-zero digit. + * + * @param pincode The pincode string to validate + * @return true if the pincode is valid, false otherwise + */ +fun String?.isValidIndianPincode(): Boolean { + if (this.isNullOrEmpty()) return false + + val basicPattern = Regex("^[1-9][0-9]{5}$") + + if (!basicPattern.matches(this)) return false + + return true +} + fun getGlobalErrorType(errorCode: Int?): String { return if ( errorCode == NO_INTERNET || 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 ea5d0a9124..a01c65ad73 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 @@ -26,6 +26,7 @@ import com.navi.naviwidgets.models.CentreTitleSubtitleWidgetData import com.navi.naviwidgets.models.FooterWithCardAndSnackbarWidgetData import com.navi.naviwidgets.models.GenericWidgetDataInfo import com.navi.naviwidgets.models.HeaderWithTrackerWidgetData +import com.navi.naviwidgets.models.MemberSelectionWidgetData import com.navi.naviwidgets.models.NameDobEditTextWidgetData import com.navi.naviwidgets.models.PincodeInputWidgetData import com.navi.naviwidgets.models.TextListCardWidgetData @@ -353,9 +354,18 @@ class WidgetDataDeserializer : JsonDeserializer { WidgetTypes.PINCODE_INPUT_WIDGET.value -> { Gson().fromJson(jsonObject, PincodeInputWidgetData::class.java) } + WidgetTypes.PINCODE_INPUT_WIDGET_V2.value -> { + Gson().fromJson(jsonObject, PincodeInputWidgetDataV2::class.java) + } + WidgetTypes.MEMBER_SELECTION_WIDGET.value -> { + Gson().fromJson(jsonObject, MemberSelectionWidgetData::class.java) + } WidgetTypes.TEXT_WITH_AGE_SELECTOR_WIDGET.value -> { Gson().fromJson(jsonObject, TextWithAgeSelectorWidget::class.java) } + WidgetTypes.TEXT_WITH_AGE_SELECTOR_WIDGET_V2.value -> { + Gson().fromJson(jsonObject, TextWithAgeSelectorWidgetV2::class.java) + } WidgetTypes.TEXT_WITH_HEIGHT_WEIGHT_SELECTOR_WIDGET.value -> { Gson().fromJson(jsonObject, TextWithHeightWeightSelectorWidget::class.java) } @@ -368,6 +378,9 @@ class WidgetDataDeserializer : JsonDeserializer { WidgetTypes.NAME_DOB_EDIT_TEXT_WIDGET.value -> { Gson().fromJson(jsonObject, NameDobEditTextWidgetData::class.java) } + WidgetTypes.NAME_DOB_TEXT_WIDGET.value -> { + Gson().fromJson(jsonObject, NameDobTextWidgetData::class.java) + } WidgetTypes.CARD_WITH_FLIP_ANIMATION.value -> { Gson().fromJson(jsonObject, CardWithFlipAnimationWidget::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 ce03a90a8a..f82d647c61 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 @@ -90,11 +90,15 @@ enum class WidgetTypes(val value: String) { DETAILS_STATUS_WIDGET("DETAILS_STATUS_WIDGET"), HEADER_WITH_TRACKER_WIDGET("HEADER_WITH_TRACKER_WIDGET"), PINCODE_INPUT_WIDGET("PINCODE_INPUT_WIDGET"), + PINCODE_INPUT_WIDGET_V2("PINCODE_INPUT_WIDGET_V2"), + MEMBER_SELECTION_WIDGET("MEMBER_SELECTION_WIDGET"), TEXT_WITH_AGE_SELECTOR_WIDGET("TEXT_WITH_AGE_SELECTOR_WIDGET"), + TEXT_WITH_AGE_SELECTOR_WIDGET_V2("TEXT_WITH_AGE_SELECTOR_WIDGET_V2"), TEXT_WITH_HEIGHT_WEIGHT_SELECTOR_WIDGET("TEXT_WITH_HEIGHT_WEIGHT_SELECTOR_WIDGET"), MULTITYPE_SELECTOR_WIDGET("MULTITYPE_SELECTOR_WIDGET"), CHECK_BOX_DROPDOWN_SELECTOR_WIDGET("CHECK_BOX_DROPDOWN_SELECTOR_WIDGET"), NAME_DOB_EDIT_TEXT_WIDGET("NAME_DOB_EDIT_TEXT_WIDGET"), + NAME_DOB_TEXT_WIDGET("NAME_DOB_TEXT_WIDGET"), AUTO_CAROUSEL_CARDS_PD_WIDGET("AUTO_CAROUSEL_CARDS_PD_WIDGET"), CARD_HORIZONTAL_AUTO_CAROUSEL_WIDGET("CARD_HORIZONTAL_AUTO_CAROUSEL_WIDGET"), CARD_WITH_CTA_AND_IMAGE_WIDGET("CARD_WITH_CTA_AND_IMAGE_WIDGET"), diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/AppColors.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/AppColors.kt index 8a822413c8..2aeff23e8f 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/AppColors.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/composewidget/reusable/AppColors.kt @@ -33,6 +33,7 @@ val colorLightGrey: Color = Color(0xFF14BC51) val colorSuccessGreen: Color = Color(0XFF22A940) val colorOffWhite: Color = Color(0xFFF9F9FA) val bottomSheetColorShadow: Color = Color(0x4DB0C0D9) +val colorGreyB5ACB9: Color = Color(0xFFB5ACB9) const val colorTextTertiaryHex = "#6B6B6B" const val colorTextPrimaryHex = "#191919" const val colorTextSecondaryHex = "#444444" @@ -49,3 +50,4 @@ const val transparentHex = "#00000000" const val ColorRedHex = "#FF0000" const val ColorTextSecondary = "#A8A8A8" const val TextGray = "#4B4968" +const val darkShadowColorHex = "#4DD1D9E6" diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/extensions/ComposeWidgetExt.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/extensions/ComposeWidgetExt.kt index e4fc1b2473..9aa7b4e804 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/extensions/ComposeWidgetExt.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/extensions/ComposeWidgetExt.kt @@ -723,9 +723,10 @@ fun getBrush(gradient: Gradient?): Brush { @Composable fun setWidgetLayoutParams( widgetLayoutParams: WidgetLayoutParams?, + modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { - var modifier = Modifier.fillMaxWidth().wrapContentHeight().setMargin(widgetLayoutParams) + var modifier = modifier.fillMaxWidth().wrapContentHeight().setMargin(widgetLayoutParams) if (widgetLayoutParams?.gradient.isNotNull()) { modifier = modifier.then(Modifier.background(brush = getBrush(widgetLayoutParams?.gradient))) diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeaderWithTrackerWidgetData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeaderWithTrackerWidgetData.kt index 1b38464be4..b778af40e8 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeaderWithTrackerWidgetData.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeaderWithTrackerWidgetData.kt @@ -57,6 +57,7 @@ data class HeaderWithTrackerWidgetBody( @SerializedName("leftTrackerInfo") val leftTrackerInfo: HeaderTrackerInfo? = null, @SerializedName("rightIcon") val rightIcon: ImageFieldData? = null, @SerializedName("rightTrackerInfo") val rightTrackerInfo: HeaderTrackerInfo? = null, + @SerializedName("backgroundColor") val backgroundColor: String? = null, ) data class HeaderTrackerInfo( diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeroSectionData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeroSectionData.kt new file mode 100644 index 0000000000..18d085c46c --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/HeroSectionData.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.models + +import com.navi.design.utils.BackgroundDrawableData +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData + +data class HeroSectionData( + val backgroundData: BackgroundDrawableData? = null, + val title: TextFieldData? = null, + val image: ImageFieldData? = null, +) diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/MemberSelectionWidgetData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/MemberSelectionWidgetData.kt new file mode 100644 index 0000000000..8ae93b44a4 --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/MemberSelectionWidgetData.kt @@ -0,0 +1,76 @@ +/* + * + * * 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.AnalyticsEvent +import com.navi.naviwidgets.models.response.CardProperties +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData + +data class MemberSelectionWidgetData( + @SerializedName("widgetData") val widgetData: MemberSelectionWidgetBody? = null +) : + GenericWidgetDataInfo( + widgetId = WIDGET_NAME, + widgetNameForBaseAdapter = WIDGET_NAME, + isDependentWidget = false, + dependencyWidgetId = null, + isDependencyWidgetShowing = false, + widgetError = null, + ) + +data class MemberSelectionWidgetBody( + val parentId: String? = null, + val headerData: HeroSectionData? = null, + val cardProperties: CardProperties? = null, + val title: TextFieldData? = null, + val trackerData: TrackerData? = null, + val genderData: GenderSelectionData? = null, + val selectionType: SelectionType = SelectionType.CHECKBOX, + val itemList: List? = null, +) { + data class GenderSelectionData( + val analyticsEventProperties: AnalyticsEvent? = null, + val title: TextFieldData? = null, + val items: List? = null, + ) { + data class SelectionItemData( + val id: String? = null, + val title: TextFieldData? = null, + val isSelected: Boolean = false, + ) + } + + data class MemberSelectionItemData( + val id: String? = null, + val assetId: String? = null, + val title: TextFieldData? = null, + val image: ImageFieldData? = null, + val complementaryTitle: TextFieldData? = null, + val complementaryImage: ImageFieldData? = null, + val dropdownType: DropdownType = DropdownType.NONE, + val countData: CounterDropDownData? = null, + val isSelected: Boolean = false, + val analyticsEventProperties: AnalyticsEvent? = null, + ) +} + +enum class SelectionType { + RADIO, + CHECKBOX, + NONE, +} + +enum class DropdownType { + GENDER, + COUNTER, + NONE, +} + +private const val WIDGET_NAME = "MEMBER_SELECTION_WIDGET" diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/NameDobTextWidgetData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/NameDobTextWidgetData.kt new file mode 100644 index 0000000000..4ec4c718d8 --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/NameDobTextWidgetData.kt @@ -0,0 +1,44 @@ +/* + * + * * Copyright © 2024-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.models.response.CardProperties +import com.navi.naviwidgets.models.response.TextFieldData + +data class NameDobTextWidgetData( + @SerializedName("widgetData") val widgetData: NameDobTextBody? = null +) : + GenericWidgetDataInfo( + widgetId = WIDGET_NAME, + widgetNameForBaseAdapter = WIDGET_NAME, + isDependentWidget = false, + dependencyWidgetId = null, + isDependencyWidgetShowing = false, + widgetError = null, + ) + +data class NameDobTextBody( + val parentId: String? = null, + val headerData: HeroSectionData? = null, + val cardProperties: CardProperties? = null, + val title: TextFieldData? = null, + val trackerData: TrackerData? = null, + val itemList: List? = null, +) { + data class NameDobItemData( + val id: String? = null, + val assetId: String? = null, + val title: TextFieldData? = null, + val inputData: TextFieldData? = null, + val cta: CtaData? = null, + ) +} + +private const val WIDGET_NAME = "NAME_DOB_TEXT_WIDGET" diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/PincodeInputWidgetDataV2.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/PincodeInputWidgetDataV2.kt new file mode 100644 index 0000000000..5062d0b792 --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/PincodeInputWidgetDataV2.kt @@ -0,0 +1,48 @@ +/* + * + * * 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.models.response.CardProperties +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.naviwidgets.models.response.TextFieldData + +data class PincodeInputWidgetDataV2( + @SerializedName("widgetData") val widgetData: PincodeInputWidgetBodyV2? = null +) : + GenericWidgetDataInfo( + widgetId = WIDGET_NAME, + widgetNameForBaseAdapter = WIDGET_NAME, + isDependentWidget = false, + dependencyWidgetId = null, + isDependencyWidgetShowing = false, + widgetError = null, + ) + +data class PincodeInputWidgetBodyV2( + val parentId: String? = null, + val headerData: HeroSectionData? = null, + val cardProperties: CardProperties? = null, + val title: TextFieldData? = null, + val subtitle: TextFieldData? = null, + val trackerData: TrackerData? = null, + val calloutData: CalloutData? = null, + val bottomContent: TextFieldData? = null, + val errorBottomContent: TextFieldData? = null, + var editTextData: EditTextData? = null, + val settingsCta: CtaData? = null, +) { + data class CalloutData( + val icon: ImageFieldData? = null, + val title: TextFieldData? = null, + val description: TextFieldData? = null, + ) +} + +private const val WIDGET_NAME = "PINCODE_INPUT_WIDGET_V2" 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 f69ec20086..219b8865cb 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 @@ -66,6 +66,7 @@ data class EditTextData( @SerializedName("value") var value: String? = null, @SerializedName("title") var title: TextFieldData? = null, @SerializedName("hint") var hint: TextFieldData? = null, + @SerializedName("trailingContent") var trailingContent: TextFieldData? = null, ) { fun updateName(value: String?) { this.value = value diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidget.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidget.kt index b2728a21b0..14df06147e 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidget.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidget.kt @@ -8,6 +8,7 @@ package com.navi.naviwidgets.models import com.google.gson.annotations.SerializedName +import com.navi.base.model.AnalyticsEvent import com.navi.base.model.CtaData import com.navi.naviwidgets.interfaces.GenericWidgetInfo import com.navi.naviwidgets.models.response.ImageFieldData @@ -75,6 +76,7 @@ data class BulletPointsData( data class SelectionItemData( @SerializedName("id") val id: String? = null, @SerializedName("assetId") val assetId: String? = null, + @SerializedName("assetImage") val assetImage: ImageFieldData? = null, @SerializedName("selectionItemTitle") val selectionItemTitle: TextFieldData? = null, @SerializedName("selectionItemDetails") val selectionItemDetails: TextFieldData? = null, @SerializedName("disableEditing") val disableEditing: Boolean = false, @@ -82,4 +84,5 @@ data class SelectionItemData( @SerializedName("selectionType") val selectionType: String? = null, @SerializedName("dropDownType") val dropDownType: String? = null, @SerializedName("pickerData") val pickerData: PickerData? = null, + @SerializedName("analyticsEventProperties") val analyticsEventProperties: AnalyticsEvent? = null, ) diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidgetV2.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidgetV2.kt new file mode 100644 index 0000000000..61a3e9488b --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TextWithAgeSelectorWidgetV2.kt @@ -0,0 +1,40 @@ +/* + * + * * 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.naviwidgets.models.response.CardProperties +import com.navi.naviwidgets.models.response.TextFieldData + +data class TextWithAgeSelectorWidgetV2( + @SerializedName("widgetData") val widgetData: TextWithAgeSelectorBody? = null +) : + GenericWidgetDataInfo( + widgetId = WIDGET_NAME, + widgetNameForBaseAdapter = WIDGET_NAME, + isDependentWidget = false, + dependencyWidgetId = null, + isDependencyWidgetShowing = false, + widgetError = null, + ) { + data class TextWithAgeSelectorBody( + val parentId: String? = null, + val headerData: HeroSectionData? = null, + val cardProperties: CardProperties? = null, + val title: TextFieldData? = null, + val trackerData: TrackerData? = null, + val itemList: List? = null, + ) +} + +enum class AgeSelectorStyle { + V1, + V2, +} + +private const val WIDGET_NAME = "TEXT_WITH_AGE_SELECTOR_WIDGET_V2" diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TrackerData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TrackerData.kt new file mode 100644 index 0000000000..97bbf8659f --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/TrackerData.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.models + +import com.navi.naviwidgets.models.response.ImageFieldData + +data class TrackerData(val items: List? = null) { + data class ItemData(val image: ImageFieldData? = null, val isCompleted: Boolean? = null) +}