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 80d4057d4c0..501591d7ebf 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 @@ -54,6 +54,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.android.model.LinkMode import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -72,7 +73,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private val analyticsTracker: FinancialConnectionsAnalyticsTracker, private val nativeRouter: NativeAuthFlowRouter, nativeAuthFlowCoordinator: NativeAuthFlowCoordinator, - initialState: FinancialConnectionsSheetState, + private val initialState: FinancialConnectionsSheetState, ) : FinancialConnectionsViewModel(initialState, nativeAuthFlowCoordinator) { private val mutex = Mutex() @@ -131,7 +132,13 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( val isInstantDebits = stateFlow.value.isInstantDebits val nativeAuthFlowEnabled = nativeRouter.nativeAuthFlowEnabled(manifest) nativeRouter.logExposure(manifest) - val hostedAuthUrl = buildHostedAuthUrl(manifest.hostedAuthUrl, isInstantDebits) + + val linkMode = initialState.initialArgs.elementsSessionContext?.linkMode + val hostedAuthUrl = buildHostedAuthUrl( + hostedAuthUrl = manifest.hostedAuthUrl, + isInstantDebits = isInstantDebits, + linkMode = linkMode, + ) if (hostedAuthUrl == null) { finishWithResult( state = stateFlow.value, @@ -166,14 +173,22 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private fun buildHostedAuthUrl( hostedAuthUrl: String?, - isInstantDebits: Boolean - ): String? = when (isInstantDebits) { - /** - * For Instant Debits, add a query parameter to the hosted auth URL so that payment account creation - * takes place on the web side of the flow and the payment method ID is returned to the app. - */ - true -> hostedAuthUrl?.let { "$it&return_payment_method=true" } - false -> hostedAuthUrl + isInstantDebits: Boolean, + linkMode: LinkMode?, + ): String? { + if (hostedAuthUrl == null) { + return null + } + + val queryParams = mutableListOf(hostedAuthUrl) + if (isInstantDebits) { + // For Instant Debits, add a query parameter to the hosted auth URL so that payment account creation + // takes place on the web side of the flow and the payment method ID is returned to the app. + queryParams.add("return_payment_method=true") + linkMode?.let { queryParams.add("link_mode=${it.value}") } + } + + return queryParams.joinToString("&") } private fun logNoBrowserAvailableAndFinish() { 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 e2b1c3be9b3..19110cb10c1 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 @@ -9,6 +9,7 @@ import com.stripe.android.core.Logger import com.stripe.android.core.exception.APIException import com.stripe.android.financialconnections.ApiKeyFixtures.sessionManifest import com.stripe.android.financialconnections.ApiKeyFixtures.syncResponse +import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsContext import com.stripe.android.financialconnections.FinancialConnectionsSheetState.AuthFlowStatus import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl @@ -31,6 +32,7 @@ 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.model.LinkMode import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -140,7 +142,12 @@ class FinancialConnectionsSheetViewModelTest { // When val viewModel = createViewModel( defaultInitialState.copy( - initialArgs = ForInstantDebits(configuration) + initialArgs = ForInstantDebits( + configuration = configuration, + elementsContext = ElementsContext( + linkMode = LinkMode.LinkPaymentMethod, + ), + ) ) ) @@ -148,11 +155,39 @@ class FinancialConnectionsSheetViewModelTest { withState(viewModel) { val viewEffect = it.viewEffect as OpenAuthFlowWithUrl assertThat(viewEffect.url).isEqualTo( - "${syncResponse.manifest.hostedAuthUrl}&return_payment_method=true" + "${syncResponse.manifest.hostedAuthUrl}&return_payment_method=true&link_mode=LINK_PAYMENT_METHOD" ) } } + @Test + 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(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false) + + // When + val viewModel = createViewModel( + defaultInitialState.copy( + initialArgs = ForInstantDebits( + configuration = configuration, + elementsContext = ElementsContext( + linkMode = null, + ), + ) + ) + ) + + // Then + withState(viewModel) { + val viewEffect = it.viewEffect as OpenAuthFlowWithUrl + assertThat(viewEffect.url).isEqualTo( + "${syncResponse.manifest.hostedAuthUrl}&return_payment_method=true" + ) + } + } + @Test fun `init - when data flow and non-native, hosted auth url without query params is launched`() = runTest { // Given