NTP-6591 | lite low balance notification and redirection (#13236)

This commit is contained in:
Shaurya Rehan
2024-10-22 11:14:51 +05:30
committed by Shivam Goyal
parent dbb8f58be4
commit 89e75416da
12 changed files with 232 additions and 17 deletions

View File

@@ -89,7 +89,7 @@ class NaviFirebaseMessagingService : FirebaseMessagingService() {
ModuleName.NAVIPAY.name ->
NaviPayNotificationHandler.handlePushNotification(
context = context,
remoteMessage = remoteMessage
data = remoteMessage.data
)
else -> {
handleNotificationForSuperApp(remoteMessage, context)

View File

@@ -155,6 +155,8 @@ object FirebaseRemoteConfigHelper {
const val NAVI_PAY_SEND_MONEY_FTUE_LIMIT = "NAVI_PAY_SEND_MONEY_FTUE_LIMIT"
const val NAVI_PAY_AUTO_FLASH_DISABLED = "NAVI_PAY_AUTO_FLASH_DISABLED"
const val NAVI_PAY_AUTO_FLASH_LIGHT_LUX_THRESHOLD = "NAVI_PAY_AUTO_FLASH_LIGHT_LUX_THRESHOLD"
const val NAVI_PAY_UPI_LITE_LOW_BALANCE_NOTIFICATION_PN_DISABLED =
"NAVI_PAY_UPI_LITE_LOW_BALANCE_NOTIFICATION_PN_DISABLED"
// PAYMENTS
const val NAVI_PMT_JUSPAY_INIT_OPTIMISATION_DISABLE =

View File

@@ -0,0 +1,15 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.common.model.view
data class UpiLiteLowBalanceNotificationData(
val title: String,
val description: String,
val hours: Int,
val ctaUrl: String
)

View File

@@ -0,0 +1,146 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.common.usecase
import android.content.Context
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_UPI_LITE_LOW_BALANCE_BANNER_MAX_LIMIT
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.utils.NaviPayNotificationHandler
import com.navi.pay.management.lite.models.NaviPayUpiLiteConfig
import com.navi.pay.management.mandate.model.network.MandateItem
import com.navi.pay.management.mandate.model.view.MandateStatus
import com.navi.pay.network.di.NaviPayGsonBuilder
import com.navi.pay.utils.ConfigKey
import com.navi.pay.utils.KEY_UPI_LITE_LAST_NOTIFICATION_TIMESTAMP
import com.navi.pay.utils.KEY_UPI_LITE_MANDATE_INFO
import com.navi.pay.utils.NAVI_PAY_NOTIFICATION_BODY
import com.navi.pay.utils.NAVI_PAY_NOTIFICATION_DATA_CTA_KEY
import com.navi.pay.utils.NAVI_PAY_NOTIFICATION_ID
import com.navi.pay.utils.NAVI_PAY_NOTIFICATION_TITLE
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.withContext
import org.joda.time.DateTime
import org.joda.time.Hours
class UpiLiteLowBalanceNotificationUseCase
@Inject
constructor(
@ApplicationContext val context: Context,
private val naviPayConfigUseCase: NaviPayConfigUseCase,
private val naviCacheRepository: NaviCacheRepository,
private val upiLiteBalanceUseCase: UpiLiteBalanceUseCase,
private val dispatcherProvider: CoroutineDispatcherProvider,
@NaviPayGsonBuilder private val gson: Gson
) {
private val naviPayAnalytics: NaviPayAnalytics.NaviPayUPILite =
NaviPayAnalytics.INSTANCE.NaviPayUPILite()
companion object {
private const val UPI_LITE_LOW_BALANCE_NOTIFICATION_ID = 2000
}
suspend fun execute(screenName: String) {
val upiLiteConfig = getUpiLiteConfig(screenName = screenName)
val upiLiteMandateInfo = getCachedMandateInfo()
val upiLiteBalance = upiLiteBalanceUseCase.execute().toDoubleOrNull() ?: 0.0
val lowBalanceThreshold =
FirebaseRemoteConfigHelper.getLong(NAVI_PAY_UPI_LITE_LOW_BALANCE_BANNER_MAX_LIMIT)
naviPayAnalytics.onDevGenericEvent(
event = "NaviPay_UpiLiteLowBalanceNotificationUseCase_execute",
params =
mapOf(
"mandateInfo" to upiLiteMandateInfo.toString(),
"config" to upiLiteConfig.toString(),
"balance" to upiLiteBalance.toString(),
"threshold" to lowBalanceThreshold.toString()
)
)
// Trigger notification only if zero non-terminal lite mandates are present
if (!shouldTriggerLowBalanceNotification(upiLiteMandateInfo = upiLiteMandateInfo)) {
return
}
// If balance is sufficient, do not trigger notification
if (upiLiteBalance >= lowBalanceThreshold) return
// Check if notification was recently triggered
if (wasRecentlyNotified(hoursLimit = upiLiteConfig.config.lowBalanceNotificationData.hours))
return
sendNotificationAndUpdateCache(upiLiteConfig = upiLiteConfig)
}
private suspend fun wasRecentlyNotified(hoursLimit: Int): Boolean {
val lastTriggeredTimeStamp =
naviCacheRepository.get(key = KEY_UPI_LITE_LAST_NOTIFICATION_TIMESTAMP)?.value
return lastTriggeredTimeStamp?.toLongOrNull()?.let { timestampMillis ->
Hours.hoursBetween(DateTime(timestampMillis), DateTime.now()).hours <= hoursLimit
} ?: false
}
private suspend fun sendNotificationAndUpdateCache(upiLiteConfig: NaviPayUpiLiteConfig) {
val notificationData = upiLiteConfig.config.lowBalanceNotificationData
NaviPayNotificationHandler.handlePushNotification(
context = context,
data =
mapOf(
NAVI_PAY_NOTIFICATION_DATA_CTA_KEY to notificationData.ctaUrl,
NAVI_PAY_NOTIFICATION_TITLE to notificationData.title,
NAVI_PAY_NOTIFICATION_BODY to notificationData.description,
NAVI_PAY_NOTIFICATION_ID to UPI_LITE_LOW_BALANCE_NOTIFICATION_ID.toString()
)
)
// Update cache with current timestamp
naviCacheRepository.save(
naviCacheEntity =
NaviCacheEntity(
key = KEY_UPI_LITE_LAST_NOTIFICATION_TIMESTAMP,
value = DateTime.now().millis.toString(),
version = 1
)
)
}
private fun shouldTriggerLowBalanceNotification(upiLiteMandateInfo: MandateItem?): Boolean {
return upiLiteMandateInfo == null ||
isMandateCompleted(upiLiteMandateInfo = upiLiteMandateInfo)
}
private fun isMandateCompleted(upiLiteMandateInfo: MandateItem?): Boolean {
return upiLiteMandateInfo?.let {
!MandateStatus.isNonCompletedLiteMandate(status = it.status)
} ?: true
}
private suspend fun getUpiLiteConfig(screenName: String): NaviPayUpiLiteConfig {
return withContext(dispatcherProvider.io) {
naviPayConfigUseCase.execute<NaviPayUpiLiteConfig>(
ConfigKey.UPI_LITE,
object : TypeToken<NaviPayUpiLiteConfig>() {}.type,
screenName = screenName
) ?: NaviPayUpiLiteConfig()
}
}
private suspend fun getCachedMandateInfo(): MandateItem? {
val liteMandateInfoFromCache = naviCacheRepository.get(key = KEY_UPI_LITE_MANDATE_INFO)
return liteMandateInfoFromCache?.value?.let { liteMandateInfo ->
gson.fromJson(liteMandateInfo, MandateItem::class.java)
}
}
}

View File

@@ -18,7 +18,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.DEFAULT_ALL
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.google.firebase.messaging.RemoteMessage
import com.navi.base.sharedpref.PreferenceManager
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.INTENT_DATA_INSERT_DISABLED
@@ -42,11 +41,11 @@ import kotlinx.coroutines.launch
object NaviPayNotificationHandler {
fun handlePushNotification(context: Context, remoteMessage: RemoteMessage) {
fun handlePushNotification(context: Context, data: Map<String, String>) {
val intent = Intent(context, NaviPayActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
val remoteDataAsHashMap = HashMap(remoteMessage.data.toMutableMap())
val remoteDataAsHashMap = HashMap(data.toMutableMap())
intent.putExtra(NAVI_PAY_NOTIFICATION_DATA_BUNDLE, remoteDataAsHashMap.getBundle())
PreferenceManager.setBooleanPreference(PENDING_REQUEST_REFRESHED_REQUIRED, true)
@@ -64,18 +63,17 @@ object NaviPayNotificationHandler {
)
val notificationContentTitle =
remoteMessage.data[NAVI_PAY_NOTIFICATION_TITLE]
data[NAVI_PAY_NOTIFICATION_TITLE]
?: context.getString(R.string.default_navi_pay_notification_title)
val notificationContentBody =
remoteMessage.data[NAVI_PAY_NOTIFICATION_BODY]
data[NAVI_PAY_NOTIFICATION_BODY]
?: context.getString(R.string.default_navi_pay_notification_body)
val channelName = context.getString(R.string.navi_pay_channel_name)
val channelId = context.getString(R.string.navi_pay_channel_id)
val notificationId =
getNotificationIdAsInt(data = remoteMessage.data[NAVI_PAY_NOTIFICATION_ID])
val notificationId = getNotificationIdAsInt(data = data[NAVI_PAY_NOTIFICATION_ID])
val vibrationPatternArray = longArrayOf(100, 200, 300)
val notificationBuilder =
NotificationCompat.Builder(context, channelId)
@@ -106,10 +104,7 @@ object NaviPayNotificationHandler {
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(notificationId, notificationBuilder.build())
if (
remoteMessage.data[NAVI_PAY_NOTIFICATION_DATA_CTA_KEY] ==
NAVI_PAY_NOTIFICATION_COLLECT_REQUEST
) {
if (data[NAVI_PAY_NOTIFICATION_DATA_CTA_KEY] == NAVI_PAY_NOTIFICATION_COLLECT_REQUEST) {
CoroutineScope(Dispatchers.IO).launch {
CollectRequestPopupEventHandler.postEvent(triggerScreenOverlayApi = true)
}

View File

@@ -29,6 +29,7 @@ import com.navi.common.R as CommonR
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_SCRATCH_CARD_OPTIMISATION_V2_ENABLED
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_UPI_LITE_LOW_BALANCE_NOTIFICATION_PN_DISABLED
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccess
import com.navi.common.network.models.isSuccessWithData
@@ -73,6 +74,7 @@ import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.SubsequentLiteMandateExecutionUseCase
import com.navi.pay.common.usecase.UpiLiteBalanceUseCase
import com.navi.pay.common.usecase.UpiLiteExperimentationUseCase
import com.navi.pay.common.usecase.UpiLiteLowBalanceNotificationUseCase
import com.navi.pay.common.usecase.UpiRequestIdUseCase
import com.navi.pay.common.usecase.ValidateVpaUseCase
import com.navi.pay.common.utils.DeviceInfoProvider
@@ -255,6 +257,7 @@ constructor(
private val naviCacheRepository: NaviCacheRepository,
private val subsequentLiteMandateExecutionUseCase: SubsequentLiteMandateExecutionUseCase,
private val upiLiteExperimentationUseCase: UpiLiteExperimentationUseCase,
private val upiLiteLowBalanceNotificationUseCase: UpiLiteLowBalanceNotificationUseCase,
naviPayActivityDataProvider: NaviPayActivityDataProvider
) : NaviPayBaseVM() {
@@ -2221,6 +2224,11 @@ constructor(
arpc = sendMoneyResponse.arpc,
accountId = selectedBankAccount.value?.accountId.orEmpty()
)
if (isLiteLowBalanceNotificationsEnabled()) {
CoroutineScope(Dispatchers.IO).launch {
upiLiteLowBalanceNotificationUseCase.execute(screenName = screenName)
}
}
}
val upiLiteBalance = upiLiteBalanceUseCase.execute()
updateScreenState(
@@ -2247,6 +2255,23 @@ constructor(
}
}
private fun isLiteLowBalanceNotificationsEnabled(): Boolean {
val isLiteLowBalanceNotificationsEnabled =
!FirebaseRemoteConfigHelper.getBoolean(
key = NAVI_PAY_UPI_LITE_LOW_BALANCE_NOTIFICATION_PN_DISABLED
)
upiLiteAnalytics.onDevGenericEvent(
event = ::isLiteLowBalanceNotificationsEnabled.name,
params =
mapOf(
"isLiteLowBalanceNotificationsEnabled" to
isLiteLowBalanceNotificationsEnabled.toString()
)
)
return isLiteLowBalanceNotificationsEnabled
}
private suspend fun syncLiteBalance(arpc: String? = null, accountId: String) {
val formattedAccountId = accountId.substringBefore(UNDERSCORE)
if (arpc.isNullOrBlank()) {

View File

@@ -8,6 +8,7 @@
package com.navi.pay.management.lite.models
import com.google.gson.annotations.SerializedName
import com.navi.pay.common.model.view.UpiLiteLowBalanceNotificationData
import com.navi.pay.utils.NAVI_PAY_UPI_LITE_LOGO_URL
data class NaviPayUpiLiteConfig(
@@ -43,5 +44,14 @@ data class NaviPayUpiLiteConfigContent(
@SerializedName("upiLiteNonOnboardedBannerTitle3")
val upiLiteNonOnboardedBannerTitle3: String = "Withdraw",
@SerializedName("upiLiteNonOnboardedBannerSubTitle3")
val upiLiteNonOnboardedBannerSubTitle3: String = "balance anytime"
val upiLiteNonOnboardedBannerSubTitle3: String = "balance anytime",
@SerializedName("lowBalanceNotificationData")
val lowBalanceNotificationData: UpiLiteLowBalanceNotificationData =
UpiLiteLowBalanceNotificationData(
title = "Lite balance is running low!",
description =
"Recharge your Lite wallet now for lightning fast PIN-free payments in the future.",
hours = 24,
ctaUrl = "naviPay/NAVI_PAY_UPI_LITE"
)
)

View File

@@ -204,7 +204,7 @@ fun UPILiteScreen(
if (naviPayActivity.needsResult) {
naviPayActivity.finish()
} else {
navigator.navigateUp()
navigator.navigateUp(naviPayActivity)
}
}
@@ -224,7 +224,7 @@ fun UPILiteScreen(
// Do nothing, back press is disabled for N seconds when top-up is in progress
}
is UpiLiteBackPressAction.GoBack -> {
navigator.navigateUp()
navigator.navigateUp(naviPayActivity)
}
}
}

View File

@@ -10,6 +10,7 @@ package com.navi.pay.onboarding.launcher.model.view
import com.navi.pay.management.common.sendmoney.model.view.PayeeEntity
import com.navi.pay.management.common.sendmoney.model.view.SendMoneyScreenSource
import com.navi.pay.management.mandate.model.view.MandateEntity
import com.ramcosta.composedestinations.spec.Direction
sealed class LauncherScreenToNextScreenNavigate {
data class OnLocalDataValidState(val customerStatus: String) :
@@ -28,4 +29,7 @@ sealed class LauncherScreenToNextScreenNavigate {
LauncherScreenToNextScreenNavigate()
data object None : LauncherScreenToNextScreenNavigate()
data class RedirectToDestination(val destination: Direction) :
LauncherScreenToNextScreenNavigate()
}

View File

@@ -150,5 +150,12 @@ private fun handleNavigation(
LauncherScreenToNextScreenNavigate.None -> {
naviPayActivity.finish()
}
is LauncherScreenToNextScreenNavigate.RedirectToDestination -> {
navigator.clearBackStackUpToAndNavigate(
destination = navigateToNextScreen.destination,
popUpTo = NavGraphs.root,
inclusive = false
)
}
}
}

View File

@@ -386,7 +386,12 @@ constructor(
message =
"checkNotificationDataAndNavigate. Next screen destination is $nextScreenDestination"
)
updateNavigateToNextScreenValue(value = LauncherScreenToNextScreenNavigate.None)
updateNavigateToNextScreenValue(
value =
LauncherScreenToNextScreenNavigate.RedirectToDestination(
destination = nextScreenDestination
)
)
} else {
navigateToNextScreenBasedOnCustomerStatus(customerStatus = customerStatus)
}
@@ -484,7 +489,12 @@ constructor(
message =
"checkNaviPayLocalUriAndNavigate. Next screen destination is $nextScreenDestination"
)
updateNavigateToNextScreenValue(value = LauncherScreenToNextScreenNavigate.None)
updateNavigateToNextScreenValue(
value =
LauncherScreenToNextScreenNavigate.RedirectToDestination(
destination = nextScreenDestination
)
)
} else {
logEvent(
message = "checkNaviPayLocalUriAndNavigate. Next screen destination is null"

View File

@@ -175,6 +175,7 @@ const val NAVI_PAY_SEND_MONEY_FTUE_NUDGE_COUNTER = "sendMoneyFtueNudgeCounter"
const val NAVI_PAY_SETTING_QR_PAGER_ANIMATION_COUNTER = "settingQrPagerAnimationCounter"
const val NAVI_PAY_AUTO_POPUP_SCRATCH_CARD_COUNTER = "autoPopupScratchCardCounter"
const val KEY_UPI_LITE_MANDATE_INFO = "liteMandateInfo"
const val KEY_UPI_LITE_LAST_NOTIFICATION_TIMESTAMP = "upiLiteLastNotificationTimestamp"
// Sync DB keys
const val NAVI_PAY_SYNC_TABLE_TRANSACTION_HISTORY_KEY = "transactionHistoryKey"