Skip to content

Commit

Permalink
Sends supports_app_verification to syncrhonize when Integrity available
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosmuvi-stripe committed Dec 28, 2024
1 parent 67af8e1 commit bc4ff3b
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
private fun fetchManifest() {
viewModelScope.launch {
kotlin.runCatching {
prepareStandardRequestManager()
getOrFetchSync(refetchCondition = Always)
val attestationInitialized = prepareStandardRequestManager()
getOrFetchSync(
refetchCondition = Always,
attestationInitialized = attestationInitialized
)
}.onFailure {
finishWithResult(stateFlow.value, Failed(it))
}.onSuccess {
Expand All @@ -125,7 +128,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
}

private suspend fun prepareStandardRequestManager(): Boolean {
val result = runCatching { integrityRequestManager.prepare() }
val result = integrityRequestManager.prepare()
result.exceptionOrNull()?.let {
analyticsTracker.track(
FinancialConnectionsAnalyticsEvent.Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ internal class GetOrFetchSync @Inject constructor(
) {

suspend operator fun invoke(
refetchCondition: RefetchCondition = RefetchCondition.None
refetchCondition: RefetchCondition = RefetchCondition.None,
attestationInitialized: Boolean? = null
): SynchronizeSessionResponse {
return repository.getOrSynchronizeFinancialConnectionsSession(
clientSecret = configuration.financialConnectionsSessionClientSecret,
applicationId = applicationId,
reFetchCondition = refetchCondition::shouldReFetch,
attestationInitialized = attestationInitialized
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal interface FinancialConnectionsManifestRepository {
suspend fun getOrSynchronizeFinancialConnectionsSession(
clientSecret: String,
applicationId: String,
attestationInitialized: Boolean?,
reFetchCondition: (SynchronizeSessionResponse) -> Boolean
): SynchronizeSessionResponse

Expand Down Expand Up @@ -206,15 +207,17 @@ private class FinancialConnectionsManifestRepositoryImpl(
override suspend fun getOrSynchronizeFinancialConnectionsSession(
clientSecret: String,
applicationId: String,
attestationInitialized: Boolean?,
reFetchCondition: (SynchronizeSessionResponse) -> Boolean
): SynchronizeSessionResponse = mutex.withLock {
val cachedSync = cachedSynchronizeSessionResponse?.takeUnless(reFetchCondition)
return cachedSync ?: synchronize(applicationId, clientSecret)
return cachedSync ?: synchronize(applicationId, clientSecret, attestationInitialized)
}

private suspend fun synchronize(
applicationId: String,
clientSecret: String,
attestationInitialized: Boolean?,
): SynchronizeSessionResponse = requestExecutor.execute(
apiRequestFactory.createPost(
url = synchronizeSessionUrl,
Expand All @@ -228,6 +231,8 @@ private class FinancialConnectionsManifestRepositoryImpl(
"forced_authflow_version" to "v3",
PARAMS_FULLSCREEN to true,
PARAMS_HIDE_CLOSE_BUTTON to true,
PARAMS_SUPPORT_APP_VERIFICATION to attestationInitialized,
PARAMS_VERIFY_APP_ID to applicationId.takeIf { attestationInitialized == true },
NetworkConstants.PARAMS_APPLICATION_ID to applicationId
),
NetworkConstants.PARAMS_CLIENT_SECRET to clientSecret
Expand Down Expand Up @@ -523,6 +528,8 @@ private class FinancialConnectionsManifestRepositoryImpl(
companion object {
internal const val PARAMS_FULLSCREEN = "fullscreen"
internal const val PARAMS_HIDE_CLOSE_BUTTON = "hide_close_button"
internal const val PARAMS_SUPPORT_APP_VERIFICATION = "supports_app_verification"
internal const val PARAMS_VERIFY_APP_ID = "verified_app_id"

internal const val synchronizeSessionUrl: String =
"${ApiRequest.API_HOST}/v1/financial_connections/sessions/synchronize"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsAccount
import com.stripe.android.financialconnections.model.FinancialConnectionsSession
import com.stripe.android.financialconnections.model.FinancialConnectionsSession.StatusDetails
import com.stripe.android.financialconnections.presentation.withState
import com.stripe.android.financialconnections.utils.TestIntegrityRequestManager
import com.stripe.android.model.IncentiveEligibilitySession
import com.stripe.android.model.LinkMode
import com.stripe.attestation.IntegrityRequestManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
Expand All @@ -44,6 +46,7 @@ import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
Expand Down Expand Up @@ -88,12 +91,31 @@ class FinancialConnectionsSheetViewModelTest {
}

@Test
fun `init - if manifest not present in initial state, fetchManifest triggered`() =
fun `init - if manifest not present in initial state and attestation ready, fetchManifest triggered`() =
runTest {
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
createViewModel(defaultInitialState)
whenever(getOrFetchSync(any(), anyOrNull())).thenReturn(syncResponse)
createViewModel(
defaultInitialState,
integrityRequestManager = TestIntegrityRequestManager(
prepareResult = Result.success(Unit)
)
)

verify(getOrFetchSync).invoke(refetchCondition = Always, attestationInitialized = true)
}

@Test
fun `init - if manifest not present in initial state and attestation fails, fetchManifest triggered`() =
runTest {
whenever(getOrFetchSync(any(), anyOrNull())).thenReturn(syncResponse)
createViewModel(
defaultInitialState,
integrityRequestManager = TestIntegrityRequestManager(
prepareResult = Result.failure(Exception())
)
)

verify(getOrFetchSync).invoke(refetchCondition = Always)
verify(getOrFetchSync).invoke(refetchCondition = Always, attestationInitialized = false)
}

@Test
Expand All @@ -109,7 +131,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `init - When no browser available, AuthFlow closes and logs error`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(false)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)

// When
val viewModel = createViewModel(defaultInitialState)
Expand Down Expand Up @@ -139,7 +161,7 @@ class FinancialConnectionsSheetViewModelTest {
runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand Down Expand Up @@ -175,7 +197,7 @@ class FinancialConnectionsSheetViewModelTest {
runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand Down Expand Up @@ -210,7 +232,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `init - when instant debits flow, hosted auth url doesn't contain link_mode if unknown`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand Down Expand Up @@ -245,7 +267,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `init - when instant debits flow, hosted auth url contains incentive info if eligible`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand Down Expand Up @@ -281,7 +303,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `init - when instant debits flow, hosted auth url does not contain incentive info if not eligible`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand Down Expand Up @@ -317,7 +339,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `init - hosted auth url contains prefill details`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand Down Expand Up @@ -354,7 +376,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `init - hosted auth url contains billing details`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand Down Expand Up @@ -408,7 +430,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `init - when data flow and non-native, hosted auth url without query params is launched`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
Expand All @@ -429,7 +451,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `handleOnNewIntent - wrong intent should fire analytics event and set fail result`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
val viewModel = createViewModel(defaultInitialState)

// When
Expand All @@ -444,7 +466,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `handleOnNewIntent - on Link flows with invalid account, error is thrown`() {
runTest {
// Given
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
val viewModel = createViewModel(
defaultInitialState.copy(initialArgs = ForInstantDebits(configuration))
Expand Down Expand Up @@ -474,7 +496,7 @@ class FinancialConnectionsSheetViewModelTest {
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(fetchFinancialConnectionsSession(any()))
.thenReturn(financialConnectionsSessionWithNoMoreAccounts)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
val viewModel = createViewModel(defaultInitialState)
val cancelIntent = cancelIntent()

Expand All @@ -495,7 +517,7 @@ class FinancialConnectionsSheetViewModelTest {
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(fetchFinancialConnectionsSession(any()))
.thenReturn(financialConnectionsSessionWithNoMoreAccounts)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
val viewModel = createViewModel(defaultInitialState)

// When
Expand All @@ -520,7 +542,7 @@ class FinancialConnectionsSheetViewModelTest {
)
)
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession)
val viewModel = createViewModel(defaultInitialState)

Expand All @@ -541,7 +563,7 @@ class FinancialConnectionsSheetViewModelTest {
fun `handleOnNewIntent - when intent with unknown received, then finish with Result#Failed`() =
runTest {
// Given
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
val viewModel = createViewModel(defaultInitialState)
val errorIntent = Intent()

Expand All @@ -563,7 +585,7 @@ class FinancialConnectionsSheetViewModelTest {
// Given
val expectedSession = financialConnectionsSession()
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession)

val viewModel = createViewModel(defaultInitialState)
Expand All @@ -590,7 +612,7 @@ class FinancialConnectionsSheetViewModelTest {
"skip_aggregation=true&referral_source=APP&client_redirect_url=123"
val nativeRedirectUrl = "stripe-auth://native-redirect/com.example.app/$aggregatorUrl"
val expectedSession = financialConnectionsSession()
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession)

val viewModel = createViewModel(defaultInitialState)
Expand All @@ -616,7 +638,7 @@ class FinancialConnectionsSheetViewModelTest {
"stripe-auth://link-accounts/com.example.app/authentication_return#$returnUrlQueryParams"
val expectedSession = financialConnectionsSession()
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession)

val viewModel = createViewModel(defaultInitialState)
Expand All @@ -641,7 +663,7 @@ class FinancialConnectionsSheetViewModelTest {
// Given
val apiException = APIException()
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(fetchFinancialConnectionsSession.invoke(any())).thenAnswer { throw apiException }
val viewModel = createViewModel(defaultInitialState)

Expand All @@ -662,7 +684,7 @@ class FinancialConnectionsSheetViewModelTest {
runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)
whenever(fetchFinancialConnectionsSession(any())).thenAnswer { throw APIException() }
val viewModel = createViewModel(defaultInitialState)
// When
Expand All @@ -682,7 +704,7 @@ class FinancialConnectionsSheetViewModelTest {
runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any()))
whenever(getOrFetchSync(any(), any()))
.thenReturn(
syncResponse.copy(
manifest = sessionManifest().copy(
Expand Down Expand Up @@ -772,7 +794,7 @@ class FinancialConnectionsSheetViewModelTest {
runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), any())).thenReturn(syncResponse)

// When
val viewModel = createViewModel(defaultInitialState)
Expand Down Expand Up @@ -807,7 +829,8 @@ class FinancialConnectionsSheetViewModelTest {
)

private fun createViewModel(
initialState: FinancialConnectionsSheetState
initialState: FinancialConnectionsSheetState,
integrityRequestManager: IntegrityRequestManager = TestIntegrityRequestManager()
): FinancialConnectionsSheetViewModel {
return FinancialConnectionsSheetViewModel(
applicationId = "com.example.app",
Expand All @@ -821,7 +844,7 @@ class FinancialConnectionsSheetViewModelTest {
browserManager = browserManager,
savedStateHandle = SavedStateHandle(),
nativeAuthFlowCoordinator = mock(),
integrityRequestManager = mock(),
integrityRequestManager = integrityRequestManager,
logger = Logger.noop()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
Expand Down Expand Up @@ -137,7 +138,7 @@ class LinkStepUpVerificationViewModelTest {
private suspend fun givenGetSyncReturns(
syncResponse: SynchronizeSessionResponse = syncResponse()
) {
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(getOrFetchSync(any(), anyOrNull())).thenReturn(syncResponse)
}

private suspend fun markLinkVerifiedReturns(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
Expand Down Expand Up @@ -49,7 +50,7 @@ class NetworkingLinkLoginWarmupViewModelTest {
@Test
fun `init - payload error navigates to error screen`() = runTest {
val error = RuntimeException("Failed to fetch manifest")
whenever(getOrFetchSync(any())).thenAnswer { throw error }
whenever(getOrFetchSync(any(), anyOrNull())).thenAnswer { throw error }

buildViewModel(NetworkingLinkLoginWarmupState())

Expand Down
Loading

0 comments on commit bc4ff3b

Please sign in to comment.