diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api index 38e0aa54223..93f6e2d0e98 100644 --- a/financial-connections/api/financial-connections.api +++ b/financial-connections/api/financial-connections.api @@ -438,14 +438,6 @@ public final class com/stripe/android/financialconnections/features/attachpaymen public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2; } -public final class com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairViewModel$Args$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairViewModel$Args; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairViewModel$Args; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$AccountItemKt { public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$AccountItemKt; public fun ()V diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt index 400bd490684..34e0744b67a 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt @@ -6,7 +6,6 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheet import com.stripe.android.financialconnections.features.accountpicker.AccountPickerViewModel import com.stripe.android.financialconnections.features.accountupdate.AccountUpdateRequiredViewModel import com.stripe.android.financialconnections.features.attachpayment.AttachPaymentViewModel -import com.stripe.android.financialconnections.features.bankauthrepair.BankAuthRepairViewModel import com.stripe.android.financialconnections.features.consent.ConsentViewModel import com.stripe.android.financialconnections.features.error.ErrorViewModel import com.stripe.android.financialconnections.features.exit.ExitViewModel @@ -50,7 +49,6 @@ internal interface FinancialConnectionsSheetNativeComponent { val manualEntryViewModelFactory: ManualEntryViewModel.Factory val manualEntrySuccessViewModelFactory: ManualEntrySuccessViewModel.Factory val partnerAuthViewModelFactory: PartnerAuthViewModel.Factory - val bankAuthRepairViewModelFactory: BankAuthRepairViewModel.Factory val successViewModelFactory: SuccessViewModel.Factory val attachPaymentViewModelFactory: AttachPaymentViewModel.Factory val resetViewModelFactory: ResetViewModel.Factory diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt index 27ac5771936..d9681e43fc5 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt @@ -30,7 +30,9 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsAna import com.stripe.android.financialconnections.analytics.FinancialConnectionsEventReporter import com.stripe.android.financialconnections.domain.GetOrFetchSync import com.stripe.android.financialconnections.domain.IsLinkWithStripe +import com.stripe.android.financialconnections.domain.IsNetworkingRelinkSession import com.stripe.android.financialconnections.domain.RealIsLinkWithStripe +import com.stripe.android.financialconnections.domain.RealIsNetworkingRelinkSession import com.stripe.android.financialconnections.features.common.enableWorkManager import com.stripe.android.financialconnections.repository.ConsumerSessionProvider import com.stripe.android.financialconnections.repository.ConsumerSessionRepository @@ -86,6 +88,9 @@ internal interface FinancialConnectionsSheetSharedModule { @Binds fun bindsIsLinkWithStripe(impl: RealIsLinkWithStripe): IsLinkWithStripe + @Binds + fun bindsIsNetworkingRelinkSession(impl: RealIsNetworkingRelinkSession): IsNetworkingRelinkSession + companion object { @Provides diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IsNetworkingRelinkSession.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IsNetworkingRelinkSession.kt new file mode 100644 index 00000000000..cece794c769 --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IsNetworkingRelinkSession.kt @@ -0,0 +1,17 @@ +package com.stripe.android.financialconnections.domain + +import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository +import javax.inject.Inject + +internal fun interface IsNetworkingRelinkSession { + operator fun invoke(): Boolean +} + +internal class RealIsNetworkingRelinkSession @Inject constructor( + private val pendingRepairRepository: CoreAuthorizationPendingNetworkingRepairRepository, +) : IsNetworkingRelinkSession { + + override fun invoke(): Boolean { + return pendingRepairRepository.get() != null + } +} diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/RepairAuthorizationSession.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/RepairAuthorizationSession.kt new file mode 100644 index 00000000000..6d0a3ef767b --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/RepairAuthorizationSession.kt @@ -0,0 +1,25 @@ +package com.stripe.android.financialconnections.domain + +import com.stripe.android.financialconnections.FinancialConnectionsSheet +import com.stripe.android.financialconnections.di.APPLICATION_ID +import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession +import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository +import javax.inject.Inject +import javax.inject.Named + +internal class RepairAuthorizationSession @Inject constructor( + private val repository: FinancialConnectionsManifestRepository, + private val configuration: FinancialConnectionsSheet.Configuration, + @Named(APPLICATION_ID) private val applicationId: String, +) { + + suspend operator fun invoke( + coreAuthorization: String + ): FinancialConnectionsAuthorizationSession { + return repository.repairAuthorizationSession( + clientSecret = configuration.financialConnectionsSessionClientSecret, + coreAuthorization = coreAuthorization, + applicationId = applicationId, + ) + } +} diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/SaveAccountToLink.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/SaveAccountToLink.kt index d2f2725b15e..0044dd45b03 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/SaveAccountToLink.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/SaveAccountToLink.kt @@ -24,6 +24,7 @@ internal class SaveAccountToLink @Inject constructor( private val successContentRepository: SuccessContentRepository, private val repository: FinancialConnectionsManifestRepository, private val accountsRepository: FinancialConnectionsAccountsRepository, + private val isNetworkingRelinkSession: IsNetworkingRelinkSession, ) { suspend fun new( @@ -90,9 +91,13 @@ internal class SaveAccountToLink @Inject constructor( }.mapCatching { action(selectedAccountIds) }.onSuccess { manifest -> - storeSavedToLinkMessage(manifest, selectedAccountIds.size) + if (!isNetworkingRelinkSession()) { + storeSavedToLinkMessage(manifest, selectedAccountIds.size) + } }.onFailure { - storeFailedToSaveToLinkMessage(selectedAccountIds.size) + if (!isNetworkingRelinkSession()) { + storeFailedToSaveToLinkMessage(selectedAccountIds.size) + } }.getOrThrow() } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountupdate/AccountUpdateRequiredViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountupdate/AccountUpdateRequiredViewModel.kt index 5cfbc79ea80..85e2243d46e 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountupdate/AccountUpdateRequiredViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountupdate/AccountUpdateRequiredViewModel.kt @@ -24,6 +24,7 @@ import com.stripe.android.financialconnections.presentation.Async import com.stripe.android.financialconnections.presentation.Async.Uninitialized import com.stripe.android.financialconnections.presentation.FinancialConnectionsViewModel import com.stripe.android.financialconnections.repository.AccountUpdateRequiredContentRepository +import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -33,6 +34,7 @@ internal class AccountUpdateRequiredViewModel @AssistedInject constructor( @Assisted initialState: AccountUpdateRequiredState, nativeAuthFlowCoordinator: NativeAuthFlowCoordinator, private val updateRequiredContentRepository: AccountUpdateRequiredContentRepository, + private val pendingRepairRepository: CoreAuthorizationPendingNetworkingRepairRepository, private val navigationManager: NavigationManager, private val eventTracker: FinancialConnectionsAnalyticsTracker, private val updateLocalManifest: UpdateLocalManifest, @@ -60,7 +62,7 @@ internal class AccountUpdateRequiredViewModel @AssistedInject constructor( val referrer = state.referrer when (val type = requireNotNull(state.payload()?.type)) { is Type.Repair -> { - handleUnsupportedRepairAction(referrer) + openBankAuthRepair(type.institution, type.authorization, referrer) } is Type.Supportability -> { openPartnerAuth(type.institution, referrer) @@ -69,15 +71,31 @@ internal class AccountUpdateRequiredViewModel @AssistedInject constructor( } } - private fun handleUnsupportedRepairAction(referrer: Pane) { - eventTracker.logError( - extraMessage = "Updating a repair account, but repairs are not supported in Mobile.", - error = UnclassifiedError("UpdateRepairAccountError"), - logger = logger, - pane = PANE, - ) - // Fall back to the institution picker for now - navigationManager.tryNavigateTo(InstitutionPicker(referrer)) + private fun openBankAuthRepair( + institution: FinancialConnectionsInstitution?, + authorization: String?, + referrer: Pane, + ) { + if (institution != null && authorization != null) { + updateLocalManifest { + it.copy(activeInstitution = institution) + } + + pendingRepairRepository.set(authorization) + navigationManager.tryNavigateTo(Destination.BankAuthRepair(referrer)) + } else { + val missingAuth = authorization == null + val missingInstitution = institution == null + eventTracker.logError( + extraMessage = "Unable to open repair flow " + + "(missing auth: $missingAuth, missing institution: $missingInstitution).", + error = UnclassifiedError("UpdateRepairAccountError"), + logger = logger, + pane = PANE, + ) + // Fall back to the institution picker + navigationManager.tryNavigateTo(InstitutionPicker(referrer)) + } } private fun openPartnerAuth( diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt index ff09332237c..993465f3976 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt @@ -9,10 +9,13 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsAna import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker import com.stripe.android.financialconnections.analytics.logError import com.stripe.android.financialconnections.di.FinancialConnectionsSheetNativeComponent +import com.stripe.android.financialconnections.domain.CachedPartnerAccount import com.stripe.android.financialconnections.domain.GetCachedAccounts import com.stripe.android.financialconnections.domain.GetOrFetchSync +import com.stripe.android.financialconnections.domain.IsNetworkingRelinkSession import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator import com.stripe.android.financialconnections.domain.PollAttachPaymentAccount +import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane import com.stripe.android.financialconnections.model.LinkAccountSessionPaymentAccount import com.stripe.android.financialconnections.model.PaymentAccountParams @@ -41,7 +44,8 @@ internal class AttachPaymentViewModel @AssistedInject constructor( private val getCachedAccounts: GetCachedAccounts, private val navigationManager: NavigationManager, private val getOrFetchSync: GetOrFetchSync, - private val logger: Logger + private val logger: Logger, + private val isNetworkingRelinkSession: IsNetworkingRelinkSession, ) : FinancialConnectionsViewModel(initialState, nativeAuthFlowCoordinator) { init { @@ -60,15 +64,8 @@ internal class AttachPaymentViewModel @AssistedInject constructor( params = PaymentAccountParams.LinkedAccount(requireNotNull(id)) ) } - if (manifest.isNetworkingUserFlow == true && manifest.accountholderIsLinkConsumer == true) { - result.networkingSuccessful?.let { - successContentRepository.set( - message = PluralId( - value = R.plurals.stripe_success_pane_desc_link_success, - count = accounts.size - ) - ) - } + if (result.networkingSuccessful == true) { + setSuccessMessageIfNecessary(manifest, accounts) } eventTracker.track( PollAttachPaymentsSucceeded( @@ -91,6 +88,20 @@ internal class AttachPaymentViewModel @AssistedInject constructor( ) } + private fun setSuccessMessageIfNecessary( + manifest: FinancialConnectionsSessionManifest, + accounts: List, + ) { + if (manifest.canSetCustomLinkSuccessMessage && !isNetworkingRelinkSession()) { + successContentRepository.set( + message = PluralId( + value = R.plurals.stripe_success_pane_desc_link_success, + count = accounts.size + ) + ) + } + } + private fun logErrors() { onAsync( AttachPaymentState::linkPaymentAccount, @@ -132,3 +143,6 @@ internal class AttachPaymentViewModel @AssistedInject constructor( internal data class AttachPaymentState( val linkPaymentAccount: Async = Uninitialized ) + +private val FinancialConnectionsSessionManifest.canSetCustomLinkSuccessMessage: Boolean + get() = isNetworkingUserFlow == true && accountholderIsLinkConsumer == true diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairScreen.kt deleted file mode 100644 index fadb4de02d7..00000000000 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairScreen.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.stripe.android.financialconnections.features.bankauthrepair - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import com.stripe.android.financialconnections.features.common.SharedPartnerAuth -import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState -import com.stripe.android.financialconnections.presentation.paneViewModel -import com.stripe.android.uicore.utils.collectAsState - -@Composable -internal fun BankAuthRepairScreen() { - // step view model - val viewModel: BankAuthRepairViewModel = paneViewModel { BankAuthRepairViewModel.factory(it) } - val state: State = viewModel.stateFlow.collectAsState() - - SharedPartnerAuth( - state = state.value, - onContinueClick = { /*TODO*/ }, - onCancelClick = { /*TODO*/ }, - onClickableTextClick = { /*TODO*/ }, - onWebAuthFlowFinished = { /*TODO*/ }, - onViewEffectLaunched = { /*TODO*/ }, - inModal = false - ) -} diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairViewModel.kt deleted file mode 100644 index fdf6f51a744..00000000000 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairViewModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.stripe.android.financialconnections.features.bankauthrepair - -import android.os.Parcelable -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory -import com.stripe.android.financialconnections.di.FinancialConnectionsSheetNativeComponent -import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator -import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState -import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane -import com.stripe.android.financialconnections.navigation.topappbar.TopAppBarStateUpdate -import com.stripe.android.financialconnections.presentation.FinancialConnectionsViewModel -import com.stripe.android.financialconnections.utils.error -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.parcelize.Parcelize - -internal class BankAuthRepairViewModel @AssistedInject constructor( - @Assisted initialState: SharedPartnerAuthState, - nativeAuthFlowCoordinator: NativeAuthFlowCoordinator, -) : FinancialConnectionsViewModel(initialState, nativeAuthFlowCoordinator) { - - override fun updateTopAppBar(state: SharedPartnerAuthState): TopAppBarStateUpdate { - return TopAppBarStateUpdate( - pane = Pane.BANK_AUTH_REPAIR, - allowBackNavigation = state.canNavigateBack, - error = state.payload.error, - ) - } - - @Parcelize - data class Args(val pane: Pane) : Parcelable - - @AssistedFactory - interface Factory { - fun create(initialState: SharedPartnerAuthState): BankAuthRepairViewModel - } - - internal companion object { - fun factory(parentComponent: FinancialConnectionsSheetNativeComponent): ViewModelProvider.Factory = - viewModelFactory { - initializer { - parentComponent.bankAuthRepairViewModelFactory.create(SharedPartnerAuthState(Args(PANE))) - } - } - - val PANE = Pane.BANK_AUTH_REPAIR - } -} diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt index b9a1d3e3590..aa5c567ed80 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt @@ -197,6 +197,7 @@ private fun SharedPartnerAuthBody( state.payload()?.let { LoadedContent( showInModal = inModal, + isRelinkSession = state.isNetworkingRelinkSession, authenticationStatus = state.authenticationStatus, payload = it, onContinueClick = onContinueClick, @@ -210,6 +211,7 @@ private fun SharedPartnerAuthBody( @Composable private fun LoadedContent( showInModal: Boolean, + isRelinkSession: Boolean, authenticationStatus: Async, payload: SharedPartnerAuthState.Payload, onContinueClick: () -> Unit, @@ -226,6 +228,7 @@ private fun LoadedContent( // is Loading or Success (completing auth after redirect) authenticationStatus = authenticationStatus, showInModal = showInModal, + showSecondaryButton = !isRelinkSession, onContinueClick = onContinueClick, onCancelClick = onCancelClick, content = requireNotNull(payload.authSession.display?.text?.oauthPrepane), @@ -240,6 +243,7 @@ private fun LoadedContent( @Composable private fun PrePaneContent( showInModal: Boolean, + showSecondaryButton: Boolean, content: OauthPrepane, authenticationStatus: Async, onContinueClick: () -> Unit, @@ -284,6 +288,7 @@ private fun PrePaneContent( status = authenticationStatus, oAuthPrepane = content, showInModal = showInModal, + showSecondaryButton = showSecondaryButton, ) } ) @@ -358,6 +363,7 @@ private fun PrepaneFooter( status: Async, oAuthPrepane: OauthPrepane, showInModal: Boolean, + showSecondaryButton: Boolean, ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -391,25 +397,28 @@ private fun PrepaneFooter( } } } - FinancialConnectionsButton( - onClick = onCancelClick, - type = Type.Secondary, - enabled = status !is Loading, - modifier = Modifier - .semantics { testTagsAsResourceId = true } - .testTag("cancel_cta") - .fillMaxWidth() - ) { - Text( - text = stringResource( - id = if (showInModal) { - R.string.stripe_prepane_cancel_cta - } else { - R.string.stripe_prepane_choose_different_bank_cta - } - ), - textAlign = TextAlign.Center - ) + + if (showSecondaryButton) { + FinancialConnectionsButton( + onClick = onCancelClick, + type = Type.Secondary, + enabled = status !is Loading, + modifier = Modifier + .semantics { testTagsAsResourceId = true } + .testTag("cancel_cta") + .fillMaxWidth() + ) { + Text( + text = stringResource( + id = if (showInModal) { + R.string.stripe_prepane_cancel_cta + } else { + R.string.stripe_prepane_choose_different_bank_cta + } + ), + textAlign = TextAlign.Center + ) + } } } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt index 41f45475ed4..6e14ab8f427 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt @@ -389,6 +389,7 @@ internal class LinkAccountPickerViewModel @AssistedInject constructor( generic = genericContent, type = Repair( authorization = authorization?.let { payload.partnerToCoreAuths?.getValue(it) }, + institution = institution, ), ) PARTNER_AUTH -> UpdateRequired( diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/notice/NoticeSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/notice/NoticeSheetViewModel.kt index 1cd67c5eb20..02974dd2fb4 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/notice/NoticeSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/notice/NoticeSheetViewModel.kt @@ -144,10 +144,15 @@ internal data class NoticeSheetState( sealed interface Type : Parcelable { @Parcelize - data class Repair(val authorization: String?) : Type + data class Repair( + val authorization: String?, + val institution: FinancialConnectionsInstitution?, + ) : Type @Parcelize - data class Supportability(val institution: FinancialConnectionsInstitution?) : Type + data class Supportability( + val institution: FinancialConnectionsInstitution?, + ) : Type } } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt index 525c84b7a28..e11db61abc9 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt @@ -1,25 +1,28 @@ package com.stripe.android.financialconnections.features.partnerauth import androidx.compose.runtime.Composable -import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import com.stripe.android.financialconnections.features.common.SharedPartnerAuth import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane import com.stripe.android.financialconnections.presentation.paneViewModel import com.stripe.android.uicore.utils.collectAsState @Composable -internal fun PartnerAuthScreen(inModal: Boolean) { +internal fun PartnerAuthScreen( + pane: Pane, + inModal: Boolean +) { val viewModel: PartnerAuthViewModel = paneViewModel { PartnerAuthViewModel.factory( parentComponent = it, - args = PartnerAuthViewModel.Args(inModal, Pane.PARTNER_AUTH) + args = PartnerAuthViewModel.Args(inModal, pane) ) } - val state: State = viewModel.stateFlow.collectAsState() + val state by viewModel.stateFlow.collectAsState() SharedPartnerAuth( inModal = inModal, - state = state.value, + state = state, onContinueClick = viewModel::onLaunchAuthClick, onCancelClick = viewModel::onCancelClick, onClickableTextClick = viewModel::onClickableTextClick, diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt index 349b0832139..080f8696e87 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt @@ -35,6 +35,7 @@ import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator import com.stripe.android.financialconnections.domain.PollAuthorizationSessionOAuthResults import com.stripe.android.financialconnections.domain.PostAuthSessionEvent import com.stripe.android.financialconnections.domain.PostAuthorizationSession +import com.stripe.android.financialconnections.domain.RepairAuthorizationSession import com.stripe.android.financialconnections.domain.RetrieveAuthorizationSession import com.stripe.android.financialconnections.exception.FinancialConnectionsError import com.stripe.android.financialconnections.exception.PartnerAuthError @@ -61,6 +62,7 @@ import com.stripe.android.financialconnections.presentation.Async.Loading import com.stripe.android.financialconnections.presentation.Async.Uninitialized import com.stripe.android.financialconnections.presentation.FinancialConnectionsViewModel import com.stripe.android.financialconnections.presentation.WebAuthFlowState +import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository import com.stripe.android.financialconnections.utils.UriUtils import com.stripe.android.financialconnections.utils.error import dagger.assisted.Assisted @@ -88,14 +90,19 @@ internal class PartnerAuthViewModel @AssistedInject constructor( private val pollAuthorizationSessionOAuthResults: PollAuthorizationSessionOAuthResults, private val logger: Logger, private val presentSheet: PresentSheet, - @Assisted initialState: SharedPartnerAuthState, + private val pendingRepairRepository: CoreAuthorizationPendingNetworkingRepairRepository, + private val repairAuthSession: RepairAuthorizationSession, + @Assisted private val initialState: SharedPartnerAuthState, nativeAuthFlowCoordinator: NativeAuthFlowCoordinator, ) : FinancialConnectionsViewModel(initialState, nativeAuthFlowCoordinator) { + private val pane: Pane + get() = initialState.pane + init { handleErrors() launchBrowserIfNonOauth() - restoreOrCreateAuthSession() + initializeState() } override fun updateTopAppBar(state: SharedPartnerAuthState): TopAppBarStateUpdate? { @@ -103,31 +110,54 @@ internal class PartnerAuthViewModel @AssistedInject constructor( null } else { TopAppBarStateUpdate( - pane = PANE, + pane = state.pane, allowBackNavigation = state.canNavigateBack, error = state.payload.error, ) } } - private fun restoreOrCreateAuthSession() = suspend { - // A session should have been created in the previous pane and set as the active - // auth session in the manifest. - // if coming from a process kill, we'll fetch the current manifest from network, - // that should contain the active auth session. - val sync: SynchronizeSessionResponse = getOrFetchSync() + private fun initializeState() { + suspend { + val sync = getOrFetchSync() + if (pane == Pane.BANK_AUTH_REPAIR) { + initializeBankAuthRepair(sync) + } else { + initializePartnerAuth(sync) + } + }.execute { + copy(payload = it) + } + } + + private suspend fun initializeBankAuthRepair( + sync: SynchronizeSessionResponse, + ): Payload { + val authorization = requireNotNull(pendingRepairRepository.get()?.coreAuthorization) + val activeInstitution = requireNotNull(sync.manifest.activeInstitution) + + val authSession = repairAuthSession(authorization) + + return Payload( + isStripeDirect = sync.manifest.isStripeDirect ?: false, + institution = activeInstitution, + authSession = authSession, + ) + } + + private suspend fun initializePartnerAuth( + sync: SynchronizeSessionResponse, + ): Payload { val manifest = sync.manifest val authSession = manifest.activeAuthSession ?: createAuthorizationSession( institution = requireNotNull(manifest.activeInstitution), sync = sync ) - Payload( + return Payload( isStripeDirect = manifest.isStripeDirect ?: false, institution = requireNotNull(manifest.activeInstitution), authSession = authSession, ) - }.execute { - copy(payload = it) } private fun recreateAuthSession() = suspend { @@ -177,11 +207,11 @@ internal class PartnerAuthViewModel @AssistedInject constructor( handleError( extraMessage = "Error fetching payload / posting AuthSession", error = it, - pane = PANE, + pane = pane, displayErrorScreen = true ) }, - onSuccess = { eventTracker.track(PaneLoaded(PANE)) } + onSuccess = { eventTracker.track(PaneLoaded(pane)) } ) onAsync( SharedPartnerAuthState::authenticationStatus, @@ -189,7 +219,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( handleError( extraMessage = "Error with authentication status", error = if (it is FinancialConnectionsError) it else PartnerAuthError(it.message), - pane = PANE, + pane = pane, displayErrorScreen = true ) } @@ -211,7 +241,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( private fun reportOAuthLaunched(sessionId: String) { postAuthSessionEvent(sessionId, AuthSessionEvent.OAuthLaunched(Date())) - eventTracker.track(PrepaneClickContinue(PANE)) + eventTracker.track(PrepaneClickContinue(pane)) } private fun launchAuthInBrowser(authSession: FinancialConnectionsAuthorizationSession) { @@ -220,7 +250,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( eventTracker.track( AuthSessionOpened( id = authSession.id, - pane = PANE, + pane = pane, flow = authSession.flow, defaultBrowser = browserManager.getPackageToHandleUri(uri = url.toUri()), ) @@ -275,7 +305,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( val authSession = getOrFetchSync().manifest.activeAuthSession eventTracker.track( AuthSessionUrlReceived( - pane = PANE, + pane = pane, url = url, authSessionId = authSession?.id, status = "failed" @@ -285,7 +315,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( extraMessage = "Auth failed, cancelling AuthSession", error = error, logger = logger, - pane = PANE + pane = pane ) when { authSession != null -> { @@ -301,7 +331,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( extraMessage = "failed cancelling session after failed web flow", error = it, logger = logger, - pane = PANE + pane = pane ) } } @@ -316,7 +346,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( val authSession = manifest.activeAuthSession eventTracker.track( AuthSessionUrlReceived( - pane = PANE, + pane = pane, url = url ?: "none", authSessionId = authSession?.id, status = "cancelled" @@ -335,13 +365,13 @@ internal class PartnerAuthViewModel @AssistedInject constructor( nextPane = nextPane ) ) - if (nextPane == PANE) { + if (nextPane == pane) { // auth session was not completed, proceed with cancellation cancelAuthSessionAndContinue(authSession = retrievedAuthSession) } else { // auth session succeeded although client didn't retrieve any deeplink. postAuthSessionEvent(authSession.id, AuthSessionEvent.Success(Date())) - navigationManager.tryNavigateTo(nextPane.destination(referrer = PANE)) + navigationManager.tryNavigateTo(nextPane.destination(referrer = pane)) } } else { cancelAuthSessionAndContinue(authSession) @@ -351,7 +381,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( "failed cancelling session after cancelled web flow. url: $url", it, logger, - PANE + pane ) setState { copy(authenticationStatus = Fail(it)) } } @@ -377,7 +407,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( // For non-OAuth institutions, navigate to Session cancellation's next pane. postAuthSessionEvent(authSession.id, AuthSessionEvent.Cancel(Date())) navigationManager.tryNavigateTo( - route = result.nextPane.destination(referrer = PANE), + route = result.nextPane.destination(referrer = pane), popUpTo = PopUpToBehavior.Current(inclusive = true), ) } @@ -391,7 +421,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( ).manifest.activeAuthSession eventTracker.track( AuthSessionUrlReceived( - pane = PANE, + pane = pane, url = url, authSessionId = authSession?.id, status = "success" @@ -408,9 +438,9 @@ internal class PartnerAuthViewModel @AssistedInject constructor( publicToken = oAuthResults.publicToken ) logger.debug("Session authorized!") - updatedSession.nextPane.destination(referrer = PANE) + updatedSession.nextPane.destination(referrer = pane) } else { - AccountPicker(referrer = PANE) + AccountPicker(referrer = pane) } FinancialConnections.emitEvent(Name.INSTITUTION_AUTHORIZED) navigationManager.tryNavigateTo(nextPane) @@ -419,7 +449,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( extraMessage = "failed authorizing session", error = it, logger = logger, - pane = PANE + pane = pane ) setState { copy(authenticationStatus = Fail(it)) } } @@ -428,7 +458,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( // if clicked uri contains an eventName query param, track click event. fun onClickableTextClick(uri: String) = viewModelScope.launch { uriUtils.getQueryParameter(uri, "eventName")?.let { eventName -> - eventTracker.track(Click(eventName, pane = PANE)) + eventTracker.track(Click(eventName, pane = pane)) } if (URLUtil.isNetworkUrl(uri)) { setState { @@ -454,7 +484,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( val notice = authSession?.display?.text?.consent?.dataAccessNotice ?: return presentSheet( content = DataAccess(notice), - referrer = PANE, + referrer = pane, ) } @@ -466,9 +496,9 @@ internal class PartnerAuthViewModel @AssistedInject constructor( fun onCancelClick() = withState { state -> if (state.inModal) { - eventTracker.track(PrepaneClickCancel(pane = PANE)) + eventTracker.track(PrepaneClickCancel(pane = pane)) } else { - eventTracker.track(PrepaneClickChooseAnotherBank(pane = PANE)) + eventTracker.track(PrepaneClickChooseAnotherBank(pane = pane)) } viewModelScope.launch { @@ -494,7 +524,7 @@ internal class PartnerAuthViewModel @AssistedInject constructor( private fun cancelInFullscreen() { navigationManager.tryNavigateTo( - route = Destination.InstitutionPicker(referrer = PANE), + route = Destination.InstitutionPicker(referrer = pane), popUpTo = PopUpToBehavior.Current( inclusive = true, ), @@ -516,7 +546,5 @@ internal class PartnerAuthViewModel @AssistedInject constructor( parentComponent.partnerAuthViewModelFactory.create(SharedPartnerAuthState(args)) } } - - private val PANE = Pane.PARTNER_AUTH } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt index c1f03670634..1f7bb320ce8 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt @@ -1,6 +1,5 @@ package com.stripe.android.financialconnections.features.partnerauth -import com.stripe.android.financialconnections.features.bankauthrepair.BankAuthRepairViewModel import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane @@ -18,15 +17,14 @@ internal data class SharedPartnerAuthState( val inModal: Boolean = false, ) { + val isNetworkingRelinkSession: Boolean + get() = pane == Pane.BANK_AUTH_REPAIR + constructor(args: PartnerAuthViewModel.Args) : this( pane = args.pane, inModal = args.inModal, ) - constructor(args: BankAuthRepairViewModel.Args) : this( - pane = args.pane, - ) - data class Payload( val isStripeDirect: Boolean, val institution: FinancialConnectionsInstitution, @@ -48,7 +46,9 @@ internal data class SharedPartnerAuthState( authenticationStatus !is Loading && authenticationStatus !is Success && // Failures posting institution -> don't allow back navigation - payload !is Fail + payload !is Fail && + // No back navigation after creating the new auth session in the relink flow + !isNetworkingRelinkSession sealed interface ViewEffect { data class OpenPartnerAuth( diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/AuthorizationRepairResponse.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/AuthorizationRepairResponse.kt new file mode 100644 index 00000000000..8991c72b50b --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/AuthorizationRepairResponse.kt @@ -0,0 +1,15 @@ +package com.stripe.android.financialconnections.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class AuthorizationRepairResponse( + val id: String, + val url: String, + val flow: String, + val institution: FinancialConnectionsInstitution, + val display: Display, + @SerialName("is_oauth") + val isOAuth: Boolean, +) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt index 4797ded5c35..4be89abfd85 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt @@ -18,7 +18,6 @@ import androidx.navigation.navArgument import com.stripe.android.financialconnections.features.accountpicker.AccountPickerScreen import com.stripe.android.financialconnections.features.accountupdate.AccountUpdateRequiredModal import com.stripe.android.financialconnections.features.attachpayment.AttachPaymentScreen -import com.stripe.android.financialconnections.features.bankauthrepair.BankAuthRepairScreen import com.stripe.android.financialconnections.features.consent.ConsentScreen import com.stripe.android.financialconnections.features.error.ErrorScreen import com.stripe.android.financialconnections.features.exit.ExitModal @@ -110,14 +109,14 @@ internal sealed class Destination( route = Pane.PARTNER_AUTH_DRAWER.value, closeWithoutConfirmation = false, logPaneLaunched = true, - composable = { PartnerAuthScreen(inModal = true) } + composable = { PartnerAuthScreen(pane = Pane.PARTNER_AUTH, inModal = true) } ) data object PartnerAuth : Destination( route = Pane.PARTNER_AUTH.value, closeWithoutConfirmation = false, logPaneLaunched = true, - composable = { PartnerAuthScreen(inModal = false) } + composable = { PartnerAuthScreen(pane = Pane.PARTNER_AUTH, inModal = false) } ) data object AccountPicker : Destination( @@ -235,7 +234,7 @@ internal sealed class Destination( route = Pane.BANK_AUTH_REPAIR.value, closeWithoutConfirmation = false, logPaneLaunched = true, - composable = { BankAuthRepairScreen() } + composable = { PartnerAuthScreen(pane = Pane.BANK_AUTH_REPAIR, inModal = false) } ) data object ManualEntrySuccess : Destination( diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt index 8c036f20fcd..7e385459219 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt @@ -3,15 +3,15 @@ package com.stripe.android.financialconnections.repository import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import com.stripe.android.core.Logger +import com.stripe.android.financialconnections.di.ActivityRetainedScope import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository.State import kotlinx.parcelize.Parcelize import javax.inject.Inject -import javax.inject.Singleton /** * Repository for storing the core authorization pending repair. */ -@Singleton +@ActivityRetainedScope internal class CoreAuthorizationPendingNetworkingRepairRepository @Inject constructor( savedStateHandle: SavedStateHandle, private val logger: Logger, @@ -26,6 +26,6 @@ internal class CoreAuthorizationPendingNetworkingRepairRepository @Inject constr @Parcelize data class State( - val coreAuthorization: String? = null + val coreAuthorization: String, ) : Parcelable } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt index f77dd57e986..e44d616f0fe 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt @@ -7,9 +7,11 @@ import com.stripe.android.core.exception.AuthenticationException import com.stripe.android.core.exception.InvalidRequestException import com.stripe.android.core.networking.ApiRequest 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.FinancialConnectionsSessionManifest +import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane import com.stripe.android.financialconnections.model.SynchronizeSessionResponse import com.stripe.android.financialconnections.network.FinancialConnectionsRequestExecutor import com.stripe.android.financialconnections.network.NetworkConstants @@ -109,6 +111,12 @@ internal interface FinancialConnectionsManifestRepository { sessionId: String ): FinancialConnectionsAuthorizationSession + suspend fun repairAuthorizationSession( + clientSecret: String, + coreAuthorization: String, + applicationId: String, + ): FinancialConnectionsAuthorizationSession + /** * Save the authorized bank accounts to Link. * @@ -344,6 +352,36 @@ private class FinancialConnectionsManifestRepositoryImpl( updateCachedActiveAuthSession("retrieveAuthorizationSession", it) } + override suspend fun repairAuthorizationSession( + clientSecret: String, + coreAuthorization: String, + applicationId: String, + ): FinancialConnectionsAuthorizationSession { + val repairSession = requestExecutor.execute( + request = apiRequestFactory.createPost( + url = generateRepairUrl, + options = provideApiRequestOptions(useConsumerPublishableKey = true), + params = mapOf( + NetworkConstants.PARAMS_CLIENT_SECRET to clientSecret, + "core_authorization" to coreAuthorization, + "return_url" to "auth-redirect/$applicationId", + ) + ), + AuthorizationRepairResponse.serializer() + ) + + return FinancialConnectionsAuthorizationSession( + id = repairSession.id, + url = repairSession.url, + flow = repairSession.flow, + display = repairSession.display, + _isOAuth = repairSession.isOAuth, + nextPane = Pane.SUCCESS, + ).also { + updateCachedActiveAuthSession("repairAuthorizationSession", it) + } + } + override suspend fun completeAuthorizationSession( clientSecret: String, sessionId: String, @@ -560,5 +598,8 @@ private class FinancialConnectionsManifestRepositoryImpl( internal const val disableNetworking: String = "${ApiRequest.API_HOST}/v1/link_account_sessions/disable_networking" + + internal const val generateRepairUrl: String = + "${ApiRequest.API_HOST}/v1/connections/repair_sessions/generate_url" } } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/SaveAccountToLinkTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/SaveAccountToLinkTest.kt index 7814d935a9b..4307593cd50 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/SaveAccountToLinkTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/SaveAccountToLinkTest.kt @@ -179,11 +179,43 @@ internal class SaveAccountToLinkTest { ) } + @Test + fun `Doesn't set success message if in networking relink session`() = runTest(testDispatcher) { + val accountsRepository = mockAccountsRepository() + val attachedPaymentAccountRepository = mock() + val successRepository = SuccessContentRepository(SavedStateHandle()) + + whenever(attachedPaymentAccountRepository.get()).thenReturn( + State( + attachedPaymentAccount = PaymentAccountParams.BankAccount( + accountNumber = "acct_123", + routingNumber = "110000000", + ) + ) + ) + + val saveAccountToLink = makeSaveAccountToLink( + accountsRepository = accountsRepository, + successRepository = successRepository, + attachedPaymentAccountRepository = attachedPaymentAccountRepository, + isNetworkingRelinkSession = { true }, + ) + + saveAccountToLink.existing( + consumerSessionClientSecret = "cscs_123", + selectedAccounts = emptyList(), + shouldPollAccountNumbers = true, + ) + + assertThat(successRepository.get()).isNull() + } + private fun makeSaveAccountToLink( repository: FinancialConnectionsManifestRepository = mockManifestRepository(), accountsRepository: FinancialConnectionsAccountsRepository = mockAccountsRepository(), successRepository: SuccessContentRepository = SuccessContentRepository(SavedStateHandle()), attachedPaymentAccountRepository: AttachedPaymentAccountRepository = mock(), + isNetworkingRelinkSession: IsNetworkingRelinkSession = IsNetworkingRelinkSession { false }, ): SaveAccountToLink { return SaveAccountToLink( locale = Locale.getDefault(), @@ -195,6 +227,7 @@ internal class SaveAccountToLinkTest { repository = repository, attachedPaymentAccountRepository = attachedPaymentAccountRepository, accountsRepository = accountsRepository, + isNetworkingRelinkSession = isNetworkingRelinkSession, ) } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt index d1f7ca7d993..c6c17eace5a 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt @@ -1,21 +1,34 @@ package com.stripe.android.financialconnections.features.partnerauth +import androidx.lifecycle.SavedStateHandle import com.stripe.android.core.Logger +import com.stripe.android.financialconnections.ApiKeyFixtures.institution +import com.stripe.android.financialconnections.ApiKeyFixtures.sessionManifest +import com.stripe.android.financialconnections.ApiKeyFixtures.syncResponse import com.stripe.android.financialconnections.CoroutineTestRule import com.stripe.android.financialconnections.TestFinancialConnectionsAnalyticsTracker import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker +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.RepairAuthorizationSession import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane import com.stripe.android.financialconnections.navigation.Destination import com.stripe.android.financialconnections.navigation.NavigationManager import com.stripe.android.financialconnections.navigation.PopUpToBehavior import com.stripe.android.financialconnections.presentation.Async +import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository import com.stripe.android.financialconnections.utils.TestNavigationManager import com.stripe.android.financialconnections.utils.UriUtils import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify @OptIn(ExperimentalCoroutinesApi::class) class PartnerAuthViewModelTest { @@ -23,6 +36,79 @@ class PartnerAuthViewModelTest { @get:Rule val testRule = CoroutineTestRule() + @Test + fun `Creates auth session when in partner auth flow`() = runTest { + val syncResponse = syncResponse( + manifest = sessionManifest().copy( + activeInstitution = institution(), + ) + ) + + val getOrFetchSync = mock { + onBlocking { invoke(any(), any()) } doReturn syncResponse + } + + val createAuthorizationSession = mock() + val repairAuthSession = mock() + + makeViewModel( + initialState = SharedPartnerAuthState( + pane = Pane.PARTNER_AUTH, + inModal = true, + ), + getOrFetchSync = getOrFetchSync, + createAuthorizationSession = createAuthorizationSession, + repairAuthSession = repairAuthSession, + ) + + verify(createAuthorizationSession).invoke( + institution = syncResponse.manifest.activeInstitution!!, + sync = syncResponse, + ) + + verify(repairAuthSession, never()).invoke(any()) + } + + @Test + fun `Creates repair session when in networking relink flow`() = runTest { + val syncResponse = syncResponse( + manifest = sessionManifest().copy( + activeInstitution = institution(), + ) + ) + + val createAuthorizationSession = mock() + val repairAuthSession = mock() + + val pendingRepairRepository = CoreAuthorizationPendingNetworkingRepairRepository( + savedStateHandle = SavedStateHandle(), + logger = Logger.noop(), + ).apply { + set("fcauth_123") + } + + val getOrFetchSync = mock { + onBlocking { invoke(any(), any()) } doReturn syncResponse + } + + makeViewModel( + initialState = SharedPartnerAuthState( + pane = Pane.BANK_AUTH_REPAIR, + inModal = false, + ), + getOrFetchSync = getOrFetchSync, + createAuthorizationSession = createAuthorizationSession, + repairAuthSession = repairAuthSession, + pendingRepairRepository = pendingRepairRepository, + ) + + verify(repairAuthSession).invoke( + coreAuthorization = "fcauth_123", + ) + + verify(createAuthorizationSession, never()).invoke(any(), any()) + } + @Test fun `Navigates back when users cancels in modal`() { val navigationManager = TestNavigationManager() @@ -30,7 +116,7 @@ class PartnerAuthViewModelTest { val viewModel = makeViewModel( initialState = SharedPartnerAuthState( - pane = Pane.PARTNER_AUTH_DRAWER, + pane = Pane.PARTNER_AUTH, payload = Async.Uninitialized, inModal = true, ), @@ -51,7 +137,7 @@ class PartnerAuthViewModelTest { val viewModel = makeViewModel( initialState = SharedPartnerAuthState( - pane = Pane.PARTNER_AUTH_DRAWER, + pane = Pane.PARTNER_AUTH, payload = Async.Uninitialized, inModal = false, ), @@ -75,10 +161,14 @@ class PartnerAuthViewModelTest { initialState: SharedPartnerAuthState, navigationManager: NavigationManager = TestNavigationManager(), tracker: FinancialConnectionsAnalyticsTracker = TestFinancialConnectionsAnalyticsTracker(), + getOrFetchSync: GetOrFetchSync = mock(), + createAuthorizationSession: PostAuthorizationSession = mock(), + repairAuthSession: RepairAuthorizationSession = mock(), + pendingRepairRepository: CoreAuthorizationPendingNetworkingRepairRepository = mock(), ): PartnerAuthViewModel { return PartnerAuthViewModel( completeAuthorizationSession = mock(), - createAuthorizationSession = mock(), + createAuthorizationSession = createAuthorizationSession, cancelAuthorizationSession = mock(), retrieveAuthorizationSession = mock(), eventTracker = tracker, @@ -88,7 +178,7 @@ class PartnerAuthViewModelTest { tracker = tracker, ), postAuthSessionEvent = mock(), - getOrFetchSync = mock(), + getOrFetchSync = getOrFetchSync, browserManager = mock(), handleError = mock(), navigationManager = navigationManager, @@ -97,6 +187,8 @@ class PartnerAuthViewModelTest { presentSheet = mock(), initialState = initialState, nativeAuthFlowCoordinator = NativeAuthFlowCoordinator(), + pendingRepairRepository = pendingRepairRepository, + repairAuthSession = repairAuthSession, ) } } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/SupportabilityViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/SupportabilityViewModelTest.kt index 6332fed939c..37a3c93f30d 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/SupportabilityViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/SupportabilityViewModelTest.kt @@ -1,5 +1,6 @@ package com.stripe.android.financialconnections.features.partnerauth +import androidx.lifecycle.SavedStateHandle import com.google.common.truth.Truth.assertThat import com.stripe.android.core.Logger import com.stripe.android.core.exception.APIException @@ -24,6 +25,7 @@ import com.stripe.android.financialconnections.model.MixedOAuthParams import com.stripe.android.financialconnections.presentation.Async import com.stripe.android.financialconnections.presentation.WebAuthFlowState import com.stripe.android.financialconnections.presentation.withState +import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository import com.stripe.android.financialconnections.utils.TestHandleError import com.stripe.android.financialconnections.utils.TestNavigationManager import com.stripe.android.financialconnections.utils.UriUtils @@ -318,6 +320,11 @@ internal class SupportabilityViewModelTest { applicationId = applicationId, nativeAuthFlowCoordinator = nativeAuthFlowCoordinator, presentSheet = mock(), + pendingRepairRepository = CoreAuthorizationPendingNetworkingRepairRepository( + savedStateHandle = SavedStateHandle(), + logger = Logger.noop(), + ), + repairAuthSession = mock(), ) } } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/AbsFinancialConnectionsManifestRepository.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/AbsFinancialConnectionsManifestRepository.kt index 80e8cfecb06..50bc8ec432c 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/AbsFinancialConnectionsManifestRepository.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/AbsFinancialConnectionsManifestRepository.kt @@ -43,6 +43,14 @@ internal abstract class AbsFinancialConnectionsManifestRepository : FinancialCon TODO("Not yet implemented") } + override suspend fun repairAuthorizationSession( + clientSecret: String, + coreAuthorization: String, + applicationId: String + ): FinancialConnectionsAuthorizationSession { + TODO("Not yet implemented") + } + override suspend fun cancelAuthorizationSession( clientSecret: String, sessionId: String diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/FakeFinancialConnectionsManifestRepository.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/FakeFinancialConnectionsManifestRepository.kt index 416514d5c4e..a1c198283e9 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/FakeFinancialConnectionsManifestRepository.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/networking/FakeFinancialConnectionsManifestRepository.kt @@ -48,6 +48,14 @@ internal class FakeFinancialConnectionsManifestRepository : FinancialConnections authSessionEvents: List ): FinancialConnectionsAuthorizationSession = postAuthSessionEvent() + override suspend fun repairAuthorizationSession( + clientSecret: String, + coreAuthorization: String, + applicationId: String + ): FinancialConnectionsAuthorizationSession { + TODO("Not yet implemented") + } + override suspend fun completeAuthorizationSession( clientSecret: String, sessionId: String,