TP-48780 | Refill journey Revamp to AP | PSEC-1136 (#8894)

Co-authored-by: kishan kumar <kishan.kumar@navi.com>
Co-authored-by: Jegatheeswaran M <jegatheeswaran.m@navi.com>
Co-authored-by: Prakhar Saxena <prakhar.saxena@navi.com>
Co-authored-by: rahul bhat <rahul.bhat@navi.com>
Co-authored-by: Shaurya Rehan <shaurya.rehan@navi.com>
Co-authored-by: Aditya Narayan Malik <aditya.narayan@navi.com>
This commit is contained in:
Anmol Agrawal
2024-01-02 20:47:24 +05:30
committed by GitHub
parent 83cc7e9857
commit f487d78a4d
46 changed files with 1233 additions and 37 deletions

View File

@@ -73,6 +73,9 @@ dependencies {
testImplementation libs.kotlinx.coroutines.test
testImplementation libs.mockk
implementation libs.accompanist.systemuicontroller
}
android.buildFeatures.buildConfig true

View File

@@ -0,0 +1,35 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.navi.ap.common.viewmodel.LambdaVM
import com.navi.common.uitron.model.action.SystemUiAction
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
fun SystemUiActionHandler(viewModel: LambdaVM) {
val systemUiController = rememberSystemUiController()
fun setSystemUiColors(systemUiAction: SystemUiAction) {
systemUiController.isStatusBarVisible = systemUiAction.isStatusBarVisible.orTrue()
systemUiController.isNavigationBarVisible = systemUiAction.isNavigationBarVisible.orTrue()
systemUiAction.statusBarColor?.color?.hexToComposeColor?.let { statusBarColor ->
systemUiController.setStatusBarColor(color = statusBarColor)
}
systemUiAction.navigationBarColor?.color?.hexToComposeColor?.let { navigationBarColour ->
systemUiController.setNavigationBarColor(color = navigationBarColour)
}
}
LaunchedEffect(Unit) {
viewModel.getActionCallback().distinctUntilChanged().collect { action ->
if (action is SystemUiAction) {
setSystemUiColors(action)
}
}
}
}

View File

@@ -2,8 +2,10 @@ package com.navi.ap.common.deserializer
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonElement
import com.navi.common.uitron.model.action.DownloadAction
import com.navi.ap.common.models.actions.UpdateDataViaHandleAction
import com.navi.common.uitron.deserializer.UiTronActionDeserializer
import com.navi.common.uitron.model.action.SystemUiAction
import com.navi.common.uitron.util.ApActionType
import com.navi.uitron.model.data.UiTronAction
import java.lang.reflect.Type
@@ -22,6 +24,12 @@ class ApUiTronActionDeserializer : UiTronActionDeserializer() {
ApActionType.UpdateDataViaHandleAction.name ->
context?.deserialize(jsonObject, UpdateDataViaHandleAction::class.java)
ApActionType.DownloadAction.name ->
context?.deserialize(jsonObject, DownloadAction::class.java)
ApActionType.SystemUiAction.name ->
context?.deserialize(jsonObject, SystemUiAction::class.java)
else -> super.deserialize(json, typeOfT, context)
}
}

View File

@@ -9,6 +9,7 @@ package com.navi.ap.common.deserializer
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonElement
import com.navi.ap.common.models.CardWithHeaderFooterAndLazyColumnWidgetData
import com.navi.ap.common.models.CollapsableItemsWithTitleWidgetData
import com.navi.ap.common.models.CustomWidgets
import com.navi.ap.common.models.DynamicColumnWidgetData
@@ -36,6 +37,8 @@ class CustomUiTronDataDeserializer : UiTronDataDeserializer() {
context?.deserialize(json, DynamicColumnWidgetData::class.java)
CustomWidgets.DYNAMIC_ROW_WIDGET.name ->
context?.deserialize(json, DynamicRowWidgetData::class.java)
CustomWidgets.CARD_WITH_HEADER_FOOTER_AND_LAZY_COLUMN_WIDGET.name ->
context?.deserialize(json, CardWithHeaderFooterAndLazyColumnWidgetData::class.java)
else -> super.deserialize(json, typeOfT, context)
}
}

View File

@@ -7,6 +7,7 @@
package com.navi.ap.common.handler
import SystemUiActionHandler
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import com.navi.ap.common.ui.ApplicationPlatformActivity
@@ -27,4 +28,6 @@ fun InitActionsHandler(viewModel: LambdaVM, activity: ApplicationPlatformActivit
HandleSdkAction(viewModel = viewModel, activity = activity)
HandleApiAction(viewModel = viewModel, activity = activity)
HandleLaunchIntentAction(viewModel = viewModel, activity = activity)
DownloadActionHandler(viewModel = viewModel, activity = activity)
SystemUiActionHandler(viewModel = viewModel)
}

View File

@@ -0,0 +1,46 @@
package com.navi.ap.common.handler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import com.navi.common.uitron.model.action.DownloadAction
import com.navi.ap.common.ui.ApplicationPlatformActivity
import com.navi.ap.common.viewmodel.LambdaVM
import com.navi.ap.utils.downloader.DownloadCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Composable
fun DownloadActionHandler(viewModel: LambdaVM, activity: ApplicationPlatformActivity) {
val coroutineScope = rememberCoroutineScope()
DisposableEffect(Unit) {
val job = coroutineScope.launch(Dispatchers.IO) {
viewModel.getActionCallback().distinctUntilChanged().collect { action ->
if (action is DownloadAction) {
viewModel.handleActions(action.downloadAction?.onStart)
activity.taskDownloadManager.startDownload(
downloadAction = action,
downloadCallback = object : DownloadCallback {
override fun onDownloadSuccess(downloadId: Long) {
viewModel.handleActions(action.downloadAction?.onCompleted)
}
override fun onDownloadFailure(downloadId: Long) {
viewModel.handleActions(action.downloadAction?.onFailed)
}
}
)
}
}
}
onDispose {
job.cancel()
}
}
}

View File

@@ -24,6 +24,10 @@ fun lambdaApiActionHandler(
val map = getResolvedFieldValue(fields = lambdaApiAction.fields, handle = viewModel.handle)
viewModel.fetchOfferDetails(lambdaApiAction = lambdaApiAction, resolvedValues = map)
}
LambdaType.REFILL_LOAN_DETAILS.name -> {
val map = getResolvedFieldValue(fields = lambdaApiAction.fields, handle = viewModel.handle)
viewModel.fetchRefillOfferDetails(map,lambdaApiAction)
}
LambdaType.APPLY_LOAN.name -> {
val map = getResolvedFieldValue(fields = lambdaApiAction.fields, handle = viewModel.handle)
viewModel.applyLoan(map)
@@ -89,5 +93,19 @@ fun lambdaApiActionHandler(
resolvedValues = map
)
}
LambdaType.FETCH_EMI_CALENDER_DETAILS.name -> {
val map =
getResolvedFieldValue(fields = lambdaApiAction.fields, handle = viewModel.handle)
viewModel.fetchEmiInstallments(resolvedValues = map, lambdaApiAction = lambdaApiAction)
}
LambdaType.DOWNLOAD_LOAN_AGREEMENT_AND_SANCTION_LETTER.name -> {
val map =
getResolvedFieldValue(fields = lambdaApiAction.fields, handle = viewModel.handle)
viewModel.initiateAgreementAndSanctionLetterDownload(
resolvedValues = map,
lambdaApiAction = lambdaApiAction
)
}
}
}

View File

@@ -0,0 +1,28 @@
package com.navi.ap.common.models
import com.navi.ap.common.models.lambdamodels.EmiCalendarResponse
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.ui.BorderStrokeData
data class CardWithHeaderFooterAndLazyColumnWidgetData(
val header : UiTronResponse? = null,
val footer : UiTronResponse? = null,
val stickyHeaderItem: UiTronResponse? = null,
val titleItem: UiTronResponse? = null,
val containerProperty: ContainerProperty? = null,
val listData: EmiCalendarResponse? = null,
) : UiTronData(){
data class ContainerProperty(
val verticalOffset : Int? = null,
val backGroundColor: String? = null,
val padding: Int? = null,
val containerHeight : Int? = null,
val borderStrokeData: BorderStrokeData? = null
)
}
data class UiTronResponseWithType(
val isSticky:Boolean = false,
val uiTronResponse: UiTronResponse?
)

View File

@@ -43,5 +43,6 @@ enum class CustomWidgets {
COLLAPSABLE_ITEMS_WITH_TITLE_WIDGET,
DYNAMIC_COLUMN_WIDGET,
DYNAMIC_ROW_WIDGET,
DYNAMIC_GRID_WIDGET
DYNAMIC_GRID_WIDGET,
CARD_WITH_HEADER_FOOTER_AND_LAZY_COLUMN_WIDGET
}

View File

@@ -0,0 +1,21 @@
package com.navi.ap.common.models.lambdamodels
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class AgreementLetterDownloadResponse(
@SerializedName("requestId") val requestId: String? = null,
@SerializedName("status") val status: String? = null,
@SerializedName("loanAgreement") val loanAgreement: DocumentDetail? = null,
@SerializedName("sanctionLetter") val sanctionLetter: DocumentDetail? = null,
) : Parcelable {
@Parcelize
data class DocumentDetail(
@SerializedName("referenceId") val referenceId: String? = null,
@SerializedName("uri") val uri: String? = null,
@SerializedName("rawCopyUri") val rawCopyUri: String? = null,
) : Parcelable
}

View File

@@ -0,0 +1,33 @@
package com.navi.ap.common.models.lambdamodels
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class EmiCalendarResponse(
@SerializedName("dataList") val dataList: List<EmiResponse>? = null,
@SerializedName("lastEmiData") val lastEmiData: EmiDetails? = null,
@SerializedName("placeHolderKey") val placeHolderKey: String? = null,
) : Parcelable {
@Parcelize
data class EmiResponse(
@SerializedName("year") val year: String? = null,
@SerializedName("emiDataList") val emiDataList: List<EmiData>? = null,
) : Parcelable
@Parcelize
data class EmiData(
@SerializedName("emiDetails") val emiDetails: EmiDetails? = null,
) : Parcelable
@Parcelize
data class EmiDetails(
@SerializedName("month") val month: Int? = null,
@SerializedName("date") val date: String? = null,
@SerializedName("amount") val amount: String? = null,
) : Parcelable
}

View File

@@ -0,0 +1,12 @@
package com.navi.ap.common.models.lambdamodels
data class EmiModel(
val amount: AmountModel? = null,
val firstEmiDueDate: String? = null,
val rateOfInterest: String? = null,
val tenureInMonths: String? = null,
) {
data class AmountModel(
val amount: String? = null,
)
}

View File

@@ -0,0 +1,116 @@
package com.navi.ap.common.renderer
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.navi.ap.common.models.CardWithHeaderFooterAndLazyColumnWidgetData
import com.navi.ap.common.models.UiTronResponseWithType
import com.navi.ap.common.models.WidgetModelDefinition
import com.navi.ap.common.viewmodel.CardWithHeaderFooterAndLazyColumnVM
import com.navi.ap.common.viewmodel.LambdaVM
import com.navi.ap.utils.constants.WIDGET_DATA
import com.navi.base.utils.orFalse
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.render.UiTronRenderer
import com.navi.uitron.utils.ShapeUtil
import com.navi.uitron.viewmodel.UiTronViewModel
import hexToComposeColor
class CardWithHeaderFooterAndLazyColumnWidget {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Render(
widget: WidgetModelDefinition<UiTronResponse>,
lambdaVM: LambdaVM,
viewModel: CardWithHeaderFooterAndLazyColumnVM = hiltViewModel(),
) {
val widgetData = remember {
widget.widgetData?.data?.get("${WIDGET_DATA}${widget.widgetId}") as? CardWithHeaderFooterAndLazyColumnWidgetData
}
val containerProperty = widgetData?.containerProperty
val widgetListState = remember {
mutableStateListOf<UiTronResponseWithType>()
}
LaunchedEffect(Unit) {
viewModel.getLazyColumnWidgetList(
widgetData, widgetData?.listData, widgetListState
)
}
Column(
modifier = Modifier
.background(containerProperty?.backGroundColor?.hexToComposeColor ?: Color.White)
.padding(containerProperty?.padding?.dp ?: 16.dp)
) {
Column(
modifier = Modifier.border(
border = BorderStroke(
containerProperty?.borderStrokeData?.width?.dp ?: 1.dp,
containerProperty?.borderStrokeData?.color?.hexToComposeColor
?: Color.Gray
),
shape = ShapeUtil.getShape(shape = containerProperty?.borderStrokeData?.shape)
)
) {
UiRenderer(uiTronResponse = widgetData?.header, viewModel = lambdaVM)
LazyColumn(
modifier = Modifier
.heightIn(Dp.Unspecified, 400.dp) //mention max height here
.offset(
y = containerProperty?.verticalOffset?.dp ?: 0.dp
)
) {
widgetListState.forEach { widgetViewWithType ->
val widgetView = widgetViewWithType.uiTronResponse
when {
widgetViewWithType.isSticky.orFalse() -> {
stickyHeader {
UiRenderer(uiTronResponse = widgetView, viewModel = lambdaVM)
}
}
else -> {
item {
UiRenderer(uiTronResponse = widgetView, viewModel = lambdaVM)
}
}
}
}
}
UiRenderer(uiTronResponse = widgetData?.footer, viewModel = lambdaVM)
}
}
}
@Composable
private fun UiRenderer(uiTronResponse: UiTronResponse?, viewModel: UiTronViewModel) {
uiTronResponse?.parentComposeView?.let {
UiTronRenderer(
uiTronResponse.data, viewModel
).Render(composeViews = it)
}
}
}

View File

@@ -8,10 +8,12 @@
package com.navi.ap.common.repository
import com.navi.ap.common.models.TelcoResendOtpRequest
import com.navi.ap.common.models.lambdamodels.AgreementLetterDownloadResponse
import com.navi.ap.common.models.lambdamodels.ApplyLoanResponse
import com.navi.ap.common.models.lambdamodels.BankDataResponse
import com.navi.ap.common.models.lambdamodels.ConsentRequest
import com.navi.ap.common.models.lambdamodels.ConsentResponse
import com.navi.ap.common.models.lambdamodels.EmiModel
import com.navi.ap.common.models.lambdamodels.IfscBranchResponse
import com.navi.ap.common.models.lambdamodels.FetchPaymentMethodsRequest
import com.navi.ap.common.models.lambdamodels.OfferDetails
@@ -33,7 +35,6 @@ import com.navi.common.network.models.SuccessResponse
import com.navi.common.uitron.model.action.ApiType
import com.navi.payment.model.SignalPaymentData
import com.navi.payment.utils.Constants
import org.json.JSONObject
open class LambdaRepository(val retrofitService: RetrofitService = retrofitService()) :
ApplicationPlatformRepository(retrofitService) {
@@ -82,6 +83,27 @@ open class LambdaRepository(val retrofitService: RetrofitService = retrofitServi
return apiResponseCallback(response = response, apiTag = ApiType.GetScreenDefinition.name)
}
suspend fun fetchRefillOfferDetails(
amount: String? = null,
tenureInMonths: String? = null,
emiPlansSelected: String? = null,
): ApRepoResult<Any> {
val response =
retrofitService.getRefillOfferDetails(
amount = amount,
tenureInMonths = tenureInMonths,
emiPlansSelected = emiPlansSelected
)
logApEvent(
Pair(
RESULT,
"${response.body()?.errors} | ${response.body()?.genericErrorBottomSheetFields}"
),
Pair(METHOD_NAME, ::fetchOfferDetails.name),
Pair(STATUS_CODE, response.code().toString())
)
return apiResponseCallback(response = response, apiTag = ApiType.GetScreenDefinition.name)
}
suspend fun applyLoan(
offerReferenceId: String? = null,
offerDetails: OfferDetails? = null,
@@ -324,4 +346,49 @@ open class LambdaRepository(val retrofitService: RetrofitService = retrofitServi
)
return apiResponseCallback(response = response, apiTag = ApiType.LambdaApiAction.name)
}
suspend fun fetchEmiInstallments(emiModel: EmiModel): ApRepoResult<Any> {
val response = retrofitService.fetchEmiInstallments(data = emiModel)
logApEvent(
Pair(
RESULT,
"${response.body()?.errors} | ${response.body()?.genericErrorBottomSheetFields}"
),
Pair(METHOD_NAME, ::fetchEmiInstallments.name),
Pair(STATUS_CODE, response.code().toString())
)
return apiResponseCallback(response = response, apiTag = ApiType.LambdaApiAction.name)
}
suspend fun initiateAgreementAndSanctionLetterDownload(
loanApplicationId: String,
documentType: String,
): ApRepoResult<AgreementLetterDownloadResponse> {
val response = retrofitService.initiateAgreementAndSanctionLetterDownload(
loanApplicationId = loanApplicationId,
documentType = documentType
)
logApEvent(
Pair(
RESULT,
"${response.body()?.errors} | ${response.body()?.genericErrorBottomSheetFields}"
),
Pair(METHOD_NAME, ::initiateAgreementAndSanctionLetterDownload.name),
Pair(STATUS_CODE, response.code().toString())
)
return apiResponseCallback(response = response, apiTag = ApiType.LambdaApiAction.name)
}
suspend fun agreementGenerationStatus(loanApplicationId : String) : ApRepoResult<AgreementLetterDownloadResponse> {
val response = retrofitService.agreementGenerationStatus(loanApplicationId)
logApEvent(
Pair(
RESULT,
"${response.body()?.errors} | ${response.body()?.genericErrorBottomSheetFields}"
),
Pair(METHOD_NAME, ::agreementGenerationStatus.name),
Pair(STATUS_CODE, response.code().toString())
)
return apiResponseCallback(response = response, apiTag = ApiType.LambdaApiAction.name)
}
}

View File

@@ -0,0 +1,56 @@
package com.navi.ap.common.serializer
import com.navi.uitron.serializer.UiTronDataSerializer
import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.navi.ap.common.models.CardWithHeaderFooterAndLazyColumnWidgetData
import com.navi.ap.common.models.CollapsableItemsWithTitleWidgetData
import com.navi.ap.common.models.CustomWidgets
import com.navi.ap.common.models.DynamicColumnWidgetData
import com.navi.ap.common.models.DynamicGridWidgetData
import com.navi.ap.common.models.DynamicRowWidgetData
import com.navi.uitron.model.data.UiTronData
import java.lang.reflect.Type
class CustomUiTronDataSerializer : UiTronDataSerializer() {
override fun serialize(
src: UiTronData?,
typeOfSrc: Type?,
context: JsonSerializationContext?,
): JsonElement? {
return when (src?.viewType) {
CustomWidgets.COLLAPSABLE_ITEMS_WITH_TITLE_WIDGET.name -> {
context?.serialize(
src as CollapsableItemsWithTitleWidgetData,
CollapsableItemsWithTitleWidgetData::class.java
)
}
CustomWidgets.DYNAMIC_GRID_WIDGET.name -> {
context?.serialize(src as DynamicGridWidgetData, DynamicGridWidgetData::class.java)
}
CustomWidgets.DYNAMIC_COLUMN_WIDGET.name -> {
context?.serialize(
src as DynamicColumnWidgetData,
DynamicColumnWidgetData::class.java
)
}
CustomWidgets.DYNAMIC_ROW_WIDGET.name -> {
context?.serialize(src as DynamicRowWidgetData, DynamicRowWidgetData::class.java)
}
CustomWidgets.CARD_WITH_HEADER_FOOTER_AND_LAZY_COLUMN_WIDGET.name -> {
context?.serialize(
src as CardWithHeaderFooterAndLazyColumnWidgetData,
CardWithHeaderFooterAndLazyColumnWidgetData::class.java
)
}
else -> super.serialize(src, typeOfSrc, context)
}
}
}

View File

@@ -161,10 +161,8 @@ fun ObserveToastVisibilityState(viewModel: LambdaVM?) {
if (viewModel == null) return
val toast = viewModel.toastStructure.collectAsState(WidgetModelDefinition())
toast.value.let {
if (it.widgetData?.data != null) {
UiTronRenderer(it.widgetData.data, viewModel)
.Render(composeViews = it.widgetData.parentComposeView.orEmpty())
}
UiTronRenderer(it.widgetData?.data, viewModel)
.Render(composeViews = it.widgetData?.parentComposeView.orEmpty())
viewModel.setToast(WidgetModelDefinition())
}
}

View File

@@ -16,6 +16,7 @@ import com.navi.ap.utils.constants.FAILURE
import com.navi.ap.utils.constants.RAZORPAY_DATA
import com.navi.ap.utils.constants.SUCCESS
import com.navi.ap.utils.constants.SUCCESS_CODE
import com.navi.ap.utils.downloader.TaskDownloadManager
import com.navi.common.ui.activity.BaseActivity
import com.razorpay.PaymentData
import com.razorpay.PaymentResultWithDataListener
@@ -33,6 +34,9 @@ abstract class SdkHandlingActivity :
protected var apScreenVM: WeakReference<LambdaVM>? = null
@Inject
lateinit var taskDownloadManager : TaskDownloadManager
override fun onDigioSuccess(digioResponse: DigioResponse) {
sdkHandler.digioHandler.handleDigioSdkResult(
digioStatus = SUCCESS,

View File

@@ -50,6 +50,7 @@ import com.navi.ap.utils.constants.REASON
import com.navi.ap.utils.constants.SCREEN_TYPE
import com.navi.ap.utils.constants.SUBMIT_EVENT_HASH_EVENT
import com.navi.ap.utils.helper.BottomSheetHelper
import com.navi.ap.utils.helper.PeriodicTaskSchedulerFacade
import com.navi.ap.utils.toMap
import com.navi.ap.utils.toMutableMap
import com.navi.base.model.CtaData
@@ -83,7 +84,7 @@ constructor(private val repository: ApplicationPlatformRepository) : BaseVM() {
@Inject
lateinit var shouldPollStrategyUseCase: UpdateShouldPollStrategyUseCase
private var periodicTaskScheduler: PeriodicTaskScheduler? = null
var periodicTaskScheduler: PeriodicTaskScheduler? = null
protected var systemBackAction: UiTronActionData? = null
private var queryMap: MutableMap<String, String?> = mutableMapOf()
private var bottomSheetQueryMap: MutableMap<String, String?> = mutableMapOf()
@@ -121,6 +122,9 @@ constructor(private val repository: ApplicationPlatformRepository) : BaseVM() {
)
val bottomSheetStateHolder = _bottomSheetStateHolder.asStateFlow()
protected val periodicTaskSchedulerFacade by lazy { PeriodicTaskSchedulerFacade() }
fun updateBottomSheetUIState(
bottomSheetState: ApBottomSheetState,
bottomSheetStateChange: Boolean? = null,
@@ -618,7 +622,7 @@ constructor(private val repository: ApplicationPlatformRepository) : BaseVM() {
)
}
protected fun stopApiPolling() {
fun stopApiPolling() {
periodicTaskScheduler?.stopTask()
periodicTaskScheduler = null
}

View File

@@ -0,0 +1,85 @@
package com.navi.ap.common.viewmodel
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.navi.ap.common.models.CardWithHeaderFooterAndLazyColumnWidgetData
import com.navi.ap.common.models.UiTronResponseWithType
import com.navi.ap.common.models.lambdamodels.EmiCalendarResponse
import com.navi.ap.utils.appendIndexToDataKeys
import com.navi.ap.utils.appendIndexToWidgetData
import com.navi.ap.utils.getGsonBuilders
import com.navi.ap.utils.injector.BasePathInjector
import com.navi.ap.utils.toJson
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.common.model.common.UiTronData
import com.navi.uitron.model.UiTronResponse
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import org.json.JSONObject
import javax.inject.Inject
@HiltViewModel
class CardWithHeaderFooterAndLazyColumnVM @Inject constructor() : ViewModel() {
private var widgetIndex = 0
fun getLazyColumnWidgetList(
widgetData: CardWithHeaderFooterAndLazyColumnWidgetData?,
listData: EmiCalendarResponse?,
widgetListState: SnapshotStateList<UiTronResponseWithType>,
) {
viewModelScope.launch(Dispatchers.Default) {
val taskList = mutableListOf<Deferred<UiTronResponseWithType>>()
listData?.dataList?.forEach { emiResponse ->
if (emiResponse.year.isNotNullAndNotEmpty()) {
val stickyHeaderTask = async {
UiTronResponseWithType(
true,
getWidget(widgetData?.stickyHeaderItem, emiResponse, widgetIndex)
)
}
widgetIndex.inc()
taskList.add(stickyHeaderTask)
}
emiResponse.emiDataList?.forEach { emiData ->
val emiDataListTask = async {
UiTronResponseWithType(
false,
getWidget(widgetData?.titleItem, emiData.emiDetails, widgetIndex)
)
}
widgetIndex.inc()
taskList.add(emiDataListTask)
}
}
widgetListState.addAll(taskList.awaitAll())
}
}
private suspend fun getWidget(
widgetData: UiTronResponse?,
data: Any? = null,
index: Int
): UiTronResponse {
val widgetObject = JSONObject(widgetData.toJson())
appendIndexToWidgetData(widgetObject, index)
val modifiedWidgetData = getGsonBuilders()
.fromJson(widgetObject.toString(), UiTronResponse::class.java)
val injectedData = BasePathInjector<Map<String, UiTronData?>, Any?>().injectData(
modifiedWidgetData.appendIndexToDataKeys(index) ?: mapOf(),
data
)
return modifiedWidgetData.copy(data = injectedData.toMutableMap())
}
}

View File

@@ -20,6 +20,7 @@ import com.navi.ap.common.models.TelcoResendOtpRequest
import com.navi.ap.common.models.WidgetModelDefinition
import com.navi.ap.common.models.lambdamodels.BankDataResponse
import com.navi.ap.common.models.lambdamodels.ConsentRequest
import com.navi.ap.common.models.lambdamodels.EmiModel
import com.navi.ap.common.models.lambdamodels.FetchPaymentMethodsRequest
import com.navi.ap.common.models.lambdamodels.IfscBranchResponse
import com.navi.ap.common.models.lambdamodels.OfferDetails
@@ -40,10 +41,13 @@ import com.navi.ap.utils.constants.BANK_NAME
import com.navi.ap.utils.constants.BANK_SEARCH_QUERY
import com.navi.ap.utils.constants.COINS_VALIDATE_UPI_ID
import com.navi.ap.utils.constants.DEFAULT_BANKS_SEARCH_QUERY
import com.navi.ap.utils.constants.DOCUMENT_TYPE
import com.navi.ap.utils.constants.EMI_AMOUNT
import com.navi.ap.utils.constants.FETCH_OFFER_DETAILS_SELECTED_LOAN_AMOUNT
import com.navi.ap.utils.constants.FETCH_OFFER_DETAILS_SELECTED_TENURE
import com.navi.ap.utils.constants.FINARKEIN_CONSENT_DATA
import com.navi.ap.utils.constants.FIRST_EMI_DUE_DATE
import com.navi.ap.utils.constants.FAILURE_CAP
import com.navi.ap.utils.constants.IFSC_CODE_KEY
import com.navi.ap.utils.constants.IFSC_SEARCH_QUERY
import com.navi.ap.utils.constants.INVALID_IFSC_CODE
@@ -56,6 +60,7 @@ import com.navi.ap.utils.constants.MIN_QUERY_LENGTH
import com.navi.ap.utils.constants.OFFER_REFERENCE_ID
import com.navi.ap.utils.constants.PG_TOKEN
import com.navi.ap.utils.constants.PREFERRED_BANKS_LIST
import com.navi.ap.utils.constants.RATE_OF_INTEREST
import com.navi.ap.utils.constants.RPD_PAYMENT_METHOD_ID
import com.navi.ap.utils.constants.RPD_PAYMENT_METHOD_TOKEN
import com.navi.ap.utils.constants.RPD_PAYMENT_METHOD_UPI_LINK_KEY
@@ -65,8 +70,9 @@ import com.navi.ap.utils.constants.SELECTED_IFSC_DATA
import com.navi.ap.utils.constants.SELECTED_LOAN_AMOUNT
import com.navi.ap.utils.constants.SELECTED_TENURE
import com.navi.ap.utils.constants.SELECTED_TENURE_PILL_INDEX
import com.navi.ap.utils.constants.TENURE_IN_MONTHS
import com.navi.ap.utils.constants.SUCCESS_CAP
import com.navi.ap.utils.constants.TRIGGER_LOADING_STATE
import com.navi.ap.utils.constants.TENURE_IN_MONTHS
import com.navi.ap.utils.constants.TRUE
import com.navi.ap.utils.getGsonBuilders
import com.navi.ap.utils.getSignalPaymentData
@@ -86,6 +92,7 @@ import com.navi.common.utils.deviceId
import com.navi.common.utils.toJsonObject
import com.navi.naviwidgets.utils.FORWARD_SLASH
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.model.data.UiTronActionData
import getInputId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -224,6 +231,83 @@ open class LambdaVM constructor(
}
}
fun fetchRefillOfferDetails(
resolvedValues: MutableMap<String, Any?>,
lambdaApiAction: LambdaApiAction
) {
viewModelScope.launch(Dispatchers.IO) {
if (_clonedScreenDefinitionState.value == null) {
_clonedScreenDefinitionState.value = getScreenStructurePreRenderState()
}
val savedAmount = handle.get<String>(FETCH_OFFER_DETAILS_SELECTED_LOAN_AMOUNT)
val savedTenure = handle.get<String>(FETCH_OFFER_DETAILS_SELECTED_TENURE)
val amount = resolvedValues[SELECTED_LOAN_AMOUNT]?.toString()
var tenure = resolvedValues[SELECTED_TENURE]?.toString()
val emiPlanSelected = resolvedValues[SELECTED_TENURE_PILL_INDEX]?.toString()
if (
savedAmount == amount &&
savedTenure == tenure &&
savedAmount != null &&
savedTenure != null
)
return@launch
handleActions(lambdaApiAction.preExecutionAction)
_lambdaState.value = LambdaState.Loading
if (
resolvedValues[LOAN_SLIDER] == TRUE &&
emiPlanSelected != LAST_SELECTED_TENURE_PILL_INDEX
) {
tenure = null
}
val lambdaResponse =
repository.fetchRefillOfferDetails(
amount = amount,
tenureInMonths = tenure,
emiPlansSelected = emiPlanSelected
)
_lambdaState.value =
when {
lambdaResponse.data != null && _clonedScreenDefinitionState.value != null -> {
LambdaState.Success(LambdaResponseType())
val updatedResponse =
BasePathInjector<ApScreenDefinitionStructure, Any?>()
.injectData(
_clonedScreenDefinitionState.value,
lambdaResponse.data
)
updatedResponse?.let {
metaData = updatedResponse.screenData?.metaData ?: mapOf()
_screenDefinitionState.value =
ApScreenDefinitionState.Success(updatedResponse)
}
systemBackAction =
updatedResponse?.screenData?.screenStructure?.systemBackCta
handle[FETCH_OFFER_DETAILS_SELECTED_LOAN_AMOUNT] = amount
handle[FETCH_OFFER_DETAILS_SELECTED_TENURE] = tenure
LambdaState.Success(LambdaResponseType())
}
lambdaResponse.errorBottomSheetStructure.isNotNull() -> {
LambdaState.Error(
lambdaResponse.statusCode,
lambdaResponse.errorBottomSheetStructure,
lambdaResponse.genericErrorBottomSheetFields
)
}
else -> {
LambdaState.Nothing
}
}
}
}
// TODO : Need to remove it when bankDetails will be powered through AP
fun applyLoan(resolvedValues: MutableMap<String, Any?>) {
viewModelScope.launch(Dispatchers.Default) {
@@ -739,7 +823,10 @@ open class LambdaVM constructor(
}
}
fun fetchMandateMethods(resolvedValues: MutableMap<String, Any?>, lambdaApiAction: LambdaApiAction) {
fun fetchMandateMethods(
resolvedValues: MutableMap<String, Any?>,
lambdaApiAction: LambdaApiAction
) {
viewModelScope.launch(Dispatchers.IO) {
if (_clonedScreenDefinitionState.value == null) {
_clonedScreenDefinitionState.value = getScreenStructurePreRenderState()
@@ -853,10 +940,121 @@ open class LambdaVM constructor(
lambdaResponse.genericErrorBottomSheetFields
)
}
else -> {
LambdaState.Nothing
}
}
}
}
}
fun fetchEmiInstallments(lambdaApiAction: LambdaApiAction, resolvedValues: MutableMap<String, Any?>) {
viewModelScope.launch(Dispatchers.IO) {
if (_clonedScreenDefinitionState.value == null) {
_clonedScreenDefinitionState.value = getScreenStructurePreRenderState()
}
_lambdaState.value = LambdaState.Loading
val lambdaResponse = repository.fetchEmiInstallments(
EmiModel(
amount = EmiModel.AmountModel(amount = resolvedValues[SELECTED_LOAN_AMOUNT].toString()),
firstEmiDueDate = resolvedValues[FIRST_EMI_DUE_DATE].toString(),
rateOfInterest = resolvedValues[RATE_OF_INTEREST].toString(),
tenureInMonths = resolvedValues[SELECTED_TENURE].toString()
)
)
_lambdaState.value = when {
lambdaResponse.data.isNotNull() && _clonedScreenDefinitionState.value.isNotNull() -> {
val updatedResponse = PathInjector<ApScreenDefinitionStructure, Any?>()
.injectData(
_clonedScreenDefinitionState.value,
lambdaResponse.data
)
updatedResponse?.let {
_screenDefinitionState.value =
ApScreenDefinitionState.Success(updatedResponse)
}
handleActions(lambdaApiAction.onSuccess)
LambdaState.Success(LambdaResponseType())
}
lambdaResponse.errorBottomSheetStructure.isNotNull() -> {
handleActions(lambdaApiAction.onFailure)
LambdaState.Error(
lambdaResponse.statusCode,
null,
lambdaResponse.genericErrorBottomSheetFields
)
}
else -> {
LambdaState.Nothing
}
}
}
}
fun initiateAgreementAndSanctionLetterDownload(
resolvedValues: MutableMap<String, Any?>,
lambdaApiAction: LambdaApiAction
) {
viewModelScope.launch(Dispatchers.IO) {
val loanApplicationId = resolvedValues[LOAN_APPLICATION_ID]?.toString().orEmpty()
val documentType = resolvedValues[DOCUMENT_TYPE]?.toString().orEmpty()
val lambdaResponse = repository.initiateAgreementAndSanctionLetterDownload(loanApplicationId = loanApplicationId, documentType = documentType)
when {
lambdaResponse.data.isNotNull() -> {
lambdaResponse.data?.requestId?.let {
periodicTaskSchedulerFacade.scheduleTask(taskId = documentType){
agreementGenerationStatus(requestId = it,lambdaApiAction = lambdaApiAction,taskId = documentType)
}
}
}
lambdaResponse.errorBottomSheetStructure.isNotNull() -> {
LambdaState.Error(
lambdaResponse.statusCode,
null,
lambdaResponse.genericErrorBottomSheetFields
)
}
}
}
}
private fun agreementGenerationStatus(requestId: String, lambdaApiAction: LambdaApiAction, taskId : String){
viewModelScope.launch {
val lambdaResponse = repository.agreementGenerationStatus(requestId)
when {
lambdaResponse.data.isNotNull() -> {
when(lambdaResponse.data?.status){
SUCCESS_CAP -> {
val updatedSuccessAction = PathInjector<UiTronActionData, Any?>()
.injectData(
lambdaApiAction.onSuccess,
lambdaResponse.data
)
handleActions(updatedSuccessAction)
periodicTaskSchedulerFacade.stopTask(taskId)
}
FAILURE_CAP -> {
periodicTaskSchedulerFacade.stopTask(taskId)
}
}
}
lambdaResponse.errorBottomSheetStructure.isNotNull() -> {
periodicTaskSchedulerFacade.stopTask(taskId)
LambdaState.Error(
lambdaResponse.statusCode,
null,
lambdaResponse.genericErrorBottomSheetFields
)
}
}
}
}
}

View File

@@ -3,6 +3,7 @@ package com.navi.ap.common.widgetfactory
import androidx.compose.runtime.Composable
import com.navi.ap.common.models.CustomWidgets
import com.navi.ap.common.models.WidgetModelDefinition
import com.navi.ap.common.renderer.CardWithHeaderFooterAndLazyColumnWidget
import com.navi.ap.common.renderer.CollapsableItemsWithTitleWidget
import com.navi.ap.common.renderer.DynamicColumnWidget
import com.navi.ap.common.renderer.DynamicGridWidget
@@ -29,6 +30,9 @@ fun CustomWidgetRenderer(
CustomWidgets.DYNAMIC_GRID_WIDGET.name -> {
DynamicGridWidget().Render(viewModel = viewModel, widget = widget)
}
CustomWidgets.CARD_WITH_HEADER_FOOTER_AND_LAZY_COLUMN_WIDGET.name -> {
CardWithHeaderFooterAndLazyColumnWidget().Render(lambdaVM = viewModel,widget = widget)
}
else -> Unit
}
}

View File

@@ -0,0 +1,16 @@
package com.navi.ap.di
import com.navi.ap.utils.downloader.TaskDownloadManager
import com.navi.ap.utils.downloader.TaskDownloadManagerImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
@Module
@InstallIn(ActivityComponent::class)
abstract class ActivityComponentManagerModule {
@Binds
abstract fun getTaskDownloadManager(taskDownloadManagerImpl: TaskDownloadManagerImpl): TaskDownloadManager
}

View File

@@ -7,6 +7,9 @@
package com.navi.ap.di
import android.app.DownloadManager
import android.content.Context
import android.content.Context.DOWNLOAD_SERVICE
import com.navi.ap.common.sdk.handler.DigioHandler
import com.navi.ap.common.sdk.handler.FinoramicHandler
import com.navi.ap.common.sdk.handler.RazorpayHandler
@@ -20,9 +23,11 @@ import com.navi.payment.razorpay.RazorpayHelper
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object ProviderModule {
@@ -75,4 +80,9 @@ object ProviderModule {
fun getUpdateShouldPollStrategyUseCase(): UpdateShouldPollStrategyUseCase =
UpdateShouldPollStrategyUseCase()
}
@Provides
@Singleton
fun getDownloadManager(@ApplicationContext context : Context) : DownloadManager? {
return context.getSystemService(DOWNLOAD_SERVICE) as? DownloadManager
}
}

View File

@@ -12,6 +12,7 @@ import com.navi.ap.utils.constants.ApScreenDestinations
import com.navi.ap.utils.identifier
import com.navi.base.model.CtaData
import com.navi.base.utils.orFalse
import com.navi.common.utils.Constants.ADDITIONAL_PARAMETERS
import javax.inject.Inject
@@ -35,6 +36,10 @@ class PlatformNavigationHandler @Inject constructor(): NavigationHandler {
apScreenDestination: ApScreenDestinations,
) {
ctaData.parameters?.forEach { item -> bundle.putString(item.key, item.value) }
val nextScreenBundle = Bundle().apply {
ctaData.additionalParameters?.forEach { item -> putString(item.key, item.value) }
}
bundle.putBundle(ADDITIONAL_PARAMETERS, nextScreenBundle)
if (apScreenDestination == ApScreenDestinations.AP_LAUNCHER) {
activity.navController.popBackStack()
@@ -44,7 +49,7 @@ class PlatformNavigationHandler @Inject constructor(): NavigationHandler {
val isScreenPresent = isDestinationScreenPresentInBackStack(activity, ctaScreenId)
if (bundle.getString(APP_ACTION) == ApNavigationActions.BACK.name) {
if (bundle.getString(APP_ACTION) == ApNavigationActions.BACK.name && ctaData.refreshScreen.orFalse().not()) {
if (isScreenPresent) {
clearBackStackTillDestinationScreen(activity, ctaScreenId)
} else {

View File

@@ -11,6 +11,7 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.navi.ap.common.repository.ApplicationPlatformRepository
import com.navi.ap.common.repository.LambdaRepository
import com.navi.ap.common.serializer.CustomUiTronDataSerializer
import com.navi.ap.network.retrofit.NaviApHttpClient
import com.navi.ap.network.retrofit.service.RetrofitService
import com.navi.ap.network.utils.getNetworkInfo
@@ -36,7 +37,6 @@ import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.ui.BaseProperty
import com.navi.uitron.model.visualtransformation.VisualTransformationData
import com.navi.uitron.serializer.ComposePropertySerializer
import com.navi.uitron.serializer.UiTronDataSerializer
import com.navi.uitron.serializer.UiTronValidationSerializer
import com.navi.uitron.serializer.VisualTransformationDataSerializer
import dagger.Module
@@ -76,7 +76,7 @@ object APNetworkModule {
@Named("apGson")
fun providesSerializer(): Gson = GsonBuilder()
.registerTypeAdapter(BaseProperty::class.java, ComposePropertySerializer())
.registerTypeAdapter(UiTronData::class.java, UiTronDataSerializer())
.registerTypeAdapter(UiTronData::class.java, CustomUiTronDataSerializer())
.registerTypeAdapter(UiTronAction::class.java, UiTronActionSerializer())
.registerTypeAdapter(UploadDataConfig::class.java, UiTronUploadDataSerializer())
.registerTypeAdapter(BaseInputValidation::class.java, UiTronValidationSerializer())

View File

@@ -1,10 +1,12 @@
package com.navi.ap.network.retrofit.service
import com.navi.ap.common.models.TelcoResendOtpRequest
import com.navi.ap.common.models.lambdamodels.AgreementLetterDownloadResponse
import com.navi.ap.common.models.lambdamodels.ApplyLoanResponse
import com.navi.ap.common.models.lambdamodels.BankDataResponse
import com.navi.ap.common.models.lambdamodels.ConsentRequest
import com.navi.ap.common.models.lambdamodels.ConsentResponse
import com.navi.ap.common.models.lambdamodels.EmiModel
import com.navi.ap.common.models.lambdamodels.FetchPaymentMethodsRequest
import com.navi.ap.common.models.lambdamodels.IfscBranchResponse
import com.navi.ap.common.models.lambdamodels.OfferDetails
@@ -44,6 +46,13 @@ interface PlLambdaService {
@Header("X-Target") target: String = ModuleName.LE.name,
): Response<GenericResponse<Any>>
@GET("/customer/refill-offer-details")
suspend fun getRefillOfferDetails(
@Query("amount") amount: String? = null,
@Query("tenureInMonths") tenureInMonths: String? = null,
@Query("emiPlansSelected") emiPlansSelected: String? = null,
@Header("X-Target") target: String = ModuleName.LE.name,
): Response<GenericResponse<Any>>
@POST("/offer/{offerReferenceId}/apply")
suspend fun applyLoan(
@Header("X-Target") target: String = ModuleName.LE.name,
@@ -132,4 +141,23 @@ interface PlLambdaService {
@Header("X-Payment-SDK-Version") version: String = Constants.PAYMENT_SDK_VERSION
): Response<GenericResponse<Any>>
@POST("/emi/fetch-installments")
suspend fun fetchEmiInstallments(
@Header("X-Target") xTarget: String = "OPL",
@Body data: EmiModel? = null,
): Response<GenericResponse<Any>>
@POST("/loan-agreement/generate/{loanApplicationId}")
suspend fun initiateAgreementAndSanctionLetterDownload(
@Path("loanApplicationId") loanApplicationId: String,
@Header("X-Target") xTarget: String = "OPL",
@Query("documentType") documentType: String? = null
): Response<GenericResponse<AgreementLetterDownloadResponse>>
@GET("/loan-agreement/requests/{requestId}")
suspend fun agreementGenerationStatus(
@Path("requestId") requestId: String,
@Header("X-Target") xTarget: String = "OPL",
): Response<GenericResponse<AgreementLetterDownloadResponse>>
}

View File

@@ -38,6 +38,8 @@ import com.navi.ap.utils.constants.SCREEN_TYPE
import com.navi.ap.utils.constants.UNDERSCORE_POLL
import com.navi.ap.utils.constants.noLoaderScreenTypes
import com.navi.ap.utils.logger.ScreenTimeLifecycleLogger
import com.navi.ap.utils.initParameters
import com.navi.ap.utils.initScreenEvent
import com.navi.base.utils.orElse
import com.navi.common.utils.EMPTY
import com.ramcosta.composedestinations.annotation.Destination
@@ -55,12 +57,12 @@ fun ApGenericScreen(
activity = activity,
renderScaffoldState = viewModel.renderScaffoldState.collectAsState().value
)
handleLoadingState(activity, viewModel)
HandleLoadingState(activity, viewModel)
LaunchedEffect(Unit){
initParameters(bundle,viewModel)
initScreenEvent(activity = activity, screenName = viewModel.getQueryMap()[APP_PLATFORM_SCREEN_ID].orEmpty(), viewModel = viewModel)
}
LaunchedEffect(Unit) {
viewModel.setQueryMap(bundle)
viewModel.eventUtils.setCurrentScreen(activity = activity, screenName = viewModel.getQueryMap()[APP_PLATFORM_SCREEN_ID].orEmpty())
activity.getApplicationPlatformSharedVM().getScreenDefinitionStructure()?.let {
viewModel.setScreenDefinitionResponse(it)
activity.getApplicationPlatformSharedVM()
@@ -108,19 +110,18 @@ fun ApGenericScreen(
handleLambdaState(activity = activity, viewModel = viewModel, lambdaState = lambdaState)
}
}
handleLoaderState(activity, viewModel)
HandleLoaderState(activity, viewModel)
}
@Composable
private fun handleLoadingState(activity: ApplicationPlatformActivity, viewModel: ApGenericScreenVM) {
private fun HandleLoadingState(activity: ApplicationPlatformActivity, viewModel: ApGenericScreenVM) {
if (viewModel.renderScaffoldState.collectAsState().value.not() && viewModel.screenDefinitionState.collectAsState().value !is ApScreenDefinitionState.Error) {
ShowShimmerLoader(activity.intent?.extras?.getString(APP_PLATFORM_APPLICATION_TYPE))
}
}
@Composable
fun handleLoaderState(activity: ApplicationPlatformActivity, viewModel: LambdaVM){
private fun HandleLoaderState(activity: ApplicationPlatformActivity, viewModel: LambdaVM){
LaunchedEffect(activity.loaderState.value) {
if (activity.loaderState.value.not()) {
viewModel.handle.get<String>(HIDE_LOADER_STATE_ID)?.let {

View File

@@ -47,6 +47,7 @@ import com.navi.ap.utils.constants.SUPER_APP_HOME
import com.navi.ap.utils.constants.UNDERSCORE_POLL
import com.navi.ap.utils.constants.noLoaderScreenTypes
import com.navi.ap.utils.logger.ScreenTimeLifecycleLogger
import com.navi.ap.utils.initParameters
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.model.CtaData
import com.navi.base.model.LineItem
@@ -71,10 +72,9 @@ fun ApLauncher(
activity = activity,
renderScaffoldState = viewModel.renderScaffoldState.collectAsState().value
)
handleLoadingState(activity, viewModel)
HandleLoadingState(activity, viewModel)
LaunchedEffect(Unit) {
viewModel.setQueryMap(bundle ?: activity.intent.extras)
initParameters(bundle = bundle ?: activity.intent.extras, viewModel = viewModel)
viewModel.getQueryMap()[APP_PLATFORM_SCREEN_ID] = LAUNCHER_SCREEN
handleRedirection(
activity = activity,
@@ -100,7 +100,7 @@ fun ApLauncher(
}
@Composable
private fun handleLoadingState(activity: ApplicationPlatformActivity, viewModel: ApGenericScreenVM) {
private fun HandleLoadingState(activity: ApplicationPlatformActivity, viewModel: ApGenericScreenVM) {
if (viewModel.createApplicationAndGetCtaResponseState.collectAsState().value == ApplicationIdState.Loading || viewModel.ctaResponseState.collectAsState().value == GetCtaState.Loading) {
ShowShimmerLoader(activity.intent?.extras?.getString(APP_PLATFORM_APPLICATION_TYPE))
}

View File

@@ -0,0 +1,44 @@
package com.navi.ap.utils
import android.os.Bundle
import com.navi.ap.common.ui.ApplicationPlatformActivity
import com.navi.ap.common.viewmodel.LambdaVM
import com.navi.ap.screens.genericscreen.vm.ApGenericScreenVM
import com.navi.ap.utils.constants.APP_PLATFORM_APPLICATION_ID
import com.navi.ap.utils.constants.APP_PLATFORM_APPLICATION_TYPE
import com.navi.ap.utils.constants.APP_PLATFORM_SCREEN_ID
import com.navi.common.utils.Constants
/**
* Put all screen-related utilities in this section for use by other screens.
**/
internal fun initParameters(bundle: Bundle?, viewModel: ApGenericScreenVM) {
val handleBundle = bundle?.getBundle(Constants.ADDITIONAL_PARAMETERS)
handleBundle?.keySet()?.forEach {
viewModel.handle[it] = handleBundle.getString(it)
}
viewModel.setQueryMap(bundle)
initHandleWithPlatformParameters(viewModel)
}
internal fun initHandleWithPlatformParameters(viewModel: ApGenericScreenVM) {
viewModel.handle[APP_PLATFORM_APPLICATION_TYPE] =
viewModel.getQueryMap()[APP_PLATFORM_APPLICATION_TYPE]
viewModel.handle[APP_PLATFORM_APPLICATION_ID] =
viewModel.getQueryMap()[APP_PLATFORM_APPLICATION_ID]
viewModel.handle[APP_PLATFORM_SCREEN_ID] =
viewModel.getQueryMap()[APP_PLATFORM_SCREEN_ID]
}
internal fun initScreenEvent(
activity: ApplicationPlatformActivity,
screenName: String,
viewModel: LambdaVM,
) {
viewModel.eventUtils.setCurrentScreen(activity = activity, screenName = screenName)
}

View File

@@ -51,6 +51,7 @@ enum class FaqType {
enum class LambdaType {
TELCO_RESEND_OTP,
LOAN_OFFER_DETAILS,
REFILL_LOAN_DETAILS,
APPLY_LOAN,
FETCH_RPD_TOKEN,
FETCH_RPD_PAYMENT_METHOD_DETAILS,
@@ -65,5 +66,8 @@ enum class LambdaType {
POST_MANDATE_STATUS,
FETCH_MANDATE_OPTIONS,
POST_PAYMENT_METHOD_STATUS,
VALIDATE_COINS_UPI_ID
VALIDATE_COINS_UPI_ID,
FETCH_EMI_CALENDER_DETAILS,
DOWNLOAD_LOAN_AGREEMENT_AND_SANCTION_LETTER
}

View File

@@ -28,3 +28,6 @@ const val SCREEN_LANDS = "screen_lands"
const val SCREEN_EXIT = "screen_exit"
const val AP_SCREEN_TIME_SPENT_EVENT = "ap_screen_time_spent"
const val ATTR_SCREEN_TIME_SPENT = "screen_time_spent"
const val STATUS = "status"
const val DEV_DOCUMENT_DOWNLOADED_STATUS = "dev_document_downloaded_status"

View File

@@ -13,6 +13,8 @@ const val SELECTED_TENURE_PILL_INDEX = "selected_tenure_pill_index"
const val OFFER_REFERENCE_ID = "offerReferenceId"
const val LOAN_APPLICATION_V2 = "LOAN_APPLICATION_V2"
const val LOAN_SLIDER = "loan_slider"
const val RATE_OF_INTEREST = "rateOfInterest"
const val FIRST_EMI_DUE_DATE = "firstEmiDueDate"
const val TRUE = "true"
const val LAST_SELECTED_TENURE_PILL_INDEX = "2"
const val FETCH_OFFER_DETAILS_SELECTED_LOAN_AMOUNT = "fetchOfferDetails_selectedLoanAmount"
@@ -42,4 +44,7 @@ const val TENURE_IN_MONTHS = "tenureInMonths"
const val ORCHESTRATION_PL = "OPL"
const val COINS_VALIDATE_UPI_ID = "coinsUPIId"
const val DEFAULT_BANKS_SEARCH_QUERY = ""
const val TRIGGER_LOADING_STATE = "trigger_loading_state"
const val TRIGGER_LOADING_STATE = "trigger_loading_state"
const val SUCCESS_CAP = "SUCCESS"
const val FAILURE_CAP = "FAILURE"
const val DOCUMENT_TYPE = "documentType"

View File

@@ -0,0 +1,68 @@
package com.navi.ap.utils.downloader
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.navi.analytics.utils.SCREEN_NAME
import com.navi.ap.utils.constants.DEV_DOCUMENT_DOWNLOADED_STATUS
import com.navi.ap.utils.constants.STATUS
import com.navi.ap.utils.constants.SUCCESS
import com.navi.ap.utils.logApEvent
import com.navi.common.constants.FAILED
import com.navi.common.uitron.model.action.DownloadAction
class DownloaderBroadcastReceiver(
private val downloadManager: DownloadManager,
private val downloadInProgressIds: MutableSet<Long?>,
private val downloadAction: DownloadAction,
private val downloadCallback: DownloadCallback,
) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let { getDownloadStatus(intent = it) }
}
private fun getDownloadStatus(intent: Intent) {
val taskId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (taskId == -1L || downloadInProgressIds.contains(taskId).not()) return
val query = DownloadManager.Query().setFilterById(taskId)
val cursor = downloadManager.query(query) ?: return
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
if (columnIndex == -1) return
handleTaskStatus(taskStatus = cursor.getLong(columnIndex), taskId = taskId)
}
}
private fun handleTaskStatus(taskStatus: Long, taskId: Long) {
when (taskStatus) {
DownloadManager.STATUS_SUCCESSFUL.toLong() -> {
removeTaskAndLog(taskId, SUCCESS)
downloadCallback.onDownloadSuccess(downloadId = taskId)
}
DownloadManager.STATUS_FAILED.toLong() -> {
removeTaskAndLog(taskId, FAILED)
downloadCallback.onDownloadFailure(downloadId = taskId)
}
else -> Unit
}
}
private fun removeTaskAndLog(taskId: Long, status: String) {
downloadInProgressIds.remove(taskId)
logApEvent(
Pair(SCREEN_NAME, downloadAction.config?.screenName.orEmpty()),
Pair(STATUS, status),
Pair(SCREEN_NAME, downloadAction.config?.screenName.orEmpty()),
eventName = DEV_DOCUMENT_DOWNLOADED_STATUS
)
}
}

View File

@@ -0,0 +1,14 @@
package com.navi.ap.utils.downloader
import com.navi.common.uitron.model.action.DownloadAction
interface TaskDownloadManager {
suspend fun startDownload(downloadAction: DownloadAction,downloadCallback: DownloadCallback)
}
interface DownloadCallback {
fun onDownloadSuccess(downloadId : Long)
fun onDownloadFailure(downloadId : Long)
}

View File

@@ -0,0 +1,65 @@
package com.navi.ap.utils.downloader
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import com.navi.common.uitron.model.action.DownloadAction
import dagger.hilt.android.qualifiers.ActivityContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class TaskDownloadManagerImpl @Inject constructor(
@ActivityContext private val context: Context,
private val downloadManager: DownloadManager?,
) : TaskDownloadManager {
private var downloadInProgressIds: MutableSet<Long?> = mutableSetOf()
private var downloadStatusReceiver: BroadcastReceiver? = null
private fun getDownloaderRequest(downloadAction: DownloadAction): DownloadManager.Request =
DownloadManager.Request(Uri.parse(downloadAction.documentUrl)).apply {
setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
downloadAction.config?.documentName
)
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setTitle(downloadAction.config?.documentName)
setAllowedOverMetered(true)
setAllowedOverRoaming(true)
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private fun initReceivers(
downloadAction: DownloadAction,
downloadCallback: DownloadCallback,
) {
if (downloadStatusReceiver != null) return
downloadManager?.let {
downloadStatusReceiver = DownloaderBroadcastReceiver(
downloadManager = downloadManager,
downloadInProgressIds = downloadInProgressIds,
downloadAction = downloadAction,
downloadCallback = downloadCallback
)
}
val downloadCompleteIntentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
context.registerReceiver(downloadStatusReceiver, downloadCompleteIntentFilter)
}
override suspend fun startDownload(
downloadAction: DownloadAction,
downloadCallback: DownloadCallback,
): Unit = withContext(Dispatchers.IO) {
initReceivers(downloadAction, downloadCallback)
val downloadId = downloadManager?.enqueue(getDownloaderRequest(downloadAction))
downloadInProgressIds.add(downloadId)
}
}

View File

@@ -0,0 +1,79 @@
package com.navi.ap.utils.helper
import com.navi.ap.utils.PeriodicTaskScheduler
import com.navi.ap.utils.constants.AP_POLL_INITIAL_DELAY
import com.navi.ap.utils.constants.AP_POLL_INTERVAL
import com.navi.ap.utils.constants.AP_POLL_RETRY_COUNT
import kotlinx.coroutines.sync.Mutex
import orVal
import java.util.concurrent.ConcurrentHashMap
/**
* Use this to schedule multiple task with taskId.
* @param taskId : Represent unique task
**/
class PeriodicTaskSchedulerFacade {
private val mutex = Mutex()
private val taskScheduler: ConcurrentHashMap<String, PeriodicTaskScheduler?> =
ConcurrentHashMap()
internal suspend fun scheduleTask(
taskId: String,
numberOfRetry: Int? = null,
pollInterval: Int? = null,
initialDelay: Int? = null,
onTimeOut: (() -> Unit)? = null,
task: () -> Unit,
) {
mutex.lock()
try {
stopAndRemoveTask(taskId)
schedulePeriodically(
taskId = taskId,
numberOfRetry = numberOfRetry.orVal(AP_POLL_RETRY_COUNT),
pollInterval = pollInterval.orVal(AP_POLL_INTERVAL),
initialDelay = initialDelay.orVal(AP_POLL_INITIAL_DELAY),
onTimeOut = onTimeOut,
task = task
)
} finally {
mutex.unlock()
}
}
internal fun stopTask(taskId: String) {
stopAndRemoveTask(taskId)
}
private fun schedulePeriodically(
taskId: String,
numberOfRetry: Int,
pollInterval: Int,
initialDelay: Int,
onTimeOut: (() -> Unit)? = null,
task: () -> Unit,
) {
val periodicTaskScheduler = PeriodicTaskScheduler(
maxAttempts = numberOfRetry,
taskIntervalSeconds = pollInterval.toLong(),
initialDelaySeconds = initialDelay.toLong(),
onTimeout = {
stopAndRemoveTask(taskId)
onTimeOut?.invoke()
},
) {
task.invoke()
}
taskScheduler[taskId] = periodicTaskScheduler
periodicTaskScheduler.startTask()
}
private fun stopAndRemoveTask(taskId: String) {
taskScheduler[taskId]?.stopTask()
taskScheduler.remove(taskId)
}
}

View File

@@ -1,6 +1,7 @@
package com.navi.ap.utils.injector
import com.navi.ap.utils.readJsonPath
import com.navi.ap.utils.toJson
import org.json.JSONArray
import org.json.JSONObject
@@ -18,16 +19,18 @@ class PathInjector<U, V>() : BasePathInjector<U, V>() {
currentObject.put(key, JSONArray(placeHolderValue.toString()))
}
}
JSON_TYPE.JSONObject -> {
val valueObject = currentObject.optJSONObject(key)
val placeHolderKeyName =
valueObject?.optString(JSON_PATH_PLACE_HOLDER_KEY_NAME).orEmpty()
injectValues(
placeHolderKeyName = placeHolderKeyName,
currentObject = currentObject,
key = key
)
if (placeHolderKeyName.startsWith('$')) {
val jsonPath = placeHolderKeyName.trim()
val placeHolderValue = readJsonPath(jsonResponse.toString(), jsonPath)
currentObject.put(key, JSONObject(placeHolderValue.toJson()))
}
}
JSON_TYPE.JSONString -> {
val placeHolderKeyName = currentObject.optString(key)
injectValues(

View File

@@ -31,5 +31,6 @@ data class CtaData(
@SerializedName("requestCode") val requestCode: Int? = null,
var bundle: Bundle? = null,
@SerializedName("buttonState") val buttonState: String? = null,
@SerializedName("needsResult") val needsResult: Boolean? = null
@SerializedName("needsResult") val needsResult: Boolean? = null,
@SerializedName("refreshScreen") val refreshScreen: Boolean? = null
) : NaviClickAction(), Parcelable

View File

@@ -16,6 +16,7 @@ import kotlinx.parcelize.Parcelize
open class LineItem(
@SerializedName("key") val key: String? = null,
@SerializedName("value") var value: String? = null,
@SerializedName("expressionValue") val expressionValue : String? = null,
@SerializedName("subtitle") val subtitle: String? = null,
@SerializedName("subValue") val subValue: String? = null,
@SerializedName("iconCode") val iconCode: String? = null,

View File

@@ -15,6 +15,7 @@ import com.navi.analytics.utils.NaviTrackEvent
import com.navi.analytics.utils.SCREEN_NAME
import com.navi.analytics.utils.SCREEN_NAME_CAMEL_CASE
import com.navi.common.utils.Constants.APP_PLATFORM_APPLICATION_ID
import com.navi.common.utils.Constants.APP_PLATFORM_APPLICATION_TYPE
import com.navi.uitron.model.data.ActionDetails
import com.navi.uitron.model.data.UiTronAction
import getInputId
@@ -65,6 +66,9 @@ data class AnalyticsActionV2(
handle.get<String>(APP_PLATFORM_APPLICATION_ID)?.let {
properties[APP_PLATFORM_APPLICATION_ID] = it
}
handle.get<String>(APP_PLATFORM_APPLICATION_TYPE)?.let {
properties[APP_PLATFORM_APPLICATION_TYPE] = it
}
NaviTrackEvent.trackEvent(
eventName = event,
eventValues = properties,

View File

@@ -2,10 +2,13 @@ package com.navi.common.uitron.model.action
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.model.CtaData
import com.navi.common.utils.log
import com.navi.uitron.model.data.ActionDetails
import com.navi.uitron.model.data.UiTronAction
import kotlinx.parcelize.Parcelize
import org.mvel2.MVEL
@Parcelize
data class CtaAction(
@@ -16,6 +19,31 @@ data class CtaAction(
actionDetails: ActionDetails
) {
val action = actionDetails.uiTronAction as CtaAction
action.ctaData?.additionalParameters?.filter { it.expressionValue != null }?.forEach {
it.value = evaluateExpression(expression = it.expressionValue, actionDetails = actionDetails)
}
actionDetails.actionCallbackFlow?.emit(action)
}
private fun evaluateExpression(expression: String?, actionDetails: ActionDetails): String? {
if (expression == null) return null
return try {
MVEL.eval(
expression,
mapOf(
Pair(ExecuteActionsCorrespondingToKey.ACTION, this),
Pair(ExecuteActionsCorrespondingToKey.ACTION_DETAILS, actionDetails)
)
).toString()
} catch (e: Exception) {
e.log()
NaviTrackEvent.trackEvent(
eventName = "dev_ExecuteActionsCorrespondingToKey_action",
eventValues = mapOf(
Pair("mvelExpression", expression),
)
)
null
}
}
}

View File

@@ -0,0 +1,36 @@
package com.navi.common.uitron.model.action
import android.os.Parcelable
import com.navi.uitron.model.data.ActionDetails
import com.navi.uitron.model.data.UiTronAction
import com.navi.uitron.model.data.UiTronActionData
import kotlinx.parcelize.Parcelize
@Parcelize
open class DownloadAction(
val documentUrl: String? = null,
val config: Config? = null,
val downloadAction: Actions? = null,
) : UiTronAction() {
@Parcelize
data class Config(
val screenName: String? = null,
val documentName: String? = null,
val allowedOverMetered: Boolean = true,
val allowedOverRoaming: Boolean = true,
) : Parcelable
@Parcelize
data class Actions(
val onStart: UiTronActionData? = null,
val onCompleted: UiTronActionData? = null,
val onFailed: UiTronActionData? = null,
) : Parcelable
override suspend fun manageAction(actionDetails: ActionDetails) {
actionDetails.actionCallbackFlow?.emit(this)
}
}

View File

@@ -0,0 +1,25 @@
package com.navi.common.uitron.model.action
import android.os.Parcelable
import com.navi.uitron.model.data.ActionDetails
import com.navi.uitron.model.data.UiTronAction
import kotlinx.parcelize.Parcelize
@Parcelize
data class SystemUiAction(
val isStatusBarVisible: Boolean? = true,
val isNavigationBarVisible: Boolean? = true,
val statusBarColor: SystemUiColor? = null,
val navigationBarColor: SystemUiColor? = null,
) : UiTronAction(), Parcelable {
@Parcelize
data class SystemUiColor(
val color: String? = null
) : Parcelable
override suspend fun manageAction(actionDetails: ActionDetails) {
actionDetails.actionCallbackFlow?.emit(this)
}
}

View File

@@ -11,8 +11,10 @@ import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.navi.common.uitron.model.action.AnalyticsActionV2
import com.navi.common.uitron.model.action.CtaAction
import com.navi.common.uitron.model.action.DownloadAction
import com.navi.common.uitron.model.action.ExecuteActionsCorrespondingToKey
import com.navi.common.uitron.model.action.LaunchIntentAction
import com.navi.common.uitron.model.action.SystemUiAction
import com.navi.common.uitron.model.action.ThirdPartySdkAction
import com.navi.common.uitron.model.action.UpdateStateHandleActionV2
import com.navi.common.uitron.model.action.UpdateViewStateActionV2
@@ -68,6 +70,10 @@ class UiTronActionSerializer : BaseUiTronActionSerializer() {
context?.serialize(src as AnalyticsActionV2, AnalyticsActionV2::class.java)
ApActionType.LaunchIntentAction.name ->
context?.serialize(src as LaunchIntentAction, LaunchIntentAction::class.java)
ApActionType.DownloadAction.name ->
context?.serialize(src as DownloadAction, DownloadAction::class.java)
ApActionType.SystemUiAction.name ->
context?.serialize(src as SystemUiAction, SystemUiAction::class.java)
else -> super.serialize(src, typeOfSrc, context)
}
}

View File

@@ -16,5 +16,7 @@ enum class ApActionType {
ExecuteActionsCorrespondingToKey,
AnalyticsActionV2,
LaunchIntentAction,
UpdateDataViaHandleAction
UpdateDataViaHandleAction,
DownloadAction,
SystemUiAction
}

View File

@@ -196,4 +196,9 @@
<string name="navi_app_common_channel_name">Navi</string>
<string name="navi_app_custom_channel_name">Miscellaneous</string>
<string name="taking_longer_time_desc">We\'re experiencing some delays in processing your request, but we\'re working hard to get it done as soon as possible. Thank you for your patience.</string>
<string name="document_downloading">Your document is being downloaded</string>
<string name="failed_document_download">Failed to download document, need write storage permission</string>
<string name="download_failed">Download failed</string>
<string name="start_download">Starting Download…</string>
</resources>