diff --git a/.github/workflows/generate_build.yml b/.github/workflows/generate_build.yml
index ab893336bc..011a04c6de 100644
--- a/.github/workflows/generate_build.yml
+++ b/.github/workflows/generate_build.yml
@@ -105,10 +105,10 @@ jobs:
run: echo ${{ secrets.RELEASE_STORE_FILE }} | base64 -d >> app/navi-release-key.jks
- name: Build - APK - ${{ inputs.environment }}-${{ inputs.type }}
if: inputs.output == 'APK'
- run: ./gradlew package${{ inputs.environment }}${{ inputs.type }}UniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }}
+ run: ./gradlew package${{ inputs.environment }}${{ inputs.type }}UniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} -PNAVIPAY_SMV_BASE_URL=${{ secrets.NAVIPAY_SMV_BASE_URL }} -PNAVIPAY_SMV_CLIENT_ID=${{ secrets.NAVIPAY_SMV_CLIENT_ID }}
- name: Build - AAB - ${{ inputs.environment }}-${{ inputs.type }}
if: inputs.output == 'AAB'
- run: ./gradlew :app:bundle${{ inputs.environment }}${{ inputs.type }} -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }}
+ run: ./gradlew :app:bundle${{ inputs.environment }}${{ inputs.type }} -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} -PNAVIPAY_SMV_BASE_URL=${{ secrets.NAVIPAY_SMV_BASE_URL }} -PNAVIPAY_SMV_CLIENT_ID=${{ secrets.NAVIPAY_SMV_CLIENT_ID }}
- name: Upload - ${{ inputs.output }} - ${{ inputs.environment }}-${{ inputs.type }}
uses: actions/upload-artifact@v4
with:
diff --git a/.github/workflows/macrobenchmark.yml b/.github/workflows/macrobenchmark.yml
index 69cfd035c9..2da8350c88 100644
--- a/.github/workflows/macrobenchmark.yml
+++ b/.github/workflows/macrobenchmark.yml
@@ -47,7 +47,7 @@ jobs:
- name: Export Release Store File
run: echo ${{ secrets.RELEASE_STORE_FILE }} | base64 -d >> app/navi-release-key.jks
- name: Build - APK - app
- run: ./gradlew packageQaReleaseUniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }}
+ run: ./gradlew packageQaReleaseUniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} -PNAVIPAY_SMV_BASE_URL=${{ secrets.NAVIPAY_SMV_BASE_URL }} -PNAVIPAY_SMV_CLIENT_ID=${{ secrets.NAVIPAY_SMV_CLIENT_ID }}
- name: Build - APK - benchmark
run: ./gradlew benchmark:assembleQaBenchmark
- name: Authenticate Cloud SDK
diff --git a/Dockerfile b/Dockerfile
index e0db5071ae..baa35ad732 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -64,7 +64,9 @@ RUN --mount=type=secret,id=RELEASE_STORE_PASSWORD \
-PGI_RAZORPAY_KEY=$(cat /run/secrets/GI_RAZORPAY_KEY) \
-PGOOGLE_MAPS_KEY=$(cat /run/secrets/GOOGLE_MAPS_KEY) \
-PCODEPUSH_DEPLOYMENT_KEY=$(cat /run/secrets/CODEPUSH_DEPLOYMENT_KEY) \
- -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=$(cat /run/secrets/NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT)
+ -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=$(cat /run/secrets/NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT) \
+ -PNAVIPAY_SMV_BASE_URL=$(cat /run/secrets/NAVIPAY_SMV_BASE_URL) \
+ -PNAVIPAY_SMV_CLIENT_ID=$(cat /run/secrets/NAVIPAY_SMV_CLIENT_ID)
RUN --mount=type=secret,id=FLAVOR \
--mount=type=secret,id=NEXUS_URL \
diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000000..3a93e05a43
--- /dev/null
+++ b/android/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ airtel.in
+ sekuramobile.com
+ jio.com
+ passport.airtel.in
+ partnerapi.jio.com
+
+
diff --git a/android/app/src/qa/res/xml/network_security_config.xml b/android/app/src/qa/res/xml/network_security_config.xml
index ad427445dc..17f23aae45 100644
--- a/android/app/src/qa/res/xml/network_security_config.xml
+++ b/android/app/src/qa/res/xml/network_security_config.xml
@@ -2,12 +2,19 @@
+
+ airtel.in
+ sekuramobile.com
+ jio.com
+ passport.airtel.in
+ partnerapi.jio.com
+
diff --git a/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt b/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt
index c9064588a6..671418abe3 100644
--- a/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt
+++ b/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt
@@ -15,6 +15,7 @@ data class LitmusExperimentResponse(
)
data class VariantInfo(
+ @SerializedName("name") val name: String,
@SerializedName("enabled") val enabled: Boolean,
@SerializedName("payload") val payload: Map?,
)
diff --git a/android/navi-pay/build.gradle b/android/navi-pay/build.gradle
index 7715a257c8..50f3cafe40 100644
--- a/android/navi-pay/build.gradle
+++ b/android/navi-pay/build.gradle
@@ -51,11 +51,17 @@ android {
isDefault true
dimension "app"
buildConfigField 'String', 'NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT', formatString('LOCAL')
+ buildConfigField 'String', 'NAVIPAY_SMV_BASE_URL', formatString('https://api.sandbox.bureau.id/v2/auth/initiate')
+ buildConfigField 'String', 'NAVIPAY_SMV_CLIENT_ID', formatString('093f0fc3-bbe5-4944-a6c3-1bf410ae4237')
}
prod {
dimension "app"
- if (project.hasProperty('NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT')) {
+ if (project.hasProperty('NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT')
+ && project.hasProperty('NAVIPAY_SMV_BASE_URL')
+ && project.hasProperty('NAVIPAY_SMV_CLIENT_ID')) {
buildConfigField 'String', 'NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT', formatString("$NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT")
+ buildConfigField 'String', 'NAVIPAY_SMV_BASE_URL', formatString("$NAVIPAY_SMV_BASE_URL")
+ buildConfigField 'String', 'NAVIPAY_SMV_CLIENT_ID', formatString("$NAVIPAY_SMV_CLIENT_ID")
}
}
}
diff --git a/android/navi-pay/src/main/AndroidManifest.xml b/android/navi-pay/src/main/AndroidManifest.xml
index 9435c0ab00..e7bfbca81e 100644
--- a/android/navi-pay/src/main/AndroidManifest.xml
+++ b/android/navi-pay/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt
index eb1df25660..b8954142d8 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt
@@ -23,6 +23,7 @@ import com.navi.pay.common.model.view.CheckBalanceAnalyticsEventData
import com.navi.pay.common.model.view.DeviceData
import com.navi.pay.common.model.view.DeviceDetails
import com.navi.pay.common.model.view.NaviPayErrorConfig
+import com.navi.pay.common.model.view.NetworkProvider
import com.navi.pay.common.model.view.SimInfo
import com.navi.pay.common.setup.model.NaviPayCustomerStatus
import com.navi.pay.common.utils.NaviPayCommonUtils.toGetSimInfo
@@ -41,6 +42,7 @@ import com.navi.pay.onboarding.account.add.model.view.AccountType
import com.navi.pay.onboarding.account.add.model.view.BankEntity
import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity
import com.navi.pay.onboarding.account.linked.model.view.LinkedAccountsScreenSource
+import com.navi.pay.onboarding.binding.model.network.BindingType
import com.navi.pay.onboarding.binding.model.network.CustomerResponse
import com.navi.pay.onboarding.binding.model.network.ServiceProvider
import com.navi.pay.onboarding.faq.model.view.UpiVideoEntity
@@ -256,17 +258,19 @@ class NaviPayAnalytics private constructor() {
)
}
- fun smsSentSuccessfullyStartingPolling(
+ fun onStartBindStatusPolling(
onboardingSource: String? = null,
naviPaySessionAttributes: Map? = null,
+ bindingType: BindingType,
) {
NaviTrackEvent.trackEventOnClickStream(
- eventName = "NaviPay_Dev_smsSentSuccessfullyStartingPolling",
+ eventName = "NaviPay_Dev_onStartBindStatusPolling",
eventValues =
mapOf(
"naviPayOnboardingSource" to onboardingSource.orEmpty(),
"naviPayOnboardingSessionId" to
naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(),
+ "bindingType" to bindingType.name,
),
)
}
@@ -426,15 +430,15 @@ class NaviPayAnalytics private constructor() {
}
fun startSimBinding(
- simInfo: SimInfo?,
+ provider: NetworkProvider,
onboardingSource: String? = null,
naviPaySessionAttributes: Map? = null,
) {
NaviTrackEvent.trackEventOnClickStream(
"NaviPay_Setup_SimBinding_Start",
mapOf(
- Pair("simProvider", simInfo?.carrierName.orEmpty()),
- Pair("simId", simInfo?.subscriptionId.orEmpty()),
+ Pair("simProvider", provider.name),
+ Pair("simId", provider.ssid),
Pair("naviPayOnboardingSource", onboardingSource.orEmpty()),
Pair(
"naviPayOnboardingSessionId",
@@ -640,6 +644,52 @@ class NaviPayAnalytics private constructor() {
),
)
}
+
+ fun onSmvExperimentDisabled() {
+ NaviTrackEvent.trackEventOnClickStream("NaviPay_SmvExperimentDisabled")
+ }
+
+ fun onProviderNotSupportedForSmv(
+ displayableCarrierName: String,
+ smvSupportedProviders: List,
+ ) {
+ NaviTrackEvent.trackEventOnClickStream(
+ "NaviPay_ProviderNotSupportedForSmv",
+ mapOf(
+ "displayableCarrierName" to displayableCarrierName,
+ "smvSupportedProviders" to smvSupportedProviders.toString(),
+ ),
+ )
+ }
+
+ fun onSmvPreviouslyTriggered() {
+ NaviTrackEvent.trackEventOnClickStream("NaviPay_SmvPreviouslyTriggered")
+ }
+
+ fun onSelectedSimIsNotDefaultDataSim(selectedSimId: String, defaultDataSimId: String) {
+ NaviTrackEvent.trackEventOnClickStream(
+ "NaviPay_SelectedSimIsNotDefaultDataSim",
+ mapOf("selectedSimId" to selectedSimId, "defaultDataSimId" to defaultDataSimId),
+ )
+ }
+
+ fun onRouteNetworkViaCellularResult(result: Boolean) {
+ NaviTrackEvent.trackEventOnClickStream(
+ "NaviPay_RouteNetworkViaCellularResult",
+ mapOf("result" to result.toString()),
+ )
+ }
+
+ fun onInitiateSmvResponse(smvInitiateResponse: String?) {
+ NaviTrackEvent.trackEventOnClickStream(
+ "NaviPay_InitiateSmvResponse",
+ mapOf("smvInitiateResponse" to smvInitiateResponse.toString()),
+ )
+ }
+
+ fun onSmvInitiated() {
+ NaviTrackEvent.trackEventOnClickStream("NaviPay_SmvInitiated")
+ }
}
inner class NaviPayPermission {
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt
index dcce90fd52..57647fb8bb 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt
@@ -1,6 +1,6 @@
/*
*
- * * Copyright © 2024 by Navi Technologies Limited
+ * * Copyright © 2024-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -9,11 +9,18 @@ package com.navi.pay.common.connectivity
import android.content.Context
import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
import android.net.NetworkCapabilities
+import android.net.NetworkRequest
import com.navi.pay.common.model.view.SimInfo
import com.navi.pay.common.utils.NaviPayCommonUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.time.Duration
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
interface NaviPayNetworkConnectivity {
fun isInternetConnected(): Boolean
@@ -21,6 +28,14 @@ interface NaviPayNetworkConnectivity {
fun isAirplaneModeOn(): Boolean
fun getCurrentSimInfoList(): List
+
+ /**
+ * @throws TimeoutCancellationException if the binding process takes longer than the specified
+ * timeout.
+ */
+ suspend fun bindNetworkToCellular(timeout: Duration): Boolean
+
+ fun unbindNetwork()
}
class NaviPayNetworkConnectivityImpl @Inject constructor(@ApplicationContext val context: Context) :
@@ -29,13 +44,14 @@ class NaviPayNetworkConnectivityImpl @Inject constructor(@ApplicationContext val
override fun isInternetConnected(): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- val networkCapabilities = connectivityManager.activeNetwork ?: return false
- val actNw = connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
+ val activeNetwork = connectivityManager.activeNetwork ?: return false
+ val activeNetworkCapability =
+ connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
val result =
when {
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
+ activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
+ activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
+ activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
@@ -49,4 +65,64 @@ class NaviPayNetworkConnectivityImpl @Inject constructor(@ApplicationContext val
override fun getCurrentSimInfoList(): List {
return NaviPayCommonUtils.getCurrentSimInfoList(context = context)
}
+
+ override suspend fun bindNetworkToCellular(timeout: Duration): Boolean {
+
+ return withTimeout(timeout = timeout) {
+ suspendCancellableCoroutine { continuation ->
+ val connectivityManager =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val activeNetwork = connectivityManager.activeNetwork
+ val activeNetworkCapability =
+ connectivityManager.getNetworkCapabilities(activeNetwork)
+
+ if (
+ activeNetwork != null &&
+ activeNetworkCapability != null &&
+ activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ ) {
+ continuation.resume(true)
+ return@suspendCancellableCoroutine
+ }
+
+ val networkRequest =
+ NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build()
+
+ val networkCallback =
+ object : NetworkCallback() {
+
+ override fun onAvailable(network: Network) {
+ super.onAvailable(network)
+ if (connectivityManager.boundNetworkForProcess != network) {
+ connectivityManager.bindProcessToNetwork(network)
+ }
+ continuation.resume(true)
+ }
+
+ override fun onUnavailable() {
+ super.onUnavailable()
+ if (continuation.isActive) {
+ continuation.resume(false)
+ }
+ }
+ }
+
+ connectivityManager.requestNetwork(networkRequest, networkCallback)
+
+ continuation.invokeOnCancellation {
+ connectivityManager.unregisterNetworkCallback(networkCallback)
+ }
+ }
+ }
+ }
+
+ override fun unbindNetwork() {
+ val connectivityManager =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ connectivityManager.bindProcessToNetwork(null)
+ }
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt
index 8f19ab2231..32211249ed 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt
@@ -9,7 +9,6 @@ package com.navi.pay.common.utils
import android.Manifest
import android.app.Activity
-import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -21,7 +20,6 @@ import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.provider.Settings
-import android.telephony.SmsManager
import android.telephony.SubscriptionManager
import android.view.View
import androidx.activity.result.ActivityResultLauncher
@@ -40,7 +38,6 @@ import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
-import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.deeplink.util.DeeplinkConstants
import com.navi.base.deeplink.util.DeeplinkConstants.PRODUCT_HELP_PAGE
@@ -69,7 +66,6 @@ import com.navi.common.upi.WITHOUT_ONBOARDING_FLOW
import com.navi.common.utils.CommonRootDeviceUtil
import com.navi.common.utils.Constants.AT_THE_RATE
import com.navi.common.utils.capitalize
-import com.navi.common.utils.log
import com.navi.pay.BuildConfig
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
@@ -103,8 +99,6 @@ import com.navi.pay.utils.COMMA
import com.navi.pay.utils.DOT_IFSC_DOT_NPCI
import com.navi.pay.utils.HEX_FORMAT
import com.navi.pay.utils.HYPHEN
-import com.navi.pay.utils.INTENT_ACTION_SMS_DELIVERED
-import com.navi.pay.utils.INTENT_ACTION_SMS_SENT
import com.navi.pay.utils.KEY_CUSTOMER_STATUS
import com.navi.pay.utils.KEY_DEVICE_FINGERPRINT
import com.navi.pay.utils.KEY_UPI_LITE_ACTIVE
@@ -180,7 +174,7 @@ object NaviPayCommonUtils {
return ssidList
}
- private fun displayableCarrierName(actualCarrierName: String?): String {
+ fun displayableCarrierName(actualCarrierName: String?): String {
return when {
actualCarrierName == null -> EMPTY
actualCarrierName.contains(other = CARRIER_JIO, ignoreCase = true) -> CARRIER_JIO
@@ -226,71 +220,6 @@ object NaviPayCommonUtils {
naviPayAnalytics.onAppSettingsScreenLaunched()
}
- fun sendSMS(
- destinationNumberList: List,
- messageContent: String,
- subscriptionId: Int,
- activityContext: Context,
- ) {
- if (
- ActivityCompat.checkSelfPermission(activityContext, Manifest.permission.SEND_SMS) !=
- PackageManager.PERMISSION_GRANTED
- ) {
- return
- }
-
- if (isAirplaneModeOn(context = activityContext)) {
- return
- }
-
- val smsManager =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- activityContext
- .getSystemService(SmsManager::class.java)
- .createForSubscriptionId(subscriptionId)
- } else {
- SmsManager.getSmsManagerForSubscriptionId(subscriptionId)
- }
-
- val sentPendingIntent =
- PendingIntent.getBroadcast(
- activityContext,
- (0..1000).random(),
- Intent(INTENT_ACTION_SMS_SENT),
- PendingIntent.FLAG_IMMUTABLE,
- )
-
- val deliveredPendingIntent =
- PendingIntent.getBroadcast(
- activityContext,
- (0..1000).random(),
- Intent(INTENT_ACTION_SMS_DELIVERED),
- PendingIntent.FLAG_IMMUTABLE,
- )
-
- try {
- NaviTrackEvent.trackEventOnClickStream(
- eventName = "dev_navipay_send_sms",
- eventValues =
- mapOf(
- "destinationNumberList" to destinationNumberList.toString(),
- "messageContentLength" to messageContent.length.toString(),
- ),
- )
- destinationNumberList.forEach {
- smsManager.sendTextMessage(
- it,
- null,
- messageContent,
- sentPendingIntent,
- deliveredPendingIntent,
- )
- }
- } catch (e: Exception) {
- e.log()
- }
- }
-
fun getNaviPayAccessEligibility(context: Context): NaviPayAccessEligibility {
return if (
!FirebaseRemoteConfigHelper.getRawBoolean(FirebaseRemoteConfigHelper.NAVI_UPI_ENABLED)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt
index 58b122bd07..537b8ae360 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt
@@ -61,6 +61,7 @@ import com.navi.pay.management.moneytransfer.scanpay.LightSensorManagerImpl
import com.navi.pay.management.paytocontacts.PhoneContactManager
import com.navi.pay.management.paytocontacts.PhoneContactManagerImpl
import com.navi.pay.network.NaviPayHttpClient
+import com.navi.pay.network.retrofit.NaviPayExternalRetrofitService
import com.navi.pay.network.retrofit.NaviPayRetrofitService
import com.navi.pay.npcicl.NpciSessionHandler
import com.navi.pay.npcicl.NpciSessionHandlerImpl
@@ -124,6 +125,15 @@ object NaviPayNetworkModule {
.addConverterFactory(GsonConverterFactory.create(deserializer))
.build()
+ @Singleton
+ @Provides
+ fun providesNaviPayExternalApiService(): NaviPayExternalRetrofitService =
+ Retrofit.Builder()
+ .baseUrl("https://api.github.com/")
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(NaviPayExternalRetrofitService::class.java)
+
@Singleton
@Provides
fun providesNaviPayApiService(@NaviPayRetrofit retrofit: Retrofit): NaviPayRetrofitService =
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayExternalRetrofitService.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayExternalRetrofitService.kt
new file mode 100644
index 0000000000..a1c93cc363
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayExternalRetrofitService.kt
@@ -0,0 +1,18 @@
+/*
+ *
+ * * Copyright © 2024-2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.network.retrofit
+
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.QueryMap
+import retrofit2.http.Url
+
+interface NaviPayExternalRetrofitService {
+ @GET
+ suspend fun initiateSmv(@Url url: String, @QueryMap params: Map): Response
+}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt
index f76c308c66..41c7c59a7f 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt
@@ -1,6 +1,6 @@
/*
*
- * * Copyright © 2022-2024 by Navi Technologies Limited
+ * * Copyright © 2022-2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -129,7 +129,7 @@ interface NaviPayRetrofitService {
@Body addAccountRequest: AddAccountRequest
): Response>
- @POST("/gateway-service/$NAVI_PAY_API_VERSION/navipay/bind-device")
+ @POST("/gateway-service/$NAVI_PAY_API_VERSION2/navipay/bind-device")
suspend fun bindDevice(
@Body bindDeviceRequest: BindDeviceRequest
): Response>
@@ -144,7 +144,7 @@ interface NaviPayRetrofitService {
@Body declineDeviceRequest: DeclineDeviceRequest
): Response>
- @POST("/gateway-service/v2/navipay/account/linked-accounts")
+ @POST("/gateway-service/$NAVI_PAY_API_VERSION2/navipay/account/linked-accounts")
suspend fun fetchLinkedAccounts(
@Body linkedAccountsRequest: LinkedAccountsRequest
): Response>
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt
index 19b6520c1d..e13a2e3488 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt
@@ -16,4 +16,5 @@ data class BindDeviceRequest(
@SerializedName("udfParameters") val udfParameters: String = "{}",
@SerializedName("merchantCustomerId") val merchantCustomerId: String,
@SerializedName("deviceData") val deviceData: DeviceData,
+ @SerializedName("bindingType") val bindingType: BindingType,
)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt
index f359636938..b7b04ac017 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt
@@ -11,10 +11,20 @@ import com.google.gson.annotations.SerializedName
data class BindDeviceResponse(
@SerializedName("merchantCustomerId") val merchantCustomerId: String,
+ @SerializedName("internalDeviceId") val internalDeviceId: String,
+ @SerializedName("smsBindingData") val smsBindingData: SmsBindingData?,
+ @SerializedName("smvBindingData") val smvBindingData: SmvBindingData?,
+)
+
+data class SmvBindingData(
+ @SerializedName("smvContent") val smvContent: String,
+ @SerializedName("aggregator") val aggregator: String,
+)
+
+data class SmsBindingData(
@SerializedName("serviceProviders") val serviceProviders: List,
@SerializedName("smsContent") val smsContent: String,
@SerializedName("expiryTimestamp") val expiryTimestamp: String,
- @SerializedName("internalDeviceId") val internalDeviceId: String,
)
data class ServiceProvider(
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt
index 4777e21ba5..19d35aae1e 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt
@@ -14,4 +14,5 @@ data class BindDeviceStatusRequest(
@SerializedName("deviceData") val deviceData: DeviceData,
@SerializedName("internalDeviceId") val internalDeviceId: String,
@SerializedName("merchantCustomerId") val merchantCustomerId: String,
+ @SerializedName("bindingType") val bindingType: BindingType,
)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt
new file mode 100644
index 0000000000..5bb96afd21
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt
@@ -0,0 +1,13 @@
+/*
+ *
+ * * Copyright © 2024-2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.onboarding.binding.model.network
+
+enum class BindingType {
+ SMV,
+ SMS,
+}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt
index 0785f88104..027b9ff038 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt
@@ -16,21 +16,14 @@ data class NaviPayOnboardingConfig(
)
data class NaviPayOnboardingConfigContent(
+ @SerializedName("simSelectionSheetTitleV2")
val simSelectionSheetTitleV2: String = "Verify your number %s",
- @SerializedName("singleSimSelectionSheetDescription")
- val singleSimSelectionSheetDescriptionV2: String =
- "We will send an SMS from your number. Please do not press back or close the app.",
- @SerializedName("multipleSimSelectionSheetDescription")
- val multipleSimSelectionSheetDescriptionV2: String =
- "We will send an SMS from your number. Please select the linked SIM and do not close the app.",
@SerializedName("simSelectionSheetTitle")
val simSelectionSheetTitle: String = "Validate mobile number",
@SerializedName("simSelectionSheetDescription")
val simSelectionSheetDescription: String = "We will send an SMS from {{PHONE_NUMBER}}.",
@SerializedName("multipleSimSheetDescriptionLine2")
val multipleSimSheetDescriptionLine2: String = "Please select your sim provider.",
- @SerializedName("smsChargesText")
- val smsChargesText: String = "*Standard SMS charges will apply",
@SerializedName("declineDeviceBindingTitle")
val declineDeviceBindingTitle: String = "Mobile verification failed",
@SerializedName("declineDeviceBindingDescription")
@@ -42,9 +35,6 @@ data class NaviPayOnboardingConfigContent(
val declineDeviceBindingSecondaryButtonText: String = "Cancel",
@SerializedName("deviceBindingProgressTitle")
val deviceBindingProgressTitle: String = "Verifying your mobile number",
- @SerializedName("deviceBindingProgressDescription")
- val deviceBindingProgressDescription: String =
- "Please do not press back or close the app. This usually takes a few seconds.",
@SerializedName("deviceBindingProgressTitleList")
val deviceBindingProgressTitleList: List =
listOf("Verifying your number", "Please wait, taking longer than usual"),
@@ -70,4 +60,6 @@ data class NaviPayOnboardingConfigContent(
@SerializedName("smsVerificationLocallyExpiredDescription")
val smsVerificationLocallyExpiredDescription: String =
"NPCI verification request has expired. Please try again.",
+ @SerializedName("smvSupportedProviders")
+ val smvSupportedProviders: List = listOf("Jio", "VI"),
)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/SmvInitiateResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/SmvInitiateResponse.kt
new file mode 100644
index 0000000000..d57e1ae6d9
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/SmvInitiateResponse.kt
@@ -0,0 +1,12 @@
+/*
+ *
+ * * Copyright © 2024-2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.onboarding.binding.model.network
+
+import com.google.gson.annotations.SerializedName
+
+data class SmvInitiateResponse(@SerializedName("code") val code: String)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt
new file mode 100644
index 0000000000..b63605115f
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt
@@ -0,0 +1,30 @@
+/*
+ *
+ * * Copyright © 2024-2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.onboarding.binding.model.view
+
+sealed class DeviceBindingState {
+ data object None : DeviceBindingState()
+
+ data object Initiated : DeviceBindingState()
+
+ data object Binding : DeviceBindingState()
+
+ data object Verifying : DeviceBindingState()
+
+ data object Success : DeviceBindingState()
+
+ data object Failure : DeviceBindingState()
+
+ data object DeclineBinding : DeviceBindingState()
+
+ companion object {
+ fun DeviceBindingState.isVerificationOngoing(): Boolean {
+ return this == Binding || this == Verifying
+ }
+ }
+}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmsVerificationState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmsVerificationState.kt
deleted file mode 100644
index 21beedcbac..0000000000
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmsVerificationState.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- *
- * * Copyright © 2024 by Navi Technologies Limited
- * * All rights reserved. Strictly confidential
- *
- */
-
-package com.navi.pay.onboarding.binding.model.view
-
-sealed class SmsVerificationState {
- data object None : SmsVerificationState()
-
- data object Initiated : SmsVerificationState()
-
- data object Sending : SmsVerificationState()
-
- data object Verifying : SmsVerificationState()
-
- data object Success : SmsVerificationState()
-
- data object Failure : SmsVerificationState()
-
- data object DeclineBinding : SmsVerificationState()
-
- companion object {
- fun SmsVerificationState.isSmsVerificationOngoing(): Boolean {
- return this == Sending || this == Verifying
- }
- }
-}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmvEligibilityStatus.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmvEligibilityStatus.kt
new file mode 100644
index 0000000000..45bd04f332
--- /dev/null
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmvEligibilityStatus.kt
@@ -0,0 +1,10 @@
+/*
+ *
+ * * Copyright © 2024-2025 by Navi Technologies Limited
+ * * All rights reserved. Strictly confidential
+ *
+ */
+
+package com.navi.pay.onboarding.binding.model.view
+
+data class SmvEligibilityStatus(val isEligible: Boolean, val isSmsPermissionEnabled: Boolean)
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt
index cc34759cf4..aa5ef73480 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt
@@ -10,6 +10,7 @@ package com.navi.pay.onboarding.binding.repository
import com.navi.common.checkmate.model.MetricInfo
import com.navi.common.network.models.RepoResult
import com.navi.common.network.retrofit.ResponseCallback
+import com.navi.pay.network.retrofit.NaviPayExternalRetrofitService
import com.navi.pay.network.retrofit.NaviPayRetrofitService
import com.navi.pay.onboarding.binding.model.network.BindDeviceRequest
import com.navi.pay.onboarding.binding.model.network.BindDeviceResponse
@@ -18,10 +19,14 @@ import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusResponse
import com.navi.pay.onboarding.binding.model.network.DeclineDeviceRequest
import com.navi.pay.onboarding.binding.model.network.DeclineDeviceResponse
import javax.inject.Inject
+import retrofit2.Response
class NaviPayOnboardingRepository
@Inject
-constructor(private val naviPayRetrofitService: NaviPayRetrofitService) : ResponseCallback() {
+constructor(
+ private val naviPayRetrofitService: NaviPayRetrofitService,
+ private val naviPayExternalRetrofitService: NaviPayExternalRetrofitService,
+) : ResponseCallback() {
suspend fun bindDevice(
bindDeviceRequest: BindDeviceRequest,
@@ -58,4 +63,8 @@ constructor(private val naviPayRetrofitService: NaviPayRetrofitService) : Respon
metricInfo = metricInfo,
)
}
+
+ suspend fun initiateSmv(url: String, params: Map): Response {
+ return naviPayExternalRetrofitService.initiateSmv(url = url, params = params)
+ }
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt
index b8206f427a..e64edf5bf9 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt
@@ -45,7 +45,6 @@ import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.model.view.NaviPayScreenType
import com.navi.pay.common.model.view.NaviPermissionResult
import com.navi.pay.common.model.view.PermissionResult
-import com.navi.pay.common.model.view.SimInfo
import com.navi.pay.common.model.view.rememberMultiplePermissions
import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.utils.ErrorEventHandler
@@ -57,7 +56,6 @@ import com.navi.pay.destinations.NaviPayPermissionScreenV2Destination
import com.navi.pay.onboarding.account.add.model.view.EnabledAccountAdditionTypes
import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetType
import com.navi.pay.onboarding.binding.model.view.OnboardingDeviceData
-import com.navi.pay.onboarding.binding.model.view.SmsVerificationState
import com.navi.pay.onboarding.binding.viewmodel.NaviPayOnboardingViewModel
import com.navi.pay.onboarding.common.NaviPayOnBoardingActions
import com.navi.pay.permission.model.view.PermissionState
@@ -143,25 +141,11 @@ fun NaviPayOnboardingScreen(
val coroutineScope = rememberCoroutineScope()
val bottomSheetStateHolder by
naviPayOnboardingViewModel.bottomSheetStateHolder.collectAsStateWithLifecycle()
- val smsVerificationState by
- naviPayOnboardingViewModel.smsVerificationState.collectAsStateWithLifecycle()
- val selectedSimInfo by naviPayOnboardingViewModel.selectedSimInfo.collectAsStateWithLifecycle()
val enabledAccountTypes by
naviPayOnboardingViewModel.enabledAccountTypes.collectAsStateWithLifecycle()
val preferredBankCode by
naviPayOnboardingViewModel.preferredBankCode.collectAsStateWithLifecycle()
- selectedSimInfo?.let {
- ObserverSmsVerificationState(
- naviPayOnboardingActivity = naviPayOnboardingActivity,
- selectedSimInfo = it,
- smsVerificationState = smsVerificationState,
- naviPayOnboardingViewModel = naviPayOnboardingViewModel,
- naviPayAnalytics = naviPayAnalytics,
- onboardingSource = onboardingSource,
- )
- }
-
val bottomSheetState =
rememberModalBottomSheetState(skipPartiallyExpanded = true, confirmValueChange = { false })
@@ -224,7 +208,7 @@ fun NaviPayOnboardingScreen(
context = naviPayOnboardingActivity.applicationContext
),
)
- naviPayOnboardingViewModel.startSimBinding()
+ naviPayOnboardingViewModel.confirmSimSelection()
}
NaviPermissionResult.HardDenied -> {
naviPayAnalytics.onPermissionDenied(
@@ -595,54 +579,6 @@ private fun InitLifecycleListener(naviPayOnboardingViewModel: NaviPayOnboardingV
}
}
-@Composable
-private fun ObserverSmsVerificationState(
- naviPayOnboardingActivity: NaviPayOnboardingActivity,
- selectedSimInfo: SimInfo,
- smsVerificationState: SmsVerificationState,
- naviPayOnboardingViewModel: NaviPayOnboardingViewModel,
- naviPayAnalytics: NaviPayAnalytics.NaviPaySetup,
- onboardingSource: String,
-) {
- when (smsVerificationState) {
- SmsVerificationState.Sending -> {
- LaunchedEffect(Unit) {
- val serviceProviderNumberList =
- naviPayOnboardingViewModel.bindDeviceResponse.serviceProviders.map { it.number }
- NaviPayCommonUtils.sendSMS(
- destinationNumberList = serviceProviderNumberList,
- messageContent = naviPayOnboardingViewModel.bindDeviceResponse.smsContent,
- subscriptionId = selectedSimInfo.subscriptionId.toInt(),
- activityContext = naviPayOnboardingActivity.baseContext,
- )
-
- naviPayAnalytics.startSimBinding(
- simInfo = selectedSimInfo,
- onboardingSource = onboardingSource,
- naviPaySessionAttributes =
- naviPayOnboardingViewModel.getNaviPaySessionAttributes(),
- )
- naviPayOnboardingViewModel.checkForSMSSentAndStartPolling(
- vmnNumbers = serviceProviderNumberList,
- smsContent = naviPayOnboardingViewModel.bindDeviceResponse.smsContent,
- )
- }
- }
- SmsVerificationState.Success -> {
- LaunchedEffect(Unit) {
- naviPayAnalytics.simBindingSuccess(
- onboardingSource = onboardingSource,
- naviPaySessionAttributes =
- naviPayOnboardingViewModel.getNaviPaySessionAttributes(),
- )
- naviPayOnboardingViewModel.addScanAndPayLauncherWidget()
- naviPayOnboardingViewModel.handleNavigationOnCustomerStatus()
- }
- }
- else -> Unit
- }
-}
-
private fun onAllPermissionsGrantedForOnboarding(
applicationContext: Context,
naviPayOnboardingViewModel: NaviPayOnboardingViewModel,
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt
index ecdbeaccca..1b8fd4e01f 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt
@@ -40,9 +40,10 @@ import com.navi.pay.common.model.view.SimInfo
import com.navi.pay.common.model.view.displayableIndex
import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.ui.ImageWithBackground
-import com.navi.pay.common.ui.ThemeRoundedButton
+import com.navi.pay.common.ui.LoaderRoundedButton
import com.navi.pay.onboarding.binding.viewmodel.NaviPayOnboardingViewModel
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS
+import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE
import com.navi.pay.utils.clickableDebounce
@Composable
@@ -66,15 +67,8 @@ fun SimSelectionContent(
)
}
- val description = remember {
- if (currentSimInfoList.size > 1) {
- naviPayOnboardingViewModel.naviPayOnboardingConfig.config
- .multipleSimSelectionSheetDescriptionV2
- } else {
- naviPayOnboardingViewModel.naviPayOnboardingConfig.config
- .singleSimSelectionSheetDescriptionV2
- }
- }
+ val isSmvEligibilityCheckOngoing by
+ naviPayOnboardingViewModel.isSmvEligibilityCheckOngoing.collectAsStateWithLifecycle()
Column(
modifier =
@@ -101,7 +95,13 @@ fun SimSelectionContent(
Spacer(modifier = Modifier.height(8.dp))
NaviText(
- text = description,
+ text =
+ stringResource(
+ id =
+ if (currentSimInfoList.size > 1)
+ R.string.np_multi_sim_selection_sheet_description
+ else R.string.np_single_sim_selection_sheet_description
+ ),
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
@@ -144,7 +144,7 @@ fun SimSelectionContent(
Spacer(modifier = Modifier.height(8.dp))
NaviText(
- text = naviPayOnboardingViewModel.naviPayOnboardingConfig.config.smsChargesText,
+ text = stringResource(R.string.sms_verification_charges_info),
fontSize = 10.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
@@ -153,9 +153,12 @@ fun SimSelectionContent(
Spacer(modifier = Modifier.height(32.dp))
- ThemeRoundedButton(
+ LoaderRoundedButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.verify),
+ enabled = !isSmvEligibilityCheckOngoing,
+ lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE,
+ showLoader = isSmvEligibilityCheckOngoing,
) {
if (currentSimInfoList.size == 1) {
naviPayAnalytics.onSingleSimConfirmationClick(
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt
index 0ec8682642..85a6076083 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt
@@ -16,12 +16,15 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
@@ -29,10 +32,11 @@ import com.navi.naviwidgets.extensions.NaviText
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.theme.color.NaviPayColor
-import com.navi.pay.common.ui.ThemeRoundedButton
+import com.navi.pay.common.ui.LoaderRoundedButton
import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetType
import com.navi.pay.onboarding.binding.viewmodel.NaviPayOnboardingViewModel
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS
+import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE
@Composable
fun SingleSimConfirmationBottomSheetContent(
@@ -54,10 +58,8 @@ fun SingleSimConfirmationBottomSheetContent(
)
}
- val description = remember {
- naviPayOnboardingViewModel.naviPayOnboardingConfig.config
- .singleSimSelectionSheetDescriptionV2
- }
+ val isSmvEligibilityCheckOngoing by
+ naviPayOnboardingViewModel.isSmvEligibilityCheckOngoing.collectAsStateWithLifecycle()
Column(
modifier =
@@ -82,7 +84,7 @@ fun SingleSimConfirmationBottomSheetContent(
Spacer(modifier = Modifier.height(8.dp))
NaviText(
- text = description,
+ text = stringResource(id = R.string.np_single_sim_selection_sheet_description),
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
@@ -102,17 +104,21 @@ fun SingleSimConfirmationBottomSheetContent(
Spacer(modifier = Modifier.height(32.dp))
- ThemeRoundedButton(
+ LoaderRoundedButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.verify),
- ) {
- val selectedSim = currentSimInfoList[0]
- naviPayAnalytics.onSingleSimConfirmationClick(
- simInfo = selectedSim,
- isFirstTimeUserExperience = false,
- )
- naviPayOnboardingViewModel.selectSim(selectedSimInfo = selectedSim)
- naviPayOnboardingViewModel.confirmSimSelection()
- }
+ enabled = !isSmvEligibilityCheckOngoing,
+ lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE,
+ showLoader = isSmvEligibilityCheckOngoing,
+ onClick = {
+ val selectedSim = currentSimInfoList[0]
+ naviPayAnalytics.onSingleSimConfirmationClick(
+ simInfo = selectedSim,
+ isFirstTimeUserExperience = false,
+ )
+ naviPayOnboardingViewModel.selectSim(selectedSimInfo = selectedSim)
+ naviPayOnboardingViewModel.confirmSimSelection()
+ },
+ )
}
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt
index 13ebc49d14..653bc7ac03 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt
@@ -10,6 +10,7 @@ package com.navi.pay.onboarding.binding.viewmodel
import android.app.Activity
import android.content.Intent
import android.os.Build
+import android.telephony.SubscriptionManager
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.lifecycle.SavedStateHandle
@@ -17,8 +18,11 @@ import androidx.lifecycle.viewModelScope
import com.google.gson.reflect.TypeToken
import com.navi.base.AppServiceManager
import com.navi.base.cache.datastore.DataStoreHelper
+import com.navi.base.cache.model.NaviCacheEntity
+import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.utils.BaseUtils
import com.navi.base.utils.ResourceProvider
+import com.navi.base.utils.orFalse
import com.navi.base.utils.orTrue
import com.navi.common.di.CoroutineDispatcherProvider
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
@@ -29,8 +33,10 @@ import com.navi.common.network.models.isError
import com.navi.common.network.models.isSuccess
import com.navi.common.network.models.isSuccessWithData
import com.navi.common.repo.PermissionSubmitRepository
+import com.navi.common.usecase.LitmusExperimentsUseCase
import com.navi.common.utils.EMPTY
import com.navi.common.utils.NaviApiPoller
+import com.navi.pay.BuildConfig
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_SETUP
@@ -72,14 +78,16 @@ import com.navi.pay.onboarding.binding.model.network.BindDeviceRequest
import com.navi.pay.onboarding.binding.model.network.BindDeviceResponse
import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusRequest
import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusResponse
+import com.navi.pay.onboarding.binding.model.network.BindingType
import com.navi.pay.onboarding.binding.model.network.CustomerRequest
import com.navi.pay.onboarding.binding.model.network.DeclineDeviceRequest
import com.navi.pay.onboarding.binding.model.network.NaviPayOnboardingConfig
+import com.navi.pay.onboarding.binding.model.view.DeviceBindingState
+import com.navi.pay.onboarding.binding.model.view.DeviceBindingState.Companion.isVerificationOngoing
import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetStateHolder
import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetType
import com.navi.pay.onboarding.binding.model.view.OnboardingDeviceData
-import com.navi.pay.onboarding.binding.model.view.SmsVerificationState
-import com.navi.pay.onboarding.binding.model.view.SmsVerificationState.Companion.isSmsVerificationOngoing
+import com.navi.pay.onboarding.binding.model.view.SmvEligibilityStatus
import com.navi.pay.onboarding.binding.repository.NaviPayOnboardingRepository
import com.navi.pay.onboarding.common.NaviPayOnBoardingActions
import com.navi.pay.onboarding.common.NaviPayOnboardingActionsType
@@ -91,7 +99,9 @@ import com.navi.pay.permission.utils.PermissionUtils
import com.navi.pay.tstore.list.usecase.SyncOrderHistoryUseCase
import com.navi.pay.utils.DS_KEY_NAVI_PAY_CUSTOMER_STATUS
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITHOUT_PLUS
+import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING
import com.navi.pay.utils.NAVI_PAY_API_STATUS_SUCCESS
+import com.navi.pay.utils.NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED
import com.navi.pay.utils.NAVI_PAY_ENCRYPT_SHARED_PREF_DATA_KEYS
import com.navi.pay.utils.NAVI_PAY_GREEN_TICK_LOTTIE
import com.navi.pay.utils.NAVI_PAY_NON_ENCRYPT_SHARED_PREF_DATA_KEYS
@@ -110,6 +120,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.BufferOverflow
@@ -123,6 +134,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
+import org.json.JSONObject
@HiltViewModel
class NaviPayOnboardingViewModel
@@ -153,7 +165,9 @@ constructor(
private val liteAccountSyncUseCase: LiteAccountSyncUseCase,
private val permissionSubmitRepository: PermissionSubmitRepository,
private val syncUpiLiteMandateInfoUseCase: SyncUpiLiteMandateInfoUseCase,
- private val savedStateHandle: SavedStateHandle,
+ private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
+ private val naviCacheRepository: NaviCacheRepository,
+ savedStateHandle: SavedStateHandle,
) : NaviPayBaseVM() {
private val naviPayAnalytics: NaviPayAnalytics.NaviPaySetup =
@@ -175,12 +189,9 @@ constructor(
)
val onboardingAction = _onboardingAction.asSharedFlow()
- private val _isFirstTimeUserExperience = MutableStateFlow(false)
- val isFirstTimeUserExperience = _isFirstTimeUserExperience.asStateFlow()
+ private val isFirstTimeUserExperience = MutableStateFlow(false)
- private val _smsVerificationState =
- MutableStateFlow(SmsVerificationState.None)
- val smsVerificationState = _smsVerificationState.asStateFlow()
+ private val deviceBindingState = MutableStateFlow(DeviceBindingState.None)
private val _simInfoList =
MutableStateFlow(
@@ -199,7 +210,7 @@ constructor(
val requestPermission = _requestPermission.asSharedFlow()
private var deviceData: DeviceData? = null
- lateinit var bindDeviceResponse: BindDeviceResponse
+ private lateinit var bindDeviceResponse: BindDeviceResponse
private val deferredApiCallList = mutableListOf>()
private val _isHardUpdateRequired = MutableSharedFlow(replay = 1)
@@ -235,6 +246,9 @@ constructor(
private val _finishActivity = MutableSharedFlow(replay = 1)
val finishActivity = _finishActivity.asSharedFlow()
+ private val _isSmvEligibilityCheckOngoing = MutableStateFlow(false)
+ val isSmvEligibilityCheckOngoing = _isSmvEligibilityCheckOngoing.asStateFlow()
+
private val _redirectToSetPinAndFinishActivity = MutableSharedFlow(replay = 1)
val redirectToSetPinAndFinishActivity = _redirectToSetPinAndFinishActivity.asSharedFlow()
@@ -246,7 +260,12 @@ constructor(
var naviPayOnboardingConfig = NaviPayOnboardingConfig()
private var isAwayFromMainScreen = false
- private val naviApiPoller by lazy { NaviApiPoller(repeatInterval = POLLING_INTERVAL) }
+ private val naviApiPoller by lazy {
+ NaviApiPoller(
+ repeatInterval = POLLING_INTERVAL,
+ totalPollingDurationInMillis = MAX_POLLING_DURATION_IN_MILLIS,
+ )
+ }
private val naviPayDefaultConfig = NaviPayDefaultConfig()
val appUpgradeData =
@@ -263,6 +282,7 @@ constructor(
private companion object {
private val POLLING_INTERVAL = 2.5.seconds
+ private const val MAX_POLLING_DURATION_IN_MILLIS = 60_000L
private const val TAG_CUSTOMER_SETUP_ERROR = "CUSTOMER_SETUP_ERROR"
}
@@ -348,7 +368,7 @@ constructor(
}
fun updateIsFirstTimeUserExperience(isFirstTimeUserExperience: Boolean) {
- _isFirstTimeUserExperience.update { isFirstTimeUserExperience }
+ this.isFirstTimeUserExperience.update { isFirstTimeUserExperience }
}
private fun updateBindingProgress(progress: Float) {
@@ -470,10 +490,6 @@ constructor(
showLoadingBottomSheet(isVisible = false)
onSetupSuccess(customerStatus = naviPaySetupStatus.customerStatus)
}
- else -> {
- showLoadingBottomSheet(isVisible = false)
- updateOnboardingAction(onboardingAction = NaviPayOnBoardingActions.FinishScreen)
- }
}
}
}
@@ -588,7 +604,7 @@ constructor(
}
private fun isDeviceVerificationOngoing(): Boolean {
- return smsVerificationState.value.isSmsVerificationOngoing()
+ return deviceBindingState.value.isVerificationOngoing()
}
private fun showLoadingBottomSheet(isVisible: Boolean) {
@@ -657,21 +673,37 @@ constructor(
fun confirmSimSelection() {
viewModelScope.safeLaunch(coroutineDispatcherProvider.io) {
- val multipleStatePermission =
- PermissionUtils.permissionDataMap.getOrElse(
- FIRST_TIME_SCREEN_PERMISSION_KEY,
- defaultValue = { emptyList() },
- )
+ val smvEligibilityStatus = getSmvEligibilityStatus(provider = getNetworkProvider())
- if (
- permissionStateProvider.isPermissionGranted(
- permissionList = multipleStatePermission.flatMap { it.qualifierList }
- )
- ) {
- startSimBinding()
- } else {
- _requestPermission.emit(FIRST_TIME_SCREEN_PERMISSION_KEY)
+ if (!smvEligibilityStatus.isEligible) {
+ checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv = false)
+ return@safeLaunch
}
+
+ // SMV is eligible cases
+ if (!smvEligibilityStatus.isSmsPermissionEnabled) {
+ startSimBinding(isBindingEligibleForSmv = true)
+ } else {
+ checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv = true)
+ }
+ }
+ }
+
+ private suspend fun checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv: Boolean) {
+ val multipleStatePermission =
+ PermissionUtils.permissionDataMap.getOrElse(
+ FIRST_TIME_SCREEN_PERMISSION_KEY,
+ defaultValue = { emptyList() },
+ )
+
+ if (
+ permissionStateProvider.isPermissionGranted(
+ permissionList = multipleStatePermission.flatMap { it.qualifierList }
+ )
+ ) {
+ startSimBinding(isBindingEligibleForSmv = isBindingEligibleForSmv)
+ } else {
+ _requestPermission.emit(FIRST_TIME_SCREEN_PERMISSION_KEY)
}
}
@@ -757,145 +789,205 @@ constructor(
fun getNaviPaySessionAttributes(): Map =
naviPaySessionHelper.getNaviPaySessionAttributes()
- fun startSimBinding() {
- viewModelScope.safeLaunch(Dispatchers.IO) {
- naviPayAnalytics.onPermissionGranted(
+ private suspend fun startSimBinding(isBindingEligibleForSmv: Boolean) {
+ naviPayAnalytics.onPermissionGranted(
+ onboardingSource = onboardingSource.value,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ smsPermissionGiven = true,
+ phonePermissionGiven = true,
+ )
+ if (selectedSimInfo.value == null || userPhoneNumber.isBlank()) {
+ notifyError()
+ naviPayAnalytics.onSimDataNotFoundBottomDevEvent(
+ simInfoList = simInfoList.value,
+ userPhoneNumber = userPhoneNumber,
onboardingSource = onboardingSource.value,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
- smsPermissionGiven = true,
- phonePermissionGiven = true,
)
- if (selectedSimInfo.value == null || userPhoneNumber.isBlank()) {
- notifyError()
- naviPayAnalytics.onSimDataNotFoundBottomDevEvent(
- simInfoList = simInfoList.value,
- userPhoneNumber = userPhoneNumber,
- onboardingSource = onboardingSource.value,
- naviPaySessionAttributes = getNaviPaySessionAttributes(),
- )
- return@safeLaunch
- }
- _bindingProgress.update { 0f }
- updateBottomSheetUIState(
- showBottomSheet = true,
- bottomSheetUIState =
- NaviPayOnboardingBottomSheetType.BindingInProgressBottomSheet(
- rolodexTitleList =
- naviPayOnboardingConfig.config.deviceBindingProgressTitleList,
- descriptionText =
- naviPayOnboardingConfig.config.deviceBindingProgressDescription,
+ return
+ }
+ _bindingProgress.update { 0f }
+ updateBottomSheetUIState(
+ showBottomSheet = true,
+ bottomSheetUIState =
+ NaviPayOnboardingBottomSheetType.BindingInProgressBottomSheet(
+ rolodexTitleList =
+ naviPayOnboardingConfig.config.deviceBindingProgressTitleList,
+ descriptionText =
+ resourceProvider.getString(
+ resId = R.string.np_device_binding_progress_description
+ ),
+ ),
+ )
+ updateDeviceBindingState(DeviceBindingState.Initiated)
+
+ // Step 1: Get customer API call to get merchant customer ID
+ deviceData =
+ DeviceData(
+ deviceFingerPrint = "",
+ provider =
+ NetworkProvider(
+ ssid = selectedSimInfo.value?.subscriptionId ?: EMPTY,
+ phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber",
+ simSlot = selectedSimInfo.value?.simSlotIndex.toString(),
+ name = selectedSimInfo.value?.carrierName ?: EMPTY,
),
- )
- updateSmsVerificationState(SmsVerificationState.Initiated)
-
- // Step 1: Get customer API call to get merchant customer ID
- deviceData =
- DeviceData(
- deviceFingerPrint = "",
- provider =
- NetworkProvider(
- ssid = selectedSimInfo.value?.subscriptionId ?: EMPTY,
- phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber",
- simSlot = selectedSimInfo.value?.simSlotIndex.toString(),
- name = selectedSimInfo.value?.carrierName ?: EMPTY,
- ),
- deviceId = onboardingDeviceData.deviceId,
- packageName = onboardingDeviceData.packageName,
- )
-
- naviPayAnalytics.onGetCustomerCallBeforeBindingStarts(
- deviceData = deviceData,
- onboardingSource = onboardingSource.value,
- naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ deviceId = onboardingDeviceData.deviceId,
+ packageName = onboardingDeviceData.packageName,
)
- val customerAPIResponse =
- commonRepository.getCustomer(
- customerRequest = CustomerRequest(deviceData = deviceData!!),
- metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP),
- )
+ naviPayAnalytics.onGetCustomerCallBeforeBindingStarts(
+ deviceData = deviceData,
+ onboardingSource = onboardingSource.value,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
- if (!customerAPIResponse.isSuccessWithData()) {
- updateSmsVerificationState(SmsVerificationState.Failure)
- onBindingError(customerAPIResponse)
- return@safeLaunch
+ val customerAPIResponse =
+ commonRepository.getCustomer(
+ customerRequest = CustomerRequest(deviceData = deviceData!!),
+ metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP),
+ )
+
+ if (!customerAPIResponse.isSuccessWithData()) {
+ updateDeviceBindingState(DeviceBindingState.Failure)
+ onBindingError(customerAPIResponse)
+ return
+ }
+
+ val customerResponse = customerAPIResponse.data!!
+
+ // Step 2: Bind device API call to get SMS content & VMNs list
+
+ val provider = getNetworkProvider()
+
+ val deviceDetails =
+ DeviceDetails(
+ deviceId = onboardingDeviceData.deviceId,
+ manufacturer = Build.MANUFACTURER,
+ model = Build.MODEL,
+ osVersion = Build.VERSION.SDK_INT.toString(),
+ packageName = onboardingDeviceData.packageName,
+ provider = provider,
+ )
+
+ naviPayAnalytics.onBindDeviceCallForBinding(
+ deviceDetails = deviceDetails,
+ onboardingSource = onboardingSource.value,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+
+ val bindDeviceAPIResponse =
+ naviPayOnboardingRepository.bindDevice(
+ bindDeviceRequest =
+ BindDeviceRequest(
+ deviceData = deviceData!!,
+ deviceDetails = deviceDetails,
+ merchantCustomerId = customerResponse.merchantCustomerId,
+ bindingType =
+ if (isBindingEligibleForSmv) BindingType.SMV else BindingType.SMS,
+ ),
+ metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP),
+ )
+
+ if (!bindDeviceAPIResponse.isSuccessWithData()) {
+ updateDeviceBindingState(DeviceBindingState.Failure)
+ if (bindDeviceAPIResponse.errors?.getOrNull(0)?.code == NON_VERIFIED_SENDER_LOGIN) {
+ handleCaseForNonVerifiedSenderLogin(bindDeviceAPIResponse = bindDeviceAPIResponse)
+ } else {
+ onBindingError(bindDeviceAPIResponse)
}
+ return
+ }
- val customerResponse = customerAPIResponse.data!!
+ this@NaviPayOnboardingViewModel.bindDeviceResponse = bindDeviceAPIResponse.data!!
- // Step 2: Bind device API call to get SMS content & VMNs list
-
- val deviceDetails =
- DeviceDetails(
- deviceId = onboardingDeviceData.deviceId,
- manufacturer = Build.MANUFACTURER,
- model = Build.MODEL,
- osVersion = Build.VERSION.SDK_INT.toString(),
- packageName = onboardingDeviceData.packageName,
- provider =
- NetworkProvider(
- ssid = selectedSimInfo.value?.subscriptionId ?: EMPTY,
- phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber",
- simSlot = selectedSimInfo.value?.simSlotIndex.toString(),
- name = selectedSimInfo.value?.carrierName ?: EMPTY,
- ),
- )
-
- naviPayAnalytics.onBindDeviceCallForBinding(
- deviceDetails = deviceDetails,
- onboardingSource = onboardingSource.value,
- naviPaySessionAttributes = getNaviPaySessionAttributes(),
- )
-
- val bindDeviceAPIResponse =
- naviPayOnboardingRepository.bindDevice(
- bindDeviceRequest =
- BindDeviceRequest(
- deviceData = deviceData!!,
- deviceDetails = deviceDetails,
- merchantCustomerId = customerResponse.merchantCustomerId,
- ),
- metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP),
- )
-
- if (!bindDeviceAPIResponse.isSuccessWithData()) {
- updateSmsVerificationState(SmsVerificationState.Failure)
- if (bindDeviceAPIResponse.errors?.getOrNull(0)?.code == NON_VERIFIED_SENDER_LOGIN) {
- handleCaseForNonVerifiedSenderLogin(
- bindDeviceAPIResponse = bindDeviceAPIResponse
- )
- } else {
- onBindingError(bindDeviceAPIResponse)
- }
- return@safeLaunch
- }
-
- this@NaviPayOnboardingViewModel.bindDeviceResponse = bindDeviceAPIResponse.data!!
-
- naviPayAnalytics.onSmsBindingVMNReceived(
- serviceProviders =
- this@NaviPayOnboardingViewModel.bindDeviceResponse.serviceProviders,
- onboardingSource = onboardingSource.value,
- naviPaySessionAttributes = getNaviPaySessionAttributes(),
- )
-
- // Step 3: Send SMS to VMNs and trigger startStatusPolling()
- updateSmsVerificationState(SmsVerificationState.Sending)
+ if (isBindingEligibleForSmv) {
+ processBindDeviceResponseForSmv()
+ } else {
+ processBindDeviceResponseForSms(provider = provider)
}
}
- fun checkForSMSSentAndStartPolling(vmnNumbers: List, smsContent: String) {
+ private suspend fun processBindDeviceResponseForSms(provider: NetworkProvider) {
+ naviPayAnalytics.onSmsBindingVMNReceived(
+ serviceProviders = bindDeviceResponse.smsBindingData!!.serviceProviders,
+ onboardingSource = onboardingSource.value,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+
+ // Step 3: Send SMS to VMNs and trigger startStatusPolling()
+ updateDeviceBindingState(DeviceBindingState.Binding)
+
+ sendSms(provider = provider)
+
+ checkForSMSSentAndStartPolling()
+ }
+
+ private suspend fun sendSms(provider: NetworkProvider) {
+
+ val serviceProviderNumberList =
+ bindDeviceResponse.smsBindingData!!.serviceProviders.map { it.number }
+
+ smsManager.sendSms(
+ destinationNumberList = serviceProviderNumberList,
+ messageContent = bindDeviceResponse.smsBindingData!!.smsContent,
+ subscriptionId = selectedSimInfo.value?.subscriptionId?.toIntOrNull() ?: 0,
+ )
+
+ naviPayAnalytics.startSimBinding(
+ provider = provider,
+ onboardingSource = onboardingSource.value,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+ }
+
+ private suspend fun processBindDeviceResponseForSmv() {
+
+ updateDeviceBindingState(DeviceBindingState.Binding)
+ naviPayAnalytics.onSmvInitiated()
+ val smvContent = bindDeviceResponse.smvBindingData?.smvContent ?: ""
+
+ val initiateSmvResponse =
+ naviPayOnboardingRepository.initiateSmv(
+ params =
+ mapOf(
+ "clientId" to BuildConfig.NAVIPAY_SMV_CLIENT_ID,
+ "transactionId" to smvContent,
+ "msisdn" to "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber",
+ ),
+ url = BuildConfig.NAVIPAY_SMV_BASE_URL,
+ )
+
+ naviPayAnalytics.onInitiateSmvResponse(
+ smvInitiateResponse = initiateSmvResponse.raw().toString()
+ )
+ naviPayNetworkConnectivity.unbindNetwork()
+
+ if (initiateSmvResponse.code() == 200 && initiateSmvResponse.body() == null) {
+ startStatusPolling(bindingType = BindingType.SMV)
+ } else {
+ // Mark in cache that SMV was triggered & failed
+ markSmvTriggeredAndFailed()
+ updateBottomSheetUIState(showBottomSheet = false)
+ updateDeviceBindingState(deviceBindingState = DeviceBindingState.Failure)
+ notifyError(errorConfig = getGenericErrorConfig().copy(cancelable = false))
+ }
+ }
+
+ private fun checkForSMSSentAndStartPolling() {
viewModelScope.safeLaunch(coroutineDispatcherProvider.io) {
naviPayAnalytics.checkForSMSSentInDevice(
onboardingSource = onboardingSource.value,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
+ val vmnNumbers = bindDeviceResponse.smsBindingData!!.serviceProviders.map { it.number }
+ val smsContent = bindDeviceResponse.smsBindingData!!.smsContent
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < SMS_SENT_CHECK_TIMEOUT) {
delay(timeMillis = 1000)
if (checkIfMessageIsSent(vmnNumbers = vmnNumbers, smsContent = smsContent)) {
- startStatusPolling()
+ startStatusPolling(bindingType = BindingType.SMS)
return@safeLaunch
}
}
@@ -912,7 +1004,7 @@ constructor(
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
- updateSmsVerificationState(SmsVerificationState.Failure)
+ updateDeviceBindingState(DeviceBindingState.Failure)
updateBottomSheetUIState(showBottomSheet = false)
notifyError(
errorConfig =
@@ -945,27 +1037,43 @@ constructor(
private suspend fun checkIfMessageIsSent(vmnNumbers: List, smsContent: String) =
smsManager.sentSmsVerification(numberList = vmnNumbers, messageContent = smsContent)
- @Suppress("UNCHECKED_CAST")
- private suspend fun startStatusPolling() {
+ private suspend fun startStatusPolling(bindingType: BindingType) {
- updateSmsVerificationState(SmsVerificationState.Verifying)
- naviPayAnalytics.smsSentSuccessfullyStartingPolling(
+ updateDeviceBindingState(DeviceBindingState.Verifying)
+ naviPayAnalytics.onStartBindStatusPolling(
onboardingSource = onboardingSource.value,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ bindingType = bindingType,
)
naviApiPoller
- .startPolling {
- naviPayOnboardingRepository.getBindDeviceStatus(
- bindDeviceStatusRequest =
- BindDeviceStatusRequest(
- deviceData = deviceData!!,
- internalDeviceId = bindDeviceResponse.internalDeviceId,
- merchantCustomerId = bindDeviceResponse.merchantCustomerId,
- ),
- metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP),
- )
- }
+ .startPolling(
+ onTimeout = {
+ updateBottomSheetUIState(showBottomSheet = false)
+ notifyError(
+ errorConfig = getGenericErrorConfig().copy(cancelable = false)
+ ) // TODO: Update messaging
+ naviPayAnalytics.simBindingFailure(
+ errorType = "Binding timeout",
+ error = null,
+ onboardingSource = onboardingSource.value,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+ updateDeviceBindingState(DeviceBindingState.Failure)
+ },
+ onPollExecute = {
+ naviPayOnboardingRepository.getBindDeviceStatus(
+ bindDeviceStatusRequest =
+ BindDeviceStatusRequest(
+ deviceData = deviceData!!,
+ internalDeviceId = bindDeviceResponse.internalDeviceId,
+ merchantCustomerId = bindDeviceResponse.merchantCustomerId,
+ bindingType = bindingType,
+ ),
+ metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP),
+ )
+ },
+ )
.collectLatest {
try {
if (!naviApiPoller.isPollerActive()) {
@@ -979,6 +1087,7 @@ constructor(
deviceId = onboardingDeviceData.deviceId,
packageName = onboardingDeviceData.packageName,
merchantCustomerId = bindDeviceResponse.merchantCustomerId,
+ bindingType = bindingType,
)
} catch (exception: Exception) {
updateBottomSheetUIState(showBottomSheet = false)
@@ -989,7 +1098,7 @@ constructor(
onboardingSource = onboardingSource.value,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
- updateSmsVerificationState(SmsVerificationState.Failure)
+ updateDeviceBindingState(DeviceBindingState.Failure)
naviApiPoller.stopPolling()
}
}
@@ -1000,20 +1109,24 @@ constructor(
deviceId: String,
packageName: String,
merchantCustomerId: String,
+ bindingType: BindingType,
) {
if (!bindDeviceStatusAPIResponse.isSuccessWithData()) {
when (bindDeviceStatusAPIResponse.errors?.getOrNull(0)?.code) {
SMS_VERIFICATION_PENDING -> {}
else -> {
naviPayAnalytics.simBindingFailure(
- errorType = SmsVerificationState.Failure.toString(),
+ errorType = DeviceBindingState.Failure.toString(),
error = bindDeviceStatusAPIResponse.errors?.firstOrNull(),
onboardingSource = onboardingSource.value,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
)
- updateSmsVerificationState(SmsVerificationState.Failure)
+ updateDeviceBindingState(DeviceBindingState.Failure)
naviApiPoller.stopPolling()
onBindingError(bindDeviceStatusAPIResponse)
+ if (bindingType == BindingType.SMV) {
+ markSmvTriggeredAndFailed()
+ }
}
}
return
@@ -1056,7 +1169,32 @@ constructor(
deviceInfoProvider.saveMerchantCustomerId(merchantCustomerId = merchantCustomerId)
- updateSmsVerificationState(SmsVerificationState.Success)
+ handleDeviceBindingSuccess()
+ }
+
+ private fun handleDeviceBindingSuccess() {
+
+ updateDeviceBindingState(DeviceBindingState.Success)
+
+ naviPayAnalytics.simBindingSuccess(
+ onboardingSource = onboardingSource.value,
+ naviPaySessionAttributes = getNaviPaySessionAttributes(),
+ )
+
+ naviPayWidgetManager.addScanAndPayLauncherWidget()
+
+ handleNavigationOnCustomerStatus()
+ }
+
+ private suspend fun markSmvTriggeredAndFailed() {
+ naviCacheRepository.save(
+ naviCacheEntity =
+ NaviCacheEntity(
+ key = NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED,
+ value = "true",
+ version = 1,
+ )
+ )
}
fun declineDeviceBinding() {
@@ -1070,7 +1208,7 @@ constructor(
naviApiPoller.stopPolling()
- updateSmsVerificationState(SmsVerificationState.DeclineBinding)
+ updateDeviceBindingState(DeviceBindingState.DeclineBinding)
naviPayAnalytics.onDeclineDevicePopupShown(
onboardingSource = onboardingSource.value,
naviPaySessionAttributes = getNaviPaySessionAttributes(),
@@ -1115,7 +1253,7 @@ constructor(
}
}
- fun handleNavigationOnCustomerStatus() {
+ private fun handleNavigationOnCustomerStatus() {
viewModelScope.launch(Dispatchers.IO) {
val updatedDeviceData = deviceData ?: deviceInfoProvider.getDeviceData()
@@ -1268,21 +1406,21 @@ constructor(
)
}
- private fun updateSmsVerificationState(
- smsVerificationState: SmsVerificationState = SmsVerificationState.None
+ private fun updateDeviceBindingState(
+ deviceBindingState: DeviceBindingState = DeviceBindingState.None
) {
- _smsVerificationState.value = smsVerificationState
- when (smsVerificationState) {
- SmsVerificationState.Success -> {
+ this.deviceBindingState.value = deviceBindingState
+ when (deviceBindingState) {
+ DeviceBindingState.Success -> {
updateBindingProgress(progress = 1f)
}
- SmsVerificationState.Initiated -> {
+ DeviceBindingState.Initiated -> {
updateBindingProgress(progress = 0.05f)
}
- SmsVerificationState.Sending -> {
+ DeviceBindingState.Binding -> {
updateBindingProgress(progress = 0.65f)
}
- SmsVerificationState.Verifying -> {
+ DeviceBindingState.Verifying -> {
updateBindingProgress(progress = 0.80f)
}
else -> {
@@ -1305,6 +1443,14 @@ constructor(
}
}
+ private fun getNetworkProvider() =
+ NetworkProvider(
+ ssid = selectedSimInfo.value?.subscriptionId.orEmpty(),
+ phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber",
+ simSlot = selectedSimInfo.value?.simSlotIndex.toString(),
+ name = selectedSimInfo.value?.carrierName ?: com.navi.base.utils.EMPTY,
+ )
+
@OptIn(ExperimentalCoroutinesApi::class)
fun clearReplayCacheForHardUpdateParam() {
_isHardUpdateRequired.resetReplayCache()
@@ -1353,8 +1499,6 @@ constructor(
}
}
- fun addScanAndPayLauncherWidget() = naviPayWidgetManager.addScanAndPayLauncherWidget()
-
private fun handleFlowSpecificOnboardingIntent() {
viewModelScope.safeLaunch(coroutineDispatcherProvider.io) {
val customerStatus = naviPayCustomerStatusHandler.getCustomerStatus()
@@ -1516,6 +1660,104 @@ constructor(
}
}
+ private suspend fun getSmvEligibilityStatus(provider: NetworkProvider): SmvEligibilityStatus {
+
+ // TODO: To be removed for SMV go live
+ return SmvEligibilityStatus(isEligible = false, isSmsPermissionEnabled = true)
+
+ _isSmvEligibilityCheckOngoing.update { true }
+
+ val smvLitmusExperimentResponse =
+ litmusExperimentsUseCase.execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING)
+
+ val isSmvExperimentEnabled = smvLitmusExperimentResponse?.variant?.enabled.orFalse()
+
+ val isSmsPermissionEnabled =
+ if (isSmvExperimentEnabled) {
+ val experimentPayload =
+ smvLitmusExperimentResponse?.variant?.payload?.get("value") as? String
+
+ JSONObject(experimentPayload ?: "").optBoolean("sms_permission")
+ } else {
+ false
+ }
+
+ if (!isSmvExperimentEnabled) {
+ naviPayAnalytics.onSmvExperimentDisabled()
+
+ _isSmvEligibilityCheckOngoing.update { false }
+ return SmvEligibilityStatus(
+ isEligible = false,
+ isSmsPermissionEnabled = isSmsPermissionEnabled,
+ )
+ }
+
+ // Check if provider supports SMV
+ if (
+ NaviPayCommonUtils.displayableCarrierName(actualCarrierName = provider.name) !in
+ naviPayOnboardingConfig.config.smvSupportedProviders
+ ) {
+ naviPayAnalytics.onProviderNotSupportedForSmv(
+ displayableCarrierName =
+ NaviPayCommonUtils.displayableCarrierName(actualCarrierName = provider.name),
+ smvSupportedProviders = naviPayOnboardingConfig.config.smvSupportedProviders,
+ )
+ _isSmvEligibilityCheckOngoing.update { false }
+ return SmvEligibilityStatus(
+ isEligible = false,
+ isSmsPermissionEnabled = isSmsPermissionEnabled,
+ )
+ }
+
+ val isSmvTriggeredPreviously =
+ naviCacheRepository
+ .get(key = NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED)
+ ?.value
+ ?.toBoolean() ?: false
+
+ if (isSmvTriggeredPreviously) {
+ naviPayAnalytics.onSmvPreviouslyTriggered()
+ _isSmvEligibilityCheckOngoing.update { false }
+ return SmvEligibilityStatus(
+ isEligible = false,
+ isSmsPermissionEnabled = isSmsPermissionEnabled,
+ )
+ }
+
+ // Check if SIM selected for binding is default data SIM
+ val defaultDataSubscriptionId = SubscriptionManager.getDefaultDataSubscriptionId()
+
+ if (provider.ssid != defaultDataSubscriptionId.toString()) {
+ naviPayAnalytics.onSelectedSimIsNotDefaultDataSim(
+ selectedSimId = provider.ssid,
+ defaultDataSimId = defaultDataSubscriptionId.toString(),
+ )
+ _isSmvEligibilityCheckOngoing.update { false }
+ return SmvEligibilityStatus(
+ isEligible = false,
+ isSmsPermissionEnabled = isSmsPermissionEnabled,
+ )
+ }
+
+ // Finally check if routing to cellular succeeded or not
+ val routeNetworkViaCellularResult = checkAndRouteNetworkViaCellular()
+ naviPayAnalytics.onRouteNetworkViaCellularResult(result = routeNetworkViaCellularResult)
+
+ _isSmvEligibilityCheckOngoing.update { false }
+ return SmvEligibilityStatus(
+ isEligible = routeNetworkViaCellularResult,
+ isSmsPermissionEnabled = isSmsPermissionEnabled,
+ )
+ }
+
+ private suspend fun checkAndRouteNetworkViaCellular(): Boolean {
+ return try {
+ naviPayNetworkConnectivity.bindNetworkToCellular(timeout = 3.seconds)
+ } catch (exception: TimeoutCancellationException) {
+ false
+ }
+ }
+
override val screenName: String
get() = NAVI_PAY_SETUP
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt
index bd32c80ffe..f881179414 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt
@@ -7,16 +7,32 @@
package com.navi.pay.onboarding.binding.viewmodel
+import android.Manifest
+import android.app.PendingIntent
import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
import android.provider.Telephony
import android.provider.Telephony.Sms
+import androidx.core.app.ActivityCompat
+import com.navi.analytics.utils.NaviTrackEvent
import com.navi.common.utils.log
+import com.navi.pay.common.utils.NaviPayCommonUtils.isAirplaneModeOn
+import com.navi.pay.utils.INTENT_ACTION_SMS_DELIVERED
+import com.navi.pay.utils.INTENT_ACTION_SMS_SENT
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlin.math.min
interface SmsManager {
suspend fun sentSmsVerification(numberList: List, messageContent: String): Boolean
+
+ suspend fun sendSms(
+ destinationNumberList: List,
+ messageContent: String,
+ subscriptionId: Int,
+ )
}
class SmsManagerImpl @Inject constructor(@ApplicationContext private val context: Context) :
@@ -78,4 +94,68 @@ class SmsManagerImpl @Inject constructor(@ApplicationContext private val context
return false
}
}
+
+ override suspend fun sendSms(
+ destinationNumberList: List,
+ messageContent: String,
+ subscriptionId: Int,
+ ) {
+ if (
+ ActivityCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) !=
+ PackageManager.PERMISSION_GRANTED
+ ) {
+ return
+ }
+
+ if (isAirplaneModeOn(context = context)) {
+ return
+ }
+
+ val smsManager =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ context
+ .getSystemService(android.telephony.SmsManager::class.java)
+ .createForSubscriptionId(subscriptionId)
+ } else {
+ android.telephony.SmsManager.getSmsManagerForSubscriptionId(subscriptionId)
+ }
+
+ val sentPendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ (0..1000).random(),
+ Intent(INTENT_ACTION_SMS_SENT),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+
+ val deliveredPendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ (0..1000).random(),
+ Intent(INTENT_ACTION_SMS_DELIVERED),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+
+ try {
+ NaviTrackEvent.trackEventOnClickStream(
+ eventName = "dev_navipay_send_sms",
+ eventValues =
+ mapOf(
+ "destinationNumberList" to destinationNumberList.toString(),
+ "messageContentLength" to messageContent.length.toString(),
+ ),
+ )
+ destinationNumberList.forEach {
+ smsManager.sendTextMessage(
+ it,
+ null,
+ messageContent,
+ sentPendingIntent,
+ deliveredPendingIntent,
+ )
+ }
+ } catch (e: Exception) {
+ e.log()
+ }
+ }
}
diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt
index 0fdb8ce1cd..3a12e72e0f 100644
--- a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt
+++ b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt
@@ -157,6 +157,8 @@ const val NAVI_PAY_SETTING_QR_PAGER_ANIMATION_COUNTER = "settingQrPagerAnimation
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"
+const val NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED =
+ "naviPayDeviceBindingIsSmvTriggeredAndFailed"
// Sync DB keys
const val NAVI_PAY_SYNC_TABLE_TRANSACTION_HISTORY_KEY = "transactionHistoryKey"
@@ -177,6 +179,7 @@ const val LITMUS_EXPERIMENT_NAVIPAY_CHECK_BALANCE_DURING_TRANSACTION =
const val LITMUS_EXPERIMENT_NAVIPAY_CHRISTMAS_CELEBRATION = "NaviPay-exp-rwd-holiday-animation"
const val LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER =
"NaviPay-frequent-contact-in-qr-scanner"
+const val LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING = "NaviPay-exp-smv-binding"
val NAVI_PAY_LITMUS_EXPERIMENTS =
listOf(
LITMUS_EXPERIMENT_NAVIPAY_LITE_AUTO_TOP_UP,
@@ -185,6 +188,7 @@ val NAVI_PAY_LITMUS_EXPERIMENTS =
LITMUS_EXPERIMENT_NAVIPAY_CHECK_BALANCE_DURING_TRANSACTION,
LITMUS_EXPERIMENT_NAVIPAY_CHRISTMAS_CELEBRATION,
LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER,
+ LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING,
)
// Generic
diff --git a/android/navi-pay/src/main/res/raw/navi_pay_mock.json b/android/navi-pay/src/main/res/raw/navi_pay_mock.json
index ed65a499e8..228e35b06c 100644
--- a/android/navi-pay/src/main/res/raw/navi_pay_mock.json
+++ b/android/navi-pay/src/main/res/raw/navi_pay_mock.json
@@ -1,224 +1,11 @@
{
- "NPAY_UPI_Onboarding": {
- "version": 1,
- "config": {
- "otherUpiDetails": [
- {
- "title": "Receive money from any UPI app",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/PhonePe_logo.png"
- }
- ],
- "items": [
- {
- "title": "Payment methods",
- "items": [
- {
- "title": "Bank\naccount",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS",
- "needsResult": true
- }
- },
- {
- "title": "Credit\nline",
- "tag": "New",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS",
- "needsResult": true
- }
- },
- {
- "title": "RuPay\ncredit card",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS",
- "needsResult": true
- }
- },
- {
- "title": "UPI\nLite",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_UPI_LITE",
- "needsResult": true
- }
- }
- ]
- },
- {
- "title": "UPI options",
- "items": [
- {
- "title": "UPI\nGlobal",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_MANDATE_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Payment\nrequest",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_PENDING_REQUESTS_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Manage\nautopay",
- "badgeCount": "3",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_MANDATE_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Self\ntransfer",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_SELF_TRANSFER",
- "needsResult": true
- }
- },
- {
- "title": "UPI\nnumber",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_UPI_NUMBER_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Blocked\nusers",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_BLOCKED_USERS_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "De-register",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_BLOCKED_USERS_SCREEN",
- "needsResult": true
- }
- }
- ]
- }
- ],
- "addAccountIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Add-Account-Profile-Icon.png",
- "nonOnboardedQrIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Non-Onboarded-QR.png"
- }
- },
- "NPAY_UPI_Non_Onboarding": {
- "version": 1,
- "config": {
- "otherUpiDetails": [
- {
- "title": "Receive money from any UPI app",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/PhonePe_logo.png"
- }
- ],
- "items": [
- {
- "title": "Payment methods",
- "items": [
- {
- "title": "Bank\naccount",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS",
- "needsResult": true
- }
- },
- {
- "title": "Credit\nline",
- "tag": "New",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS",
- "needsResult": true
- }
- },
- {
- "title": "RuPay\ncredit card",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS",
- "needsResult": true
- }
- },
- {
- "title": "UPI\nLite",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_UPI_LITE",
- "needsResult": true
- }
- }
- ]
- },
- {
- "title": "UPI options",
- "items": [
- {
- "title": "UPI\nGlobal",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_MANDATE_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Payment\nrequest",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_PENDING_REQUESTS_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Manage\nautopay",
- "badgeCount": "3",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_MANDATE_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Self\ntransfer",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_SELF_TRANSFER",
- "needsResult": true
- }
- },
- {
- "title": "UPI\nnumber",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_UPI_NUMBER_SCREEN",
- "needsResult": true
- }
- },
- {
- "title": "Blocked\nusers",
- "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png",
- "actionData": {
- "url": "naviPay/NAVI_PAY_BLOCKED_USERS_SCREEN",
- "needsResult": true
- }
- }
- ]
- }
- ],
- "addAccountIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Add-Account-Profile-Icon.png",
- "nonOnboardedQrIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Non-Onboarded-QR.png"
+ "BindDeviceResponse": {
+ "merchantCustomerId": "12345",
+ "internalDeviceId": "5678",
+ "smvBindingData": {
+ "url": "https://api.sandbox.bureau.id/v2/auth/initiate",
+ "clientId": "093f0fc3-bbe5-4944-a6c3-1bf410ae4237",
+ "aggregator": "bureau"
}
}
}
\ No newline at end of file
diff --git a/android/navi-pay/src/main/res/values/strings.xml b/android/navi-pay/src/main/res/values/strings.xml
index e273b3df14..dfca81f1c3 100644
--- a/android/navi-pay/src/main/res/values/strings.xml
+++ b/android/navi-pay/src/main/res/values/strings.xml
@@ -14,7 +14,15 @@
To verify your phone number for UPI payments.
Phone
To verify your SIM card with your registered mobile number.
- *Standard SMS charges will apply
+ *Standard SMS charges may apply
+ Select the linked SIM to verify your number. Please do not go back or close the app.
+ We will verify your mobile number. Please do not go back or close the app.
+ Please do not go back or close the app. This usually takes a few seconds.
+ Proceed
+ Select bank
+ Add your bank account linked with +91-%s
+ +91-%s
+ Search bank here
Popular banks
No bank accounts found
No bank accounts found linked to this phone number for the selected bank