From 1a9b1e797ae58f7557aff589bdf7e0d65ead4e2a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carlos=20Mu=C3=B1oz?=
 <99293320+carlosmuvi-stripe@users.noreply.github.com>
Date: Fri, 10 Jan 2025 12:38:51 -0800
Subject: [PATCH] [FC] Prepares integrity on first sync call (#9818)

# Summary
Call `Integrity#prepare` on Activity A (`FinancialConnectionsSheetActivity`) and reuse the same Integrity instance in Activity B (`FinancialConnectionsNativeActivity`) when generating tokens.

# Motivation

https://docs.google.com/document/d/1joKz5UZHLVazmecfMHbq6gB6n4wj5u8To6AtqYgq_tc/edit?tab=t.0#heading=h.cz1xkpga7giy

# Testing
- [x] Manually verified
- [x] Added error tracking for failed integrity checks
---
 example/dependencies/dependencies.txt         |  7 ++++++
 .../dependencies/dependencies.txt             | 17 +++++++++----
 financial-connections/build.gradle            |  1 +
 .../dependencies/dependencies.txt             | 11 +++++++++
 .../FinancialConnectionsSheetViewModel.kt     | 24 +++++++++++++++++--
 ...nnectionsSingletonSharedComponentHolder.kt | 21 +++++++++++++++-
 .../FinancialConnectionsSheetViewModelTest.kt |  1 +
 .../dependencies/dependencies.txt             |  7 ++++++
 .../IntegrityStandardRequestManager.kt        |  4 ++++
 9 files changed, 85 insertions(+), 8 deletions(-)

diff --git a/example/dependencies/dependencies.txt b/example/dependencies/dependencies.txt
index 693b4a10ad8..4449cc766a1 100644
--- a/example/dependencies/dependencies.txt
+++ b/example/dependencies/dependencies.txt
@@ -956,6 +956,13 @@
 |    +--- project :stripe-ui-core (*)
 |    +--- project :payments-model (*)
 |    +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
+|    +--- project :stripe-attestation
+|    |    +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
+|    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*)
+|    |    \--- com.google.android.play:integrity:1.4.0
+|    |         +--- com.google.android.gms:play-services-basement:18.4.0 (*)
+|    |         +--- com.google.android.gms:play-services-tasks:18.2.0 (*)
+|    |         \--- com.google.android.play:core-common:2.0.4
 |    +--- androidx.activity:activity-ktx:1.8.2 (*)
 |    +--- androidx.annotation:annotation:1.9.0 (*)
 |    +--- androidx.appcompat:appcompat:1.7.0 (*)
diff --git a/financial-connections-example/dependencies/dependencies.txt b/financial-connections-example/dependencies/dependencies.txt
index beb14bafeeb..aeeaf0e04f8 100644
--- a/financial-connections-example/dependencies/dependencies.txt
+++ b/financial-connections-example/dependencies/dependencies.txt
@@ -594,6 +594,17 @@
 |    |    +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 (*)
 |    |    \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.21 (*)
 |    +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
+|    +--- project :stripe-attestation
+|    |    +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
+|    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*)
+|    |    \--- com.google.android.play:integrity:1.4.0
+|    |         +--- com.google.android.gms:play-services-basement:18.4.0
+|    |         |    +--- androidx.collection:collection:1.0.0 -> 1.4.0 (*)
+|    |         |    +--- androidx.core:core:1.2.0 -> 1.13.1 (*)
+|    |         |    \--- androidx.fragment:fragment:1.1.0 -> 1.8.4 (*)
+|    |         +--- com.google.android.gms:play-services-tasks:18.2.0
+|    |         |    \--- com.google.android.gms:play-services-basement:18.4.0 (*)
+|    |         \--- com.google.android.play:core-common:2.0.4
 |    +--- androidx.activity:activity-ktx:1.8.2 (*)
 |    +--- androidx.annotation:annotation:1.9.0 (*)
 |    +--- androidx.appcompat:appcompat:1.7.0 (*)
@@ -847,11 +858,7 @@
 |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3 -> 1.9.0
 |    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*)
 |    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0 (*)
-|    |    +--- com.google.android.gms:play-services-tasks:16.0.1 -> 18.2.0
-|    |    |    \--- com.google.android.gms:play-services-basement:18.4.0
-|    |    |         +--- androidx.collection:collection:1.0.0 -> 1.4.0 (*)
-|    |    |         +--- androidx.core:core:1.2.0 -> 1.13.1 (*)
-|    |    |         \--- androidx.fragment:fragment:1.1.0 -> 1.8.4 (*)
+|    |    +--- com.google.android.gms:play-services-tasks:16.0.1 -> 18.2.0 (*)
 |    |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.21 (*)
 |    +--- com.google.android.material:material:1.12.0 (*)
 |    +--- com.google.android.gms:play-services-wallet:19.4.0
diff --git a/financial-connections/build.gradle b/financial-connections/build.gradle
index a4000f3b55f..a72d929a7ec 100644
--- a/financial-connections/build.gradle
+++ b/financial-connections/build.gradle
@@ -23,6 +23,7 @@ dependencies {
     api project(":stripe-core")
     api project(":stripe-ui-core")
     api project(":payments-model")
+    implementation project(":stripe-attestation")
 
     implementation libs.androidx.activity
     implementation libs.androidx.annotation
diff --git a/financial-connections/dependencies/dependencies.txt b/financial-connections/dependencies/dependencies.txt
index 39c1da4ee4a..65769fa3ed2 100644
--- a/financial-connections/dependencies/dependencies.txt
+++ b/financial-connections/dependencies/dependencies.txt
@@ -542,6 +542,17 @@
 |    +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 (*)
 |    \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.21 (*)
 +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
++--- project :stripe-attestation
+|    +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
+|    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*)
+|    \--- com.google.android.play:integrity:1.4.0
+|         +--- com.google.android.gms:play-services-basement:18.4.0
+|         |    +--- androidx.collection:collection:1.0.0 -> 1.4.0 (*)
+|         |    +--- androidx.core:core:1.2.0 -> 1.13.1 (*)
+|         |    \--- androidx.fragment:fragment:1.1.0 -> 1.5.4 (*)
+|         +--- com.google.android.gms:play-services-tasks:18.2.0
+|         |    \--- com.google.android.gms:play-services-basement:18.4.0 (*)
+|         \--- com.google.android.play:core-common:2.0.4
 +--- androidx.activity:activity-ktx:1.8.2 (*)
 +--- androidx.annotation:annotation:1.9.0 (*)
 +--- androidx.appcompat:appcompat:1.7.0 (*)
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt
index 4c32eb7c616..9280b6f2d93 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt
@@ -21,6 +21,7 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffe
 import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl
 import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenNativeAuthFlow
 import com.stripe.android.financialconnections.FinancialConnectionsSheetViewModel.Companion.QUERY_PARAM_PAYMENT_METHOD
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.ErrorCode
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.Metadata
@@ -58,6 +59,7 @@ import com.stripe.android.financialconnections.navigation.topappbar.TopAppBarSta
 import com.stripe.android.financialconnections.presentation.FinancialConnectionsViewModel
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
 import com.stripe.android.financialconnections.utils.parcelable
+import com.stripe.attestation.IntegrityRequestManager
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
@@ -68,6 +70,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
     @Named(APPLICATION_ID) private val applicationId: String,
     savedStateHandle: SavedStateHandle,
     private val getOrFetchSync: GetOrFetchSync,
+    private val integrityRequestManager: IntegrityRequestManager,
     private val fetchFinancialConnectionsSession: FetchFinancialConnectionsSession,
     private val fetchFinancialConnectionsSessionForToken: FetchFinancialConnectionsSessionForToken,
     private val logger: Logger,
@@ -86,7 +89,9 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
         if (initialState.initialArgs.isValid()) {
             eventReporter.onPresented(initialState.initialArgs.configuration)
             // avoid re-fetching manifest if already exists (this will happen on process recreations)
-            if (initialState.manifest == null) fetchManifest()
+            if (initialState.manifest == null) {
+                initAuthFlow()
+            }
         } else {
             val result = Failed(
                 IllegalStateException("Invalid configuration provided when instantiating activity")
@@ -108,9 +113,10 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
      * Fetches the [FinancialConnectionsSessionManifest] from the Stripe API to get the hosted auth flow URL
      * as well as the success and cancel callback URLs to verify.
      */
-    private fun fetchManifest() {
+    private fun initAuthFlow() {
         viewModelScope.launch {
             kotlin.runCatching {
+                prepareStandardRequestManager()
                 getOrFetchSync(refetchCondition = Always)
             }.onFailure {
                 finishWithResult(stateFlow.value, Failed(it))
@@ -120,6 +126,20 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
         }
     }
 
+    private suspend fun prepareStandardRequestManager(): Boolean {
+        val result = integrityRequestManager.prepare()
+        result.onFailure {
+            analyticsTracker.track(
+                FinancialConnectionsAnalyticsEvent.Error(
+                    extraMessage = "Failed to warm up the IntegrityStandardRequestManager",
+                    pane = Pane.CONSENT,
+                    exception = it
+                )
+            )
+        }
+        return result.isSuccess
+    }
+
     /**
      * Builds the ChromeCustomTab intent to launch the hosted auth flow and launches it.
      *
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt
index f3a11b80d2e..20506c22ba2 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt
@@ -1,9 +1,15 @@
 package com.stripe.android.financialconnections.di
 
 import android.app.Application
+import com.stripe.android.core.Logger
+import com.stripe.attestation.BuildConfig
+import com.stripe.attestation.IntegrityRequestManager
+import com.stripe.attestation.IntegrityStandardRequestManager
+import com.stripe.attestation.RealStandardIntegrityManagerFactory
 import dagger.BindsInstance
 import dagger.Component
 import dagger.Module
+import dagger.Provides
 import javax.inject.Singleton
 
 /**
@@ -32,6 +38,8 @@ internal object FinancialConnectionsSingletonSharedComponentHolder {
 @Component(modules = [FinancialConnectionsSingletonSharedModule::class])
 internal interface FinancialConnectionsSingletonSharedComponent {
 
+    fun providesIntegrityRequestManager(): IntegrityRequestManager
+
     @Component.Factory
     interface Factory {
         fun create(@BindsInstance application: Application): FinancialConnectionsSingletonSharedComponent
@@ -39,4 +47,15 @@ internal interface FinancialConnectionsSingletonSharedComponent {
 }
 
 @Module
-internal class FinancialConnectionsSingletonSharedModule
+internal class FinancialConnectionsSingletonSharedModule {
+
+    @Provides
+    @Singleton
+    fun providesIntegrityStandardRequestManager(
+        context: Application
+    ): IntegrityRequestManager = IntegrityStandardRequestManager(
+        cloudProjectNumber = 527113280969, // stripe-financial-connections
+        logError = { message, error -> Logger.getInstance(BuildConfig.DEBUG).error(message, error) },
+        factory = RealStandardIntegrityManagerFactory(context)
+    )
+}
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt
index ed07b47973a..ae728bef74a 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt
@@ -821,6 +821,7 @@ class FinancialConnectionsSheetViewModelTest {
             browserManager = browserManager,
             savedStateHandle = SavedStateHandle(),
             nativeAuthFlowCoordinator = mock(),
+            integrityRequestManager = mock(),
             logger = Logger.noop()
         )
     }
diff --git a/paymentsheet-example/dependencies/dependencies.txt b/paymentsheet-example/dependencies/dependencies.txt
index 0a0cb90ae27..c6af9188644 100644
--- a/paymentsheet-example/dependencies/dependencies.txt
+++ b/paymentsheet-example/dependencies/dependencies.txt
@@ -1100,6 +1100,13 @@
 |    +--- project :stripe-ui-core (*)
 |    +--- project :payments-model (*)
 |    +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
+|    +--- project :stripe-attestation
+|    |    +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.21 (*)
+|    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*)
+|    |    \--- com.google.android.play:integrity:1.4.0
+|    |         +--- com.google.android.gms:play-services-basement:18.4.0 (*)
+|    |         +--- com.google.android.gms:play-services-tasks:18.2.0 (*)
+|    |         \--- com.google.android.play:core-common:2.0.4
 |    +--- androidx.activity:activity-ktx:1.8.2 (*)
 |    +--- androidx.annotation:annotation:1.9.0 (*)
 |    +--- androidx.appcompat:appcompat:1.7.0 (*)
diff --git a/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt b/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt
index 3cb6e6dc0cf..e8acdccc9bb 100644
--- a/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt
+++ b/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt
@@ -40,6 +40,10 @@ class IntegrityStandardRequestManager(
     private var integrityTokenProvider: StandardIntegrityTokenProvider? = null
 
     override suspend fun prepare(): Result<Unit> = runCatching {
+        if (integrityTokenProvider != null) {
+            return Result.success(Unit)
+        }
+
         val finishedTask: Task<StandardIntegrityTokenProvider> = standardIntegrityManager
             .prepareIntegrityToken(
                 PrepareIntegrityTokenRequest.builder()