Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fixes and testing for merchant-initiated repair flows #10051

Merged
merged 6 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ data class LinkAccountSessionBody(
val permissions: String? = null,
@SerialName("customer_email")
val customerEmail: String? = null,
@SerialName("customer_id")
val customerId: String? = null,
@SerialName("test_environment")
val testEnvironment: String? = null,
@SerialName("test_mode")
val testMode: Boolean? = null,
@SerialName("stripe_account_id")
val stripeAccountId: String? = null,
@SerialName("relink_authorization")
val relinkAuthorization: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ data class PaymentIntentBody(
val stripeAccountId: String? = null,
@SerialName("link_mode")
val linkMode: String? = null,
@SerialName("relink_authorization")
val relinkAuthorization: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.stripe.android.financialconnections.example.settings

import com.stripe.android.financialconnections.example.Experience
import com.stripe.android.financialconnections.example.Flow
import com.stripe.android.financialconnections.example.Merchant
import com.stripe.android.financialconnections.example.data.model.LinkAccountSessionBody
import com.stripe.android.financialconnections.example.data.model.PaymentIntentBody

internal data class CustomerIdSetting(
override val selectedOption: String = "",
override val key: String = "customer_id",
) : Saveable<String>, SingleChoiceSetting<String>(
displayName = "Customer ID",
options = emptyList(),
selectedOption = selectedOption
) {
override fun lasRequest(body: LinkAccountSessionBody): LinkAccountSessionBody = body.copy(
customerId = selectedOption.takeIf { it.isNotBlank() },
)

override fun paymentIntentRequest(body: PaymentIntentBody): PaymentIntentBody = body.copy(
customerId = selectedOption.takeIf { it.isNotBlank() },
)

override fun shouldDisplay(merchant: Merchant, flow: Flow, experience: Experience): Boolean {
return experience == Experience.FinancialConnections
}

override fun valueUpdated(
currentSettings: List<Setting<*>>,
value: String
): List<Setting<*>> {
return replace(currentSettings, this.copy(selectedOption = value))
}

override fun convertToString(value: String): String = value
override fun convertToValue(value: String): String = value
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ internal data class PlaygroundSettings(
NativeSetting(),
PermissionsSetting(),
EmailSetting(),
CustomerIdSetting(),
StripeAccountIdSetting().takeIf { BuildConfig.TEST_ENVIRONMENT != "edge" },
RelinkAuthorizationSetting(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.stripe.android.financialconnections.example.settings

import com.stripe.android.financialconnections.example.Experience
import com.stripe.android.financialconnections.example.Flow
import com.stripe.android.financialconnections.example.Merchant
import com.stripe.android.financialconnections.example.data.model.LinkAccountSessionBody
import com.stripe.android.financialconnections.example.data.model.PaymentIntentBody

internal data class RelinkAuthorizationSetting(
override val selectedOption: String = "",
override val key: String = "relink_authorization",
) : Saveable<String>, SingleChoiceSetting<String>(
displayName = "Relink Authorization",
options = emptyList(),
selectedOption = selectedOption
) {
override fun lasRequest(body: LinkAccountSessionBody): LinkAccountSessionBody = body.copy(
relinkAuthorization = selectedOption.takeIf { it.isNotBlank() },
)

override fun paymentIntentRequest(body: PaymentIntentBody): PaymentIntentBody = body.copy(
relinkAuthorization = selectedOption.takeIf { it.isNotBlank() },
)

override fun shouldDisplay(merchant: Merchant, flow: Flow, experience: Experience): Boolean {
return experience == Experience.FinancialConnections
}

override fun valueUpdated(
currentSettings: List<Setting<*>>,
value: String
): List<Setting<*>> {
return replace(currentSettings, this.copy(selectedOption = value))
}

override fun convertToString(value: String): String = value
override fun convertToValue(value: String): String = value
}
2 changes: 0 additions & 2 deletions financial-connections/api/financial-connections.api
Original file line number Diff line number Diff line change
Expand Up @@ -512,10 +512,8 @@ public final class com/stripe/android/financialconnections/features/common/Compo
public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$SharedPartnerAuthKt {
public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$SharedPartnerAuthKt;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
public static field lambda-2 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
}

public final class com/stripe/android/financialconnections/features/consent/ui/ComposableSingletons$ConsentLogoHeaderKt {
Expand Down
1 change: 1 addition & 0 deletions financial-connections/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@

<!-- Partner auth -->
<string name="stripe_prepane_cancel_cta">Cancel</string>
<string name="stripe_prepane_choose_different_bank_cta">Choose a different bank</string>

<!-- Loading -->
<string name="stripe_loading_pill_label">Hang on, nearly there</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,20 @@ internal sealed class FinancialConnectionsAnalyticsEvent(
mapOf("pane" to pane.analyticsValue)
)

class PrepaneClickCancel(
pane: Pane
) : FinancialConnectionsAnalyticsEvent(
name = "click.prepane.cancel",
mapOf("pane" to pane.analyticsValue)
)

class PrepaneClickChooseAnotherBank(
pane: Pane
) : FinancialConnectionsAnalyticsEvent(
name = "click.prepane.choose_another_bank",
mapOf("pane" to pane.analyticsValue)
)

class AuthSessionOpened(
pane: Pane,
flow: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ private fun PrePaneContent(
onContinueClick = onContinueClick,
onCancelClick = onCancelClick,
status = authenticationStatus,
oAuthPrepane = content
oAuthPrepane = content,
showInModal = showInModal,
)
}
)
Expand Down Expand Up @@ -353,7 +354,8 @@ private fun PrepaneFooter(
onContinueClick: () -> Unit,
onCancelClick: () -> Unit,
status: Async<AuthenticationStatus>,
oAuthPrepane: OauthPrepane
oAuthPrepane: OauthPrepane,
showInModal: Boolean,
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
Expand Down Expand Up @@ -397,7 +399,13 @@ private fun PrepaneFooter(
.fillMaxWidth()
) {
Text(
text = stringResource(R.string.stripe_prepane_cancel_cta),
text = stringResource(
id = if (showInModal) {
R.string.stripe_prepane_cancel_cta
} else {
R.string.stripe_prepane_choose_different_bank_cta
}
),
tillh-stripe marked this conversation as resolved.
Show resolved Hide resolved
textAlign = TextAlign.Center
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsAna
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.AuthSessionUrlReceived
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.Click
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PaneLoaded
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PrepaneClickCancel
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PrepaneClickChooseAnotherBank
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PrepaneClickContinue
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.Name
Expand Down Expand Up @@ -48,6 +50,7 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsAuthori
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.navigation.Destination
import com.stripe.android.financialconnections.navigation.Destination.AccountPicker
import com.stripe.android.financialconnections.navigation.NavigationManager
import com.stripe.android.financialconnections.navigation.PopUpToBehavior
Expand Down Expand Up @@ -461,18 +464,43 @@ internal class PartnerAuthViewModel @AssistedInject constructor(
}
}

fun onCancelClick() = viewModelScope.launch {
// set loading state while cancelling the active auth session, and navigate back
setState { copy(authenticationStatus = Loading(value = Status(Action.CANCELLING))) }
runCatching {
val authSession = requireNotNull(
getOrFetchSync(refetchCondition = IfMissingActiveAuthSession).manifest.activeAuthSession
)
cancelAuthorizationSession(authSession.id)
fun onCancelClick() = withState { state ->
if (state.inModal) {
eventTracker.track(PrepaneClickCancel(pane = PANE))
} else {
eventTracker.track(PrepaneClickChooseAnotherBank(pane = PANE))
}
tillh-stripe marked this conversation as resolved.
Show resolved Hide resolved

viewModelScope.launch {
// set loading state while cancelling the active auth session, and navigate back
setState { copy(authenticationStatus = Loading(value = Status(Action.CANCELLING))) }
runCatching {
val authSession = requireNotNull(
getOrFetchSync(refetchCondition = IfMissingActiveAuthSession).manifest.activeAuthSession
)
cancelAuthorizationSession(authSession.id)
}
if (state.inModal) {
cancelInModal()
} else {
cancelInFullscreen()
}
}
}

private fun cancelInModal() {
navigationManager.tryNavigateBack()
}

private fun cancelInFullscreen() {
navigationManager.tryNavigateTo(
route = Destination.InstitutionPicker(referrer = PANE),
popUpTo = PopUpToBehavior.Current(
inclusive = true,
),
)
}

@Parcelize
data class Args(val inModal: Boolean, val pane: Pane) : Parcelable

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.stripe.android.financialconnections.features.partnerauth

import com.stripe.android.core.Logger
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.NativeAuthFlowCoordinator
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.utils.TestNavigationManager
import com.stripe.android.financialconnections.utils.UriUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock

@OptIn(ExperimentalCoroutinesApi::class)
class PartnerAuthViewModelTest {

@get:Rule
val testRule = CoroutineTestRule()

@Test
fun `Navigates back when users cancels in modal`() {
val navigationManager = TestNavigationManager()
val tracker = TestFinancialConnectionsAnalyticsTracker()

val viewModel = makeViewModel(
initialState = SharedPartnerAuthState(
pane = Pane.PARTNER_AUTH_DRAWER,
payload = Async.Uninitialized,
inModal = true,
),
navigationManager = navigationManager,
tracker = tracker,
)

viewModel.onCancelClick()

navigationManager.assertNavigatedBack()
tracker.assertContainsEvent("linked_accounts.click.prepane.cancel")
}

@Test
fun `Navigates to institution picker when users cancels in full-screen pane`() {
val navigationManager = TestNavigationManager()
val tracker = TestFinancialConnectionsAnalyticsTracker()

val viewModel = makeViewModel(
initialState = SharedPartnerAuthState(
pane = Pane.PARTNER_AUTH_DRAWER,
payload = Async.Uninitialized,
inModal = false,
),
navigationManager = navigationManager,
tracker = tracker,
)

viewModel.onCancelClick()

navigationManager.assertNavigatedTo(
destination = Destination.InstitutionPicker,
pane = Pane.PARTNER_AUTH,
popUpTo = PopUpToBehavior.Current(
inclusive = true,
),
)
tracker.assertContainsEvent("linked_accounts.click.prepane.choose_another_bank")
}

private fun makeViewModel(
initialState: SharedPartnerAuthState,
navigationManager: NavigationManager = TestNavigationManager(),
tracker: FinancialConnectionsAnalyticsTracker = TestFinancialConnectionsAnalyticsTracker(),
): PartnerAuthViewModel {
return PartnerAuthViewModel(
completeAuthorizationSession = mock(),
createAuthorizationSession = mock(),
cancelAuthorizationSession = mock(),
retrieveAuthorizationSession = mock(),
eventTracker = tracker,
applicationId = "com.app.id",
uriUtils = UriUtils(
logger = Logger.noop(),
tracker = tracker,
),
postAuthSessionEvent = mock(),
getOrFetchSync = mock(),
browserManager = mock(),
handleError = mock(),
navigationManager = navigationManager,
pollAuthorizationSessionOAuthResults = mock(),
logger = Logger.noop(),
presentSheet = mock(),
initialState = initialState,
nativeAuthFlowCoordinator = NativeAuthFlowCoordinator(),
)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions maestro/financial-connections/Livemode-Data-Finbank.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ tags:
- startRecording: ${'/tmp/test_results/livemode-data-finbank-' + new Date().getTime()}
- clearState
- openLink: stripeconnectionsexample://playground?integration_type=Standalone&experience=FinancialConnections&flow=Data&financial_connections_override_native=native&merchant=live_testing&financial_connections_test_mode=false
- scrollUntilVisible:
element:
id: "connect_accounts"
- tapOn:
id: "connect_accounts"
# Wait until the consent button is visible
Expand Down
3 changes: 3 additions & 0 deletions maestro/financial-connections/Livemode-Data-MXBank.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ tags:
- startRecording: ${'/tmp/test_results/livemode-data-mxbank-' + new Date().getTime()}
- clearState
- openLink: stripeconnectionsexample://playground?integration_type=Standalone&experience=FinancialConnections&flow=Data&financial_connections_override_native=native&merchant=live_testing&financial_connections_test_mode=false
- scrollUntilVisible:
element:
id: "connect_accounts"
- tapOn:
id: "connect_accounts"
# Wait until the consent button is visible
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ tags:
- startRecording: ${'/tmp/test_results/testmode-data-testoauthinstitution-' + new Date().getTime()}
- clearState
- openLink: stripeconnectionsexample://playground?integration_type=Standalone&experience=FinancialConnections&flow=Data&financial_connections_override_native=native&merchant=testmode&financial_connections_test_mode=true
- scrollUntilVisible:
element:
id: "connect_accounts"
- tapOn:
id: "connect_accounts"
# Wait until the consent button is visible
Expand Down
Loading
Loading