NTP-60715 | auto open contact permission pop-up (#16011)

This commit is contained in:
Shaurya Rehan
2025-05-02 16:51:13 +05:30
committed by GitHub
parent 7698eb52aa
commit 61f2e1b62c
8 changed files with 110 additions and 27 deletions

View File

@@ -11,6 +11,7 @@ import androidx.test.core.app.ApplicationProvider
import app.cash.turbine.test
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.ResourceProvider
import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.analytics.NaviPayToContacts
import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity
@@ -18,7 +19,6 @@ import com.navi.pay.common.model.config.NaviPayDefaultConfig
import com.navi.pay.common.model.view.NaviPaySessionHelper
import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.ValidateVpaUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
import com.navi.pay.common.utils.FrequentOrdersHelper
import com.navi.pay.entry.NaviPayActivityDataProvider
import com.navi.pay.management.paytocontacts.PhoneContactManager
@@ -39,7 +39,6 @@ import org.junit.Test
@HiltAndroidTest
class PayToContactsViewModelTest : NaviPayAndroidTest() {
private var deviceInfoProvider: DeviceInfoProvider = mockk()
private var contactManager: PhoneContactManager = mockk()
private var naviPayNetworkConnectivity: NaviPayNetworkConnectivity = mockk()
private var naviPayConfigUseCase: NaviPayConfigUseCase = mockk()
@@ -48,6 +47,7 @@ class PayToContactsViewModelTest : NaviPayAndroidTest() {
private var resourceProvider: ResourceProvider = mockk()
private var validateVpaUseCase: ValidateVpaUseCase = mockk()
private var naviPayActivityDataProvider: NaviPayActivityDataProvider = mockk()
private var litmusExperimentsUseCase: LitmusExperimentsUseCase = mockk()
private val naviPayAnalytics: NaviPayToContacts = mockk(relaxed = true)
private lateinit var viewModel: PayToContactsViewModel
@@ -68,10 +68,10 @@ class PayToContactsViewModelTest : NaviPayAndroidTest() {
coEvery { resourceProvider.getString(any()) } returns ""
coEvery { frequentOrdersHelper.getFrequentOrderList(any<Int>()) } returns emptyList()
coEvery { naviPaySessionHelper.getNaviPaySessionAttributes() } returns emptyMap()
coEvery { litmusExperimentsUseCase.execute(experimentName = any()) } returns null
viewModel =
PayToContactsViewModel(
deviceInfoProvider = deviceInfoProvider,
contactManager = contactManager,
naviPayNetworkConnectivity = naviPayNetworkConnectivity,
naviPayConfigUseCase = naviPayConfigUseCase,
@@ -81,6 +81,7 @@ class PayToContactsViewModelTest : NaviPayAndroidTest() {
validateVpaUseCase = validateVpaUseCase,
naviPayActivityDataProvider = naviPayActivityDataProvider,
naviPayAnalytics = naviPayAnalytics,
litmusExperimentsUseCase = litmusExperimentsUseCase,
)
}

View File

@@ -23,6 +23,8 @@ sealed interface NaviPermissionResult {
// Here user has denied permission so app can show a UI before asking for permission again.
data object ShowRationale : NaviPermissionResult
data object None : NaviPermissionResult
}
fun handlePermissionResult(

View File

@@ -329,6 +329,8 @@ fun QrScannerScreenContent(
naviPaySessionAttributes = qrScannerViewModel.getNaviPaySessionAttributes(),
)
}
else -> {}
}
}

View File

@@ -145,6 +145,11 @@ fun PayToContactsScreen(
payToContactsViewModel.frequentOrdersHeading.collectAsStateWithLifecycle()
val shouldShowYourContactsTitle by
payToContactsViewModel.shouldShowYourContactsTitle.collectAsStateWithLifecycle()
val isAutoOpenContactPermissionExperimentEnabled by
payToContactsViewModel.isAutoOpenContactPermissionExperimentEnabled
.collectAsStateWithLifecycle()
val showPermissionWidget by
payToContactsViewModel.showPermissionWidget.collectAsStateWithLifecycle()
val onBackClick = {
payToContactsViewModel.cancelPaymentRequest()
@@ -226,6 +231,7 @@ fun PayToContactsScreen(
naviPaySessionAttributes =
payToContactsViewModel.getNaviPaySessionAttributes()
)
payToContactsViewModel.updatePermissionResult(permissionResult = it)
}
NaviPermissionResult.HardDenied -> {
naviPayAnalytics.onPayToContactsPermissionDenied(
@@ -233,11 +239,14 @@ fun PayToContactsScreen(
naviPaySessionAttributes =
payToContactsViewModel.getNaviPaySessionAttributes(),
)
naviPayActivity.launchPermissionSettingsScreen()
naviPayAnalytics.onAppSettingsScreenLaunched(
naviPaySessionAttributes =
payToContactsViewModel.getNaviPaySessionAttributes()
)
payToContactsViewModel.updatePermissionResult(permissionResult = it)
if (payToContactsViewModel.isPermissionLaunchedFromAllowClick) {
naviPayActivity.launchPermissionSettingsScreen()
naviPayAnalytics.onAppSettingsScreenLaunched(
naviPaySessionAttributes =
payToContactsViewModel.getNaviPaySessionAttributes()
)
}
}
NaviPermissionResult.ShowRationale -> {
naviPayAnalytics.onPayToContactsPermissionDenied(
@@ -245,11 +254,14 @@ fun PayToContactsScreen(
naviPaySessionAttributes =
payToContactsViewModel.getNaviPaySessionAttributes(),
)
payToContactsViewModel.updatePermissionResult(permissionResult = it)
}
else -> {}
}
}
val onAllowPermissionButtonClicked = {
payToContactsViewModel.isPermissionLaunchedFromAllowClick = true
readContactsPermissionsState.launchMultiplePermissionRequest()
}
@@ -294,7 +306,7 @@ fun PayToContactsScreen(
LaunchedEffect(readContactsPermissionsState.allPermissionsGranted) {
if (readContactsPermissionsState.allPermissionsGranted) {
payToContactsViewModel.updateContactPermissionStatus(true)
payToContactsViewModel.updateContactPermissionStatus(isContactPermissionGranted = true)
payToContactsViewModel.fetchContacts()
} else {
payToContactsViewModel.updateContactPermissionStatus(false)
@@ -304,6 +316,15 @@ fun PayToContactsScreen(
isContactListEmpty = contactList.isEmpty(),
isFrequentOrderListEmpty = filteredFrequentOrdersList.isEmpty(),
)
if (
isAutoOpenContactPermissionExperimentEnabled &&
!payToContactsViewModel.isPermissionPopupSeenOnLanded
) {
payToContactsViewModel.onPermissionPopupSeenOnLanded()
payToContactsViewModel.isPermissionLaunchedFromAllowClick = false
readContactsPermissionsState.launchMultiplePermissionRequest()
}
}
}
@@ -354,6 +375,7 @@ fun PayToContactsScreen(
focusManager.clearFocus()
payToContactsViewModel.onHelpCtaClicked()
},
showPermissionWidget = showPermissionWidget,
)
}
}
@@ -383,6 +405,7 @@ fun RenderPayToContactsScreen(
invalidState: WarningErrorInfoState,
isEmptyState: Boolean,
onHelpCtaClicked: () -> Unit,
showPermissionWidget: Boolean,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
@@ -506,6 +529,7 @@ fun RenderPayToContactsScreen(
newContact = newContact,
shouldShowYourContactsTitle = shouldShowYourContactsTitle,
scrollState = scrollState,
showPermissionWidget = showPermissionWidget,
)
}
},
@@ -552,6 +576,7 @@ private fun PayToContactScreenScaffoldContent(
newContact: PhoneContactEntity?,
shouldShowYourContactsTitle: Boolean,
scrollState: LazyListState,
showPermissionWidget: Boolean,
) {
LazyColumn(
modifier = modifier.fillMaxHeight(),
@@ -627,7 +652,7 @@ private fun PayToContactScreenScaffoldContent(
)
}
}
} else {
} else if (showPermissionWidget) {
item { PayToContactsPermissionView(onPrimaryButtonClicked = onPrimaryButtonClicked) }
}

View File

@@ -13,10 +13,10 @@ import com.navi.base.model.CtaData
import com.navi.base.utils.ResourceProvider
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.common.constants.EMPTY
import com.navi.common.extensions.or
import com.navi.common.extensions.removeSpaces
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_SEND_MONEY_TO_CONTACTS_SCREEN_V2
import com.navi.pay.analytics.NaviPayToContacts
@@ -25,9 +25,9 @@ import com.navi.pay.common.model.config.NaviPayDefaultConfig
import com.navi.pay.common.model.network.ValidateVpaRequest
import com.navi.pay.common.model.network.ValidateVpaResponse
import com.navi.pay.common.model.view.NaviPaySessionHelper
import com.navi.pay.common.model.view.NaviPermissionResult
import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.ValidateVpaUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
import com.navi.pay.common.utils.FrequentOrdersHelper
import com.navi.pay.common.utils.NaviPayCommonUtils.getHelpCtaData
import com.navi.pay.common.utils.NaviPayCommonUtils.getNormalisedPhoneNumber
@@ -47,9 +47,11 @@ import com.navi.pay.tstore.list.model.view.FrequentOrderEntity
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS
import com.navi.pay.utils.INVALID_VPA
import com.navi.pay.utils.LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION
import com.navi.pay.utils.NAVI_PAY_SEARCH_QUERY_API_DELAY
import com.navi.pay.utils.NAVI_PAY_WIDGET_CLICKED_KEY
import com.navi.pay.utils.NOT_LINKED_TO_UPI
import com.navi.pay.utils.NaviPayExperimentVariantType
import com.navi.pay.utils.PHONE_NUMBER_LENGTH
import com.navi.pay.utils.REMOVE_WHITESPACE_REGEX
import com.navi.pay.utils.isValidPhoneNumberLength
@@ -78,7 +80,6 @@ import kotlinx.coroutines.launch
class PayToContactsViewModel
@Inject
constructor(
private val deviceInfoProvider: DeviceInfoProvider,
private val contactManager: PhoneContactManager,
private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity,
private val naviPayConfigUseCase: NaviPayConfigUseCase,
@@ -88,6 +89,7 @@ constructor(
private val validateVpaUseCase: ValidateVpaUseCase,
private val naviPayActivityDataProvider: NaviPayActivityDataProvider,
private val naviPayAnalytics: NaviPayToContacts,
private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
) : NaviPayBaseVM() {
private var naviPayDefaultConfig = NaviPayDefaultConfig()
@@ -134,6 +136,28 @@ constructor(
private val _isNewContactVisible = MutableStateFlow(false)
val isNewContactVisible = _isNewContactVisible.asStateFlow()
private val _permissionResult =
MutableStateFlow<NaviPermissionResult>(NaviPermissionResult.None)
val permissionResult = _permissionResult.asStateFlow()
private val _isAutoOpenContactPermissionExperimentEnabled = MutableStateFlow(false)
val isAutoOpenContactPermissionExperimentEnabled =
_isAutoOpenContactPermissionExperimentEnabled.asStateFlow()
val showPermissionWidget =
combine(permissionResult, isAutoOpenContactPermissionExperimentEnabled) {
permissionResult,
isAutoOpenContactPermissionExperimentEnabled ->
permissionResult != NaviPermissionResult.None ||
!isAutoOpenContactPermissionExperimentEnabled
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
private val _navigateToNextScreenFromHelpCta = MutableSharedFlow<CtaData?>()
val navigateToNextScreenFromHelpCta = _navigateToNextScreenFromHelpCta.asSharedFlow()
@@ -165,6 +189,10 @@ constructor(
private var lastSearchQuery = ""
var isPermissionPopupSeenOnLanded = false
var isPermissionLaunchedFromAllowClick = false
private var isContactPermissionGranted = false
private val helpCtaData = getHelpCtaData(screenName = screenName)
@@ -243,31 +271,43 @@ constructor(
init {
viewModelScope.launch(context = Dispatchers.IO) {
updateNaviPaySessionId()
triggerEventForShortcutWidget()
updateNaviPayDefaultConfig()
initializeSynchronously()
initializeAsynchronously()
}
}
private suspend fun initializeSynchronously() {
updateNaviPaySessionId()
updateNaviPayDefaultConfig()
setLitmusExperimentValues()
}
private suspend fun initializeAsynchronously() {
coroutineScope {
launch { triggerEventForShortcutWidget() }
launch { fetchFrequentOrders() }
launch { observeAndHandleSearchResults() }
}
}
private suspend fun setLitmusExperimentValues() {
_isAutoOpenContactPermissionExperimentEnabled.update {
val autoOpenContactPermissionExperimentVariant =
litmusExperimentsUseCase
.execute(experimentName = LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION)
?.variant
autoOpenContactPermissionExperimentVariant?.name ==
NaviPayExperimentVariantType.TEST.value &&
autoOpenContactPermissionExperimentVariant.enabled
}
}
private fun triggerEventForShortcutWidget() {
viewModelScope.launch(Dispatchers.IO) {
naviPayActivityDataProvider
.getIntentData()
?.getString(NAVI_PAY_WIDGET_CLICKED_KEY)
?.let {
naviPayAnalytics.onSendToContactShortcutClicked(
eventName = it,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
}
naviPayActivityDataProvider.getIntentData()?.getString(NAVI_PAY_WIDGET_CLICKED_KEY)?.let {
naviPayAnalytics.onSendToContactShortcutClicked(
eventName = it,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
}
}
@@ -366,6 +406,14 @@ constructor(
updateShimmerState(showShimmer = false)
}
fun onPermissionPopupSeenOnLanded() {
isPermissionPopupSeenOnLanded = true
}
fun updatePermissionResult(permissionResult: NaviPermissionResult) {
_permissionResult.update { permissionResult }
}
private fun updateIsWarningOrErrorState(
isError: Boolean? = null,
isWarning: Boolean? = null,

View File

@@ -312,6 +312,7 @@ fun NaviPayOnboardingScreen(
)
)
}
else -> {}
}
}
@@ -389,6 +390,7 @@ fun NaviPayOnboardingScreen(
)
)
}
else -> {}
}
}

View File

@@ -172,7 +172,6 @@ const val NAVI_PAY_SYNC_TABLE_CUSTOMER_ONBOARDING_DATA_KEY = "customerOnboarding
// Litmus experiments
const val LITMUS_EXPERIMENT_NAVIPAY_LITE_DEFAULT_ENTERED_AMOUNT =
"NaviPay-lite-default-entered-amount"
// Transaction ledger Experiment
const val LITMUS_EXPERIMENT_NAVIPAY_TRANSACTION_LEDGER = "NaviPay-exp-txn-ledger"
const val LITMUS_EXPERIMENT_NAVIPAY_ORDER_TAG_SUMMARY = "NaviPay-order-tag-summary"
const val LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING = "NaviPay-exp-smv-binding"
@@ -184,6 +183,8 @@ const val LITMUS_EXPERIMENT_NAVIPAY_UPI_LITE_USP = "NaviPay-exp-lite-usp-exp"
const val LITMUS_EXPERIMENT_NAVIPAY_HIGHLIGHT_NEW_PAYEE = "NaviPay-exp-highlight-new-payee"
const val LITMUS_EXPERIMENT_ORDER_HISTORY_ERROR_WIDGET = "NaviTStore-exp-tds-error-widget"
const val LITMUS_EXPERIMENT_NAVIPAY_PAYMENT_RETRY_EXPERIENCE = "NaviPay-payment-retry-experience"
const val LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION = "NaviPay-auto-open-contact-permission"
val NAVI_PAY_LITMUS_EXPERIMENTS =
listOf(
LITMUS_EXPERIMENT_NAVIPAY_TRANSACTION_LEDGER,
@@ -200,6 +201,7 @@ val NAVI_PAY_LITMUS_EXPERIMENTS =
LITMUS_EXPERIMENT_NAVI_IPL_POWERPLAY,
LITMUS_EXPERIMENT_NAVIPAY_PAYMENT_RETRY_EXPERIENCE,
LITMUS_EXPERIMENT_ORDER_HISTORY_ERROR_WIDGET,
LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION,
)
// Generic

View File

@@ -307,6 +307,7 @@ constructor(
is NaviPermissionResult.ShowRationale -> {
scanCardAnalytics.onCameraPermissionDenied(showRationale = true)
}
else -> {}
}
}