diff --git a/CHANGELOG.md b/CHANGELOG.md index 455809f76c4..307de44ac39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,15 @@ * Instead of providing only the `PaymentMethod` ID, `CreateIntentCallback` now provides the entire `PaymentMethod` object. * `CreateIntentCallbackForServerSideConfirmation` has been removed. If you’re using server-side confirmation, use `CreateIntentCallback` and its new `shouldSavePaymentMethod` parameter. +### Financial Connections +* [FIXED][6794](https://github.com/stripe/stripe-android/pull/6794) Gracefully fails when no web browser available. + ## 20.25.4 - 2023-05-30 ### All SDKs * [FIXED][6771](https://github.com/stripe/stripe-android/pull/6771) Fixed the length of phone number field. + +### Financial Connections * [CHANGED][6789](https://github.com/stripe/stripe-android/pull/6789) Updated Mavericks to 3.0.3. ## 20.25.3 - 2023-05-23 diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api index a8f6a46561e..837069c019a 100644 --- a/financial-connections/api/financial-connections.api +++ b/financial-connections/api/financial-connections.api @@ -269,7 +269,12 @@ public final class com/stripe/android/financialconnections/domain/Text$Creator : public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/stripe/android/financialconnections/exception/CustomManualEntryRequiredError : java/lang/Exception { +public final class com/stripe/android/financialconnections/exception/AppInitializationError : com/stripe/android/core/exception/StripeException { + public static final field $stable I + public fun (Ljava/lang/String;)V +} + +public final class com/stripe/android/financialconnections/exception/CustomManualEntryRequiredError : com/stripe/android/core/exception/StripeException { public static final field $stable I public fun ()V } diff --git a/financial-connections/res/values/strings.xml b/financial-connections/res/values/strings.xml index 506a5f61d09..3d922c5df64 100644 --- a/financial-connections/res/values/strings.xml +++ b/financial-connections/res/values/strings.xml @@ -135,6 +135,7 @@ We\'re experiencing high volumes. Try again in an hour and if you need immediate assistance, please contact us. Something went wrong. Disconnected + No Web browser available to start connecting your accounts. Please install a browser and try again. Your account was connected to %1$s but could not be saved to Link with Stripe at this time. Your accounts were connected to %1$s but could not be saved to Link with Stripe at this time. diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt index 68e6767d67a..f43f4e324c5 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt @@ -3,6 +3,7 @@ package com.stripe.android.financialconnections import android.content.Intent import android.net.Uri import android.os.Bundle +import android.widget.Toast import androidx.activity.addCallback import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult @@ -87,9 +88,12 @@ internal class FinancialConnectionsSheetActivity : AppCompatActivity(), Maverick ) ) - is FinishWithResult -> finishWithResult( - viewEffect.result - ) + is FinishWithResult -> { + viewEffect.finishToast?.let { + Toast.makeText(this, it, Toast.LENGTH_LONG).show() + } + finishWithResult(viewEffect.result) + } is OpenNativeAuthFlow -> startNativeAuthFlowForResult.launch( Intent( diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt index 8db6d50af7c..50886478de1 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt @@ -1,5 +1,6 @@ package com.stripe.android.financialconnections +import androidx.annotation.StringRes import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.PersistState import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs @@ -80,6 +81,7 @@ internal sealed class FinancialConnectionsSheetViewEffect { * Finish [FinancialConnectionsSheetActivity] with a given [FinancialConnectionsSheetActivityResult] */ data class FinishWithResult( - val result: FinancialConnectionsSheetActivityResult + val result: FinancialConnectionsSheetActivityResult, + @StringRes val finishToast: Int? = null ) : FinancialConnectionsSheetViewEffect() } 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 c2e9a64b15a..9f9a2d7cd0e 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 @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import android.net.Uri import androidx.activity.result.ActivityResult +import androidx.annotation.StringRes import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -12,13 +13,17 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheetState.Au import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenNativeAuthFlow +import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker +import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.Error import com.stripe.android.financialconnections.analytics.FinancialConnectionsEventReporter import com.stripe.android.financialconnections.di.APPLICATION_ID import com.stripe.android.financialconnections.di.DaggerFinancialConnectionsSheetComponent import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSession import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSessionForToken +import com.stripe.android.financialconnections.domain.IsBrowserAvailable import com.stripe.android.financialconnections.domain.NativeAuthFlowRouter import com.stripe.android.financialconnections.domain.SynchronizeFinancialConnectionsSession +import com.stripe.android.financialconnections.exception.AppInitializationError import com.stripe.android.financialconnections.exception.CustomManualEntryRequiredError import com.stripe.android.financialconnections.features.manualentry.isCustomManualEntryError import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.ForData @@ -30,6 +35,7 @@ import com.stripe.android.financialconnections.launcher.FinancialConnectionsShee import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Failed import com.stripe.android.financialconnections.model.FinancialConnectionsSession import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest +import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane import com.stripe.android.financialconnections.model.SynchronizeSessionResponse import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity import com.stripe.android.financialconnections.utils.parcelable @@ -49,6 +55,8 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private val fetchFinancialConnectionsSessionForToken: FetchFinancialConnectionsSessionForToken, private val logger: Logger, private val eventReporter: FinancialConnectionsEventReporter, + private val analyticsTracker: FinancialConnectionsAnalyticsTracker, + private val isBrowserAvailable: IsBrowserAvailable, private val nativeRouter: NativeAuthFlowRouter, initialState: FinancialConnectionsSheetState ) : MavericksViewModel(initialState) { @@ -93,13 +101,13 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * */ private fun openAuthFlow(sync: SynchronizeSessionResponse) { - // stores manifest in state for future references. - val manifest = sync.manifest - val nativeAuthFlowEnabled = nativeRouter.nativeAuthFlowEnabled(sync.manifest) - viewModelScope.launch { - nativeRouter.logExposure(sync.manifest) + if (isBrowserAvailable().not()) { + logNoBrowserAvailableAndFinish() + return } - if (manifest.hostedAuthUrl == null) { + val nativeAuthFlowEnabled = nativeRouter.nativeAuthFlowEnabled(sync.manifest) + viewModelScope.launch { nativeRouter.logExposure(sync.manifest) } + if (sync.manifest.hostedAuthUrl == null) { withState { finishWithResult( state = it, @@ -114,18 +122,30 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( } setState { copy( - manifest = manifest, + manifest = sync.manifest, webAuthFlowStatus = authFlowStatus, viewEffect = if (nativeAuthFlowEnabled) { OpenNativeAuthFlow(initialArgs.configuration, sync) } else { - OpenAuthFlowWithUrl(manifest.hostedAuthUrl) + OpenAuthFlowWithUrl(sync.manifest.hostedAuthUrl) } ) } } } + private fun logNoBrowserAvailableAndFinish() { + viewModelScope.launch { + val error = AppInitializationError("No Web browser available to launch AuthFlow") + analyticsTracker.track(Error(Pane.UNEXPECTED_ERROR, error)) + finishWithResult( + state = awaitState(), + result = Failed(error), + finishMessage = R.string.stripe_no_browser_installed + ) + } + } + /** * Activity recreation changes the lifecycle order: * @@ -167,6 +187,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( state = state, result = Canceled ) + AuthFlowStatus.INTERMEDIATE_DEEPLINK -> setState { copy( webAuthFlowStatus = AuthFlowStatus.ON_EXTERNAL_ACTIVITY @@ -195,6 +216,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( state = state, result = Canceled ) + AuthFlowStatus.INTERMEDIATE_DEEPLINK -> setState { copy( webAuthFlowStatus = AuthFlowStatus.ON_EXTERNAL_ACTIVITY @@ -417,10 +439,11 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private fun finishWithResult( state: FinancialConnectionsSheetState, - result: FinancialConnectionsSheetActivityResult + result: FinancialConnectionsSheetActivityResult, + @StringRes finishMessage: Int? = null, ) { eventReporter.onResult(state.initialArgs.configuration, result) - setState { copy(viewEffect = FinishWithResult(result)) } + setState { copy(viewEffect = FinishWithResult(result, finishMessage)) } } companion object : diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsEvent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsEvent.kt index 5d257904cce..bdff7322c67 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsEvent.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsEvent.kt @@ -151,15 +151,6 @@ internal sealed class FinancialConnectionsEvent( ).filterNotNullValues() ) - class ClickLinkAnotherAccount( - pane: Pane, - ) : FinancialConnectionsEvent( - name = "click.link_another_account", - mapOf( - "pane" to pane.value, - ).filterNotNullValues() - ) - class NetworkingNewConsumer( pane: Pane, ) : FinancialConnectionsEvent( diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IsBrowserAvailable.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IsBrowserAvailable.kt new file mode 100644 index 00000000000..e1821381f90 --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IsBrowserAvailable.kt @@ -0,0 +1,21 @@ +package com.stripe.android.financialconnections.domain + +import android.app.Application +import android.content.Intent +import android.net.Uri +import javax.inject.Inject + +/** + * Check if a browser is available on the device. + */ +internal class IsBrowserAvailable @Inject constructor( + private val context: Application, +) { + + operator fun invoke(): Boolean { + val url = "https://" + val webAddress = Uri.parse(url) + val intentWeb = Intent(Intent.ACTION_VIEW, webAddress) + return intentWeb.resolveActivity(context.packageManager) != null + } +} diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/AccountNoneEligibleForPaymentMethodError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/AccountNoneEligibleForPaymentMethodError.kt index 3afb4c05fb6..209511172bd 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/AccountNoneEligibleForPaymentMethodError.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/AccountNoneEligibleForPaymentMethodError.kt @@ -12,14 +12,3 @@ internal class AccountNoneEligibleForPaymentMethodError( name = "AccountNoneEligibleForPaymentMethodError", stripeException = stripeException ) - -internal abstract class FinancialConnectionsError( - val name: String, - stripeException: StripeException, -) : StripeException( - stripeException.stripeError, - stripeException.requestId, - stripeException.statusCode, - stripeException.cause, - stripeException.message -) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/AppInitializationError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/AppInitializationError.kt new file mode 100644 index 00000000000..d121b99ac3c --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/AppInitializationError.kt @@ -0,0 +1,11 @@ +package com.stripe.android.financialconnections.exception + +import com.stripe.android.core.exception.StripeException + +class AppInitializationError(message: String) : StripeException( + message = message, + cause = null, + requestId = null, + statusCode = 0, + stripeError = null +) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/CustomManualEntryRequiredError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/CustomManualEntryRequiredError.kt index 1224ef19469..7fe2d563e6e 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/CustomManualEntryRequiredError.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/CustomManualEntryRequiredError.kt @@ -1,7 +1,9 @@ package com.stripe.android.financialconnections.exception +import com.stripe.android.core.exception.StripeException + /** * The AuthFlow was prematurely cancelled due to user requesting manual entry. * */ -class CustomManualEntryRequiredError : Exception() +class CustomManualEntryRequiredError : StripeException() diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsError.kt new file mode 100644 index 00000000000..bd478a62340 --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsError.kt @@ -0,0 +1,17 @@ +package com.stripe.android.financialconnections.exception + +import com.stripe.android.core.exception.StripeException + +/** + * Base class for errors that occur during the financial connections flow. + */ +internal abstract class FinancialConnectionsError( + val name: String, + stripeException: StripeException, +) : StripeException( + stripeException.stripeError, + stripeException.requestId, + stripeException.statusCode, + stripeException.cause, + stripeException.message +) 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 92f15088f0e..2ef3fd273b5 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 @@ -16,7 +16,9 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffe import com.stripe.android.financialconnections.analytics.FinancialConnectionsEventReporter import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSession import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSessionForToken +import com.stripe.android.financialconnections.domain.IsBrowserAvailable import com.stripe.android.financialconnections.domain.SynchronizeFinancialConnectionsSession +import com.stripe.android.financialconnections.exception.AppInitializationError import com.stripe.android.financialconnections.exception.CustomManualEntryRequiredError import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.ForLink @@ -56,6 +58,8 @@ class FinancialConnectionsSheetViewModelTest { private val syncResponse = syncResponse() private val fetchFinancialConnectionsSession = mock() + private val isBrowserAvailable = mock() + private val analyticsTracker = TestFinancialConnectionsAnalyticsTracker() private val fetchFinancialConnectionsSessionForToken = mock() private val synchronizeFinancialConnectionsSession = @@ -67,6 +71,7 @@ class FinancialConnectionsSheetViewModelTest { @Test fun `init - eventReporter fires onPresented`() { runTest { + whenever(isBrowserAvailable.invoke()).thenReturn(true) createViewModel(defaultInitialState.copy(manifest = sessionManifest())) verify(eventReporter).onPresented(configuration) } @@ -90,10 +95,39 @@ class FinancialConnectionsSheetViewModelTest { } } + @Test + fun `init - When no browser available, AuthFlow closes and logs error`() = runTest { + // Given + whenever(isBrowserAvailable.invoke()).thenReturn(false) + whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) + + // When + val viewModel = createViewModel(defaultInitialState) + + // Then + withState(viewModel) { + assertThat(it.webAuthFlowStatus).isEqualTo(AuthFlowStatus.NONE) + require(it.viewEffect is FinishWithResult) + require(it.viewEffect.result is Failed) + assertThat(it.viewEffect.result.error) + .isInstanceOf(AppInitializationError::class.java) + analyticsTracker.assertContainsEvent( + expectedEventName = "linked_accounts.error.unexpected", + expectedParams = mapOf( + "pane" to "unexpected_error", + "error" to "AppInitializationError", + "error_type" to "AppInitializationError", + "error_message" to "No Web browser available to launch AuthFlow", + ) + ) + } + } + @Test fun `handleOnNewIntent - wrong intent should fire analytics event and set fail result`() { runTest { // Given + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) val viewModel = createViewModel(defaultInitialState) @@ -111,6 +145,7 @@ class FinancialConnectionsSheetViewModelTest { runTest { // Given val linkedAccountId = "1234" + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) val viewModel = createViewModel( defaultInitialState.copy(initialArgs = ForLink(configuration)) @@ -139,6 +174,7 @@ class FinancialConnectionsSheetViewModelTest { runTest { // Given whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) + whenever(isBrowserAvailable.invoke()).thenReturn(true) val viewModel = createViewModel( defaultInitialState.copy(initialArgs = ForLink(configuration)) ) @@ -164,6 +200,7 @@ class FinancialConnectionsSheetViewModelTest { fun `handleOnNewIntent - intent with cancel url should fire analytics event and set cancel result`() { runTest { // Given + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(fetchFinancialConnectionsSession(any())) .thenReturn(financialConnectionsSessionWithNoMoreAccounts) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) @@ -184,6 +221,7 @@ class FinancialConnectionsSheetViewModelTest { fun `handleOnNewIntent - when intent with cancel URL received, then finish with Result#Cancel`() = runTest { // Given + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(fetchFinancialConnectionsSession(any())) .thenReturn(financialConnectionsSessionWithNoMoreAccounts) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) @@ -211,6 +249,7 @@ class FinancialConnectionsSheetViewModelTest { ) ) ) + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession) val viewModel = createViewModel(defaultInitialState) @@ -253,6 +292,7 @@ class FinancialConnectionsSheetViewModelTest { runTest { // Given val expectedSession = financialConnectionsSession() + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession) @@ -302,8 +342,10 @@ class FinancialConnectionsSheetViewModelTest { runTest { // Given val returnUrlQueryParams = "authSessionId=bcsess_123&code=success&memberGuid=MBR-123" - val returnUrl = "stripe-auth://link-accounts/com.example.app/authentication_return#$returnUrlQueryParams" + val returnUrl = + "stripe-auth://link-accounts/com.example.app/authentication_return#$returnUrlQueryParams" val expectedSession = financialConnectionsSession() + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession) @@ -328,6 +370,7 @@ class FinancialConnectionsSheetViewModelTest { runTest { // Given val apiException = APIException() + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) whenever(fetchFinancialConnectionsSession.invoke(any())).thenAnswer { throw apiException } val viewModel = createViewModel(defaultInitialState) @@ -348,6 +391,7 @@ class FinancialConnectionsSheetViewModelTest { fun `handleOnNewIntent - when error fetching account session, then finish with Result#Failed`() = runTest { // Given + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) whenever(fetchFinancialConnectionsSession(any())).thenAnswer { throw APIException() } val viewModel = createViewModel(defaultInitialState) @@ -368,6 +412,7 @@ class FinancialConnectionsSheetViewModelTest { fun `onResume - when flow is still active and no config changes, finish with Result#Cancelled`() { runTest { // Given + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()) .thenReturn( syncResponse.copy( @@ -423,6 +468,7 @@ class FinancialConnectionsSheetViewModelTest { fun `init - when repository returns sync response, stores in state`() { runTest { // Given + whenever(isBrowserAvailable.invoke()).thenReturn(true) whenever(synchronizeFinancialConnectionsSession()).thenReturn(syncResponse) // When @@ -468,6 +514,8 @@ class FinancialConnectionsSheetViewModelTest { fetchFinancialConnectionsSessionForToken = fetchFinancialConnectionsSessionForToken, eventReporter = eventReporter, nativeRouter = mock(), + analyticsTracker = analyticsTracker, + isBrowserAvailable = isBrowserAvailable, logger = Logger.noop() ) }