TP-62796 | Payment migration (#10380)

Co-authored-by: Prajjaval Verma <prajjaval.verma@navi.com>
This commit is contained in:
Sangaraboina Rishvik Vardhan
2024-04-15 13:21:09 +05:30
committed by GitHub
parent 9885c977be
commit eb2ddb355c
18 changed files with 670 additions and 78 deletions

View File

@@ -266,6 +266,12 @@
android:exported="false"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".paymentreview.autopayoption.ui.PaymentStatusActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/GiAppThemeBlue" />
<activity
android:name=".notifications.DeepLinkActivity"
android:exported="true"

View File

@@ -391,5 +391,6 @@ class NavigationHandler @Inject constructor(private val uiControllerUtil: UiCont
const val HEADER_WITH_ITEMS_AND_FOOTER_BOTTOM_SHEET = "header_with_items_and_footer_bottomsheet"
const val CHECKBOX_WITH_DROPDOWN_BOTTOMSHEET = "checkbox_with_dropdown_bottomsheet"
const val TRIAL_INFO_BOTTOM_SHEET = "trial_info_bottom_sheet"
const val URL_PAYMENT_STATUS_SCREEN = "payment_status_screen"
}
}

View File

@@ -147,6 +147,10 @@ constructor(context: Context, attrs: AttributeSet? = null) : BaseNaviWidgetView(
}
}
fun showLoader(boolean: Boolean){
binding?.loadingButton?.isVisible = boolean
}
override fun enableLayout(isEnabled: Boolean, showLoader: Boolean) {
if (isEnabled) {
binding?.button?.enableView()

View File

@@ -16,6 +16,7 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.activity.viewModels
import androidx.core.os.bundleOf
import androidx.databinding.DataBindingUtil
import com.navi.base.model.CtaData
import com.navi.base.model.CtaType
@@ -31,15 +32,18 @@ import com.navi.insurance.databinding.ActivityInsuranceContainerBinding
import com.navi.insurance.health.viewmodel.InsuranceContainerActivityVM
import com.navi.insurance.navigator.NaviInsuranceDeeplinkNavigator
import com.navi.insurance.payment.PaymentStatusBottomSheet
import com.navi.insurance.paymentreview.autopayoption.ui.PaymentReviewFragment
import com.navi.insurance.paymentreview.autopayoption.ui.PaymentStatusListener
import com.navi.insurance.util.AMOUNT
import com.navi.insurance.util.Constants
import com.navi.naviwidgets.utils.PAN_VERIFICATION_REQUEST_CODE
import com.navi.payment.utils.Constants.PAYMENT_METHOD_REQUEST_CODE
import dagger.hilt.android.AndroidEntryPoint
import com.navi.insurance.R as NaviR
@AndroidEntryPoint
class InsuranceContainerActivity : GiBaseActivity(), PaymentStatusBottomSheetListener {
class InsuranceContainerActivity : GiBaseActivity(), PaymentStatusBottomSheetListener, PaymentStatusListener {
private lateinit var binding: ActivityInsuranceContainerBinding
private val viewModel by viewModels<InsuranceContainerActivityVM>()
@@ -64,11 +68,15 @@ class InsuranceContainerActivity : GiBaseActivity(), PaymentStatusBottomSheetLis
)
return
}
if (requestCode == PAYMENT_METHOD_REQUEST_CODE) {
val fragment = supportFragmentManager.findFragmentByTag(PaymentReviewFragment.TAG)
fragment?.onActivityResult(requestCode, resultCode, data)
}
if (requestCode == Constants.PAYMENT_ACTIVITY_REQUEST_CODE) {
val paymentCta = viewModel.paymentRetryCta.value
val amountToPay =
paymentCta?.parameters?.find { item -> item.key == AMOUNT }?.value?.toDoubleWithSafe()
(paymentCta?.parameters?.find { item -> item.key == AMOUNT }?.value?:data?.getStringExtra(AMOUNT))?.toDoubleWithSafe()
if (resultCode == RESULT_OK) {
// Payment success, close activity
finish()
@@ -185,4 +193,44 @@ class InsuranceContainerActivity : GiBaseActivity(), PaymentStatusBottomSheetLis
)
}
}
override fun setPaymentResult(resultCode: Int, isCancelledByUser: Boolean, ctaData: CtaData?, bundle: Bundle?) {
ctaData?.let { cta ->
cta.analyticsEventProperties?.let { analyticsEvent ->
NaviInsuranceAnalytics.postAnalyticsEvent(analyticsEvent.name.orEmpty(), analyticsEvent.properties)
}
NaviInsuranceDeeplinkNavigator.navigate(
activity = this,
ctaData = cta,
finish = cta.finish.orFalse(),
clearTask = cta.clearTask.orFalse()
)
return
}
val paymentCta = viewModel.paymentRetryCta.value
val amountToPay =
(paymentCta?.parameters?.find { item -> item.key == AMOUNT }?.value?:bundle?.getString(AMOUNT))?.toDoubleWithSafe()
if (resultCode == RESULT_OK) {
// Payment success, close activity
finish()
} else {
// Payment failed, show retry dialog
viewModel.paymentFallbackCta?.let {
NaviInsuranceDeeplinkNavigator.navigate(
this,
it,
finish = it.finish.orFalse(),
clearTask = it.clearTask.orFalse()
)
} ?: run {
PaymentStatusBottomSheet.getInstance(
this,
amountToPay,
isCancelledByUser
)
.show(supportFragmentManager, PaymentStatusBottomSheet.TAG)
}
}
}
}

View File

@@ -12,6 +12,7 @@ import com.navi.common.network.retrofit.ResponseCallback
import com.navi.insurance.models.AutoPayInitMandateResponse
import com.navi.insurance.models.PaymentOrderDetail
import com.navi.insurance.models.PaymentStateResponse
import com.navi.insurance.models.TurboPaymentOrderDetail
import com.navi.insurance.models.request.AutoPayHashRequest
import com.navi.insurance.models.request.InitiateInstallmentPaymentRequest
import com.navi.insurance.models.request.InitiatePaymentRequest
@@ -54,6 +55,26 @@ class PaymentRepository @Inject constructor(private val retrofitService: Retrofi
)
}
suspend fun initiatePaymentForInstallmentV2(
policyId: String, request: InitiateInstallmentPaymentRequest
): RepoResult<TurboPaymentOrderDetail> {
return apiResponseCallback(
retrofitService.initiatePaymentForInstallmentV2(policyId, request)
)
}
suspend fun getPaymentStatusForCustomerV2(
orderReferenceId: String,
clientStatus: String?,
pgClientStatus: String? = null,
flowIdentifier: String?,
policyId: String?
) = apiResponseCallback(
retrofitService.getPaymentStatusForCustomerV2(
orderReferenceId, clientStatus, pgClientStatus, flowIdentifier, policyId
)
)
suspend fun generateHash(request: PaymentHashRequest) =
apiResponseCallback<PaymentHashResponse>(retrofitService.generateHash(request))

View File

@@ -6,9 +6,11 @@
package com.navi.insurance.models
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.navi.base.model.AnalyticsEvent
import com.navi.base.model.CtaData
import kotlinx.parcelize.Parcelize
data class PaymentOrderDetail(
@SerializedName("errorStateData") val errorStateData: ErrorStateData? = null,
@@ -20,7 +22,7 @@ data class PaymentOrderDetail(
@SerializedName("requestId") val requestId: String? = null,
@SerializedName("quoteMetadata") val quoteMetaData: QuoteMetaData? = null,
@SerializedName("paymentStatusAnalyticsEvents")
var paymentStatusAnalyticsEvents: PaymentStatusAnalyticsEvents? = null
var paymentStatusAnalyticsEvents: PaymentStatusAnalyticsEvents? = null,
) {
data class ErrorStateData(
@SerializedName("errorMessage") val errorMessage: String? = null,
@@ -28,6 +30,23 @@ data class PaymentOrderDetail(
)
}
@Parcelize
data class TurboPaymentOrderDetail(
@SerializedName("requestId") val requestId: String? = null,
@SerializedName("orderReferenceId") val orderReferenceId: String? = null,
@SerializedName("paymentSdkToken") val paymentSdkToken: String? = null,
@SerializedName("pollingConfig") val pollingConfig: PollingConfig,
@SerializedName("nextPageCta") val nextPageCta: CtaData? = null,
@SerializedName("apiFailureCta") val apiFailureCta: CtaData? = null
) : Parcelable {
@Parcelize
data class PollingConfig(
@SerializedName("initialDelay") val initialDelay: Long? = null,
@SerializedName("pollingInterval") val pollingInterval: Long? = null,
@SerializedName("maxNumberOfRetries") val maxNumberOfRetries: Int? = null
) : Parcelable
}
data class BillingDetails(
val billingAmount: Double? = null,
val currency: String? = null,

View File

@@ -0,0 +1,18 @@
package com.navi.insurance.models.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.navi.naviwidgets.models.LottieFieldData
import com.navi.naviwidgets.models.response.TextFieldData
import kotlinx.parcelize.Parcelize
@Parcelize
data class PaymentStatusScreenResponse(
@SerializedName("lottieFieldData")
val lottieFieldData: LottieFieldData? = null,
@SerializedName("subTitle")
val subTitle: TextFieldData? = null,
@SerializedName("title")
val title: TextFieldData? = null
) : Parcelable

View File

@@ -119,6 +119,7 @@ import com.navi.insurance.health.fragment.UpdatedPremiumBottomSheet
import com.navi.insurance.notifications.Screen
import com.navi.insurance.payment.PaymentStatusBottomSheet
import com.navi.insurance.payment.activity.PaymentReviewActivity
import com.navi.insurance.paymentreview.autopayoption.ui.PaymentStatusActivity
import com.navi.insurance.quoteredesign.QuoteActivity
import com.navi.insurance.quoteredesign.fragments.QuotePremiumDetailsBottomSheet
import com.navi.insurance.renewal.RenewalActivity
@@ -476,6 +477,9 @@ object NaviInsuranceDeeplinkNavigator {
intent = Intent(activity, PaymentActivity::class.java)
bundle.putString(EXTRA_FROM, TAG)
}
NavigationHandler.URL_PAYMENT_STATUS_SCREEN -> {
intent = Intent(activity, PaymentStatusActivity::class.java)
}
NavigationHandler.URL_PAYMENT_REVIEW_ACTIVITY -> {
intent = Intent(activity, PaymentReviewActivity::class.java)
}

View File

@@ -111,6 +111,12 @@ interface RetrofitService {
@Body data: InitiateInstallmentPaymentRequest
): Response<GenericResponse<PaymentOrderDetail>>
@POST("/v2/payment/{policyId}/initiate")
suspend fun initiatePaymentForInstallmentV2(
@Path("policyId") policyId: String,
@Body data: InitiateInstallmentPaymentRequest
): Response<GenericResponse<TurboPaymentOrderDetail>>
@GET("/v1/payment/status")
suspend fun getPaymentStatusV1(
@Query("paymentReferenceId") paymentReferenceId: String,
@@ -120,6 +126,15 @@ interface RetrofitService {
@Query("flowIdentifier") flowIdentifier: String?
): Response<GenericResponse<PaymentStateResponse>>
@GET("/v2/payment/{orderReferenceId}/status")
suspend fun getPaymentStatusForCustomerV2(
@Path("orderReferenceId") orderReferenceId: String,
@Query("clientStatus") clientStatus: String?,
@Query("pgClientStatus") pgClientStatus: String? = null,
@Query("flowIdentifier") flowIdentifier: String?,
@Query("policyId") policyId: String?,
): Response<GenericResponse<PaymentStateResponse>>
@GET("/payment/{policyId}/status")
suspend fun getPaymentStatusForInstallment(
@Path("policyId") policyId: String,

View File

@@ -1,16 +1,29 @@
package com.navi.insurance.paymentreview.autopayoption.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.gson.reflect.TypeToken
import com.navi.base.model.*
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.model.AnalyticsEvent
import com.navi.base.model.CtaData
import com.navi.base.model.CtaType
import com.navi.base.model.NaviClickAction
import com.navi.base.utils.isNull
import com.navi.base.utils.orFalse
import com.navi.common.ResponseState
@@ -19,36 +32,51 @@ import com.navi.common.network.models.ErrorMessage
import com.navi.design.utils.isValidHexColor
import com.navi.insurance.R
import com.navi.insurance.analytics.InsuranceAnalyticsConstants
import com.navi.insurance.analytics.NaviInsuranceAnalytics
import com.navi.insurance.common.GiBaseFragment
import com.navi.insurance.common.GiBaseVM
import com.navi.insurance.common.models.NaviWidgetData
import com.navi.insurance.common.util.IdProvider
import com.navi.insurance.common.widgets.BaseNaviWidgetView
import com.navi.insurance.common.widgets.FooterWithTitleAndSubtitleLayout
import com.navi.insurance.common.widgets.replaceLayout
import com.navi.insurance.databinding.LayoutPaymentReviewFragmentBinding
import com.navi.insurance.health.activity.BaseActivity
import com.navi.insurance.health.activity.InsuranceContainerActivity
import com.navi.insurance.health.viewmodel.InsuranceContainerActivityVM
import com.navi.insurance.models.request.InitiateInstallmentPaymentRequest
import com.navi.insurance.navigator.NaviInsuranceDeeplinkNavigator
import com.navi.insurance.network.ApiErrorTagType
import com.navi.insurance.paymentreview.autopayoption.viewmodel.PaymentInitState
import com.navi.insurance.paymentreview.autopayoption.viewmodel.PaymentReviewVM
import com.navi.insurance.util.*
import com.navi.naviwidgets.adapters.NaviAdapter
import com.navi.naviwidgets.callbacks.WidgetCallback
import com.navi.naviwidgets.extensions.FloatingButtonOverlay
import com.navi.naviwidgets.extensions.getJsonObject
import com.navi.naviwidgets.extensions.setTextFieldData
import com.navi.naviwidgets.models.GenericWidgetDataInfo
import com.navi.naviwidgets.models.WidgetChangedData
import com.navi.naviwidgets.models.response.TextFieldData
import com.navi.naviwidgets.utils.UPDATE_WIDGET_DATA
import com.navi.naviwidgets.viewholder.ViewHolderFactoryImpl
import com.navi.naviwidgets.views.NaviErrorPageView
import com.navi.payment.listener.PaymentResultListener
import com.navi.payment.model.common.GenericErrorResponse
import com.navi.payment.model.common.PaymentSdkInitParams
import com.navi.payment.model.common.PaymentSdkTypes
import com.navi.payment.model.initiatesdk.PaymentPrefetchMethodRequest
import com.navi.payment.paymenthandler.model.ClientScreenOwner
import com.navi.payment.paymentscreen.utils.PaymentNavigator
import com.navi.payment.utils.PaymentSource
import com.navi.paymentclients.viewmodel.base.PaymentManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.util.Locale
import javax.inject.Inject
import kotlin.collections.set
import com.navi.payment.utils.Constants as PaymentConstants
@AndroidEntryPoint
class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
@@ -62,34 +90,70 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
private var applicationType: String? = null
private var eventName: String? = null
private var parentActivity: InsuranceContainerActivity? = null
private val paymentManager: PaymentManager by lazy { ViewModelProvider(this)[PaymentManager::class.java] }
private var amount = "0"
@Inject
lateinit var idProvider: IdProvider
@Inject
lateinit var paymentNavigator: PaymentNavigator
private var paymentStatusListener: PaymentStatusListener? = null
val paymentsResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val status = data?.extras?.getString(Constants.STATUS.uppercase(Locale.getDefault()))
when (status) {
PaymentSdkTypes.DISMISS_LOADER.name -> {}
PaymentSdkTypes.TRANSACTION_SUCCESS.name, PaymentSdkTypes.TRANSACTION_FAILURE.name, PaymentSdkTypes.TRANSACTION_PENDING.name -> {
activity?.let { activity ->
DeepLinkManager.getDeepLinkListener()?.navigateTo(
activity,
(viewModel.paymentInitState.value as? PaymentInitState.Success)?.data?.nextPageCta ?: CtaData(),
false,
bundleOf(
Pair(PAYMENT_FLOW_IDENTIFIER, arguments?.getString(PAYMENT_FLOW_IDENTIFIER)),
Pair(PAYMENT_ORDER_DETAIL, (viewModel.paymentInitState.value as? PaymentInitState.Success)?.data),
Pair(Constants.POLICY_ID, id),
Pair(AMOUNT, amount)
),
requestCode = BaseActivity.GI_REQUEST_CODE,
clearTask = false
)
}
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
paymentStatusListener = context as? PaymentStatusListener
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
binding =
DataBindingUtil.inflate(
inflater,
R.layout.layout_payment_review_fragment,
container,
false
)
rvAdapter =
NaviAdapter(
widgetCallback = this,
factory = ViewHolderFactoryImpl(),
list = listOf(),
isRecyclable = true
)
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_payment_review_fragment, container, false
)
rvAdapter = NaviAdapter(
widgetCallback = this,
factory = ViewHolderFactoryImpl(),
list = listOf(),
isRecyclable = true
)
id = arguments?.getString(ID)
applicationType = arguments?.getString(APPLICATION_TYPE_EXTRA)
?: run { activity?.intent?.getStringExtra(APPLICATION_TYPE_EXTRA) }
eventName = arguments?.getString(EVENT_NAME_EXTRA)
?: run { activity?.intent?.getStringExtra(EVENT_NAME_EXTRA) }
applicationType = arguments?.getString(APPLICATION_TYPE_EXTRA) ?: run {
activity?.intent?.getStringExtra(APPLICATION_TYPE_EXTRA)
}
eventName = arguments?.getString(EVENT_NAME_EXTRA) ?: run {
activity?.intent?.getStringExtra(EVENT_NAME_EXTRA)
}
sendInitScreenEvent()
paymentFlowIdentifier = arguments?.getString(PAYMENT_FLOW_IDENTIFIER)
parentActivity = activity as? InsuranceContainerActivity
@@ -109,6 +173,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
super.onViewCreated(view, savedInstanceState)
initErrorView()
observeResponse()
addObservers()
}
private fun initErrorView() {
@@ -158,8 +223,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
}
paymentReviewResponse.header?.getOrNull(0)?.widgetData?.let {
updateContainer(
it,
binding.headerContainer
it, binding.headerContainer
)
}
if (paymentReviewResponse.content?.isEmpty() == false) {
@@ -168,8 +232,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
paymentReviewResponse.footer?.getOrNull(0)?.widgetData?.let {
footerData = it
updateContainer(
it,
binding.footerContainer
it, binding.footerContainer
)
}
paymentReviewResponse.floatingButtonData?.let { fbButtonData ->
@@ -197,9 +260,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
}
private fun updateContainer(
naviWidgetData: NaviWidgetData,
container: ViewGroup,
clickableData: CtaData? = null
naviWidgetData: NaviWidgetData, container: ViewGroup, clickableData: CtaData? = null
) {
viewLifecycleOwner.lifecycleScope.launch {
val binding = container.replaceLayout(idProvider.getNaviWidgetLayoutId(naviWidgetData))
@@ -216,34 +277,45 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
CtaType.HELP_BOTTOM_SHEET.value -> {
openHelpCenter()
}
CtaType.OPTION_SELECTED.value -> {
handleFallBackCta(it)
NaviInsuranceDeeplinkNavigator.navigate(
activity,
ctaData = it,
finish = false,
needsResult = true,
requestCode = Constants.PAYMENT_ACTIVITY_REQUEST_CODE,
callbackHandler = object : RequestToCallbackHandler {
override fun onCallbackRaised() {
viewModel.fetchPaymentReview(id, paymentFlowIdentifier)
if (it.parameters?.firstOrNull { params -> params.key == Constants.IS_TURBO_CHECKOUT }?.value.toBoolean()
&& it.parameters?.firstOrNull {params -> params.key == Constants.IS_AUTOPAY }?.value.toBoolean().not()) {
val policyId = getParameterFromCta(it, Constants.POLICY_ID)
val request = InitiateInstallmentPaymentRequest(
installmentNumber = getParameterFromCta(it, Constants.INSTALMENT_NUMBER)?.toInt(),
paymentType = getParameterFromCta(it, Constants.PAYMENT_TYPE),
installmentDate = getParameterFromCta(it, Constants.INSTALMENT_DATE),
clientSuggestedPaymentProvider = null,
)
viewModel.initiatePayment(request, policyId.orEmpty())
}
else{
NaviInsuranceDeeplinkNavigator.navigate(
activity,
ctaData = it,
finish = false,
needsResult = true,
requestCode = Constants.PAYMENT_ACTIVITY_REQUEST_CODE,
callbackHandler = object : RequestToCallbackHandler {
override fun onCallbackRaised() {
viewModel.fetchPaymentReview(id, paymentFlowIdentifier)
}
}
}
)
)
}
}
else ->
NaviInsuranceDeeplinkNavigator.navigate(
activity = activity,
ctaData = it,
bundle = Bundle(),
finish = it.finish.orFalse(),
clearTask = it.clearTask.orFalse(),
callbackHandler = object : RequestToCallbackHandler {
override fun onCallbackRaised() {
viewModel.fetchPaymentReview(id, paymentFlowIdentifier)
}
else -> NaviInsuranceDeeplinkNavigator.navigate(activity = activity,
ctaData = it,
bundle = Bundle(),
finish = it.finish.orFalse(),
clearTask = it.clearTask.orFalse(),
callbackHandler = object : RequestToCallbackHandler {
override fun onCallbackRaised() {
viewModel.fetchPaymentReview(id, paymentFlowIdentifier)
}
)
})
}
}
(binding?.root as? BaseNaviWidgetView)?.setClickData(clickableData)
@@ -255,8 +327,7 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
if (keyValue.key == Constants.PAYMENT_FALLBACK_CTA) {
val dataType = object : TypeToken<CtaData>() {}.type
val fallBackCta = getJsonObject<CtaData>(
dataType,
keyValue.value
dataType, keyValue.value
)
activityViewModel.paymentFallbackCta = fallBackCta
}
@@ -281,6 +352,9 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
if (it.key == Constants.UPDATED_WIDGET_DATA) {
data = it.value.toString()
}
if(it.key == AMOUNT){
amount = it.value.toString()
}
}
position?.let { itemPosition ->
if (!binding.landingPageRv.isComputingLayout) {
@@ -292,10 +366,8 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
// Retry the update later when the RecyclerView is in a stable state
binding.landingPageRv.post {
rvAdapter?.notifyItemChanged(
itemPosition,
WidgetChangedData(
widgetAction = UPDATE_WIDGET_DATA,
payload = data
itemPosition, WidgetChangedData(
widgetAction = UPDATE_WIDGET_DATA, payload = data
)
)
}
@@ -303,15 +375,13 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
}
footerData?.let {
updateContainer(
it,
binding.footerContainer,
naviClickAction as? CtaData
it, binding.footerContainer, naviClickAction as? CtaData
)
}
}
CtaType.OPEN_PAYMENT.value -> {
NaviInsuranceDeeplinkNavigator.navigate(
activity,
NaviInsuranceDeeplinkNavigator.navigate(activity,
naviClickAction,
finish = naviClickAction.finish.orFalse(),
clearTask = naviClickAction.clearTask.orFalse(),
@@ -321,25 +391,114 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
override fun onCallbackRaised() {
viewModel.fetchPaymentReview(id, paymentFlowIdentifier)
}
}
)
})
}
else -> {
NaviInsuranceDeeplinkNavigator.navigate(
activity = activity,
NaviInsuranceDeeplinkNavigator.navigate(activity = activity,
ctaData = naviClickAction,
bundle = bundle,
callbackHandler = object : RequestToCallbackHandler {
override fun onCallbackRaised() {
viewModel.fetchPaymentReview(id, paymentFlowIdentifier)
}
}
)
})
}
}
}
}
private fun addObservers() {
lifecycleScope.launch {
viewModel.paymentInitState.collect {
binding.root.isClickable = it !is PaymentInitState.Loading
when (it) {
is PaymentInitState.Loading -> {
viewModel.loadingScreen.value = true
}
is PaymentInitState.Error -> {
viewModel.loadingScreen.value = false
}
is PaymentInitState.Success -> {
initPaymentSDK(
it.data.requestId.orEmpty(), it.data.paymentSdkToken.orEmpty(), it.data.nextPageCta
)
}
else -> {}
}
}
}
viewModel.loadingScreen.observeNonNull(this) {
if (it) {
binding.transparentLayout.visibility = View.VISIBLE
(binding.footerContainer.children.first() as? FooterWithTitleAndSubtitleLayout)?.showLoader(
true
)
} else {
binding.transparentLayout.visibility = View.GONE
(binding.footerContainer.children.first() as? FooterWithTitleAndSubtitleLayout)?.showLoader(
false
)
}
}
viewModel.errorResponse.observeNonNull(this) {
paymentStatusListener?.setPaymentResult(Activity.RESULT_CANCELED, false, bundle = bundleOf(Pair(AMOUNT, amount)))
}
}
private fun initPaymentSDK(requestId: String, token: String, nextPageCta: CtaData?) {
val paymentSdkInitParams = PaymentSdkInitParams(
token = token,
requestId = requestId,
screenType = PaymentConstants.MINI_PAYMENT_SCREEN,
paymentPreFetchMethodRequest = PaymentPrefetchMethodRequest(
previousScreenName = "home"
),
paymentSource = PaymentSource.HI.name
)
paymentManager.initPaymentsSdk(
paymentSdkInitParams = paymentSdkInitParams,
paymentResultListener = object : PaymentResultListener {
override fun onResultLoading() {
}
override fun onResultSuccess(payload: JSONObject?) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
viewModel.loadingScreen.value = false
}
paymentNavigator.launchOrNavigateFromPaymentSdk(
paymentSdkInitParams,
payload,
object : ClientScreenOwner {
override fun getActivity() = activity
override fun getResultLauncher(): ActivityResultLauncher<Intent> {
return paymentsResultLauncher
}
override fun getFragManager() = childFragmentManager
override fun getContainerId() = R.id.container
override fun getFragment(): Fragment = this@PaymentReviewFragment
})
}
override fun onResultFailed(error: GenericErrorResponse) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
viewModel.loadingScreen.value = false
}
paymentStatusListener?.setPaymentResult(Activity.RESULT_CANCELED, false, bundle = bundleOf(Pair(AMOUNT, amount)))
}
},
activityResultLauncher = paymentsResultLauncher
)
}
private fun appendIdAndTypeToEvent(analyticsEvent: AnalyticsEvent): AnalyticsEvent {
val analyticsEventProperties = analyticsEvent.properties ?: mutableMapOf()
analyticsEventProperties[ID] = id.orEmpty()
@@ -348,7 +507,8 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
return analyticsEvent.copy(properties = analyticsEventProperties)
}
override fun widgetAnalytics(analyticsEvent: AnalyticsEvent?) { analyticsEvent?.let {
override fun widgetAnalytics(analyticsEvent: AnalyticsEvent?) {
analyticsEvent?.let {
analyticsHandler.sendEvent(appendIdAndTypeToEvent(analyticsEvent), screenName)
}
}
@@ -371,6 +531,15 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
binding.landingPageRv.visibility = View.VISIBLE
}
private fun getParameterFromCta(ctaData: CtaData?, property: String?): String? {
ctaData?.parameters?.forEach { keyValue ->
if (keyValue.key == property) {
return keyValue.value
}
}
return null
}
override val screenName: String = TAG
override fun getViewModel(): GiBaseVM = viewModel
override fun getRetryAction(errorMessage: ErrorMessage?): () -> Unit = {}
@@ -379,4 +548,13 @@ class PaymentReviewFragment : GiBaseFragment(), WidgetCallback {
companion object {
const val TAG = "paymentReviewScreen"
}
}
interface PaymentStatusListener {
fun setPaymentResult(
resultCode: Int,
isCancelledByUser: Boolean = false,
ctaData: CtaData? = null,
bundle: Bundle? = null
)
}

View File

@@ -0,0 +1,137 @@
package com.navi.insurance.paymentreview.autopayoption.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.google.gson.reflect.TypeToken
import com.navi.base.model.CtaData
import com.navi.insurance.common.GiBaseActivity
import com.navi.insurance.common.util.NavigationHandler
import com.navi.insurance.models.TurboPaymentOrderDetail
import com.navi.insurance.models.response.PaymentStatusScreenResponse
import com.navi.insurance.paymentreview.autopayoption.viewmodel.HITurboCheckoutVM
import com.navi.insurance.paymentreview.autopayoption.viewmodel.PaymentInitState
import com.navi.insurance.util.AMOUNT
import com.navi.insurance.util.CONTENT_DATA_JSON_STRING
import com.navi.insurance.util.Constants
import com.navi.insurance.util.Constants.IS_AUTOPAY
import com.navi.insurance.util.Constants.IS_INSTALMENT
import com.navi.insurance.util.Constants.KEY_CTA_DATA
import com.navi.insurance.util.Constants.PAYMENTS_STATUS_IS_USER_CANCELLED_EXTRA
import com.navi.insurance.util.FAILURE
import com.navi.insurance.util.PAYMENT_FLOW_IDENTIFIER
import com.navi.insurance.util.PAYMENT_ORDER_DETAIL
import com.navi.insurance.util.SUCCESS
import com.navi.naviwidgets.extensions.NaviLottie
import com.navi.naviwidgets.extensions.NaviTextWidgetized
import com.navi.naviwidgets.extensions.getJsonObject
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class PaymentStatusActivity : GiBaseActivity() {
private val viewModel by viewModels<HITurboCheckoutVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val dataType = object : TypeToken<PaymentStatusScreenResponse>() {}.type
val paymentStatusScreenResponse = getJsonObject<PaymentStatusScreenResponse>(dataType, intent?.getStringExtra(
CONTENT_DATA_JSON_STRING
))
setContent {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
NaviTextWidgetized(textFieldData = paymentStatusScreenResponse?.title)
paymentStatusScreenResponse?.lottieFieldData?.let {
NaviLottie(
modifier = Modifier
.width(200.dp)
.height(200.dp)
.padding(vertical = 16.dp), lottie = it
)
}
NaviTextWidgetized(textFieldData = paymentStatusScreenResponse?.subTitle)
}
}
}
val turboPaymentOrderDetail = intent?.getParcelableExtra<TurboPaymentOrderDetail>(
PAYMENT_ORDER_DETAIL
)
viewModel.getPaymentStatus(
orderReferenceId = turboPaymentOrderDetail?.orderReferenceId.orEmpty(),
clientStatus = null,
pgClientStatus = null,
flowIdentifier = intent?.getStringExtra(PAYMENT_FLOW_IDENTIFIER),
pollingConfig = turboPaymentOrderDetail?.pollingConfig,
policyId = intent?.getStringExtra(Constants.POLICY_ID)
)
lifecycleScope.launch {
viewModel.paymentStatus.collect {
when(it){
is PaymentInitState.Success -> {
when (it.data?.status) {
SUCCESS -> {
setPaymentResult(Activity.RESULT_OK, ctaData = it.data.redirectionCta)
}
FAILURE -> {
setPaymentResult(Activity.RESULT_CANCELED, false, it.data.fallbackCta)
}
}
}
is PaymentInitState.Error -> {
setPaymentResult(Activity.RESULT_CANCELED, false, turboPaymentOrderDetail?.apiFailureCta)
}
else -> {}
}
}
}
}
private fun setPaymentResult(
result: Int,
isCancelledByUser: Boolean = false,
ctaData: CtaData? = null
) {
val resultIntent = getPaymentResultIntent(isCancelledByUser, ctaData)
setResult(result, resultIntent)
finish()
}
private fun getPaymentResultIntent(
isCancelledByUser: Boolean,
ctaData: CtaData? = null
): Intent {
val resultIntent = Intent()
resultIntent.run {
putExtra(AMOUNT, intent.getStringExtra(AMOUNT))
putExtra(IS_AUTOPAY, intent.getStringExtra(IS_AUTOPAY).toBoolean())
putExtra(IS_INSTALMENT, intent.getStringExtra(IS_INSTALMENT).toBoolean())
putExtra(PAYMENTS_STATUS_IS_USER_CANCELLED_EXTRA, isCancelledByUser)
putExtra(KEY_CTA_DATA, ctaData)
}
return resultIntent
}
override fun onBackPressed() {
//Back press is disable for this screen
}
override val screenName: String = NavigationHandler.URL_PAYMENT_STATUS_SCREEN
}

View File

@@ -0,0 +1,113 @@
package com.navi.insurance.paymentreview.autopayoption.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.navi.base.utils.orFalse
import com.navi.common.network.models.RepoResult
import com.navi.insurance.common.GiBaseVM
import com.navi.insurance.common.util.ActionHandler
import com.navi.insurance.health.repository.PaymentRepository
import com.navi.insurance.models.PaymentStateResponse
import com.navi.insurance.models.TurboPaymentOrderDetail
import com.navi.insurance.models.request.InitiateInstallmentPaymentRequest
import com.navi.insurance.network.ApiErrorTagType
import com.navi.insurance.util.FAILURE
import com.navi.insurance.util.SUCCESS
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
open class HITurboCheckoutVM
@Inject constructor(actionHandler: ActionHandler) : GiBaseVM(actionHandler) {
private val _paymentInitState =
MutableStateFlow<PaymentInitState<TurboPaymentOrderDetail>>(PaymentInitState.Initial)
val paymentInitState: StateFlow<PaymentInitState<TurboPaymentOrderDetail>> = _paymentInitState
val loadingScreen =
MutableLiveData<Boolean>()
private val _paymentStatus =
MutableStateFlow<PaymentInitState<PaymentStateResponse?>>(PaymentInitState.Initial)
val paymentStatus: StateFlow<PaymentInitState<PaymentStateResponse?>> = _paymentStatus
private var statusJob: Job? = null
@Inject
lateinit var paymentRepository: PaymentRepository
fun initiatePayment(
request: InitiateInstallmentPaymentRequest, policyId: String
) {
viewModelScope.launch {
_paymentInitState.value = PaymentInitState.Loading
val response = paymentRepository.initiatePaymentForInstallmentV2(policyId, request)
if (response.error == null && response.data != null) {
_paymentInitState.value = PaymentInitState.Success(response.data!!)
} else {
_paymentInitState.value = PaymentInitState.Error(response.error?.message.orEmpty())
updateErrorMessage(
response.error, ApiErrorTagType.INITIATE_PAYMENT_ERROR.value, false
)
setError(response.errors)
}
}
}
fun getPaymentStatus(
orderReferenceId: String,
clientStatus: String? = null,
pgClientStatus: String? = null,
flowIdentifier: String? = null,
pollingConfig: TurboPaymentOrderDetail.PollingConfig? = null,
policyId: String?
) {
_paymentStatus.value = PaymentInitState.Loading
if (statusJob?.isActive.orFalse()) {
statusJob?.cancel()
}
statusJob = viewModelScope.launch {
val initialDelay = pollingConfig?.initialDelay ?: 5000L
val interval = pollingConfig?.pollingInterval ?: 3000L
val noOfRetries = pollingConfig?.maxNumberOfRetries ?: 10
delay(initialDelay)
var count = 0
var response: RepoResult<PaymentStateResponse> = RepoResult()
while (count < noOfRetries && isActive) {
response = paymentRepository.getPaymentStatusForCustomerV2(
orderReferenceId, clientStatus, pgClientStatus, flowIdentifier, policyId
)
if (response.error != null || listOf(
SUCCESS,
FAILURE
).contains(response.data?.status)
) break
count++
delay(interval)
}
if (isActive) {
if (response.error == null) {
_paymentStatus.value = PaymentInitState.Success(response.data)
} else {
_paymentStatus.value = PaymentInitState.Error()
}
}
}
}
}
sealed class PaymentInitState<out T> {
data object Loading : PaymentInitState<Nothing>()
data class Error(val message: String? = null) : PaymentInitState<Nothing>()
data class Success<T>(val data: T) : PaymentInitState<T>()
data object Initial : PaymentInitState<Nothing>()
}

View File

@@ -27,7 +27,7 @@ constructor(
private val repository: PaymentReviewRepository,
actionHandler: ActionHandler,
private val dispatcher: CoroutineDispatcherProvider
) : GiBaseVM(actionHandler) {
) : HITurboCheckoutVM(actionHandler) {
private val _paymentRequestFlow =
MutableStateFlow<ResponseState<PaymentReviewResponse>>(ResponseState.Idle)

View File

@@ -135,6 +135,7 @@ object Constants {
const val FAQ_CONTEXT_SCREEN = "FAQ_CONTEXT_SCREEN"
const val POLICY_ID = "policyId"
const val EMI = "EMI"
const val IS_CANCELLED = "IS_CANCELLED"
const val EMI_DETAILS_DATA = "EMI_DETAILS_DATA"
const val EMI_TIMELINE_DATA = "EMI_TIMELINE_DATA"
@@ -151,6 +152,7 @@ object Constants {
const val PAYMENT_FLOW = "paymentFlow"
const val EMI_PREPAYMENT = "EMI_PREPAYMENT"
const val IS_AUTOPAY = "isAutopay"
const val IS_TURBO_CHECKOUT = "isTurboCheckout"
const val IS_AUTOPAY_PROCESSING = "isAutoPayProcessing"
const val IS_AUTOPAY_FROM_QUOTE = "isAutopayFromQuote"
const val PAYMENT_SUCCESS_CTA = "paymentSuccessCta"

View File

@@ -54,4 +54,5 @@ const val ID = "id"
const val PAYMENT_FLOW_IDENTIFIER = "paymentFlowIdentifier"
const val REDIRECTION_CTA = "redirectionCta"
const val RELOAD_PARENT_SCREEN = "reloadParentScreen"
const val PAYMENT_ORDER_DETAIL = "PAYMENT_ORDER_DETAIL"

View File

@@ -87,6 +87,22 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Pay now" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/loading_button"
style="@style/ButtonFontStylePurple"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:paddingHorizontal="@dimen/dp_40"
app:layout_constraintTop_toTopOf="@id/button"
app:layout_constraintStart_toStartOf="@id/button"
app:layout_constraintEnd_toEndOf="@id/button"
app:layout_constraintBottom_toBottomOf="@id/button"
app:lottie_url="https://public-assets.prod.navi-sa.in/home_uitron/cta_loader.json"
app:lottie_autoPlay="true"
app:lottie_loop="true"
android:visibility="invisible"/>
</com.navi.insurance.common.widgets.FooterWithTitleAndSubtitleLayout>
</layout>

View File

@@ -61,5 +61,13 @@
android:id="@+id/floating_action_button_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:id="@+id/transparent_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -10,5 +10,6 @@ package com.navi.payment.utils;
public enum PaymentSource {
DG,
BBPS,
PL
PL,
HI
}