Skip to content

Commit 1af4f01

Browse files
authored
Add call to /institution_selected endpoint (#10407)
1 parent 7b81342 commit 1af4f01

File tree

10 files changed

+163
-5
lines changed

10 files changed

+163
-5
lines changed

financial-connections/api/financial-connections.api

+8
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,14 @@ public final class com/stripe/android/financialconnections/model/FinancialConnec
610610
public synthetic fun newArray (I)[Ljava/lang/Object;
611611
}
612612

613+
public final class com/stripe/android/financialconnections/model/FinancialConnectionsInstitutionSelected$Creator : android/os/Parcelable$Creator {
614+
public fun <init> ()V
615+
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/FinancialConnectionsInstitutionSelected;
616+
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
617+
public final fun newArray (I)[Lcom/stripe/android/financialconnections/model/FinancialConnectionsInstitutionSelected;
618+
public synthetic fun newArray (I)[Ljava/lang/Object;
619+
}
620+
613621
public final class com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest$Creator : android/os/Parcelable$Creator {
614622
public fun <init> ()V
615623
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.stripe.android.financialconnections.domain
2+
3+
import com.stripe.android.financialconnections.FinancialConnectionsSheetConfiguration
4+
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
5+
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
6+
import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
7+
import javax.inject.Inject
8+
9+
internal class SelectInstitution @Inject constructor(
10+
private val repository: FinancialConnectionsManifestRepository,
11+
private val configuration: FinancialConnectionsSheetConfiguration,
12+
) {
13+
14+
suspend operator fun invoke(
15+
institution: FinancialConnectionsInstitution,
16+
): FinancialConnectionsInstitutionSelected {
17+
return repository.selectInstitution(
18+
clientSecret = configuration.financialConnectionsSessionClientSecret,
19+
institution = institution,
20+
)
21+
}
22+
}

financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModel.kt

+14-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.stripe.android.financialconnections.domain.HandleError
2424
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
2525
import com.stripe.android.financialconnections.domain.PostAuthorizationSession
2626
import com.stripe.android.financialconnections.domain.SearchInstitutions
27+
import com.stripe.android.financialconnections.domain.SelectInstitution
2728
import com.stripe.android.financialconnections.domain.UpdateLocalManifest
2829
import com.stripe.android.financialconnections.features.institutionpicker.InstitutionPickerState.Payload
2930
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
@@ -34,6 +35,7 @@ import com.stripe.android.financialconnections.navigation.Destination
3435
import com.stripe.android.financialconnections.navigation.Destination.ManualEntry
3536
import com.stripe.android.financialconnections.navigation.Destination.PartnerAuth
3637
import com.stripe.android.financialconnections.navigation.Destination.PartnerAuthDrawer
38+
import com.stripe.android.financialconnections.navigation.destination
3739
import com.stripe.android.financialconnections.navigation.topappbar.TopAppBarStateUpdate
3840
import com.stripe.android.financialconnections.presentation.Async
3941
import com.stripe.android.financialconnections.presentation.Async.Loading
@@ -53,6 +55,7 @@ import kotlinx.coroutines.launch
5355
internal class InstitutionPickerViewModel @AssistedInject constructor(
5456
private val configuration: FinancialConnectionsSheetConfiguration,
5557
private val postAuthorizationSession: PostAuthorizationSession,
58+
private val selectInstitution: SelectInstitution,
5659
private val getOrFetchSync: GetOrFetchSync,
5760
private val searchInstitutions: SearchInstitutions,
5861
private val featuredInstitutions: FeaturedInstitutions,
@@ -209,9 +212,17 @@ internal class InstitutionPickerViewModel @AssistedInject constructor(
209212
activeAuthSession = null
210213
)
211214
}
212-
// navigate to next step
213-
val authSession = postAuthorizationSession(institution, getOrFetchSync())
214-
navigateToPartnerAuth(authSession)
215+
216+
val manifest = getOrFetchSync().manifest
217+
if (manifest.consentAcquired) {
218+
val authSession = postAuthorizationSession(institution, getOrFetchSync())
219+
navigateToPartnerAuth(authSession)
220+
} else {
221+
// This implies that we have shown the institution picker first and haven't shown the consent
222+
// pane yet. Mark the institution as selected and let the backend guide us to the consent pane.
223+
val response = selectInstitution.invoke(institution)
224+
navigationManager.tryNavigateTo(response.manifest.nextPane.destination(referrer = PANE))
225+
}
215226
}.execute { async ->
216227
copy(
217228
selectedInstitutionId = institution.id.takeIf { async is Loading },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.stripe.android.financialconnections.model
2+
3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
5+
import kotlinx.serialization.SerialName
6+
import kotlinx.serialization.Serializable
7+
8+
@Serializable
9+
@Parcelize
10+
internal data class FinancialConnectionsInstitutionSelected(
11+
@SerialName("manifest")
12+
val manifest: FinancialConnectionsSessionManifest,
13+
@SerialName("text")
14+
val text: TextUpdate? = null,
15+
) : Parcelable

financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt

+6
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ internal data class FinancialConnectionsSessionManifest(
5757
@SerialName(value = "consent_required")
5858
val consentRequired: Boolean,
5959

60+
@SerialName(value = "consent_acquired_at")
61+
val consentAcquiredAt: String?,
62+
6063
@SerialName(value = "custom_manual_entry_handling")
6164
val customManualEntryHandling: Boolean,
6265

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

193+
val consentAcquired: Boolean
194+
get() = !consentRequired || consentAcquiredAt != null
195+
190196
/**
191197
*
192198
*

financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt

+36
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.stripe.android.financialconnections.analytics.AuthSessionEvent
1010
import com.stripe.android.financialconnections.model.AuthorizationRepairResponse
1111
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
1212
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
13+
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
1314
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
1415
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
1516
import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
@@ -72,6 +73,17 @@ internal interface FinancialConnectionsManifestRepository {
7273
institution: FinancialConnectionsInstitution,
7374
): FinancialConnectionsAuthorizationSession
7475

76+
@Throws(
77+
AuthenticationException::class,
78+
InvalidRequestException::class,
79+
APIConnectionException::class,
80+
APIException::class
81+
)
82+
suspend fun selectInstitution(
83+
clientSecret: String,
84+
institution: FinancialConnectionsInstitution,
85+
): FinancialConnectionsInstitutionSelected
86+
7587
suspend fun postAuthorizationSessionEvent(
7688
clientSecret: String,
7789
clientTimestamp: Date,
@@ -291,6 +303,27 @@ private class FinancialConnectionsManifestRepositoryImpl(
291303
}
292304
}
293305

306+
override suspend fun selectInstitution(
307+
clientSecret: String,
308+
institution: FinancialConnectionsInstitution,
309+
): FinancialConnectionsInstitutionSelected {
310+
val request = apiRequestFactory.createPost(
311+
url = institutionSelectedUrl,
312+
options = provideApiRequestOptions(useConsumerPublishableKey = true),
313+
params = mapOf(
314+
NetworkConstants.PARAMS_CLIENT_SECRET to clientSecret,
315+
"currently_selected_institution" to institution.id,
316+
)
317+
)
318+
return requestExecutor.execute(
319+
request,
320+
FinancialConnectionsInstitutionSelected.serializer()
321+
).also { newResponse ->
322+
updateActiveInstitution("selectInstitution", institution)
323+
updateCachedManifest("selectInstitution", newResponse.manifest)
324+
}
325+
}
326+
294327
override suspend fun postAuthorizationSessionEvent(
295328
clientSecret: String,
296329
clientTimestamp: Date,
@@ -601,5 +634,8 @@ private class FinancialConnectionsManifestRepositoryImpl(
601634

602635
internal const val generateRepairUrl: String =
603636
"${ApiRequest.API_HOST}/v1/connections/repair_sessions/generate_url"
637+
638+
private const val institutionSelectedUrl: String =
639+
"${ApiRequest.API_HOST}/v1/link_account_sessions/institution_selected"
604640
}
605641
}

financial-connections/src/test/java/com/stripe/android/financialconnections/ApiKeyFixtures.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ internal object ApiKeyFixtures {
5252
livemode = true
5353
)
5454

55-
fun sessionManifest() = FinancialConnectionsSessionManifest(
55+
fun sessionManifest(
56+
consentAcquiredAt: String? = "some_date",
57+
) = FinancialConnectionsSessionManifest(
5658
allowManualEntry = true,
5759
consentRequired = true,
5860
customManualEntryHandling = true,
@@ -73,7 +75,8 @@ internal object ApiKeyFixtures {
7375
manualEntryMode = ManualEntryMode.AUTOMATIC,
7476
successUrl = SUCCESS_URL,
7577
cancelUrl = CANCEL_URL,
76-
hostedAuthUrl = HOSTED_AUTH_URL
78+
hostedAuthUrl = HOSTED_AUTH_URL,
79+
consentAcquiredAt = consentAcquiredAt,
7780
)
7881

7982
fun authorizationSession() = FinancialConnectionsAuthorizationSession(

financial-connections/src/test/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModelTest.kt

+41
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.stripe.android.financialconnections.domain.GetOrFetchSync
1212
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
1313
import com.stripe.android.financialconnections.domain.PostAuthorizationSession
1414
import com.stripe.android.financialconnections.domain.SearchInstitutions
15+
import com.stripe.android.financialconnections.domain.SelectInstitution
1516
import com.stripe.android.financialconnections.domain.UpdateLocalManifest
1617
import com.stripe.android.financialconnections.exception.InstitutionPlannedDowntimeError
1718
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
@@ -32,7 +33,10 @@ import org.junit.Rule
3233
import org.junit.Test
3334
import org.junit.rules.TestRule
3435
import org.mockito.kotlin.any
36+
import org.mockito.kotlin.eq
3537
import org.mockito.kotlin.mock
38+
import org.mockito.kotlin.never
39+
import org.mockito.kotlin.verify
3640
import org.mockito.kotlin.verifyNoInteractions
3741
import org.mockito.kotlin.whenever
3842
import kotlin.test.assertEquals
@@ -52,6 +56,7 @@ internal class InstitutionPickerViewModelTest {
5256
private val updateLocalManifest = mock<UpdateLocalManifest>()
5357
private val navigationManager = TestNavigationManager()
5458
private val postAuthorizationSession = mock<PostAuthorizationSession>()
59+
private val selectInstitution = mock<SelectInstitution>()
5560
private val eventTracker = TestFinancialConnectionsAnalyticsTracker()
5661
private val nativeAuthFlowCoordinator = NativeAuthFlowCoordinator()
5762
private val defaultConfiguration = FinancialConnectionsSheetConfiguration(
@@ -72,6 +77,7 @@ internal class InstitutionPickerViewModelTest {
7277
logger = Logger.noop(),
7378
eventTracker = eventTracker,
7479
postAuthorizationSession = postAuthorizationSession,
80+
selectInstitution = selectInstitution,
7581
handleError = handleError,
7682
initialState = state,
7783
nativeAuthFlowCoordinator = nativeAuthFlowCoordinator,
@@ -327,6 +333,41 @@ internal class InstitutionPickerViewModelTest {
327333
)
328334
}
329335

336+
@Test
337+
fun `Creates normal auth session when not in 'institution picker first' flow`() = runTest {
338+
val manifest = ApiKeyFixtures.sessionManifest().copy(
339+
consentRequired = true,
340+
consentAcquiredAt = "some date",
341+
)
342+
val institution = ApiKeyFixtures.institution()
343+
344+
givenManifestReturns(manifest)
345+
givenCreateSessionForInstitutionReturns(ApiKeyFixtures.authorizationSession())
346+
347+
val viewModel = buildViewModel(InstitutionPickerState())
348+
viewModel.onInstitutionSelected(institution, fromFeatured = true)
349+
350+
verify(postAuthorizationSession).invoke(eq(institution), any())
351+
verify(selectInstitution, never()).invoke(any())
352+
}
353+
354+
@Test
355+
fun `Calls institution_selected when in 'institution picker first' flow`() = runTest {
356+
val manifest = ApiKeyFixtures.sessionManifest().copy(
357+
consentRequired = true,
358+
consentAcquiredAt = null,
359+
)
360+
val institution = ApiKeyFixtures.institution()
361+
362+
givenManifestReturns(manifest)
363+
364+
val viewModel = buildViewModel(InstitutionPickerState())
365+
viewModel.onInstitutionSelected(institution, fromFeatured = true)
366+
367+
verify(postAuthorizationSession, never()).invoke(any(), any())
368+
verify(selectInstitution).invoke(institution)
369+
}
370+
330371
private suspend fun givenCreateSessionForInstitutionThrows(throwable: Throwable) {
331372
whenever(postAuthorizationSession(any(), any())).then { throw throwable }
332373
}

financial-connections/src/test/java/com/stripe/android/financialconnections/networking/AbsFinancialConnectionsManifestRepository.kt

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.stripe.android.financialconnections.networking
33
import com.stripe.android.financialconnections.analytics.AuthSessionEvent
44
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
55
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
6+
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
67
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
78
import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
89
import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
@@ -107,4 +108,11 @@ internal abstract class AbsFinancialConnectionsManifestRepository : FinancialCon
107108
): SynchronizeSessionResponse {
108109
TODO("Not yet implemented")
109110
}
111+
112+
override suspend fun selectInstitution(
113+
clientSecret: String,
114+
institution: FinancialConnectionsInstitution
115+
): FinancialConnectionsInstitutionSelected {
116+
TODO("Not yet implemented")
117+
}
110118
}

financial-connections/src/test/java/com/stripe/android/financialconnections/networking/FakeFinancialConnectionsManifestRepository.kt

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.stripe.android.financialconnections.ApiKeyFixtures
44
import com.stripe.android.financialconnections.analytics.AuthSessionEvent
55
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
66
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
7+
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitutionSelected
78
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
89
import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
910
import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
@@ -109,4 +110,11 @@ internal class FakeFinancialConnectionsManifestRepository : FinancialConnections
109110
override fun updateLocalManifest(
110111
block: (FinancialConnectionsSessionManifest) -> FinancialConnectionsSessionManifest
111112
) = Unit
113+
114+
override suspend fun selectInstitution(
115+
clientSecret: String,
116+
institution: FinancialConnectionsInstitution
117+
): FinancialConnectionsInstitutionSelected {
118+
TODO("Not yet implemented")
119+
}
112120
}

0 commit comments

Comments
 (0)