Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add call to /institution_selected endpoint #10407

Merged
merged 1 commit into from
Mar 18, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions financial-connections/api/financial-connections.api
Original file line number Diff line number Diff line change
@@ -610,6 +610,14 @@ public final class com/stripe/android/financialconnections/model/FinancialConnec
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/model/FinancialConnectionsInstitutionSelected$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/FinancialConnectionsInstitutionSelected;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/model/FinancialConnectionsInstitutionSelected;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.stripe.android.financialconnections.domain

import com.stripe.android.financialconnections.FinancialConnectionsSheetConfiguration
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
import javax.inject.Inject

internal class SelectInstitution @Inject constructor(
private val repository: FinancialConnectionsManifestRepository,
private val configuration: FinancialConnectionsSheetConfiguration,
) {

suspend operator fun invoke(
institution: FinancialConnectionsInstitution,
): FinancialConnectionsInstitutionSelected {
return repository.selectInstitution(
clientSecret = configuration.financialConnectionsSessionClientSecret,
institution = institution,
)
}
}
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ import com.stripe.android.financialconnections.domain.HandleError
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
import com.stripe.android.financialconnections.domain.PostAuthorizationSession
import com.stripe.android.financialconnections.domain.SearchInstitutions
import com.stripe.android.financialconnections.domain.SelectInstitution
import com.stripe.android.financialconnections.domain.UpdateLocalManifest
import com.stripe.android.financialconnections.features.institutionpicker.InstitutionPickerState.Payload
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
@@ -34,6 +35,7 @@ import com.stripe.android.financialconnections.navigation.Destination
import com.stripe.android.financialconnections.navigation.Destination.ManualEntry
import com.stripe.android.financialconnections.navigation.Destination.PartnerAuth
import com.stripe.android.financialconnections.navigation.Destination.PartnerAuthDrawer
import com.stripe.android.financialconnections.navigation.destination
import com.stripe.android.financialconnections.navigation.topappbar.TopAppBarStateUpdate
import com.stripe.android.financialconnections.presentation.Async
import com.stripe.android.financialconnections.presentation.Async.Loading
@@ -53,6 +55,7 @@ import kotlinx.coroutines.launch
internal class InstitutionPickerViewModel @AssistedInject constructor(
private val configuration: FinancialConnectionsSheetConfiguration,
private val postAuthorizationSession: PostAuthorizationSession,
private val selectInstitution: SelectInstitution,
private val getOrFetchSync: GetOrFetchSync,
private val searchInstitutions: SearchInstitutions,
private val featuredInstitutions: FeaturedInstitutions,
@@ -209,9 +212,17 @@ internal class InstitutionPickerViewModel @AssistedInject constructor(
activeAuthSession = null
)
}
// navigate to next step
val authSession = postAuthorizationSession(institution, getOrFetchSync())
navigateToPartnerAuth(authSession)

val manifest = getOrFetchSync().manifest
if (manifest.consentAcquired) {
val authSession = postAuthorizationSession(institution, getOrFetchSync())
navigateToPartnerAuth(authSession)
} else {
// This implies that we have shown the institution picker first and haven't shown the consent
// pane yet. Mark the institution as selected and let the backend guide us to the consent pane.
val response = selectInstitution.invoke(institution)
navigationManager.tryNavigateTo(response.manifest.nextPane.destination(referrer = PANE))
}
}.execute { async ->
copy(
selectedInstitutionId = institution.id.takeIf { async is Loading },
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.stripe.android.financialconnections.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@Parcelize
internal data class FinancialConnectionsInstitutionSelected(
@SerialName("manifest")
val manifest: FinancialConnectionsSessionManifest,
@SerialName("text")
val text: TextUpdate? = null,
) : Parcelable
Original file line number Diff line number Diff line change
@@ -57,6 +57,9 @@ internal data class FinancialConnectionsSessionManifest(
@SerialName(value = "consent_required")
val consentRequired: Boolean,

@SerialName(value = "consent_acquired_at")
val consentAcquiredAt: String?,

@SerialName(value = "custom_manual_entry_handling")
val customManualEntryHandling: Boolean,

@@ -187,6 +190,9 @@ internal data class FinancialConnectionsSessionManifest(
val theme: Theme? = null,
) : Parcelable {

val consentAcquired: Boolean
get() = !consentRequired || consentAcquiredAt != null

/**
*
*
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import com.stripe.android.financialconnections.analytics.AuthSessionEvent
import com.stripe.android.financialconnections.model.AuthorizationRepairResponse
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
@@ -72,6 +73,17 @@ internal interface FinancialConnectionsManifestRepository {
institution: FinancialConnectionsInstitution,
): FinancialConnectionsAuthorizationSession

@Throws(
AuthenticationException::class,
InvalidRequestException::class,
APIConnectionException::class,
APIException::class
)
suspend fun selectInstitution(
clientSecret: String,
institution: FinancialConnectionsInstitution,
): FinancialConnectionsInstitutionSelected

suspend fun postAuthorizationSessionEvent(
clientSecret: String,
clientTimestamp: Date,
@@ -291,6 +303,27 @@ private class FinancialConnectionsManifestRepositoryImpl(
}
}

override suspend fun selectInstitution(
clientSecret: String,
institution: FinancialConnectionsInstitution,
): FinancialConnectionsInstitutionSelected {
val request = apiRequestFactory.createPost(
url = institutionSelectedUrl,
options = provideApiRequestOptions(useConsumerPublishableKey = true),
params = mapOf(
NetworkConstants.PARAMS_CLIENT_SECRET to clientSecret,
"currently_selected_institution" to institution.id,
)
)
return requestExecutor.execute(
request,
FinancialConnectionsInstitutionSelected.serializer()
).also { newResponse ->
updateActiveInstitution("selectInstitution", institution)
updateCachedManifest("selectInstitution", newResponse.manifest)
}
}

override suspend fun postAuthorizationSessionEvent(
clientSecret: String,
clientTimestamp: Date,
@@ -601,5 +634,8 @@ private class FinancialConnectionsManifestRepositoryImpl(

internal const val generateRepairUrl: String =
"${ApiRequest.API_HOST}/v1/connections/repair_sessions/generate_url"

private const val institutionSelectedUrl: String =
"${ApiRequest.API_HOST}/v1/link_account_sessions/institution_selected"
}
}
Original file line number Diff line number Diff line change
@@ -52,7 +52,9 @@ internal object ApiKeyFixtures {
livemode = true
)

fun sessionManifest() = FinancialConnectionsSessionManifest(
fun sessionManifest(
consentAcquiredAt: String? = "some_date",
) = FinancialConnectionsSessionManifest(
allowManualEntry = true,
consentRequired = true,
customManualEntryHandling = true,
@@ -73,7 +75,8 @@ internal object ApiKeyFixtures {
manualEntryMode = ManualEntryMode.AUTOMATIC,
successUrl = SUCCESS_URL,
cancelUrl = CANCEL_URL,
hostedAuthUrl = HOSTED_AUTH_URL
hostedAuthUrl = HOSTED_AUTH_URL,
consentAcquiredAt = consentAcquiredAt,
)

fun authorizationSession() = FinancialConnectionsAuthorizationSession(
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import com.stripe.android.financialconnections.domain.GetOrFetchSync
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
import com.stripe.android.financialconnections.domain.PostAuthorizationSession
import com.stripe.android.financialconnections.domain.SearchInstitutions
import com.stripe.android.financialconnections.domain.SelectInstitution
import com.stripe.android.financialconnections.domain.UpdateLocalManifest
import com.stripe.android.financialconnections.exception.InstitutionPlannedDowntimeError
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
@@ -32,7 +33,10 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
import kotlin.test.assertEquals
@@ -52,6 +56,7 @@ internal class InstitutionPickerViewModelTest {
private val updateLocalManifest = mock<UpdateLocalManifest>()
private val navigationManager = TestNavigationManager()
private val postAuthorizationSession = mock<PostAuthorizationSession>()
private val selectInstitution = mock<SelectInstitution>()
private val eventTracker = TestFinancialConnectionsAnalyticsTracker()
private val nativeAuthFlowCoordinator = NativeAuthFlowCoordinator()
private val defaultConfiguration = FinancialConnectionsSheetConfiguration(
@@ -72,6 +77,7 @@ internal class InstitutionPickerViewModelTest {
logger = Logger.noop(),
eventTracker = eventTracker,
postAuthorizationSession = postAuthorizationSession,
selectInstitution = selectInstitution,
handleError = handleError,
initialState = state,
nativeAuthFlowCoordinator = nativeAuthFlowCoordinator,
@@ -327,6 +333,41 @@ internal class InstitutionPickerViewModelTest {
)
}

@Test
fun `Creates normal auth session when not in 'institution picker first' flow`() = runTest {
val manifest = ApiKeyFixtures.sessionManifest().copy(
consentRequired = true,
consentAcquiredAt = "some date",
)
val institution = ApiKeyFixtures.institution()

givenManifestReturns(manifest)
givenCreateSessionForInstitutionReturns(ApiKeyFixtures.authorizationSession())

val viewModel = buildViewModel(InstitutionPickerState())
viewModel.onInstitutionSelected(institution, fromFeatured = true)

verify(postAuthorizationSession).invoke(eq(institution), any())
verify(selectInstitution, never()).invoke(any())
}

@Test
fun `Calls institution_selected when in 'institution picker first' flow`() = runTest {
val manifest = ApiKeyFixtures.sessionManifest().copy(
consentRequired = true,
consentAcquiredAt = null,
)
val institution = ApiKeyFixtures.institution()

givenManifestReturns(manifest)

val viewModel = buildViewModel(InstitutionPickerState())
viewModel.onInstitutionSelected(institution, fromFeatured = true)

verify(postAuthorizationSession, never()).invoke(any(), any())
verify(selectInstitution).invoke(institution)
}

private suspend fun givenCreateSessionForInstitutionThrows(throwable: Throwable) {
whenever(postAuthorizationSession(any(), any())).then { throw throwable }
}
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package com.stripe.android.financialconnections.networking
import com.stripe.android.financialconnections.analytics.AuthSessionEvent
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
@@ -107,4 +108,11 @@ internal abstract class AbsFinancialConnectionsManifestRepository : FinancialCon
): SynchronizeSessionResponse {
TODO("Not yet implemented")
}

override suspend fun selectInstitution(
clientSecret: String,
institution: FinancialConnectionsInstitution
): FinancialConnectionsInstitutionSelected {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import com.stripe.android.financialconnections.ApiKeyFixtures
import com.stripe.android.financialconnections.analytics.AuthSessionEvent
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
@@ -109,4 +110,11 @@ internal class FakeFinancialConnectionsManifestRepository : FinancialConnections
override fun updateLocalManifest(
block: (FinancialConnectionsSessionManifest) -> FinancialConnectionsSessionManifest
) = Unit

override suspend fun selectInstitution(
clientSecret: String,
institution: FinancialConnectionsInstitution
): FinancialConnectionsInstitutionSelected {
TODO("Not yet implemented")
}
}