diff --git a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt index e88dfe4a76..c205ce2310 100644 --- a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt +++ b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt @@ -174,6 +174,8 @@ object FirebaseRemoteConfigHelper { "NAVI_PMT_TRANSACTION_STORE_EXPIRY_TIME_MILLIS" const val NAVI_PMT_ONE_CLICK_CHECKOUT_API_TIMEOUT_MILLIS = "NAVI_PMT_ONE_CLICK_CHECKOUT_API_TIMEOUT_MILLIS" + const val ENABLE_NAVI_PAYMENT_KILL_SWITCH_MECHANISM = + "ENABLE_NAVI_PAYMENT_KILL_SWITCH_MECHANISM" // BBPS const val NAVI_BBPS_REWARDS_NUDGE_CACHE_TTL_IN_MILLIS = diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/NaviPaymentAnalytics.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/NaviPaymentAnalytics.kt index f311276c64..742adfdeba 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/NaviPaymentAnalytics.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/NaviPaymentAnalytics.kt @@ -23,6 +23,7 @@ import com.navi.payment.nativepayment.model.RewardsInfoV2 import com.navi.payment.nativepayment.model.WebPaymentAction import com.navi.payment.nativepayment.model.transactionStatusRequest.TransactionStatusRequest import com.navi.payment.nativepayment.presentation.reducer.ImageSource +import com.navi.payment.nativepayment.utils.NaviPaymentErrorConfig import com.navi.payment.nativepayment.viewmodel.ScanCardResult import com.navi.payment.paymentscreen.model.ErrorReason import com.navi.payment.utils.putIfNotNullAndNotEmpty @@ -180,6 +181,32 @@ class NaviPaymentAnalytics private constructor() { eventValues = eventAttributes, ) } + + fun onErrorOccurredInGetQuerySnapshot(exception: Exception) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPmt_UptimePoller_ErrorOccurredInGetQuerySnapshot", + mapOf( + "exception" to exception.toString(), + "exceptionMessage" to exception.message.toString(), + ), + ) + } + + fun onErrorOccurredInProcessingQuerySnapshot(exception: Exception) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPmt_UptimePoller_ErrorOccurredInProcessingQuerySnapshot", + mapOf( + "exception" to exception.toString(), + "exceptionMessage" to exception.message.toString(), + ), + ) + } + + fun onPaymentsUptimePollerSyncSuccess() { + NaviTrackEvent.trackEventOnClickStream( + "NaviPmt_UptimePoller_PaymentsUptimePollerSyncSuccess" + ) + } } inner class NPSMainScreen { @@ -1503,6 +1530,16 @@ class NaviPaymentAnalytics private constructor() { ) } + fun onBottomSheetCloseClicked( + bottomSheetState: String, + error: NaviPaymentErrorConfig? = null, + ) { + NaviTrackEvent.trackEvent( + "NaviPMT_OneClickCheckoutScreen_BottomSheetCloseClicked", + mapOf("bottom_sheet_state" to bottomSheetState, "error" to error.toString()), + ) + } + fun onRedirectingBottomSheetClosed(selectedBankAccountId: String) { NaviTrackEvent.trackEvent( "NaviPMT_OneClickCheckoutScreen_RedirectingBottomSheetClosed", diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/common/usecase/PaymentsUptimePollerUseCase.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/common/usecase/PaymentsUptimePollerUseCase.kt new file mode 100644 index 0000000000..c69929f9e4 --- /dev/null +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/common/usecase/PaymentsUptimePollerUseCase.kt @@ -0,0 +1,78 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.payment.nativepayment.common.usecase + +import com.google.firebase.firestore.QuerySnapshot +import com.navi.base.utils.FirestoreDataProvider +import com.navi.common.utils.NaviApiPoller +import com.navi.pay.utils.parallelMap +import com.navi.payment.nativepayment.NaviPaymentAnalytics +import com.navi.payment.nativepayment.db.model.toPaymentsUptimeEntity +import com.navi.payment.nativepayment.repository.PaymentsUptimeRepository +import com.navi.payment.utils.Constants.FIRESTORE_PAYMENTS_UPTIME_COLLECTION_PATH +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Duration.Companion.minutes + +@Singleton +class PaymentsUptimePollerUseCase +@Inject +constructor( + private val firestoreDataProvider: FirestoreDataProvider, + private val paymentsUptimeRepository: PaymentsUptimeRepository, +) { + private val PAYMENTS_UPTIME_POLLING_INTERVAL = 5.minutes + private val paymentsUptimePoller by lazy { + NaviApiPoller(repeatInterval = PAYMENTS_UPTIME_POLLING_INTERVAL) + } + private val naviPaymentAnalytics = NaviPaymentAnalytics.INSTANCE.CommonAnalytics() + + suspend fun executePollerForPaymentsUptime() { + paymentsUptimePoller.stopPolling() + paymentsUptimePoller + .startPolling { + try { + firestoreDataProvider.getQuerySnapShotForCollectionPath( + collectionPath = FIRESTORE_PAYMENTS_UPTIME_COLLECTION_PATH + ) + } catch (exception: Exception) { + naviPaymentAnalytics.onErrorOccurredInGetQuerySnapshot(exception = exception) + paymentsUptimePoller.stopPolling() + } + } + .collect { + try { + val paymentsUptimeQuerySnapShot = it as? QuerySnapshot? + processQuerySnapShotForPaymentsUptime( + paymentsUptimeQuerySnapShot = paymentsUptimeQuerySnapShot + ) + naviPaymentAnalytics.onPaymentsUptimePollerSyncSuccess() + } catch (exception: Exception) { + naviPaymentAnalytics.onErrorOccurredInProcessingQuerySnapshot( + exception = exception + ) + paymentsUptimePoller.stopPolling() + } + } + } + + private suspend fun processQuerySnapShotForPaymentsUptime( + paymentsUptimeQuerySnapShot: QuerySnapshot? + ) { + paymentsUptimeQuerySnapShot?.let { querySnapShot -> + val paymentsUptimeEntityList = + querySnapShot.documents.parallelMap { it.toPaymentsUptimeEntity() } + + if (paymentsUptimeEntityList.isNotEmpty()) { + paymentsUptimeRepository.deleteAllExistingDataAndInsertAll( + uptimeEntityList = paymentsUptimeEntityList + ) + } + } + } +} diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/components/PaymentCheckoutFooter.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/components/PaymentCheckoutFooter.kt index d70e8e8acd..a220484ca5 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/components/PaymentCheckoutFooter.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/components/PaymentCheckoutFooter.kt @@ -69,6 +69,7 @@ import com.navi.pay.R import com.navi.pay.common.arc.ui.ArcNudgeBottomSheetContent import com.navi.pay.common.model.view.CheckBalanceState import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.ui.BottomSheetContentWithVerticalPrimarySecondaryButton import com.navi.pay.common.ui.ImageWithCircularBackground import com.navi.pay.common.ui.LoaderRoundedButton import com.navi.pay.common.ui.NaviPayModalBottomSheet @@ -260,6 +261,39 @@ fun PaymentCheckoutFooter( is OneClickCheckoutFooterBottomSheetUiState.RedirectingBottomSheet -> { RedirectingBottomSheetContent(title = bottomSheetType.titleText) } + is OneClickCheckoutFooterBottomSheetUiState.Error -> { + BottomSheetContentWithVerticalPrimarySecondaryButton( + iconId = bottomSheetType.errorConfig.iconResId, + headerText = bottomSheetType.errorConfig.title, + descriptionText = bottomSheetType.errorConfig.description, + primaryButtonText = + bottomSheetType.errorConfig.firstPrimaryButtonConfig.text, + secondaryButtonText = + bottomSheetType.errorConfig.firstSecondaryButtonConfig?.text, + onPrimaryButtonClicked = { + coroutineScope + .launch { sheetState.hide() } + .invokeOnCompletion { + naviCheckoutViewModel.onBottomSheetCloseClicked() + } + }, + onSecondaryButtonClicked = { + coroutineScope + .launch { sheetState.hide() } + .invokeOnCompletion { + naviCheckoutViewModel.onBottomSheetCloseClicked() + } + }, + onDismissClicked = { + coroutineScope + .launch { sheetState.hide() } + .invokeOnCompletion { + naviCheckoutViewModel.onBottomSheetCloseClicked() + } + }, + shouldShowCloseIcon = bottomSheetType.errorConfig.showDismissIcon, + ) + } else -> {} } } diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/NaviPaymentAppDatabase.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/NaviPaymentAppDatabase.kt index b484520e22..8647a6f705 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/NaviPaymentAppDatabase.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/NaviPaymentAppDatabase.kt @@ -14,28 +14,35 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.navi.common.utils.Constants.DEFAULT import com.navi.payment.nativepayment.db.converter.BanksEntityConverter +import com.navi.payment.nativepayment.db.converter.PaymentsUptimeStatusConverter import com.navi.payment.nativepayment.db.dao.BankListDao +import com.navi.payment.nativepayment.db.dao.PaymentsUptimeDao import com.navi.payment.nativepayment.db.dao.TransactionStatusRequestDao import com.navi.payment.nativepayment.db.model.BankListEntity +import com.navi.payment.nativepayment.db.model.PaymentsUptimeEntity import com.navi.payment.nativepayment.db.model.TransactionStatusRequestEntity import com.navi.payment.nativepayment.model.transactionStatusRequest.TransactionStatus @Database( - entities = [TransactionStatusRequestEntity::class, BankListEntity::class], - version = 3, + entities = + [TransactionStatusRequestEntity::class, BankListEntity::class, PaymentsUptimeEntity::class], + version = 4, exportSchema = false, ) -@TypeConverters(BanksEntityConverter::class) +@TypeConverters(BanksEntityConverter::class, PaymentsUptimeStatusConverter::class) abstract class NaviPaymentAppDatabase : RoomDatabase() { abstract fun transactionStatusRequestDao(): TransactionStatusRequestDao abstract fun bankListDao(): BankListDao + abstract fun paymentUptimeDao(): PaymentsUptimeDao + companion object { const val NAVI_PAYMENT_DATABASE_NAME = "navi-payment.db" const val TRANSACTION_STATUS_REQUEST_TABLE = "transaction_status_request" const val BANKS_TABLE = "banks" + const val UPTIME_STATUS_TABLE = "uptime_status" } } @@ -105,3 +112,16 @@ val NAVI_PAYMENT_APP_DATABASE_MIGRATION_2_3 = } } } + +val NAVI_PAYMENT_APP_DATABASE_MIGRATION_3_4 = + object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS ${NaviPaymentAppDatabase.UPTIME_STATUS_TABLE} (" + + "`vertical` TEXT PRIMARY KEY NOT NULL, " + + "`downtimeTitle` TEXT, " + + "`downtimeDescription` TEXT, " + + "`status` TEXT NOT NULL, " + ) + } + } diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/cleaner/NaviPaymentModuleTableCleaner.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/cleaner/NaviPaymentModuleTableCleaner.kt index bd8618a899..57fbfc957a 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/cleaner/NaviPaymentModuleTableCleaner.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/cleaner/NaviPaymentModuleTableCleaner.kt @@ -23,6 +23,9 @@ constructor(private val naviPaymentAppDatabase: NaviPaymentAppDatabase) : NaviMo NaviPaymentAppDatabase.BANKS_TABLE -> { runWithCatching { naviPaymentAppDatabase.bankListDao().deleteAll() } } + NaviPaymentAppDatabase.UPTIME_STATUS_TABLE -> { + runWithCatching { naviPaymentAppDatabase.paymentUptimeDao().deleteAll() } + } } } diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/converter/PaymentsUptimeStatusConverter.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/converter/PaymentsUptimeStatusConverter.kt new file mode 100644 index 0000000000..c61ece2f3f --- /dev/null +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/converter/PaymentsUptimeStatusConverter.kt @@ -0,0 +1,24 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.payment.nativepayment.db.converter + +import androidx.room.TypeConverter +import com.navi.payment.nativepayment.db.model.PaymentsUptimeStatus + +class PaymentsUptimeStatusConverter { + + @TypeConverter + fun fromPaymentsUptimeStatus(uptimeStatus: PaymentsUptimeStatus): String { + return uptimeStatus.name + } + + @TypeConverter + fun toPaymentsUptimeStatus(uptimeStatus: String): PaymentsUptimeStatus { + return PaymentsUptimeStatus.valueOf(uptimeStatus) + } +} diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/dao/PaymentsUptimeDao.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/dao/PaymentsUptimeDao.kt new file mode 100644 index 0000000000..f95f6d9570 --- /dev/null +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/dao/PaymentsUptimeDao.kt @@ -0,0 +1,35 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.payment.nativepayment.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.Transaction +import androidx.room.Upsert +import androidx.sqlite.db.SupportSQLiteQuery +import com.navi.payment.nativepayment.db.NaviPaymentAppDatabase.Companion.UPTIME_STATUS_TABLE +import com.navi.payment.nativepayment.db.model.PaymentsUptimeEntity + +@Dao +interface PaymentsUptimeDao { + @Upsert suspend fun insertAll(paymentsUptimeEntityList: List) + + @Query("DELETE FROM $UPTIME_STATUS_TABLE") suspend fun deleteAll() + + @RawQuery suspend fun deleteRows(query: SupportSQLiteQuery): Int + + @Query("SELECT * FROM $UPTIME_STATUS_TABLE WHERE vertical = :vertical") + suspend fun getPaymentsUptimeEntityForVertical(vertical: String): List + + @Transaction + suspend fun deleteAllExistingDataAndInsertAll(uptimeEntityList: List) { + deleteAll() + insertAll(uptimeEntityList) + } +} diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/di/NaviPaymentDbModule.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/di/NaviPaymentDbModule.kt index 4e71b31840..4159d08a95 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/di/NaviPaymentDbModule.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/di/NaviPaymentDbModule.kt @@ -48,4 +48,9 @@ object NaviPaymentDbModule { @Provides fun providesBankListDao(naviPaymentAppDatabase: NaviPaymentAppDatabase) = naviPaymentAppDatabase.bankListDao() + + @Singleton + @Provides + fun providesPaymentsUptimeDao(naviPaymentAppDatabase: NaviPaymentAppDatabase) = + naviPaymentAppDatabase.paymentUptimeDao() } diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/model/PaymentsUptimeEntity.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/model/PaymentsUptimeEntity.kt new file mode 100644 index 0000000000..a9f4e5b39e --- /dev/null +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/model/PaymentsUptimeEntity.kt @@ -0,0 +1,39 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.payment.nativepayment.db.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.google.firebase.firestore.DocumentSnapshot +import com.navi.pay.utils.getIfscForBankUptime +import com.navi.payment.nativepayment.db.NaviPaymentAppDatabase.Companion.UPTIME_STATUS_TABLE +import com.navi.payment.nativepayment.db.converter.PaymentsUptimeStatusConverter + +@Entity(tableName = UPTIME_STATUS_TABLE) +data class PaymentsUptimeEntity( + @PrimaryKey @ColumnInfo("vertical") val vertical: String, + @ColumnInfo("downtimeTitle") val downtimeTitle: String? = null, + @ColumnInfo("downtimeDescription") val downtimeDescription: String? = null, + @ColumnInfo("status") + @TypeConverters(PaymentsUptimeStatusConverter::class) + val status: PaymentsUptimeStatus, +) + +fun DocumentSnapshot.toPaymentsUptimeEntity(): PaymentsUptimeEntity { + return PaymentsUptimeEntity( + vertical = this.id.getIfscForBankUptime(), + downtimeTitle = this.getString("downtimeTitle"), + downtimeDescription = this.getString("downtimeDescription"), + status = + PaymentsUptimeStatus.getBankUptimeStatusFromString( + bankUptimeStatus = this.getString("status") ?: "" + ), + ) +} diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/model/PaymentsUptimeStatus.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/model/PaymentsUptimeStatus.kt new file mode 100644 index 0000000000..705590b03e --- /dev/null +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/db/model/PaymentsUptimeStatus.kt @@ -0,0 +1,21 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.payment.nativepayment.db.model + +enum class PaymentsUptimeStatus { + UP, + WARNING, + DOWN; + + companion object { + fun getBankUptimeStatusFromString(bankUptimeStatus: String): PaymentsUptimeStatus { + return entries.singleOrNull { it.name.equals(bankUptimeStatus, ignoreCase = true) } + ?: UP + } + } +} diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/model/OneClickCheckoutFooterBottomSheetStateHolder.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/model/OneClickCheckoutFooterBottomSheetStateHolder.kt index 9c00223c04..fa7c4836a7 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/model/OneClickCheckoutFooterBottomSheetStateHolder.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/model/OneClickCheckoutFooterBottomSheetStateHolder.kt @@ -7,6 +7,8 @@ package com.navi.payment.nativepayment.model +import com.navi.payment.nativepayment.utils.NaviPaymentErrorConfig + data class OneClickCheckoutFooterBottomSheetStateHolder( val showBottomSheet: Boolean, val bottomSheetUIState: OneClickCheckoutFooterBottomSheetUiState, @@ -25,4 +27,7 @@ sealed class OneClickCheckoutFooterBottomSheetUiState { data class RedirectingBottomSheet(val titleText: String) : OneClickCheckoutFooterBottomSheetUiState() + + data class Error(val errorConfig: NaviPaymentErrorConfig) : + OneClickCheckoutFooterBottomSheetUiState() } diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/repository/PaymentsUptimeRepository.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/repository/PaymentsUptimeRepository.kt new file mode 100644 index 0000000000..c2bdaa9879 --- /dev/null +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/repository/PaymentsUptimeRepository.kt @@ -0,0 +1,35 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.payment.nativepayment.repository + +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.ENABLE_NAVI_PAYMENT_KILL_SWITCH_MECHANISM +import com.navi.payment.nativepayment.db.dao.PaymentsUptimeDao +import com.navi.payment.nativepayment.db.model.PaymentsUptimeEntity +import com.navi.payment.nativepayment.db.model.PaymentsUptimeStatus +import javax.inject.Inject + +class PaymentsUptimeRepository +@Inject +constructor(private val paymentsUptimeDao: PaymentsUptimeDao) { + + suspend fun getPaymentsUptimeEntityForVertical(vertical: String): PaymentsUptimeEntity { + + if (!FirebaseRemoteConfigHelper.getBoolean(ENABLE_NAVI_PAYMENT_KILL_SWITCH_MECHANISM)) { + return PaymentsUptimeEntity(vertical = vertical, status = PaymentsUptimeStatus.UP) + } + + val paymentsUptimeEntity = paymentsUptimeDao.getPaymentsUptimeEntityForVertical(vertical) + + return paymentsUptimeEntity.firstOrNull() + ?: PaymentsUptimeEntity(vertical = vertical, status = PaymentsUptimeStatus.UP) + } + + suspend fun deleteAllExistingDataAndInsertAll(uptimeEntityList: List) = + paymentsUptimeDao.deleteAllExistingDataAndInsertAll(uptimeEntityList = uptimeEntityList) +} diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModel.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModel.kt index 30fed3feb5..f888a0c0ad 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModel.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModel.kt @@ -12,20 +12,26 @@ import androidx.lifecycle.viewModelScope import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.navi.base.AppServiceManager +import com.navi.base.utils.ResourceProvider import com.navi.base.utils.orElse import com.navi.base.utils.orFalse import com.navi.common.network.models.isSuccessWithData import com.navi.common.upi.UpiDataType import com.navi.common.usecase.LitmusExperimentsUseCase import com.navi.common.utils.Constants.DEFAULT +import com.navi.naviwidgets.R as WidgetsR +import com.navi.pay.R as NaviPayR import com.navi.pay.common.setup.NaviPayManager import com.navi.pay.common.usecase.NaviPayConfigUseCase +import com.navi.pay.common.viewmodel.NaviPayBaseVM.Companion.ERROR_DEFAULT_TAG import com.navi.pay.management.lite.models.NaviPayUpiLiteConfig import com.navi.pay.utils.UPI_LITE_CONFIG +import com.navi.payment.R import com.navi.payment.model.common.PaymentSdkInitParams import com.navi.payment.model.initiatesdk.PaymentPrefetchDetail import com.navi.payment.model.initiatesdk.PaymentPrefetchMethodRequest import com.navi.payment.nativepayment.NaviPaymentAnalyticScreenName +import com.navi.payment.nativepayment.common.usecase.PaymentsUptimePollerUseCase import com.navi.payment.nativepayment.common.usecase.TransactionStatusUseCase import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider.Companion.ACTION_TYPE @@ -33,14 +39,20 @@ import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider.Companion import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider.Companion.PAYMENT_INITIATE_START_TIME import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider.Companion.SCREEN_TYPE import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider.Companion.UPI_LITE_MAX_PAYABLE_AMOUNT_PER_TRANSACTION +import com.navi.payment.nativepayment.db.model.PaymentsUptimeStatus import com.navi.payment.nativepayment.model.NaviPaymentScreenType import com.navi.payment.nativepayment.model.NetBankingPaymentInstrument import com.navi.payment.nativepayment.model.PaymentActionType import com.navi.payment.nativepayment.model.PaymentInitResponse import com.navi.payment.nativepayment.model.S2sPaymentMethodResponse import com.navi.payment.nativepayment.repository.PaymentRepository +import com.navi.payment.nativepayment.repository.PaymentsUptimeRepository import com.navi.payment.nativepayment.usecase.NetBankingUseCase import com.navi.payment.nativepayment.usecase.PmsLinkedAccountUseCase +import com.navi.payment.nativepayment.utils.NaviPaymentBottomSheetButtonAction +import com.navi.payment.nativepayment.utils.NaviPaymentButtonTheme +import com.navi.payment.nativepayment.utils.NaviPaymentErrorButtonConfig +import com.navi.payment.nativepayment.utils.NaviPaymentErrorConfig import com.navi.payment.nativepayment.utils.getPayloadBasedOnType import com.navi.payment.nativepayment.utils.toGenericErrorResponse import com.navi.payment.nativepayment.viewmodel.NaviPaymentBaseVM @@ -74,11 +86,21 @@ constructor( protected val transactionStatusUseCase: TransactionStatusUseCase, protected val litmusExperimentsUseCase: LitmusExperimentsUseCase, private val naviPayConfigUseCase: NaviPayConfigUseCase, + private val paymentsUptimePollerUseCase: PaymentsUptimePollerUseCase, + protected val paymentsUptimeRepository: PaymentsUptimeRepository, + protected val resourceProvider: ResourceProvider, ) : NaviPaymentBaseVM(screenName = NaviPaymentAnalyticScreenName.CHECKOUT_SCREEN.screenName) { protected val _paymentResponse = MutableSharedFlow(replay = 0) val paymentResponse = _paymentResponse.asSharedFlow() + init { + updateUpiLiteConfig() + viewModelScope.safeLaunch(Dispatchers.IO) { + paymentsUptimePollerUseCase.executePollerForPaymentsUptime() + } + } + fun initiatePayment(paymentSdkInitParams: PaymentSdkInitParams) { viewModelScope.safeLaunch { initializePaymentSession(paymentSdkInitParams.paymentSource.orEmpty()) @@ -211,7 +233,6 @@ constructor( protected suspend fun initializePaymentSession(source: String) { clearPaymentData() fetchPmsExperimentsData(source) - updateUpiLiteConfig() paymentDataProvider.add(PAYMENT_INITIATE_START_TIME, System.currentTimeMillis()) _paymentResponse.emit(PaymentInitResponse(isSuccess = false)) } @@ -230,4 +251,35 @@ constructor( ) } } + + protected suspend fun isPaymentsServiceDown(source: String): NaviPaymentErrorConfig? { + val paymentsUptimeEntity = + paymentsUptimeRepository.getPaymentsUptimeEntityForVertical(source) + return if (paymentsUptimeEntity.status == PaymentsUptimeStatus.DOWN) { + NaviPaymentErrorConfig( + iconResId = WidgetsR.drawable.ic_error_outlined, + title = + paymentsUptimeEntity.downtimeTitle.orElse( + resourceProvider.getString(NaviPayR.string.np_psp_down_message_title) + ), + description = + paymentsUptimeEntity.downtimeDescription.orElse( + resourceProvider.getString(NaviPayR.string.np_psp_down_message_desc) + ), + buttonConfigs = + listOf( + NaviPaymentErrorButtonConfig( + text = resourceProvider.getString(R.string.okay_got_it), + type = NaviPaymentButtonTheme.Primary, + action = NaviPaymentBottomSheetButtonAction.Dismiss, + ) + ), + code = UNKNOWN_ERROR_CODE, + tag = ERROR_DEFAULT_TAG, + showDismissIcon = false, + ) + } else { + null + } + } } diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModelV2.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModelV2.kt index 3e841ef144..9d75d85cd5 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModelV2.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/sharedviewmodel/NaviCheckoutViewModelV2.kt @@ -53,6 +53,7 @@ import com.navi.payment.R import com.navi.payment.model.common.PaymentSdkInitParams import com.navi.payment.nativepayment.NaviPaymentAnalyticScreenName import com.navi.payment.nativepayment.NaviPaymentAnalytics +import com.navi.payment.nativepayment.common.usecase.PaymentsUptimePollerUseCase import com.navi.payment.nativepayment.common.usecase.TransactionStatusUseCase import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider import com.navi.payment.nativepayment.dataprovider.PaymentDataProvider.Companion.ACTION_TYPE @@ -81,9 +82,11 @@ import com.navi.payment.nativepayment.presentation.reducer.OneClickScreenContrac import com.navi.payment.nativepayment.presentation.reducer.OneClickScreenEffect import com.navi.payment.nativepayment.presentation.reducer.OneClickScreenEvent import com.navi.payment.nativepayment.repository.PaymentRepository +import com.navi.payment.nativepayment.repository.PaymentsUptimeRepository import com.navi.payment.nativepayment.usecase.NetBankingUseCase import com.navi.payment.nativepayment.usecase.PmsLinkedAccountUseCase import com.navi.payment.nativepayment.utils.Constants.NAVI_PAY_ACTION +import com.navi.payment.nativepayment.utils.NaviPaymentErrorConfig import com.navi.payment.nativepayment.utils.getDiscountAdjustedAmount import com.navi.payment.nativepayment.utils.getPreferredSelectedAccount import com.navi.payment.nativepayment.utils.mapToLinkedAccountEntity @@ -143,7 +146,9 @@ constructor( transactionStatusUseCase: TransactionStatusUseCase, litmusExperimentsUseCase: LitmusExperimentsUseCase, naviPayConfigUseCase: NaviPayConfigUseCase, - private val resourceProvider: ResourceProvider, + paymentsUptimePollerUseCase: PaymentsUptimePollerUseCase, + paymentsUptimeRepository: PaymentsUptimeRepository, + resourceProvider: ResourceProvider, private val accountListCheckBalanceUseCase: AccountListCheckBalanceUseCase, private val naviPayPspManager: NaviPayPspManager, private val arcNudgeUseCase: ArcNudgeUseCase, @@ -159,6 +164,9 @@ constructor( transactionStatusUseCase = transactionStatusUseCase, litmusExperimentsUseCase = litmusExperimentsUseCase, naviPayConfigUseCase = naviPayConfigUseCase, + paymentsUptimePollerUseCase = paymentsUptimePollerUseCase, + paymentsUptimeRepository = paymentsUptimeRepository, + resourceProvider = resourceProvider, ), OneClickScreenContract { @@ -246,6 +254,10 @@ constructor( } is OneClickScreenEvent.OnBankAccountItemClicked -> { + isPaymentsServiceDown(paymentSource?.name.orEmpty())?.let { + handlePaymentsDowntime(it) + return@safeLaunch + } navigationScreen = NaviPaymentScreenType.FULL_PAYMENT_SCREEN naviPaymentAnalytics.onBankAccountClicked( selectedBankAccountId = state.value.selectedAccount?.accountId.orEmpty() @@ -617,6 +629,10 @@ constructor( state.value.selectedAccount?.eligibilityState?.isAccountEligible?.not().orFalse() private suspend fun handleCtaClicked(generateToken: () -> Unit) { + isPaymentsServiceDown(paymentSource?.name.orEmpty())?.let { + handlePaymentsDowntime(it) + return + } when (ctaAction) { CheckoutCtaAction.SEND_MONEY_NAVIGATION -> { navigationScreen = NaviPaymentScreenType.ONE_CLICK_CHECKOUT_SCREEN @@ -792,8 +808,14 @@ constructor( selectedBankAccountId = state.value.selectedAccount?.accountId.orEmpty() ) } else { - naviPaymentAnalytics.onRedirectingBottomSheetClosed( - selectedBankAccountId = state.value.selectedAccount?.accountId.orEmpty() + val error = + (bottomSheetStateHolder.value.bottomSheetUIState + as? OneClickCheckoutFooterBottomSheetUiState.Error) + ?.errorConfig + naviPaymentAnalytics.onBottomSheetCloseClicked( + bottomSheetState = + bottomSheetStateHolder.value.bottomSheetUIState::class.java.simpleName, + error = error, ) } updateBottomSheetUIState( @@ -1108,4 +1130,11 @@ constructor( it <= AppServiceManager.appVersionCode } ?: false) } + + private fun handlePaymentsDowntime(errorConfig: NaviPaymentErrorConfig) { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = OneClickCheckoutFooterBottomSheetUiState.Error(errorConfig), + ) + } } diff --git a/android/navi-payment/src/main/java/com/navi/payment/paymentscreen/model/ErrorReason.kt b/android/navi-payment/src/main/java/com/navi/payment/paymentscreen/model/ErrorReason.kt index 5364fe25d0..78b620f232 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/paymentscreen/model/ErrorReason.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/paymentscreen/model/ErrorReason.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2024 by Navi Technologies Limited + * * Copyright © 2024-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -27,4 +27,6 @@ sealed class PMSErrorReason : Parcelable, ErrorReason { data object TokenNotFoundError : PMSErrorReason() data object Unknown : PMSErrorReason() + + data object PaymentsServiceDown : PMSErrorReason() } diff --git a/android/navi-payment/src/main/java/com/navi/payment/utils/Constants.kt b/android/navi-payment/src/main/java/com/navi/payment/utils/Constants.kt index 0c93d1d0f4..b6683e6e6f 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/utils/Constants.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/utils/Constants.kt @@ -170,6 +170,9 @@ object Constants { const val SCREEN_NAME = "screen_name" const val IS_AMOUNT_READ_ONLY = "is_amount_read_only" + // Firestore + const val FIRESTORE_PAYMENTS_UPTIME_COLLECTION_PATH = "navipmt-uptime-status" + // CARDS const val CARD_VALIDATION_DEBOUNCE_TIME_IN_MILLIS = 50L const val DEFAULT_MIN_CVV_LENGTH = 3