TP-87434 | Introduce data ingestion check before web redirection (#13059)

Co-authored-by: Kishan Kumar <kishan.kumar@navi.com>
This commit is contained in:
Sanjay P
2024-10-14 21:04:33 +05:30
committed by GitHub
parent a16fa7e042
commit 5790c0e506
9 changed files with 222 additions and 54 deletions

View File

@@ -5940,6 +5940,9 @@ class NaviAnalytics private constructor() {
const val REDIRECTION_TO_WEB_STARTED = "redirection_to_web_started"
const val REDIRECTION_TO_WEB_FAILED = "redirection_to_web_failed"
const val REDIRECTION_TO_WEB_SUCCESS = "redirection_to_web_success"
const val REDIRECTION_TO_WEB_SUCCESS_ON_TIME_OUT = "redirection_to_web_success_on_time_out"
const val REDIRECTION_TO_WEB_SUCCESS_ON_DATA_RECEIVED =
"redirection_to_web_success_on_data_received"
const val REDIRECTION_TO_WEB_AUTH_API_FAILED = "redirection_to_web_auth_api_failed"
const val REDIRECTION_TO_WEB_DATA_INGESTION_INITIATED =
"redirection_to_web_data_ingestion_initiated"

View File

@@ -7,11 +7,14 @@
package com.naviapp.webredirection.presentation.activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
@@ -26,12 +29,16 @@ import com.navi.common.model.ModuleNameV2
import com.navi.common.ui.activity.BaseActivity
import com.navi.common.ui.errorview.FullScreenErrorComposeView
import com.navi.common.useruploaddata.model.IngestionStatusType
import com.navi.common.useruploaddata.model.PreSignedUrlListResponse
import com.navi.common.useruploaddata.model.UserDataUploadCallbackResponse
import com.navi.common.useruploaddata.viewmodel.UserDataViewModel
import com.navi.common.utils.Constants.STATUS
import com.navi.common.utils.Constants.URL
import com.navi.common.utils.Constants.VERTICAL_TYPE
import com.navi.common.utils.log
import com.navi.common.utils.observeWithTimeout
import com.navi.payment.nativepayment.tribute.NaviPaymentWebBridge
import com.naviapp.R
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_AUTH_API_FAILED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_DATA_INGESTION_FAILED
@@ -41,6 +48,8 @@ import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_DA
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_FAILED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_STARTED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_SUCCESS
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_SUCCESS_ON_DATA_RECEIVED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_WEB_SUCCESS_ON_TIME_OUT
import com.naviapp.common.navigator.NaviDeepLinkNavigator
import com.naviapp.home.dashboard.ui.compose.loansTab.LoansTabContentShimmer
import com.naviapp.home.dashboard.ui.compose.loansTab.RenderWebView
@@ -97,7 +106,6 @@ class WebRedirectionActivity : BaseActivity() {
initialise()
setContent {
InitialiseContent(
redirectToExternalWeb = ::redirectToExternalWeb,
handleNavigation = ::handleNavigation,
handleException = ::handleException
)
@@ -105,20 +113,51 @@ class WebRedirectionActivity : BaseActivity() {
initialiseDataIngestion()
webRedirectionVM.fetchTemporarySessionToken()
observeWebRedirectionNavigation()
observeSmsUploadDataResponse()
}
private fun observeSmsUploadDataResponse() {
userDataViewModel.preSignedUrlList.observeWithTimeout(
lifecycleOwner = this,
timeout =
FirebaseRemoteConfigHelper.getLong(
DATA_INGESTION_TIMEOUT,
defaultValue = DATA_INGESTION_TIMEOUT_DEFAULT_VALUE
),
onTimeout = {
NaviTrackEvent.trackEvent(eventName = REDIRECTION_TO_WEB_SUCCESS_ON_TIME_OUT)
handleRedirection(PreSignedUrlListResponse(null))
},
observer = { preSignedUrlResponse ->
NaviTrackEvent.trackEvent(
eventName = REDIRECTION_TO_WEB_SUCCESS_ON_DATA_RECEIVED,
eventValues =
mapOf(PRE_SIGNED_URL_LIST_RESPONSE to preSignedUrlResponse.toString())
)
handleRedirection(preSignedUrlResponse)
}
)
}
private fun handleRedirection(preSignedUrlResponse: PreSignedUrlListResponse?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
webRedirectionVM.screenState.collect {
webRedirectionVM.startRedirectionProcess(preSignedUrlResponse)
}
}
}
}
private fun observeWebRedirectionNavigation() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
webRedirectionVM.webRedirectionCtaData.collect { ctaData ->
ctaData?.let {
trackEvent(NAVIGATING_TO_WEB_BROWSER)
NaviDeepLinkNavigator.navigateTo(
activity = this@WebRedirectionActivity,
ctaData = it,
finish = true
)
}
webRedirectionVM.webPageUrl.collect { webPageUrl ->
NaviTrackEvent.trackEvent(
eventName = REDIRECTION_TO_WEB_SUCCESS,
eventValues = mapOf(URL to webPageUrl.toString())
)
webPageUrl?.let { url -> launchWebPageInChrome(url) }
}
}
}
@@ -136,7 +175,7 @@ class WebRedirectionActivity : BaseActivity() {
businessVertical = ModuleNameV2.PL.name,
isOpLifecycleBound = false,
needTohandleIngestionPolling = false,
callback = { timeTaken, userUploadDataResponse ->
callback = { timeTaken, _, userUploadDataResponse ->
userUploadDataResponse.ingestionStatusList?.ingestionStatusList.let {
ingestionStatusList ->
if (
@@ -200,7 +239,6 @@ class WebRedirectionActivity : BaseActivity() {
@Composable
private fun InitialiseContent(
redirectToExternalWeb: (ctaData: CtaData) -> Unit,
handleNavigation: (CtaData) -> Unit,
handleException: (Throwable) -> Unit
) {
@@ -209,15 +247,6 @@ class WebRedirectionActivity : BaseActivity() {
webRedirectionVM.webRedirectionPlatform.collectAsStateWithLifecycle()
val data by webRedirectionVM.screenState.collectAsStateWithLifecycle()
val webRedirectionData by webRedirectionVM.webRedirectionData.collectAsStateWithLifecycle()
LaunchedEffect(key1 = data) {
if (
data is UiState.Success && webRedirectionPlatform != WebRedirectionPlatform.WEBVIEW
) {
trackEvent(eventName = REDIRECTION_TO_WEB_SUCCESS)
redirectToExternalWeb((data as UiState.Success).ctaData)
}
}
when (webRedirectionPlatform) {
WebRedirectionPlatform.WEBVIEW ->
InternalWebViewScreen(
@@ -299,10 +328,6 @@ class WebRedirectionActivity : BaseActivity() {
}
}
private fun redirectToExternalWeb(ctaData: CtaData) {
webRedirectionVM.startRedirectionProcess()
}
private fun handleNavigation(ctaData: CtaData) =
NaviDeepLinkNavigator.navigateTo(
activity = this,
@@ -340,6 +365,38 @@ class WebRedirectionActivity : BaseActivity() {
)
}
/**
* Opens a web page in the default web browser, specifically targeting Google Chrome.
*
* This function attempts to launch an intent that directs the user to a specified URL. If
* Google Chrome is not installed on the device, it falls back to any available web browser.
*
* @param url The URL of the web page to be opened. It should be a valid URL format (e.g.,
* "https://www.example.com").
* @throws ActivityNotFoundException if no applications can handle the intent to view the URL.
*
* After attempting to open the URL, this function calls `finish()`, which will close the
* current activity.
*/
private fun launchWebPageInChrome(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { setPackage(CHROME_PACKAGE) }
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
intent.setPackage(null)
e.log()
try {
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, getString(R.string.no_browser_found), Toast.LENGTH_LONG).show()
e.log()
}
} finally {
finish()
}
}
override val screenName: String
get() = NaviAnalytics.WEB_REDIRECTION_SCREEN
@@ -349,5 +406,9 @@ class WebRedirectionActivity : BaseActivity() {
companion object {
const val WEB_REDIRECTION_SESSION_ID = "WEB_REDIRECTION_SESSION_ID"
const val NAVIGATING_TO_WEB_BROWSER = "NAVIGATING_TO_WEB_BROWSER"
const val DATA_INGESTION_TIMEOUT = "DATA_INGESTION_TIMEOUT"
const val DATA_INGESTION_TIMEOUT_DEFAULT_VALUE = 3000L
const val CHROME_PACKAGE = "com.android.chrome"
const val PRE_SIGNED_URL_LIST_RESPONSE = "pre_signed_url_list_response"
}
}

View File

@@ -17,12 +17,16 @@ import com.navi.base.model.LineItem
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.BaseUtils
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.base.utils.orFalse
import com.navi.common.constants.APP_VERSION_CODE
import com.navi.common.constants.OS_VERSION
import com.navi.common.constants.OS_VERSION_NAME
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.WEB_REDIRECTION_DEFAULT_DELAY_IN_MILLIS
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.WEB_REDIRECTION_SMS_UPLOADED_DELAY_IN_MILLIS
import com.navi.common.network.models.GenericErrorResponse
import com.navi.common.useruploaddata.model.IngestionType
import com.navi.common.useruploaddata.model.PreSignedUrlListResponse
import com.navi.common.utils.Constants.URL
import com.navi.common.utils.getSessionId
import com.navi.common.utils.isValidResponse
@@ -31,6 +35,7 @@ import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.navigator.NaviDeepLinkNavigator.WEB_URL
import com.naviapp.manager.usecase.UserDataUploadWorkerUseCase
import com.naviapp.registration.helper.isReadSmsPermissionGranted
import com.naviapp.utils.Constants
import com.naviapp.utils.buildUrlWithParameters
import com.naviapp.utils.generateRandomString
import com.naviapp.utils.generateSHA256Hash
@@ -78,8 +83,8 @@ constructor(
private val _webRedirectionData = MutableStateFlow<WebRedirectionData?>(null)
val webRedirectionData = _webRedirectionData.asStateFlow()
private val _webRedirectionCtaData = MutableStateFlow<CtaData?>(null)
val webRedirectionCtaData = _webRedirectionCtaData.asStateFlow()
private val _webPageUrl = MutableStateFlow<String?>(null)
val webPageUrl = _webPageUrl.asStateFlow()
private var businessUnit: String? = null
@@ -110,26 +115,49 @@ constructor(
}
}
fun startRedirectionProcess() {
fun startRedirectionProcess(preSignedUrlListResponse: PreSignedUrlListResponse?) {
viewModelScope.launch(Dispatchers.IO) {
val webRedirectionSessionId = "${businessUnit}_$WEB_REDIRECTION_SESSION_ID"
if (PreferenceManager.getStringPreference(webRedirectionSessionId) != getSessionId()) {
delay(getWebRedirectionDelayInMillis())
getSessionId()?.let {
PreferenceManager.setStringPreference(webRedirectionSessionId, it)
if (isSuccessAndRedirectToNonWebView()) {
val webRedirectionDelay =
calculateRedirectDelayForSmsUpload(preSignedUrlListResponse)
val webRedirectionSessionId = "${businessUnit}_$WEB_REDIRECTION_SESSION_ID"
if (
PreferenceManager.getStringPreference(webRedirectionSessionId) != getSessionId()
) {
delay(webRedirectionDelay)
getSessionId()?.let {
PreferenceManager.setStringPreference(webRedirectionSessionId, it)
}
}
}
if (screenState.value is UiState.Success) {
_webRedirectionCtaData.value = (screenState.value as UiState.Success).ctaData
_webPageUrl.value = extractWebPageUrl()
}
}
}
private fun getWebRedirectionDelayInMillis(): Long {
val webRedirectionDelayInMillis =
private fun extractWebPageUrl(): String? {
val ctaData = (screenState.value as UiState.Success).ctaData
return ctaData.parameters?.firstOrNull { it.key == Constants.URL }?.value
}
private fun isSuccessAndRedirectToNonWebView() =
screenState.value is UiState.Success &&
webRedirectionPlatform.value != WebRedirectionPlatform.WEBVIEW
private fun calculateRedirectDelayForSmsUpload(
preSignedUrlListResponse: PreSignedUrlListResponse?
): Long {
val smsUploadStatus =
preSignedUrlListResponse
?.preSignedUrlList
?.find { it.ingestionType == IngestionType.SMS.name }
?.upload
.orFalse()
return if (smsUploadStatus) {
FirebaseRemoteConfigHelper.getLong(WEB_REDIRECTION_SMS_UPLOADED_DELAY_IN_MILLIS)
} else {
webRedirectionData.value?.webRedirectionDelayInMillis
?: FirebaseRemoteConfigHelper.getLong(WEB_REDIRECTION_DEFAULT_DELAY_IN_MILLIS)
return webRedirectionDelayInMillis
}
}
private fun generateUrlForWebRedirect(codeVerifier: String, token: String): String? {

View File

@@ -523,4 +523,5 @@
<string name="scan_pay">Scan &amp; pay</string>
<string name="pl_web_redirection_title">For a better experience, redirecting to Navi official webpage...</string>
<string name="pl_web_redirection_subtitle">https://navi-finserv.com</string>
<string name="no_browser_found">No web browser found. Please install a browser app, such as Google Chrome or Mozilla Firefox, from the Google Play Store to continue.</string>
</resources>

View File

@@ -115,7 +115,7 @@ fun HandleUploadDataAction(
uploadAppsDataToS3,
businessVertical.orEmpty(),
numberOfRetriesLeft,
) { _, userDataUploadCallbackResponse ->
) { _, _, userDataUploadCallbackResponse ->
val needIngestionPolling =
FirebaseRemoteConfigHelper.getBoolean(
FirebaseRemoteConfigHelper.UW_INGESTION_POLLING,

View File

@@ -214,6 +214,8 @@ object FirebaseRemoteConfigHelper {
const val DEEPLINK_RESOLVER_WAIT_TIME = "DEEPLINK_RESOLVER_WAIT_TIME"
const val WEB_REDIRECTION_DEFAULT_DELAY_IN_MILLIS = "WEB_REDIRECTION_DEFAULT_DELAY_IN_MILLIS"
const val WEB_REDIRECTION_SMS_UPLOADED_DELAY_IN_MILLIS =
"WEB_REDIRECTION_SMS_UPLOADED_DELAY_IN_MILLIS"
const val RETRY_INTERCEPTOR_ENABLED = "RETRY_INTERCEPTOR_ENABLED"
const val ROOT_CA_ADDITION_TARGET_SDK = "ROOT_CA_ADDITION_TARGET_SDK"

View File

@@ -81,7 +81,9 @@ object PermissionUtil {
needTohandleIngestionPolling: Boolean = false,
callback:
((
timeTakenToUpload: Long, userUploadDataResponse: UserDataUploadCallbackResponse
timeTakenToUpload: Long,
preSignedUrlListResponse: PreSignedUrlListResponse?,
userUploadDataResponse: UserDataUploadCallbackResponse
) -> Unit)? =
null
) {
@@ -116,7 +118,9 @@ object PermissionUtil {
scope: CoroutineScope,
callback:
(
timeTakenToUpload: Long, userUploadDataResponse: UserDataUploadCallbackResponse
timeTakenToUpload: Long,
preSignedUrlListResponse: PreSignedUrlListResponse?,
userUploadDataResponse: UserDataUploadCallbackResponse
) -> Unit,
needTohandleIngestionPolling: Boolean
) {
@@ -130,7 +134,11 @@ object PermissionUtil {
while ((response.error != null || !response.errors.isNullOrEmpty()) && retryCount <= 4) {
if (retryCount == 4) {
withContext(Dispatchers.Main) {
callback.invoke(0, UserDataUploadCallbackResponse(null, null))
callback.invoke(
0,
PreSignedUrlListResponse(null),
UserDataUploadCallbackResponse(null, null)
)
}
return
}
@@ -168,7 +176,11 @@ object PermissionUtil {
uploadedResponseData
)
withContext(Dispatchers.Main) {
callback.invoke(timeTakenToUploadAppData, userDataUploadCallbackResponse)
callback.invoke(
timeTakenToUploadAppData,
response.data,
userDataUploadCallbackResponse
)
}
if (needTohandleIngestionPolling) {
UploadDataPollingUtil().handleUploadDataResponse(userDataUploadCallbackResponse)
@@ -197,7 +209,7 @@ object PermissionUtil {
uploadedResponse.data
)
withContext(Dispatchers.Main) {
callback.invoke(-1, userDataUploadCallbackResponse)
callback.invoke(-1, response.data, userDataUploadCallbackResponse)
}
if (needTohandleIngestionPolling) {
UploadDataPollingUtil()

View File

@@ -16,6 +16,7 @@ import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.orFalse
import com.navi.common.model.DeviceDetail
import com.navi.common.useruploaddata.model.IngestionStatusType
import com.navi.common.useruploaddata.model.PreSignedUrlListResponse
import com.navi.common.useruploaddata.model.UserDataUploadCallbackResponse
import com.navi.common.useruploaddata.repository.UserDataRepository
import com.navi.common.useruploaddata.utils.PermissionUtil
@@ -36,6 +37,10 @@ class UserDataViewModel(application: Application) : AndroidViewModel(application
val deviceDataSentStatus: LiveData<Boolean>
get() = _deviceDataSentStatus
private val _preSignedUrlList = MutableLiveData<PreSignedUrlListResponse>()
val preSignedUrlList: LiveData<PreSignedUrlListResponse>
get() = _preSignedUrlList
fun sendUserDataToAws(
uploadSms: Boolean = false,
uploadAppUsedInfo: Boolean = false,
@@ -44,7 +49,9 @@ class UserDataViewModel(application: Application) : AndroidViewModel(application
needTohandleIngestionPolling: Boolean = false,
callback:
((
timeTakenToUpload: Long, userUploadDataResponse: UserDataUploadCallbackResponse
timeTakenToUpload: Long,
preSignedUrlListResponse: PreSignedUrlListResponse?,
userUploadDataResponse: UserDataUploadCallbackResponse
) -> Unit)? =
null
) {
@@ -56,9 +63,13 @@ class UserDataViewModel(application: Application) : AndroidViewModel(application
uploadAppUsedInfo,
businessVertical,
needTohandleIngestionPolling
) { timeTakenToUpload, userDataUploadCallbackResponse ->
) { timeTakenToUpload, presignedUrlResponse, userDataUploadCallbackResponse ->
_userDataSentStatus.value = userDataUploadCallbackResponse
callback?.invoke(timeTakenToUpload, userDataUploadCallbackResponse)
callback?.invoke(
timeTakenToUpload,
presignedUrlResponse,
userDataUploadCallbackResponse
)
}
Timber.d("SmsTask, ContactsTask and Device Details Task Done")
}
@@ -71,9 +82,12 @@ class UserDataViewModel(application: Application) : AndroidViewModel(application
numberOfRetriesLeft: Int = 0,
isOpLifecycleBound: Boolean = true,
needTohandleIngestionPolling: Boolean = false,
dataUploadCallback: (PreSignedUrlListResponse?) -> Unit = {},
callback:
((
timeTakenToUpload: Long, userUploadDataResponse: UserDataUploadCallbackResponse
timeTakenToUpload: Long,
preSignedUrlListResponse: PreSignedUrlListResponse?,
userUploadDataResponse: UserDataUploadCallbackResponse
) -> Unit)? =
null
) {
@@ -82,8 +96,8 @@ class UserDataViewModel(application: Application) : AndroidViewModel(application
uploadAppsDataToS3,
businessVertical,
isOpLifecycleBound,
needTohandleIngestionPolling
) { timeTakenToUpload, userDataUploadCallbackResponse ->
needTohandleIngestionPolling,
) { timeTakenToUpload, presignedUrlResponse, userDataUploadCallbackResponse ->
if (
numberOfRetriesLeft > 0 &&
userDataUploadCallbackResponse.ingestionStatusList
@@ -98,11 +112,17 @@ class UserDataViewModel(application: Application) : AndroidViewModel(application
numberOfRetriesLeft - 1,
isOpLifecycleBound,
needTohandleIngestionPolling,
dataUploadCallback,
callback
)
} else {
_preSignedUrlList.value = presignedUrlResponse
_userDataSentStatus.value = userDataUploadCallbackResponse
callback?.invoke(timeTakenToUpload, userDataUploadCallbackResponse)
callback?.invoke(
timeTakenToUpload,
presignedUrlResponse,
userDataUploadCallbackResponse
)
}
}
}

View File

@@ -1,15 +1,21 @@
/*
*
* * Copyright © 2019-2022 by Navi Technologies Limited
* * Copyright © 2019-2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.common.utils
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
fun <T> LiveData<T>.observeNullable(owner: LifecycleOwner, observer: (T?) -> Unit) =
observe(owner, Observer<T> { v -> observer.invoke(v) })
@@ -17,3 +23,38 @@ fun <T> LiveData<T>.observeNullable(owner: LifecycleOwner, observer: (T?) -> Uni
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, observer: (T) -> Unit) {
this.observe(owner, Observer { it?.let { observer(it) } })
}
fun <T> LiveData<T>.observeWithTimeout(
lifecycleOwner: LifecycleOwner,
timeout: Long,
onTimeout: () -> Unit,
observer: (T) -> Unit
) {
var localOnTimeOut: (() -> Unit)? = onTimeout
val localObserver =
Observer<T> { value ->
value?.let {
observer(it)
localOnTimeOut = null
}
}
this.observe(lifecycleOwner, localObserver)
val job =
lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
delay(timeout)
withContext(Dispatchers.Main.immediate) {
this@observeWithTimeout.removeObserver(localObserver)
localOnTimeOut?.invoke()
}
}
lifecycleOwner.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
job.cancel()
removeObserver(localObserver)
}
}
)
}