NTP-58342 | Captcha Widget (#16219)

This commit is contained in:
Prajjaval Verma
2025-05-27 16:58:56 +05:30
committed by GitHub
parent ff45071fa1
commit 35a8ba483c
9 changed files with 322 additions and 6 deletions

View File

@@ -0,0 +1,153 @@
/*
*
* * Copyright © 2022-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.insurance.common.custom_view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.widget.doOnTextChanged
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.navi.base.model.CtaData
import com.navi.base.model.CtaType
import com.navi.base.utils.log
import com.navi.base.utils.orZero
import com.navi.insurance.R
import com.navi.insurance.common.models.CaptchaWidgetData
import com.navi.insurance.common.models.NaviWidgetData
import com.navi.insurance.common.util.Base64Helper
import com.navi.insurance.common.widgets.FormBaseWidgetView
import com.navi.insurance.databinding.CaptchaWidgetLayoutBinding
import com.navi.insurance.models.request.FormTypedKeyData
import com.navi.naviwidgets.extensions.setTextFieldData
import com.navi.naviwidgets.models.WidgetLayoutParams
import com.navi.naviwidgets.utils.setWidgetLayoutParams
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.WithFragmentBindings
import javax.inject.Inject
@WithFragmentBindings
@AndroidEntryPoint
class CaptchaWidgetLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FormBaseWidgetView(context, attrs) {
@Inject lateinit var base64Helper: Base64Helper
private var binding: CaptchaWidgetLayoutBinding? = null
private var ctaCallback: ((cta: CtaData) -> Unit)? = null
private var widgetData: CaptchaWidgetData? = null
private var captchaText: String = ""
override fun updateLayout(
binding: ViewDataBinding?,
naviWidgetData: NaviWidgetData,
callback: ((cta: CtaData) -> Unit)?,
fragment: Fragment?,
updateDataCallback: ((data: Any?) -> Unit)?,
) {
if (naviWidgetData is CaptchaWidgetData && binding is CaptchaWidgetLayoutBinding) {
this.binding = binding
this.widgetData = naviWidgetData
this.ctaCallback = callback
initViews(naviWidgetData, binding, callback)
}
}
private fun initViews(
widgetData: CaptchaWidgetData,
binding: CaptchaWidgetLayoutBinding,
callback: ((cta: CtaData) -> Unit)?,
) {
binding.captchaTitle.setTextFieldData(widgetData.title)
binding.refreshText.setTextFieldData(widgetData.refreshText)
widgetData.captchaImage?.let { base64Image ->
try {
base64Helper.decodeBase64ToBitmap(base64Image, binding.captchaImage)
} catch (e: Exception) {
e.log()
}
}
binding.refreshText.setOnClickListener {
widgetData.refreshCta?.let { cta -> callback?.invoke(cta) }
}
if (widgetData.inputField?.captchaIncorrect == true) {
binding.errorText.apply {
setText(widgetData.inputField.incorrectErrorText)
visibility = VISIBLE
}
binding.captchaInput.apply {
setText(widgetData.inputField.incorrectInput)
captchaText = widgetData.inputField.incorrectInput ?: ""
setBackground(
AppCompatResources.getDrawable(context, R.drawable.rounded_red_border_rect)
)
}
}
widgetData.inputField?.let { inputField ->
binding.captchaInput.apply {
hint = inputField.hint
inputField.inputLength?.let { maxLength ->
filters = arrayOf(android.text.InputFilter.LengthFilter(maxLength))
}
doOnTextChanged { text, _, _, _ ->
val input = text?.toString()?.trim() ?: ""
if (input.matches(Regex(inputField.validationRegex ?: "[0-9]+"))) {
captchaText = input
binding.errorText.visibility = GONE
setBackground(
AppCompatResources.getDrawable(
context,
R.drawable.rounded_grey_border_rect,
)
)
} else {
binding.errorText.apply {
setText(inputField.validationErrorText)
visibility = VISIBLE
}
setBackground(
AppCompatResources.getDrawable(
context,
R.drawable.rounded_red_border_rect,
)
)
}
callback?.invoke(CtaData(type = CtaType.VALIDATE_FORM_PAGE.value))
}
inputType = android.text.InputType.TYPE_CLASS_NUMBER
}
}
}
override fun getWidgetData(): List<FormTypedKeyData>? {
return widgetData?.inputField?.widgetKey?.let { key ->
listOf(FormTypedKeyData(attributes = mapOf(key to captchaText)))
}
}
override fun isValidWidget(): Boolean {
return captchaText.isNotEmpty() &&
(captchaText.length == widgetData?.inputField?.inputLength.orZero())
}
fun setLayoutParams(widgetLayoutParams: WidgetLayoutParams) {
binding?.root?.let { setWidgetLayoutParams(widgetLayoutParams, it) }
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
binding = null
ctaCallback = null
widgetData = null
}
}

View File

@@ -152,6 +152,7 @@ class NaviWidgetDeserializer : JsonDeserializer<NaviWidget> {
NaviWidgetType.LOTTIE_WIDGET.value -> true
NaviWidgetType.FOOTER_WITH_SLIDE_UP_ANIMATION.value -> true
NaviWidgetType.FOOTER_WITH_TITLE_SUBTITLE.value -> true
NaviWidgetType.CAPTCHA_WIDGET.value -> true
else -> false
}
}
@@ -287,6 +288,7 @@ class NaviWidgetDeserializer : JsonDeserializer<NaviWidget> {
NaviWidgetType.FOOTER_WITH_SLIDE_UP_ANIMATION ->
FooterWithSlideUpAnimation::class.java
NaviWidgetType.FOOTER_WITH_TITLE_SUBTITLE -> FooterWithTitleSubtileData::class.java
NaviWidgetType.CAPTCHA_WIDGET -> CaptchaWidgetData::class.java
},
)
}

View File

@@ -81,6 +81,7 @@ class FormViewHolderRegistry {
EXPANDABLE_QUESTIONS_WIDGET,
CHECKBOX_WITH_GRID_OPTIONS_WIDGET,
LOTTIE_WIDGET,
CAPTCHA_WIDGET,
}
fun setLottieHelper(lottieRemoteHelper: LottieRemoteHelper?) {
@@ -1431,6 +1432,32 @@ class FormViewHolderRegistry {
)
}
}
FormWidgetType.CAPTCHA_WIDGET.name -> {
if (widget.widgetData is CaptchaWidgetData) {
viewGroup.addView(
CaptchaWidgetLayout(context).apply {
updateLayout(
binding =
DataBindingUtil.inflate<CaptchaWidgetLayoutBinding>(
LayoutInflater.from(context),
R.layout.captcha_widget_layout,
this,
true,
),
callback = callback,
fragment = fragment,
naviWidgetData = widget.widgetData,
updateDataCallback = updateDataCallback,
)
widget.widgetLayoutParams?.let { setLayoutParams(it) }
if (widget.maskSensitiveData == true) {
tag = SENSITIVE_VIEW
}
},
index,
)
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
/*
*
* * Copyright © 2022-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.insurance.common.models
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.navi.base.model.CtaData
import com.navi.naviwidgets.models.response.TextFieldData
import kotlinx.parcelize.Parcelize
data class CaptchaWidgetData(
val title: TextFieldData? = null,
val captchaImage: String? = null,
val refreshText: TextFieldData? = null,
val refreshCta: CtaData? = null,
val inputField: TextInputField? = null,
) : NaviWidgetData() {
override var widgetNameForBaseAdapter: String? = NaviWidgetType.CAPTCHA_WIDGET.name
}
@Parcelize
data class TextInputField(
@SerializedName("inputLength") val inputLength: Int? = null,
@SerializedName("title") val title: TextFieldData? = null,
@SerializedName("hint") val hint: String? = null,
@SerializedName("validationRegex") val validationRegex: String? = null,
@SerializedName("validationErrorText") val validationErrorText: String? = null,
@SerializedName("incorrectErrorText") val incorrectErrorText: String? = null,
@SerializedName("incorrectInput") val incorrectInput: String? = null,
@SerializedName("captchaIncorrect") val captchaIncorrect: Boolean? = null,
@SerializedName("widgetKey") val widgetKey: String? = null,
@SerializedName("disabled") val disabled: Boolean? = null,
) : Parcelable

View File

@@ -96,10 +96,11 @@ enum class NaviWidgetType(val value: String) {
SECTION_ITEMS_WITH_SLIDE_SELECTOR_WIDGET("SECTION_ITEMS_WITH_SLIDE_SELECTOR_WIDGET"),
SECTION_WITH_ITEMS_WIDGET_V2("SECTION_WITH_ITEMS_WIDGET_V2"),
CHECKBOX_WITH_GRID_OPTIONS_WIDGET("CHECKBOX_WITH_GRID_OPTIONS_WIDGET"),
LOTTIE_WIDGET("LOTTIE_WIDGET"),
CAPTCHA_WIDGET("CAPTCHA_WIDGET"),
FEEDBACK_FORM_WIDGET("FEEDBACK_FORM_WIDGET"),
EXPANDABLE_QUESTIONS_WIDGET("EXPANDABLE_QUESTIONS_WIDGET"),
TAB_LAYOUT_WITH_NUMBER_PICKER_WIDGET("TAB_LAYOUT_WITH_NUMBER_PICKER_WIDGET"),
LOTTIE_WIDGET("LOTTIE_WIDGET"),
FOOTER_WITH_SLIDE_UP_ANIMATION("FOOTER_WITH_SLIDE_UP_ANIMATION"),
FOOTER_WITH_TITLE_SUBTITLE("FOOTER_WITH_TITLE_SUBTITLE"),
}

View File

@@ -481,6 +481,16 @@ class PostPurchaseFormBasedFragment : GiBaseFragment(), ActionHandler.ActionOwne
ctaTitle = btnCta?.title,
)
}
CtaType.RELOAD_SCREEN.value -> {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.fetchFormPageResponse(
referenceId ?: viewModel.preKycPage,
journeyKey,
requestPageType,
applicationId,
)
}
}
else -> {
val params = cta.parameters?.toMutableList()
if (params?.contains(LineItem(key = ASSET_DETAILS_EXTRA)) == false) {

View File

@@ -99,7 +99,7 @@ constructor(
private val GI_REQUEST_CODE = 113
private val keyQuoteId = "quoteId"
private val keyPageType = "pageType"
private val preKycPage = "pre_kyc_page"
val preKycPage = "pre_kyc_page"
suspend fun fetchFormPageResponse(
referenceId: String?,

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/captcha_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/primaryColor"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Enter captcha" />
<TextView
android:id="@+id/refresh_text"
style="@style/TextV8Style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refresh"
android:drawableEnd="@drawable/refresh"
android:drawablePadding="2dp"
android:drawableTint="@color/ctaPurplePrimaryColor"
android:background="@android:color/transparent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/captcha_title" />
<EditText
android:id="@+id/captcha_input"
style="@style/TextInputV2Style"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/rounded_grey_border_rect"
android:gravity="center_vertical"
android:inputType="text"
android:maxLines="1"
android:paddingHorizontal="@dimen/dp_16"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/captcha_image"
app:layout_constraintTop_toBottomOf="@id/captcha_title"
tools:hint="Enter captcha here" />
<ImageView
android:id="@+id/captcha_image"
android:layout_width="144dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="@drawable/rounded_grey_border_rect"
android:padding="4dp"
android:scaleType="fitXY"
app:layout_constraintStart_toEndOf="@id/captcha_input"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/captcha_input" />
<com.navi.design.textview.NaviTextView
android:id="@+id/error_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawableStart="@drawable/ic_alert_error_red"
android:drawablePadding="@dimen/dp_5"
android:drawableTint="@color/color_EF0000"
android:fontFamily="@font/navi_body_regular"
android:gravity="center_vertical"
android:textColor="@color/color_EF0000"
android:textSize="@dimen/sp_12"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/captcha_input"
tools:text="Invalid captcha"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -418,7 +418,8 @@ fun TextView.setTextFieldData(
null
}
if (!textDrawableData.left?.url.isNullOrEmpty() && leftDrawable == null) {
textDrawableData.left?.url?.let {
drawablePadding = textDrawableData.left.drawablePadding
textDrawableData.left.url?.let {
loadDrawableFromUrl(
context,
it,
@@ -434,7 +435,8 @@ fun TextView.setTextFieldData(
}
}
} else if (!textDrawableData.right?.url.isNullOrEmpty() && rightDrawable == null) {
textDrawableData.right?.url?.let {
drawablePadding = textDrawableData.right.drawablePadding
textDrawableData.right.url?.let {
loadDrawableFromUrl(
context,
it,
@@ -450,7 +452,8 @@ fun TextView.setTextFieldData(
}
}
} else if (!textDrawableData.top?.url.isNullOrEmpty() && topDrawable == null) {
textDrawableData.top?.url?.let {
drawablePadding = textDrawableData.top.drawablePadding
textDrawableData.top.url?.let {
loadDrawableFromUrl(
context,
it,
@@ -466,7 +469,8 @@ fun TextView.setTextFieldData(
}
}
} else if (!textDrawableData.bottom?.url.isNullOrEmpty() && bottomDrawable == null) {
textDrawableData.bottom?.url?.let {
drawablePadding = textDrawableData.bottom.drawablePadding
textDrawableData.bottom.url?.let {
loadDrawableFromUrl(
context,
it,